@@ -355,14 +355,88 @@ pub trait TryFromMultipartWithState<S: Send + Sync>: Sized {
355355 ) -> impl std:: future:: Future < Output = Result < Self , TypedMultipartError > > + Send ;
356356}
357357
358+ /// A multipart [`Field`] wrapper that meters every byte read against the
359+ /// request-wide `max_total_bytes` aggregate cap — **non-cooperatively**.
360+ ///
361+ /// `#[derive(Multipart)]` hands each [`TryFromFieldWithState`] parser a
362+ /// `MeteredField` instead of a raw [`Field`], so a **custom** field parser that
363+ /// reads bytes via [`MeteredField::chunk`] / [`MeteredField::bytes`] is
364+ /// accounted automatically: it can no longer read unboundedly past the
365+ /// configured `max_total_bytes` the way a raw `field.chunk()` could. The
366+ /// metadata accessors delegate to the wrapped field.
367+ pub struct MeteredField < ' a > {
368+ inner : Field < ' a > ,
369+ }
370+
371+ impl < ' a > MeteredField < ' a > {
372+ /// Wrap a raw axum field. Public + `#[doc(hidden)]`: the
373+ /// `#[derive(Multipart)]` loop constructs this in the user's crate; it is
374+ /// not part of the stable hand-written API.
375+ #[ doc( hidden) ]
376+ #[ must_use]
377+ pub fn __from_field ( inner : Field < ' a > ) -> Self {
378+ Self { inner }
379+ }
380+
381+ /// The field's form name, if present.
382+ #[ must_use]
383+ pub fn name ( & self ) -> Option < & str > {
384+ self . inner . name ( )
385+ }
386+
387+ /// The original client filename, if present.
388+ #[ must_use]
389+ pub fn file_name ( & self ) -> Option < & str > {
390+ self . inner . file_name ( )
391+ }
392+
393+ /// The field's declared content type, if present.
394+ #[ must_use]
395+ pub fn content_type ( & self ) -> Option < & str > {
396+ self . inner . content_type ( )
397+ }
398+
399+ /// Read the next chunk, metering its length against the request-wide
400+ /// `max_total_bytes` cap **before** yielding it. Returns
401+ /// [`TypedMultipartError::RequestTooLarge`] once the running total crosses
402+ /// the cap.
403+ pub async fn chunk ( & mut self ) -> Result < Option < axum:: body:: Bytes > , TypedMultipartError > {
404+ let next = self . inner . chunk ( ) . await ?;
405+ if let Some ( chunk) = & next {
406+ register_multipart_bytes ( self . inner . name ( ) . unwrap_or_default ( ) , chunk. len ( ) ) ?;
407+ }
408+ Ok ( next)
409+ }
410+
411+ /// Read the whole field into owned bytes, metering every chunk against the
412+ /// aggregate cap.
413+ pub async fn bytes ( mut self ) -> Result < axum:: body:: Bytes , TypedMultipartError > {
414+ let mut acc: Vec < u8 > = Vec :: new ( ) ;
415+ while let Some ( chunk) = self . chunk ( ) . await ? {
416+ acc. extend_from_slice ( & chunk) ;
417+ }
418+ Ok ( axum:: body:: Bytes :: from ( acc) )
419+ }
420+ }
421+
422+ impl From < & MeteredField < ' _ > > for FieldMetadata {
423+ fn from ( field : & MeteredField < ' _ > ) -> Self {
424+ Self :: from ( & field. inner )
425+ }
426+ }
427+
358428/// Parse a single multipart field into a value.
359429///
360430/// Built-in implementations exist for `String`, `bool`, all integer and float
361431/// types, `char`, `tempfile::NamedTempFile`, and `FieldData<T>`.
362432pub trait TryFromFieldWithState < S : Send + Sync > : Sized {
363433 /// Parse a single field into `Self`, optionally enforcing a byte-size limit.
434+ ///
435+ /// The field arrives as a [`MeteredField`]: every byte read through it
436+ /// counts against the request-wide `max_total_bytes` aggregate cap, so even
437+ /// a hand-written custom parser cannot bypass the limit.
364438 fn try_from_field_with_state (
365- field : Field < ' _ > ,
439+ field : MeteredField < ' _ > ,
366440 limit_bytes : Option < usize > ,
367441 state : & S ,
368442 ) -> impl std:: future:: Future < Output = Result < Self , TypedMultipartError > > + Send ;
@@ -448,7 +522,7 @@ where
448522 S : Send + Sync ,
449523{
450524 async fn try_from_field_with_state (
451- field : Field < ' _ > ,
525+ field : MeteredField < ' _ > ,
452526 limit_bytes : Option < usize > ,
453527 state : & S ,
454528 ) -> Result < Self , TypedMultipartError > {
@@ -616,15 +690,17 @@ pub fn register_multipart_part() -> Result<(), TypedMultipartError> {
616690/// ([`read_field_data`] / the `NamedTempFile` path) already call this once per
617691/// `field.chunk()`, so typed multipart structs are accounted automatically.
618692///
619- /// A **custom [`TryFromFieldWithState`] implementation that consumes a field's
620- /// bytes itself** (via `field.chunk()` / `field.bytes()`) MUST call this once
621- /// per chunk to participate in the aggregate cap — otherwise that field's bytes
622- /// are invisible to `max_total_bytes` and a single custom-parsed field can read
623- /// unboundedly past the configured policy. The per-field `limit_bytes` passed to
624- /// the trait method still bounds that one field, but only this call enforces the
625- /// request-wide total. Mirrors the cooperative contract of
626- /// [`register_multipart_part`]: outside the extractor's task-local scope (e.g. a
627- /// direct unit test of a derived parser) it no-ops rather than failing.
693+ /// Built-in field parsers — and any **custom [`TryFromFieldWithState`]**
694+ /// implementation — read a field's bytes through [`MeteredField::chunk`] /
695+ /// [`MeteredField::bytes`], which call this automatically once per chunk. A
696+ /// custom parser therefore **cannot** bypass the aggregate cap: [`MeteredField`]
697+ /// owns the only access to the field's bytes (the raw axum [`Field`] is never
698+ /// exposed), so every byte is counted regardless of how the parser is written.
699+ /// The per-field `limit_bytes` passed to the trait method still bounds that one
700+ /// field; this call enforces the request-wide total. Mirrors the cooperative
701+ /// contract of [`register_multipart_part`]: outside the extractor's task-local
702+ /// scope (e.g. a direct unit test of a derived parser) it no-ops rather than
703+ /// failing.
628704pub fn register_multipart_bytes (
629705 field_name : & str ,
630706 chunk_len : usize ,
@@ -731,18 +807,20 @@ where
731807/// When a limit is set the cumulative size is checked after each chunk
732808/// and an over-limit chunk is rejected *before* it is copied in.
733809async fn read_field_data (
734- mut field : Field < ' _ > ,
810+ mut field : MeteredField < ' _ > ,
735811 limit : Option < usize > ,
736812 initial_capacity : usize ,
737- ) -> Result < ( Field < ' _ > , Vec < u8 > ) , TypedMultipartError > {
813+ ) -> Result < ( MeteredField < ' _ > , Vec < u8 > ) , TypedMultipartError > {
738814 // Part counting now happens once per part in the derived loop
739815 // (`register_multipart_part`), so the field parsers no longer count.
740816 // Initial capacity is independent from the hard byte limit: tiny scalar
741817 // fields keep the 256B cap without preallocating 256B per bool/number.
742818 let capacity = limit. map_or ( initial_capacity, |limit| initial_capacity. min ( limit) ) ;
743819 let mut buf = Vec :: with_capacity ( capacity) ;
820+ // `MeteredField::chunk` already counts each chunk against the request-wide
821+ // `max_total_bytes` aggregate cap, so the per-field reader no longer calls
822+ // `register_multipart_bytes` itself (doing so would double-count).
744823 while let Some ( chunk) = field. chunk ( ) . await ? {
745- register_multipart_bytes ( field. name ( ) . unwrap_or_default ( ) , chunk. len ( ) ) ?;
746824 if let Some ( limit) = limit
747825 && buf. len ( ) . saturating_add ( chunk. len ( ) ) > limit
748826 {
@@ -844,130 +922,15 @@ pub fn set_default_temp_file_field_limit_bytes(limit_bytes: usize) -> usize {
844922 DEFAULT_TEMP_FILE_FIELD_LIMIT . swap ( limit_bytes, Ordering :: Relaxed )
845923}
846924
847- impl < S : Send + Sync > TryFromFieldWithState < S > for String {
848- async fn try_from_field_with_state (
849- field : Field < ' _ > ,
850- limit_bytes : Option < usize > ,
851- _state : & S ,
852- ) -> Result < Self , TypedMultipartError > {
853- // An ABSENT limit (`None`) applies the generous default cap; an
854- // explicit `#[form_data(limit = "unlimited")]` arrives as
855- // `Some(usize::MAX)` (set by the derive macro) and stays unbounded;
856- // an explicit byte size wins as `Some(n)`.
857- let limit = limit_bytes. unwrap_or ( DEFAULT_STRING_FIELD_LIMIT_BYTES ) ;
858- let ( field, data) =
859- read_field_data ( field, Some ( limit) , STRING_INITIAL_CAPACITY_BYTES ) . await ?;
860- Self :: from_utf8 ( data) . map_err ( |e| TypedMultipartError :: WrongFieldType {
861- field_name : field. name ( ) . unwrap_or_default ( ) . to_string ( ) ,
862- wanted : Cow :: Borrowed ( "String" ) ,
863- source : e. to_string ( ) ,
864- } )
865- }
866- }
867-
868- // ─── bool ───────────────────────────────────────────────────────────────────
869-
870- impl < S : Send + Sync > TryFromFieldWithState < S > for bool {
871- async fn try_from_field_with_state (
872- field : Field < ' _ > ,
873- limit_bytes : Option < usize > ,
874- _state : & S ,
875- ) -> Result < Self , TypedMultipartError > {
876- let ( field, data) = read_field_data (
877- field,
878- Some ( tiny_scalar_limit ( limit_bytes) ) ,
879- TINY_SCALAR_INITIAL_CAPACITY_BYTES ,
880- )
881- . await ?;
882- let text = std:: str:: from_utf8 ( & data) . map_err ( |e| TypedMultipartError :: WrongFieldType {
883- field_name : field. name ( ) . unwrap_or_default ( ) . to_string ( ) ,
884- wanted : Cow :: Borrowed ( "bool" ) ,
885- source : e. to_string ( ) ,
886- } ) ?;
887- str_to_bool ( text) . ok_or_else ( || TypedMultipartError :: WrongFieldType {
888- field_name : field. name ( ) . unwrap_or_default ( ) . to_string ( ) ,
889- wanted : Cow :: Borrowed ( "bool" ) ,
890- source : format ! ( "invalid boolean value: `{text}`" ) ,
891- } )
892- }
893- }
894-
895- // ─── Numeric types ──────────────────────────────────────────────────────────
896-
897- macro_rules! impl_try_from_field_for_number {
898- ( $( $ty: ty) ,* $( , ) ?) => {
899- $(
900- impl <S : Send + Sync > TryFromFieldWithState <S > for $ty {
901- async fn try_from_field_with_state(
902- field: Field <' _>,
903- limit_bytes: Option <usize >,
904- _state: & S ,
905- ) -> Result <Self , TypedMultipartError > {
906- let ( field, data) = read_field_data(
907- field,
908- Some ( tiny_scalar_limit( limit_bytes) ) ,
909- TINY_SCALAR_INITIAL_CAPACITY_BYTES ,
910- ) . await ?;
911- let text = std:: str :: from_utf8( & data) . map_err( |e| {
912- TypedMultipartError :: WrongFieldType {
913- field_name: field. name( ) . unwrap_or_default( ) . to_string( ) ,
914- wanted: Cow :: Borrowed ( stringify!( $ty) ) ,
915- source: e. to_string( ) ,
916- }
917- } ) ?;
918- text. trim( ) . parse:: <$ty>( ) . map_err( |e| {
919- TypedMultipartError :: WrongFieldType {
920- field_name: field. name( ) . unwrap_or_default( ) . to_string( ) ,
921- wanted: Cow :: Borrowed ( stringify!( $ty) ) ,
922- source: e. to_string( ) ,
923- }
924- } )
925- }
926- }
927- ) *
928- } ;
929- }
930-
931- impl_try_from_field_for_number ! (
932- i8 , i16 , i32 , i64 , i128 , u8 , u16 , u32 , u64 , u128 , isize , usize , f32 , f64 ,
933- ) ;
934-
935- // ─── char ───────────────────────────────────────────────────────────────────
936-
937- impl < S : Send + Sync > TryFromFieldWithState < S > for char {
938- async fn try_from_field_with_state (
939- field : Field < ' _ > ,
940- limit_bytes : Option < usize > ,
941- _state : & S ,
942- ) -> Result < Self , TypedMultipartError > {
943- let ( field, data) = read_field_data (
944- field,
945- Some ( tiny_scalar_limit ( limit_bytes) ) ,
946- TINY_SCALAR_INITIAL_CAPACITY_BYTES ,
947- )
948- . await ?;
949- let text = std:: str:: from_utf8 ( & data) . map_err ( |e| TypedMultipartError :: WrongFieldType {
950- field_name : field. name ( ) . unwrap_or_default ( ) . to_string ( ) ,
951- wanted : Cow :: Borrowed ( "char" ) ,
952- source : e. to_string ( ) ,
953- } ) ?;
954- let mut chars = text. chars ( ) ;
955- match ( chars. next ( ) , chars. next ( ) ) {
956- ( Some ( c) , None ) => Ok ( c) ,
957- _ => Err ( TypedMultipartError :: WrongFieldType {
958- field_name : field. name ( ) . unwrap_or_default ( ) . to_string ( ) ,
959- wanted : Cow :: Borrowed ( "char" ) ,
960- source : "expected exactly one character" . to_string ( ) ,
961- } ) ,
962- }
963- }
964- }
925+ // Scalar field parsers (`String`, `bool`, integers/floats, `char`) live in a
926+ // sidecar module so `multipart.rs` stays within the 1000-line source cap.
927+ mod scalar_parsers;
965928
966929// ─── NamedTempFile ──────────────────────────────────────────────────────────
967930
968931impl < S : Send + Sync > TryFromFieldWithState < S > for tempfile:: NamedTempFile {
969932 async fn try_from_field_with_state (
970- mut field : Field < ' _ > ,
933+ mut field : MeteredField < ' _ > ,
971934 limit_bytes : Option < usize > ,
972935 _state : & S ,
973936 ) -> Result < Self , TypedMultipartError > {
@@ -998,7 +961,8 @@ impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
998961 let limit_bytes = limit_bytes. unwrap_or_else ( default_temp_file_field_limit_bytes) ;
999962 let mut total = 0usize ;
1000963 while let Some ( chunk) = field. chunk ( ) . await ? {
1001- register_multipart_bytes ( field. name ( ) . unwrap_or_default ( ) , chunk. len ( ) ) ?;
964+ // `MeteredField::chunk` already counts the chunk against the
965+ // request-wide `max_total_bytes` aggregate cap (no double-count).
1002966 // `saturating_add` (matching `read_field_data`) prevents a
1003967 // pathological chunk size from wrapping `total` and slipping
1004968 // past the limit check below.
0 commit comments