stuffr_core/source/
mod.rs1pub mod decode_side;
5mod file;
6pub mod limit;
7mod reader;
8mod seek_guard;
9pub mod spill;
10pub mod stream_only;
11
12pub use decode_side::{DecodeSideSource, is_decode_side, mark_decode_side};
13pub use file::FileSource;
14pub use limit::{Counting, CountingWriter, DEFAULT_MAX_RATIO, RATIO_FLOOR, RatioGuard};
15pub use reader::{PeekSource, ReaderSource};
16pub use seek_guard::{GuardedSeek, MAX_SEEK_POSITION};
17pub use spill::{SpillPolicy, SpillSource};
18pub use stream_only::StreamOnly;
19
20use std::io::{Read, Seek};
21
22pub trait SeekRead: Read + Seek + Send {}
24impl<T: Read + Seek + Send> SeekRead for T {}
25
26#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
27pub struct SourceCaps {
28 pub seekable: bool,
29 pub len: Option<u64>,
31}
32
33pub trait Source: Read + Send {
35 fn caps(&self) -> SourceCaps;
36
37 fn as_seek(&mut self) -> Option<&mut dyn SeekRead>;
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use std::io::Read;
45
46 #[test]
47 fn reader_source_is_not_seekable_and_has_unknown_length() {
48 let mut s = ReaderSource::new(std::io::Cursor::new(b"hello".to_vec()));
49 assert!(!s.caps().seekable);
52 assert_eq!(s.caps().len, None);
53 assert!(s.as_seek().is_none());
54
55 let mut buf = Vec::new();
56 s.read_to_end(&mut buf).unwrap();
57 assert_eq!(buf, b"hello");
58 }
59
60 #[test]
61 fn file_source_is_seekable_and_knows_its_length() {
62 let mut f = tempfile::NamedTempFile::new().unwrap();
63 std::io::Write::write_all(&mut f, b"0123456789").unwrap();
64 let mut s = FileSource::open(f.path()).unwrap();
65
66 assert!(s.caps().seekable);
67 assert_eq!(s.caps().len, Some(10));
68
69 let seek = s.as_seek().expect("file must expose seek");
70 seek.seek(std::io::SeekFrom::Start(4)).unwrap();
71 let mut buf = [0u8; 3];
72 seek.read_exact(&mut buf).unwrap();
73 assert_eq!(&buf, b"456");
74 }
75
76 #[test]
95 fn every_seekable_rung_refuses_a_position_no_file_can_hold() {
96 use crate::error::Error;
97 use std::io::SeekFrom;
98
99 fn refusal(src: &mut dyn Source) -> Error {
100 let seek = src.as_seek().expect("this rung must be seekable");
101 let err = seek
102 .seek(SeekFrom::Start(u64::MAX))
103 .expect_err("a position past i64::MAX must be refused");
104 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData, "{err}");
105 Error::from_decode_io(err)
106 }
107
108 let mut f = tempfile::NamedTempFile::new().unwrap();
109 std::io::Write::write_all(&mut f, b"0123456789").unwrap();
110 let mut file_rung = FileSource::open(f.path()).unwrap();
111
112 let mut in_memory = SpillSource::materialize(
113 Box::new(ReaderSource::new(std::io::Cursor::new(
114 b"0123456789".to_vec(),
115 ))),
116 &SpillPolicy::Temp {
117 dir: None,
118 mem_cap: 1024,
119 max: 1 << 20,
120 },
121 )
122 .unwrap();
123 assert!(
124 !in_memory.spilled_to_disk(),
125 "premise: this rung is a Cursor"
126 );
127
128 let mut on_disk = SpillSource::materialize(
129 Box::new(ReaderSource::new(std::io::Cursor::new(
130 b"0123456789".to_vec(),
131 ))),
132 &SpillPolicy::Temp {
133 dir: None,
134 mem_cap: 4,
135 max: 1 << 20,
136 },
137 )
138 .unwrap();
139 assert!(on_disk.spilled_to_disk(), "premise: this rung is a File");
140
141 for rung in [
142 &mut file_rung as &mut dyn Source,
143 &mut in_memory,
144 &mut on_disk,
145 ] {
146 let classified = refusal(rung);
147 assert!(
148 matches!(classified, Error::Corrupt(_)),
149 "the input's fault, not stuffr's: {classified:?}"
150 );
151 assert_eq!(
152 classified.exit_code(),
153 5,
154 "never exit 1, and never exit 6 — nothing was allocated from the offset"
155 );
156 }
157 }
158
159 #[test]
160 fn peek_source_replays_the_prefix_it_consumed() {
161 let inner: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(
163 b"MAGIC-then-payload".to_vec(),
164 )));
165 let mut p = PeekSource::fill(inner, 5).unwrap();
166 assert_eq!(p.prefix(), b"MAGIC");
167
168 let mut all = Vec::new();
169 p.read_to_end(&mut all).unwrap();
170 assert_eq!(all, b"MAGIC-then-payload");
171 }
172
173 #[test]
174 fn peek_source_tolerates_input_shorter_than_the_request() {
175 let inner: Box<dyn Source> =
176 Box::new(ReaderSource::new(std::io::Cursor::new(b"ab".to_vec())));
177 let mut p = PeekSource::fill(inner, 4096).unwrap();
178 assert_eq!(p.prefix(), b"ab");
179
180 let mut all = Vec::new();
181 p.read_to_end(&mut all).unwrap();
182 assert_eq!(all, b"ab");
183 }
184}