@@ -11,7 +11,7 @@ const { File: NodeFile } = require('node:buffer')
1111const File = globalThis . File ?? NodeFile
1212
1313const formDataNameBuffer = Buffer . from ( 'form-data; name="' )
14- const filenameBuffer = Buffer . from ( '; filename' )
14+ const filenameBuffer = Buffer . from ( 'filename' )
1515const dd = Buffer . from ( '--' )
1616const ddcrlf = Buffer . from ( '--\r\n' )
1717
@@ -75,7 +75,7 @@ function multipartFormDataParser (input, mimeType) {
7575 // Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
7676 // parameters["boundary"].
7777 if ( boundaryString === undefined ) {
78- return 'failure'
78+ throw parsingError ( 'missing boundary in content-type header' )
7979 }
8080
8181 const boundary = Buffer . from ( `--${ boundaryString } ` , 'utf8' )
@@ -111,7 +111,7 @@ function multipartFormDataParser (input, mimeType) {
111111 if ( input . subarray ( position . position , position . position + boundary . length ) . equals ( boundary ) ) {
112112 position . position += boundary . length
113113 } else {
114- return 'failure'
114+ throw parsingError ( 'expected a value starting with -- and the boundary' )
115115 }
116116
117117 // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
@@ -127,7 +127,7 @@ function multipartFormDataParser (input, mimeType) {
127127 // 5.3. If position does not point to a sequence of bytes starting with 0x0D
128128 // 0x0A (CR LF), return failure.
129129 if ( input [ position . position ] !== 0x0d || input [ position . position + 1 ] !== 0x0a ) {
130- return 'failure'
130+ throw parsingError ( 'expected CRLF' )
131131 }
132132
133133 // 5.4. Advance position by 2. (This skips past the newline.)
@@ -138,10 +138,6 @@ function multipartFormDataParser (input, mimeType) {
138138 // is not failure. Otherwise, return failure.
139139 const result = parseMultipartFormDataHeaders ( input , position )
140140
141- if ( result === 'failure' ) {
142- return 'failure'
143- }
144-
145141 let { name, filename, contentType, encoding } = result
146142
147143 // 5.6. Advance position by 2. (This skips past the empty line that marks
@@ -157,7 +153,7 @@ function multipartFormDataParser (input, mimeType) {
157153 const boundaryIndex = input . indexOf ( boundary . subarray ( 2 ) , position . position )
158154
159155 if ( boundaryIndex === - 1 ) {
160- return 'failure'
156+ throw parsingError ( 'expected boundary after body' )
161157 }
162158
163159 body = input . subarray ( position . position , boundaryIndex - 4 )
@@ -174,7 +170,7 @@ function multipartFormDataParser (input, mimeType) {
174170 // 5.9. If position does not point to a sequence of bytes starting with
175171 // 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
176172 if ( input [ position . position ] !== 0x0d || input [ position . position + 1 ] !== 0x0a ) {
177- return 'failure'
173+ throw parsingError ( 'expected CRLF' )
178174 } else {
179175 position . position += 2
180176 }
@@ -230,7 +226,7 @@ function parseMultipartFormDataHeaders (input, position) {
230226 if ( input [ position . position ] === 0x0d && input [ position . position + 1 ] === 0x0a ) {
231227 // 2.1.1. If name is null, return failure.
232228 if ( name === null ) {
233- return 'failure'
229+ throw parsingError ( 'header name is null' )
234230 }
235231
236232 // 2.1.2. Return name, filename and contentType.
@@ -250,12 +246,12 @@ function parseMultipartFormDataHeaders (input, position) {
250246
251247 // 2.4. If header name does not match the field-name token production, return failure.
252248 if ( ! HTTP_TOKEN_CODEPOINTS . test ( headerName . toString ( ) ) ) {
253- return 'failure'
249+ throw parsingError ( 'header name does not match the field-name token production' )
254250 }
255251
256252 // 2.5. If the byte at position is not 0x3A (:), return failure.
257253 if ( input [ position . position ] !== 0x3a ) {
258- return 'failure'
254+ throw parsingError ( 'expected :' )
259255 }
260256
261257 // 2.6. Advance position by 1.
@@ -278,7 +274,7 @@ function parseMultipartFormDataHeaders (input, position) {
278274 // 2. If position does not point to a sequence of bytes starting with
279275 // `form-data; name="`, return failure.
280276 if ( ! bufferStartsWith ( input , formDataNameBuffer , position ) ) {
281- return 'failure'
277+ throw parsingError ( 'expected form-data; name=" for content-disposition header' )
282278 }
283279
284280 // 3. Advance position so it points at the byte after the next 0x22 (")
@@ -290,34 +286,61 @@ function parseMultipartFormDataHeaders (input, position) {
290286 // failure.
291287 name = parseMultipartFormDataName ( input , position )
292288
293- if ( name === null ) {
294- return 'failure'
295- }
296-
297289 // 5. If position points to a sequence of bytes starting with `; filename="`:
298- if ( bufferStartsWith ( input , filenameBuffer , position ) ) {
299- // Note: undici also handles filename*
300- let check = position . position + filenameBuffer . length
301-
302- if ( input [ check ] === 0x2a ) {
303- position . position += 1
304- check += 1
305- }
306-
307- if ( input [ check ] !== 0x3d || input [ check + 1 ] !== 0x22 ) { // ="
308- return 'failure'
309- }
310-
311- // 1. Advance position so it points at the byte after the next 0x22 (") byte
312- // (the one in the sequence of bytes matched above).
313- position . position += 12
314-
315- // 2. Set filename to the result of parsing a multipart/form-data name given
316- // input and position, if the result is not failure. Otherwise, return failure.
317- filename = parseMultipartFormDataName ( input , position )
318-
319- if ( filename === null ) {
320- return 'failure'
290+ if ( input [ position . position ] === 0x3b /* ; */ && input [ position . position + 1 ] === 0x20 /* ' ' */ ) {
291+ const at = { position : position . position + 2 }
292+
293+ if ( bufferStartsWith ( input , filenameBuffer , at ) ) {
294+ if ( input [ at . position + 8 ] === 0x2a /* '*' */ ) {
295+ at . position += 10 // skip past filename*=
296+
297+ // Remove leading http tab and spaces. See RFC for examples.
298+ // https://datatracker.ietf.org/doc/html/rfc6266#section-5
299+ collectASequenceOfBytes (
300+ ( char ) => char === 0x20 || char === 0x09 ,
301+ input ,
302+ at
303+ )
304+
305+ const headerValue = collectASequenceOfBytes (
306+ ( char ) => char !== 0x20 && char !== 0x0d && char !== 0x0a , // ' ' or CRLF
307+ input ,
308+ at
309+ )
310+
311+ if (
312+ ( headerValue [ 0 ] !== 0x75 && headerValue [ 0 ] !== 0x55 ) || // u or U
313+ ( headerValue [ 1 ] !== 0x74 && headerValue [ 1 ] !== 0x54 ) || // t or T
314+ ( headerValue [ 2 ] !== 0x66 && headerValue [ 2 ] !== 0x46 ) || // f or F
315+ headerValue [ 3 ] !== 0x2d || // -
316+ headerValue [ 4 ] !== 0x38 // 8
317+ ) {
318+ throw parsingError ( 'unknown encoding, expected utf-8\'\'' )
319+ }
320+
321+ // skip utf-8''
322+ filename = decodeURIComponent ( new TextDecoder ( ) . decode ( headerValue . subarray ( 7 ) ) )
323+
324+ position . position = at . position
325+ } else {
326+ // 1. Advance position so it points at the byte after the next 0x22 (") byte
327+ // (the one in the sequence of bytes matched above).
328+ position . position += 11
329+
330+ // Remove leading http tab and spaces. See RFC for examples.
331+ // https://datatracker.ietf.org/doc/html/rfc6266#section-5
332+ collectASequenceOfBytes (
333+ ( char ) => char === 0x20 || char === 0x09 ,
334+ input ,
335+ position
336+ )
337+
338+ position . position ++ // skip past " after removing whitespace
339+
340+ // 2. Set filename to the result of parsing a multipart/form-data name given
341+ // input and position, if the result is not failure. Otherwise, return failure.
342+ filename = parseMultipartFormDataName ( input , position )
343+ }
321344 }
322345 }
323346
@@ -367,7 +390,7 @@ function parseMultipartFormDataHeaders (input, position) {
367390 // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
368391 // (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
369392 if ( input [ position . position ] !== 0x0d && input [ position . position + 1 ] !== 0x0a ) {
370- return 'failure'
393+ throw parsingError ( 'expected CRLF' )
371394 } else {
372395 position . position += 2
373396 }
@@ -393,7 +416,7 @@ function parseMultipartFormDataName (input, position) {
393416
394417 // 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
395418 if ( input [ position . position ] !== 0x22 ) {
396- return null // name could be 'failure'
419+ throw parsingError ( 'expected "' )
397420 } else {
398421 position . position ++
399422 }
@@ -468,6 +491,10 @@ function bufferStartsWith (buffer, start, position) {
468491 return true
469492}
470493
494+ function parsingError ( cause ) {
495+ return new TypeError ( 'Failed to parse body as FormData.' , { cause : new TypeError ( cause ) } )
496+ }
497+
471498module . exports = {
472499 multipartFormDataParser,
473500 validateBoundary
0 commit comments