mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
refactor(FormData): refactor formdata serializer to support async blob backing (#11050)
This commit is contained in:
parent
4e3ec47857
commit
0a2ced5728
3 changed files with 51 additions and 177 deletions
|
@ -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}--`));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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");
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
||||
/**
|
||||
* @param {FormData} formdata
|
||||
* @returns {{body: Uint8Array, contentType: string}}
|
||||
* convert FormData to a Blob synchronous without reading all of the files
|
||||
* @param {globalThis.FormData} formData
|
||||
*/
|
||||
function encodeFormData(formdata) {
|
||||
const builder = new MultipartBuilder(formdata);
|
||||
return {
|
||||
body: builder.getBody(),
|
||||
contentType: builder.getContentType(),
|
||||
};
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
chunks.push(`--${boundary}--`);
|
||||
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
7
extensions/fetch/internal.d.ts
vendored
7
extensions/fetch/internal.d.ts
vendored
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue