vespera 0.4.0

A fully automated OpenAPI engine for Axum with zero-config route and schema discovery
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
//! Native multipart form data extraction for Vespera.
//!
//! Replaces the `axum_typed_multipart` crate with a zero-dependency (beyond axum)
//! implementation of typed multipart extraction. All types here are referenced by
//! the `#[derive(Multipart)]` macro's generated code.
//!
//! # Key types
//!
//! - [`TypedMultipart<T>`] — Axum extractor that parses `multipart/form-data` into `T`
//! - [`TypedMultipartError`] — Error type for multipart parsing failures
//! - [`FieldData<T>`] — Wrapper providing file metadata alongside field contents
//! - [`FieldMetadata`] — Metadata extracted from a multipart field
//! - [`TryFromMultipartWithState<S>`] — Trait for parsing a full multipart request
//! - [`TryFromFieldWithState<S>`] — Trait for parsing a single multipart field

use std::cell::RefCell;
use std::sync::atomic::{AtomicUsize, Ordering};

use axum::extract::multipart::Field;
use axum::extract::{FromRequest, Request};

mod error;
pub use error::TypedMultipartError;
// Re-export the truncation helper so sibling modules (`scalar_parsers`, the
// inline test module) keep their existing `super::truncate_reflected_value`
// path — the helper logically belongs with the error type that bounds it.
use error::truncate_reflected_value;

// ═══════════════════════════════════════════════════════════════════════════════
// Traits
// ═══════════════════════════════════════════════════════════════════════════════

/// Parse a full multipart request body into a struct.
///
/// Typically generated by `#[derive(Multipart)]`. Each field in the struct
/// is matched against multipart field names and parsed via
/// [`TryFromFieldWithState`].
pub trait TryFromMultipartWithState<S: Send + Sync>: Sized {
    /// Parse the multipart stream into `Self`.
    fn try_from_multipart_with_state(
        multipart: &mut axum::extract::Multipart,
        state: &S,
    ) -> impl std::future::Future<Output = Result<Self, TypedMultipartError>> + Send;
}

/// A multipart [`Field`] wrapper that meters every byte read against the
/// request-wide `max_total_bytes` aggregate cap — **non-cooperatively**.
///
/// `#[derive(Multipart)]` hands each [`TryFromFieldWithState`] parser a
/// `MeteredField` instead of a raw [`Field`], so a **custom** field parser that
/// reads bytes via [`MeteredField::chunk`] / [`MeteredField::bytes`] is
/// accounted automatically: it can no longer read unboundedly past the
/// configured `max_total_bytes` the way a raw `field.chunk()` could. The
/// metadata accessors delegate to the wrapped field.
pub struct MeteredField<'a> {
    inner: Field<'a>,
}

impl<'a> MeteredField<'a> {
    /// Wrap a raw axum field. Public + `#[doc(hidden)]`: the
    /// `#[derive(Multipart)]` loop constructs this in the user's crate; it is
    /// not part of the stable hand-written API.
    #[doc(hidden)]
    #[must_use]
    pub fn __from_field(inner: Field<'a>) -> Self {
        Self { inner }
    }

    /// The field's form name, if present.
    #[must_use]
    pub fn name(&self) -> Option<&str> {
        self.inner.name()
    }

    /// The original client filename, if present.
    #[must_use]
    pub fn file_name(&self) -> Option<&str> {
        self.inner.file_name()
    }

    /// The field's declared content type, if present.
    #[must_use]
    pub fn content_type(&self) -> Option<&str> {
        self.inner.content_type()
    }

    /// Read the next chunk, metering its length against the request-wide
    /// `max_total_bytes` cap **before** yielding it. Returns
    /// [`TypedMultipartError::RequestTooLarge`] once the running total crosses
    /// the cap.
    pub async fn chunk(&mut self) -> Result<Option<axum::body::Bytes>, TypedMultipartError> {
        let next = self.inner.chunk().await?;
        if let Some(chunk) = &next {
            register_multipart_bytes(self.inner.name().unwrap_or_default(), chunk.len())?;
        }
        Ok(next)
    }

