From f21d293bec3def081cf16e137db7b53382bfb9ad Mon Sep 17 00:00:00 2001 From: Thomas Hebb Date: Fri, 4 Apr 2025 01:20:55 -0400 Subject: [PATCH] Fix: Allow absolute URLs as paths, e.g. for load() When using protobuf.js in a browser, it's useful to be able to load proto files from other origins. But currently, path.normalize() mangles URLs by removing the second slash after the scheme (e.g. "https://example.com/" becomes "https:/example.com"). Fix that issue, and better integrate the existing UNC support, by introducing a new path.absolutePrefix() function that checks for all the various prefixes that indicate a path is absolute and returns the one it finds (if any). By including both slashes in the return value for URLs, we exclude them from the normalization logic. Note that this almost certainly still isn't perfect, as URLs, UNC paths, and even Windows drive letters are all quite complex. But I don't have the time or project context to make a major change here. --- index.d.ts | 7 ++++++ lib/path/README.md | 3 +++ lib/path/index.d.ts | 7 ++++++ lib/path/index.js | 47 ++++++++++++++++++++++++++++++----------- lib/path/tests/index.js | 17 +++++++++++++++ 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/index.d.ts b/index.d.ts index 81ca086ae..c168da9b5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2423,6 +2423,13 @@ export namespace util { /** A minimal path module to resolve Unix, Windows and URL paths alike. */ namespace path { + /** + * Gets the prefix of an absolute UNC path, Unix path, or URL that makes it absolute, if present. + * @param path Path to test + * @returns the prefix, or null if path is relative + */ + function absolutePrefix(path: string): (string|null); + /** * Tests if the specified path is absolute. * @param path Path to test diff --git a/lib/path/README.md b/lib/path/README.md index 1c1a2ba2f..e02de1ae1 100644 --- a/lib/path/README.md +++ b/lib/path/README.md @@ -16,4 +16,7 @@ API * **path.resolve(originPath: `string`, includePath: `string`, [alreadyNormalized=false: `boolean`]): `string`**
Resolves the specified include path against the specified origin path. +* **path.absolutePrefix(path: `string`): `string|null`**
+ Gets the prefix of an absolute Windows, UNC, or Unix path or a URL that indicates it's absolute. + **License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause) diff --git a/lib/path/index.d.ts b/lib/path/index.d.ts index b664d81f6..d16a4a66b 100644 --- a/lib/path/index.d.ts +++ b/lib/path/index.d.ts @@ -1,3 +1,10 @@ +/** + * Gets the prefix of an absolute Windows, UNC, or Unix path or a URL that indicates it's absolute. + * @param {string} path Path to test + * @returns {string|null} the prefix, or null if path is relative + */ +export function absolutePrefix(path: string): (string|null); + /** * Tests if the specified path is absolute. * @param {string} path Path to test diff --git a/lib/path/index.js b/lib/path/index.js index 8c7ee12fb..e957a5d8c 100644 --- a/lib/path/index.js +++ b/lib/path/index.js @@ -7,6 +7,31 @@ */ var path = exports; +var absolutePrefix = +/** + * Gets the prefix of an absolute Windows, UNC, or Unix path or a URL that indicates it's absolute. + * @param {string} path Path to test + * @returns {string|null} the prefix, or null if path is relative + */ +path.absolutePrefix = function absolutePrefix(path) { + var match; + if (path.startsWith("\\\\")) { + // UNC path + return "\\\\"; + } else if ((match = /^\w*:\/\//.exec(path)) !== null) { + // URL + return match[0]; + } else if ((match = /^\w:/.exec(path)) !== null) { + // Windows path + return match[0]; + } else if (path.startsWith("/")) { + // Unix path + return "/"; + } else { + return null; + } +} + var isAbsolute = /** * Tests if the specified path is absolute. @@ -14,7 +39,7 @@ var isAbsolute = * @returns {boolean} `true` if path is absolute */ path.isAbsolute = function isAbsolute(path) { - return /^(?:\/|\w+:|\\\\\w+)/.test(path); + return absolutePrefix(path) !== null; }; var normalize = @@ -24,20 +49,18 @@ var normalize = * @returns {string} Normalized path */ path.normalize = function normalize(path) { - var firstTwoCharacters = path.substring(0,2); - var uncPrefix = ""; - if (firstTwoCharacters === "\\\\") { - uncPrefix = firstTwoCharacters; - path = path.substring(2); + var absolute = false; + var prefix = absolutePrefix(path); + if (prefix !== null) { + absolute = true; + path = path.substring(prefix.length); + } else { + prefix = ""; } path = path.replace(/\\/g, "/") .replace(/\/{2,}/g, "/"); - var parts = path.split("/"), - absolute = isAbsolute(path), - prefix = ""; - if (absolute) - prefix = parts.shift() + "/"; + var parts = path.split("/"); for (var i = 0; i < parts.length;) { if (parts[i] === "..") { if (i > 0 && parts[i - 1] !== "..") @@ -51,7 +74,7 @@ path.normalize = function normalize(path) { else ++i; } - return uncPrefix + prefix + parts.join("/"); + return prefix + parts.join("/"); }; /** diff --git a/lib/path/tests/index.js b/lib/path/tests/index.js index 6b6155667..96a9811cc 100644 --- a/lib/path/tests/index.js +++ b/lib/path/tests/index.js @@ -12,6 +12,9 @@ tape.test("path", function(test) { test.ok(path.isAbsolute("\\\\some-unc\\path\\file.js"), "should identify windows unc paths"); + test.ok(path.isAbsolute("https://example.com/path/file.js"), "should identify absolute URLs"); + test.ok(path.isAbsolute("://example.com/path/file.js"), "should identify protocol-relative URLs"); + var paths = [ { actual: "X:\\some\\..\\.\\path\\\\file.js", @@ -61,6 +64,20 @@ tape.test("path", function(test) { origin: "\\\\some-unc\\path\\..\\origin.js", expected: "\\\\some-unc/file.js" } + }, { + actual: "https://example.com/path/../file.js", + normal: "https://example.com/file.js", + resolve: { + origin: "/some/local/path", + expected: "https://example.com/file.js" + } + }, { + actual: "path/file2.js", + normal: "path/file2.js", + resolve: { + origin: "https://example.com/file1.js", + expected: "https://example.com/path/file2.js" + } } ];