From f29ab4092f8387293ff9c46e0653891a15f6af8f Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Tue, 11 Aug 2020 18:12:47 -0700 Subject: [PATCH] lib: add UNC support to url.pathToFileURL() Fixes: https://github.com/nodejs/node/issues/34736 PR-URL: https://github.com/nodejs/node/pull/34743 Reviewed-By: Guy Bedford Reviewed-By: James M Snell Reviewed-By: Luigi Pinca Reviewed-By: Rich Trott --- lib/internal/url.js | 66 ++++++++++++++++++------- test/parallel/test-url-fileurltopath.js | 4 +- test/parallel/test-url-pathtofileurl.js | 24 ++++++++- 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/lib/internal/url.js b/lib/internal/url.js index e79f3d7ce4f2fc..16f5043df9c641 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -27,6 +27,7 @@ const { getConstructorOf, removeColors } = require('internal/util'); const { ERR_ARG_NOT_ITERABLE, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_INVALID_CALLBACK, ERR_INVALID_FILE_URL_HOST, ERR_INVALID_FILE_URL_PATH, @@ -1369,27 +1370,54 @@ const backslashRegEx = /\\/g; const newlineRegEx = /\n/g; const carriageReturnRegEx = /\r/g; const tabRegEx = /\t/g; + +function encodePathChars(filepath) { + if (filepath.includes('%')) + filepath = filepath.replace(percentRegEx, '%25'); + // In posix, backslash is a valid character in paths: + if (!isWindows && filepath.includes('\\')) + filepath = filepath.replace(backslashRegEx, '%5C'); + if (filepath.includes('\n')) + filepath = filepath.replace(newlineRegEx, '%0A'); + if (filepath.includes('\r')) + filepath = filepath.replace(carriageReturnRegEx, '%0D'); + if (filepath.includes('\t')) + filepath = filepath.replace(tabRegEx, '%09'); + return filepath; +} + function pathToFileURL(filepath) { - let resolved = path.resolve(filepath); - // path.resolve strips trailing slashes so we must add them back - const filePathLast = filepath.charCodeAt(filepath.length - 1); - if ((filePathLast === CHAR_FORWARD_SLASH || - (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && - resolved[resolved.length - 1] !== path.sep) - resolved += '/'; const outURL = new URL('file://'); - if (resolved.includes('%')) - resolved = resolved.replace(percentRegEx, '%25'); - // 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; + if (isWindows && filepath.startsWith('\\\\')) { + // UNC path format: \\server\share\resource + const paths = filepath.split('\\'); + if (paths.length <= 3) { + throw new ERR_INVALID_ARG_VALUE( + 'filepath', + filepath, + 'Missing UNC resource path' + ); + } + const hostname = paths[2]; + if (hostname.length === 0) { + throw new ERR_INVALID_ARG_VALUE( + 'filepath', + filepath, + 'Empty UNC servername' + ); + } + outURL.hostname = domainToASCII(hostname); + outURL.pathname = encodePathChars(paths.slice(3).join('/')); + } else { + let resolved = path.resolve(filepath); + // path.resolve strips trailing slashes so we must add them back + const filePathLast = filepath.charCodeAt(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; } diff --git a/test/parallel/test-url-fileurltopath.js b/test/parallel/test-url-fileurltopath.js index 74a217e8b68bde..75cf1479f2f876 100644 --- a/test/parallel/test-url-fileurltopath.js +++ b/test/parallel/test-url-fileurltopath.js @@ -94,7 +94,9 @@ assert.throws(() => url.fileURLToPath('https://a/b/c'), { // Euro sign (BMP code point) { path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' }, // Rocket emoji (non-BMP code point) - { path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' } + { path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' }, + // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows) + { path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' }, ]; } else { testCases = [ diff --git a/test/parallel/test-url-pathtofileurl.js b/test/parallel/test-url-pathtofileurl.js index 395447ab4cef0d..79adb58b77d754 100644 --- a/test/parallel/test-url-pathtofileurl.js +++ b/test/parallel/test-url-pathtofileurl.js @@ -23,6 +23,26 @@ const url = require('url'); assert.ok(fileURL.includes('%25')); } +{ + if (isWindows) { + // UNC path: \\server\share\resource + + // Missing server: + assert.throws(() => url.pathToFileURL('\\\\\\no-server'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + // Missing share or resource: + assert.throws(() => url.pathToFileURL('\\\\host'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + } else { + // UNC paths on posix are considered a single path that has backslashes: + const fileURL = url.pathToFileURL('\\\\nas\\share\\path.txt').href; + assert.match(fileURL, /file:\/\/.+%5C%5Cnas%5Cshare%5Cpath\.txt$/); + } +} + { let testCases; if (isWindows) { @@ -68,7 +88,9 @@ const url = require('url'); // 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' } + { path: 'C:\\🚀', expected: 'file:///C:/%F0%9F%9A%80' }, + // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows) + { path: '\\\\nas\\My Docs\\File.doc', expected: 'file://nas/My%20Docs/File.doc' } ]; } else { testCases = [