Проверка ширины показала, что строку можно обрезать. В итоге кандзи разделился пополам.
Имя попало в таблицу терминала и вышло из него поврежденным. Фамилия была 𠮷田.
Первый символ — это не обычный 吉. Это 𠮷 (U+20BB7). Это редкая форма, используемая в реальных японских фамилиях. Таблица усекла ячейку, чтобы она вписалась в колонку, и в результате остался поврежденный символ.
Баг крылся в одной единственной строке кода. Это была оптимизация, которая решала, что строку можно безопасно обрезать по индексу.
У строки в JavaScript есть три разных показателя длины: • Кодовые единицы (.length): "𠮷".length равно 2. • Кодовые точки: [..."𠮷"].length равно 1. • Ширина отображения: 𠮷 занимает 2 колонки.
Для стандартного английского текста все эти числа совпадают. Это совпадение создает иллюзию безопасности кода.
Символ 𠮷 нарушает это правило. У него 2 кодовые единицы, так как это суррогатная пара. Он занимает 2 колонки, так как это широкий символ. Числа совпадают (2 = 2), но по разным причинам.
Библиотека cli-table3 использовала «быстрый путь» (fast path): Если длина в кодовых единицах равна ширине отображения, то обрезать строку по индексу.
Это работало годами, потому что у распространенных японских иероглифов, таких как 漢, длина равна 1, а ширина — 2. Они никогда не попадали на этот «быстрый путь».
«Быстрый путь» срабатывает только для редких символов, таких как 𠮷, или эмодзи. У таких символов длина равна 2 и ширина равна 2. Код ошибочно принимает их за простые одноединичные символы и обрезает их пополам по индексу. В итоге остается одиночный суррогат. Именно поэтому в терминале отображается «битый» квадрат.
Чтобы это исправить, необходимо:
- Добавить проверку в «быстрый путь», чтобы исключить суррогатные пары.
- Выполнять обрезку по кодовым точкам, а не по кодовым единицам.
Использование Array.from(str) помогает, так как итерация происходит по кодовым точкам. Это гарантирует, что вы никогда не разрежете символ пополам.
Урок прост: никогда не измеряйте одним типом единиц, а обрезайте другим. Если вы измеряете ширину отображения или кодовые точки, вы должны и обрезать, используя те же самые единицы.
Тестируйте свой код символами из CJK Extension B или эмодзи. ASCII никогда не выявит этот баг.
Optional learning community: https://greymoth-jp.github.io/cjk-failure-corpus/
