A Width Check Said the String Was Safe to Cut. It Split a Kanji in Half.
Een naam werd in een tabel in de terminal ingevoerd en kwam kapot uit. De achternaam was 𠮷田.
Het eerste karakter is niet het gebruikelijke 吉. Het is 𠮷 (U+20BB7). Dit is een zeldzame vorm die in echte Japanse achternamen wordt gebruikt. De tabel heeft de cel afgekort om in een kolom te passen. Hierdoor bleef een defect karakter achter.
De bug zat in één enkele regel code. Het was een optimalisatie die besloot dat een string veilig op basis van de index ingekort kon worden.
Een JavaScript-string heeft drie verschillende lengtes:
• Code units (.length): "𠮷".length is 2.
• Code points: [..."𠮷"].length is 1.
• Weergavebreedte: 𠮷 neemt 2 kolommen in beslag.
Voor standaard Engelse tekst zijn deze getallen allemaal gelijk. Deze toevalligheid zorgt ervoor dat de code veilig lijkt.
Het karakter 𠮷 doorbreekt deze regel. Het heeft 2 code units omdat het een surrogate pair is. Het neemt 2 kolommen in beslag omdat het een breed karakter is. De getallen komen overeen (2 = 2), maar om verschillende redenen.
De library cli-table3 gebruikte een fast path:
Als de lengte van de code units gelijk is aan de weergavebreedte, kort de string dan in op basis van de index.
Dit werkte jarenlang omdat veelvoorkomende Japanse karakters zoals 漢 een lengte van 1 en een breedte van 2 hebben. Ze kwamen nooit in de fast path terecht.
De fast path wordt alleen geactiveerd voor zeldzame karakters zoals 𠮷 of emoji's. Deze karakters hebben een lengte van 2 en een breedte van 2. De code denkt dat het eenvoudige karakters van één unit zijn. De code splitst ze doormidden op basis van de index. Hierdoor blijft er een eenzame surrogate achter. Dit is de reden waarom de terminal een defect blokje laat zien.
Om dit op te lossen, moet je:
- De fast path beschermen om surrogate pairs uit te sluiten.
- Inkorten op basis van code points in plaats van code units.
Het gebruik van Array.from(str) helpt omdat het iterereert op basis van code points. Dit zorgt ervoor dat je nooit een karakter doormidden snijdt.
De les is simpel: Meet nooit met de ene eenheid en kort in met de andere. Als je de weergavebreedte of code points meet, moet je ook met diezelfde eenheden inkorten.
Test je code met een CJK Extension B-karakter of een emoji. ASCII zal deze bug nooit onthullen.
Optionele leercommunity: https://greymoth-jp.github.io/cjk-failure-corpus/
