Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 26 additions & 2 deletions test/parallel/test-path-win32-normalize-device-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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:' },
Expand Down Expand Up @@ -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.
Expand Down