การตรวจสอบความกว้างทำให้คันจิพัง
ชื่อหนึ่งถูกใส่ลงในตารางเทอร์มินัลแล้วออกมาในสภาพที่พัง ชื่อสกุลนั้นคือ 𠮷田
ตัวอักษรแรกไม่ใช่ 吉 ที่พบเห็นทั่วไป แต่เป็น 𠮷 (U+20BB7) ซึ่งเป็นรูปแบบที่หาได้ยากและใช้ในนามสกุลญี่ปุ่นจริงๆ ตารางได้ทำการตัดข้อความ (truncate) ในเซลล์เพื่อให้พอดีกับคอลัมน์ แทนที่จะแสดงชื่อ มันกลับพิมพ์ตัวอักษรที่พังออกมา ตัวคันจิถูกแบ่งออกเป็นสองส่วน
บั๊กนี้ซ่อนอยู่ในโค้ดทางลัดเพียงบรรทัดเดียว โค้ดตัดสินใจว่าสตริงนั้นปลอดภัยที่จะตัดตามดัชนี (index) ก่อนที่จะทำการตัดจริงๆ ตรรกะนี้ล้มเหลวเนื่องจากวิธีการที่ JavaScript จัดการกับสตริง
สตริงใน JavaScript มีความยาวที่แตกต่างกันสามแบบ:
- ความยาวหน่วยรหัส (Code unit length):
"𠮷".lengthคือ 2 ซึ่งเป็นการนับหน่วย UTF-16 - จำนวนจุดรหัส (Code point count):
[..."𠮷"].lengthคือ 1 ซึ่งเป็นการนับจำนวนตัวอักษรจริงๆ - ความกว้างในการแสดงผล (Display width): จำนวนคอลัมน์ที่ใช้ในเทอร์มินัลคือ 2
สำหรับข้อความภาษาอังกฤษทั่วไป ตัวเลขเหล่านี้จะเท่ากัน "abc" มี 3 หน่วย, 3 จุดรหัส และ 3 คอลัมน์ โค้ดส่วนใหญ่จึงทึกทักเอาว่าความบังเอิญนี้คือกฎเกณฑ์
ตัวอักษร 𠮷 ทำลายกฎนั้น มันมี 2 หน่วยรหัสและ 2 คอลัมน์ ตัวเลขดูเหมือนจะตรงกัน แต่เป็นเพราะเหตุผลที่ต่างกัน โค้ดเห็นว่า 2 เท่ากับ 2 จึงเลือกใช้เส้นทางลัด (fast path) ในการตัดสตริงตามดัชนี
เมื่อมันตัดสตริงที่ดัชนี 3 มันได้ตัวอักษรแรกแบบเต็มตัว แต่ได้ตัวที่สองมาเพียงครึ่งเดียว สิ่งนี้ทำให้เหลือ surrogate (ตัวแทนรหัส) เพียงตัวเดียวทิ้งไว้ ซึ่งเทอร์มินัลจะแสดงผลเป็นกล่องที่พัง (broken box)
ตัวอักษรญี่ปุ่นทั่วไปอย่าง 漢 นั้นปลอดภัย เพราะมี 1 หน่วยรหัสและ 2 คอลัมน์ เนื่องจาก 1 ไม่เท่ากับ 2 โค้ดจึงหลีกเลี่ยงทางลัดที่พังนั้น บั๊กนี้จึงจะเกิดขึ้นกับตัวอักษรที่หาได้ยากและอีโมจิเท่านั้น
ในการแก้ไขเรื่องนี้ คุณต้อง:
- ป้องกันเส้นทางลัด (guard the fast path) เพื่อปฏิเสธสตริงที่มี high surrogates
- ตัดข้อความ (trim) โดยใช้จุดรหัส (code points) ทั้งหมด แทนที่จะใช้หน่วยรหัส (code units)
การใช้ Array.from(str) สามารถแก้ไขปัญหานี้ได้ เพราะมันจะวนลูปตามจุดรหัส (code point) โดยจะมองตัวอักษรเป็นหนึ่งหน่วยที่สมบูรณ์
บทเรียนนี้เรียบง่าย: อย่าวัดด้วยหน่วยหนึ่งแล้วไปตัดด้วยอีกหน่วยหนึ่ง หากคุณวัดความกว้างในการแสดงผลแต่ไปตัดด้วยดัชนีของหน่วยรหัส คุณจะทำให้ข้อมูลของผู้ใช้เสียหาย
ทดสอบโค้ดของคุณด้วยตัวอักษร CJK ที่หาได้ยากหรืออีโมจิ ตัวอักษร ASCII จะไม่แสดงข้อผิดพลาดเหล่านี้ คุณต้องป้อนข้อมูลที่โค้ดของคุณ "กลัว" เข้าไป
Optional learning community: https://greymoth-jp.github.io/cjk-failure-corpus/
