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}