Skip to main content

zencodec_io/
buffered.rs

1//! Random access over a sequential stream, by buffering under a policy.
2
3use alloc::vec::Vec;
4
5use crate::StdSourceError;
6use zencodec::source::{AccessPattern, ReadAt, SourceError};
7
8/// How a [`BufferedReadAt`] retains bytes from a sequential stream.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum BufferPolicy {
12    /// Retain only the most recent `window_bytes`. A read older than the
13    /// window fails with [`SourceError::BeyondWindow`]. Right for
14    /// [`AccessPattern::Sequential`] and small
15    /// [`AccessPattern::BoundedBacktrack`] decoders.
16    Window {
17        /// Bytes retained behind the read watermark.
18        window_bytes: usize,
19    },
20    /// Retain every byte consumed, up to `max_bytes`; more fails with
21    /// [`SourceError::BeyondLimit`]. Right for
22    /// [`AccessPattern::RandomAccess`] decoders, with the cap sized to the
23    /// largest file the driver is willing to hold in memory.
24    Spool {
25        /// Maximum retained bytes.
26        max_bytes: usize,
27    },
28}
29
30impl BufferPolicy {
31    /// A compatibility heuristic that may clamp the required lookback.
32    /// Use [`Self::try_for_pattern`] when the access pattern must be satisfied.
33    pub const fn for_pattern(pattern: AccessPattern, max_bytes: usize) -> Self {
34        match pattern {
35            AccessPattern::Sequential => Self::Window {
36                window_bytes: 64 * 1024,
37            },
38            AccessPattern::BoundedBacktrack { window_bytes } => Self::Window {
39                window_bytes: if window_bytes > max_bytes as u64 {
40                    max_bytes
41                } else {
42                    window_bytes as usize
43                },
44            },
45            _ => Self::Spool { max_bytes },
46        }
47    }
48
49    /// Choose a policy whose retained history satisfies `pattern` within the cap.
50    ///
51    /// A required backtrack window larger than the cap is refused, never clamped.
52    /// A random-access stream can still exceed its spool cap at runtime. This
53    /// caps retained payload, not total process memory or caller-owned output.
54    pub const fn try_for_pattern(
55        pattern: AccessPattern,
56        max_bytes: usize,
57    ) -> Result<Self, SourceError> {
58        match pattern {
59            AccessPattern::Sequential => Ok(Self::Window {
60                window_bytes: if max_bytes < 64 * 1024 {
61                    max_bytes
62                } else {
63                    64 * 1024
64                },
65            }),
66            AccessPattern::BoundedBacktrack { window_bytes } => {
67                if window_bytes > max_bytes as u64 {
68                    Err(SourceError::BeyondLimit {
69                        needed: window_bytes,
70                        limit: max_bytes as u64,
71                    })
72                } else {
73                    Ok(Self::Window {
74                        window_bytes: window_bytes as usize,
75                    })
76                }
77            }
78            _ => Ok(Self::Spool { max_bytes }),
79        }
80    }
81
82    const fn cap(&self) -> usize {
83        match self {
84            Self::Window { window_bytes } => *window_bytes,
85            Self::Spool { max_bytes } => *max_bytes,
86        }
87    }
88}
89
90/// [`ReadAt`] over a sequential [`std::io::Read`] stream, buffered per a
91/// [`BufferPolicy`].
92///
93/// Buffering manufactures the seeks the stream cannot provide, at an
94/// explicit price: a windowed source fails a far-back read, a spool fails
95/// past its cap. It never silently grows or returns wrong bytes.
96pub struct BufferedReadAt<R: std::io::Read> {
97    reader: R,
98    policy: BufferPolicy,
99    /// Retained bytes; `buf[0]` is at `buf_start`.
100    buf: Vec<u8>,
101    buf_start: u64,
102    /// Bytes consumed from the reader so far.
103    watermark: u64,
104    eof: bool,
105    /// An EOF probe consumed a byte beyond the spool cap. It cannot be replayed.
106    exceeded: Option<(u64, u64)>,
107}
108
109impl<R: std::io::Read> BufferedReadAt<R> {
110    /// Create a buffered source over `reader` with `policy`.
111    pub fn new(reader: R, policy: BufferPolicy) -> Self {
112        Self {
113            reader,
114            policy,
115            buf: Vec::new(),
116            buf_start: 0,
117            watermark: 0,
118            eof: false,
119            exceeded: None,
120        }
121    }
122
123    /// Bytes consumed from the underlying reader so far.
124    pub fn watermark(&self) -> u64 {
125        self.watermark
126    }
127
128    /// Payload bytes retained for later random/backtrack reads.
129    /// Excludes the caller's destination and fixed stack scratch used while skipping.
130    pub fn retained_bytes(&self) -> usize {
131        self.buf.len()
132    }
133
134    fn check_live(&self) -> Result<(), StdSourceError> {
135        if let Some((needed, limit)) = self.exceeded {
136            return Err(SourceError::BeyondLimit { needed, limit }.into());
137        }
138        Ok(())
139    }
140
141    fn reserve_retained(&mut self, incoming: usize) -> Result<(), StdSourceError> {
142        let needed = self
143            .buf
144            .len()
145            .saturating_add(incoming)
146            .min(self.policy.cap());
147        if needed > self.buf.capacity() {
148            self.buf
149                .try_reserve_exact(needed - self.buf.len())
150                .map_err(|source| StdSourceError::Allocation {
151                    requested: needed,
152                    source,
153                })?;
154        }
155        Ok(())
156    }
157
158    fn remember(&mut self, bytes: &[u8]) {
159        match self.policy {
160            BufferPolicy::Window { window_bytes } => {
161                if bytes.len() >= window_bytes {
162                    self.buf.clear();
163                    self.buf
164                        .extend_from_slice(&bytes[bytes.len() - window_bytes..]);
165                } else {
166                    let drop = self
167                        .buf
168                        .len()
169                        .saturating_add(bytes.len())
170                        .saturating_sub(window_bytes);
171                    self.buf.drain(..drop);
172                    self.buf.extend_from_slice(bytes);
173                }
174                self.buf_start = self.watermark - self.buf.len() as u64;
175            }
176            BufferPolicy::Spool { .. } => self.buf.extend_from_slice(bytes),
177        }
178    }
179
180    // Read directly into caller storage. Reserve cache capacity before consuming
181    // input; a window retains only its tail even for a much larger forward read.
182    fn read_more(&mut self, out: &mut [u8]) -> Result<usize, StdSourceError> {
183        self.check_live()?;
184        if self.eof || out.is_empty() {
185            return Ok(0);
186        }
187        let mut want = out.len();
188        if let BufferPolicy::Spool { max_bytes } = self.policy {
189            want = want.min(max_bytes - self.buf.len());
190            if want == 0 {
191                // One fixed stack byte distinguishes exact-cap EOF from excess
192                // input. A non-EOF result is terminal and never enters the cache.
193                let mut probe = [0];
194                let n = loop {
195                    match self.reader.read(&mut probe) {
196                        Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
197                        result => break result?,
198                    }
199                };
200                if n == 0 {
201                    self.eof = true;
202                    return Ok(0);
203                }
204                self.watermark += 1;
205                let limit = max_bytes as u64;
206                self.exceeded = Some((self.watermark, limit));
207                return Err(SourceError::BeyondLimit {
208                    needed: self.watermark,
209                    limit,
210                }
211                .into());
212            }
213        }
214        self.reserve_retained(want)?;
215        let n = loop {
216            match self.reader.read(&mut out[..want]) {
217                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
218                result => break result?,
219            }
220        };
221        if n == 0 {
222            self.eof = true;
223        } else {
224            self.watermark += n as u64;
225            self.remember(&out[..n]);
226        }
227        Ok(n)
228    }
229
230    fn advance_to(&mut self, offset: u64) -> Result<(), StdSourceError> {
231        let mut scratch = [0; 16 * 1024];
232        while self.watermark < offset && !self.eof {
233            let want = (offset - self.watermark).min(scratch.len() as u64) as usize;
234            self.read_more(&mut scratch[..want])?;
235        }
236        Ok(())
237    }
238}
239
240impl<R: std::io::Read> ReadAt for BufferedReadAt<R> {
241    type Error = StdSourceError;
242
243    /// Consumes the stream to EOF. Under a window policy the bytes are
244    /// evicted as they pass, so the length is known but early bytes are
245    /// gone; under a spool they are retained up to the cap. At the exact spool
246    /// cap, a one-byte stack probe distinguishes EOF from excess input. Excess
247    /// consumes that one byte and makes subsequent operations fail; retained
248    /// payload never exceeds the cap. This is not a total-input-byte limit.
249    fn len(&mut self) -> Result<u64, Self::Error> {
250        self.check_live()?;
251        let mut scratch = [0; 16 * 1024];
252        while self.read_more(&mut scratch)? != 0 {}
253        Ok(self.watermark)
254    }
255
256    fn read_exact_at(&mut self, offset: u64, out: &mut [u8]) -> Result<(), Self::Error> {
257        if out.is_empty() {
258            return Ok(());
259        }
260        self.check_live()?;
261        let end = offset
262            .checked_add(out.len() as u64)
263            .ok_or(SourceError::UnexpectedEof {
264                offset,
265                len: out.len() as u64,
266                total: None,
267            })?;
268        if let BufferPolicy::Spool { max_bytes } = self.policy
269            && end > max_bytes as u64
270        {
271            return Err(SourceError::BeyondLimit {
272                needed: end,
273                limit: max_bytes as u64,
274            }
275            .into());
276        }
277        if self.read_at(offset, out)? != out.len() {
278            return Err(SourceError::UnexpectedEof {
279                offset,
280                len: out.len() as u64,
281                total: self.eof.then_some(self.watermark),
282            }
283            .into());
284        }
285        Ok(())
286    }
287
288    /// Reads forward only as far as `offset + out.len()`: the end of the
289    /// stream is found by a short read, never by consuming it to learn the
290    /// length.
291    fn read_at(&mut self, offset: u64, out: &mut [u8]) -> Result<usize, Self::Error> {
292        if out.is_empty() {
293            return Ok(0);
294        }
295        self.check_live()?;
296        offset
297            .checked_add(out.len() as u64)
298            .ok_or(SourceError::UnexpectedEof {
299                offset,
300                len: out.len() as u64,
301                total: self.len_hint(),
302            })?;
303        if offset < self.buf_start {
304            return Err(SourceError::BeyondWindow {
305                offset,
306                oldest_retained: self.buf_start,
307            }
308            .into());
309        }
310        self.advance_to(offset)?;
311        if offset > self.watermark {
312            return Ok(0);
313        }
314        let mut filled = ((self.watermark - offset) as usize).min(out.len());
315        if filled != 0 {
316            let start = (offset - self.buf_start) as usize;
317            out[..filled].copy_from_slice(&self.buf[start..start + filled]);
318        }
319        while filled < out.len() {
320            let n = self.read_more(&mut out[filled..])?;
321            if n == 0 {
322                break;
323            }
324            filled += n;
325        }
326        Ok(filled)
327    }
328
329    fn len_hint(&self) -> Option<u64> {
330        self.eof.then_some(self.watermark)
331    }
332
333    fn read_many_at(&mut self, reqs: &mut [(u64, &mut [u8])]) -> Result<(), Self::Error> {
334        // Serve in stream order so the window only moves forward.
335        reqs.sort_unstable_by_key(|(offset, _)| *offset);
336        for (offset, out) in reqs.iter_mut() {
337            self.read_exact_at(*offset, out)?;
338        }
339        Ok(())
340    }
341}
342
343impl BufferPolicy {
344    /// Byte cap of this policy (window size or spool limit).
345    pub const fn max_bytes(&self) -> usize {
346        self.cap()
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use std::io::{self, Read};
354
355    #[test]
356    fn checked_policy_never_clamps_required_history() {
357        for cap in [0, 1, 16, 65_536] {
358            assert!(
359                BufferPolicy::try_for_pattern(AccessPattern::Sequential, cap)
360                    .unwrap()
361                    .cap()
362                    <= cap
363            );
364            for required in [0, 1, 16, 65_536, u64::MAX] {
365                let result = BufferPolicy::try_for_pattern(
366                    AccessPattern::BoundedBacktrack {
367                        window_bytes: required,
368                    },
369                    cap,
370                );
371                if required > cap as u64 {
372                    assert!(
373                        matches!(result, Err(SourceError::BeyondLimit { needed, limit })
374                        if needed == required && limit == cap as u64)
375                    );
376                } else {
377                    assert_eq!(
378                        result.unwrap(),
379                        BufferPolicy::Window {
380                            window_bytes: required as usize
381                        }
382                    );
383                }
384            }
385        }
386    }
387
388    #[test]
389    fn forward_reads_retain_only_the_window_even_when_output_is_larger() {
390        let input: Vec<u8> = (0..101).collect();
391        for window in [0, 1, 7, 16, 128] {
392            for chunk in [1, 7, 17, 64, 128] {
393                let mut source = BufferedReadAt::new(
394                    io::Cursor::new(&input),
395                    BufferPolicy::Window {
396                        window_bytes: window,
397                    },
398                );
399                let mut output = Vec::new();
400                let mut buf = alloc::vec![0; chunk];
401                loop {
402                    let n = source.read_at(output.len() as u64, &mut buf).unwrap();
403                    output.extend_from_slice(&buf[..n]);
404                    assert!(source.buf.len() <= window);
405                    if n == 0 {
406                        break;
407                    }
408                }
409                assert_eq!(output, input);
410                let retained = window.min(input.len());
411                let offset = input.len() - retained;
412                let mut back = alloc::vec![0; retained];
413                source.read_exact_at(offset as u64, &mut back).unwrap();
414                assert_eq!(back, input[offset..]);
415                if offset != 0 {
416                    assert!(matches!(
417                        source.read_at(offset as u64 - 1, &mut [0; 1]),
418                        Err(StdSourceError::Source(SourceError::BeyondWindow { .. }))
419                    ));
420                }
421            }
422        }
423    }
424
425    #[test]
426    fn allocation_refusal_precedes_input_consumption() {
427        let mut source = BufferedReadAt::new(
428            io::repeat(0),
429            BufferPolicy::Spool {
430                max_bytes: usize::MAX,
431            },
432        );
433        assert!(matches!(
434            source.reserve_retained(usize::MAX),
435            Err(StdSourceError::Allocation { .. })
436        ));
437        assert_eq!(source.watermark(), 0);
438        assert!(source.buf.is_empty());
439    }
440
441    struct InterruptedOnce(bool);
442    impl Read for InterruptedOnce {
443        fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
444            if !self.0 {
445                self.0 = true;
446                return Err(io::ErrorKind::Interrupted.into());
447            }
448            out.fill(7);
449            Ok(out.len())
450        }
451    }
452
453    #[test]
454    fn interrupted_reads_do_not_lose_data_or_progress() {
455        let mut source = BufferedReadAt::new(
456            InterruptedOnce(false),
457            BufferPolicy::Window { window_bytes: 2 },
458        );
459        let mut out = [0; 8];
460        source.read_exact_at(0, &mut out).unwrap();
461        assert_eq!(out, [7; 8]);
462        assert_eq!(source.watermark(), 8);
463        assert_eq!(source.retained_bytes(), 2);
464    }
465}