Skip to main content

hdf5_pure/
reader.rs

1//! Reading API: File, Dataset, and Group handles for reading HDF5 files.
2
3use std::collections::HashMap;
4use std::io::{Read, Seek, SeekFrom};
5
6use crate::attribute::{extract_attributes_full, extract_attributes_full_from_source};
7use crate::chunk_cache::{ChunkCache, ChunkCacheConfig, ChunkCacheStats};
8use crate::compound::CompoundType;
9use crate::convert::TryToUsize;
10use crate::data_layout::DataLayout;
11use crate::data_read;
12use crate::dataspace::Dataspace;
13use crate::datatype::{Datatype, ReferenceType};
14use crate::error::{Error, FormatError};
15use crate::file_space_info::{FileSpaceInfo, FileSpaceStrategy};
16use crate::filter_pipeline::FilterPipeline;
17use crate::free_space_manager;
18use crate::group_v1::GroupEntry;
19use crate::group_v2;
20use crate::libver::LibVer;
21use crate::message_type::MessageType;
22use crate::object_header::ObjectHeader;
23use crate::signature;
24use crate::source::{
25    BytesSource, FileSource, MetadataCacheConfig, MetadataCachingSource, ReadSeekSource,
26};
27use crate::superblock::Superblock;
28use crate::vl_data::{self, VlenStringReadOptions};
29
30use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype};
31
32// ---------------------------------------------------------------------------
33// File
34// ---------------------------------------------------------------------------
35
36/// Backing store for a [`File`]: either the whole file buffered in memory, or a
37/// lazy [`FileSource`] that reads regions on demand (see [`File::open_streaming`]).
38enum Backend {
39    InMemory(Vec<u8>),
40    Streaming(Box<dyn FileSource + Send + Sync>),
41}
42
43/// A borrowed `FileSource` view over a [`File`]'s backend, used by the
44/// streaming-capable read paths so one call site serves both backends.
45pub(crate) enum SourceView<'a> {
46    Mem(&'a [u8]),
47    Stream(&'a (dyn FileSource + Send + Sync)),
48}
49
50impl FileSource for SourceView<'_> {
51    fn len(&self) -> u64 {
52        match self {
53            SourceView::Mem(b) => b.len() as u64,
54            SourceView::Stream(s) => s.len(),
55        }
56    }
57    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
58        match self {
59            SourceView::Mem(b) => BytesSource::new(*b).read_at(offset, buf),
60            SourceView::Stream(s) => s.read_at(offset, buf),
61        }
62    }
63
64    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
65        match self {
66            SourceView::Mem(b) => BytesSource::new(*b).read_metadata_at(offset, len),
67            SourceView::Stream(s) => s.read_metadata_at(offset, len),
68        }
69    }
70}
71
72/// A `FileSource` view shifted forward by a base address: every read at a
73/// base-relative `offset` is served from `inner` at `offset + base`. Used by the
74/// dataset-payload read path on a file with a userblock, where the data-layout's
75/// on-disk addresses (contiguous data, chunk index, and chunk data) are stored
76/// relative to the base address — presenting the reader this shifted view lets
77/// those relative addresses index it directly, exactly as the in-memory path
78/// slices the buffer at `base`. `len`/`read_at` shift by the base; `read_metadata_at`
79/// forwards to the inner source (at the absolute offset) so its metadata cache is
80/// shared, while payload reads keep the default uncached `read_exact_at`.
81struct BaseOffsetSource<'a, S: FileSource + ?Sized> {
82    inner: &'a S,
83    base: u64,
84}
85
86impl<S: FileSource + ?Sized> FileSource for BaseOffsetSource<'_, S> {
87    fn len(&self) -> u64 {
88        self.inner.len().saturating_sub(self.base)
89    }
90
91    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
92        let abs = offset
93            .checked_add(self.base)
94            .ok_or(FormatError::OffsetOverflow {
95                offset,
96                length: buf.len() as u64,
97            })?;
98        self.inner.read_at(abs, buf)
99    }
100
101    /// Forward metadata reads to the inner source at the absolute offset so the
102    /// inner source's metadata cache is shared (chunk-index walks on a streaming
103    /// userblock file otherwise re-read every node). Payload reads keep the default
104    /// `read_exact_at`, which stays uncached so user data does not evict metadata.
105    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
106        let abs = offset
107            .checked_add(self.base)
108            .ok_or(FormatError::OffsetOverflow {
109                offset,
110                length: len as u64,
111            })?;
112        self.inner.read_metadata_at(abs, len)
113    }
114}
115
116/// File-access options applied when opening an HDF5 file.
117///
118/// This is the `hdf5-pure` analogue of the HDF5 file access property list
119/// settings relevant to read-time memory usage. The metadata cache only affects
120/// streaming opens; in-memory opens already have the whole file in one buffer.
121/// The chunk cache is the file-wide default corresponding to HDF5
122/// `H5Pset_cache`'s raw-data chunk-cache settings and applies to datasets
123/// opened from either backend.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub struct FileAccessOptions {
126    metadata_cache: MetadataCacheConfig,
127    chunk_cache: ChunkCacheConfig,
128}
129
130impl FileAccessOptions {
131    /// Create options with the crate's default access behavior.
132    pub const fn new() -> Self {
133        Self {
134            metadata_cache: MetadataCacheConfig::disabled(),
135            chunk_cache: ChunkCacheConfig::new(),
136        }
137    }
138
139    /// Configure the bounded streaming metadata cache.
140    pub const fn with_metadata_cache(mut self, metadata_cache: MetadataCacheConfig) -> Self {
141        self.metadata_cache = metadata_cache;
142        self
143    }
144
145    /// Configure the per-dataset raw chunk cache used by datasets opened from
146    /// this file. This is the `H5Pset_cache`-style file-wide default.
147    pub const fn with_chunk_cache(mut self, chunk_cache: ChunkCacheConfig) -> Self {
148        self.chunk_cache = chunk_cache;
149        self
150    }
151
152    /// Return the configured streaming metadata cache.
153    pub const fn metadata_cache(&self) -> MetadataCacheConfig {
154        self.metadata_cache
155    }
156
157    /// Return the configured per-dataset chunk cache.
158    pub const fn chunk_cache(&self) -> ChunkCacheConfig {
159        self.chunk_cache
160    }
161}
162
163/// Dataset-access options applied when opening a single dataset.
164///
165/// This is the `hdf5-pure` analogue of an HDF5 Dataset Access Property List
166/// (DAPL). Its chunk cache corresponds to `H5Pset_chunk_cache`: it overrides,
167/// for this one dataset, the file-wide chunk-cache default configured with
168/// [`FileAccessOptions::with_chunk_cache`] (the `H5Pset_cache` analogue). When
169/// left unset, the dataset inherits that file-wide default — matching the DAPL
170/// default sentinels (`H5D_CHUNK_CACHE_*_DEFAULT`), which also mean "use the
171/// file's setting".
172///
173/// [`ChunkCacheConfig`] maps `H5Pset_chunk_cache`'s `rdcc_nslots` and
174/// `rdcc_nbytes`; its `rdcc_w0` preemption policy is not modeled, because this
175/// read cache uses strict LRU eviction (as noted on
176/// [`ChunkCacheConfig::from_h5p_cache`]).
177///
178/// Pass it to [`File::dataset_with_options`] or [`Group::dataset_with_options`].
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
180pub struct DatasetAccessOptions {
181    chunk_cache: Option<ChunkCacheConfig>,
182}
183
184impl DatasetAccessOptions {
185    /// Create options that inherit every file-wide access default.
186    pub const fn new() -> Self {
187        Self { chunk_cache: None }
188    }
189
190    /// Override the raw chunk cache for this one dataset, ignoring the file-wide
191    /// default. This is the `H5Pset_chunk_cache` analogue.
192    pub const fn with_chunk_cache(mut self, chunk_cache: ChunkCacheConfig) -> Self {
193        self.chunk_cache = Some(chunk_cache);
194        self
195    }
196
197    /// Return the chunk-cache override, or `None` when the dataset inherits the
198    /// file-wide default.
199    pub const fn chunk_cache(&self) -> Option<ChunkCacheConfig> {
200        self.chunk_cache
201    }
202
203    /// Resolve the effective chunk-cache config: the per-dataset override if one
204    /// was set, otherwise the file-wide `default`.
205    const fn resolved_chunk_cache(&self, default: ChunkCacheConfig) -> ChunkCacheConfig {
206        match self.chunk_cache {
207            Some(config) => config,
208            None => default,
209        }
210    }
211}
212
213/// Test whether a file looks like an HDF5 file, without reading it whole.
214///
215/// This is the spelling of the C library's `H5Fis_accessible` /
216/// `H5Fis_hdf5`: it opens the file and scans only the 8-byte candidate windows
217/// where the HDF5 signature is permitted (offsets 0, 512, 1024, 2048, …), so it
218/// never buffers the whole file. Returns:
219///
220/// - `Ok(true)` — the HDF5 signature was found,
221/// - `Ok(false)` — the file opened but has no HDF5 signature,
222/// - `Err(..)` — the file could not be opened (missing, permissions, …).
223///
224/// It validates only the signature, not the rest of the format; a truncated or
225/// corrupt file past the signature still reports `true`. Use [`File::open`] to
226/// fully parse and validate.
227pub fn is_hdf5<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<bool> {
228    let handle = std::fs::File::open(path)?;
229    let source = ReadSeekSource::new(handle).map_err(std::io::Error::other)?;
230    match signature::find_signature_in(&source) {
231        Ok(_) => Ok(true),
232        Err(FormatError::SignatureNotFound) => Ok(false),
233        Err(e) => Err(std::io::Error::other(e)),
234    }
235}
236
237/// Test whether an in-memory buffer begins (at a permitted offset) with the
238/// HDF5 signature. The buffer-backed counterpart of [`is_hdf5`].
239pub fn is_hdf5_bytes(data: &[u8]) -> bool {
240    signature::find_signature(data).is_ok()
241}
242
243/// An open HDF5 file for reading.
244pub struct File {
245    backend: Backend,
246    superblock: Superblock,
247    /// Byte offset to add to all relative addresses (= original base_address).
248    addr_offset: u64,
249    /// Live file handle, retained only when the file was opened with
250    /// [`File::open_swmr`] so [`File::refresh`] can re-read appended data.
251    handle: Option<std::fs::File>,
252    /// File Space Info parsed from the superblock extension, if the file records
253    /// one. Best-effort: a malformed or unreadable extension leaves this `None`
254    /// rather than failing the open.
255    file_space_info: Option<FileSpaceInfo>,
256    access_options: FileAccessOptions,
257}
258
259impl File {
260    /// Open an HDF5 file from a filesystem path.
261    ///
262    /// Reads the file into memory once. To follow a file that a concurrent
263    /// single writer is appending to (SWMR), use [`File::open_swmr`] instead.
264    /// To read a file larger than memory (e.g. on a 32-bit host) without
265    /// buffering it, use [`File::open_streaming`].
266    pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
267        Self::open_with_options(path, FileAccessOptions::new())
268    }
269
270    /// Open an HDF5 file from a filesystem path with explicit access options.
271    ///
272    /// Like [`open`](Self::open), this buffers the whole file in memory. Use
273    /// [`open_streaming_with_options`](Self::open_streaming_with_options) when
274    /// the metadata cache budget should apply to lazy metadata reads.
275    pub fn open_with_options<P: AsRef<std::path::Path>>(
276        path: P,
277        options: FileAccessOptions,
278    ) -> Result<Self, Error> {
279        let bytes = std::fs::read(path.as_ref()).map_err(Error::Io)?;
280        Self::from_bytes_with_options(bytes, options)
281    }
282
283    /// Open an HDF5 file for **streaming** reads, fetching regions on demand from
284    /// the file instead of buffering it whole.
285    ///
286    /// This lets a host read a file larger than its address space — the original
287    /// motivation being 32-bit targets reading multi-gigabyte files (issue #27).
288    /// Metadata and dataset chunks are read through a [`ReadSeekSource`], so peak
289    /// memory stays close to one chunk plus the metadata being parsed.
290    ///
291    /// Current limits (the buffered [`File::open`] has none of these): only
292    /// latest-format (v2) groups resolve — a v1 symbol-table group on the path
293    /// is rejected — and attribute reading on the streaming backend is not yet
294    /// supported. Dataset reads (contiguous, compact, and all chunked index
295    /// types) are fully supported.
296    pub fn open_streaming<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
297        Self::open_streaming_with_options(path, FileAccessOptions::new())
298    }
299
300    /// Open an HDF5 file for streaming reads with explicit access options.
301    pub fn open_streaming_with_options<P: AsRef<std::path::Path>>(
302        path: P,
303        options: FileAccessOptions,
304    ) -> Result<Self, Error> {
305        let handle = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
306        let source = ReadSeekSource::new(handle).map_err(Error::Format)?;
307        let source: Box<dyn FileSource + Send + Sync> = if options.metadata_cache.is_enabled() {
308            Box::new(MetadataCachingSource::new(source, options.metadata_cache))
309        } else {
310            Box::new(source)
311        };
312        let (superblock, addr_offset) = Self::parse_superblock_source(source.as_ref())?;
313        Ok(Self::from_parts(
314            Backend::Streaming(source),
315            superblock,
316            addr_offset,
317            None,
318            options,
319        ))
320    }
321
322    /// Open an HDF5 file for SWMR (single-writer/multiple-reader) reading.
323    ///
324    /// Like [`File::open`], but retains a live handle to the file so that
325    /// [`File::refresh`] can re-read data appended by a concurrent writer
326    /// (whether produced by this crate's append writer, the reference HDF5 C
327    /// library, or h5py in SWMR mode). The initial view is a consistent
328    /// snapshot; call [`File::refresh`] to advance to a newer one.
329    ///
330    /// Only the `std` build supports this (it requires a live filesystem
331    /// handle); the in-memory [`File::from_bytes`] path cannot refresh.
332    pub fn open_swmr<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
333        Self::open_swmr_with_options(path, FileAccessOptions::new())
334    }
335
336    /// Open an HDF5 file for SWMR reading with explicit access options.
337    ///
338    /// SWMR reads currently keep an in-memory mirror for refresh semantics, so
339    /// only the per-dataset chunk-cache settings affect this backend.
340    pub fn open_swmr_with_options<P: AsRef<std::path::Path>>(
341        path: P,
342        options: FileAccessOptions,
343    ) -> Result<Self, Error> {
344        let mut handle = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
345        let mut data = Vec::new();
346        handle.read_to_end(&mut data).map_err(Error::Io)?;
347        let (superblock, addr_offset) = Self::parse_superblock(&data)?;
348        Ok(Self::from_parts(
349            Backend::InMemory(data),
350            superblock,
351            addr_offset,
352            Some(handle),
353            options,
354        ))
355    }
356
357    /// Open an HDF5 file from an in-memory byte vector.
358    pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> {
359        Self::from_bytes_with_options(data, FileAccessOptions::new())
360    }
361
362    /// Open an HDF5 file from an in-memory byte vector with explicit access options.
363    pub fn from_bytes_with_options(
364        data: Vec<u8>,
365        options: FileAccessOptions,
366    ) -> Result<Self, Error> {
367        let (superblock, addr_offset) = Self::parse_superblock(&data)?;
368        Ok(Self::from_parts(
369            Backend::InMemory(data),
370            superblock,
371            addr_offset,
372            None,
373            options,
374        ))
375    }
376
377    /// A `FileSource` view over the backend, for the streaming-capable paths.
378    pub(crate) fn source(&self) -> SourceView<'_> {
379        match &self.backend {
380            Backend::InMemory(v) => SourceView::Mem(v),
381            Backend::Streaming(s) => SourceView::Stream(s.as_ref()),
382        }
383    }
384
385    /// Parse the superblock from `data`, returning it (with `root_group_address`
386    /// normalized to an absolute offset) and the base-address offset.
387    fn parse_superblock(data: &[u8]) -> Result<(Superblock, u64), Error> {
388        let sig_offset = signature::find_signature(data)?;
389        let mut superblock = Superblock::parse(data, sig_offset)?;
390        let addr_offset = superblock.base_address;
391        // Normalize root_group_address to absolute so resolve_path_any works.
392        superblock.root_group_address += addr_offset;
393        Ok((superblock, addr_offset))
394    }
395
396    /// Streaming counterpart of [`parse_superblock`]: locate and parse the
397    /// superblock by reading only small windows from the source.
398    fn parse_superblock_source<S: FileSource + ?Sized>(
399        source: &S,
400    ) -> Result<(Superblock, u64), Error> {
401        let sig_offset = signature::find_signature_in(source)?;
402        let mut superblock = Superblock::parse_from_source(source, sig_offset)?;
403        let addr_offset = superblock.base_address;
404        superblock.root_group_address += addr_offset;
405        Ok((superblock, addr_offset))
406    }
407
408    /// Assemble a [`File`] from parsed parts, then load the File Space Info from
409    /// the superblock extension (best-effort, so a bad extension never fails the
410    /// open).
411    fn from_parts(
412        backend: Backend,
413        superblock: Superblock,
414        addr_offset: u64,
415        handle: Option<std::fs::File>,
416        access_options: FileAccessOptions,
417    ) -> Self {
418        let mut file = File {
419            backend,
420            superblock,
421            addr_offset,
422            handle,
423            file_space_info: None,
424            access_options,
425        };
426        file.file_space_info = file.read_file_space_info();
427        file
428    }
429
430    /// Parse the File Space Info message from the superblock extension, if the
431    /// file records one and it can be read. Best-effort: any failure (no
432    /// extension, unreadable object header, malformed message) yields `None`.
433    fn read_file_space_info(&self) -> Option<FileSpaceInfo> {
434        let rel = self.superblock.superblock_extension_address?;
435        if rel == u64::MAX {
436            return None;
437        }
438        let abs = self.addr_offset.checked_add(rel)?;
439        let header = self.parse_header(abs).ok()?;
440        let msg = header
441            .messages
442            .iter()
443            .find(|m| m.msg_type == MessageType::FileSpaceInfo)?;
444        FileSpaceInfo::parse(
445            &msg.data,
446            self.superblock.offset_size,
447            self.superblock.length_size,
448        )
449        .ok()
450    }
451
452    /// Re-read the file from disk to pick up data appended by a concurrent
453    /// writer, then re-parse the superblock.
454    ///
455    /// This is the SWMR reader's refresh primitive (analogous to the C library's
456    /// `H5Drefresh` / h5py's `Dataset.refresh()`): after it returns, newly
457    /// fetched [`Dataset`]/[`Group`] handles observe the writer's appended
458    /// chunks and extended dimensions, because they re-parse object headers at
459    /// their (stable) addresses against the refreshed bytes. Existing handles
460    /// borrow `&self`, so they must be dropped before calling this; re-fetch
461    /// them afterward.
462    ///
463    /// Returns [`Error::SwmrUnsupported`] if the file was not opened with
464    /// [`File::open_swmr`]. The superblock is checksum-validated on every
465    /// re-read; a transient parse failure (a writer caught mid-flush) is
466    /// retried a bounded number of times before being surfaced.
467    ///
468    /// Cost: each call re-reads the entire file from disk (`O(file size)`).
469    /// That keeps the implementation simple and correct, but when following a
470    /// large, steadily growing log it is the cost paid per refresh; budget
471    /// refresh frequency accordingly.
472    pub fn refresh(&mut self) -> Result<(), Error> {
473        let handle = self.handle.as_mut().ok_or(Error::SwmrUnsupported)?;
474
475        // A writer only appends (the file grows) and updates a few fixed-size,
476        // individually checksummed structures in place (superblock EOF, object
477        // header dimensions, array header counts). Re-reading the whole file and
478        // re-validating the superblock checksum yields a consistent view; if the
479        // superblock is caught mid-update, retry.
480        const MAX_ATTEMPTS: u32 = 100;
481        let mut last_err = None;
482        for attempt in 0..MAX_ATTEMPTS {
483            let mut data = Vec::new();
484            handle.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
485            handle.read_to_end(&mut data).map_err(Error::Io)?;
486            match Self::parse_superblock(&data) {
487                Ok((superblock, addr_offset)) => {
488                    self.backend = Backend::InMemory(data);
489                    self.superblock = superblock;
490                    self.addr_offset = addr_offset;
491                    self.file_space_info = self.read_file_space_info();
492                    return Ok(());
493                }
494                Err(e) => {
495                    last_err = Some(e);
496                    // Brief backoff before re-reading; the writer's in-place
497                    // updates are tiny, so a short pause clears the window. Skip
498                    // it on the final attempt, where there is no re-read to come.
499                    if attempt + 1 < MAX_ATTEMPTS {
500                        std::thread::sleep(std::time::Duration::from_micros(
501                            50 * (attempt + 1) as u64,
502                        ));
503                    }
504                }
505            }
506        }
507        // The loop always runs at least once and only reaches here via the
508        // `Err` arm, so `last_err` is always `Some`; surface the real error.
509        Err(last_err.expect("refresh retried at least once before failing"))
510    }
511
512    /// Returns a handle to the root group.
513    pub fn root(&self) -> Group<'_> {
514        Group {
515            file: self,
516            // root_group_address was normalized to absolute in from_bytes()
517            address: self.superblock.root_group_address,
518        }
519    }
520
521    /// Resolve a path to an object-header address, dispatching on the backend.
522    fn resolve_path(&self, path: &str) -> Result<u64, Error> {
523        Ok(match &self.backend {
524            Backend::InMemory(v) => group_v2::resolve_path_any(v, &self.superblock, path)?,
525            Backend::Streaming(s) => {
526                group_v2::resolve_path_any_from_source(s.as_ref(), &self.superblock, path)?
527            }
528        })
529    }
530
531    /// Resolve a path and return a `Dataset` handle.
532    ///
533    /// The dataset uses the file-wide chunk-cache default (configured with
534    /// [`FileAccessOptions::with_chunk_cache`]). To override the cache for this
535    /// one dataset, use [`dataset_with_options`](Self::dataset_with_options).
536    pub fn dataset(&self, path: &str) -> Result<Dataset<'_>, Error> {
537        self.dataset_with_options(path, DatasetAccessOptions::new())
538    }
539
540    /// Resolve a path and return a `Dataset` handle, applying per-dataset
541    /// [`DatasetAccessOptions`] that override file-wide access defaults.
542    ///
543    /// This is the dataset-open-with-access-property-list path (HDF5's DAPL):
544    /// the options' chunk cache corresponds to `H5Pset_chunk_cache` and takes
545    /// precedence, for this dataset only, over the `H5Pset_cache`-style
546    /// file-wide default.
547    pub fn dataset_with_options(
548        &self,
549        path: &str,
550        options: DatasetAccessOptions,
551    ) -> Result<Dataset<'_>, Error> {
552        let addr = self.resolve_path(path)?;
553        let hdr = self.parse_header(addr)?;
554        if !has_message(&hdr, MessageType::DataLayout) {
555            return Err(Error::NotADataset(path.to_string()));
556        }
557        let chunk_cache = options.resolved_chunk_cache(self.access_options.chunk_cache);
558        Ok(Dataset {
559            file: self,
560            address: addr,
561            header: hdr,
562            chunk_cache: ChunkCache::with_config(chunk_cache),
563            chunk_cache_config: chunk_cache,
564        })
565    }
566
567    /// Resolve a path and return a `Group` handle.
568    pub fn group(&self, path: &str) -> Result<Group<'_>, Error> {
569        let addr = self.resolve_path(path)?;
570        Ok(Group {
571            file: self,
572            address: addr,
573        })
574    }
575
576    /// Returns the raw file bytes for an in-memory file, or an empty slice for a
577    /// streaming file (which has no whole-file buffer).
578    pub fn as_bytes(&self) -> &[u8] {
579        match &self.backend {
580            Backend::InMemory(v) => v,
581            Backend::Streaming(_) => &[],
582        }
583    }
584
585    /// Return the access options used when opening this file.
586    pub const fn access_options(&self) -> FileAccessOptions {
587        self.access_options
588    }
589
590    /// Returns a reference to the parsed superblock.
591    pub fn superblock(&self) -> &Superblock {
592        &self.superblock
593    }
594
595    /// The whole-file byte image when this file is buffered in memory
596    /// ([`open`](Self::open) / [`from_bytes`](Self::from_bytes)); `None` for a
597    /// streaming file ([`open_streaming`](Self::open_streaming)). Cross-file
598    /// object copy ([`EditSession::copy_from`](crate::EditSession::copy_from))
599    /// uses this to read source objects by absolute address.
600    pub(crate) fn in_memory_image(&self) -> Option<&[u8]> {
601        match &self.backend {
602            Backend::InMemory(data) => Some(data),
603            Backend::Streaming(_) => None,
604        }
605    }
606
607    /// The base address (`H5F` superblock base address), i.e. the byte offset
608    /// added to every stored relative address. Zero for a file with no
609    /// userblock.
610    pub(crate) fn base_address(&self) -> u64 {
611        self.addr_offset
612    }
613
614    /// The file-space management strategy this file records in its superblock
615    /// extension (set with `H5Pset_file_space_strategy`), or `None` if the file
616    /// records none — the default, which the C library also writes as "no
617    /// message". See [`file_space_info`](Self::file_space_info) for the full
618    /// record (persist flag, threshold, page size).
619    pub fn file_space_strategy(&self) -> Option<FileSpaceStrategy> {
620        self.file_space_info.as_ref().map(|info| info.strategy)
621    }
622
623    /// The full [`FileSpaceInfo`] recorded in this file's superblock extension,
624    /// if present and readable.
625    pub fn file_space_info(&self) -> Option<&FileSpaceInfo> {
626        self.file_space_info.as_ref()
627    }
628
629    /// The free regions a file persists on disk in its free-space managers (when
630    /// written with `H5Pset_file_space_strategy(..., persist = true)`), as
631    /// `(address, length)` pairs sorted by address.
632    ///
633    /// Empty when the file does not persist free space, or for the streaming
634    /// backend (which does not load the manager blocks). The addresses are file
635    /// offsets (relative to the base address); reading data is unaffected by the
636    /// presence or absence of these managers.
637    pub fn persisted_free_space(&self) -> Vec<(u64, u64)> {
638        let Some(info) = &self.file_space_info else {
639            return Vec::new();
640        };
641        if !info.persist {
642            return Vec::new();
643        }
644        let Backend::InMemory(data) = &self.backend else {
645            return Vec::new();
646        };
647        let mut sections = free_space_manager::read_persisted_sections(
648            data,
649            &info.manager_addrs,
650            self.addr_offset,
651            self.superblock.offset_size,
652        )
653        .unwrap_or_default();
654        sections.sort_by_key(|s| s.addr);
655        sections.into_iter().map(|s| (s.addr, s.size)).collect()
656    }
657
658    /// The size of the underlying file in bytes (the HDF5 `H5Fget_filesize`).
659    ///
660    /// This is the total byte length of the backing store — for a streaming
661    /// file the length reported by its source, for an in-memory file the length
662    /// of its buffer. It includes any userblock prefix and trailing bytes, so it
663    /// may exceed the superblock's logical end-of-file address; compare against
664    /// [`Superblock::eof_address`](crate::superblock::Superblock) (reachable via
665    /// [`File::superblock`]) to detect appended or unaccounted tail bytes.
666    pub fn file_size(&self) -> u64 {
667        self.source().len()
668    }
669
670    /// The minimum library version required to read this file, derived from its
671    /// superblock version (the *low bound* of HDF5's `H5Fget_libver_bounds`).
672    ///
673    /// A version 3 superblock, for example, reports [`LibVer::V110`] because it
674    /// was introduced in HDF5 1.10.
675    pub fn libver_bound(&self) -> LibVer {
676        LibVer::from_superblock_version(self.superblock.version)
677    }
678
679    fn parse_header(&self, address: u64) -> Result<ObjectHeader, FormatError> {
680        let os = self.superblock.offset_size;
681        let ls = self.superblock.length_size;
682        match &self.backend {
683            Backend::InMemory(v) => {
684                ObjectHeader::parse_with_base(v, address.to_usize()?, os, ls, self.addr_offset)
685            }
686            Backend::Streaming(s) => {
687                ObjectHeader::parse_from_source(s.as_ref(), address, os, ls, self.addr_offset)
688            }
689        }
690    }
691
692    /// Resolve a base-relative object-header address (the value stored in an
693    /// HDF5 `H5R_OBJECT` reference element) to the [`Object`] it points at.
694    ///
695    /// The stored address is relative to the superblock base address, so any
696    /// MAT-file userblock is accounted for here. A null (`0`) or undefined
697    /// (`HADDR_UNDEF`) address, or one whose object header is neither a dataset
698    /// nor a group, yields [`FormatError::InvalidObjectReference`].
699    fn object_at_relative(&self, rel_addr: u64) -> Result<Object<'_>, Error> {
700        // HADDR_UNDEF and the null address never name a real object. (Relative
701        // address 0 is where the superblock sits, not an object header.)
702        if rel_addr == u64::MAX || rel_addr == 0 {
703            return Err(FormatError::InvalidObjectReference(rel_addr).into());
704        }
705        let abs = rel_addr
706            .checked_add(self.addr_offset)
707            .ok_or(FormatError::InvalidObjectReference(rel_addr))?;
708        let hdr = self.parse_header(abs)?;
709        if has_message(&hdr, MessageType::DataLayout) {
710            let chunk_cache =
711                DatasetAccessOptions::new().resolved_chunk_cache(self.access_options.chunk_cache);
712            Ok(Object::Dataset(Box::new(Dataset {
713                file: self,
714                address: abs,
715                header: hdr,
716                chunk_cache: ChunkCache::with_config(chunk_cache),
717                chunk_cache_config: chunk_cache,
718            })))
719        } else if is_group(&hdr) {
720            Ok(Object::Group(Group {
721                file: self,
722                address: abs,
723            }))
724        } else {
725            Err(FormatError::InvalidObjectReference(rel_addr).into())
726        }
727    }
728
729    fn offset_size(&self) -> u8 {
730        self.superblock.offset_size
731    }
732
733    fn length_size(&self) -> u8 {
734        self.superblock.length_size
735    }
736
737    /// Resolve the children of a group object header, dispatching on the backend
738    /// and converting link addresses to absolute.
739    fn group_children(&self, hdr: &ObjectHeader) -> Result<Vec<GroupEntry>, Error> {
740        let (os, ls, base) = (self.offset_size(), self.length_size(), self.addr_offset);
741        let mut entries = match &self.backend {
742            Backend::InMemory(v) => group_v2::resolve_group_entries(v, hdr, os, ls, base),
743            Backend::Streaming(s) => {
744                group_v2::resolve_group_entries_from_source(s.as_ref(), hdr, os, ls, base)
745            }
746        }
747        .map_err(Error::Format)?;
748        for entry in &mut entries {
749            entry.object_header_address += base;
750        }
751        Ok(entries)
752    }
753
754    /// Read all attributes attached to an object header, dispatching on the
755    /// backend.
756    fn attrs_of(&self, hdr: &ObjectHeader) -> Result<HashMap<String, AttrValue>, Error> {
757        let (os, ls, base) = (self.offset_size(), self.length_size(), self.addr_offset);
758        let attr_msgs = self.attr_messages_of(hdr)?;
759        Ok(attrs_to_map(&attr_msgs, &self.source(), os, ls, base))
760    }
761
762    /// Names of every attribute message on `hdr`, including ones whose datatype
763    /// [`attrs_of`](Self::attrs_of) cannot decode into an [`AttrValue`] (and so
764    /// silently omits from its map). Repack diffs this against the decoded map to
765    /// refuse rather than drop an attribute it cannot reproduce.
766    pub(crate) fn attr_message_names_of(&self, hdr: &ObjectHeader) -> Result<Vec<String>, Error> {
767        Ok(self
768            .attr_messages_of(hdr)?
769            .into_iter()
770            .map(|a| a.name)
771            .collect())
772    }
773
774    /// Extract every attribute message attached to an object header (compact,
775    /// shared, and dense storage), dispatching on the backend.
776    fn attr_messages_of(
777        &self,
778        hdr: &ObjectHeader,
779    ) -> Result<Vec<crate::attribute::AttributeMessage>, Error> {
780        let (os, ls) = (self.offset_size(), self.length_size());
781        match &self.backend {
782            Backend::InMemory(v) => Ok(extract_attributes_full(v, hdr, os, ls)?),
783            Backend::Streaming(s) => Ok(extract_attributes_full_from_source(
784                s.as_ref(),
785                hdr,
786                os,
787                ls,
788            )?),
789        }
790    }
791
792    /// Read a dataset's raw bytes for the given layout, dispatching on the backend.
793    fn read_dataset_raw(
794        &self,
795        dl: &DataLayout,
796        ds: &Dataspace,
797        dt: &Datatype,
798        pipeline: Option<&FilterPipeline>,
799        cache: &ChunkCache,
800    ) -> Result<Vec<u8>, FormatError> {
801        let (os, ls) = (self.offset_size(), self.length_size());
802        // Every on-disk address in `dl` — the contiguous data address, the chunk
803        // index root, and (followed deeper in the chunked reader) every B-tree /
804        // fixed-array / extensible-array node and chunk-data address — is stored
805        // relative to the base address. Present the payload reader a base-relative
806        // view of the file so all of them index it directly: slice the in-memory
807        // buffer at `base`, or wrap the streaming source to add `base` to each
808        // read. For a plain file (`base == 0`) this is the identity.
809        let base = self.addr_offset;
810        match &self.backend {
811            Backend::InMemory(v) => {
812                let frame = if base == 0 {
813                    v.as_slice()
814                } else {
815                    let start = base.to_usize()?;
816                    v.get(start..).ok_or(FormatError::UnexpectedEof {
817                        expected: start,
818                        available: v.len(),
819                    })?
820                };
821                data_read::read_raw_data_cached(frame, dl, ds, dt, pipeline, os, ls, cache)
822            }
823            Backend::Streaming(s) if base == 0 => data_read::read_raw_data_cached_from_source(
824                s.as_ref(),
825                dl,
826                ds,
827                dt,
828                pipeline,
829                os,
830                ls,
831                cache,
832            ),
833            Backend::Streaming(s) => {
834                let framed = BaseOffsetSource {
835                    inner: s.as_ref(),
836                    base,
837                };
838                data_read::read_raw_data_cached_from_source(
839                    &framed, dl, ds, dt, pipeline, os, ls, cache,
840                )
841            }
842        }
843    }
844}
845
846impl std::fmt::Debug for File {
847    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
848        f.debug_struct("File")
849            .field("size", &self.source().len())
850            .field("superblock_version", &self.superblock.version)
851            .finish()
852    }
853}
854
855// ---------------------------------------------------------------------------
856// Object reference target
857// ---------------------------------------------------------------------------
858
859/// The resolved target of an HDF5 object reference (`H5R_OBJECT`): either a
860/// group or a dataset.
861///
862/// Produced by [`Dataset::dereference`]. MATLAB `.mat` files use object
863/// references pervasively — a cell array stores one reference per element, and
864/// the `#subsystem#` machinery references its payloads — so resolving a
865/// reference to the group or dataset it names is the foundation for reading
866/// those structures.
867///
868/// The [`Dataset`](Object::Dataset) handle is boxed: it carries a parsed object
869/// header and is much larger than a [`Group`](Object::Group) handle, so boxing
870/// keeps `Object` (and a `Vec<Object>`) compact without a size disparity. The
871/// `Box` derefs transparently, so `&obj_dataset` is usable wherever a
872/// `&Dataset` is expected.
873pub enum Object<'f> {
874    /// The reference points at a group's object header.
875    Group(Group<'f>),
876    /// The reference points at a dataset's object header.
877    Dataset(Box<Dataset<'f>>),
878}
879
880impl std::fmt::Debug for Object<'_> {
881    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
882        match self {
883            Object::Group(_) => f.write_str("Object::Group"),
884            Object::Dataset(_) => f.write_str("Object::Dataset"),
885        }
886    }
887}
888
889// ---------------------------------------------------------------------------
890// Group handle
891// ---------------------------------------------------------------------------
892
893/// A lightweight handle to an HDF5 group.
894pub struct Group<'f> {
895    file: &'f File,
896    address: u64,
897}
898
899impl<'f> Group<'f> {
900    /// Address of this group's object header (base-adjusted, file-absolute).
901    /// Used to resolve object references that point at this group.
902    pub(crate) fn header_address(&self) -> u64 {
903        self.address
904    }
905
906    /// List the names of datasets in this group.
907    pub fn datasets(&self) -> Result<Vec<String>, Error> {
908        let entries = self.children()?;
909        let mut names = Vec::new();
910        for entry in &entries {
911            let hdr = self.file.parse_header(entry.object_header_address)?;
912            if has_message(&hdr, MessageType::DataLayout) {
913                names.push(entry.name.clone());
914            }
915        }
916        Ok(names)
917    }
918
919    /// List the names of subgroups in this group.
920    pub fn groups(&self) -> Result<Vec<String>, Error> {
921        let entries = self.children()?;
922        let mut names = Vec::new();
923        for entry in &entries {
924            let hdr = self.file.parse_header(entry.object_header_address)?;
925            if is_group(&hdr) {
926                names.push(entry.name.clone());
927            }
928        }
929        Ok(names)
930    }
931
932    /// Read all attributes of this group.
933    pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
934        let hdr = self.file.parse_header(self.address)?;
935        self.file.attrs_of(&hdr)
936    }
937
938    /// Names of every attribute on this group, including any whose datatype
939    /// [`attrs`](Self::attrs) cannot represent. Used by repack to detect an
940    /// attribute it would otherwise drop.
941    pub(crate) fn attr_names(&self) -> Result<Vec<String>, Error> {
942        let hdr = self.file.parse_header(self.address)?;
943        self.file.attr_message_names_of(&hdr)
944    }
945
946    /// Get a dataset within this group by name.
947    ///
948    /// The dataset uses the file-wide chunk-cache default. To override the cache
949    /// for this one dataset, use
950    /// [`dataset_with_options`](Self::dataset_with_options).
951    pub fn dataset(&self, name: &str) -> Result<Dataset<'f>, Error> {
952        self.dataset_with_options(name, DatasetAccessOptions::new())
953    }
954
955    /// Get a dataset within this group by name, applying per-dataset
956    /// [`DatasetAccessOptions`] that override file-wide access defaults (HDF5's
957    /// DAPL; see `H5Pset_chunk_cache`).
958    pub fn dataset_with_options(
959        &self,
960        name: &str,
961        options: DatasetAccessOptions,
962    ) -> Result<Dataset<'f>, Error> {
963        let entries = self.children()?;
964        let entry = entries
965            .iter()
966            .find(|e| e.name == name)
967            .ok_or_else(|| Error::Format(FormatError::PathNotFound(name.to_string())))?;
968        let hdr = self.file.parse_header(entry.object_header_address)?;
969        if !has_message(&hdr, MessageType::DataLayout) {
970            return Err(Error::NotADataset(name.to_string()));
971        }
972        let chunk_cache = options.resolved_chunk_cache(self.file.access_options.chunk_cache);
973        Ok(Dataset {
974            file: self.file,
975            address: entry.object_header_address,
976            header: hdr,
977            chunk_cache: ChunkCache::with_config(chunk_cache),
978            chunk_cache_config: chunk_cache,
979        })
980    }
981
982    /// Get a subgroup within this group by name.
983    pub fn group(&self, name: &str) -> Result<Group<'f>, Error> {
984        let entries = self.children()?;
985        let entry = entries
986            .iter()
987            .find(|e| e.name == name)
988            .ok_or_else(|| Error::Format(FormatError::PathNotFound(name.to_string())))?;
989        Ok(Group {
990            file: self.file,
991            address: entry.object_header_address,
992        })
993    }
994
995    fn children(&self) -> Result<Vec<GroupEntry>, Error> {
996        let hdr = self.file.parse_header(self.address)?;
997        self.file.group_children(&hdr)
998    }
999}
1000
1001// ---------------------------------------------------------------------------
1002// Dataset handle
1003// ---------------------------------------------------------------------------
1004
1005/// A lightweight handle to an HDF5 dataset.
1006pub struct Dataset<'f> {
1007    file: &'f File,
1008    /// Address of this dataset's object header (base-adjusted, file-absolute).
1009    /// Used to resolve object references that point at this dataset.
1010    address: u64,
1011    header: ObjectHeader,
1012    // Held per-dataset: the chunk index is keyed only by chunk coordinate, so
1013    // a file-level cache would alias chunk addresses across datasets.
1014    chunk_cache: ChunkCache,
1015    // The effective chunk-cache config for this dataset: the file-wide default
1016    // or a per-dataset DAPL override. Reported by `chunk_cache_config`.
1017    chunk_cache_config: ChunkCacheConfig,
1018}
1019
1020impl<'f> std::fmt::Debug for Dataset<'f> {
1021    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1022        f.debug_struct("Dataset")
1023            .field("messages", &self.header.messages.len())
1024            .finish()
1025    }
1026}
1027
1028impl<'f> Dataset<'f> {
1029    /// Address of this dataset's object header (base-adjusted, file-absolute).
1030    /// Used to resolve object references that point at this dataset.
1031    pub(crate) fn header_address(&self) -> u64 {
1032        self.address
1033    }
1034
1035    /// The effective raw chunk-cache configuration for this dataset.
1036    ///
1037    /// This reflects the per-dataset [`DatasetAccessOptions`] override when one
1038    /// was supplied to [`File::dataset_with_options`] /
1039    /// [`Group::dataset_with_options`], otherwise the file-wide default. It is
1040    /// the read-side analogue of HDF5's `H5Pget_chunk_cache`.
1041    pub const fn chunk_cache_config(&self) -> ChunkCacheConfig {
1042        self.chunk_cache_config
1043    }
1044
1045    /// A point-in-time snapshot of this dataset handle's chunk-cache occupancy.
1046    ///
1047    /// Lets callers confirm a chunk-cache configuration (set with
1048    /// [`FileAccessOptions::with_chunk_cache`]) is taking effect: after a
1049    /// chunked read, an enabled cache reports a loaded index and retained
1050    /// chunks; a disabled one (or one over its budget) reports fewer or none.
1051    /// The cache is per-handle, so a freshly opened [`Dataset`] reports an empty
1052    /// snapshot until its first read.
1053    pub fn chunk_cache_stats(&self) -> ChunkCacheStats {
1054        self.chunk_cache.stats()
1055    }
1056
1057    /// Returns the shape (dimensions) of the dataset.
1058    pub fn shape(&self) -> Result<Vec<u64>, Error> {
1059        let ds = self.dataspace()?;
1060        Ok(ds.dimensions.clone())
1061    }
1062
1063    /// Returns the simplified datatype of the dataset.
1064    pub fn dtype(&self) -> Result<DType, Error> {
1065        let dt = self.datatype()?;
1066        Ok(classify_datatype(&dt))
1067    }
1068
1069    /// Read all data as `f64` values.
1070    pub fn read_f64(&self) -> Result<Vec<f64>, Error> {
1071        let raw = self.read_raw()?;
1072        let dt = self.datatype()?;
1073        Ok(data_read::read_as_f64(&raw, &dt)?)
1074    }
1075
1076    /// Read all data as `f32` values.
1077    pub fn read_f32(&self) -> Result<Vec<f32>, Error> {
1078        let raw = self.read_raw()?;
1079        let dt = self.datatype()?;
1080        Ok(data_read::read_as_f32(&raw, &dt)?)
1081    }
1082
1083    /// Read all data as `i32` values.
1084    pub fn read_i32(&self) -> Result<Vec<i32>, Error> {
1085        let raw = self.read_raw()?;
1086        let dt = self.datatype()?;
1087        Ok(data_read::read_as_i32(&raw, &dt)?)
1088    }
1089
1090    /// Read all data as `i64` values.
1091    pub fn read_i64(&self) -> Result<Vec<i64>, Error> {
1092        let raw = self.read_raw()?;
1093        let dt = self.datatype()?;
1094        Ok(data_read::read_as_i64(&raw, &dt)?)
1095    }
1096
1097    /// Read all data as `u64` values.
1098    pub fn read_u64(&self) -> Result<Vec<u64>, Error> {
1099        let raw = self.read_raw()?;
1100        let dt = self.datatype()?;
1101        Ok(data_read::read_as_u64(&raw, &dt)?)
1102    }
1103
1104    /// Read all data as `u8` values.
1105    pub fn read_u8(&self) -> Result<Vec<u8>, Error> {
1106        self.read_raw()
1107    }
1108
1109    /// Read all data as `i8` values.
1110    #[expect(
1111        clippy::cast_possible_wrap,
1112        reason = "read_i8 reinterprets each stored byte as the signed i8 the caller requested"
1113    )]
1114    pub fn read_i8(&self) -> Result<Vec<i8>, Error> {
1115        let raw = self.read_raw()?;
1116        Ok(raw.iter().map(|&b| b as i8).collect())
1117    }
1118
1119    /// Read all data as `i16` values.
1120    pub fn read_i16(&self) -> Result<Vec<i16>, Error> {
1121        let raw = self.read_raw()?;
1122        let dt = self.datatype()?;
1123        Ok(data_read::read_as_i16(&raw, &dt)?)
1124    }
1125
1126    /// Read all data as `u16` values.
1127    pub fn read_u16(&self) -> Result<Vec<u16>, Error> {
1128        let raw = self.read_raw()?;
1129        let dt = self.datatype()?;
1130        Ok(data_read::read_as_u16(&raw, &dt)?)
1131    }
1132
1133    /// Read all data as `u32` values.
1134    pub fn read_u32(&self) -> Result<Vec<u32>, Error> {
1135        let raw = self.read_raw()?;
1136        let dt = self.datatype()?;
1137        Ok(data_read::read_as_u32(&raw, &dt)?)
1138    }
1139
1140    /// Read all data as `String` values.
1141    ///
1142    /// Fixed-length and variable-length HDF5 string datasets are both
1143    /// supported. Use [`read_vlen_strings`](Self::read_vlen_strings) when
1144    /// variable-length allocation limits are required.
1145    pub fn read_string(&self) -> Result<Vec<String>, Error> {
1146        let dt = self.datatype()?;
1147        if vl_data::is_vlen_string_datatype(&dt) {
1148            self.read_vlen_strings(VlenStringReadOptions::default())
1149        } else {
1150            let raw = self.read_raw()?;
1151            Ok(data_read::read_as_strings(&raw, &dt)?)
1152        }
1153    }
1154
1155    /// Return the total bytes referenced by this VL string dataset.
1156    ///
1157    /// This is the payload equivalent of HDF5's `H5Dvlen_get_buf_size`: it
1158    /// excludes `Vec<String>` and `String` allocation metadata.
1159    pub fn vlen_string_payload_size(&self) -> Result<u64, Error> {
1160        let datatype = self.datatype()?;
1161        if !vl_data::is_vlen_string_datatype(&datatype) {
1162            return Err(FormatError::TypeMismatch {
1163                expected: "VariableLength string",
1164                actual: "non-VariableLength string",
1165            }
1166            .into());
1167        }
1168        let dataspace = self.dataspace()?;
1169        let raw = self.read_raw()?;
1170        Ok(vl_data::vlen_string_payload_size(
1171            &raw,
1172            dataspace.num_elements(),
1173            self.file.offset_size(),
1174        )?)
1175    }
1176
1177    /// Read a VL string dataset with explicit allocation limits.
1178    ///
1179    /// Both limits are checked before any string payload is materialized.
1180    pub fn read_vlen_strings(&self, options: VlenStringReadOptions) -> Result<Vec<String>, Error> {
1181        let mut strings = Vec::new();
1182        self.visit_vlen_strings(options, |string| strings.push(string.to_owned()))?;
1183        Ok(strings)
1184    }
1185
1186    /// Visit a VL string dataset one element at a time.
1187    ///
1188    /// The string slice passed to `visitor` is valid only for the duration of
1189    /// that callback. This avoids retaining all decoded string payloads at once.
1190    pub fn visit_vlen_strings<F>(
1191        &self,
1192        options: VlenStringReadOptions,
1193        visitor: F,
1194    ) -> Result<(), Error>
1195    where
1196        F: FnMut(&str),
1197    {
1198        let datatype = self.datatype()?;
1199        if !vl_data::is_vlen_string_datatype(&datatype) {
1200            return Err(FormatError::TypeMismatch {
1201                expected: "VariableLength string",
1202                actual: "non-VariableLength string",
1203            }
1204            .into());
1205        }
1206        let dataspace = self.dataspace()?;
1207        if let Some(limit) = options.max_elements()
1208            && dataspace.num_elements() > limit as u64
1209        {
1210            return Err(FormatError::VariableLengthElementLimitExceeded {
1211                limit,
1212                actual: dataspace.num_elements(),
1213            }
1214            .into());
1215        }
1216        let raw = self.read_raw()?;
1217        let source = self.file.source();
1218        Ok(vl_data::visit_vl_strings_from_source(
1219            &source,
1220            &raw,
1221            dataspace.num_elements(),
1222            self.file.offset_size(),
1223            self.file.length_size(),
1224            self.file.addr_offset,
1225            options,
1226            visitor,
1227        )?)
1228    }
1229
1230    /// Read a VL string dataset's exact heap bytes, preserving the
1231    /// null-vs-empty distinction and never lossily decoding.
1232    ///
1233    /// Unlike [`read_vlen_strings`](Self::read_vlen_strings), which returns
1234    /// `String`s via `from_utf8_lossy` and so cannot reproduce embedded NULs or
1235    /// non-UTF-8 payloads, this yields each element's raw bytes (or a null
1236    /// marker). It underpins faithful rewriting (e.g. repack) of VL strings.
1237    pub(crate) fn read_vlen_string_bytes(
1238        &self,
1239        options: VlenStringReadOptions,
1240    ) -> Result<Vec<vl_data::VlByteObject>, Error> {
1241        let datatype = self.datatype()?;
1242        if !vl_data::is_vlen_string_datatype(&datatype) {
1243            return Err(FormatError::TypeMismatch {
1244                expected: "VariableLength string",
1245                actual: "non-VariableLength string",
1246            }
1247            .into());
1248        }
1249        let dataspace = self.dataspace()?;
1250        if let Some(limit) = options.max_elements()
1251            && dataspace.num_elements() > limit as u64
1252        {
1253            return Err(FormatError::VariableLengthElementLimitExceeded {
1254                limit,
1255                actual: dataspace.num_elements(),
1256            }
1257            .into());
1258        }
1259        let raw = self.read_raw()?;
1260        let source = self.file.source();
1261        Ok(vl_data::read_vl_byte_objects_from_source(
1262            &source,
1263            &raw,
1264            dataspace.num_elements(),
1265            self.file.offset_size(),
1266            self.file.length_size(),
1267            self.file.addr_offset,
1268            1, // a VL string's base type is a single byte
1269            options,
1270        )?)
1271    }
1272
1273    /// Read every element of a *non-string* variable-length (sequence) dataset as
1274    /// its exact heap bytes, alongside the base-type element size in bytes.
1275    ///
1276    /// Each element's heap object holds `length * element_size` bytes, where
1277    /// `length` is the stored element count and `element_size` is the byte width
1278    /// of the sequence's base type. Returning the raw bytes (not decoded values)
1279    /// keeps a faithful rewrite (repack) byte-exact for any base type whose bytes
1280    /// carry no embedded heap or file addresses. Errors with a
1281    /// [`TypeMismatch`](crate::FormatError::TypeMismatch) if the datatype is not a
1282    /// non-string VL datatype.
1283    pub(crate) fn read_vlen_sequence_bytes(
1284        &self,
1285        options: VlenStringReadOptions,
1286    ) -> Result<(Vec<vl_data::VlByteObject>, usize), Error> {
1287        let datatype = self.datatype()?;
1288        let Datatype::VariableLength { base_type, .. } = &datatype else {
1289            return Err(FormatError::TypeMismatch {
1290                expected: "non-string VariableLength",
1291                actual: "non-VariableLength",
1292            }
1293            .into());
1294        };
1295        if vl_data::is_vlen_string_datatype(&datatype) {
1296            return Err(FormatError::TypeMismatch {
1297                expected: "non-string VariableLength",
1298                actual: "VariableLength string",
1299            }
1300            .into());
1301        }
1302        let element_size = base_type.type_size() as usize;
1303        if element_size == 0 {
1304            return Err(
1305                FormatError::VlDataError("non-string VL base type has zero size".into()).into(),
1306            );
1307        }
1308        let dataspace = self.dataspace()?;
1309        if let Some(limit) = options.max_elements()
1310            && dataspace.num_elements() > limit as u64
1311        {
1312            return Err(FormatError::VariableLengthElementLimitExceeded {
1313                limit,
1314                actual: dataspace.num_elements(),
1315            }
1316            .into());
1317        }
1318        let raw = self.read_raw()?;
1319        let source = self.file.source();
1320        let objects = vl_data::read_vl_byte_objects_from_source(
1321            &source,
1322            &raw,
1323            dataspace.num_elements(),
1324            self.file.offset_size(),
1325            self.file.length_size(),
1326            self.file.addr_offset,
1327            element_size,
1328            options,
1329        )?;
1330        Ok((objects, element_size))
1331    }
1332
1333    /// Read all attributes of this dataset.
1334    pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
1335        self.file.attrs_of(&self.header)
1336    }
1337
1338    /// Names of every attribute on this dataset, including any whose datatype
1339    /// [`attrs`](Self::attrs) cannot represent. Used by repack to detect an
1340    /// attribute it would otherwise drop.
1341    pub(crate) fn attr_names(&self) -> Result<Vec<String>, Error> {
1342        self.file.attr_message_names_of(&self.header)
1343    }
1344
1345    /// Returns the exact HDF5 datatype, including compound field offsets and
1346    /// total record size.
1347    pub fn datatype(&self) -> Result<Datatype, Error> {
1348        let msg = find_message(&self.header, MessageType::Datatype)?;
1349        let (dt, _) = Datatype::parse(&msg.data)?;
1350        Ok(dt)
1351    }
1352
1353    pub(crate) fn dataspace(&self) -> Result<Dataspace, Error> {
1354        let msg = find_message(&self.header, MessageType::Dataspace)?;
1355        Ok(Dataspace::parse(&msg.data, self.file.length_size())?)
1356    }
1357
1358    pub(crate) fn data_layout(&self) -> Result<DataLayout, Error> {
1359        let msg = find_message(&self.header, MessageType::DataLayout)?;
1360        Ok(DataLayout::parse(
1361            &msg.data,
1362            self.file.offset_size(),
1363            self.file.length_size(),
1364        )?)
1365    }
1366
1367    pub(crate) fn filter_pipeline(&self) -> Option<FilterPipeline> {
1368        self.header
1369            .messages
1370            .iter()
1371            .find(|m| m.msg_type == MessageType::FilterPipeline)
1372            .and_then(|msg| FilterPipeline::parse(&msg.data).ok())
1373    }
1374
1375    /// The raw, still-compressed on-disk bytes of every allocated chunk of this
1376    /// chunked dataset, with each chunk's `(address, on-disk size, filter mask,
1377    /// logical offset)` — the same `ChunkInfo`s the chunked reader walks before
1378    /// decompressing. Used by repack to copy compressed chunks verbatim without
1379    /// ever decoding them.
1380    ///
1381    /// Returns `Err` if the layout is not chunked. Returns `Ok(vec![])` for an
1382    /// empty / never-allocated chunked dataset (no index address). Covers every
1383    /// index type the reader supports (v3 B-tree and v4 single-chunk, implicit,
1384    /// fixed-array, and extensible-array).
1385    pub(crate) fn raw_chunks(&self) -> Result<Vec<crate::chunked_read::ChunkInfo>, Error> {
1386        let DataLayout::Chunked {
1387            chunk_dimensions,
1388            btree_address,
1389            version,
1390            chunk_index_type,
1391            single_chunk_filtered_size,
1392            single_chunk_filter_mask,
1393        } = self.data_layout()?
1394        else {
1395            return Err(Error::Format(crate::error::FormatError::ChunkedReadError(
1396                "raw_chunks called on a non-chunked dataset".into(),
1397            )));
1398        };
1399        // An undefined index address means no storage is allocated yet.
1400        let Some(addr) = btree_address else {
1401            return Ok(Vec::new());
1402        };
1403        let dataspace = self.dataspace()?;
1404        let elem_size = self.datatype()?.type_size() as usize;
1405        let base = self.file.addr_offset;
1406        let source = self.file.source();
1407        // The chunk index — its root at `addr` and every internal node — stores
1408        // addresses relative to the base address. Walk it through a base-relative
1409        // view so those resolve, then shift each returned chunk address back to an
1410        // absolute file offset, since callers (repack) read the chunk bytes from
1411        // the full file source.
1412        if base == 0 {
1413            return Ok(crate::chunked_read::collect_chunks_for_layout_from_source(
1414                &source,
1415                version,
1416                chunk_index_type,
1417                addr,
1418                single_chunk_filtered_size,
1419                single_chunk_filter_mask,
1420                &chunk_dimensions,
1421                &dataspace,
1422                elem_size,
1423                self.file.offset_size(),
1424                self.file.length_size(),
1425            )?);
1426        }
1427        let framed = BaseOffsetSource {
1428            inner: &source,
1429            base,
1430        };
1431        let mut chunks = crate::chunked_read::collect_chunks_for_layout_from_source(
1432            &framed,
1433            version,
1434            chunk_index_type,
1435            addr,
1436            single_chunk_filtered_size,
1437            single_chunk_filter_mask,
1438            &chunk_dimensions,
1439            &dataspace,
1440            elem_size,
1441            self.file.offset_size(),
1442            self.file.length_size(),
1443        )?;
1444        for c in &mut chunks {
1445            c.address =
1446                c.address
1447                    .checked_add(base)
1448                    .ok_or(crate::error::FormatError::OffsetOverflow {
1449                        offset: c.address,
1450                        length: 0,
1451                    })?;
1452        }
1453        Ok(chunks)
1454    }
1455
1456    /// The raw `FilterPipeline` message bytes from this dataset's object header,
1457    /// if it has one. Repack reuses this verbatim so that every filter — including
1458    /// ones this crate cannot itself apply (ZFP, SZIP, unknown) — is reproduced
1459    /// byte-for-byte in the repacked file's pipeline message.
1460    pub(crate) fn filter_pipeline_message_bytes(&self) -> Option<Vec<u8>> {
1461        self.header
1462            .messages
1463            .iter()
1464            .find(|m| m.msg_type == MessageType::FilterPipeline)
1465            .map(|msg| msg.data.clone())
1466    }
1467
1468    /// Read the dataset's exact unfiltered element bytes.
1469    ///
1470    /// For compound datasets this preserves all file padding and uses the
1471    /// offsets reported by [`datatype`](Self::datatype).
1472    pub fn read_raw(&self) -> Result<Vec<u8>, Error> {
1473        let dt = self.datatype()?;
1474        let ds = self.dataspace()?;
1475        let dl = self.data_layout()?;
1476        // The data layout's on-disk addresses are left base-relative here;
1477        // `read_dataset_raw` applies the base address centrally (for both
1478        // contiguous and chunked layouts) by reading from a base-relative view of
1479        // the file.
1480        let pipeline = self.filter_pipeline();
1481        Ok(self
1482            .file
1483            .read_dataset_raw(&dl, &ds, &dt, pipeline.as_ref(), &self.chunk_cache)?)
1484    }
1485
1486    /// Interpret this dataset as an array of HDF5 object references
1487    /// (`H5R_OBJECT`) and resolve each, in storage order, to the [`Object`] it
1488    /// points at.
1489    ///
1490    /// MATLAB cell arrays and the `#subsystem#` machinery store their members
1491    /// this way: the dataset holds one object-header address per element, each
1492    /// naming an object elsewhere in the file (conventionally under the hidden
1493    /// `#refs#` group).
1494    ///
1495    /// # Errors
1496    ///
1497    /// - [`FormatError::TypeMismatch`] if this dataset's datatype is not an
1498    ///   object reference.
1499    /// - [`FormatError::InvalidObjectReference`] if an element is a null or
1500    ///   undefined reference, or does not point at a group or dataset.
1501    pub fn dereference(&self) -> Result<Vec<Object<'f>>, Error> {
1502        let dt = self.datatype()?;
1503        if !matches!(
1504            dt,
1505            Datatype::Reference {
1506                ref_type: ReferenceType::Object,
1507                ..
1508            }
1509        ) {
1510            return Err(FormatError::TypeMismatch {
1511                expected: "object reference",
1512                actual: "non-reference datatype",
1513            }
1514            .into());
1515        }
1516        // An object reference stores an 8-byte object-header address. Refuse a
1517        // sub-address-width element rather than read a truncated address.
1518        let elem_size = dt.type_size().to_usize()?;
1519        if elem_size < 8 {
1520            return Err(FormatError::TypeMismatch {
1521                expected: "8-byte object reference",
1522                actual: "object reference narrower than 8 bytes",
1523            }
1524            .into());
1525        }
1526        let raw = self.read_raw()?;
1527        if raw.is_empty() {
1528            return Ok(Vec::new());
1529        }
1530        if !raw.len().is_multiple_of(elem_size) {
1531            return Err(FormatError::DataSizeMismatch {
1532                expected: elem_size,
1533                actual: raw.len(),
1534            }
1535            .into());
1536        }
1537        let mut out = Vec::with_capacity(raw.len() / elem_size);
1538        for chunk in raw.chunks_exact(elem_size) {
1539            let addr = u64::from_le_bytes(chunk[..8].try_into().expect("chunk has >= 8 bytes"));
1540            out.push(self.file.object_at_relative(addr)?);
1541        }
1542        Ok(out)
1543    }
1544
1545    /// Decode all elements of a compound dataset field by field.
1546    ///
1547    /// Built-in implementations support numeric tuples with one through twelve
1548    /// fields. Decoding uses the file's field offsets rather than Rust's tuple
1549    /// memory layout, so padded compound records are supported safely.
1550    pub fn read_compound<T: CompoundType>(&self) -> Result<Vec<T>, Error> {
1551        let datatype = self.datatype()?;
1552        let element_size = datatype.type_size().to_usize()?;
1553        if !matches!(datatype, Datatype::Compound { .. }) {
1554            return Err(FormatError::TypeMismatch {
1555                expected: "Compound",
1556                actual: "non-Compound",
1557            }
1558            .into());
1559        }
1560        let raw = self.read_raw()?;
1561        if element_size == 0 || !raw.len().is_multiple_of(element_size) {
1562            return Err(FormatError::DataSizeMismatch {
1563                expected: element_size,
1564                actual: raw.len(),
1565            }
1566            .into());
1567        }
1568        raw.chunks_exact(element_size)
1569            .map(|bytes| T::decode(&datatype, bytes).map_err(Error::from))
1570            .collect()
1571    }
1572
1573    /// Verify this dataset against its stored provenance hash.
1574    ///
1575    /// Recomputes the SHA-256 of the dataset's raw bytes and compares it with
1576    /// the `_provenance_sha256` attribute written by
1577    /// [`DatasetBuilder::with_provenance`](crate::DatasetBuilder::with_provenance).
1578    /// Returns [`VerifyResult::NoHash`](crate::VerifyResult::NoHash) when the
1579    /// dataset carries no provenance hash, so a missing hash is distinguishable
1580    /// from an actual mismatch.
1581    #[cfg(feature = "provenance")]
1582    pub fn verify_provenance(&self) -> Result<crate::provenance::VerifyResult, Error> {
1583        use crate::provenance::{ATTR_SHA256, VerifyResult, sha256_hex};
1584
1585        let attrs = self.attrs()?;
1586        let stored = match attrs.get(ATTR_SHA256) {
1587            Some(AttrValue::String(s) | AttrValue::AsciiString(s)) => {
1588                s.trim_end_matches('\0').to_string()
1589            }
1590            _ => return Ok(VerifyResult::NoHash),
1591        };
1592
1593        let computed = sha256_hex(&self.read_raw()?);
1594        if computed == stored {
1595            Ok(VerifyResult::Ok)
1596        } else {
1597            Ok(VerifyResult::Mismatch { stored, computed })
1598        }
1599    }
1600}
1601
1602// ---------------------------------------------------------------------------
1603// Helpers
1604// ---------------------------------------------------------------------------
1605
1606fn find_message(
1607    header: &ObjectHeader,
1608    msg_type: MessageType,
1609) -> Result<&crate::object_header::HeaderMessage, Error> {
1610    header
1611        .messages
1612        .iter()
1613        .find(|m| m.msg_type == msg_type)
1614        .ok_or(Error::MissingMessage(msg_type))
1615}
1616
1617fn has_message(header: &ObjectHeader, msg_type: MessageType) -> bool {
1618    header.messages.iter().any(|m| m.msg_type == msg_type)
1619}
1620
1621fn is_group(header: &ObjectHeader) -> bool {
1622    header.messages.iter().any(|m| {
1623        m.msg_type == MessageType::LinkInfo
1624            || m.msg_type == MessageType::Link
1625            || m.msg_type == MessageType::SymbolTable
1626    })
1627}
1628
1629#[cfg(test)]
1630mod tests {
1631    use super::*;
1632    use crate::FileBuilder;
1633
1634    /// One 256-element i32 dataset, chunked into 32-element chunks, in memory.
1635    fn chunked_file_bytes() -> Vec<u8> {
1636        let data: Vec<i32> = (0..256).collect();
1637        let mut b = FileBuilder::new();
1638        b.create_dataset("chunked")
1639            .with_i32_data(&data)
1640            .with_shape(&[256])
1641            .with_chunks(&[32]);
1642        b.finish().unwrap()
1643    }
1644
1645    // The DAPL override must drive the *live* `ChunkCache`, not merely the value
1646    // reported by `chunk_cache_config()`. These assertions reach the crate's
1647    // `#[cfg(test)]` cache introspection (unavailable to integration tests), so
1648    // they fail if the resolved config ever stops flowing into the real cache.
1649
1650    #[test]
1651    fn enabled_override_populates_live_cache_over_disabled_file_default() {
1652        let file = File::from_bytes_with_options(
1653            chunked_file_bytes(),
1654            FileAccessOptions::new().with_chunk_cache(ChunkCacheConfig::disabled()),
1655        )
1656        .unwrap();
1657
1658        let ds = file
1659            .dataset_with_options(
1660                "chunked",
1661                DatasetAccessOptions::new().with_chunk_cache(ChunkCacheConfig::new()),
1662            )
1663            .unwrap();
1664        assert_eq!(ds.read_i32().unwrap(), (0..256).collect::<Vec<i32>>());
1665
1666        // The enabled override built the chunk index and retained chunks; the
1667        // disabled file default would have left both empty.
1668        assert!(ds.chunk_cache_stats().index_loaded());
1669        assert!(ds.chunk_cache_stats().cached_chunks() > 0);
1670    }
1671
1672    #[test]
1673    fn disabled_override_suppresses_live_cache_over_enabled_file_default() {
1674        let file = File::from_bytes_with_options(
1675            chunked_file_bytes(),
1676            FileAccessOptions::new().with_chunk_cache(ChunkCacheConfig::new()),
1677        )
1678        .unwrap();
1679
1680        let ds = file
1681            .dataset_with_options(
1682                "chunked",
1683                DatasetAccessOptions::new().with_chunk_cache(ChunkCacheConfig::disabled()),
1684            )
1685            .unwrap();
1686        assert_eq!(ds.read_i32().unwrap(), (0..256).collect::<Vec<i32>>());
1687
1688        // The disabled override suppressed the index and chunk retention; the
1689        // enabled file default would have populated both.
1690        assert!(!ds.chunk_cache_stats().index_loaded());
1691        assert_eq!(ds.chunk_cache_stats().cached_chunks(), 0);
1692    }
1693}