From 1ff4ca5f5ffc3cab5fdc9a7d84fc2f93e9371f19 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 1 Oct 2026 00:48:19 +0200 Subject: [PATCH] Escape the invisible and confusable characters of the sources The tests of create-files.ts, format.ts, opening.ts and create-input.ts held literal code points that a reader cannot tell apart, or cannot see: U+212A KELVIN SIGN and U+017F LONG S, fullwidth and dotted I, ZWJ, a combining acute, U+202E and U+FEFF, and equalFold compared with two of them in its code. They are now \uXXXX escapes, with the same values; no source in src or scripts holds a format character. Co-Authored-By: Claude Opus 5.5 --- src/lib/inspector/create-files.test.ts | 16 ++++++++-------- src/lib/inspector/create-files.ts | 2 +- src/lib/inspector/create-input.test.ts | 2 +- src/lib/inspector/format.test.ts | 2 +- src/lib/inspector/opening.test.ts | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lib/inspector/create-files.test.ts b/src/lib/inspector/create-files.test.ts index 5c26ae8..8cced1d 100644 --- a/src/lib/inspector/create-files.test.ts +++ b/src/lib/inspector/create-files.test.ts @@ -38,19 +38,19 @@ describe('systemFile', () => { ['.DS_Store', '.DS_Store'], ['.ds_store', '.DS_Store'], ['.DS_STORE', '.DS_Store'], - ['.Dſ_Store', '.DS_Store'], + ['.D\u017f_Store', '.DS_Store'], ['Thumbs.db', 'Thumbs.db'], - ['Thumbſ.db', 'Thumbs.db'], + ['Thumb\u017f.db', 'Thumbs.db'], ['THUMBS.DB', 'Thumbs.db'], ['desktop.ini', 'desktop.ini'], - ['desKtop.ini', 'desktop.ini'], + ['des\u212atop.ini', 'desktop.ini'], ['DESKTOP.INI', 'desktop.ini'], ['desktop.ini ', undefined], - ['desktop.ini', undefined], - ['desktop.İni', undefined], - ['desktop.ıni', undefined], - ['desk‍top.ini', undefined], - ['Thumbs.db́', undefined], + ['\uff44esktop.ini', undefined], + ['desktop.\u0130ni', undefined], + ['desktop.\u0131ni', undefined], + ['desk\u200dtop.ini', undefined], + ['Thumbs.db\u0301', undefined], ['._foto.jpg', '._*'], ['._', '._*'], ['_.foto', undefined], diff --git a/src/lib/inspector/create-files.ts b/src/lib/inspector/create-files.ts index 0d9a4c5..8c2f2f2 100644 --- a/src/lib/inspector/create-files.ts +++ b/src/lib/inspector/create-files.ts @@ -41,7 +41,7 @@ function equalFold(s: string, ascii: string): boolean { if (cps.length !== ascii.length) return false; return cps.every((c, i) => { const want = lowerASCII(ascii[i]!); - return lowerASCII(c) === want || (want === 'k' && c === 'K') || (want === 's' && c === 'ſ'); + return lowerASCII(c) === want || (want === 'k' && c === '\u212a') || (want === 's' && c === '\u017f'); }); } diff --git a/src/lib/inspector/create-input.test.ts b/src/lib/inspector/create-input.test.ts index 499a4f2..b200a2d 100644 --- a/src/lib/inspector/create-input.test.ts +++ b/src/lib/inspector/create-input.test.ts @@ -235,7 +235,7 @@ describe('the names of the files', () => { expect(downloadName('regalo.DKC', '.dkc', 'x.dkc')).toBe('regalo.DKC'); expect(downloadName('regalo', '.dkk', 'x.dkk')).toBe('regalo.dkk'); expect(downloadName('../a/b\\c', '.dkc', 'x.dkc')).toBe('.._a_b_c.dkc'); - expect(downloadName('evil‮cod.dkc', '.dkc', 'x.dkc')).toBe('evil_cod.dkc'); + expect(downloadName('evil\u202ecod.dkc', '.dkc', 'x.dkc')).toBe('evil_cod.dkc'); expect(downloadName(' ', '.dkc', 'x.dkc')).toBe('x.dkc'); }); }); diff --git a/src/lib/inspector/format.test.ts b/src/lib/inspector/format.test.ts index c3647c1..41b1c55 100644 --- a/src/lib/inspector/format.test.ts +++ b/src/lib/inspector/format.test.ts @@ -69,7 +69,7 @@ describe('steps and codes', () => { describe('text', () => { it('replaces what is not printable in a file name', () => { expect(safeFileName('informe 2026 — ñ.pdf')).toBe('informe 2026 — ñ.pdf'); - expect(safeFileName('a‮b\tc\nd\u{1f600}')).toBe('a_b_c_d\u{1f600}'); + expect(safeFileName('a\u202eb\tc\nd\u{1f600}')).toBe('a_b_c_d\u{1f600}'); }); it('shows printable UTF-8 as text', () => { diff --git a/src/lib/inspector/opening.test.ts b/src/lib/inspector/opening.test.ts index b1ae07c..61e0564 100644 --- a/src/lib/inspector/opening.test.ts +++ b/src/lib/inspector/opening.test.ts @@ -181,16 +181,16 @@ describe('plaintextPreview', () => { }); it('shows a text written on Windows: CR LF as a line feed and no BOM; the download keeps the bytes', () => { - expect(whole('primera\r\nsegunda\r\n')).toEqual({ text: 'primera\nsegunda\n', cut: false }); + expect(whole('\ufeffprimera\r\nsegunda\r\n')).toEqual({ text: 'primera\nsegunda\n', cut: false }); // A lone CR, or a BOM that is not at the start, is not a printable text. expect(whole('a\rb')).toBeUndefined(); - expect(whole('ab')).toBeUndefined(); + expect(whole('a\ufeffb')).toBeUndefined(); }); it('shows nothing of what is not UTF-8 made of printable characters', () => { expect(plaintextPreview(new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x00]), true)).toBeUndefined(); expect(plaintextPreview(new Uint8Array([0xff, 0xfe, 0x41, 0x00]), true)).toBeUndefined(); - expect(whole('a‮b')).toBeUndefined(); + expect(whole('a\u202eb')).toBeUndefined(); }); it('drops a character of 2, 3 or 4 bytes that the first bytes cut, and a CR whose LF they cut', () => {