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
26 changes: 19 additions & 7 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,12 +401,23 @@ const win32 = {
// We matched a device root (e.g. \\\\.\\PHYSICALDRIVE0)
device = `\\\\${firstPart}`;
rootEnd = 4;
const colonIndex = StringPrototypeIndexOf(path, ':');
// Special case: handle \\?\COM1: or similar reserved device paths
const possibleDevice = StringPrototypeSlice(path, 4, colonIndex + 1);
if (isWindowsReservedName(possibleDevice, possibleDevice.length - 1)) {
device = `\\\\?\\${possibleDevice}`;
rootEnd = 4 + possibleDevice.length;
// Determine the end of the root part (the first slash or end of string)
let rootPartEnd = 4;
while (rootPartEnd < len && !isPathSeparator(StringPrototypeCharCodeAt(path, rootPartEnd))) {
rootPartEnd++;
}

const rootPart = StringPrototypeSlice(path, 4, rootPartEnd);
const colonIndexInRoot = StringPrototypeIndexOf(rootPart, ':');

if (colonIndexInRoot !== -1) {
if (isWindowsReservedName(rootPart, colonIndexInRoot)) {
device = `\\\\${firstPart}\\${rootPart}`;
rootEnd = 4 + rootPart.length;
}
} else if (isWindowsReservedName(rootPart, rootPart.length)) {
device = `\\\\${firstPart}\\${rootPart}`;
rootEnd = 4 + rootPart.length;
}
} else if (j === len) {
// We matched a UNC root only
Expand Down Expand Up @@ -471,7 +482,8 @@ const win32 = {
} while ((index = StringPrototypeIndexOf(path, ':', index + 1)) !== -1);
}
const colonIndex = StringPrototypeIndexOf(path, ':');
if (isWindowsReservedName(path, colonIndex)) {
// Ensure colonIndex is valid before calling isWindowsReservedName
if (colonIndex !== -1 && isWindowsReservedName(path, colonIndex)) {
return `.\\${device ?? ''}${tail}`;
}
if (device === undefined) {
Expand Down
53 changes: 53 additions & 0 deletions test/parallel/test-path-win32-normalize-device-missing-colon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const path = require('path');

if (!common.isWindows)
common.skip('this test is for win32 only');

// 1. Basic cases for reserved names without colon (Both prefixes)
assert.strictEqual(path.win32.normalize('\\\\.\\CON'), '\\\\.\\CON');
assert.strictEqual(path.win32.normalize('\\\\?\\PRN'), '\\\\?\\PRN');

// 2. Mixed slashes check (Ensuring isPathSeparator works for both prefixes)
assert.strictEqual(
path.win32.normalize('\\\\.\\CON/file.txt'),
'\\\\.\\CON\\file.txt'
);
assert.strictEqual(
path.win32.normalize('\\\\?\\PRN/folder/sub'),
'\\\\?\\PRN\\folder\\sub'
);

// 3. Alternate Data Streams (Testing prefix symmetry for ADS)
assert.strictEqual(
path.win32.normalize('\\\\.\\CON\\file:ADS'),
'\\\\.\\CON\\file:ADS'
);
assert.strictEqual(
path.win32.normalize('\\\\?\\PRN\\data:stream'),
'\\\\?\\PRN\\data:stream'
);

// 4. Negative cases (Preventing over-matching)
// These should stay as-is because they are not exact reserved names
assert.strictEqual(path.win32.normalize('\\\\.\\CON-prefix'), '\\\\.\\CON-prefix');
assert.strictEqual(path.win32.normalize('\\\\?\\PRN-suffix'), '\\\\?\\PRN-suffix');

// 5. Join behavior (Ensuring the device acts as a persistent root)
const joined = path.win32.join('\\\\.\\CON', '..');
assert.strictEqual(joined, '\\\\.\\');

// 6. Cover root WITH colon (To cover line 413 in image_9e987d.png)
assert.strictEqual(path.win32.normalize('\\\\?\\CON:'), '\\\\?\\CON:\\');

// 7. Cover path WITHOUT any colon (To cover line 490 in image_9e9859.png)
assert.strictEqual(path.win32.normalize('CON'), 'CON');

// 8. Cover path WITH colon but NOT reserved (To cover partial branch at 490)
assert.strictEqual(path.win32.normalize('C:file.txt'), 'C:file.txt');

// 9. Ensure reserved names with colons get the .\ prefix (To fully green line 490)
assert.strictEqual(path.win32.normalize('CON:file'), '.\\CON:file');
21 changes: 21 additions & 0 deletions test/parallel/test-path-win32-normalize-device-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,27 @@ for (const { input, expected } of normalizeDeviceNameTests) {
`path.win32.normalize(${JSON.stringify(input)}) === ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`);
}

const normalizeReservedDeviceRootTests = [
{ input: '\\\\.\\CON', expected: '\\\\.\\CON' },
{ input: '\\\\?\\PRN', expected: '\\\\?\\PRN' },
{ input: '\\\\.\\AUX\\file.txt', expected: '\\\\.\\AUX\\file.txt' },
{ input: '\\\\?\\COM1/folder/file', expected: '\\\\?\\COM1\\folder\\file' },
{ input: '\\\\.\\CON\\file:ADS', expected: '\\\\.\\CON\\file:ADS' },
{ input: '\\\\?\\PRN\\data:stream', expected: '\\\\?\\PRN\\data:stream' },
{ input: '\\\\.\\CON-prefix', expected: '\\\\.\\CON-prefix' },
{ input: '\\\\?\\PRN-suffix', expected: '\\\\?\\PRN-suffix' },
{ input: '\\\\.\\NOT_A_DEVICE', expected: '\\\\.\\NOT_A_DEVICE' },
];

for (const { input, expected } of normalizeReservedDeviceRootTests) {
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
Loading