praxis_filter/body/
buffer.rs1use bytes::Bytes;
7
8pub struct BodyBuffer {
27 chunks: Vec<Bytes>,
29
30 max_bytes: usize,
32
33 total_bytes: usize,
35}
36
37impl BodyBuffer {
38 #[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 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 pub fn total_bytes(&self) -> usize {
71 self.total_bytes
72 }
73
74 #[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#[derive(Debug, thiserror::Error)]
113#[error("body exceeds maximum size: {attempted} bytes attempted, {limit} byte limit")]
114pub struct BodyBufferOverflow {
115 pub attempted: usize,
117
118 pub limit: usize,
120}
121
122#[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}