Inúmeros erros de digitação e código Copy-Paste se tornaram o tópico principal do artigo adicional sobre a verificação do código Haiku pelo analisador PVS-Studio. No entanto, este artigo fala principalmente sobre erros relacionados à falta de consideração e refatoração falhada, em vez de erros de digitação. Os erros encontrados demonstram a força do fator humano no desenvolvimento de software.
1. Introdução
O Haiku é um sistema operacional de código aberto gratuito para computadores pessoais. Uma equipe de desenvolvimento internacional está atualmente trabalhando nos componentes do sistema. O Porting Libre Office no SO e a primeira versão R1 Beta 1 se destacam entre as recentes melhorias significativas no desenvolvimento.
A equipe de desenvolvedores do
PVS-Studio acompanha esse desenvolvimento de projeto desde 2015 e publica análises de defeitos de código. Esta é a quarta revisão de todos os tempos. Você pode ler os artigos anteriores por estes links:
- Análise do Sistema Operacional Haiku (Família BeOS), por PVS-Studio, Parte 1 ;
- Análise do sistema operacional Haiku (família BeOS) por PVS-Studio. Parte 2 ;
- Como dar um tiro no pé em C e C ++. Livro de receitas do Haiku OS
O recurso da última análise de código é a capacidade de usar a versão oficial do PVS-Studio for Linux. Nem o PVS-Studio para Linux, nem um relatório conveniente para exibir erros estavam disponíveis em 2015. Desta vez, enviaremos o relatório completo em um formato conveniente para os desenvolvedores do Haiku.
Clássico
V501 Existem
subexpressões idênticas à esquerda e à direita do operador '-': (addr_t) b - (addr_t) b BitmapManager.cpp 51
int compare_app_pointer(const ServerApp* a, const ServerApp* b) { return (addr_t)b - (addr_t)b; }
Todo desenvolvedor precisa misturar as variáveis
aeb ,
xey ,
iej ... pelo menos uma vez na vida.
V501 Existem
subexpressões idênticas à esquerda e à direita do '||' operador: input == __null || input == __nulo MediaClient.cpp 182
status_t BMediaClient::Unbind(BMediaInput* input, BMediaOutput* output) { CALLED(); if (input == NULL || input == NULL) return B_ERROR; if (input->fOwner != this || output->fOwner != this) return B_ERROR; input->fBind = NULL; output->fBind = NULL; return B_OK; }
O mesmo ponteiro de
entrada é verificado na condição duas vezes. Enquanto o ponteiro de
saída permaneceu desmarcado, o que pode resultar na desreferência de ponteiro nulo.
Código fixo:
if (input == NULL || output == NULL) return B_ERROR;
V583 O
operador '?:', Independentemente de sua expressão condicional, sempre retorna um e o mesmo valor: 500000. usb_modeswitch.cpp 361
static status_t my_transfer_data(....) { .... do { bigtime_t timeout = directionIn ? 500000 : 500000; result = acquire_sem_etc(device->notify, 1, B_RELATIVE_TIMEOUT, timeout); .... } while (result == B_INTERRUPTED); .... }
O operador ternário se tornou inútil, quando o autor do código cometeu um erro e escreveu dois valores de retorno idênticos -
500000 .
V519 A variável 'm_kindex1' recebe valores duas vezes sucessivamente. Talvez isso seja um erro. Verifique as linhas: 40, 41. agg_trans_double_path.cpp 41
trans_double_path::trans_double_path() : m_kindex1(0.0), m_kindex2(0.0), m_base_length(0.0), m_base_height(1.0), m_status1(initial), m_status2(initial), m_preserve_x_scale(true) { } void trans_double_path::reset() { m_src_vertices1.remove_all(); m_src_vertices2.remove_all(); m_kindex1 = 0.0; m_kindex1 = 0.0; m_status1 = initial; m_status2 = initial; }
Há um erro na função de
redefinição : um erro de digitação no índice de variável
m_kindex2 . Essa variável não será redefinida, o que provavelmente afetará a execução de outros fragmentos de código.
V501 Existem
subexpressões idênticas à esquerda e à direita do operador '>': fg [order_type :: R]> fg [order_type :: R] agg_span_image_filter_rgba.h 898
typedef Source source_type; typedef typename source_type::color_type color_type; typedef typename source_type::order_type order_type; void generate(color_type* span, int x, int y, unsigned len) { .... if(fg[0] < 0) fg[0] = 0; if(fg[1] < 0) fg[1] = 0; if(fg[2] < 0) fg[2] = 0; if(fg[3] < 0) fg[3] = 0; if(fg[order_type::A] > base_mask) fg[order_type::A] = base_mask; if(fg[order_type::R] > fg[order_type::R])fg[order_type::R] = fg[order_type::R]; if(fg[order_type::G] > fg[order_type::G])fg[order_type::G] = fg[order_type::G]; if(fg[order_type::B] > fg[order_type::B])fg[order_type::B] = fg[order_type::B]; .... }
Nas últimas linhas, existem dois problemas ao mesmo tempo: comparação e atribuição de variáveis iguais. Nem sequer posso sugerir qual foi a ideia do autor. Vou observar esse trecho como suspeito.
V570 A variável 'wPipeIndex' é atribuída a si mesma. CEchoGals_transport.cpp 244
ECHOSTATUS CEchoGals::CloseAudio (....) { .... wPipeIndex = wPipeIndex; m_ProcessId[ wPipeIndex ] = NULL; m_Pipes[ wPipeIndex ].wInterleave = 0; .... }
A variável
wPipeIndex é inicializada por seu próprio valor. Provavelmente, um erro de digitação foi feito.
Erros com ponteiros
V522 A desreferenciação do ponteiro nulo 'currentInterface' pode ocorrer. Device.cpp 258
Device::Device(....) : .... { .... usb_interface_info* currentInterface = NULL;
O ponteiro
currentInterface é inicializado por nulo e depois verificado ao entrar nas ramificações do operador do
comutador , mas não em todos os casos. O analisador avisa que, ao pular para o rótulo do caso
USB_DESCRIPTOR_ENDPOINT_COMPANION , pode ocorrer uma desreferência do ponteiro nulo.
V522 A desreferenciação do ponteiro nulo 'diretório' pode ocorrer. PathMonitor.cpp 1465
bool PathHandler::_EntryCreated(....) { .... Directory* directory = directoryNode->ToDirectory(); if (directory == NULL) {
Eu acho que há um erro na condição de comparação do ponteiro de
diretório com o valor nulo; a condição tem que ser o oposto. Com a implementação atual, se a variável
dryRun for
falsa , o ponteiro nulo do
diretório será desreferenciado.
V522 A desreferenciação do ponteiro nulo 'entrada' pode ocorrer. MediaRecorder.cpp 343
void GetInput(media_input* input); const media_input& BMediaRecorder::MediaInput() const { CALLED(); media_input* input = NULL; fNode->GetInput(input); return *input; }
O ponteiro de
entrada é inicializado por nulo e permanece com esse valor, pois o ponteiro não está sendo alterado na função GetInput. Em outros métodos da classe
BMediaRecorder , a implementação é diferente, por exemplo:
status_t BMediaRecorder::_Connect(....) { ....
Está tudo correto aqui, mas o primeiro fragmento precisa ser reescrito, caso contrário, a função retornará uma referência a um objeto local.
V522 A desreferenciação do ponteiro nulo 'mustFree' pode ocorrer. RequestUnflattener.cpp 35
status_t Reader::Read(int32 size, void** buffer, bool* mustFree) { if (size < 0 || !buffer || mustFree)
Na expressão condicional em que todos os dados incorretos são verificados, o autor cometeu um erro de digitação ao verificar o ponteiro
mustFree . Provavelmente, a função deve sair ao ter o valor nulo deste ponteiro:
if (size < 0 || !buffer || !mustFree)
V757 É possível que uma variável incorreta seja comparada com nullptr após a conversão do tipo usando 'dynamic_cast'. Verifique as linhas: 474, 476. recover.cpp 474
void checkStructure(Disk &disk) { .... Inode* missing = gMissing.Get(run); dir = dynamic_cast<Directory *>(missing); if (missing == NULL) { .... } .... }
O desenvolvedor deve ter verificado o ponteiro
dir em vez de
faltar após a conversão do tipo. A propósito, os desenvolvedores de C # também cometem erros semelhantes. Isso prova mais uma vez que alguns erros não dependem do idioma usado.
Mais alguns lugares semelhantes no código:
- V757 É possível que uma variável incorreta seja comparada com nullptr após a conversão do tipo usando 'dynamic_cast'. Verifique as linhas: 355, 357. ExpandoMenuBar.cpp 355
- V757 É possível que uma variável incorreta seja comparada com nullptr após a conversão do tipo usando 'dynamic_cast'. Verifique as linhas: 600, 601. ValControl.cpp 600
Erros de índice
A saturação da matriz V557 é possível. O índice 'BT_SCO' está apontando além do limite da matriz. h2upper.cpp 75
struct bt_usb_dev { .... struct list nbuffersTx[(1 + 1 + 0 + 0)];
A matriz
bdev-> nbuffersTx consiste apenas de 2 elementos, mas é tratada pela constante BT_SCO, que é 3. Aí vem o índice da matriz surefire fora dos limites.
A saturação da matriz V557 é possível. A função 'ieee80211_send_setup' processa o valor '16'. Inspecione o quarto argumento. Verifique as linhas: 842, 911. ieee80211_output.c 842
struct ieee80211_node { .... struct ieee80211_tx_ampdu ni_tx_ampdu[16];
Outro índice de matriz fora dos limites. Desta vez, apenas por um elemento. A análise interprocedural ajudou a revelar o caso em que o array
ni-> ni_tx_ampdu , composto por 16 elementos, foi endereçado pelo índice 16. Nas matrizes C e C ++, o índice é zero.
V781 O valor da variável 'vetor' é verificado depois que foi usado. Talvez haja um erro na lógica do programa. Verifique as linhas: 802, 805. oce_if.c 802
#define OCE_MAX_EQ 32 typedef struct oce_softc { .... OCE_INTR_INFO intrs[OCE_MAX_EQ]; .... } OCE_SOFTC, *POCE_SOFTC; static int oce_alloc_intr(POCE_SOFTC sc, int vector, void (*isr) (void *arg, int pending)) { POCE_INTR_INFO ii = &sc->intrs[vector]; int rc = 0, rr; if (vector >= OCE_MAX_EQ) return (EINVAL); .... }
O analisador detectou que um elemento da matriz
sc-> intrs foi endereçado por um índice inválido, que estava fora dos limites. O motivo é a ordem incorreta das operações no código. Primeiro, o elemento é endereçado e depois vem a verificação se o valor do índice é válido.
Alguns podem dizer que não haverá problemas. Ele não remove o valor do elemento da matriz, apenas pega o endereço da célula. Mas não, não é assim que se faz as coisas. Leia mais: "A
desreferenciação de ponteiro nulo causa comportamento indefinido ".
V519 A variável recebe valores atribuídos duas vezes sucessivamente. Talvez isso seja um erro. Verifique as linhas: 199, 200. nvme_ctrlr.c 200
static void nvme_ctrlr_set_intel_supported_features(struct nvme_ctrlr *ctrlr) { bool *supported_feature = ctrlr->feature_supported; supported_feature[NVME_INTEL_FEAT_MAX_LBA] = true; supported_feature[NVME_INTEL_FEAT_MAX_LBA] = true; supported_feature[NVME_INTEL_FEAT_NATIVE_MAX_LBA] = true; supported_feature[NVME_INTEL_FEAT_POWER_GOVERNOR_SETTING] = true; supported_feature[NVME_INTEL_FEAT_SMBUS_ADDRESS] = true; supported_feature[NVME_INTEL_FEAT_LED_PATTERN] = true; supported_feature[NVME_INTEL_FEAT_RESET_TIMED_WORKLOAD_COUNTERS] = true; supported_feature[NVME_INTEL_FEAT_LATENCY_TRACKING] = true; }
O elemento da matriz com o índice
NVME_INTEL_FEAT_MAX_LBA recebe o mesmo valor. A boa notícia é que essa função apresenta todas as constantes possíveis, tornando esse código apenas o resultado da programação Copy-Paste. Mas as chances são de erros aparecerão aqui.
V519 A
variável 'copiedPath [len]' recebe valores duas vezes sucessivamente. Talvez isso seja um erro. Verifique as linhas: 92, 93. kernel_emu.cpp 93
int UserlandFS::KernelEmu::new_path(const char *path, char **copy) { ....
Bem, aqui o programador teve azar ao copiar. O símbolo "ponto" é adicionado a uma linha e é reescrito com um terminal nulo. É muito provável que o autor tenha copiado a linha e esquecido de aumentar o índice.
Condições estranhas
V517 O uso do
padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 1407, 1410. FindPanel.cpp 1407
void FindPanel::BuildAttrQuery(BQuery* query, bool &dynamicDate) const { .... case B_BOOL_TYPE: { uint32 value; if (strcasecmp(textControl->Text(), "true") == 0) { value = 1; } else if (strcasecmp(textControl->Text(), "true") == 0) { value = 1; } else value = (uint32)atoi(textControl->Text()); value %= 2; query->PushUInt32(value); break; } .... }
A cópia do código levou a dois erros ao mesmo tempo. As expressões condicionais são idênticas. Provavelmente, uma comparação com a string "false" em vez de "true" deve estar em uma delas. Além disso, na ramificação que lida com o valor "false", o
valor que deve ser alterado de
1 para
0 . O algoritmo requer que quaisquer outros valores, diferentes de
verdadeiro ou
falso, sejam convertidos em um número usando a função
atoi . Mas devido a um erro, o texto "false" entrará na função.
A expressão
V547 'erro == ((int) 0)' é sempre verdadeira. Directory.cpp 688
int32 BDirectory::CountEntries() { status_t error = Rewind(); if (error != B_OK) return error; int32 count = 0; BPrivate::Storage::LongDirEntry entry; while (error == B_OK) { if (GetNextDirents(&entry, sizeof(entry), 1) != 1) break; if (strcmp(entry.d_name, ".") != 0 && strcmp(entry.d_name, "..") != 0) count++; } Rewind(); return (error == B_OK ? count : error); }
O analisador detectou que o valor da variável de
erro sempre será
B_OK . Definitivamente, essa modificação de variável foi perdida no loop while.
V564 O operador '&' é aplicado ao valor do tipo bool. Você provavelmente esqueceu de incluir parênteses ou pretende usar o operador '&&'. strtod.c 545
static int lo0bits(ULong *y) { int k; ULong x = *y; .... if (!(x & 1)) { k++; x >>= 1; if (!x & 1)
É mais provável que na última expressão condicional tenha esquecido de colocar colchetes, como nas condições acima. É provável que o operador complementar esteja fora dos colchetes:
if (!(x & 1))
V590 Considere inspecionar esta expressão. A expressão é excessiva ou contém uma impressão incorreta. PoseView.cpp 5851
bool BPoseView::AttributeChanged(const BMessage* message) { .... result = poseModel->OpenNode(); if (result == B_OK || result != B_BUSY) break; .... }
Isso não é óbvio, mas o resultado da condição não depende do valor do valor B_OK. Portanto, pode ser simplificado:
If (result != B_BUSY) break;
Você pode verificá-lo facilmente desenhando uma tabela verdade para os valores da variável
resultado . Se alguém quiser considerar especificamente outros valores, diferentes de
B_OK e
B_BUSY , o código deve ser reescrito de outra maneira.
Mais dois fragmentos semelhantes:
- V590 Considere inspecionar esta expressão. A expressão é excessiva ou contém uma impressão incorreta. Tracker.cpp 1714
- V590 Considere inspecionar esta expressão. A expressão é excessiva ou contém uma impressão incorreta. if_ipw.c 1871
V590 Considere inspecionar o 'argc == 0 || argc! = 2 'expressão. A expressão é excessiva ou contém uma impressão incorreta. cmds.c 2667
void unsetoption(int argc, char *argv[]) { .... if (argc == 0 || argc != 2) { fprintf(ttyout, "usage: %s option\n", argv[0]); return; } .... }
Este é talvez o exemplo mais simples que demonstra o trabalho do diagnóstico
V590 . Você precisa exibir a descrição do programa, caso não haja argumentos passados ou se não houver dois deles. Obviamente, quaisquer valores que não sejam dois, incluindo zero, não satisfarão a condição. Portanto, a condição pode ser simplificada com segurança para isso:
if (argc != 2) { fprintf(ttyout, "usage: %s option\n", argv[0]); return; }
V590 Considere inspecionar o '* ptr =='; ' && * ptr! = '\ 0' 'expressão. A expressão é excessiva ou contém uma impressão incorreta. pc.c 316
ULONG parse_expression(char *str) { .... ptr = skipwhite(ptr); while (*ptr == SEMI_COLON && *ptr != '\0') { ptr++; if (*ptr == '\0') continue; val = assignment_expr(&ptr); } .... }
Neste exemplo, o operador lógico foi alterado, mas a lógica ainda é a mesma. Aqui, a condição do loop while depende apenas se o caractere é igual a
SEMI_COLON ou não.
V590 Considere inspecionar esta expressão. A expressão é excessiva ou contém uma impressão incorreta. writembr.cpp 99
int main(int argc, char** argv) { .... string choice; getline(cin, choice, '\n'); if (choice == "no" || choice == "" || choice != "yes") { cerr << "MBR was NOT written" << endl; fs.close(); return B_ERROR; } .... }
Já existem três condições neste exemplo. Também pode ser simplificado antes de verificar se o usuário escolheu "yes" ou não:
if (choice != "yes") { cerr << "MBR was NOT written" << endl; fs.close(); return B_ERROR; }
Diversos
V530 O valor de retorno da função 'begin' deve ser utilizado. IMAPFolder.cpp 414
void IMAPFolder::RegisterPendingBodies(...., const BMessenger* replyTo) { .... IMAP::MessageUIDList::const_iterator iterator = uids.begin(); for (; iterator != uids.end(); iterator++) { if (replyTo != NULL) fPendingBodies[*iterator].push_back(*replyTo); else fPendingBodies[*iterator].begin();
O analisador encontrou uma chamada inútil do iterador
begin (). Não consigo imaginar como consertar o código. Os desenvolvedores devem prestar atenção a este código.
V609 Divida por zero. Intervalo do denominador [0..64]. UiUtils.cpp 544
static int32 GetSIMDFormatByteSize(uint32 format) { switch (format) { case SIMD_RENDER_FORMAT_INT8: return sizeof(char); case SIMD_RENDER_FORMAT_INT16: return sizeof(int16); case SIMD_RENDER_FORMAT_INT32: return sizeof(int32); case SIMD_RENDER_FORMAT_INT64: return sizeof(int64); case SIMD_RENDER_FORMAT_FLOAT: return sizeof(float); case SIMD_RENDER_FORMAT_DOUBLE: return sizeof(double); } return 0; } const BString& UiUtils::FormatSIMDValue(const BVariant& value, uint32 bitSize, uint32 format, BString& _output) { _output.SetTo("{"); char* data = (char*)value.ToPointer(); uint32 count = bitSize / (GetSIMDFormatByteSize(format) * 8);
A função
GetSIMDFormatByteSize realmente retorna
0 como um valor padrão, o que pode levar à divisão por zero.
V654 A condição 'specificSequence! = Sequence' do loop é sempre falsa. pthread_key.cpp 55
static void* get_key_value(pthread_thread* thread, uint32 key, int32 sequence) { pthread_key_data& keyData = thread->specific[key]; int32 specificSequence; void* value; do { specificSequence = keyData.sequence; if (specificSequence != sequence) return NULL; value = keyData.value; } while (specificSequence != sequence); keyData.value = NULL; return value; }
O analisador está certo de que a condição do operador
while é sempre falsa. Devido a isso, o loop não executa mais de uma iteração. Em outras palavras, nada mudaria se você escrevesse
enquanto (0) . Tudo isso é estranho e esse código contém um erro de lógica. Os desenvolvedores devem considerar cuidadosamente esse trecho.
V672 Provavelmente não há necessidade de criar a nova variável 'caminho' aqui. Um dos argumentos da função possui o mesmo nome e esse argumento é uma referência. Verifique as linhas: 348, 429. translate.cpp 429
status_t Translator::FindPath(...., TypeList &path, double &pathQuality) { .... TypeList path; double quality; if (FindPath(&formats[j], stream, typesSeen, path, quality) == B_OK) { if (bestQuality < quality * formatQuality) { bestQuality = quality * formatQuality; bestPath.SetTo(path); bestPath.Add(formats[j].type); status = B_OK; } } .... }
A variável de
caminho é passada para a função
FindPath por referência. O que significa que essa variável pode ser modificada no corpo da função. Mas há uma variável local com o mesmo nome, que é modificada. Nesse caso, todas as alterações permanecerão apenas na variável local. O autor do código pode querer renomear ou remover a variável local.
V705 É possível que o bloco 'else' tenha sido esquecido ou comentado, alterando a lógica de operação do programa. HostnameView.cpp 109
status_t HostnameView::_LoadHostname() { BString fHostnameString; char hostname[MAXHOSTNAMELEN]; if (gethostname(hostname, MAXHOSTNAMELEN) == 0) { fHostnameString.SetTo(hostname, MAXHOSTNAMELEN); fHostname->SetText(fHostnameString); return B_OK; } else return B_ERROR; }
O exemplo de formatação incorreta de código. A palavra-chave "pendente"
else ainda não altera a lógica, mas depois que um fragmento de código é inserido antes do operador de
retorno , a lógica não será a mesma.
V763 O parâmetro 'menu' é sempre reescrito no corpo da função antes de ser usado. video.cpp 648
bool video_mode_hook(Menu *menu, MenuItem *item) { video_mode *mode = NULL; menu = item->Submenu(); item = menu->FindMarked(); .... }
Encontrei muitos casos em que os argumentos da função são reescritos ao entrar na função. Esse comportamento engana outros desenvolvedores que chamam essas mesmas funções.
A lista inteira de lugares suspeitos:
- V763 O parâmetro 'force_16bit' é sempre reescrito no corpo da função antes de ser usado. ata_adapter.cpp 151
- V763 O parâmetro 'force_16bit' é sempre reescrito no corpo da função antes de ser usado. ata_adapter.cpp 179
- V763 O parâmetro 'menu' é sempre reescrito no corpo da função antes de ser usado. video.cpp 264
- V763 O parâmetro 'length' é sempre reescrito no corpo da função antes de ser usado. MailMessage.cpp 677
- V763 O parâmetro 'entry' é sempre reescrito no corpo da função antes de ser usado. IconCache.cpp 773
- V763 O parâmetro 'entry' é sempre reescrito no corpo da função antes de ser usado. IconCache.cpp 832
- V763 O parâmetro 'entry' é sempre reescrito no corpo da função antes de ser usado. IconCache.cpp 864
- V763 O parâmetro 'rect' é sempre reescrito no corpo da função antes de ser utilizado. ErrorLogWindow.cpp 56
- V763 O parâmetro 'updateRect' é sempre reescrito no corpo da função antes de ser usado. CalendarMenuWindow.cpp 49
- V763 O parâmetro 'rect' é sempre reescrito no corpo da função antes de ser utilizado. MemoryView.cpp 165
- V763 O parâmetro 'rect' é sempre reescrito no corpo da função antes de ser utilizado. TypeEditors.cpp 1124
- V763 O parâmetro 'height' é sempre reescrito no corpo da função antes de ser usado. Workspaces.cpp 857
- V763 O parâmetro 'width' é sempre reescrito no corpo da função antes de ser usado. Espaços de trabalho.cpp 856
- V763 O parâmetro 'frame' é sempre reescrito no corpo da função antes de ser usado. SwatchGroup.cpp 48
- V763 O parâmetro 'frame' é sempre reescrito no corpo da função antes de ser usado. PlaylistWindow.cpp 89
- V763 O parâmetro 'rect' é sempre reescrito no corpo da função antes de ser utilizado. ConfigView.cpp 78
- V763 O parâmetro 'm' é sempre reescrito no corpo da função antes de ser usado. mkntfs.c 3917
- O parâmetro V763 'rxchainmask' é sempre reescrito no corpo da função antes de ser usado. ar5416_cal.c 463
- V763 O parâmetro 'c' é sempre reescrito no corpo da função antes de ser usado. if_iwn.c 6854
Conclusão
O projeto Haiku é uma fonte de erros interessantes e raros. Adicionamos ao nosso banco de dados alguns exemplos de erros e corrigimos alguns problemas do analisador que apareciam ao analisar o código.
Se você não verifica seu código com algumas ferramentas de análise de código há muito tempo, alguns dos problemas que descrevi provavelmente estão ocultos no seu código. Use o PVS-Studio em seu projeto (se escrito em C, C ++, C # ou Java) para controlar a qualidade do código. Baixe o analisador
aqui sem registro ou sms.
Deseja experimentar o Haiku e tiver dúvidas? Os desenvolvedores do Haiku convidam você para o
canal de telegrama .