Skip to main content

hdf5_pure/
source.rs

1//! Random-access byte sources for the reader: the [`FileSource`] 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//! [`FileSource`] 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 `FileSource`; 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 FileSource`
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//!    [`FileSource::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 [`FileSource::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`] by default so one large heap
107    /// 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 FileSource {
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`](FileSource::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`](FileSource::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`](FileSource::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 `FileSource` through references and boxes so `&S`, `&dyn FileSource`,
225// and `Box<dyn FileSource>` are all usable wherever an `S: FileSource` is.
226impl<S: FileSource + ?Sized> FileSource 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: FileSource + ?Sized> FileSource 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// In-memory backend
263// ---------------------------------------------------------------------------
264
265/// A [`FileSource`] over an in-memory byte buffer: anything that is
266/// `AsRef<[u8]>` (`Vec<u8>`, `&[u8]`, `Box<[u8]>`, `Arc<[u8]>`, …).
267///
268/// This is the always-available backend that mirrors the crate's current
269/// in-memory model, usable on WASM and `no_std`.
270#[derive(Debug, Clone, Copy)]
271pub struct BytesSource<T>(pub T);
272
273impl<T: AsRef<[u8]>> BytesSource<T> {
274    /// Wrap an in-memory byte buffer.
275    pub fn new(bytes: T) -> Self {
276        BytesSource(bytes)
277    }
278}
279
280impl<T: AsRef<[u8]>> FileSource for BytesSource<T> {
281    fn len(&self) -> u64 {
282        self.0.as_ref().len() as u64
283    }
284
285    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
286        let bytes = self.0.as_ref();
287        let start = offset.to_usize()?;
288        let end = start
289            .checked_add(buf.len())
290            .ok_or(FormatError::OffsetOverflow {
291                offset,
292                length: buf.len() as u64,
293            })?;
294        if end > bytes.len() {
295            return Err(FormatError::UnexpectedEof {
296                expected: end,
297                available: bytes.len(),
298            });
299        }
300        buf.copy_from_slice(&bytes[start..end]);
301        Ok(())
302    }
303}
304
305// ---------------------------------------------------------------------------
306// Metadata-caching wrapper (std)
307// ---------------------------------------------------------------------------
308
309#[cfg(feature = "std")]
310struct CachedMetadataRead {
311    offset: u64,
312    len: usize,
313    bytes: Vec<u8>,
314    last_access: u64,
315}
316
317#[cfg(feature = "std")]
318struct MetadataReadCache {
319    entries: Vec<CachedMetadataRead>,
320    current_bytes: usize,
321    tick: u64,
322}
323
324#[cfg(feature = "std")]
325impl MetadataReadCache {
326    fn new() -> Self {
327        Self {
328            entries: Vec::new(),
329            current_bytes: 0,
330            tick: 0,
331        }
332    }
333
334    fn get(&mut self, offset: u64, len: usize) -> Option<Vec<u8>> {
335        self.tick = self.tick.wrapping_add(1);
336        let tick = self.tick;
337        for entry in &mut self.entries {
338            if entry.offset == offset && entry.len == len {
339                entry.last_access = tick;
340                return Some(entry.bytes.clone());
341            }
342        }
343        None
344    }
345
346    fn insert(&mut self, offset: u64, len: usize, bytes: Vec<u8>, max_bytes: usize) {
347        if len == 0 || bytes.len() > max_bytes {
348            return;
349        }
350
351        self.tick = self.tick.wrapping_add(1);
352        let tick = self.tick;
353
354        for entry in &mut self.entries {
355            if entry.offset == offset && entry.len == len {
356                self.current_bytes = self.current_bytes - entry.bytes.len() + bytes.len();
357                entry.bytes = bytes;
358                entry.last_access = tick;
359                self.evict_to_budget(max_bytes);
360                return;
361            }
362        }
363
364        self.current_bytes += bytes.len();
365        self.entries.push(CachedMetadataRead {
366            offset,
367            len,
368            bytes,
369            last_access: tick,
370        });
371        self.evict_to_budget(max_bytes);
372    }
373
374    fn evict_to_budget(&mut self, max_bytes: usize) {
375        while self.current_bytes > max_bytes && !self.entries.is_empty() {
376            let lru_idx = self
377                .entries
378                .iter()
379                .enumerate()
380                .min_by_key(|(_, entry)| entry.last_access)
381                .map(|(idx, _)| idx)
382                .unwrap();
383            let removed = self.entries.swap_remove(lru_idx);
384            self.current_bytes -= removed.bytes.len();
385        }
386    }
387}
388
389/// A [`FileSource`] wrapper with a bounded cache for metadata reads.
390///
391/// The wrapper only caches calls to [`FileSource::read_metadata_at`]. Plain
392/// [`FileSource::read_exact_at`] calls still go directly to the inner source,
393/// which keeps raw dataset payloads out of the metadata cache.
394#[cfg(feature = "std")]
395pub struct MetadataCachingSource<S> {
396    inner: S,
397    config: MetadataCacheConfig,
398    cache: std::sync::Mutex<MetadataReadCache>,
399}
400
401#[cfg(feature = "std")]
402impl<S> MetadataCachingSource<S> {
403    /// Wrap a source with the supplied metadata-cache configuration.
404    pub fn new(inner: S, config: MetadataCacheConfig) -> Self {
405        Self {
406            inner,
407            config,
408            cache: std::sync::Mutex::new(MetadataReadCache::new()),
409        }
410    }
411}
412
413#[cfg(feature = "std")]
414impl<S: FileSource> FileSource for MetadataCachingSource<S> {
415    fn len(&self) -> u64 {
416        self.inner.len()
417    }
418
419    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
420        self.inner.read_at(offset, buf)
421    }
422
423    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
424        self.inner.read_exact_at(offset, len)
425    }
426
427    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
428        if !self.config.is_enabled()
429            || len == 0
430            || len > self.config.max_entry_bytes
431            || len > self.config.max_bytes
432        {
433            return self.inner.read_metadata_at(offset, len);
434        }
435
436        if let Some(bytes) = self
437            .cache
438            .lock()
439            .unwrap_or_else(std::sync::PoisonError::into_inner)
440            .get(offset, len)
441        {
442            return Ok(bytes);
443        }
444
445        let bytes = self.inner.read_metadata_at(offset, len)?;
446        self.cache
447            .lock()
448            .unwrap_or_else(std::sync::PoisonError::into_inner)
449            .insert(offset, len, bytes.clone(), self.config.max_bytes);
450        Ok(bytes)
451    }
452}
453
454// ---------------------------------------------------------------------------
455// Read + Seek backend (std)
456// ---------------------------------------------------------------------------
457
458/// A lazy [`FileSource`] over any [`std::io::Read`] + [`std::io::Seek`] (a
459/// [`std::fs::File`], an in-memory `Cursor`, etc.).
460///
461/// Each [`read_at`](FileSource::read_at) performs a `seek` + `read_exact`, so no
462/// more than the requested bytes are ever held in memory. This is the backend
463/// that lets a 32-bit host read a file larger than its address space: the
464/// metadata and one working chunk fit even when the whole file does not.
465///
466/// The reader is wrapped in a [`std::sync::Mutex`] so the source is `Sync` and
467/// `read_at` can take `&self` (seeking needs `&mut` access). This serializes
468/// concurrent reads, which is correct though not maximally parallel; a future
469/// backend can use positioned reads (`pread`/`seek_read`) to avoid the lock.
470#[cfg(feature = "std")]
471pub struct ReadSeekSource<R> {
472    inner: std::sync::Mutex<R>,
473    len: u64,
474}
475
476#[cfg(feature = "std")]
477impl<R: std::io::Read + std::io::Seek> ReadSeekSource<R> {
478    /// Wrap a `Read + Seek`, measuring its length by seeking to the end (then
479    /// restoring nothing — every `read_at` seeks absolutely anyway).
480    pub fn new(mut reader: R) -> Result<Self, FormatError> {
481        let len = reader
482            .seek(std::io::SeekFrom::End(0))
483            .map_err(|e| FormatError::Source(format_io(&e)))?;
484        Ok(ReadSeekSource {
485            inner: std::sync::Mutex::new(reader),
486            len,
487        })
488    }
489}
490
491#[cfg(feature = "std")]
492impl<R: std::io::Read + std::io::Seek> FileSource for ReadSeekSource<R> {
493    fn len(&self) -> u64 {
494        self.len
495    }
496
497    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
498        // Bound-check up front so a request past EOF is a clean error rather
499        // than a backend-specific short read.
500        let end = offset
501            .checked_add(buf.len() as u64)
502            .ok_or(FormatError::OffsetOverflow {
503                offset,
504                length: buf.len() as u64,
505            })?;
506        if end > self.len {
507            return Err(FormatError::UnexpectedEof {
508                // `expected`/`available` are byte counts; report them as the
509                // best `usize` we can without truncating on a 32-bit host.
510                expected: end.to_usize().unwrap_or(usize::MAX),
511                available: self.len.to_usize().unwrap_or(usize::MAX),
512            });
513        }
514        let mut guard = self
515            .inner
516            .lock()
517            .unwrap_or_else(std::sync::PoisonError::into_inner);
518        guard
519            .seek(std::io::SeekFrom::Start(offset))
520            .map_err(|e| FormatError::Source(format_io(&e)))?;
521        guard
522            .read_exact(buf)
523            .map_err(|e| FormatError::Source(format_io(&e)))?;
524        Ok(())
525    }
526}
527
528/// Render an `std::io::Error` to a short owned string for [`FormatError::Source`]
529/// (which is `no_std`-friendly and cannot hold the error itself).
530#[cfg(feature = "std")]
531fn format_io(e: &std::io::Error) -> std::string::String {
532    std::format!("{e}")
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538
539    #[cfg(not(feature = "std"))]
540    use alloc::vec;
541
542    #[test]
543    fn bytes_source_reads_and_reports_len() {
544        let data = (0u8..=255).collect::<Vec<u8>>();
545        let src = BytesSource::new(data.clone());
546        assert_eq!(src.len(), 256);
547        assert!(!src.is_empty());
548
549        let mut buf = [0u8; 4];
550        src.read_at(10, &mut buf).unwrap();
551        assert_eq!(buf, [10, 11, 12, 13]);
552
553        let owned = src.read_exact_at(250, 6).unwrap();
554        assert_eq!(owned, vec![250, 251, 252, 253, 254, 255]);
555    }
556
557    #[test]
558    fn bytes_source_short_read_is_eof() {
559        let src = BytesSource::new(vec![1u8, 2, 3]);
560        let mut buf = [0u8; 4];
561        let err = src.read_at(0, &mut buf).unwrap_err();
562        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
563        // Reading exactly to the end is fine.
564        let mut ok = [0u8; 3];
565        src.read_at(0, &mut ok).unwrap();
566        assert_eq!(ok, [1, 2, 3]);
567    }
568
569    #[test]
570    fn bytes_source_offset_past_end_is_eof() {
571        let src = BytesSource::new(vec![0u8; 8]);
572        let mut buf = [0u8; 1];
573        assert!(matches!(
574            src.read_at(8, &mut buf).unwrap_err(),
575            FormatError::UnexpectedEof { .. }
576        ));
577        // Zero-length read at EOF succeeds.
578        src.read_at(8, &mut []).unwrap();
579    }
580
581    #[test]
582    fn read_exact_at_rejects_oversized_len_without_allocating() {
583        // A length far larger than the source must error cleanly rather than
584        // attempt to reserve the buffer first. Before the pre-allocation bounds
585        // check, this called `vec![0u8; usize::MAX]` and aborted the process.
586        let src = BytesSource::new(vec![1u8, 2, 3, 4]);
587        assert!(matches!(
588            src.read_exact_at(0, usize::MAX).unwrap_err(),
589            FormatError::UnexpectedEof { .. }
590        ));
591        // A read that fits is unaffected.
592        assert_eq!(src.read_exact_at(1, 3).unwrap(), vec![2, 3, 4]);
593    }
594
595    #[test]
596    fn empty_source() {
597        let src = BytesSource::new(Vec::<u8>::new());
598        assert_eq!(src.len(), 0);
599        assert!(src.is_empty());
600    }
601
602    #[test]
603    fn forwarding_through_reference() {
604        let src = BytesSource::new(vec![9u8, 8, 7]);
605        let r: &dyn FileSource = &src;
606        let mut buf = [0u8; 2];
607        r.read_at(1, &mut buf).unwrap();
608        assert_eq!(buf, [8, 7]);
609    }
610
611    #[test]
612    fn forwarding_through_reference_preserves_metadata_reads() {
613        use core::cell::Cell;
614
615        struct MetadataSource {
616            metadata_reads: Cell<usize>,
617        }
618
619        impl FileSource for MetadataSource {
620            fn len(&self) -> u64 {
621                16
622            }
623
624            fn read_at(&self, _offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
625                buf.fill(0);
626                Ok(())
627            }
628
629            fn read_metadata_at(&self, _offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
630                self.metadata_reads.set(self.metadata_reads.get() + 1);
631                Ok(vec![0xAB; len])
632            }
633        }
634
635        fn read_metadata_via_trait<T: FileSource>(source: T) -> Vec<u8> {
636            source.read_metadata_at(4, 3).unwrap()
637        }
638
639        let source = MetadataSource {
640            metadata_reads: Cell::new(0),
641        };
642
643        assert_eq!(read_metadata_via_trait(&source), vec![0xAB; 3]);
644        assert_eq!(source.metadata_reads.get(), 1);
645    }
646
647    #[cfg(feature = "std")]
648    #[test]
649    fn metadata_cache_caches_only_metadata_reads() {
650        use std::sync::{
651            Arc,
652            atomic::{AtomicUsize, Ordering},
653        };
654
655        struct CountingSource {
656            data: Vec<u8>,
657            reads: Arc<AtomicUsize>,
658        }
659
660        impl FileSource for CountingSource {
661            fn len(&self) -> u64 {
662                self.data.len() as u64
663            }
664
665            fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
666                self.reads.fetch_add(1, Ordering::SeqCst);
667                BytesSource::new(&self.data).read_at(offset, buf)
668            }
669        }
670
671        let reads = Arc::new(AtomicUsize::new(0));
672        let source = MetadataCachingSource::new(
673            CountingSource {
674                data: (0u8..16).collect(),
675                reads: Arc::clone(&reads),
676            },
677            MetadataCacheConfig::new(16),
678        );
679
680        assert_eq!(source.read_metadata_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
681        assert_eq!(source.read_metadata_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
682        assert_eq!(reads.load(Ordering::SeqCst), 1);
683
684        assert_eq!(source.read_exact_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
685        assert_eq!(source.read_exact_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
686        assert_eq!(reads.load(Ordering::SeqCst), 3);
687    }
688
689    #[cfg(feature = "std")]
690    #[test]
691    fn read_seek_source_matches_in_memory() {
692        use std::io::Cursor;
693        let data = (0u8..200).collect::<Vec<u8>>();
694        let mem = BytesSource::new(data.clone());
695        let seek = ReadSeekSource::new(Cursor::new(data.clone())).unwrap();
696        assert_eq!(seek.len(), mem.len());
697
698        // Every read_at against the lazy source matches the in-memory source.
699        for &(off, len) in &[(0u64, 1usize), (5, 10), (199, 1), (100, 50)] {
700            let a = mem.read_exact_at(off, len).unwrap();
701            let b = seek.read_exact_at(off, len).unwrap();
702            assert_eq!(a, b, "mismatch at offset {off} len {len}");
703        }
704    }
705
706    #[cfg(feature = "std")]
707    #[test]
708    fn read_seek_source_past_end_is_error() {
709        use std::io::Cursor;
710        let seek = ReadSeekSource::new(Cursor::new(vec![1u8, 2, 3, 4])).unwrap();
711        let mut buf = [0u8; 3];
712        assert!(matches!(
713            seek.read_at(2, &mut buf).unwrap_err(),
714            FormatError::UnexpectedEof { .. }
715        ));
716    }
717
718    #[cfg(feature = "std")]
719    #[test]
720    fn read_seek_source_is_sync() {
721        // Compile-time assertion that the std backend is Send + Sync so it can
722        // back a parallel reader.
723        fn assert_send_sync<T: Send + Sync>() {}
724        assert_send_sync::<ReadSeekSource<std::io::Cursor<Vec<u8>>>>();
725    }
726}