    /// Read the whole field into owned bytes, metering every chunk against the
    /// aggregate cap.
    pub async fn bytes(mut self) -> Result<axum::body::Bytes, TypedMultipartError> {
        self.bytes_with_limit_inner(None, 0)
            .await
            .map(axum::body::Bytes::from)
    }

    /// Read the whole field into owned bytes with a hard per-field limit.
    ///
    /// The limit is checked before copying each chunk into the accumulator, and
    /// every chunk is still counted against the request-wide aggregate cap.
    pub async fn bytes_with_limit(
        mut self,
        limit_bytes: usize,
        initial_capacity: usize,
    ) -> Result<axum::body::Bytes, TypedMultipartError> {
        self.bytes_with_limit_inner(Some(limit_bytes), initial_capacity)
            .await
            .map(axum::body::Bytes::from)
    }

    async fn bytes_with_limit_inner(
        &mut self,
        limit: Option<usize>,
        initial_capacity: usize,
    ) -> Result<Vec<u8>, TypedMultipartError> {
        let capacity = limit.map_or(initial_capacity, |limit| initial_capacity.min(limit));
        let mut acc: Vec<u8> = Vec::with_capacity(capacity);
        while let Some(chunk) = self.chunk().await? {
            if let Some(limit) = limit
                && acc.len().saturating_add(chunk.len()) > limit
            {
                return Err(TypedMultipartError::FieldTooLarge {
                    field_name: self.name().unwrap_or_default().to_string(),
                    limit_bytes: limit,
                });
            }
            acc.extend_from_slice(&chunk);
        }
        Ok(acc)
    }
}

impl From<&MeteredField<'_>> for FieldMetadata {
    fn from(field: &MeteredField<'_>) -> Self {
        Self::from(&field.inner)
    }
}

/// Parse a single multipart field into a value.
///
/// Built-in implementations exist for `String`, `bool`, all integer and float
/// types, `char`, `tempfile::NamedTempFile`, and `FieldData<T>`.
pub trait TryFromFieldWithState<S: Send + Sync>: Sized {
    /// Parse a single field into `Self`, optionally enforcing a byte-size limit.
    ///
    /// The field arrives as a [`MeteredField`]: every byte read through it
    /// counts against the request-wide `max_total_bytes` aggregate cap, so even
    /// a hand-written custom parser cannot bypass the limit.
    fn try_from_field_with_state(
        field: MeteredField<'_>,
        limit_bytes: Option<usize>,
        state: &S,
    ) -> impl std::future::Future<Output = Result<Self, TypedMultipartError>> + Send;
}

// ═══════════════════════════════════════════════════════════════════════════════
// Field metadata
// ═══════════════════════════════════════════════════════════════════════════════

/// Metadata extracted from a multipart field part.
#[derive(Debug, Clone)]
pub struct FieldMetadata {
    /// The field name (`name` attribute in the form).
    pub name: Option<String>,
    /// The original filename (present for file uploads).
    pub file_name: Option<String>,
    /// The MIME content type of the field.
    pub content_type: Option<String>,
    /// Full HTTP headers associated with this multipart part, when explicitly captured.
    ///
    /// Vespera's built-in parsers only need `name`, `file_name`, and `content_type`,
    /// so the default `FieldData<T>` path no longer clones the whole `HeaderMap` for
    /// every field. Use [`FieldMetadata::with_headers`] when constructing metadata
    /// manually and the complete part header map is part of your API contract.
    pub headers: Option<axum::http::HeaderMap>,
}

impl FieldMetadata {
    /// Return the captured full multipart part headers, if they were collected.
    #[must_use]
    pub const fn headers(&self) -> Option<&axum::http::HeaderMap> {
        self.headers.as_ref()
    }

    /// Attach a full header snapshot to existing metadata.
    #[must_use]
    pub fn with_headers(mut self, headers: axum::http::HeaderMap) -> Self {
        self.headers = Some(headers);
        self
    }
}

