Skip to main content

stuffr_core/source/
stream_only.rs

1//! Adapter presenting a plain reader as a non-seekable [`Source`].
2
3use std::io::Read;
4
5use super::{SeekRead, Source, SourceCaps};
6
7/// Wraps a plain reader as a non-seekable [`Source`].
8///
9/// Most codecs decode forward-only and have no index to expose, so this is what
10/// they return from [`crate::Codec::decoder`]. A codec that *does* carry a seek
11/// table — seekable zstd, an xz block index — returns something genuinely
12/// seekable instead, which is the whole reason `decoder` returns a `Source`
13/// rather than a bare reader: the capability survives the layer boundary and
14/// the ladder can run between codec and container.
15pub struct StreamOnly<R>(R);
16
17impl<R: Read + Send> StreamOnly<R> {
18    pub fn new(inner: R) -> Self {
19        Self(inner)
20    }
21}
22
23impl<R: Read + Send> Read for StreamOnly<R> {
24    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
25        self.0.read(buf)
26    }
27}
28
29impl<R: Read + Send> Source for StreamOnly<R> {
30    fn caps(&self) -> SourceCaps {
31        SourceCaps {
32            seekable: false,
33            len: None,
34        }
35    }
36
37    fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
38        None
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use std::io::Read;
46
47    #[test]
48    fn stream_only_passes_bytes_through_unchanged() {
49        let mut s = StreamOnly::new(std::io::Cursor::new(b"payload".to_vec()));
50        let mut out = Vec::new();
51        s.read_to_end(&mut out).unwrap();
52        assert_eq!(out, b"payload");
53    }
54
55    #[test]
56    fn stream_only_reports_no_seek_even_over_a_seekable_reader() {
57        // A Cursor is seekable, but a codec decoding forward-only has no index
58        // to expose, so its output must not claim random access it cannot back.
59        let mut s = StreamOnly::new(std::io::Cursor::new(b"payload".to_vec()));
60        assert!(!s.caps().seekable);
61        assert_eq!(s.caps().len, None);
62        assert!(s.as_seek().is_none());
63    }
64}