summaryrefslogtreecommitdiff
path: root/cli/rt/23_multipart.js
diff options
context:
space:
mode:
Diffstat (limited to 'cli/rt/23_multipart.js')
-rw-r--r--cli/rt/23_multipart.js199
1 files changed, 199 insertions, 0 deletions
diff --git a/cli/rt/23_multipart.js b/cli/rt/23_multipart.js
new file mode 100644
index 000000000..78c1d28a1
--- /dev/null
+++ b/cli/rt/23_multipart.js
@@ -0,0 +1,199 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+((window) => {
+ const { Buffer } = window.__bootstrap.buffer;
+ const { bytesSymbol, Blob } = window.__bootstrap.blob;
+ const { DomFile } = window.__bootstrap.domFile;
+ const { getHeaderValueParams } = window.__bootstrap.webUtil;
+
+ const decoder = new TextDecoder();
+ const encoder = new TextEncoder();
+ const CR = "\r".charCodeAt(0);
+ const LF = "\n".charCodeAt(0);
+
+ class MultipartBuilder {
+ constructor(formData, boundary) {
+ this.formData = formData;
+ this.boundary = boundary ?? this.#createBoundary();
+ this.writer = new Buffer();
+ }
+
+ getContentType() {
+ return `multipart/form-data; boundary=${this.boundary}`;
+ }
+
+ getBody() {
+ for (const [fieldName, fieldValue] of this.formData.entries()) {
+ if (fieldValue instanceof DomFile) {
+ this.#writeFile(fieldName, fieldValue);
+ } else this.#writeField(fieldName, fieldValue);
+ }
+
+ this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`));
+
+ return this.writer.bytes();
+ }
+
+ #createBoundary = () => {
+ return (
+ "----------" +
+ Array.from(Array(32))
+ .map(() => Math.random().toString(36)[2] || 0)
+ .join("")
+ );
+ };
+
+ #writeHeaders = (headers) => {
+ let buf = this.writer.empty() ? "" : "\r\n";
+
+ buf += `--${this.boundary}\r\n`;
+ for (const [key, value] of headers) {
+ buf += `${key}: ${value}\r\n`;
+ }
+ buf += `\r\n`;
+
+ this.writer.write(encoder.encode(buf));
+ };
+
+ #writeFileHeaders = (
+ field,
+ filename,
+ type,
+ ) => {
+ const headers = [
+ [
+ "Content-Disposition",
+ `form-data; name="${field}"; filename="${filename}"`,
+ ],
+ ["Content-Type", type || "application/octet-stream"],
+ ];
+ return this.#writeHeaders(headers);
+ };
+
+ #writeFieldHeaders = (field) => {
+ const headers = [["Content-Disposition", `form-data; name="${field}"`]];
+ return this.#writeHeaders(headers);
+ };
+
+ #writeField = (field, value) => {
+ this.#writeFieldHeaders(field);
+ this.writer.writeSync(encoder.encode(value));
+ };
+
+ #writeFile = (field, value) => {
+ this.#writeFileHeaders(field, value.name, value.type);
+ this.writer.writeSync(value[bytesSymbol]);
+ };
+ }
+
+ class MultipartParser {
+ constructor(body, boundary) {
+ if (!boundary) {
+ throw new TypeError("multipart/form-data must provide a boundary");
+ }
+
+ this.boundary = `--${boundary}`;
+ this.body = body;
+ this.boundaryChars = encoder.encode(this.boundary);
+ }
+
+ #parseHeaders = (headersText) => {
+ const headers = new Headers();
+ const rawHeaders = headersText.split("\r\n");
+ for (const rawHeader of rawHeaders) {
+ const sepIndex = rawHeader.indexOf(":");
+ if (sepIndex < 0) {
+ continue; // Skip this header
+ }
+ const key = rawHeader.slice(0, sepIndex);
+ const value = rawHeader.slice(sepIndex + 1);
+ headers.set(key, value);
+ }
+
+ return {
+ headers,
+ disposition: getHeaderValueParams(
+ headers.get("Content-Disposition") ?? "",
+ ),
+ };
+ };
+
+ parse() {
+ 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 += String.fromCharCode(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 = this.body.subarray(
+ fileStart,
+ i - boundaryIndex - 1,
+ );
+ // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
+ const filename = disposition.get("filename");
+ const name = disposition.get("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, decoder.decode(content));
+ }
+ }
+ } else if (state === 5 && isNewLine) {
+ state = 1;
+ }
+ }
+
+ return formData;
+ }
+ }
+
+ window.__bootstrap.multipart = {
+ MultipartBuilder,
+ MultipartParser,
+ };
+})(this);