@@ -184,7 +184,8 @@ export class Multipart implements Part {
184184 const parts : Component [ ] = [ ] ;
185185
186186 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 {
188189 const part = await Component . file ( value ) ;
189190 part . headers . set ( "Content-Disposition" , `form-data; name="${ key } "; filename="${ value . name } "` ) ;
190191 parts . push ( part ) ;
@@ -213,26 +214,128 @@ export class Multipart implements Part {
213214 return - 1 ;
214215 }
215216
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+
216294 /**
217295 * Extract media type and boundary from a `Content-Type` header
218296 */
219297 private static parseContentType ( contentType : string ) : { mediaType : string | null , boundary : string | null } {
220- const parts = contentType . split ( ";" ) ;
298+ const firstSemicolonIndex = contentType . indexOf ( ";" ) ;
221299
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+ }
224305
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+ }
226321
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 ) ) ;
233337 }
234-
235- return { mediaType, boundary} ;
338+ return formData ;
236339 }
237340
238341 /**
0 commit comments