Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

url: support LF, CR and TAB in pathToFileURL #23720

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -1350,11 +1350,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
Expand All @@ -1369,6 +1380,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;
}
Expand Down
100 changes: 100 additions & 0 deletions test/parallel/test-url-pathtofileurl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}