From e9a79b82590d3841b5509be7c9604ad7957d8f81 Mon Sep 17 00:00:00 2001 From: Charles Samborski Date: Wed, 17 Oct 2018 22:05:37 +0200 Subject: [PATCH] url: support LF, CR and TAB in pathToFileURL Fixes: https://github.com/nodejs/node/issues/23696 PR-URL: https://github.com/nodejs/node/pull/23720 Reviewed-By: James M Snell Reviewed-By: Guy Bedford Reviewed-By: Tiancheng "Timothy" Gu Reviewed-By: Ruben Bridgewater --- lib/internal/url.js | 23 +++++- test/parallel/test-url-pathtofileurl.js | 100 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/lib/internal/url.js b/lib/internal/url.js index 440fc0315ed309..ae1adad8c1a214 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 ad8203cd7b1a87..6cdfa5dcd336ec 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); + } +}