בדיקת רוחב אמרה שהמחרוזת בטוחה לחיתוך. היא פצלה קאנג'י לחצי.

שם שהוזן לטבלת טרמינל יצא שבור. שם המשפחה היה 𠮷田.

התו הראשון אינו 吉 הנפוץ. זהו 𠮷 (U+20BB7). זוהי צורה נדירה המשמשת בשמות משפחה יפניים אמיתיים. הטבלה קיצרה את התא כדי שיתאים לעמודה. היא הותירה מאחוריה תו שבור.

הבאג התחבא בשורה אחת של קוד. זו הייתה אופטימיזציה שהחליטה שמחרוזת בטוחה לחיתוך לפי אינדקס.

למחרוזת JavaScript יש שלושה אורכים שונים: • יחידות קוד (.length): ה-length של "𠮷" הוא 2. • נקודות קוד (Code points): ה-length של [..."𠮷"] הוא 1. • רוחב תצוגה: 𠮷 תופס 2 עמודות.

עבור טקסט אנגלי סטנדרטי, כל המספרים הללו זהים. צירוף מקרים זה גורם לקוד להיראות בטוח.

התו 𠮷 שובר את הכלל הזה. יש לו 2 יחידות קוד מכיוון שהוא surrogate pair. יש לו 2 עמודות מכיוון שהוא תו רחב. המספרים תואמים (2 = 2), אך מסיבות שונות.

הספרייה cli-table3 השתמשה ב-fast path: אם אורך יחידות הקוד שווה לרוחב התצוגה, אז חתוך את המחרוזת לפי אינדקס.

זה עבד במשך שנים מכיוון שלתווים יפניים נפוצים כמו 漢 יש אורך של 1 ורוחב של 2. הם מעולם לא הגיעו ל-fast path.

ה-fast path מופעל רק עבור תווים נדירים כמו 𠮷 או אימוג'ים. לתווים אלו יש אורך של 2 ורוחב של 2. הקוד חושב שהם תווים פשוטים בעלי יחידה אחת. הוא חותך אותם לחצי לפי אינדקס. זה משאיר surrogate בודד מאחור. זו הסיבה שהטרמינל מציג ריבוע שבור.

כדי לתקן זאת, עליך:

  • להגן על ה-fast path כדי להחריג surrogate pairs.
  • לבצע trim לפי נקודות קוד (code points) במקום לפי יחידות קוד (code units).

שימוש ב-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/