refactor(FormData): refactor formdata serializer to support async blob backing (#11050)

This commit is contained in:
Jimmy Wärting 2021-06-22 14:06:37 +02:00 committed by GitHub
parent 4e3ec47857
commit 0a2ced5728
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 51 additions and 177 deletions

View file

@ -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,
};

View file

@ -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";

View file

@ -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,