From 0a2ced57285aa0ee4b47426382c32fb53c4e07cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20W=C3=A4rting?= Date: Tue, 22 Jun 2021 14:06:37 +0200 Subject: [PATCH] refactor(FormData): refactor formdata serializer to support async blob backing (#11050) --- extensions/fetch/21_formdata.js | 205 +++++++------------------------- extensions/fetch/22_body.js | 12 +- extensions/fetch/internal.d.ts | 7 +- 3 files changed, 49 insertions(+), 175 deletions(-) diff --git a/extensions/fetch/21_formdata.js b/extensions/fetch/21_formdata.js index bbf051da121674..f0033a332548da 100644 --- a/extensions/fetch/21_formdata.js +++ b/extensions/fetch/21_formdata.js @@ -13,7 +13,7 @@ ((window) => { const core = window.Deno.core; const webidl = globalThis.__bootstrap.webidl; - const { Blob, File, _byteSequence } = globalThis.__bootstrap.file; + const { Blob, File } = globalThis.__bootstrap.file; const entryList = Symbol("entry list"); @@ -25,10 +25,10 @@ */ function createEntry(name, value, filename) { if (value instanceof Blob && !(value instanceof File)) { - value = new File([value[_byteSequence]], "blob", { type: value.type }); + value = new File([value], "blob", { type: value.type }); } if (value instanceof File && filename !== undefined) { - value = new File([value[_byteSequence]], filename, { + value = new File([value], filename, { type: value.type, lastModified: value.lastModified, }); @@ -242,170 +242,44 @@ webidl.configurePrototype(FormData); - 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(core.encode(`\r\n--${this.boundary}--`)); + const escape = (str, isFilename) => + (isFilename ? str : str.replace(/\r?\n|\r/g, "\r\n")) + .replace(/\n/g, "%0A") + .replace(/\r/g, "%0D") + .replace(/"/g, "%22"); - 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; + /** + * convert FormData to a Blob synchronous without reading all of the files + * @param {globalThis.FormData} formData + */ + function formDataToBlob(formData) { + const boundary = `${Math.random()}${Math.random()}` + .replaceAll(".", "").slice(-28).padStart(32, "-"); + const chunks = []; + const prefix = `--${boundary}\r\nContent-Disposition: form-data; name="`; + + for (const [name, value] of formData) { + if (typeof value === "string") { + chunks.push( + prefix + escape(name) + '"' + CRLF + CRLF + + value.replace(/\r(?!\n)|(? 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(core.encode(buf)); - } + chunks.push(`--${boundary}--`); - /** - * @param {string} field - * @param {string} filename - * @param {string} [type] - * @returns {void} - */ - #writeFileHeaders( - field, - filename, - type, - ) { - const escapedField = this.#headerEscape(field); - const escapedFilename = this.#headerEscape(filename, true); - /** @type {[string, string][]} */ - const headers = [ - [ - "Content-Disposition", - `form-data; name="${escapedField}"; filename="${escapedFilename}"`, - ], - ["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="${this.#headerEscape(field)}"`, - ]]; - return this.#writeHeaders(headers); - } - - /** - * @param {string} field - * @param {string} value - * @returns {void} - */ - #writeField(field, value) { - this.#writeFieldHeaders(field); - this.chunks.push(core.encode(this.#normalizeNewlines(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 {string} string - * @returns {string} - */ - #normalizeNewlines(string) { - return string.replace(/\r(?!\n)|(?