summaryrefslogtreecommitdiff
path: root/op_crates/fetch
diff options
context:
space:
mode:
authorLuca Casonato <lucacasonato@yahoo.com>2021-04-14 22:49:16 +0200
committerGitHub <noreply@github.com>2021-04-14 22:49:16 +0200
commit353e79c796efc6c6aa328abb4d7cef5e642944af (patch)
tree5ae6d1778c0a1dfa53ac62e409d6da8cdb31285f /op_crates/fetch
parent5214acd3d9dec56ee159544f0f6bf9834a62c097 (diff)
chore: align FormData to spec (#10169)
This PR aligns `FormData` to spec. All WPT tests are passing.
Diffstat (limited to 'op_crates/fetch')
-rw-r--r--op_crates/fetch/21_formdata.js529
-rw-r--r--op_crates/fetch/26_fetch.js483
-rw-r--r--op_crates/fetch/internal.d.ts9
-rw-r--r--op_crates/fetch/lib.rs4
4 files changed, 586 insertions, 439 deletions
diff --git a/op_crates/fetch/21_formdata.js b/op_crates/fetch/21_formdata.js
new file mode 100644
index 000000000..106b67da4
--- /dev/null
+++ b/op_crates/fetch/21_formdata.js
@@ -0,0 +1,529 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../file/internal.d.ts" />
+/// <reference path="../file/lib.deno_file.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <reference path="./11_streams_types.d.ts" />
+/// <reference path="./lib.deno_fetch.d.ts" />
+/// <reference lib="esnext" />
+"use strict";
+
+((window) => {
+ const webidl = globalThis.__bootstrap.webidl;
+ const { Blob, File, _byteSequence } = globalThis.__bootstrap.file;
+
+ const entryList = Symbol("entry list");
+
+ /**
+ * @param {string} name
+ * @param {string | Blob} value
+ * @param {string | undefined} filename
+ * @returns {FormDataEntry}
+ */
+ function createEntry(name, value, filename) {
+ if (value instanceof Blob && !(value instanceof File)) {
+ value = new File([value[_byteSequence]], "blob", { type: value.type });
+ }
+ if (value instanceof File && filename !== undefined) {
+ value = new File([value[_byteSequence]], filename, {
+ type: value.type,
+ lastModified: value.lastModified,
+ });
+ }
+ return {
+ name,
+ // @ts-expect-error because TS is not smart enough
+ value,
+ };
+ }
+
+ /**
+ * @typedef FormDataEntry
+ * @property {string} name
+ * @property {FormDataEntryValue} value
+ */
+
+ class FormData {
+ get [Symbol.toStringTag]() {
+ return "FormData";
+ }
+
+ /** @type {FormDataEntry[]} */
+ [entryList] = [];
+
+ /** @param {void} form */
+ constructor(form) {
+ if (form !== undefined) {
+ webidl.illegalConstructor();
+ }
+ this[webidl.brand] = webidl.brand;
+ }
+
+ /**
+ * @param {string} name
+ * @param {string | Blob} valueOrBlobValue
+ * @param {string} [filename]
+ * @returns {void}
+ */
+ append(name, valueOrBlobValue, filename) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'append' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ if (valueOrBlobValue instanceof Blob) {
+ valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ if (filename !== undefined) {
+ filename = webidl.converters["USVString"](filename, {
+ prefix,
+ context: "Argument 3",
+ });
+ }
+ } else {
+ valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ }
+
+ const entry = createEntry(name, valueOrBlobValue, filename);
+
+ this[entryList].push(entry);
+ }
+
+ /**
+ * @param {string} name
+ * @returns {void}
+ */
+ delete(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'name' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ const list = this[entryList];
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].name === name) {
+ list.splice(i, 1);
+ i--;
+ }
+ }
+ }
+
+ /**
+ * @param {string} name
+ * @returns {FormDataEntryValue | null}
+ */
+ get(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'get' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ for (const entry of this[entryList]) {
+ if (entry.name === name) return entry.value;
+ }
+ return null;
+ }
+
+ /**
+ * @param {string} name
+ * @returns {FormDataEntryValue[]}
+ */
+ getAll(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'getAll' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ const returnList = [];
+ for (const entry of this[entryList]) {
+ if (entry.name === name) returnList.push(entry.value);
+ }
+ return returnList;
+ }
+
+ /**
+ * @param {string} name
+ * @returns {boolean}
+ */
+ has(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'has' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ for (const entry of this[entryList]) {
+ if (entry.name === name) return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param {string} name
+ * @param {string | Blob} valueOrBlobValue
+ * @param {string} [filename]
+ * @returns {void}
+ */
+ set(name, valueOrBlobValue, filename) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'set' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ if (valueOrBlobValue instanceof Blob) {
+ valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ if (filename !== undefined) {
+ filename = webidl.converters["USVString"](filename, {
+ prefix,
+ context: "Argument 3",
+ });
+ }
+ } else {
+ valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, {
+ prefix,
+ context: "Argument 2",
+ });
+ }
+
+ const entry = createEntry(name, valueOrBlobValue, filename);
+
+ const list = this[entryList];
+ let added = false;
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].name === name) {
+ if (!added) {
+ list[i] = entry;
+ added = true;
+ } else {
+ list.splice(i, 1);
+ i--;
+ }
+ }
+ }
+ if (!added) {
+ list.push(entry);
+ }
+ }
+ }
+
+ webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value");
+
+ const encoder = new TextEncoder();
+
+ class MultipartBuilder {
+ /**
+ * @param {FormData} formData
+ */
+ constructor(formData) {
+ this.entryList = formData[entryList];
+ this.boundary = this.#createBoundary();
+ /** @type {Uint8Array[]} */
+ this.chunks = [];
+ }
+
+ /**
+ * @returns {string}
+ */
+ getContentType() {
+ return `multipart/form-data; boundary=${this.boundary}`;
+ }
+
+ /**
+ * @returns {Uint8Array}
+ */
+ getBody() {
+ for (const { name, value } of this.entryList) {
+ if (value instanceof File) {
+ this.#writeFile(name, value);
+ } else this.#writeField(name, value);
+ }
+
+ this.chunks.push(encoder.encode(`\r\n--${this.boundary}--`));
+
+ let totalLength = 0;
+ for (const chunk of this.chunks) {
+ totalLength += chunk.byteLength;
+ }
+
+ const finalBuffer = new Uint8Array(totalLength);
+ let i = 0;
+ for (const chunk of this.chunks) {
+ finalBuffer.set(chunk, i);
+ i += chunk.byteLength;
+ }
+
+ return finalBuffer;
+ }
+
+ #createBoundary = () => {
+ return (
+ "----------" +
+ Array.from(Array(32))
+ .map(() => Math.random().toString(36)[2] || 0)
+ .join("")
+ );
+ };
+
+ /**
+ * @param {[string, string][]} headers
+ * @returns {void}
+ */
+ #writeHeaders = (headers) => {
+ let buf = (this.chunks.length === 0) ? "" : "\r\n";
+
+ buf += `--${this.boundary}\r\n`;
+ for (const [key, value] of headers) {
+ buf += `${key}: ${value}\r\n`;
+ }
+ buf += `\r\n`;
+
+ this.chunks.push(encoder.encode(buf));
+ };
+
+ /**
+ * @param {string} field
+ * @param {string} filename
+ * @param {string} [type]
+ * @returns {void}
+ */
+ #writeFileHeaders = (
+ field,
+ filename,
+ type,
+ ) => {
+ /** @type {[string, string][]} */
+ const headers = [
+ [
+ "Content-Disposition",
+ `form-data; name="${field}"; filename="${filename}"`,
+ ],
+ ["Content-Type", type || "application/octet-stream"],
+ ];
+ return this.#writeHeaders(headers);
+ };
+
+ /**
+ * @param {string} field
+ * @returns {void}
+ */
+ #writeFieldHeaders = (field) => {
+ /** @type {[string, string][]} */
+ const headers = [["Content-Disposition", `form-data; name="${field}"`]];
+ return this.#writeHeaders(headers);
+ };
+
+ /**
+ * @param {string} field
+ * @param {string} value
+ * @returns {void}
+ */
+ #writeField = (field, value) => {
+ this.#writeFieldHeaders(field);
+ this.chunks.push(encoder.encode(value));
+ };
+
+ /**
+ * @param {string} field
+ * @param {File} value
+ * @returns {void}
+ */
+ #writeFile = (field, value) => {
+ this.#writeFileHeaders(field, value.name, value.type);
+ this.chunks.push(value[_byteSequence]);
+ };
+ }
+
+ /**
+ * @param {FormData} formdata
+ * @returns {{body: Uint8Array, contentType: string}}
+ */
+ function encodeFormData(formdata) {
+ const builder = new MultipartBuilder(formdata);
+ return {
+ body: builder.getBody(),
+ contentType: builder.getContentType(),
+ };
+ }
+
+ /**
+ * @param {string} value
+ * @returns {Map<string, string>}
+ */
+ function parseContentDisposition(value) {
+ /** @type {Map<string, string>} */
+ const params = new Map();
+ // Forced to do so for some Map constructor param mismatch
+ value
+ .split(";")
+ .slice(1)
+ .map((s) => s.trim().split("="))
+ .filter((arr) => arr.length > 1)
+ .map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")])
+ .forEach(([k, v]) => params.set(k, v));
+ return params;
+ }
+
+ const LF = "\n".codePointAt(0);
+ const CR = "\r".codePointAt(0);
+ const decoder = new TextDecoder("utf-8");
+
+ class MultipartParser {
+ /**
+ * @param {Uint8Array} body
+ * @param {string | undefined} boundary
+ */
+ constructor(body, boundary) {
+ if (!boundary) {
+ throw new TypeError("multipart/form-data must provide a boundary");
+ }
+
+ this.boundary = `--${boundary}`;
+ this.body = body;
+ this.boundaryChars = encoder.encode(this.boundary);
+ }
+
+ /**
+ * @param {string} headersText
+ * @returns {{ headers: Headers, disposition: Map<string, string> }}
+ */
+ #parseHeaders = (headersText) => {
+ const headers = new Headers();
+ const rawHeaders = headersText.split("\r\n");
+ for (const rawHeader of rawHeaders) {
+ const sepIndex = rawHeader.indexOf(":");
+ if (sepIndex < 0) {
+ continue; // Skip this header
+ }
+ const key = rawHeader.slice(0, sepIndex);
+ const value = rawHeader.slice(sepIndex + 1);
+ headers.set(key, value);
+ }
+
+ const disposition = parseContentDisposition(
+ headers.get("Content-Disposition") ?? "",
+ );
+
+ return { headers, disposition };
+ };
+
+ /**
+ * @returns {FormData}
+ */
+ parse() {
+ const formData = new FormData();
+ let headerText = "";
+ let boundaryIndex = 0;
+ let state = 0;
+ let fileStart = 0;
+
+ for (let i = 0; i < this.body.length; i++) {
+ const byte = this.body[i];
+ const prevByte = this.body[i - 1];
+ const isNewLine = byte === LF && prevByte === CR;
+
+ if (state === 1 || state === 2 || state == 3) {
+ headerText += String.fromCharCode(byte);
+ }
+ if (state === 0 && isNewLine) {
+ state = 1;
+ } else if (state === 1 && isNewLine) {
+ state = 2;
+ const headersDone = this.body[i + 1] === CR &&
+ this.body[i + 2] === LF;
+
+ if (headersDone) {
+ state = 3;
+ }
+ } else if (state === 2 && isNewLine) {
+ state = 3;
+ } else if (state === 3 && isNewLine) {
+ state = 4;
+ fileStart = i + 1;
+ } else if (state === 4) {
+ if (this.boundaryChars[boundaryIndex] !== byte) {
+ boundaryIndex = 0;
+ } else {
+ boundaryIndex++;
+ }
+
+ if (boundaryIndex >= this.boundary.length) {
+ const { headers, disposition } = this.#parseHeaders(headerText);
+ const content = this.body.subarray(
+ fileStart,
+ i - boundaryIndex - 1,
+ );
+ // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
+ const filename = disposition.get("filename");
+ const name = disposition.get("name");
+
+ state = 5;
+ // Reset
+ boundaryIndex = 0;
+ headerText = "";
+
+ if (!name) {
+ continue; // Skip, unknown name
+ }
+
+ if (filename) {
+ const blob = new Blob([content], {
+ type: headers.get("Content-Type") || "application/octet-stream",
+ });
+ formData.append(name, blob, filename);
+ } else {
+ formData.append(name, decoder.decode(content));
+ }
+ }
+ } else if (state === 5 && isNewLine) {
+ state = 1;
+ }
+ }
+
+ return formData;
+ }
+ }
+
+ /**
+ * @param {Uint8Array} body
+ * @param {string | undefined} boundary
+ * @returns {FormData}
+ */
+ function parseFormData(body, boundary) {
+ const parser = new MultipartParser(body, boundary);
+ return parser.parse();
+ }
+
+ globalThis.__bootstrap.formData = { FormData, encodeFormData, parseFormData };
+})(globalThis);
diff --git a/op_crates/fetch/26_fetch.js b/op_crates/fetch/26_fetch.js
index e713b48ed..d07121e86 100644
--- a/op_crates/fetch/26_fetch.js
+++ b/op_crates/fetch/26_fetch.js
@@ -3,6 +3,7 @@
// @ts-check
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../web/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
/// <reference path="./11_streams_types.d.ts" />
/// <reference path="./internal.d.ts" />
@@ -16,11 +17,12 @@
// provided by "deno_web"
const { URLSearchParams } = window.__bootstrap.url;
const { getLocationHref } = window.__bootstrap.location;
+ const { FormData, parseFormData, encodeFormData } =
+ window.__bootstrap.formData;
+ const { parseMimeType } = window.__bootstrap.mimesniff;
- const { requiredArguments } = window.__bootstrap.fetchUtil;
const { ReadableStream, isReadableStreamDisturbed } =
window.__bootstrap.streams;
- const { DomIterableMixin } = window.__bootstrap.domIterable;
const { Headers } = window.__bootstrap.headers;
const { Blob, _byteSequence, File } = window.__bootstrap.file;
@@ -203,395 +205,6 @@
}
/**
- * @param {string} value
- * @returns {Map<string, string>}
- */
- function getHeaderValueParams(value) {
- /** @type {Map<string, string>} */
- const params = new Map();
- // Forced to do so for some Map constructor param mismatch
- value
- .split(";")
- .slice(1)
- .map((s) => s.trim().split("="))
- .filter((arr) => arr.length > 1)
- .map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")])
- .forEach(([k, v]) => params.set(k, v));
- return params;
- }
-
- const decoder = new TextDecoder();
- const encoder = new TextEncoder();
- const CR = "\r".charCodeAt(0);
- const LF = "\n".charCodeAt(0);
-
- const dataSymbol = Symbol("data");
-
- /**
- * @param {Blob | string} value
- * @param {string | undefined} filename
- * @returns {FormDataEntryValue}
- */
- function parseFormDataValue(value, filename) {
- if (value instanceof File) {
- return new File([value], filename || value.name, {
- type: value.type,
- lastModified: value.lastModified,
- });
- } else if (value instanceof Blob) {
- return new File([value], filename || "blob", {
- type: value.type,
- });
- } else {
- return String(value);
- }
- }
-
- class FormDataBase {
- /** @type {[name: string, entry: FormDataEntryValue][]} */
- [dataSymbol] = [];
-
- /**
- * @param {string} name
- * @param {string | Blob} value
- * @param {string} [filename]
- * @returns {void}
- */
- append(name, value, filename) {
- requiredArguments("FormData.append", arguments.length, 2);
- name = String(name);
- this[dataSymbol].push([name, parseFormDataValue(value, filename)]);
- }
-
- /**
- * @param {string} name
- * @returns {void}
- */
- delete(name) {
- requiredArguments("FormData.delete", arguments.length, 1);
- name = String(name);
- let i = 0;
- while (i < this[dataSymbol].length) {
- if (this[dataSymbol][i][0] === name) {
- this[dataSymbol].splice(i, 1);
- } else {
- i++;
- }
- }
- }
-
- /**
- * @param {string} name
- * @returns {FormDataEntryValue[]}
- */
- getAll(name) {
- requiredArguments("FormData.getAll", arguments.length, 1);
- name = String(name);
- const values = [];
- for (const entry of this[dataSymbol]) {
- if (entry[0] === name) {
- values.push(entry[1]);
- }
- }
-
- return values;
- }
-
- /**
- * @param {string} name
- * @returns {FormDataEntryValue | null}
- */
- get(name) {
- requiredArguments("FormData.get", arguments.length, 1);
- name = String(name);
- for (const entry of this[dataSymbol]) {
- if (entry[0] === name) {
- return entry[1];
- }
- }
-
- return null;
- }
-
- /**
- * @param {string} name
- * @returns {boolean}
- */
- has(name) {
- requiredArguments("FormData.has", arguments.length, 1);
- name = String(name);
- return this[dataSymbol].some((entry) => entry[0] === name);
- }
-
- /**
- * @param {string} name
- * @param {string | Blob} value
- * @param {string} [filename]
- * @returns {void}
- */
- set(name, value, filename) {
- requiredArguments("FormData.set", arguments.length, 2);
- name = String(name);
-
- // If there are any entries in the context object’s entry list whose name
- // is name, replace the first such entry with entry and remove the others
- let found = false;
- let i = 0;
- while (i < this[dataSymbol].length) {
- if (this[dataSymbol][i][0] === name) {
- if (!found) {
- this[dataSymbol][i][1] = parseFormDataValue(value, filename);
- found = true;
- } else {
- this[dataSymbol].splice(i, 1);
- continue;
- }
- }
- i++;
- }
-
- // Otherwise, append entry to the context object’s entry list.
- if (!found) {
- this[dataSymbol].push([name, parseFormDataValue(value, filename)]);
- }
- }
-
- get [Symbol.toStringTag]() {
- return "FormData";
- }
- }
-
- class FormData extends DomIterableMixin(FormDataBase, dataSymbol) {}
-
- class MultipartBuilder {
- /**
- * @param {FormData} formData
- * @param {string} [boundary]
- */
- constructor(formData, boundary) {
- this.formData = formData;
- this.boundary = boundary ?? this.#createBoundary();
- this.writer = new Buffer();
- }
-
- /**
- * @returns {string}
- */
- getContentType() {
- return `multipart/form-data; boundary=${this.boundary}`;
- }
-
- /**
- * @returns {Uint8Array}
- */
- getBody() {
- for (const [fieldName, fieldValue] of this.formData.entries()) {
- if (fieldValue instanceof File) {
- this.#writeFile(fieldName, fieldValue);
- } else this.#writeField(fieldName, fieldValue);
- }
-
- this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`));
-
- return this.writer.bytes();
- }
-
- #createBoundary = () => {
- return (
- "----------" +
- Array.from(Array(32))
- .map(() => Math.random().toString(36)[2] || 0)
- .join("")
- );
- };
-
- /**
- * @param {[string, string][]} headers
- * @returns {void}
- */
- #writeHeaders = (headers) => {
- let buf = this.writer.empty() ? "" : "\r\n";
-
- buf += `--${this.boundary}\r\n`;
- for (const [key, value] of headers) {
- buf += `${key}: ${value}\r\n`;
- }
- buf += `\r\n`;
-
- this.writer.writeSync(encoder.encode(buf));
- };
-
- /**
- * @param {string} field
- * @param {string} filename
- * @param {string} [type]
- * @returns {void}
- */
- #writeFileHeaders = (
- field,
- filename,
- type,
- ) => {
- /** @type {[string, string][]} */
- const headers = [
- [
- "Content-Disposition",
- `form-data; name="${field}"; filename="${filename}"`,
- ],
- ["Content-Type", type || "application/octet-stream"],
- ];
- return this.#writeHeaders(headers);
- };
-
- /**
- * @param {string} field
- * @returns {void}
- */
- #writeFieldHeaders = (field) => {
- /** @type {[string, string][]} */
- const headers = [["Content-Disposition", `form-data; name="${field}"`]];
- return this.#writeHeaders(headers);
- };
-
- /**
- * @param {string} field
- * @param {string} value
- * @returns {void}
- */
- #writeField = (field, value) => {
- this.#writeFieldHeaders(field);
- this.writer.writeSync(encoder.encode(value));
- };
-
- /**
- * @param {string} field
- * @param {File} value
- * @returns {void}
- */
- #writeFile = (field, value) => {
- this.#writeFileHeaders(field, value.name, value.type);
- this.writer.writeSync(value[_byteSequence]);
- };
- }
-
- class MultipartParser {
- /**
- * @param {Uint8Array} body
- * @param {string | undefined} boundary
- */
- constructor(body, boundary) {
- if (!boundary) {
- throw new TypeError("multipart/form-data must provide a boundary");
- }
-
- this.boundary = `--${boundary}`;
- this.body = body;
- this.boundaryChars = encoder.encode(this.boundary);
- }
-
- /**
- * @param {string} headersText
- * @returns {{ headers: Headers, disposition: Map<string, string> }}
- */
- #parseHeaders = (headersText) => {
- const headers = new Headers();
- const rawHeaders = headersText.split("\r\n");
- for (const rawHeader of rawHeaders) {
- const sepIndex = rawHeader.indexOf(":");
- if (sepIndex < 0) {
- continue; // Skip this header
- }
- const key = rawHeader.slice(0, sepIndex);
- const value = rawHeader.slice(sepIndex + 1);
- headers.set(key, value);
- }
-
- return {
- headers,
- disposition: getHeaderValueParams(
- headers.get("Content-Disposition") ?? "",
- ),
- };
- };
-
- /**
- * @returns {FormData}
- */
- parse() {
- const formData = new FormData();
- let headerText = "";
- let boundaryIndex = 0;
- let state = 0;
- let fileStart = 0;
-
- for (let i = 0; i < this.body.length; i++) {
- const byte = this.body[i];
- const prevByte = this.body[i - 1];
- const isNewLine = byte === LF && prevByte === CR;
-
- if (state === 1 || state === 2 || state == 3) {
- headerText += String.fromCharCode(byte);
- }
- if (state === 0 && isNewLine) {
- state = 1;
- } else if (state === 1 && isNewLine) {
- state = 2;
- const headersDone = this.body[i + 1] === CR &&
- this.body[i + 2] === LF;
-
- if (headersDone) {
- state = 3;
- }
- } else if (state === 2 && isNewLine) {
- state = 3;
- } else if (state === 3 && isNewLine) {
- state = 4;
- fileStart = i + 1;
- } else if (state === 4) {
- if (this.boundaryChars[boundaryIndex] !== byte) {
- boundaryIndex = 0;
- } else {
- boundaryIndex++;
- }
-
- if (boundaryIndex >= this.boundary.length) {
- const { headers, disposition } = this.#parseHeaders(headerText);
- const content = this.body.subarray(
- fileStart,
- i - boundaryIndex - 1,
- );
- // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
- const filename = disposition.get("filename");
- const name = disposition.get("name");
-
- state = 5;
- // Reset
- boundaryIndex = 0;
- headerText = "";
-
- if (!name) {
- continue; // Skip, unknown name
- }
-
- if (filename) {
- const blob = new Blob([content], {
- type: headers.get("Content-Type") || "application/octet-stream",
- });
- formData.append(name, blob, filename);
- } else {
- formData.append(name, decoder.decode(content));
- }
- }
- } else if (state === 5 && isNewLine) {
- state = 1;
- }
- }
-
- return formData;
- }
- }
-
- /**
* @param {string} name
* @param {BodyInit | null} bodySource
*/
@@ -785,46 +398,46 @@
/** @returns {Promise<FormData>} */
async formData() {
const formData = new FormData();
- if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) {
- const params = getHeaderValueParams(this.#contentType);
-
- // ref: https://tools.ietf.org/html/rfc2046#section-5.1
- const boundary = params.get("boundary");
- const body = new Uint8Array(await this.arrayBuffer());
- const multipartParser = new MultipartParser(body, boundary);
-
- return multipartParser.parse();
- } else if (
- hasHeaderValueOf(this.#contentType, "application/x-www-form-urlencoded")
- ) {
- // From https://github.com/github/fetch/blob/master/fetch.js
- // Copyright (c) 2014-2016 GitHub, Inc. MIT License
- const body = await this.text();
- try {
- body
- .trim()
- .split("&")
- .forEach((bytes) => {
- if (bytes) {
- const split = bytes.split("=");
- if (split.length >= 2) {
- // @ts-expect-error this is safe because of the above check
- const name = split.shift().replace(/\+/g, " ");
- const value = split.join("=").replace(/\+/g, " ");
- formData.append(
- decodeURIComponent(name),
- decodeURIComponent(value),
- );
+ const mimeType = parseMimeType(this.#contentType);
+ if (mimeType) {
+ if (mimeType.type === "multipart" && mimeType.subtype === "form-data") {
+ // ref: https://tools.ietf.org/html/rfc2046#section-5.1
+ const boundary = mimeType.parameters.get("boundary");
+ const body = new Uint8Array(await this.arrayBuffer());
+ return parseFormData(body, boundary);
+ } else if (
+ mimeType.type === "application" &&
+ mimeType.subtype === "x-www-form-urlencoded"
+ ) {
+ // From https://github.com/github/fetch/blob/master/fetch.js
+ // Copyright (c) 2014-2016 GitHub, Inc. MIT License
+ const body = await this.text();
+ try {
+ body
+ .trim()
+ .split("&")
+ .forEach((bytes) => {
+ if (bytes) {
+ const split = bytes.split("=");
+ if (split.length >= 2) {
+ // @ts-expect-error this is safe because of the above check
+ const name = split.shift().replace(/\+/g, " ");
+ const value = split.join("=").replace(/\+/g, " ");
+ formData.append(
+ decodeURIComponent(name),
+ decodeURIComponent(value),
+ );
+ }
}
- }
- });
- } catch (e) {
- throw new TypeError("Invalid form urlencoded format");
+ });
+ } catch (e) {
+ throw new TypeError("Invalid form urlencoded format");
+ }
+ return formData;
}
- return formData;
- } else {
- throw new TypeError("Invalid form data");
}
+
+ throw new TypeError("Invalid form data");
}
/** @returns {Promise<string>} */
@@ -1374,17 +987,9 @@
body = init.body[_byteSequence];
contentType = init.body.type;
} else if (init.body instanceof FormData) {
- let boundary;
- if (headers.has("content-type")) {
- const params = getHeaderValueParams("content-type");
- boundary = params.get("boundary");
- }
- const multipartBuilder = new MultipartBuilder(
- init.body,
- boundary,
- );
- body = multipartBuilder.getBody();
- contentType = multipartBuilder.getContentType();
+ const res = encodeFormData(init.body);
+ body = res.body;
+ contentType = res.contentType;
} else if (init.body instanceof ReadableStream) {
body = init.body;
}
diff --git a/op_crates/fetch/internal.d.ts b/op_crates/fetch/internal.d.ts
index e02bc6ed2..3206008c5 100644
--- a/op_crates/fetch/internal.d.ts
+++ b/op_crates/fetch/internal.d.ts
@@ -19,6 +19,15 @@ declare namespace globalThis {
Headers: typeof Headers;
};
+ declare var formData: {
+ FormData: typeof FormData;
+ encodeFormData(formdata: FormData): {
+ body: Uint8Array;
+ contentType: string;
+ };
+ parseFormData(body: Uint8Array, boundary: string | undefined): FormData;
+ };
+
declare var streams: {
ReadableStream: typeof ReadableStream;
isReadableStreamDisturbed(stream: ReadableStream): boolean;
diff --git a/op_crates/fetch/lib.rs b/op_crates/fetch/lib.rs
index ae716af41..9eeda059a 100644
--- a/op_crates/fetch/lib.rs
+++ b/op_crates/fetch/lib.rs
@@ -71,6 +71,10 @@ pub fn init(isolate: &mut JsRuntime) {
include_str!("20_headers.js"),
),
(
+ "deno:op_crates/fetch/21_formdata.js",
+ include_str!("21_formdata.js"),
+ ),
+ (
"deno:op_crates/fetch/26_fetch.js",
include_str!("26_fetch.js"),
),