Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit e2c5533

Browse files
authored
Merge pull request #52 from shinspiegel/multipart_module
feat: created new multiparser
2 parents d3ce58f + 6f42b20 commit e2c5533

File tree

5 files changed

+250
-36
lines changed

5 files changed

+250
-36
lines changed

api.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { compress as brotli } from 'https://deno.land/x/[email protected]/mod.ts'
2-
import { FormDataReader } from 'https://deno.land/x/[email protected]/multipart.ts'
32
import { gzipEncode } from 'https://deno.land/x/[email protected]/mod.ts'
43
import log from './log.ts'
4+
import { multiParser } from './multiparser.ts'
55
import { ServerRequest } from './std.ts'
66
import type { APIRequest, FormDataBody } from './types.ts'
77

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

10298
if (type === "json") {
103-
try {
104-
const buff: Uint8Array = await Deno.readAll(this.body);
105-
const encoded = new TextDecoder("utf-8").decode(buff);
106-
const json = JSON.parse(encoded);
107-
return json;
108-
} catch (err) {
109-
console.error("Failed to parse the request body.", err);
110-
}
99+
const buff: Uint8Array = await Deno.readAll(this.body);
100+
const encoded = new TextDecoder("utf-8").decode(buff);
101+
const json = JSON.parse(encoded);
102+
return json;
111103
}
112104

113105
if (type === "form-data") {
114-
try {
115-
const boundary = this.headers.get("content-type");
116-
117-
if (!boundary) throw new Error("Failed to get the content-type")
118-
119-
const reader = new FormDataReader(boundary, this.body);
120-
const { fields, files } = await reader.read({ maxSize: 1024 * 1024 * 10 });
121-
122-
return {
123-
get: (key: string) => fields[key],
124-
getFile: (key: string) => files?.find(i => i.name === key)
125-
}
126-
127-
} catch (err) {
128-
console.error("Failed to parse the request form-data", err)
129-
}
106+
const contentType = this.headers.get("content-type") as string
107+
const form = await multiParser(this.body, contentType);
108+
return form;
130109
}
131110
}
132111

multiparser.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { bytes } from "./std.ts";
2+
import { FormDataBody, FormFile } from "./types.ts";
3+
4+
const encoder = new TextEncoder();
5+
const decoder = new TextDecoder();
6+
7+
const encode = {
8+
contentType: encoder.encode("Content-Type"),
9+
filename: encoder.encode("filename"),
10+
name: encoder.encode("name"),
11+
dashdash: encoder.encode("--"),
12+
boundaryEqual: encoder.encode("boundary="),
13+
returnNewline2: encoder.encode("\r\n\r\n"),
14+
carriageReturn: encoder.encode("\r"),
15+
};
16+
17+
export async function multiParser(
18+
body: Deno.Reader,
19+
contentType: string
20+
): Promise<FormDataBody> {
21+
let buf = await Deno.readAll(body);
22+
let boundaryByte = getBoundary(contentType);
23+
24+
if (!boundaryByte) {
25+
throw new Error("No boundary data information");
26+
}
27+
28+
// Generate an array of Uint8Array
29+
const pieces = getFieldPieces(buf, boundaryByte!);
30+
31+
// Set all the pieces into one single object
32+
const form = getForm(pieces);
33+
34+
return form;
35+
}
36+
37+
function createFormData(): FormDataBody {
38+
return {
39+
fields: {},
40+
files: [],
41+
getFile(key: string) {
42+
return this.files.find((i) => i.name === key);
43+
},
44+
get(key: string) {
45+
return this.fields[key];
46+
},
47+
};
48+
}
49+
50+
function getForm(pieces: Uint8Array[]) {
51+
let form: FormDataBody = createFormData();
52+
53+
for (let piece of pieces) {
54+
const { headerByte, contentByte } = splitPiece(piece);
55+
const headers = getHeaders(headerByte);
56+
57+
// it's a string field
58+
if (typeof headers === "string") {
59+
// empty content, discard it
60+
if (contentByte.byteLength === 1 && contentByte[0] === 13) {
61+
continue;
62+
}
63+
64+
// headers = "field1"
65+
else {
66+
form.fields[headers] = decoder.decode(contentByte);
67+
}
68+
}
69+
70+
// it's a file field
71+
else {
72+
let file: FormFile = {
73+
name: headers.name,
74+
filename: headers.filename,
75+
contentType: headers.contentType,
76+
size: contentByte.byteLength,
77+
content: contentByte,
78+
};
79+
80+
form.files.push(file);
81+
}
82+
}
83+
return form;
84+
}
85+
86+
function getHeaders(headerByte: Uint8Array) {
87+
let contentTypeIndex = bytes.findIndex(headerByte, encode.contentType);
88+
89+
// no contentType, it may be a string field, return name only
90+
if (contentTypeIndex < 0) {
91+
return getNameOnly(headerByte);
92+
}
93+
94+
// file field, return with name, filename and contentType
95+
else {
96+
return getHeaderNContentType(headerByte, contentTypeIndex);
97+
}
98+
}
99+
100+
function getHeaderNContentType(
101+
headerByte: Uint8Array,
102+
contentTypeIndex: number,
103+
) {
104+
let headers: Record<string, string> = {};
105+
106+
let contentDispositionByte = headerByte.slice(0, contentTypeIndex - 2);
107+
headers = getHeaderOnly(contentDispositionByte);
108+
109+
// jump over <Content-Type: >
110+
let contentTypeByte = headerByte.slice(
111+
contentTypeIndex + encode.contentType.byteLength + 2,
112+
);
113+
114+
headers.contentType = decoder.decode(contentTypeByte);
115+
return headers;
116+
}
117+
118+
function getHeaderOnly(headerLineByte: Uint8Array) {
119+
let headers: Record<string, string> = {};
120+
121+
let filenameIndex = bytes.findIndex(headerLineByte, encode.filename);
122+
if (filenameIndex < 0) {
123+
headers.name = getNameOnly(headerLineByte);
124+
} else {
125+
headers = getNameNFilename(headerLineByte, filenameIndex);
126+
}
127+
return headers;
128+
}
129+
130+
function getNameNFilename(headerLineByte: Uint8Array, filenameIndex: number) {
131+
// fetch filename first
132+
let nameByte = headerLineByte.slice(0, filenameIndex - 2);
133+
let filenameByte = headerLineByte.slice(
134+
filenameIndex + encode.filename.byteLength + 2,
135+
headerLineByte.byteLength - 1,
136+
);
137+
138+
let name = getNameOnly(nameByte);
139+
let filename = decoder.decode(filenameByte);
140+
return { name, filename };
141+
}
142+
143+
function getNameOnly(headerLineByte: Uint8Array) {
144+
let nameIndex = bytes.findIndex(headerLineByte, encode.name);
145+
146+
// jump <name="> and get string inside double quote => "string"
147+
let nameByte = headerLineByte.slice(
148+
nameIndex + encode.name.byteLength + 2,
149+
headerLineByte.byteLength - 1,
150+
);
151+
152+
return decoder.decode(nameByte);
153+
}
154+
155+
function splitPiece(piece: Uint8Array) {
156+
const contentIndex = bytes.findIndex(piece, encode.returnNewline2);
157+
const headerByte = piece.slice(0, contentIndex);
158+
const contentByte = piece.slice(contentIndex + 4);
159+
160+
return { headerByte, contentByte };
161+
}
162+
163+
function getFieldPieces(
164+
buf: Uint8Array,
165+
boundaryByte: Uint8Array,
166+
): Uint8Array[] {
167+
const startBoundaryByte = bytes.concat(encode.dashdash, boundaryByte);
168+
const endBoundaryByte = bytes.concat(startBoundaryByte, encode.dashdash);
169+
170+
const pieces = [];
171+
172+
while (!bytes.hasPrefix(buf, endBoundaryByte)) {
173+
// jump over boundary + '\r\n'
174+
buf = buf.slice(startBoundaryByte.byteLength + 2);
175+
let boundaryIndex = bytes.findIndex(buf, startBoundaryByte);
176+
177+
// get field content piece
178+
pieces.push(buf.slice(0, boundaryIndex - 1));
179+
buf = buf.slice(boundaryIndex);
180+
}
181+
182+
return pieces;
183+
}
184+
185+
function getBoundary(contentType: string): Uint8Array | undefined {
186+
let contentTypeByte = encoder.encode(contentType);
187+
let boundaryIndex = bytes.findIndex(contentTypeByte, encode.boundaryEqual);
188+
189+
if (boundaryIndex >= 0) {
190+
// jump over 'boundary=' to get the real boundary
191+
let boundary = contentTypeByte.slice(
192+
boundaryIndex + encode.boundaryEqual.byteLength,
193+
);
194+
return boundary;
195+
} else {
196+
return undefined;
197+
}
198+
}

