summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--extensions/fetch/21_formdata.js205
-rw-r--r--extensions/fetch/22_body.js12
-rw-r--r--extensions/fetch/internal.d.ts7
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,