summaryrefslogtreecommitdiff
path: root/op_crates/fetch/26_fetch.js
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/26_fetch.js
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/26_fetch.js')
-rw-r--r--op_crates/fetch/26_fetch.js483
1 files changed, 44 insertions, 439 deletions
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;
}