Skip to content

Commit 8d60e88

Browse files
committed
Fix jni
1 parent 26bb863 commit 8d60e88

18 files changed

Lines changed: 712 additions & 165 deletions

File tree

crates/vespera/src/multipart.rs

Lines changed: 98 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -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>`.
362432
pub 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.
628704
pub 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.
733809
async 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

968931
impl<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

Comments
 (0)