summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/internal/url.js23
-rw-r--r--test/parallel/test-url-pathtofileurl.js100
2 files changed, 120 insertions, 3 deletions
diff --git a/lib/internal/url.js b/lib/internal/url.js
index 440fc0315e..ae1adad8c1 100644
--- a/lib/internal/url.js
+++ b/lib/internal/url.js
@@ -1339,11 +1339,22 @@ function fileURLToPath(path) {
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}
-// We percent-encode % character when converting from file path to URL,
-// as this is the only character that won't be percent encoded by
-// default URL percent encoding when pathname is set.
+// The following characters are percent-encoded when converting from file path
+// to URL:
+// - %: The percent character is the only character not encoded by the
+// `pathname` setter.
+// - \: Backslash is encoded on non-windows platforms since it's a valid
+// character but the `pathname` setters replaces it by a forward slash.
+// - LF: The newline character is stripped out by the `pathname` setter.
+// (See whatwg/url#419)
+// - CR: The carriage return character is also stripped out by the `pathname`
+// setter.
+// - TAB: The tab character is also stripped out by the `pathname` setter.
const percentRegEx = /%/g;
const backslashRegEx = /\\/g;
+const newlineRegEx = /\n/g;
+const carriageReturnRegEx = /\r/g;
+const tabRegEx = /\t/g;
function pathToFileURL(filepath) {
let resolved = path.resolve(filepath);
// path.resolve strips trailing slashes so we must add them back
@@ -1358,6 +1369,12 @@ function pathToFileURL(filepath) {
// in posix, "/" is a valid character in paths
if (!isWindows && resolved.includes('\\'))
resolved = resolved.replace(backslashRegEx, '%5C');
+ if (resolved.includes('\n'))
+ resolved = resolved.replace(newlineRegEx, '%0A');
+ if (resolved.includes('\r'))
+ resolved = resolved.replace(carriageReturnRegEx, '%0D');
+ if (resolved.includes('\t'))
+ resolved = resolved.replace(tabRegEx, '%09');
outURL.pathname = resolved;
return outURL;
}
diff --git a/test/parallel/test-url-pathtofileurl.js b/test/parallel/test-url-pathtofileurl.js
index ad8203cd7b..6cdfa5dcd3 100644
--- a/test/parallel/test-url-pathtofileurl.js
+++ b/test/parallel/test-url-pathtofileurl.js
@@ -22,3 +22,103 @@ const url = require('url');
const fileURL = url.pathToFileURL('test/%').href;
assert.ok(fileURL.includes('%25'));
}
+
+{
+ 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' }
+ ];
+ } 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' },
+ ];
+ }
+
+ for (const { path, expected } of testCases) {
+ const actual = url.pathToFileURL(path).href;
+ assert.strictEqual(actual, expected);
+ }
+}