Skip to main content

stuffr_core/source/
mod.rs

1//! Input abstraction. A container never opens a file — it asks a `Source` what
2//! it can do, and the ladder (see [`crate::ladder`]) supplies the best rung.
3
4mod file;
5pub mod limit;
6mod reader;
7pub mod spill;
8pub mod stream_only;
9
10pub use file::FileSource;
11pub use limit::{Counting, CountingWriter, DEFAULT_MAX_RATIO, RATIO_FLOOR, RatioGuard};
12pub use reader::{PeekSource, ReaderSource};
13pub use spill::{SpillPolicy, SpillSource};
14pub use stream_only::StreamOnly;
15
16use std::io::{Read, Seek};
17
18/// Blanket-implemented marker so `&mut dyn SeekRead` is usable as a trait object.
19pub trait SeekRead: Read + Seek + Send {}
20impl<T: Read + Seek + Send> SeekRead for T {}
21
22#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
23pub struct SourceCaps {
24    pub seekable: bool,
25    /// Total length if known. `None` for pipes.
26    pub len: Option<u64>,
27}
28
29/// An input stream that reports its own capabilities.
30pub trait Source: Read + Send {
31    fn caps(&self) -> SourceCaps;
32
33    /// Returns a seekable view, or `None` if this source cannot seek.
34    fn as_seek(&mut self) -> Option<&mut dyn SeekRead>;
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use std::io::Read;
41
42    #[test]
43    fn reader_source_is_not_seekable_and_has_unknown_length() {
44        let mut s = ReaderSource::new(std::io::Cursor::new(b"hello".to_vec()));
45        // A Cursor *is* seekable, but ReaderSource deliberately erases that:
46        // it models "arrived on a pipe". Use FileSource for real seek.
47        assert!(!s.caps().seekable);
48        assert_eq!(s.caps().len, None);
49        assert!(s.as_seek().is_none());
50
51        let mut buf = Vec::new();
52        s.read_to_end(&mut buf).unwrap();
53        assert_eq!(buf, b"hello");
54    }
55
56    #[test]
57    fn file_source_is_seekable_and_knows_its_length() {
58        let mut f = tempfile::NamedTempFile::new().unwrap();
59        std::io::Write::write_all(&mut f, b"0123456789").unwrap();
60        let mut s = FileSource::open(f.path()).unwrap();
61
62        assert!(s.caps().seekable);
63        assert_eq!(s.caps().len, Some(10));
64
65        let seek = s.as_seek().expect("file must expose seek");
66        seek.seek(std::io::SeekFrom::Start(4)).unwrap();
67        let mut buf = [0u8; 3];
68        seek.read_exact(&mut buf).unwrap();
69        assert_eq!(&buf, b"456");
70    }
71
72    #[test]
73    fn peek_source_replays_the_prefix_it_consumed() {
74        // This is the property the probe depends on: peeking must not consume.
75        let inner: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(
76            b"MAGIC-then-payload".to_vec(),
77        )));
78        let mut p = PeekSource::fill(inner, 5).unwrap();
79        assert_eq!(p.prefix(), b"MAGIC");
80
81        let mut all = Vec::new();
82        p.read_to_end(&mut all).unwrap();
83        assert_eq!(all, b"MAGIC-then-payload");
84    }
85
86    #[test]
87    fn peek_source_tolerates_input_shorter_than_the_request() {
88        let inner: Box<dyn Source> =
89            Box::new(ReaderSource::new(std::io::Cursor::new(b"ab".to_vec())));
90        let mut p = PeekSource::fill(inner, 4096).unwrap();
91        assert_eq!(p.prefix(), b"ab");
92
93        let mut all = Vec::new();
94        p.read_to_end(&mut all).unwrap();
95        assert_eq!(all, b"ab");
96    }
97}