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>
This commit is contained in:
parent
e4ecc026b0
commit
c08aaab1ed
5 changed files with 60 additions and 3 deletions
36
tests/utils/upload-name.test.ts
Normal file
36
tests/utils/upload-name.test.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Buffer } from "node:buffer";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { decodeUploadFilename } from "@/utils/upload-name";
|
||||
|
||||
/** จำลองสิ่งที่ undici ทำกับชื่อไฟล์: ถอดรหัส UTF-8 bytes เป็น latin1 (mojibake) */
|
||||
function mojibake(name: string): string {
|
||||
return Buffer.from(name, "utf8").toString("latin1");
|
||||
}
|
||||
|
||||
describe("decodeUploadFilename", () => {
|
||||
it("ชื่อไทยที่ undici ถอดเป็น latin1 → คืนชื่อเดิม", () => {
|
||||
expect(decodeUploadFilename(mojibake("คำร้องทดสอบอัปโหลด.pdf"))).toBe(
|
||||
"คำร้องทดสอบอัปโหลด.pdf",
|
||||
);
|
||||
});
|
||||
|
||||
it("ชื่อไทยผสมตัวเลข/วงเล็บ/ช่องว่าง", () => {
|
||||
expect(decodeUploadFilename(mojibake("คดีดำที่ 1234 (สำเนา).pdf"))).toBe(
|
||||
"คดีดำที่ 1234 (สำเนา).pdf",
|
||||
);
|
||||
});
|
||||
|
||||
it("ชื่ออังกฤษล้วนไม่เปลี่ยน (รอบ latin1⇄utf8 เป็น identity)", () => {
|
||||
expect(decodeUploadFilename("scan-2026_final.PDF")).toBe("scan-2026_final.PDF");
|
||||
});
|
||||
|
||||
it("ชื่อที่ส่งมาถูกต้องอยู่แล้ว (ไทย ≥ U+0E00 ไม่มีตัวช่วง latin1) ไม่ถูกแตะ", () => {
|
||||
expect(decodeUploadFilename("คำร้อง.pdf")).toBe("คำร้อง.pdf");
|
||||
});
|
||||
|
||||
it("ชื่อ latin1 จริง (byte เดี่ยวประกอบเป็น UTF-8 ไม่ได้) เก็บของเดิม", () => {
|
||||
// "café" ที่ sender เข้ารหัส latin1 จริง: é = byte 0xE9 เดี่ยว → ถอด UTF-8 ได้ U+FFFD
|
||||
expect(decodeUploadFilename("café.pdf")).toBe("café.pdf");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue