summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuca Casonato <lucacasonato@yahoo.com>2021-04-14 22:49:16 +0200
committerGitHub <noreply@github.com>2021-04-14 22:49:16 +0200
commit353e79c796efc6c6aa328abb4d7cef5e642944af (patch)
tree5ae6d1778c0a1dfa53ac62e409d6da8cdb31285f
parent5214acd3d9dec56ee159544f0f6bf9834a62c097 (diff)
chore: align FormData to spec (#10169)
This PR aligns `FormData` to spec. All WPT tests are passing.
-rw-r--r--cli/tests/unit/form_data_test.ts212
-rw-r--r--cli/tests/unit/unit_tests.ts1
-rw-r--r--op_crates/fetch/21_formdata.js529
-rw-r--r--op_crates/fetch/26_fetch.js483
-rw-r--r--op_crates/fetch/internal.d.ts9
-rw-r--r--op_crates/fetch/lib.rs4
-rw-r--r--op_crates/file/01_file.js12
-rw-r--r--op_crates/file/02_filereader.js4
-rw-r--r--op_crates/file/internal.d.ts2
-rw-r--r--op_crates/web/01_mimesniff.js52
-rw-r--r--op_crates/web/internal.d.ts3
-rw-r--r--op_crates/web/lib.deno_web.d.ts51
-rw-r--r--op_crates/webidl/00_webidl.js45
-rw-r--r--op_crates/webidl/internal.d.ts12
-rw-r--r--runtime/js/99_main.js3
m---------test_util/wpt0
-rw-r--r--tools/wpt/expectation.json16
17 files changed, 741 insertions, 697 deletions
diff --git a/cli/tests/unit/form_data_test.ts b/cli/tests/unit/form_data_test.ts
deleted file mode 100644
index 1a948631d..000000000
--- a/cli/tests/unit/form_data_test.ts
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
-import {
- assert,
- assertEquals,
- assertStringIncludes,
- unitTest,
-} from "./test_util.ts";
-
-unitTest(function formDataHasCorrectNameProp(): void {
- assertEquals(FormData.name, "FormData");
-});
-
-unitTest(function formDataParamsAppendSuccess(): void {
- const formData = new FormData();
- formData.append("a", "true");
- assertEquals(formData.get("a"), "true");
-});
-
-unitTest(function formDataParamsDeleteSuccess(): void {
- const formData = new FormData();
- formData.append("a", "true");
- formData.append("b", "false");
- assertEquals(formData.get("b"), "false");
- formData.delete("b");
- assertEquals(formData.get("a"), "true");
- assertEquals(formData.get("b"), null);
-});
-
-unitTest(function formDataParamsGetAllSuccess(): void {
- const formData = new FormData();
- formData.append("a", "true");
- formData.append("b", "false");
- formData.append("a", "null");
- assertEquals(formData.getAll("a"), ["true", "null"]);
- assertEquals(formData.getAll("b"), ["false"]);
- assertEquals(formData.getAll("c"), []);
-});
-
-unitTest(function formDataParamsGetSuccess(): void {
- const formData = new FormData();
- formData.append("a", "true");
- formData.append("b", "false");
- formData.append("a", "null");
- // deno-lint-ignore no-explicit-any
- formData.append("d", undefined as any);
- // deno-lint-ignore no-explicit-any
- formData.append("e", null as any);
- assertEquals(formData.get("a"), "true");
- assertEquals(formData.get("b"), "false");
- assertEquals(formData.get("c"), null);
- assertEquals(formData.get("d"), "undefined");
- assertEquals(formData.get("e"), "null");
-});
-
-unitTest(function formDataParamsHasSuccess(): void {
- const formData = new FormData();
- formData.append("a", "true");
- formData.append("b", "false");
- assert(formData.has("a"));
- assert(formData.has("b"));
- assert(!formData.has("c"));
-});
-
-unitTest(function formDataParamsSetSuccess(): void {
- const formData = new FormData();
- formData.append("a", "true");
- formData.append("b", "false");
- formData.append("a", "null");
- assertEquals(formData.getAll("a"), ["true", "null"]);
- assertEquals(formData.getAll("b"), ["false"]);
- formData.set("a", "false");
- assertEquals(formData.getAll("a"), ["false"]);
- // deno-lint-ignore no-explicit-any
- formData.set("d", undefined as any);
- assertEquals(formData.get("d"), "undefined");
- // deno-lint-ignore no-explicit-any
- formData.set("e", null as any);
- assertEquals(formData.get("e"), "null");
-});
-
-unitTest(function fromDataUseFile(): void {
- const formData = new FormData();
- const file = new File(["foo"], "bar", {
- type: "text/plain",
- });
- formData.append("file", file);
- assertEquals(formData.get("file"), file);
-});
-
-unitTest(function formDataSetEmptyBlobSuccess(): void {
- const formData = new FormData();
- formData.set("a", new Blob([]), "blank.txt");
- formData.get("a");
- /* TODO Fix this test.
- assert(file instanceof File);
- if (typeof file !== "string") {
- assertEquals(file.name, "blank.txt");
- }
- */
-});
-
-unitTest(function formDataBlobFilename(): void {
- const formData = new FormData();
- const content = new TextEncoder().encode("deno");
- formData.set("a", new Blob([content]));
- const file = formData.get("a");
- assert(file instanceof File);
- assertEquals(file.name, "blob");
-});
-
-unitTest(function formDataParamsForEachSuccess(): void {
- const init = [
- ["a", "54"],
- ["b", "true"],
- ];
- const formData = new FormData();
- for (const [name, value] of init) {
- formData.append(name, value);
- }
- let callNum = 0;
- formData.forEach((value, key, parent): void => {
- assertEquals(formData, parent);
- assertEquals(value, init[callNum][1]);
- assertEquals(key, init[callNum][0]);
- callNum++;
- });
- assertEquals(callNum, init.length);
-});
-
-unitTest(function formDataParamsArgumentsCheck(): void {
- const methodRequireOneParam = [
- "delete",
- "getAll",
- "get",
- "has",
- "forEach",
- ] as const;
-
- const methodRequireTwoParams = ["append", "set"] as const;
-
- methodRequireOneParam.forEach((method): void => {
- const formData = new FormData();
- let hasThrown = 0;
- let errMsg = "";
- try {
- // deno-lint-ignore no-explicit-any
- (formData as any)[method]();
- hasThrown = 1;
- } catch (err) {
- errMsg = err.message;
- if (err instanceof TypeError) {
- hasThrown = 2;
- } else {
- hasThrown = 3;
- }
- }
- assertEquals(hasThrown, 2);
- assertStringIncludes(
- errMsg,
- `${method} requires at least 1 argument, but only 0 present`,
- );
- });
-
- methodRequireTwoParams.forEach((method: string): void => {
- const formData = new FormData();
- let hasThrown = 0;
- let errMsg = "";
-
- try {
- // deno-lint-ignore no-explicit-any
- (formData as any)[method]();
- hasThrown = 1;
- } catch (err) {
- errMsg = err.message;
- if (err instanceof TypeError) {
- hasThrown = 2;
- } else {
- hasThrown = 3;
- }
- }
- 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
- (formData as any)[method]("foo");
- hasThrown = 1;
- } catch (err) {
- errMsg = err.message;
- if (err instanceof TypeError) {
- hasThrown = 2;
- } else {
- hasThrown = 3;
- }
- }
- assertEquals(hasThrown, 2);
- assertStringIncludes(
- errMsg,
- `${method} requires at least 2 arguments, but only 1 present`,
- );
- });
-});
-
-unitTest(function toStringShouldBeWebCompatibility(): void {
- const formData = new FormData();
- assertEquals(formData.toString(), "[object FormData]");
-});
diff --git a/cli/tests/unit/unit_tests.ts b/cli/tests/unit/unit_tests.ts
index ebf87651d..4cfd3d961 100644
--- a/cli/tests/unit/unit_tests.ts
+++ b/cli/tests/unit/unit_tests.ts
@@ -24,7 +24,6 @@ import "./file_test.ts";
import "./filereader_test.ts";
import "./files_test.ts";
import "./filter_function_test.ts";
-import "./form_data_test.ts";
import "./format_error_test.ts";
import "./fs_events_test.ts";
import "./get_random_values_test.ts";
diff --git a/op_crates/fetch/21_formdata.js b/op_crates/fetch/21_formdata.js
new file mode 100644
index 000000000..106b67da4
--- /dev/null
+++ b/op_crates/fetch/21_formdata.js
@@ -0,0 +1,529 @@
+// 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 webidl = globalThis.__bootstrap.webidl;
+ const { Blob, File, _byteSequence } = globalThis.__bootstrap.file;
+
+ const entryList = Symbol("entry list");
+
+ /**
+ * @param {string} name
+ * @param {string | Blob} value
+ * @param {string | undefined} filename
+ * @returns {FormDataEntry}
+ */
+ function createEntry(name, value, filename) {
+ if (value instanceof Blob && !(value instanceof File)) {
+ value = new File([value[_byteSequence]], "blob", { type: value.type });
+ }
+ if (value instanceof File && filename !== undefined) {
+ value = new File([value[_byteSequence]], filename, {
+ type: value.type,
+ lastModified: value.lastModified,
+ });
+ }
+ return {
+ name,
+ // @ts-expect-error because TS is not smart enough
+ value,
+ };
+ }
+
+ /**
+ * @typedef FormDataEntry
+ * @property {string} name
+ * @property {FormDataEntryValue} value
+ */
+
+ class FormData {
+ get [Symbol.toStringTag]() {
+ return "FormData";
+ }
+
+ /** @type {FormDataEntry[]} */
+ [entryList] = [];
+
+ /** @param {void} form */
+ constructor(form) {
+ if (form !== undefined) {
+ webidl.illegalConstructor();
+ }
+ this[webidl.brand] = webidl.brand;
+ }
+
+ /**
+ * @param {string} name
+ * @param {string | Blob} valueOrBlobValue
+ * @param {string} [filename]
+ * @returns {void}
+ */
+ append(name, valueOrBlobValue, filename) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'append' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ if (valueOrBlobValue instanceof Blob) {
+ valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ if (filename !== undefined) {
+ filename = webidl.converters["USVString"](filename, {
+ prefix,
+ context: "Argument 3",
+ });
+ }
+ } else {
+ valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ }
+
+ const entry = createEntry(name, valueOrBlobValue, filename);
+
+ this[entryList].push(entry);
+ }
+
+ /**
+ * @param {string} name
+ * @returns {void}
+ */
+ delete(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'name' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ const list = this[entryList];
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].name === name) {
+ list.splice(i, 1);
+ i--;
+ }
+ }
+ }
+
+ /**
+ * @param {string} name
+ * @returns {FormDataEntryValue | null}
+ */
+ get(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'get' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ for (const entry of this[entryList]) {
+ if (entry.name === name) return entry.value;
+ }
+ return null;
+ }
+
+ /**
+ * @param {string} name
+ * @returns {FormDataEntryValue[]}
+ */
+ getAll(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'getAll' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ const returnList = [];
+ for (const entry of this[entryList]) {
+ if (entry.name === name) returnList.push(entry.value);
+ }
+ return returnList;
+ }
+
+ /**
+ * @param {string} name
+ * @returns {boolean}
+ */
+ has(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'has' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ for (const entry of this[entryList]) {
+ if (entry.name === name) return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param {string} name
+ * @param {string | Blob} valueOrBlobValue
+ * @param {string} [filename]
+ * @returns {void}
+ */
+ set(name, valueOrBlobValue, filename) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'set' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ if (valueOrBlobValue instanceof Blob) {
+ valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ if (filename !== undefined) {
+ filename = webidl.converters["USVString"](filename, {
+ prefix,
+ context: "Argument 3",
+ });
+ }
+ } else {
+ valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ }
+
+ const entry = createEntry(name, valueOrBlobValue, filename);
+
+ const list = this[entryList];
+ let added = false;
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].name === name) {
+ if (!added) {
+ list[i] = entry;
+ added = true;
+ } else {
+ list.splice(i, 1);
+ i--;
+ }
+ }
+ }
+ if (!added) {
+ list.push(entry);
+ }
+ }
+ }
+
+ webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value");
+
+ const encoder = new TextEncoder();
+
+ class MultipartBuilder {
+ /**
+ * @param {FormData} formData
+ */
+ constructor(formData) {
+ this.entryList = formData[entryList];
+ this.boundary = this.#createBoundary();
+ /** @type {Uint8Array[]} */
+ this.chunks = [];
+ }
+
+ /**
+ * @returns {string}
+ */
+ getContentType() {
+ return `multipart/form-data; boundary=${this.boundary}`;
+ }
+
+ /**
+ * @returns {Uint8Array}
+ */
+ getBody() {
+ for (const { name, value } of this.entryList) {
+ if (value instanceof File) {
+ this.#writeFile(name, value);
+ } else this.#writeField(name, value);
+ }
+
+ this.chunks.push(encoder.encode(`\r\n--${this.boundary}--`));
+
+ let totalLength = 0;
+ for (const chunk of this.chunks) {
+ totalLength += chunk.byteLength;
+ }
+
+ const finalBuffer = new Uint8Array(totalLength);
+ let i = 0;
+ for (const chunk of this.chunks) {
+ finalBuffer.set(chunk, i);
+ i += chunk.byteLength;
+ }
+
+ return finalBuffer;
+ }
+
+ #createBoundary = () => {
+ return (
+ "----------" +
+ Array.from(Array(32))
+ .map(() => Math.random().toString(36)[2] || 0)
+ .join("")
+ );
+ };
+
+ /**
+ * @param {[string, string][]} headers
+ * @returns {void}
+ */
+ #writeHeaders = (headers) => {
+ let buf = (this.chunks.length === 0) ? "" : "\r\n";
+
+ buf += `--${this.boundary}\r\n`;
+ for (const [key, value] of headers) {
+ buf += `${key}: ${value}\r\n`;
+ }
+ buf += `\r\n`;
+
+ this.chunks.push(encoder.encode(buf));
+ };
+
+ /**
+ * @param {string} field
+ * @param {string} filename
+ * @param {string} [type]
+ * @returns {void}
+ */
+ #writeFileHeaders = (
+ field,
+ filename,
+ type,
+ ) => {
+ /** @type {[string, string][]} */
+ const headers = [
+ [
+ "Content-Disposition",
+ `form-data; name="${field}"; filename="${filename}"`,
+ ],
+ ["Content-Type", type || "application/octet-stream"],
+ ];
+ return this.#writeHeaders(headers);
+ };
+
+ /**
+ * @param {string} field
+ * @returns {void}
+ */
+ #writeFieldHeaders = (field) => {
+ /** @type {[string, string][]} */
+ const headers = [["Content-Disposition", `form-data; name="${field}"`]];
+ return this.#writeHeaders(headers);
+ };
+
+ /**
+ * @param {string} field
+ * @param {string} value
+ * @returns {void}
+ */
+ #writeField = (field, value) => {
+ this.#writeFieldHeaders(field);
+ this.chunks.push(encoder.encode(value));
+ };
+
+ /**
+ * @param {string} field
+ * @param {File} value
+ * @returns {void}
+ */
+ #writeFile = (field, value) => {
+ this.#writeFileHeaders(field, value.name, value.type);
+ this.chunks.push(value[_byteSequence]);
+ };
+ }
+
+ /**
+ * @param {FormData} formdata
+ * @returns {{body: Uint8Array, contentType: string}}
+ */
+ function encodeFormData(formdata) {
+ const builder = new MultipartBuilder(formdata);
+ return {
+ body: builder.getBody(),
+ contentType: builder.getContentType(),
+ };
+ }
+
+ /**
+ * @param {string} value
+ * @returns {Map<string, string>}
+ */
+ function parseContentDisposition(value) {
+ /** @type {Map<string, string>} */
+ const params = new Map();
+ // Forced to do so for some Map constructor param mismatch
+ value
+ .split(";")
+ .slice(1)
+ .map((s) => s.trim().split("="))
+ .filter((arr) => arr.length > 1)
+ .map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")])
+ .forEach(([k, v]) => params.set(k, v));
+ return params;
+ }
+
+ const LF = "\n".codePointAt(0);
+ const CR = "\r".codePointAt(0);
+ const decoder = new TextDecoder("utf-8");
+
+ class MultipartParser {
+ /**
+ * @param {Uint8Array} body
+ * @param {string | undefined} boundary
+ */
+ constructor(body, boundary) {
+ if (!boundary) {
+ throw new TypeError("multipart/form-data must provide a boundary");
+ }
+
+ this.boundary = `--${boundary}`;
+ this.body = body;
+ this.boundaryChars = encoder.encode(this.boundary);
+ }
+
+ /**
+ * @param {string} headersText
+ * @returns {{ headers: Headers, disposition: Map<string, string> }}
+ */
+ #parseHeaders = (headersText) => {
+ const headers = new Headers();
+ const rawHeaders = headersText.split("\r\n");
+ for (const rawHeader of rawHeaders) {
+ const sepIndex = rawHeader.indexOf(":");
+ if (sepIndex < 0) {
+ continue; // Skip this header
+ }
+ const key = rawHeader.slice(0, sepIndex);
+ const value = rawHeader.slice(sepIndex + 1);
+ headers.set(key, value);
+ }
+
+ const disposition = parseContentDisposition(
+ headers.get("Content-Disposition") ?? "",
+ );
+
+ return { headers, disposition };
+ };
+
+ /**
+ * @returns {FormData}
+ */
+ parse() {
+ const formData = new FormData();
+ let headerText = "";
+ let boundaryIndex = 0;
+ let state = 0;
+ let fileStart = 0;
+
+ for (let i = 0; i < this.body.length; i++) {
+ const byte = this.body[i];
+ const prevByte = this.body[i - 1];
+ const isNewLine = byte === LF && prevByte === CR;
+
+ if (state === 1 || state === 2 || state == 3) {
+ headerText += String.fromCharCode(byte);
+ }
+ if (state === 0 && isNewLine) {
+ state = 1;
+ } else if (state === 1 && isNewLine) {
+ state = 2;
+ const headersDone = this.body[i + 1] === CR &&
+ this.body[i + 2] === LF;
+
+ if (headersDone) {
+ state = 3;
+ }
+ } else if (state === 2 && isNewLine) {
+ state = 3;
+ } else if (state === 3 && isNewLine) {
+ state = 4;
+ fileStart = i + 1;
+ } else if (state === 4) {
+ if (this.boundaryChars[boundaryIndex] !== byte) {
+ boundaryIndex = 0;
+ } else {
+ boundaryIndex++;
+ }
+
+ if (boundaryIndex >= this.boundary.length) {
+ const { headers, disposition } = this.#parseHeaders(headerText);
+ const content = this.body.subarray(
+ fileStart,
+ i - boundaryIndex - 1,
+ );
+ // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
+ const filename = disposition.get("filename");
+ const name = disposition.get("name");
+
+ state = 5;
+ // Reset
+ boundaryIndex = 0;
+ headerText = "";
+
+ if (!name) {
+ continue; // Skip, unknown name
+ }
+
+ if (filename) {
+ const blob = new Blob([content], {
+ type: headers.get("Content-Type") || "application/octet-stream",
+ });
+ formData.append(name, blob, filename);
+ } else {
+ formData.append(name, decoder.decode(content));
+ }
+ }
+ } else if (state === 5 && isNewLine) {
+ state = 1;
+ }
+ }
+
+ return formData;
+ }
+ }
+
+ /**
+ * @param {Uint8Array} body
+ * @param {string | undefined} boundary
+ * @returns {FormData}
+ */
+ function parseFormData(body, boundary) {
+ const parser = new MultipartParser(body, boundary);
+ return parser.parse();
+ }
+
+ globalThis.__bootstrap.formData = { FormData, encodeFormData, parseFormData };
+})(globalThis);
diff --git a/op_crates/fetch/26_fetch.js b/op_crates/fetch/26_fetch.js
index e713b48ed..d07121e86 100644
--- a/op_crates/fetch/26_fetch.js
+++ b/op_crates/fetch/26_fetch.js
@@ -3,6 +3,7 @@
// @ts-check
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../web/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
/// <reference path="./11_streams_types.d.ts" />
/// <reference path="./internal.d.ts" />
@@ -16,11 +17,12 @@
// provided by "deno_web"
const { URLSearchParams } = window.__bootstrap.url;
const { getLocationHref } = window.__bootstrap.location;
+ const { FormData, parseFormData, encodeFormData } =
+ window.__bootstrap.formData;
+ const { parseMimeType } = window.__bootstrap.mimesniff;
- const { requiredArguments } = window.__bootstrap.fetchUtil;
const { ReadableStream, isReadableStreamDisturbed } =
window.__bootstrap.streams;
- const { DomIterableMixin } = window.__bootstrap.domIterable;
const { Headers } = window.__bootstrap.headers;
const { Blob, _byteSequence, File } = window.__bootstrap.file;
@@ -203,395 +205,6 @@
}
/**
- * @param {string} value
- * @returns {Map<string, string>}
- */
- function getHeaderValueParams(value) {
- /** @type {Map<string, string>} */
- const params = new Map();
- // Forced to do so for some Map constructor param mismatch
- value
- .split(";")
- .slice(1)
- .map((s) => s.trim().split("="))
- .filter((arr) => arr.length > 1)
- .map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")])
- .forEach(([k, v]) => params.set(k, v));
- return params;
- }
-
- const decoder = new TextDecoder();
- const encoder = new TextEncoder();
- const CR = "\r".charCodeAt(0);
- const LF = "\n".charCodeAt(0);
-
- const dataSymbol = Symbol("data");
-
- /**
- * @param {Blob | string} value
- * @param {string | undefined} filename
- * @returns {FormDataEntryValue}
- */
- function parseFormDataValue(value, filename) {
- if (value instanceof File) {
- return new File([value], filename || value.name, {
- type: value.type,
- lastModified: value.lastModified,
- });
- } else if (value instanceof Blob) {
- return new File([value], filename || "blob", {
- type: value.type,
- });
- } else {
- return String(value);
- }
- }
-
- class FormDataBase {
- /** @type {[name: string, entry: FormDataEntryValue][]} */
- [dataSymbol] = [];
-
- /**
- * @param {string} name
- * @param {string | Blob} value
- * @param {string} [filename]
- * @returns {void}
- */
- append(name, value, filename) {
- requiredArguments("FormData.append", arguments.length, 2);
- name = String(name);
- this[dataSymbol].push([name, parseFormDataValue(value, filename)]);
- }
-
- /**
- * @param {string} name
- * @returns {void}
- */
- delete(name) {
- requiredArguments("FormData.delete", arguments.length, 1);
- name = String(name);
- let i = 0;
- while (i < this[dataSymbol].length) {
- if (this[dataSymbol][i][0] === name) {
- this[dataSymbol].splice(i, 1);
- } else {
- i++;
- }
- }
- }
-
- /**
- * @param {string} name
- * @returns {FormDataEntryValue[]}
- */
- getAll(name) {
- requiredArguments("FormData.getAll", arguments.length, 1);
- name = String(name);
- const values = [];
- for (const entry of this[dataSymbol]) {
- if (entry[0] === name) {
- values.push(entry[1]);
- }
- }
-
- return values;
- }
-
- /**
- * @param {string} name
- * @returns {FormDataEntryValue | null}
- */
- get(name) {
- requiredArguments("FormData.get", arguments.length, 1);
- name = String(name);
- for (const entry of this[dataSymbol]) {
- if (entry[0] === name) {
- return entry[1];
- }
- }
-
- return null;
- }
-
- /**
- * @param {string} name
- * @returns {boolean}
- */
- has(name) {
- requiredArguments("FormData.has", arguments.length, 1);
- name = String(name);
- return this[dataSymbol].some((entry) => entry[0] === name);
- }
-
- /**
- * @param {string} name
- * @param {string | Blob} value
- * @param {string} [filename]
- * @returns {void}
- */
- set(name, value, filename) {
- requiredArguments("FormData.set", arguments.length, 2);
- name = String(name);
-
- // If there are any entries in the context object’s entry list whose name
- // is name, replace the first such entry with entry and remove the others
- let found = false;
- let i = 0;
- while (i < this[dataSymbol].length) {
- if (this[dataSymbol][i][0] === name) {
- if (!found) {
- this[dataSymbol][i][1] = parseFormDataValue(value, filename);
- found = true;
- } else {
- this[dataSymbol].splice(i, 1);
- continue;
- }
- }
- i++;
- }
-
- // Otherwise, append entry to the context object’s entry list.
- if (!found) {
- this[dataSymbol].push([name, parseFormDataValue(value, filename)]);
- }
- }
-
- get [Symbol.toStringTag]() {
- return "FormData";
- }
- }
-
- class FormData extends DomIterableMixin(FormDataBase, dataSymbol) {}
-
- class MultipartBuilder {
- /**
- * @param {FormData} formData
- * @param {string} [boundary]
- */
- constructor(formData, boundary) {
- this.formData = formData;
- this.boundary = boundary ?? this.#createBoundary();
- this.writer = new Buffer();
- }
-
- /**
- * @returns {string}
- */
- getContentType() {
- return `multipart/form-data; boundary=${this.boundary}`;
- }
-
- /**
- * @returns {Uint8Array}
- */
- getBody() {
- for (const [fieldName, fieldValue] of this.formData.entries()) {
- if (fieldValue instanceof File) {
- this.#writeFile(fieldName, fieldValue);
- } else this.#writeField(fieldName, fieldValue);
- }
-
- this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`));
-
- return this.writer.bytes();
- }
-
- #createBoundary = () => {
- return (
- "----------" +
- Array.from(Array(32))
- .map(() => Math.random().toString(36)[2] || 0)
- .join("")
- );
- };
-
- /**
- * @param {[string, string][]} headers
- * @returns {void}
- */
- #writeHeaders = (headers) => {
- let buf = this.writer.empty() ? "" : "\r\n";
-
- buf += `--${this.boundary}\r\n`;
- for (const [key, value] of headers) {
- buf += `${key}: ${value}\r\n`;
- }
- buf += `\r\n`;
-
- this.writer.writeSync(encoder.encode(buf));
- };
-
- /**
- * @param {string} field
- * @param {string} filename
- * @param {string} [type]
- * @returns {void}
- */
- #writeFileHeaders = (
- field,
- filename,
- type,
- ) => {
- /** @type {[string, string][]} */
- const headers = [
- [
- "Content-Disposition",
- `form-data; name="${field}"; filename="${filename}"`,
- ],
- ["Content-Type", type || "application/octet-stream"],
- ];
- return this.#writeHeaders(headers);
- };
-
- /**
- * @param {string} field
- * @returns {void}
- */
- #writeFieldHeaders = (field) => {
- /** @type {[string, string][]} */
- const headers = [["Content-Disposition", `form-data; name="${field}"`]];
- return this.#writeHeaders(headers);
- };
-
- /**
- * @param {string} field
- * @param {string} value
- * @returns {void}
- */
- #writeField = (field, value) => {
- this.#writeFieldHeaders(field);
- this.writer.writeSync(encoder.encode(value));
- };
-
- /**
- * @param {string} field
- * @param {File} value
- * @returns {void}
- */
- #writeFile = (field, value) => {
- this.#writeFileHeaders(field, value.name, value.type);
- this.writer.writeSync(value[_byteSequence]);
- };
- }
-
- class MultipartParser {
- /**
- * @param {Uint8Array} body
- * @param {string | undefined} boundary
- */
- constructor(body, boundary) {
- if (!boundary) {
- throw new TypeError("multipart/form-data must provide a boundary");
- }
-
- this.boundary = `--${boundary}`;
- this.body = body;
- this.boundaryChars = encoder.encode(this.boundary);
- }
-
- /**
- * @param {string} headersText
- * @returns {{ headers: Headers, disposition: Map<string, string> }}
- */
- #parseHeaders = (headersText) => {
- const headers = new Headers();
- const rawHeaders = headersText.split("\r\n");
- for (const rawHeader of rawHeaders) {
- const sepIndex = rawHeader.indexOf(":");
- if (sepIndex < 0) {
- continue; // Skip this header
- }
- const key = rawHeader.slice(0, sepIndex);
- const value = rawHeader.slice(sepIndex + 1);
- headers.set(key, value);
- }
-
- return {
- headers,
- disposition: getHeaderValueParams(
- headers.get("Content-Disposition") ?? "",
- ),
- };
- };
-
- /**
- * @returns {FormData}
- */
- parse() {
- const formData = new FormData();
- let headerText = "";
- let boundaryIndex = 0;
- let state = 0;
- let fileStart = 0;
-
- for (let i = 0; i < this.body.length; i++) {
- const byte = this.body[i];
- const prevByte = this.body[i - 1];
- const isNewLine = byte === LF && prevByte === CR;
-
- if (state === 1 || state === 2 || state == 3) {
- headerText += String.fromCharCode(byte);
- }
- if (state === 0 && isNewLine) {
- state = 1;
- } else if (state === 1 && isNewLine) {
- state = 2;
- const headersDone = this.body[i + 1] === CR &&
- this.body[i + 2] === LF;
-
- if (headersDone) {
- state = 3;
- }
- } else if (state === 2 && isNewLine) {
- state = 3;
- } else if (state === 3 && isNewLine) {
- state = 4;
- fileStart = i + 1;
- } else if (state === 4) {
- if (this.boundaryChars[boundaryIndex] !== byte) {
- boundaryIndex = 0;
- } else {
- boundaryIndex++;
- }
-
- if (boundaryIndex >= this.boundary.length) {
- const { headers, disposition } = this.#parseHeaders(headerText);
- const content = this.body.subarray(
- fileStart,
- i - boundaryIndex - 1,
- );
- // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
- const filename = disposition.get("filename");
- const name = disposition.get("name");
-
- state = 5;
- // Reset
- boundaryIndex = 0;
- headerText = "";
-
- if (!name) {
- continue; // Skip, unknown name
- }
-
- if (filename) {
- const blob = new Blob([content], {
- type: headers.get("Content-Type") || "application/octet-stream",
- });
- formData.append(name, blob, filename);
- } else {
- formData.append(name, decoder.decode(content));
- }
- }
- } else if (state === 5 && isNewLine) {
- state = 1;
- }
- }
-
- return formData;
- }
- }
-
- /**
* @param {string} name
* @param {BodyInit | null} bodySource
*/
@@ -785,46 +398,46 @@
/** @returns {Promise<FormData>} */
async formData() {
const formData = new FormData();
- if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) {
- const params = getHeaderValueParams(this.#contentType);
-
- // ref: https://tools.ietf.org/html/rfc2046#section-5.1
- const boundary = params.get("boundary");
- const body = new Uint8Array(await this.arrayBuffer());
- const multipartParser = new MultipartParser(body, boundary);
-
- return multipartParser.parse();
- } else if (
- hasHeaderValueOf(this.#contentType, "application/x-www-form-urlencoded")
- ) {
- // From https://github.com/github/fetch/blob/master/fetch.js
- // Copyright (c) 2014-2016 GitHub, Inc. MIT License
- const body = await this.text();
- try {
- body
- .trim()
- .split("&")
- .forEach((bytes) => {
- if (bytes) {
- const split = bytes.split("=");
- if (split.length >= 2) {
- // @ts-expect-error this is safe because of the above check
- const name = split.shift().replace(/\+/g, " ");
- const value = split.join("=").replace(/\+/g, " ");
- formData.append(
- decodeURIComponent(name),
- decodeURIComponent(value),
- );
+ const mimeType = parseMimeType(this.#contentType);
+ if (mimeType) {
+ if (mimeType.type === "multipart" && mimeType.subtype === "form-data") {
+ // ref: https://tools.ietf.org/html/rfc2046#section-5.1
+ const boundary = mimeType.parameters.get("boundary");
+ const body = new Uint8Array(await this.arrayBuffer());
+ return parseFormData(body, boundary);
+ } else if (
+ mimeType.type === "application" &&
+ mimeType.subtype === "x-www-form-urlencoded"
+ ) {
+ // From https://github.com/github/fetch/blob/master/fetch.js
+ // Copyright (c) 2014-2016 GitHub, Inc. MIT License
+ const body = await this.text();
+ try {
+ body
+ .trim()
+ .split("&")
+ .forEach((bytes) => {
+ if (bytes) {
+ const split = bytes.split("=");
+ if (split.length >= 2) {
+ // @ts-expect-error this is safe because of the above check
+ const name = split.shift().replace(/\+/g, " ");
+ const value = split.join("=").replace(/\+/g, " ");
+ formData.append(
+ decodeURIComponent(name),
+ decodeURIComponent(value),
+ );
+ }
}
- }
- });
- } catch (e) {
- throw new TypeError("Invalid form urlencoded format");
+ });
+ } catch (e) {
+ throw new TypeError("Invalid form urlencoded format");
+ }
+ return formData;
}
- return formData;
- } else {
- throw new TypeError("Invalid form data");
}
+
+ throw new TypeError("Invalid form data");
}
/** @returns {Promise<string>} */
@@ -1374,17 +987,9 @@
body = init.body[_byteSequence];
contentType = init.body.type;
} else if (init.body instanceof FormData) {
- let boundary;
- if (headers.has("content-type")) {
- const params = getHeaderValueParams("content-type");
- boundary = params.get("boundary");
- }
- const multipartBuilder = new MultipartBuilder(
- init.body,
- boundary,
- );
- body = multipartBuilder.getBody();
- contentType = multipartBuilder.getContentType();
+ const res = encodeFormData(init.body);
+ body = res.body;
+ contentType = res.contentType;
} else if (init.body instanceof ReadableStream) {
body = init.body;
}
diff --git a/op_crates/fetch/internal.d.ts b/op_crates/fetch/internal.d.ts
index e02bc6ed2..3206008c5 100644
--- a/op_crates/fetch/internal.d.ts
+++ b/op_crates/fetch/internal.d.ts
@@ -19,6 +19,15 @@ declare namespace globalThis {
Headers: typeof Headers;
};
+ declare var formData: {
+ FormData: typeof FormData;
+ encodeFormData(formdata: FormData): {
+ body: Uint8Array;
+ contentType: string;
+ };
+ parseFormData(body: Uint8Array, boundary: string | undefined): FormData;
+ };
+
declare var streams: {
ReadableStream: typeof ReadableStream;
isReadableStreamDisturbed(stream: ReadableStream): boolean;
diff --git a/op_crates/fetch/lib.rs b/op_crates/fetch/lib.rs
index ae716af41..9eeda059a 100644
--- a/op_crates/fetch/lib.rs
+++ b/op_crates/fetch/lib.rs
@@ -71,6 +71,10 @@ pub fn init(isolate: &mut JsRuntime) {
include_str!("20_headers.js"),
),
(
+ "deno:op_crates/fetch/21_formdata.js",
+ include_str!("21_formdata.js"),
+ ),
+ (
"deno:op_crates/fetch/26_fetch.js",
include_str!("26_fetch.js"),
),
diff --git a/op_crates/file/01_file.js b/op_crates/file/01_file.js
index 17762c14a..47b44a08e 100644
--- a/op_crates/file/01_file.js
+++ b/op_crates/file/01_file.js
@@ -139,6 +139,10 @@
const _byteSequence = Symbol("[[ByteSequence]]");
class Blob {
+ get [Symbol.toStringTag]() {
+ return "Blob";
+ }
+
/** @type {string} */
#type;
@@ -286,10 +290,6 @@
}
return bytes.buffer;
}
-
- get [Symbol.toStringTag]() {
- return "Blob";
- }
}
webidl.converters["Blob"] = webidl.createInterfaceConverter("Blob", Blob);
@@ -336,6 +336,10 @@
const _LastModfied = Symbol("[[LastModified]]");
class File extends Blob {
+ get [Symbol.toStringTag]() {
+ return "File";
+ }
+
/** @type {string} */
[_Name];
/** @type {number} */
diff --git a/op_crates/file/02_filereader.js b/op_crates/file/02_filereader.js
index b32cbfce9..640c6dd9e 100644
--- a/op_crates/file/02_filereader.js
+++ b/op_crates/file/02_filereader.js
@@ -24,6 +24,10 @@
const aborted = Symbol("[[aborted]]");
class FileReader extends EventTarget {
+ get [Symbol.toStringTag]() {
+ return "FileReader";
+ }
+
/** @type {"empty" | "loading" | "done"} */
[state] = "empty";
/** @type {null | string | ArrayBuffer} */
diff --git a/op_crates/file/internal.d.ts b/op_crates/file/internal.d.ts
index 91a61d811..dd892e736 100644
--- a/op_crates/file/internal.d.ts
+++ b/op_crates/file/internal.d.ts
@@ -9,7 +9,7 @@ declare namespace globalThis {
Blob: typeof Blob & {
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
};
- _byteSequence: unique symbol;
+ readonly _byteSequence: unique symbol;
File: typeof File & {
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
};
diff --git a/op_crates/web/01_mimesniff.js b/op_crates/web/01_mimesniff.js
index 918343f2c..f58130132 100644
--- a/op_crates/web/01_mimesniff.js
+++ b/op_crates/web/01_mimesniff.js
@@ -10,6 +10,25 @@
((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];
@@ -35,14 +54,25 @@
"\u007E",
...ASCII_ALPHANUMERIC,
];
- const HTTP_TOKEN_CODE_POINT_RE = new RegExp(`^[${HTTP_TOKEN_CODE_POINT}]+$`);
+ 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(
- `^[${HTTP_QUOTED_STRING_TOKEN_POINT}]+$`,
+ `^[${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",
);
/**
@@ -106,8 +136,8 @@
*/
function parseMimeType(input) {
// 1.
- input = input.replaceAll(new RegExp(`^[${HTTP_WHITESPACE}]+`, "g"), "");
- input = input.replaceAll(new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"), "");
+ input = input.replaceAll(HTTP_WHITESPACE_PREFIX_RE, "");
+ input = input.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
// 2.
let position = 0;
@@ -123,9 +153,7 @@
position = res1.position;
// 4.
- if (type === "" || !HTTP_TOKEN_CODE_POINT_RE.test(type)) {
- return null;
- }
+ if (type === "" || !HTTP_TOKEN_CODE_POINT_RE.test(type)) return null;
// 5.
if (position >= endOfInput) return null;
@@ -143,12 +171,10 @@
position = res2.position;
// 8.
- subtype = subtype.replaceAll(new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"), "");
+ subtype = subtype.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
// 9.
- if (subtype === "" || !HTTP_TOKEN_CODE_POINT_RE.test(subtype)) {
- return null;
- }
+ if (subtype === "" || !HTTP_TOKEN_CODE_POINT_RE.test(subtype)) return null;
// 10.
const mimeType = {
@@ -216,7 +242,7 @@
// 11.9.2.
parameterValue = parameterValue.replaceAll(
- new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"),
+ HTTP_WHITESPACE_SUFFIX_RE,
"",
);
@@ -224,7 +250,7 @@
if (parameterValue === "") continue;
}
- // 11.9.
+ // 11.10.
if (
parameterName !== "" && HTTP_TOKEN_CODE_POINT_RE.test(parameterName) &&
HTTP_QUOTED_STRING_TOKEN_POINT_RE.test(parameterValue) &&
diff --git a/op_crates/web/internal.d.ts b/op_crates/web/internal.d.ts
index 18220b08e..5681edc7b 100644
--- a/op_crates/web/internal.d.ts
+++ b/op_crates/web/internal.d.ts
@@ -4,6 +4,9 @@
/// <reference lib="esnext" />
declare namespace globalThis {
+ declare var TextEncoder: typeof TextEncoder;
+ declare var TextDecoder: typeof TextDecoder;
+
declare namespace __bootstrap {
declare var infra: {
collectSequenceOfCodepoints(
diff --git a/op_crates/web/lib.deno_web.d.ts b/op_crates/web/lib.deno_web.d.ts
index 79b56f68e..0fce6ce6b 100644
--- a/op_crates/web/lib.deno_web.d.ts
+++ b/op_crates/web/lib.deno_web.d.ts
@@ -87,45 +87,44 @@ declare class Event {
*/
declare class EventTarget {
/** Appends an event listener for events whose type attribute value is type.
- * The callback argument sets the callback that will be invoked when the event
- * is dispatched.
- *
- * The options argument sets listener-specific options. For compatibility this
- * can be a boolean, in which case the method behaves exactly as if the value
- * was specified as options's capture.
- *
- * When set to true, options's capture prevents callback from being invoked
- * when the event's eventPhase attribute value is BUBBLING_PHASE. When false
- * (or not present), callback will not be invoked when event's eventPhase
- * attribute value is CAPTURING_PHASE. Either way, callback will be invoked if
- * event's eventPhase attribute value is AT_TARGET.
- *
- * When set to true, options's passive indicates that the callback will not
- * cancel the event by invoking preventDefault(). This is used to enable
- * performance optimizations described in § 2.8 Observing event listeners.
- *
- * When set to true, options's once indicates that the callback will only be
- * invoked once after which the event listener will be removed.
- *
- * The event listener is appended to target's event listener list and is not
- * appended if it has the same type, callback, and capture. */
+ * The callback argument sets the callback that will be invoked when the event
+ * is dispatched.
+ *
+ * The options argument sets listener-specific options. For compatibility this
+ * can be a boolean, in which case the method behaves exactly as if the value
+ * was specified as options's capture.
+ *
+ * When set to true, options's capture prevents callback from being invoked
+ * when the event's eventPhase attribute value is BUBBLING_PHASE. When false
+ * (or not present), callback will not be invoked when event's eventPhase
+ * attribute value is CAPTURING_PHASE. Either way, callback will be invoked if
+ * event's eventPhase attribute value is AT_TARGET.
+ *
+ * When set to true, options's passive indicates that the callback will not
+ * cancel the event by invoking preventDefault(). This is used to enable
+ * performance optimizations described in § 2.8 Observing event listeners.
+ *
+ * When set to true, options's once indicates that the callback will only be
+ * invoked once after which the event listener will be removed.
+ *
+ * The event listener is appended to target's event listener list and is not
+ * appended if it has the same type, callback, and capture. */
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions,
): void;
/** Dispatches a synthetic event event to target and returns true if either
- * event's cancelable attribute value is false or its preventDefault() method
- * was not invoked, and false otherwise. */
+ * event's cancelable attribute value is false or its preventDefault() method
+ * was not invoked, and false otherwise. */
dispatchEvent(event: Event): boolean;
/** Removes the event listener in target's event listener list with the same
- * type, callback, and options. */
+ * type, callback, and options. */
removeEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: EventListenerOptions | boolean,
): void;
- [Symbol.toStringTag]: string;
}
interface EventListener {
diff --git a/op_crates/webidl/00_webidl.js b/op_crates/webidl/00_webidl.js
index 843fd329e..508abe44d 100644
--- a/op_crates/webidl/00_webidl.js
+++ b/op_crates/webidl/00_webidl.js
@@ -802,6 +802,50 @@
throw new TypeError("Illegal constructor");
}
+ function mixinPairIterable(name, prototype, dataSymbol, keyKey, valueKey) {
+ const methods = {
+ *entries() {
+ assertBranded(this, prototype);
+ for (const entry of this[dataSymbol]) {
+ yield [entry[keyKey], entry[valueKey]];
+ }
+ },
+ [Symbol.iterator]() {
+ assertBranded(this, prototype);
+ return this.entries();
+ },
+ *keys() {
+ assertBranded(this, prototype);
+ for (const entry of this[dataSymbol]) {
+ yield entry[keyKey];
+ }
+ },
+ *values() {
+ assertBranded(this, prototype);
+ for (const entry of this[dataSymbol]) {
+ yield entry[valueKey];
+ }
+ },
+ forEach(idlCallback, thisArg) {
+ assertBranded(this, prototype);
+ const prefix = `Failed to execute 'forEach' on '${name}'`;
+ requiredArguments(arguments.length, 1, { prefix });
+ idlCallback = converters["Function"](idlCallback, {
+ prefix,
+ context: "Argument 1",
+ });
+ idlCallback = idlCallback.bind(thisArg ?? globalThis);
+ const pairs = this[dataSymbol];
+ for (let i = 0; i < pairs.length; i++) {
+ const entry = pairs[i];
+ idlCallback(entry[valueKey], entry[keyKey], this);
+ }
+ },
+ };
+
+ return Object.assign(prototype.prototype, methods);
+ }
+
window.__bootstrap ??= {};
window.__bootstrap.webidl = {
makeException,
@@ -817,5 +861,6 @@
createBranded,
assertBranded,
illegalConstructor,
+ mixinPairIterable,
};
})(this);
diff --git a/op_crates/webidl/internal.d.ts b/op_crates/webidl/internal.d.ts
index 425ee674e..ca72566a5 100644
--- a/op_crates/webidl/internal.d.ts
+++ b/op_crates/webidl/internal.d.ts
@@ -286,6 +286,18 @@ declare namespace globalThis {
v: Record<K, V>,
opts: ValueConverterOpts,
) => any;
+
+ /**
+ * Mix in the iterable declarations defined in WebIDL.
+ * https://heycam.github.io/webidl/#es-iterable
+ */
+ declare function mixinPairIterable(
+ name: string,
+ prototype: any,
+ dataSymbol: symbol,
+ keyKey: string | number | symbol,
+ valueKey: string | number | symbol,
+ ): void;
}
}
}
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index 977755007..dd7e3793c 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -29,6 +29,7 @@ delete Object.prototype.__proto__;
const webgpu = window.__bootstrap.webgpu;
const webSocket = window.__bootstrap.webSocket;
const file = window.__bootstrap.file;
+ const formData = window.__bootstrap.formData;
const fetch = window.__bootstrap.fetch;
const prompt = window.__bootstrap.prompt;
const denoNs = window.__bootstrap.denoNs;
@@ -261,7 +262,7 @@ delete Object.prototype.__proto__;
EventTarget: util.nonEnumerable(EventTarget),
File: util.nonEnumerable(file.File),
FileReader: util.nonEnumerable(fileReader.FileReader),
- FormData: util.nonEnumerable(fetch.FormData),
+ FormData: util.nonEnumerable(formData.FormData),
Headers: util.nonEnumerable(headers.Headers),
MessageEvent: util.nonEnumerable(MessageEvent),
Performance: util.nonEnumerable(performance.Performance),
diff --git a/test_util/wpt b/test_util/wpt
-Subproject a522daf78a71c2252d10c978f09cf0575aceb79
+Subproject e19bdbe96243f2ba548c1fd01c0812d645ba0c6
diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json
index 587bd69c8..98fc105d5 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -578,6 +578,10 @@
"Parsing: <file://example.net/C:/> against <about:blank>",
"Parsing: <file://1.2.3.4/C:/> against <about:blank>",
"Parsing: <file://[1::8]/C:/> against <about:blank>",
+ "Parsing: <C|/> against <file://host/>",
+ "Parsing: </C:/> against <file://host/>",
+ "Parsing: <file:C:/> against <file://host/>",
+ "Parsing: <file:/C:/> against <file://host/>",
"Parsing: <file://localhost//a//../..//foo> against <about:blank>",
"Parsing: <file://localhost////foo> against <about:blank>",
"Parsing: <file:////foo> against <about:blank>",
@@ -753,5 +757,17 @@
"queue-microtask.any.js": true
}
}
+ },
+ "xhr": {
+ "formdata": {
+ "append.any.js": true,
+ "constructor.any.js": true,
+ "delete.any.js": true,
+ "foreach.any.js": true,
+ "get.any.js": true,
+ "has.any.js": true,
+ "set-blob.any.js": true,
+ "set.any.js": true
+ }
}
} \ No newline at end of file