diff --git a/lib/internal/url.js b/lib/internal/url.js index 302d3fa08753a5..b59a1ff4dc21f2 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -1414,6 +1414,8 @@ const backslashRegEx = /\\/g; const newlineRegEx = /\n/g; const carriageReturnRegEx = /\r/g; const tabRegEx = /\t/g; +const questionRegex = /\?/; +const hashRegex = /#/; function encodePathChars(filepath) { if (StringPrototypeIncludes(filepath, '%')) @@ -1431,8 +1433,8 @@ function encodePathChars(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) { @@ -1453,18 +1455,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 (StringPrototypeIncludes(resolved, '?')) + resolved = RegExpPrototypeSymbolReplace(questionRegex, resolved, '%3F'); + if (StringPrototypeIncludes(resolved, '#')) + resolved = RegExpPrototypeSymbolReplace(hashRegex, resolved, '%23'); + return new URL(`file://${resolved}`); } function toPathIfFileURL(fileURLOrPath) {