Skip to content

Commit

Permalink
Merge pull request #52 from shinspiegel/multipart_module
Browse files Browse the repository at this point in the history
feat: created new multiparser
  • Loading branch information
ije committed Nov 30, 2020
2 parents d3ce58f + 6f42b20 commit e2c5533
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 36 deletions.
43 changes: 11 additions & 32 deletions api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { compress as brotli } from 'https://deno.land/x/[email protected]/mod.ts'
import { FormDataReader } from 'https://deno.land/x/[email protected]/multipart.ts'
import { gzipEncode } from 'https://deno.land/x/[email protected]/mod.ts'
import log from './log.ts'
import { multiParser } from './multiparser.ts'
import { ServerRequest } from './std.ts'
import type { APIRequest, FormDataBody } from './types.ts'

Expand Down Expand Up @@ -90,43 +90,22 @@ export class Request extends ServerRequest implements APIRequest {
async decodeBody(type: "form-data"): Promise<FormDataBody>
async decodeBody(type: string): Promise<any> {
if (type === "text") {
try {
const buff: Uint8Array = await Deno.readAll(this.body);
const encoded = new TextDecoder("utf-8").decode(buff);
return encoded;
} catch (err) {
console.error("Failed to parse the request body.", err);
}
const buff: Uint8Array = await Deno.readAll(this.body);
const encoded = new TextDecoder("utf-8").decode(buff);
return encoded;
}

if (type === "json") {
try {
const buff: Uint8Array = await Deno.readAll(this.body);
const encoded = new TextDecoder("utf-8").decode(buff);
const json = JSON.parse(encoded);
return json;
} catch (err) {
console.error("Failed to parse the request body.", err);
}
const buff: Uint8Array = await Deno.readAll(this.body);
const encoded = new TextDecoder("utf-8").decode(buff);
const json = JSON.parse(encoded);
return json;
}

if (type === "form-data") {
try {
const boundary = this.headers.get("content-type");

if (!boundary) throw new Error("Failed to get the content-type")

const reader = new FormDataReader(boundary, this.body);
const { fields, files } = await reader.read({ maxSize: 1024 * 1024 * 10 });

return {
get: (key: string) => fields[key],
getFile: (key: string) => files?.find(i => i.name === key)
}

} catch (err) {
console.error("Failed to parse the request form-data", err)
}
const contentType = this.headers.get("content-type") as string
const form = await multiParser(this.body, contentType);
return form;
}
}

Expand Down
198 changes: 198 additions & 0 deletions multiparser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { bytes } from "./std.ts";
import { FormDataBody, FormFile } from "./types.ts";

const encoder = new TextEncoder();
const decoder = new TextDecoder();

const encode = {
contentType: encoder.encode("Content-Type"),
filename: encoder.encode("filename"),
name: encoder.encode("name"),
dashdash: encoder.encode("--"),
boundaryEqual: encoder.encode("boundary="),
returnNewline2: encoder.encode("\r\n\r\n"),
carriageReturn: encoder.encode("\r"),
};

export async function multiParser(
body: Deno.Reader,
contentType: string
): Promise<FormDataBody> {
let buf = await Deno.readAll(body);
let boundaryByte = getBoundary(contentType);

if (!boundaryByte) {
throw new Error("No boundary data information");
}

// Generate an array of Uint8Array
const pieces = getFieldPieces(buf, boundaryByte!);

// Set all the pieces into one single object
const form = getForm(pieces);

return form;
}

function createFormData(): FormDataBody {
return {
fields: {},
files: [],
getFile(key: string) {
return this.files.find((i) => i.name === key);
},
get(key: string) {
return this.fields[key];
},
};
}

function getForm(pieces: Uint8Array[]) {
let form: FormDataBody = createFormData();

for (let piece of pieces) {
const { headerByte, contentByte } = splitPiece(piece);
const headers = getHeaders(headerByte);

// it's a string field
if (typeof headers === "string") {
// empty content, discard it
if (contentByte.byteLength === 1 && contentByte[0] === 13) {
continue;
}

// headers = "field1"
else {
form.fields[headers] = decoder.decode(contentByte);
}
}

// it's a file field
else {
let file: FormFile = {
name: headers.name,
filename: headers.filename,
contentType: headers.contentType,
size: contentByte.byteLength,
content: contentByte,
};

form.files.push(file);
}
}
return form;
}

function getHeaders(headerByte: Uint8Array) {
let contentTypeIndex = bytes.findIndex(headerByte, encode.contentType);

// no contentType, it may be a string field, return name only
if (contentTypeIndex < 0) {
return getNameOnly(headerByte);
}

// file field, return with name, filename and contentType
else {
return getHeaderNContentType(headerByte, contentTypeIndex);
}
}

function getHeaderNContentType(
headerByte: Uint8Array,
contentTypeIndex: number,
) {
let headers: Record<string, string> = {};

let contentDispositionByte = headerByte.slice(0, contentTypeIndex - 2);
headers = getHeaderOnly(contentDispositionByte);

// jump over <Content-Type: >
let contentTypeByte = headerByte.slice(
contentTypeIndex + encode.contentType.byteLength + 2,
);

headers.contentType = decoder.decode(contentTypeByte);
return headers;
}

function getHeaderOnly(headerLineByte: Uint8Array) {
let headers: Record<string, string> = {};

let filenameIndex = bytes.findIndex(headerLineByte, encode.filename);
if (filenameIndex < 0) {
headers.name = getNameOnly(headerLineByte);
} else {
headers = getNameNFilename(headerLineByte, filenameIndex);
}
return headers;
}

function getNameNFilename(headerLineByte: Uint8Array, filenameIndex: number) {
// fetch filename first
let nameByte = headerLineByte.slice(0, filenameIndex - 2);
let filenameByte = headerLineByte.slice(
filenameIndex + encode.filename.byteLength + 2,
headerLineByte.byteLength - 1,
);

let name = getNameOnly(nameByte);
let filename = decoder.decode(filenameByte);
return { name, filename };
}

function getNameOnly(headerLineByte: Uint8Array) {
let nameIndex = bytes.findIndex(headerLineByte, encode.name);

// jump <name="> and get string inside double quote => "string"
let nameByte = headerLineByte.slice(
nameIndex + encode.name.byteLength + 2,
headerLineByte.byteLength - 1,
);

return decoder.decode(nameByte);
}

function splitPiece(piece: Uint8Array) {
const contentIndex = bytes.findIndex(piece, encode.returnNewline2);
const headerByte = piece.slice(0, contentIndex);
const contentByte = piece.slice(contentIndex + 4);

return { headerByte, contentByte };
}

function getFieldPieces(
buf: Uint8Array,
boundaryByte: Uint8Array,
): Uint8Array[] {
const startBoundaryByte = bytes.concat(encode.dashdash, boundaryByte);
const endBoundaryByte = bytes.concat(startBoundaryByte, encode.dashdash);

const pieces = [];

while (!bytes.hasPrefix(buf, endBoundaryByte)) {
// jump over boundary + '\r\n'
buf = buf.slice(startBoundaryByte.byteLength + 2);
let boundaryIndex = bytes.findIndex(buf, startBoundaryByte);

// get field content piece
pieces.push(buf.slice(0, boundaryIndex - 1));
buf = buf.slice(boundaryIndex);
}

return pieces;
}

function getBoundary(contentType: string): Uint8Array | undefined {
let contentTypeByte = encoder.encode(contentType);
let boundaryIndex = bytes.findIndex(contentTypeByte, encode.boundaryEqual);

if (boundaryIndex >= 0) {
// jump over 'boundary=' to get the real boundary
let boundary = contentTypeByte.slice(
boundaryIndex + encode.boundaryEqual.byteLength,
);
return boundary;
} else {
return undefined;
}
}
34 changes: 34 additions & 0 deletions multiparser_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { multiParser } from "./multiparser.ts";

const encoder = new TextEncoder();

const contentType = "multipart/form-data; boundary=ALEPH-BOUNDARY";
const simpleString = '--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="string_1"\r\n\r\nsimple string here\r--ALEPH-BOUNDARY--';
const complexString = 'some text to be ignored\r\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="id"\r\n\r\n666\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="title"\r\n\r\nHello World\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="multiline"\r\n\r\nworld,\n hello\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="file1"; filename="file_name.ext"\rContent-Type: video/mp2t\r\n\r\nsome random data\r--ALEPH-BOUNDARY--\rmore text to be ignored to be ignored\r';

Deno.test(`basic multiparser string`, async () => {
const buff = new Deno.Buffer(encoder.encode(simpleString));
const multiForm = await multiParser(buff, contentType);

assertEquals(multiForm.get("string_1"), "simple string here");
});

Deno.test(`complex multiparser string`, async () => {
const buff = new Deno.Buffer(encoder.encode(complexString));
const multiFrom = await multiParser(buff, contentType);

// Asseting multiple string values
assertEquals(multiFrom.get("id"), "666");
assertEquals(multiFrom.get("title"), "Hello World");
assertEquals(multiFrom.get("multiline"), "world,\n hello");

// Asserting a file information
const file = multiFrom.getFile("file1");

if (!file) { return }

assertEquals(file.name, "file1");
assertEquals(file.contentType, "video/mp2t");
assertEquals(file.size, 16);
});
1 change: 1 addition & 0 deletions std.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * as bytes from 'https://deno.land/[email protected]/bytes/mod.ts'
export { Untar } from 'https://deno.land/[email protected]/archive/tar.ts'
export * as colors from 'https://deno.land/[email protected]/fmt/colors.ts'
export { ensureDir } from 'https://deno.land/[email protected]/fs/ensure_dir.ts'
Expand Down
10 changes: 6 additions & 4 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,10 @@ export interface RouterURL {
* The form data body
*/
export interface FormDataBody {
get(key: string): string
getFile(key: string): FormFile
fields: Record<string, string>;
files: FormFile[];
get(key: string): string | undefined;
getFile(key: string): FormFile | undefined;
}

/**
Expand All @@ -126,5 +128,5 @@ export interface FormFile {
content: Uint8Array
contentType: string
filename: string
originalName: string
}
size: number
}

0 comments on commit e2c5533

Please sign in to comment.