1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 07:14:47 -05:00

chore: align Headers to spec (#10199)

This commit aligns `Headers` to spec. It also removes the now unused
03_dom_iterable.js file. We now pass all relevant `Headers` WPT. We do
not implement any sort of header filtering, as we are a server side
runtime.

This is likely not the most efficient implementation of `Headers` yet.
It is however spec compliant. Once all the APIs in the `HTTP` hot loop
are correct we can start optimizing them. It is likely that this commit
reduces bench throughput temporarily.
This commit is contained in:
Luca Casonato 2021-04-19 01:00:13 +02:00 committed by GitHub
parent 0c5ecec8f6
commit 0552eaf569
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 552 additions and 514 deletions

View file

@ -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);
});

View file

@ -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",

View file

@ -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" }`,
);
});

View file

@ -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";

View file

@ -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);

View file

@ -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;
const webidl = window.__bootstrap.webidl;
const {
HTTP_WHITESPACE_PREFIX_RE,
HTTP_WHITESPACE_SUFFIX_RE,
HTTP_TOKEN_CODE_POINT_RE,
byteLowerCase,
} = window.__bootstrap.infra;
// From node-fetch
// Copyright (c) 2016 David Frank. MIT License.
const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
const _headerList = Symbol("header list");
const _iterableHeaders = Symbol("iterable headers");
const _guard = Symbol("guard");
function isHeaders(value) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return value instanceof Headers;
/**
* @typedef Header
* @type {[string, string]}
*/
/**
* @typedef HeaderList
* @type {Header[]}
*/
/**
* @typedef {string} potentialValue
* @returns {string}
*/
function normalizeHeaderValue(potentialValue) {
potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_PREFIX_RE, "");
potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
return potentialValue;
}
const headersData = Symbol("headers data");
// 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];
}
// 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`);
}
}
function validateValue(value) {
if (invalidHeaderCharRegex.test(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`);
}
}
/** 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 (
value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D")
) {
throw new TypeError("Header value is not valid.");
}
// 3.
if (headers[_guard] == "immutable") {
throw new TypeError("Headers are immutable.");
}
// 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;
}
}
if (setCookieValues.length) {
return setCookieValues.join(", ");
}
return undefined;
list.push([name, value]);
}
/** 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;
/**
* @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");
}
}
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 {
data[i][1] = value;
return;
const value = getHeader(list, name);
if (value === null) throw new TypeError("Unreachable");
headers.push([name, value]);
}
}
return headers;
}
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++;
/** @param {HeadersInit} [init] */
constructor(init = undefined) {
const prefix = "Failed to construct 'Event'";
if (init !== undefined) {
init = webidl.converters["HeadersInit"](init, {
prefix,
context: "Argument 1",
});
}
}
}
function dataHas(data, key) {
for (const [dataKey] of data) {
if (dataKey === key) {
return true;
}
}
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);
}
}
this[webidl.brand] = webidl.brand;
this[_guard] = "none";
if (init !== undefined) {
fillHeaders(this, init);
}
}
[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--;
}
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,

View file

@ -58,10 +58,6 @@ pub fn init(isolate: &mut JsRuntime) {
"deno:op_crates/fetch/01_fetch_util.js",
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"),

View file

@ -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);

View file

@ -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
@ -131,8 +73,16 @@
return { result: input.substring(positionStart, position + 1), position };
}
/**
* @typedef MimeType
* @property {string} type
* @property {string} subtype
* @property {Map<string,string>} parameters
*/
/**
* @param {string} input
* @returns {MimeType | null}
*/
function parseMimeType(input) {
// 1.

View file

@ -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;

View file

@ -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 methods = {
*entries() {
assertBranded(this, prototype);
for (const entry of this[dataSymbol]) {
yield [entry[keyKey], entry[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() {
assertBranded(this, prototype);
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);

@ -1 +1 @@
Subproject commit e19bdbe96243f2ba548c1fd01c0812d645ba0c6f
Subproject commit 579608584916d582d38d0159666aae9a6aaf07ad

View file

@ -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": {

View file

@ -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.");
}
}