diff options
-rw-r--r-- | extensions/fetch/21_formdata.js | 205 | ||||
-rw-r--r-- | extensions/fetch/22_body.js | 12 | ||||
-rw-r--r-- | 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 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, }; diff --git a/extensions/fetch/22_body.js b/extensions/fetch/22_body.js index d74269f24..475af035f 100644 --- a/extensions/fetch/22_body.js +++ b/extensions/fetch/22_body.js @@ -16,7 +16,7 @@ const core = window.Deno.core; const webidl = globalThis.__bootstrap.webidl; const { parseUrlEncoded } = globalThis.__bootstrap.url; - const { parseFormData, formDataFromEntries, encodeFormData } = + const { parseFormData, formDataFromEntries, formDataToBlob } = globalThis.__bootstrap.formData; const mimesniff = globalThis.__bootstrap.mimesniff; const { isReadableStreamDisturbed, errorReadableStream } = @@ -311,11 +311,11 @@ const copy = u8.slice(0, u8.byteLength); source = copy; } else if (object instanceof FormData) { - const res = encodeFormData(object); - stream = { body: res.body, consumed: false }; - source = object; - length = res.body.byteLength; - contentType = res.contentType; + const res = formDataToBlob(object); + stream = res.stream(); + source = res; + length = res.size; + contentType = res.type; } else if (object instanceof URLSearchParams) { source = core.encode(object.toString()); contentType = "application/x-www-form-urlencoded;charset=UTF-8"; diff --git a/extensions/fetch/internal.d.ts b/extensions/fetch/internal.d.ts index 6bdcc34ae..a84e0bcce 100644 --- a/extensions/fetch/internal.d.ts +++ b/extensions/fetch/internal.d.ts @@ -41,10 +41,9 @@ declare namespace globalThis { declare namespace formData { declare type FormData = typeof FormData; - declare function encodeFormData(formdata: FormData): { - body: Uint8Array; - contentType: string; - }; + declare function formDataToBlob( + formData: globalThis.FormData, + ): Blob; declare function parseFormData( body: Uint8Array, boundary: string | undefined, |