Skip to content

Commit 86e06aa

Browse files
committed
path: fix normalization of Windows device paths missing colon
test: add test case for device paths missing colon style: fix linter errors path: fix normalization of Windows device paths missing colon
1 parent 35fed19 commit 86e06aa

File tree

3 files changed

+93
-7
lines changed

3 files changed

+93
-7
lines changed

lib/path.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,23 @@ const win32 = {
401401
// We matched a device root (e.g. \\\\.\\PHYSICALDRIVE0)
402402
device = `\\\\${firstPart}`;
403403
rootEnd = 4;
404-
const colonIndex = StringPrototypeIndexOf(path, ':');
405-
// Special case: handle \\?\COM1: or similar reserved device paths
406-
const possibleDevice = StringPrototypeSlice(path, 4, colonIndex + 1);
407-
if (isWindowsReservedName(possibleDevice, possibleDevice.length - 1)) {
408-
device = `\\\\?\\${possibleDevice}`;
409-
rootEnd = 4 + possibleDevice.length;
404+
// Determine the end of the root part (the first slash or end of string)
405+
let rootPartEnd = 4;
406+
while (rootPartEnd < len && !isPathSeparator(StringPrototypeCharCodeAt(path, rootPartEnd))) {
407+
rootPartEnd++;
408+
}
409+
410+
const rootPart = StringPrototypeSlice(path, 4, rootPartEnd);
411+
const colonIndexInRoot = StringPrototypeIndexOf(rootPart, ':');
412+
413+
if (colonIndexInRoot !== -1) {
414+
if (isWindowsReservedName(rootPart, colonIndexInRoot)) {
415+
device = `\\\\${firstPart}\\${rootPart}`;
416+
rootEnd = 4 + rootPart.length;
417+
}
418+
} else if (isWindowsReservedName(rootPart, rootPart.length)) {
419+
device = `\\\\${firstPart}\\${rootPart}`;
420+
rootEnd = 4 + rootPart.length;
410421
}
411422
} else if (j === len) {
412423
// We matched a UNC root only
@@ -471,7 +482,8 @@ const win32 = {
471482
} while ((index = StringPrototypeIndexOf(path, ':', index + 1)) !== -1);
472483
}
473484
const colonIndex = StringPrototypeIndexOf(path, ':');
474-
if (isWindowsReservedName(path, colonIndex)) {
485+
// Ensure colonIndex is valid before calling isWindowsReservedName
486+
if (colonIndex !== -1 && isWindowsReservedName(path, colonIndex)) {
475487
return `.\\${device ?? ''}${tail}`;
476488
}
477489
if (device === undefined) {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const path = require('path');
6+
7+
if (!common.isWindows)
8+
common.skip('this test is for win32 only');
9+
10+
// 1. Basic cases for reserved names without colon (Both prefixes)
11+
assert.strictEqual(path.win32.normalize('\\\\.\\CON'), '\\\\.\\CON');
12+
assert.strictEqual(path.win32.normalize('\\\\?\\PRN'), '\\\\?\\PRN');
13+
14+
// 2. Mixed slashes check (Ensuring isPathSeparator works for both prefixes)
15+
assert.strictEqual(
16+
path.win32.normalize('\\\\.\\CON/file.txt'),
17+
'\\\\.\\CON\\file.txt'
18+
);
19+
assert.strictEqual(
20+
path.win32.normalize('\\\\?\\PRN/folder/sub'),
21+
'\\\\?\\PRN\\folder\\sub'
22+
);
23+
24+
// 3. Alternate Data Streams (Testing prefix symmetry for ADS)
25+
assert.strictEqual(
26+
path.win32.normalize('\\\\.\\CON\\file:ADS'),
27+
'\\\\.\\CON\\file:ADS'
28+
);
29+
assert.strictEqual(
30+
path.win32.normalize('\\\\?\\PRN\\data:stream'),
31+
'\\\\?\\PRN\\data:stream'
32+
);
33+
34+
// 4. Negative cases (Preventing over-matching)
35+
// These should stay as-is because they are not exact reserved names
36+
assert.strictEqual(path.win32.normalize('\\\\.\\CON-prefix'), '\\\\.\\CON-prefix');
37+
assert.strictEqual(path.win32.normalize('\\\\?\\PRN-suffix'), '\\\\?\\PRN-suffix');
38+
39+
// 5. Join behavior (Ensuring the device acts as a persistent root)
40+
const joined = path.win32.join('\\\\.\\CON', '..');
41+
assert.strictEqual(joined, '\\\\.\\');
42+
43+
// 6. Cover root WITH colon (To cover line 413 in image_9e987d.png)
44+
assert.strictEqual(path.win32.normalize('\\\\?\\CON:'), '\\\\?\\CON:\\');
45+
46+
// 7. Cover path WITHOUT any colon (To cover line 490 in image_9e9859.png)
47+
assert.strictEqual(path.win32.normalize('CON'), 'CON');
48+
49+
// 8. Cover path WITH colon but NOT reserved (To cover partial branch at 490)
50+
assert.strictEqual(path.win32.normalize('C:file.txt'), 'C:file.txt');
51+
52+
// 9. Ensure reserved names with colons get the .\ prefix (To fully green line 490)
53+
assert.strictEqual(path.win32.normalize('CON:file'), '.\\CON:file');

test/parallel/test-path-win32-normalize-device-names.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,27 @@ for (const { input, expected } of normalizeDeviceNameTests) {
115115
`path.win32.normalize(${JSON.stringify(input)}) === ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`);
116116
}
117117

118+
const normalizeReservedDeviceRootTests = [
119+
{ input: '\\\\.\\CON', expected: '\\\\.\\CON' },
120+
{ input: '\\\\?\\PRN', expected: '\\\\?\\PRN' },
121+
{ input: '\\\\.\\AUX\\file.txt', expected: '\\\\.\\AUX\\file.txt' },
122+
{ input: '\\\\?\\COM1/folder/file', expected: '\\\\?\\COM1\\folder\\file' },
123+
{ input: '\\\\.\\CON\\file:ADS', expected: '\\\\.\\CON\\file:ADS' },
124+
{ input: '\\\\?\\PRN\\data:stream', expected: '\\\\?\\PRN\\data:stream' },
125+
{ input: '\\\\.\\CON-prefix', expected: '\\\\.\\CON-prefix' },
126+
{ input: '\\\\?\\PRN-suffix', expected: '\\\\?\\PRN-suffix' },
127+
{ input: '\\\\.\\NOT_A_DEVICE', expected: '\\\\.\\NOT_A_DEVICE' },
128+
];
129+
130+
for (const { input, expected } of normalizeReservedDeviceRootTests) {
131+
const actual = path.win32.normalize(input);
132+
assert.strictEqual(
133+
actual,
134+
expected,
135+
`path.win32.normalize(${JSON.stringify(input)}) === ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`
136+
);
137+
}
138+
118139
assert.strictEqual(path.win32.normalize('CON:foo/../bar'), '.\\CON:bar');
119140

120141
// This should NOT be prefixed because 'c:' is treated as a drive letter.

0 commit comments

Comments
 (0)