diff options
author | Jimmy Wärting <jimmy@warting.se> | 2021-06-22 14:06:37 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-22 14:06:37 +0200 |
commit | 0a2ced57285aa0ee4b47426382c32fb53c4e07cd (patch) | |
tree | 4c55deea8cf181f4e6363a7368384d690905b3c5 /extensions/fetch/21_formdata.js | |
parent | 4e3ec478573ede7247fd306cad1ea5bf2d5c9565 (diff) |
refactor(FormData): refactor formdata serializer to support async blob backing (#11050)
Diffstat (limited to 'extensions/fetch/21_formdata.js')
-rw-r--r-- | extensions/fetch/21_formdata.js | 205 |
1 files changed, 40 insertions, 165 deletions
diff --git a/extensions/fetch/21_formdata.js b/extensions/fetch/21_formdata.js index bbf051da1..f0033a332 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)|(?<!\r)\n/g, CRLF) + CRLF, + ); + } else { + chunks.push( + prefix + escape(name) + `"; filename="${escape(value.name, true)}"` + + CRLF + + `Content-Type: ${value.type || "application/octet-stream"}\r\n\r\n`, + value, + CRLF, + ); } - - 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(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)|(?<!\r)\n/g, "\r\n"); - } - - /** - * Performs the percent-escaping and the normalization required for field - * names and filenames in Content-Disposition headers. - * @param {string} name - * @param {boolean} isFilename Whether we are encoding a filename. This - * skips the newline normalization that takes place for field names. - * @returns {string} - */ - #headerEscape(name, isFilename = false) { - if (!isFilename) { - name = this.#normalizeNewlines(name); - } - return name - .replaceAll("\n", "%0A") - .replaceAll("\r", "%0D") - .replaceAll('"', "%22"); - } - } - - /** - * @param {FormData} formdata - * @returns {{body: Uint8Array, contentType: string}} - */ - function encodeFormData(formdata) { - const builder = new MultipartBuilder(formdata); - return { - body: builder.getBody(), - contentType: builder.getContentType(), - }; + return new Blob(chunks, { + type: "multipart/form-data; boundary=" + boundary, + }); } /** @@ -426,8 +300,9 @@ return params; } - const LF = "\n".codePointAt(0); - const CR = "\r".codePointAt(0); + const CRLF = "\r\n"; + const LF = CRLF.codePointAt(1); + const CR = CRLF.codePointAt(0); class MultipartParser { /** @@ -575,7 +450,7 @@ globalThis.__bootstrap.formData = { FormData, - encodeFormData, + formDataToBlob, parseFormData, formDataFromEntries, }; |