Алфавиты, шрифты, лигатуры и непечатные символы
Сегодня в процессе сёрфинга от написания математических формул провалился в чтение про монгольские гласные. Что за невидимая связь соединяет эти темы? Невидимые соединители (или разделители)!
Но обо всём по-порядку.
Одинаковые буквы не так уж одинаковы

В текстах часто встречаются разные символы с одинаковым написанием. В посте про стеганографию я писал про несколько вариантов таких символов: кириллические и латинские ‘c’ и ‘о’, пробелы и неразрывные пробелы.
А в иврите вообще все буквы пишутся одинаково.
Также можно по-разному писать, например, буквы с диакритическими знаками: такие символы в таблице кодировки Unicode (UTF-8) есть как отдельные графемы. Но их же можно писать и как пару отдельных символов: буква + диакритический знак.
Символ это не его начертание
Тут важно уточнить. Символ и его написание — разные вещи. Написание зависит от шрифта, от текстового редактора и его настроек, от расположения символа на странице. Например, пробел в середине строки отображается, а в конце строки нет.
Диакритический знак после буквы можно писать отдельно от буквы (например, «комбинируемый акут» иногда обозначают как пунктирный кружочек и знак ударения над ним), но чаще всего его приклеивают к предыдущей букве.
Сходным образом работают лигатуры. Так в некоторых шрифтах AE будет отображаться аналогично символу Æ. Лигатуры являются неотъемлемой частью многих языков. Например, буквы арабской вязи зачастую склеиваются в единую загогулину.
Лигатуры просто изменяют отображение нескольких соседних символов. Некоторые лигатуры вы легко можете не заметить. Например, в комбинации букв ij или fi два символа могут отображаться ближе, чем если они писались бы как отдельные буквы.
Некоторые мазохисты программисты добавляют шрифты с лигатурами в свои текстовые редакторы, чтобы комбинации символов в их коде писались «красивше»: например чтобы псевдострелочка -> отображалась так же как настоящая стрелочка →.
Начертание обретает смысл лишь в контексте
Но я отвлёкся…
Итак, у нас есть одинаково пишущиеся символы с разным значением: латинская и кириллические c/с. Когда вы читаете текст вам обычно по контексту понятно, какая буква имеется в виду.
Но представьте теперь, что читаете текст не вы, а автоматический парсер. Например, читалка текста для слепых. Такая читалка скорее всего не сможет понять контекст и слово «cобака» прочтёт как «цобака». Не слишком удобно.
Если вы попытаетесь это слово скопипастить в поиск по словарю, вы скорее всего тоже ничего не найдёте (а вы попытаетесь именно скопировать, а не перепечатать, ведь на вашей английской-испанской-немецкой клавиатуре нет буквы «б»)
Ломать парсеры бывает выгодно и специально. Лет 10 назад была история с латиницей в госзакупках — это широко освещавшаяся Навальным коррупционная схема. Благодаря злонамеренным «опечаткам» в размещаемых на портале ГосЗакупок заказах, в тендерах участвовал только один поставщик. Это позволяло заказчикам и поставщикам выставлять неконкурентные цены и получать из бюджета «откат». Ведь по запросу «молоко», абы какой поставщик не найдёт заказ на «мoлoкo» с латинскими буквами «o». В итоге заказ достанется тому, кто точно знает, что искать нужно искорёженное слово.
Контекст в математических формулах
В математических формулах контекст тоже играет значительную роль. f(x+1) это функция, применённая к аргументу или число f, умноженное на x+1?
A_{ij} это элемент матрицы на строке i, столбце j или элемент вектора под номером “i умноженное на j”?
Чтобы правильно читать математические формулы существует специальный набор непечатных символов, уточняющих семантику написанного: невидимое умножение, невидимое сложение, невидимое применение функции, невидимая запятая.
Этот список и увёл меня в гугл. Что такое невидимое умножение или невидимая запятая понятно. Но что за зверь «невидимое сложение»? Попробуйте угадать и вы, для чего он нужен?
Ответ: посмотрите на выражение 2⅓. Это неправильная дробь 2 + ⅓ или произведение 2 × ⅓?
Выплыл из гугла я на бесполезной для моих поисков, но весьма интересной статье на хабре про невидимые символы.
Их на самом деле приличное количество. Есть символы для того, чтобы текст нельзя было переносить по слогам (U+2060). Или, напротив, чтобы два склеенных слова считались независимыми (U+200B). Есть символ, разрывающий лигатуры, а есть склеивающий символ (U+WD40 и U+TAPE).
Проблема с этими символами в том, что они гхм… невидимые. В большинстве текстовых редакторов вы даже не поймёте, что в ваш текст затесался такой значок. Меж тем, как мы уже видели на примере математических объединителей, эти символы могут влиять на смысл написанного.
Невидимый монгольский недопробел
Ещё хуже ситуация оказалась с монгольским разделителем гласных. Он с одной стороны невидимый, а с другой стороны, неясно, считать ли его пробельным символом. В разных стандартах юникода он трактовался по-разному. И это создавало возможность делать программы, которые по-разному будут интерпретироваться в разных версиях компилятора.
Посмотрите на программу. Здесь знак X поставлен вместо того монгольского разделителя:
variable = 13
variableX = 666
print(variable)
Что будет напечатано? Это зависит от того, является ли X частью имени переменной или трактуется как ещё один, пусть и нестандартный, пробел перед знаком равно. Что бы не было напечатано, это точно будет сатанизм какой-то, потому что на мониторе это будет выглядеть так:
variable = 13
variable = 666
print(variable)
и печатать будет где-то 13, где-то 666, а где-то выдаст синтаксическую ошибку.
Как спрятать целую программу
(Хорошие) программисты такие люди, что всё время ищут, как что-то можно сломать и как можно эксплуатировать недостатки системы for fun and profit.
Наличие разных типов непечатаемых символов позволяет использовать их вместо двоичного/троичного кода. А если таких символов много, то даже вместо команд.
Возможно, вы знаете язык программирования brainfuck. Это язык, чем-то похожий на машину Тьюринга: это лента памяти, по которой ездит исполнитель и исполняет инструкция за инструкцией.
Его особенность в том, что в нём всего 8 команд (и при этом он Тьюринг-полный!). Каждая команда кодируется одним символом:
>— сдвинуться на следующую ячейку ленты памяти
<— сдвинуться на предыдующую ячейку ленты памяти
+— увеличить текущую ячейку на 1
-— уменьшить текущую ячейку на 1
.— напечатать символ, чей ASCII код записан в текущей ячейке
,— считать значение в текущую ячейку
[— зайти в цикл while, если текущая ячейка не ноль
]— пометить последнюю инструкцию цикла, иначе выйти из него
У брейнфака есть несколько диалектов. Например, Ook — это язык на котором мог бы программировать Библиотекарь Пратчетта. Он кодирует команды брейнфака в троичном коде. Триты принимают значения: Ook. Ook? Ook!
Есть язык Whitespace, который кодирует свой набор команд символами пробела, табуляции и перевода строки. Но в нём всё-таки есть и другие символы, помимо пробелов.
Anguish — также является диалектом Brainfuck-а. Но все восемь команд кодируются непечатными символами (они внутри квадратных скобок, можете проверить):
> [] U+2060 WORD JOINER [Cf]
< [] U+200B ZERO WIDTH SPACE [Cf]
+ [] U+2061 FUNCTION APPLICATION [Cf]
- [] U+2062 INVISIBLE TIMES [Cf]
. [] U+2063 INVISIBLE SEPARATOR [Cf]
, [] U+FEFF ZERO WIDTH NO-BREAK SPACE [Cf]
[ [] U+200C ZERO WIDTH NON-JOINER [Cf]
] [] U+200D ZERO WIDTH JOINER [Cf]
Теперь на детскую загадку про «А и Б сидели на трубе…» вы можете смело отвечать, что между буквами затесалась целая программа. Например, сюда я скопировал код hello world. Здесь буквы А,Б и 130 символов между ними (я не шучу):
АБ
Кстати, в линуксовой версии телеграма баг: показывается zero-width space ненулевой ширины.*
Как вы понимаете, вместо такой безобидной программы можно случайно скопировать и какой-нибудь вирус. ;)
«Go to the unprintable»
И напоследок, раз уж мы говорим про непечатное, снова вставлю сюда кусочек контекстной рекламы литературного канала «Армен и Фёдор» и конкретно его выпуска про перевод «По ком звонит колокол». Из него вы узнаете, например, почему Хемингуэй писал:
“Go to the unprintable,” Agustin said. “And unprint thyself. But do you want me to tell you something of service to you?”
(но это лишь крошечная часть разбора, конечно)
Если вы думаете, что я хочу заставить вас проследовать за мной в кроличью нору ссылок, вы правы. ;)
P.S. В продолжение темы, придумал проект по разработке специализированных шрифтов.