Skip to content

Commit e27a6c1

Browse files
committed
create FormData from Multipart instance
1 parent e625bca commit e27a6c1

File tree

1 file changed

+114
-12
lines changed

1 file changed

+114
-12
lines changed

src/Multipart.ts

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -214,26 +214,128 @@ export class Multipart implements Part {
214214
return -1;
215215
}
216216

217+
/**
218+
* Parse header params in the format `key=value;foo = "bar"; baz`
219+
*/
220+
private static parseHeaderParams(input: string): Map<string, string> {
221+
const params = new Map();
222+
let currentKey = "";
223+
let currentValue = "";
224+
let insideQuotes = false;
225+
let escaping = false;
226+
let readingKey = true;
227+
let valueHasBegun = false;
228+
229+
for (const char of input) {
230+
if (escaping) {
231+
currentValue += char;
232+
escaping = false;
233+
continue;
234+
}
235+
236+
if (char === "\\") {
237+
if (!readingKey) escaping = true;
238+
continue;
239+
}
240+
241+
if (char === '"') {
242+
if (!readingKey) {
243+
if (valueHasBegun && !insideQuotes) currentValue += char;
244+
else {
245+
insideQuotes = !insideQuotes;
246+
valueHasBegun = true;
247+
}
248+
}
249+
else currentKey += char;
250+
continue;
251+
}
252+
253+
if (char === ";" && !insideQuotes) {
254+
currentKey = currentKey.trim();
255+
if (currentKey.length > 0) {
256+
if (readingKey)
257+
params.set(currentKey, "");
258+
params.set(currentKey, currentValue);
259+
}
260+
261+
currentKey = "";
262+
currentValue = "";
263+
readingKey = true;
264+
valueHasBegun = false;
265+
insideQuotes = false;
266+
continue;
267+
}
268+
269+
if (char === "=" && readingKey && !insideQuotes) {
270+
readingKey = false;
271+
continue;
272+
}
273+
274+
if (char === " " && !readingKey && !insideQuotes && !valueHasBegun)
275+
continue;
276+
277+
if (readingKey) currentKey += char;
278+
else {
279+
valueHasBegun = true;
280+
currentValue += char;
281+
}
282+
}
283+
284+
currentKey = currentKey.trim();
285+
if (currentKey.length > 0) {
286+
if (readingKey)
287+
params.set(currentKey, "");
288+
params.set(currentKey, currentValue);
289+
}
290+
291+
return params;
292+
}
293+
217294
/**
218295
* Extract media type and boundary from a `Content-Type` header
219296
*/
220297
private static parseContentType(contentType: string): { mediaType: string | null, boundary: string | null } {
221-
const parts = contentType.split(";");
298+
const firstSemicolonIndex = contentType.indexOf(";");
222299

223-
if (parts.length === 0) return {mediaType: null, boundary: null};
224-
const mediaType = parts[0]!.trim();
300+
if (firstSemicolonIndex === -1) return {mediaType: contentType, boundary: null};
301+
const mediaType = contentType.slice(0, firstSemicolonIndex);
302+
const params = Multipart.parseHeaderParams(contentType.slice(firstSemicolonIndex + 1));
303+
return {mediaType, boundary: params.get("boundary") ?? null};
304+
}
225305

226-
let boundary = null;
306+
/**
307+
* Extract name, filename and whether form-data from a `Content-Disposition` header
308+
*/
309+
private static parseContentDisposition(contentDisposition: string): {
310+
formData: boolean,
311+
name: string | null,
312+
filename: string | null,
313+
} {
314+
const params = Multipart.parseHeaderParams(contentDisposition);
315+
return {
316+
formData: params.has("form-data"),
317+
name: params.get("name") ?? null,
318+
filename: params.get("filename") ?? null,
319+
};
320+
}
227321

228-
for (const param of parts.slice(1)) {
229-
const equalsIndex = param.indexOf("=");
230-
if (equalsIndex === -1) continue;
231-
const key = param.slice(0, equalsIndex).trim();
232-
const value = param.slice(equalsIndex + 1).trim();
233-
if (key === "boundary" && value.length > 0) boundary = value;
322+
/**
323+
* Create FormData from this multipart.
324+
* Only parts that have `Content-Disposition` set to `form-data` and a non-empty `name` will be included.
325+
*/
326+
public formData(): FormData {
327+
const formData = new FormData();
328+
for (const part of this.parts) {
329+
if (!part.headers.has("Content-Disposition")) continue;
330+
const params = Multipart.parseContentDisposition(part.headers.get("Content-Disposition")!);
331+
if (!params.formData || params.name === null) continue;
332+
if (params.filename !== null) {
333+
const file: File = new File([part.body], params.filename, {type: part.headers.get("Content-Type") ?? void 0});
334+
formData.append(params.name, file);
335+
}
336+
else formData.append(params.name, new TextDecoder().decode(part.body));
234337
}
235-
236-
return {mediaType, boundary};
338+
return formData;
237339
}
238340

239341
/**

0 commit comments

Comments
 (0)