Skip to main content

eggserve_core/primitives/
body.rs

1//! Safe body source abstraction for response streaming.
2//!
3//! A [`BodySource`] owns the data needed to produce a response body without
4//! reopening filesystem paths. For file-backed variants, the resolver-opened
5//! file handle is carried forward — the service layer converts it to a Hyper
6//! streaming body at response time.
7//!
8//! # Conversion model
9//!
10//! Converting a [`super::secure_root::ResolvedFile`] into a [`BodySource`]
11//! **consumes** the file capability. This prevents accidental double-use:
12//! each resolved file can produce exactly one body source.
13
14use std::fs::File;
15use std::io::{self, Read, Seek, SeekFrom};
16
17use crate::primitives::response::FileRange;
18
19/// Errors that can arise when converting a resolved file into a body source.
20#[derive(Debug)]
21pub enum BodySourceError {
22    /// The requested byte range is invalid for the file size.
23    InvalidRange,
24    /// The resolved file has already been consumed into a body source.
25    AlreadyConsumed,
26}
27
28impl std::fmt::Display for BodySourceError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::InvalidRange => write!(f, "invalid byte range"),
32            Self::AlreadyConsumed => write!(f, "resolved file already consumed"),
33        }
34    }
35}
36
37impl std::error::Error for BodySourceError {}
38
39/// The kind of body a response will carry.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum BodyKind {
42    Empty,
43    Bytes,
44    FileFull,
45    FileRange,
46}
47
48/// A resolved response body that owns its data without reopening paths.
49///
50/// For file-backed variants, the [`File`] was opened during path resolution
51/// (e.g. via `openat(O_NOFOLLOW)` on Unix) and is carried forward here.
52/// The service layer converts it to a Hyper streaming body at response time.
53#[derive(Debug)]
54pub enum BodySource {
55    /// No body content (e.g. HEAD response, 304, 416).
56    Empty,
57    /// An in-memory byte buffer.
58    Bytes(Vec<u8>),
59    /// A full static file. The file was opened during resolution.
60    FileFull {
61        file: File,
62        len: u64,
63        mime: &'static str,
64    },
65    /// A byte range of a static file.
66    FileRange {
67        file: File,
68        range: FileRange,
69        total_len: u64,
70        mime: &'static str,
71    },
72}
73
74impl BodySource {
75    /// Returns the [`BodyKind`] discriminant.
76    pub fn kind(&self) -> BodyKind {
77        match self {
78            Self::Empty => BodyKind::Empty,
79            Self::Bytes(_) => BodyKind::Bytes,
80            Self::FileFull { .. } => BodyKind::FileFull,
81            Self::FileRange { .. } => BodyKind::FileRange,
82        }
83    }
84
85    /// Returns the content length in bytes, if known without performing I/O.
86    pub fn len(&self) -> u64 {
87        match self {
88            Self::Empty => 0,
89            Self::Bytes(b) => b.len() as u64,
90            Self::FileFull { len, .. } => *len,
91            Self::FileRange { range, .. } => range.len(),
92        }
93    }
94
95    /// Returns `true` if the body is known to be zero-length.
96    pub fn is_empty(&self) -> bool {
97        self.len() == 0
98    }
99
100    /// Returns the byte range, if this is a range body.
101    pub fn range(&self) -> Option<FileRange> {
102        match self {
103            Self::FileRange { range, .. } => Some(*range),
104            _ => None,
105        }
106    }
107
108    /// Read the entire body into memory.
109    ///
110    /// This is suitable for small files and test verification. For production
111    /// streaming, the service layer should convert the body source to a Hyper
112    /// streaming body instead of reading into memory.
113    ///
114    /// # Errors
115    ///
116    /// Returns an I/O error if the file cannot be read or the range cannot be
117    /// seeked to.
118    pub fn read_all(&mut self) -> io::Result<Vec<u8>> {
119        match self {
120            Self::Empty => Ok(Vec::new()),
121            Self::Bytes(b) => Ok(b.clone()),
122            Self::FileFull { file, .. } => {
123                let mut buf = Vec::new();
124                file.read_to_end(&mut buf)?;
125                Ok(buf)
126            }
127            Self::FileRange { file, range, .. } => {
128                file.seek(SeekFrom::Start(range.start))?;
129                let len = usize::try_from(range.len()).map_err(|_| {
130                    io::Error::new(io::ErrorKind::InvalidInput, "body range too large")
131                })?;
132                let mut buf = vec![0u8; len];
133                file.read_exact(&mut buf)?;
134                Ok(buf)
135            }
136        }
137    }
138
139    /// Read the entire body into memory, capped at `max_bytes`.
140    ///
141    /// Returns at most `max_bytes` bytes. If the body is larger, the excess
142    /// is silently truncated. This prevents unbounded allocation when reading
143    /// file-backed bodies whose size may be large.
144    ///
145    /// # Errors
146    ///
147    /// Returns an I/O error if the file cannot be read or the range cannot be
148    /// seeked to.
149    pub fn read_all_bounded(&mut self, max_bytes: usize) -> io::Result<Vec<u8>> {
150        match self {
151            Self::Empty => Ok(Vec::new()),
152            Self::Bytes(b) => {
153                let len = b.len().min(max_bytes);
154                Ok(b[..len].to_vec())
155            }
156            Self::FileFull { file, .. } => {
157                let mut buf = vec![0u8; max_bytes];
158                let n = file.read(&mut buf)?;
159                buf.truncate(n);
160                Ok(buf)
161            }
162            Self::FileRange { file, range, .. } => {
163                file.seek(SeekFrom::Start(range.start))?;
164                let len = (range.len() as usize).min(max_bytes);
165                let mut buf = vec![0u8; len];
166                file.read_exact(&mut buf)?;
167                Ok(buf)
168            }
169        }
170    }
171
172    /// Read a specific byte range from the body.
173    ///
174    /// For file-full bodies, `start` and `end_inclusive` are absolute offsets
175    /// into the file. For file-range bodies, they are offsets within the range.
176    ///
177    /// # Errors
178    ///
179    /// Returns an I/O error if the seek or read fails.
180    pub fn read_range(&mut self, start: u64, end_inclusive: u64) -> io::Result<Vec<u8>> {
181        if end_inclusive < start {
182            return Ok(Vec::new());
183        }
184        match self {
185            Self::Empty => Ok(Vec::new()),
186            Self::Bytes(b) => {
187                let s: usize = start.try_into().map_err(|_| {
188                    io::Error::new(io::ErrorKind::InvalidInput, "start offset too large")
189                })?;
190                let e_plus_1: usize = end_inclusive
191                    .checked_add(1)
192                    .and_then(|v| v.try_into().ok())
193                    .ok_or_else(|| {
194                        io::Error::new(io::ErrorKind::InvalidInput, "end offset too large")
195                    })?;
196                let e = e_plus_1.min(b.len());
197                if s >= b.len() {
198                    return Ok(Vec::new());
199                }
200                Ok(b[s..e].to_vec())
201            }
202            Self::FileFull { file, len, .. } => {
203                if start >= *len {
204                    return Err(io::Error::new(
205                        io::ErrorKind::InvalidInput,
206                        "read start exceeds file length",
207                    ));
208                }
209                if end_inclusive >= *len {
210                    return Err(io::Error::new(
211                        io::ErrorKind::InvalidInput,
212                        "read end exceeds file length",
213                    ));
214                }
215                let request_len = end_inclusive
216                    .checked_sub(start)
217                    .and_then(|v| v.checked_add(1))
218                    .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid range"))?;
219                let effective_len = request_len.min(*len - start);
220                file.seek(SeekFrom::Start(start))?;
221                let effective_len = usize::try_from(effective_len).map_err(|_| {
222                    io::Error::new(io::ErrorKind::InvalidInput, "read range too large")
223                })?;
224                let mut buf = vec![0u8; effective_len];
225                file.read_exact(&mut buf)?;
226                Ok(buf)
227            }
228            Self::FileRange { file, range, .. } => {
229                let sub_len = end_inclusive
230                    .checked_sub(start)
231                    .and_then(|v| v.checked_add(1))
232                    .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid range"))?;
233                if sub_len > range.len() {
234                    return Err(io::Error::new(
235                        io::ErrorKind::InvalidInput,
236                        "sub-range exceeds body range",
237                    ));
238                }
239                let absolute_start = range.start.checked_add(start).ok_or_else(|| {
240                    io::Error::new(io::ErrorKind::InvalidInput, "absolute offset overflow")
241                })?;
242                let absolute_end = absolute_start + sub_len - 1;
243                if absolute_end > range.end_inclusive {
244                    return Err(io::Error::new(
245                        io::ErrorKind::InvalidInput,
246                        "sub-range exceeds body range",
247                    ));
248                }
249                file.seek(SeekFrom::Start(absolute_start))?;
250                let sub_len = usize::try_from(sub_len).map_err(|_| {
251                    io::Error::new(io::ErrorKind::InvalidInput, "sub-range too large")
252                })?;
253                let mut buf = vec![0u8; sub_len];
254                file.read_exact(&mut buf)?;
255                Ok(buf)
256            }
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use std::fs;
265    use tempfile::TempDir;
266
267    fn make_file(content: &[u8]) -> (TempDir, File) {
268        let tmp = TempDir::new().unwrap();
269        let path = tmp.path().join("test.bin");
270        fs::write(&path, content).unwrap();
271        let file = File::open(&path).unwrap();
272        (tmp, file)
273    }
274
275    #[test]
276    fn empty_body_source() {
277        let mut bs = BodySource::Empty;
278        assert_eq!(bs.kind(), BodyKind::Empty);
279        assert_eq!(bs.len(), 0);
280        assert!(bs.is_empty());
281        assert!(bs.range().is_none());
282        assert_eq!(bs.read_all().unwrap(), Vec::<u8>::new());
283    }
284
285    #[test]
286    fn bytes_body_source() {
287        let mut bs = BodySource::Bytes(b"hello".to_vec());
288        assert_eq!(bs.kind(), BodyKind::Bytes);
289        assert_eq!(bs.len(), 5);
290        assert!(!bs.is_empty());
291        assert_eq!(bs.read_all().unwrap(), b"hello");
292    }
293
294    #[test]
295    fn file_full_body_source() {
296        let (_tmp, file) = make_file(b"hello world");
297        let mut bs = BodySource::FileFull {
298            file,
299            len: 11,
300            mime: "text/plain",
301        };
302        assert_eq!(bs.kind(), BodyKind::FileFull);
303        assert_eq!(bs.len(), 11);
304        assert!(!bs.is_empty());
305        assert!(bs.range().is_none());
306        assert_eq!(bs.read_all().unwrap(), b"hello world");
307    }
308
309    #[test]
310    fn file_range_body_source() {
311        let (_tmp, file) = make_file(b"hello world");
312        let mut bs = BodySource::FileRange {
313            file,
314            range: FileRange::new(0, 4),
315            total_len: 11,
316            mime: "text/plain",
317        };
318        assert_eq!(bs.kind(), BodyKind::FileRange);
319        assert_eq!(bs.len(), 5);
320        assert!(!bs.is_empty());
321        assert_eq!(bs.range(), Some(FileRange::new(0, 4)));
322        assert_eq!(bs.read_all().unwrap(), b"hello");
323    }
324
325    #[test]
326    fn file_range_body_source_middle() {
327        let (_tmp, file) = make_file(b"hello world");
328        let mut bs = BodySource::FileRange {
329            file,
330            range: FileRange::new(6, 10),
331            total_len: 11,
332            mime: "text/plain",
333        };
334        assert_eq!(bs.read_all().unwrap(), b"world");
335    }
336
337    #[test]
338    fn read_range_on_bytes() {
339        let mut bs = BodySource::Bytes(b"abcdef".to_vec());
340        assert_eq!(bs.read_range(1, 3).unwrap(), b"bcd");
341    }
342
343    #[test]
344    fn read_range_on_file_full() {
345        let (_tmp, file) = make_file(b"abcdef");
346        let mut bs = BodySource::FileFull {
347            file,
348            len: 6,
349            mime: "text/plain",
350        };
351        assert_eq!(bs.read_range(2, 4).unwrap(), b"cde");
352    }
353
354    #[test]
355    fn read_range_on_file_range() {
356        let (_tmp, file) = make_file(b"abcdef");
357        let mut bs = BodySource::FileRange {
358            file,
359            range: FileRange::new(1, 4),
360            total_len: 6,
361            mime: "text/plain",
362        };
363        // Absolute range 1-4, read sub-range 1-2 (relative to range start)
364        assert_eq!(bs.read_range(1, 2).unwrap(), b"cd");
365    }
366
367    #[test]
368    fn read_range_empty_on_out_of_bounds() {
369        let mut bs = BodySource::Bytes(b"ab".to_vec());
370        assert_eq!(bs.read_range(5, 10).unwrap(), Vec::<u8>::new());
371    }
372
373    #[test]
374    fn read_range_inverted_returns_empty() {
375        let mut bs = BodySource::Bytes(b"ab".to_vec());
376        assert_eq!(bs.read_range(3, 1).unwrap(), Vec::<u8>::new());
377    }
378
379    #[test]
380    fn read_range_file_full_within_bounds() {
381        let (_tmp, file) = make_file(b"hello world");
382        let mut bs = BodySource::FileFull {
383            file,
384            len: 11,
385            mime: "text/plain",
386        };
387        assert_eq!(bs.read_range(0, 4).unwrap(), b"hello");
388        assert_eq!(bs.read_range(6, 10).unwrap(), b"world");
389    }
390
391    #[test]
392    fn read_range_file_full_beyond_eof_rejected() {
393        let (_tmp, file) = make_file(b"hello");
394        let mut bs = BodySource::FileFull {
395            file,
396            len: 5,
397            mime: "text/plain",
398        };
399        let result = bs.read_range(0, 99);
400        assert!(result.is_err());
401    }
402
403    #[test]
404    fn read_range_file_full_start_past_len_rejected() {
405        let (_tmp, file) = make_file(b"hello");
406        let mut bs = BodySource::FileFull {
407            file,
408            len: 5,
409            mime: "text/plain",
410        };
411        let result = bs.read_range(10, 20);
412        assert!(result.is_err());
413    }
414
415    #[test]
416    fn read_range_file_range_within_bounds() {
417        let (_tmp, file) = make_file(b"hello world");
418        let mut bs = BodySource::FileRange {
419            file,
420            range: FileRange::new(0, 4),
421            total_len: 11,
422            mime: "text/plain",
423        };
424        assert_eq!(bs.read_range(0, 2).unwrap(), b"hel");
425    }
426
427    #[test]
428    fn read_range_file_range_beyond_end_rejected() {
429        let (_tmp, file) = make_file(b"hello world");
430        let mut bs = BodySource::FileRange {
431            file,
432            range: FileRange::new(0, 4),
433            total_len: 11,
434            mime: "text/plain",
435        };
436        let result = bs.read_range(0, 10);
437        assert!(result.is_err());
438    }
439
440    #[test]
441    fn read_range_bytes_large_offset_rejected() {
442        let mut bs = BodySource::Bytes(b"test".to_vec());
443        let result = bs.read_range(u64::MAX, u64::MAX);
444        assert!(result.is_err());
445    }
446
447    #[test]
448    fn read_range_file_range_overflow_rejected() {
449        let (_tmp, file) = make_file(b"hello world");
450        let mut bs = BodySource::FileRange {
451            file,
452            range: FileRange::new(u64::MAX - 2, u64::MAX),
453            total_len: 11,
454            mime: "text/plain",
455        };
456        let result = bs.read_range(5, 10);
457        assert!(result.is_err());
458    }
459
460    #[test]
461    fn body_source_error_display() {
462        assert_eq!(
463            BodySourceError::InvalidRange.to_string(),
464            "invalid byte range"
465        );
466        assert_eq!(
467            BodySourceError::AlreadyConsumed.to_string(),
468            "resolved file already consumed"
469        );
470    }
471
472    #[test]
473    fn read_all_bounded_bytes_within_limit() {
474        let mut bs = BodySource::Bytes(b"hello".to_vec());
475        assert_eq!(bs.read_all_bounded(10).unwrap(), b"hello");
476    }
477
478    #[test]
479    fn read_all_bounded_bytes_truncated() {
480        let mut bs = BodySource::Bytes(b"hello".to_vec());
481        assert_eq!(bs.read_all_bounded(3).unwrap(), b"hel");
482    }
483
484    #[test]
485    fn read_all_bounded_bytes_zero_limit() {
486        let mut bs = BodySource::Bytes(b"hello".to_vec());
487        assert_eq!(bs.read_all_bounded(0).unwrap(), b"");
488    }
489
490    #[test]
491    fn read_all_bounded_empty() {
492        let mut bs = BodySource::Empty;
493        assert_eq!(bs.read_all_bounded(100).unwrap(), b"");
494    }
495
496    #[test]
497    fn read_all_bounded_file_full_within_limit() {
498        let (_tmp, file) = make_file(b"hello world");
499        let mut bs = BodySource::FileFull {
500            file,
501            len: 11,
502            mime: "text/plain",
503        };
504        assert_eq!(bs.read_all_bounded(100).unwrap(), b"hello world");
505    }
506
507    #[test]
508    fn read_all_bounded_file_full_truncated() {
509        let (_tmp, file) = make_file(b"hello world");
510        let mut bs = BodySource::FileFull {
511            file,
512            len: 11,
513            mime: "text/plain",
514        };
515        assert_eq!(bs.read_all_bounded(5).unwrap(), b"hello");
516    }
517
518    #[test]
519    fn read_all_bounded_file_range_within_limit() {
520        let (_tmp, file) = make_file(b"hello world");
521        let mut bs = BodySource::FileRange {
522            file,
523            range: FileRange::new(0, 4),
524            total_len: 11,
525            mime: "text/plain",
526        };
527        assert_eq!(bs.read_all_bounded(100).unwrap(), b"hello");
528    }
529
530    #[test]
531    fn read_all_bounded_file_range_truncated() {
532        let (_tmp, file) = make_file(b"hello world");
533        let mut bs = BodySource::FileRange {
534            file,
535            range: FileRange::new(0, 10),
536            total_len: 11,
537            mime: "text/plain",
538        };
539        assert_eq!(bs.read_all_bounded(3).unwrap(), b"hel");
540    }
541}