From 8f15d01bff2af9a93c6f7a7586103be3628a7c47 Mon Sep 17 00:00:00 2001 From: Aviv Keller <38299977+RedYetiDev@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:57:34 -0400 Subject: [PATCH] lib, url: add a `windows` option to path parsing --- doc/api/esm.md | 4 +- doc/api/url.md | 24 ++- lib/internal/url.js | 26 +-- lib/url.js | 4 +- test/parallel/test-url-fileurltopath.js | 216 +++++++++++++----------- test/parallel/test-url-pathtofileurl.js | 209 ++++++++++++----------- 6 files changed, 267 insertions(+), 216 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index c8e8b677718d78..7a5c053f317d5b 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -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 diff --git a/doc/api/url.md b/doc/api/url.md index ac07caa0405e3b..5db978aaa11692 100644 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -1151,13 +1151,23 @@ console.log(url.domainToUnicode('xn--iñvalid.com')); // Prints an empty string ``` -### `url.fileURLToPath(url)` +### `url.fileURLToPath(url[, options])` * `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])` * `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 diff --git a/lib/internal/url.js b/lib/internal/url.js index 0e69ff52b5edef..4fe09195092e8a 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -44,6 +44,7 @@ const { getConstructorOf, removeColors, kEnumerableProperty, + kEmptyObject, SideEffectFreeRegExpPrototypeSymbolReplace, } = require('internal/util'); @@ -1460,14 +1461,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 @@ -1489,11 +1491,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'); @@ -1504,8 +1507,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); @@ -1526,20 +1530,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. diff --git a/lib/url.js b/lib/url.js index b6b5376c379a6c..6919bc0f905e29 100644 --- a/lib/url.js +++ b/lib/url.js @@ -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 = { diff --git a/test/parallel/test-url-fileurltopath.js b/test/parallel/test-url-fileurltopath.js index 75cf1479f2f876..6bd4e280483a19 100644 --- a/test/parallel/test-url-fileurltopath.js +++ b/test/parallel/test-url-fileurltopath.js @@ -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); } diff --git a/test/parallel/test-url-pathtofileurl.js b/test/parallel/test-url-pathtofileurl.js index d18b5a41fdfc2f..abbd6c95cb53dc 100644 --- a/test/parallel/test-url-pathtofileurl.js +++ b/test/parallel/test-url-pathtofileurl.js @@ -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