Проверка ширины показала, что строку можно обрезать. В итоге кандзи разделился пополам.

Имя попало в таблицу терминала и вышло из него поврежденным. Фамилия была 𠮷田.

Первый символ — это не обычный 吉. Это 𠮷 (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 никогда не выявит этот баг.

Source: https://dev.to/greymothjp/a-width-check-said-the-string-was-safe-to-cut-it-split-a-kanji-in-half-4hjk

Optional learning community: https://greymoth-jp.github.io/cjk-failure-corpus/