بررسی عرض نشان داد که رشته برای برش ایمن است، اما یک کانجی را از وسط نصف کرد.
نامی وارد یک جدول ترمینال شد و به شکلی شکسته از آن خارج شد. نام خانوادگی 𠮷田 بود.
کاراکتر اول آن 吉 رایج نیست، بلکه 𠮷 (U+20BB7) است. این یک شکل نادر است که در نامهای خانوادگی واقعی ژاپنی استفاده میشود. جدول برای جا شدن در یک ستون، سلول را کوتاه (truncate) کرد و یک کاراکتر شکسته به جای گذاشت.
این باگ در یک خط کد نهفته بود. یک بهینهسازی بود که تصمیم میگرفت یک رشته بر اساس ایندکس (index) برای برش ایمن است.
یک رشته JavaScript سه طول متفاوت دارد:
• واحدهای کد (.length): مقدار "𠮷".length برابر ۲ است.
• نقاط کد (Code points): مقدار [..."𠮷"].length برابر ۱ است.
• عرض نمایش (Display width): کاراکتر 𠮷 دو ستون را اشغال میکند.
برای متنهای استاندارد انگلیسی، تمام این اعداد یکسان هستند. این تصادف باعث میشود کد ایمن به نظر برسد.
کاراکتر 𠮷 این قاعده را میشکند. این کاراکتر ۲ واحد کد دارد زیرا یک جفت جایگزین (surrogate pair) است. همچنین ۲ ستون اشغال میکند چون یک کاراکتر عریض (wide character) است. اعداد با هم مطابقت دارند (۲ = ۲)، اما به دلایل متفاوت.
کتابخانه cli-table3 از یک مسیر سریع (fast path) استفاده میکرد:
اگر طول واحد کد با عرض نمایش برابر بود، رشته را بر اساس ایندکس برش بده.
این روش سالها کار کرد، زیرا کاراکترهای رایج ژاپنی مانند 漢 دارای طول ۱ و عرض ۲ هستند. آنها هرگز به مسیر سریع برخورد نمیکردند.
مسیر سریع فقط برای کاراکترهای نادر مانند 𠮷 یا ایموجیها فعال میشود. این کاراکترها طول ۲ و عرض ۲ دارند. کد تصور میکند آنها کاراکترهای سادهی تکواحدی هستند و آنها را از طریق ایندکس از وسط نصف میکند. این کار باعث میشود یک جفت جایگزین (surrogate) تنها باقی بماند. به همین دلیل است که ترمینال یک مربع شکسته نشان میدهد.
برای رفع این مشکل، باید:
- مسیر سریع را طوری محافظت کنید که جفتهای جایگزین (surrogate pairs) را شامل نشود.
- به جای واحدهای کد، بر اساس نقاط کد (code points) عملیات Trim را انجام دهید.
استفاده از Array.from(str) کمک میکند، زیرا بر اساس نقطه کد پیمایش (iterate) میکند. این کار تضمین میکند که هرگز یک کاراکتر را از وسط نصف نکنید.
درس ساده است: هرگز با یک واحد اندازهگیری نکنید و با واحدی دیگر برش ندهید. اگر عرض نمایش یا نقاط کد را اندازهگیری میکنید، باید با همان واحدها برش را انجام دهید.
کد خود را با یک کاراکتر CJK Extension B یا یک ایموجی تست کنید. ASCII هرگز این باگ را آشکار نخواهد کرد.
Optional learning community: https://greymoth-jp.github.io/cjk-failure-corpus/
