Skip to content

Commit

Permalink
lib, url: fix #51609 by adding a win argument
Browse files Browse the repository at this point in the history
  • Loading branch information
RedYetiDev committed Apr 20, 2024
1 parent 2cd3073 commit 35ba48e
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 215 deletions.
4 changes: 2 additions & 2 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
[`path.dirname()`]: path.md#pathdirnamepath
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
[`url.fileURLToPath()`]: url.md#urlfileurltopathurl
[`url.fileURLToPath()`]: url.md#urlfileurltopathurl-options
[cjs-module-lexer]: https://github.com/nodejs/cjs-module-lexer/tree/1.2.2
[commonjs-extension-resolution-loader]: https://github.com/nodejs/loaders-test/tree/main/commonjs-extension-resolution-loader
[custom https loader]: module.md#import-from-https
Expand All @@ -1184,4 +1184,4 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[special scheme]: https://url.spec.whatwg.org/#special-scheme
[status code]: process.md#exit-codes
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
[url.pathToFileURL]: url.md#urlpathtofileurlpath
[url.pathToFileURL]: url.md#urlpathtofileurlpath-options
22 changes: 20 additions & 2 deletions doc/api/url.md
Original file line number Diff line number Diff line change
Expand Up @@ -1151,13 +1151,22 @@ console.log(url.domainToUnicode('xn--iñvalid.com'));
// Prints an empty string
```

### `url.fileURLToPath(url)`
### `url.fileURLToPath(url[, options])`

<!-- YAML
added: v10.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/52509
description: The `options` argument can now be used to
determine how to parse the `path` argument.
-->

* `url` {URL | string} The file URL string or URL object to convert to a path.
* `options` {Object}
* `windows` {boolean|undefined} `true` if the `path` should be
treated as a windows filepath, `false` otherwise.
**Default:** `undefined`.
* Returns: {string} The fully-resolved platform-specific Node.js file path.

This function ensures the correct decodings of percent-encoded characters as
Expand Down Expand Up @@ -1251,13 +1260,22 @@ console.log(url.format(myURL, { fragment: false, unicode: true, auth: false }));
// Prints 'https://測試/?abc'
```
### `url.pathToFileURL(path)`
### `url.pathToFileURL(path[, options])`
<!-- YAML
added: v10.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/52509
description: The `options` argument can now be used to
determine how to parse the `path` argument.
-->
* `path` {string} The path to convert to a File URL.
* `options` {Object}
* `windows` {boolean|undefined} `true` if the `path` should be
treated as a windows filepath, `false` otherwise.
**Default:** `undefined`.
* Returns: {URL} The file URL object.
This function ensures that `path` is resolved absolutely, and that the URL
Expand Down
22 changes: 12 additions & 10 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -1460,14 +1460,14 @@ function getPathFromURLPosix(url) {
return decodeURIComponent(pathname);
}

