1use crate::error::{Error, Result};
8use crate::fidelity::{Fidelity, FidelityReport, Rung};
9use crate::format::{ContainerCaps, FormatId};
10use crate::source::{Source, SpillPolicy, SpillSource};
11
12#[derive(Clone, PartialEq, Eq, Debug)]
13pub enum StreamPolicy {
14 Exact,
16 Adaptive {
18 allow_forward_only: bool,
19 spill: SpillPolicy,
20 allow_degraded: bool,
21 },
22 ForwardOnly,
24}
25
26impl Default for StreamPolicy {
27 fn default() -> Self {
28 StreamPolicy::Adaptive {
29 allow_forward_only: true,
30 spill: SpillPolicy::default(),
31 allow_degraded: true,
32 }
33 }
34}
35
36#[non_exhaustive]
47pub struct Resolved {
48 pub source: Box<dyn Source>,
49 pub rung: Rung,
50 pub report: FidelityReport,
51}
52
53impl std::fmt::Debug for Resolved {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("Resolved")
58 .field("source", &"<dyn Source>")
59 .field("rung", &self.rung)
60 .field("report", &self.report)
61 .finish()
62 }
63}
64
65static NO_SPILL: SpillPolicy = SpillPolicy::Off;
70
71fn seed_report(rung: Rung, format: FormatId, caps: ContainerCaps) -> FidelityReport {
78 let mut report = FidelityReport::new(rung);
79 if rung.is_authoritative() {
80 return report;
81 }
82 if caps.trailing_index {
83 report.warn(Fidelity::TrailingIndexUnread { format });
84 report.warn(Fidelity::EntryCountUnknown);
85 }
86 report
87}
88
89pub fn resolve(
95 src: Box<dyn Source>,
96 format: FormatId,
97 caps: ContainerCaps,
98 policy: &StreamPolicy,
99) -> Result<Resolved> {
100 if src.caps().seekable {
102 return Ok(Resolved {
103 source: src,
104 rung: Rung::Exact,
105 report: FidelityReport::exact(),
106 });
107 }
108
109 let (allow_forward_only, spill, allow_degraded) = match policy {
110 StreamPolicy::Exact => return Err(Error::NotSeekable { format }),
111 StreamPolicy::ForwardOnly => (true, &NO_SPILL, false),
112 StreamPolicy::Adaptive {
113 allow_forward_only,
114 spill,
115 allow_degraded,
116 } => (*allow_forward_only, spill, *allow_degraded),
117 };
118
119 if caps.forward_parse && allow_forward_only && !caps.needs_seek {
121 return Ok(Resolved {
122 source: src,
123 rung: Rung::ForwardOnly,
124 report: seed_report(Rung::ForwardOnly, format, caps),
125 });
126 }
127
128 if spill.is_enabled() {
130 let spilled = SpillSource::materialize(src, spill)?;
131 return Ok(Resolved {
132 source: Box::new(spilled),
133 rung: Rung::Spilled,
134 report: FidelityReport::new(Rung::Spilled),
135 });
136 }
137
138 if allow_degraded && caps.degraded_parse {
140 return Ok(Resolved {
141 source: src,
142 rung: Rung::Degraded,
143 report: seed_report(Rung::Degraded, format, caps),
144 });
145 }
146
147 Err(Error::NotSeekable { format })
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::source::{FileSource, ReaderSource};
154 use crate::{Fidelity, FormatId, Rung};
155
156 const ZIP: FormatId = FormatId::new("zip");
157 const SQUASHFS: FormatId = FormatId::new("squashfs");
158
159 fn pipe(bytes: &[u8]) -> Box<dyn Source> {
160 Box::new(ReaderSource::new(std::io::Cursor::new(bytes.to_vec())))
161 }
162
163 fn file(bytes: &[u8]) -> Box<dyn Source> {
164 let mut f = tempfile::NamedTempFile::new().unwrap();
165 std::io::Write::write_all(&mut f, bytes).unwrap();
166 let (_, path) = f.keep().unwrap();
167 Box::new(FileSource::open(&path).unwrap())
168 }
169
170 fn zip_caps() -> ContainerCaps {
172 ContainerCaps {
173 read: true,
174 forward_parse: true,
175 trailing_index: true,
176 per_entry_codec: true,
177 ..Default::default()
178 }
179 }
180
181 fn squashfs_caps() -> ContainerCaps {
183 ContainerCaps {
184 read: true,
185 needs_seek: true,
186 ..Default::default()
187 }
188 }
189
190 #[test]
191 fn seekable_input_reaches_exact_and_reports_no_loss() {
192 let r = resolve(file(b"payload"), ZIP, zip_caps(), &StreamPolicy::default()).unwrap();
193 assert_eq!(r.rung, Rung::Exact);
194 assert!(r.report.is_lossless());
195 }
196
197 #[test]
198 fn pipe_prefers_forward_only_over_spilling_to_disk() {
199 let r = resolve(pipe(b"payload"), ZIP, zip_caps(), &StreamPolicy::default()).unwrap();
201 assert_eq!(r.rung, Rung::ForwardOnly);
202 assert!(!r.report.is_lossless());
203 }
204
205 #[test]
206 fn forward_only_on_a_trailing_index_format_records_the_index_loss() {
207 let r = resolve(pipe(b"payload"), ZIP, zip_caps(), &StreamPolicy::default()).unwrap();
208 assert!(
209 r.report
210 .warnings
211 .contains(&Fidelity::TrailingIndexUnread { format: ZIP })
212 );
213 assert!(r.report.warnings.contains(&Fidelity::EntryCountUnknown));
214 }
215
216 #[test]
217 fn a_forward_parseable_format_without_a_trailing_index_loses_nothing_specific() {
218 let tar = ContainerCaps {
220 read: true,
221 forward_parse: true,
222 ..Default::default()
223 };
224 let r = resolve(
225 pipe(b"payload"),
226 FormatId::new("tar"),
227 tar,
228 &StreamPolicy::default(),
229 )
230 .unwrap();
231 assert_eq!(r.rung, Rung::ForwardOnly);
232 assert!(r.report.warnings.is_empty());
233 }
234
235 #[test]
236 fn needs_seek_format_on_a_pipe_spills_and_stays_authoritative() {
237 let r = resolve(
238 pipe(b"payload"),
239 SQUASHFS,
240 squashfs_caps(),
241 &StreamPolicy::default(),
242 )
243 .unwrap();
244 assert_eq!(r.rung, Rung::Spilled);
245 assert!(r.rung.is_authoritative());
246 assert!(r.report.is_lossless());
247 assert!(
248 r.source.caps().seekable,
249 "spilling must produce a seekable source"
250 );
251 }
252
253 #[test]
254 fn spilling_preserves_the_bytes() {
255 let mut r = resolve(
256 pipe(b"0123456789"),
257 SQUASHFS,
258 squashfs_caps(),
259 &StreamPolicy::default(),
260 )
261 .unwrap();
262 let mut out = Vec::new();
263 std::io::Read::read_to_end(&mut r.source, &mut out).unwrap();
264 assert_eq!(out, b"0123456789");
265 }
266
267 #[test]
268 fn exact_policy_refuses_a_pipe_and_names_the_format() {
269 let err = resolve(pipe(b"x"), SQUASHFS, squashfs_caps(), &StreamPolicy::Exact).unwrap_err();
270 assert!(matches!(err, crate::Error::NotSeekable { format } if format == SQUASHFS));
271 }
272
273 #[test]
274 fn forward_only_policy_never_spills() {
275 let err = resolve(
276 pipe(b"x"),
277 SQUASHFS,
278 squashfs_caps(),
279 &StreamPolicy::ForwardOnly,
280 )
281 .unwrap_err();
282 assert!(matches!(err, crate::Error::NotSeekable { .. }));
283 }
284
285 #[test]
286 fn forward_only_policy_still_works_where_the_format_allows_it() {
287 let r = resolve(pipe(b"x"), ZIP, zip_caps(), &StreamPolicy::ForwardOnly).unwrap();
288 assert_eq!(r.rung, Rung::ForwardOnly);
289 }
290
291 #[test]
292 fn spill_off_falls_through_to_degraded_when_the_format_can_salvage() {
293 let sevenz = ContainerCaps {
294 read: true,
295 degraded_parse: true,
296 trailing_index: true,
297 solid: true,
298 needs_seek: true,
299 ..Default::default()
300 };
301 let policy = StreamPolicy::Adaptive {
302 allow_forward_only: true,
303 spill: SpillPolicy::Off,
304 allow_degraded: true,
305 };
306 let r = resolve(pipe(b"x"), FormatId::new("7z"), sevenz, &policy).unwrap();
307 assert_eq!(r.rung, Rung::Degraded);
308 assert!(!r.rung.is_authoritative());
309 }
310
311 #[test]
312 fn spill_off_and_no_salvage_path_is_an_error_not_a_lie() {
313 let policy = StreamPolicy::Adaptive {
314 allow_forward_only: true,
315 spill: SpillPolicy::Off,
316 allow_degraded: true,
317 };
318 let err = resolve(pipe(b"x"), SQUASHFS, squashfs_caps(), &policy).unwrap_err();
319 assert!(matches!(err, crate::Error::NotSeekable { .. }));
320 }
321
322 #[test]
323 fn disallowing_forward_only_forces_a_spill_even_where_forward_would_work() {
324 let policy = StreamPolicy::Adaptive {
326 allow_forward_only: false,
327 spill: SpillPolicy::default(),
328 allow_degraded: true,
329 };
330 let r = resolve(pipe(b"payload"), ZIP, zip_caps(), &policy).unwrap();
331 assert_eq!(r.rung, Rung::Spilled);
332 assert!(r.report.is_lossless());
333 }
334
335 #[test]
336 fn default_policy_enables_every_rung() {
337 match StreamPolicy::default() {
338 StreamPolicy::Adaptive {
339 allow_forward_only,
340 allow_degraded,
341 spill,
342 } => {
343 assert!(allow_forward_only && allow_degraded && spill.is_enabled());
344 }
345 other => panic!("unexpected default: {other:?}"),
346 }
347 }
348}