Skip to content

Commit

Permalink
feat(cli/js/web/url): Support drive letters in file URLs on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
nayeemrmn committed May 4, 2020
1 parent c9c4321 commit 80c3f6a
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 11 deletions.
27 changes: 27 additions & 0 deletions cli/js/tests/url_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ unitTest(function urlRequireHost(): void {
});
});

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 urlBaseURL(): void {
const base = new URL(
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
Expand All @@ -187,6 +195,25 @@ unitTest(function urlRelativeWithBase(): void {
assertEquals(new URL("../b", "file:///a/a/a").href, "file:///a/b");
});

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"
);
});

unitTest(function emptyBasePath(): void {
assertEquals(new URL("", "http://example.com").href, "http://example.com/");
});
Expand Down
53 changes: 42 additions & 11 deletions cli/js/web/url.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { build } from "../build.ts";
import { getRandomValues } from "../ops/get_random_values.ts";
import { customInspect } from "./console.ts";
import { urls } from "./url_search_params.ts";
import { getRandomValues } from "../ops/get_random_values.ts";

interface URLParts {
protocol: string;
Expand Down Expand Up @@ -42,7 +43,7 @@ const MAX_PORT = 2 ** 16 - 1;
// = ["deno.land", "80"]
function takePattern(string: string, pattern: RegExp): [string, string] {
let capture = "";
const rest = string.replace(pattern, (match, capture_) => {
const rest = string.replace(pattern, (_, capture_) => {
capture = capture_;
return "";
});
Expand Down Expand Up @@ -116,7 +117,12 @@ function isAbsolutePath(path: string): boolean {

// Resolves `.`s and `..`s where possible.
// Preserves repeating and trailing `/`s by design.
function normalizePath(path: string): string {
// 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: string, isFilePath = false): string {
if (build.os == "windows" && isFilePath) {
path = path.replace(/^\/*([A-Za-z]:)(\/|$)/, "/$1/");
}
const isAbsolute = isAbsolutePath(path);
path = path.replace(/^\//, "");
const pathSegments = path.split("/");
Expand Down Expand Up @@ -147,27 +153,47 @@ function normalizePath(path: string): string {
}

// Standard URL basing logic, applied to paths.
function resolvePathFromBase(path: string, basePath: string): string {
const normalizedPath = normalizePath(path);
function resolvePathFromBase(
path: string,
basePath: string,
isFilePath = false
): string {
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;
}

if (isAbsolutePath(normalizedPath)) {
return normalizedPath;
return `${driveLetterPrefix}${normalizedPath}`;
}
const normalizedBasePath = normalizePath(basePath);
if (!isAbsolutePath(normalizedBasePath)) {
throw new TypeError("Base path must be absolute.");
}

// Special case.
if (path == "") {
return normalizedBasePath;
return `${driveLetterPrefix}${normalizedBasePath}`;
}

// Remove everything after the last `/` in `normalizedBasePath`.
const prefix = normalizedBasePath.replace(/[^\/]*$/, "");
// If `normalizedPath` ends with `.` or `..`, add a trailing space.
// If `normalizedPath` ends with `.` or `..`, add a trailing slash.
const suffix = normalizedPath.replace(/(?<=(^|\/)(\.|\.\.))$/, "/");

return normalizePath(prefix + suffix);
return `${driveLetterPrefix}${normalizePath(prefix + suffix)}`;
}

function isValidPort(value: string): boolean {
Expand Down Expand Up @@ -393,6 +419,7 @@ export class URLImpl implements URL {
}

if (urlParts.protocol) {
urlParts.path = normalizePath(urlParts.path, urlParts.protocol == "file");
parts.set(this, urlParts);
} else if (baseParts) {
parts.set(this, {
Expand All @@ -401,7 +428,11 @@ export class URLImpl implements URL {
password: baseParts.password,
hostname: baseParts.hostname,
port: baseParts.port,
path: resolvePathFromBase(urlParts.path, baseParts.path || "/"),
path: resolvePathFromBase(
urlParts.path,
baseParts.path || "/",
baseParts.protocol == "file"
),
query: urlParts.query,
hash: urlParts.hash,
});
Expand Down

0 comments on commit 80c3f6a

Please sign in to comment.