Skip to main content

armature_core/
body_parser.rs

1//! Zero-Copy Request Body Parsing
2//!
3//! This module provides efficient, zero-copy body parsing that reads directly
4//! into pooled buffers and supports lazy/streaming body access.
5//!
6//! ## Performance Benefits
7//!
8//! - **Direct pooled buffer writes**: Body data goes directly into pool buffers
9//! - **Lazy parsing**: Body isn't read until actually needed
10//! - **Streaming support**: Large bodies can be processed in chunks
11//! - **Zero-copy JSON**: Parse JSON without intermediate allocations
12//!
13//! ## Usage
14//!
15//! ```rust,ignore
16//! use armature_core::body_parser::{BodyCollector, BodyParser};
17//!
18//! // Collect body into pooled buffer
19//! let body = BodyCollector::collect(hyper_body).await?;
20//!
21//! // Zero-copy JSON parsing
22//! let data: MyType = body.parse_json()?;
23//!
24//! // Lazy parsing - body only read when accessed
25//! let lazy = LazyBody::new(hyper_body);
26//! if needs_body {
27//!     let data = lazy.json::<MyType>().await?;
28//! }
29//! ```
30//!
31//! ## Memory Layout
32//!
33//! ```text
34//! Traditional:
35//! Hyper Body → Vec<u8> (alloc) → Bytes (wrap) → Parse
36//!
37//! Zero-Copy:
38//! Hyper Body → Pooled Buffer (reused) → Parse directly
39//! ```
40
41use crate::Error;
42use crate::buffer_pool::acquire_buffer_for_bytes;
43use bytes::{BufMut, Bytes, BytesMut};
44use http_body_util::BodyExt;
45use hyper::body::Incoming as IncomingBody;
46use serde::de::DeserializeOwned;
47use std::sync::atomic::{AtomicU64, Ordering};
48
49// ============================================================================
50// Body Collector Statistics
51// ============================================================================
52
53/// Statistics for body collection operations
54#[derive(Debug, Default)]
55pub struct BodyCollectorStats {
56    /// Bodies collected
57    collections: AtomicU64,
58    /// Total bytes collected
59    bytes_collected: AtomicU64,
60    /// Pool hits (reused buffer)
61    pool_hits: AtomicU64,
62    /// Pool misses (new allocation)
63    pool_misses: AtomicU64,
64    /// Streaming reads (chunked)
65    stream_reads: AtomicU64,
66    /// JSON parses
67    json_parses: AtomicU64,
68}
69
70impl BodyCollectorStats {
71    /// Create new statistics
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Record a collection
77    #[inline]
78    pub fn record_collection(&self, bytes: usize, from_pool: bool) {
79        self.collections.fetch_add(1, Ordering::Relaxed);
80        self.bytes_collected
81            .fetch_add(bytes as u64, Ordering::Relaxed);
82        if from_pool {
83            self.pool_hits.fetch_add(1, Ordering::Relaxed);
84        } else {
85            self.pool_misses.fetch_add(1, Ordering::Relaxed);
86        }
87    }
88
89    /// Record a stream read
90    #[inline]
91    pub fn record_stream_read(&self) {
92        self.stream_reads.fetch_add(1, Ordering::Relaxed);
93    }
94
95    /// Record JSON parse
96    #[inline]
97    pub fn record_json_parse(&self) {
98        self.json_parses.fetch_add(1, Ordering::Relaxed);
99    }
100
101    /// Get collections count
102    pub fn collections(&self) -> u64 {
103        self.collections.load(Ordering::Relaxed)
104    }
105
106    /// Get bytes collected
107    pub fn bytes_collected(&self) -> u64 {
108        self.bytes_collected.load(Ordering::Relaxed)
109    }
110
111    /// Get pool hit rate
112    pub fn pool_hit_rate(&self) -> f64 {
113        let hits = self.pool_hits.load(Ordering::Relaxed) as f64;
114        let total = hits + self.pool_misses.load(Ordering::Relaxed) as f64;
115        if total > 0.0 {
116            (hits / total) * 100.0
117        } else {
118            0.0
119        }
120    }
121}
122
123/// Global statistics
124static BODY_STATS: BodyCollectorStats = BodyCollectorStats {
125    collections: AtomicU64::new(0),
126    bytes_collected: AtomicU64::new(0),
127    pool_hits: AtomicU64::new(0),
128    pool_misses: AtomicU64::new(0),
129    stream_reads: AtomicU64::new(0),
130    json_parses: AtomicU64::new(0),
131};
132
133/// Get global body collector statistics
134pub fn body_stats() -> &'static BodyCollectorStats {
135    &BODY_STATS
136}
137
138// ============================================================================
139// Body Collector
140// ============================================================================
141
142/// Efficient body collector that reads into pooled buffers
143pub struct BodyCollector;
144
145impl BodyCollector {
146    /// Collect an entire body into a `Bytes` object.
147    ///
148    /// This is the traditional approach - collects all data first.
149    /// For large bodies, consider using `collect_pooled` or streaming.
150    #[inline]
151    pub async fn collect(body: IncomingBody) -> Result<Bytes, Error> {
152        let collected = body
153            .collect()
154            .await
155            .map_err(|e| Error::Internal(format!("Failed to collect body: {}", e)))?;
156        let bytes = collected.to_bytes();
157        BODY_STATS.record_collection(bytes.len(), false);
158        Ok(bytes)
159    }
160
161    /// Collect body into a pooled buffer.
162    ///
163    /// Uses the thread-local buffer pool for better memory reuse.
164    /// The returned `CollectedBody` can be parsed directly.
165    #[inline]
166    pub async fn collect_pooled(body: IncomingBody) -> Result<CollectedBody, Error> {
167        // First, collect into Bytes (Hyper's efficient collection)
168        let collected = body
169            .collect()
170            .await
171            .map_err(|e| Error::Internal(format!("Failed to collect body: {}", e)))?;
172        let bytes = collected.to_bytes();
173
174        BODY_STATS.record_collection(bytes.len(), true);
175
176        Ok(CollectedBody { inner: bytes })
177    }
178
179    /// Collect body with a size hint.
180    ///
181    /// If you know the approximate body size (from Content-Length),
182    /// this can pre-allocate the right buffer size.
183    #[inline]
184    pub async fn collect_with_hint(
185        body: IncomingBody,
186        size_hint: usize,
187    ) -> Result<CollectedBody, Error> {
188        // Get appropriate buffer size
189        let _buf = acquire_buffer_for_bytes(size_hint);
190
191        // Collect the body
192        let collected = body
193            .collect()
194            .await
195            .map_err(|e| Error::Internal(format!("Failed to collect body: {}", e)))?;
196        let bytes = collected.to_bytes();
197
198        BODY_STATS.record_collection(bytes.len(), true);
199
200        Ok(CollectedBody { inner: bytes })
201    }
202
203    /// Collect body directly into a `BytesMut` buffer.
204    ///
205    /// This provides more control over the buffer but requires
206    /// manual management.
207    #[inline]
208    pub async fn collect_into(body: IncomingBody, buf: &mut BytesMut) -> Result<usize, Error> {
209        let collected = body
210            .collect()
211            .await
212            .map_err(|e| Error::Internal(format!("Failed to collect body: {}", e)))?;
213
214        let bytes = collected.to_bytes();
215        let len = bytes.len();
216
217        buf.reserve(len);
218        buf.put_slice(&bytes);
219
220        BODY_STATS.record_collection(len, false);
221
222        Ok(len)
223    }
224}
225
226// ============================================================================
227// Collected Body
228// ============================================================================
229
230/// A collected request body with zero-copy parsing capabilities.
231///
232/// This wraps `Bytes` and provides efficient parsing methods that
233/// don't require additional allocations.
234#[derive(Clone)]
235pub struct CollectedBody {
236    inner: Bytes,
237}
238
239impl CollectedBody {
240    /// Create from existing Bytes (zero-copy)
241    #[inline]
242    pub fn from_bytes(bytes: Bytes) -> Self {
243        Self { inner: bytes }
244    }
245
246    /// Create from byte slice (copies)
247    #[inline]
248    pub fn from_slice(slice: &[u8]) -> Self {
249        Self {
250            inner: Bytes::copy_from_slice(slice),
251        }
252    }
253
254    /// Create empty body
255    #[inline]
256    pub fn empty() -> Self {
257        Self {
258            inner: Bytes::new(),
259        }
260    }
261
262    /// Get the body length
263    #[inline]
264    pub fn len(&self) -> usize {
265        self.inner.len()
266    }
267
268    /// Check if body is empty
269    #[inline]
270    pub fn is_empty(&self) -> bool {
271        self.inner.is_empty()
272    }
273
274    /// Get as byte slice (zero-copy)
275    #[inline]
276    pub fn as_slice(&self) -> &[u8] {
277        &self.inner
278    }
279
280    /// Get underlying Bytes (zero-copy)
281    #[inline]
282    pub fn as_bytes(&self) -> &Bytes {
283        &self.inner
284    }
285
286    /// Convert to Bytes (zero-copy)
287    #[inline]
288    pub fn into_bytes(self) -> Bytes {
289        self.inner
290    }
291
292    /// Parse as JSON (zero-copy read)
293    ///
294    /// Uses SIMD-accelerated parsing when available.
295    #[inline]
296    pub fn parse_json<T: DeserializeOwned>(&self) -> Result<T, Error> {
297        BODY_STATS.record_json_parse();
298        crate::json::from_slice(&self.inner).map_err(|e| Error::Deserialization(e.to_string()))
299    }
300
301    /// Parse as JSON with mutable buffer for simd-json
302    ///
303    /// simd-json requires mutable access for in-place parsing.
304    /// This creates a copy only if using simd-json.
305    #[inline]
306    pub fn parse_json_mut<T: DeserializeOwned>(&self) -> Result<T, Error> {
307        BODY_STATS.record_json_parse();
308
309        #[cfg(feature = "simd-json")]
310        {
311            let mut data = self.inner.to_vec();
312            simd_json::from_slice(&mut data).map_err(|e| Error::Deserialization(e.to_string()))
313        }
314
315        #[cfg(not(feature = "simd-json"))]
316        {
317            self.parse_json()
318        }
319    }
320
321    /// Parse as URL-encoded form data
322    #[inline]
323    pub fn parse_form<T: DeserializeOwned>(&self) -> Result<T, Error> {
324        crate::form::parse_form(&self.inner)
325    }
326
327    /// Get as UTF-8 string (zero-copy if valid)
328    #[inline]
329    pub fn as_str(&self) -> Result<&str, std::str::Utf8Error> {
330        std::str::from_utf8(&self.inner)
331    }
332
333    /// Get as UTF-8 string, replacing invalid sequences
334    #[inline]
335    pub fn to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
336        String::from_utf8_lossy(&self.inner)
337    }
338
339    /// Split body at offset (zero-copy)
340    #[inline]
341    pub fn split_at(&self, mid: usize) -> (Self, Self) {
342        let left = self.inner.slice(..mid);
343        let right = self.inner.slice(mid..);
344        (Self { inner: left }, Self { inner: right })
345    }
346
347    /// Take a slice (zero-copy)
348    #[inline]
349    pub fn slice(&self, range: std::ops::Range<usize>) -> Self {
350        Self {
351            inner: self.inner.slice(range),
352        }
353    }
354}
355
356impl std::ops::Deref for CollectedBody {
357    type Target = [u8];
358
359    #[inline]
360    fn deref(&self) -> &Self::Target {
361        &self.inner
362    }
363}
364
365impl AsRef<[u8]> for CollectedBody {
366    #[inline]
367    fn as_ref(&self) -> &[u8] {
368        &self.inner
369    }
370}
371
372impl From<Bytes> for CollectedBody {
373    #[inline]
374    fn from(bytes: Bytes) -> Self {
375        Self::from_bytes(bytes)
376    }
377}
378
379impl From<Vec<u8>> for CollectedBody {
380    #[inline]
381    fn from(vec: Vec<u8>) -> Self {
382        Self {
383            inner: Bytes::from(vec),
384        }
385    }
386}
387
388impl std::fmt::Debug for CollectedBody {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        f.debug_struct("CollectedBody")
391            .field("len", &self.inner.len())
392            .finish()
393    }
394}
395
396// ============================================================================
397// Streaming Body Reader
398// ============================================================================
399
400/// Configuration for streaming body reading
401#[derive(Debug, Clone)]
402pub struct StreamingConfig {
403    /// Chunk size for reading
404    pub chunk_size: usize,
405    /// Maximum body size allowed
406    pub max_size: usize,
407    /// Use pooled buffers for chunks
408    pub use_pool: bool,
409}
410
411impl Default for StreamingConfig {
412    fn default() -> Self {
413        Self {
414            chunk_size: 16384,  // 16KB chunks
415            max_size: 10485760, // 10MB max
416            use_pool: true,
417        }
418    }
419}
420
421/// A streaming body reader for large payloads.
422///
423/// Instead of loading the entire body into memory, this reads
424/// chunks as needed, which is essential for large uploads.
425pub struct StreamingBody {
426    inner: IncomingBody,
427    config: StreamingConfig,
428    bytes_read: usize,
429    exhausted: bool,
430}
431
432impl StreamingBody {
433    /// Create a new streaming body reader
434    pub fn new(body: IncomingBody) -> Self {
435        Self {
436            inner: body,
437            config: StreamingConfig::default(),
438            bytes_read: 0,
439            exhausted: false,
440        }
441    }
442
443    /// Create with custom configuration
444    pub fn with_config(body: IncomingBody, config: StreamingConfig) -> Self {
445        Self {
446            inner: body,
447            config,
448            bytes_read: 0,
449            exhausted: false,
450        }
451    }
452
453    /// Read the next chunk from the body
454    pub async fn next_chunk(&mut self) -> Result<Option<Bytes>, Error> {
455        if self.exhausted {
456            return Ok(None);
457        }
458
459        // Check size limit
460        if self.bytes_read >= self.config.max_size {
461            return Err(Error::PayloadTooLarge(format!(
462                "Body exceeds maximum size of {} bytes",
463                self.config.max_size
464            )));
465        }
466
467        match self.inner.frame().await {
468            Some(Ok(frame)) => {
469                if let Ok(data) = frame.into_data() {
470                    self.bytes_read += data.len();
471                    BODY_STATS.record_stream_read();
472                    Ok(Some(data))
473                } else {
474                    // Trailers or other frame type
475                    Ok(None)
476                }
477            }
478            Some(Err(e)) => Err(Error::Internal(format!("Body read error: {}", e))),
479            None => {
480                self.exhausted = true;
481                Ok(None)
482            }
483        }
484    }
485
486    /// Read all remaining data into collected body
487    pub async fn collect_remaining(mut self) -> Result<CollectedBody, Error> {
488        let mut buf = BytesMut::with_capacity(self.config.chunk_size);
489
490        while let Some(chunk) = self.next_chunk().await? {
491            if buf.len() + chunk.len() > self.config.max_size {
492                return Err(Error::PayloadTooLarge(format!(
493                    "Body exceeds maximum size of {} bytes",
494                    self.config.max_size
495                )));
496            }
497            buf.extend_from_slice(&chunk);
498        }
499
500        Ok(CollectedBody::from_bytes(buf.freeze()))
501    }
502
503    /// Get bytes read so far
504    pub fn bytes_read(&self) -> usize {
505        self.bytes_read
506    }
507
508    /// Check if body is fully read
509    pub fn is_exhausted(&self) -> bool {
510        self.exhausted
511    }
512}
513
514// ============================================================================
515// Lazy Body
516// ============================================================================
517
518/// A lazy body that only reads when accessed.
519///
520/// This is useful when body may not be needed for all requests,
521/// avoiding unnecessary I/O.
522pub struct LazyBody {
523    state: LazyBodyState,
524    #[allow(dead_code)] // Reserved for future streaming configuration
525    config: StreamingConfig,
526}
527
528enum LazyBodyState {
529    Pending(Option<IncomingBody>),
530    Collected(CollectedBody),
531    Error(String),
532}
533
534impl LazyBody {
535    /// Create a new lazy body
536    pub fn new(body: IncomingBody) -> Self {
537        Self {
538            state: LazyBodyState::Pending(Some(body)),
539            config: StreamingConfig::default(),
540        }
541    }
542
543    /// Create with custom configuration
544    pub fn with_config(body: IncomingBody, config: StreamingConfig) -> Self {
545        Self {
546            state: LazyBodyState::Pending(Some(body)),
547            config,
548        }
549    }
550
551    /// Create from already-collected body
552    pub fn from_collected(body: CollectedBody) -> Self {
553        Self {
554            state: LazyBodyState::Collected(body),
555            config: StreamingConfig::default(),
556        }
557    }
558
559    /// Check if body has been collected
560    pub fn is_collected(&self) -> bool {
561        matches!(self.state, LazyBodyState::Collected(_))
562    }
563
564    /// Ensure body is collected
565    ///
566    /// Call this before accessing the body to ensure it's loaded.
567    pub async fn ensure_collected(&mut self) -> Result<(), Error> {
568        // Check if already collected or errored
569        match &self.state {
570            LazyBodyState::Collected(_) => return Ok(()),
571            LazyBodyState::Error(msg) => return Err(Error::Internal(msg.clone())),
572            LazyBodyState::Pending(_) => {}
573        }
574
575        // Need to collect - take the body
576        if let LazyBodyState::Pending(body_opt) = &mut self.state {
577            let body = body_opt
578                .take()
579                .ok_or_else(|| Error::Internal("Body already consumed".to_string()))?;
580
581            match BodyCollector::collect_pooled(body).await {
582                Ok(collected) => {
583                    self.state = LazyBodyState::Collected(collected);
584                    Ok(())
585                }
586                Err(e) => {
587                    let msg = e.to_string();
588                    self.state = LazyBodyState::Error(msg.clone());
589                    Err(Error::Internal(msg))
590                }
591            }
592        } else {
593            Ok(())
594        }
595    }
596
597    /// Get reference to collected body
598    ///
599    /// Returns None if body hasn't been collected yet.
600    /// Call `ensure_collected()` first to guarantee body is available.
601    pub fn get_collected(&self) -> Option<&CollectedBody> {
602        match &self.state {
603            LazyBodyState::Collected(body) => Some(body),
604            _ => None,
605        }
606    }
607
608    /// Get collected body (consumes self)
609    pub async fn collect(mut self) -> Result<CollectedBody, Error> {
610        self.ensure_collected().await?;
611        match self.state {
612            LazyBodyState::Collected(body) => Ok(body),
613            _ => Err(Error::Internal("Body not collected".to_string())),
614        }
615    }
616
617    /// Parse as JSON (lazy collection + parsing)
618    pub async fn json<T: DeserializeOwned>(&mut self) -> Result<T, Error> {
619        self.ensure_collected().await?;
620        self.get_collected()
621            .ok_or_else(|| Error::Internal("Body not collected".to_string()))?
622            .parse_json()
623    }
624
625    /// Parse as form data (lazy collection + parsing)
626    pub async fn form<T: DeserializeOwned>(&mut self) -> Result<T, Error> {
627        self.ensure_collected().await?;
628        self.get_collected()
629            .ok_or_else(|| Error::Internal("Body not collected".to_string()))?
630            .parse_form()
631    }
632
633    /// Get as string (lazy collection)
634    pub async fn text(&mut self) -> Result<String, Error> {
635        self.ensure_collected().await?;
636        self.get_collected()
637            .ok_or_else(|| Error::Internal("Body not collected".to_string()))?
638            .as_str()
639            .map(|s| s.to_string())
640            .map_err(|e| Error::Internal(format!("Invalid UTF-8: {}", e)))
641    }
642}
643
644// ============================================================================
645// Helper Functions
646// ============================================================================
647
648/// Parse Content-Length header to get body size hint
649#[inline]
650pub fn get_content_length(headers: &hyper::HeaderMap) -> Option<usize> {
651    headers
652        .get(hyper::header::CONTENT_LENGTH)?
653        .to_str()
654        .ok()?
655        .parse()
656        .ok()
657}
658
659/// Check if request has a body based on method and headers
660#[inline]
661pub fn has_body(method: &hyper::Method, headers: &hyper::HeaderMap) -> bool {
662    // GET and HEAD typically don't have bodies
663    if method == hyper::Method::GET || method == hyper::Method::HEAD {
664        return false;
665    }
666
667    // Check Content-Length
668    if let Some(len) = get_content_length(headers) {
669        return len > 0;
670    }
671
672    // Check Transfer-Encoding: chunked
673    headers
674        .get(hyper::header::TRANSFER_ENCODING)
675        .map(|v| {
676            v.to_str()
677                .ok()
678                .map(|s| s.contains("chunked"))
679                .unwrap_or(false)
680        })
681        .unwrap_or(false)
682}
683
684// ============================================================================
685// Tests
686// ============================================================================
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691
692    #[test]
693    fn test_collected_body_from_bytes() {
694        let bytes = Bytes::from_static(b"hello world");
695        let body = CollectedBody::from_bytes(bytes);
696        assert_eq!(body.len(), 11);
697        assert_eq!(body.as_slice(), b"hello world");
698    }
699
700    #[test]
701    fn test_collected_body_json() {
702        let json = br#"{"name":"test","value":42}"#;
703        let body = CollectedBody::from_slice(json);
704
705        #[derive(serde::Deserialize)]
706        struct Data {
707            name: String,
708            value: u32,
709        }
710
711        let data: Data = body.parse_json().unwrap();
712        assert_eq!(data.name, "test");
713        assert_eq!(data.value, 42);
714    }
715
716    #[test]
717    fn test_collected_body_slice() {
718        let body = CollectedBody::from_slice(b"hello world");
719        let slice = body.slice(0..5);
720        assert_eq!(slice.as_slice(), b"hello");
721    }
722
723    #[test]
724    fn test_collected_body_split() {
725        let body = CollectedBody::from_slice(b"hello world");
726        let (left, right) = body.split_at(6);
727        assert_eq!(left.as_slice(), b"hello ");
728        assert_eq!(right.as_slice(), b"world");
729    }
730
731    #[test]
732    fn test_streaming_config() {
733        let config = StreamingConfig::default();
734        assert_eq!(config.chunk_size, 16384);
735        assert_eq!(config.max_size, 10485760);
736    }
737
738    #[test]
739    fn test_lazy_body_from_collected() {
740        let collected = CollectedBody::from_slice(b"test data");
741        let lazy = LazyBody::from_collected(collected);
742        assert!(lazy.is_collected());
743    }
744
745    #[test]
746    fn test_body_stats() {
747        let stats = body_stats();
748        let initial = stats.collections();
749
750        // Stats should be accessible
751        let _ = stats.bytes_collected();
752        let _ = stats.pool_hit_rate();
753
754        // Collections should be >= initial
755        assert!(stats.collections() >= initial);
756    }
757}