summaryrefslogtreecommitdiff
path: root/ext/fetch/21_formdata.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fetch/21_formdata.js')
-rw-r--r--ext/fetch/21_formdata.js507
1 files changed, 507 insertions, 0 deletions
diff --git a/ext/fetch/21_formdata.js b/ext/fetch/21_formdata.js
new file mode 100644
index 000000000..25ed32c2d
--- /dev/null
+++ b/ext/fetch/21_formdata.js
@@ -0,0 +1,507 @@
+// 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="../web/lib.deno_web.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <reference path="../web/06_streams_types.d.ts" />
+/// <reference path="./lib.deno_fetch.d.ts" />
+/// <reference lib="esnext" />
+"use strict";
+
+((window) => {
+ const core = window.Deno.core;
+ const webidl = globalThis.__bootstrap.webidl;
+ const { Blob, File } = globalThis.__bootstrap.file;
+ const {
+ ArrayPrototypeMap,
+ ArrayPrototypePush,
+ ArrayPrototypeSlice,
+ ArrayPrototypeSplice,
+ ArrayPrototypeFilter,
+ ArrayPrototypeForEach,
+ Map,
+ MapPrototypeGet,
+ MapPrototypeSet,
+ MathRandom,
+ Symbol,
+ SymbolToStringTag,
+ StringFromCharCode,
+ StringPrototypeTrim,
+ StringPrototypeSlice,
+ StringPrototypeSplit,
+ StringPrototypeReplace,
+ StringPrototypeIndexOf,
+ StringPrototypePadStart,
+ StringPrototypeCodePointAt,
+ StringPrototypeReplaceAll,
+ TypeError,
+ TypedArrayPrototypeSubarray,
+ } = window.__bootstrap.primordials;
+
+ 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], "blob", { type: value.type });
+ }
+ if (value instanceof File && filename !== undefined) {
+ value = new File([value], 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 [SymbolToStringTag]() {
+ 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);
+
+ ArrayPrototypePush(this[entryList], 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) {
+ ArrayPrototypeSplice(list, 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) ArrayPrototypePush(returnList, 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 {
+ ArrayPrototypeSplice(list, i, 1);
+ i--;
+ }
+ }
+ }
+ if (!added) {
+ ArrayPrototypePush(list, entry);
+ }
+ }
+ }
+
+ webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value");
+
+ webidl.configurePrototype(FormData);
+
+ const escape = (str, isFilename) =>
+ StringPrototypeReplace(
+ StringPrototypeReplace(
+ StringPrototypeReplace(
+ (isFilename ? str : StringPrototypeReplace(str, /\r?\n|\r/g, "\r\n")),
+ /\n/g,
+ "%0A",
+ ),
+ /\r/g,
+ "%0D",
+ ),
+ /"/g,
+ "%22",
+ );
+
+ /**
+ * convert FormData to a Blob synchronous without reading all of the files
+ * @param {globalThis.FormData} formData
+ */
+ function formDataToBlob(formData) {
+ const boundary = StringPrototypePadStart(
+ StringPrototypeSlice(
+ StringPrototypeReplaceAll(`${MathRandom()}${MathRandom()}`, ".", ""),
+ -28,
+ ),
+ 32,
+ "-",
+ );
+ const chunks = [];
+ const prefix = `--${boundary}\r\nContent-Disposition: form-data; name="`;
+
+ for (const [name, value] of formData) {
+ if (typeof value === "string") {
+ ArrayPrototypePush(
+ chunks,
+ prefix + escape(name) + '"' + CRLF + CRLF +
+ StringPrototypeReplace(value, /\r(?!\n)|(?<!\r)\n/g, CRLF) + CRLF,
+ );
+ } else {
+ ArrayPrototypePush(
+ chunks,
+ prefix + escape(name) + `"; filename="${escape(value.name, true)}"` +
+ CRLF +
+ `Content-Type: ${value.type || "application/octet-stream"}\r\n\r\n`,
+ value,
+ CRLF,
+ );
+ }
+ }
+
+ ArrayPrototypePush(chunks, `--${boundary}--`);
+
+ return new Blob(chunks, {
+ type: "multipart/form-data; boundary=" + boundary,
+ });
+ }
+
+ /**
+ * @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
+ ArrayPrototypeForEach(
+ ArrayPrototypeMap(
+ ArrayPrototypeFilter(
+ ArrayPrototypeMap(
+ ArrayPrototypeSlice(StringPrototypeSplit(value, ";"), 1),
+ (s) => StringPrototypeSplit(StringPrototypeTrim(s), "="),
+ ),
+ (arr) => arr.length > 1,
+ ),
+ ([k, v]) => [k, StringPrototypeReplace(v, /^"([^"]*)"$/, "$1")],
+ ),
+ ([k, v]) => MapPrototypeSet(params, k, v),
+ );
+
+ return params;
+ }
+
+ const CRLF = "\r\n";
+ const LF = StringPrototypeCodePointAt(CRLF, 1);
+ const CR = StringPrototypeCodePointAt(CRLF, 0);
+
+ 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 = core.encode(this.boundary);
+ }
+
+ /**
+ * @param {string} headersText
+ * @returns {{ headers: Headers, disposition: Map<string, string> }}
+ */
+ #parseHeaders(headersText) {
+ const headers = new Headers();
+ const rawHeaders = StringPrototypeSplit(headersText, "\r\n");
+ for (const rawHeader of rawHeaders) {
+ const sepIndex = StringPrototypeIndexOf(rawHeader, ":");
+ if (sepIndex < 0) {
+ continue; // Skip this header
+ }
+ const key = StringPrototypeSlice(rawHeader, 0, sepIndex);
+ const value = StringPrototypeSlice(rawHeader, sepIndex + 1);
+ headers.set(key, value);
+ }
+
+ const disposition = parseContentDisposition(
+ headers.get("Content-Disposition") ?? "",
+ );
+
+ return { headers, disposition };
+ }
+
+ /**
+ * @returns {FormData}
+ */
+ parse() {
+ // Body must be at least 2 boundaries + \r\n + -- on the last boundary.
+ if (this.body.length < (this.boundary.length * 2) + 4) {
+ throw new TypeError("Form data too short to be valid.");
+ }
+
+ 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 += StringFromCharCode(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 = TypedArrayPrototypeSubarray(
+ this.body,
+ fileStart,
+ i - boundaryIndex - 1,
+ );
+ // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
+ const filename = MapPrototypeGet(disposition, "filename");
+ const name = MapPrototypeGet(disposition, "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, core.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();
+ }
+
+ /**
+ * @param {FormDataEntry[]} entries
+ * @returns {FormData}
+ */
+ function formDataFromEntries(entries) {
+ const fd = new FormData();
+ fd[entryList] = entries;
+ return fd;
+ }
+
+ webidl.converters["FormData"] = webidl
+ .createInterfaceConverter("FormData", FormData);
+
+ globalThis.__bootstrap.formData = {
+ FormData,
+ formDataToBlob,
+ parseFormData,
+ formDataFromEntries,
+ };
+})(globalThis);