@@ -449,7 +449,9 @@ impl FromRequest for Multipart {
449449 let boundary = extract_boundary ( content_type)
450450 . ok_or_else ( || ApiError :: bad_request ( "Missing boundary in Content-Type" ) ) ?;
451451
452- // Get body
452+ // Buffer streaming bodies from live HTTP connections before parsing.
453+ req. load_body ( ) . await ?;
454+
453455 let body = req
454456 . take_body ( )
455457 . ok_or_else ( || ApiError :: internal ( "Body already consumed" ) ) ?;
@@ -562,96 +564,131 @@ fn extract_boundary(content_type: &str) -> Option<String> {
562564 } )
563565}
564566
565- /// Parse multipart form data
566- fn parse_multipart ( body : & Bytes , boundary : & str ) -> Result < Vec < MultipartField > > {
567- let mut fields = Vec :: new ( ) ;
568- let delimiter = format ! ( "--{}" , boundary) ;
569- let end_delimiter = format ! ( "--{}--" , boundary) ;
570-
571- // Convert body to string for easier parsing
572- // Note: This is a simplified parser. For production, consider using multer crate.
573- let body_str = String :: from_utf8_lossy ( body) ;
567+ fn find_subsequence ( haystack : & [ u8 ] , needle : & [ u8 ] , from : usize ) -> Option < usize > {
568+ haystack[ from..]
569+ . windows ( needle. len ( ) )
570+ . position ( |window| window == needle)
571+ . map ( |pos| from + pos)
572+ }
574573
575- // Split by delimiter
576- let parts: Vec < & str > = body_str. split ( & delimiter) . collect ( ) ;
574+ fn trim_trailing_crlf ( mut data : Vec < u8 > ) -> Vec < u8 > {
575+ while data. ends_with ( b"\r \n " ) {
576+ data. truncate ( data. len ( ) . saturating_sub ( 2 ) ) ;
577+ }
578+ while data. ends_with ( b"\n " ) {
579+ data. truncate ( data. len ( ) . saturating_sub ( 1 ) ) ;
580+ }
581+ data
582+ }
577583
578- for part in parts. iter ( ) . skip ( 1 ) {
579- // Skip empty parts and end delimiter
580- let part = part. trim_start_matches ( "\r \n " ) . trim_start_matches ( '\n' ) ;
581- if part. is_empty ( ) || part. starts_with ( "--" ) {
584+ fn parse_multipart_part ( part : & [ u8 ] ) -> Option < MultipartField > {
585+ let ( header_end, body_start) = if let Some ( pos) = find_subsequence ( part, b"\r \n \r \n " , 0 ) {
586+ ( pos, pos + 4 )
587+ } else if let Some ( pos) = find_subsequence ( part, b"\n \n " , 0 ) {
588+ ( pos, pos + 2 )
589+ } else {
590+ return None ;
591+ } ;
592+
593+ let headers_section = String :: from_utf8_lossy ( & part[ ..header_end] ) ;
594+ let body_section = trim_trailing_crlf ( part[ body_start..] . to_vec ( ) ) ;
595+
596+ let mut name = None ;
597+ let mut filename = None ;
598+ let mut content_type = None ;
599+
600+ for header_line in headers_section. lines ( ) {
601+ let header_line = header_line. trim ( ) ;
602+ if header_line. is_empty ( ) {
582603 continue ;
583604 }
584605
585- // Find header/body separator (blank line)
586- let header_body_split = if let Some ( pos) = part. find ( "\r \n \r \n " ) {
587- pos
588- } else if let Some ( pos) = part. find ( "\n \n " ) {
589- pos
590- } else {
591- continue ;
592- } ;
593-
594- let headers_section = & part[ ..header_body_split] ;
595- let body_section = & part[ header_body_split..]
596- . trim_start_matches ( "\r \n \r \n " )
597- . trim_start_matches ( "\n \n " ) ;
598-
599- // Remove trailing boundary markers from body
600- let body_section = body_section
601- . trim_end_matches ( & end_delimiter)
602- . trim_end_matches ( & delimiter)
603- . trim_end_matches ( "\r \n " )
604- . trim_end_matches ( '\n' ) ;
605-
606- // Parse headers
607- let mut name = None ;
608- let mut filename = None ;
609- let mut content_type = None ;
610-
611- for header_line in headers_section. lines ( ) {
612- let header_line = header_line. trim ( ) ;
613- if header_line. is_empty ( ) {
614- continue ;
615- }
616-
617- if let Some ( ( key, value) ) = header_line. split_once ( ':' ) {
618- let key = key. trim ( ) . to_lowercase ( ) ;
619- let value = value. trim ( ) ;
620-
621- match key. as_str ( ) {
622- "content-disposition" => {
623- // Parse name and filename from Content-Disposition
624- for part in value. split ( ';' ) {
625- let part = part. trim ( ) ;
626- if part. starts_with ( "name=" ) {
627- name = Some (
628- part. trim_start_matches ( "name=" )
629- . trim_matches ( '"' )
630- . to_string ( ) ,
631- ) ;
632- } else if part. starts_with ( "filename=" ) {
633- filename = Some (
634- part. trim_start_matches ( "filename=" )
635- . trim_matches ( '"' )
636- . to_string ( ) ,
637- ) ;
638- }
606+ if let Some ( ( key, value) ) = header_line. split_once ( ':' ) {
607+ let key = key. trim ( ) . to_lowercase ( ) ;
608+ let value = value. trim ( ) ;
609+
610+ match key. as_str ( ) {
611+ "content-disposition" => {
612+ for segment in value. split ( ';' ) {
613+ let segment = segment. trim ( ) ;
614+ if segment. starts_with ( "name=" ) {
615+ name = Some (
616+ segment
617+ . trim_start_matches ( "name=" )
618+ . trim_matches ( '"' )
619+ . to_string ( ) ,
620+ ) ;
621+ } else if segment. starts_with ( "filename=" ) {
622+ filename = Some (
623+ segment
624+ . trim_start_matches ( "filename=" )
625+ . trim_matches ( '"' )
626+ . to_string ( ) ,
627+ ) ;
639628 }
640629 }
641- "content-type" => {
642- content_type = Some ( value. to_string ( ) ) ;
643- }
644- _ => { }
645630 }
631+ "content-type" => {
632+ content_type = Some ( value. to_string ( ) ) ;
633+ }
634+ _ => { }
646635 }
647636 }
637+ }
648638
649- fields. push ( MultipartField :: new (
650- name,
651- filename,
652- content_type,
653- Bytes :: copy_from_slice ( body_section. as_bytes ( ) ) ,
654- ) ) ;
639+ Some ( MultipartField :: new (
640+ name,
641+ filename,
642+ content_type,
643+ Bytes :: from ( body_section) ,
644+ ) )
645+ }
646+
647+ /// Parse multipart form data from raw bytes (binary-safe).
648+ fn parse_multipart ( body : & Bytes , boundary : & str ) -> Result < Vec < MultipartField > > {
649+ let delimiter = format ! ( "--{}" , boundary) ;
650+ let delim = delimiter. as_bytes ( ) ;
651+
652+ let first = find_subsequence ( body, delim, 0 )
653+ . ok_or_else ( || ApiError :: bad_request ( "No multipart boundary found" ) ) ?;
654+
655+ let mut fields = Vec :: new ( ) ;
656+ let mut cursor = first + delim. len ( ) ;
657+
658+ if body[ cursor..] . starts_with ( b"--" ) {
659+ return Ok ( fields) ;
660+ }
661+
662+ if body[ cursor..] . starts_with ( b"\r \n " ) {
663+ cursor += 2 ;
664+ } else if body. get ( cursor) == Some ( & b'\n' ) {
665+ cursor += 1 ;
666+ }
667+
668+ while cursor < body. len ( ) {
669+ let next = find_subsequence ( body, delim, cursor) ;
670+ let part_end = next. unwrap_or ( body. len ( ) ) ;
671+ let part = & body[ cursor..part_end] ;
672+
673+ if !part. is_empty ( ) {
674+ if let Some ( field) = parse_multipart_part ( part) {
675+ fields. push ( field) ;
676+ }
677+ }
678+
679+ let Some ( next_pos) = next else {
680+ break ;
681+ } ;
682+
683+ cursor = next_pos + delim. len ( ) ;
684+ if body[ cursor..] . starts_with ( b"--" ) {
685+ break ;
686+ }
687+ if body[ cursor..] . starts_with ( b"\r \n " ) {
688+ cursor += 2 ;
689+ } else if body. get ( cursor) == Some ( & b'\n' ) {
690+ cursor += 1 ;
691+ }
655692 }
656693
657694 Ok ( fields)
@@ -866,6 +903,27 @@ mod tests {
866903 assert ! ( fields[ 1 ] . is_file( ) ) ;
867904 }
868905
906+ #[ tokio:: test]
907+ async fn test_parse_multipart_preserves_binary_payload ( ) {
908+ let boundary = "test-boundary" ;
909+ let binary = vec ! [ 0x4D , 0x5A , 0x90 , 0x00 , 0xFF , 0xFE , 0x00 , 0x00 ] ;
910+ let mut body = format ! (
911+ "--{boundary}\r \n Content-Disposition: form-data; name=\" project_name\" \r \n \r \n app\r \n \
912+ --{boundary}\r \n Content-Disposition: form-data; name=\" binary\" ; filename=\" app.bin\" \r \n \
913+ Content-Type: application/octet-stream\r \n \r \n "
914+ )
915+ . into_bytes ( ) ;
916+ body. extend_from_slice ( & binary) ;
917+ body. extend_from_slice ( format ! ( "\r \n --{boundary}--\r \n " ) . as_bytes ( ) ) ;
918+
919+ let fields = parse_multipart ( & Bytes :: from ( body) , boundary) . unwrap ( ) ;
920+ assert_eq ! ( fields. len( ) , 2 ) ;
921+ assert_eq ! (
922+ fields[ 1 ] . bytes( ) . await . expect( "binary field" ) ,
923+ binary. as_slice( )
924+ ) ;
925+ }
926+
869927 #[ test]
870928 fn test_multipart_config ( ) {
871929 let config = MultipartConfig :: new ( )
0 commit comments