Skip to content

Commit 4f76d57

Browse files
authored
Merge pull request #1 from jazelly/copilot/handle-extended-path-on-windows
Copilot/handle extended path on windows
2 parents d9645d7 + 4a66705 commit 4f76d57

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

lib/fs.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ const {
132132
const {
133133
CHAR_FORWARD_SLASH,
134134
CHAR_BACKWARD_SLASH,
135+
CHAR_COLON,
136+
CHAR_QUESTION_MARK,
137+
CHAR_UPPERCASE_A,
138+
CHAR_UPPERCASE_C,
139+
CHAR_UPPERCASE_Z,
140+
CHAR_LOWERCASE_A,
141+
CHAR_LOWERCASE_N,
142+
CHAR_LOWERCASE_Z,
135143
} = require('internal/constants');
136144
const {
137145
isInt32,
@@ -2628,6 +2636,43 @@ function unwatchFile(filename, listener) {
26282636
}
26292637

26302638

2639+
// Strips the Windows extended-length path prefix (\\?\) from a resolved path.
2640+
// Extended-length paths (\\?\C:\... or \\?\UNC\...) are a Win32 API mechanism
2641+
// to bypass MAX_PATH limits. Node.js should handle them transparently by
2642+
// converting to standard paths for internal processing. The \\?\ prefix is
2643+
// re-added when needed via path.toNamespacedPath() before system calls.
2644+
// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2645+
function stripExtendedPathPrefix(p) {
2646+
// Check for \\?\ prefix
2647+
if (p.length >= 4 &&
2648+
StringPrototypeCharCodeAt(p, 0) === CHAR_BACKWARD_SLASH &&
2649+
StringPrototypeCharCodeAt(p, 1) === CHAR_BACKWARD_SLASH &&
2650+
StringPrototypeCharCodeAt(p, 2) === CHAR_QUESTION_MARK &&
2651+
StringPrototypeCharCodeAt(p, 3) === CHAR_BACKWARD_SLASH) {
2652+
// \\?\C:\ -> C:\ (extended drive path)
2653+
if (p.length >= 6) {
2654+
const drive = StringPrototypeCharCodeAt(p, 4);
2655+
if (((drive >= CHAR_UPPERCASE_A && drive <= CHAR_UPPERCASE_Z) ||
2656+
(drive >= CHAR_LOWERCASE_A && drive <= CHAR_LOWERCASE_Z)) &&
2657+
StringPrototypeCharCodeAt(p, 5) === CHAR_COLON) {
2658+
return StringPrototypeSlice(p, 4);
2659+
}
2660+
}
2661+
// \\?\UNC\server\share -> \\server\share (extended UNC path)
2662+
if (p.length >= 8 &&
2663+
(StringPrototypeCharCodeAt(p, 4) === 85 /* U */ ||
2664+
StringPrototypeCharCodeAt(p, 4) === 117 /* u */) &&
2665+
(StringPrototypeCharCodeAt(p, 5) === 78 /* N */ ||
2666+
StringPrototypeCharCodeAt(p, 5) === CHAR_LOWERCASE_N) &&
2667+
(StringPrototypeCharCodeAt(p, 6) === CHAR_UPPERCASE_C ||
2668+
StringPrototypeCharCodeAt(p, 6) === 99 /* c */) &&
2669+
StringPrototypeCharCodeAt(p, 7) === CHAR_BACKWARD_SLASH) {
2670+
return '\\\\' + StringPrototypeSlice(p, 8);
2671+
}
2672+
}
2673+
return p;
2674+
}
2675+
26312676
let splitRoot;
26322677
if (isWindows) {
26332678
// Regex to find the device root on Windows (e.g. 'c:\\'), including trailing
@@ -2690,6 +2735,12 @@ function realpathSync(p, options) {
26902735
validatePath(p);
26912736
p = pathModule.resolve(p);
26922737

2738+
// On Windows, strip the extended-length path prefix (\\?\) so that the
2739+
// path walking logic below works with standard drive-letter or UNC roots.
2740+
if (isWindows) {
2741+
p = stripExtendedPathPrefix(p);
2742+
}
2743+
26932744
const cache = options[realpathCacheKey];
26942745
const maybeCachedResult = cache?.get(p);
26952746
if (maybeCachedResult) {
@@ -2793,6 +2844,11 @@ function realpathSync(p, options) {
27932844
// Resolve the link, then start over
27942845
p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos));
27952846

2847+
// Strip extended path prefix again in case pathModule.resolve re-added it
2848+
if (isWindows) {
2849+
p = stripExtendedPathPrefix(p);
2850+
}
2851+
27962852
// Skip over roots
27972853
current = base = splitRoot(p);
27982854
pos = current.length;
@@ -2851,6 +2907,12 @@ function realpath(p, options, callback) {
28512907
validatePath(p);
28522908
p = pathModule.resolve(p);
28532909

2910+
// On Windows, strip the extended-length path prefix (\\?\) so that the
2911+
// path walking logic below works with standard drive-letter or UNC roots.
2912+
if (isWindows) {
2913+
p = stripExtendedPathPrefix(p);
2914+
}
2915+
28542916
const seenLinks = new SafeMap();
28552917
const knownHard = new SafeSet();
28562918

@@ -2951,6 +3013,12 @@ function realpath(p, options, callback) {
29513013
function gotResolvedLink(resolvedLink) {
29523014
// Resolve the link, then start over
29533015
p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos));
3016+
3017+
// Strip extended path prefix again in case pathModule.resolve re-added it
3018+
if (isWindows) {
3019+
p = stripExtendedPathPrefix(p);
3020+
}
3021+
29543022
current = base = splitRoot(p);
29553023
pos = current.length;
29563024

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// This test verifies that fs.realpathSync and fs.realpath correctly handle
5+
// Windows extended-length path prefixes (\\?\C:\... and \\?\UNC\...).
6+
// See: https://github.com/nodejs/node/issues/62446
7+
8+
if (!common.isWindows)
9+
common.skip('Windows-specific test.');
10+
11+
const assert = require('assert');
12+
const fs = require('fs');
13+
const path = require('path');
14+
const tmpdir = require('../common/tmpdir');
15+
16+
tmpdir.refresh();
17+
18+
const testFile = tmpdir.resolve('extended-path-test.js');
19+
fs.writeFileSync(testFile, 'module.exports = 42;');
20+
21+
// Construct the extended-length path for the test file.
22+
// The \\?\ prefix is a Win32 API mechanism to bypass MAX_PATH limits.
23+
const extendedPath = `\\\\?\\${testFile}`;
24+
25+
// fs.realpathSync should handle the \\?\ prefix and return a standard path.
26+
{
27+
const result = fs.realpathSync(extendedPath);
28+
// The result should be the resolved path without the \\?\ prefix.
29+
assert.strictEqual(result.toLowerCase(), testFile.toLowerCase());
30+
}
31+
32+
// fs.realpath (async) should also handle the \\?\ prefix.
33+
fs.realpath(extendedPath, common.mustSucceed((result) => {
34+
assert.strictEqual(result.toLowerCase(), testFile.toLowerCase());
35+
}));
36+
37+
// Also test that the extended path for the drive root works.
38+
{
39+
const driveRoot = path.parse(testFile).root; // e.g., 'C:\'
40+
const extendedRoot = `\\\\?\\${driveRoot}`;
41+
const result = fs.realpathSync(extendedRoot);
42+
assert.strictEqual(result.toLowerCase(), driveRoot.toLowerCase());
43+
}
44+
45+
// Test extended-length path with subdirectory.
46+
const subDir = tmpdir.resolve('sub', 'dir');
47+
fs.mkdirSync(subDir, { recursive: true });
48+
const subFile = path.join(subDir, 'file.txt');
49+
fs.writeFileSync(subFile, 'hello');
50+
51+
{
52+
const extendedSubFile = `\\\\?\\${subFile}`;
53+
const result = fs.realpathSync(extendedSubFile);
54+
assert.strictEqual(result.toLowerCase(), subFile.toLowerCase());
55+
}

0 commit comments

Comments
 (0)