@@ -184,7 +184,8 @@ export class Multipart implements Part {
184
184
const parts : Component [ ] = [ ] ;
185
185
186
186
for ( const [ key , value ] of formData . entries ( ) ) {
187
- if ( typeof value === "string" ) parts . push ( new Component ( { "Content-Disposition" : `form-data; name="${ key } "` } , new TextEncoder ( ) . encode ( value ) ) ) ; else {
187
+ if ( typeof value === "string" ) parts . push ( new Component ( { "Content-Disposition" : `form-data; name="${ key } "` } , new TextEncoder ( ) . encode ( value ) ) ) ;
188
+ else {
188
189
const part = await Component . file ( value ) ;
189
190
part . headers . set ( "Content-Disposition" , `form-data; name="${ key } "; filename="${ value . name } "` ) ;
190
191
parts . push ( part ) ;
@@ -213,26 +214,128 @@ export class Multipart implements Part {
213
214
return - 1 ;
214
215
}
215
216
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
+
216
294
/**
217
295
* Extract media type and boundary from a `Content-Type` header
218
296
*/
219
297
private static parseContentType ( contentType : string ) : { mediaType : string | null , boundary : string | null } {
220
- const parts = contentType . split ( ";" ) ;
298
+ const firstSemicolonIndex = contentType . indexOf ( ";" ) ;
221
299
222
- if ( parts . length === 0 ) return { mediaType : null , boundary : null } ;
223
- 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
+ }
224
305
225
- 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
+ }
226
321
227
- for ( const param of parts . slice ( 1 ) ) {
228
- const equalsIndex = param . indexOf ( "=" ) ;
229
- if ( equalsIndex === - 1 ) continue ;
230
- const key = param . slice ( 0 , equalsIndex ) . trim ( ) ;
231
- const value = param . slice ( equalsIndex + 1 ) . trim ( ) ;
232
- 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 ) ) ;
233
337
}
234
-
235
- return { mediaType, boundary} ;
338
+ return formData ;
236
339
}
237
340
238
341
/**
0 commit comments