summaryrefslogtreecommitdiff
path: root/cli/js
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js')
-rw-r--r--cli/js/tests/url_test.ts36
-rw-r--r--cli/js/web/url.ts105
-rw-r--r--cli/js/web/url_search_params.ts8
3 files changed, 112 insertions, 37 deletions
diff --git a/cli/js/tests/url_test.ts b/cli/js/tests/url_test.ts
index 5b403fb1c..68fcbd95e 100644
--- a/cli/js/tests/url_test.ts
+++ b/cli/js/tests/url_test.ts
@@ -169,6 +169,42 @@ unitTest(function urlDriveLetter() {
assertEquals(new URL("http://example.com/C:").href, "http://example.com/C:");
});
+unitTest(function urlHostnameUpperCase() {
+ assertEquals(new URL("https://EXAMPLE.COM").href, "https://example.com/");
+});
+
+unitTest(function urlTrim() {
+ assertEquals(new URL(" https://example.com ").href, "https://example.com/");
+});
+
+unitTest(function urlEncoding() {
+ assertEquals(
+ new URL("https://a !$&*()=,;+'\"@example.com").username,
+ "a%20!$&*()%3D,%3B+%27%22"
+ );
+ assertEquals(
+ new URL("https://:a !$&*()=,;+'\"@example.com").password,
+ "a%20!$&*()%3D,%3B+%27%22"
+ );
+ // FIXME: https://url.spec.whatwg.org/#idna
+ // assertEquals(
+ // new URL("https://a !$&*()=,+'\"").hostname,
+ // "a%20%21%24%26%2A%28%29%3D%2C+%27%22"
+ // );
+ assertEquals(
+ new URL("https://example.com/a ~!@$&*()=:/,;+'\"\\").pathname,
+ "/a%20~!@$&*()=:/,;+'%22/"
+ );
+ assertEquals(
+ new URL("https://example.com?a ~!@$&*()=:/,;?+'\"\\").search,
+ "?a%20~!@$&*()=:/,;?+%27%22\\"
+ );
+ assertEquals(
+ new URL("https://example.com#a ~!@#$&*()=:/,;?+'\"\\").hash,
+ "#a%20~!@#$&*()=:/,;?+'%22\\"
+ );
+});
+
unitTest(function urlBaseURL(): void {
const base = new URL(
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
diff --git a/cli/js/web/url.ts b/cli/js/web/url.ts
index 41f855914..bbe405f85 100644
--- a/cli/js/web/url.ts
+++ b/cli/js/web/url.ts
@@ -11,7 +11,7 @@ interface URLParts {
hostname: string;
port: string;
path: string;
- query: string | null;
+ query: string;
hash: string;
}
@@ -53,7 +53,7 @@ function takePattern(string: string, pattern: RegExp): [string, string] {
function parse(url: string, isBase = true): URLParts | undefined {
const parts: Partial<URLParts> = {};
let restUrl;
- [parts.protocol, restUrl] = takePattern(url, /^([a-z]+):/);
+ [parts.protocol, restUrl] = takePattern(url.trim(), /^([a-z]+):/);
if (isBase && parts.protocol == "") {
return undefined;
}
@@ -61,9 +61,6 @@ function parse(url: string, isBase = true): URLParts | undefined {
parts.username = "";
parts.password = "";
[parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/);
- if (parts.hostname.includes(":")) {
- return undefined;
- }
parts.port = "";
} else if (specialSchemes.includes(parts.protocol)) {
let restAuthority;
@@ -80,7 +77,9 @@ function parse(url: string, isBase = true): URLParts | undefined {
restAuthentication,
/^([^:]*)/
);
+ parts.username = encodeUserinfo(parts.username);
[parts.password] = takePattern(restAuthentication, /^:(.*)/);
+ parts.password = encodeUserinfo(parts.password);
[parts.hostname, restAuthority] = takePattern(restAuthority, /^([^:]+)/);
[parts.port] = takePattern(restAuthority, /^:(.*)/);
if (!isValidPort(parts.port)) {
@@ -92,10 +91,17 @@ function parse(url: string, isBase = true): URLParts | undefined {
parts.hostname = "";
parts.port = "";
}
+ try {
+ parts.hostname = encodeHostname(parts.hostname).toLowerCase();
+ } catch {
+ return undefined;
+ }
[parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/);
- parts.path = parts.path.replace(/\\/g, "/");
+ parts.path = encodePathname(parts.path.replace(/\\/g, "/"));
[parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/);
+ parts.query = encodeSearch(parts.query);
[parts.hash] = takePattern(restUrl, /^(#.*)/);
+ parts.hash = encodeHash(parts.hash);
return parts as URLParts;
}
@@ -259,9 +265,7 @@ export class URLImpl implements URL {
value = `#${value}`;
}
// hashes can contain % and # unescaped
- parts.get(this)!.hash = escape(value)
- .replace(/%25/g, "%")
- .replace(/%23/g, "#");
+ parts.get(this)!.hash = encodeHash(value);
}
}
@@ -282,7 +286,9 @@ export class URLImpl implements URL {
set hostname(value: string) {
value = String(value);
- parts.get(this)!.hostname = encodeURIComponent(value);
+ try {
+ parts.get(this)!.hostname = encodeHostname(value);
+ } catch {}
}
get href(): string {
@@ -319,7 +325,7 @@ export class URLImpl implements URL {
set password(value: string) {
value = String(value);
- parts.get(this)!.password = encodeURIComponent(value);
+ parts.get(this)!.password = encodeUserinfo(value);
}
get pathname(): string {
@@ -332,7 +338,7 @@ export class URLImpl implements URL {
value = `/${value}`;
}
// paths can contain % unescaped
- parts.get(this)!.path = escape(value).replace(/%25/g, "%");
+ parts.get(this)!.path = encodePathname(value);
}
get port(): string {
@@ -366,27 +372,13 @@ export class URLImpl implements URL {
}
get search(): string {
- const query = parts.get(this)!.query;
- if (query === null || query === "") {
- return "";
- }
-
- return query;
+ return parts.get(this)!.query;
}
set search(value: string) {
value = String(value);
- let query: string | null;
-
- if (value === "") {
- query = null;
- } else if (value.charAt(0) !== "?") {
- query = `?${value}`;
- } else {
- query = value;
- }
-
- parts.get(this)!.query = query;
+ const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`;
+ parts.get(this)!.query = encodeSearch(query);
this.#updateSearchParams();
}
@@ -396,7 +388,7 @@ export class URLImpl implements URL {
set username(value: string) {
value = String(value);
- parts.get(this)!.username = encodeURIComponent(value);
+ parts.get(this)!.username = encodeUserinfo(value);
}
get searchParams(): URLSearchParams {
@@ -474,3 +466,56 @@ export class URLImpl implements URL {
blobURLMap.delete(url);
}
}
+
+function charInC0ControlSet(c: string): boolean {
+ return c >= "\u0000" && c <= "\u001F";
+}
+
+function charInSearchSet(c: string): boolean {
+ // prettier-ignore
+ return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u0027", "\u003C", "\u003E"].includes(c) || c > "\u007E";
+}
+
+function charInFragmentSet(c: string): boolean {
+ // prettier-ignore
+ return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u003C", "\u003E", "\u0060"].includes(c);
+}
+
+function charInPathSet(c: string): boolean {
+ // prettier-ignore
+ return charInFragmentSet(c) || ["\u0023", "\u003F", "\u007B", "\u007D"].includes(c);
+}
+
+function charInUserinfoSet(c: string): boolean {
+ // "\u0027" ("'") seemingly isn't in the spec, but matches Chrome and Firefox.
+ // prettier-ignore
+ return charInPathSet(c) || ["\u0027", "\u002F", "\u003A", "\u003B", "\u003D", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E", "\u007C"].includes(c);
+}
+
+function encodeChar(c: string): string {
+ return `%${c.charCodeAt(0).toString(16)}`.toUpperCase();
+}
+
+function encodeUserinfo(s: string): string {
+ return [...s].map((c) => (charInUserinfoSet(c) ? encodeChar(c) : c)).join("");
+}
+
+function encodeHostname(s: string): string {
+ // FIXME: https://url.spec.whatwg.org/#idna
+ if (s.includes(":")) {
+ throw new TypeError("Invalid hostname.");
+ }
+ return encodeURIComponent(s);
+}
+
+function encodePathname(s: string): string {
+ return [...s].map((c) => (charInPathSet(c) ? encodeChar(c) : c)).join("");
+}
+
+function encodeSearch(s: string): string {
+ return [...s].map((c) => (charInSearchSet(c) ? encodeChar(c) : c)).join("");
+}
+
+function encodeHash(s: string): string {
+ return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join("");
+}
diff --git a/cli/js/web/url_search_params.ts b/cli/js/web/url_search_params.ts
index 35439b0e2..2abac3cd0 100644
--- a/cli/js/web/url_search_params.ts
+++ b/cli/js/web/url_search_params.ts
@@ -76,13 +76,7 @@ export class URLSearchParamsImpl implements URLSearchParams {
if (url == null) {
return;
}
-
- let query: string | null = this.toString();
- if (query === "") {
- query = null;
- }
-
- parts.get(url)!.query = query;
+ parts.get(url)!.query = this.toString();
};
#append = (name: string, value: string): void => {