Sprawdzenie szerokości uznało, że ciąg można bezpiecznie uciąć. Przecięło kanji na pół.
Nazwisko wprowadzone do tabeli w terminalu okazało się uszkodzone. Nazwisko brzmiało 𠮷田.
Pierwszy znak to nie powszechny 吉. To 𠮷 (U+20BB7). Jest to rzadka forma używana w prawdziwych japońskich nazwiskach. Tabela skróciła komórkę, aby zmieściła się w kolumnie, pozostawiając uszkodzony znak.
Błąd tkwił w jednej linii kodu. Była to optymalizacja, która uznawała, że ciąg znaków można bezpiecznie uciąć według indeksu.
Ciąg znaków w JavaScript ma trzy różne długości: • Jednostki kodu (code units) (.length): "𠮷".length wynosi 2. • Punkty kodu (code points): [..."𠮷"].length wynosi 1. • Szerokość wyświetlania (display width): 𠮷 zajmuje 2 kolumny.
Dla standardowego tekstu angielskiego wszystkie te liczby są takie same. Ten zbieg okoliczności sprawia, że kod wydaje się bezpieczny.
Znak 𠮷 łamie tę zasadę. Ma 2 jednostki kodu, ponieważ jest parą zastępczą (surrogate pair). Zajmuje 2 kolumny, ponieważ jest znakiem szerokim. Liczby się zgadzają (2 = 2), ale z różnych powodów.
Biblioteka cli-table3 stosowała ścieżkę fast path: Jeśli długość jednostek kodu jest równa szerokości wyświetlania, utnij ciąg według indeksu.
Działało to przez lata, ponieważ powszechne znaki japońskie, takie jak 漢, mają długość 1 i szerokość 2. Nigdy nie trafiały one na ścieżkę fast path.
Ścieżka fast path aktywuje się tylko dla rzadkich znaków, takich jak 𠮷 lub emoji. Znaki te mają długość 2 i szerokość 2. Kod uznaje je za proste znaki jednościowe. Przecina je w połowie według indeksu, pozostawiając pojedynczą parę zastępczą. To dlatego terminal wyświetla uszkodzony kwadrat.
Aby to naprawić, należy:
- Zabezpieczyć ścieżkę fast path, aby wykluczyć pary zastępcze.
- Przycinać według punktów kodu zamiast jednostek kodu.
Użycie Array.from(str) pomaga, ponieważ iteruje ono po punktach kodu. Dzięki temu masz pewność, że nigdy nie przetniesz znaku na pół.
Lekcja jest prosta: Nigdy nie mierz jedną jednostką, a nie tnij inną. Jeśli mierzysz szerokość wyświetlania lub punkty kodu, musisz ciąć, używając tych samych jednostek.
Przetestuj swój kod znakiem z CJK Extension B lub emoji. ASCII nigdy nie ujawni tego błędu.
Opcjalna społeczność edukacyjna: https://greymoth-jp.github.io/cjk-failure-corpus/