multiparser_test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
2+
import { multiParser } from "./multiparser.ts";
3+
4+
const encoder = new TextEncoder();
5+
6+
const contentType = "multipart/form-data; boundary=ALEPH-BOUNDARY";
7+
const simpleString = '--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="string_1"\r\n\r\nsimple string here\r--ALEPH-BOUNDARY--';
8+
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';
9+
10+
Deno.test(`basic multiparser string`, async () => {
11+
const buff = new Deno.Buffer(encoder.encode(simpleString));
12+
const multiForm = await multiParser(buff, contentType);
13+
14+
assertEquals(multiForm.get("string_1"), "simple string here");
15+
});
16+
17+
Deno.test(`complex multiparser string`, async () => {
18+
const buff = new Deno.Buffer(encoder.encode(complexString));
19+
const multiFrom = await multiParser(buff, contentType);
20+
21+
// Asseting multiple string values
22+
assertEquals(multiFrom.get("id"), "666");
23+
assertEquals(multiFrom.get("title"), "Hello World");
24+
assertEquals(multiFrom.get("multiline"), "world,\n hello");
25+
26+
// Asserting a file information
27+
const file = multiFrom.getFile("file1");
28+
29+
if (!file) { return }
30+
31+
assertEquals(file.name, "file1");
32+
assertEquals(file.contentType, "video/mp2t");
33+
assertEquals(file.size, 16);
34+
});

std.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * as bytes from 'https://deno.land/[email protected]/bytes/mod.ts'
12
export { Untar } from 'https://deno.land/[email protected]/archive/tar.ts'
23
export * as colors from 'https://deno.land/[email protected]/fmt/colors.ts'
34
export { ensureDir } from 'https://deno.land/[email protected]/fs/ensure_dir.ts'

types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,10 @@ export interface RouterURL {
114114
* The form data body
115115
*/
116116
export interface FormDataBody {
117-
get(key: string): string
118-
getFile(key: string): FormFile
117+
fields: Record<string, string>;
118+
files: FormFile[];
119+
get(key: string): string | undefined;
120+
getFile(key: string): FormFile | undefined;
119121
}
120122

121123
/**
@@ -126,5 +128,5 @@ export interface FormFile {
126128
content: Uint8Array
127129
contentType: string
128130
filename: string
129-
originalName: string
130-
}
131+
size: number
132+
}

0 commit comments

Comments
 (0)