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