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

Commit 03405dd

Browse files
committed
feat: created new multiparser
1 parent d3ce58f commit 03405dd

File tree

4 files changed

+207
-36
lines changed

4 files changed

+207
-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: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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+
// let form: Form = { fields: {}, files: {} };
53+
54+
for (let piece of pieces) {
55+
const { headerByte, contentByte } = splitPiece(piece);
56+
const headers = getHeaders(headerByte);
57+
58+
// it's a string field
59+
if (typeof headers === "string") {
60+
// empty content, discard it
61+
if (contentByte.byteLength === 1 && contentByte[0] === 13) {
62+
continue;
63+
} else {
64+
// headers = "field1"
65+
form.fields[headers] = decoder.decode(contentByte);
66+
}
67+
} // it's a file field
68+
else {
69+
let file: FormFile = {
70+
name: headers.name,
71+
filename: headers.filename,
72+
contentType: headers.contentType,
73+
size: contentByte.byteLength,
74+
content: contentByte,
75+
};
76+
77+
form.files.push(file);
78+
}
79+
}
80+
return form;
81+
}
82+
83+
function getHeaders(headerByte: Uint8Array) {
84+
let contentTypeIndex = bytes.findIndex(headerByte, encode.contentType);
85+
86+
// no contentType, it may be a string field, return name only
87+
if (contentTypeIndex < 0) {
88+
return getNameOnly(headerByte);
89+
} // file field, return with name, filename and contentType
90+
else {
91+
return getHeaderNContentType(headerByte, contentTypeIndex);
92+
}
93+
}
94+
95+
function getHeaderNContentType(
96+
headerByte: Uint8Array,
97+
contentTypeIndex: number,
98+
) {
99+
let headers: Record<string, string> = {};
100+
101+
let contentDispositionByte = headerByte.slice(0, contentTypeIndex - 2);
102+
headers = getHeaderOnly(contentDispositionByte);
103+
104+
// jump over <Content-Type: >
105+
let contentTypeByte = headerByte.slice(
106+
contentTypeIndex + encode.contentType.byteLength + 2,
107+
);
108+
109+
headers.contentType = decoder.decode(contentTypeByte);
110+
return headers;
111+
}
112+
113+
function getHeaderOnly(headerLineByte: Uint8Array) {
114+
let headers: Record<string, string> = {};
115+
116+
let filenameIndex = bytes.findIndex(headerLineByte, encode.filename);
117+
if (filenameIndex < 0) {
118+
headers.name = getNameOnly(headerLineByte);
119+
} else {
120+
headers = getNameNFilename(headerLineByte, filenameIndex);
121+
}
122+
return headers;
123+
}
124+
125+
function getNameNFilename(headerLineByte: Uint8Array, filenameIndex: number) {
126+
// fetch filename first
127+
let nameByte = headerLineByte.slice(0, filenameIndex - 2);
128+
let filenameByte = headerLineByte.slice(
129+
filenameIndex + encode.filename.byteLength + 2,
130+
headerLineByte.byteLength - 1,
131+
);
132+
133+
let name = getNameOnly(nameByte);
134+
let filename = decoder.decode(filenameByte);
135+
return { name, filename };
136+
}
137+
138+
function getNameOnly(headerLineByte: Uint8Array) {
139+
let nameIndex = bytes.findIndex(headerLineByte, encode.name);
140+
// jump <name="> and get string inside double quote => "string"
141+
let nameByte = headerLineByte.slice(
142+
nameIndex + encode.name.byteLength + 2,
143+
headerLineByte.byteLength - 1,
144+
);
145+
return decoder.decode(nameByte);
146+
}
147+
148+
function splitPiece(piece: Uint8Array) {
149+
const contentIndex = bytes.findIndex(piece, encode.returnNewline2);
150+
const headerByte = piece.slice(0, contentIndex);
151+
const contentByte = piece.slice(contentIndex + 4);
152+
153+
return { headerByte, contentByte };
154+
}
155+
156+
function getFieldPieces(
157+
buf: Uint8Array,
158+
boundaryByte: Uint8Array,
159+
): Uint8Array[] {
160+
const startBoundaryByte = bytes.concat(encode.dashdash, boundaryByte);
161+
const endBoundaryByte = bytes.concat(startBoundaryByte, encode.dashdash);
162+
163+
const pieces = [];
164+
165+
while (!bytes.hasPrefix(buf, endBoundaryByte)) {
166+
// jump over boundary + '\r\n'
167+
buf = buf.slice(startBoundaryByte.byteLength + 2);
168+
let boundaryIndex = bytes.findIndex(buf, startBoundaryByte);
169+
// get field content piece
170+
pieces.push(buf.slice(0, boundaryIndex - 1));
171+
buf = buf.slice(boundaryIndex);
172+
}
173+
174+
return pieces;
175+
}
176+
177+
function getBoundary(contentType: string): Uint8Array | undefined {
178+
let contentTypeByte = encoder.encode(contentType);
179+
let boundaryIndex = bytes.findIndex(contentTypeByte, encode.boundaryEqual);
180+
if (boundaryIndex >= 0) {
181+
// jump over 'boundary=' to get the real boundary
182+
let boundary = contentTypeByte.slice(
183+
boundaryIndex + encode.boundaryEqual.byteLength,
184+
);
185+
return boundary;
186+
} else {
187+
return undefined;
188+
}
189+
}

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)