From a8f74aa381c99e9c3c3d8fdfde02919966a3a824 Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Fri, 24 Jul 2020 02:37:11 +0100 Subject: [PATCH] fix: Improve URL compatibility (#6807) - Fix protocol regex. - Truncate repeated leading slashes in file paths. - Make drive letter support platform-independent. - Drop the hostname if a drive letter is parsed. - Fix drive letter normalization and basing. - Allow basing over the host. - Fix same-protocol basing. - Remove Windows UNC path support. - Reverts #6418. This is non-standard. Wouldn't be too much of a problem but it makes other parts of the spec hard to realize. --- cli/rt/11_url.js | 302 ++++++++++++++++++--------------- cli/tests/unit/url_test.ts | 189 ++++++++++----------- std/path/from_file_url_test.ts | 26 +-- std/path/posix.ts | 3 +- std/path/win32.ts | 2 +- 5 files changed, 265 insertions(+), 257 deletions(-) diff --git a/cli/rt/11_url.js b/cli/rt/11_url.js index 435d3454e7..202d35e814 100644 --- a/cli/rt/11_url.js +++ b/cli/rt/11_url.js @@ -1,7 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. ((window) => { - const { build } = window.__bootstrap.build; const { getRandomValues } = window.__bootstrap.crypto; const { customInspect } = window.__bootstrap.console; const { sendSync } = window.__bootstrap.dispatchJson; @@ -264,74 +263,118 @@ return [capture, rest]; } - function parse(url, isBase = true) { + function parse(url, baseParts = null) { const parts = {}; let restUrl; - [parts.protocol, restUrl] = takePattern(url.trim(), /^([a-z]+):/); - if (isBase && parts.protocol == "") { - return undefined; + let usedNonBase = false; + [parts.protocol, restUrl] = takePattern( + url.trim(), + /^([A-Za-z][+-.0-9A-Za-z]*):/, + ); + parts.protocol = parts.protocol.toLowerCase(); + if (parts.protocol == "") { + if (baseParts == null) { + return null; + } + parts.protocol = baseParts.protocol; + } else if ( + parts.protocol != baseParts?.protocol || + !specialSchemes.includes(parts.protocol) + ) { + usedNonBase = true; } const isSpecial = specialSchemes.includes(parts.protocol); if (parts.protocol == "file") { parts.slashes = "//"; parts.username = ""; parts.password = ""; - [parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/); - parts.port = ""; - if (build.os == "windows" && parts.hostname == "") { - // UNC paths. e.g. "\\\\localhost\\foo\\bar" on Windows should be - // representable as `new URL("file:////localhost/foo/bar")` which is - // equivalent to: `new URL("file://localhost/foo/bar")`. + if (usedNonBase || restUrl.match(/^[/\\]{2}/)) { [parts.hostname, restUrl] = takePattern( restUrl, - /^[/\\]{2,}([^/\\?#]*)/, - ); - } - } else { - let restAuthority; - if (isSpecial) { - parts.slashes = "//"; - [restAuthority, restUrl] = takePattern( - restUrl, - /^[/\\]{2,}([^/\\?#]*)/, + /^[/\\]{2}([^/\\?#]*)/, ); + usedNonBase = true; } else { - parts.slashes = restUrl.match(/^[/\\]{2}/) ? "//" : ""; - [restAuthority, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/); + parts.hostname = baseParts.hostname; } - let restAuthentication; - [restAuthentication, restAuthority] = takePattern( - restAuthority, - /^(.*)@/, - ); - [parts.username, restAuthentication] = takePattern( - restAuthentication, - /^([^:]*)/, - ); - parts.username = encodeUserinfo(parts.username); - [parts.password] = takePattern(restAuthentication, /^:(.*)/); - parts.password = encodeUserinfo(parts.password); - [parts.hostname, restAuthority] = takePattern( - restAuthority, - /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/, - ); - [parts.port] = takePattern(restAuthority, /^:(.*)/); - if (!isValidPort(parts.port)) { - return undefined; - } - if (parts.hostname == "" && isSpecial && isBase) { - return undefined; + parts.port = ""; + } else { + if (usedNonBase || restUrl.match(/^[/\\]{2}/)) { + let restAuthority; + if (isSpecial) { + parts.slashes = "//"; + [restAuthority, restUrl] = takePattern( + restUrl, + /^[/\\]*([^/\\?#]*)/, + ); + } else { + parts.slashes = restUrl.match(/^[/\\]{2}/) ? "//" : ""; + [restAuthority, restUrl] = takePattern( + restUrl, + /^[/\\]{2}([^/\\?#]*)/, + ); + } + let restAuthentication; + [restAuthentication, restAuthority] = takePattern( + restAuthority, + /^(.*)@/, + ); + [parts.username, restAuthentication] = takePattern( + restAuthentication, + /^([^:]*)/, + ); + parts.username = encodeUserinfo(parts.username); + [parts.password] = takePattern(restAuthentication, /^:(.*)/); + parts.password = encodeUserinfo(parts.password); + [parts.hostname, restAuthority] = takePattern( + restAuthority, + /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/, + ); + [parts.port] = takePattern(restAuthority, /^:(.*)/); + if (!isValidPort(parts.port)) { + return null; + } + if (parts.hostname == "" && isSpecial) { + return null; + } + usedNonBase = true; + } else { + parts.username = baseParts.username; + parts.password = baseParts.password; + parts.hostname = baseParts.hostname; + parts.port = baseParts.port; } } try { parts.hostname = encodeHostname(parts.hostname, isSpecial); } catch { - return undefined; + return null; } [parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/); parts.path = encodePathname(parts.path.replace(/\\/g, "/")); - [parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/); - parts.query = encodeSearch(parts.query); + if (usedNonBase) { + parts.path = normalizePath(parts.path, parts.protocol == "file"); + } else { + if (parts.path != "") { + usedNonBase = true; + } + parts.path = resolvePathFromBase( + parts.path, + baseParts.path || "/", + baseParts.protocol == "file", + ); + } + // Drop the hostname if a drive letter is parsed. + if (parts.protocol == "file" && parts.path.match(/^\/+[A-Za-z]:(\/|$)/)) { + parts.hostname = ""; + } + if (usedNonBase || restUrl.startsWith("?")) { + [parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/); + parts.query = encodeSearch(parts.query); + usedNonBase = true; + } else { + parts.query = baseParts.query; + } [parts.hash] = takePattern(restUrl, /^(#.*)/); parts.hash = encodeHash(parts.hash); return parts; @@ -348,22 +391,26 @@ // Keep it outside of URL to avoid any attempts of access. const blobURLMap = new Map(); - function isAbsolutePath(path) { - return path.startsWith("/"); - } - // Resolves `.`s and `..`s where possible. // Preserves repeating and trailing `/`s by design. - // On Windows, drive letter paths will be given a leading slash, and also a - // trailing slash if there are no other components e.g. "C:" -> "/C:/". - function normalizePath(path, isFilePath = false) { - if (build.os == "windows" && isFilePath) { - path = path.replace(/^\/*([A-Za-z]:)(\/|$)/, "/$1/"); - } - const isAbsolute = isAbsolutePath(path); + // Assumes drive letter file paths will have a leading slash. + function normalizePath(path, isFilePath) { + const isAbsolute = path.startsWith("/"); path = path.replace(/^\//, ""); const pathSegments = path.split("/"); + let driveLetter = null; + if (isFilePath && pathSegments[0].match(/^[A-Za-z]:$/)) { + driveLetter = pathSegments.shift(); + } + + if (isFilePath && isAbsolute) { + while (pathSegments.length > 1 && pathSegments[0] == "") { + pathSegments.shift(); + } + } + + let ensureTrailingSlash = false; const newPathSegments = []; for (let i = 0; i < pathSegments.length; i++) { const previous = newPathSegments[newPathSegments.length - 1]; @@ -373,64 +420,67 @@ (previous != undefined || isAbsolute) ) { newPathSegments.pop(); - } else if (pathSegments[i] != ".") { + ensureTrailingSlash = true; + } else if (pathSegments[i] == ".") { + ensureTrailingSlash = true; + } else { newPathSegments.push(pathSegments[i]); + ensureTrailingSlash = false; } } + if (driveLetter != null) { + newPathSegments.unshift(driveLetter); + } + if (newPathSegments.length == 0 && !isAbsolute) { + newPathSegments.push("."); + ensureTrailingSlash = false; + } let newPath = newPathSegments.join("/"); - if (!isAbsolute) { - if (newPathSegments.length == 0) { - newPath = "."; - } - } else { + if (isAbsolute) { newPath = `/${newPath}`; } + if (ensureTrailingSlash) { + newPath = newPath.replace(/\/*$/, "/"); + } return newPath; } // Standard URL basing logic, applied to paths. - function resolvePathFromBase( - path, - basePath, - isFilePath = false, - ) { - let normalizedPath = normalizePath(path, isFilePath); - let normalizedBasePath = normalizePath(basePath, isFilePath); - - let driveLetterPrefix = ""; - if (build.os == "windows" && isFilePath) { - let driveLetter; - let baseDriveLetter; - [driveLetter, normalizedPath] = takePattern( - normalizedPath, - /^(\/[A-Za-z]:)(?=\/)/, - ); - [baseDriveLetter, normalizedBasePath] = takePattern( - normalizedBasePath, - /^(\/[A-Za-z]:)(?=\/)/, - ); - driveLetterPrefix = driveLetter || baseDriveLetter; + function resolvePathFromBase(path, basePath, isFilePath) { + let basePrefix; + let suffix; + const baseDriveLetter = basePath.match(/^\/+[A-Za-z]:(?=\/|$)/)?.[0]; + if (isFilePath && path.match(/^\/+[A-Za-z]:(\/|$)/)) { + basePrefix = ""; + suffix = path; + } else if (path.startsWith("/")) { + if (isFilePath && baseDriveLetter) { + basePrefix = baseDriveLetter; + suffix = path; + } else { + basePrefix = ""; + suffix = path; + } + } else if (path != "") { + basePath = normalizePath(basePath, isFilePath); + path = normalizePath(path, isFilePath); + // Remove everything after the last `/` in `basePath`. + if (baseDriveLetter && isFilePath) { + basePrefix = `${baseDriveLetter}${ + basePath.slice(baseDriveLetter.length).replace(/[^\/]*$/, "") + }`; + } else { + basePrefix = basePath.replace(/[^\/]*$/, ""); + } + basePrefix = basePrefix.replace(/\/*$/, "/"); + // If `normalizedPath` ends with `.` or `..`, add a trailing slash. + suffix = path.replace(/(?<=(^|\/)(\.|\.\.))$/, "/"); + } else { + basePrefix = basePath; + suffix = ""; } - - if (isAbsolutePath(normalizedPath)) { - return `${driveLetterPrefix}${normalizedPath}`; - } - if (!isAbsolutePath(normalizedBasePath)) { - throw new TypeError("Base path must be absolute."); - } - - // Special case. - if (path == "") { - return `${driveLetterPrefix}${normalizedBasePath}`; - } - - // Remove everything after the last `/` in `normalizedBasePath`. - const prefix = normalizedBasePath.replace(/[^\/]*$/, ""); - // If `normalizedPath` ends with `.` or `..`, add a trailing slash. - const suffix = normalizedPath.replace(/(?<=(^|\/)(\.|\.\.))$/, "/"); - - return `${driveLetterPrefix}${normalizePath(prefix + suffix)}`; + return normalizePath(basePrefix + suffix, isFilePath); } function isValidPort(value) { @@ -628,46 +678,22 @@ } constructor(url, base) { - let baseParts; + let baseParts = null; + new.target; if (base) { - baseParts = typeof base === "string" ? parse(base) : parts.get(base); - if (baseParts === undefined) { + baseParts = base instanceof URL ? parts.get(base) : parse(base); + if (baseParts == null) { throw new TypeError("Invalid base URL."); } } - const urlParts = typeof url === "string" - ? parse(url, !baseParts) - : parts.get(url); - if (urlParts == undefined) { - throw new TypeError("Invalid URL."); - } - - if (urlParts.protocol) { - urlParts.path = normalizePath( - urlParts.path, - urlParts.protocol == "file", - ); - parts.set(this, urlParts); - } else if (baseParts) { - parts.set(this, { - protocol: baseParts.protocol, - slashes: baseParts.slashes, - username: baseParts.username, - password: baseParts.password, - hostname: baseParts.hostname, - port: baseParts.port, - path: resolvePathFromBase( - urlParts.path, - baseParts.path || "/", - baseParts.protocol == "file", - ), - query: urlParts.query, - hash: urlParts.hash, - }); - } else { + const urlParts = url instanceof URL + ? parts.get(url) + : parse(url, baseParts); + if (urlParts == null) { throw new TypeError("Invalid URL."); } + parts.set(this, urlParts); this.#updateSearchParams(); } diff --git a/cli/tests/unit/url_test.ts b/cli/tests/unit/url_test.ts index b50f6a25bd..ed8ae55001 100644 --- a/cli/tests/unit/url_test.ts +++ b/cli/tests/unit/url_test.ts @@ -27,6 +27,23 @@ unitTest(function urlParsing(): void { ); }); +unitTest(function urlProtocolParsing(): void { + assertEquals(new URL("Aa+-.1://foo").protocol, "aa+-.1:"); + assertEquals(new URL("aA+-.1://foo").protocol, "aa+-.1:"); + assertThrows(() => new URL("1://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("+://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("-://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL(".://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("_://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("=://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("!://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL(`"://foo`), TypeError, "Invalid URL."); + assertThrows(() => new URL("$://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("%://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("^://foo"), TypeError, "Invalid URL."); + assertThrows(() => new URL("*://foo"), TypeError, "Invalid URL."); +}); + unitTest(function urlAuthenticationParsing(): void { const specialUrl = new URL("http://foo:bar@baz"); assertEquals(specialUrl.username, "foo"); @@ -201,46 +218,35 @@ unitTest(function urlBackSlashes(): void { ); }); +unitTest(function urlProtocolSlashes(): void { + assertEquals(new URL("http:foo").href, "http://foo/"); + assertEquals(new URL("http://foo").href, "http://foo/"); + assertEquals(new URL("file:foo").href, "file:///foo"); + assertEquals(new URL("file://foo").href, "file://foo/"); + assertEquals(new URL("abcd:foo").href, "abcd:foo"); + assertEquals(new URL("abcd://foo").href, "abcd://foo"); +}); + unitTest(function urlRequireHost(): void { assertEquals(new URL("file:///").href, "file:///"); - assertThrows(() => { - new URL("ftp:///"); - }); - assertThrows(() => { - new URL("http:///"); - }); - assertThrows(() => { - new URL("https:///"); - }); - assertThrows(() => { - new URL("ws:///"); - }); - assertThrows(() => { - new URL("wss:///"); - }); + assertThrows(() => new URL("ftp:///"), TypeError, "Invalid URL."); + assertThrows(() => new URL("http:///"), TypeError, "Invalid URL."); + assertThrows(() => new URL("https:///"), TypeError, "Invalid URL."); + assertThrows(() => new URL("ws:///"), TypeError, "Invalid URL."); + assertThrows(() => new URL("wss:///"), TypeError, "Invalid URL."); }); unitTest(function urlDriveLetter() { - assertEquals( - new URL("file:///C:").href, - Deno.build.os == "windows" ? "file:///C:/" : "file:///C:", - ); - assertEquals(new URL("http://example.com/C:").href, "http://example.com/C:"); -}); - -unitTest(function urlUncHostname() { - assertEquals( - new URL("file:////").href, - Deno.build.os == "windows" ? "file:///" : "file:////", - ); - assertEquals( - new URL("file:////server").href, - Deno.build.os == "windows" ? "file://server/" : "file:////server", - ); - assertEquals( - new URL("file:////server/file").href, - Deno.build.os == "windows" ? "file://server/file" : "file:////server/file", - ); + assertEquals(new URL("file:///C:").href, "file:///C:"); + assertEquals(new URL("file:///C:/").href, "file:///C:/"); + assertEquals(new URL("file:///C:/..").href, "file:///C:/"); + // Don't recognise drive letters with extra leading slashes. + assertEquals(new URL("file:////C:/..").href, "file:///"); + // Drop the hostname if a drive letter is parsed. + assertEquals(new URL("file://foo/C:").href, "file:///C:"); + // Don't recognise drive letters in non-file protocols. + assertEquals(new URL("http://foo/C:/..").href, "http://foo/"); + assertEquals(new URL("abcd://foo/C:/..").href, "abcd://foo/"); }); unitTest(function urlHostnameUpperCase() { @@ -254,6 +260,12 @@ unitTest(function urlEmptyPath() { assertEquals(new URL("abcd://foo").pathname, ""); }); +unitTest(function urlPathRepeatedSlashes() { + assertEquals(new URL("http://foo//bar//").pathname, "//bar//"); + assertEquals(new URL("file://foo///bar//").pathname, "/bar//"); + assertEquals(new URL("abcd://foo//bar//").pathname, "//bar//"); +}); + unitTest(function urlTrim() { assertEquals(new URL(" http://example.com ").href, "http://example.com/"); }); @@ -284,66 +296,59 @@ unitTest(function urlEncoding() { ); }); -unitTest(function urlBaseURL(): void { - const base = new URL( - "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat", - ); - const url = new URL("/foo/bar?baz=foo#qux", base); - assertEquals(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux"); +unitTest(function urlBase(): void { + assertEquals(new URL("d", new URL("http://foo/a?b#c")).href, "http://foo/d"); - assertEquals( - new URL("D", "https://foo.bar/path/a/b/c/d").href, - "https://foo.bar/path/a/b/c/D", - ); + assertEquals(new URL("", "http://foo/a/b?c#d").href, "http://foo/a/b?c"); + assertEquals(new URL("", "file://foo/a/b?c#d").href, "file://foo/a/b?c"); + assertEquals(new URL("", "abcd://foo/a/b?c#d").href, "abcd://foo/a/b?c"); - assertEquals(new URL("D", "https://foo.bar").href, "https://foo.bar/D"); - assertEquals(new URL("D", "https://foo.bar/").href, "https://foo.bar/D"); + assertEquals(new URL("#e", "http://foo/a/b?c#d").href, "http://foo/a/b?c#e"); + assertEquals(new URL("#e", "file://foo/a/b?c#d").href, "file://foo/a/b?c#e"); + assertEquals(new URL("#e", "abcd://foo/a/b?c#d").href, "abcd://foo/a/b?c#e"); - assertEquals( - new URL("/d", "https://foo.bar/path/a/b/c/d").href, - "https://foo.bar/d", - ); -}); + assertEquals(new URL("?e", "http://foo/a/b?c#d").href, "http://foo/a/b?e"); + assertEquals(new URL("?e", "file://foo/a/b?c#d").href, "file://foo/a/b?e"); + assertEquals(new URL("?e", "abcd://foo/a/b?c#d").href, "abcd://foo/a/b?e"); -unitTest(function urlBaseString(): void { - const url = new URL( - "/foo/bar?baz=foo#qux", - "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat", - ); - assertEquals(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux"); -}); + assertEquals(new URL("e", "http://foo/a/b?c#d").href, "http://foo/a/e"); + assertEquals(new URL("e", "file://foo/a/b?c#d").href, "file://foo/a/e"); + assertEquals(new URL("e", "abcd://foo/a/b?c#d").href, "abcd://foo/a/e"); -unitTest(function urlRelativeWithBase(): void { - assertEquals(new URL("", "file:///a/a/a").href, "file:///a/a/a"); - assertEquals(new URL(".", "file:///a/a/a").href, "file:///a/a/"); - assertEquals(new URL("..", "file:///a/a/a").href, "file:///a/"); - assertEquals(new URL("b", "file:///a/a/a").href, "file:///a/a/b"); - assertEquals(new URL("b", "file:///a/a/a/").href, "file:///a/a/a/b"); - assertEquals(new URL("b/", "file:///a/a/a").href, "file:///a/a/b/"); - assertEquals(new URL("../b", "file:///a/a/a").href, "file:///a/b"); + assertEquals(new URL(".", "http://foo/a/b?c#d").href, "http://foo/a/"); + assertEquals(new URL(".", "file://foo/a/b?c#d").href, "file://foo/a/"); + assertEquals(new URL(".", "abcd://foo/a/b?c#d").href, "abcd://foo/a/"); + + assertEquals(new URL("..", "http://foo/a/b?c#d").href, "http://foo/"); + assertEquals(new URL("..", "file://foo/a/b?c#d").href, "file://foo/"); + assertEquals(new URL("..", "abcd://foo/a/b?c#d").href, "abcd://foo/"); + + assertEquals(new URL("/e", "http://foo/a/b?c#d").href, "http://foo/e"); + assertEquals(new URL("/e", "file://foo/a/b?c#d").href, "file://foo/e"); + assertEquals(new URL("/e", "abcd://foo/a/b?c#d").href, "abcd://foo/e"); + + assertEquals(new URL("//bar", "http://foo/a/b?c#d").href, "http://bar/"); + assertEquals(new URL("//bar", "file://foo/a/b?c#d").href, "file://bar/"); + assertEquals(new URL("//bar", "abcd://foo/a/b?c#d").href, "abcd://bar"); + + assertEquals(new URL("efgh:", "http://foo/a/b?c#d").href, "efgh:"); + assertEquals(new URL("efgh:", "file://foo/a/b?c#d").href, "efgh:"); + assertEquals(new URL("efgh:", "abcd://foo/a/b?c#d").href, "efgh:"); }); unitTest(function urlDriveLetterBase() { - assertEquals( - new URL("/b", "file:///C:/a/b").href, - Deno.build.os == "windows" ? "file:///C:/b" : "file:///b", - ); - assertEquals( - new URL("D:", "file:///C:/a/b").href, - Deno.build.os == "windows" ? "file:///D:/" : "file:///C:/a/D:", - ); - assertEquals( - new URL("/D:", "file:///C:/a/b").href, - Deno.build.os == "windows" ? "file:///D:/" : "file:///D:", - ); - assertEquals( - new URL("D:/b", "file:///C:/a/b").href, - Deno.build.os == "windows" ? "file:///D:/b" : "file:///C:/a/D:/b", - ); + assertEquals(new URL("/b", "file:///C:/a/b").href, "file:///C:/b"); + assertEquals(new URL("/D:", "file:///C:/a/b").href, "file:///D:"); }); -unitTest(function emptyBasePath(): void { - assertEquals(new URL("", "http://example.com").href, "http://example.com/"); +unitTest(function urlSameProtocolBase() { + assertEquals(new URL("http:", "http://foo/a").href, "http://foo/a"); + assertEquals(new URL("file:", "file://foo/a").href, "file://foo/a"); + assertEquals(new URL("abcd:", "abcd://foo/a").href, "abcd:"); + + assertEquals(new URL("http:b", "http://foo/a").href, "http://foo/b"); + assertEquals(new URL("file:b", "file://foo/a").href, "file://foo/b"); + assertEquals(new URL("abcd:b", "abcd://foo/a").href, "abcd:b"); }); unitTest(function deletingAllParamsRemovesQuestionMarkFromURL(): void { @@ -391,12 +396,6 @@ unitTest(function protocolNotHttpOrFile() { assertEquals(url.origin, "null"); }); -unitTest(function createBadUrl(): void { - assertThrows(() => { - new URL("0.0.0.0:8080"); - }); -}); - unitTest(function throwForInvalidPortConstructor(): void { const urls = [ // If port is greater than 2^16 − 1, validation error, return failure. @@ -416,14 +415,6 @@ unitTest(function throwForInvalidPortConstructor(): void { new URL("https://baz.qat:0"); }); -unitTest(function throwForInvalidSchemeConstructor(): void { - assertThrows( - () => new URL("invalid_scheme://baz.qat"), - TypeError, - "Invalid URL.", - ); -}); - unitTest(function doNotOverridePortIfInvalid(): void { const initialPort = "3000"; const ports = [ diff --git a/std/path/from_file_url_test.ts b/std/path/from_file_url_test.ts index 8bbc4e9865..7a0432f68e 100644 --- a/std/path/from_file_url_test.ts +++ b/std/path/from_file_url_test.ts @@ -7,14 +7,13 @@ Deno.test("[path] fromFileUrl", function () { assertEquals(posix.fromFileUrl("file:///home/foo"), "/home/foo"); assertEquals(posix.fromFileUrl("https://example.com/foo"), "/foo"); assertEquals(posix.fromFileUrl("file:///"), "/"); - // FIXME(nayeemrmn): Remove the condition. UNC paths are supported here when - // run on Windows (matching the underlying URL class), but - // `posix.fromFileUrl()` should not support them under any circumstance. - if (Deno.build.os != "windows") { - assertEquals(posix.fromFileUrl("file:////"), "//"); - assertEquals(posix.fromFileUrl("file:////server"), "//server"); - assertEquals(posix.fromFileUrl("file:////server/file"), "//server/file"); - } + // Drive letters are supported platform-independently to align with the WHATWG + // URL specification. + assertEquals(posix.fromFileUrl("file:///c:"), "c:/"); + assertEquals(posix.fromFileUrl("file:///c:/"), "c:/"); + assertEquals(posix.fromFileUrl("file:///C:/"), "C:/"); + assertEquals(posix.fromFileUrl("file:///C:/Users/"), "C:/Users/"); + assertEquals(posix.fromFileUrl("file:///C:foo/bar"), "/C:foo/bar"); }); Deno.test("[path] fromFileUrl (win32)", function () { @@ -22,18 +21,9 @@ Deno.test("[path] fromFileUrl (win32)", function () { assertEquals(win32.fromFileUrl("file:///home/foo"), "\\home\\foo"); assertEquals(win32.fromFileUrl("https://example.com/foo"), "\\foo"); assertEquals(win32.fromFileUrl("file:///"), "\\"); - // FIXME(nayeemrmn): Remove the condition. UNC paths are only supported here - // when run on Windows (matching the underlying URL class), but - // `win32.fromFileUrl()` should support them under every circumstance. - if (Deno.build.os == "windows") { - assertEquals(win32.fromFileUrl("file:////"), "\\"); - assertEquals(win32.fromFileUrl("file:////server"), "\\"); - assertEquals(win32.fromFileUrl("file:////server/file"), "\\file"); - } - assertEquals(win32.fromFileUrl("file:///c"), "\\c"); assertEquals(win32.fromFileUrl("file:///c:"), "c:\\"); assertEquals(win32.fromFileUrl("file:///c:/"), "c:\\"); assertEquals(win32.fromFileUrl("file:///C:/"), "C:\\"); assertEquals(win32.fromFileUrl("file:///C:/Users/"), "C:\\Users\\"); - assertEquals(win32.fromFileUrl("file:///C:cwd/another"), "\\C:cwd\\another"); + assertEquals(win32.fromFileUrl("file:///C:foo/bar"), "\\C:foo\\bar"); }); diff --git a/std/path/posix.ts b/std/path/posix.ts index 03d07a84a8..3c4262203e 100644 --- a/std/path/posix.ts +++ b/std/path/posix.ts @@ -435,5 +435,6 @@ export function parse(path: string): ParsedPath { * are ignored. */ export function fromFileUrl(url: string | URL): string { - return new URL(String(url)).pathname; + return (url instanceof URL ? url : new URL(url)).pathname + .replace(/^\/*([A-Za-z]:)(\/|$)/, "$1/"); } diff --git a/std/path/win32.ts b/std/path/win32.ts index 66ed1ff149..cf0e695373 100644 --- a/std/path/win32.ts +++ b/std/path/win32.ts @@ -914,7 +914,7 @@ export function parse(path: string): ParsedPath { * are ignored. */ export function fromFileUrl(url: string | URL): string { - return new URL(String(url)).pathname + return (url instanceof URL ? url : new URL(url)).pathname .replace(/^\/*([A-Za-z]:)(\/|$)/, "$1/") .replace(/\//g, "\\"); }