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}