summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/tests/unit/dom_iterable_test.ts88
-rw-r--r--cli/tests/unit/fetch_test.ts10
-rw-r--r--cli/tests/unit/headers_test.ts48
-rw-r--r--cli/tests/unit/unit_tests.ts1
-rw-r--r--op_crates/fetch/03_dom_iterable.js80
-rw-r--r--op_crates/fetch/20_headers.js502
-rw-r--r--op_crates/fetch/lib.rs4
-rw-r--r--op_crates/web/00_infra.js104
-rw-r--r--op_crates/web/01_mimesniff.js82
-rw-r--r--op_crates/web/internal.d.ts25
-rw-r--r--op_crates/webidl/00_webidl.js92
m---------test_util/wpt0
-rw-r--r--tools/wpt/expectation.json9
-rw-r--r--tools/wpt/runner.ts3
14 files changed, 543 insertions, 505 deletions
diff --git a/cli/tests/unit/dom_iterable_test.ts b/cli/tests/unit/dom_iterable_test.ts
deleted file mode 100644
index 4e94cfdba..000000000
--- a/cli/tests/unit/dom_iterable_test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
-
-import { assert, assertEquals, unitTest } from "./test_util.ts";
-
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-function setup() {
- const dataSymbol = Symbol("data symbol");
- class Base {
- [dataSymbol] = new Map<string, number>();
-
- constructor(
- data: Array<[string, number]> | IterableIterator<[string, number]>,
- ) {
- for (const [key, value] of data) {
- this[dataSymbol].set(key, value);
- }
- }
- }
-
- return {
- Base,
- // This is using an internal API we don't want published as types, so having
- // to cast to any to "trick" TypeScript
- // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
- DomIterable: Deno[Deno.internal].DomIterableMixin(Base, dataSymbol),
- };
-}
-
-unitTest(function testDomIterable(): void {
- const { DomIterable, Base } = setup();
-
- const fixture: Array<[string, number]> = [
- ["foo", 1],
- ["bar", 2],
- ];
-
- const domIterable = new DomIterable(fixture);
-
- assertEquals(Array.from(domIterable.entries()), fixture);
- assertEquals(Array.from(domIterable.values()), [1, 2]);
- assertEquals(Array.from(domIterable.keys()), ["foo", "bar"]);
-
- let result: Array<[string, number]> = [];
- for (const [key, value] of domIterable) {
- assert(key != null);
- assert(value != null);
- result.push([key, value]);
- }
- assertEquals(fixture, result);
-
- result = [];
- const scope = {};
- function callback(
- this: typeof scope,
- value: number,
- key: string,
- parent: typeof domIterable,
- ): void {
- assertEquals(parent, domIterable);
- assert(key != null);
- assert(value != null);
- assert(this === scope);
- result.push([key, value]);
- }
- domIterable.forEach(callback, scope);
- assertEquals(fixture, result);
-
- assertEquals(DomIterable.name, Base.name);
-});
-
-unitTest(function testDomIterableScope(): void {
- const { DomIterable } = setup();
-
- const domIterable = new DomIterable([["foo", 1]]);
-
- // deno-lint-ignore no-explicit-any
- function checkScope(thisArg: any, expected: any): void {
- function callback(this: typeof thisArg): void {
- assertEquals(this, expected);
- }
- domIterable.forEach(callback, thisArg);
- }
-
- checkScope(0, Object(0));
- checkScope("", Object(""));
- checkScope(null, window);
- checkScope(undefined, window);
-});
diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts
index 0fb9da8f4..427ab9b53 100644
--- a/cli/tests/unit/fetch_test.ts
+++ b/cli/tests/unit/fetch_test.ts
@@ -661,8 +661,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
- "hello: World\r\n",
"foo: Bar\r\n",
+ "hello: World\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
@@ -695,9 +695,9 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
- "hello: World\r\n",
- "foo: Bar\r\n",
"content-type: text/plain;charset=UTF-8\r\n",
+ "foo: Bar\r\n",
+ "hello: World\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
@@ -733,8 +733,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
- "hello: World\r\n",
"foo: Bar\r\n",
+ "hello: World\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
@@ -1115,8 +1115,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
- "hello: World\r\n",
"foo: Bar\r\n",
+ "hello: World\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
diff --git a/cli/tests/unit/headers_test.ts b/cli/tests/unit/headers_test.ts
index aa8834052..c79673d84 100644
--- a/cli/tests/unit/headers_test.ts
+++ b/cli/tests/unit/headers_test.ts
@@ -1,10 +1,5 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
-import {
- assert,
- assertEquals,
- assertStringIncludes,
- unitTest,
-} from "./test_util.ts";
+import { assert, assertEquals, unitTest } from "./test_util.ts";
const {
inspectArgs,
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
@@ -25,10 +20,7 @@ unitTest(function newHeaderTest(): void {
// deno-lint-ignore no-explicit-any
new Headers(null as any);
} catch (e) {
- assertEquals(
- e.message,
- "Failed to construct 'Headers'; The provided value was not valid",
- );
+ assert(e instanceof TypeError);
}
});
@@ -271,13 +263,11 @@ unitTest(function headerParamsArgumentsCheck(): void {
methodRequireOneParam.forEach((method): void => {
const headers = new Headers();
let hasThrown = 0;
- let errMsg = "";
try {
// deno-lint-ignore no-explicit-any
(headers as any)[method]();
hasThrown = 1;
} catch (err) {
- errMsg = err.message;
if (err instanceof TypeError) {
hasThrown = 2;
} else {
@@ -285,23 +275,17 @@ unitTest(function headerParamsArgumentsCheck(): void {
}
}
assertEquals(hasThrown, 2);
- assertStringIncludes(
- errMsg,
- `${method} requires at least 1 argument, but only 0 present`,
- );
});
methodRequireTwoParams.forEach((method): void => {
const headers = new Headers();
let hasThrown = 0;
- let errMsg = "";
try {
// deno-lint-ignore no-explicit-any
(headers as any)[method]();
hasThrown = 1;
} catch (err) {
- errMsg = err.message;
if (err instanceof TypeError) {
hasThrown = 2;
} else {
@@ -309,19 +293,13 @@ unitTest(function headerParamsArgumentsCheck(): void {
}
}
assertEquals(hasThrown, 2);
- assertStringIncludes(
- errMsg,
- `${method} requires at least 2 arguments, but only 0 present`,
- );
hasThrown = 0;
- errMsg = "";
try {
// deno-lint-ignore no-explicit-any
(headers as any)[method]("foo");
hasThrown = 1;
} catch (err) {
- errMsg = err.message;
if (err instanceof TypeError) {
hasThrown = 2;
} else {
@@ -329,10 +307,6 @@ unitTest(function headerParamsArgumentsCheck(): void {
}
}
assertEquals(hasThrown, 2);
- assertStringIncludes(
- errMsg,
- `${method} requires at least 2 arguments, but only 1 present`,
- );
});
});
@@ -361,8 +335,8 @@ unitTest(function headersAppendMultiple(): void {
const actual = [...headers];
assertEquals(actual, [
["set-cookie", "foo=bar"],
- ["x-deno", "foo, bar"],
["set-cookie", "bar=baz"],
+ ["x-deno", "foo, bar"],
]);
});
@@ -372,22 +346,12 @@ unitTest(function headersAppendDuplicateSetCookieKey(): void {
headers.append("Set-cookie", "baz=bar");
const actual = [...headers];
assertEquals(actual, [
+ ["set-cookie", "foo=bar"],
["set-cookie", "foo=baz"],
["set-cookie", "baz=bar"],
]);
});
-unitTest(function headersSetDuplicateCookieKey(): void {
- const headers = new Headers([["Set-Cookie", "foo=bar"]]);
- headers.set("set-Cookie", "foo=baz");
- headers.set("set-cookie", "bar=qat");
- const actual = [...headers];
- assertEquals(actual, [
- ["set-cookie", "foo=baz"],
- ["set-cookie", "bar=qat"],
- ]);
-});
-
unitTest(function headersGetSetCookie(): void {
const headers = new Headers([
["Set-Cookie", "foo=bar"],
@@ -411,7 +375,7 @@ unitTest(function customInspectReturnsCorrectHeadersFormat(): void {
const singleHeader = new Headers([["Content-Type", "application/json"]]);
assertEquals(
stringify(singleHeader),
- "Headers { content-type: application/json }",
+ `Headers { "content-type": "application/json" }`,
);
const multiParamHeader = new Headers([
["Content-Type", "application/json"],
@@ -419,6 +383,6 @@ unitTest(function customInspectReturnsCorrectHeadersFormat(): void {
]);
assertEquals(
stringify(multiParamHeader),
- "Headers { content-type: application/json, content-length: 1337 }",
+ `Headers { "content-length": "1337", "content-type": "application/json" }`,
);
});
diff --git a/cli/tests/unit/unit_tests.ts b/cli/tests/unit/unit_tests.ts
index 4cfd3d961..28924165f 100644
--- a/cli/tests/unit/unit_tests.ts
+++ b/cli/tests/unit/unit_tests.ts
@@ -35,7 +35,6 @@ import "./io_test.ts";
import "./link_test.ts";
import "./make_temp_test.ts";
import "./metrics_test.ts";
-import "./dom_iterable_test.ts";
import "./mkdir_test.ts";
import "./net_test.ts";
import "./os_test.ts";
diff --git a/op_crates/fetch/03_dom_iterable.js b/op_crates/fetch/03_dom_iterable.js
deleted file mode 100644
index 2a3c72fba..000000000
--- a/op_crates/fetch/03_dom_iterable.js
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
-"use strict";
-
-((window) => {
- const { requiredArguments } = window.__bootstrap.fetchUtil;
-
- function DomIterableMixin(
- Base,
- dataSymbol,
- ) {
- // we have to cast `this` as `any` because there is no way to describe the
- // Base class in a way where the Symbol `dataSymbol` is defined. So the
- // runtime code works, but we do lose a little bit of type safety.
-
- // Additionally, we have to not use .keys() nor .values() since the internal
- // slot differs in type - some have a Map, which yields [K, V] in
- // Symbol.iterator, and some have an Array, which yields V, in this case
- // [K, V] too as they are arrays of tuples.
-
- const DomIterable = class extends Base {
- *entries() {
- for (const entry of this[dataSymbol]) {
- yield entry;
- }
- }
-
- *keys() {
- for (const [key] of this[dataSymbol]) {
- yield key;
- }
- }
-
- *values() {
- for (const [, value] of this[dataSymbol]) {
- yield value;
- }
- }
-
- forEach(
- callbackfn,
- thisArg,
- ) {
- requiredArguments(
- `${this.constructor.name}.forEach`,
- arguments.length,
- 1,
- );
- callbackfn = callbackfn.bind(
- thisArg == null ? globalThis : Object(thisArg),
- );
- for (const [key, value] of this[dataSymbol]) {
- callbackfn(value, key, this);
- }
- }
-
- *[Symbol.iterator]() {
- for (const entry of this[dataSymbol]) {
- yield entry;
- }
- }
- };
-
- // we want the Base class name to be the name of the class.
- Object.defineProperty(DomIterable, "name", {
- value: Base.name,
- configurable: true,
- });
-
- return DomIterable;
- }
-
- window.__bootstrap.internals = {
- ...window.__bootstrap.internals ?? {},
- DomIterableMixin,
- };
-
- window.__bootstrap.domIterable = {
- DomIterableMixin,
- };
-})(this);
diff --git a/op_crates/fetch/20_headers.js b/op_crates/fetch/20_headers.js
index df11d40b6..ce46e5dee 100644
--- a/op_crates/fetch/20_headers.js
+++ b/op_crates/fetch/20_headers.js
@@ -1,247 +1,327 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../file/internal.d.ts" />
+/// <reference path="../file/lib.deno_file.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <reference path="./11_streams_types.d.ts" />
+/// <reference path="./lib.deno_fetch.d.ts" />
+/// <reference lib="esnext" />
"use strict";
((window) => {
- const { DomIterableMixin } = window.__bootstrap.domIterable;
- const { requiredArguments } = window.__bootstrap.fetchUtil;
-
- // From node-fetch
- // Copyright (c) 2016 David Frank. MIT License.
- const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
- const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
-
- function isHeaders(value) {
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
- return value instanceof Headers;
- }
+ const webidl = window.__bootstrap.webidl;
+ const {
+ HTTP_WHITESPACE_PREFIX_RE,
+ HTTP_WHITESPACE_SUFFIX_RE,
+ HTTP_TOKEN_CODE_POINT_RE,
+ byteLowerCase,
+ } = window.__bootstrap.infra;
- const headersData = Symbol("headers data");
+ const _headerList = Symbol("header list");
+ const _iterableHeaders = Symbol("iterable headers");
+ const _guard = Symbol("guard");
- // TODO(bartlomieju): headerGuard? Investigate if it is needed
- // node-fetch did not implement this but it is in the spec
- function normalizeParams(name, value) {
- name = String(name).toLowerCase();
- value = String(value).trim();
- return [name, value];
- }
+ /**
+ * @typedef Header
+ * @type {[string, string]}
+ */
- // The following name/value validations are copied from
- // https://github.com/bitinn/node-fetch/blob/master/src/headers.js
- // Copyright (c) 2016 David Frank. MIT License.
- function validateName(name) {
- if (invalidTokenRegex.test(name) || name === "") {
- throw new TypeError(`${name} is not a legal HTTP header name`);
- }
- }
+ /**
+ * @typedef HeaderList
+ * @type {Header[]}
+ */
- function validateValue(value) {
- if (invalidHeaderCharRegex.test(value)) {
- throw new TypeError(`${value} is not a legal HTTP header value`);
- }
+ /**
+ * @typedef {string} potentialValue
+ * @returns {string}
+ */
+ function normalizeHeaderValue(potentialValue) {
+ potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_PREFIX_RE, "");
+ potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
+ return potentialValue;
}
- /** Appends a key and value to the header list.
- *
- * The spec indicates that when a key already exists, the append adds the new
- * value onto the end of the existing value. The behaviour of this though
- * varies when the key is `set-cookie`. In this case, if the key of the cookie
- * already exists, the value is replaced, but if the key of the cookie does not
- * exist, and additional `set-cookie` header is added.
- *
- * The browser specification of `Headers` is written for clients, and not
- * servers, and Deno is a server, meaning that it needs to follow the patterns
- * expected for servers, of which a `set-cookie` header is expected for each
- * unique cookie key, but duplicate cookie keys should not exist. */
- function dataAppend(
- data,
- key,
- value,
- ) {
- for (let i = 0; i < data.length; i++) {
- const [dataKey] = data[i];
- if (key === "set-cookie" && dataKey === "set-cookie") {
- const [, dataValue] = data[i];
- const [dataCookieKey] = dataValue.split("=");
- const [cookieKey] = value.split("=");
- if (dataCookieKey === cookieKey) {
- data[i][1] = value;
- return;
- }
- } else {
- if (dataKey === key) {
- data[i][1] += `, ${value}`;
- return;
+ /**
+ * @param {Headers} headers
+ * @param {HeadersInit} object
+ */
+ function fillHeaders(headers, object) {
+ if (Array.isArray(object)) {
+ for (const header of object) {
+ if (header.length !== 2) {
+ throw new TypeError(
+ `Invalid header. Length must be 2, but is ${header.length}`,
+ );
}
+ appendHeader(headers, header[0], header[1]);
+ }
+ } else {
+ for (const key of Object.keys(object)) {
+ appendHeader(headers, key, object[key]);
}
}
- data.push([key, value]);
}
- /** Gets a value of a key in the headers list.
- *
- * This varies slightly from spec behaviour in that when the key is `set-cookie`
- * the value returned will look like a concatenated value, when in fact, if the
- * headers were iterated over, each individual `set-cookie` value is a unique
- * entry in the headers list. */
- function dataGet(
- data,
- key,
- ) {
- const setCookieValues = [];
- for (const [dataKey, value] of data) {
- if (dataKey === key) {
- if (key === "set-cookie") {
- setCookieValues.push(value);
- } else {
- return value;
- }
- }
+ /**
+ * https://fetch.spec.whatwg.org/#concept-headers-append
+ * @param {Headers} headers
+ * @param {string} name
+ * @param {string} value
+ */
+ function appendHeader(headers, name, value) {
+ // 1.
+ value = normalizeHeaderValue(value);
+
+ // 2.
+ if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
+ throw new TypeError("Header name is not valid.");
}
- if (setCookieValues.length) {
- return setCookieValues.join(", ");
+ if (
+ value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D")
+ ) {
+ throw new TypeError("Header value is not valid.");
}
- return undefined;
- }
- /** Sets a value of a key in the headers list.
- *
- * The spec indicates that the value should be replaced if the key already
- * exists. The behaviour here varies, where if the key is `set-cookie` the key
- * of the cookie is inspected, and if the key of the cookie already exists,
- * then the value is replaced. If the key of the cookie is not found, then
- * the value of the `set-cookie` is added to the list of headers.
- *
- * The browser specification of `Headers` is written for clients, and not
- * servers, and Deno is a server, meaning that it needs to follow the patterns
- * expected for servers, of which a `set-cookie` header is expected for each
- * unique cookie key, but duplicate cookie keys should not exist. */
- function dataSet(
- data,
- key,
- value,
- ) {
- for (let i = 0; i < data.length; i++) {
- const [dataKey] = data[i];
- if (dataKey === key) {
- // there could be multiple set-cookie headers, but all others are unique
- if (key === "set-cookie") {
- const [, dataValue] = data[i];
- const [dataCookieKey] = dataValue.split("=");
- const [cookieKey] = value.split("=");
- if (cookieKey === dataCookieKey) {
- data[i][1] = value;
- return;
- }
- } else {
- data[i][1] = value;
- return;
- }
- }
+ // 3.
+ if (headers[_guard] == "immutable") {
+ throw new TypeError("Headers are immutable.");
}
- data.push([key, value]);
- }
- function dataDelete(data, key) {
- let i = 0;
- while (i < data.length) {
- const [dataKey] = data[i];
- if (dataKey === key) {
- data.splice(i, 1);
- } else {
- i++;
+ // 7.
+ const list = headers[_headerList];
+ const lowercaseName = byteLowerCase(name);
+ for (let i = 0; i < list.length; i++) {
+ if (byteLowerCase(list[i][0]) === lowercaseName) {
+ name = list[i][0];
+ break;
}
}
+ list.push([name, value]);
}
- function dataHas(data, key) {
- for (const [dataKey] of data) {
- if (dataKey === key) {
- return true;
- }
+ /**
+ * @param {HeaderList} list
+ * @param {string} name
+ */
+ function getHeader(list, name) {
+ const lowercaseName = byteLowerCase(name);
+ const entries = list.filter((entry) =>
+ byteLowerCase(entry[0]) === lowercaseName
+ ).map((entry) => entry[1]);
+ if (entries.length === 0) {
+ return null;
+ } else {
+ return entries.join("\x2C\x20");
}
- return false;
}
- // ref: https://fetch.spec.whatwg.org/#dom-headers
- class HeadersBase {
- constructor(init) {
- if (init === null) {
- throw new TypeError(
- "Failed to construct 'Headers'; The provided value was not valid",
- );
- } else if (isHeaders(init)) {
- this[headersData] = [...init];
- } else {
- this[headersData] = [];
- if (Array.isArray(init)) {
- for (const tuple of init) {
- // If header does not contain exactly two items,
- // then throw a TypeError.
- // ref: https://fetch.spec.whatwg.org/#concept-headers-fill
- requiredArguments(
- "Headers.constructor tuple array argument",
- tuple.length,
- 2,
- );
-
- this.append(tuple[0], tuple[1]);
- }
- } else if (init) {
- for (const [rawName, rawValue] of Object.entries(init)) {
- this.append(rawName, rawValue);
+ class Headers {
+ /** @type {HeaderList} */
+ [_headerList] = [];
+ /** @type {"immutable"| "request"| "request-no-cors"| "response" | "none"} */
+ [_guard];
+
+ get [_iterableHeaders]() {
+ const list = this[_headerList];
+
+ const headers = [];
+ const headerNamesSet = new Set();
+ for (const entry of list) {
+ headerNamesSet.add(byteLowerCase(entry[0]));
+ }
+ const names = [...headerNamesSet].sort();
+ for (const name of names) {
+ // The following if statement, and if block of the following statement
+ // are not spec compliant. `set-cookie` is the only header that can not
+ // be concatentated, so must be given to the user as multiple headers.
+ // The else block of the if statement is spec compliant again.
+ if (name == "set-cookie") {
+ const setCookie = list.filter((entry) =>
+ byteLowerCase(entry[0]) === "set-cookie"
+ );
+ if (setCookie.length === 0) throw new TypeError("Unreachable");
+ for (const entry of setCookie) {
+ headers.push([name, entry[1]]);
}
+ } else {
+ const value = getHeader(list, name);
+ if (value === null) throw new TypeError("Unreachable");
+ headers.push([name, value]);
}
}
+ return headers;
}
- [Symbol.for("Deno.customInspect")]() {
- let length = this[headersData].length;
- let output = "";
- for (const [key, value] of this[headersData]) {
- const prefix = length === this[headersData].length ? " " : "";
- const postfix = length === 1 ? " " : ", ";
- output = output + `${prefix}${key}: ${value}${postfix}`;
- length--;
+ /** @param {HeadersInit} [init] */
+ constructor(init = undefined) {
+ const prefix = "Failed to construct 'Event'";
+ if (init !== undefined) {
+ init = webidl.converters["HeadersInit"](init, {
+ prefix,
+ context: "Argument 1",
+ });
+ }
+
+ this[webidl.brand] = webidl.brand;
+ this[_guard] = "none";
+ if (init !== undefined) {
+ fillHeaders(this, init);
}
- return `Headers {${output}}`;
}
- // ref: https://fetch.spec.whatwg.org/#concept-headers-append
+ /**
+ * @param {string} name
+ * @param {string} value
+ */
append(name, value) {
- requiredArguments("Headers.append", arguments.length, 2);
- const [newname, newvalue] = normalizeParams(name, value);
- validateName(newname);
- validateValue(newvalue);
- dataAppend(this[headersData], newname, newvalue);
+ webidl.assertBranded(this, Headers);
+ const prefix = "Failed to execute 'append' on 'Headers'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+ name = webidl.converters["ByteString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ value = webidl.converters["ByteString"](value, {
+ prefix,
+ context: "Argument 2",
+ });
+ appendHeader(this, name, value);
}
+ /**
+ * @param {string} name
+ */
delete(name) {
- requiredArguments("Headers.delete", arguments.length, 1);
- const [newname] = normalizeParams(name);
- validateName(newname);
- dataDelete(this[headersData], newname);
+ const prefix = "Failed to execute 'delete' on 'Headers'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ name = webidl.converters["ByteString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
+ throw new TypeError("Header name is not valid.");
+ }
+ if (this[_guard] == "immutable") {
+ throw new TypeError("Headers are immutable.");
+ }
+
+ const list = this[_headerList];
+ const lowercaseName = byteLowerCase(name);
+ for (let i = 0; i < list.length; i++) {
+ if (byteLowerCase(list[i][0]) === lowercaseName) {
+ list.splice(i, 1);
+ i--;
+ }
+ }
}
+ /**
+ * @param {string} name
+ */
get(name) {
- requiredArguments("Headers.get", arguments.length, 1);
- const [newname] = normalizeParams(name);
- validateName(newname);
- return dataGet(this[headersData], newname) ?? null;
+ const prefix = "Failed to execute 'get' on 'Headers'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ name = webidl.converters["ByteString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
+ throw new TypeError("Header name is not valid.");
+ }
+
+ const list = this[_headerList];
+ return getHeader(list, name);
}
+ /**
+ * @param {string} name
+ */
has(name) {
- requiredArguments("Headers.has", arguments.length, 1);
- const [newname] = normalizeParams(name);
- validateName(newname);
- return dataHas(this[headersData], newname);
+ const prefix = "Failed to execute 'has' on 'Headers'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ name = webidl.converters["ByteString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
+ throw new TypeError("Header name is not valid.");
+ }
+
+ const list = this[_headerList];
+ const lowercaseName = byteLowerCase(name);
+ for (let i = 0; i < list.length; i++) {
+ if (byteLowerCase(list[i][0]) === lowercaseName) {
+ return true;
+ }
+ }
+ return false;
}
+ /**
+ * @param {string} name
+ * @param {string} value
+ */
set(name, value) {
- requiredArguments("Headers.set", arguments.length, 2);
- const [newName, newValue] = normalizeParams(name, value);
- validateName(newName);
- validateValue(newValue);
- dataSet(this[headersData], newName, newValue);
+ webidl.assertBranded(this, Headers);
+ const prefix = "Failed to execute 'set' on 'Headers'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+ name = webidl.converters["ByteString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ value = webidl.converters["ByteString"](value, {
+ prefix,
+ context: "Argument 2",
+ });
+
+ value = normalizeHeaderValue(value);
+
+ // 2.
+ if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
+ throw new TypeError("Header name is not valid.");
+ }
+ if (
+ value.includes("\x00") || value.includes("\x0A") ||
+ value.includes("\x0D")
+ ) {
+ throw new TypeError("Header value is not valid.");
+ }
+
+ if (this[_guard] == "immutable") {
+ throw new TypeError("Headers are immutable.");
+ }
+
+ const list = this[_headerList];
+ const lowercaseName = byteLowerCase(name);
+ let added = false;
+ for (let i = 0; i < list.length; i++) {
+ if (byteLowerCase(list[i][0]) === lowercaseName) {
+ if (!added) {
+ list[i][1] = value;
+ added = true;
+ } else {
+ list.splice(i, 1);
+ i--;
+ }
+ }
+ }
+ if (!added) {
+ list.push([name, value]);
+ }
+ }
+
+ [Symbol.for("Deno.customInspect")](inspect) {
+ const headers = {};
+ for (const header of this) {
+ headers[header[0]] = header[1];
+ }
+ return `Headers ${inspect(headers)}`;
}
get [Symbol.toStringTag]() {
@@ -249,7 +329,35 @@
}
}
- class Headers extends DomIterableMixin(HeadersBase, headersData) {}
+ webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1);
+
+ webidl.converters["sequence<ByteString>"] = webidl
+ .createSequenceConverter(webidl.converters["ByteString"]);
+ webidl.converters["sequence<sequence<ByteString>>"] = webidl
+ .createSequenceConverter(webidl.converters["sequence<ByteString>"]);
+ webidl.converters["record<ByteString, ByteString>"] = webidl
+ .createRecordConverter(
+ webidl.converters["ByteString"],
+ webidl.converters["ByteString"],
+ );
+ webidl.converters["HeadersInit"] = (V, opts) => {
+ // Union for (sequence<sequence<ByteString>> or record<ByteString, ByteString>)
+ if (typeof V === "object" && V !== null) {
+ if (V[Symbol.iterator] !== undefined) {
+ return webidl.converters["sequence<sequence<ByteString>>"](V, opts);
+ }
+ return webidl.converters["record<ByteString, ByteString>"](V, opts);
+ }
+ throw webidl.makeException(
+ TypeError,
+ "The provided value is not of type '(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'",
+ opts,
+ );
+ };
+ webidl.converters["Headers"] = webidl.createInterfaceConverter(
+ "Headers",
+ Headers,
+ );
window.__bootstrap.headers = {
Headers,
diff --git a/op_crates/fetch/lib.rs b/op_crates/fetch/lib.rs
index 9eeda059a..030f8a809 100644
--- a/op_crates/fetch/lib.rs
+++ b/op_crates/fetch/lib.rs
@@ -59,10 +59,6 @@ pub fn init(isolate: &mut JsRuntime) {
include_str!("01_fetch_util.js"),
),
(
- "deno:op_crates/fetch/03_dom_iterable.js",
- include_str!("03_dom_iterable.js"),
- ),
- (
"deno:op_crates/fetch/11_streams.js",
include_str!("11_streams.js"),
),
diff --git a/op_crates/web/00_infra.js b/op_crates/web/00_infra.js
index 0590ffd03..ff9cb7cd4 100644
--- a/op_crates/web/00_infra.js
+++ b/op_crates/web/00_infra.js
@@ -8,6 +8,74 @@
"use strict";
((window) => {
+ const ASCII_DIGIT = ["\u0030-\u0039"];
+ const ASCII_UPPER_ALPHA = ["\u0041-\u005A"];
+ const ASCII_LOWER_ALPHA = ["\u0061-\u007A"];
+ const ASCII_ALPHA = [...ASCII_UPPER_ALPHA, ...ASCII_LOWER_ALPHA];
+ const ASCII_ALPHANUMERIC = [...ASCII_DIGIT, ...ASCII_ALPHA];
+
+ const HTTP_TAB_OR_SPACE = ["\u0009", "\u0020"];
+ const HTTP_WHITESPACE = ["\u000A", "\u000D", ...HTTP_TAB_OR_SPACE];
+
+ const HTTP_TOKEN_CODE_POINT = [
+ "\u0021",
+ "\u0023",
+ "\u0024",
+ "\u0025",
+ "\u0026",
+ "\u0027",
+ "\u002A",
+ "\u002B",
+ "\u002D",
+ "\u002E",
+ "\u005E",
+ "\u005F",
+ "\u0060",
+ "\u007C",
+ "\u007E",
+ ...ASCII_ALPHANUMERIC,
+ ];
+ const HTTP_TOKEN_CODE_POINT_RE = new RegExp(
+ `^[${regexMatcher(HTTP_TOKEN_CODE_POINT)}]+$`,
+ );
+ const HTTP_QUOTED_STRING_TOKEN_POINT = [
+ "\u0009",
+ "\u0020-\u007E",
+ "\u0080-\u00FF",
+ ];
+ const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new RegExp(
+ `^[${regexMatcher(HTTP_QUOTED_STRING_TOKEN_POINT)}]+$`,
+ );
+ const HTTP_WHITESPACE_MATCHER = regexMatcher(HTTP_WHITESPACE);
+ const HTTP_WHITESPACE_PREFIX_RE = new RegExp(
+ `^[${HTTP_WHITESPACE_MATCHER}]+`,
+ "g",
+ );
+ const HTTP_WHITESPACE_SUFFIX_RE = new RegExp(
+ `[${HTTP_WHITESPACE_MATCHER}]+$`,
+ "g",
+ );
+
+ /**
+ * Turn a string of chars into a regex safe matcher.
+ * @param {string[]} chars
+ * @returns {string}
+ */
+ function regexMatcher(chars) {
+ const matchers = chars.map((char) => {
+ if (char.length === 1) {
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
+ } else if (char.length === 3 && char[1] === "-") {
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}-\\u${
+ char.charCodeAt(2).toString(16).padStart(4, "0")
+ }`;
+ } else {
+ throw TypeError("unreachable");
+ }
+ });
+ return matchers.join("");
+ }
+
/**
* https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
* @param {string} input
@@ -25,7 +93,43 @@
return { result: input.slice(start, position), position };
}
+ /**
+ * @param {string} s
+ * @returns {string}
+ */
+ function byteUpperCase(s) {
+ return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) {
+ return c.toUpperCase();
+ });
+ }
+
+ /**
+ * @param {string} s
+ * @returns {string}
+ */
+ function byteLowerCase(s) {
+ return String(s).replace(/[A-Z]/g, function byteUpperCaseReplace(c) {
+ return c.toLowerCase();
+ });
+ }
+
window.__bootstrap.infra = {
collectSequenceOfCodepoints,
+ ASCII_DIGIT,
+ ASCII_UPPER_ALPHA,
+ ASCII_LOWER_ALPHA,
+ ASCII_ALPHA,
+ ASCII_ALPHANUMERIC,
+ HTTP_TAB_OR_SPACE,
+ HTTP_WHITESPACE,
+ HTTP_TOKEN_CODE_POINT,
+ HTTP_TOKEN_CODE_POINT_RE,
+ HTTP_QUOTED_STRING_TOKEN_POINT,
+ HTTP_QUOTED_STRING_TOKEN_POINT_RE,
+ HTTP_WHITESPACE_PREFIX_RE,
+ HTTP_WHITESPACE_SUFFIX_RE,
+ regexMatcher,
+ byteUpperCase,
+ byteLowerCase,
};
})(globalThis);
diff --git a/op_crates/web/01_mimesniff.js b/op_crates/web/01_mimesniff.js
index f58130132..534e39c31 100644
--- a/op_crates/web/01_mimesniff.js
+++ b/op_crates/web/01_mimesniff.js
@@ -8,72 +8,14 @@
"use strict";
((window) => {
- const { collectSequenceOfCodepoints } = window.__bootstrap.infra;
-
- /**
- * @param {string[]} chars
- * @returns {string}
- */
- function regexMatcher(chars) {
- const matchers = chars.map((char) => {
- if (char.length === 1) {
- return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
- } else if (char.length === 3 && char[1] === "-") {
- return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}-\\u${
- char.charCodeAt(2).toString(16).padStart(4, "0")
- }`;
- } else {
- throw TypeError("unreachable");
- }
- });
- return matchers.join("");
- }
-
- const HTTP_TAB_OR_SPACE = ["\u0009", "\u0020"];
- const HTTP_WHITESPACE = ["\u000A", "\u000D", ...HTTP_TAB_OR_SPACE];
-
- const ASCII_DIGIT = ["\u0030-\u0039"];
- const ASCII_UPPER_ALPHA = ["\u0041-\u005A"];
- const ASCII_LOWER_ALPHA = ["\u0061-\u007A"];
- const ASCII_ALPHA = [...ASCII_UPPER_ALPHA, ...ASCII_LOWER_ALPHA];
- const ASCII_ALPHANUMERIC = [...ASCII_DIGIT, ...ASCII_ALPHA];
- const HTTP_TOKEN_CODE_POINT = [
- "\u0021",
- "\u0023",
- "\u0025",
- "\u0026",
- "\u0027",
- "\u002A",
- "\u002B",
- "\u002D",
- "\u002E",
- "\u005E",
- "\u005F",
- "\u0060",
- "\u007C",
- "\u007E",
- ...ASCII_ALPHANUMERIC,
- ];
- const HTTP_TOKEN_CODE_POINT_RE = new RegExp(
- `^[${regexMatcher(HTTP_TOKEN_CODE_POINT)}]+$`,
- );
- const HTTP_QUOTED_STRING_TOKEN_POINT = [
- "\u0009",
- "\u0020-\u007E",
- "\u0080-\u00FF",
- ];
- const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new RegExp(
- `^[${regexMatcher(HTTP_QUOTED_STRING_TOKEN_POINT)}]+$`,
- );
- const HTTP_WHITESPACE_MATCHER = regexMatcher(HTTP_WHITESPACE);
- const HTTP_WHITESPACE_PREFIX_RE = new RegExp(
- `^[${HTTP_WHITESPACE_MATCHER}]+`,
- "g",
- );
- const HTTP_WHITESPACE_SUFFIX_RE = new RegExp(
- `[${HTTP_WHITESPACE_MATCHER}]+$`,
- "g",
- );
+ const {
+ collectSequenceOfCodepoints,
+ HTTP_WHITESPACE,
+ HTTP_WHITESPACE_PREFIX_RE,
+ HTTP_WHITESPACE_SUFFIX_RE,
+ HTTP_QUOTED_STRING_TOKEN_POINT_RE,
+ HTTP_TOKEN_CODE_POINT_RE,
+ } = window.__bootstrap.infra;
/**
* https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
@@ -132,7 +74,15 @@
}
/**
+ * @typedef MimeType
+ * @property {string} type
+ * @property {string} subtype
+ * @property {Map<string,string>} parameters
+ */
+
+ /**
* @param {string} input
+ * @returns {MimeType | null}
*/
function parseMimeType(input) {
// 1.
diff --git a/op_crates/web/internal.d.ts b/op_crates/web/internal.d.ts
index 5681edc7b..a5b653218 100644
--- a/op_crates/web/internal.d.ts
+++ b/op_crates/web/internal.d.ts
@@ -17,15 +17,32 @@ declare namespace globalThis {
result: string;
position: number;
};
+ ASCII_DIGIT: string[];
+ ASCII_UPPER_ALPHA: string[];
+ ASCII_LOWER_ALPHA: string[];
+ ASCII_ALPHA: string[];
+ ASCII_ALPHANUMERIC: string[];
+ HTTP_TAB_OR_SPACE: string[];
+ HTTP_WHITESPACE: string[];
+ HTTP_TOKEN_CODE_POINT: string[];
+ HTTP_TOKEN_CODE_POINT_RE: RegExp;
+ HTTP_QUOTED_STRING_TOKEN_POINT: string[];
+ HTTP_QUOTED_STRING_TOKEN_POINT_RE: RegExp;
+ HTTP_WHITESPACE_PREFIX_RE: RegExp;
+ HTTP_WHITESPACE_SUFFIX_RE: RegExp;
+ regexMatcher(chars: string[]): string;
+ byteUpperCase(s: string): string;
+ byteLowerCase(s: string): string;
};
- declare var mimesniff: {
- parseMimeType(input: string): {
+ declare namespace mimesniff {
+ declare interface MimeType {
type: string;
subtype: string;
parameters: Map<string, string>;
- } | null;
- };
+ }
+ declare function parseMimeType(input: string): MimeType | null;
+ }
declare var eventTarget: {
EventTarget: typeof EventTarget;
diff --git a/op_crates/webidl/00_webidl.js b/op_crates/webidl/00_webidl.js
index 508abe44d..63946c9a1 100644
--- a/op_crates/webidl/00_webidl.js
+++ b/op_crates/webidl/00_webidl.js
@@ -764,12 +764,16 @@
opts,
);
}
+ const keys = Reflect.ownKeys(V);
const result = {};
- for (const key of V) {
- const typedKey = keyConverter(key, opts);
- const value = V[key];
- const typedValue = valueConverter(value, opts);
- result[typedKey] = typedValue;
+ for (const key of keys) {
+ const desc = Object.getOwnPropertyDescriptor(V, key);
+ if (desc !== undefined && desc.enumerable === true) {
+ const typedKey = keyConverter(key, opts);
+ const value = V[key];
+ const typedValue = valueConverter(value, opts);
+ result[typedKey] = typedValue;
+ }
}
return result;
};
@@ -802,29 +806,81 @@
throw new TypeError("Illegal constructor");
}
+ function define(target, source) {
+ for (const key of Reflect.ownKeys(source)) {
+ const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
+ if (descriptor && !Reflect.defineProperty(target, key, descriptor)) {
+ throw new TypeError(`Cannot redefine property: ${String(key)}`);
+ }
+ }
+ }
+
+ const _iteratorInternal = Symbol("iterator internal");
+
+ const globalIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(
+ [][Symbol.iterator](),
+ ));
+
function mixinPairIterable(name, prototype, dataSymbol, keyKey, valueKey) {
+ const iteratorPrototype = Object.create(globalIteratorPrototype, {
+ [Symbol.toStringTag]: { configurable: true, value: `${name} Iterator` },
+ });
+ define(iteratorPrototype, {
+ next() {
+ const internal = this && this[_iteratorInternal];
+ if (!internal) {
+ throw new TypeError(
+ `next() called on a value that is not a ${name} iterator object`,
+ );
+ }
+ const { target, kind, index } = internal;
+ const values = target[dataSymbol];
+ const len = values.length;
+ if (index >= len) {
+ return { value: undefined, done: true };
+ }
+ const pair = values[index];
+ internal.index = index + 1;
+ let result;
+ switch (kind) {
+ case "key":
+ result = pair[keyKey];
+ break;
+ case "value":
+ result = pair[valueKey];
+ break;
+ case "key+value":
+ result = [pair[keyKey], pair[valueKey]];
+ break;
+ }
+ return { value: result, done: false };
+ },
+ });
+ function createDefaultIterator(target, kind) {
+ const iterator = Object.create(iteratorPrototype);
+ Object.defineProperty(iterator, _iteratorInternal, {
+ value: { target, kind, index: 0 },
+ configurable: true,
+ });
+ return iterator;
+ }
+
const methods = {
- *entries() {
+ entries() {
assertBranded(this, prototype);
- for (const entry of this[dataSymbol]) {
- yield [entry[keyKey], entry[valueKey]];
- }
+ return createDefaultIterator(this, "key+value");
},
[Symbol.iterator]() {
assertBranded(this, prototype);
- return this.entries();
+ return createDefaultIterator(this, "key+value");
},
- *keys() {
+ keys() {
assertBranded(this, prototype);
- for (const entry of this[dataSymbol]) {
- yield entry[keyKey];
- }
+ return createDefaultIterator(this, "key");
},
- *values() {
+ values() {
assertBranded(this, prototype);
- for (const entry of this[dataSymbol]) {
- yield entry[valueKey];
- }
+ return createDefaultIterator(this, "value");
},
forEach(idlCallback, thisArg) {
assertBranded(this, prototype);
diff --git a/test_util/wpt b/test_util/wpt
-Subproject e19bdbe96243f2ba548c1fd01c0812d645ba0c6
+Subproject 579608584916d582d38d0159666aae9a6aaf07a
diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json
index 98fc105d5..5291b95f2 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -684,6 +684,15 @@
"Check isReloadNavigation attribute",
"Check isHistoryNavigation attribute"
]
+ },
+ "headers": {
+ "headers-basic.any.js": true,
+ "headers-casing.any.js": true,
+ "headers-combine.any.js": true,
+ "headers-errors.any.js": true,
+ "headers-normalize.any.js": true,
+ "headers-record.any.js": true,
+ "headers-structure.any.js": true
}
},
"data-urls": {
diff --git a/tools/wpt/runner.ts b/tools/wpt/runner.ts
index 28b2db0ee..4949c6269 100644
--- a/tools/wpt/runner.ts
+++ b/tools/wpt/runner.ts
@@ -25,6 +25,9 @@ export async function runWithTestUtil<T>(
}
const passedTime = performance.now() - start;
if (passedTime > 15000) {
+ proc.kill(2);
+ await proc.status();
+ proc.close();
throw new Error("Timed out while trying to start wpt test util.");
}
}