From 86e06aaa4473024a56bdede08e0cb0f7bbfc7795 Mon Sep 17 00:00:00 2001 From: HassanFleyah Date: Tue, 27 Jan 2026 16:40:16 +0300 Subject: [PATCH] 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 --- lib/path.js | 26 ++++++--- ...th-win32-normalize-device-missing-colon.js | 53 +++++++++++++++++++ .../test-path-win32-normalize-device-names.js | 21 ++++++++ 3 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 test/parallel/test-path-win32-normalize-device-missing-colon.js diff --git a/lib/path.js b/lib/path.js index 63b037cddfb986..8d222eeaf0da61 100644 --- a/lib/path.js +++ b/lib/path.js @@ -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 @@ -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) { diff --git a/test/parallel/test-path-win32-normalize-device-missing-colon.js b/test/parallel/test-path-win32-normalize-device-missing-colon.js new file mode 100644 index 00000000000000..e7e4e8f5993c92 --- /dev/null +++ b/test/parallel/test-path-win32-normalize-device-missing-colon.js @@ -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'); diff --git a/test/parallel/test-path-win32-normalize-device-names.js b/test/parallel/test-path-win32-normalize-device-names.js index b34c9061e56539..ffd95d74864afb 100644 --- a/test/parallel/test-path-win32-normalize-device-names.js +++ b/test/parallel/test-path-win32-normalize-device-names.js @@ -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.