From db4bb685f18470dbf1f02d2640f9125f3c214929 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 7 Dec 2021 21:07:36 +0100 Subject: [PATCH] Add fileURLToPath --- README.md | 7 ++++++ test.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ url.js | 35 +++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/README.md b/README.md index 8b35460..eb9ae07 100644 --- a/README.md +++ b/README.md @@ -106,3 +106,10 @@ an anchor tag. Examples: url.resolve('/one/two/three', 'four') // '/one/two/four' url.resolve('http://example.com/', '/one') // 'http://example.com/one' url.resolve('http://example.com/one', '/two') // 'http://example.com/two' + +### url.fileURLToPath(url) + +Take a string or WHATWG `URL` object representing a filepath, and return the POSIX +filepath as a string. + + url.fileURLToPath('file:///etc/hosts') // '/etc/hosts' diff --git a/test.js b/test.js index f90e745..778c657 100644 --- a/test.js +++ b/test.js @@ -2055,3 +2055,74 @@ relativeTests2.forEach(function (relativeTest) { assert.equal(actual, expected, 'format(' + relativeTest[1] + ') == ' + expected + '\nactual:' + actual); }); }); + +var fileURLToPathTestCases = [ + // 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' } +]; + +fileURLToPathTestCases.forEach(function (fileURLToPathTestCase) { + test('fileURLToPath(' + fileURLToPathTestCase.fileURL + ')', function () { + var fromString = url.fileURLToPath(fileURLToPathTestCase.fileURL); + assert.strictEqual(fromString, fileURLToPathTestCase.path); + var fromURL = url.fileURLToPath(new URL(fileURLToPathTestCase.fileURL)); + assert.strictEqual(fromURL, fileURLToPathTestCase.path); + }); +}); + +[ + 'https://host/y', + 'file://host/a', + new URL('https://host/y'), + 'file:///a%2F/', + '', + null, + undefined, + 1, + {}, + true +].forEach(function (val) { + test('fileURLToPath(' + val + ')', function () { + assert['throws'](function () { url.fileURLToPath(val); }, TypeError); + }); +}); diff --git a/url.js b/url.js index 79cb0a6..54757a7 100644 --- a/url.js +++ b/url.js @@ -771,4 +771,39 @@ exports.resolve = urlResolve; exports.resolveObject = urlResolveObject; exports.format = urlFormat; +function isURLInstance(fileURLOrPath) { + return fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin; +} + +function getPathFromURLPosix(url) { + if (url.hostname !== '') { + throw new TypeError('File URL host must be "localhost" or empty on darwin'); + } + var pathname = url.pathname; + for (var n = 0; n < pathname.length; n++) { + if (pathname[n] === '%') { + var third = pathname.codePointAt(n + 2) | 0x20; + if (pathname[n + 1] === '2' && third === 102) { + throw new TypeError('File URL path must not include encoded / characters'); + } + } + } + return decodeURIComponent(pathname); +} + +function fileURLToPath(path) { + if (typeof path === 'string') { + path = new URL(path); + } else if (!isURLInstance(path)) { + throw new TypeError('The "path" argument must be of type string or an instance of URL. Received ' + path); + } + if (path.protocol !== 'file:') { + throw new TypeError('The URL must be of scheme file'); + } + return getPathFromURLPosix(path); +} + +exports.fileURLToPath = fileURLToPath; + exports.Url = Url; +exports.URL = URL;