Skip to content

Commit f29ab40

Browse files
mceachenaddaleax
authored andcommitted
lib: add UNC support to url.pathToFileURL()
Fixes: #34736 PR-URL: #34743 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent b4d9e0d commit f29ab40

File tree

3 files changed

+73
-21
lines changed

3 files changed

+73
-21
lines changed

lib/internal/url.js

+47-19
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const { getConstructorOf, removeColors } = require('internal/util');
2727
const {
2828
ERR_ARG_NOT_ITERABLE,
2929
ERR_INVALID_ARG_TYPE,
30+
ERR_INVALID_ARG_VALUE,
3031
ERR_INVALID_CALLBACK,
3132
ERR_INVALID_FILE_URL_HOST,
3233
ERR_INVALID_FILE_URL_PATH,
@@ -1369,27 +1370,54 @@ const backslashRegEx = /\\/g;
13691370
const newlineRegEx = /\n/g;
13701371
const carriageReturnRegEx = /\r/g;
13711372
const tabRegEx = /\t/g;
1373+
1374+
function encodePathChars(filepath) {
1375+
if (filepath.includes('%'))
1376+
filepath = filepath.replace(percentRegEx, '%25');
1377+
// In posix, backslash is a valid character in paths:
1378+
if (!isWindows && filepath.includes('\\'))
1379+
filepath = filepath.replace(backslashRegEx, '%5C');
1380+
if (filepath.includes('\n'))
1381+
filepath = filepath.replace(newlineRegEx, '%0A');
1382+
if (filepath.includes('\r'))
1383+
filepath = filepath.replace(carriageReturnRegEx, '%0D');
1384+
if (filepath.includes('\t'))
1385+
filepath = filepath.replace(tabRegEx, '%09');
1386+
return filepath;
1387+
}
1388+
13721389
function pathToFileURL(filepath) {
1373-
let resolved = path.resolve(filepath);
1374-
// path.resolve strips trailing slashes so we must add them back
1375-
const filePathLast = filepath.charCodeAt(filepath.length - 1);
1376-
if ((filePathLast === CHAR_FORWARD_SLASH ||
1377-
(isWindows && filePathLast === CHAR_BACKWARD_SLASH)) &&
1378-
resolved[resolved.length - 1] !== path.sep)
1379-
resolved += '/';
13801390
const outURL = new URL('file://');
1381-
if (resolved.includes('%'))
1382-
resolved = resolved.replace(percentRegEx, '%25');
1383-
// In posix, "/" is a valid character in paths
1384-
if (!isWindows && resolved.includes('\\'))
1385-
resolved = resolved.replace(backslashRegEx, '%5C');
1386-
if (resolved.includes('\n'))
1387-
resolved = resolved.replace(newlineRegEx, '%0A');
1388-
if (resolved.includes('\r'))
1389-
resolved = resolved.replace(carriageReturnRegEx, '%0D');
1390-
if (resolved.includes('\t'))
1391-
resolved = resolved.replace(tabRegEx, '%09');
1392-
outURL.pathname = resolved;
1391+
if (isWindows && filepath.startsWith('\\\\')) {
1392+
// UNC path format: \\server\share\resource
1393+
const paths = filepath.split('\\');
1394+
if (paths.length <= 3) {
1395+
throw new ERR_INVALID_ARG_VALUE(
1396+
'filepath',
1397+
filepath,
1398+
'Missing UNC resource path'
1399+
);
1400+
}
1401+
const hostname = paths[2];
1402+
if (hostname.length === 0) {
1403+
throw new ERR_INVALID_ARG_VALUE(
1404+
'filepath',
1405+
filepath,
1406+
'Empty UNC servername'
1407+
);
1408+
}
1409+
outURL.hostname = domainToASCII(hostname);
1410+
outURL.pathname = encodePathChars(paths.slice(3).join('/'));
1411+
} else {
1412+
let resolved = path.resolve(filepath);
1413+
// path.resolve strips trailing slashes so we must add them back
1414+
const filePathLast = filepath.charCodeAt(filepath.length - 1);
1415+
if ((filePathLast === CHAR_FORWARD_SLASH ||
1416+
(isWindows && filePathLast === CHAR_BACKWARD_SLASH)) &&
1417+
resolved[resolved.length - 1] !== path.sep)
1418+
resolved += '/';
1419+
outURL.pathname = encodePathChars(resolved);
1420+
}
13931421
return outURL;
13941422
}
13951423

test/parallel/test-url-fileurltopath.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ assert.throws(() => url.fileURLToPath('https://a/b/c'), {
9494
// Euro sign (BMP code point)
9595
{ path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' },
9696
// Rocket emoji (non-BMP code point)
97-
{ path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' }
97+
{ path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' },
98+
// UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
99+
{ path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' },
98100
];
99101
} else {
100102
testCases = [

test/parallel/test-url-pathtofileurl.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ const url = require('url');
2323
assert.ok(fileURL.includes('%25'));
2424
}
2525

26+
{
27+
if (isWindows) {
28+
// UNC path: \\server\share\resource
29+
30+
// Missing server:
31+
assert.throws(() => url.pathToFileURL('\\\\\\no-server'), {
32+
code: 'ERR_INVALID_ARG_VALUE'
33+
});
34+
35+
// Missing share or resource:
36+
assert.throws(() => url.pathToFileURL('\\\\host'), {
37+
code: 'ERR_INVALID_ARG_VALUE'
38+
});
39+
} else {
40+
// UNC paths on posix are considered a single path that has backslashes:
41+
const fileURL = url.pathToFileURL('\\\\nas\\share\\path.txt').href;
42+
assert.match(fileURL, /file:\/\/.+%5C%5Cnas%5Cshare%5Cpath\.txt$/);
43+
}
44+
}
45+
2646
{
2747
let testCases;
2848
if (isWindows) {
@@ -68,7 +88,9 @@ const url = require('url');
6888
// Euro sign (BMP code point)
6989
{ path: 'C:\\€', expected: 'file:///C:/%E2%82%AC' },
7090
// Rocket emoji (non-BMP code point)
71-
{ path: 'C:\\🚀', expected: 'file:///C:/%F0%9F%9A%80' }
91+
{ path: 'C:\\🚀', expected: 'file:///C:/%F0%9F%9A%80' },
92+
// UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
93+
{ path: '\\\\nas\\My Docs\\File.doc', expected: 'file://nas/My%20Docs/File.doc' }
7294
];
7395
} else {
7496
testCases = [

0 commit comments

Comments
 (0)