deno/ext/url/00_url.js
2024-04-16 17:11:57 +02:00

907 lines
24 KiB
JavaScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// @ts-check
/// <reference path="../../core/internal.d.ts" />
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../webidl/internal.d.ts" />
import { primordials } from "ext:core/mod.js";
import {
op_url_get_serialization,
op_url_parse,
op_url_parse_search_params,
op_url_parse_with_base,
op_url_reparse,
op_url_stringify_search_params,
} from "ext:core/ops";
const {
ArrayIsArray,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSome,
ArrayPrototypeSort,
ArrayPrototypeSplice,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
SafeArrayIterator,
StringPrototypeSlice,
StringPrototypeStartsWith,
Symbol,
SymbolFor,
SymbolIterator,
TypeError,
Uint32Array,
} = primordials;
import * as webidl from "ext:deno_webidl/00_webidl.js";
import { createFilteredInspectProxy } from "ext:deno_console/01_console.js";
const _list = Symbol("list");
const _urlObject = Symbol("url object");
// WARNING: must match rust code's UrlSetter::*
const SET_HASH = 0;
const SET_HOST = 1;
const SET_HOSTNAME = 2;
const SET_PASSWORD = 3;
const SET_PATHNAME = 4;
const SET_PORT = 5;
const SET_PROTOCOL = 6;
const SET_SEARCH = 7;
const SET_USERNAME = 8;
// Helper functions
/**
* @param {string} href
* @param {number} setter
* @param {string} value
* @returns {string}
*/
function opUrlReparse(href, setter, value) {
const status = op_url_reparse(
href,
setter,
value,
componentsBuf,
);
return getSerialization(status, href);
}
/**
* @param {string} href
* @param {string} [maybeBase]
* @returns {number}
*/
function opUrlParse(href, maybeBase) {
if (maybeBase === undefined) {
return op_url_parse(href, componentsBuf);
}
return op_url_parse_with_base(
href,
maybeBase,
componentsBuf,
);
}
/**
* @param {number} status
* @param {string} href
* @param {string} [maybeBase]
* @returns {string}
*/
function getSerialization(status, href, maybeBase) {
if (status === 0) {
return href;
} else if (status === 1) {
return op_url_get_serialization();
} else {
throw new TypeError(
`Invalid URL: '${href}'` +
(maybeBase ? ` with base '${maybeBase}'` : ""),
);
}
}
class URLSearchParams {
[_list];
[_urlObject] = null;
/**
* @param {string | [string][] | Record<string, string>} init
*/
constructor(init = "") {
const prefix = "Failed to construct 'URL'";
init = webidl.converters
["sequence<sequence<USVString>> or record<USVString, USVString> or USVString"](
init,
prefix,
"Argument 1",
);
this[webidl.brand] = webidl.brand;
if (!init) {
// if there is no query string, return early
this[_list] = [];
return;
}
if (typeof init === "string") {
// Overload: USVString
// If init is a string and starts with U+003F (?),
// remove the first code point from init.
if (init[0] == "?") {
init = StringPrototypeSlice(init, 1);
}
this[_list] = op_url_parse_search_params(init);
} else if (ArrayIsArray(init)) {
// Overload: sequence<sequence<USVString>>
this[_list] = ArrayPrototypeMap(init, (pair, i) => {
if (pair.length !== 2) {
throw new TypeError(
`${prefix}: Item ${
i + 0
} in the parameter list does have length 2 exactly.`,
);
}
return [pair[0], pair[1]];
});
} else {
// Overload: record<USVString, USVString>
this[_list] = ArrayPrototypeMap(
ObjectKeys(init),
(key) => [key, init[key]],
);
}
}
#updateUrlSearch() {
const url = this[_urlObject];
if (url === null) {
return;
}
// deno-lint-ignore prefer-primordials
url[_updateUrlSearch](this.toString());
}
/**
* @param {string} name
* @param {string} value
*/
append(name, value) {
webidl.assertBranded(this, URLSearchParamsPrototype);
const prefix = "Failed to execute 'append' on 'URLSearchParams'";
webidl.requiredArguments(arguments.length, 2, prefix);
name = webidl.converters.USVString(name, prefix, "Argument 1");
value = webidl.converters.USVString(value, prefix, "Argument 2");
ArrayPrototypePush(this[_list], [name, value]);
this.#updateUrlSearch();
}
/**
* @param {string} name
* @param {string} [value]
*/
delete(name, value = undefined) {
webidl.assertBranded(this, URLSearchParamsPrototype);
const prefix = "Failed to execute 'append' on 'URLSearchParams'";
webidl.requiredArguments(arguments.length, 1, prefix);
name = webidl.converters.USVString(name, prefix, "Argument 1");
const list = this[_list];
let i = 0;
if (value === undefined) {
while (i < list.length) {
if (list[i][0] === name) {
ArrayPrototypeSplice(list, i, 1);
} else {
i++;
}
}
} else {
value = webidl.converters.USVString(value, prefix, "Argument 2");
while (i < list.length) {
if (list[i][0] === name && list[i][1] === value) {
ArrayPrototypeSplice(list, i, 1);
} else {
i++;
}
}
}
this.#updateUrlSearch();
}
/**
* @param {string} name
* @returns {string[]}
*/
getAll(name) {
webidl.assertBranded(this, URLSearchParamsPrototype);
const prefix = "Failed to execute 'getAll' on 'URLSearchParams'";
webidl.requiredArguments(arguments.length, 1, prefix);
name = webidl.converters.USVString(name, prefix, "Argument 1");
const values = [];
const entries = this[_list];
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
if (entry[0] === name) {
ArrayPrototypePush(values, entry[1]);
}
}
return values;
}
/**
* @param {string} name
* @return {string | null}
*/
get(name) {
webidl.assertBranded(this, URLSearchParamsPrototype);
const prefix = "Failed to execute 'get' on 'URLSearchParams'";
webidl.requiredArguments(arguments.length, 1, prefix);
name = webidl.converters.USVString(name, prefix, "Argument 1");
const entries = this[_list];
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
if (entry[0] === name) {
return entry[1];
}
}
return null;
}
/**
* @param {string} name
* @param {string} [value]
* @return {boolean}
*/
has(name, value = undefined) {
webidl.assertBranded(this, URLSearchParamsPrototype);
const prefix = "Failed to execute 'has' on 'URLSearchParams'";
webidl.requiredArguments(arguments.length, 1, prefix);
name = webidl.converters.USVString(name, prefix, "Argument 1");
if (value !== undefined) {
value = webidl.converters.USVString(value, prefix, "Argument 2");
return ArrayPrototypeSome(
this[_list],
(entry) => entry[0] === name && entry[1] === value,
);
}
return ArrayPrototypeSome(this[_list], (entry) => entry[0] === name);
}
/**
* @param {string} name
* @param {string} value
*/
set(name, value) {
webidl.assertBranded(this, URLSearchParamsPrototype);
const prefix = "Failed to execute 'set' on 'URLSearchParams'";
webidl.requiredArguments(arguments.length, 2, prefix);
name = webidl.converters.USVString(name, prefix, "Argument 1");
value = webidl.converters.USVString(value, prefix, "Argument 2");
const list = this[_list];
// If there are any name-value pairs whose name is name, in list,
// set the value of the first such name-value pair to value
// and remove the others.
let found = false;
let i = 0;
while (i < list.length) {
if (list[i][0] === name) {
if (!found) {
list[i][1] = value;
found = true;
i++;
} else {
ArrayPrototypeSplice(list, i, 1);
}
} else {
i++;
}
}
// Otherwise, append a new name-value pair whose name is name
// and value is value, to list.
if (!found) {
ArrayPrototypePush(list, [name, value]);
}
this.#updateUrlSearch();
}
sort() {
webidl.assertBranded(this, URLSearchParamsPrototype);
ArrayPrototypeSort(
this[_list],
(a, b) => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1),
);
this.#updateUrlSearch();
}
/**
* @return {string}
*/
toString() {
webidl.assertBranded(this, URLSearchParamsPrototype);
return op_url_stringify_search_params(this[_list]);
}
get size() {
webidl.assertBranded(this, URLSearchParamsPrototype);
return this[_list].length;
}
}
webidl.mixinPairIterable("URLSearchParams", URLSearchParams, _list, 0, 1);
webidl.configureInterface(URLSearchParams);
const URLSearchParamsPrototype = URLSearchParams.prototype;
webidl.converters["URLSearchParams"] = webidl.createInterfaceConverter(
"URLSearchParams",
URLSearchParamsPrototype,
);
const _updateUrlSearch = Symbol("updateUrlSearch");
function trim(s) {
if (s.length === 1) return "";
return s;
}
// Represents a "no port" value. A port in URL cannot be greater than 2^16 - 1
const NO_PORT = 65536;
const skipInit = Symbol();
const componentsBuf = new Uint32Array(8);
class URL {
/** @type {URLSearchParams|null} */
#queryObject = null;
/** @type {string} */
#serialization;
/** @type {number} */
#schemeEnd;
/** @type {number} */
#usernameEnd;
/** @type {number} */
#hostStart;
/** @type {number} */
#hostEnd;
/** @type {number} */
#port;
/** @type {number} */
#pathStart;
/** @type {number} */
#queryStart;
/** @type {number} */
#fragmentStart;
[_updateUrlSearch](value) {
this.#serialization = opUrlReparse(
this.#serialization,
SET_SEARCH,
value,
);
this.#updateComponents();
}
/**
* @param {string} url
* @param {string} [base]
*/
constructor(url, base = undefined) {
// skip initialization for URL.parse
if (url === skipInit) {
return;
}
const prefix = "Failed to construct 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
url = webidl.converters.DOMString(url, prefix, "Argument 1");
if (base !== undefined) {
base = webidl.converters.DOMString(base, prefix, "Argument 2");
}
const status = opUrlParse(url, base);
this[webidl.brand] = webidl.brand;
this.#serialization = getSerialization(status, url, base);
this.#updateComponents();
}
/**
* @param {string} url
* @param {string} [base]
*/
static parse(url, base = undefined) {
const prefix = "Failed to execute 'URL.parse'";
webidl.requiredArguments(arguments.length, 1, prefix);
url = webidl.converters.DOMString(url, prefix, "Argument 1");
if (base !== undefined) {
base = webidl.converters.DOMString(base, prefix, "Argument 2");
}
const status = opUrlParse(url, base);
if (status !== 0 && status !== 1) {
return null;
}
// If initialized with webidl.createBranded, private properties are not be accessible,
// so it is passed through the constructor
const self = new this(skipInit);
self[webidl.brand] = webidl.brand;
self.#serialization = getSerialization(status, url, base);
self.#updateComponents();
return self;
}
/**
* @param {string} url
* @param {string} [base]
*/
static canParse(url, base = undefined) {
const prefix = "Failed to execute 'URL.canParse'";
webidl.requiredArguments(arguments.length, 1, prefix);
url = webidl.converters.DOMString(url, prefix, "Argument 1");
if (base !== undefined) {
base = webidl.converters.DOMString(base, prefix, "Argument 2");
}
const status = opUrlParse(url, base);
return status === 0 || status === 1;
}
#updateComponents() {
({
0: this.#schemeEnd,
1: this.#usernameEnd,
2: this.#hostStart,
3: this.#hostEnd,
4: this.#port,
5: this.#pathStart,
6: this.#queryStart,
7: this.#fragmentStart,
} = componentsBuf);
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(URLPrototype, this),
keys: [
"href",
"origin",
"protocol",
"username",
"password",
"host",
"hostname",
"port",
"pathname",
"hash",
"search",
],
}),
inspectOptions,
);
}
#updateSearchParams() {
if (this.#queryObject !== null) {
const params = this.#queryObject[_list];
const newParams = op_url_parse_search_params(
StringPrototypeSlice(this.search, 1),
);
ArrayPrototypeSplice(
params,
0,
params.length,
...new SafeArrayIterator(newParams),
);
}
}
#hasAuthority() {
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L824
return StringPrototypeStartsWith(
StringPrototypeSlice(this.#serialization, this.#schemeEnd),
"://",
);
}
/** @return {string} */
get hash() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L263
return this.#fragmentStart
? trim(StringPrototypeSlice(this.#serialization, this.#fragmentStart))
: "";
}
/** @param {string} value */
set hash(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'hash' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_HASH,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {string} */
get host() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L101
return StringPrototypeSlice(
this.#serialization,
this.#hostStart,
this.#pathStart,
);
}
/** @param {string} value */
set host(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'host' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_HOST,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {string} */
get hostname() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L988
return StringPrototypeSlice(
this.#serialization,
this.#hostStart,
this.#hostEnd,
);
}
/** @param {string} value */
set hostname(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'hostname' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_HOSTNAME,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {string} */
get href() {
webidl.assertBranded(this, URLPrototype);
return this.#serialization;
}
/** @param {string} value */
set href(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'href' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
const status = opUrlParse(value);
this.#serialization = getSerialization(status, value);
this.#updateComponents();
this.#updateSearchParams();
}
/** @return {string} */
get origin() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/origin.rs#L14
const scheme = StringPrototypeSlice(
this.#serialization,
0,
this.#schemeEnd,
);
if (
scheme === "http" || scheme === "https" || scheme === "ftp" ||
scheme === "ws" || scheme === "wss"
) {
return `${scheme}://${this.host}`;
}
if (scheme === "blob") {
// TODO(@littledivy): Fast path.
try {
return new URL(this.pathname).origin;
} catch {
return "null";
}
}
return "null";
}
/** @return {string} */
get password() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L914
if (
this.#hasAuthority() &&
this.#usernameEnd !== this.#serialization.length &&
this.#serialization[this.#usernameEnd] === ":"
) {
return StringPrototypeSlice(
this.#serialization,
this.#usernameEnd + 1,
this.#hostStart - 1,
);
}
return "";
}
/** @param {string} value */
set password(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'password' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_PASSWORD,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {string} */
get pathname() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L1203
if (!this.#queryStart && !this.#fragmentStart) {
return StringPrototypeSlice(this.#serialization, this.#pathStart);
}
const nextComponentStart = this.#queryStart || this.#fragmentStart;
return StringPrototypeSlice(
this.#serialization,
this.#pathStart,
nextComponentStart,
);
}
/** @param {string} value */
set pathname(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'pathname' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_PATHNAME,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {string} */
get port() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L196
if (this.#port === NO_PORT) {
return StringPrototypeSlice(
this.#serialization,
this.#hostEnd,
this.#pathStart,
);
} else {
return StringPrototypeSlice(
this.#serialization,
this.#hostEnd + 1, /* : */
this.#pathStart,
);
}
}
/** @param {string} value */
set port(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'port' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_PORT,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {string} */
get protocol() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L56
return StringPrototypeSlice(
this.#serialization,
0,
this.#schemeEnd + 1, /* : */
);
}
/** @param {string} value */
set protocol(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'protocol' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_PROTOCOL,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {string} */
get search() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L249
const afterPath = this.#queryStart || this.#fragmentStart ||
this.#serialization.length;
const afterQuery = this.#fragmentStart || this.#serialization.length;
return trim(
StringPrototypeSlice(this.#serialization, afterPath, afterQuery),
);
}
/** @param {string} value */
set search(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'search' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_SEARCH,
value,
);
this.#updateComponents();
this.#updateSearchParams();
} catch {
/* pass */
}
}
/** @return {string} */
get username() {
webidl.assertBranded(this, URLPrototype);
// https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L881
const schemeSeparatorLen = 3; /* :// */
if (
this.#hasAuthority() &&
this.#usernameEnd > this.#schemeEnd + schemeSeparatorLen
) {
return StringPrototypeSlice(
this.#serialization,
this.#schemeEnd + schemeSeparatorLen,
this.#usernameEnd,
);
} else {
return "";
}
}
/** @param {string} value */
set username(value) {
webidl.assertBranded(this, URLPrototype);
const prefix = "Failed to set 'username' on 'URL'";
webidl.requiredArguments(arguments.length, 1, prefix);
value = webidl.converters.DOMString(value, prefix, "Argument 1");
try {
this.#serialization = opUrlReparse(
this.#serialization,
SET_USERNAME,
value,
);
this.#updateComponents();
} catch {
/* pass */
}
}
/** @return {URLSearchParams} */
get searchParams() {
if (this.#queryObject == null) {
this.#queryObject = new URLSearchParams(this.search);
this.#queryObject[_urlObject] = this;
}
return this.#queryObject;
}
/** @return {string} */
toString() {
webidl.assertBranded(this, URLPrototype);
return this.#serialization;
}
/** @return {string} */
toJSON() {
webidl.assertBranded(this, URLPrototype);
return this.#serialization;
}
}
webidl.configureInterface(URL);
const URLPrototype = URL.prototype;
/**
* This function implements application/x-www-form-urlencoded parsing.
* https://url.spec.whatwg.org/#concept-urlencoded-parser
* @param {Uint8Array} bytes
* @returns {[string, string][]}
*/
function parseUrlEncoded(bytes) {
return op_url_parse_search_params(null, bytes);
}
webidl
.converters[
"sequence<sequence<USVString>> or record<USVString, USVString> or USVString"
] = (V, prefix, context, opts) => {
// Union for (sequence<sequence<USVString>> or record<USVString, USVString> or USVString)
if (webidl.type(V) === "Object" && V !== null) {
if (V[SymbolIterator] !== undefined) {
return webidl.converters["sequence<sequence<USVString>>"](
V,
prefix,
context,
opts,
);
}
return webidl.converters["record<USVString, USVString>"](
V,
prefix,
context,
opts,
);
}
return webidl.converters.USVString(V, prefix, context, opts);
};
export {
parseUrlEncoded,
URL,
URLPrototype,
URLSearchParams,
URLSearchParamsPrototype,
};