summaryrefslogtreecommitdiff
path: root/cli/js/web/fetch
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/web/fetch')
-rw-r--r--cli/js/web/fetch/multipart.ts120
1 files changed, 120 insertions, 0 deletions
diff --git a/cli/js/web/fetch/multipart.ts b/cli/js/web/fetch/multipart.ts
new file mode 100644
index 000000000..792f9b5ee
--- /dev/null
+++ b/cli/js/web/fetch/multipart.ts
@@ -0,0 +1,120 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+import { DenoBlob } from "../blob.ts";
+import { TextEncoder, TextDecoder } from "../text_encoding.ts";
+import { getHeaderValueParams } from "../util.ts";
+
+const decoder = new TextDecoder();
+const encoder = new TextEncoder();
+const CR = "\r".charCodeAt(0);
+const LF = "\n".charCodeAt(0);
+
+interface MultipartHeaders {
+ headers: Headers;
+ disposition: Map<string, string>;
+}
+
+export class MultipartParser {
+ readonly boundary: string;
+ readonly boundaryChars: Uint8Array;
+ readonly body: Uint8Array;
+ constructor(body: Uint8Array, boundary: string) {
+ 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: string): MultipartHeaders => {
+ 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(): FormData {
+ 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 DenoBlob([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;
+ }
+}