mirror of
https://github.com/denoland/deno.git
synced 2025-09-29 13:44:47 +00:00
chore: align FormData to spec (#10169)
This PR aligns `FormData` to spec. All WPT tests are passing.
This commit is contained in:
parent
5214acd3d9
commit
353e79c796
17 changed files with 742 additions and 698 deletions
|
@ -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]");
|
|
||||||
});
|
|
|
@ -24,7 +24,6 @@ import "./file_test.ts";
|
||||||
import "./filereader_test.ts";
|
import "./filereader_test.ts";
|
||||||
import "./files_test.ts";
|
import "./files_test.ts";
|
||||||
import "./filter_function_test.ts";
|
import "./filter_function_test.ts";
|
||||||
import "./form_data_test.ts";
|
|
||||||
import "./format_error_test.ts";
|
import "./format_error_test.ts";
|
||||||
import "./fs_events_test.ts";
|
import "./fs_events_test.ts";
|
||||||
import "./get_random_values_test.ts";
|
import "./get_random_values_test.ts";
|
||||||
|
|
529
op_crates/fetch/21_formdata.js
Normal file
529
op_crates/fetch/21_formdata.js
Normal file
|
@ -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);
|
|
@ -3,6 +3,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
/// <reference path="../../core/lib.deno_core.d.ts" />
|
/// <reference path="../../core/lib.deno_core.d.ts" />
|
||||||
/// <reference path="../web/internal.d.ts" />
|
/// <reference path="../web/internal.d.ts" />
|
||||||
|
/// <reference path="../url/internal.d.ts" />
|
||||||
/// <reference path="../web/lib.deno_web.d.ts" />
|
/// <reference path="../web/lib.deno_web.d.ts" />
|
||||||
/// <reference path="./11_streams_types.d.ts" />
|
/// <reference path="./11_streams_types.d.ts" />
|
||||||
/// <reference path="./internal.d.ts" />
|
/// <reference path="./internal.d.ts" />
|
||||||
|
@ -16,11 +17,12 @@
|
||||||
// provided by "deno_web"
|
// provided by "deno_web"
|
||||||
const { URLSearchParams } = window.__bootstrap.url;
|
const { URLSearchParams } = window.__bootstrap.url;
|
||||||
const { getLocationHref } = window.__bootstrap.location;
|
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 } =
|
const { ReadableStream, isReadableStreamDisturbed } =
|
||||||
window.__bootstrap.streams;
|
window.__bootstrap.streams;
|
||||||
const { DomIterableMixin } = window.__bootstrap.domIterable;
|
|
||||||
const { Headers } = window.__bootstrap.headers;
|
const { Headers } = window.__bootstrap.headers;
|
||||||
const { Blob, _byteSequence, File } = window.__bootstrap.file;
|
const { Blob, _byteSequence, File } = window.__bootstrap.file;
|
||||||
|
|
||||||
|
@ -202,395 +204,6 @@
|
||||||
return new RegExp(`^${value}(?:[\\s;]|$)`).test(s);
|
return new RegExp(`^${value}(?:[\\s;]|$)`).test(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 {string} name
|
||||||
* @param {BodyInit | null} bodySource
|
* @param {BodyInit | null} bodySource
|
||||||
|
@ -785,46 +398,46 @@
|
||||||
/** @returns {Promise<FormData>} */
|
/** @returns {Promise<FormData>} */
|
||||||
async formData() {
|
async formData() {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) {
|
const mimeType = parseMimeType(this.#contentType);
|
||||||
const params = getHeaderValueParams(this.#contentType);
|
if (mimeType) {
|
||||||
|
if (mimeType.type === "multipart" && mimeType.subtype === "form-data") {
|
||||||
// ref: https://tools.ietf.org/html/rfc2046#section-5.1
|
// ref: https://tools.ietf.org/html/rfc2046#section-5.1
|
||||||
const boundary = params.get("boundary");
|
const boundary = mimeType.parameters.get("boundary");
|
||||||
const body = new Uint8Array(await this.arrayBuffer());
|
const body = new Uint8Array(await this.arrayBuffer());
|
||||||
const multipartParser = new MultipartParser(body, boundary);
|
return parseFormData(body, boundary);
|
||||||
|
} else if (
|
||||||
return multipartParser.parse();
|
mimeType.type === "application" &&
|
||||||
} else if (
|
mimeType.subtype === "x-www-form-urlencoded"
|
||||||
hasHeaderValueOf(this.#contentType, "application/x-www-form-urlencoded")
|
) {
|
||||||
) {
|
// From https://github.com/github/fetch/blob/master/fetch.js
|
||||||
// From https://github.com/github/fetch/blob/master/fetch.js
|
// Copyright (c) 2014-2016 GitHub, Inc. MIT License
|
||||||
// Copyright (c) 2014-2016 GitHub, Inc. MIT License
|
const body = await this.text();
|
||||||
const body = await this.text();
|
try {
|
||||||
try {
|
body
|
||||||
body
|
.trim()
|
||||||
.trim()
|
.split("&")
|
||||||
.split("&")
|
.forEach((bytes) => {
|
||||||
.forEach((bytes) => {
|
if (bytes) {
|
||||||
if (bytes) {
|
const split = bytes.split("=");
|
||||||
const split = bytes.split("=");
|
if (split.length >= 2) {
|
||||||
if (split.length >= 2) {
|
// @ts-expect-error this is safe because of the above check
|
||||||
// @ts-expect-error this is safe because of the above check
|
const name = split.shift().replace(/\+/g, " ");
|
||||||
const name = split.shift().replace(/\+/g, " ");
|
const value = split.join("=").replace(/\+/g, " ");
|
||||||
const value = split.join("=").replace(/\+/g, " ");
|
formData.append(
|
||||||
formData.append(
|
decodeURIComponent(name),
|
||||||
decodeURIComponent(name),
|
decodeURIComponent(value),
|
||||||
decodeURIComponent(value),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
} catch (e) {
|
||||||
} catch (e) {
|
throw new TypeError("Invalid form urlencoded format");
|
||||||
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>} */
|
/** @returns {Promise<string>} */
|
||||||
|
@ -1374,17 +987,9 @@
|
||||||
body = init.body[_byteSequence];
|
body = init.body[_byteSequence];
|
||||||
contentType = init.body.type;
|
contentType = init.body.type;
|
||||||
} else if (init.body instanceof FormData) {
|
} else if (init.body instanceof FormData) {
|
||||||
let boundary;
|
const res = encodeFormData(init.body);
|
||||||
if (headers.has("content-type")) {
|
body = res.body;
|
||||||
const params = getHeaderValueParams("content-type");
|
contentType = res.contentType;
|
||||||
boundary = params.get("boundary");
|
|
||||||
}
|
|
||||||
const multipartBuilder = new MultipartBuilder(
|
|
||||||
init.body,
|
|
||||||
boundary,
|
|
||||||
);
|
|
||||||
body = multipartBuilder.getBody();
|
|
||||||
contentType = multipartBuilder.getContentType();
|
|
||||||
} else if (init.body instanceof ReadableStream) {
|
} else if (init.body instanceof ReadableStream) {
|
||||||
body = init.body;
|
body = init.body;
|
||||||
}
|
}
|
||||||
|
|
9
op_crates/fetch/internal.d.ts
vendored
9
op_crates/fetch/internal.d.ts
vendored
|
@ -19,6 +19,15 @@ declare namespace globalThis {
|
||||||
Headers: typeof Headers;
|
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: {
|
declare var streams: {
|
||||||
ReadableStream: typeof ReadableStream;
|
ReadableStream: typeof ReadableStream;
|
||||||
isReadableStreamDisturbed(stream: ReadableStream): boolean;
|
isReadableStreamDisturbed(stream: ReadableStream): boolean;
|
||||||
|
|
|
@ -70,6 +70,10 @@ pub fn init(isolate: &mut JsRuntime) {
|
||||||
"deno:op_crates/fetch/20_headers.js",
|
"deno:op_crates/fetch/20_headers.js",
|
||||||
include_str!("20_headers.js"),
|
include_str!("20_headers.js"),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"deno:op_crates/fetch/21_formdata.js",
|
||||||
|
include_str!("21_formdata.js"),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"deno:op_crates/fetch/26_fetch.js",
|
"deno:op_crates/fetch/26_fetch.js",
|
||||||
include_str!("26_fetch.js"),
|
include_str!("26_fetch.js"),
|
||||||
|
|
|
@ -139,6 +139,10 @@
|
||||||
const _byteSequence = Symbol("[[ByteSequence]]");
|
const _byteSequence = Symbol("[[ByteSequence]]");
|
||||||
|
|
||||||
class Blob {
|
class Blob {
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return "Blob";
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
#type;
|
#type;
|
||||||
|
|
||||||
|
@ -286,10 +290,6 @@
|
||||||
}
|
}
|
||||||
return bytes.buffer;
|
return bytes.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
get [Symbol.toStringTag]() {
|
|
||||||
return "Blob";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
webidl.converters["Blob"] = webidl.createInterfaceConverter("Blob", Blob);
|
webidl.converters["Blob"] = webidl.createInterfaceConverter("Blob", Blob);
|
||||||
|
@ -336,6 +336,10 @@
|
||||||
const _LastModfied = Symbol("[[LastModified]]");
|
const _LastModfied = Symbol("[[LastModified]]");
|
||||||
|
|
||||||
class File extends Blob {
|
class File extends Blob {
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return "File";
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
[_Name];
|
[_Name];
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
const aborted = Symbol("[[aborted]]");
|
const aborted = Symbol("[[aborted]]");
|
||||||
|
|
||||||
class FileReader extends EventTarget {
|
class FileReader extends EventTarget {
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return "FileReader";
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {"empty" | "loading" | "done"} */
|
/** @type {"empty" | "loading" | "done"} */
|
||||||
[state] = "empty";
|
[state] = "empty";
|
||||||
/** @type {null | string | ArrayBuffer} */
|
/** @type {null | string | ArrayBuffer} */
|
||||||
|
|
2
op_crates/file/internal.d.ts
vendored
2
op_crates/file/internal.d.ts
vendored
|
@ -9,7 +9,7 @@ declare namespace globalThis {
|
||||||
Blob: typeof Blob & {
|
Blob: typeof Blob & {
|
||||||
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
|
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
|
||||||
};
|
};
|
||||||
_byteSequence: unique symbol;
|
readonly _byteSequence: unique symbol;
|
||||||
File: typeof File & {
|
File: typeof File & {
|
||||||
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
|
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,25 @@
|
||||||
((window) => {
|
((window) => {
|
||||||
const { collectSequenceOfCodepoints } = window.__bootstrap.infra;
|
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_TAB_OR_SPACE = ["\u0009", "\u0020"];
|
||||||
const HTTP_WHITESPACE = ["\u000A", "\u000D", ...HTTP_TAB_OR_SPACE];
|
const HTTP_WHITESPACE = ["\u000A", "\u000D", ...HTTP_TAB_OR_SPACE];
|
||||||
|
|
||||||
|
@ -35,14 +54,25 @@
|
||||||
"\u007E",
|
"\u007E",
|
||||||
...ASCII_ALPHANUMERIC,
|
...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 = [
|
const HTTP_QUOTED_STRING_TOKEN_POINT = [
|
||||||
"\u0009",
|
"\u0009",
|
||||||
"\u0020-\u007E",
|
"\u0020-\u007E",
|
||||||
"\u0080-\u00FF",
|
"\u0080-\u00FF",
|
||||||
];
|
];
|
||||||
const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new RegExp(
|
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) {
|
function parseMimeType(input) {
|
||||||
// 1.
|
// 1.
|
||||||
input = input.replaceAll(new RegExp(`^[${HTTP_WHITESPACE}]+`, "g"), "");
|
input = input.replaceAll(HTTP_WHITESPACE_PREFIX_RE, "");
|
||||||
input = input.replaceAll(new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"), "");
|
input = input.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
|
||||||
|
|
||||||
// 2.
|
// 2.
|
||||||
let position = 0;
|
let position = 0;
|
||||||
|
@ -123,9 +153,7 @@
|
||||||
position = res1.position;
|
position = res1.position;
|
||||||
|
|
||||||
// 4.
|
// 4.
|
||||||
if (type === "" || !HTTP_TOKEN_CODE_POINT_RE.test(type)) {
|
if (type === "" || !HTTP_TOKEN_CODE_POINT_RE.test(type)) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5.
|
// 5.
|
||||||
if (position >= endOfInput) return null;
|
if (position >= endOfInput) return null;
|
||||||
|
@ -143,12 +171,10 @@
|
||||||
position = res2.position;
|
position = res2.position;
|
||||||
|
|
||||||
// 8.
|
// 8.
|
||||||
subtype = subtype.replaceAll(new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"), "");
|
subtype = subtype.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
|
||||||
|
|
||||||
// 9.
|
// 9.
|
||||||
if (subtype === "" || !HTTP_TOKEN_CODE_POINT_RE.test(subtype)) {
|
if (subtype === "" || !HTTP_TOKEN_CODE_POINT_RE.test(subtype)) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10.
|
// 10.
|
||||||
const mimeType = {
|
const mimeType = {
|
||||||
|
@ -216,7 +242,7 @@
|
||||||
|
|
||||||
// 11.9.2.
|
// 11.9.2.
|
||||||
parameterValue = parameterValue.replaceAll(
|
parameterValue = parameterValue.replaceAll(
|
||||||
new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"),
|
HTTP_WHITESPACE_SUFFIX_RE,
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -224,7 +250,7 @@
|
||||||
if (parameterValue === "") continue;
|
if (parameterValue === "") continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11.9.
|
// 11.10.
|
||||||
if (
|
if (
|
||||||
parameterName !== "" && HTTP_TOKEN_CODE_POINT_RE.test(parameterName) &&
|
parameterName !== "" && HTTP_TOKEN_CODE_POINT_RE.test(parameterName) &&
|
||||||
HTTP_QUOTED_STRING_TOKEN_POINT_RE.test(parameterValue) &&
|
HTTP_QUOTED_STRING_TOKEN_POINT_RE.test(parameterValue) &&
|
||||||
|
|
3
op_crates/web/internal.d.ts
vendored
3
op_crates/web/internal.d.ts
vendored
|
@ -4,6 +4,9 @@
|
||||||
/// <reference lib="esnext" />
|
/// <reference lib="esnext" />
|
||||||
|
|
||||||
declare namespace globalThis {
|
declare namespace globalThis {
|
||||||
|
declare var TextEncoder: typeof TextEncoder;
|
||||||
|
declare var TextDecoder: typeof TextDecoder;
|
||||||
|
|
||||||
declare namespace __bootstrap {
|
declare namespace __bootstrap {
|
||||||
declare var infra: {
|
declare var infra: {
|
||||||
collectSequenceOfCodepoints(
|
collectSequenceOfCodepoints(
|
||||||
|
|
51
op_crates/web/lib.deno_web.d.ts
vendored
51
op_crates/web/lib.deno_web.d.ts
vendored
|
@ -87,45 +87,44 @@ declare class Event {
|
||||||
*/
|
*/
|
||||||
declare class EventTarget {
|
declare class EventTarget {
|
||||||
/** Appends an event listener for events whose type attribute value is type.
|
/** 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
|
* The callback argument sets the callback that will be invoked when the event
|
||||||
* is dispatched.
|
* is dispatched.
|
||||||
*
|
*
|
||||||
* The options argument sets listener-specific options. For compatibility this
|
* 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
|
* can be a boolean, in which case the method behaves exactly as if the value
|
||||||
* was specified as options's capture.
|
* was specified as options's capture.
|
||||||
*
|
*
|
||||||
* When set to true, options's capture prevents callback from being invoked
|
* When set to true, options's capture prevents callback from being invoked
|
||||||
* when the event's eventPhase attribute value is BUBBLING_PHASE. When false
|
* when the event's eventPhase attribute value is BUBBLING_PHASE. When false
|
||||||
* (or not present), callback will not be invoked when event's eventPhase
|
* (or not present), callback will not be invoked when event's eventPhase
|
||||||
* attribute value is CAPTURING_PHASE. Either way, callback will be invoked if
|
* attribute value is CAPTURING_PHASE. Either way, callback will be invoked if
|
||||||
* event's eventPhase attribute value is AT_TARGET.
|
* event's eventPhase attribute value is AT_TARGET.
|
||||||
*
|
*
|
||||||
* When set to true, options's passive indicates that the callback will not
|
* When set to true, options's passive indicates that the callback will not
|
||||||
* cancel the event by invoking preventDefault(). This is used to enable
|
* cancel the event by invoking preventDefault(). This is used to enable
|
||||||
* performance optimizations described in § 2.8 Observing event listeners.
|
* performance optimizations described in § 2.8 Observing event listeners.
|
||||||
*
|
*
|
||||||
* When set to true, options's once indicates that the callback will only be
|
* When set to true, options's once indicates that the callback will only be
|
||||||
* invoked once after which the event listener will be removed.
|
* invoked once after which the event listener will be removed.
|
||||||
*
|
*
|
||||||
* The event listener is appended to target's event listener list and is not
|
* The event listener is appended to target's event listener list and is not
|
||||||
* appended if it has the same type, callback, and capture. */
|
* appended if it has the same type, callback, and capture. */
|
||||||
addEventListener(
|
addEventListener(
|
||||||
type: string,
|
type: string,
|
||||||
listener: EventListenerOrEventListenerObject | null,
|
listener: EventListenerOrEventListenerObject | null,
|
||||||
options?: boolean | AddEventListenerOptions,
|
options?: boolean | AddEventListenerOptions,
|
||||||
): void;
|
): void;
|
||||||
/** Dispatches a synthetic event event to target and returns true if either
|
/** Dispatches a synthetic event event to target and returns true if either
|
||||||
* event's cancelable attribute value is false or its preventDefault() method
|
* event's cancelable attribute value is false or its preventDefault() method
|
||||||
* was not invoked, and false otherwise. */
|
* was not invoked, and false otherwise. */
|
||||||
dispatchEvent(event: Event): boolean;
|
dispatchEvent(event: Event): boolean;
|
||||||
/** Removes the event listener in target's event listener list with the same
|
/** Removes the event listener in target's event listener list with the same
|
||||||
* type, callback, and options. */
|
* type, callback, and options. */
|
||||||
removeEventListener(
|
removeEventListener(
|
||||||
type: string,
|
type: string,
|
||||||
callback: EventListenerOrEventListenerObject | null,
|
callback: EventListenerOrEventListenerObject | null,
|
||||||
options?: EventListenerOptions | boolean,
|
options?: EventListenerOptions | boolean,
|
||||||
): void;
|
): void;
|
||||||
[Symbol.toStringTag]: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventListener {
|
interface EventListener {
|
||||||
|
|
|
@ -802,6 +802,50 @@
|
||||||
throw new TypeError("Illegal constructor");
|
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 ??= {};
|
||||||
window.__bootstrap.webidl = {
|
window.__bootstrap.webidl = {
|
||||||
makeException,
|
makeException,
|
||||||
|
@ -817,5 +861,6 @@
|
||||||
createBranded,
|
createBranded,
|
||||||
assertBranded,
|
assertBranded,
|
||||||
illegalConstructor,
|
illegalConstructor,
|
||||||
|
mixinPairIterable,
|
||||||
};
|
};
|
||||||
})(this);
|
})(this);
|
||||||
|
|
12
op_crates/webidl/internal.d.ts
vendored
12
op_crates/webidl/internal.d.ts
vendored
|
@ -286,6 +286,18 @@ declare namespace globalThis {
|
||||||
v: Record<K, V>,
|
v: Record<K, V>,
|
||||||
opts: ValueConverterOpts,
|
opts: ValueConverterOpts,
|
||||||
) => any;
|
) => 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ delete Object.prototype.__proto__;
|
||||||
const webgpu = window.__bootstrap.webgpu;
|
const webgpu = window.__bootstrap.webgpu;
|
||||||
const webSocket = window.__bootstrap.webSocket;
|
const webSocket = window.__bootstrap.webSocket;
|
||||||
const file = window.__bootstrap.file;
|
const file = window.__bootstrap.file;
|
||||||
|
const formData = window.__bootstrap.formData;
|
||||||
const fetch = window.__bootstrap.fetch;
|
const fetch = window.__bootstrap.fetch;
|
||||||
const prompt = window.__bootstrap.prompt;
|
const prompt = window.__bootstrap.prompt;
|
||||||
const denoNs = window.__bootstrap.denoNs;
|
const denoNs = window.__bootstrap.denoNs;
|
||||||
|
@ -261,7 +262,7 @@ delete Object.prototype.__proto__;
|
||||||
EventTarget: util.nonEnumerable(EventTarget),
|
EventTarget: util.nonEnumerable(EventTarget),
|
||||||
File: util.nonEnumerable(file.File),
|
File: util.nonEnumerable(file.File),
|
||||||
FileReader: util.nonEnumerable(fileReader.FileReader),
|
FileReader: util.nonEnumerable(fileReader.FileReader),
|
||||||
FormData: util.nonEnumerable(fetch.FormData),
|
FormData: util.nonEnumerable(formData.FormData),
|
||||||
Headers: util.nonEnumerable(headers.Headers),
|
Headers: util.nonEnumerable(headers.Headers),
|
||||||
MessageEvent: util.nonEnumerable(MessageEvent),
|
MessageEvent: util.nonEnumerable(MessageEvent),
|
||||||
Performance: util.nonEnumerable(performance.Performance),
|
Performance: util.nonEnumerable(performance.Performance),
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit a522daf78a71c2252d10c978f09cf0575aceb794
|
Subproject commit e19bdbe96243f2ba548c1fd01c0812d645ba0c6f
|
|
@ -578,6 +578,10 @@
|
||||||
"Parsing: <file://example.net/C:/> against <about:blank>",
|
"Parsing: <file://example.net/C:/> against <about:blank>",
|
||||||
"Parsing: <file://1.2.3.4/C:/> against <about:blank>",
|
"Parsing: <file://1.2.3.4/C:/> against <about:blank>",
|
||||||
"Parsing: <file://[1::8]/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//a//../..//foo> against <about:blank>",
|
||||||
"Parsing: <file://localhost////foo> against <about:blank>",
|
"Parsing: <file://localhost////foo> against <about:blank>",
|
||||||
"Parsing: <file:////foo> against <about:blank>",
|
"Parsing: <file:////foo> against <about:blank>",
|
||||||
|
@ -753,5 +757,17 @@
|
||||||
"queue-microtask.any.js": true
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue