Texto Zalgo - O que é e como evitá-lo (ou "Unicode e suas bizarrices 2")
Este post é mais um da série de "bizarrices do Unicode" que estou escrevendo. O primeiro foi esse.
Breve introdução
Um texto Zalgo é algo assim:
T̃͟͏̧̟͓̯̘͓͙͔o̤̫͋ͯͫ̂ ̥͍̫̻͚̦͖͇̌ͪ̇ͤ̑̐͋̾̕i̢͖̩͙͐͑ͬ̄̿̍̚ͅn̵̢̼̙̳̒̄ͥ̋̐v̡̟̗̹̻̜͕̲ͣ̐ͤͤ͒́oͫ͂̆͑ͩ҉͇̰͚̹̠̫͔̗k̷̭̬̭͙̹̺̯ͩ̌̾̒̋̓ͤ͛͘͠e̥͙̓̄̕ ̵̫͈ͪţ̱̺̺̑̿̉̌͛̂̇h͙̣̬̓̂͞ę̡̲̟͎͉̟͛̓̉̆̉͘ ͍̯̱͎̬͍ͬ̒ͣͩ͟͡ḥ̗͖̝̮̗̼ͮ̋̉̃͐̿ͪͅi̞͉̯͖̞͉̙ͬͦ̄͋̈̂ͥ̊́̕v̶̝̼̫͔̬̯̯ͯ͑̈͠e̪͓͕̦̪̗̠ͯ͛͌̀̉͘ͅ-̍̉ͦ̈́͌͏̸͉͍͖̥͓̭̗̖mͣͣͪ̇͂͏̳̤̺͓̜̪͙͕i͋̏̎ͤ͆́͏̛̰͉̮̬͙̩̩ͅn̴̴̞̗ͩ̓̉̋̕d̝͇̬̣́̃ͦ̀͡ ̖̩̞̯̇̍́̆ͮͤ̄ͬ͂͜͡r̷͎͙͉̀͒͐͛́è̫̹͙̔ͭͦ͑ͩͮͯṗ̶̸͍̺̘͈́̄ͫ̅ͣ̇̃̚r̨͚͎̙̻̜ͬͬ̆͆e̩̞̹͈̱͕ͮ̈́̒̑͛̊̌͛͘ŝ̱̹̠̂ͭͦ̅̅͛̚͜e̝̳̞̊̍̐ñ͔̟̳̱͔ͣ̅̄ͮt̴̫̻̮̤̿̆̂̊͘i̷̫ͤͨ̏ͤͧͅͅn̛̜̠͋͐͆̆͑̎̍͑̚g̶̜ͧ́ͭͯ͐ͬ͛̇͜ ̳̩͉̣̤̜̰̠̌ͤ̆̚͟c̷̝̥͉͓͕͓͇̣͉̒̈̇ͧ͆̌ͣͣ͟ḩͥ̅ͥ̇ͩ͗҉̳͎̱ą̍̀͆͑̈́͂҉͍̤̘͔̹̭̱͢o̥̮̘ͨ̎̄̎́͢s̯̮̖͒.̨͋̈́͑̈́ͪ̚҉̻͓͔̤̯̲̤̺̀ ̔ͨ̋̾̒̋̚̚͏͓̰͙̘͈I̧̗͇ͮ͠n̠̬͗ͮ̿ͭ͌̉̈ͦv̦͚͇̠̹̰̭ͤo̭̞ͫ̃ͨ̏ͥ̒́̚k̯̱͛ͫ͆͂̇͡͝i̗̪͎͈͕͚̥̫ͥ͂̐n̖̤͔̟̖͇̠͋̐̀ͅͅg̛͑ͪ͏͇̪͓̘ ̎̒ͯͥͭ͂͏̠͇̮̪t̡͍̫̘̯̼̣̋ͤ̒h̨͇͕̺͍͈̠͖̤̱ͮ̃e͖̝̣̽ͬ͊̀ ͍ͫ̅̆͊́͌͡fͫ̾͏̭͎̰̣̱̺̯̥é̻̪͖͙̮͓̊̌̓ͩ̑̆ē͙̤̪͈͉̦̭͛̃ͩͧͭ̏͒̕͢ľ̡҉̪͙̻̣̪̼̲i̜ͯ̄̐͠͝n̸̨̩̰̺̱̗̍̉̄̉̆͐̎g̹͖͈ͥ͗̍͝ͅ ̙̹͉̰̪̠͍͖͔ͧ̾̈́ͫ̔͋̈̍o̵̤̥͍ͬ̐̀̿̇ͯ̅f̵̜̟͚́͛͜͠ ̺̭̆̑ͭc̶̢̫̭͓̭̐͗h͍͉̞̩͕ͮ̉́͛͑ͤͯ͒͡ä̜̱͙͎̯̟ͮ̂͑̄͊̍̑͜oͫ̋̿͌͢͏̟̲̗s̵̮̟̙̻̗̭̲͂̍͑͑͂ͤ́̚ͅ.̵̨͉̟͓ͦͯͬͤ͌ͧ͑̾ ̝̥̰͋ͬ͂̋͛͑̚W̶̹̹̣̟͕ͪͤ̽̀i̳̪͉͇͇̟̲̇̃́ͧ̾ͭ̿̆ͯ͡t̬̪̬̑̿͊͊́̚h̋͑̈̇͋҉̴͖̜ͅͅo̴̠̭̯̫̽ͨṳ̡̟͕̝̦͓͖̪̇́́͢t̬̙̖̝͈͔̻͛͊ ̵̛̗̝̝̰̫̞̠̙̽̄̊͆o̺̮͆ͯ͊͑ͭ̚̕ṙ̶̟̺̱̜̺̻̘̰̀͊ͮ̀͡ḋ̢̰̂̎ͦͥe̛̩͍̯̍ͭͩͤͦ̚r͍̲̾ͯ̊͛͡.̮̙͙̠̦̜̳̋̓ͩ͒̓̒͜͝ ̷͋͢͏̬̤̼T̸̯̞̥ͤ̽͛̈̎̚͜ȟ̨̌͋͏̲͚̪̩͚̺͖̠é̻̰̞̣̍̕͟ ̧̩͔̥̘͓̘̽̂͑̏ͣ͑̓̃͝N̬̙͈̗͍̣̻̓́ͨͫͮ͘͜͡e͍̜̠͓̹̙͓̱͑͡z̵̙̦̳̲͓ͭ̑ͮ̀p͒͐҉̹͎̱̯̰͜ȩ̨̗̝͓̥̳̣̈̽ͧ͋̎́͑ͯ̚͞r̮̖̟̆ͩḑ͍͉͚̪̲̭͈̮ͮͬͣͭ̽̐̄̓̑́͜i̶̪͎̹̰̭͎͋͗ͧ͌̎͛͜͢a͉̦͇̖͓̰ͣͭ̾͗n̮̞̣̝̤̤͔̎̊͜ ̧̗͐̃̊͂͢h̴͙̳͉̺͕͌̉ͫ͐͒̋͊i̢̳͓̤̖͎̼ͤ̃̓ͫ̐ͬ̆͌̕͢ṽ͎͉̳̭̭̠̠̬̠͂͐͊̾͊ȩ̣̺̖̒̏̌͠-̛͕̼̞͌͑ͣͨͩ̃̒͢m̶̱̣̥̥̭̉̿ͩͧ̓͛̚ͅi̢͍̙̠ͩ͐̏n̹̩̬͓̬̦̲̈́̉͊̐̎d̨̧̛̤̭̯̥͓ͧ̒ͥ ̞̙̮͌ͫͬͪͤ̌ợ̫̏̋͋͒̈̀f̶̳̿̀̊͐ͅ ̑͘҉̱̯̤̘͕̺͡c̢̭̭̜̲̙̥͊̓ͤ̍ͨ́ẖ̫̖̆̃ͪ̌ͭ͞ä̷̫̺̩̘̪͍́ͤͫͫ̒ͦ͆͂͠͡o̥̖̥͚̾̋͋͊ͨ̆ͅs̘͉͔̲̫̪͕̠̟ͯ̒͋ͦ̒͢.̣̦̦̞ͮͬͩ ̰̦̤̞̹̮̟ͩ̓̂̓̉͒̏ͫ͢Z̎́̉͘͏̗̮a̧͌͋̂͛̏͏͏̣̩͍̞l̛̟̥͎ͨ͒̐ͤ͛ͅg̰̣ͫͣ̂̋͊ô̷̶͕̩͎̳̻̣̯̘̋̊̅ͧ̏ͫ.̴̮̯̺̻̻̣̻̣̗̿ͯ͒ ̹̮̮̱͖̫̝̋ͥ͆̌͛ͬ͌̄͗͟H̢̅҉̘̠̻e̷͊͊͋ͪ̑҉̮̻̬̞̖̖̭̩̩̕ ̬̪̍͜ͅw̴̧̳͓͓̤̤ͮ̍ͨ͒͜ͅh̳̱ͪ͋ͦ͗o͚͎̭͎͍̺̽ͣͦͅ ̣͙͊̑ͣ̄W͍͔̻͉̲̹͇̼̑̇ͥ̔̎ͮ̚͟a̫̞ͪ̔͘͜į̷̶͔͍̰͙̖̻̲ͯ̓̄͌̄̒̔̆t̷͚͓̼̼̔̈̄̀̚sͭ̎͛ͨ͏̼̻̗͚̼̫ ̵̨̦ͪ͂̂B͂̈́҉̸͎̩͓e͌͂҉̤͕̲͇̮h̴̤͎̟̫̐̀̐ͬį̖̪̜̦̋̔n̨̯͚̽͗̾̀̾̿̏̀d̙͔ͬͧ̃ͯ̀ͦͫ͌ ̸̮͓͎̣̻̪̹̒̈́ͧ̀͐̓̿͘͠T̮͎̫̝ͫ̔͛̈ͯ̈ͬͣ̈́͟h̲̉̆̀e̬̭ͧ̉ͮ̓ͥ̈́ͨ ̉̆̏͠͏̖̯͚̰W͎̞͕̺ͣ̉̓ͧ̎̽͟a̢̝̝̎̅̌̂ͦ͡ḽ͖͉̽ͥ̌̓̉̆l̠̣̾ͣ̾͛̕.̷̡̩̰̀̑͑̽͋̃̌̓ ̴͙̼̭̺̙̱͚̳̪ͨ̈́́Z̵̶̲̘̗̩̜̍ͭ̀̿͒̊ͫA̴̸ͧ̊̚҉̺͖̠͖̜̠̩͇L̶̲̠̾͗ͦ͐͌ͯͤ̇G̫̞͔̭̻̖̥̜̗͊ͪͦͧ̔ͩ̀͘͜Õ̷͉͈̖̭̰̲̪̾ͭͯ͆̚
Caso o seu browser não renderize corretamente, segue uma imagem:
Não é algo tão difícil de criar, existem vários geradores online.
Embora não pareça texto "normal", meio que é - vc pode copiar e colar normalmente (se vai ser mostrado corretamente, é outra história, depende do editor que vc estiver usando).
Mas como isso funciona? Como é criado, e como detectá-lo e evitar que minha aplicação aceite entradas de dados como esta?
Para responder a estas perguntas, primeiro precisamos entender como um zalgo text é feito.
Unicode Combining Characters
No Unicode existe a definição de combining characters. Basicamente, de maneira bem resumida, são caracteres que podem ser combinados com outros, para formar caracteres diferentes.
Um exemplo existente no português são os acentos (agudo e circunflexo), o til e a cedilha. Então se você combina a letra a
minúscula com o acento agudo (COMBINING ACUTE ACCENT), o resultado é o caractere á
(letra "a" acentuada).
Atualmente existem mais de 2000 caracteres com esta característica de poderem ser combinados com outros, listados nas categorias Mn (Mark, Nonspacing), Me (Mark, Enclosing) e Mc (Mark, Spacing).
O fato é que existem muitos idiomas nos quais é perfeitamente válido ter mais de um combining character aplicado à mesma letra, e por isso o Unicode permite esta situação. No caso do texto no início do texto, por exemplo, os primeiros caracteres são:
caractere | code point | Categoria | Nome |
---|---|---|---|
T | U+0054 | Lu | LATIN CAPITAL LETTER T |
̃ | U+0303 | Mn | COMBINING TILDE |
͟ | U+035F | Mn | COMBINING DOUBLE MACRON BELOW |
͏ | U+034F | Mn | COMBINING GRAPHEME JOINER |
̧ | U+0327 | Mn | COMBINING CEDILLA |
̟ | U+031F | Mn | COMBINING PLUS SIGN BELOW |
͓ | U+0353 | Mn | COMBINING X BELOW |
̯ | U+032F | Mn | COMBINING INVERTED BREVE BELOW |
̘ | U+0318 | Mn | COMBINING LEFT TACK BELOW |
͓ | U+0353 | Mn | COMBINING X BELOW |
͙ | U+0359 | Mn | COMBINING ASTERISK BELOW |
͔ | U+0354 | Mn | COMBINING LEFT ARROWHEAD BELOW |
Para entender o que é um code point, leia aqui.
Ou seja, o texto começa com a letra "T" seguida de 11 combining characters. Somente esta sequência de code points produz isso:
T̃͟͏̧̟͓̯̘͓͙͔
Segue uma imagem caso não renderize corretamente:
O restante do zalgo text segue o mesmo padrão: uma letra seguida de vários combining characters.
Outra característica que faz com que o zalgo text fique dessa forma tão peculiar é o algoritmo de stacking (definido no mesmo documento já citado), que define o que pode acontecer quando mais de um combining character é aplicado à mesma letra.
Basicamente, cada novo combining character pode acabar sendo renderizado acima ou abaixo dos já existentes (cada um tem uma regra que diz em que posição ele deve ir, mas a renderização exata também depende da fonte utilizada). Veja abaixo o que acontece quando vamos adicionando combining characters à letra T (a cada linha, um novo combining character é adicionado aos já existentes):
T <-- letra "T" sem nenhum combining character
T̃ <-- adicionado COMBINING TILDE
T̃͟ <-- adicionado COMBINING DOUBLE MACRON BELOW
T̃͟͏ <-- adicionado COMBINING GRAPHEME JOINER
T̃͟͏̧ <-- adicionado COMBINING CEDILLA
T̃͟͏̧̟ <-- adicionado COMBINING PLUS SIGN BELOW
T̃͟͏̧̟͓ <-- adicionado COMBINING X BELOW
T̃͟͏̧̟͓̯ <-- adicionado COMBINING INVERTED BREVE BELOW
T̃͟͏̧̟͓̯̘ <-- adicionado COMBINING LEFT TACK BELOW
T̃͟͏̧̟͓̯̘͓ <-- adicionado COMBINING X BELOW
T̃͟͏̧̟͓̯̘͓͙ <-- adicionado COMBINING ASTERISK BELOW
T̃͟͏̧̟͓̯̘͓͙͔ <-- adicionado COMBINING LEFT ARROWHEAD BELOW
Ou seja, como o Unicode permite vários combining characters aplicados ao mesmo caractere, e na renderização esses são "empilhados" de forma acumulativa (podendo inclusive "invadir" as linhas de cima ou de baixo), o resultado é esse visual tão característico do zalgo text.
Uma tentativa de detectar zalgo text
Sendo assim, um critério para detectar que o texto é zalgo poderia ser verificar se há "muitos" combining characters seguidos. Os exemplos abaixo serão em PHP, por ter suporte amplo a Unicode Property Escapes. Um exemplo seria:
$texto = // string possivelmente contendo zalgo text
if (preg_match('/\p{M}{2,}/u', $texto)) {
echo "zalgo\n";
} else {
echo "not zalgo\n";
}
A ideia é usar regex com Unicode properties. No caso, \p{M}
pega qualquer caractere que esteja nas categorias "Mark" (as três já citadas: Mn (Mark, Nonspacing), Me (Mark, Enclosing) e Mc (Mark, Spacing)).
E o quantificador {2,}
indica "duas ou mais ocorrências", ou seja, se tiver 2 ou mais combining characters, eu já considero que é zalgo. Isso pode ser o suficiente se você quer aceitar apenas textos em português, já que neste idioma os caracteres têm no máximo um combining character.
Mas vale lembrar que há idiomas em que este limite pode ser maior. O Unicode define o conceito de Stream-safe Text Format, que de certa forma "define um limite" de 30 combining characters seguidos. Mas na prática, 30 é muito, pois a maior sequência conhecida é o caractere tibetano HAKṢHMALAWARAYAṀ, que é uma letra seguida de 8 combining characters. É esse aqui, e admito que para quem não conhece, pode muito bem ser confundido com zalgo text:
ཧྐྵྨླྺྼྻྂ
Segue imagem caso não tenha conseguido ver direito:
Então a menos que você vá aceitar textos em tibetano, usar \p{M}{8,}
seria uma alternativa válida. Dependendo da quantidade escolhida, você pode acabar excluindo textos válidos em outros idiomas (muitos usam 2, 3 ou até mais combining characters), então vai ter que ajustar esse valor de acordo com as strings que você recebe, para evitar falsos positivos.
Claro que também pode-se argumentar - e aí é uma discussão bem subjetiva - que com apenas 2 ou 3 combining characters o texto não fica "zalgo o suficiente" (veja os exemplos do "T" acima, com apenas 2 ou 3 combining characters adicionados), pois visualmente não ficaria tão... "zalguificado". Ou seja, é bem difícil definir um critério preciso que atenda a todos os casos.
Outra forma de fazer seria usar a extensão php-intl
(não esqueça de instalá-la). Com ela podemos iterar pelos code points da string e verificar se há uma sequência de combining characters maior que determinado tamanho:
// verifica se o caractere é um combining character
function isCombining($char) {
$t = IntlChar::charType($char);
return $t === IntlChar::CHAR_CATEGORY_NON_SPACING_MARK ||
$t === IntlChar::CHAR_CATEGORY_ENCLOSING_MARK ||
$t === IntlChar::CHAR_CATEGORY_COMBINING_SPACING_MARK;
}
function isZalgo($text) {
$max_allowed = 2; // considera que mais de 2 combining characters já é zalgo
$i = IntlBreakIterator::createCodePointInstance();
$i->setText($text);
$seq = 0; // conta o tamanho da sequência de combining characters
foreach($i->getPartsIterator() as $c) {
if (isCombining($c)) {
$seq++;
} else {
if ($seq > $max_allowed) {
return TRUE;
}
$seq = 0;
}
}
return $seq > $max_allowed;
}
$text = // texto que você quer verificar
if (isZalgo($text)) {
// zalgo detected!
}
Por aí vc vai encontrar soluções diversas, como remover todos os combining characters (que no caso, vai acabar removendo os acentos válidos também, o que pode não ser o desejado), escolher quais remover, etc. Não tem solução universal, tudo depende do contexto.
No fim, vale o que já foi dito: quanto maior a quantidade de combining characters seguidos que são aceitos, menos casos válidos são deixados de fora. Em compensação, aumenta as possibilidades de zalgo text, pois quanto mais caracteres seguidos são aceitos, mais combinações - incluindo as inválidas (sendo que "inválidas" vai depender do contexto, do idioma usado, etc) - são possíveis.
E neste caso, outra alternativa seria ter whitelists de sequências válidas de combining characters nos idiomas que sua aplicação vai aceitar. Por exemplo, em português, seriam apenas vogais seguidas de um dos acentos, ou a letra c
seguida do cedilha (COMBINING CEDILLA). Algo do tipo:
// textos em português: testa se depois do "c" tem a cedilha, ou se tem alguma consoante
// com acento, ou se tem vogal com algo diferente do acento agudo, circunflexo e til (ou a letra "a" com crase)
// (testado em alguns casos mais básicos, precisa de mais testes para ter certeza que é eficaz)
if (preg_match('/c[^\P{M}\x{327}]|[^aeiouc]\p{M}|[eiou][^\P{M}\x{301}-\x{303}]|a[^\P{M}\x{300}-\x{303}]/iu', $texto)) {
echo "invalido\n";
}
Ou seja, em vez de detectar um zalgo text, eu tento verificar se é um texto válido (cuja definição varia conforme o contexto). E aí tanto faz se o texto inválido é zalgo ou seja lá o que for. É mais trabalhoso, mas tem o tradeoff de ser mais assertivo. No fim, não há uma solução mágica que funcione em 100% dos casos.
Referências: