Текстовые редакторы, основная задача которых — отображение моноширинного шрифта (например, кода), должны, как и следует из названия, показывать символы одной ширины.

Но есть нюанс
В Unicode есть символы, видеть которые не положено. Текстовый редактор может просто отрендерить текст с таким символом, а может предпринять какие-то действия, чтобы сделать его заметным.
Кто же они?
| Код | Пример | Название |
|---|---|---|
| U+2060 | foo?bar | WORD JOINER |
| U+2061 | foo?bar | FUNCTION APPLICATION |
| U+2062 | foo?bar | INVISIBLE TIMES |
| U+2063 | foo?bar | INVISIBLE SEPARATOR |
| U+180E | foo?bar | MONGOLIAN VOWEL SEPARATOR |
| U+200B | foo?bar | ZERO WIDTH SPACE |
| U+200C | foo???bar | ZERO WIDTH NON-JOINER |
| U+200D | foo?bar | ZERO WIDTH JOINER |
| U+FEFF | foo?bar | ZERO WIDTH NO-BREAK SPACE |
Word joiner, U+2060
Пришёл на смену zero-width no-break space (U+FEFF), потому что U+FEFF стал использоваться для кодирования BOM (byte-order mark, несколько байт в начале файла, обозначающие его кодировку и порядок байт). Этот символ запрещает перенос строки там, где он встречается.
Zero-width no-break space, U+FEFF
Устаревший символ, заменён на word joiner, использовался в тех же целях.
Zero-width joiner, U+200D
Используется в индийских и арабских шрифтах для объединения символов, которые без него не были бы соединены.
Zero-width non-joiner, U+200C
В начертаниях с лигатурами можно вставить его между буквами, чтобы лигатуры не было:

Он встречается даже на клавиатурах:

Zero-width space, U+200B
Используется, когда нужно обозначить границу слов, не вставляя пробел. Этот текст будет переноситься по словам:
Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word?Word
А этот нет:
WordWordWordWordWordWordWordWordWordWordWordWordWordWordWordWordWordWordWordWordWordWord
Invisible Operators: function application U+2061, invisible times U+2062, invisible separator U+2063
"Невидимые операторы", добавленные в Unicode 3.2. Нужны для обозначения математических операций в выражениях.
Например, эта запись: Aij
Может означать или индекс (i, j) в двумерном массиве, или индекс i*j в одномерном. Для устранения неоднозначности можно использовать или Invisible times, или Invisible separator, чтобы было понятно, что имелось в виду.
Аналогично, f (x + y), это или умножение, или функция.
Визуально они не должны отличаться, но некоторые парсеры смогут понять, что имелось в виду.
Mongolian vowel separator, U+180E
Из названия понятно, для чего он. Этот символ уже не раз вызывал проблемы. Очень хорошо описан в этом ответе.
Как это выглядит
Конечно же, отображение зависит не только от редактора, но ещё и от шрифта, посмотрим на рендеринг текста, не меняя настроек редакторов.
Atom, Sublime, VSCode, Xamarin Studio, XCode, Notepad++:

Cat не показывает их:

Но если его запустить с параметром -A в linux или -v в macOS, то почти все символы видны (спасибо за подсказку в комментариях):
cat -v invisibles.txt
U+2060 foo?M-^A?bar WORD JOINER
U+2061 foo?M-^A?bar FUNCTION APPLICATION
U+2062 foo?M-^A?bar INVISIBLE TIMES
U+2063 foo?M-^A?bar INVISIBLE SEPARATOR
U+180E foo?M-^Nbar MONGOLIAN VOWEL SEPARATOR
U+200B foo?M-^@M-^Kbar ZERO WIDTH SPACE
U+200C foo?M-^@?M-^@?M-^@M-^Lbar ZERO WIDTH NON-JOINER
U+200D foo?M-^@M-^Mbar ZERO WIDTH JOINER
U+FEFF foo?bar ZERO WIDTH NO-BREAK SPACEVim тоже не сообщает о некоторых символах, даже с включённой настройкой set list, а вот less справляется лучше:

Web
GitHub, вот так показываются эти символы в pull request-ах и diff-ах:

Один из популярных редакторов кода, CodeMirror:

В том же CodeMirror, используемом jsbin, в IE часть символов видна:

ACE догадывается, что там бяка, и говорит, что что-то тут нечисто, но вот что именно — показывает не всегда:

Редакторы кода и diff tools
Редакторы на платформе IntelliJ:

Разные инструменты сравнения кода под macOS (P4Merge, FileMerge, KDiff3):

KDiff3, попытка засчитана, но этого не достаточно.
SourceTree: не обрабатывает текст вообще никак, плохо:

Tortoise, тоже почти ничего:

git diff: молодец, показал всё, ещё и выделил (хотя, на самом деле, сделал это less). Просто прекрасно, для diff tools это образец для подражания:

Anguish: brainfuck, которого нет
Кто-то сделал язык программирования Anguish, использующий только невидимые символы. Он основан на brainfuck, но использует не знаки пунктуации, а символы, о которых мы говорили выше. Есть даже интерпретатор на Perl и примеры использования.
Эксплуатация
Плохой код, фу таким быть, сделать закладку можно совсем просто:
function f() {
// ну вы поняли, на что заменить
return 'access_denined';
}
let code = f();
if (code === 'access_denied') {
return 401;
}Что делать
Пиши чистый код, %username%. Следуй best practices, их придумали не просто так, а для того чтобы держать меньше вещей в голове, в том числе своевременно замечая такие штуки. Увидел магическую строчку, странный или непроверяемый default case, ещё что-то: есть время — не поленись, перепиши как надо. Проводи код-ревью, смотри что коммитят в твою репу, поддерживай хорошее покрытие. Помни, что строке может быть не только то, что видно на экране, проверь в hex-редакторе, если возникло подозрение.
Вообще, вероятность реализации бэкдора через невидимый символ, конечно, есть, но скорее нет, чем да: найти его достаточно просто, а вставить закладку в говнокод можно и другими методами.
Почитать
- Unicode Demystified, A Practical Programmer’s Guide to the Encoding Standard, by Richard Gillam (вы знаете, где искать) — хорошая книга про unicode, многое рассказано, в том числе и о таких символах
Комментарии (42)

iloskutov
08.10.2016 15:59+10Cat не показывает их
Он вообще ничего не показывает — он просто выводит содержимое файла на консоль, а она уже отображает его так, как считает нужным. По Вашему же скриншоту видно, что поведение разных терминалов несколько отличается, но cat здесь совершенно ни при чём

JIghtuse
08.10.2016 19:22+2Да, стоило его хотя бы с опцией
-Aзапустить — отображает непечатаемое. Не знаю, всё ли — исходников тестов не приложено.
Antelle
08.10.2016 19:34+2Исходник вот: https://gist.github.com/antelle/216aa1f34c0b9dee89e574934325d2ce
Проверил на маке, видно почти всё, только опция в cat под macos называется -v. Под linux не проверял, могу предположить, что будет так же, возможно ещё последний появится.
outputcat -v invisibles.txt U+2060 foo?M-^A?bar WORD JOINER U+2061 foo?M-^A?bar FUNCTION APPLICATION U+2062 foo?M-^A?bar INVISIBLE TIMES U+2063 foo?M-^A?bar INVISIBLE SEPARATOR U+180E foo?M-^Nbar MONGOLIAN VOWEL SEPARATOR U+200B foo?M-^@M-^Kbar ZERO WIDTH SPACE U+200C foo?M-^@?M-^@?M-^@M-^Lbar ZERO WIDTH NON-JOINER U+200D foo?M-^@M-^Mbar ZERO WIDTH JOINER U+FEFF foo?bar ZERO WIDTH NO-BREAK SPACE

Sirikid
08.10.2016 18:28+1Очевидно мы должны добавить в Unicode специальный символ для отображения невидимых символов (в случае необходимости).

Antelle
08.10.2016 18:38+1Обычно принято использовать или bullet (•), или replacement character (?), или
квадратик такой

Power
08.10.2016 21:03+7А
git diffне молодец и он ничего не выделил. Он просто раскрасил строчки целиком и скормил на входless.

vbif
08.10.2016 23:54+1В Википедии до сих пор не могут решить, что делать с неразрывными пробелами в викификаторе: при викификации конструкции типа « » превращаются в неразрывные пробелы, а неразрывные пробелы при последующей викификации превращаются в обычные. Такое поведение автор объясняет тем, что неразрывные пробелы в редакторе не отличить от обычных, и потому надо от них избавляться. Тема поднималась уже не один раз, и все соглашались, что это — очевидная недоработка, что так делать нельзя, но отремонтировать до сих пор никто не берётся.

Crandel
09.10.2016 07:28+2Arch linux, Emacs через терминал
Скриншот
equand
09.10.2016 11:49+1FreeBSD default ee editor
U+2060 fooa~A bar WORD JOINER U+2061 fooa~A?bar FUNCTION APPLICATION U+2062 fooa~A?bar INVISIBLE TIMES U+2063 fooa~A?bar INVISIBLE SEPARATOR U+180E fooa ~Nbar MONGOLIAN VOWEL SEPARATOR U+200B fooa~@~Kbar ZERO WIDTH SPACE U+200C fooA~@A~@a~@~Lbar ZERO WIDTH NON-JOINER U+200D fooa~@~Mbar ZERO WIDTH JOINER U+FEFF fooi»?bar ZERO WIDTH NO-BREAK SPACE U+0020 foo bar SPACE U+00A0 foo bar NO-BREAK SPACE U+1680 fooa~Z~@bar OGHAM SPACE MARK U+2000 fooa~@~@bar EN QUAD U+2001 fooa~@~Abar EM QUAD U+2002 fooa~@~Bbar EN SPACE U+2003 fooa~@~Cbar EM SPACE U+2004 fooa~@~Dbar THREE-PER-EM SPACE U+2005 fooa~@~Ebar FOUR-PER-EM SPACE U+2006 fooa~@~Fbar SIX-PER-EM SPACE U+2007 fooa~@~Gbar FIGURE SPACE U+2008 fooa~@~Hbar PUNCTUATION SPACE U+2009 fooa~@~Ibar THIN SPACE U+200A fooa~@~Jbar HAIR SPACE U+202F fooa~@?bar NARROW NO-BREAK SPACE U+205F fooa~A~_bar MEDIUM MATHEMATICAL SPACE U+3000 fooa~@~@bar IDEOGRAPHIC SPACE
FreeBSD default vi
U+2060 foo\xe2\x81\xa0bar WORD JOINER U+2061 foo\xe2\x81\xa1bar FUNCTION APPLICATION U+2062 foo\xe2\x81\xa2bar INVISIBLE TIMES U+2063 foo\xe2\x81\xa3bar INVISIBLE SEPARATOR U+180E foo\xe1\xa0\x8ebar MONGOLIAN VOWEL SEPARATOR U+200B foo\xe2\x80\x8bbar ZERO WIDTH SPACE U+200C foo\xc2\x80\xc2\x80\xe2\x80\x8cbar ZERO WIDTH NON-JOINER U+200D foo\xe2\x80\x8dbar ZERO WIDTH JOINER U+FEFF foo\xef\xbb\xbfbar ZERO WIDTH NO-BREAK SPACE U+0020 foo bar SPACE U+00A0 foo bar NO-BREAK SPACE U+1680 foo\xe1\x9a\x80bar OGHAM SPACE MARK U+2000 foo\xe2\x80\x80bar EN QUAD U+2001 foo\xe2\x80\x81bar EM QUAD U+2002 foo\xe2\x80\x82bar EN SPACE U+2003 foo\xe2\x80\x83bar EM SPACE U+2004 foo\xe2\x80\x84bar THREE-PER-EM SPACE U+2005 foo\xe2\x80\x85bar FOUR-PER-EM SPACE U+2006 foo\xe2\x80\x86bar SIX-PER-EM SPACE U+2007 foo\xe2\x80\x87bar FIGURE SPACE U+2008 foo\xe2\x80\x88bar PUNCTUATION SPACE U+2009 foo\xe2\x80\x89bar THIN SPACE U+200A foo\xe2\x80\x8abar HAIR SPACE U+202F foo\xe2\x80\xafbar NARROW NO-BREAK SPACE U+205F foo\xe2\x81\x9fbar MEDIUM MATHEMATICAL SPACE U+3000 foo\xe3\x80\x80bar IDEOGRAPHIC SPACE
vadimr
09.10.2016 12:57+4Интересно, сколько проблем возникает из-за невероятно актуальной предоставленной программистам возможности именовать объекты на древнемонгольском. При том, что сами монголы давно перешли на кириллицу.

a-motion
10.10.2016 09:09+1Полагаю, что существуют люди, которые изучают древнемонгольский, и эти люди, в целом, ничуть не хуже нас с вами. Я лично около двадцати пяти лет назад немало натерпелся от бородатых программистов, которые рассуждали так: «интересно, сколько проблем может возникнуть из-за невероятно актуальной предоставленной программистам возможности писать по-русски, при том, что русские отлично способны использовать транслит».
Проблемы криворуких программистов, которым в 2016 году в диковинку существование таких сущностей — должны оставаться проблемами [отныне безработных] программистов.
vadimr
10.10.2016 09:28+2Изучение древнемонгольского каким-то образом побуждает именовать на этом языке переменные в программах?

a-motion
10.10.2016 09:55-3Изучение древнемонгольского побуждает алкать возможности вводить такие символы.
Здравый смысл побуждает не использовать конструкцию
if valid(symbol) then ... else ...в парсерах. Потому что не автору парсера решать, что мне втемяшится использовать в качестве идентификатора, это не его собачье дело (если он, конечно, достаточно квалифицирован.)
Я так именую методы, которые пишут некий «особенный» внутренний лог:
?и?, и я не стану пользоваться говноязыком, который мне это сделать не позволит.vadimr
10.10.2016 10:00+3Забавно Вам будет отлаживать Вашу программу с какого-нибудь удалённого терминала, не поддерживающего юникодный ввод.

a-motion
10.10.2016 10:33-3Забавно Вам будет отлаживать Вашу программу с какого-нибудь удалённого терминала,
не поддерживающего юникодный ввод.Здесь смешно все: и наличие в наше время терминалов, не поддерживающих юникод, и отладка с удаленного терминала. Оглядитесь, тут вокруг уже XXI век наступил, причем — больше пятнадцати лет тому назад. Ну и да, я умею настраивать деплой так, чтобы ржать в голос над словами «отладка с удаленного терминала».

Alex_kk
09.10.2016 18:25+1Статья полезна тем, что показывает, что есть неочевидное там, где, казалось бы, все очевидно.
А что касается практического использования Unicode в разработке, то следует помнить о «нестандартных» «пробельных» символах (см., напр., \u00A0, \u0082, \u0083, \u0085) и о символах управления потоком (RTL/LTR; см., напр., \u202A \u202B \u202C \u202D \u202E). На опыте проверено, что такие символы могут встречаться в пользовательском вводе (и, что удивительно, в системах электронного документооборота в России!). За идентификаторами в программах мы, программисты, уж как нибудь уследим. :) А вот за пользователем…
И, разумеется, следует помнить об эквивалентности графических изображений, возникающих из различных комбинаций кодов Unicode (Unicode Equivalence).
hdfan2
10.10.2016 09:23+4Вспомнилось. На приёме у логопеда:
— Доктор, у меня проблема. Я не выговариваю букву «...»!
— Какую?
— НУ, БУКВУ «...»!!!

Unk
10.10.2016 17:08+4Я решил довести идею скрытых символов до
абсурдавозможности применения и написал невидимый кодировщик

stychos
11.10.2016 12:31+1Реквестирую название указанной в топике клавиатуры.

Antelle
11.10.2016 12:39Вот отсюда: https://en.wikipedia.org/wiki/File:German-T2-Keyboard-Prototype-May-2012.jpg
Скорее всего, типографские символы — гравировка просто.
grossws
11.10.2016 12:47Они через alt gr набираются, если ОС поддерживает. Т. е. в обычном режиме там используется три функции на клавишу.

Antelle
11.10.2016 12:50Набираются, но кажется, stychos интересуется, где взять клавиатуру, чтобы нарисованы именно были.

stychos
11.10.2016 12:52Да, было бы интересно иметь клавиатуру с третьим ярусом из всех непечатаемых и дополняющих символов. Кажется, это то, чего ещё нет )

Antelle
11.10.2016 12:54Закажите гравировку или печать, оно недорого стоит, могут что угодно нанести.

stychos
11.10.2016 12:56Я практически в третьем мире живу, тут не так просто сделать качественную гравировку или печать чего-либо кроме самоклеек. Хотя, наверно, китайцы сейчас могут делать штучные заказы и на эту тему — надо будет попробовать.

stychos
11.10.2016 12:54В macOS выбор богат для AltGr, но всё равно, далеко не все. Иногда бывает необходимо, например, часто вставлять символ «Indent to here».

a-motion
11.10.2016 12:57В macOS выбор богат для AltGr
Бгг. Вот моя раскладка: https://github.com/mudasobwa/huya-xkb
Кроме того, даже в MacOS можно сделать свою какую-угодно раскладку, просто, как и все остальное, немного неестественным образом.

stychos
11.10.2016 12:59Почему неестественным? Есть очень даже удобная утилитка для этого. Я просто думал, может есть готовые best practices для типографики, отлитые в опыте и железе :)
А раскладка знатная!
a-motion
11.10.2016 13:03Неестественным — потому, что когда мне нужен внезапно на полчаса какой-нибудь нестандартный символ, я редактирую текстовый файл и обновляю кэш: клавиша включена. Я посмотрю, как вы утилиткой себе на часик при редактировании презентации какой-нибудь впилите нестандартный буллит.

vbif
11.10.2016 13:14Интересно, никто ещё не придумал удобных менеджеров буфера обмена, чтобы можно было хранить одновременно несколько фрагментов, и при необходимости вставлять нужный по хоткею? Помню, что-то подобное пытались в msoffice сделать.

a-motion
11.10.2016 13:23+1Как бы в Linux из коробки, в неестественных OS — не знаю, но думаю тоже кто-нибудь запилил.

stychos
11.10.2016 14:20+1В макоси также можно отредактировать на полчасика файл с ремапами, да и раскладку тоже можно ручками в текстовом редакторе отредактировать, не вижу особых отличий xml от Вашего конфига. Совсем ленивые пользуются Ukelele, вроде тоже не создаёт сложностей.
WST
Вспомнилось, как много лет назад я иногда злоупотреблял такими символами, чтобы побыстрее исчерпать трафик или квоту на объём БД у своей «жертвы». Например, пишу фразу «Привет! Как дела?», а внутри спрятаны тысячи невидимых знаков. Короткое сообщение начинает «весить» достаточно много. Правда, иногда эти символы выдают себя нагрузкой на браузер (или Jabber-клиент) — к примеру, становится «трудно» выделять текст, прокрутка подтормаживает и т.д.
kemko
Эффект видимо зависит от понимания жертвой своего jabber-клиента. У меня всегда было включено сжатие, на экономию со школьных обедов не так много можно было купить интернет-трафика.
WST
Чаще всего «жертвами» были изрядно досаждавшие попрошайством ботов арабы, у которых нередко были такие слабые телефоны, что на zlib им тупо не хватало памяти (всё ж таки, я о временах J2ME изначально говорил).
kemko
Видимо я был мажором. На Siemens CX65 zlib вполне работал.