function fileURLToPath(path) {
function fileURLToPath(path, { windows } = {}) {
if (typeof path === 'string')
path = new URL(path);
else if (!isURL(path))
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
if (path.protocol !== 'file:')
throw new ERR_INVALID_URL_SCHEME('file');
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
return (windows ?? isWindows) ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}

// The following characters are percent-encoded when converting from file path
Expand All @@ -1489,11 +1489,11 @@ const tabRegEx = /\t/g;
const questionRegex = /\?/g;
const hashRegex = /#/g;

function encodePathChars(filepath) {
function encodePathChars(filepath, { windows } = {}) {
if (StringPrototypeIndexOf(filepath, '%') !== -1)
filepath = RegExpPrototypeSymbolReplace(percentRegEx, filepath, '%25');
// In posix, backslash is a valid character in paths:
if (!isWindows && StringPrototypeIndexOf(filepath, '\\') !== -1)
if (!(windows ?? isWindows) && StringPrototypeIndexOf(filepath, '\\') !== -1)
filepath = RegExpPrototypeSymbolReplace(backslashRegEx, filepath, '%5C');
if (StringPrototypeIndexOf(filepath, '\n') !== -1)
filepath = RegExpPrototypeSymbolReplace(newlineRegEx, filepath, '%0A');
Expand All @@ -1504,8 +1504,8 @@ function encodePathChars(filepath) {
return filepath;
}

function pathToFileURL(filepath) {
if (isWindows && StringPrototypeStartsWith(filepath, '\\\\')) {
function pathToFileURL(filepath, { windows } = {}) {
if ((windows ?? isWindows) && StringPrototypeStartsWith(filepath, '\\\\')) {
const outURL = new URL('file://');
// UNC path format: \\server\share\resource
const hostnameEndIndex = StringPrototypeIndexOf(filepath, '\\', 2);
Expand All @@ -1526,20 +1526,22 @@ function pathToFileURL(filepath) {
const hostname = StringPrototypeSlice(filepath, 2, hostnameEndIndex);
outURL.hostname = domainToASCII(hostname);
outURL.pathname = encodePathChars(
RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/'));
RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/'),
{ windows },
);
return outURL;
}
let resolved = path.resolve(filepath);
let resolved = (windows ?? isWindows) ? path.win32.resolve(filepath) : path.posix.resolve(filepath);
// path.resolve strips trailing slashes so we must add them back
const filePathLast = StringPrototypeCharCodeAt(filepath,
filepath.length - 1);
if ((filePathLast === CHAR_FORWARD_SLASH ||
(isWindows && filePathLast === CHAR_BACKWARD_SLASH)) &&
((windows ?? isWindows) && filePathLast === CHAR_BACKWARD_SLASH)) &&
resolved[resolved.length - 1] !== path.sep)
resolved += '/';

// Call encodePathChars first to avoid encoding % again for ? and #.
resolved = encodePathChars(resolved);
resolved = encodePathChars(resolved, { windows });

// Question and hash character should be included in pathname.
// Therefore, encoding is required to eliminate parsing them in different states.
Expand Down
212 changes: 111 additions & 101 deletions test/parallel/test-url-fileurltopath.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,106 +49,116 @@ assert.throws(() => url.fileURLToPath('https://a/b/c'), {
}
}

{
let testCases;
if (isWindows) {
testCases = [
// Lowercase ascii alpha
{ path: 'C:\\foo', fileURL: 'file:///C:/foo' },
// Uppercase ascii alpha
{ path: 'C:\\FOO', fileURL: 'file:///C:/FOO' },
// dir
{ path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' },
// trailing separator
{ path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' },
// dot
{ path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' },
// space
{ path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' },
// question mark
{ path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' },
// number sign
{ path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' },
// ampersand
{ path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' },
// equals
{ path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' },
// colon
{ path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' },
// semicolon
{ path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' },
// percent
{ path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' },
// backslash
{ path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' },
// backspace
{ path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' },
// tab
{ path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' },
// newline
{ path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' },
// carriage return
{ path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' },
// latin1
{ path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' },
// UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
{ path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' },
];
} else {
testCases = [
// Lowercase ascii alpha
{ path: '/foo', fileURL: 'file:///foo' },
// Uppercase ascii alpha
{ path: '/FOO', fileURL: 'file:///FOO' },
// dir
{ path: '/dir/foo', fileURL: 'file:///dir/foo' },
// trailing separator
{ path: '/dir/', fileURL: 'file:///dir/' },
// dot
{ path: '/foo.mjs', fileURL: 'file:///foo.mjs' },
// space
{ path: '/foo bar', fileURL: 'file:///foo%20bar' },
// question mark
{ path: '/foo?bar', fileURL: 'file:///foo%3Fbar' },
// number sign
{ path: '/foo#bar', fileURL: 'file:///foo%23bar' },
// ampersand
{ path: '/foo&bar', fileURL: 'file:///foo&bar' },
// equals
{ path: '/foo=bar', fileURL: 'file:///foo=bar' },
// colon
{ path: '/foo:bar', fileURL: 'file:///foo:bar' },
// semicolon
{ path: '/foo;bar', fileURL: 'file:///foo;bar' },
// percent
{ path: '/foo%bar', fileURL: 'file:///foo%25bar' },
// backslash
{ path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' },
// backspace
{ path: '/foo\bbar', fileURL: 'file:///foo%08bar' },
// tab
{ path: '/foo\tbar', fileURL: 'file:///foo%09bar' },
// newline
{ path: '/foo\nbar', fileURL: 'file:///foo%0Abar' },
// carriage return
{ path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' },
// latin1
{ path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: '/€', fileURL: 'file:///%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' },
];
}
const windowsTestCases = [
// Lowercase ascii alpha
{ path: 'C:\\foo', fileURL: 'file:///C:/foo' },
// Uppercase ascii alpha
{ path: 'C:\\FOO', fileURL: 'file:///C:/FOO' },
// dir
{ path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' },
// trailing separator
{ path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' },
// dot
{ path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' },
// space
{ path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' },
// question mark
{ path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' },
// number sign
{ path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' },
// ampersand
{ path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' },
// equals
{ path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' },
// colon
{ path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' },
// semicolon
{ path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' },
// percent
{ path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' },
// backslash
{ path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' },
// backspace
{ path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' },
// tab
{ path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' },
// newline
{ path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' },
// carriage return
{ path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' },
// latin1
{ path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' },
// UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
{ path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' },
];
const posixTestCases = [
// Lowercase ascii alpha
{ path: '/foo', fileURL: 'file:///foo' },
// Uppercase ascii alpha
{ path: '/FOO', fileURL: 'file:///FOO' },
// dir
{ path: '/dir/foo', fileURL: 'file:///dir/foo' },
// trailing separator
{ path: '/dir/', fileURL: 'file:///dir/' },
// dot
{ path: '/foo.mjs', fileURL: 'file:///foo.mjs' },
// space
{ path: '/foo bar', fileURL: 'file:///foo%20bar' },
// question mark
{ path: '/foo?bar', fileURL: 'file:///foo%3Fbar' },
// number sign
{ path: '/foo#bar', fileURL: 'file:///foo%23bar' },
// ampersand
{ path: '/foo&bar', fileURL: 'file:///foo&bar' },
// equals
{ path: '/foo=bar', fileURL: 'file:///foo=bar' },
// colon
{ path: '/foo:bar', fileURL: 'file:///foo:bar' },
// semicolon
{ path: '/foo;bar', fileURL: 'file:///foo;bar' },
// percent
{ path: '/foo%bar', fileURL: 'file:///foo%25bar' },
// backslash
{ path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' },
// backspace
{ path: '/foo\bbar', fileURL: 'file:///foo%08bar' },
// tab
{ path: '/foo\tbar', fileURL: 'file:///foo%09bar' },
// newline
{ path: '/foo\nbar', fileURL: 'file:///foo%0Abar' },
// carriage return
{ path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' },
// latin1
{ path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: '/€', fileURL: 'file:///%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' },
];

for (const { path, fileURL } of testCases) {
const fromString = url.fileURLToPath(fileURL);
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL));
assert.strictEqual(fromURL, path);
}
for (const { path, fileURL } of windowsTestCases) {
const fromString = url.fileURLToPath(fileURL, { windows: true });
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL));
assert.strictEqual(fromURL, path);

Check failure on line 147 in test/parallel/test-url-fileurltopath.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stderr --- node:assert:126 throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + actual - expected + '/C:/foo' - 'C:\\foo' at Object.<anonymous> (/Users/runner/work/node/node/test/parallel/test-url-fileurltopath.js:147:10) at Module._compile (node:internal/modules/cjs/loader:1476:14) at Module._extensions..js (node:internal/modules/cjs/loader:1555:10) at Module.load (node:internal/modules/cjs/loader:1288:32) at Module._load (node:internal/modules/cjs/loader:1104:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:191:14) at node:internal/main/run_main_module:28:49 { generatedMessage: true, code: 'ERR_ASSERTION', actual: '/C:/foo', expected: 'C:\\foo', operator: 'strictEqual' } Node.js v22.0.0-pre Command: out/Release/node --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./tools/github_reporter/index.js --test-reporter-destination=stdout /Users/runner/work/node/node/test/parallel/test-url-fileurltopath.js

Check failure on line 147 in test/parallel/test-url-fileurltopath.js

View workflow job for this annotation

GitHub Actions / test-linux

--- stderr --- node:assert:126 throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + actual - expected + '/C:/foo' - 'C:\\foo' at Object.<anonymous> (/home/runner/work/node/node/test/parallel/test-url-fileurltopath.js:147:10) at Module._compile (node:internal/modules/cjs/loader:1476:14) at Module._extensions..js (node:internal/modules/cjs/loader:1555:10) at Module.load (node:internal/modules/cjs/loader:1288:32) at Module._load (node:internal/modules/cjs/loader:1104:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:191:14) at node:internal/main/run_main_module:28:49 { generatedMessage: true, code: 'ERR_ASSERTION', actual: '/C:/foo', expected: 'C:\\foo', operator: 'strictEqual' } Node.js v22.0.0-pre Command: out/Release/node --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./tools/github_reporter/index.js --test-reporter-destination=stdout /home/runner/work/node/node/test/parallel/test-url-fileurltopath.js

Check failure on line 147 in test/parallel/test-url-fileurltopath.js

View workflow job for this annotation

GitHub Actions / test-asan

--- stderr --- node:assert:126 throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + actual - expected + '/C:/foo' - 'C:\\foo' at Object.<anonymous> (/home/runner/work/node/node/test/parallel/test-url-fileurltopath.js:147:10) at Module._compile (node:internal/modules/cjs/loader:1476:14) at Module._extensions..js (node:internal/modules/cjs/loader:1555:10) at Module.load (node:internal/modules/cjs/loader:1288:32) at Module._load (node:internal/modules/cjs/loader:1104:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:191:14) at node:internal/main/run_main_module:28:49 { generatedMessage: true, code: 'ERR_ASSERTION', actual: '/C:/foo', expected: 'C:\\foo', operator: 'strictEqual' } Node.js v22.0.0-pre Command: out/Release/node --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./tools/github_reporter/index.js --test-reporter-destination=stdout /home/runner/work/node/node/test/parallel/test-url-fileurltopath.js
}

for (const { path, fileURL } of posixTestCases) {
const fromString = url.fileURLToPath(fileURL, { windows: false });
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL));
assert.strictEqual(fromURL, path);
}

const defaultTestCases = isWindows ? windowsTestCases : posixTestCases;

for (const { path, fileURL } of defaultTestCases) {
const fromString = url.fileURLToPath(fileURL);
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL));
assert.strictEqual(fromURL, path);
}
Loading

0 comments on commit 35ba48e

Please sign in to comment.