mirror of
https://github.com/denoland/deno.git
synced 2025-08-04 02:48:24 +00:00
Rename extensions/ directory to ext/ (#11643)
This commit is contained in:
parent
3a69941151
commit
a0285e2eb8
134 changed files with 77 additions and 77 deletions
507
ext/fetch/21_formdata.js
Normal file
507
ext/fetch/21_formdata.js
Normal file
|
@ -0,0 +1,507 @@
|
|||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
// @ts-check
|
||||
/// <reference path="../webidl/internal.d.ts" />
|
||||
/// <reference path="../web/internal.d.ts" />
|
||||
/// <reference path="../web/lib.deno_web.d.ts" />
|
||||
/// <reference path="./internal.d.ts" />
|
||||
/// <reference path="../web/06_streams_types.d.ts" />
|
||||
/// <reference path="./lib.deno_fetch.d.ts" />
|
||||
/// <reference lib="esnext" />
|
||||
"use strict";
|
||||
|
||||
((window) => {
|
||||
const core = window.Deno.core;
|
||||
const webidl = globalThis.__bootstrap.webidl;
|
||||
const { Blob, File } = globalThis.__bootstrap.file;
|
||||
const {
|
||||
ArrayPrototypeMap,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeSlice,
|
||||
ArrayPrototypeSplice,
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypeForEach,
|
||||
Map,
|
||||
MapPrototypeGet,
|
||||
MapPrototypeSet,
|
||||
MathRandom,
|
||||
Symbol,
|
||||
SymbolToStringTag,
|
||||
StringFromCharCode,
|
||||
StringPrototypeTrim,
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeSplit,
|
||||
StringPrototypeReplace,
|
||||
StringPrototypeIndexOf,
|
||||
StringPrototypePadStart,
|
||||
StringPrototypeCodePointAt,
|
||||
StringPrototypeReplaceAll,
|
||||
TypeError,
|
||||
TypedArrayPrototypeSubarray,
|
||||
} = window.__bootstrap.primordials;
|
||||
|
||||
const entryList = Symbol("entry list");
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string | Blob} value
|
||||
* @param {string | undefined} filename
|
||||
* @returns {FormDataEntry}
|
||||
*/
|
||||
function createEntry(name, value, filename) {
|
||||
if (value instanceof Blob && !(value instanceof File)) {
|
||||
value = new File([value], "blob", { type: value.type });
|
||||
}
|
||||
if (value instanceof File && filename !== undefined) {
|
||||
value = new File([value], filename, {
|
||||
type: value.type,
|
||||
lastModified: value.lastModified,
|
||||
});
|
||||
}
|
||||
return {
|
||||
name,
|
||||
// @ts-expect-error because TS is not smart enough
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef FormDataEntry
|
||||
* @property {string} name
|
||||
* @property {FormDataEntryValue} value
|
||||
*/
|
||||
|
||||
class FormData {
|
||||
get [SymbolToStringTag]() {
|
||||
return "FormData";
|
||||
}
|
||||
|
||||
/** @type {FormDataEntry[]} */
|
||||
[entryList] = [];
|
||||
|
||||
/** @param {void} form */
|
||||
constructor(form) {
|
||||
if (form !== undefined) {
|
||||
webidl.illegalConstructor();
|
||||
}
|
||||
this[webidl.brand] = webidl.brand;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string | Blob} valueOrBlobValue
|
||||
* @param {string} [filename]
|
||||
* @returns {void}
|
||||
*/
|
||||
append(name, valueOrBlobValue, filename) {
|
||||
webidl.assertBranded(this, FormData);
|
||||
const prefix = "Failed to execute 'append' on 'FormData'";
|
||||
webidl.requiredArguments(arguments.length, 2, { prefix });
|
||||
|
||||
name = webidl.converters["USVString"](name, {
|
||||
prefix,
|
||||
context: "Argument 1",
|
||||
});
|
||||
if (valueOrBlobValue instanceof Blob) {
|
||||
valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, {
|
||||
prefix,
|
||||
context: "Argument 2",
|
||||
});
|
||||
if (filename !== undefined) {
|
||||
filename = webidl.converters["USVString"](filename, {
|
||||
prefix,
|
||||
context: "Argument 3",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, {
|
||||
prefix,
|
||||
context: "Argument 2",
|
||||
});
|
||||
}
|
||||
|
||||
const entry = createEntry(name, valueOrBlobValue, filename);
|
||||
|
||||
ArrayPrototypePush(this[entryList], entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {void}
|
||||
*/
|
||||
delete(name) {
|
||||
webidl.assertBranded(this, FormData);
|
||||
const prefix = "Failed to execute 'name' on 'FormData'";
|
||||
webidl.requiredArguments(arguments.length, 1, { prefix });
|
||||
|
||||
name = webidl.converters["USVString"](name, {
|
||||
prefix,
|
||||
context: "Argument 1",
|
||||
});
|
||||
|
||||
const list = this[entryList];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].name === name) {
|
||||
ArrayPrototypeSplice(list, i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {FormDataEntryValue | null}
|
||||
*/
|
||||
get(name) {
|
||||
webidl.assertBranded(this, FormData);
|
||||
const prefix = "Failed to execute 'get' on 'FormData'";
|
||||
webidl.requiredArguments(arguments.length, 1, { prefix });
|
||||
|
||||
name = webidl.converters["USVString"](name, {
|
||||
prefix,
|
||||
context: "Argument 1",
|
||||
});
|
||||
|
||||
for (const entry of this[entryList]) {
|
||||
if (entry.name === name) return entry.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {FormDataEntryValue[]}
|
||||
*/
|
||||
getAll(name) {
|
||||
webidl.assertBranded(this, FormData);
|
||||
const prefix = "Failed to execute 'getAll' on 'FormData'";
|
||||
webidl.requiredArguments(arguments.length, 1, { prefix });
|
||||
|
||||
name = webidl.converters["USVString"](name, {
|
||||
prefix,
|
||||
context: "Argument 1",
|
||||
});
|
||||
|
||||
const returnList = [];
|
||||
for (const entry of this[entryList]) {
|
||||
if (entry.name === name) ArrayPrototypePush(returnList, entry.value);
|
||||
}
|
||||
return returnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(name) {
|
||||
webidl.assertBranded(this, FormData);
|
||||
const prefix = "Failed to execute 'has' on 'FormData'";
|
||||
webidl.requiredArguments(arguments.length, 1, { prefix });
|
||||
|
||||
name = webidl.converters["USVString"](name, {
|
||||
prefix,
|
||||
context: "Argument 1",
|
||||
});
|
||||
|
||||
for (const entry of this[entryList]) {
|
||||
if (entry.name === name) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string | Blob} valueOrBlobValue
|
||||
* @param {string} [filename]
|
||||
* @returns {void}
|
||||
*/
|
||||
set(name, valueOrBlobValue, filename) {
|
||||
webidl.assertBranded(this, FormData);
|
||||
const prefix = "Failed to execute 'set' on 'FormData'";
|
||||
webidl.requiredArguments(arguments.length, 2, { prefix });
|
||||
|
||||
name = webidl.converters["USVString"](name, {
|
||||
prefix,
|
||||
context: "Argument 1",
|
||||
});
|
||||
if (valueOrBlobValue instanceof Blob) {
|
||||
valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, {
|
||||
prefix,
|
||||
context: "Argument 2",
|
||||
});
|
||||
if (filename !== undefined) {
|
||||
filename = webidl.converters["USVString"](filename, {
|
||||
prefix,
|
||||
context: "Argument 3",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, {
|
||||
prefix,
|
||||
context: "Argument 2",
|
||||
});
|
||||
}
|
||||
|
||||
const entry = createEntry(name, valueOrBlobValue, filename);
|
||||
|
||||
const list = this[entryList];
|
||||
let added = false;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].name === name) {
|
||||
if (!added) {
|
||||
list[i] = entry;
|
||||
added = true;
|
||||
} else {
|
||||
ArrayPrototypeSplice(list, i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
ArrayPrototypePush(list, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value");
|
||||
|
||||
webidl.configurePrototype(FormData);
|
||||
|
||||
const escape = (str, isFilename) =>
|
||||
StringPrototypeReplace(
|
||||
StringPrototypeReplace(
|
||||
StringPrototypeReplace(
|
||||
(isFilename ? str : StringPrototypeReplace(str, /\r?\n|\r/g, "\r\n")),
|
||||
/\n/g,
|
||||
"%0A",
|
||||
),
|
||||
/\r/g,
|
||||
"%0D",
|
||||
),
|
||||
/"/g,
|
||||
"%22",
|
||||
);
|
||||
|
||||
/**
|
||||
* convert FormData to a Blob synchronous without reading all of the files
|
||||
* @param {globalThis.FormData} formData
|
||||
*/
|
||||
function formDataToBlob(formData) {
|
||||
const boundary = StringPrototypePadStart(
|
||||
StringPrototypeSlice(
|
||||
StringPrototypeReplaceAll(`${MathRandom()}${MathRandom()}`, ".", ""),
|
||||
-28,
|
||||
),
|
||||
32,
|
||||
"-",
|
||||
);
|
||||
const chunks = [];
|
||||
const prefix = `--${boundary}\r\nContent-Disposition: form-data; name="`;
|
||||
|
||||
for (const [name, value] of formData) {
|
||||
if (typeof value === "string") {
|
||||
ArrayPrototypePush(
|
||||
chunks,
|
||||
prefix + escape(name) + '"' + CRLF + CRLF +
|
||||
StringPrototypeReplace(value, /\r(?!\n)|(?<!\r)\n/g, CRLF) + CRLF,
|
||||
);
|
||||
} else {
|
||||
ArrayPrototypePush(
|
||||
chunks,
|
||||
prefix + escape(name) + `"; filename="${escape(value.name, true)}"` +
|
||||
CRLF +
|
||||
`Content-Type: ${value.type || "application/octet-stream"}\r\n\r\n`,
|
||||
value,
|
||||
CRLF,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayPrototypePush(chunks, `--${boundary}--`);
|
||||
|
||||
return new Blob(chunks, {
|
||||
type: "multipart/form-data; boundary=" + boundary,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @returns {Map<string, string>}
|
||||
*/
|
||||
function parseContentDisposition(value) {
|
||||
/** @type {Map<string, string>} */
|
||||
const params = new Map();
|
||||
// Forced to do so for some Map constructor param mismatch
|
||||
ArrayPrototypeForEach(
|
||||
ArrayPrototypeMap(
|
||||
ArrayPrototypeFilter(
|
||||
ArrayPrototypeMap(
|
||||
ArrayPrototypeSlice(StringPrototypeSplit(value, ";"), 1),
|
||||
(s) => StringPrototypeSplit(StringPrototypeTrim(s), "="),
|
||||
),
|
||||
(arr) => arr.length > 1,
|
||||
),
|
||||
([k, v]) => [k, StringPrototypeReplace(v, /^"([^"]*)"$/, "$1")],
|
||||
),
|
||||
([k, v]) => MapPrototypeSet(params, k, v),
|
||||
);
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
const CRLF = "\r\n";
|
||||
const LF = StringPrototypeCodePointAt(CRLF, 1);
|
||||
const CR = StringPrototypeCodePointAt(CRLF, 0);
|
||||
|
||||
class MultipartParser {
|
||||
/**
|
||||
* @param {Uint8Array} body
|
||||
* @param {string | undefined} boundary
|
||||
*/
|
||||
constructor(body, boundary) {
|
||||
if (!boundary) {
|
||||
throw new TypeError("multipart/form-data must provide a boundary");
|
||||
}
|
||||
|
||||
this.boundary = `--${boundary}`;
|
||||
this.body = body;
|
||||
this.boundaryChars = core.encode(this.boundary);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} headersText
|
||||
* @returns {{ headers: Headers, disposition: Map<string, string> }}
|
||||
*/
|
||||
#parseHeaders(headersText) {
|
||||
const headers = new Headers();
|
||||
const rawHeaders = StringPrototypeSplit(headersText, "\r\n");
|
||||
for (const rawHeader of rawHeaders) {
|
||||
const sepIndex = StringPrototypeIndexOf(rawHeader, ":");
|
||||
if (sepIndex < 0) {
|
||||
continue; // Skip this header
|
||||
}
|
||||
const key = StringPrototypeSlice(rawHeader, 0, sepIndex);
|
||||
const value = StringPrototypeSlice(rawHeader, sepIndex + 1);
|
||||
headers.set(key, value);
|
||||
}
|
||||
|
||||
const disposition = parseContentDisposition(
|
||||
headers.get("Content-Disposition") ?? "",
|
||||
);
|
||||
|
||||
return { headers, disposition };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {FormData}
|
||||
*/
|
||||
parse() {
|
||||
// Body must be at least 2 boundaries + \r\n + -- on the last boundary.
|
||||
if (this.body.length < (this.boundary.length * 2) + 4) {
|
||||
throw new TypeError("Form data too short to be valid.");
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
let headerText = "";
|
||||
let boundaryIndex = 0;
|
||||
let state = 0;
|
||||
let fileStart = 0;
|
||||
|
||||
for (let i = 0; i < this.body.length; i++) {
|
||||
const byte = this.body[i];
|
||||
const prevByte = this.body[i - 1];
|
||||
const isNewLine = byte === LF && prevByte === CR;
|
||||
|
||||
if (state === 1 || state === 2 || state == 3) {
|
||||
headerText += StringFromCharCode(byte);
|
||||
}
|
||||
if (state === 0 && isNewLine) {
|
||||
state = 1;
|
||||
} else if (state === 1 && isNewLine) {
|
||||
state = 2;
|
||||
const headersDone = this.body[i + 1] === CR &&
|
||||
this.body[i + 2] === LF;
|
||||
|
||||
if (headersDone) {
|
||||
state = 3;
|
||||
}
|
||||
} else if (state === 2 && isNewLine) {
|
||||
state = 3;
|
||||
} else if (state === 3 && isNewLine) {
|
||||
state = 4;
|
||||
fileStart = i + 1;
|
||||
} else if (state === 4) {
|
||||
if (this.boundaryChars[boundaryIndex] !== byte) {
|
||||
boundaryIndex = 0;
|
||||
} else {
|
||||
boundaryIndex++;
|
||||
}
|
||||
|
||||
if (boundaryIndex >= this.boundary.length) {
|
||||
const { headers, disposition } = this.#parseHeaders(headerText);
|
||||
const content = TypedArrayPrototypeSubarray(
|
||||
this.body,
|
||||
fileStart,
|
||||
i - boundaryIndex - 1,
|
||||
);
|
||||
// https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
|
||||
const filename = MapPrototypeGet(disposition, "filename");
|
||||
const name = MapPrototypeGet(disposition, "name");
|
||||
|
||||
state = 5;
|
||||
// Reset
|
||||
boundaryIndex = 0;
|
||||
headerText = "";
|
||||
|
||||
if (!name) {
|
||||
continue; // Skip, unknown name
|
||||
}
|
||||
|
||||
if (filename) {
|
||||
const blob = new Blob([content], {
|
||||
type: headers.get("Content-Type") || "application/octet-stream",
|
||||
});
|
||||
formData.append(name, blob, filename);
|
||||
} else {
|
||||
formData.append(name, core.decode(content));
|
||||
}
|
||||
}
|
||||
} else if (state === 5 && isNewLine) {
|
||||
state = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} body
|
||||
* @param {string | undefined} boundary
|
||||
* @returns {FormData}
|
||||
*/
|
||||
function parseFormData(body, boundary) {
|
||||
const parser = new MultipartParser(body, boundary);
|
||||
return parser.parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormDataEntry[]} entries
|
||||
* @returns {FormData}
|
||||
*/
|
||||
function formDataFromEntries(entries) {
|
||||
const fd = new FormData();
|
||||
fd[entryList] = entries;
|
||||
return fd;
|
||||
}
|
||||
|
||||
webidl.converters["FormData"] = webidl
|
||||
.createInterfaceConverter("FormData", FormData);
|
||||
|
||||
globalThis.__bootstrap.formData = {
|
||||
FormData,
|
||||
formDataToBlob,
|
||||
parseFormData,
|
||||
formDataFromEntries,
|
||||
};
|
||||
})(globalThis);
|
Loading…
Add table
Add a link
Reference in a new issue