diff --git a/src/filename.ts b/src/filename.ts index dd52e7b..95b7503 100644 --- a/src/filename.ts +++ b/src/filename.ts @@ -92,7 +92,10 @@ export const shortenKeepingExtension = ( // short so that the whole fits in a file name, keeping the title's extension // whenever it fits. `ext` (".mov"), when given, takes the place of the // title's extension before the name is cut short; a live photo's image and -// video are each named with their own. +// video are each named with their own. The cut can leave a device name +// (`CONSOLE.` becomes `CON.`); that name gets +// a leading `_`, as `sanitizeFileName` gives one, and is cut a byte shorter to +// leave room for it. export const defaultFileName = ( title: string, fileID: number, @@ -101,11 +104,8 @@ export const defaultFileName = ( const { prefix = "", ext } = opts; const safe = sanitizeFileName(title, `file-${fileID}`); const name = ext === undefined ? safe : withExtension(safe, ext); - return ( - prefix + - shortenKeepingExtension( - name, - MAX_NAME_BYTES - Buffer.byteLength(prefix), - ) - ); + const room = MAX_NAME_BYTES - Buffer.byteLength(prefix); + const cut = prefix + shortenKeepingExtension(name, room); + if (!RESERVED_DEVICE_NAME.test(cut)) return cut; + return `_${prefix}${shortenKeepingExtension(name, room - 1)}`; }; diff --git a/test/filename/filename.test.ts b/test/filename/filename.test.ts index 1c76b67..8abed29 100644 --- a/test/filename/filename.test.ts +++ b/test/filename/filename.test.ts @@ -216,4 +216,13 @@ describe("defaultFileName", () => { `${"t".repeat(250)}.heic`, ); }); + + // Cutting `CONSOLE` or `NULL` short before a long extension leaves `CON` + // or `NUL`, so the name gets the `_` `sanitizeFileName` gives a device + // name, and the part before the extension gives up a byte for it. + it("never returns a Windows device name, even once cut short", () => { + const ext = `.${"e".repeat(251)}`; + expect(defaultFileName(`CONSOLE${ext}`, 42)).toBe(`_CO${ext}`); + expect(defaultFileName(`NULL${ext}`, 42)).toBe(`_NU${ext}`); + }); });