Skip to main content

praxis_filter/body/
buffer.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Body chunk accumulation and overflow handling.
5
6use bytes::Bytes;
7
8// -----------------------------------------------------------------------------
9// BodyBuffer
10// -----------------------------------------------------------------------------
11
12/// Accumulates body chunks for buffer mode delivery.
13///
14/// ```
15/// use bytes::Bytes;
16/// use praxis_filter::BodyBuffer;
17///
18/// let mut buf = BodyBuffer::new(1024);
19/// assert!(buf.push(Bytes::from_static(b"hello ")).is_ok());
20/// assert!(buf.push(Bytes::from_static(b"world")).is_ok());
21/// assert_eq!(buf.total_bytes(), 11);
22///
23/// let frozen = buf.freeze();
24/// assert_eq!(frozen, Bytes::from_static(b"hello world"));
25/// ```
26pub struct BodyBuffer {
27    /// Accumulated body chunks.
28    chunks: Vec<Bytes>,
29
30    /// Maximum allowed bytes.
31    max_bytes: usize,
32
33    /// Total bytes accumulated so far.
34    total_bytes: usize,
35}
36
37impl BodyBuffer {
38    /// Create a new buffer with the given size limit.
39    #[must_use]
40    pub fn new(max_bytes: usize) -> Self {
41        Self {
42            chunks: Vec::new(),
43            max_bytes,
44            total_bytes: 0,
45        }
46    }
47
48    /// Append a chunk to the buffer.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`BodyBufferOverflow`] if adding this chunk would exceed `max_bytes`.
53    pub fn push(&mut self, chunk: Bytes) -> Result<(), BodyBufferOverflow> {
54        let new_total = self.total_bytes + chunk.len();
55
56        if new_total > self.max_bytes {
57            return Err(BodyBufferOverflow {
58                limit: self.max_bytes,
59                attempted: new_total,
60            });
61        }
62
63        self.total_bytes = new_total;
64        self.chunks.push(chunk);
65
66        Ok(())
67    }
68
69    /// Total bytes accumulated so far.
70    pub fn total_bytes(&self) -> usize {
71        self.total_bytes
72    }
73
74    /// Consume the buffer and return a single contiguous `Bytes`.
75    ///
76    /// # Panics
77    ///
78    /// Never actually panics. Internal `expect` is guarded by a length check.
79    #[expect(clippy::expect_used, reason = "guarded by length check")]
80    pub fn freeze(self) -> Bytes {
81        match self.chunks.len() {
82            0 => Bytes::new(),
83            1 => self.chunks.into_iter().next().expect("length checked"),
84            _ => {
85                let mut combined = Vec::with_capacity(self.total_bytes);
86
87                for chunk in self.chunks {
88                    combined.extend_from_slice(&chunk);
89                }
90
91                Bytes::from(combined)
92            },
93        }
94    }
95}
96
97// -----------------------------------------------------------------------------
98// BodyBufferOverflow
99// -----------------------------------------------------------------------------
100
101/// Error returned when a body buffer exceeds its size limit.
102///
103/// ```
104/// use bytes::Bytes;
105/// use praxis_filter::BodyBuffer;
106///
107/// let mut buf = BodyBuffer::new(5);
108/// let err = buf.push(Bytes::from_static(b"too long")).unwrap_err();
109/// assert_eq!(err.limit, 5);
110/// assert_eq!(err.attempted, 8);
111/// ```
112#[derive(Debug, thiserror::Error)]
113#[error("body exceeds maximum size: {attempted} bytes attempted, {limit} byte limit")]
114pub struct BodyBufferOverflow {
115    /// The size that was attempted.
116    pub attempted: usize,
117
118    /// The configured maximum.
119    pub limit: usize,
120}
121
122// -----------------------------------------------------------------------------
123// Tests
124// -----------------------------------------------------------------------------
125
126#[cfg(test)]
127#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
128#[allow(
129    clippy::unwrap_used,
130    clippy::expect_used,
131    clippy::indexing_slicing,
132    clippy::panic,
133    reason = "tests"
134)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn buffer_empty_freeze_returns_empty_bytes() {
140        let buf = BodyBuffer::new(1024);
141
142        assert_eq!(buf.total_bytes(), 0, "empty buffer should have zero bytes");
143
144        let frozen = buf.freeze();
145
146        assert!(frozen.is_empty(), "freezing empty buffer should yield empty Bytes");
147    }
148
149    #[test]
150    fn buffer_single_chunk_freeze_avoids_copy() {
151        let mut buf = BodyBuffer::new(1024);
152        buf.push(Bytes::from_static(b"hello")).unwrap();
153
154        assert_eq!(buf.total_bytes(), 5, "single chunk should report correct byte count");
155
156        let frozen = buf.freeze();
157
158        assert_eq!(
159            frozen,
160            Bytes::from_static(b"hello"),
161            "single chunk freeze should return exact bytes"
162        );
163    }
164
165    #[test]
166    fn buffer_multiple_chunks_concatenate() {
167        let mut buf = BodyBuffer::new(1024);
168        buf.push(Bytes::from_static(b"hello ")).unwrap();
169        buf.push(Bytes::from_static(b"world")).unwrap();
170
171        assert_eq!(buf.total_bytes(), 11, "multiple chunks should sum byte counts");
172
173        let frozen = buf.freeze();
174
175        assert_eq!(
176            frozen,
177            Bytes::from_static(b"hello world"),
178            "multiple chunks should concatenate on freeze"
179        );
180    }
181
182    #[test]
183    fn buffer_rejects_overflow() {
184        let mut buf = BodyBuffer::new(10);
185        buf.push(Bytes::from_static(b"12345")).unwrap();
186
187        let err = buf.push(Bytes::from_static(b"123456")).unwrap_err();
188
189        assert_eq!(err.limit, 10, "overflow error should report configured limit");
190        assert_eq!(err.attempted, 11, "overflow error should report attempted size");
191    }
192
193    #[test]
194    fn buffer_exact_limit_succeeds() {
195        let mut buf = BodyBuffer::new(10);
196        buf.push(Bytes::from_static(b"12345")).unwrap();
197        buf.push(Bytes::from_static(b"12345")).unwrap();
198
199        assert_eq!(buf.total_bytes(), 10, "exact-limit push should report correct bytes");
200
201        let frozen = buf.freeze();
202
203        assert_eq!(
204            frozen.len(),
205            10,
206            "frozen buffer at exact limit should have correct length"
207        );
208    }
209
210    #[test]
211    fn zero_size_buffer_rejects_nonempty_push() {
212        let mut buf = BodyBuffer::new(0);
213
214        let err = buf.push(Bytes::from_static(b"x")).unwrap_err();
215        assert_eq!(err.limit, 0, "zero-size buffer limit should be 0");
216        assert_eq!(err.attempted, 1, "attempted size should be 1 byte");
217
218        let mut buf2 = BodyBuffer::new(0);
219        buf2.push(Bytes::new()).unwrap();
220        assert_eq!(
221            buf2.total_bytes(),
222            0,
223            "pushing empty bytes into zero-size buffer should succeed"
224        );
225    }
226
227    #[test]
228    fn buffer_overflow_display_message() {
229        let err = BodyBufferOverflow {
230            limit: 100,
231            attempted: 150,
232        };
233
234        assert_eq!(
235            err.to_string(),
236            "body exceeds maximum size: 150 bytes attempted, 100 byte limit",
237            "overflow Display should include limit and attempted size"
238        );
239    }
240}