การตรวจสอบความกว้างบอกว่าสตริงนี้ตัดได้ปลอดภัย แต่มันกลับทำให้คันจิขาดครึ่ง

ชื่อหนึ่งถูกป้อนลงในตารางบนเทอร์มินัล แต่ผลลัพธ์ที่ออกมากลับพัง นามสกุลนั้นคือ 𠮷田

ตัวอักษรแรกไม่ใช่ 吉 ที่ใช้กันทั่วไป แต่มันคือ 𠮷 (U+20BB7) ซึ่งเป็นรูปแบบที่หาได้ยากและใช้ในนามสกุลญี่ปุ่นจริงๆ ตารางได้ทำการตัดข้อความ (truncate) ในเซลล์เพื่อให้พอดีกับคอลัมน์ ส่งผลให้เหลือตัวอักษรที่พังทลายทิ้งไว้

บั๊กนี้อยู่ในโค้ดเพียงบรรทัดเดียว มันคือส่วนของการเพิ่มประสิทธิภาพ (optimization) ที่ตัดสินว่าสตริงนั้นปลอดภัยที่จะตัดด้วย index

สตริงใน JavaScript มีความยาวที่แตกต่างกัน 3 แบบ: • Code units (.length): "𠮷".length คือ 2 • Code points: [..."𠮷"].length คือ 1 • Display width: 𠮷 ใช้พื้นที่ 2 คอลัมน์

สำหรับข้อความภาษาอังกฤษมาตรฐาน ตัวเลขเหล่านี้จะมีค่าเท่ากันทั้งหมด ความบังเอิญนี้ทำให้โค้ดดูเหมือนจะปลอดภัย

ตัวอักษร 𠮷 ทำลายกฎนี้ เนื่องจากมันเป็น surrogate pair จึงมี 2 code units และเนื่องจากมันเป็นตัวอักษรแบบกว้าง (wide character) มันจึงใช้พื้นที่ 2 คอลัมน์ ตัวเลขจึงตรงกัน (2 = 2) แต่ด้วยเหตุผลที่ต่างกัน

ไลบรารี cli-table3 ใช้ fast path ดังนี้: หากความยาวของ code unit เท่ากับ display width ให้ตัดสตริงด้วย index

วิธีนี้ใช้งานได้มาหลายปีเพราะตัวอักษรญี่ปุ่นทั่วไปอย่าง 漢 มีความยาว 1 และความกว้าง 2 ซึ่งไม่เคยเข้าเงื่อนไขของ fast path เลย

fast path จะทำงานเฉพาะกับตัวอักษรที่หาได้ยากอย่าง 𠮷 หรือ emoji เท่านั้น ตัวอักษรเหล่านี้มีความยาว 2 และความกว้าง 2 โค้ดจึงเข้าใจผิดว่าพวกมันเป็นตัวอักษรแบบหน่วยเดียว (one-unit characters) และทำการตัดแบ่งครึ่งด้วย index สิ่งนี้ทำให้เหลือเพียง surrogate ตัวเดียวทิ้งไว้ นี่คือเหตุผลที่เทอร์มินัลแสดงผลเป็นกล่องที่แสดงผลผิดพลาด

ในการแก้ไขเรื่องนี้ คุณต้อง:

  • เพิ่มการป้องกัน (guard) ใน fast path เพื่อยกเว้น surrogate pairs
  • ตัดข้อความ (trim) โดยใช้ code points แทน code units

การใช้ Array.from(str) ช่วยได้ เพราะมันจะวนลูป (iterate) ตาม code point ซึ่งช่วยให้มั่นใจได้ว่าคุณจะไม่ตัดตัวอักษรขาดครึ่ง

บทเรียนนี้เรียบง่ายมาก: อย่าวัดด้วยหน่วยหนึ่งแล้วไปตัดด้วยอีกหน่วยหนึ่ง หากคุณวัดด้วย display width หรือ code points คุณก็ต้องตัดโดยใช้หน่วยเดียวกันเหล่านั้น

ทดสอบโค้ดของคุณด้วยตัวอักษร CJK Extension B หรือ emoji เพราะ 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/