mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 15:24:46 -05:00
Fix URL encoding (#5557)
This commit is contained in:
parent
c3ec16535f
commit
93c2164673
3 changed files with 112 additions and 37 deletions
|
@ -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"
|
||||
|
|
|
@ -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("");
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
Loading…
Reference in a new issue