diff --git a/lib/path.js b/lib/path.js index 63b037cddfb9..19488870eff7 100644 --- a/lib/path.js +++ b/lib/path.js @@ -83,6 +83,32 @@ function isWindowsReservedName(path, colonIndex) { return ArrayPrototypeIncludes(WINDOWS_RESERVED_NAMES, devicePart); } +function hasWindowsReservedNamePrefix(path) { + // Windows resolves a reserved device name to the device itself when the name + // is followed by a colon or by a dot: `NUL.txt` and `NUL.tar.gz` are both + // equivalent to `NUL`. Test the leading path component up to its first `.` + // or `:` instead of relying on `isWindowsReservedName()` being called with a + // colon index of -1, which merely sliced off the last character and so + // matched unrelated names such as `CONx` while missing `NUL.txt`. + // + // A bare reserved name carrying neither `.` nor `:` is deliberately left + // alone, preserving `normalize('CON') === 'CON'`. + const len = path.length; + let i = 0; + while (i < len && !isPathSeparator(StringPrototypeCharCodeAt(path, i))) { + i++; + } + let j = 0; + while (j < i) { + const code = StringPrototypeCharCodeAt(path, j); + if (code === CHAR_DOT || code === CHAR_COLON) { + return isWindowsReservedName(path, j); + } + j++; + } + return false; +} + function isWindowsDeviceRoot(code) { return (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) || (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z); @@ -470,8 +496,7 @@ const win32 = { } } while ((index = StringPrototypeIndexOf(path, ':', index + 1)) !== -1); } - const colonIndex = StringPrototypeIndexOf(path, ':'); - if (isWindowsReservedName(path, colonIndex)) { + if (hasWindowsReservedNamePrefix(path)) { return `.\\${device ?? ''}${tail}`; } if (device === undefined) { diff --git a/test/parallel/test-path-win32-normalize-device-names.js b/test/parallel/test-path-win32-normalize-device-names.js index b34c9061e565..4d9f776c2f2b 100644 --- a/test/parallel/test-path-win32-normalize-device-names.js +++ b/test/parallel/test-path-win32-normalize-device-names.js @@ -28,7 +28,7 @@ const normalizeDeviceNameTests = [ { input: 'con:', expected: '.\\con:.' }, { input: 'CON:.', expected: '.\\CON:.' }, { input: 'coN:', expected: '.\\coN:.' }, - { input: 'LPT9.foo', expected: 'LPT9.foo' }, + { input: 'LPT9.foo', expected: '.\\LPT9.foo' }, { input: 'COM9:', expected: '.\\COM9:.' }, { input: 'COM9.', expected: '.\\COM9.' }, { input: 'C:COM9', expected: 'C:COM9' }, @@ -76,7 +76,7 @@ const normalizeDeviceNameTests = [ { input: 'D:bar', expected: 'D:bar' }, { input: 'CON', expected: 'CON' }, - { input: 'CON.TXT', expected: 'CON.TXT' }, + { input: 'CON.TXT', expected: '.\\CON.TXT' }, { input: 'COM10:', expected: '.\\COM10:' }, { input: 'LPT10:', expected: '.\\LPT10:' }, { input: 'CONNINGTOWER:', expected: '.\\CONNINGTOWER:' }, @@ -115,6 +115,30 @@ for (const { input, expected } of normalizeDeviceNameTests) { `path.win32.normalize(${JSON.stringify(input)}) === ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`); } +const reservedNameBoundaryTests = [ + { input: 'NUL.txt', expected: '.\\NUL.txt' }, + { input: 'NUL.tar.gz', expected: '.\\NUL.tar.gz' }, + { input: 'CON.TXT', expected: '.\\CON.TXT' }, + { input: 'LPT9.foo', expected: '.\\LPT9.foo' }, + { input: 'COM1.prn', expected: '.\\COM1.prn' }, + { input: 'COM¹.txt', expected: '.\\COM¹.txt' }, + { input: 'con.txt', expected: '.\\con.txt' }, + { input: 'CONx', expected: 'CONx' }, + { input: 'NULLED.txt', expected: 'NULLED.txt' }, + { input: 'COM10.txt', expected: 'COM10.txt' }, + { input: 'LPT10.foo', expected: 'LPT10.foo' }, + { input: 'PRNINTER.tar', expected: 'PRNINTER.tar' }, + { input: 'C:file.txt', expected: 'C:file.txt' }, + { input: 'CON\\path', expected: 'CON\\path' }, + { input: 'NUL/path', expected: 'NUL\\path' }, +]; + +for (const { input, expected } of reservedNameBoundaryTests) { + const actual = path.win32.normalize(input); + assert.strictEqual(actual, expected, + `path.win32.normalize(${JSON.stringify(input)}) === ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`); +} + assert.strictEqual(path.win32.normalize('CON:foo/../bar'), '.\\CON:bar'); // This should NOT be prefixed because 'c:' is treated as a drive letter.