mirror of
https://github.com/denoland/deno.git
synced 2024-11-25 15:29:32 -05: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 "./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";
|
||||
|
|
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
|
||||
/// <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;
|
||||
|
||||
|
@ -202,395 +204,6 @@
|
|||
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 {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;
|
||||
}
|
||||
|
|
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;
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
|
@ -70,6 +70,10 @@ pub fn init(isolate: &mut JsRuntime) {
|
|||
"deno:op_crates/fetch/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",
|
||||
include_str!("26_fetch.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} */
|
||||
|
|
|
@ -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} */
|
||||
|
|
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 & {
|
||||
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
|
||||
};
|
||||
_byteSequence: unique symbol;
|
||||
readonly _byteSequence: unique symbol;
|
||||
File: typeof File & {
|
||||
[globalThis.__bootstrap.file._byteSequence]: Uint8Array;
|
||||
};
|
||||
|
|
|
@ -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) &&
|
||||
|
|
3
op_crates/web/internal.d.ts
vendored
3
op_crates/web/internal.d.ts
vendored
|
@ -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(
|
||||
|
|
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 {
|
||||
/** 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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
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>,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit a522daf78a71c2252d10c978f09cf0575aceb794
|
||||
Subproject commit e19bdbe96243f2ba548c1fd01c0812d645ba0c6f
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue