Normalização Unicode e caracteres invisíveis
Texto que parece igual nem sempre é igual. O Unicode permite montar um mesmo caractere visível de várias formas, admite caracteres que não desenham nada e inclui pares indistinguíveis na tela mas diferentes por dentro. Este guia explica de onde vêm essas diferenças e o que fazer com elas antes que virem um login que falha, um registro duplicado ou um domínio falsificado.
Pontos de código, bytes e o que significa "comprimento"
Uma string tem pelo menos três comprimentos plausíveis, e eles discordam. "héllo" pode ser 5 caracteres para quem lê, 5 ou 6 pontos de código conforme o modo como o é foi construído, e 6 ou 7 bytes em UTF-8. Emojis ampliam a diferença: um emoji de família é uma única imagem montada a partir de várias pessoas unidas por conectores invisíveis, e a maioria das linguagens informa seu comprimento como algo entre 2 e 11.
Escolha a unidade que responde à sua pergunta. O comprimento em bytes é o que o limite de uma coluna e um cabeçalho HTTP medem. A contagem de pontos de código é o que quase toda API de strings devolve. O que uma pessoa entende por "caracteres" é a contagem de clusters de grafemas — aquilo que move o cursor um passo — e fazer um truncamento cair nessa fronteira é a diferença entre um corte limpo e um emoji quebrado.
O mesmo texto, duas codificações: NFC e NFD
"é" pode ser um único ponto de código (U+00E9) ou dois: um "e" simples seguido de um acento agudo combinante (U+0301). Os dois desenham igual, mas são sequências diferentes, então uma comparação ingênua diz que são strings distintas, um índice único do banco aceita ambos e uma busca por um não acha o outro. Normalizar é escolher uma forma canônica: NFC compõe no ponto de código único, NFD decompõe em base mais marca.
Não é um caso exótico. Texto digitado numa plataforma e colado de outra diverge rotineiramente — o macOS historicamente armazenou nomes de arquivo decompostos enquanto a maior parte da web envia a forma composta — e o hangul coreano tem a mesma divisão entre sílabas pré-compostas e jamo separados. NFC é o padrão certo para armazenamento e transmissão; o importante é escolher um e aplicá-lo na borda, não polvilhá-lo onde uma comparação por acaso falhou.
NFKC e NFKD: quando "quase igual" deve contar
As formas com K dobram também as diferenças de compatibilidade. Sob NFKC, a ligadura fi vira "fi", o A de largura total vira "A", o sobrescrito ² vira "2" e o espaço inquebrável vira um comum. É exatamente o que você quer ao comparar identificadores, nomes de usuário ou termos de busca, onde duas grafias não deveriam poder reivindicar a mesma vaga.
E é exatamente o que você não quer para exibir ou armazenar texto arbitrário, porque a transformação perde informação e é irreversível: letras matemáticas estilizadas desabam em ASCII simples e a formatação escolhida pelo autor desaparece. Use as formas com K para derivar uma chave de comparação e mantenha o original para mostrar de volta ao usuário.
Os caracteres que você não vê
Muitos pontos de código não desenham nada. Uma marca de ordem de bytes no início de um arquivo quebra a primeira chave de um documento JSON ou o primeiro cabeçalho de um CSV. Espaços e unidores de largura zero sobrevivem a um copiar e colar de um documento e quebram silenciosamente uma correspondência exata. Espaços inquebráveis parecem espaços mas não separam ao dividir por espaço em branco, e hifens suaves só aparecem quando a palavra quebra de linha.
Alguns são piores que bagunça. Caracteres de sobreposição bidirecional podem reordenar como o código-fonte é exibido sem mudar o que o compilador lê — o truque "Trojan Source", em que um comentário na tela é código executável por baixo. E homóglifos — а cirílico, ο grego, letras de largura total — deixam um domínio ou nome de usuário exatamente igual a um conhecido. Se uma string cruza uma fronteira de confiança, inspecione seus pontos de código em vez de confiar nos olhos.
Regras que mantêm o texto seguro
Normalize na borda, uma vez. Converta o texto que entra para NFC na porta do sistema e armazene assim; daí em diante as comparações internas comparam igual com igual. Para qualquer coisa usada como identidade — nomes de usuário, parte local do e-mail, slugs, chaves de busca — derive uma chave de comparação separada com NFKC mais dobra de maiúsculas, e imponha unicidade sobre essa chave, não sobre o valor exibido.
Depois decida, explicitamente, o que você permite. Remova a BOM, rejeite ou elimine caracteres de largura zero e controles bidirecionais em campos que não têm motivo para contê-los, e trate um identificador com escritas misturadas como suspeito, não como esperto. Nada disso é caro, e tudo junto sai muito mais barato que depurar por que um usuário específico não consegue entrar com uma senha que digita corretamente.
- NFC para armazenar e transportar; NFKC + dobra de maiúsculas para chaves de comparação.
- Normalize uma vez na borda, não a cada comparação.
- Remova a BOM antes de analisar JSON, CSV ou arquivos de configuração.
- Inspecione os pontos de código quando uma string parece certa mas se comporta errado.