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}