mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -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) => {
|
((window) => {
|
||||||
const core = window.Deno.core;
|
const core = window.Deno.core;
|
||||||
const webidl = globalThis.__bootstrap.webidl;
|
const webidl = globalThis.__bootstrap.webidl;
|
||||||
const { Blob, File, _byteSequence } = globalThis.__bootstrap.file;
|
const { Blob, File } = globalThis.__bootstrap.file;
|
||||||
|
|
||||||
const entryList = Symbol("entry list");
|
const entryList = Symbol("entry list");
|
||||||
|
|
||||||
|
@ -25,10 +25,10 @@
|
||||||
*/
|
*/
|
||||||
function createEntry(name, value, filename) {
|
function createEntry(name, value, filename) {
|
||||||
if (value instanceof Blob && !(value instanceof File)) {
|
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) {
|
if (value instanceof File && filename !== undefined) {
|
||||||
value = new File([value[_byteSequence]], filename, {
|
value = new File([value], filename, {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
lastModified: value.lastModified,
|
lastModified: value.lastModified,
|
||||||
});
|
});
|
||||||
|
@ -242,170 +242,44 @@
|
||||||
|
|
||||||
webidl.configurePrototype(FormData);
|
webidl.configurePrototype(FormData);
|
||||||
|
|
||||||
class MultipartBuilder {
|
const escape = (str, isFilename) =>
|
||||||
/**
|
(isFilename ? str : str.replace(/\r?\n|\r/g, "\r\n"))
|
||||||
* @param {FormData} formData
|
.replace(/\n/g, "%0A")
|
||||||
*/
|
.replace(/\r/g, "%0D")
|
||||||
constructor(formData) {
|
.replace(/"/g, "%22");
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {FormData} formdata
|
* convert FormData to a Blob synchronous without reading all of the files
|
||||||
* @returns {{body: Uint8Array, contentType: string}}
|
* @param {globalThis.FormData} formData
|
||||||
*/
|
*/
|
||||||
function encodeFormData(formdata) {
|
function formDataToBlob(formData) {
|
||||||
const builder = new MultipartBuilder(formdata);
|
const boundary = `${Math.random()}${Math.random()}`
|
||||||
return {
|
.replaceAll(".", "").slice(-28).padStart(32, "-");
|
||||||
body: builder.getBody(),
|
const chunks = [];
|
||||||
contentType: builder.getContentType(),
|
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;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LF = "\n".codePointAt(0);
|
const CRLF = "\r\n";
|
||||||
const CR = "\r".codePointAt(0);
|
const LF = CRLF.codePointAt(1);
|
||||||
|
const CR = CRLF.codePointAt(0);
|
||||||
|
|
||||||
class MultipartParser {
|
class MultipartParser {
|
||||||
/**
|
/**
|
||||||
|
@ -575,7 +450,7 @@
|
||||||
|
|
||||||
globalThis.__bootstrap.formData = {
|
globalThis.__bootstrap.formData = {
|
||||||
FormData,
|
FormData,
|
||||||
encodeFormData,
|
formDataToBlob,
|
||||||
parseFormData,
|
parseFormData,
|
||||||
formDataFromEntries,
|
formDataFromEntries,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
const core = window.Deno.core;
|
const core = window.Deno.core;
|
||||||
const webidl = globalThis.__bootstrap.webidl;
|
const webidl = globalThis.__bootstrap.webidl;
|
||||||
const { parseUrlEncoded } = globalThis.__bootstrap.url;
|
const { parseUrlEncoded } = globalThis.__bootstrap.url;
|
||||||
const { parseFormData, formDataFromEntries, encodeFormData } =
|
const { parseFormData, formDataFromEntries, formDataToBlob } =
|
||||||
globalThis.__bootstrap.formData;
|
globalThis.__bootstrap.formData;
|
||||||
const mimesniff = globalThis.__bootstrap.mimesniff;
|
const mimesniff = globalThis.__bootstrap.mimesniff;
|
||||||
const { isReadableStreamDisturbed, errorReadableStream } =
|
const { isReadableStreamDisturbed, errorReadableStream } =
|
||||||
|
@ -311,11 +311,11 @@
|
||||||
const copy = u8.slice(0, u8.byteLength);
|
const copy = u8.slice(0, u8.byteLength);
|
||||||
source = copy;
|
source = copy;
|
||||||
} else if (object instanceof FormData) {
|
} else if (object instanceof FormData) {
|
||||||
const res = encodeFormData(object);
|
const res = formDataToBlob(object);
|
||||||
stream = { body: res.body, consumed: false };
|
stream = res.stream();
|
||||||
source = object;
|
source = res;
|
||||||
length = res.body.byteLength;
|
length = res.size;
|
||||||
contentType = res.contentType;
|
contentType = res.type;
|
||||||
} else if (object instanceof URLSearchParams) {
|
} else if (object instanceof URLSearchParams) {
|
||||||
source = core.encode(object.toString());
|
source = core.encode(object.toString());
|
||||||
contentType = "application/x-www-form-urlencoded;charset=UTF-8";
|
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 namespace formData {
|
||||||
declare type FormData = typeof FormData;
|
declare type FormData = typeof FormData;
|
||||||
declare function encodeFormData(formdata: FormData): {
|
declare function formDataToBlob(
|
||||||
body: Uint8Array;
|
formData: globalThis.FormData,
|
||||||
contentType: string;
|
): Blob;
|
||||||
};
|
|
||||||
declare function parseFormData(
|
declare function parseFormData(
|
||||||
body: Uint8Array,
|
body: Uint8Array,
|
||||||
boundary: string | undefined,
|
boundary: string | undefined,
|
||||||
|
|
Loading…
Add table
Reference in a new issue