Skip to content

Commit 64428dc

Browse files
avivkelleraduh95
authored andcommitted
lib, url: add a windows option to path parsing
PR-URL: #52509 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Zeyu "Alex" Yang <[email protected]> Reviewed-By: LiviaMedeiros <[email protected]>
1 parent 90c6e77 commit 64428dc

File tree

6 files changed

+267
-216
lines changed

6 files changed

+267
-216
lines changed

doc/api/esm.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,7 +1175,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11751175
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
11761176
[`path.dirname()`]: path.md#pathdirnamepath
11771177
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
1178-
[`url.fileURLToPath()`]: url.md#urlfileurltopathurl
1178+
[`url.fileURLToPath()`]: url.md#urlfileurltopathurl-options
11791179
[cjs-module-lexer]: https://github.com/nodejs/cjs-module-lexer/tree/1.2.2
11801180
[commonjs-extension-resolution-loader]: https://github.com/nodejs/loaders-test/tree/main/commonjs-extension-resolution-loader
11811181
[custom https loader]: module.md#import-from-https
@@ -1184,4 +1184,4 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11841184
[special scheme]: https://url.spec.whatwg.org/#special-scheme
11851185
[status code]: process.md#exit-codes
11861186
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
1187-
[url.pathToFileURL]: url.md#urlpathtofileurlpath
1187+
[url.pathToFileURL]: url.md#urlpathtofileurlpath-options

doc/api/url.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,13 +1151,23 @@ console.log(url.domainToUnicode('xn--iñvalid.com'));
11511151
// Prints an empty string
11521152
```
11531153

1154-
### `url.fileURLToPath(url)`
1154+
### `url.fileURLToPath(url[, options])`
11551155

11561156
<!-- YAML
11571157
added: v10.12.0
1158+
changes:
1159+
- version: REPLACEME
1160+
pr-url: https://github.com/nodejs/node/pull/52509
1161+
description: The `options` argument can now be used to
1162+
determine how to parse the `path` argument.
11581163
-->
11591164

11601165
* `url` {URL | string} The file URL string or URL object to convert to a path.
1166+
* `options` {Object}
1167+
* `windows` {boolean|undefined} `true` if the `path` should be
1168+
return as a windows filepath, `false` for posix, and
1169+
`undefined` for the system default.
1170+
**Default:** `undefined`.
11611171
* Returns: {string} The fully-resolved platform-specific Node.js file path.
11621172

11631173
This function ensures the correct decodings of percent-encoded characters as
@@ -1251,13 +1261,23 @@ console.log(url.format(myURL, { fragment: false, unicode: true, auth: false }));
12511261
// Prints 'https://測試/?abc'
12521262
```
12531263
1254-
### `url.pathToFileURL(path)`
1264+
### `url.pathToFileURL(path[, options])`
12551265
12561266
<!-- YAML
12571267
added: v10.12.0
1268+
changes:
1269+
- version: REPLACEME
1270+
pr-url: https://github.com/nodejs/node/pull/52509
1271+
description: The `options` argument can now be used to
1272+
determine how to return the `path` value.
12581273
-->
12591274
12601275
* `path` {string} The path to convert to a File URL.
1276+
* `options` {Object}
1277+
* `windows` {boolean|undefined} `true` if the `path` should be
1278+
treated as a windows filepath, `false` for posix, and
1279+
`undefined` for the system default.
1280+
**Default:** `undefined`.
12611281
* Returns: {URL} The file URL object.
12621282
12631283
This function ensures that `path` is resolved absolutely, and that the URL

lib/internal/url.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const {
4444
getConstructorOf,
4545
removeColors,
4646
kEnumerableProperty,
47+
kEmptyObject,
4748
SideEffectFreeRegExpPrototypeSymbolReplace,
4849
} = require('internal/util');
4950

@@ -1480,14 +1481,15 @@ function getPathFromURLPosix(url) {
14801481
return decodeURIComponent(pathname);
14811482
}
14821483

