eCourt-note/utils/upload-name.ts

17 lines
1.7 KiB
TypeScript
Raw Permalink Normal View History

fix(upload): decode mojibake of Thai filenames อัปโหลดไฟล์ชื่อไทยแล้ว title ที่แสดงในรายการเพี้ยนเป็น latin1 mojibake ("คำร้องทดสอบอัปโหลด.pdf" → "คำรเน‰à¸­à¸‡…") — คำขอผู้ใช้ 2026-10-05 (1) สาเหตุ: browser ใส่ชื่อไฟล์ (UTF-8) ลง header Content-Disposition ของ multipart body ตรง ๆ แต่ undici ของ Node (ตัว parse request.formData() ที่ Next.js ใช้) ถอดรหัส header เป็น latin1 ทำให้ file.name ใน Server Action เพี้ยนก่อนถึงโค้ด — เนื้อไฟล์ ไม่เกี่ยว (ผ่าน arrayBuffer() ตรง ๆ) พิสูจน์ด้วย Playwright อัปโหลดจริงก่อนแก้ได้สตริง mojibake ตรงกับที่ผู้ใช้รายงาน (2) แก้: utils/upload-name.ts ใหม่ — decodeUploadFilename เข้ารหัส ชื่อกลับเป็น byte แล้วถอดด้วย UTF-8 ให้คืนชื่อเดิม พร้อม guard 2 ชั้นกันแตะชื่อที่ถูกต้องอยู่แล้ว: ไม่มีตัวอักษรช่วง U+0080–U+00FF เลย = ASCII ล้วน/ชื่อถูกอยู่แล้ว (ไทย ≥ U+0E00) ไม่แต้อยู่เดิม / ถอดแล้วได้ U+FFFD (ไบต์ไม่ประกอบ UTF-8 สมบูรณ์ = ชื่อเป็น latin1 จริง) เก็บของเดิม — uploadFile เรียกครั้งเดียว ที่จุดเข้า ก่อนใช้ทั้ง allowedExt และ sanitizeTitle (3) tests: tests/utils/upload-name.test.ts ใหม่ 5 เคส (ชื่อไทย mojibake → คืนเดิม / ไทยผสมตัวเลขวงเล็บ / ASCII ไม่เปลี่ยน / ชื่อถูกอยู่แล้วไม่ถูกแตะ / latin1 จริงเก็บของเดิม) (4) code review 2026-10-05: ยอมรับความเสี่ยง false positive กรณีชื่อ latin1 จริงที่ byte คู่กันบังเอิญประกอบเป็น UTF-8 ครบ ("¢.pdf") — แคบมากเพราะ browser ส่งชื่อเป็น UTF-8 เสมอ และ heuristic นี้ คือ workaround มาตรฐานของปัญหา undici (บีบ guard เพิ่มจะทำให้ ชื่อ latin1-accent ที่โดน mojibake จริง ๆ กลับไม่ถูกซ่อม) (5) docs sync: CLAUDE.md (tree entry) + rules/data-layer.md (กติกา uploadFile) ผ่าน tsc + vitest + ยืนยันใน browser ว่าการ์ดแสดงชื่อไทยตรงเป๊ะ Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-05 11:30:56 +07:00
// N4 — ถอดรหัสชื่อไฟล์ที่ FormData ของ Server Action ส่งมา
// browser ใส่ชื่อไฟล์ (UTF-8) ลง header Content-Disposition ของ multipart body ตรง ๆ
// แต่ undici ของ Node (ตัว parse request.formData() ที่ Next.js ใช้) ถอดรหัส header
// เป็น latin1 ทำให้ชื่อที่ไม่ใช่ ASCII เพี้ยน ("คำร้อง.pdf" → "คำร้อง.pdf")
// — เข้ารหัสกลับเป็น byte แล้วถอดด้วย UTF-8 จะได้ชื่อเดิมคืน
// guard 2 ชั้นกันแตะชื่อที่ถูกต้องอยู่แล้ว: ชื่อที่ undici ถอดมามีแต่ตัวอักษร
// ช่วง U+0000–U+00FF เท่านั้น ดังนั้นไม่มีตัวไหนใน U+0080–U+00FF เลย = ASCII ล้วนหรือ
// ชื่อที่ส่งมาถูกต้อง (ไทยอยู่ U+0E00+) = ไม่แต้อยู่เดิม / ถอดแล้วได้ U+FFFD
// (ไบต์ไม่ประกอบเป็น UTF-8 ที่สมบูรณ์ — ชื่อเป็น latin1 จริง) = เก็บของเดิมไว้
const LATIN1_MOJIBAKE_RE = /[\u0080-ÿ]/;
export function decodeUploadFilename(name: string): string {
if (!LATIN1_MOJIBAKE_RE.test(name)) return name;
const fixed = Buffer.from(name, "latin1").toString("utf8");
return fixed.includes("<22>") ? name : fixed;
}