diff --git a/lib/internal/url.js b/lib/internal/url.js index 8b1cbe457af7a2..73168f40e13c64 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -1422,25 +1422,27 @@ const backslashRegEx = /\\/g; const newlineRegEx = /\n/g; const carriageReturnRegEx = /\r/g; const tabRegEx = /\t/g; +const questionRegex = /\?/g; +const hashRegex = /#/g; function encodePathChars(filepath) { - if (StringPrototypeIncludes(filepath, '%')) + if (StringPrototypeIndexOf(filepath, '%') !== -1) filepath = RegExpPrototypeSymbolReplace(percentRegEx, filepath, '%25'); // In posix, backslash is a valid character in paths: - if (!isWindows && StringPrototypeIncludes(filepath, '\\')) + if (!isWindows && StringPrototypeIndexOf(filepath, '\\') !== -1) filepath = RegExpPrototypeSymbolReplace(backslashRegEx, filepath, '%5C'); - if (StringPrototypeIncludes(filepath, '\n')) + if (StringPrototypeIndexOf(filepath, '\n') !== -1) filepath = RegExpPrototypeSymbolReplace(newlineRegEx, filepath, '%0A'); - if (StringPrototypeIncludes(filepath, '\r')) + if (StringPrototypeIndexOf(filepath, '\r') !== -1) filepath = RegExpPrototypeSymbolReplace(carriageReturnRegEx, filepath, '%0D'); - if (StringPrototypeIncludes(filepath, '\t')) + if (StringPrototypeIndexOf(filepath, '\t') !== -1) filepath = RegExpPrototypeSymbolReplace(tabRegEx, filepath, '%09'); return filepath; } function pathToFileURL(filepath) { - const outURL = new URL('file://'); if (isWindows && StringPrototypeStartsWith(filepath, '\\\\')) { + const outURL = new URL('file://'); // UNC path format: \\server\share\resource const hostnameEndIndex = StringPrototypeIndexOf(filepath, '\\', 2); if (hostnameEndIndex === -1) { @@ -1461,18 +1463,29 @@ function pathToFileURL(filepath) { outURL.hostname = domainToASCII(hostname); outURL.pathname = encodePathChars( RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/')); - } else { - let resolved = path.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)) && - resolved[resolved.length - 1] !== path.sep) - resolved += '/'; - outURL.pathname = encodePathChars(resolved); - } - return outURL; + return outURL; + } + let resolved = path.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)) && + resolved[resolved.length - 1] !== path.sep) + resolved += '/'; + + // Call encodePathChars first to avoid encoding % again for ? and #. + resolved = encodePathChars(resolved); + + // Question and hash character should be included in pathname. + // Therefore, encoding is required to eliminate parsing them in different states. + // This is done as an optimization to not creating a URL instance and + // later triggering pathname setter, which impacts performance + if (StringPrototypeIndexOf(resolved, '?') !== -1) + resolved = RegExpPrototypeSymbolReplace(questionRegex, resolved, '%3F'); + if (StringPrototypeIndexOf(resolved, '#') !== -1) + resolved = RegExpPrototypeSymbolReplace(hashRegex, resolved, '%23'); + return new URL(`file://${resolved}`); } function toPathIfFileURL(fileURLOrPath) {