Neste artigo, convidamos você a tentar encontrar um bug em uma função muito simples do projeto GNU Midnight Commander. Porque Por nenhuma razão em particular. Apenas por diversão. Bem, tudo bem, é mentira. Na verdade, queríamos mostrar outro bug que um revisor humano tem dificuldade em encontrar e o analisador de código estático PVS-Studio pode detectar sem esforço.
Um usuário nos enviou um e-mail outro dia, perguntando por que ele estava recebendo um aviso sobre a função
EatWhitespace (veja o código abaixo). Esta questão não é tão trivial quanto possa parecer. Tente descobrir por si mesmo o que há de errado com este código.
static int EatWhitespace (FILE * InFile) { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); }
Como você pode ver, o
EatWhitespace é uma pequena função; seu corpo é ainda menor que o comentário :). Agora, vamos verificar alguns detalhes.
Aqui está a descrição da função
getc :
int getc ( FILE * stream );
Retorna o caractere atualmente apontado pelo indicador de posição do arquivo interno do fluxo especificado. O indicador de posição do arquivo interno é avançado para o próximo caractere. Se o fluxo estiver no final do arquivo quando chamado, a função retornará
EOF e definirá o indicador de fim de arquivo para o fluxo. Se ocorrer um erro de leitura, a função retornará EOF e definirá o indicador de erro para o fluxo (ferror).
E aqui está a descrição da função
isspace :
int isspace( int ch );
Verifica se o caractere fornecido é um caractere de espaço em branco, conforme classificado pelo código de idioma C atualmente instalado. No código do idioma padrão, os caracteres de espaço em branco são os seguintes:
- espaço (0x20, '');
- feed de formulário (0x0c, '\ f');
- avanço de linha LF (0x0a, '\ n');
- retorno de carro CR (0x0d, '\ r');
- guia horizontal (0x09, '\ t');
- guia vertical (0x0b, '\ v').
Valor de retorno Valor diferente de zero se o caractere for um espaço em branco; zero caso contrário.
A função
EatWhitespace deve pular todos os caracteres de espaço em branco, exceto o avanço de linha '\ n'. A função também interromperá a leitura do arquivo quando encontrar o Fim do arquivo (EOF).
Agora que você sabe tudo isso, tente encontrar o bug!
Os dois unicórnios abaixo garantirão que você não espreite acidentalmente o comentário.

Figura 1. Hora da procura de erros. Os unicórnios estão esperando.Ainda sem sorte?
Bem, veja bem, é porque mentimos para você sobre o
isspace . Bwa-ha-ha! Não é uma função padrão - é uma macro personalizada. Sim, somos maus e deixamos você confuso.

Figura 2. Unicorn confusa os leitores sobre o isspace .A culpa não é nossa ou de nosso unicórnio, é claro. A falha para toda a confusão está nos autores do projeto GNU Midnight Commander, que fizeram sua própria implementação do
isspace no arquivo charset.h:
#ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t')
Com essa macro, os autores confundiram outros desenvolvedores. O código foi escrito sob a suposição de que
isspace é uma função padrão, que considera retorno de carro (0x0d, '\ r') um caractere de espaço em branco.
A macro personalizada, por sua vez, trata apenas caracteres de espaço e tabulação como caracteres de espaço em branco. Vamos substituir essa macro e ver o que acontece.
for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile))
A subexpressão ('\ n'! = C) é desnecessária (redundante), pois sempre será avaliada como verdadeira. É sobre isso que o PVS-Studio avisa emitindo o aviso:
V560 Uma parte da expressão condicional é sempre verdadeira: ('\ n'! = C). params.c 136.
Para deixar claro, vamos examinar três resultados possíveis:
- Fim do arquivo atingido. EOF não é um caractere de espaço ou tabulação. A subexpressão ('\ n'! = C) não é avaliada devido à avaliação de curto-circuito . O loop termina.
- A função leu algum caractere que não é um caractere de espaço ou tabulação. A subexpressão ('\ n'! = C) não é avaliada devido à avaliação de curto-circuito. O loop termina.
- A função leu um caractere de espaço ou tabulação horizontal. A subexpressão ('\ n'! = C) é avaliada, mas seu resultado é sempre verdadeiro.
Em outras palavras, o código acima é equivalente ao seguinte:
for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile))
Descobrimos que não funciona da maneira desejada. Agora vamos ver quais são as implicações.
Um desenvolvedor, que escreveu a chamada do
isspace no corpo da função
EatWhitespace , esperava que a função padrão fosse chamada. É por isso que eles adicionaram a condição impedindo que o caractere LF ('\ n') seja tratado como um caractere de espaço em branco.
Isso significa que, além do espaço e dos caracteres de tabulação horizontal, eles planejavam pular o feed de formulário e os caracteres de tabulação vertical também.
O mais notável é que eles queriam que o caractere de retorno de carro (0x0d, '\ r') também fosse ignorado. Porém, isso não acontece - o loop termina ao encontrar esse personagem. O programa acabará se comportando inesperadamente se novas linhas forem representadas pela sequência CR + LF, que é o tipo usado em alguns sistemas não UNIX, como o Microsoft Windows.
Para mais detalhes sobre os motivos históricos do uso de LF ou CR + LF como caracteres de nova linha, consulte a página da Wikipedia "
Nova linha ".
A função
EatWhitespace foi criada para processar arquivos da mesma maneira, independentemente de eles
usarem LF ou CR + LF como caracteres de nova linha. Mas falha no caso de CR + LF. Em outras palavras, se o seu arquivo é do mundo Windows, você está com problemas :).
Embora isso possa não ser um bug sério, especialmente considerando que o GNU Midnight Commander é usado em sistemas operacionais semelhantes ao UNIX, onde LF (0x0a, '\ n') é usado como um caractere de nova linha, insignificantes como esse ainda tendem a levar a irritantes problemas com a compatibilidade de dados preparados no Linux e Windows.
O que torna esse bug interessante é que você tem quase certeza de ignorá-lo enquanto realiza a revisão de código padrão. É fácil esquecer as especificidades da implementação da macro e alguns autores do projeto talvez não as conheçam. É um exemplo muito vívido de como a análise estática de código contribui para a revisão de código e outras técnicas de detecção de bugs.
Substituir funções padrão é uma má prática. A propósito, discutimos um caso semelhante da macro
#define sprintf std :: printf no artigo recente "
Apreciar a análise de código estático ".
Uma solução melhor seria atribuir à macro um nome exclusivo, por exemplo,
is_space_or_tab . Isso teria ajudado a evitar toda a confusão.
Talvez a função
isspace padrão
tenha sido muito lenta e o programador tenha criado uma versão mais rápida, suficiente para suas necessidades. Mas eles ainda não deveriam ter feito dessa maneira. Uma solução mais segura seria definir
isspace para que você obtenha código não compilável, enquanto a funcionalidade desejada pode ser implementada como uma macro com um nome exclusivo.
Obrigado pela leitura. Não hesite em
baixar o PVS-Studio e experimentá-lo com seus projetos. Como lembrete, agora também oferecemos suporte a Java.