1483-
function fileURLToPath(path) {
1484+
function fileURLToPath(path, options = kEmptyObject) {
1485+
const windows = options?.windows;
14841486
if (typeof path === 'string')
14851487
path = new URL(path);
14861488
else if (!isURL(path))
14871489
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
14881490
if (path.protocol !== 'file:')
14891491
throw new ERR_INVALID_URL_SCHEME('file');
1490-
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
1492+
return (windows ?? isWindows) ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
14911493
}
14921494

14931495
// The following characters are percent-encoded when converting from file path
@@ -1509,11 +1511,12 @@ const tabRegEx = /\t/g;
15091511
const questionRegex = /\?/g;
15101512
const hashRegex = /#/g;
15111513

1512-
function encodePathChars(filepath) {
1514+
function encodePathChars(filepath, options = kEmptyObject) {
1515+
const windows = options?.windows;
15131516
if (StringPrototypeIndexOf(filepath, '%') !== -1)
15141517
filepath = RegExpPrototypeSymbolReplace(percentRegEx, filepath, '%25');
15151518
// In posix, backslash is a valid character in paths:
1516-
if (!isWindows && StringPrototypeIndexOf(filepath, '\\') !== -1)
1519+
if (!(windows ?? isWindows) && StringPrototypeIndexOf(filepath, '\\') !== -1)
15171520
filepath = RegExpPrototypeSymbolReplace(backslashRegEx, filepath, '%5C');
15181521
if (StringPrototypeIndexOf(filepath, '\n') !== -1)
15191522
filepath = RegExpPrototypeSymbolReplace(newlineRegEx, filepath, '%0A');
@@ -1524,8 +1527,9 @@ function encodePathChars(filepath) {
15241527
return filepath;
15251528
}
15261529

1527-
function pathToFileURL(filepath) {
1528-
if (isWindows && StringPrototypeStartsWith(filepath, '\\\\')) {
1530+
function pathToFileURL(filepath, options = kEmptyObject) {
1531+
const windows = options?.windows;
1532+
if ((windows ?? isWindows) && StringPrototypeStartsWith(filepath, '\\\\')) {
15291533
const outURL = new URL('file://');
15301534
// UNC path format: \\server\share\resource
15311535
const hostnameEndIndex = StringPrototypeIndexOf(filepath, '\\', 2);
@@ -1546,20 +1550,22 @@ function pathToFileURL(filepath) {
15461550
const hostname = StringPrototypeSlice(filepath, 2, hostnameEndIndex);
15471551
outURL.hostname = domainToASCII(hostname);
15481552
outURL.pathname = encodePathChars(
1549-
RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/'));
1553+
RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/'),
1554+
{ windows },
1555+
);
15501556
return outURL;
15511557
}
1552-
let resolved = path.resolve(filepath);
1558+
let resolved = (windows ?? isWindows) ? path.win32.resolve(filepath) : path.posix.resolve(filepath);
15531559
// path.resolve strips trailing slashes so we must add them back
15541560
const filePathLast = StringPrototypeCharCodeAt(filepath,
15551561
filepath.length - 1);
15561562
if ((filePathLast === CHAR_FORWARD_SLASH ||
1557-
(isWindows && filePathLast === CHAR_BACKWARD_SLASH)) &&
1563+
((windows ?? isWindows) && filePathLast === CHAR_BACKWARD_SLASH)) &&
15581564
resolved[resolved.length - 1] !== path.sep)
15591565
resolved += '/';
15601566

15611567
// Call encodePathChars first to avoid encoding % again for ? and #.
1562-
resolved = encodePathChars(resolved);
1568+
resolved = encodePathChars(resolved, { windows });
15631569

15641570
// Question and hash character should be included in pathname.
15651571
// Therefore, encoding is required to eliminate parsing them in different states.

lib/url.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,10 +1020,10 @@ Url.prototype.parseHost = function parseHost() {
10201020
// When used internally, we are not obligated to associate TypeError with
10211021
// this function, so non-strings can be rejected by underlying implementation.
10221022
// Public API has to validate input and throw appropriate error.
1023-
function pathToFileURL(path) {
1023+
function pathToFileURL(path, options) {
10241024
validateString(path, 'path');
10251025

1026-
return _pathToFileURL(path);
1026+
return _pathToFileURL(path, options);
10271027
}
10281028

10291029
module.exports = {

test/parallel/test-url-fileurltopath.js

Lines changed: 115 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -49,106 +49,120 @@ assert.throws(() => url.fileURLToPath('https://a/b/c'), {
4949
}
5050
}
5151

52-
{
53-
let testCases;
54-
if (isWindows) {
55-
testCases = [
56-
// Lowercase ascii alpha
57-
{ path: 'C:\\foo', fileURL: 'file:///C:/foo' },
58-
// Uppercase ascii alpha
59-
{ path: 'C:\\FOO', fileURL: 'file:///C:/FOO' },
60-
// dir
61-
{ path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' },
62-
// trailing separator
63-
{ path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' },
64-
// dot
65-
{ path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' },
66-
// space
67-
{ path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' },
68-
// question mark
69-
{ path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' },
70-
// number sign
71-
{ path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' },
72-
// ampersand
73-
{ path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' },
74-
// equals
75-
{ path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' },
76-
// colon
77-
{ path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' },
78-
// semicolon
79-
{ path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' },
80-
// percent
81-
{ path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' },
82-
// backslash
83-
{ path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' },
84-
// backspace
85-
{ path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' },
86-
// tab
87-
{ path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' },
88-
// newline
89-
{ path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' },
90-
// carriage return
91-
{ path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' },
92-
// latin1
93-
{ path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
94-
// Euro sign (BMP code point)
95-
{ path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' },
96-
// Rocket emoji (non-BMP code point)
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' },
100-
];
101-
} else {
102-
testCases = [
103-
// Lowercase ascii alpha
104-
{ path: '/foo', fileURL: 'file:///foo' },
105-
// Uppercase ascii alpha
106-
{ path: '/FOO', fileURL: 'file:///FOO' },
107-
// dir
108-
{ path: '/dir/foo', fileURL: 'file:///dir/foo' },
109-
// trailing separator
110-
{ path: '/dir/', fileURL: 'file:///dir/' },
111-
// dot
112-
{ path: '/foo.mjs', fileURL: 'file:///foo.mjs' },
113-
// space
114-
{ path: '/foo bar', fileURL: 'file:///foo%20bar' },
115-
// question mark
116-
{ path: '/foo?bar', fileURL: 'file:///foo%3Fbar' },
117-
// number sign
118-
{ path: '/foo#bar', fileURL: 'file:///foo%23bar' },
119-
// ampersand
120-
{ path: '/foo&bar', fileURL: 'file:///foo&bar' },
121-
// equals
122-
{ path: '/foo=bar', fileURL: 'file:///foo=bar' },
123-
// colon
124-
{ path: '/foo:bar', fileURL: 'file:///foo:bar' },
125-
// semicolon
126-
{ path: '/foo;bar', fileURL: 'file:///foo;bar' },
127-
// percent
128-
{ path: '/foo%bar', fileURL: 'file:///foo%25bar' },
129-
// backslash
130-
{ path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' },
131-
// backspace
132-
{ path: '/foo\bbar', fileURL: 'file:///foo%08bar' },
133-
// tab
134-
{ path: '/foo\tbar', fileURL: 'file:///foo%09bar' },
135-
// newline
136-
{ path: '/foo\nbar', fileURL: 'file:///foo%0Abar' },
137-
// carriage return
138-
{ path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' },
139-
// latin1
140-
{ path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
141-
// Euro sign (BMP code point)
142-
{ path: '/€', fileURL: 'file:///%E2%82%AC' },
143-
// Rocket emoji (non-BMP code point)
144-
{ path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' },
145-
];
146-
}
52+
const windowsTestCases = [
53+
// Lowercase ascii alpha
54+
{ path: 'C:\\foo', fileURL: 'file:///C:/foo' },
55+
// Uppercase ascii alpha
56+
{ path: 'C:\\FOO', fileURL: 'file:///C:/FOO' },
57+
// dir
58+
{ path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' },
59+
// trailing separator
60+
{ path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' },
61+
// dot
62+
{ path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' },
63+
// space
64+
{ path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' },
65+
// question mark
66+
{ path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' },
67+
// number sign
68+
{ path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' },
69+
// ampersand
70+
{ path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' },
71+
// equals
72+
{ path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' },
73+
// colon
74+
{ path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' },
75+
// semicolon
76+
{ path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' },
77+
// percent
78+
{ path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' },
79+
// backslash
80+
{ path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' },
81+
// backspace
82+
{ path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' },
83+
// tab
84+
{ path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' },
85+
// newline
86+
{ path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' },
87+
// carriage return
88+
{ path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' },
89+
// latin1
90+
{ path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
91+
// Euro sign (BMP code point)
92+
{ path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' },
93+
// Rocket emoji (non-BMP code point)
94+
{ path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' },
95+
// UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
96+
{ path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' },
97+
];
98+
const posixTestCases = [
99+
// Lowercase ascii alpha
100+
{ path: '/foo', fileURL: 'file:///foo' },
101+
// Uppercase ascii alpha
102+
{ path: '/FOO', fileURL: 'file:///FOO' },
103+
// dir
104+
{ path: '/dir/foo', fileURL: 'file:///dir/foo' },
105+
// trailing separator
106+
{ path: '/dir/', fileURL: 'file:///dir/' },
107+
// dot
108+
{ path: '/foo.mjs', fileURL: 'file:///foo.mjs' },
109+
// space
110+
{ path: '/foo bar', fileURL: 'file:///foo%20bar' },
111+
// question mark
112+
{ path: '/foo?bar', fileURL: 'file:///foo%3Fbar' },
113+
// number sign
114+
{ path: '/foo#bar', fileURL: 'file:///foo%23bar' },
115+
// ampersand
116+
{ path: '/foo&bar', fileURL: 'file:///foo&bar' },
117+
// equals
118+
{ path: '/foo=bar', fileURL: 'file:///foo=bar' },
119+
// colon
120+
{ path: '/foo:bar', fileURL: 'file:///foo:bar' },
121+
// semicolon
122+
{ path: '/foo;bar', fileURL: 'file:///foo;bar' },
123+
// percent
124+
{ path: '/foo%bar', fileURL: 'file:///foo%25bar' },
125+
// backslash
126+
{ path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' },
127+
// backspace
128+
{ path: '/foo\bbar', fileURL: 'file:///foo%08bar' },
129+
// tab
130+
{ path: '/foo\tbar', fileURL: 'file:///foo%09bar' },
131+
// newline
132+
{ path: '/foo\nbar', fileURL: 'file:///foo%0Abar' },
133+
// carriage return
134+
{ path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' },
135+
// latin1
136+
{ path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
137+
// Euro sign (BMP code point)
138+
{ path: '/€', fileURL: 'file:///%E2%82%AC' },
139+
// Rocket emoji (non-BMP code point)
140+
{ path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' },
141+
];
147142

148-
for (const { path, fileURL } of testCases) {
149-
const fromString = url.fileURLToPath(fileURL);
150-
assert.strictEqual(fromString, path);
151-
const fromURL = url.fileURLToPath(new URL(fileURL));
152-
assert.strictEqual(fromURL, path);
153-
}
143+
for (const { path, fileURL } of windowsTestCases) {
144+
const fromString = url.fileURLToPath(fileURL, { windows: true });
145+
assert.strictEqual(fromString, path);
146+
const fromURL = url.fileURLToPath(new URL(fileURL), { windows: true });
147+
assert.strictEqual(fromURL, path);
148+
}
149+
150+
for (const { path, fileURL } of posixTestCases) {
151+
const fromString = url.fileURLToPath(fileURL, { windows: false });
152+
assert.strictEqual(fromString, path);
153+
const fromURL = url.fileURLToPath(new URL(fileURL), { windows: false });
154+
assert.strictEqual(fromURL, path);
155+
}
156+
157+
const defaultTestCases = isWindows ? windowsTestCases : posixTestCases;
158+
159+
// Test when `options` is null
160+
const whenNullActual = url.fileURLToPath(new URL(defaultTestCases[0].fileURL), null);
161+
assert.strictEqual(whenNullActual, defaultTestCases[0].path);
162+
163+
for (const { path, fileURL } of defaultTestCases) {
164+
const fromString = url.fileURLToPath(fileURL);
165+
assert.strictEqual(fromString, path);
166+
const fromURL = url.fileURLToPath(new URL(fileURL));
167+
assert.strictEqual(fromURL, path);
154168
}

0 commit comments

Comments
 (0)