impl From<&Field<'_>> for FieldMetadata {
    fn from(field: &Field<'_>) -> Self {
        Self {
            name: field.name().map(String::from),
            file_name: field.file_name().map(String::from),
            content_type: field.content_type().map(String::from),
            headers: None,
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// FieldData<T>
// ═══════════════════════════════════════════════════════════════════════════════

/// A multipart field's parsed contents along with its metadata.
///
/// Use this wrapper when you need access to the file name, content type,
/// or other headers alongside the parsed value.
///
/// ```rust,ignore
/// use vespera::multipart::FieldData;
/// use tempfile::NamedTempFile;
///
/// #[derive(Multipart, Schema)]
/// pub struct Upload {
///     pub file: FieldData<NamedTempFile>,
/// }
/// ```
#[derive(Debug)]
pub struct FieldData<T> {
    /// Metadata about the field (name, filename, content-type, headers).
    pub metadata: FieldMetadata,
    /// The parsed contents of the field.
    pub contents: T,
}

impl<T, S> TryFromFieldWithState<S> for FieldData<T>
where
    T: TryFromFieldWithState<S> + Send,
    S: Send + Sync,
{
    async fn try_from_field_with_state(
        field: MeteredField<'_>,
        limit_bytes: Option<usize>,
        state: &S,
    ) -> Result<Self, TypedMultipartError> {
        let metadata = FieldMetadata::from(&field);
        let contents = T::try_from_field_with_state(field, limit_bytes, state).await?;
        Ok(Self { metadata, contents })
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// TypedMultipart<T> extractor
// ═══════════════════════════════════════════════════════════════════════════════

/// Axum extractor for typed multipart form data.
///
/// Wraps a struct `T` that implements [`TryFromMultipartWithState`] (typically
/// via `#[derive(Multipart)]`).
///
/// ```rust,ignore
/// use vespera::multipart::{TypedMultipart, FieldData};
/// use tempfile::NamedTempFile;
///
/// #[derive(Multipart, Schema)]
/// pub struct UploadRequest {
///     pub name: String,
///     pub file: FieldData<NamedTempFile>,
/// }
///
/// #[vespera::route(post)]
/// pub async fn upload(
///     TypedMultipart(req): TypedMultipart<UploadRequest>,
/// ) -> Json<String> {
///     Json(req.name)
/// }
/// ```
pub struct TypedMultipart<T>(pub T);

impl<T> std::ops::Deref for TypedMultipart<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> std::ops::DerefMut for TypedMultipart<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

/// Default aggregate cap for a typed multipart request body.
///
/// Sized as a **bounded safety budget**, not a generous allowance: it is the
/// guard that still applies when applications disable or raise axum's
/// [`DefaultBodyLimit`](axum::extract::DefaultBodyLimit) (notably the
/// in-process / JNI upload path, where axum's HTTP-layer limit never runs). At
/// 64 MiB a single request can no longer pin hundreds of MiB of buffered text
/// fields / temp-file I/O — the practical DoS budget the previous 512 MiB
/// default handed every caller. Applications that legitimately accept larger
/// typed uploads opt in explicitly via [`TypedMultipartWithLimits`] or
/// [`set_default_multipart_limits`]; genuinely large payloads should stream.
pub const DEFAULT_MULTIPART_MAX_TOTAL_BYTES: usize = 64 * 1024 * 1024; // 64 MiB

/// Default maximum number of parts in a typed multipart request.
pub const DEFAULT_MULTIPART_MAX_FIELDS: usize = 1024;

static DEFAULT_MULTIPART_TOTAL_LIMIT: AtomicUsize =
    AtomicUsize::new(DEFAULT_MULTIPART_MAX_TOTAL_BYTES);
static DEFAULT_MULTIPART_FIELD_LIMIT: AtomicUsize = AtomicUsize::new(DEFAULT_MULTIPART_MAX_FIELDS);

/// Aggregate resource policy for [`TypedMultipart`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MultipartLimits {
    /// Maximum cumulative bytes accepted across all parsed fields.
    pub max_total_bytes: usize,
    /// Maximum number of parsed fields accepted in one request.
    pub max_fields: usize,
}

impl MultipartLimits {
    /// Construct an aggregate multipart policy.
    #[must_use]
    pub const fn new(max_total_bytes: usize, max_fields: usize) -> Self {
        Self {
            max_total_bytes,
            max_fields,
        }
    }
}

/// Return the process-wide default aggregate multipart policy.
#[must_use]
pub fn default_multipart_limits() -> MultipartLimits {
    MultipartLimits::new(
        DEFAULT_MULTIPART_TOTAL_LIMIT.load(Ordering::Relaxed),
        DEFAULT_MULTIPART_FIELD_LIMIT.load(Ordering::Relaxed),
    )
}

/// Set the process-wide default aggregate multipart policy.
///
/// Prefer calling this during application startup, before request handling. For
/// per-route policies use [`TypedMultipartWithLimits`], which avoids global
/// process state and is therefore safer in tests and multi-tenant apps.
pub fn set_default_multipart_limits(limits: MultipartLimits) -> MultipartLimits {
    MultipartLimits::new(
        DEFAULT_MULTIPART_TOTAL_LIMIT.swap(limits.max_total_bytes, Ordering::Relaxed),
        DEFAULT_MULTIPART_FIELD_LIMIT.swap(limits.max_fields, Ordering::Relaxed),
    )
}

#[derive(Debug)]
struct MultipartAggregateState {
    limits: MultipartLimits,
    total_bytes: usize,
    fields: usize,
}

impl MultipartAggregateState {
    const fn new(limits: MultipartLimits) -> Self {
        Self {
            limits,
            total_bytes: 0,
            fields: 0,
        }
    }
}

tokio::task_local! {
    static MULTIPART_AGGREGATE: RefCell<MultipartAggregateState>;
}

/// Count one multipart PART against the request-wide `max_fields` limit.
///
/// Invoked by the derived `TryFromMultipart` loop **once per wire part** —
/// before the field name is resolved — so EVERY part (known, unknown, or
/// nameless) is counted exactly once.  Counting inside the per-known-field
/// parsers instead let unknown parts in non-strict mode (the `_ => {}`
/// dispatch arm) slip past the cap entirely, so a request with thousands of
/// unknown parts could burn unbounded parser/boundary-scan work without ever
/// tripping `TooManyFields`.
pub fn register_multipart_part() -> Result<(), TypedMultipartError> {
    MULTIPART_AGGREGATE
        .try_with(|state| {
            let mut state = state.borrow_mut();
            state.fields = state.fields.saturating_add(1);
            if state.fields > state.limits.max_fields {
                return Err(TypedMultipartError::TooManyFields {
                    limit_fields: state.limits.max_fields,
                });
            }
            Ok(())
        })
        // The derived impl can be unit-tested outside the extractor scope; with
        // no request aggregate present, counting no-ops rather than failing.
        .unwrap_or(Ok(()))
}

/// Count `chunk_len` bytes of one multipart field against the request-wide
/// `max_total_bytes` aggregate limit, returning [`TypedMultipartError::RequestTooLarge`]
/// once the running total crosses the cap.
///
/// The public counterpart of [`register_multipart_part`] for the **byte**
/// dimension of [`MultipartLimits`]. Vespera's built-in field parsers
/// ([`read_field_data`] / the `NamedTempFile` path) already call this once per
/// `field.chunk()`, so typed multipart structs are accounted automatically.
///
/// Built-in field parsers — and any **custom [`TryFromFieldWithState`]**
/// implementation — read a field's bytes through [`MeteredField::chunk`] /
/// [`MeteredField::bytes`], which call this automatically once per chunk. A
/// custom parser therefore **cannot** bypass the aggregate cap: [`MeteredField`]
/// owns the only access to the field's bytes (the raw axum [`Field`] is never
/// exposed), so every byte is counted regardless of how the parser is written.
/// The per-field `limit_bytes` passed to the trait method still bounds that one
/// field; this call enforces the request-wide total. Mirrors the cooperative
/// contract of [`register_multipart_part`]: outside the extractor's task-local
/// scope (e.g. a direct unit test of a derived parser) it no-ops rather than
/// failing.
pub fn register_multipart_bytes(
    field_name: &str,
    chunk_len: usize,
) -> Result<(), TypedMultipartError> {
    MULTIPART_AGGREGATE
        .try_with(|state| {
            let mut state = state.borrow_mut();
            state.total_bytes = state.total_bytes.saturating_add(chunk_len);
            if state.total_bytes > state.limits.max_total_bytes {
                return Err(TypedMultipartError::RequestTooLarge {
                    field_name: field_name.to_owned(),
                    limit_bytes: state.limits.max_total_bytes,
                });
            }
            Ok(())
        })
        .unwrap_or(Ok(()))
}

/// Axum extractor variant with const aggregate multipart limits.
///
/// Use this when a route needs a tighter or looser request-level policy than
/// the process default. Per-field `#[form_data(limit = "...")]` caps still
/// apply independently: the effective policy is whichever per-field or
/// aggregate limit is exceeded first.
pub struct TypedMultipartWithLimits<
    T,
    const MAX_TOTAL_BYTES: usize,
    const MAX_FIELDS: usize = DEFAULT_MULTIPART_MAX_FIELDS,
>(pub T);

async fn parse_typed_multipart_with_limits<T, S>(
    req: Request,
    state: &S,
    limits: MultipartLimits,
) -> Result<T, TypedMultipartError>
where
    T: TryFromMultipartWithState<S>,
    S: Send + Sync + 'static,
{
    let mut multipart = axum::extract::Multipart::from_request(req, state)
        .await
        .map_err(TypedMultipartError::from)?;
    MULTIPART_AGGREGATE
        .scope(
            RefCell::new(MultipartAggregateState::new(limits)),
            async move { T::try_from_multipart_with_state(&mut multipart, state).await },
        )
        .await
}

impl<T, S> FromRequest<S> for TypedMultipart<T>
where
    T: TryFromMultipartWithState<S>,
    S: Send + Sync + 'static,
{
    type Rejection = TypedMultipartError;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let value =
            parse_typed_multipart_with_limits(req, state, default_multipart_limits()).await?;
        Ok(Self(value))
    }
}

impl<T, S, const MAX_TOTAL_BYTES: usize, const MAX_FIELDS: usize> FromRequest<S>
    for TypedMultipartWithLimits<T, MAX_TOTAL_BYTES, MAX_FIELDS>
where
    T: TryFromMultipartWithState<S>,
    S: Send + Sync + 'static,
{
    type Rejection = TypedMultipartError;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let value = parse_typed_multipart_with_limits(
            req,
            state,
            MultipartLimits::new(MAX_TOTAL_BYTES, MAX_FIELDS),
        )
        .await?;
        Ok(Self(value))
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// Built-in TryFromFieldWithState implementations
// ═══════════════════════════════════════════════════════════════════════════════

// ─── Helpers ────────────────────────────────────────────────────────────────

/// Read all bytes from a multipart field into an owned `Vec<u8>`,
/// enforcing an optional size limit.
///
/// Bytes are accumulated chunk-by-chunk directly into the returned
/// `Vec` — the same buffer `String::from_utf8` later reuses without a
/// copy.  This deliberately avoids the previous
/// `field.bytes().await?.to_vec()` on the unlimited path, which built
/// an owned `Bytes` and then copied it into a *second* allocation,
/// doubling peak memory for large text/scalar fields.  (Returning
/// `Bytes` instead would only shift that second copy onto the `String`
/// parser, so direct `Vec` accumulation is the allocation-minimal
/// shape for every current caller.)
///
/// When a limit is set the cumulative size is checked after each chunk
/// and an over-limit chunk is rejected *before* it is copied in.
struct FieldBytes<'a> {
    field: MeteredField<'a>,
    data: Vec<u8>,
}

async fn read_field_data(
    mut field: MeteredField<'_>,
    limit: Option<usize>,
    initial_capacity: usize,
) -> Result<FieldBytes<'_>, TypedMultipartError> {
    // Part counting now happens once per part in the derived loop
    // (`register_multipart_part`), so the field parsers no longer count.
    // Initial capacity is independent from the hard byte limit: tiny scalar
    // fields keep the 256B cap without preallocating 256B per bool/number.
    let buf = field
        .bytes_with_limit_inner(limit, initial_capacity)
        .await?;
    Ok(FieldBytes { field, data: buf })
}

/// Default cap for tiny scalar multipart fields when no explicit
/// `#[form_data(limit = "...")]` is supplied. 256 bytes is far beyond any
/// legitimate bool/number/char payload while preventing unbounded buffering.
const DEFAULT_TINY_SCALAR_LIMIT_BYTES: usize = 256;
const TINY_SCALAR_INITIAL_CAPACITY_BYTES: usize = 16;
const STRING_INITIAL_CAPACITY_BYTES: usize = 64;

/// Resolve the buffering cap for a tiny scalar field: the explicit
/// per-field `#[form_data(limit = "...")]` if present, otherwise the
/// conservative [`DEFAULT_TINY_SCALAR_LIMIT_BYTES`] default.  A cap is
/// always applied — scalars never buffer unbounded input.
fn tiny_scalar_limit(limit_bytes: Option<usize>) -> usize {
    limit_bytes.unwrap_or(DEFAULT_TINY_SCALAR_LIMIT_BYTES)
}

/// Parse a string as a boolean using clap-style conventions.
///
/// Surrounding ASCII whitespace is ignored, so a multipart text value that
/// arrives with incidental padding (e.g. a trailing newline) parses like the
/// trimmed token — matching the numeric field impls, which `text.trim().parse()`.
///
/// Accepted truthy values: `true`, `yes`, `y`, `1`, `on`
/// Accepted falsy  values: `false`, `no`, `n`, `0`, `off`
fn str_to_bool(s: &str) -> Option<bool> {
    const TRUTHY: [&str; 5] = ["true", "yes", "y", "1", "on"];
    const FALSY: [&str; 5] = ["false", "no", "n", "0", "off"];
    let s = s.trim();
    if TRUTHY.iter().any(|t| s.eq_ignore_ascii_case(t)) {
        Some(true)
    } else if FALSY.iter().any(|f| s.eq_ignore_ascii_case(f)) {
        Some(false)
    } else {
        None
    }
}

// ─── String ─────────────────────────────────────────────────────────────────

/// Default buffering cap for an **unannotated** `String` multipart field.
///
/// Generous enough for any realistic text field (form text, JSON blobs,
/// small base64) yet converts the former *unbounded* accumulation into a
/// bounded one — closing a per-request memory-exhaustion vector where a
/// client could stream gigabytes into a single text field.  Opt out per
/// field with `#[form_data(limit = "unlimited")]`, or raise / lower it with
/// an explicit `#[form_data(limit = "...")]`.
const DEFAULT_STRING_FIELD_LIMIT_BYTES: usize = 1024 * 1024; // 1 MiB

/// Default streaming cap for an **unannotated** `NamedTempFile` multipart field.
///
/// The cap is intentionally larger than text fields: unannotated temp-file uploads
/// are real file uploads, but still need a denial-of-service guard by default.
/// Explicit `#[form_data(limit = "unlimited")]` continues to opt out by passing
/// `usize::MAX` through the derive-generated parser. Applications can tune the
/// process-wide default before handling requests with
/// [`set_default_temp_file_field_limit_bytes`].
///
/// Note: `"unlimited"` lifts only this **per-field** cap. The request-wide
/// aggregate budget ([`DEFAULT_MULTIPART_MAX_TOTAL_BYTES`], 64 MiB by default)
/// still applies, so a single `"unlimited"` field is bounded by the aggregate
/// rather than being truly unbounded. To raise the aggregate, use
/// [`TypedMultipartWithLimits`] (per-route) or [`set_default_multipart_limits`]
/// (process-wide); genuinely large uploads should stream instead.
pub const DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES: usize = 16 * 1024 * 1024; // 16 MiB

static DEFAULT_TEMP_FILE_FIELD_LIMIT: AtomicUsize =
    AtomicUsize::new(DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES);

/// Return the current process-wide default cap for unannotated `NamedTempFile` fields.
#[must_use]
pub fn default_temp_file_field_limit_bytes() -> usize {
    DEFAULT_TEMP_FILE_FIELD_LIMIT.load(Ordering::Relaxed)
}

/// Set the process-wide default cap for unannotated `NamedTempFile` fields.
///
/// Call this during application startup, before request handling begins. Per-field
/// `#[form_data(limit = "...")]` annotations still take precedence, including the
/// explicit `"unlimited"` opt-out. The previous cap is returned to support tests or
/// embedders that need to restore their process setting.
pub fn set_default_temp_file_field_limit_bytes(limit_bytes: usize) -> usize {
    DEFAULT_TEMP_FILE_FIELD_LIMIT.swap(limit_bytes, Ordering::Relaxed)
}

// Scalar field parsers (`String`, `bool`, integers/floats, `char`) live in a
// sidecar module so `multipart.rs` stays within the 1000-line source cap.
mod scalar_parsers;

// ─── NamedTempFile ──────────────────────────────────────────────────────────

impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
    async fn try_from_field_with_state(
        mut field: MeteredField<'_>,
        limit_bytes: Option<usize>,
        _state: &S,
    ) -> Result<Self, TypedMultipartError> {
        // Part counting happens once per part in the derived loop
        // (`register_multipart_part`); the temp-file parser no longer counts.
        // Temp-file creation AND reopen() are both blocking syscalls —
        // run them together on the blocking pool so neither stalls the
        // async worker (the reopen previously ran inline on the async
        // task).  `NamedTempFile` (not `tokio::fs::File`) is retained so
        // cleanup-on-drop semantics survive; the reopened std handle is
        // wrapped in `tokio::fs` below so large writes also route to the
        // blocking pool.  `temp` keeps ownership of the path + delete-on-
        // drop guard.
        let (temp, std_file) = tokio::task::spawn_blocking(|| {
            let temp = Self::new()?;
            let std_file = temp.reopen()?;
            Ok::<_, std::io::Error>((temp, std_file))
        })
        .await
        .map_err(|e| TypedMultipartError::Other {
            source: e.to_string(),
        })?
        .map_err(|e| TypedMultipartError::Other {
            source: e.to_string(),
        })?;
        let mut file = tokio::fs::File::from_std(std_file);

        let limit_bytes = limit_bytes.unwrap_or_else(default_temp_file_field_limit_bytes);
        let mut total = 0usize;
        while let Some(chunk) = field.chunk().await? {
            // `MeteredField::chunk` already counts the chunk against the
            // request-wide `max_total_bytes` aggregate cap (no double-count).
            // `saturating_add` (matching `read_field_data`) prevents a
            // pathological chunk size from wrapping `total` and slipping
            // past the limit check below.
            total = total.saturating_add(chunk.len());
            if total > limit_bytes {
                return Err(TypedMultipartError::FieldTooLarge {
                    field_name: field.name().unwrap_or_default().to_string(),
                    limit_bytes,
                });
            }
            tokio::io::AsyncWriteExt::write_all(&mut file, &chunk)
                .await
                .map_err(|e| TypedMultipartError::Other {
                    source: e.to_string(),
                })?;
        }
        tokio::io::AsyncWriteExt::flush(&mut file)
            .await
            .map_err(|e| TypedMultipartError::Other {
                source: e.to_string(),
            })?;

        Ok(temp)
    }
}

#[cfg(test)]
mod tests;