-
Notifications
You must be signed in to change notification settings - Fork 170
/
multiparser.ts
executable file
·198 lines (161 loc) · 5.51 KB
/
multiparser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import { bytes } from '../deps.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.indexOf(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.indexOf(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.indexOf(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.indexOf(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.startsWith(buf, endBoundaryByte)) {
// jump over boundary + '\r\n'
buf = buf.slice(startBoundaryByte.byteLength + 2)
let boundaryIndex = bytes.indexOf(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.indexOf(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
}
}