diff options
-rw-r--r-- | cli/tests/unit/dom_iterable_test.ts | 88 | ||||
-rw-r--r-- | cli/tests/unit/fetch_test.ts | 10 | ||||
-rw-r--r-- | cli/tests/unit/headers_test.ts | 48 | ||||
-rw-r--r-- | cli/tests/unit/unit_tests.ts | 1 | ||||
-rw-r--r-- | op_crates/fetch/03_dom_iterable.js | 80 | ||||
-rw-r--r-- | op_crates/fetch/20_headers.js | 502 | ||||
-rw-r--r-- | op_crates/fetch/lib.rs | 4 | ||||
-rw-r--r-- | op_crates/web/00_infra.js | 104 | ||||
-rw-r--r-- | op_crates/web/01_mimesniff.js | 82 | ||||
-rw-r--r-- | op_crates/web/internal.d.ts | 25 | ||||
-rw-r--r-- | op_crates/webidl/00_webidl.js | 92 | ||||
m--------- | test_util/wpt | 0 | ||||
-rw-r--r-- | tools/wpt/expectation.json | 9 | ||||
-rw-r--r-- | tools/wpt/runner.ts | 3 |
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."); } } |