Skip to main content

hdf5_pure/
source.rs

1//! Random-access byte sources for the reader: the [`Source`] trait and its
2//! backends.
3//!
4//! # Why this exists
5//!
6//! Today the reader holds the **entire file** in one `Vec<u8>` ([`crate::File`])
7//! and threads a `&[u8]` of that whole buffer through every parser, indexing it
8//! by absolute offset. That is simple and fast, but it has a hard ceiling: a
9//! file larger than the process address space cannot be loaded at all. On a
10//! 32-bit host (`usize` is 32 bits, ~4 GiB of usable address space) a 20 GiB
11//! HDF5 file produced on a 64-bit machine simply cannot be `read()` into a
12//! `Vec`, no matter how carefully offsets are converted (see [`crate::convert`],
13//! which makes the *narrowing* safe but cannot conjure address space). This is
14//! the core of issue #27.
15//!
16//! HDF5 metadata (superblock, object headers, B-trees, heaps) is tiny relative
17//! to the dataset payload, and the format is designed for random access by
18//! absolute file offset. So the durable fix is to read **on demand** from a
19//! seekable source instead of materializing the whole file: keep only a small
20//! working set (the metadata being parsed, plus the data chunks currently being
21//! decompressed) resident at any time.
22//!
23//! [`Source`] is that abstraction. It is deliberately minimal and
24//! `no_std`/`alloc`-friendly (the trait and the in-memory backends need no
25//! `std`), so it works on the same constrained targets the rest of the crate
26//! supports.
27//!
28//! # Backends
29//!
30//! - [`BytesSource`] — wraps any owned-or-borrowed byte buffer (`Vec<u8>`,
31//!   `&[u8]`, `Box<[u8]>`, `Arc<[u8]>`, …). This is the in-memory model the
32//!   current [`crate::File`] uses; it is always available, including on WASM and
33//!   `no_std`.
34//! - [`ReadSeekSource`] (`std` only) — wraps any `Read + Seek` (a
35//!   [`std::fs::File`], a `Cursor`, etc.) and reads bytes lazily via
36//!   `seek` + `read`. This is the backend that lets a 32-bit host read a file
37//!   far larger than its address space, because it never holds more than the
38//!   bytes a single `read_at` requests.
39//!
40//! A windowed `mmap` backend (an optional, `std`-plus-OS feature pulling a crate
41//! like `memmap2`) is a natural future addition behind this same trait. Note
42//! that a *whole-file* mmap does **not** solve the 32-bit problem — mapping
43//! 20 GiB still needs 20 GiB of virtual address space — so only a *windowed*
44//! mmap (map/unmap sub-ranges) or plain `Read + Seek` works there. It is left
45//! out for now rather than adding a dependency speculatively.
46//!
47//! # Migration plan (this is the first increment)
48//!
49//! The reader is not yet ported onto `Source`; that is a staged effort
50//! tracked by issue #27. This module is the foundation the later stages build
51//! on. The intended path, smallest-risk first:
52//!
53//! 1. **Foundation (this commit).** Land the trait + in-memory and `Read+Seek`
54//!    backends + tests. Nothing in the existing reader changes, so there is no
55//!    risk to the current in-memory path.
56//! 2. **A cursor.** Introduce a small `Cursor<'a>` over a `&'a dyn Source`
57//!    that offers the `read_offset` / `read_length` / "give me bytes at
58//!    `[off, off+len)`" idioms the parsers already use, with the checked
59//!    [`crate::convert`] conversions built in. The ~15 duplicated per-module
60//!    `read_offset` helpers collapse into it.
61//! 3. **Bulk path first.** Port the contiguous and chunked **data** readers
62//!    (`data_read`, `chunked_read`, `parallel_read`) to fetch each chunk via
63//!    [`Source::read_at`] instead of slicing the whole-file buffer. This is
64//!    self-contained (a chunk is already `{address, size}`) and captures most of
65//!    the memory win, since the data payload is what is actually large. The
66//!    zero-copy `&'a [u8]` return of `data_read::read_raw_data_zerocopy` becomes
67//!    an owned `Vec<u8>` / `Cow` here, since a window may be evicted.
68//! 4. **Metadata parsers.** Migrate the remaining ~56 functions that take a
69//!    whole-file `&[u8]` to borrow the cursor, reading each bounded structure
70//!    into a small buffer on demand.
71//! 5. **Entry point.** Add `File::open_streaming` / `File::from_source` that
72//!    construct a [`crate::File`] backed by a [`ReadSeekSource`], plus SWMR
73//!    `refresh` over a live streaming handle (the consistent-snapshot semantics
74//!    need care over a source that is being appended to).
75//!
76//! Until step 5 lands, opening a file still buffers it; this module is the
77//! building block that makes the staged migration possible without a single
78//! risky rewrite.
79
80#[cfg(not(feature = "std"))]
81use alloc::{vec, vec::Vec};
82
83use crate::convert::TryToUsize;
84use crate::error::FormatError;
85
86/// Default maximum size of one entry admitted to a streaming metadata cache.
87pub const DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES: usize = 64 * 1024;
88
89/// Initial metadata-cache settings for streaming file access.
90///
91/// This is the `hdf5-pure` counterpart to the memory-budget portion of HDF5's
92/// `H5Pset_mdc_config`: it bounds the bytes retained for parsed metadata reads
93/// while a file is opened through [`crate::File::open_streaming_with_options`].
94/// Raw dataset payload reads use `Source::read_exact_at` and are not
95/// admitted to this cache.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct MetadataCacheConfig {
98    max_bytes: usize,
99    max_entry_bytes: usize,
100}
101
102impl MetadataCacheConfig {
103    /// Create a metadata cache with the given total byte budget.
104    ///
105    /// Individual cached reads are capped at
106    /// `DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES` (64 KiB) by default so one large
107    /// heap or index block cannot monopolize the cache. Use
108    /// [`with_max_entry_bytes`](Self::with_max_entry_bytes) to change that.
109    pub const fn new(max_bytes: usize) -> Self {
110        let max_entry_bytes = if max_bytes < DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES {
111            max_bytes
112        } else {
113            DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES
114        };
115        Self {
116            max_bytes,
117            max_entry_bytes,
118        }
119    }
120
121    /// Disable metadata read caching.
122    pub const fn disabled() -> Self {
123        Self {
124            max_bytes: 0,
125            max_entry_bytes: 0,
126        }
127    }
128
129    /// Set the maximum size of a single metadata read admitted to the cache.
130    pub const fn with_max_entry_bytes(mut self, max_entry_bytes: usize) -> Self {
131        self.max_entry_bytes = max_entry_bytes;
132        self
133    }
134
135    /// Return the total metadata-cache byte budget.
136    pub const fn max_bytes(&self) -> usize {
137        self.max_bytes
138    }
139
140    /// Return the maximum size of one cached metadata entry.
141    pub const fn max_entry_bytes(&self) -> usize {
142        self.max_entry_bytes
143    }
144
145    /// Whether metadata read caching is enabled.
146    pub const fn is_enabled(&self) -> bool {
147        self.max_bytes > 0 && self.max_entry_bytes > 0
148    }
149}
150
151impl Default for MetadataCacheConfig {
152    fn default() -> Self {
153        Self::disabled()
154    }
155}
156
157/// A random-access, read-only source of the bytes of an HDF5 file.
158///
159/// Offsets are `u64` (HDF5's native address width); lengths of individual reads
160/// are `usize` (they must fit in a caller-provided buffer). Implementations must
161/// either fill the whole request or return an error — a short read is always an
162/// error, never silently truncated.
163pub trait Source {
164    /// Total number of bytes the source can supply.
165    fn len(&self) -> u64;
166
167    /// Whether the source is empty (zero bytes).
168    fn is_empty(&self) -> bool {
169        self.len() == 0
170    }
171
172    /// Read exactly `buf.len()` bytes starting at absolute offset `offset`,
173    /// filling `buf`.
174    ///
175    /// Returns [`FormatError::UnexpectedEof`] if fewer than `buf.len()` bytes are
176    /// available at `offset`, [`FormatError::OffsetOverflow`] if
177    /// `offset + buf.len()` overflows, [`FormatError::ValueTooLargeForPlatform`]
178    /// if `offset` does not fit this platform's `usize` (for in-memory
179    /// backends), or [`FormatError::Source`] for a backend I/O failure.
180    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
181
182    /// Read `len` bytes starting at `offset` into a freshly allocated `Vec`.
183    ///
184    /// Convenience wrapper over [`read_at`](Source::read_at) for callers that
185    /// want an owned buffer; the lazy backends keep no more than this resident.
186    ///
187    /// The request is bounds-checked against [`len`](Source::len) *before* the
188    /// buffer is allocated. The metadata parsers feed `len` values straight from
189    /// the file (a chunk-0 body size, a continuation-block length, a heap object
190    /// size), so a malformed file could otherwise name a multi-gigabyte length
191    /// and make this reserve `vec![0u8; len]` up front only for the read to fail
192    /// EOF anyway — a cheap denial of service. Rejecting an out-of-range request
193    /// before allocating avoids that; the error returned is identical to the one
194    /// the underlying [`read_at`](Source::read_at) would have produced.
195    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
196        let end = offset
197            .checked_add(len as u64)
198            .ok_or(FormatError::OffsetOverflow {
199                offset,
200                length: len as u64,
201            })?;
202        if end > self.len() {
203            return Err(FormatError::UnexpectedEof {
204                expected: end.to_usize().unwrap_or(usize::MAX),
205                available: self.len().to_usize().unwrap_or(usize::MAX),
206            });
207        }
208        let mut buf = vec![0u8; len];
209        self.read_at(offset, &mut buf)?;
210        Ok(buf)
211    }
212
213    /// Read metadata bytes, allowing source implementations to apply a bounded
214    /// metadata cache.
215    ///
216    /// The default implementation performs an uncached exact read. Raw dataset
217    /// payload readers intentionally call [`read_exact_at`](Self::read_exact_at)
218    /// instead, so a metadata cache does not retain user data chunks.
219    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
220        self.read_exact_at(offset, len)
221    }
222}
223
224// Forward `Source` through references and boxes so `&S`, `&dyn Source`,
225// and `Box<dyn Source>` are all usable wherever an `S: Source` is.
226impl<S: Source + ?Sized> Source for &S {
227    fn len(&self) -> u64 {
228        (**self).len()
229    }
230    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
231        (**self).read_at(offset, buf)
232    }
233
234    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
235        (**self).read_exact_at(offset, len)
236    }
237
238    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
239        (**self).read_metadata_at(offset, len)
240    }
241}
242
243#[cfg(feature = "std")]
244impl<S: Source + ?Sized> Source for std::boxed::Box<S> {
245    fn len(&self) -> u64 {
246        (**self).len()
247    }
248    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
249        (**self).read_at(offset, buf)
250    }
251
252    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
253        (**self).read_exact_at(offset, len)
254    }
255
256    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
257        (**self).read_metadata_at(offset, len)
258    }
259}
260
261// ---------------------------------------------------------------------------
262// Base-relative view
263// ---------------------------------------------------------------------------
264
265/// A [`Source`] view shifted forward by a base address: every read at a
266/// base-relative `offset` is served from `inner` at `offset + base`.
267///
268/// Used wherever on-disk addresses are stored relative to the superblock's base
269/// address rather than absolutely — the data layout's contiguous-data, chunk-index,
270/// and chunk addresses on a file with a userblock, and the fractal-heap address in
271/// an Attribute Info message. Presenting this shifted view lets those relative
272/// addresses index it directly, exactly as an in-memory path slices the buffer at
273/// `base`. For a plain (base-0) file it is the identity.
274///
275/// `len`/`read_at` shift by the base; `read_metadata_at` forwards to the inner
276/// source at the *absolute* offset so the inner source's metadata cache is shared
277/// (a chunk-index walk on a streaming userblock file would otherwise re-read every
278/// node), while payload reads keep the default uncached `read_exact_at` so user
279/// data does not evict metadata.
280pub(crate) struct BaseOffsetSource<'a, S: Source + ?Sized> {
281    pub(crate) inner: &'a S,
282    pub(crate) base: u64,
283}
284
285impl<S: Source + ?Sized> Source for BaseOffsetSource<'_, S> {
286    fn len(&self) -> u64 {
287        self.inner.len().saturating_sub(self.base)
288    }
289
290    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
291        let abs = offset
292            .checked_add(self.base)
293            .ok_or(FormatError::OffsetOverflow {
294                offset,
295                length: buf.len() as u64,
296            })?;
297        self.inner.read_at(abs, buf)
298    }
299
300    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
301        let abs = offset
302            .checked_add(self.base)
303            .ok_or(FormatError::OffsetOverflow {
304                offset,
305                length: len as u64,
306            })?;
307        self.inner.read_metadata_at(abs, len)
308    }
309}
310
311// ---------------------------------------------------------------------------
312// In-memory backend
313// ---------------------------------------------------------------------------
314
315/// A [`Source`] over an in-memory byte buffer: anything that is
316/// `AsRef<[u8]>` (`Vec<u8>`, `&[u8]`, `Box<[u8]>`, `Arc<[u8]>`, …).
317///
318/// This is the always-available backend that mirrors the crate's current
319/// in-memory model, usable on WASM and `no_std`.
320#[derive(Debug, Clone, Copy)]
321pub struct BytesSource<T>(pub T);
322
323impl<T: AsRef<[u8]>> BytesSource<T> {
324    /// Wrap an in-memory byte buffer.
325    pub fn new(bytes: T) -> Self {
326        BytesSource(bytes)
327    }
328}
329
330impl<T: AsRef<[u8]>> Source for BytesSource<T> {
331    fn len(&self) -> u64 {
332        self.0.as_ref().len() as u64
333    }
334
335    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
336        let bytes = self.0.as_ref();
337        let start = offset.to_usize()?;
338        let end = start
339            .checked_add(buf.len())
340            .ok_or(FormatError::OffsetOverflow {
341                offset,
342                length: buf.len() as u64,
343            })?;
344        if end > bytes.len() {
345            return Err(FormatError::UnexpectedEof {
346                expected: end,
347                available: bytes.len(),
348            });
349        }
350        buf.copy_from_slice(&bytes[start..end]);
351        Ok(())
352    }
353}
354
355// ---------------------------------------------------------------------------
356// Metadata-caching wrapper (std)
357// ---------------------------------------------------------------------------
358
359#[cfg(feature = "std")]
360struct CachedMetadataRead {
361    offset: u64,
362    len: usize,
363    bytes: Vec<u8>,
364    last_access: u64,
365}
366
367/// The bounded LRU store behind [`MetadataCachingSource`], also embedded
368/// directly by the mirrorless write image (`crate::image::HandleImage`), which
369/// must invalidate entries that overlap an in-place write.
370#[cfg(feature = "std")]
371pub(crate) struct MetadataReadCache {
372    entries: Vec<CachedMetadataRead>,
373    current_bytes: usize,
374    tick: u64,
375}
376
377#[cfg(feature = "std")]
378impl MetadataReadCache {
379    pub(crate) fn new() -> Self {
380        Self {
381            entries: Vec::new(),
382            current_bytes: 0,
383            tick: 0,
384        }
385    }
386
387    /// Drop every cached entry that overlaps `[offset, offset + len)`, so a
388    /// read after an in-place write never observes stale bytes.
389    pub(crate) fn invalidate_overlapping(&mut self, offset: u64, len: usize) {
390        if len == 0 {
391            return;
392        }
393        let end = offset.saturating_add(len as u64);
394        let mut removed = 0usize;
395        self.entries.retain(|entry| {
396            let entry_end = entry.offset.saturating_add(entry.len as u64);
397            let overlaps = entry.offset < end && offset < entry_end;
398            if overlaps {
399                removed += entry.bytes.len();
400            }
401            !overlaps
402        });
403        self.current_bytes -= removed;
404    }
405
406    pub(crate) fn get(&mut self, offset: u64, len: usize) -> Option<Vec<u8>> {
407        self.tick = self.tick.wrapping_add(1);
408        let tick = self.tick;
409        for entry in &mut self.entries {
410            if entry.offset == offset && entry.len == len {
411                entry.last_access = tick;
412                return Some(entry.bytes.clone());
413            }
414        }
415        None
416    }
417
418    pub(crate) fn insert(&mut self, offset: u64, len: usize, bytes: Vec<u8>, max_bytes: usize) {
419        if len == 0 || bytes.len() > max_bytes {
420            return;
421        }
422
423        self.tick = self.tick.wrapping_add(1);
424        let tick = self.tick;
425
426        for entry in &mut self.entries {
427            if entry.offset == offset && entry.len == len {
428                self.current_bytes = self.current_bytes - entry.bytes.len() + bytes.len();
429                entry.bytes = bytes;
430                entry.last_access = tick;
431                self.evict_to_budget(max_bytes);
432                return;
433            }
434        }
435
436        self.current_bytes += bytes.len();
437        self.entries.push(CachedMetadataRead {
438            offset,
439            len,
440            bytes,
441            last_access: tick,
442        });
443        self.evict_to_budget(max_bytes);
444    }
445
446    fn evict_to_budget(&mut self, max_bytes: usize) {
447        while self.current_bytes > max_bytes && !self.entries.is_empty() {
448            let lru_idx = self
449                .entries
450                .iter()
451                .enumerate()
452                .min_by_key(|(_, entry)| entry.last_access)
453                .map(|(idx, _)| idx)
454                .unwrap();
455            let removed = self.entries.swap_remove(lru_idx);
456            self.current_bytes -= removed.bytes.len();
457        }
458    }
459}
460
461/// A [`Source`] wrapper with a bounded cache for metadata reads.
462///
463/// The wrapper only caches calls to [`Source::read_metadata_at`]. Plain
464/// [`Source::read_exact_at`] calls still go directly to the inner source,
465/// which keeps raw dataset payloads out of the metadata cache.
466#[cfg(feature = "std")]
467pub struct MetadataCachingSource<S> {
468    inner: S,
469    config: MetadataCacheConfig,
470    cache: std::sync::Mutex<MetadataReadCache>,
471}
472
473#[cfg(feature = "std")]
474impl<S> MetadataCachingSource<S> {
475    /// Wrap a source with the supplied metadata-cache configuration.
476    pub fn new(inner: S, config: MetadataCacheConfig) -> Self {
477        Self {
478            inner,
479            config,
480            cache: std::sync::Mutex::new(MetadataReadCache::new()),
481        }
482    }
483}
484
485#[cfg(feature = "std")]
486impl<S: Source> Source for MetadataCachingSource<S> {
487    fn len(&self) -> u64 {
488        self.inner.len()
489    }
490
491    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
492        self.inner.read_at(offset, buf)
493    }
494
495    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
496        self.inner.read_exact_at(offset, len)
497    }
498
499    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
500        if !self.config.is_enabled()
501            || len == 0
502            || len > self.config.max_entry_bytes
503            || len > self.config.max_bytes
504        {
505            return self.inner.read_metadata_at(offset, len);
506        }
507
508        if let Some(bytes) = self
509            .cache
510            .lock()
511            .unwrap_or_else(std::sync::PoisonError::into_inner)
512            .get(offset, len)
513        {
514            return Ok(bytes);
515        }
516
517        let bytes = self.inner.read_metadata_at(offset, len)?;
518        self.cache
519            .lock()
520            .unwrap_or_else(std::sync::PoisonError::into_inner)
521            .insert(offset, len, bytes.clone(), self.config.max_bytes);
522        Ok(bytes)
523    }
524}
525
526// ---------------------------------------------------------------------------
527// Read + Seek backend (std)
528// ---------------------------------------------------------------------------
529
530/// A lazy [`Source`] over any [`std::io::Read`] + [`std::io::Seek`] (a
531/// [`std::fs::File`], an in-memory `Cursor`, etc.).
532///
533/// Each [`read_at`](Source::read_at) performs a `seek` + `read_exact`, so no
534/// more than the requested bytes are ever held in memory. This is the backend
535/// that lets a 32-bit host read a file larger than its address space: the
536/// metadata and one working chunk fit even when the whole file does not.
537///
538/// The reader is wrapped in a [`std::sync::Mutex`] so the source is `Sync` and
539/// `read_at` can take `&self` (seeking needs `&mut` access). This serializes
540/// concurrent reads, which is correct though not maximally parallel; a future
541/// backend can use positioned reads (`pread`/`seek_read`) to avoid the lock.
542#[cfg(feature = "std")]
543pub struct ReadSeekSource<R> {
544    inner: std::sync::Mutex<R>,
545    len: u64,
546}
547
548#[cfg(feature = "std")]
549impl<R: std::io::Read + std::io::Seek> ReadSeekSource<R> {
550    /// Wrap a `Read + Seek`, measuring its length by seeking to the end (then
551    /// restoring nothing — every `read_at` seeks absolutely anyway).
552    pub fn new(mut reader: R) -> Result<Self, FormatError> {
553        let len = reader
554            .seek(std::io::SeekFrom::End(0))
555            .map_err(|e| FormatError::Source(format_io(&e)))?;
556        Ok(ReadSeekSource {
557            inner: std::sync::Mutex::new(reader),
558            len,
559        })
560    }
561}
562
563#[cfg(feature = "std")]
564impl<R: std::io::Read + std::io::Seek> Source for ReadSeekSource<R> {
565    fn len(&self) -> u64 {
566        self.len
567    }
568
569    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
570        // Bound-check up front so a request past EOF is a clean error rather
571        // than a backend-specific short read.
572        let end = offset
573            .checked_add(buf.len() as u64)
574            .ok_or(FormatError::OffsetOverflow {
575                offset,
576                length: buf.len() as u64,
577            })?;
578        if end > self.len {
579            return Err(FormatError::UnexpectedEof {
580                // `expected`/`available` are byte counts; report them as the
581                // best `usize` we can without truncating on a 32-bit host.
582                expected: end.to_usize().unwrap_or(usize::MAX),
583                available: self.len.to_usize().unwrap_or(usize::MAX),
584            });
585        }
586        let mut guard = self
587            .inner
588            .lock()
589            .unwrap_or_else(std::sync::PoisonError::into_inner);
590        guard
591            .seek(std::io::SeekFrom::Start(offset))
592            .map_err(|e| FormatError::Source(format_io(&e)))?;
593        guard
594            .read_exact(buf)
595            .map_err(|e| FormatError::Source(format_io(&e)))?;
596        Ok(())
597    }
598}
599
600/// Render an `std::io::Error` to a short owned string for [`FormatError::Source`]
601/// (which is `no_std`-friendly and cannot hold the error itself).
602#[cfg(feature = "std")]
603fn format_io(e: &std::io::Error) -> std::string::String {
604    std::format!("{e}")
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610
611    #[cfg(not(feature = "std"))]
612    use alloc::vec;
613
614    #[test]
615    fn bytes_source_reads_and_reports_len() {
616        let data = (0u8..=255).collect::<Vec<u8>>();
617        let src = BytesSource::new(data.clone());
618        assert_eq!(src.len(), 256);
619        assert!(!src.is_empty());
620
621        let mut buf = [0u8; 4];
622        src.read_at(10, &mut buf).unwrap();
623        assert_eq!(buf, [10, 11, 12, 13]);
624
625        let owned = src.read_exact_at(250, 6).unwrap();
626        assert_eq!(owned, vec![250, 251, 252, 253, 254, 255]);
627    }
628
629    #[test]
630    fn bytes_source_short_read_is_eof() {
631        let src = BytesSource::new(vec![1u8, 2, 3]);
632        let mut buf = [0u8; 4];
633        let err = src.read_at(0, &mut buf).unwrap_err();
634        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
635        // Reading exactly to the end is fine.
636        let mut ok = [0u8; 3];
637        src.read_at(0, &mut ok).unwrap();
638        assert_eq!(ok, [1, 2, 3]);
639    }
640
641    #[test]
642    fn bytes_source_offset_past_end_is_eof() {
643        let src = BytesSource::new(vec![0u8; 8]);
644        let mut buf = [0u8; 1];
645        assert!(matches!(
646            src.read_at(8, &mut buf).unwrap_err(),
647            FormatError::UnexpectedEof { .. }
648        ));
649        // Zero-length read at EOF succeeds.
650        src.read_at(8, &mut []).unwrap();
651    }
652
653    #[test]
654    fn read_exact_at_rejects_oversized_len_without_allocating() {
655        // A length far larger than the source must error cleanly rather than
656        // attempt to reserve the buffer first. Before the pre-allocation bounds
657        // check, this called `vec![0u8; usize::MAX]` and aborted the process.
658        let src = BytesSource::new(vec![1u8, 2, 3, 4]);
659        assert!(matches!(
660            src.read_exact_at(0, usize::MAX).unwrap_err(),
661            FormatError::UnexpectedEof { .. }
662        ));
663        // A read that fits is unaffected.
664        assert_eq!(src.read_exact_at(1, 3).unwrap(), vec![2, 3, 4]);
665    }
666
667    #[test]
668    fn empty_source() {
669        let src = BytesSource::new(Vec::<u8>::new());
670        assert_eq!(src.len(), 0);
671        assert!(src.is_empty());
672    }
673
674    #[test]
675    fn forwarding_through_reference() {
676        let src = BytesSource::new(vec![9u8, 8, 7]);
677        let r: &dyn Source = &src;
678        let mut buf = [0u8; 2];
679        r.read_at(1, &mut buf).unwrap();
680        assert_eq!(buf, [8, 7]);
681    }
682
683    #[test]
684    fn forwarding_through_reference_preserves_metadata_reads() {
685        use core::cell::Cell;
686
687        struct MetadataSource {
688            metadata_reads: Cell<usize>,
689        }
690
691        impl Source for MetadataSource {
692            fn len(&self) -> u64 {
693                16
694            }
695
696            fn read_at(&self, _offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
697                buf.fill(0);
698                Ok(())
699            }
700
701            fn read_metadata_at(&self, _offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
702                self.metadata_reads.set(self.metadata_reads.get() + 1);
703                Ok(vec![0xAB; len])
704            }
705        }
706
707        fn read_metadata_via_trait<T: Source>(source: T) -> Vec<u8> {
708            source.read_metadata_at(4, 3).unwrap()
709        }
710
711        let source = MetadataSource {
712            metadata_reads: Cell::new(0),
713        };
714
715        assert_eq!(read_metadata_via_trait(&source), vec![0xAB; 3]);
716        assert_eq!(source.metadata_reads.get(), 1);
717    }
718
719    #[cfg(feature = "std")]
720    #[test]
721    fn metadata_cache_caches_only_metadata_reads() {
722        use std::sync::{
723            Arc,
724            atomic::{AtomicUsize, Ordering},
725        };
726
727        struct CountingSource {
728            data: Vec<u8>,
729            reads: Arc<AtomicUsize>,
730        }
731
732        impl Source for CountingSource {
733            fn len(&self) -> u64 {
734                self.data.len() as u64
735            }
736
737            fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
738                self.reads.fetch_add(1, Ordering::SeqCst);
739                BytesSource::new(&self.data).read_at(offset, buf)
740            }
741        }
742
743        let reads = Arc::new(AtomicUsize::new(0));
744        let source = MetadataCachingSource::new(
745            CountingSource {
746                data: (0u8..16).collect(),
747                reads: Arc::clone(&reads),
748            },
749            MetadataCacheConfig::new(16),
750        );
751
752        assert_eq!(source.read_metadata_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
753        assert_eq!(source.read_metadata_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
754        assert_eq!(reads.load(Ordering::SeqCst), 1);
755
756        assert_eq!(source.read_exact_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
757        assert_eq!(source.read_exact_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
758        assert_eq!(reads.load(Ordering::SeqCst), 3);
759    }
760
761    #[cfg(feature = "std")]
762    #[test]
763    fn read_seek_source_matches_in_memory() {
764        use std::io::Cursor;
765        let data = (0u8..200).collect::<Vec<u8>>();
766        let mem = BytesSource::new(data.clone());
767        let seek = ReadSeekSource::new(Cursor::new(data.clone())).unwrap();
768        assert_eq!(seek.len(), mem.len());
769
770        // Every read_at against the lazy source matches the in-memory source.
771        for &(off, len) in &[(0u64, 1usize), (5, 10), (199, 1), (100, 50)] {
772            let a = mem.read_exact_at(off, len).unwrap();
773            let b = seek.read_exact_at(off, len).unwrap();
774            assert_eq!(a, b, "mismatch at offset {off} len {len}");
775        }
776    }
777
778    #[cfg(feature = "std")]
779    #[test]
780    fn read_seek_source_past_end_is_error() {
781        use std::io::Cursor;
782        let seek = ReadSeekSource::new(Cursor::new(vec![1u8, 2, 3, 4])).unwrap();
783        let mut buf = [0u8; 3];
784        assert!(matches!(
785            seek.read_at(2, &mut buf).unwrap_err(),
786            FormatError::UnexpectedEof { .. }
787        ));
788    }
789
790    #[cfg(feature = "std")]
791    #[test]
792    fn read_seek_source_is_sync() {
793        // Compile-time assertion that the std backend is Send + Sync so it can
794        // back a parallel reader.
795        fn assert_send_sync<T: Send + Sync>() {}
796        assert_send_sync::<ReadSeekSource<std::io::Cursor<Vec<u8>>>>();
797    }
798}