lib, url: add a windows option to path parsing

PR-URL: https://github.com/nodejs/node/pull/52509
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Zeyu "Alex" Yang <himself65@outlook.com>
Reviewed-By: LiviaMedeiros <livia@cirno.name>
This commit is contained in:
Aviv Keller 2024-04-24 11:47:48 -04:00 committed by GitHub
parent 91dc8c93a3
commit ac9aa37bcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 267 additions and 216 deletions

View File

@ -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
@ -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

View File

@ -1151,13 +1151,23 @@ 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
return as a windows filepath, `false` for posix, and
`undefined` for the system default.
**Default:** `undefined`.
* Returns: {string} The fully-resolved platform-specific Node.js file path.
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 }));
// 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 return the `path` value.
-->
* `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` for posix, and
`undefined` for the system default.
**Default:** `undefined`.
* Returns: {URL} The file URL object.
This function ensures that `path` is resolved absolutely, and that the URL

View File

@ -44,6 +44,7 @@ const {
getConstructorOf,
removeColors,
kEnumerableProperty,
kEmptyObject,
SideEffectFreeRegExpPrototypeSymbolReplace,
} = require('internal/util');
@ -1480,14 +1481,15 @@ function getPathFromURLPosix(url) {
return decodeURIComponent(pathname);
}
function fileURLToPath(path) {
function fileURLToPath(path, options = kEmptyObject) {
const windows = options?.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
@ -1509,11 +1511,12 @@ const tabRegEx = /\t/g;
const questionRegex = /\?/g;
const hashRegex = /#/g;
function encodePathChars(filepath) {
function encodePathChars(filepath, options = kEmptyObject) {
const windows = options?.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');
@ -1524,8 +1527,9 @@ function encodePathChars(filepath) {
return filepath;
}
function pathToFileURL(filepath) {
if (isWindows && StringPrototypeStartsWith(filepath, '\\\\')) {
function pathToFileURL(filepath, options = kEmptyObject) {
const windows = options?.windows;
if ((windows ?? isWindows) && StringPrototypeStartsWith(filepath, '\\\\')) {
const outURL = new URL('file://');
// UNC path format: \\server\share\resource
const hostnameEndIndex = StringPrototypeIndexOf(filepath, '\\', 2);
@ -1546,20 +1550,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.

View File

@ -1020,10 +1020,10 @@ Url.prototype.parseHost = function parseHost() {
// When used internally, we are not obligated to associate TypeError with
// this function, so non-strings can be rejected by underlying implementation.
// Public API has to validate input and throw appropriate error.
function pathToFileURL(path) {
function pathToFileURL(path, options) {
validateString(path, 'path');
return _pathToFileURL(path);
return _pathToFileURL(path, options);
}
module.exports = {

View File

@ -49,106 +49,120 @@ 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), { windows: true });
assert.strictEqual(fromURL, path);
}
for (const { path, fileURL } of posixTestCases) {
const fromString = url.fileURLToPath(fileURL, { windows: false });
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL), { windows: false });
assert.strictEqual(fromURL, path);
}
const defaultTestCases = isWindows ? windowsTestCases : posixTestCases;
// Test when `options` is null
const whenNullActual = url.fileURLToPath(new URL(defaultTestCases[0].fileURL), null);
assert.strictEqual(whenNullActual, defaultTestCases[0].path);
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);
}

View File

@ -1,4 +1,5 @@
'use strict';
const { isWindows } = require('../common');
const assert = require('assert');
const url = require('url');
@ -60,106 +61,116 @@ const url = require('url');
}
}
{
let testCases;
if (isWindows) {
testCases = [
// Lowercase ascii alpha
{ path: 'C:\\foo', expected: 'file:///C:/foo' },
// Uppercase ascii alpha
{ path: 'C:\\FOO', expected: 'file:///C:/FOO' },
// dir
{ path: 'C:\\dir\\foo', expected: 'file:///C:/dir/foo' },
// trailing separator
{ path: 'C:\\dir\\', expected: 'file:///C:/dir/' },
// dot
{ path: 'C:\\foo.mjs', expected: 'file:///C:/foo.mjs' },
// space
{ path: 'C:\\foo bar', expected: 'file:///C:/foo%20bar' },
// question mark
{ path: 'C:\\foo?bar', expected: 'file:///C:/foo%3Fbar' },
// number sign
{ path: 'C:\\foo#bar', expected: 'file:///C:/foo%23bar' },
// ampersand
{ path: 'C:\\foo&bar', expected: 'file:///C:/foo&bar' },
// equals
{ path: 'C:\\foo=bar', expected: 'file:///C:/foo=bar' },
// colon
{ path: 'C:\\foo:bar', expected: 'file:///C:/foo:bar' },
// semicolon
{ path: 'C:\\foo;bar', expected: 'file:///C:/foo;bar' },
// percent
{ path: 'C:\\foo%bar', expected: 'file:///C:/foo%25bar' },
// backslash
{ path: 'C:\\foo\\bar', expected: 'file:///C:/foo/bar' },
// backspace
{ path: 'C:\\foo\bbar', expected: 'file:///C:/foo%08bar' },
// tab
{ path: 'C:\\foo\tbar', expected: 'file:///C:/foo%09bar' },
// newline
{ path: 'C:\\foo\nbar', expected: 'file:///C:/foo%0Abar' },
// carriage return
{ path: 'C:\\foo\rbar', expected: 'file:///C:/foo%0Dbar' },
// latin1
{ path: 'C:\\fóóbàr', expected: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: 'C:\\€', expected: 'file:///C:/%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: 'C:\\🚀', expected: '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', expected: 'file://nas/My%20Docs/File.doc' },
];
} else {
testCases = [
// Lowercase ascii alpha
{ path: '/foo', expected: 'file:///foo' },
// Uppercase ascii alpha
{ path: '/FOO', expected: 'file:///FOO' },
// dir
{ path: '/dir/foo', expected: 'file:///dir/foo' },
// trailing separator
{ path: '/dir/', expected: 'file:///dir/' },
// dot
{ path: '/foo.mjs', expected: 'file:///foo.mjs' },
// space
{ path: '/foo bar', expected: 'file:///foo%20bar' },
// question mark
{ path: '/foo?bar', expected: 'file:///foo%3Fbar' },
// number sign
{ path: '/foo#bar', expected: 'file:///foo%23bar' },
// ampersand
{ path: '/foo&bar', expected: 'file:///foo&bar' },
// equals
{ path: '/foo=bar', expected: 'file:///foo=bar' },
// colon
{ path: '/foo:bar', expected: 'file:///foo:bar' },
// semicolon
{ path: '/foo;bar', expected: 'file:///foo;bar' },
// percent
{ path: '/foo%bar', expected: 'file:///foo%25bar' },
// backslash
{ path: '/foo\\bar', expected: 'file:///foo%5Cbar' },
// backspace
{ path: '/foo\bbar', expected: 'file:///foo%08bar' },
// tab
{ path: '/foo\tbar', expected: 'file:///foo%09bar' },
// newline
{ path: '/foo\nbar', expected: 'file:///foo%0Abar' },
// carriage return
{ path: '/foo\rbar', expected: 'file:///foo%0Dbar' },
// latin1
{ path: '/fóóbàr', expected: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: '/€', expected: 'file:///%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: '/🚀', expected: 'file:///%F0%9F%9A%80' },
];
}
const windowsTestCases = [
// Lowercase ascii alpha
{ path: 'C:\\foo', expected: 'file:///C:/foo' },
// Uppercase ascii alpha
{ path: 'C:\\FOO', expected: 'file:///C:/FOO' },
// dir
{ path: 'C:\\dir\\foo', expected: 'file:///C:/dir/foo' },
// trailing separator
{ path: 'C:\\dir\\', expected: 'file:///C:/dir/' },
// dot
{ path: 'C:\\foo.mjs', expected: 'file:///C:/foo.mjs' },
// space
{ path: 'C:\\foo bar', expected: 'file:///C:/foo%20bar' },
// question mark
{ path: 'C:\\foo?bar', expected: 'file:///C:/foo%3Fbar' },
// number sign
{ path: 'C:\\foo#bar', expected: 'file:///C:/foo%23bar' },
// ampersand
{ path: 'C:\\foo&bar', expected: 'file:///C:/foo&bar' },
// equals
{ path: 'C:\\foo=bar', expected: 'file:///C:/foo=bar' },
// colon
{ path: 'C:\\foo:bar', expected: 'file:///C:/foo:bar' },
// semicolon
{ path: 'C:\\foo;bar', expected: 'file:///C:/foo;bar' },
// percent
{ path: 'C:\\foo%bar', expected: 'file:///C:/foo%25bar' },
// backslash
{ path: 'C:\\foo\\bar', expected: 'file:///C:/foo/bar' },
// backspace
{ path: 'C:\\foo\bbar', expected: 'file:///C:/foo%08bar' },
// tab
{ path: 'C:\\foo\tbar', expected: 'file:///C:/foo%09bar' },
// newline
{ path: 'C:\\foo\nbar', expected: 'file:///C:/foo%0Abar' },
// carriage return
{ path: 'C:\\foo\rbar', expected: 'file:///C:/foo%0Dbar' },
// latin1
{ path: 'C:\\fóóbàr', expected: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: 'C:\\€', expected: 'file:///C:/%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: 'C:\\🚀', expected: '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', expected: 'file://nas/My%20Docs/File.doc' },
];
const posixTestCases = [
// Lowercase ascii alpha
{ path: '/foo', expected: 'file:///foo' },
// Uppercase ascii alpha
{ path: '/FOO', expected: 'file:///FOO' },
// dir
{ path: '/dir/foo', expected: 'file:///dir/foo' },
// trailing separator
{ path: '/dir/', expected: 'file:///dir/' },
// dot
{ path: '/foo.mjs', expected: 'file:///foo.mjs' },
// space
{ path: '/foo bar', expected: 'file:///foo%20bar' },
// question mark
{ path: '/foo?bar', expected: 'file:///foo%3Fbar' },
// number sign
{ path: '/foo#bar', expected: 'file:///foo%23bar' },
// ampersand
{ path: '/foo&bar', expected: 'file:///foo&bar' },
// equals
{ path: '/foo=bar', expected: 'file:///foo=bar' },
// colon
{ path: '/foo:bar', expected: 'file:///foo:bar' },
// semicolon
{ path: '/foo;bar', expected: 'file:///foo;bar' },
// percent
{ path: '/foo%bar', expected: 'file:///foo%25bar' },
// backslash
{ path: '/foo\\bar', expected: 'file:///foo%5Cbar' },
// backspace
{ path: '/foo\bbar', expected: 'file:///foo%08bar' },
// tab
{ path: '/foo\tbar', expected: 'file:///foo%09bar' },
// newline
{ path: '/foo\nbar', expected: 'file:///foo%0Abar' },
// carriage return
{ path: '/foo\rbar', expected: 'file:///foo%0Dbar' },
// latin1
{ path: '/fóóbàr', expected: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: '/€', expected: 'file:///%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: '/🚀', expected: 'file:///%F0%9F%9A%80' },
];
for (const { path, expected } of testCases) {
const actual = url.pathToFileURL(path).href;
assert.strictEqual(actual, expected);
}
for (const { path, expected } of windowsTestCases) {
const actual = url.pathToFileURL(path, { windows: true }).href;
assert.strictEqual(actual, expected);
}
for (const { path, expected } of posixTestCases) {
const actual = url.pathToFileURL(path, { windows: false }).href;
assert.strictEqual(actual, expected);
}
const testCases = isWindows ? windowsTestCases : posixTestCases;
// Test when `options` is null
const whenNullActual = url.pathToFileURL(testCases[0].path, null);
assert.strictEqual(whenNullActual.href, testCases[0].expected);
for (const { path, expected } of testCases) {
const actual = url.pathToFileURL(path).href;
assert.strictEqual(actual, expected);
}
// Test for non-string parameter