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};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7
8use crate::edit::{
9 AppendBuilder, AppendGeometry, AppendTarget, EditBacking, MemoryStrategy, SpaceAccounting,
10 WriteEngine,
11};
12use crate::element::H5Element;
13use crate::type_builders::{DatasetBuilder, VL_REF_SIZE};
14
15use crate::attribute::{extract_attributes_full, extract_attributes_full_from_source};
16use crate::chunk_cache::{ChunkCache, ChunkCacheConfig, ChunkCacheStats};
17use crate::compound::CompoundType;
18use crate::convert::TryToUsize;
19use crate::data_layout::DataLayout;
20use crate::data_read;
21use crate::dataspace::Dataspace;
22use crate::datatype::{Datatype, ReferenceType};
23use crate::error::{Error, FormatError};
24use crate::file_create_properties::FileCreateProperties;
25use crate::file_lock::{self, FileLocking, OpenIntent};
26use crate::file_space_info::{FileSpaceInfo, FileSpaceStrategy};
27use crate::filter_pipeline::FilterPipeline;
28use crate::free_space_manager;
29use crate::group_v1::GroupEntry;
30use crate::group_v2;
31use crate::layout_info::{Chunk, ChunkIndex, Filter, Layout};
32use crate::libver::LibVer;
33use crate::message_type::MessageType;
34use crate::object_header::ObjectHeader;
35use crate::signature;
36use crate::source::{
37 BaseOffsetSource, BytesSource, MetadataCacheConfig, MetadataCachingSource, ReadSeekSource,
38 Source,
39};
40use crate::superblock::Superblock;
41use crate::vl_data::{self, VlenStringReadOptions};
42
43use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype};
44
45// ---------------------------------------------------------------------------
46// File
47// ---------------------------------------------------------------------------
48
49/// Backing store for a [`File`]: either the whole file buffered in memory, or a
50/// lazy [`Source`] that reads regions on demand (see [`File::open_streaming`]).
51enum Backend {
52 InMemory(Vec<u8>),
53 Streaming(Box<dyn Source + Send + Sync>),
54 /// A read-write file opened with [`File::open_rw`] or
55 /// [`File::open_rw_bounded`]: a [`WriteEngine`] (exclusive OS lock + staged
56 /// edit queues + append geometry cache) behind a lock, so owned handles can
57 /// both read and mutate in place. Handle write methods route to the engine,
58 /// and `File::commit` applies staged structural edits.
59 ///
60 /// Either backing — a whole-file mirror, or positioned I/O against the
61 /// handle — appears here as the same `WriteEngine`; which one an open
62 /// resolved to is the engine's own business rather than the backend's
63 /// (issue #198). Reads
64 /// borrow the mirror's slice when there is one and go through the image's
65 /// `Source` otherwise; see [`with_engine`](FileInner::with_engine). Boxed to
66 /// keep the `Backend` enum small (a `WriteEngine` is far larger than the
67 /// other variants).
68 Edit(Box<Mutex<WriteEngine>>),
69}
70
71/// A borrowed `Source` view over a [`File`]'s backend, used by the
72/// streaming-capable read paths so one call site serves both backends.
73pub(crate) enum SourceView<'a> {
74 Mem(&'a [u8]),
75 Stream(&'a (dyn Source + Send + Sync)),
76}
77
78impl Source for SourceView<'_> {
79 fn len(&self) -> u64 {
80 match self {
81 SourceView::Mem(b) => b.len() as u64,
82 SourceView::Stream(s) => s.len(),
83 }
84 }
85 fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
86 match self {
87 SourceView::Mem(b) => BytesSource::new(*b).read_at(offset, buf),
88 SourceView::Stream(s) => s.read_at(offset, buf),
89 }
90 }
91
92 fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
93 match self {
94 SourceView::Mem(b) => BytesSource::new(*b).read_metadata_at(offset, len),
95 SourceView::Stream(s) => s.read_metadata_at(offset, len),
96 }
97 }
98}
99
100/// A base-relative view of an in-memory file: `bytes` with its first `base` bytes
101/// (the userblock) cut off, so every address stored relative to the base address
102/// indexes it directly. The in-memory counterpart of [`BaseOffsetSource`], and the
103/// identity for a plain file.
104fn frame(bytes: &[u8], base: u64) -> Result<&[u8], FormatError> {
105 if base == 0 {
106 return Ok(bytes);
107 }
108 let start = base.to_usize()?;
109 bytes.get(start..).ok_or(FormatError::UnexpectedEof {
110 expected: start,
111 available: bytes.len(),
112 })
113}
114
115/// File-access properties applied when opening an HDF5 file.
116///
117/// This is the `hdf5-pure` analogue of an HDF5 **file access property list**
118/// (`fapl`): one value carrying every access-time setting, built once and passed
119/// to whichever open a caller reaches for, exactly as a `fapl` is handed to
120/// `H5Fopen`. Every `*_with_options` constructor on [`File`] accepts it, so a
121/// read path and a read-write path can share one configuration.
122///
123/// The `Properties` suffix means the type stands in for one whole HDF5 property
124/// list, so every setting on it has a C counterpart to look up. It is a stand-in
125/// and not a port: a plain `Copy` value, with no handle to create or close, no
126/// runtime property registry, and no setter that can fail. `fapl` and each
127/// `H5Pset_*` it models are doc aliases, so a search for either lands here.
128///
129/// - The metadata cache (`H5Pset_mdc_config`) applies to the streaming and
130/// bounded backends; an in-memory open already holds the whole file in one
131/// buffer.
132/// - The chunk cache (`H5Pset_cache`) is the file-wide default for datasets
133/// opened from any backend, overridable per dataset with
134/// [`DatasetAccessProperties`].
135/// - The locking policy (`H5Pset_file_locking`) applies to the read-write opens.
136/// Readers and the SWMR writer take no lock by design, so they ignore it.
137///
138/// See the [property-support reference] for the full property-by-property map.
139///
140/// [property-support reference]: https://github.com/stephenberry/hdf5-pure/blob/main/docs/reference/property-support.md
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
142#[doc(alias = "fapl")]
143pub struct FileAccessProperties {
144 metadata_cache: MetadataCacheConfig,
145 chunk_cache: ChunkCacheConfig,
146 locking: FileLocking,
147 memory_strategy: Option<MemoryStrategy>,
148}
149
150/// Former name of [`FileAccessProperties`].
151#[deprecated(
152 since = "0.26.0",
153 note = "renamed to `FileAccessProperties`: a type standing in for a whole HDF5 property list now carries the `Properties` suffix"
154)]
155pub type FileAccessOptions = FileAccessProperties;
156
157impl FileAccessProperties {
158 /// A value carrying the crate's default access behavior.
159 pub const fn new() -> Self {
160 Self {
161 metadata_cache: MetadataCacheConfig::disabled(),
162 chunk_cache: ChunkCacheConfig::new(),
163 locking: FileLocking::Enabled,
164 memory_strategy: None,
165 }
166 }
167
168 /// Configure the bounded streaming metadata cache.
169 #[doc(alias = "H5Pset_mdc_config")]
170 pub const fn with_metadata_cache(mut self, metadata_cache: MetadataCacheConfig) -> Self {
171 self.metadata_cache = metadata_cache;
172 self
173 }
174
175 /// Configure the per-dataset raw chunk cache used by datasets opened from
176 /// this file. This is the `H5Pset_cache`-style file-wide default.
177 #[doc(alias = "H5Pset_cache")]
178 pub const fn with_chunk_cache(mut self, chunk_cache: ChunkCacheConfig) -> Self {
179 self.chunk_cache = chunk_cache;
180 self
181 }
182
183 /// Set the OS advisory file-locking policy for the read-write opens.
184 ///
185 /// Defaults to [`FileLocking::Enabled`]. Use [`FileLocking::Disabled`] only
186 /// when an external mechanism already guarantees single-writer access, or
187 /// [`FileLocking::BestEffort`] on a filesystem (such as some network mounts)
188 /// where the OS lock is unavailable. Setting `HDF5_USE_FILE_LOCKING` in the
189 /// environment overrides this, as in the C library.
190 ///
191 /// Readers and [`File::open_swmr_writer`] take no lock by design and ignore
192 /// this.
193 #[doc(alias = "H5Pset_file_locking")]
194 pub const fn with_locking(mut self, locking: FileLocking) -> Self {
195 self.locking = locking;
196 self
197 }
198
199 /// Set how much memory a read-write open may use to hold the file.
200 ///
201 /// Unset by default, which lets the entry point choose:
202 /// [`File::open_rw`] uses [`MemoryStrategy::Auto`], preferring the bounded
203 /// engine and falling back to the whole-file mirror for a file it cannot
204 /// edit, while the deprecated [`File::open_rw_bounded`] uses
205 /// [`MemoryStrategy::Bounded`], refusing such a file rather than quietly
206 /// spending `O(file size)` memory on a caller who asked not to. Setting this
207 /// overrides both, in either direction; [`MemoryStrategy::Mirrored`] takes
208 /// the whole-file mirror unconditionally, as `open_rw` did before it learned
209 /// to dispatch.
210 ///
211 /// The read-only opens ignore this: they build no editing session at all, and
212 /// their own names say what memory they spend. [`File::open_swmr_writer`]
213 /// does build one, and always mirrors: it accepts
214 /// [`MemoryStrategy::Auto`] and [`MemoryStrategy::Mirrored`], both of which
215 /// the mirror satisfies, and refuses an explicit [`MemoryStrategy::Bounded`]
216 /// with [`Error::EditUnsupported`] rather than quietly not honoring it. Ask a
217 /// `File` which backend it resolved to with [`File::edit_backing`].
218 pub const fn with_memory_strategy(mut self, memory_strategy: MemoryStrategy) -> Self {
219 self.memory_strategy = Some(memory_strategy);
220 self
221 }
222
223 /// Return the configured streaming metadata cache.
224 pub const fn metadata_cache(&self) -> MetadataCacheConfig {
225 self.metadata_cache
226 }
227
228 /// Return the configured per-dataset chunk cache.
229 pub const fn chunk_cache(&self) -> ChunkCacheConfig {
230 self.chunk_cache
231 }
232
233 /// Return the configured file-locking policy.
234 pub const fn locking(&self) -> FileLocking {
235 self.locking
236 }
237
238 /// Return the configured memory strategy, or `None` when none was asked for
239 /// and the entry point's own default applies. This is what was *requested*;
240 /// for which backend an open resolved to, see [`File::edit_backing`].
241 ///
242 /// The `Option` exists so the deprecated [`File::open_rw_bounded`] can
243 /// default to [`MemoryStrategy::Bounded`] while [`File::open_rw`] defaults to
244 /// [`MemoryStrategy::Auto`]; when that pair is removed this should collapse to
245 /// a plain `MemoryStrategy` with `Auto` as the default, in that same release
246 /// rather than as a second break on this accessor.
247 pub const fn memory_strategy(&self) -> Option<MemoryStrategy> {
248 self.memory_strategy
249 }
250}
251
252/// Dataset-access properties applied when opening a single dataset.
253///
254/// This is the `hdf5-pure` analogue of an HDF5 **dataset access property list**
255/// (`dapl`). Its chunk cache corresponds to `H5Pset_chunk_cache`: it overrides,
256/// for this one dataset, the file-wide chunk-cache default configured with
257/// [`FileAccessProperties::with_chunk_cache`] (the `H5Pset_cache` analogue). When
258/// left unset, the dataset inherits that file-wide default — matching the `dapl`
259/// default sentinels (`H5D_CHUNK_CACHE_*_DEFAULT`), which also mean "use the
260/// file's setting".
261///
262/// The `Properties` suffix means the type stands in for one whole HDF5 property
263/// list, so every setting on it has a C counterpart to look up. It is a stand-in
264/// and not a port: a plain `Copy` value, with no handle to create or close, no
265/// runtime property registry, and no setter that can fail. `dapl` and each
266/// `H5Pset_*` it models are doc aliases, so a search for either lands here.
267/// The chunk cache is the one `dapl` property modeled; see the
268/// [property-support reference] for the rest.
269///
270/// [`ChunkCacheConfig`] maps `H5Pset_chunk_cache`'s `rdcc_nslots` and
271/// `rdcc_nbytes`; its `rdcc_w0` preemption policy is not modeled, because this
272/// read cache uses strict LRU eviction (as noted on
273/// [`ChunkCacheConfig::from_h5p_cache`]).
274///
275/// Pass it to [`File::dataset_with_options`] or [`Group::dataset_with_options`].
276///
277/// [property-support reference]: https://github.com/stephenberry/hdf5-pure/blob/main/docs/reference/property-support.md
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
279#[doc(alias = "dapl")]
280pub struct DatasetAccessProperties {
281 chunk_cache: Option<ChunkCacheConfig>,
282}
283
284/// Former name of [`DatasetAccessProperties`].
285#[deprecated(
286 since = "0.26.0",
287 note = "renamed to `DatasetAccessProperties`: a type standing in for a whole HDF5 property list now carries the `Properties` suffix"
288)]
289pub type DatasetAccessOptions = DatasetAccessProperties;
290
291impl DatasetAccessProperties {
292 /// A value that inherits every file-wide access default.
293 pub const fn new() -> Self {
294 Self { chunk_cache: None }
295 }
296
297 /// Override the raw chunk cache for this one dataset, ignoring the file-wide
298 /// default. This is the `H5Pset_chunk_cache` analogue.
299 #[doc(alias = "H5Pset_chunk_cache")]
300 pub const fn with_chunk_cache(mut self, chunk_cache: ChunkCacheConfig) -> Self {
301 self.chunk_cache = Some(chunk_cache);
302 self
303 }
304
305 /// Return the chunk-cache override, or `None` when the dataset inherits the
306 /// file-wide default.
307 pub const fn chunk_cache(&self) -> Option<ChunkCacheConfig> {
308 self.chunk_cache
309 }
310
311 /// Resolve the effective chunk-cache config: the per-dataset override if one
312 /// was set, otherwise the file-wide `default`.
313 const fn resolved_chunk_cache(&self, default: ChunkCacheConfig) -> ChunkCacheConfig {
314 match self.chunk_cache {
315 Some(config) => config,
316 None => default,
317 }
318 }
319}
320
321/// Test whether a file looks like an HDF5 file, without reading it whole.
322///
323/// This is the spelling of the C library's `H5Fis_accessible` /
324/// `H5Fis_hdf5`: it opens the file and scans only the 8-byte candidate windows
325/// where the HDF5 signature is permitted (offsets 0, 512, 1024, 2048, …), so it
326/// never buffers the whole file. Returns:
327///
328/// - `Ok(true)` — the HDF5 signature was found,
329/// - `Ok(false)` — the file opened but has no HDF5 signature,
330/// - `Err(..)` — the file could not be opened (missing, permissions, …).
331///
332/// It validates only the signature, not the rest of the format; a truncated or
333/// corrupt file past the signature still reports `true`. Use [`File::open`] to
334/// fully parse and validate.
335pub fn is_hdf5<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<bool> {
336 let handle = std::fs::File::open(path)?;
337 let source = ReadSeekSource::new(handle).map_err(std::io::Error::other)?;
338 match signature::find_signature_in(&source) {
339 Ok(_) => Ok(true),
340 Err(FormatError::SignatureNotFound) => Ok(false),
341 Err(e) => Err(std::io::Error::other(e)),
342 }
343}
344
345/// Test whether an in-memory buffer begins (at a permitted offset) with the
346/// HDF5 signature. The buffer-backed counterpart of [`is_hdf5`].
347pub fn is_hdf5_bytes(data: &[u8]) -> bool {
348 signature::find_signature(data).is_ok()
349}
350
351/// An open HDF5 file for reading.
352struct FileInner {
353 backend: Backend,
354 superblock: Superblock,
355 /// Byte offset to add to all relative addresses (= original base_address).
356 addr_offset: u64,
357 /// Live file handle, retained only when the file was opened with
358 /// [`File::open_swmr`] so [`File::refresh`] can re-read appended data.
359 handle: Option<std::fs::File>,
360 /// File Space Info parsed from the superblock extension, if the file records
361 /// one. Best-effort: a malformed or unreadable extension leaves this `None`
362 /// rather than failing the open.
363 file_space_info: Option<FileSpaceInfo>,
364 access_properties: FileAccessProperties,
365 /// Set by [`File::close`] to seal a read-write file: after it, a write
366 /// through any surviving [`Dataset`]/[`Group`] handle or [`File`] clone
367 /// returns [`Error::FileClosed`]. Reads still work. Only ever set on a
368 /// `Backend::Edit` file.
369 closed: AtomicBool,
370 /// True for a file opened with [`File::open_swmr_writer`]: no OS lock is held,
371 /// the superblock's SWMR-write flag is raised, only immediate
372 /// [`Dataset::append`] is permitted (the staged surface is refused), and the
373 /// flag is cleared on [`File::close`] / `Drop`. `false` for every other file.
374 swmr_write: bool,
375}
376
377impl Drop for FileInner {
378 /// Best-effort cleanup for a writer dropped without an explicit
379 /// [`File::close`], running only when the last `Arc<FileInner>` clone drops;
380 /// a clean `close` already did this work and set `closed`, so this is
381 /// idempotent and skipped in that case.
382 ///
383 /// - A SWMR writer clears the superblock's SWMR-write flag (mirroring
384 /// `File::close`).
385 /// - A read-write file that persists its free space rewrites its on-disk
386 /// free-space managers into canonical shape (issue #173), so a
387 /// dropped-without-`close` handle leaves the same file a clean `close`
388 /// would (a no-op unless an immediate append grew the file past them). A
389 /// true crash (`SIGKILL`, power loss) skips `drop` entirely; the appended
390 /// data is still durable.
391 ///
392 /// Staged edits are *not* committed here: dropping a handle discards them,
393 /// which is what `close` exists to distinguish.
394 fn drop(&mut self) {
395 if self.closed.load(Ordering::Acquire) {
396 return;
397 }
398 let Backend::Edit(m) = &self.backend else {
399 return;
400 };
401 let mut session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
402 if self.swmr_write {
403 let _ = session.set_consistency_flags(0);
404 return;
405 }
406 let _ = session.finalize_persist();
407 let _ = session.sync();
408 }
409}
410
411impl FileInner {
412 /// Open an HDF5 file from a filesystem path.
413 ///
414 /// Reads the file into memory once. To follow a file that a concurrent
415 /// single writer is appending to (SWMR), use [`File::open_swmr`] instead.
416 /// To read a file larger than memory (e.g. on a 32-bit host) without
417 /// buffering it, use [`File::open_streaming`].
418 pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
419 Self::open_with_options(path, FileAccessProperties::new())
420 }
421
422 /// Open an HDF5 file from a filesystem path with explicit access properties.
423 ///
424 /// Like [`open`](Self::open), this buffers the whole file in memory. Use
425 /// [`open_streaming_with_options`](Self::open_streaming_with_options) when
426 /// the metadata cache budget should apply to lazy metadata reads.
427 pub fn open_with_options<P: AsRef<std::path::Path>>(
428 path: P,
429 properties: FileAccessProperties,
430 ) -> Result<Self, Error> {
431 let bytes = std::fs::read(path.as_ref()).map_err(Error::Io)?;
432 let inner = Self::from_bytes_with_options(bytes, properties)?;
433 // The status-flag check belongs to the *path* opens, not to
434 // `from_bytes_with_options` (issue #245). A caller who already holds the
435 // bytes has taken its own snapshot: there is no live file to coordinate
436 // over, and the recovery this refusal would name — `clear_swmr_flag`,
437 // which needs write access to a path — is not available to it either.
438 // This is a deliberate divergence from the C library, which checks under
439 // its in-memory core driver too.
440 file_lock::check_status_flags(&inner.superblock, OpenIntent::Read, path.as_ref())?;
441 Ok(inner)
442 }
443
444 /// Open an HDF5 file for **streaming** reads, fetching regions on demand from
445 /// the file instead of buffering it whole.
446 ///
447 /// This lets a host read a file larger than its address space — the original
448 /// motivation being 32-bit targets reading multi-gigabyte files (issue #27).
449 /// Metadata and dataset chunks are read through a `ReadSeekSource`, so peak
450 /// memory stays close to one chunk plus the metadata being parsed.
451 ///
452 /// Reads match the buffered [`File::open`]: every storage layout and chunk
453 /// index type, both group forms (v2 and v1 symbol-table), and compact,
454 /// dense, shared, and variable-length attributes. What differs:
455 /// [`as_bytes`](Self::as_bytes) returns an empty slice (there is no
456 /// whole-file buffer), [`persisted_free_space`](Self::persisted_free_space)
457 /// returns no regions, a streaming file cannot be the *source* of a
458 /// cross-file copy, and chunk decompression is sequential (the `parallel`
459 /// feature accelerates only buffered reads).
460 pub fn open_streaming<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
461 Self::open_streaming_with_options(path, FileAccessProperties::new())
462 }
463
464 /// Open an HDF5 file for streaming reads with explicit access properties.
465 pub fn open_streaming_with_options<P: AsRef<std::path::Path>>(
466 path: P,
467 properties: FileAccessProperties,
468 ) -> Result<Self, Error> {
469 let handle = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
470 let source = ReadSeekSource::new(handle).map_err(Error::Format)?;
471 let source: Box<dyn Source + Send + Sync> = if properties.metadata_cache.is_enabled() {
472 Box::new(MetadataCachingSource::new(
473 source,
474 properties.metadata_cache,
475 ))
476 } else {
477 Box::new(source)
478 };
479 let (superblock, addr_offset) = Self::parse_superblock_source(source.as_ref())?;
480 file_lock::check_status_flags(&superblock, OpenIntent::Read, path.as_ref())?;
481 Ok(Self::from_parts(
482 Backend::Streaming(source),
483 superblock,
484 addr_offset,
485 None,
486 properties,
487 ))
488 }
489
490 /// Open an HDF5 file for SWMR (single-writer/multiple-reader) reading.
491 ///
492 /// Like [`File::open`], but retains a live handle to the file so that
493 /// [`File::refresh`] can re-read data appended by a concurrent writer
494 /// (whether produced by this crate's append writer, the reference HDF5 C
495 /// library, or h5py in SWMR mode). The initial view is a consistent
496 /// snapshot; call [`File::refresh`] to advance to a newer one.
497 ///
498 /// Only the `std` build supports this (it requires a live filesystem
499 /// handle); the in-memory [`File::from_bytes`] path cannot refresh.
500 pub fn open_swmr<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
501 Self::open_swmr_with_options(path, FileAccessProperties::new())
502 }
503
504 /// Open an HDF5 file for SWMR reading with explicit access properties.
505 ///
506 /// SWMR reads currently keep an in-memory mirror for refresh semantics, so
507 /// only the per-dataset chunk-cache settings affect this backend.
508 pub fn open_swmr_with_options<P: AsRef<std::path::Path>>(
509 path: P,
510 properties: FileAccessProperties,
511 ) -> Result<Self, Error> {
512 let mut handle = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
513 let mut data = Vec::new();
514 handle.read_to_end(&mut data).map_err(Error::Io)?;
515 let (superblock, addr_offset) = Self::parse_superblock(&data)?;
516 file_lock::check_status_flags(&superblock, OpenIntent::SwmrRead, path.as_ref())?;
517 Ok(Self::from_parts(
518 Backend::InMemory(data),
519 superblock,
520 addr_offset,
521 Some(handle),
522 properties,
523 ))
524 }
525
526 /// Open an HDF5 file from an in-memory byte vector.
527 pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> {
528 Self::from_bytes_with_options(data, FileAccessProperties::new())
529 }
530
531 /// Open an HDF5 file from an in-memory byte vector with explicit access properties.
532 pub fn from_bytes_with_options(
533 data: Vec<u8>,
534 properties: FileAccessProperties,
535 ) -> Result<Self, Error> {
536 let (superblock, addr_offset) = Self::parse_superblock(&data)?;
537 Ok(Self::from_parts(
538 Backend::InMemory(data),
539 superblock,
540 addr_offset,
541 None,
542 properties,
543 ))
544 }
545
546 /// Open an existing HDF5 file for reading **and** in-place editing, applying
547 /// `properties` (its [`FileLocking`] policy governs the OS file lock held for
548 /// the file's life, and its chunk cache is the file-wide default).
549 fn open_rw<P: AsRef<std::path::Path>>(
550 path: P,
551 properties: FileAccessProperties,
552 ) -> Result<Self, Error> {
553 Self::open_rw_with_default(path, properties, MemoryStrategy::Auto)
554 }
555
556 /// Open read-write under the properties' memory strategy, falling back to
557 /// `default` when the caller expressed none. The two public read-write entry
558 /// points differ only in that default: [`File::open_rw`] prefers the bounded
559 /// engine but takes the mirror for a file the bounded engine cannot edit,
560 /// while [`File::open_rw_bounded`] refuses that file instead (issue #198,
561 /// step 4).
562 fn open_rw_with_default<P: AsRef<std::path::Path>>(
563 path: P,
564 properties: FileAccessProperties,
565 default: MemoryStrategy,
566 ) -> Result<Self, Error> {
567 let session = WriteEngine::open_rw_with_strategy(
568 path.as_ref(),
569 properties.metadata_cache,
570 properties.locking,
571 properties.memory_strategy.unwrap_or(default),
572 )?;
573 Self::from_rw_session(session, properties)
574 }
575
576 /// Wrap an opened [`WriteEngine`] as a read-write [`Backend::Edit`] file.
577 fn from_rw_session(
578 session: WriteEngine,
579 properties: FileAccessProperties,
580 ) -> Result<Self, Error> {
581 // The engine parsed and normalized this at open; take it rather than
582 // re-parsing, so the image need not be able to hand out a slice.
583 let superblock = session.superblock().clone();
584 let addr_offset = superblock.base_address;
585 Ok(Self::from_parts(
586 Backend::Edit(Box::new(Mutex::new(session))),
587 superblock,
588 addr_offset,
589 None,
590 properties,
591 ))
592 }
593
594 /// Open for SWMR writing: no OS lock, superblock SWMR-write flag raised.
595 fn open_swmr_writer<P: AsRef<std::path::Path>>(
596 path: P,
597 properties: FileAccessProperties,
598 ) -> Result<Self, Error> {
599 // The SWMR writer always mirrors. `Auto` and unset are *satisfied* by
600 // that — they ask for the bounded engine where it applies and accept the
601 // mirror where it does not — but `Bounded` is a guarantee, and honoring a
602 // guarantee by ignoring it is how a caller ends up spending `O(file size)`
603 // memory it asked not to. Refusing is also the permissive direction to be
604 // wrong in: if this writer ever runs bounded, the refusal stops firing,
605 // which breaks nobody.
606 if properties.memory_strategy == Some(MemoryStrategy::Bounded) {
607 return Err(Error::EditUnsupported(
608 "the SWMR writer always holds the file in a whole-file mirror; leave \
609 MemoryStrategy unset, or pass MemoryStrategy::Auto or MemoryStrategy::Mirrored, \
610 to open it",
611 ));
612 }
613 let mut inner = Self::from_rw_session(WriteEngine::open_swmr_writer(path)?, properties)?;
614 inner.swmr_write = true;
615 Ok(inner)
616 }
617
618 /// Open for bounded-memory reading and appending (issue #147): no
619 /// whole-file mirror, and no fallback to one; see [`File::open_rw_bounded`].
620 fn open_rw_bounded<P: AsRef<std::path::Path>>(
621 path: P,
622 properties: FileAccessProperties,
623 ) -> Result<Self, Error> {
624 Self::open_rw_with_default(path, properties, MemoryStrategy::Bounded)
625 }
626
627 /// After the caller has confirmed a [`Backend::Edit`] backend, gate the
628 /// mutation: refuse a sealed file with [`Error::FileClosed`], and in
629 /// SWMR-writer mode refuse a staged edit (`staged = true`) with
630 /// [`Error::SwmrStagedUnsupported`] — only immediate appends are allowed.
631 fn check_mutable(&self, staged: bool) -> Result<(), Error> {
632 if self.closed.load(Ordering::Acquire) {
633 return Err(Error::FileClosed);
634 }
635 if staged && self.swmr_write {
636 return Err(Error::SwmrStagedUnsupported);
637 }
638 Ok(())
639 }
640
641 /// Gate a staged edit *without* taking the session lock: the backend must
642 /// offer the staged surface, and the file must still be mutable.
643 ///
644 /// This is the same gate the locking helpers apply before locking, split out
645 /// so a public method taking a user closure can report a read-only or sealed
646 /// file up front, run the closure with no lock held, and take the lock only
647 /// to record the result (issue #200).
648 fn check_staged_writable(&self) -> Result<(), Error> {
649 match &self.backend {
650 Backend::Edit(_) => self.check_mutable(true),
651 _ => Err(Error::ReadOnly),
652 }
653 }
654
655 /// A `Source` view over the backend, for the streaming-capable paths.
656 pub(crate) fn source(&self) -> SourceView<'_> {
657 match &self.backend {
658 Backend::InMemory(v) => SourceView::Mem(v),
659 Backend::Streaming(s) => SourceView::Stream(s.as_ref()),
660 // A mirror or bounded file's bytes live behind a lock and cannot be
661 // lent out as a borrowed view; the read paths that reach every
662 // backend go through [`with_source`](Self::with_source) instead.
663 Backend::Edit(_) => SourceView::Mem(&[]),
664 }
665 }
666
667 /// Run `f` with a random-access view of this file's bytes, taking the
668 /// write-engine lock when the backend requires one. Unlike
669 /// [`source`](Self::source) — which cannot lend a borrowed view out of a
670 /// lock and returns an empty view for the mirror and bounded backends —
671 /// this serves every backend, so it is the dispatch for read paths (heap
672 /// reads for variable-length data, chunk enumeration) that must also work
673 /// on a read-write file. `f` must not re-enter this file's backend (the
674 /// engine lock is held while it runs).
675 pub(crate) fn with_source<R>(&self, f: impl FnOnce(&dyn Source) -> R) -> R {
676 match &self.backend {
677 Backend::InMemory(v) => f(&BytesSource::new(v.as_slice())),
678 Backend::Streaming(s) => f(s.as_ref()),
679 Backend::Edit(m) => {
680 let core = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
681 f(core.image())
682 }
683 }
684 }
685
686 /// Run a read against a read-write session's file image, choosing the form
687 /// its backing can serve: `on_slice` when the session holds the whole file
688 /// in memory, so a slice-walking parser borrows the bytes instead of copying
689 /// them, and `on_source` otherwise.
690 ///
691 /// Both closures must compute the same thing. The pair exists because a
692 /// mirror can hand out a whole-file slice and a file-backed image cannot,
693 /// not because the two backings answer differently (issue #198).
694 fn with_engine<R>(
695 engine: &Mutex<WriteEngine>,
696 on_slice: impl FnOnce(&[u8]) -> R,
697 on_source: impl FnOnce(&dyn Source) -> R,
698 ) -> R {
699 let core = engine
700 .lock()
701 .unwrap_or_else(std::sync::PoisonError::into_inner);
702 match core.image_slice() {
703 Some(data) => on_slice(data),
704 None => on_source(core.image()),
705 }
706 }
707
708 /// Parse the superblock from `data`, returning it (with `root_group_address`
709 /// normalized to an absolute offset) and the base-address offset.
710 fn parse_superblock(data: &[u8]) -> Result<(Superblock, u64), Error> {
711 let sig_offset = signature::find_signature(data)?;
712 let mut superblock = Superblock::parse(data, sig_offset)?;
713 let addr_offset = superblock.base_address;
714 // Normalize root_group_address to absolute so resolve_path_any works.
715 superblock.root_group_address = superblock
716 .root_group_address
717 .checked_add(addr_offset)
718 .ok_or(FormatError::OffsetOverflow {
719 offset: superblock.root_group_address,
720 length: addr_offset,
721 })?;
722 debug_assert!(superblock.root_group_address >= addr_offset);
723 Ok((superblock, addr_offset))
724 }
725
726 /// Streaming counterpart of [`parse_superblock`]: locate and parse the
727 /// superblock by reading only small windows from the source.
728 fn parse_superblock_source<S: Source + ?Sized>(source: &S) -> Result<(Superblock, u64), Error> {
729 let sig_offset = signature::find_signature_in(source)?;
730 let mut superblock = Superblock::parse_from_source(source, sig_offset)?;
731 let addr_offset = superblock.base_address;
732 superblock.root_group_address = superblock
733 .root_group_address
734 .checked_add(addr_offset)
735 .ok_or(FormatError::OffsetOverflow {
736 offset: superblock.root_group_address,
737 length: addr_offset,
738 })?;
739 debug_assert!(superblock.root_group_address >= addr_offset);
740 Ok((superblock, addr_offset))
741 }
742
743 /// Assemble a [`File`] from parsed parts, then load the File Space Info from
744 /// the superblock extension (best-effort, so a bad extension never fails the
745 /// open).
746 fn from_parts(
747 backend: Backend,
748 superblock: Superblock,
749 addr_offset: u64,
750 handle: Option<std::fs::File>,
751 access_properties: FileAccessProperties,
752 ) -> Self {
753 let mut file = FileInner {
754 backend,
755 superblock,
756 addr_offset,
757 handle,
758 file_space_info: None,
759 access_properties,
760 closed: AtomicBool::new(false),
761 swmr_write: false,
762 };
763 file.file_space_info = file.read_file_space_info();
764 file
765 }
766
767 /// Parse the File Space Info message from the superblock extension, if the
768 /// file records one and it can be read. Best-effort: any failure (no
769 /// extension, unreadable object header, malformed message) yields `None`.
770 fn read_file_space_info(&self) -> Option<FileSpaceInfo> {
771 let rel = self.superblock.superblock_extension_address?;
772 if rel == u64::MAX {
773 return None;
774 }
775 let abs = self.addr_offset.checked_add(rel)?;
776 let header = self.parse_header(abs).ok()?;
777 let msg = header
778 .messages
779 .iter()
780 .find(|m| m.msg_type == MessageType::FileSpaceInfo)?;
781 FileSpaceInfo::parse(
782 &msg.data,
783 self.superblock.offset_size,
784 self.superblock.length_size,
785 )
786 .ok()
787 }
788
789 /// Re-read the file from disk to pick up data appended by a concurrent
790 /// writer, then re-parse the superblock.
791 ///
792 /// This is the SWMR reader's refresh primitive (analogous to the C library's
793 /// `H5Drefresh` / h5py's `Dataset.refresh()`): after it returns, newly
794 /// fetched [`Dataset`]/[`Group`] handles observe the writer's appended
795 /// chunks and extended dimensions, because they re-parse object headers at
796 /// their (stable) addresses against the refreshed bytes. Existing handles
797 /// borrow `&self`, so they must be dropped before calling this; re-fetch
798 /// them afterward.
799 ///
800 /// Returns [`Error::SwmrUnsupported`] if the file was not opened with
801 /// [`File::open_swmr`]. The superblock is checksum-validated on every
802 /// re-read; a transient parse failure (a writer caught mid-flush) is
803 /// retried a bounded number of times before being surfaced.
804 ///
805 /// Cost: each call re-reads the entire file from disk (`O(file size)`).
806 /// That keeps the implementation simple and correct, but when following a
807 /// large, steadily growing log it is the cost paid per refresh; budget
808 /// refresh frequency accordingly.
809 pub fn refresh(&mut self) -> Result<(), Error> {
810 let handle = self.handle.as_mut().ok_or(Error::SwmrUnsupported)?;
811
812 // A writer only appends (the file grows) and updates a few fixed-size,
813 // individually checksummed structures in place (superblock EOF, object
814 // header dimensions, array header counts). Re-reading the whole file and
815 // re-validating the superblock checksum yields a consistent view; if the
816 // superblock is caught mid-update, retry.
817 const MAX_ATTEMPTS: u32 = 100;
818 let mut last_err = None;
819 for attempt in 0..MAX_ATTEMPTS {
820 let mut data = Vec::new();
821 handle.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
822 handle.read_to_end(&mut data).map_err(Error::Io)?;
823 match Self::parse_superblock(&data) {
824 Ok((superblock, addr_offset)) => {
825 self.backend = Backend::InMemory(data);
826 self.superblock = superblock;
827 self.addr_offset = addr_offset;
828 self.file_space_info = self.read_file_space_info();
829 return Ok(());
830 }
831 Err(e) => {
832 last_err = Some(e);
833 // Brief backoff before re-reading; the writer's in-place
834 // updates are tiny, so a short pause clears the window. Skip
835 // it on the final attempt, where there is no re-read to come.
836 if attempt + 1 < MAX_ATTEMPTS {
837 std::thread::sleep(std::time::Duration::from_micros(
838 50 * (attempt + 1) as u64,
839 ));
840 }
841 }
842 }
843 }
844 // The loop always runs at least once and only reaches here via the
845 // `Err` arm, so `last_err` is always `Some`; surface the real error.
846 Err(last_err.expect("refresh retried at least once before failing"))
847 }
848
849 /// Resolve a path to an object-header address, dispatching on the backend.
850 fn resolve_path(&self, path: &str) -> Result<u64, Error> {
851 Ok(match &self.backend {
852 Backend::InMemory(v) => group_v2::resolve_path_any(v, &self.superblock, path)?,
853 Backend::Streaming(s) => {
854 group_v2::resolve_path_any_from_source(s.as_ref(), &self.superblock, path)?
855 }
856 // A staged commit can relocate the object tree's root, so this
857 // file's cached superblock may name a stale one; resolve against the
858 // session's own superblock, which the commit updates.
859 Backend::Edit(m) => {
860 let core = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
861 let sb = core.superblock().clone();
862 match core.image_slice() {
863 Some(data) => group_v2::resolve_path_any(data, &sb, path)?,
864 None => group_v2::resolve_path_any_from_source(core.image(), &sb, path)?,
865 }
866 }
867 })
868 }
869
870 /// The current root-group address (base-adjusted, absolute). For a read-write
871 /// [`Backend::Edit`] file a prior relocating commit can have moved the
872 /// root, so take the session's own superblock, which the commit updates;
873 /// other backends use this file's cached one.
874 fn mirror_root_address(&self) -> u64 {
875 if let Backend::Edit(m) = &self.backend {
876 let core = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
877 return core.superblock().root_group_address;
878 }
879 self.superblock.root_group_address
880 }
881
882 /// Returns the raw file bytes for an in-memory file, or an empty slice for a
883 /// streaming file (which has no whole-file buffer).
884 pub fn as_bytes(&self) -> &[u8] {
885 match &self.backend {
886 Backend::InMemory(v) => v,
887 // A streaming, mirror, or bounded file has no borrowable whole-file
888 // buffer.
889 Backend::Streaming(_) | Backend::Edit(_) => &[],
890 }
891 }
892
893 /// Return the access properties used when opening this file.
894 pub const fn access_properties(&self) -> FileAccessProperties {
895 self.access_properties
896 }
897
898 /// The backend this file's editing session resolved to, or `None` when there
899 /// is no editing session to ask. Always [`EditBacking::Mirrored`] for the
900 /// SWMR writer, which builds a session but does not dispatch on the strategy.
901 fn edit_backing(&self) -> Option<EditBacking> {
902 match &self.backend {
903 Backend::Edit(m) => Some(m.lock().unwrap_or_else(|e| e.into_inner()).edit_backing()),
904 _ => None,
905 }
906 }
907
908 /// Returns a reference to the parsed superblock.
909 pub fn superblock(&self) -> &Superblock {
910 &self.superblock
911 }
912
913 /// The whole-file byte image when this file is buffered in memory
914 /// ([`open`](Self::open) / [`from_bytes`](Self::from_bytes)); `None` for a
915 /// streaming file ([`open_streaming`](Self::open_streaming)). Cross-file
916 /// object copy ([`File::copy_from`](crate::File::copy_from)) uses this to read
917 /// source objects by absolute address.
918 pub(crate) fn in_memory_image(&self) -> Option<&[u8]> {
919 match &self.backend {
920 Backend::InMemory(data) => Some(data),
921 Backend::Streaming(_) | Backend::Edit(_) => None,
922 }
923 }
924
925 /// The base address (`H5F` superblock base address), i.e. the byte offset
926 /// added to every stored relative address. Zero for a file with no
927 /// userblock.
928 pub(crate) fn base_address(&self) -> u64 {
929 self.addr_offset
930 }
931
932 /// The file-space management strategy this file records in its superblock
933 /// extension (set with `H5Pset_file_space_strategy`), or `None` if the file
934 /// records none — the default, which the C library also writes as "no
935 /// message". See [`file_space_info`](Self::file_space_info) for the full
936 /// record (persist flag, threshold, page size).
937 pub fn file_space_strategy(&self) -> Option<FileSpaceStrategy> {
938 self.file_space_info.as_ref().map(|info| info.strategy)
939 }
940
941 /// The full [`FileSpaceInfo`] recorded in this file's superblock extension,
942 /// if present and readable.
943 pub fn file_space_info(&self) -> Option<&FileSpaceInfo> {
944 self.file_space_info.as_ref()
945 }
946
947 /// The free regions a file persists on disk in its free-space managers (when
948 /// written with `H5Pset_file_space_strategy(..., persist = true)`), as
949 /// `(address, length)` pairs sorted by address.
950 ///
951 /// Empty when the file does not persist free space, or for the streaming
952 /// backend (which does not load the manager blocks). The addresses are file
953 /// offsets (relative to the base address); reading data is unaffected by the
954 /// presence or absence of these managers.
955 pub fn persisted_free_space(&self) -> Vec<(u64, u64)> {
956 let Some(info) = &self.file_space_info else {
957 return Vec::new();
958 };
959 if !info.persist {
960 return Vec::new();
961 }
962 let Backend::InMemory(data) = &self.backend else {
963 return Vec::new();
964 };
965 let mut sections = free_space_manager::read_persisted_sections(
966 data,
967 &info.manager_addrs,
968 self.addr_offset,
969 self.superblock.offset_size,
970 )
971 .unwrap_or_default();
972 sections.sort_by_key(|s| s.addr);
973 sections.into_iter().map(|s| (s.addr, s.size)).collect()
974 }
975
976 /// The size of the underlying file in bytes (the HDF5 `H5Fget_filesize`).
977 ///
978 /// This is the total byte length of the backing store — for a streaming
979 /// file the length reported by its source, for an in-memory file the length
980 /// of its buffer. It includes any userblock prefix and trailing bytes, so it
981 /// may exceed the superblock's logical end-of-file address; compare against
982 /// `Superblock::eof_address` (reachable via
983 /// [`File::superblock`]) to detect appended or unaccounted tail bytes.
984 pub fn file_size(&self) -> u64 {
985 match &self.backend {
986 Backend::Edit(m) => {
987 let core = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
988 core.image().len()
989 }
990 _ => self.source().len(),
991 }
992 }
993
994 /// The minimum library version required to read this file, derived from its
995 /// superblock version (the *low bound* of HDF5's `H5Fget_libver_bounds`).
996 ///
997 /// A version 3 superblock, for example, reports [`LibVer::V110`] because it
998 /// was introduced in HDF5 1.10.
999 pub fn libver_bound(&self) -> LibVer {
1000 LibVer::from_superblock_version(self.superblock.version)
1001 }
1002
1003 fn parse_header(&self, address: u64) -> Result<ObjectHeader, FormatError> {
1004 let os = self.superblock.offset_size;
1005 let ls = self.superblock.length_size;
1006 match &self.backend {
1007 Backend::InMemory(v) => {
1008 ObjectHeader::parse_with_base(v, address.to_usize()?, os, ls, self.addr_offset)
1009 }
1010 Backend::Streaming(s) => {
1011 ObjectHeader::parse_from_source(s.as_ref(), address, os, ls, self.addr_offset)
1012 }
1013 Backend::Edit(m) => Self::with_engine(
1014 m,
1015 |d| ObjectHeader::parse_with_base(d, address.to_usize()?, os, ls, self.addr_offset),
1016 |s| ObjectHeader::parse_from_source(s, address, os, ls, self.addr_offset),
1017 ),
1018 }
1019 }
1020
1021 /// Resolve a base-relative object-header address (the value stored in an
1022 /// HDF5 `H5R_OBJECT` reference element) to the [`Object`] it points at.
1023 ///
1024 /// The stored address is relative to the superblock base address, so any
1025 /// MAT-file userblock is accounted for here. A null (`0`) or undefined
1026 /// (`HADDR_UNDEF`) address, or one whose object header is neither a dataset
1027 /// nor a group, yields [`FormatError::InvalidObjectReference`].
1028 fn object_at_relative(file: &Arc<FileInner>, rel_addr: u64) -> Result<Object, Error> {
1029 // HADDR_UNDEF and the null address never name a real object. (Relative
1030 // address 0 is where the superblock sits, not an object header.)
1031 if rel_addr == u64::MAX || rel_addr == 0 {
1032 return Err(FormatError::InvalidObjectReference(rel_addr).into());
1033 }
1034 let abs = rel_addr
1035 .checked_add(file.addr_offset)
1036 .ok_or(FormatError::InvalidObjectReference(rel_addr))?;
1037 let hdr = file.parse_header(abs)?;
1038 if has_message(&hdr, MessageType::DataLayout) {
1039 let chunk_cache = DatasetAccessProperties::new()
1040 .resolved_chunk_cache(file.access_properties.chunk_cache);
1041 Ok(Object::Dataset(Box::new(Dataset {
1042 file: file.clone(),
1043 address: abs,
1044 header: hdr,
1045 chunk_cache: ChunkCache::with_config(chunk_cache),
1046 chunk_cache_config: chunk_cache,
1047 path: None,
1048 })))
1049 } else if is_group(&hdr) {
1050 Ok(Object::Group(Group {
1051 file: file.clone(),
1052 address: abs,
1053 path: None,
1054 }))
1055 } else {
1056 Err(FormatError::InvalidObjectReference(rel_addr).into())
1057 }
1058 }
1059
1060 fn offset_size(&self) -> u8 {
1061 self.superblock.offset_size
1062 }
1063
1064 fn length_size(&self) -> u8 {
1065 self.superblock.length_size
1066 }
1067
1068 /// Resolve the children of a group object header, dispatching on the backend
1069 /// and converting link addresses to absolute.
1070 fn group_children(&self, hdr: &ObjectHeader) -> Result<Vec<GroupEntry>, Error> {
1071 let (os, ls, base) = (self.offset_size(), self.length_size(), self.addr_offset);
1072 let mut entries = match &self.backend {
1073 Backend::InMemory(v) => group_v2::resolve_group_entries(v, hdr, os, ls, base),
1074 Backend::Streaming(s) => {
1075 group_v2::resolve_group_entries_from_source(s.as_ref(), hdr, os, ls, base)
1076 }
1077 Backend::Edit(m) => Self::with_engine(
1078 m,
1079 |d| group_v2::resolve_group_entries(d, hdr, os, ls, base),
1080 |s| group_v2::resolve_group_entries_from_source(s, hdr, os, ls, base),
1081 ),
1082 }
1083 .map_err(Error::Format)?;
1084 for entry in &mut entries {
1085 // The stored address is relative to the base address; normalize to an
1086 // absolute file offset. A crafted entry (e.g. the HADDR_UNDEF sentinel)
1087 // must not wrap or panic.
1088 entry.object_header_address = entry.object_header_address.checked_add(base).ok_or(
1089 FormatError::OffsetOverflow {
1090 offset: entry.object_header_address,
1091 length: base,
1092 },
1093 )?;
1094 }
1095 Ok(entries)
1096 }
1097
1098 /// Read all attributes attached to an object header, dispatching on the
1099 /// backend.
1100 fn attrs_of(&self, hdr: &ObjectHeader) -> Result<HashMap<String, AttrValue>, Error> {
1101 let (os, ls, base) = (self.offset_size(), self.length_size(), self.addr_offset);
1102 let attr_msgs = self.attr_messages_of(hdr)?;
1103 match &self.backend {
1104 Backend::Edit(m) => Ok(Self::with_engine(
1105 m,
1106 |d| attrs_to_map(&attr_msgs, &BytesSource::new(d), os, ls, base),
1107 |s| attrs_to_map(&attr_msgs, s, os, ls, base),
1108 )),
1109 _ => Ok(attrs_to_map(&attr_msgs, &self.source(), os, ls, base)),
1110 }
1111 }
1112
1113 /// Names of every attribute message on `hdr`, including ones whose datatype
1114 /// [`attrs_of`](Self::attrs_of) cannot decode into an [`AttrValue`] (and so
1115 /// silently omits from its map). Repack diffs this against the decoded map to
1116 /// refuse rather than drop an attribute it cannot reproduce.
1117 pub(crate) fn attr_message_names_of(&self, hdr: &ObjectHeader) -> Result<Vec<String>, Error> {
1118 Ok(self
1119 .attr_messages_of(hdr)?
1120 .into_iter()
1121 .map(|a| a.name)
1122 .collect())
1123 }
1124
1125 /// Extract every attribute message attached to an object header (compact,
1126 /// shared, and dense storage), dispatching on the backend.
1127 fn attr_messages_of(
1128 &self,
1129 hdr: &ObjectHeader,
1130 ) -> Result<Vec<crate::attribute::AttributeMessage>, Error> {
1131 let (os, ls) = (self.offset_size(), self.length_size());
1132 // Compact attributes come out of `hdr`, but the two addresses this walk
1133 // follows are read from message bodies and so are stored relative to the
1134 // base address: the Attribute Info message's fractal-heap address, and a
1135 // shared attribute's message address. Frame the file at `base` exactly as
1136 // [`Self::read_dataset_raw`] does, so both index it directly. For a plain
1137 // file (`base == 0`) this is the identity; without it, a userblock file's
1138 // dense attributes are looked for one userblock too early.
1139 let base = self.addr_offset;
1140 match &self.backend {
1141 Backend::InMemory(v) => Ok(extract_attributes_full(frame(v, base)?, hdr, os, ls)?),
1142 Backend::Streaming(s) if base == 0 => Ok(extract_attributes_full_from_source(
1143 s.as_ref(),
1144 hdr,
1145 os,
1146 ls,
1147 )?),
1148 Backend::Streaming(s) => {
1149 let framed = BaseOffsetSource {
1150 inner: s.as_ref(),
1151 base,
1152 };
1153 Ok(extract_attributes_full_from_source(&framed, hdr, os, ls)?)
1154 }
1155 Backend::Edit(m) => Self::with_engine(
1156 m,
1157 |d| Ok(extract_attributes_full(frame(d, base)?, hdr, os, ls)?),
1158 |s| {
1159 if base == 0 {
1160 Ok(extract_attributes_full_from_source(s, hdr, os, ls)?)
1161 } else {
1162 let framed = BaseOffsetSource { inner: s, base };
1163 Ok(extract_attributes_full_from_source(&framed, hdr, os, ls)?)
1164 }
1165 },
1166 ),
1167 }
1168 }
1169
1170 /// Read a dataset's raw bytes for the given layout, dispatching on the backend.
1171 fn read_dataset_raw(
1172 &self,
1173 dl: &DataLayout,
1174 ds: &Dataspace,
1175 dt: &Datatype,
1176 pipeline: Option<&FilterPipeline>,
1177 cache: &ChunkCache,
1178 ) -> Result<Vec<u8>, FormatError> {
1179 let (os, ls) = (self.offset_size(), self.length_size());
1180 // Every on-disk address in `dl` — the contiguous data address, the chunk
1181 // index root, and (followed deeper in the chunked reader) every B-tree /
1182 // fixed-array / extensible-array node and chunk-data address — is stored
1183 // relative to the base address. Present the payload reader a base-relative
1184 // view of the file so all of them index it directly: slice the in-memory
1185 // buffer at `base`, or wrap the streaming source to add `base` to each
1186 // read. For a plain file (`base == 0`) this is the identity.
1187 let base = self.addr_offset;
1188 match &self.backend {
1189 Backend::InMemory(v) => data_read::read_raw_data_cached(
1190 frame(v, base)?,
1191 dl,
1192 ds,
1193 dt,
1194 pipeline,
1195 os,
1196 ls,
1197 cache,
1198 ),
1199 Backend::Streaming(s) if base == 0 => data_read::read_raw_data_cached_from_source(
1200 s.as_ref(),
1201 dl,
1202 ds,
1203 dt,
1204 pipeline,
1205 os,
1206 ls,
1207 cache,
1208 ),
1209 Backend::Streaming(s) => {
1210 let framed = BaseOffsetSource {
1211 inner: s.as_ref(),
1212 base,
1213 };
1214 data_read::read_raw_data_cached_from_source(
1215 &framed, dl, ds, dt, pipeline, os, ls, cache,
1216 )
1217 }
1218 Backend::Edit(m) => Self::with_engine(
1219 m,
1220 |data| {
1221 let frame = if base == 0 {
1222 data
1223 } else {
1224 let start = base.to_usize()?;
1225 data.get(start..).ok_or(FormatError::UnexpectedEof {
1226 expected: start,
1227 available: data.len(),
1228 })?
1229 };
1230 data_read::read_raw_data_cached(frame, dl, ds, dt, pipeline, os, ls, cache)
1231 },
1232 |s| {
1233 let framed = BaseOffsetSource { inner: s, base };
1234 data_read::read_raw_data_cached_from_source(
1235 &framed, dl, ds, dt, pipeline, os, ls, cache,
1236 )
1237 },
1238 ),
1239 }
1240 }
1241
1242 /// Windowed counterpart of [`read_dataset_raw`](Self::read_dataset_raw): read
1243 /// the raw element bytes of the row window `[start_row, start_row + num_rows)`,
1244 /// touching only the storage it overlaps. Reads through the same base-framed
1245 /// `Source`, so on-disk addresses resolve the same way. The caller clamps
1246 /// the window to the dataset.
1247 #[allow(clippy::too_many_arguments)]
1248 fn read_dataset_raw_rows(
1249 &self,
1250 dl: &DataLayout,
1251 ds: &Dataspace,
1252 dt: &Datatype,
1253 pipeline: Option<&FilterPipeline>,
1254 cache: &ChunkCache,
1255 start_row: u64,
1256 num_rows: u64,
1257 ) -> Result<Vec<u8>, FormatError> {
1258 let (os, ls) = (self.offset_size(), self.length_size());
1259 let elem_size = dt.type_size() as usize;
1260 // Elements per row (product of inner dims; 1 when 0-D or 1-D). Checked so
1261 // a crafted dataspace whose inner dims overflow `usize` errors instead of
1262 // panicking (debug) or wrapping (release).
1263 let row_elems: usize = ds.dimensions.iter().skip(1).try_fold(1usize, |acc, &d| {
1264 acc.checked_mul(d.to_usize()?)
1265 .ok_or(FormatError::OffsetOverflow {
1266 offset: acc as u64,
1267 length: d,
1268 })
1269 })?;
1270 let row_bytes = row_elems
1271 .checked_mul(elem_size)
1272 .ok_or(FormatError::OffsetOverflow {
1273 offset: row_elems as u64,
1274 length: elem_size as u64,
1275 })?;
1276
1277 // Compact data is inline in the layout message — no I/O, no framing.
1278 if let DataLayout::Compact { data } = dl {
1279 let start = start_row.to_usize()?.checked_mul(row_bytes);
1280 let len = num_rows.to_usize()?.checked_mul(row_bytes);
1281 let (Some(start), Some(len)) = (start, len) else {
1282 return Err(FormatError::OffsetOverflow {
1283 offset: start_row,
1284 length: row_bytes as u64,
1285 });
1286 };
1287 let end = start.checked_add(len).ok_or(FormatError::OffsetOverflow {
1288 offset: start as u64,
1289 length: len as u64,
1290 })?;
1291 return data
1292 .get(start..end)
1293 .map(<[u8]>::to_vec)
1294 .ok_or(FormatError::DataSizeMismatch {
1295 expected: end,
1296 actual: data.len(),
1297 });
1298 }
1299
1300 let base = self.addr_offset;
1301 match &self.backend {
1302 Backend::InMemory(v) => {
1303 let frame = if base == 0 {
1304 v.as_slice()
1305 } else {
1306 let start = base.to_usize()?;
1307 v.get(start..).ok_or(FormatError::UnexpectedEof {
1308 expected: start,
1309 available: v.len(),
1310 })?
1311 };
1312 read_rows_framed(
1313 &BytesSource::new(frame),
1314 dl,
1315 ds,
1316 dt,
1317 pipeline,
1318 os,
1319 ls,
1320 cache,
1321 start_row,
1322 num_rows,
1323 row_bytes,
1324 )
1325 }
1326 Backend::Streaming(s) if base == 0 => read_rows_framed(
1327 s.as_ref(),
1328 dl,
1329 ds,
1330 dt,
1331 pipeline,
1332 os,
1333 ls,
1334 cache,
1335 start_row,
1336 num_rows,
1337 row_bytes,
1338 ),
1339 Backend::Streaming(s) => {
1340 let framed = BaseOffsetSource {
1341 inner: s.as_ref(),
1342 base,
1343 };
1344 read_rows_framed(
1345 &framed, dl, ds, dt, pipeline, os, ls, cache, start_row, num_rows, row_bytes,
1346 )
1347 }
1348 Backend::Edit(m) => Self::with_engine(
1349 m,
1350 |data| {
1351 let frame = if base == 0 {
1352 data
1353 } else {
1354 let start = base.to_usize()?;
1355 data.get(start..).ok_or(FormatError::UnexpectedEof {
1356 expected: start,
1357 available: data.len(),
1358 })?
1359 };
1360 read_rows_framed(
1361 &BytesSource::new(frame),
1362 dl,
1363 ds,
1364 dt,
1365 pipeline,
1366 os,
1367 ls,
1368 cache,
1369 start_row,
1370 num_rows,
1371 row_bytes,
1372 )
1373 },
1374 |s| {
1375 let framed = BaseOffsetSource { inner: s, base };
1376 read_rows_framed(
1377 &framed, dl, ds, dt, pipeline, os, ls, cache, start_row, num_rows,
1378 row_bytes,
1379 )
1380 },
1381 ),
1382 }
1383 }
1384}
1385
1386/// Read a row window through an already base-framed `Source`. Contiguous
1387/// layouts are one bounded sub-read; chunked layouts use the windowed chunk
1388/// reader (only the rank-0 crafted-file corner falls back to a whole read
1389/// plus slice).
1390#[allow(clippy::too_many_arguments)]
1391fn read_rows_framed<S: Source + ?Sized>(
1392 source: &S,
1393 dl: &DataLayout,
1394 ds: &Dataspace,
1395 dt: &Datatype,
1396 pipeline: Option<&FilterPipeline>,
1397 os: u8,
1398 ls: u8,
1399 cache: &ChunkCache,
1400 start_row: u64,
1401 num_rows: u64,
1402 row_bytes: usize,
1403) -> Result<Vec<u8>, FormatError> {
1404 // A zero-row window reads nothing, uniformly across the *supported* layouts.
1405 // Return early so that over unallocated storage — where the whole-dataset
1406 // readers differ (a contiguous None errors with `NoDataAllocated`, a chunked
1407 // None errors with "no address") — the contiguous and chunked arms agree
1408 // instead of one erroring and one succeeding. A `Virtual` layout is
1409 // unsupported and must still error like `read_raw` does, so it is excluded
1410 // here and falls through to the match.
1411 if num_rows == 0 && !matches!(dl, DataLayout::Virtual { .. }) {
1412 return Ok(Vec::new());
1413 }
1414 match dl {
1415 DataLayout::Compact { .. } => unreachable!("compact is handled before framing"),
1416 DataLayout::Contiguous { address, size } => {
1417 let addr = address.ok_or(FormatError::NoDataAllocated)?;
1418 let start =
1419 start_row
1420 .checked_mul(row_bytes as u64)
1421 .ok_or(FormatError::OffsetOverflow {
1422 offset: start_row,
1423 length: row_bytes as u64,
1424 })?;
1425 let len =
1426 num_rows
1427 .to_usize()?
1428 .checked_mul(row_bytes)
1429 .ok_or(FormatError::OffsetOverflow {
1430 offset: num_rows,
1431 length: row_bytes as u64,
1432 })?;
1433 // Never read past the dataset's own contiguous storage.
1434 if start.saturating_add(len as u64) > *size {
1435 return Err(FormatError::DataSizeMismatch {
1436 expected: start.to_usize()?.saturating_add(len),
1437 actual: (*size).to_usize()?,
1438 });
1439 }
1440 let off = addr.checked_add(start).ok_or(FormatError::OffsetOverflow {
1441 offset: addr,
1442 length: start,
1443 })?;
1444 source.read_exact_at(off, len)
1445 }
1446 DataLayout::Chunked { .. } => {
1447 match crate::chunked_read::read_chunked_rows_from_source(
1448 source, dl, ds, dt, pipeline, os, ls, cache, start_row, num_rows,
1449 )? {
1450 Some(bytes) => Ok(bytes),
1451 // Rank-0 chunked (a crafted-file corner): fall back to a whole
1452 // read, then slice.
1453 None => {
1454 let full = data_read::read_raw_data_cached_from_source(
1455 source, dl, ds, dt, pipeline, os, ls, cache,
1456 )?;
1457 let start = start_row.to_usize()? * row_bytes;
1458 let len = num_rows.to_usize()? * row_bytes;
1459 full.get(start..start + len).map(<[u8]>::to_vec).ok_or(
1460 FormatError::DataSizeMismatch {
1461 expected: start + len,
1462 actual: full.len(),
1463 },
1464 )
1465 }
1466 }
1467 }
1468 DataLayout::Virtual { .. } => Err(FormatError::UnsupportedVirtualLayout),
1469 }
1470}
1471
1472impl std::fmt::Debug for FileInner {
1473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1474 f.debug_struct("File")
1475 .field("size", &self.file_size())
1476 .field("superblock_version", &self.superblock.version)
1477 .finish()
1478 }
1479}
1480
1481/// An open HDF5 file.
1482///
1483/// A `File` is an owned, cheaply cloneable handle to an open file: cloning it (or
1484/// deriving a [`Dataset`]/[`Group`] from it) shares one underlying open file
1485/// rather than re-reading it. Object handles returned by [`dataset`](Self::dataset),
1486/// [`group`](Self::group), and [`root`](Self::root) are **owned** — they keep the
1487/// file open for as long as they live and carry no borrow of the `File`, so they
1488/// can be stored in a struct, cached, and moved across threads.
1489#[derive(Clone)]
1490pub struct File {
1491 inner: Arc<FileInner>,
1492}
1493
1494impl std::fmt::Debug for File {
1495 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1496 std::fmt::Debug::fmt(&*self.inner, f)
1497 }
1498}
1499
1500impl File {
1501 /// Open an HDF5 file from a filesystem path.
1502 ///
1503 /// Reads the file into memory once. To follow a file that a concurrent
1504 /// single writer is appending to (SWMR), use [`File::open_swmr`] instead.
1505 /// To read a file larger than memory (e.g. on a 32-bit host) without
1506 /// buffering it, use [`File::open_streaming`].
1507 ///
1508 /// A file whose superblock marks it as held by a writer is refused with
1509 /// [`Error::FileMarkedInUse`](crate::Error::FileMarkedInUse) — the check
1510 /// `H5Fopen` makes of the same byte. That means a live writer or one that
1511 /// exited without closing the file; clear a stale flag with
1512 /// [`clear_swmr_flag`](Self::clear_swmr_flag), and follow a live SWMR writer
1513 /// with [`open_swmr`](Self::open_swmr). [`from_bytes`](Self::from_bytes) does
1514 /// not check, since its caller already holds the bytes — which is also the
1515 /// way to read a flagged file on a read-only mount, where clearing the flag
1516 /// would need write access.
1517 pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
1518 Ok(File {
1519 inner: Arc::new(FileInner::open(path)?),
1520 })
1521 }
1522
1523 /// Open an HDF5 file from a filesystem path with explicit access properties.
1524 pub fn open_with_options<P: AsRef<std::path::Path>>(
1525 path: P,
1526 properties: FileAccessProperties,
1527 ) -> Result<Self, Error> {
1528 Ok(File {
1529 inner: Arc::new(FileInner::open_with_options(path, properties)?),
1530 })
1531 }
1532
1533 /// Open an HDF5 file for **streaming** reads, fetching regions on demand from
1534 /// the file instead of buffering it whole.
1535 ///
1536 /// This lets a host read a file larger than its address space. Metadata and
1537 /// dataset chunks are read through a `ReadSeekSource`, so peak memory stays
1538 /// close to one chunk plus the metadata being parsed. Attribute reading and
1539 /// v1 symbol-table groups on the resolved path are not yet supported on this
1540 /// backend.
1541 ///
1542 /// Like [`open`](Self::open), this refuses a file whose superblock marks it
1543 /// as held by a writer.
1544 pub fn open_streaming<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
1545 Ok(File {
1546 inner: Arc::new(FileInner::open_streaming(path)?),
1547 })
1548 }
1549
1550 /// Open an HDF5 file for streaming reads with explicit access properties.
1551 pub fn open_streaming_with_options<P: AsRef<std::path::Path>>(
1552 path: P,
1553 properties: FileAccessProperties,
1554 ) -> Result<Self, Error> {
1555 Ok(File {
1556 inner: Arc::new(FileInner::open_streaming_with_options(path, properties)?),
1557 })
1558 }
1559
1560 /// Open an HDF5 file for SWMR (single-writer/multiple-reader) reading.
1561 ///
1562 /// Like [`File::open`], but retains a live handle to the file so that
1563 /// [`File::refresh`] can re-read data appended by a concurrent writer.
1564 ///
1565 /// This is the open that *follows* a file marked as held by a SWMR writer,
1566 /// where [`open`](Self::open) refuses one. Only a half-set mark is refused
1567 /// here, with [`Error::FileMarkedInUse`](crate::Error::FileMarkedInUse):
1568 /// either bit without the other. Write access alone is what a plain
1569 /// (non-SWMR) writer leaves, and there is no protocol for following a writer
1570 /// that is not publishing consistent prefixes; the SWMR bit alone is a state
1571 /// no writer produces. Both bits is the live SWMR writer this exists to
1572 /// follow, and neither is a quiescent file.
1573 #[doc(alias = "H5F_ACC_SWMR_READ")]
1574 pub fn open_swmr<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
1575 Ok(File {
1576 inner: Arc::new(FileInner::open_swmr(path)?),
1577 })
1578 }
1579
1580 /// Open an HDF5 file for SWMR reading with explicit access properties.
1581 pub fn open_swmr_with_options<P: AsRef<std::path::Path>>(
1582 path: P,
1583 properties: FileAccessProperties,
1584 ) -> Result<Self, Error> {
1585 Ok(File {
1586 inner: Arc::new(FileInner::open_swmr_with_options(path, properties)?),
1587 })
1588 }
1589
1590 /// Open an HDF5 file from an in-memory byte vector.
1591 pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> {
1592 Ok(File {
1593 inner: Arc::new(FileInner::from_bytes(data)?),
1594 })
1595 }
1596
1597 /// Open an HDF5 file from an in-memory byte vector with explicit access properties.
1598 pub fn from_bytes_with_options(
1599 data: Vec<u8>,
1600 properties: FileAccessProperties,
1601 ) -> Result<Self, Error> {
1602 Ok(File {
1603 inner: Arc::new(FileInner::from_bytes_with_options(data, properties)?),
1604 })
1605 }
1606
1607 /// Open an existing HDF5 file for reading **and** in-place editing.
1608 ///
1609 /// Unlike [`open`](Self::open) (read-only, buffered), this takes an exclusive
1610 /// OS file lock held for the file's life and lets owned handles modify the
1611 /// file — immediate [`Dataset::append`]s, plus [`Dataset::write`]/`set_attr`,
1612 /// [`Group::create_dataset`]/`create_group`/`delete`/`set_attr`, and
1613 /// [`copy`](Self::copy)/[`copy_from`](Self::copy_from) staged until
1614 /// [`commit`](Self::commit). The file must use 8-byte offsets and lengths and
1615 /// keep its superblock at its base address (a canonical userblock, as in a
1616 /// MATLAB `.mat` file, is supported); anything else is refused with
1617 /// [`Error::EditUnsupported`](crate::Error::EditUnsupported).
1618 ///
1619 /// The fast immediate [`Dataset::append`] additionally requires a
1620 /// latest-format (version-2/3) file with no userblock and an
1621 /// Extensible-Array-indexed dataset; [`Dataset::append_staged`] covers the
1622 /// general case.
1623 ///
1624 /// Two things can turn this open away because another writer holds the file:
1625 /// the exclusive OS lock, reported as
1626 /// [`Error::FileLocked`](crate::Error::FileLocked), and the superblock's
1627 /// status-flags byte, reported as
1628 /// [`Error::FileMarkedInUse`](crate::Error::FileMarkedInUse). The second
1629 /// covers what the first cannot — a SWMR writer takes no lock, and a writer
1630 /// that exited without closing the file leaves the flag behind; recover a
1631 /// stale one with [`clear_swmr_flag`](Self::clear_swmr_flag).
1632 ///
1633 /// # Memory
1634 ///
1635 /// This picks its backing from the file rather than making the caller pick a
1636 /// function (issue #198): a latest-format file with no userblock is edited
1637 /// **bounded**, holding only the metadata being parsed plus the configured
1638 /// caches plus what an edit is building, so resident memory does not scale
1639 /// with the file; anything else falls back to a whole-file in-memory mirror,
1640 /// which is what makes a pre-v2 or userblock file editable at all. The two
1641 /// backings are the same engine over different storage and offer the same
1642 /// edit surface, differing in one trade: the bounded one applies a large
1643 /// immediate append in whole-chunk batches, each crash-atomic on its own, so
1644 /// a crash mid-call leaves a valid shorter dataset rather than none of the
1645 /// append. Ask a file which it got with
1646 /// [`edit_backing`](Self::edit_backing), and demand one with
1647 /// [`FileAccessProperties::with_memory_strategy`] —
1648 /// [`MemoryStrategy::Mirrored`] restores the unconditional mirror this
1649 /// entry point used before it learned to dispatch.
1650 #[doc(alias = "H5Fopen")]
1651 pub fn open_rw<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
1652 Self::open_rw_with_options(path, FileAccessProperties::new())
1653 }
1654
1655 /// Open an existing file for reading and in-place editing with explicit
1656 /// access properties — see [`open_rw`](Self::open_rw).
1657 ///
1658 /// The properties carry the locking policy (the `H5Pset_file_locking` analogue,
1659 /// [`FileAccessProperties::with_locking`]), the memory strategy
1660 /// ([`FileAccessProperties::with_memory_strategy`], which overrides the
1661 /// dispatch described on [`open_rw`](Self::open_rw)), the metadata cache used
1662 /// by the bounded backing, and the file-wide chunk-cache default applied to
1663 /// datasets opened from this file. Because one [`FileAccessProperties`] value
1664 /// serves every open, the same configuration can be shared with a read path.
1665 pub fn open_rw_with_options<P: AsRef<std::path::Path>>(
1666 path: P,
1667 properties: FileAccessProperties,
1668 ) -> Result<Self, Error> {
1669 Ok(File {
1670 inner: Arc::new(FileInner::open_rw(path, properties)?),
1671 })
1672 }
1673
1674 /// Open exactly as [`open_rw`](Self::open_rw) does, but behind an image that
1675 /// withholds its whole-file slice, so every read takes the `Source` path
1676 /// rather than the slice fast path.
1677 ///
1678 /// Each read this file serves has two forms (see `with_engine`), and only
1679 /// the slice form runs in production until a mirrorless backing lands
1680 /// (issue #198). Opening the same file both ways and comparing is what
1681 /// holds the other form to the same answers in the meantime.
1682 #[cfg(test)]
1683 pub(crate) fn open_rw_source_only(path: &std::path::Path) -> Result<Self, Error> {
1684 Ok(File {
1685 inner: Arc::new(FileInner::from_rw_session(
1686 WriteEngine::open_source_only(path)?,
1687 FileAccessProperties::new(),
1688 )?),
1689 })
1690 }
1691
1692 /// Open an existing file for **SWMR** (single-writer/multiple-reader)
1693 /// appending: take **no** OS lock (so concurrent readers, and Windows'
1694 /// mandatory locks, are never blocked) and raise the superblock's SWMR-write
1695 /// flag so a reader may attach with [`File::open_swmr`], the C library's
1696 /// `H5F_ACC_SWMR_READ`, or h5py `swmr=True`.
1697 ///
1698 /// Only immediate [`Dataset::append`] is permitted, and only over the SWMR
1699 /// subset — an **unfiltered**, chunk-aligned append, so a concurrent reader
1700 /// only ever observes a consistent prefix; a filtered or non-chunk-aligned
1701 /// append returns [`Error::SwmrAppendUnsupported`](crate::Error::SwmrAppendUnsupported).
1702 /// The staged edit surface (`write`/`set_attr`/`create_*`/`delete`/`copy`/
1703 /// `commit`) returns
1704 /// [`Error::SwmrStagedUnsupported`](crate::Error::SwmrStagedUnsupported).
1705 /// [`close`](Self::close) clears the SWMR-write flag; a writer that exits
1706 /// without a clean close leaves it set — recover with
1707 /// [`clear_swmr_flag`](Self::clear_swmr_flag). While the flag stands, this
1708 /// open is refused with
1709 /// [`Error::FileMarkedInUse`](crate::Error::FileMarkedInUse), which is what
1710 /// keeps a second writer off a file SWMR gives only one (no OS lock is held
1711 /// to do it).
1712 ///
1713 /// Requires a latest-format (version-3 superblock) file with no userblock
1714 /// and no persisted free-space; other files are refused with
1715 /// [`Error::SwmrAppendUnsupported`](crate::Error::SwmrAppendUnsupported).
1716 /// The version-3 requirement is the C library's: neither library reads the
1717 /// SWMR-write flag back on an older superblock, so raising one there would
1718 /// announce the writer to nobody.
1719 #[doc(alias = "H5F_ACC_SWMR_WRITE")]
1720 pub fn open_swmr_writer<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
1721 Self::open_swmr_writer_with_options(path, FileAccessProperties::new())
1722 }
1723
1724 /// Open for SWMR appending with explicit access properties — see
1725 /// [`open_swmr_writer`](Self::open_swmr_writer).
1726 ///
1727 /// The properties' chunk cache is the file-wide default for datasets opened
1728 /// from this file. Its locking policy is ignored, which costs the caller
1729 /// nothing: SWMR takes no OS lock by design, which is stronger than any
1730 /// locking a caller could ask for. Its memory strategy is *not* ignored the
1731 /// same way — this writer always mirrors, so an explicit
1732 /// [`MemoryStrategy::Bounded`] is a guarantee it cannot meet and is refused
1733 /// with [`Error::EditUnsupported`]; [`MemoryStrategy::Auto`] and
1734 /// [`MemoryStrategy::Mirrored`] are both satisfied by the mirror.
1735 pub fn open_swmr_writer_with_options<P: AsRef<std::path::Path>>(
1736 path: P,
1737 properties: FileAccessProperties,
1738 ) -> Result<Self, Error> {
1739 Ok(File {
1740 inner: Arc::new(FileInner::open_swmr_writer(path, properties)?),
1741 })
1742 }
1743
1744 /// Open an existing HDF5 file for reading and editing with **bounded
1745 /// memory** (issue #147): no whole-file mirror is ever built, so peak
1746 /// memory stays at the metadata being parsed plus the configured caches
1747 /// plus a few chunks of append working set — independent of the file size
1748 /// and of the size of each append call.
1749 ///
1750 /// # Deprecated
1751 ///
1752 /// [`open_rw`](Self::open_rw) now edits such a file bounded on its own, so
1753 /// this is no longer a different capability set — only a different default
1754 /// for a file the bounded engine cannot edit (a pre-v2 superblock, or a
1755 /// userblock). This refuses that file with
1756 /// [`Error::EditUnsupported`](crate::Error::EditUnsupported); `open_rw`
1757 /// mirrors it instead. To keep the refusal, say so:
1758 ///
1759 /// ```no_run
1760 /// use hdf5_pure::{File, FileAccessProperties, MemoryStrategy};
1761 /// # fn main() -> Result<(), hdf5_pure::Error> {
1762 /// let file = File::open_rw_with_options(
1763 /// "data.h5",
1764 /// FileAccessProperties::new().with_memory_strategy(MemoryStrategy::Bounded),
1765 /// )?;
1766 /// # Ok(()) }
1767 /// ```
1768 ///
1769 /// # Behavior
1770 ///
1771 /// This is the read-write sibling of [`open_streaming`](Self::open_streaming):
1772 /// reads are served by positioned I/O with the same capabilities as the
1773 /// streaming backend, while immediate [`Dataset::append`] runs the same
1774 /// crash-atomic engine as [`open_rw`](Self::open_rw) — filtered whole-chunk
1775 /// / unfiltered any-length, durable before it returns, no `commit` needed.
1776 /// A large append is applied in whole-chunk batches, each crash-atomic, so
1777 /// a crash mid-call leaves a valid shorter dataset. An exclusive OS file
1778 /// lock is held for the file's life.
1779 ///
1780 /// The staged edit surface ([`Dataset::write`]/`set_attr`/`append_staged`,
1781 /// [`Group::create_dataset`]/`create_group`/`delete`/`set_attr`,
1782 /// [`commit`](Self::commit)/[`copy`](Self::copy)/[`copy_from`](Self::copy_from),
1783 /// and [`space_accounting`](Self::space_accounting)) is the same as
1784 /// [`open_rw`](Self::open_rw)'s: both open the same engine, differing only in
1785 /// how it holds the file's bytes (issue #198). A commit here holds only what
1786 /// it is building, so its resident memory is bounded by the edit rather than
1787 /// by the file — with [`copy`](Self::copy) the exception, since copying an
1788 /// object reads the whole of it into memory first.
1789 ///
1790 /// A file that persists its free space
1791 /// (`H5Pset_file_space_strategy(persist = true)`, non-paged) is supported:
1792 /// its on-disk free-space managers are seeded at open and rewritten into
1793 /// canonical shape when the file is closed — by an explicit
1794 /// [`close`](Self::close) or, best-effort, when the last handle drops (issue
1795 /// #173). Only a true crash (`SIGKILL`, power loss) skips that rewrite; the
1796 /// appended data is still durable and reopens correctly, the managers merely
1797 /// stay non-canonical until the next clean rewrite. A genuine **paged** file
1798 /// (`H5F_FSPACE_STRATEGY_PAGE` with `persist = true`) is also supported:
1799 /// appends stay page-homogeneous (raw and metadata in separate pages) and
1800 /// the per-page-type managers are rewritten at close. A paged file that
1801 /// does *not* persist its free space is refused at open — recreate it with
1802 /// `persist = true` to grow it in place. That refusal is shared with
1803 /// `open_rw`, which cannot commit such a file either.
1804 ///
1805 /// Requires a latest-format (v2/v3 superblock) file with 8-byte offsets and
1806 /// lengths and no userblock; other files are refused at open with
1807 /// [`Error::EditUnsupported`](crate::Error::EditUnsupported).
1808 #[deprecated(
1809 since = "0.28.0",
1810 note = "use `File::open_rw`, which now edits such a file bounded on its own; for the strict refusal, pass `FileAccessProperties::new().with_memory_strategy(MemoryStrategy::Bounded)` to `File::open_rw_with_options`"
1811 )]
1812 pub fn open_rw_bounded<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
1813 Self::open_rw_bounded_inner(path, FileAccessProperties::new())
1814 }
1815
1816 /// Open a file for bounded-memory reading and appending with explicit
1817 /// access properties — see [`open_rw_bounded`](Self::open_rw_bounded), which
1818 /// this is deprecated alongside.
1819 ///
1820 /// Both configured caches apply to this backend: the metadata cache bounds
1821 /// bytes retained for metadata reads (entries touched by an in-place write
1822 /// are invalidated, so reads never observe stale bytes), and the chunk
1823 /// cache bounds decompressed chunks retained by each [`Dataset`] handle. An
1824 /// explicit [`FileAccessProperties::with_memory_strategy`] wins over this
1825 /// entry point's bounded default, in either direction.
1826 #[deprecated(
1827 since = "0.28.0",
1828 note = "use `File::open_rw_with_options`; it honors the same `with_memory_strategy`, and defaults to falling back to the mirror rather than refusing"
1829 )]
1830 pub fn open_rw_bounded_with_options<P: AsRef<std::path::Path>>(
1831 path: P,
1832 properties: FileAccessProperties,
1833 ) -> Result<Self, Error> {
1834 Self::open_rw_bounded_inner(path, properties)
1835 }
1836
1837 /// The body both deprecated bounded entry points share, so that neither has
1838 /// to call the other and trip its own deprecation warning.
1839 fn open_rw_bounded_inner<P: AsRef<std::path::Path>>(
1840 path: P,
1841 properties: FileAccessProperties,
1842 ) -> Result<Self, Error> {
1843 Ok(File {
1844 inner: Arc::new(FileInner::open_rw_bounded(path, properties)?),
1845 })
1846 }
1847
1848 /// Clear a stale SWMR-write flag left in `path` by a writer that exited
1849 /// without a clean [`close`](Self::close) — the `h5clear -s` equivalent, for
1850 /// recovering a file that both this crate and the reference C library
1851 /// otherwise refuse to open ([`Error::FileMarkedInUse`](crate::Error::FileMarkedInUse)).
1852 /// A no-op if the flag is already clear.
1853 ///
1854 /// It takes the exclusive OS lock first, so it cannot clear the flag out
1855 /// from under a *live* [`open_rw`](Self::open_rw) writer. A live SWMR writer
1856 /// holds no lock, so make sure it is really gone: clearing the flag under
1857 /// one leaves its readers with no record that it is publishing.
1858 pub fn clear_swmr_flag<P: AsRef<std::path::Path>>(path: P) -> Result<(), Error> {
1859 crate::file_lock::clear_swmr_flag_at(path.as_ref())
1860 }
1861
1862 /// Create a new, empty HDF5 file at `path` and open it for reading and
1863 /// writing, so its contents can be built entirely through owned handles
1864 /// ([`Group::create_dataset`]/[`create_group`](Group::create_group), then
1865 /// [`commit`](Self::commit)).
1866 ///
1867 /// Overwrites any existing file at `path`. For an all-at-once write, use
1868 /// [`FileBuilder`](crate::FileBuilder) instead.
1869 #[doc(alias = "H5Fcreate")]
1870 pub fn create<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
1871 Self::create_with_options(
1872 path,
1873 FileCreateProperties::new(),
1874 FileAccessProperties::new(),
1875 )
1876 }
1877
1878 /// Create a new, empty HDF5 file with explicit creation and access properties,
1879 /// then open it for reading and writing — see [`create`](Self::create).
1880 ///
1881 /// Mirrors `H5Fcreate(name, flags, fcpl_id, fapl_id)`: `create` carries the
1882 /// creation properties recorded in the new file (userblock, file-space
1883 /// strategy, library-version bounds), and `access` the properties governing
1884 /// the handle returned (locking policy, chunk cache). Both are values, so a
1885 /// layout defined once can be reused across every file an application writes.
1886 ///
1887 /// A creation property is validated as the file is written, so an invalid
1888 /// userblock or page size surfaces here rather than when the properties were
1889 /// built. A file created with [`FileSpaceStrategy::Page`] can be grown
1890 /// through either editor, by an immediate [`Dataset::append`] or a staged
1891 /// commit, provided it also persists its free space (issue #198).
1892 pub fn create_with_options<P: AsRef<std::path::Path>>(
1893 path: P,
1894 create: FileCreateProperties,
1895 access: FileAccessProperties,
1896 ) -> Result<Self, Error> {
1897 // Refuse a pair the reopen below would refuse, before anything is
1898 // written: this call promises a file *and* an open handle, and half of
1899 // that is worse than neither.
1900 if let Some(reason) = crate::edit::create_would_refuse_reopen(&create, &access) {
1901 return Err(Error::EditUnsupported(reason));
1902 }
1903 let mut builder = crate::writer::FileBuilder::new();
1904 builder.with_create_properties(create);
1905 let bytes = builder.finish()?;
1906 std::fs::write(path.as_ref(), bytes).map_err(Error::Io)?;
1907 Self::open_rw_with_options(path, access)
1908 }
1909
1910 /// Apply all staged structural edits made through this file's handles —
1911 /// [`Dataset::write`]/`set_attr`/`remove_attr` and
1912 /// [`Group::create_group`]/`delete` — as one transaction. Immediate
1913 /// [`Dataset::append`]s need no commit.
1914 ///
1915 /// Requires a read-write file ([`File::open_rw`]); a read-only file returns
1916 /// [`Error::ReadOnly`](crate::Error::ReadOnly). A commit that relocates
1917 /// objects invalidates outstanding handles — re-fetch any you keep using.
1918 pub fn commit(&self) -> Result<(), Error> {
1919 self.with_mirror_session(true, |session| session.commit())
1920 }
1921
1922 /// Copy the object at `src` to `dst` within this file (the in-file
1923 /// `H5Ocopy`), staged until [`commit`](Self::commit).
1924 ///
1925 /// Requires a read-write file ([`File::open_rw`]); a read-only file returns
1926 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
1927 pub fn copy(&self, src: &str, dst: &str) -> Result<(), Error> {
1928 self.with_mirror_session(true, |session| {
1929 session.copy(&normalize_path(src), &normalize_path(dst));
1930 Ok(())
1931 })
1932 }
1933
1934 /// Copy the object at `src` in `source` — a separate, buffered read-only
1935 /// file — into this file at `dst`: the cross-file `H5Ocopy`, staged until
1936 /// [`commit`](Self::commit).
1937 ///
1938 /// `source` must be a buffered file ([`File::open`] or [`File::from_bytes`],
1939 /// not [`File::open_streaming`]) that uses 8-byte offsets and has no
1940 /// userblock; anything else is refused with
1941 /// [`Error::EditUnsupported`](crate::Error::EditUnsupported). The source
1942 /// subtree is read and validated eagerly, so `source` need not outlive this
1943 /// call. Requires a read-write destination ([`File::open_rw`]); a read-only
1944 /// one returns [`Error::ReadOnly`](crate::Error::ReadOnly).
1945 pub fn copy_from(&self, source: &File, src: &str, dst: &str) -> Result<(), Error> {
1946 self.with_mirror_session(true, |session| session.copy_from(source, src, dst))
1947 }
1948
1949 /// Report whether this file has structural edits staged but not yet applied
1950 /// by [`commit`](Self::commit) — [`Dataset::write`]/`set_attr`/`remove_attr`,
1951 /// [`Dataset::append_staged`], [`Group::create_group`]/`create_dataset`/
1952 /// `delete`/`set_attr`/`remove_attr`, and [`copy`](Self::copy)/
1953 /// [`copy_from`](Self::copy_from). Immediate [`Dataset::append`]s are never
1954 /// staged and do not count. Always `false` for a read-only file.
1955 pub fn has_staged_edits(&self) -> bool {
1956 match &self.inner.backend {
1957 Backend::Edit(m) => {
1958 let session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
1959 session.has_staged_edits()
1960 }
1961 _ => false,
1962 }
1963 }
1964
1965 /// Report this read-write file's live space usage as a [`SpaceAccounting`] —
1966 /// the current logical size, total reusable free bytes, and reusable free
1967 /// regions. It reflects committed state plus immediate in-place appends, not
1968 /// edits still staged for [`commit`](Self::commit).
1969 ///
1970 /// Requires a read-write file ([`File::open_rw`]); a read-only file returns
1971 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
1972 pub fn space_accounting(&self) -> Result<SpaceAccounting, Error> {
1973 match &self.inner.backend {
1974 Backend::Edit(m) => {
1975 let session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
1976 Ok(session.space_accounting())
1977 }
1978 _ => Err(Error::ReadOnly),
1979 }
1980 }
1981
1982 /// Commit any staged edits and seal this file. The exclusive OS lock is
1983 /// released once the last handle derived from this file is also dropped.
1984 ///
1985 /// After `close`, a write through any surviving [`Dataset`]/[`Group`] handle
1986 /// or [`File`] clone returns [`Error::FileClosed`](crate::Error::FileClosed);
1987 /// reads still work.
1988 pub fn close(self) -> Result<(), Error> {
1989 if matches!(self.inner.backend, Backend::Edit(_)) {
1990 if self.inner.swmr_write {
1991 // SWMR mode stages nothing (the staged surface is refused), so do
1992 // not commit — clear the SWMR-write flag and flush, marking the
1993 // file cleanly closed for any concurrent reader.
1994 if let Backend::Edit(m) = &self.inner.backend {
1995 let mut session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
1996 session.set_consistency_flags(0)?;
1997 }
1998 } else {
1999 self.commit()?;
2000 // Immediate appends grow the file past any persisted free-space
2001 // managers without running a commit tail, so re-home them here.
2002 // A no-op unless this session left them stale.
2003 self.with_mirror_session(false, |session| {
2004 session.finalize_persist()?;
2005 session.sync()
2006 })?;
2007 }
2008 self.inner.closed.store(true, Ordering::Release);
2009 }
2010 Ok(())
2011 }
2012
2013 /// Run `f` with the locked write session of a read-write file. `staged`
2014 /// distinguishes an edit applied by [`commit`](Self::commit) from an immediate
2015 /// one. Returns [`Error::ReadOnly`](crate::Error::ReadOnly) for a read-only
2016 /// file, [`Error::FileClosed`](crate::Error::FileClosed) once the file is
2017 /// sealed by [`close`](Self::close), and
2018 /// [`Error::SwmrStagedUnsupported`](crate::Error::SwmrStagedUnsupported) for a
2019 /// staged edit on a SWMR-writer file.
2020 fn with_mirror_session<R>(
2021 &self,
2022 staged: bool,
2023 f: impl FnOnce(&mut WriteEngine) -> Result<R, Error>,
2024 ) -> Result<R, Error> {
2025 let Backend::Edit(m) = &self.inner.backend else {
2026 return Err(Error::ReadOnly);
2027 };
2028 self.inner.check_mutable(staged)?;
2029 let mut session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2030 f(&mut session)
2031 }
2032
2033 /// Returns an owned handle to the root group.
2034 pub fn root(&self) -> Group {
2035 Group {
2036 // A relocating commit on a read-write file can move the root, so
2037 // resolve it from the live mirror rather than the cached superblock.
2038 address: self.inner.mirror_root_address(),
2039 file: self.inner.clone(),
2040 path: Some(String::new()),
2041 }
2042 }
2043
2044 /// Resolve a path and return an owned [`Dataset`] handle.
2045 ///
2046 /// The dataset uses the file-wide chunk-cache default (configured with
2047 /// [`FileAccessProperties::with_chunk_cache`]). To override the cache for this
2048 /// one dataset, use [`dataset_with_options`](Self::dataset_with_options).
2049 pub fn dataset(&self, path: &str) -> Result<Dataset, Error> {
2050 self.dataset_with_options(path, DatasetAccessProperties::new())
2051 }
2052
2053 /// Resolve a path and return an owned [`Dataset`] handle, applying per-dataset
2054 /// [`DatasetAccessProperties`] that override file-wide access defaults.
2055 ///
2056 /// This is the dataset-open-with-access-property-list path (HDF5's `dapl`):
2057 /// the properties' chunk cache corresponds to `H5Pset_chunk_cache` and takes
2058 /// precedence, for this dataset only, over the `H5Pset_cache`-style
2059 /// file-wide default.
2060 pub fn dataset_with_options(
2061 &self,
2062 path: &str,
2063 properties: DatasetAccessProperties,
2064 ) -> Result<Dataset, Error> {
2065 let addr = self.inner.resolve_path(path)?;
2066 let hdr = self.inner.parse_header(addr)?;
2067 if !has_message(&hdr, MessageType::DataLayout) {
2068 return Err(Error::NotADataset(path.to_string()));
2069 }
2070 let chunk_cache = properties.resolved_chunk_cache(self.inner.access_properties.chunk_cache);
2071 Ok(Dataset {
2072 file: self.inner.clone(),
2073 address: addr,
2074 header: hdr,
2075 chunk_cache: ChunkCache::with_config(chunk_cache),
2076 chunk_cache_config: chunk_cache,
2077 path: Some(normalize_path(path)),
2078 })
2079 }
2080
2081 /// Resolve a path and return an owned [`Group`] handle.
2082 pub fn group(&self, path: &str) -> Result<Group, Error> {
2083 let addr = self.inner.resolve_path(path)?;
2084 Ok(Group {
2085 file: self.inner.clone(),
2086 address: addr,
2087 path: Some(normalize_path(path)),
2088 })
2089 }
2090
2091 /// Re-read the file from disk to pick up data appended by a concurrent
2092 /// writer, then re-parse the superblock.
2093 ///
2094 /// This is the SWMR reader's refresh primitive. Returns
2095 /// [`Error::SwmrUnsupported`] if the file was not opened with
2096 /// [`File::open_swmr`], and [`Error::HandlesOutstanding`] if any owned
2097 /// [`Dataset`]/[`Group`] handle (or a clone of this `File`) is still alive —
2098 /// drop them before refreshing, then re-fetch them afterward, since they
2099 /// observe the new bytes only when re-derived from the refreshed file.
2100 pub fn refresh(&mut self) -> Result<(), Error> {
2101 let inner = Arc::get_mut(&mut self.inner).ok_or(Error::HandlesOutstanding)?;
2102 inner.refresh()
2103 }
2104
2105 // --- delegating value getters (forward to the shared inner state) ---
2106
2107 /// Returns the raw file bytes for an in-memory file, or an empty slice for a
2108 /// streaming file (which has no whole-file buffer).
2109 pub fn as_bytes(&self) -> &[u8] {
2110 self.inner.as_bytes()
2111 }
2112
2113 /// Return the access properties used when opening this file.
2114 pub fn access_properties(&self) -> FileAccessProperties {
2115 self.inner.access_properties()
2116 }
2117
2118 /// Former name of [`access_properties`](Self::access_properties).
2119 #[deprecated(since = "0.26.0", note = "renamed to `access_properties`")]
2120 pub fn access_options(&self) -> FileAccessProperties {
2121 self.access_properties()
2122 }
2123
2124 /// Which backend this file's read-write session resolved to:
2125 /// [`EditBacking::Bounded`] when it reads through a handle, or
2126 /// [`EditBacking::Mirrored`] when it holds a whole-file image.
2127 ///
2128 /// This is how a caller who opened with [`MemoryStrategy::Auto`] finds out
2129 /// whether the fallback was taken, and so whether memory scales with the
2130 /// file. A file with no editing session — a read-only open, a streaming open
2131 /// — reports `None`.
2132 ///
2133 /// The answer is an [`EditBacking`] rather than the [`MemoryStrategy`] that
2134 /// was asked for, because `Auto` is a preference between the two backends and
2135 /// not an outcome either can report; `.into()` converts back when a later
2136 /// reopen should be pinned to what this one got.
2137 pub fn edit_backing(&self) -> Option<EditBacking> {
2138 self.inner.edit_backing()
2139 }
2140
2141 /// Returns a reference to the parsed superblock.
2142 pub fn superblock(&self) -> &Superblock {
2143 self.inner.superblock()
2144 }
2145
2146 /// The file-space management strategy this file records in its superblock
2147 /// extension, or `None` if it records none.
2148 pub fn file_space_strategy(&self) -> Option<FileSpaceStrategy> {
2149 self.inner.file_space_strategy()
2150 }
2151
2152 /// The full [`FileSpaceInfo`] recorded in this file's superblock extension,
2153 /// if present and readable.
2154 pub fn file_space_info(&self) -> Option<&FileSpaceInfo> {
2155 self.inner.file_space_info()
2156 }
2157
2158 /// The free regions a file persists on disk in its free-space managers, as
2159 /// `(address, length)` pairs sorted by address.
2160 pub fn persisted_free_space(&self) -> Vec<(u64, u64)> {
2161 self.inner.persisted_free_space()
2162 }
2163
2164 /// The size of the underlying file in bytes (the HDF5 `H5Fget_filesize`).
2165 pub fn file_size(&self) -> u64 {
2166 self.inner.file_size()
2167 }
2168
2169 /// The minimum library version required to read this file, derived from its
2170 /// superblock version (the *low bound* of HDF5's `H5Fget_libver_bounds`).
2171 pub fn libver_bound(&self) -> LibVer {
2172 self.inner.libver_bound()
2173 }
2174
2175 /// A `Source` view over the backend, for the streaming-capable paths.
2176 pub(crate) fn source(&self) -> SourceView<'_> {
2177 self.inner.source()
2178 }
2179
2180 /// The whole-file byte image when this file is buffered in memory; `None`
2181 /// for a streaming file. Used by cross-file object copy.
2182 pub(crate) fn in_memory_image(&self) -> Option<&[u8]> {
2183 self.inner.in_memory_image()
2184 }
2185
2186 /// The base address (superblock base address) added to every stored relative
2187 /// address. Zero for a file with no userblock.
2188 pub(crate) fn base_address(&self) -> u64 {
2189 self.inner.base_address()
2190 }
2191}
2192
2193// ---------------------------------------------------------------------------
2194// Object reference target
2195// ---------------------------------------------------------------------------
2196
2197/// The resolved target of an HDF5 object reference (`H5R_OBJECT`): either a
2198/// group or a dataset.
2199///
2200/// Produced by [`Dataset::dereference`]. MATLAB `.mat` files use object
2201/// references pervasively — a cell array stores one reference per element, and
2202/// the `#subsystem#` machinery references its payloads — so resolving a
2203/// reference to the group or dataset it names is the foundation for reading
2204/// those structures.
2205///
2206/// The [`Dataset`](Object::Dataset) handle is boxed: it carries a parsed object
2207/// header and is much larger than a [`Group`](Object::Group) handle, so boxing
2208/// keeps `Object` (and a `Vec<Object>`) compact without a size disparity. The
2209/// `Box` derefs transparently, so `&obj_dataset` is usable wherever a
2210/// `&Dataset` is expected.
2211///
2212/// Non-exhaustive: a reference can name an object kind this crate does not yet
2213/// resolve — a committed (named) datatype is refused with
2214/// [`FormatError::InvalidObjectReference`](crate::FormatError::InvalidObjectReference)
2215/// today — so match with a `_` arm.
2216#[non_exhaustive]
2217pub enum Object {
2218 /// The reference points at a group's object header.
2219 Group(Group),
2220 /// The reference points at a dataset's object header.
2221 Dataset(Box<Dataset>),
2222}
2223
2224impl std::fmt::Debug for Object {
2225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2226 match self {
2227 Object::Group(_) => f.write_str("Object::Group"),
2228 Object::Dataset(_) => f.write_str("Object::Dataset"),
2229 }
2230 }
2231}
2232
2233// ---------------------------------------------------------------------------
2234// Group handle
2235// ---------------------------------------------------------------------------
2236
2237/// A group that exists only as a staged edit, handed to
2238/// [`Group::create_group_with`]'s closure so attributes can be set on a group
2239/// that is not yet committed (and so has no resolvable header to hang a
2240/// [`Group`] handle off).
2241///
2242/// Every method stages; nothing is written until [`File::commit`], and a staged
2243/// object is not resolvable by name until then.
2244///
2245/// The closure holding this records into a buffer rather than into the file's
2246/// writable session, so nothing is locked while it runs. The recorded operations
2247/// are applied together when it returns, which is also why a staged object is not
2248/// resolvable until [`File::commit`].
2249pub struct StagedGroup<'a> {
2250 ops: &'a mut Vec<StagedOp>,
2251 path: String,
2252}
2253
2254impl StagedGroup<'_> {
2255 /// Stage an attribute on this group, applied with its creation on
2256 /// [`File::commit`].
2257 pub fn set_attr(&mut self, name: &str, value: AttrValue) -> &mut Self {
2258 self.ops.push(StagedOp::SetGroupAttr {
2259 path: self.path.clone(),
2260 name: name.to_string(),
2261 value,
2262 });
2263 self
2264 }
2265
2266 /// Stage an empty subgroup of this group.
2267 ///
2268 /// To configure it in the same commit, use
2269 /// [`create_group_with`](Self::create_group_with).
2270 pub fn create_group(&mut self, name: &str) -> &mut Self {
2271 self.create_group_with(name, |_| {})
2272 }
2273
2274 /// Stage a subgroup of this group, configured through `build`.
2275 pub fn create_group_with(
2276 &mut self,
2277 name: &str,
2278 build: impl FnOnce(&mut StagedGroup<'_>),
2279 ) -> &mut Self {
2280 let child = format!("{}/{}", self.path, name);
2281 self.ops.push(StagedOp::CreateGroup(child.clone()));
2282 let mut staged = StagedGroup {
2283 ops: &mut *self.ops,
2284 path: child,
2285 };
2286 build(&mut staged);
2287 self
2288 }
2289
2290 /// Stage a dataset in this group, configured through `build`.
2291 pub fn create_dataset(
2292 &mut self,
2293 name: &str,
2294 build: impl FnOnce(&mut DatasetBuilder),
2295 ) -> &mut Self {
2296 let mut builder = DatasetBuilder::new(name);
2297 build(&mut builder);
2298 self.ops.push(StagedOp::CreateDataset {
2299 path: format!("{}/{}", self.path, name),
2300 builder: Box::new(builder),
2301 });
2302 self
2303 }
2304}
2305
2306/// One edit recorded by a [`StagedGroup`] closure, replayed onto the writable
2307/// session after the closure returns.
2308///
2309/// The indirection is what keeps user code off the session lock: the closure
2310/// touches only this buffer, so calling back into the same [`File`] from inside
2311/// it is at worst wrongly ordered rather than a deadlock (issue #200).
2312enum StagedOp {
2313 CreateGroup(String),
2314 SetGroupAttr {
2315 path: String,
2316 name: String,
2317 value: AttrValue,
2318 },
2319 CreateDataset {
2320 path: String,
2321 /// Boxed because a `DatasetBuilder` dwarfs the other variants, and a
2322 /// closure staging many groups would otherwise pay its size per entry.
2323 builder: Box<DatasetBuilder>,
2324 },
2325}
2326
2327impl StagedOp {
2328 /// Record this edit on the session. Applied in the order the closure made
2329 /// the calls, so a group is always staged before its own attributes and
2330 /// children.
2331 fn apply(self, session: &mut WriteEngine) {
2332 match self {
2333 StagedOp::CreateGroup(path) => session.create_group(&path),
2334 StagedOp::SetGroupAttr { path, name, value } => {
2335 session.set_group_attr(&path, &name, value);
2336 }
2337 StagedOp::CreateDataset { path, builder } => {
2338 session.stage_created_dataset(&path, *builder);
2339 }
2340 }
2341 }
2342}
2343
2344/// An owned handle to an HDF5 group.
2345pub struct Group {
2346 file: Arc<FileInner>,
2347 address: u64,
2348 /// Root-relative path of this group (e.g. `""` for the root, `"a/b"`), used
2349 /// to address the group and its children for write operations on a
2350 /// read-write file. `None` for a group reached by object reference
2351 /// ([`Dataset::dereference`]), which has no resolvable path.
2352 path: Option<String>,
2353}
2354
2355impl Group {
2356 /// Address of this group's object header (base-adjusted, file-absolute).
2357 /// Used to resolve object references that point at this group.
2358 pub(crate) fn header_address(&self) -> u64 {
2359 self.address
2360 }
2361
2362 /// List the names of datasets in this group.
2363 pub fn datasets(&self) -> Result<Vec<String>, Error> {
2364 let entries = self.children()?;
2365 let mut names = Vec::new();
2366 for entry in &entries {
2367 let hdr = self.file.parse_header(entry.object_header_address)?;
2368 if has_message(&hdr, MessageType::DataLayout) {
2369 names.push(entry.name.clone());
2370 }
2371 }
2372 Ok(names)
2373 }
2374
2375 /// List the names of subgroups in this group.
2376 pub fn groups(&self) -> Result<Vec<String>, Error> {
2377 let entries = self.children()?;
2378 let mut names = Vec::new();
2379 for entry in &entries {
2380 let hdr = self.file.parse_header(entry.object_header_address)?;
2381 if is_group(&hdr) {
2382 names.push(entry.name.clone());
2383 }
2384 }
2385 Ok(names)
2386 }
2387
2388 /// Read all attributes of this group.
2389 ///
2390 /// Each value takes the [`AttrValue`] variant that describes its on-disk
2391 /// encoding, so the variant reflects the charset and dataspace its writer
2392 /// chose rather than the shape of the data alone: a one-element array stays
2393 /// an array, and an ASCII string does not arrive as a UTF-8
2394 /// [`String`](AttrValue::String). Prefer the accessors — [`AttrValue::as_str`],
2395 /// [`as_strings`](AttrValue::as_strings), [`as_i64`](AttrValue::as_i64) and
2396 /// the rest — over matching on the variant, unless the encoding is the thing
2397 /// you care about. **The variant may become more specific in a future
2398 /// release** as `AttrValue` grows narrower ones (fixed widths, variable-length
2399 /// strings), and a `_` arm is required regardless because the enum is
2400 /// `#[non_exhaustive]`.
2401 ///
2402 /// An attribute whose datatype has no `AttrValue` representation is omitted
2403 /// from the map rather than reported as an error.
2404 pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
2405 let hdr = self.file.parse_header(self.address)?;
2406 self.file.attrs_of(&hdr)
2407 }
2408
2409 /// Names of every attribute on this group, including any whose datatype
2410 /// [`attrs`](Self::attrs) cannot represent. Used by repack to detect an
2411 /// attribute it would otherwise drop.
2412 pub(crate) fn attr_names(&self) -> Result<Vec<String>, Error> {
2413 let hdr = self.file.parse_header(self.address)?;
2414 self.file.attr_message_names_of(&hdr)
2415 }
2416
2417 /// Get a dataset within this group by name.
2418 ///
2419 /// The dataset uses the file-wide chunk-cache default. To override the cache
2420 /// for this one dataset, use
2421 /// [`dataset_with_options`](Self::dataset_with_options).
2422 pub fn dataset(&self, name: &str) -> Result<Dataset, Error> {
2423 self.dataset_with_options(name, DatasetAccessProperties::new())
2424 }
2425
2426 /// Get a dataset within this group by name, applying per-dataset
2427 /// [`DatasetAccessProperties`] that override file-wide access defaults (HDF5's
2428 /// `dapl`; see `H5Pset_chunk_cache`).
2429 pub fn dataset_with_options(
2430 &self,
2431 name: &str,
2432 properties: DatasetAccessProperties,
2433 ) -> Result<Dataset, Error> {
2434 let entries = self.children()?;
2435 let entry = entries
2436 .iter()
2437 .find(|e| e.name == name)
2438 .ok_or_else(|| Error::Format(FormatError::PathNotFound(name.to_string())))?;
2439 let hdr = self.file.parse_header(entry.object_header_address)?;
2440 if !has_message(&hdr, MessageType::DataLayout) {
2441 return Err(Error::NotADataset(name.to_string()));
2442 }
2443 let chunk_cache = properties.resolved_chunk_cache(self.file.access_properties.chunk_cache);
2444 Ok(Dataset {
2445 file: self.file.clone(),
2446 address: entry.object_header_address,
2447 header: hdr,
2448 chunk_cache: ChunkCache::with_config(chunk_cache),
2449 chunk_cache_config: chunk_cache,
2450 path: self.child_path(name),
2451 })
2452 }
2453
2454 /// Get a subgroup within this group by name.
2455 pub fn group(&self, name: &str) -> Result<Group, Error> {
2456 let entries = self.children()?;
2457 let entry = entries
2458 .iter()
2459 .find(|e| e.name == name)
2460 .ok_or_else(|| Error::Format(FormatError::PathNotFound(name.to_string())))?;
2461 Ok(Group {
2462 file: self.file.clone(),
2463 address: entry.object_header_address,
2464 path: self.child_path(name),
2465 })
2466 }
2467
2468 /// The root-relative path of a child named `name`, or `None` if this group
2469 /// itself has no resolvable path (reached by object reference).
2470 fn child_path(&self, name: &str) -> Option<String> {
2471 self.path.as_ref().map(|p| {
2472 if p.is_empty() {
2473 name.to_string()
2474 } else {
2475 format!("{p}/{name}")
2476 }
2477 })
2478 }
2479
2480 /// Create an empty subgroup `name` within this group, staged until
2481 /// [`File::commit`].
2482 ///
2483 /// To give the new group attributes or children in the same commit, use
2484 /// [`create_group_with`](Self::create_group_with).
2485 ///
2486 /// Requires a read-write file ([`File::open_rw`]), else
2487 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
2488 ///
2489 /// ```no_run
2490 /// # use hdf5_pure::File;
2491 /// # fn main() -> Result<(), hdf5_pure::Error> {
2492 /// let file = File::open_rw("runs.h5")?;
2493 /// file.root().create_group("run2")?;
2494 /// file.commit()?;
2495 /// # Ok(())
2496 /// # }
2497 /// ```
2498 pub fn create_group(&self, name: &str) -> Result<(), Error> {
2499 self.create_group_with(name, |_| {})
2500 }
2501
2502 /// Create a subgroup `name` within this group, configuring it through
2503 /// `build` (attributes, nested groups and datasets), staged until
2504 /// [`File::commit`].
2505 ///
2506 /// The closure exists because [`set_attr`](Self::set_attr) needs a group
2507 /// that already *resolves*, so it cannot reach a group that is itself still
2508 /// staged; this can, and the creation and its attributes land in one commit.
2509 /// For a plain empty group use [`create_group`](Self::create_group).
2510 ///
2511 /// The closure records into a buffer rather than into the file itself, and
2512 /// nothing it stages resolves until [`File::commit`], so reading the same
2513 /// [`File`] from inside it sees the file as it was before this call.
2514 ///
2515 /// Requires a read-write file ([`File::open_rw`]), else
2516 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
2517 ///
2518 /// ```no_run
2519 /// # use hdf5_pure::{AttrValue, File};
2520 /// # fn main() -> Result<(), hdf5_pure::Error> {
2521 /// let file = File::open_rw("runs.h5")?;
2522 /// file.root().create_group_with("run2", |g| {
2523 /// g.set_attr("count", AttrValue::I64(7));
2524 /// g.set_attr("label", AttrValue::String("second".into()));
2525 /// })?;
2526 /// file.commit()?;
2527 /// # Ok(())
2528 /// # }
2529 /// ```
2530 pub fn create_group_with(
2531 &self,
2532 name: &str,
2533 build: impl FnOnce(&mut StagedGroup<'_>),
2534 ) -> Result<(), Error> {
2535 let child = self.child_edit_path(name)?;
2536 let mut ops = vec![StagedOp::CreateGroup(child.clone())];
2537 build(&mut StagedGroup {
2538 ops: &mut ops,
2539 path: child,
2540 });
2541 self.apply_staged(ops)
2542 }
2543
2544 /// Create a dataset `name` within this group, configuring it through `build`
2545 /// (shape, data, chunks, filters, …), staged until [`File::commit`].
2546 ///
2547 /// As with [`create_group_with`](Self::create_group_with), the closure
2548 /// configures a builder rather than the file, so it may read the same
2549 /// [`File`] — it will see the file as it was before this call.
2550 ///
2551 /// Requires a read-write file ([`File::open_rw`]), else
2552 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
2553 pub fn create_dataset(
2554 &self,
2555 name: &str,
2556 build: impl FnOnce(&mut DatasetBuilder),
2557 ) -> Result<(), Error> {
2558 let child = self.child_edit_path(name)?;
2559 let mut builder = DatasetBuilder::new(name);
2560 build(&mut builder);
2561 self.apply_staged(vec![StagedOp::CreateDataset {
2562 path: child,
2563 builder: Box::new(builder),
2564 }])
2565 }
2566
2567 /// Delete the object named `name` from this group, staged until
2568 /// [`File::commit`]. See [`create_group`](Self::create_group) for the
2569 /// file-mode rules.
2570 pub fn delete(&self, name: &str) -> Result<(), Error> {
2571 self.with_child_session(name, |session, child| {
2572 session.delete(child);
2573 Ok(())
2574 })
2575 }
2576
2577 /// Add or update a compact attribute on this group, staged until
2578 /// [`File::commit`]. Use [`remove_attr`](Self::remove_attr) to remove one.
2579 /// The [`root`](File::root) group's attributes are edited the same way.
2580 ///
2581 /// Requires a read-write file ([`File::open_rw`]), else
2582 /// [`Error::ReadOnly`](crate::Error::ReadOnly). An attribute set too large
2583 /// for compact storage, or a group using dense (fractal-heap) attribute
2584 /// storage, is refused on [`File::commit`].
2585 pub fn set_attr(&self, name: &str, value: AttrValue) -> Result<(), Error> {
2586 self.with_own_session(|session, path| {
2587 session.set_group_attr(path, name, value);
2588 Ok(())
2589 })
2590 }
2591
2592 /// Remove a compact attribute from this group, staged until [`File::commit`].
2593 /// See [`set_attr`](Self::set_attr) for the file-mode rules.
2594 pub fn remove_attr(&self, name: &str) -> Result<(), Error> {
2595 self.with_own_session(|session, path| {
2596 session.remove_group_attr(path, name);
2597 Ok(())
2598 })
2599 }
2600
2601 /// Run `f` with the writable session and the root-relative path of child
2602 /// `name`. Returns [`Error::ReadOnly`](crate::Error::ReadOnly) if the file is
2603 /// read-only or this group has no resolvable path.
2604 fn with_child_session<R>(
2605 &self,
2606 name: &str,
2607 f: impl FnOnce(&mut WriteEngine, &str) -> Result<R, Error>,
2608 ) -> Result<R, Error> {
2609 let Backend::Edit(m) = &self.file.backend else {
2610 return Err(Error::ReadOnly);
2611 };
2612 self.file.check_mutable(true)?;
2613 let child = self.child_path(name).ok_or(Error::ReadOnly)?;
2614 let mut session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2615 f(&mut session, &child)
2616 }
2617
2618 /// Validate that this group can stage an edit to child `name` and return the
2619 /// child's root-relative path, *without* taking the session lock.
2620 ///
2621 /// Paired with [`apply_staged`](Self::apply_staged): the checks run first so
2622 /// a read-only or sealed file is reported before any user closure runs, the
2623 /// closure then runs unlocked, and the lock is taken only to record what it
2624 /// built (issue #200).
2625 fn child_edit_path(&self, name: &str) -> Result<String, Error> {
2626 self.file.check_staged_writable()?;
2627 self.child_path(name).ok_or(Error::ReadOnly)
2628 }
2629
2630 /// Record already-built edits on the writable session, holding the lock only
2631 /// for the duration of the replay.
2632 ///
2633 /// The file is re-checked here because the closure that produced `ops` ran
2634 /// unlocked and could have closed the file in the meantime; staging into a
2635 /// sealed file would otherwise be silently accepted and then dropped.
2636 fn apply_staged(&self, ops: Vec<StagedOp>) -> Result<(), Error> {
2637 self.file.check_staged_writable()?;
2638 let Backend::Edit(m) = &self.file.backend else {
2639 // `check_staged_writable` accepts only a mirror backend, and a
2640 // file's backend is fixed for its lifetime.
2641 return Err(Error::ReadOnly);
2642 };
2643 let mut session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2644 for op in ops {
2645 op.apply(&mut session);
2646 }
2647 Ok(())
2648 }
2649
2650 /// Run `f` with the writable session and this group's *own* root-relative
2651 /// path (for attribute edits, which act on the group itself rather than a
2652 /// child). Returns [`Error::ReadOnly`](crate::Error::ReadOnly) if the file is
2653 /// read-only or this group has no resolvable path, and
2654 /// [`Error::FileClosed`](crate::Error::FileClosed) once the file is sealed.
2655 fn with_own_session<R>(
2656 &self,
2657 f: impl FnOnce(&mut WriteEngine, &str) -> Result<R, Error>,
2658 ) -> Result<R, Error> {
2659 let Backend::Edit(m) = &self.file.backend else {
2660 return Err(Error::ReadOnly);
2661 };
2662 self.file.check_mutable(true)?;
2663 let path = self.path.clone().ok_or(Error::ReadOnly)?;
2664 let mut session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2665 f(&mut session, &path)
2666 }
2667
2668 fn children(&self) -> Result<Vec<GroupEntry>, Error> {
2669 let hdr = self.file.parse_header(self.address)?;
2670 self.file.group_children(&hdr)
2671 }
2672}
2673
2674// ---------------------------------------------------------------------------
2675// Dataset handle
2676// ---------------------------------------------------------------------------
2677
2678/// An owned handle to an HDF5 dataset.
2679pub struct Dataset {
2680 file: Arc<FileInner>,
2681 /// Address of this dataset's object header (base-adjusted, file-absolute).
2682 /// Used to resolve object references that point at this dataset.
2683 address: u64,
2684 header: ObjectHeader,
2685 // Held per-dataset: the chunk index is keyed only by chunk coordinate, so
2686 // a file-level cache would alias chunk addresses across datasets.
2687 chunk_cache: ChunkCache,
2688 // The effective chunk-cache config for this dataset: the file-wide default
2689 // or a per-dataset DAPL override. Reported by `chunk_cache_config`.
2690 chunk_cache_config: ChunkCacheConfig,
2691 /// Root-relative path of this dataset, used to address it for write
2692 /// operations on a read-write file. `None` for a dataset reached by object
2693 /// reference ([`Dataset::dereference`]), which has no resolvable path.
2694 path: Option<String>,
2695}
2696
2697impl std::fmt::Debug for Dataset {
2698 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2699 f.debug_struct("Dataset")
2700 .field("messages", &self.header.messages.len())
2701 .finish()
2702 }
2703}
2704
2705impl Dataset {
2706 /// Address of this dataset's object header (base-adjusted, file-absolute).
2707 /// Used to resolve object references that point at this dataset.
2708 pub(crate) fn header_address(&self) -> u64 {
2709 self.address
2710 }
2711
2712 /// Append `data` to this dataset in place, growing it along its first
2713 /// (unlimited) dimension, and refresh this handle so subsequent reads observe
2714 /// the new length.
2715 ///
2716 /// The file must have been opened for writing with [`File::open_rw`] or
2717 /// [`File::open_rw_bounded`]; a read-only file returns
2718 /// [`Error::ReadOnly`](crate::Error::ReadOnly). The target must be a chunked,
2719 /// rank-1, unlimited, Extensible-Array-indexed dataset, and filtered datasets
2720 /// take whole chunks where unfiltered ones take any length; anything else
2721 /// returns
2722 /// [`Error::AppendInPlaceUnsupported`](crate::Error::AppendInPlaceUnsupported).
2723 /// The append is immediate and crash-atomic (no `commit` needed).
2724 ///
2725 /// A handle reached by object reference ([`dereference`](Self::dereference))
2726 /// has no resolvable path, so it names its dataset by the object-header
2727 /// address it was reached through and can append like any other — until the
2728 /// session stages or commits an edit. A commit can move that header, and the
2729 /// bytes it vacates still parse as the dataset they were, so an append
2730 /// against the old address would land in a header nothing points at. Rather
2731 /// than do that silently, such an append is refused once edits are staged or
2732 /// a commit has run; re-open the dataset by path to keep appending. A
2733 /// path-named handle is unaffected, because the path is resolved afresh every
2734 /// time.
2735 pub fn append<T: H5Element>(&mut self, data: &[T]) -> Result<(), Error> {
2736 let g = self.append_geometry()?;
2737 self.append_batches(g, data.len() as u64, |b, r| {
2738 b.append(&data[r]);
2739 })
2740 }
2741
2742 /// Append raw little-endian element bytes to this dataset in place. Prefer
2743 /// [`append`](Self::append) when the element type is known; see it for the
2744 /// file-mode and eligibility rules.
2745 pub fn append_raw(&mut self, bytes: &[u8]) -> Result<(), Error> {
2746 let g = self.append_geometry()?;
2747 let es = g.element_size.max(1);
2748 // Whole-element length is checked before any batch applies, so the
2749 // refusal is atomic (the per-batch validation would only reject the
2750 // final, short batch after earlier ones had durably committed).
2751 if bytes.len() % es != 0 {
2752 return Err(Error::AppendInPlaceUnsupported(
2753 "appended byte length is not a whole number of elements",
2754 ));
2755 }
2756 let total = (bytes.len() / es) as u64;
2757 self.append_batches(g, total, |b, r| {
2758 b.append_raw(&bytes[r.start * es..r.end * es]);
2759 })
2760 }
2761
2762 /// How an append names this dataset to the session: by path when the handle
2763 /// has one, so the session can check the target against its own staged
2764 /// edits, and otherwise by the object-header address the handle was reached
2765 /// through — which is what lets a handle obtained by object reference append
2766 /// at all.
2767 fn append_target(&self) -> AppendTarget<'_> {
2768 match &self.path {
2769 Some(path) => AppendTarget::Path(path),
2770 None => AppendTarget::Header(self.address),
2771 }
2772 }
2773
2774 /// Fetch (locating on first use) this dataset's append geometry from the
2775 /// write session, which also applies every refusal that does not depend on
2776 /// the bytes being appended.
2777 fn append_geometry(&self) -> Result<AppendGeometry, Error> {
2778 let Backend::Edit(m) = &self.file.backend else {
2779 return Err(Error::ReadOnly);
2780 };
2781 self.file.check_mutable(false)?;
2782 let mut engine = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2783 engine.append_geometry(self.append_target())
2784 }
2785
2786 /// Immediate in-place append, driven batch by batch. The call is split into
2787 /// aligned batches — the trailing partial chunk is filled first, then
2788 /// whole-chunk batches under the session's byte budget — and `fill` builds
2789 /// each batch's bytes on demand, so a bounded session's peak memory holds
2790 /// one batch rather than the whole call. A session that keeps the whole file
2791 /// resident reports one unbounded batch, so the call stays a single
2792 /// crash-atomic apply there.
2793 ///
2794 /// Every predictable refusal (wrong datatype, ineligible dataset,
2795 /// non-chunk-aligned filtered append) is raised before the first batch is
2796 /// applied. The cached header and chunk cache are then refreshed so later
2797 /// reads on this handle observe the new length.
2798 fn append_batches(
2799 &mut self,
2800 g: AppendGeometry,
2801 total_elems: u64,
2802 fill: impl Fn(&mut AppendBuilder, std::ops::Range<usize>),
2803 ) -> Result<(), Error> {
2804 let Backend::Edit(m) = &self.file.backend else {
2805 return Err(Error::ReadOnly);
2806 };
2807 // Atomic refusal before any batch: a filtered append must be
2808 // whole-chunk (the engine re-checks per batch as a backstop).
2809 if g.filtered && (g.current_dim % g.chunk_elems != 0 || total_elems % g.chunk_elems != 0) {
2810 return Err(Error::AppendInPlaceUnsupported(
2811 "a filtered dataset can only be appended in place in whole chunks (the current \
2812 length and the appended length must both be multiples of the chunk length); \
2813 use Dataset::append_staged for a non-chunk-aligned filtered append",
2814 ));
2815 }
2816 let mut dim = g.current_dim;
2817 let mut done = 0u64;
2818 loop {
2819 // An empty append still runs one (empty) engine call, so datatype
2820 // validation happens whether or not there are elements.
2821 self.file.check_mutable(false)?;
2822 let to_boundary = (g.chunk_elems - dim % g.chunk_elems) % g.chunk_elems;
2823 let take = (total_elems - done).min(to_boundary.saturating_add(g.full_batch_elems));
2824 let mut b = AppendBuilder::new();
2825 fill(&mut b, done.to_usize()?..(done + take).to_usize()?);
2826 {
2827 let mut engine = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2828 engine.append_inplace_gathered(self.append_target(), &b, 4)?;
2829 }
2830 dim += take;
2831 done += take;
2832 if done >= total_elems {
2833 break;
2834 }
2835 }
2836 self.header = self.file.parse_header(self.address)?;
2837 // Same staleness rule as `with_session_mut`: the append repointed or
2838 // extended the chunk index this handle may have cached.
2839 self.chunk_cache.clear();
2840 Ok(())
2841 }
2842
2843 /// Overwrite this dataset's values, staged until [`File::commit`]. The new
2844 /// data must match the dataset's existing shape and datatype.
2845 ///
2846 /// The file must have been opened with [`File::open_rw`], else
2847 /// [`Error::ReadOnly`](crate::Error::ReadOnly). Unlike [`append`](Self::append)
2848 /// (immediate), this is a staged edit applied on [`File::commit`].
2849 pub fn write<T: H5Element>(&mut self, data: &[T]) -> Result<(), Error> {
2850 // Build off the lock — `H5Element` is user-implementable, so
2851 // `write_into` is potentially user code (see `write_staged`).
2852 self.check_staged_edit()?;
2853 let mut builder = DatasetBuilder::new("");
2854 T::write_into(&mut builder, data);
2855 self.with_session_mut(true, |session, path| {
2856 session.stage_dataset_write(path, builder);
2857 Ok(())
2858 })
2859 }
2860
2861 /// Overwrite this dataset's values through its full [`DatasetBuilder`],
2862 /// staged until [`File::commit`] — the builder-level counterpart of
2863 /// [`write`](Self::write), for element kinds that are not [`H5Element`]
2864 /// (variable-length strings, raw bytes with an explicit datatype).
2865 ///
2866 /// The replacement must match the on-disk datatype and shape exactly; a
2867 /// reshape or retype is refused on [`File::commit`].
2868 ///
2869 /// The file must have been opened with [`File::open_rw`], else
2870 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
2871 ///
2872 /// ```no_run
2873 /// # use hdf5_pure::File;
2874 /// # fn main() -> Result<(), hdf5_pure::Error> {
2875 /// let file = File::open_rw("labels.h5")?;
2876 /// let mut ds = file.dataset("names")?;
2877 /// ds.write_staged(|b| {
2878 /// b.with_vlen_strings(&["ada", "grace", "katherine"]);
2879 /// })?;
2880 /// file.commit()?;
2881 /// # Ok(())
2882 /// # }
2883 /// ```
2884 /// The closure configures a standalone builder, not the file, so it may read
2885 /// the same [`File`]; nothing it stages resolves until [`File::commit`].
2886 pub fn write_staged(&mut self, build: impl FnOnce(&mut DatasetBuilder)) -> Result<(), Error> {
2887 // Report a read-only, sealed, or unaddressable dataset before running the
2888 // closure, then run it with no lock held; `stage_dataset_write` names the
2889 // builder from the dataset's path (issue #200).
2890 self.check_staged_edit()?;
2891 let mut builder = DatasetBuilder::new("");
2892 build(&mut builder);
2893 self.with_session_mut(true, |session, path| {
2894 session.stage_dataset_write(path, builder);
2895 Ok(())
2896 })
2897 }
2898
2899 /// Stage an append to this dataset applied on [`File::commit`] — the staged,
2900 /// index-rebuilding counterpart of the immediate [`append`](Self::append).
2901 ///
2902 /// Unlike [`append`](Self::append) (immediate, amortized `O(1)`,
2903 /// Extensible-Array only, unfiltered any-length / filtered whole-chunk), this
2904 /// rebuilds the chunk index on commit and so also grows **filtered** datasets
2905 /// by any length (a trailing partial chunk is rewritten) and datasets whose
2906 /// Extensible-Array index is not yet allocated. Configure the appended
2907 /// elements through `build` on the [`AppendBuilder`]; repeated calls within
2908 /// the builder concatenate in order. The dataset must be chunked, unlimited
2909 /// along axis 0, Extensible-Array indexed, rank 1, use a re-encodable filter
2910 /// pipeline, and have a single hard link, otherwise
2911 /// [`Error::AppendUnsupported`](crate::Error::AppendUnsupported) is returned
2912 /// on [`File::commit`].
2913 ///
2914 /// The file must have been opened with [`File::open_rw`], else
2915 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
2916 /// The closure configures a standalone builder, not the file, so it may read
2917 /// the same [`File`]; nothing it stages resolves until [`File::commit`].
2918 pub fn append_staged(&mut self, build: impl FnOnce(&mut AppendBuilder)) -> Result<(), Error> {
2919 self.check_staged_edit()?;
2920 let mut builder = AppendBuilder::new();
2921 build(&mut builder);
2922 self.with_session_mut(true, |session, path| {
2923 session.stage_dataset_append(path, builder);
2924 Ok(())
2925 })
2926 }
2927
2928 /// Add or update a compact attribute on this dataset, staged until
2929 /// [`File::commit`]. Use [`remove_attr`](Self::remove_attr) to remove one.
2930 ///
2931 /// The file must have been opened with [`File::open_rw`], else
2932 /// [`Error::ReadOnly`](crate::Error::ReadOnly).
2933 pub fn set_attr(&mut self, name: &str, value: AttrValue) -> Result<(), Error> {
2934 self.with_session_mut(true, |session, path| {
2935 session.set_dataset_attr(path, name, value);
2936 Ok(())
2937 })
2938 }
2939
2940 /// Remove a compact attribute from this dataset, staged until
2941 /// [`File::commit`]. See [`set_attr`](Self::set_attr) for the file-mode rules.
2942 pub fn remove_attr(&mut self, name: &str) -> Result<(), Error> {
2943 self.with_session_mut(true, |session, path| {
2944 session.remove_dataset_attr(path, name);
2945 Ok(())
2946 })
2947 }
2948
2949 /// Gate a staged edit on this dataset *without* taking the session lock:
2950 /// the file must accept staged edits, and this handle must have a resolvable
2951 /// path.
2952 ///
2953 /// The path check belongs here rather than only in
2954 /// [`with_session_mut`](Self::with_session_mut) so that every reason to
2955 /// refuse is reported *before* a user closure runs, not after. A handle
2956 /// reached by object reference ([`dereference`](Self::dereference)) has no
2957 /// path, and would otherwise have its closure run and its result discarded.
2958 fn check_staged_edit(&self) -> Result<(), Error> {
2959 self.file.check_staged_writable()?;
2960 if self.path.is_none() {
2961 return Err(Error::ReadOnly);
2962 }
2963 Ok(())
2964 }
2965
2966 /// Run `f` with the writable session and this dataset's path, then refresh
2967 /// the cached header so a later read on this handle reflects any immediate
2968 /// change (e.g. an append's new dimension). Returns
2969 /// [`Error::ReadOnly`](crate::Error::ReadOnly) if the file is read-only or the
2970 /// handle has no resolvable path (reached by object reference).
2971 fn with_session_mut<R>(
2972 &mut self,
2973 staged: bool,
2974 f: impl FnOnce(&mut WriteEngine, &str) -> Result<R, Error>,
2975 ) -> Result<R, Error> {
2976 let Backend::Edit(m) = &self.file.backend else {
2977 return Err(Error::ReadOnly);
2978 };
2979 self.file.check_mutable(staged)?;
2980 let path = self.path.clone().ok_or(Error::ReadOnly)?;
2981 let out = {
2982 let mut session = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2983 f(&mut session, &path)?
2984 };
2985 self.header = self.file.parse_header(self.address)?;
2986 // An append relocates the trailing chunk and grows the chunk index, so
2987 // this handle's cached index and retained chunks are stale; drop them
2988 // so the next read re-walks the live index.
2989 self.chunk_cache.clear();
2990 Ok(out)
2991 }
2992
2993 /// The effective raw chunk-cache configuration for this dataset.
2994 ///
2995 /// This reflects the per-dataset [`DatasetAccessProperties`] override when one
2996 /// was supplied to [`File::dataset_with_options`] /
2997 /// [`Group::dataset_with_options`], otherwise the file-wide default. It is
2998 /// the read-side analogue of HDF5's `H5Pget_chunk_cache`.
2999 pub const fn chunk_cache_config(&self) -> ChunkCacheConfig {
3000 self.chunk_cache_config
3001 }
3002
3003 /// A point-in-time snapshot of this dataset handle's chunk-cache occupancy.
3004 ///
3005 /// Lets callers confirm a chunk-cache configuration (set with
3006 /// [`FileAccessProperties::with_chunk_cache`]) is taking effect: after a
3007 /// chunked read, an enabled cache reports a loaded index and retained
3008 /// chunks; a disabled one (or one over its budget) reports fewer or none.
3009 /// The cache is per-handle, so a freshly opened [`Dataset`] reports an empty
3010 /// snapshot until its first read.
3011 pub fn chunk_cache_stats(&self) -> ChunkCacheStats {
3012 self.chunk_cache.stats()
3013 }
3014
3015 /// Returns the shape (dimensions) of the dataset.
3016 pub fn shape(&self) -> Result<Vec<u64>, Error> {
3017 let ds = self.dataspace()?;
3018 Ok(ds.dimensions.clone())
3019 }
3020
3021 /// The dataset's maximum dimensions, when it is extensible. An unlimited
3022 /// dimension is reported as `u64::MAX`. Returns `Ok(None)` for a fixed-shape
3023 /// dataset (no maximum-dimensions record, or one equal to the current shape).
3024 ///
3025 /// Together with [`is_chunked`](Self::is_chunked) and
3026 /// [`chunk_shape`](Self::chunk_shape), this lets a caller check up front
3027 /// whether a dataset is eligible for
3028 /// [`Dataset::append_staged`](crate::Dataset::append_staged)
3029 /// (which requires a chunked dataset whose first maximum dimension is
3030 /// `u64::MAX`) instead of relying on the append's refusal error.
3031 pub fn maxshape(&self) -> Result<Option<Vec<u64>>, Error> {
3032 let ds = self.dataspace()?;
3033 match &ds.max_dimensions {
3034 Some(md) if *md != ds.dimensions => Ok(Some(md.clone())),
3035 _ => Ok(None),
3036 }
3037 }
3038
3039 /// Whether the dataset uses chunked storage (as opposed to contiguous or
3040 /// compact). Filtered datasets are always chunked. Returns `false` for a
3041 /// dataset with no data-layout message or a non-chunked layout.
3042 pub fn is_chunked(&self) -> bool {
3043 matches!(self.data_layout(), Ok(DataLayout::Chunked { .. }))
3044 }
3045
3046 /// The dataset's chunk dimensions (one per dataset rank), or `Ok(None)` when
3047 /// the dataset is not chunked. The element-size dimension the on-disk layout
3048 /// appends is stripped, so the result lines up with
3049 /// [`shape`](Self::shape) / [`maxshape`](Self::maxshape).
3050 pub fn chunk_shape(&self) -> Result<Option<Vec<u64>>, Error> {
3051 let DataLayout::Chunked {
3052 chunk_dimensions, ..
3053 } = self.data_layout()?
3054 else {
3055 return Ok(None);
3056 };
3057 let rank = self.dataspace()?.dimensions.len();
3058 if chunk_dimensions.len() <= rank {
3059 return Ok(None);
3060 }
3061 Ok(Some(
3062 chunk_dimensions[..rank]
3063 .iter()
3064 .map(|&c| u64::from(c))
3065 .collect(),
3066 ))
3067 }
3068
3069 /// The HDF5 filter IDs applied to this dataset's chunks, in pipeline
3070 /// (application) order, or an empty vector when the dataset is unfiltered.
3071 /// The IDs are the registered HDF5 filter numbers — e.g. 1 = deflate,
3072 /// 2 = shuffle, 3 = fletcher32, 6 = scale-offset — so a caller can inspect
3073 /// the pipeline without decoding a chunk.
3074 pub fn filters(&self) -> Vec<u16> {
3075 self.filter_pipeline_parsed()
3076 .map(|p| p.filters.iter().map(|f| f.filter_id).collect())
3077 .unwrap_or_default()
3078 }
3079
3080 /// How and where this dataset's raw data is stored: compact, contiguous,
3081 /// chunked, or virtual.
3082 ///
3083 /// The structured companion to [`is_chunked`](Self::is_chunked) and
3084 /// [`chunk_shape`](Self::chunk_shape), which it subsumes: one call that
3085 /// classifies the layout and, for a [`Layout::Contiguous`] dataset, gives the
3086 /// absolute address and byte size to seek to, or for a [`Layout::Chunked`]
3087 /// dataset the chunk shape and [`ChunkIndex`] kind. This parses only the
3088 /// data-layout message; it never walks the chunk index or reads any data —
3089 /// use [`chunks`](Self::chunks) for per-chunk locations. The curated analogue
3090 /// of `H5Pget_layout`.
3091 ///
3092 /// Returns `Err` if the dataset has no data-layout message, if it cannot be
3093 /// parsed, or if a chunked dataset uses an index kind this crate does not
3094 /// recognize.
3095 pub fn layout(&self) -> Result<Layout, Error> {
3096 Ok(match self.data_layout()? {
3097 DataLayout::Compact { data } => Layout::Compact {
3098 size: data.len() as u64,
3099 },
3100 DataLayout::Contiguous { address, size } => Layout::Contiguous {
3101 address: self.absolute_address(address)?,
3102 size,
3103 },
3104 DataLayout::Chunked {
3105 version,
3106 chunk_index_type,
3107 ..
3108 } => Layout::Chunked {
3109 // Reuse `chunk_shape` so the two accessors can never disagree on
3110 // how the element-size dimension is stripped.
3111 chunk_shape: self.chunk_shape()?.unwrap_or_default(),
3112 index: ChunkIndex::from_layout(version, chunk_index_type)?,
3113 },
3114 DataLayout::Virtual { .. } => Layout::Virtual,
3115 })
3116 }
3117
3118 /// The [`ChunkIndex`] kind of this chunked dataset, or `Ok(None)` when the
3119 /// dataset is not chunked.
3120 ///
3121 /// A convenience shortcut for the `index` of [`Layout::Chunked`], for the
3122 /// common up-front append-eligibility check
3123 /// ([`ChunkIndex::supports_inplace_append`]). Complements
3124 /// [`maxshape`](Self::maxshape) and [`chunk_shape`](Self::chunk_shape).
3125 ///
3126 /// Returns `Err` if the data-layout message is missing or cannot be parsed,
3127 /// or if a chunked dataset uses an index kind this crate does not recognize.
3128 pub fn chunk_index(&self) -> Result<Option<ChunkIndex>, Error> {
3129 match self.data_layout()? {
3130 DataLayout::Chunked {
3131 version,
3132 chunk_index_type,
3133 ..
3134 } => Ok(Some(ChunkIndex::from_layout(version, chunk_index_type)?)),
3135 _ => Ok(None),
3136 }
3137 }
3138
3139 /// Enumerate every allocated chunk of this chunked dataset — one [`Chunk`]
3140 /// (logical offset, absolute file address, on-disk stored size, filter mask)
3141 /// per chunk, in index order.
3142 ///
3143 /// This reads only the chunk index, not the chunk data, so a caller can seek
3144 /// to and decode chunks one at a time without materializing the whole
3145 /// dataset. The curated analogue of `H5Dget_num_chunks` + `H5Dget_chunk_info`
3146 /// (`chunks()?.len()` is the chunk count).
3147 ///
3148 /// Returns `Ok(vec![])` for a chunked dataset whose storage has not been
3149 /// allocated yet (including a not-yet-written dataset that will use a
3150 /// [`ChunkIndex::BTreeV2`] index). Returns `Err` if the dataset is not chunked
3151 /// (check [`layout`](Self::layout) or [`is_chunked`](Self::is_chunked) first),
3152 /// or if its allocated storage is indexed by a [`ChunkIndex::BTreeV2`] index,
3153 /// which has no enumerator yet.
3154 pub fn chunks(&self) -> Result<Vec<Chunk>, Error> {
3155 let rank = self.dataspace()?.dimensions.len();
3156 Ok(self
3157 .raw_chunks()?
3158 .into_iter()
3159 .map(|c| Chunk {
3160 offset: c.offsets.into_iter().take(rank).collect(),
3161 address: c.address,
3162 storage_size: u64::from(c.chunk_size),
3163 filter_mask: c.filter_mask,
3164 })
3165 .collect())
3166 }
3167
3168 /// This dataset's filter pipeline as an ordered list of [`Filter`]s — each
3169 /// with its identifier, optional name, optional/mandatory flag, and client
3170 /// data — or an empty vector when the dataset is unfiltered.
3171 ///
3172 /// The detailed companion to [`filters`](Self::filters), which returns just
3173 /// the identifiers. Filters are listed in application (write) order — the
3174 /// on-disk pipeline order, matching [`filters`](Self::filters); a reader
3175 /// inverts them in the *reverse* of this order to decode a chunk. The curated
3176 /// analogue of `H5Pget_nfilters` + `H5Pget_filter2`.
3177 pub fn filter_pipeline(&self) -> Vec<Filter> {
3178 self.filter_pipeline_parsed()
3179 .map(|p| {
3180 p.filters
3181 .into_iter()
3182 .map(|f| Filter {
3183 id: f.filter_id,
3184 name: f.name,
3185 is_optional: f.flags & 0x1 != 0,
3186 client_data: f.client_data,
3187 })
3188 .collect()
3189 })
3190 .unwrap_or_default()
3191 }
3192
3193 /// Shift a base-relative on-disk address to an absolute file offset using the
3194 /// superblock base address (`addr_offset`). A no-op for the common
3195 /// base-zero file. Returns `Ok(None)` for an unallocated (undefined) address.
3196 fn absolute_address(&self, address: Option<u64>) -> Result<Option<u64>, Error> {
3197 match address {
3198 Some(rel) => Ok(Some(rel.checked_add(self.file.addr_offset).ok_or(
3199 crate::error::FormatError::OffsetOverflow {
3200 offset: rel,
3201 length: 0,
3202 },
3203 )?)),
3204 None => Ok(None),
3205 }
3206 }
3207
3208 /// Returns the simplified datatype of the dataset.
3209 pub fn dtype(&self) -> Result<DType, Error> {
3210 let dt = self.datatype()?;
3211 Ok(classify_datatype(&dt))
3212 }
3213
3214 /// The size in bytes of one on-disk element of this dataset's datatype —
3215 /// HDF5's datatype storage size (`H5Tget_size`).
3216 ///
3217 /// This is the byte width of a single stored element: 8 for `f64`, the
3218 /// declared length for a fixed-length string, the record size for a compound
3219 /// type, or the reference/descriptor size for a variable-length type (whose
3220 /// payload lives separately in the file's global heaps).
3221 ///
3222 /// Multiplied by the element count from [`shape`](Self::shape), it is the
3223 /// exact number of raw bytes a full [`read_raw`](Self::read_raw)
3224 /// materializes. A caller reading an untrusted file can use it to bound that
3225 /// allocation up front rather than trusting the file's declared extent: a
3226 /// dataset can name a small element count yet a per-element size of billions
3227 /// of bytes, so the product — not the count alone — is what a read allocates.
3228 pub fn element_size(&self) -> Result<u64, Error> {
3229 Ok(u64::from(self.datatype()?.type_size()))
3230 }
3231
3232 /// The raw bytes of this dataset's user-defined fill value, encoded in its
3233 /// datatype, or `None` when no user-defined fill value is set (the library
3234 /// default or an explicitly undefined fill). Reads whichever Fill Value
3235 /// message the header carries — the current `0x0005` (versions 1/2/3) or the
3236 /// legacy `0x0004` — so files from this crate, the reference C library, and
3237 /// h5py are all handled.
3238 pub(crate) fn defined_fill_bytes(&self) -> Result<Option<Vec<u8>>, Error> {
3239 let msg = self
3240 .header
3241 .messages
3242 .iter()
3243 .find(|m| m.msg_type == MessageType::FillValue)
3244 .or_else(|| {
3245 self.header
3246 .messages
3247 .iter()
3248 .find(|m| m.msg_type == MessageType::FillValueOld)
3249 });
3250 match msg {
3251 Some(m) => Ok(crate::fill_value::parse_defined_fill_value(
3252 m.msg_type, &m.data,
3253 )?),
3254 None => Ok(None),
3255 }
3256 }
3257
3258 /// Read all data as `f64` values.
3259 pub fn read_f64(&self) -> Result<Vec<f64>, Error> {
3260 let raw = self.read_raw()?;
3261 let dt = self.datatype()?;
3262 Ok(data_read::read_as_f64(&raw, &dt)?)
3263 }
3264
3265 /// Read all data as `f32` values.
3266 pub fn read_f32(&self) -> Result<Vec<f32>, Error> {
3267 let raw = self.read_raw()?;
3268 let dt = self.datatype()?;
3269 Ok(data_read::read_as_f32(&raw, &dt)?)
3270 }
3271
3272 /// Read all data as `i32` values.
3273 pub fn read_i32(&self) -> Result<Vec<i32>, Error> {
3274 let raw = self.read_raw()?;
3275 let dt = self.datatype()?;
3276 Ok(data_read::read_as_i32(&raw, &dt)?)
3277 }
3278
3279 /// Read all data as `i64` values.
3280 pub fn read_i64(&self) -> Result<Vec<i64>, Error> {
3281 let raw = self.read_raw()?;
3282 let dt = self.datatype()?;
3283 Ok(data_read::read_as_i64(&raw, &dt)?)
3284 }
3285
3286 /// Read all data as `u64` values.
3287 pub fn read_u64(&self) -> Result<Vec<u64>, Error> {
3288 let raw = self.read_raw()?;
3289 let dt = self.datatype()?;
3290 Ok(data_read::read_as_u64(&raw, &dt)?)
3291 }
3292
3293 /// Read all data as `u8` values.
3294 pub fn read_u8(&self) -> Result<Vec<u8>, Error> {
3295 self.read_raw()
3296 }
3297
3298 /// Read all data as `i8` values.
3299 #[expect(
3300 clippy::cast_possible_wrap,
3301 reason = "read_i8 reinterprets each stored byte as the signed i8 the caller requested"
3302 )]
3303 pub fn read_i8(&self) -> Result<Vec<i8>, Error> {
3304 let raw = self.read_raw()?;
3305 Ok(raw.iter().map(|&b| b as i8).collect())
3306 }
3307
3308 /// Read all data as `i16` values.
3309 pub fn read_i16(&self) -> Result<Vec<i16>, Error> {
3310 let raw = self.read_raw()?;
3311 let dt = self.datatype()?;
3312 Ok(data_read::read_as_i16(&raw, &dt)?)
3313 }
3314
3315 /// Read all data as `u16` values.
3316 pub fn read_u16(&self) -> Result<Vec<u16>, Error> {
3317 let raw = self.read_raw()?;
3318 let dt = self.datatype()?;
3319 Ok(data_read::read_as_u16(&raw, &dt)?)
3320 }
3321
3322 /// Read all data as `u32` values.
3323 pub fn read_u32(&self) -> Result<Vec<u32>, Error> {
3324 let raw = self.read_raw()?;
3325 let dt = self.datatype()?;
3326 Ok(data_read::read_as_u32(&raw, &dt)?)
3327 }
3328
3329 /// Read all data as `String` values.
3330 ///
3331 /// Fixed-length and variable-length HDF5 string datasets are both
3332 /// supported. Use [`read_vlen_strings`](Self::read_vlen_strings) when
3333 /// variable-length allocation limits are required.
3334 pub fn read_string(&self) -> Result<Vec<String>, Error> {
3335 let dt = self.datatype()?;
3336 if vl_data::is_vlen_string_datatype(&dt) {
3337 self.read_vlen_strings(VlenStringReadOptions::default())
3338 } else {
3339 let raw = self.read_raw()?;
3340 Ok(data_read::read_as_strings(&raw, &dt)?)
3341 }
3342 }
3343
3344 /// Return the total bytes referenced by this VL string dataset.
3345 ///
3346 /// This is the payload equivalent of HDF5's `H5Dvlen_get_buf_size`: it
3347 /// excludes `Vec<String>` and `String` allocation metadata.
3348 pub fn vlen_string_payload_size(&self) -> Result<u64, Error> {
3349 let datatype = self.datatype()?;
3350 if !vl_data::is_vlen_string_datatype(&datatype) {
3351 return Err(FormatError::TypeMismatch {
3352 expected: "VariableLength string",
3353 actual: "non-VariableLength string",
3354 }
3355 .into());
3356 }
3357 let dataspace = self.dataspace()?;
3358 let raw = self.read_raw()?;
3359 Ok(vl_data::vlen_string_payload_size(
3360 &raw,
3361 dataspace.num_elements(),
3362 self.file.offset_size(),
3363 )?)
3364 }
3365
3366 /// Read a VL string dataset with explicit allocation limits.
3367 ///
3368 /// Both limits are checked before any string payload is materialized.
3369 pub fn read_vlen_strings(&self, options: VlenStringReadOptions) -> Result<Vec<String>, Error> {
3370 let mut strings = Vec::new();
3371 self.visit_vlen_strings(options, |string| strings.push(string.to_owned()))?;
3372 Ok(strings)
3373 }
3374
3375 /// Visit a VL string dataset one element at a time.
3376 ///
3377 /// The string slice passed to `visitor` is valid only for the duration of
3378 /// that callback. This avoids retaining all decoded string payloads at once.
3379 ///
3380 /// On a read-write file ([`File::open_rw`] / [`File::open_rw_bounded`]) the
3381 /// visitor runs while the file's engine lock is held, so it must not read
3382 /// or write through this file (or a clone / handle of it) — doing so
3383 /// deadlocks. Collect values and act on them after the call instead.
3384 pub fn visit_vlen_strings<F>(
3385 &self,
3386 options: VlenStringReadOptions,
3387 visitor: F,
3388 ) -> Result<(), Error>
3389 where
3390 F: FnMut(&str),
3391 {
3392 let datatype = self.datatype()?;
3393 if !vl_data::is_vlen_string_datatype(&datatype) {
3394 return Err(FormatError::TypeMismatch {
3395 expected: "VariableLength string",
3396 actual: "non-VariableLength string",
3397 }
3398 .into());
3399 }
3400 let dataspace = self.dataspace()?;
3401 if let Some(limit) = options.max_elements()
3402 && dataspace.num_elements() > limit as u64
3403 {
3404 return Err(FormatError::VariableLengthElementLimitExceeded {
3405 limit,
3406 actual: dataspace.num_elements(),
3407 }
3408 .into());
3409 }
3410 let raw = self.read_raw()?;
3411 self.file.with_source(|source| {
3412 Ok(vl_data::visit_vl_strings_from_source(
3413 source,
3414 &raw,
3415 dataspace.num_elements(),
3416 self.file.offset_size(),
3417 self.file.length_size(),
3418 self.file.addr_offset,
3419 options,
3420 visitor,
3421 )?)
3422 })
3423 }
3424
3425 /// Read a VL string dataset's exact heap bytes, preserving the
3426 /// null-vs-empty distinction and never lossily decoding.
3427 ///
3428 /// Unlike [`read_vlen_strings`](Self::read_vlen_strings), which returns
3429 /// `String`s via `from_utf8_lossy` and so cannot reproduce embedded NULs or
3430 /// non-UTF-8 payloads, this yields each element's raw bytes (or a null
3431 /// marker). It underpins faithful rewriting (e.g. repack) of VL strings.
3432 pub(crate) fn read_vlen_string_bytes(
3433 &self,
3434 options: VlenStringReadOptions,
3435 ) -> Result<Vec<vl_data::VlByteObject>, Error> {
3436 let datatype = self.datatype()?;
3437 if !vl_data::is_vlen_string_datatype(&datatype) {
3438 return Err(FormatError::TypeMismatch {
3439 expected: "VariableLength string",
3440 actual: "non-VariableLength string",
3441 }
3442 .into());
3443 }
3444 let dataspace = self.dataspace()?;
3445 if let Some(limit) = options.max_elements()
3446 && dataspace.num_elements() > limit as u64
3447 {
3448 return Err(FormatError::VariableLengthElementLimitExceeded {
3449 limit,
3450 actual: dataspace.num_elements(),
3451 }
3452 .into());
3453 }
3454 let raw = self.read_raw()?;
3455 self.file.with_source(|source| {
3456 Ok(vl_data::read_vl_byte_objects_from_source(
3457 source,
3458 &raw,
3459 dataspace.num_elements(),
3460 self.file.offset_size(),
3461 self.file.length_size(),
3462 self.file.addr_offset,
3463 1, // a VL string's base type is a single byte
3464 options,
3465 )?)
3466 })
3467 }
3468
3469 /// Read every element of a *non-string* variable-length (sequence) dataset as
3470 /// its exact heap bytes, alongside the base-type element size in bytes.
3471 ///
3472 /// Each element's heap object holds `length * element_size` bytes, where
3473 /// `length` is the stored element count and `element_size` is the byte width
3474 /// of the sequence's base type. Returning the raw bytes (not decoded values)
3475 /// keeps a faithful rewrite (repack) byte-exact for any base type whose bytes
3476 /// carry no embedded heap or file addresses. Errors with a
3477 /// [`TypeMismatch`](crate::FormatError::TypeMismatch) if the datatype is not a
3478 /// non-string VL datatype.
3479 pub(crate) fn read_vlen_sequence_bytes(
3480 &self,
3481 options: VlenStringReadOptions,
3482 ) -> Result<(Vec<vl_data::VlByteObject>, usize), Error> {
3483 let datatype = self.datatype()?;
3484 let Datatype::VariableLength { base_type, .. } = &datatype else {
3485 return Err(FormatError::TypeMismatch {
3486 expected: "non-string VariableLength",
3487 actual: "non-VariableLength",
3488 }
3489 .into());
3490 };
3491 if vl_data::is_vlen_string_datatype(&datatype) {
3492 return Err(FormatError::TypeMismatch {
3493 expected: "non-string VariableLength",
3494 actual: "VariableLength string",
3495 }
3496 .into());
3497 }
3498 let element_size = base_type.type_size() as usize;
3499 if element_size == 0 {
3500 return Err(
3501 FormatError::VlDataError("non-string VL base type has zero size".into()).into(),
3502 );
3503 }
3504 let dataspace = self.dataspace()?;
3505 if let Some(limit) = options.max_elements()
3506 && dataspace.num_elements() > limit as u64
3507 {
3508 return Err(FormatError::VariableLengthElementLimitExceeded {
3509 limit,
3510 actual: dataspace.num_elements(),
3511 }
3512 .into());
3513 }
3514 let raw = self.read_raw()?;
3515 let objects = self.file.with_source(|source| {
3516 vl_data::read_vl_byte_objects_from_source(
3517 source,
3518 &raw,
3519 dataspace.num_elements(),
3520 self.file.offset_size(),
3521 self.file.length_size(),
3522 self.file.addr_offset,
3523 element_size,
3524 options,
3525 )
3526 })?;
3527 Ok((objects, element_size))
3528 }
3529
3530 /// Read a dataset whose datatype *contains* variable-length references
3531 /// without being variable-length itself — a compound with a VL member, or an
3532 /// array of them (issue #201).
3533 ///
3534 /// Returns everything a rewrite needs: the element bytes, where each embedded
3535 /// reference sits within them, and the heap payload each one names. That lets
3536 /// the writer re-stage the payloads into a new file's global heap and rewrite
3537 /// the references in place, which is what keeps a rewrite from carrying the
3538 /// source file's heap addresses into the destination.
3539 ///
3540 /// The references are resolved one slot at a time, so `options`' limits apply
3541 /// per slot rather than across the whole dataset.
3542 pub(crate) fn read_embedded_vlen_bytes(
3543 &self,
3544 slots: &[vl_data::EmbeddedVlSlot],
3545 options: VlenStringReadOptions,
3546 ) -> Result<vl_data::EmbeddedVlData, Error> {
3547 let stride = self.datatype()?.type_size() as usize;
3548 let dataspace = self.dataspace()?;
3549 let n = dataspace.num_elements();
3550 if let Some(limit) = options.max_elements()
3551 && n > limit as u64
3552 {
3553 return Err(
3554 FormatError::VariableLengthElementLimitExceeded { limit, actual: n }.into(),
3555 );
3556 }
3557
3558 // A zero-element dataset owns no element bytes, and the C library leaves
3559 // such a dataset's storage unallocated — reading it would fail for a
3560 // missing chunk address rather than yield an empty buffer.
3561 let raw = if n == 0 { Vec::new() } else { self.read_raw()? };
3562 let n_usize = n.to_usize()?;
3563 let needed = n_usize
3564 .checked_mul(stride)
3565 .ok_or(FormatError::OffsetOverflow {
3566 offset: n,
3567 length: stride as u64,
3568 })?;
3569 if raw.len() < needed {
3570 return Err(FormatError::UnexpectedEof {
3571 expected: needed,
3572 available: raw.len(),
3573 }
3574 .into());
3575 }
3576
3577 let mut offsets = Vec::with_capacity(n_usize * slots.len());
3578 let mut objects = Vec::with_capacity(n_usize * slots.len());
3579 for slot in slots {
3580 // Gather this slot's reference from every element into a dense buffer,
3581 // which is the shape the shared VL reader consumes. Each slot has its
3582 // own base-type width, so they are resolved a slot at a time rather
3583 // than in one pass.
3584 let mut dense = Vec::with_capacity(n_usize * VL_REF_SIZE);
3585 for e in 0..n_usize {
3586 let at = e * stride + slot.byte_offset;
3587 dense.extend_from_slice(&raw[at..at + VL_REF_SIZE]);
3588 offsets.push(at);
3589 }
3590 let resolved = self.file.with_source(|source| {
3591 vl_data::read_vl_byte_objects_from_source(
3592 source,
3593 &dense,
3594 n,
3595 self.file.offset_size(),
3596 self.file.length_size(),
3597 self.file.addr_offset,
3598 slot.element_size,
3599 options,
3600 )
3601 })?;
3602 objects.extend(resolved);
3603 }
3604 Ok(vl_data::EmbeddedVlData {
3605 raw,
3606 offsets,
3607 objects,
3608 })
3609 }
3610
3611 /// Read all attributes of this dataset.
3612 ///
3613 /// The variant of each value describes its on-disk encoding; see
3614 /// [`Group::attrs`] for what that means for matching on it, and prefer the
3615 /// [`AttrValue`] accessors.
3616 pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
3617 self.file.attrs_of(&self.header)
3618 }
3619
3620 /// Names of every attribute on this dataset, including any whose datatype
3621 /// [`attrs`](Self::attrs) cannot represent. Used by repack to detect an
3622 /// attribute it would otherwise drop.
3623 pub(crate) fn attr_names(&self) -> Result<Vec<String>, Error> {
3624 self.file.attr_message_names_of(&self.header)
3625 }
3626
3627 /// Returns the exact HDF5 datatype, including compound field offsets and
3628 /// total record size.
3629 pub fn datatype(&self) -> Result<Datatype, Error> {
3630 let msg = find_message(&self.header, MessageType::Datatype)?;
3631 let (dt, _) = Datatype::parse(&msg.data)?;
3632 Ok(dt)
3633 }
3634
3635 pub(crate) fn dataspace(&self) -> Result<Dataspace, Error> {
3636 let msg = find_message(&self.header, MessageType::Dataspace)?;
3637 Ok(Dataspace::parse(&msg.data, self.file.length_size())?)
3638 }
3639
3640 pub(crate) fn data_layout(&self) -> Result<DataLayout, Error> {
3641 let msg = find_message(&self.header, MessageType::DataLayout)?;
3642 Ok(DataLayout::parse(
3643 &msg.data,
3644 self.file.offset_size(),
3645 self.file.length_size(),
3646 )?)
3647 }
3648
3649 pub(crate) fn filter_pipeline_parsed(&self) -> Option<FilterPipeline> {
3650 self.header
3651 .messages
3652 .iter()
3653 .find(|m| m.msg_type == MessageType::FilterPipeline)
3654 .and_then(|msg| FilterPipeline::parse(&msg.data).ok())
3655 }
3656
3657 /// The raw, still-compressed on-disk bytes of every allocated chunk of this
3658 /// chunked dataset, with each chunk's `(address, on-disk size, filter mask,
3659 /// logical offset)` — the same `ChunkInfo`s the chunked reader walks before
3660 /// decompressing. Used by repack to copy compressed chunks verbatim without
3661 /// ever decoding them.
3662 ///
3663 /// Returns `Err` if the layout is not chunked. Returns `Ok(vec![])` for an
3664 /// empty / never-allocated chunked dataset (no index address). Covers every
3665 /// index type the reader supports (v3 B-tree and v4 single-chunk, implicit,
3666 /// fixed-array, and extensible-array).
3667 pub(crate) fn raw_chunks(&self) -> Result<Vec<crate::chunked_read::ChunkInfo>, Error> {
3668 let DataLayout::Chunked {
3669 chunk_dimensions,
3670 btree_address,
3671 version,
3672 chunk_index_type,
3673 single_chunk_filtered_size,
3674 single_chunk_filter_mask,
3675 } = self.data_layout()?
3676 else {
3677 return Err(Error::Format(crate::error::FormatError::ChunkedReadError(
3678 "chunk enumeration requires a chunked dataset".into(),
3679 )));
3680 };
3681 // An undefined index address means no storage is allocated yet.
3682 let Some(addr) = btree_address else {
3683 return Ok(Vec::new());
3684 };
3685 let dataspace = self.dataspace()?;
3686 let elem_size = self.datatype()?.type_size() as usize;
3687 let base = self.file.addr_offset;
3688 // The chunk index — its root at `addr` and every internal node — stores
3689 // addresses relative to the base address. Walk it through a base-relative
3690 // view so those resolve, then shift each returned chunk address back to an
3691 // absolute file offset, since callers (repack) read the chunk bytes from
3692 // the full file source.
3693 self.file.with_source(|source| {
3694 if base == 0 {
3695 return Ok(crate::chunked_read::collect_chunks_for_layout_from_source(
3696 source,
3697 version,
3698 chunk_index_type,
3699 addr,
3700 single_chunk_filtered_size,
3701 single_chunk_filter_mask,
3702 &chunk_dimensions,
3703 &dataspace,
3704 elem_size,
3705 self.file.offset_size(),
3706 self.file.length_size(),
3707 )?);
3708 }
3709 let framed = BaseOffsetSource {
3710 inner: source,
3711 base,
3712 };
3713 let mut chunks = crate::chunked_read::collect_chunks_for_layout_from_source(
3714 &framed,
3715 version,
3716 chunk_index_type,
3717 addr,
3718 single_chunk_filtered_size,
3719 single_chunk_filter_mask,
3720 &chunk_dimensions,
3721 &dataspace,
3722 elem_size,
3723 self.file.offset_size(),
3724 self.file.length_size(),
3725 )?;
3726 for c in &mut chunks {
3727 c.address = c.address.checked_add(base).ok_or(
3728 crate::error::FormatError::OffsetOverflow {
3729 offset: c.address,
3730 length: 0,
3731 },
3732 )?;
3733 }
3734 Ok(chunks)
3735 })
3736 }
3737
3738 /// The raw `FilterPipeline` message bytes from this dataset's object header,
3739 /// if it has one. Repack reuses this verbatim so that every filter — including
3740 /// ones this crate cannot itself apply (ZFP, SZIP, unknown) — is reproduced
3741 /// byte-for-byte in the repacked file's pipeline message.
3742 pub(crate) fn filter_pipeline_message_bytes(&self) -> Option<Vec<u8>> {
3743 self.header
3744 .messages
3745 .iter()
3746 .find(|m| m.msg_type == MessageType::FilterPipeline)
3747 .map(|msg| msg.data.clone())
3748 }
3749
3750 /// Read the dataset's exact unfiltered element bytes.
3751 ///
3752 /// For compound datasets this preserves all file padding and uses the
3753 /// offsets reported by [`datatype`](Self::datatype).
3754 pub fn read_raw(&self) -> Result<Vec<u8>, Error> {
3755 let dt = self.datatype()?;
3756 let ds = self.dataspace()?;
3757 let dl = self.data_layout()?;
3758 // The data layout's on-disk addresses are left base-relative here;
3759 // `read_dataset_raw` applies the base address centrally (for both
3760 // contiguous and chunked layouts) by reading from a base-relative view of
3761 // the file.
3762 let pipeline = self.filter_pipeline_parsed();
3763 Ok(self
3764 .file
3765 .read_dataset_raw(&dl, &ds, &dt, pipeline.as_ref(), &self.chunk_cache)?)
3766 }
3767
3768 /// Read the raw element bytes of the row window `[start_row, start_row + num_rows)`
3769 /// — a range along the first dimension.
3770 ///
3771 /// The windowed companion to [`read_raw`](Self::read_raw): only the storage the
3772 /// window overlaps is read — a bounded sub-read for compact and contiguous
3773 /// layouts, just the overlapping chunks for chunked layouts — so peak memory
3774 /// scales with the window, not the dataset. Use it to stream a large dataset a
3775 /// fixed number of rows at a time.
3776 ///
3777 /// Each row keeps its full inner shape, and the bytes match what
3778 /// [`read_raw`](Self::read_raw) produces for those rows, so the typed
3779 /// `read_*_rows` helpers decode a window like their whole-dataset forms. The
3780 /// window is clamped to the first dimension: a read past the end returns only
3781 /// the rows that exist, and a 0-D scalar is one row. A window covering every
3782 /// row delegates to [`read_raw`](Self::read_raw), so a full-range window never
3783 /// costs more than a whole read. Variable-length string
3784 /// bytes are heap references, not text — use
3785 /// [`read_string_rows`](Self::read_string_rows).
3786 pub fn read_raw_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<u8>, Error> {
3787 let dt = self.datatype()?;
3788 let ds = self.dataspace()?;
3789 let dl = self.data_layout()?;
3790
3791 let n0 = ds.dimensions.first().copied().unwrap_or(1);
3792 let start = start_row.min(n0);
3793 let count = num_rows.min(n0 - start);
3794
3795 // A window covering every row is exactly a whole read: delegate, so it
3796 // never costs a window-shaped copy on top of one.
3797 if start == 0 && count == n0 {
3798 let pipeline = self.filter_pipeline_parsed();
3799 return Ok(self.file.read_dataset_raw(
3800 &dl,
3801 &ds,
3802 &dt,
3803 pipeline.as_ref(),
3804 &self.chunk_cache,
3805 )?);
3806 }
3807
3808 Ok(self.file.read_dataset_raw_rows(
3809 &dl,
3810 &ds,
3811 &dt,
3812 self.filter_pipeline_parsed().as_ref(),
3813 &self.chunk_cache,
3814 start,
3815 count,
3816 )?)
3817 }
3818
3819 /// Windowed [`read_f64`](Self::read_f64) — decodes only the row window.
3820 pub fn read_f64_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<f64>, Error> {
3821 let raw = self.read_raw_rows(start_row, num_rows)?;
3822 Ok(data_read::read_as_f64(&raw, &self.datatype()?)?)
3823 }
3824
3825 /// Windowed [`read_f32`](Self::read_f32) — decodes only the row window.
3826 pub fn read_f32_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<f32>, Error> {
3827 let raw = self.read_raw_rows(start_row, num_rows)?;
3828 Ok(data_read::read_as_f32(&raw, &self.datatype()?)?)
3829 }
3830
3831 /// Windowed [`read_i8`](Self::read_i8) — decodes only the row window.
3832 #[expect(
3833 clippy::cast_possible_wrap,
3834 reason = "read_i8 reinterprets each stored byte as the signed i8 the caller requested"
3835 )]
3836 pub fn read_i8_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<i8>, Error> {
3837 let raw = self.read_raw_rows(start_row, num_rows)?;
3838 Ok(raw.iter().map(|&b| b as i8).collect())
3839 }
3840
3841 /// Windowed [`read_i16`](Self::read_i16) — decodes only the row window.
3842 pub fn read_i16_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<i16>, Error> {
3843 let raw = self.read_raw_rows(start_row, num_rows)?;
3844 Ok(data_read::read_as_i16(&raw, &self.datatype()?)?)
3845 }
3846
3847 /// Windowed [`read_i32`](Self::read_i32) — decodes only the row window.
3848 pub fn read_i32_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<i32>, Error> {
3849 let raw = self.read_raw_rows(start_row, num_rows)?;
3850 Ok(data_read::read_as_i32(&raw, &self.datatype()?)?)
3851 }
3852
3853 /// Windowed [`read_i64`](Self::read_i64) — decodes only the row window.
3854 pub fn read_i64_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<i64>, Error> {
3855 let raw = self.read_raw_rows(start_row, num_rows)?;
3856 Ok(data_read::read_as_i64(&raw, &self.datatype()?)?)
3857 }
3858
3859 /// Windowed [`read_u8`](Self::read_u8) — reads only the row window.
3860 pub fn read_u8_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<u8>, Error> {
3861 self.read_raw_rows(start_row, num_rows)
3862 }
3863
3864 /// Windowed [`read_u16`](Self::read_u16) — decodes only the row window.
3865 pub fn read_u16_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<u16>, Error> {
3866 let raw = self.read_raw_rows(start_row, num_rows)?;
3867 Ok(data_read::read_as_u16(&raw, &self.datatype()?)?)
3868 }
3869
3870 /// Windowed [`read_u32`](Self::read_u32) — decodes only the row window.
3871 pub fn read_u32_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<u32>, Error> {
3872 let raw = self.read_raw_rows(start_row, num_rows)?;
3873 Ok(data_read::read_as_u32(&raw, &self.datatype()?)?)
3874 }
3875
3876 /// Windowed [`read_u64`](Self::read_u64) — decodes only the row window.
3877 pub fn read_u64_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<u64>, Error> {
3878 let raw = self.read_raw_rows(start_row, num_rows)?;
3879 Ok(data_read::read_as_u64(&raw, &self.datatype()?)?)
3880 }
3881
3882 /// Windowed [`read_string`](Self::read_string).
3883 ///
3884 /// Fixed-length strings decode straight from the window. Variable-length
3885 /// strings resolve only the window's heap references, so the window memory
3886 /// bound holds for them too: peak allocation is the window's references,
3887 /// its text, and the metadata of the heap collections it touches.
3888 pub fn read_string_rows(&self, start_row: u64, num_rows: u64) -> Result<Vec<String>, Error> {
3889 let dt = self.datatype()?;
3890 if vl_data::is_vlen_string_datatype(&dt) {
3891 // The window's heap references, read memory-bounded like any other
3892 // fixed-size element (4-byte length + collection address + 4-byte
3893 // object index), one row spanning its inner dimensions. Resolving
3894 // only those against the global heap keeps the bound — the same
3895 // resolution `read_string` runs over the whole dataset's references.
3896 let raw = self.read_raw_rows(start_row, num_rows)?;
3897 let ref_size = 4 + self.file.offset_size() as usize + 4;
3898 let num_elements = (raw.len() / ref_size) as u64;
3899 let mut strings = Vec::new();
3900 self.file.with_source(|source| -> Result<(), Error> {
3901 Ok(vl_data::visit_vl_strings_from_source(
3902 source,
3903 &raw,
3904 num_elements,
3905 self.file.offset_size(),
3906 self.file.length_size(),
3907 self.file.addr_offset,
3908 VlenStringReadOptions::default(),
3909 |string| strings.push(String::from(string)),
3910 )?)
3911 })?;
3912 return Ok(strings);
3913 }
3914 let raw = self.read_raw_rows(start_row, num_rows)?;
3915 Ok(data_read::read_as_strings(&raw, &dt)?)
3916 }
3917
3918 /// Interpret this dataset as an array of HDF5 object references
3919 /// (`H5R_OBJECT`) and resolve each, in storage order, to the [`Object`] it
3920 /// points at.
3921 ///
3922 /// MATLAB cell arrays and the `#subsystem#` machinery store their members
3923 /// this way: the dataset holds one object-header address per element, each
3924 /// naming an object elsewhere in the file (conventionally under the hidden
3925 /// `#refs#` group).
3926 ///
3927 /// # Errors
3928 ///
3929 /// - [`FormatError::TypeMismatch`] if this dataset's datatype is not an
3930 /// object reference.
3931 /// - [`FormatError::InvalidObjectReference`] if an element is a null or
3932 /// undefined reference, or does not point at a group or dataset.
3933 pub fn dereference(&self) -> Result<Vec<Object>, Error> {
3934 let dt = self.datatype()?;
3935 if !matches!(
3936 dt,
3937 Datatype::Reference {
3938 ref_type: ReferenceType::Object,
3939 ..
3940 }
3941 ) {
3942 return Err(FormatError::TypeMismatch {
3943 expected: "object reference",
3944 actual: "non-reference datatype",
3945 }
3946 .into());
3947 }
3948 // An object reference stores an 8-byte object-header address. Refuse a
3949 // sub-address-width element rather than read a truncated address.
3950 let elem_size = dt.type_size().to_usize()?;
3951 if elem_size < 8 {
3952 return Err(FormatError::TypeMismatch {
3953 expected: "8-byte object reference",
3954 actual: "object reference narrower than 8 bytes",
3955 }
3956 .into());
3957 }
3958 let raw = self.read_raw()?;
3959 if raw.is_empty() {
3960 return Ok(Vec::new());
3961 }
3962 if !raw.len().is_multiple_of(elem_size) {
3963 return Err(FormatError::DataSizeMismatch {
3964 expected: elem_size,
3965 actual: raw.len(),
3966 }
3967 .into());
3968 }
3969 let mut out = Vec::with_capacity(raw.len() / elem_size);
3970 for chunk in raw.chunks_exact(elem_size) {
3971 let addr = u64::from_le_bytes(chunk[..8].try_into().expect("chunk has >= 8 bytes"));
3972 out.push(FileInner::object_at_relative(&self.file, addr)?);
3973 }
3974 Ok(out)
3975 }
3976
3977 /// Decode all elements of a compound dataset field by field.
3978 ///
3979 /// Built-in implementations support numeric tuples with one through twelve
3980 /// fields. Decoding uses the file's field offsets rather than Rust's tuple
3981 /// memory layout, so padded compound records are supported safely.
3982 pub fn read_compound<T: CompoundType>(&self) -> Result<Vec<T>, Error> {
3983 let datatype = self.datatype()?;
3984 let element_size = datatype.type_size().to_usize()?;
3985 if !matches!(datatype, Datatype::Compound { .. }) {
3986 return Err(FormatError::TypeMismatch {
3987 expected: "Compound",
3988 actual: "non-Compound",
3989 }
3990 .into());
3991 }
3992 let raw = self.read_raw()?;
3993 if element_size == 0 || !raw.len().is_multiple_of(element_size) {
3994 return Err(FormatError::DataSizeMismatch {
3995 expected: element_size,
3996 actual: raw.len(),
3997 }
3998 .into());
3999 }
4000 raw.chunks_exact(element_size)
4001 .map(|bytes| T::decode(&datatype, bytes).map_err(Error::from))
4002 .collect()
4003 }
4004
4005 /// Verify this dataset against its stored provenance hash.
4006 ///
4007 /// Recomputes the SHA-256 of the dataset's raw bytes and compares it with
4008 /// the `_provenance_sha256` attribute written by
4009 /// [`DatasetBuilder::with_provenance`](crate::DatasetBuilder::with_provenance).
4010 /// Returns [`VerifyResult::NoHash`](crate::VerifyResult::NoHash) when the
4011 /// dataset carries no provenance hash, so a missing hash is distinguishable
4012 /// from an actual mismatch.
4013 #[cfg(feature = "provenance")]
4014 pub fn verify_provenance(&self) -> Result<crate::provenance::VerifyResult, Error> {
4015 use crate::provenance::{ATTR_SHA256, VerifyResult, sha256_hex};
4016
4017 let attrs = self.attrs()?;
4018 let stored = match attrs.get(ATTR_SHA256).and_then(AttrValue::as_str) {
4019 Some(s) => s.trim_end_matches('\0').to_string(),
4020 None => return Ok(VerifyResult::NoHash),
4021 };
4022
4023 let computed = sha256_hex(&self.read_raw()?);
4024 if computed == stored {
4025 Ok(VerifyResult::Ok)
4026 } else {
4027 Ok(VerifyResult::Mismatch { stored, computed })
4028 }
4029 }
4030}
4031
4032// ---------------------------------------------------------------------------
4033// Helpers
4034// ---------------------------------------------------------------------------
4035
4036fn find_message(
4037 header: &ObjectHeader,
4038 msg_type: MessageType,
4039) -> Result<&crate::object_header::HeaderMessage, Error> {
4040 header
4041 .messages
4042 .iter()
4043 .find(|m| m.msg_type == msg_type)
4044 .ok_or(Error::MissingMessage(msg_type))
4045}
4046
4047/// Normalize a user-supplied object path to the root-relative form the write
4048/// session addresses by: strip any leading/trailing `/` so `"/a/b"` and `"a/b"`
4049/// name the same object.
4050fn normalize_path(path: &str) -> String {
4051 path.trim_matches('/').to_string()
4052}
4053
4054fn has_message(header: &ObjectHeader, msg_type: MessageType) -> bool {
4055 header.messages.iter().any(|m| m.msg_type == msg_type)
4056}
4057
4058fn is_group(header: &ObjectHeader) -> bool {
4059 header.messages.iter().any(|m| {
4060 m.msg_type == MessageType::LinkInfo
4061 || m.msg_type == MessageType::Link
4062 || m.msg_type == MessageType::SymbolTable
4063 })
4064}
4065
4066#[cfg(test)]
4067mod tests {
4068 use super::*;
4069 use crate::FileBuilder;
4070
4071 /// Read everything a read-write file can serve through the paired read
4072 /// paths, as comparable text.
4073 ///
4074 /// Each entry exercises a different `with_engine` call site: path
4075 /// resolution, object-header parsing, group listing, attribute reads (both
4076 /// the compact and the dense form), a whole-dataset read, and a row-range
4077 /// read. Errors are formatted rather than unwrapped so that a *divergence in
4078 /// which error* is reported also fails the comparison.
4079 fn read_everything(file: &File) -> Vec<String> {
4080 let mut out = Vec::new();
4081 out.push(format!("root groups: {:?}", file.root().groups()));
4082 out.push(format!("root datasets: {:?}", file.root().datasets()));
4083 out.push(format!("root attrs: {:?}", sorted(file.root().attrs())));
4084
4085 for path in ["plain", "g/nested", "many_attrs", "missing", "g/missing"] {
4086 match file.dataset(path) {
4087 Ok(ds) => {
4088 out.push(format!("{path}: shape {:?}", ds.shape()));
4089 out.push(format!("{path}: attrs {:?}", sorted(ds.attrs())));
4090 out.push(format!("{path}: all {:?}", ds.read_i32()));
4091 out.push(format!("{path}: rows {:?}", ds.read_i32_rows(1, 2)));
4092 out.push(format!("{path}: raw rows {:?}", ds.read_raw_rows(0, 1)));
4093 }
4094 Err(e) => out.push(format!("{path}: error {e}")),
4095 }
4096 }
4097 out
4098 }
4099
4100 /// Attribute maps compare only after ordering; `HashMap`'s `Debug` is not
4101 /// deterministic, and an ordering difference here would be noise rather
4102 /// than the divergence this is looking for.
4103 fn sorted(attrs: Result<HashMap<String, AttrValue>, Error>) -> Vec<String> {
4104 match attrs {
4105 Ok(map) => {
4106 let mut v: Vec<String> =
4107 map.iter().map(|(k, val)| format!("{k}={val:?}")).collect();
4108 v.sort();
4109 v
4110 }
4111 Err(e) => vec![format!("error {e}")],
4112 }
4113 }
4114
4115 /// Drive `bytes` down both forms of every read a read-write file serves and
4116 /// require identical answers.
4117 ///
4118 /// Every read has a slice form (walking the whole-file mirror) and a
4119 /// `Source` form, and until a mirrorless backing lands (issue #198) only the
4120 /// slice form ever runs. This makes the other form reachable now, so it
4121 /// cannot quietly drift as its twin is edited.
4122 fn assert_both_read_paths_agree(bytes: &[u8], what: &str) {
4123 let dir = tempfile::tempdir().unwrap();
4124 let path = dir.path().join("both.h5");
4125 std::fs::write(&path, bytes).unwrap();
4126
4127 // One session at a time: `open_rw` takes an exclusive lock, and holding
4128 // two over one path fails outright where OS locks are mandatory.
4129 let via_mirror = {
4130 let f = File::open_rw(&path).unwrap();
4131 read_everything(&f)
4132 };
4133 let via_source = {
4134 let f = File::open_rw_source_only(&path).unwrap();
4135 read_everything(&f)
4136 };
4137
4138 assert_eq!(
4139 via_mirror.len(),
4140 via_source.len(),
4141 "{what}: the two read paths produced different numbers of results"
4142 );
4143 for (m, s) in via_mirror.iter().zip(&via_source) {
4144 assert_eq!(m, s, "{what}: slice and Source read paths disagree");
4145 }
4146 // Guard the guard: a helper that read nothing would make the comparison
4147 // vacuous, and a file whose datasets all failed to open would too.
4148 assert!(
4149 via_mirror.iter().any(|r| r.contains("all Ok(")),
4150 "{what}: no dataset read succeeded, so this compared nothing"
4151 );
4152 }
4153
4154 /// A file exercising each paired read: a plain dataset, a nested one behind
4155 /// a group (path resolution), and one carrying enough attributes to force
4156 /// the dense (fractal-heap) attribute layout rather than compact messages.
4157 fn both_paths_file_bytes(userblock: Option<u64>) -> Vec<u8> {
4158 let mut b = FileBuilder::new();
4159 if let Some(ub) = userblock {
4160 b.with_userblock(ub);
4161 }
4162 b.create_dataset("plain")
4163 .with_i32_data(&(0..24).collect::<Vec<i32>>())
4164 .with_shape(&[6, 4])
4165 .set_attr("units", AttrValue::String("m".into()));
4166 // Well past the eight-attribute compact limit, so the header converts to
4167 // the dense layout and the dense extraction path is the one that runs.
4168 {
4169 let ds = b
4170 .create_dataset("many_attrs")
4171 .with_i32_data(&(0..8).collect::<Vec<i32>>());
4172 for i in 0..24 {
4173 ds.set_attr(&format!("attr_{i:02}"), AttrValue::I64(i));
4174 }
4175 }
4176 let mut g = b.create_group("g");
4177 g.create_dataset("nested")
4178 .with_i32_data(&(100..112).collect::<Vec<i32>>())
4179 .with_shape(&[3, 4]);
4180 b.add_group(g.finish());
4181 b.finish().unwrap()
4182 }
4183
4184 #[test]
4185 fn both_read_paths_agree() {
4186 assert_both_read_paths_agree(&both_paths_file_bytes(None), "no userblock");
4187 }
4188
4189 /// The userblock case is the one where the two forms are built differently:
4190 /// the slice form reframes by slicing at the base address, the `Source` form
4191 /// wraps in a `BaseOffsetSource`. A file with a nonzero base is the only way
4192 /// to compare them.
4193 #[test]
4194 fn both_read_paths_agree_with_a_userblock() {
4195 assert_both_read_paths_agree(&both_paths_file_bytes(Some(512)), "512-byte userblock");
4196 }
4197
4198 /// One 256-element i32 dataset, chunked into 32-element chunks, in memory.
4199 fn chunked_file_bytes() -> Vec<u8> {
4200 let data: Vec<i32> = (0..256).collect();
4201 let mut b = FileBuilder::new();
4202 b.create_dataset("chunked")
4203 .with_i32_data(&data)
4204 .with_shape(&[256])
4205 .with_chunks(&[32]);
4206 b.finish().unwrap()
4207 }
4208
4209 // The DAPL override must drive the *live* `ChunkCache`, not merely the value
4210 // reported by `chunk_cache_config()`. These assertions reach the crate's
4211 // `#[cfg(test)]` cache introspection (unavailable to integration tests), so
4212 // they fail if the resolved config ever stops flowing into the real cache.
4213
4214 #[test]
4215 fn enabled_override_populates_live_cache_over_disabled_file_default() {
4216 let file = File::from_bytes_with_options(
4217 chunked_file_bytes(),
4218 FileAccessProperties::new().with_chunk_cache(ChunkCacheConfig::disabled()),
4219 )
4220 .unwrap();
4221
4222 let ds = file
4223 .dataset_with_options(
4224 "chunked",
4225 DatasetAccessProperties::new().with_chunk_cache(ChunkCacheConfig::new()),
4226 )
4227 .unwrap();
4228 assert_eq!(ds.read_i32().unwrap(), (0..256).collect::<Vec<i32>>());
4229
4230 // The enabled override built the chunk index and retained chunks; the
4231 // disabled file default would have left both empty.
4232 assert!(ds.chunk_cache_stats().index_loaded());
4233 assert!(ds.chunk_cache_stats().cached_chunks() > 0);
4234 }
4235
4236 #[test]
4237 fn disabled_override_suppresses_live_cache_over_enabled_file_default() {
4238 let file = File::from_bytes_with_options(
4239 chunked_file_bytes(),
4240 FileAccessProperties::new().with_chunk_cache(ChunkCacheConfig::new()),
4241 )
4242 .unwrap();
4243
4244 let ds = file
4245 .dataset_with_options(
4246 "chunked",
4247 DatasetAccessProperties::new().with_chunk_cache(ChunkCacheConfig::disabled()),
4248 )
4249 .unwrap();
4250 assert_eq!(ds.read_i32().unwrap(), (0..256).collect::<Vec<i32>>());
4251
4252 // The disabled override suppressed the index and chunk retention; the
4253 // enabled file default would have populated both.
4254 assert!(!ds.chunk_cache_stats().index_loaded());
4255 assert_eq!(ds.chunk_cache_stats().cached_chunks(), 0);
4256 }
4257
4258 /// A group child whose stored (base-relative) object-header address overflows
4259 /// `u64` once the base address is added must be rejected, not wrapped or
4260 /// panicked on. Reaching this needs a nonzero base address, so the file
4261 /// carries a userblock; the child link's stored address is then rewritten to
4262 /// `HADDR_UNDEF` (all ones) so `group_children`'s normalization overflows.
4263 #[test]
4264 fn group_child_address_base_overflow_is_rejected() {
4265 const UB: u64 = 512;
4266 let mut b = FileBuilder::new();
4267 b.with_userblock(UB);
4268 let mut child = b.create_group("child");
4269 child.create_dataset("inner").with_i32_data(&[1, 2, 3]);
4270 b.add_group(child.finish());
4271 let mut bytes = b.finish().unwrap();
4272
4273 // Baseline: the file reads and the subgroup is listed.
4274 let file = File::from_bytes(bytes.clone()).unwrap();
4275 assert_eq!(file.root().groups().unwrap(), vec!["child".to_string()]);
4276
4277 // Rewrite the child's stored object-header address to HADDR_UNDEF. It is
4278 // stored base-relative (absolute minus the userblock base) and, for this
4279 // single-child file, appears exactly once in the bytes. The link lives in
4280 // the root object header's chunk-0.
4281 let stored = file.root().group("child").unwrap().address - UB;
4282 let needle = stored.to_le_bytes();
4283 let matches: Vec<usize> = bytes
4284 .windows(8)
4285 .enumerate()
4286 .filter(|(_, w)| *w == needle)
4287 .map(|(i, _)| i)
4288 .collect();
4289 assert_eq!(
4290 matches.len(),
4291 1,
4292 "stored child address {stored:#x} was not uniquely locatable: {matches:?}"
4293 );
4294 bytes[matches[0]..matches[0] + 8].copy_from_slice(&u64::MAX.to_le_bytes());
4295
4296 // The v2 object header is checksum-protected, so a real crafted file would
4297 // carry a matching checksum; recompute the root header's over the edited
4298 // bytes so parsing reaches the address normalization rather than failing on
4299 // the checksum first. Mirrors the chunk-0 extent from `parse_v2`.
4300 #[cfg(feature = "checksum")]
4301 {
4302 let root_addr = file.root().address as usize;
4303 assert_eq!(&bytes[root_addr..root_addr + 4], b"OHDR");
4304 let flags = bytes[root_addr + 5];
4305 let mut pos = root_addr + 6;
4306 if flags & 0x20 != 0 {
4307 pos += 16;
4308 }
4309 if flags & 0x10 != 0 {
4310 pos += 4;
4311 }
4312 let width = 1usize << (flags & 0x03);
4313 let chunk0 = (0..width).fold(0usize, |acc, i| {
4314 acc | ((bytes[pos + i] as usize) << (8 * i))
4315 });
4316 pos += width;
4317 let chunk0_end = pos + chunk0;
4318 assert!(
4319 matches[0] < chunk0_end,
4320 "patched link address is outside the root header's chunk-0"
4321 );
4322 let cs = crate::checksum::jenkins_lookup3(&bytes[root_addr..chunk0_end]);
4323 bytes[chunk0_end..chunk0_end + 4].copy_from_slice(&cs.to_le_bytes());
4324 }
4325
4326 // Iterating the root now normalizes `u64::MAX + base` and must surface the
4327 // overflow as a format error rather than panicking or wrapping.
4328 let file = File::from_bytes(bytes).unwrap();
4329 match file.root().groups() {
4330 Err(Error::Format(FormatError::OffsetOverflow { offset, length })) => {
4331 assert_eq!(offset, u64::MAX);
4332 assert_eq!(length, UB);
4333 }
4334 other => panic!("expected group-child address overflow, got {other:?}"),
4335 }
4336 }
4337
4338 /// A zero-row window returns `Ok(empty)` uniformly, even over an unallocated
4339 /// contiguous dataset where the whole-dataset reader errors with
4340 /// `NoDataAllocated`. Without the early return in `read_rows_framed`, the
4341 /// contiguous arm's `address.ok_or(NoDataAllocated)?` would error here, while
4342 /// the chunked arm returns `Ok(empty)` — the cross-layout divergence this
4343 /// guards against.
4344 #[test]
4345 fn read_rows_framed_zero_row_window_is_ok_even_when_unallocated() {
4346 let dl = DataLayout::Contiguous {
4347 address: None,
4348 size: 0,
4349 };
4350 let ds = Dataspace {
4351 space_type: crate::dataspace::DataspaceType::Simple,
4352 rank: 1,
4353 dimensions: vec![0],
4354 max_dimensions: None,
4355 };
4356 let dt = Datatype::FixedPoint {
4357 size: 8,
4358 byte_order: crate::datatype::DatatypeByteOrder::LittleEndian,
4359 signed: false,
4360 bit_offset: 0,
4361 bit_precision: 64,
4362 };
4363 let cache = ChunkCache::new();
4364 let out = read_rows_framed(
4365 &BytesSource::new(b""),
4366 &dl,
4367 &ds,
4368 &dt,
4369 None,
4370 8,
4371 8,
4372 &cache,
4373 0,
4374 0,
4375 8,
4376 )
4377 .expect("a zero-row window must be Ok(empty), not NoDataAllocated");
4378 assert!(out.is_empty());
4379
4380 // A Virtual layout is unsupported and must still error for a zero-row
4381 // window, matching `read_raw`, rather than being swallowed by the early
4382 // return.
4383 let virtual_dl = DataLayout::Virtual { version: 4 };
4384 let err = read_rows_framed(
4385 &BytesSource::new(b""),
4386 &virtual_dl,
4387 &ds,
4388 &dt,
4389 None,
4390 8,
4391 8,
4392 &cache,
4393 0,
4394 0,
4395 8,
4396 )
4397 .expect_err("a virtual layout must error even for a zero-row window");
4398 assert!(
4399 matches!(err, FormatError::UnsupportedVirtualLayout),
4400 "expected UnsupportedVirtualLayout, got {err:?}"
4401 );
4402 }
4403}