Skip to main content

rust_hdf5/
file.rs

1//! HDF5 file handle — the main entry point for the public API.
2//!
3//! ```no_run
4//! use rust_hdf5::H5File;
5//!
6//! // Write
7//! let file = H5File::create("example.h5").unwrap();
8//! let ds = file.new_dataset::<u8>().shape(&[10, 20]).create("data").unwrap();
9//! ds.write_raw(&vec![0u8; 200]).unwrap();
10//! drop(file);
11//!
12//! // Read
13//! let file = H5File::open("example.h5").unwrap();
14//! let ds = file.dataset("data").unwrap();
15//! let data = ds.read_raw::<u8>().unwrap();
16//! assert_eq!(data.len(), 200);
17//! ```
18
19use std::path::Path;
20
21use crate::format::messages::superblock_ext::FileSpaceStrategy;
22use crate::io::locking::FileLocking;
23use crate::io::reader::SuperblockExtension;
24use crate::io::writer::{FileSpaceConfig, SharedMessageConfig};
25use crate::io::{Hdf5Reader, Hdf5Writer};
26
27use crate::dataset::{DatasetAccess, DatasetBuilder, H5Dataset};
28use crate::error::{Hdf5Error, Result};
29use crate::format::messages::datatype::{DatatypeMessage, DatatypeNodeVersion};
30use crate::format::messages::filter::FilterPipeline;
31use crate::format::messages::shared::MessageStorage;
32use crate::format::LibverBound;
33use crate::group::H5Group;
34use crate::types::H5Type;
35
36// ---------------------------------------------------------------------------
37// Thread-safety: choose between Rc<RefCell<>> and Arc<Mutex<>> based on
38// the `threadsafe` feature flag.
39// ---------------------------------------------------------------------------
40
41#[cfg(not(feature = "threadsafe"))]
42pub(crate) type SharedInner = std::rc::Rc<std::cell::RefCell<H5FileInner>>;
43
44#[cfg(feature = "threadsafe")]
45pub(crate) type SharedInner = std::sync::Arc<std::sync::RwLock<H5FileInner>>;
46
47/// Helper to borrow/lock the inner state immutably.
48#[cfg(not(feature = "threadsafe"))]
49pub(crate) fn borrow_inner(inner: &SharedInner) -> std::cell::Ref<'_, H5FileInner> {
50    inner.borrow()
51}
52
53/// Helper to borrow/lock the inner state mutably.
54#[cfg(not(feature = "threadsafe"))]
55pub(crate) fn borrow_inner_mut(inner: &SharedInner) -> std::cell::RefMut<'_, H5FileInner> {
56    inner.borrow_mut()
57}
58
59/// [`borrow_inner_mut`] where failing to get the lock must not panic.
60///
61/// Only [`H5Dataset`]'s drop uses this: a `Drop` that panics while another
62/// panic unwinds aborts the process, and the one thing it does with the lock
63/// — releasing cross-file handles a closed virtual dataset was holding — is
64/// re-run by the next handle drop or extent resolution.
65#[cfg(not(feature = "threadsafe"))]
66pub(crate) fn try_borrow_inner_mut(
67    inner: &SharedInner,
68) -> Option<std::cell::RefMut<'_, H5FileInner>> {
69    inner.try_borrow_mut().ok()
70}
71
72/// Helper to clone a SharedInner.
73#[cfg(not(feature = "threadsafe"))]
74pub(crate) fn clone_inner(inner: &SharedInner) -> SharedInner {
75    std::rc::Rc::clone(inner)
76}
77
78/// Helper to wrap an H5FileInner in SharedInner.
79#[cfg(not(feature = "threadsafe"))]
80pub(crate) fn new_shared(inner: H5FileInner) -> SharedInner {
81    std::rc::Rc::new(std::cell::RefCell::new(inner))
82}
83
84/// Whether two handles share one file — the same inner state, not merely
85/// equal state.
86#[cfg(not(feature = "threadsafe"))]
87pub(crate) fn same_inner(a: &SharedInner, b: &SharedInner) -> bool {
88    std::rc::Rc::ptr_eq(a, b)
89}
90
91/// Acquire a shared (read) lock on the inner state. The fine-grained writer
92/// (atomic allocator, positioned handle, per-dataset `Slot` mutexes) is safe to
93/// drive through `&H5FileInner`, so the non-extending chunk-write path takes
94/// this read guard and lets writes to different datasets proceed concurrently.
95#[cfg(feature = "threadsafe")]
96pub(crate) fn borrow_inner(inner: &SharedInner) -> std::sync::RwLockReadGuard<'_, H5FileInner> {
97    inner.read().unwrap()
98}
99
100/// Acquire an exclusive (write) lock on the inner state. Required by paths that
101/// mutate shared writer state directly — create, extend/`set_extent`, append,
102/// metadata, finalize/close.
103#[cfg(feature = "threadsafe")]
104pub(crate) fn borrow_inner_mut(
105    inner: &SharedInner,
106) -> std::sync::RwLockWriteGuard<'_, H5FileInner> {
107    inner.write().unwrap()
108}
109
110/// [`borrow_inner_mut`] where failing to get the lock must not panic; see the
111/// single-threaded twin for why.
112#[cfg(feature = "threadsafe")]
113pub(crate) fn try_borrow_inner_mut(
114    inner: &SharedInner,
115) -> Option<std::sync::RwLockWriteGuard<'_, H5FileInner>> {
116    inner.write().ok()
117}
118
119/// Whether two handles share one file — the same inner state, not merely
120/// equal state.
121#[cfg(feature = "threadsafe")]
122pub(crate) fn same_inner(a: &SharedInner, b: &SharedInner) -> bool {
123    std::sync::Arc::ptr_eq(a, b)
124}
125
126#[cfg(feature = "threadsafe")]
127pub(crate) fn clone_inner(inner: &SharedInner) -> SharedInner {
128    std::sync::Arc::clone(inner)
129}
130
131#[cfg(feature = "threadsafe")]
132pub(crate) fn new_shared(inner: H5FileInner) -> SharedInner {
133    std::sync::Arc::new(std::sync::RwLock::new(inner))
134}
135
136/// The inner state of an HDF5 file, shared with datasets via reference counting.
137///
138/// By default, this uses `Rc<RefCell<>>` for zero-overhead single-threaded use.
139/// Enable the `threadsafe` feature to use `Arc<Mutex<>>` instead, making
140/// `H5File` `Send + Sync`.
141pub(crate) enum H5FileInner {
142    // Boxed: `Hdf5Writer` and `Hdf5Reader` are both far larger than the
143    // zero-sized `Closed` sentinel, and inlining either here would size
144    // every `H5FileInner` to that variant's footprint regardless of which
145    // mode a given file is actually in.
146    Writer(Box<Hdf5Writer>),
147    Reader(Box<Hdf5Reader>),
148    /// Sentinel value used during `close()` to take ownership of the writer.
149    Closed,
150}
151
152/// An HDF5 file opened for reading or writing.
153///
154/// Datasets created from this file hold a shared reference to the underlying
155/// I/O handle, so the file does not need to outlive its datasets (they share
156/// ownership via reference counting).
157pub struct H5File {
158    pub(crate) inner: SharedInner,
159}
160
161impl H5File {
162    /// Create a new HDF5 file at `path`. Truncates if the file already exists.
163    pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
164        let writer = Hdf5Writer::create(path.as_ref())?;
165        Ok(Self {
166            inner: new_shared(H5FileInner::Writer(Box::new(writer))),
167        })
168    }
169
170    /// Open an existing HDF5 file for reading.
171    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
172        let reader = Hdf5Reader::open(path.as_ref())?;
173        Ok(Self {
174            inner: new_shared(H5FileInner::Reader(Box::new(reader))),
175        })
176    }
177
178    /// Open an existing HDF5 file for appending new datasets.
179    ///
180    /// Existing datasets are preserved. New datasets can be added and will
181    /// be written after the current end of file. Existing chunked datasets
182    /// can be extended with `write_chunk` and `extend_dataset`.
183    ///
184    /// ```no_run
185    /// use rust_hdf5::H5File;
186    /// let file = H5File::open_rw("existing.h5").unwrap();
187    /// let ds = file.new_dataset::<f64>().shape(&[100]).create("new_data").unwrap();
188    /// ds.write_raw(&vec![0.0f64; 100]).unwrap();
189    /// file.close().unwrap();
190    /// ```
191    pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
192        let writer = Hdf5Writer::open_append(path.as_ref())?;
193        Ok(Self {
194            inner: new_shared(H5FileInner::Writer(Box::new(writer))),
195        })
196    }
197
198    /// Start building open options for an HDF5 file.
199    ///
200    /// Use this to control file-locking behavior explicitly:
201    ///
202    /// ```no_run
203    /// use rust_hdf5::{H5File, FileLocking};
204    /// // Open with locking disabled (e.g. on NFS without lock support).
205    /// let file = H5File::options()
206    ///     .locking(FileLocking::Disabled)
207    ///     .open_rw("existing.h5")
208    ///     .unwrap();
209    /// # let _ = file;
210    /// ```
211    pub fn options() -> H5FileOptions {
212        H5FileOptions::default()
213    }
214
215    /// Opt in to the latest file format for datasets created after this call —
216    /// the equivalent of libhdf5's `H5Pset_libver_bounds(low = H5F_LIBVER_V200)`.
217    ///
218    /// With `latest` set, filtered chunked datasets get a version-5 data layout
219    /// message, whose chunk indexes store on-disk chunk sizes in fixed-width
220    /// (`sizeof_size`, i.e. 8-byte) fields instead of fields sized from the
221    /// uncompressed chunk size. That removes the overflow risk when a filter
222    /// *expands* a chunk, but the file is only readable by libhdf5 ≥ 2.0
223    /// (h5py bundling hdf5 1.14 rejects it with "bad version number").
224    ///
225    /// It also sets the file's library-version low bound, and so its
226    /// superblock version: version 3, where a file this crate writes without
227    /// it is version 2 (or 3 anyway, once it holds a chunked dataset).
228    ///
229    /// Off by default; the data layout of unfiltered and contiguous datasets
230    /// is unaffected. Independent of this setting, a chunk larger than 4 GiB
231    /// forces version 5 because version 4 cannot represent its size field,
232    /// matching libhdf5.
233    ///
234    /// `false` is not "back to the default": it is
235    /// [`LibverBound::Earliest`], the opposite end of the same table, where
236    /// the data layout message is version 3 and chunked datasets created
237    /// after the call go on the version-1 B-tree. A file that has never been
238    /// told a bound is the one at the crate default.
239    ///
240    /// Errors in read mode.
241    pub fn set_libver_latest(&self, latest: bool) -> Result<()> {
242        let mut inner = borrow_inner_mut(&self.inner);
243        match &mut *inner {
244            H5FileInner::Writer(writer) => {
245                writer.set_libver_latest(latest)?;
246                Ok(())
247            }
248            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
249        }
250    }
251
252    /// Set the file's low libver bound — `H5Pset_libver_bounds`'s `low`
253    /// argument, the oldest libhdf5 release the file must stay readable by.
254    ///
255    /// Objects created after this call encode their messages at the versions
256    /// that bound calls for: a compound, enum or array datatype message moves
257    /// to version 3 at [`LibverBound::V18`] and version 4 at
258    /// [`LibverBound::V112`], the way `H5T_set_version` upgrades a datatype,
259    /// while an integer or string message stays at version 1 in every file.
260    /// [`LibverBound::V200`] additionally selects the version-5 data layout
261    /// for filtered chunked datasets, as [`Self::set_libver_latest`] does.
262    ///
263    /// The bound also picks the chunk index, through the data layout message
264    /// version `H5O_layout_ver_bounds` gives it: below [`LibverBound::V110`]
265    /// that version is 3, which has no index-type field, so a chunked dataset
266    /// created after this call is indexed by the version-1 B-tree rather than
267    /// by the v1.10 index its shape would otherwise select. Datasets already
268    /// created keep the index they were made with, exactly as libhdf5 keeps
269    /// what a dataset's creation property list settled.
270    ///
271    /// Naming a bound is not the same as leaving it unset: a file created
272    /// through [`H5File::create`] names none and uses the v1.10 indexes.
273    ///
274    /// Errors in read mode.
275    pub fn set_libver_bound(&self, libver: LibverBound) -> Result<()> {
276        let mut inner = borrow_inner_mut(&self.inner);
277        match &mut *inner {
278            H5FileInner::Writer(writer) => {
279                writer.set_libver_bound(libver)?;
280                Ok(())
281            }
282            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
283        }
284    }
285
286    /// Record creation order for the links and the attributes of every
287    /// object created after this call — the equivalent of h5py's
288    /// `h5py.get_config().track_order = True`, i.e. `H5Pset_link_creation_order`
289    /// and `H5Pset_attr_creation_order` set to
290    /// `H5P_CRT_ORDER_TRACKED | H5P_CRT_ORDER_INDEXED` on the creation
291    /// property lists those objects are made with.
292    ///
293    /// Creation-order tracking belongs to the object, so groups and datasets
294    /// made before this call keep the policy they were made under — the same
295    /// split h5py has between its global config and each object's property
296    /// list. The root group is created with the file; configure it with
297    /// [`H5FileOptions::track_order`], h5py's `File(..., track_order=True)`.
298    ///
299    /// Off by default. Errors in read mode.
300    pub fn set_track_order(&self, track: bool) -> Result<()> {
301        let mut inner = borrow_inner_mut(&self.inner);
302        match &mut *inner {
303            H5FileInner::Writer(writer) => {
304                writer.set_track_order(track);
305                Ok(())
306            }
307            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
308        }
309    }
310
311    /// Record the times of every object created after this call —
312    /// `H5Pset_obj_track_times` on the creation property lists those objects
313    /// are made with, h5py's `track_times=` argument to `create_dataset` and
314    /// `create_group`.
315    ///
316    /// Off by default; see [`H5FileOptions::track_times`] for why that is
317    /// h5py's answer and not libhdf5's. Like creation-order tracking it
318    /// belongs to the object, so objects made before this call keep the policy
319    /// they were made under, and the root group takes its own from
320    /// [`H5FileOptions::track_times`].
321    ///
322    /// Errors in read mode.
323    pub fn set_track_times(&self, track: bool) -> Result<()> {
324        let mut inner = borrow_inner_mut(&self.inner);
325        match &mut *inner {
326            H5FileInner::Writer(writer) => {
327                writer.set_track_times(track);
328                Ok(())
329            }
330            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
331        }
332    }
333
334    /// Return a handle to the root group.
335    ///
336    /// The root group can be used to create datasets and sub-groups.
337    pub fn root_group(&self) -> H5Group {
338        H5Group::new(clone_inner(&self.inner), "/".to_string())
339    }
340
341    /// Create a group in the root of the file.
342    ///
343    /// ```no_run
344    /// use rust_hdf5::H5File;
345    /// let file = H5File::create("groups.h5").unwrap();
346    /// let grp = file.create_group("detector").unwrap();
347    /// ```
348    pub fn create_group(&self, name: &str) -> Result<H5Group> {
349        self.root_group().create_group(name)
350    }
351
352    /// Create a soft link in the root of the file.
353    ///
354    /// See [`H5Group::create_soft_link`](crate::group::H5Group::create_soft_link).
355    ///
356    /// ```no_run
357    /// use rust_hdf5::H5File;
358    /// let file = H5File::create("soft.h5").unwrap();
359    /// file.new_dataset::<i32>().shape([8]).create("orig").unwrap();
360    /// file.create_soft_link("alias", "/orig").unwrap();
361    /// ```
362    pub fn create_soft_link(&self, link_name: &str, target_path: &str) -> Result<()> {
363        self.root_group().create_soft_link(link_name, target_path)
364    }
365
366    /// Create an external link in the root of the file.
367    ///
368    /// See [`H5Group::create_external_link`](crate::group::H5Group::create_external_link).
369    ///
370    /// ```no_run
371    /// use rust_hdf5::H5File;
372    /// let file = H5File::create("master.h5").unwrap();
373    /// file.create_external_link("ext", "payload.h5", "/data").unwrap();
374    /// ```
375    pub fn create_external_link(
376        &self,
377        link_name: &str,
378        target_file: &str,
379        target_path: &str,
380    ) -> Result<()> {
381        self.root_group()
382            .create_external_link(link_name, target_file, target_path)
383    }
384
385    /// Commit a datatype in the root of the file.
386    ///
387    /// See [`H5Group::commit_datatype`](crate::group::H5Group::commit_datatype).
388    ///
389    /// ```no_run
390    /// use rust_hdf5::H5File;
391    /// use rust_hdf5::format::messages::datatype::DatatypeMessage;
392    /// let file = H5File::create("committed.h5").unwrap();
393    /// file.commit_datatype("temperature", DatatypeMessage::f64_type()).unwrap();
394    /// ```
395    pub fn commit_datatype(
396        &self,
397        name: &str,
398        datatype: crate::format::messages::datatype::DatatypeMessage,
399    ) -> Result<()> {
400        self.root_group().commit_datatype(name, datatype)
401    }
402
403    /// Start building a new dataset with the given element type.
404    ///
405    /// This returns a fluent builder. Call `.shape(...)` to set dimensions and
406    /// `.create("name")` to finalize.
407    ///
408    /// ```no_run
409    /// # use rust_hdf5::H5File;
410    /// let file = H5File::create("build.h5").unwrap();
411    /// let ds = file.new_dataset::<f64>().shape(&[3, 4]).create("matrix").unwrap();
412    /// ```
413    pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
414        DatasetBuilder::new(clone_inner(&self.inner))
415    }
416
417    /// Add a string attribute to the file (root group).
418    ///
419    /// The value is stored as a variable-length UTF-8 string (read back as a
420    /// Python `str` by h5py), not a fixed-length string.
421    pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
422        let inner = borrow_inner(&self.inner);
423        match &*inner {
424            H5FileInner::Writer(writer) => {
425                writer.set_vlen_string_attribute(
426                    crate::io::writer::AttrTarget::Root,
427                    name,
428                    value,
429                )?;
430                Ok(())
431            }
432            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
433        }
434    }
435
436    /// Add a numeric attribute to the file (root group).
437    pub fn set_attr_numeric<T: crate::types::H5Type>(&self, name: &str, value: &T) -> Result<()> {
438        use crate::format::messages::attribute::AttributeMessage;
439        let es = T::element_size();
440        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
441        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), raw.to_vec());
442        let inner = borrow_inner(&self.inner);
443        match &*inner {
444            H5FileInner::Writer(writer) => {
445                writer.add_root_attribute(attr)?;
446                Ok(())
447            }
448            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
449        }
450    }
451
452    /// Add a scalar attribute to the file (root group) whose datatype and raw
453    /// value the caller supplies.
454    ///
455    /// The escape hatch for a type this crate has no Rust mapping for — a
456    /// fixed-length string of a size the value alone does not imply, say,
457    /// which is what `H5Tcopy(H5T_C_S1)` plus `H5Tset_size` produces.
458    /// [`DatasetBuilder::datatype`](crate::dataset::DatasetBuilder::datatype)
459    /// is the same hatch for a dataset; every typed setter here builds one of
460    /// these underneath.
461    ///
462    /// `value` is the raw element image and must be exactly as long as the
463    /// datatype's element size.
464    ///
465    /// ```no_run
466    /// # use rust_hdf5::{DatatypeMessage, H5File};
467    /// let file = H5File::create("notes.h5").unwrap();
468    /// let mut text = vec![b'x'; 256];
469    /// text[255] = 0;
470    /// file.set_attr_typed(
471    ///     "note",
472    ///     DatatypeMessage::FixedString { size: 256, padding: 0, charset: 0 },
473    ///     text,
474    /// )
475    /// .unwrap();
476    /// ```
477    pub fn set_attr_typed(
478        &self,
479        name: &str,
480        datatype: DatatypeMessage,
481        value: Vec<u8>,
482    ) -> Result<()> {
483        use crate::format::messages::attribute::AttributeMessage;
484        if value.len() as u64 != u64::from(datatype.element_size()) {
485            return Err(Hdf5Error::InvalidState(format!(
486                "attribute '{name}' was given {} bytes for a datatype whose element is {}",
487                value.len(),
488                datatype.element_size()
489            )));
490        }
491        let attr = AttributeMessage::scalar_numeric(name, datatype, value);
492        let inner = borrow_inner(&self.inner);
493        match &*inner {
494            H5FileInner::Writer(writer) => {
495                writer.add_root_attribute(attr)?;
496                Ok(())
497            }
498            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
499        }
500    }
501
502    /// Add a numeric (or bool) **array** attribute to the file (root group).
503    ///
504    /// The values are written as a 1-D HDF5 array attribute (simple dataspace
505    /// `[values.len()]`, on-disk type `T::hdf5_type()`), read back by h5py as a
506    /// numpy array — the array counterpart of [`set_attr_numeric`](Self::set_attr_numeric).
507    /// For a multi-dimensional shape use
508    /// [`set_attr_array_numeric_nd`](Self::set_attr_array_numeric_nd).
509    pub fn set_attr_array_numeric<T: crate::types::H5Type>(
510        &self,
511        name: &str,
512        values: &[T],
513    ) -> Result<()> {
514        self.set_attr_array_numeric_nd(name, values, &[values.len()])
515    }
516
517    /// Add a numeric (or bool) **N-dimensional array** attribute to the file
518    /// (root group).
519    ///
520    /// `shape` gives the dataspace dimensions; `values` is the row-major data
521    /// and its length must equal the product of `shape` (an empty `shape` is a
522    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
523    /// of that shape. [`set_attr_array_numeric`](Self::set_attr_array_numeric)
524    /// is the 1-D convenience form.
525    pub fn set_attr_array_numeric_nd<T: crate::types::H5Type>(
526        &self,
527        name: &str,
528        values: &[T],
529        shape: &[usize],
530    ) -> Result<()> {
531        use crate::format::messages::attribute::AttributeMessage;
532        let n: usize = shape.iter().product();
533        if values.len() != n {
534            return Err(Hdf5Error::InvalidState(format!(
535                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
536                values.len()
537            )));
538        }
539        let es = T::element_size();
540        // Safety: `T: H5Type` is a `Copy` POD numeric whose byte width is `es`.
541        let raw =
542            unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
543        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
544        let attr = AttributeMessage::array_numeric(name, T::hdf5_type(), &dims, raw.to_vec());
545        let mut inner = borrow_inner_mut(&self.inner);
546        match &mut *inner {
547            H5FileInner::Writer(writer) => {
548                writer.add_root_attribute(attr)?;
549                Ok(())
550            }
551            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
552        }
553    }
554
555    /// Add a variable-length UTF-8 string **array** attribute to the file (root
556    /// group), read back by h5py as a 1-D array of `str` — the array counterpart
557    /// of [`set_attr_string`](Self::set_attr_string). For a multi-dimensional
558    /// shape use [`set_attr_string_array_nd`](Self::set_attr_string_array_nd).
559    pub fn set_attr_string_array(&self, name: &str, values: &[&str]) -> Result<()> {
560        self.set_attr_string_array_nd(name, values, &[values.len()])
561    }
562
563    /// Add a variable-length UTF-8 string **N-dimensional array** attribute to
564    /// the file (root group).
565    ///
566    /// `shape` gives the dataspace dimensions; `values` is the row-major data
567    /// and its length must equal the product of `shape` (an empty `shape` is a
568    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
569    /// of Python `str` with that shape.
570    /// [`set_attr_string_array`](Self::set_attr_string_array) is the 1-D
571    /// convenience form.
572    pub fn set_attr_string_array_nd(
573        &self,
574        name: &str,
575        values: &[&str],
576        shape: &[usize],
577    ) -> Result<()> {
578        let n: usize = shape.iter().product();
579        if values.len() != n {
580            return Err(Hdf5Error::InvalidState(format!(
581                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
582                values.len()
583            )));
584        }
585        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
586        let mut inner = borrow_inner_mut(&self.inner);
587        match &mut *inner {
588            H5FileInner::Writer(writer) => {
589                writer.set_vlen_string_array_attribute(
590                    crate::io::writer::AttrTarget::Root,
591                    name,
592                    values,
593                    &dims,
594                )?;
595                Ok(())
596            }
597            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
598        }
599    }
600
601    /// Add (or replace) an object-reference attribute on the file (root group)
602    /// — h5py's `f.attrs['entry'] = f['/data'].ref`.
603    ///
604    /// `path` names a dataset or a group (`/` is the root group) and must
605    /// already exist. The attribute takes the scalar shape h5py gives a single
606    /// reference; [`set_attr_object_references`](Self::set_attr_object_references)
607    /// is the array form. What reaches the file is the target's object header
608    /// address, which is assigned when the file is finalized.
609    pub fn set_attr_object_reference(&self, name: &str, path: &str) -> Result<()> {
610        self.set_root_reference_attr(name, &[path], &[])
611    }
612
613    /// Add (or replace) a 1-D array of object references as a file-level
614    /// attribute — the array counterpart of
615    /// [`set_attr_object_reference`](Self::set_attr_object_reference).
616    pub fn set_attr_object_references(&self, name: &str, paths: &[&str]) -> Result<()> {
617        self.set_root_reference_attr(name, paths, &[paths.len() as u64])
618    }
619
620    fn set_root_reference_attr(&self, name: &str, paths: &[&str], dims: &[u64]) -> Result<()> {
621        let inner = borrow_inner(&self.inner);
622        match &*inner {
623            H5FileInner::Writer(writer) => {
624                writer.set_object_reference_attribute(
625                    crate::io::writer::AttrTarget::Root,
626                    name,
627                    paths,
628                    dims,
629                )?;
630                Ok(())
631            }
632            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
633        }
634    }
635
636    /// Return the names of file-level (root group) attributes.
637    pub fn attr_names(&self) -> Result<Vec<String>> {
638        let inner = borrow_inner(&self.inner);
639        match &*inner {
640            H5FileInner::Reader(reader) => Ok(reader.root_attr_names()?),
641            _ => Ok(vec![]),
642        }
643    }
644
645    /// Why the file-level attribute `name` cannot be read, or `None` when it
646    /// can be. See [`H5Dataset::attr_unreadable_reason`](crate::H5Dataset::attr_unreadable_reason).
647    pub fn attr_unreadable_reason(&self, name: &str) -> Result<Option<String>> {
648        let inner = borrow_inner(&self.inner);
649        match &*inner {
650            H5FileInner::Reader(reader) => {
651                Ok(reader.root_attr_unreadable_reason(name).map(str::to_string))
652            }
653            _ => Err(Hdf5Error::InvalidState("not in read mode".into())),
654        }
655    }
656
657    /// Why the file-level attribute *set* cannot be listed, or `None` when it
658    /// can be. See
659    /// [`H5Dataset::attrs_unreadable_reason`](crate::H5Dataset::attrs_unreadable_reason).
660    pub fn attrs_unreadable_reason(&self) -> Result<Option<String>> {
661        let inner = borrow_inner(&self.inner);
662        match &*inner {
663            H5FileInner::Reader(reader) => {
664                Ok(reader.root_attrs_unreadable_reason().map(str::to_string))
665            }
666            _ => Err(Hdf5Error::InvalidState("not in read mode".into())),
667        }
668    }
669
670    /// Read a file-level string attribute.
671    pub fn attr_string(&self, name: &str) -> Result<String> {
672        let mut inner = borrow_inner_mut(&self.inner);
673        match &mut *inner {
674            H5FileInner::Reader(reader) => {
675                let attr = reader.root_attr(name)?.clone();
676                Ok(reader.attr_string_value(&attr)?)
677            }
678            _ => Err(Hdf5Error::InvalidState("not in read mode".into())),
679        }
680    }
681
682    /// The file-level metadata carried by the superblock extension: the
683    /// shared-message table, the v1 B-tree K values, the driver-info block and
684    /// the file-space strategy.
685    ///
686    /// Every field is `None` for a file written without an extension, and for
687    /// a file this handle has open for writing.
688    pub fn superblock_extension(&self) -> SuperblockExtension {
689        let inner = borrow_inner(&self.inner);
690        match &*inner {
691            H5FileInner::Reader(reader) => reader.superblock_extension().clone(),
692            _ => SuperblockExtension::default(),
693        }
694    }
695
696    /// How the object header at `path` stores each message it does not hold
697    /// privately, as `(message type, storage)` in header order.
698    ///
699    /// This is the flags byte `h5debug` prints as `<S>` / `<SA>`, and the
700    /// pointer kind beneath a shared one — the only place a file says whether
701    /// a message body is the message or a reference to one held elsewhere.
702    /// Read mode only.
703    pub fn object_message_storage(&self, path: &str) -> Result<Vec<(u8, MessageStorage)>> {
704        let mut inner = borrow_inner_mut(&self.inner);
705        match &mut *inner {
706            H5FileInner::Reader(reader) => Ok(reader.object_message_storage(path)?),
707            _ => Err(Hdf5Error::InvalidState(
708                "object_message_storage is only available in read mode".into(),
709            )),
710        }
711    }
712
713    /// The flags byte of every message the object header at `path` holds, as
714    /// `(message type, flags)` in header order, null and continuation
715    /// messages left out.
716    ///
717    /// What `h5debug` prints as `<C>`, `<DS>`, `<S>` and the rest
718    /// (`H5O__debug_real`, H5Odbg.c:409-455): which messages the library may
719    /// cache as never-changing, which it refuses to move to the shared-message
720    /// heap, and which are already there. Read mode only.
721    pub fn object_message_flags(&self, path: &str) -> Result<Vec<(u8, u8)>> {
722        let mut inner = borrow_inner_mut(&self.inner);
723        match &mut *inner {
724            H5FileInner::Reader(reader) => Ok(reader.object_message_flags(path)?),
725            _ => Err(Hdf5Error::InvalidState(
726                "object_message_flags is only available in read mode".into(),
727            )),
728        }
729    }
730
731    /// The class and version of every datatype message the object at `path`
732    /// carries, outermost first and then depth-first through compound
733    /// members, an enum's base and an array's base.
734    ///
735    /// The version is the one part of a datatype message a decode drops, and
736    /// it is not free: `H5T_set_version` (H5T.c:6584-6591) picks it from the
737    /// file's low libver bound and the type's own construction, so it is what
738    /// says which generation of library can read the type back. A
739    /// stored-shared datatype is followed to the committed type it names, so
740    /// what comes back is the version that actually describes the object.
741    pub fn object_datatype_versions(&self, path: &str) -> Result<Vec<DatatypeNodeVersion>> {
742        let mut inner = borrow_inner_mut(&self.inner);
743        match &mut *inner {
744            H5FileInner::Reader(reader) => Ok(reader.object_datatype_versions(path)?),
745            _ => Err(Hdf5Error::InvalidState(
746                "object_datatype_versions is only available in read mode".into(),
747            )),
748        }
749    }
750
751    /// Whether the object at `path` records its times —
752    /// `H5Pget_obj_track_times` on the creation property list it was made
753    /// with, read back from the header that answers it.
754    ///
755    /// A version-2 header says so with `H5O_HDR_STORE_TIMES` and the four
756    /// times behind it; a version-1 dataset says so by carrying an
757    /// `H5O_MTIME_NEW` message. A version-1 group or committed datatype says
758    /// nothing either way — it has nowhere to record a time — so this is
759    /// `false` for one however it was created. Read mode only.
760    pub fn object_records_times(&self, path: &str) -> Result<bool> {
761        let mut inner = borrow_inner_mut(&self.inner);
762        match &mut *inner {
763            H5FileInner::Reader(reader) => Ok(reader.object_records_times(path)?),
764            _ => Err(Hdf5Error::InvalidState(
765                "object_records_times is only available in read mode".into(),
766            )),
767        }
768    }
769
770    /// Bytes this file's on-disk free-space managers record as free —
771    /// libhdf5's `H5Fget_freespace`, and the number `h5stat -S` prints as
772    /// "Amount of tracked free space".
773    ///
774    /// Zero for a file that persists no managers, which is every file created
775    /// without [`H5FileOptions::file_space`] asking for `persist`. Read mode
776    /// only: an open writer's freed blocks are not on disk yet, so the two
777    /// would be different questions with one name.
778    pub fn tracked_free_space(&self) -> Result<u64> {
779        let mut inner = borrow_inner_mut(&self.inner);
780        match &mut *inner {
781            H5FileInner::Reader(reader) => Ok(reader.tracked_free_space()?),
782            _ => Err(Hdf5Error::InvalidState(
783                "tracked_free_space is only available in read mode".into(),
784            )),
785        }
786    }
787
788    /// Size in bytes of the userblock this file was written with — the
789    /// application-owned prefix the superblock follows (`H5Pget_userblock`).
790    /// Zero for a file without one, whichever mode the handle is in.
791    pub fn userblock_size(&self) -> u64 {
792        let inner = borrow_inner(&self.inner);
793        match &*inner {
794            H5FileInner::Reader(reader) => reader.userblock_size(),
795            H5FileInner::Writer(writer) => writer.userblock_size(),
796            H5FileInner::Closed => 0,
797        }
798    }
799
800    /// This file's on-disk superblock format version (0-3) — libhdf5's
801    /// `H5F_get_info2`'s `super_version`, read from the file's own header
802    /// rather than derived from any bound a caller asked for.
803    pub fn superblock_version(&self) -> Result<u8> {
804        let inner = borrow_inner(&self.inner);
805        match &*inner {
806            H5FileInner::Reader(reader) => Ok(reader.superblock_version()),
807            _ => Err(Hdf5Error::InvalidState(
808                "superblock_version is only available in read mode".into(),
809            )),
810        }
811    }
812
813    /// The lowest [`LibverBound`] consistent with this file's on-disk
814    /// superblock version — a *view* reconstructed from
815    /// [`superblock_version`](Self::superblock_version), not the bound a
816    /// writer may have named: [`LibverBound::superblock_version`] maps four
817    /// bounds onto version 3, so a version-3 file reports [`LibverBound::V110`]
818    /// regardless of which of the four actually wrote it.
819    pub fn libver_bound(&self) -> Result<LibverBound> {
820        self.superblock_version()
821            .map(LibverBound::from_superblock_version)
822    }
823
824    /// Check if the file is in write/append mode.
825    pub fn is_writable(&self) -> bool {
826        let inner = borrow_inner(&self.inner);
827        matches!(&*inner, H5FileInner::Writer(_))
828    }
829
830    /// Create a variable-length string dataset and write data.
831    ///
832    /// This is a convenience method for writing h5py-compatible vlen string
833    /// datasets using global heap storage. The datatype declares UTF-8, which
834    /// a Rust `&str` always is; [`write_vlen_strings_ascii`] writes the same
835    /// dataset under an ASCII declaration, the type h5py's
836    /// `string_dtype("ascii")` produces.
837    ///
838    /// [`write_vlen_strings_ascii`]: Self::write_vlen_strings_ascii
839    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
840        self.write_vlen_strings_charset(name, strings, 1)
841    }
842
843    /// Create a variable-length **ASCII** string dataset and write data.
844    ///
845    /// The ASCII twin of [`write_vlen_strings`](Self::write_vlen_strings),
846    /// named after the [`DatatypeMessage::vlen_string_ascii`] /
847    /// [`DatatypeMessage::vlen_string_utf8`] pair it selects between. A string
848    /// that is not 7-bit is rejected rather than stored under a datatype that
849    /// misdescribes it, so the file reads the same in every library that
850    /// trusts the declaration.
851    ///
852    /// [`DatatypeMessage::vlen_string_ascii`]: crate::DatatypeMessage::vlen_string_ascii
853    /// [`DatatypeMessage::vlen_string_utf8`]: crate::DatatypeMessage::vlen_string_utf8
854    pub fn write_vlen_strings_ascii(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
855        self.write_vlen_strings_charset(name, strings, 0)
856    }
857
858    /// The single owner of one-call vlen-string dataset creation: the two
859    /// public entry points differ only in the character set they declare.
860    fn write_vlen_strings_charset(
861        &self,
862        name: &str,
863        strings: &[&str],
864        charset: u8,
865    ) -> Result<H5Dataset> {
866        let inner = borrow_inner(&self.inner);
867        match &*inner {
868            H5FileInner::Writer(writer) => {
869                let idx = writer.create_vlen_string_dataset(name, strings, charset)?;
870                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
871                Ok(H5Dataset::new_writer(clone_inner(&self.inner), idx, parts))
872            }
873            H5FileInner::Reader(_) => {
874                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
875            }
876            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
877        }
878    }
879
880    /// Create a variable-length byte-array dataset and write data.
881    ///
882    /// Each `&[u8]` becomes one element of variable length, stored as a vlen
883    /// sequence of `u8` in global heap storage. h5py reads it back as an array
884    /// of `uint8` arrays. Returns a writer-mode handle so attributes can be
885    /// attached, like [`write_vlen_strings`](Self::write_vlen_strings).
886    ///
887    /// The `u8` case of [`write_vlen_numeric`](Self::write_vlen_numeric).
888    pub fn write_vlen_bytes(&self, name: &str, items: &[&[u8]]) -> Result<H5Dataset> {
889        self.write_vlen_numeric(name, items)
890    }
891
892    /// Create a variable-length numeric-sequence dataset and write data.
893    ///
894    /// Each `&[T]` becomes one element of variable length, stored as a global
895    /// heap object under a vlen sequence datatype over `T`; h5py reads the
896    /// dataset back as an array of `T`-typed arrays, the type
897    /// `h5py.vlen_dtype(np.dtype(...))` produces. Sequences may have any
898    /// length, including zero. Returns a writer-mode handle so attributes can
899    /// be attached, like [`write_vlen_strings`](Self::write_vlen_strings).
900    ///
901    /// ```no_run
902    /// # use rust_hdf5::H5File;
903    /// let file = H5File::create("v.h5").unwrap();
904    /// let a: &[i32] = &[1, 2, 3];
905    /// let b: &[i32] = &[];
906    /// file.write_vlen_numeric("data", &[a, b]).unwrap();
907    /// ```
908    pub fn write_vlen_numeric<T: H5Type>(&self, name: &str, items: &[&[T]]) -> Result<H5Dataset> {
909        let images = crate::dataset::vlen_sequence_images(items)?;
910        let images: Vec<&[u8]> = images.iter().map(|c| c.as_ref()).collect();
911        let inner = borrow_inner(&self.inner);
912        match &*inner {
913            H5FileInner::Writer(writer) => {
914                let idx = writer.create_vlen_sequence_dataset(name, T::hdf5_type(), &images)?;
915                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
916                Ok(H5Dataset::new_writer(clone_inner(&self.inner), idx, parts))
917            }
918            H5FileInner::Reader(_) => {
919                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
920            }
921            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
922        }
923    }
924
925    /// Create a chunked, compressed variable-length string dataset.
926    ///
927    /// Like `write_vlen_strings`, but stores the vlen references in chunked
928    /// layout with the given filter pipeline (e.g., `FilterPipeline::deflate(6)`
929    /// or `FilterPipeline::zstd(3)`). `chunk_size` is the number of strings
930    /// per chunk.
931    pub fn write_vlen_strings_compressed(
932        &self,
933        name: &str,
934        strings: &[&str],
935        chunk_size: usize,
936        pipeline: FilterPipeline,
937    ) -> Result<H5Dataset> {
938        let inner = borrow_inner(&self.inner);
939        match &*inner {
940            H5FileInner::Writer(writer) => {
941                let idx = writer
942                    .create_vlen_string_dataset_compressed(name, strings, chunk_size, pipeline)?;
943                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
944                Ok(H5Dataset::new_writer(clone_inner(&self.inner), idx, parts))
945            }
946            H5FileInner::Reader(_) => {
947                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
948            }
949            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
950        }
951    }
952
953    /// Create an empty chunked vlen string dataset ready for incremental appends.
954    ///
955    /// Use `append_vlen_strings` to add data. If `pipeline` is `Some`, chunks
956    /// are compressed (e.g., `Some(FilterPipeline::lz4())`).
957    pub fn create_appendable_vlen_dataset(
958        &self,
959        name: &str,
960        chunk_size: usize,
961        pipeline: Option<FilterPipeline>,
962    ) -> Result<H5Dataset> {
963        let inner = borrow_inner(&self.inner);
964        match &*inner {
965            H5FileInner::Writer(writer) => {
966                let idx =
967                    writer.create_appendable_vlen_string_dataset(name, chunk_size, pipeline)?;
968                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
969                Ok(H5Dataset::new_writer(clone_inner(&self.inner), idx, parts))
970            }
971            H5FileInner::Reader(_) => {
972                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
973            }
974            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
975        }
976    }
977
978    /// Append variable-length strings to an existing chunked vlen string dataset.
979    pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
980        let inner = borrow_inner(&self.inner);
981        match &*inner {
982            H5FileInner::Writer(writer) => {
983                let ds_index = writer.open_dataset_index(name)?;
984                writer.append_vlen_strings(ds_index, strings)?;
985                Ok(())
986            }
987            H5FileInner::Reader(_) => {
988                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
989            }
990            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
991        }
992    }
993
994    /// Delete a dataset name, with libhdf5's `H5Ldelete` semantics: a
995    /// path naming a hard link removes just that link, and if a hard link
996    /// still names the object whose tree name is deleted, the dataset
997    /// lives on under the link. Deleting the last name unlinks the
998    /// dataset on close and the file space it owned — data blocks,
999    /// chunk-index structures, and the global-heap objects of
1000    /// variable-length values — is freed for reuse by later writes in this
1001    /// session (the file itself does not shrink).
1002    pub fn delete_dataset(&self, name: &str) -> Result<()> {
1003        let inner = borrow_inner(&self.inner);
1004        match &*inner {
1005            H5FileInner::Writer(writer) => {
1006                writer.delete_dataset(name)?;
1007                Ok(())
1008            }
1009            _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
1010        }
1011    }
1012
1013    /// Delete a group and all its child datasets/sub-groups, freeing their
1014    /// file space the way [`delete_dataset`](Self::delete_dataset) does.
1015    ///
1016    /// Hard links reaching in from outside the deleted subtree keep their
1017    /// targets alive: a dataset or group named by such a link survives
1018    /// under the link's path (a group brings its whole subtree with it),
1019    /// and a `name` that is itself a hard link's path removes just that
1020    /// link.
1021    pub fn delete_group(&self, name: &str) -> Result<()> {
1022        let inner = borrow_inner(&self.inner);
1023        match &*inner {
1024            H5FileInner::Writer(writer) => {
1025                writer.delete_group(name)?;
1026                Ok(())
1027            }
1028            _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
1029        }
1030    }
1031
1032    /// Open an existing dataset by name (read mode).
1033    ///
1034    /// Uses libhdf5's default dataset-access properties; name others with
1035    /// [`dataset_with`](Self::dataset_with).
1036    pub fn dataset(&self, name: &str) -> Result<H5Dataset> {
1037        self.dataset_with(name, DatasetAccess::default())
1038    }
1039
1040    /// [`dataset`](Self::dataset) under named dataset-access properties —
1041    /// `H5Dopen2` with a dapl instead of `H5P_DEFAULT`.
1042    ///
1043    /// Three of the properties [`DatasetAccess`] carries decide how a
1044    /// *virtual* dataset's extent is resolved and where its sources are
1045    /// looked for; [`DatasetAccess::efile_prefix`] says where the raw data
1046    /// files of a dataset stored through an external file list are. For a
1047    /// dataset that is neither, this is exactly [`dataset`](Self::dataset).
1048    ///
1049    /// First open wins: while any handle on that dataset is alive, a later
1050    /// open of it joins that open and its own `access` is ignored, exactly
1051    /// as `H5Dopen2` ignores the dapl of an open that finds the dataset
1052    /// already in `H5FO_opened` (H5Dint.c:1496-1500, :1523-1528) — only the
1053    /// open that creates the shared info reaches `H5D__virtual_init`, which
1054    /// is where the view and the printf gap are read out of the dapl
1055    /// (H5Dvirtual.c:2178-2188). Once every handle is dropped the next open
1056    /// resolves afresh under its own properties.
1057    ///
1058    /// libhdf5 keys that shared info on the *file* rather than on one
1059    /// `H5Fopen`, so there a second `H5Fopen` of the same path still joins
1060    /// the first open's view; here each [`H5File`] is its own reader and
1061    /// binds independently.
1062    ///
1063    /// # Errors
1064    ///
1065    /// Beyond [`dataset`](Self::dataset)'s own errors, a
1066    /// [`DatasetAccess::virtual_printf_gap`] of `u64::MAX` — libhdf5's
1067    /// `HSIZE_UNDEF` — is refused, as `H5Pset_virtual_printf_gap` refuses it.
1068    ///
1069    /// The one property a joining open may not disagree about is
1070    /// [`DatasetAccess::efile_prefix`]: `H5D__open_name` refuses an open
1071    /// whose expanded external file prefix differs from the open dataset's
1072    /// (H5Dint.c:1533-1545), and so does this.
1073    pub fn dataset_with(&self, name: &str, access: DatasetAccess) -> Result<H5Dataset> {
1074        access.validate()?;
1075        // Mutable: a name that crosses an external link opens the file that
1076        // link names, and the reader caches that handle for the next one.
1077        let mut inner = borrow_inner_mut(&self.inner);
1078        match &mut *inner {
1079            H5FileInner::Reader(reader) => {
1080                // The reader's open gate reports *why* a name cannot be
1081                // opened — a dangling soft link and an unsupported object are
1082                // both present in the listing, and neither is an absence.
1083                let (open, info) = reader.open_dataset_with(name, &access)?;
1084                let shape: Vec<usize> = info.dataspace.dims.iter().map(|&d| d as usize).collect();
1085                let element_size = info.datatype.element_size() as usize;
1086                Ok(H5Dataset::new_reader(
1087                    clone_inner(&self.inner),
1088                    name.to_string(),
1089                    shape,
1090                    element_size,
1091                    open,
1092                ))
1093            }
1094            H5FileInner::Writer(_) => Err(Hdf5Error::InvalidState(
1095                "cannot open a dataset by name in write mode; use new_dataset() instead"
1096                    .to_string(),
1097            )),
1098            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
1099        }
1100    }
1101
1102    /// Reopen an existing dataset by name in write mode.
1103    ///
1104    /// [`dataset`](Self::dataset) only works in read mode; in write mode a
1105    /// dataset is normally created via [`new_dataset`](Self::new_dataset).
1106    /// This returns a write-mode handle to a dataset created earlier in the
1107    /// same session, so you can attach attributes or append chunks to it
1108    /// without keeping the original handle around — e.g. to flush cached
1109    /// first/last values onto a dataset at file-close time.
1110    ///
1111    /// # Errors
1112    ///
1113    /// Returns [`Hdf5Error::NotFound`] if no live dataset with that name
1114    /// exists, and an error in read mode (use [`dataset`](Self::dataset)).
1115    pub fn dataset_writer(&self, name: &str) -> Result<H5Dataset> {
1116        self.dataset_writer_with(name, DatasetAccess::default())
1117    }
1118
1119    /// [`dataset_writer`](Self::dataset_writer) under named dataset-access
1120    /// properties — `H5Dopen2` with a dapl instead of `H5P_DEFAULT`, in write
1121    /// mode.
1122    ///
1123    /// The one property that reaches a write is
1124    /// [`DatasetAccess::efile_prefix`]: `H5D__efl_write` joins each slot name
1125    /// of an external file list against `dset->shared->extfile_prefix`
1126    /// (H5Defl.c:429-431), which the open that settled the dataset's shared
1127    /// info built from its dapl. So this is how a dataset reopened from an
1128    /// existing file is told where its raw data files are before being
1129    /// written to; the other properties decide a *virtual* dataset's extent,
1130    /// which this writer never resolves.
1131    ///
1132    /// First open wins, and a joining open may not disagree about that
1133    /// prefix — see
1134    /// [`H5File::dataset_with`](Self::dataset_with) for the same rule on the
1135    /// read side. A dataset this session created settled its prefix from
1136    /// [`DatasetBuilder::efile_prefix`](crate::dataset::DatasetBuilder::efile_prefix),
1137    /// so while its handle is alive this call must name the same one;
1138    /// once every handle is dropped the next call settles it afresh.
1139    ///
1140    /// # Errors
1141    ///
1142    /// [`dataset_writer`](Self::dataset_writer)'s errors, plus a refusal when
1143    /// `access` names an external file prefix that disagrees with the one an
1144    /// open of this dataset is still holding.
1145    pub fn dataset_writer_with(&self, name: &str, access: DatasetAccess) -> Result<H5Dataset> {
1146        access.validate()?;
1147        let inner = borrow_inner(&self.inner);
1148        match &*inner {
1149            H5FileInner::Writer(writer) => {
1150                let index = writer.open_dataset_index(name)?;
1151                let parts = writer.dataset_handle_parts(index, &access)?;
1152                Ok(H5Dataset::new_writer(
1153                    clone_inner(&self.inner),
1154                    index,
1155                    parts,
1156                ))
1157            }
1158            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
1159                "cannot open a dataset_writer in read mode; use dataset() instead".to_string(),
1160            )),
1161            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
1162        }
1163    }
1164
1165    /// Return the names of all datasets in the root group.
1166    ///
1167    /// Works in both read and write mode: in write mode, returns the names of
1168    /// datasets created so far; in read mode, returns the names discovered
1169    /// during file open.
1170    pub fn dataset_names(&self) -> Vec<String> {
1171        let inner = borrow_inner(&self.inner);
1172        match &*inner {
1173            H5FileInner::Reader(reader) => reader
1174                .dataset_names()
1175                .iter()
1176                .map(|s| s.to_string())
1177                .collect(),
1178            H5FileInner::Writer(writer) => writer
1179                .dataset_names()
1180                .iter()
1181                .map(|s| s.to_string())
1182                .collect(),
1183            H5FileInner::Closed => Vec::new(),
1184        }
1185    }
1186
1187    /// The paths of every committed (named) datatype in this file.
1188    ///
1189    /// A committed datatype is an object in its own right, in neither
1190    /// [`dataset_names`](Self::dataset_names) nor the group listing. In write
1191    /// mode these are the types [`commit_datatype`](Self::commit_datatype)
1192    /// committed this session.
1193    pub fn named_datatype_names(&self) -> Vec<String> {
1194        let inner = borrow_inner(&self.inner);
1195        match &*inner {
1196            H5FileInner::Reader(reader) => reader
1197                .named_datatype_names()
1198                .iter()
1199                .map(|s| s.to_string())
1200                .collect(),
1201            H5FileInner::Writer(writer) => writer.committed_datatype_names(),
1202            H5FileInner::Closed => Vec::new(),
1203        }
1204    }
1205
1206    /// Open a committed (named) datatype by path (read mode).
1207    ///
1208    /// The handle opens whenever the object is there; a type this crate
1209    /// cannot decode reports why from
1210    /// [`H5NamedDatatype::datatype`](crate::named_datatype::H5NamedDatatype::datatype),
1211    /// so its attributes stay reachable.
1212    ///
1213    /// # Errors
1214    ///
1215    /// [`Hdf5Error::NotFound`] when no committed datatype is at that path.
1216    pub fn named_datatype(&self, path: &str) -> Result<crate::H5NamedDatatype> {
1217        // Mutable: a path that crosses an external link opens the file that
1218        // link names, and the reader caches that handle for the next one.
1219        let mut inner = borrow_inner_mut(&self.inner);
1220        match &mut *inner {
1221            H5FileInner::Reader(reader) => {
1222                reader.named_datatype_info(path)?;
1223                drop(inner);
1224                Ok(crate::H5NamedDatatype::new_reader(
1225                    clone_inner(&self.inner),
1226                    path.to_string(),
1227                ))
1228            }
1229            H5FileInner::Writer(_) => Err(Hdf5Error::InvalidState(
1230                "committed datatypes are readable only in read mode".to_string(),
1231            )),
1232            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
1233        }
1234    }
1235
1236    /// Explicitly close the file. For a writer, this finalizes the file
1237    /// (writes superblock, headers, etc.). For a reader, this is a no-op.
1238    ///
1239    /// The file is also auto-finalized on drop, but calling `close()` lets
1240    /// you handle errors.
1241    pub fn close(self) -> Result<()> {
1242        let old = {
1243            let mut inner = borrow_inner_mut(&self.inner);
1244            std::mem::replace(&mut *inner, H5FileInner::Closed)
1245        };
1246        match old {
1247            H5FileInner::Writer(writer) => {
1248                writer.close()?;
1249                Ok(())
1250            }
1251            H5FileInner::Reader(_) => Ok(()),
1252            H5FileInner::Closed => Ok(()),
1253        }
1254    }
1255
1256    /// Close the file without a final `fsync` (write mode only).
1257    ///
1258    /// Like [`close`](Self::close), this finalizes the file — object headers
1259    /// and superblock are written, so on return it is a complete, valid HDF5
1260    /// file readable by any process — but the trailing `sync_all` (fsync) is
1261    /// skipped. The bytes are handed to the OS but are not guaranteed durable
1262    /// against power loss or an OS crash until the OS flushes its page cache.
1263    ///
1264    /// This trades durability for speed (the fsync typically dominates close
1265    /// latency); use it for bulk output that can be regenerated. Prefer
1266    /// [`close`](Self::close) when durability matters. Dropping the file
1267    /// without calling either finalizes durably.
1268    ///
1269    /// For a reader or an already-closed file this is a no-op, matching
1270    /// [`close`](Self::close).
1271    pub fn close_no_sync(self) -> Result<()> {
1272        let old = {
1273            let mut inner = borrow_inner_mut(&self.inner);
1274            std::mem::replace(&mut *inner, H5FileInner::Closed)
1275        };
1276        match old {
1277            H5FileInner::Writer(writer) => {
1278                writer.close_no_sync()?;
1279                Ok(())
1280            }
1281            H5FileInner::Reader(_) => Ok(()),
1282            H5FileInner::Closed => Ok(()),
1283        }
1284    }
1285
1286    /// Hand every byte written so far to the operating system. Only
1287    /// meaningful in write mode.
1288    ///
1289    /// This empties the write accumulator, so another process reading the file
1290    /// afterwards sees everything written up to this point. It does not
1291    /// finalize the file — object headers and the superblock are still
1292    /// [`close`](Self::close)'s work — and it does not `fsync`.
1293    pub fn flush(&self) -> Result<()> {
1294        let mut inner = borrow_inner_mut(&self.inner);
1295        match &mut *inner {
1296            H5FileInner::Writer(writer) => Ok(writer.handle().flush()?),
1297            H5FileInner::Reader(_) => Ok(()),
1298            H5FileInner::Closed => Ok(()),
1299        }
1300    }
1301}
1302
1303/// Builder controlling how an [`H5File`] is opened.
1304///
1305/// The default policy follows the HDF5 C library: an exclusive lock is
1306/// acquired for write-mode opens and a shared lock for read-mode opens,
1307/// honoring the `HDF5_USE_FILE_LOCKING` environment variable. Calling
1308/// [`Self::locking`] overrides the env-var value.
1309#[derive(Debug, Default, Clone)]
1310pub struct H5FileOptions {
1311    locking: Option<FileLocking>,
1312    track_order: bool,
1313    track_times: bool,
1314    libver: Option<LibverBound>,
1315    userblock: u64,
1316    shared_messages: SharedMessageConfig,
1317    file_space: Option<FileSpaceConfig>,
1318    file_space_page_size: Option<u64>,
1319    elink_prefix: Option<String>,
1320}
1321
1322impl H5FileOptions {
1323    /// Construct a fresh options builder with default settings.
1324    pub fn new() -> Self {
1325        Self::default()
1326    }
1327
1328    /// Override the locking policy. Bypasses the `HDF5_USE_FILE_LOCKING`
1329    /// environment variable for the resulting open call.
1330    pub fn locking(mut self, policy: FileLocking) -> Self {
1331        self.locking = Some(policy);
1332        self
1333    }
1334
1335    /// Disable OS-level file locking entirely (equivalent to
1336    /// `HDF5_USE_FILE_LOCKING=FALSE`). Under the `mmap` feature such an open
1337    /// reads through the descriptor rather than a map, which is taken only
1338    /// under the shared lock, so a zero-copy view of it is refused.
1339    pub fn no_locking(self) -> Self {
1340        self.locking(FileLocking::Disabled)
1341    }
1342
1343    /// Try to acquire the lock but do not fail if the filesystem rejects it
1344    /// (equivalent to `HDF5_USE_FILE_LOCKING=BEST_EFFORT`).
1345    pub fn best_effort_locking(self) -> Self {
1346        self.locking(FileLocking::BestEffort)
1347    }
1348
1349    /// `H5Pset_elink_prefix` (H5Plapl.c:923): a directory the *target file*
1350    /// of an external link is looked for under, after `HDF5_EXT_PREFIX` and
1351    /// before the linking file's own directory — step 3 of
1352    /// `H5F_prefix_open_file`'s order (H5Fint.c:938-950).
1353    ///
1354    /// Unlike [`DatasetAccess::virtual_prefix`](crate::DatasetAccess), this
1355    /// one is *not* shadowed by its environment variable and gets no
1356    /// `${ORIGIN}` expansion: `H5L__extern_traverse` peeks the property
1357    /// verbatim and hands it straight to the search (H5Lexternal.c:210-215),
1358    /// with no `H5D__build_file_prefix` step in between. Measured against
1359    /// libhdf5 1.14.6 and 2.0.0: with `HDF5_EXT_PREFIX` naming a directory
1360    /// that has no target, this property still resolves it, while the same
1361    /// arrangement for a virtual source does not; and a `${ORIGIN}` written
1362    /// here stays a literal directory name.
1363    ///
1364    /// # Where this lives, and why not on the call
1365    ///
1366    /// libhdf5 keeps it in a *link access* property list, an argument every
1367    /// `H5*_by_name` call carries. Here it is a property of the open,
1368    /// because that is the narrowest scope this crate can honour: a reader
1369    /// resolves each external link's file name once and then holds that
1370    /// answer for its own life, so a prefix passed per call could not change
1371    /// a name another call had already resolved. Making it file-scoped also
1372    /// keeps it with the one other cross-file policy libhdf5 takes from a
1373    /// property list and applies to every file a path touches — the locking
1374    /// mode — and, like that one, it propagates down a chain of links, which
1375    /// is what a lapl does upstream (measured: a two-hop chain resolves its
1376    /// second hop under the prefix given at the first).
1377    ///
1378    /// Read-side only: nothing the writer does traverses an external link.
1379    pub fn elink_prefix(mut self, prefix: impl Into<String>) -> Self {
1380        self.elink_prefix = Some(prefix.into());
1381        self
1382    }
1383
1384    /// Create the file's root group with creation-order tracking, and make
1385    /// that the policy for objects created in it — h5py's
1386    /// `File(path, "w", track_order=True)`.
1387    ///
1388    /// Only [`create`](Self::create) reads this; opening an existing file
1389    /// takes the policy from the root group already on disk. Change it for
1390    /// later objects with [`H5File::set_track_order`].
1391    pub fn track_order(mut self, track: bool) -> Self {
1392        self.track_order = track;
1393        self
1394    }
1395
1396    /// Create the file's root group recording its times, and make that the
1397    /// policy for objects created in it — `H5Pset_obj_track_times`, h5py's
1398    /// `File(path, "w", track_times=True)`.
1399    ///
1400    /// An object recording times keeps the ones its header version can hold:
1401    /// four in a version-2 header's prefix, one modification time in a
1402    /// version-1 dataset's `H5O_MTIME_NEW` message, and none at all in a
1403    /// version-1 group or committed datatype, which have nowhere to put one.
1404    ///
1405    /// Off unless this says otherwise — h5py's default, not libhdf5's. h5py's
1406    /// high-level API passes `track_times=False` for every object it makes
1407    /// (`_hl/files.py:189`, `_hl/dataset.py:39`, `_hl/group.py:42`), while a
1408    /// bare creation property list leaves it on (`H5O_CRT_OHDR_FLAGS_DEF` is
1409    /// `H5O_HDR_STORE_TIMES`, H5Opkg.h:74), which is what `h5py.h5d.create`
1410    /// and libhdf5's own C API get.
1411    ///
1412    /// Only [`create`](Self::create) reads this; the root group of an existing
1413    /// file was made under whatever created it. Change it for later objects
1414    /// with [`H5File::set_track_times`].
1415    pub fn track_times(mut self, track: bool) -> Self {
1416        self.track_times = track;
1417        self
1418    }
1419
1420    /// Create the file under a library-version low bound — h5py's
1421    /// `File(path, "w", libver=("v108", "v108"))`, libhdf5's
1422    /// `H5Pset_libver_bounds` `low` argument.
1423    ///
1424    /// The bound decides the superblock version the file is written with
1425    /// ([`LibverBound::superblock_version`]) as well as the message versions
1426    /// of the objects created in it, so unlike
1427    /// [`H5File::set_libver_bound`] — which only reaches objects created
1428    /// after the call — it applies to the file itself.
1429    ///
1430    /// [`LibverBound::Earliest`] asks for the whole classic generation, the
1431    /// file libhdf5 writes at `H5F_LIBVER_EARLIEST`: a version-0 superblock,
1432    /// a symbol-table root group, version-1 object headers, symbol-table
1433    /// subgroups and the version-1 B-tree chunk index. Such a file is
1434    /// readable by libhdf5 1.6, and correspondingly gives up everything
1435    /// newer — SWMR ([`crate::swmr`]) and virtual datasets are refused in
1436    /// it, and a chunk larger than 4 GiB does not fit its index key.
1437    ///
1438    /// [`LibverBound::V18`] asks for the file libhdf5 writes at
1439    /// `H5F_LIBVER_V18`: a version-2 superblock over link-message groups and
1440    /// version-2 object headers, but still the version-3 data layout message
1441    /// and so still the version-1 B-tree chunk index — `H5O_layout_ver_bounds`
1442    /// does not reach version 4 until `V110`, and the v1.10 indexes live in
1443    /// nothing older. SWMR is refused in such a file: its status flags need a
1444    /// version-3 superblock, which this bound's row does not reach.
1445    ///
1446    /// Not calling this at all is *not* the same as asking for `Earliest`,
1447    /// nor for `V18`: the default file has the version-2 superblock and
1448    /// link-message groups of the v1.8 bound over the v1.10 chunk indexes,
1449    /// which no single bound describes.
1450    ///
1451    /// Only [`create`](Self::create) reads this; an existing file keeps the
1452    /// superblock it already has.
1453    ///
1454    /// ```no_run
1455    /// use rust_hdf5::{H5File, LibverBound};
1456    /// let file = H5File::options()
1457    ///     .libver(LibverBound::V110)
1458    ///     .create("v110.h5")
1459    ///     .unwrap();
1460    /// # let _ = file;
1461    /// ```
1462    pub fn libver(mut self, libver: LibverBound) -> Self {
1463        self.libver = Some(libver);
1464        self
1465    }
1466
1467    /// Reserve `size` bytes in front of the superblock for the application's
1468    /// own use — h5py's `File(path, "w", userblock_size=512)`, libhdf5's
1469    /// `H5Pset_userblock`.
1470    ///
1471    /// The block is the file's first `size` bytes and belongs to whoever
1472    /// writes it: an executable header, a checksum, a provenance record. HDF5
1473    /// itself only skips it — the superblock and every address in the file are
1474    /// based at `size`, and a reader finds the superblock by looking at offset
1475    /// 0 and then at [`MIN_USERBLOCK`](crate::MIN_USERBLOCK) doubled
1476    /// repeatedly, which is why the size must be zero (no block) or a power of
1477    /// two of at least that many bytes. [`create`](Self::create) reports any
1478    /// other size as an error; it is not rounded up.
1479    ///
1480    /// This crate writes the block zero-filled and never reads it back, so
1481    /// filling it is a plain write to the front of the file after
1482    /// [`H5File::close`].
1483    ///
1484    /// ```no_run
1485    /// use rust_hdf5::H5File;
1486    /// let file = H5File::options().userblock(512).create("prefixed.h5").unwrap();
1487    /// assert_eq!(file.userblock_size(), 512);
1488    /// ```
1489    pub fn userblock(mut self, size: u64) -> Self {
1490        self.userblock = size;
1491        self
1492    }
1493
1494    /// Create the file with shared object header messages — libhdf5's
1495    /// `H5Pset_shared_mesg_nindexes` + `H5Pset_shared_mesg_index` +
1496    /// `H5Pset_shared_mesg_phase_change`, which h5py exposes no binding for.
1497    ///
1498    /// A message class covered by an index is written once into a
1499    /// shared-message fractal heap, and every object header that would have
1500    /// held that exact body holds a pointer to it instead. `indexes` gives
1501    /// one `(message types, minimum message size)` pair per index, where the
1502    /// type mask is built from
1503    /// [`type_flag`](crate::format::sohm::type_flag); `list_max` and
1504    /// `btree_min` are the file-wide counts at which an index changes between
1505    /// list and v2 B-tree form.
1506    ///
1507    /// Only [`create`](Self::create) reads this, and it refuses a
1508    /// configuration libhdf5 would refuse: more than eight indexes, an index
1509    /// covering no type, or thresholds that overlap.
1510    ///
1511    /// ```no_run
1512    /// use rust_hdf5::{H5File, format::sohm::type_flag};
1513    /// use rust_hdf5::format::messages::{MSG_ATTRIBUTE, MSG_DATASPACE, MSG_DATATYPE};
1514    ///
1515    /// let types = type_flag(MSG_DATATYPE).unwrap()
1516    ///     | type_flag(MSG_DATASPACE).unwrap()
1517    ///     | type_flag(MSG_ATTRIBUTE).unwrap();
1518    /// let file = H5File::options()
1519    ///     .shared_messages(&[(types, 0)], 50, 40)
1520    ///     .create("sohm.h5")
1521    ///     .unwrap();
1522    /// # let _ = file;
1523    /// ```
1524    pub fn shared_messages(
1525        mut self,
1526        indexes: &[(u16, u32)],
1527        list_max: u16,
1528        btree_min: u16,
1529    ) -> Self {
1530        self.shared_messages = SharedMessageConfig::new(indexes, list_max, btree_min);
1531        self
1532    }
1533
1534    /// Create the file under a file-space handling strategy — libhdf5's
1535    /// `H5Pset_file_space_strategy`, h5py's `File(..., fs_strategy=...,
1536    /// fs_persist=..., fs_threshold=...)`.
1537    ///
1538    /// `strategy` picks how released space is reused:
1539    /// [`FileSpaceStrategy::FsmAggr`] keeps free-space managers and the
1540    /// metadata/raw-data aggregators (the library default),
1541    /// [`FileSpaceStrategy::Aggr`] the aggregators alone, and
1542    /// [`FileSpaceStrategy::None`] neither, so every allocation comes from the
1543    /// end of the file. [`FileSpaceStrategy::Page`] allocates on file-space
1544    /// page boundaries instead, packing everything smaller than a page into
1545    /// pages of its own kind; [`file_space_page_size`](Self::file_space_page_size)
1546    /// sets how big those pages are.
1547    ///
1548    /// `persist` writes the free-space managers into the file on close, so a
1549    /// later session — this crate or libhdf5 — finds the space this one
1550    /// released instead of appending past it. `threshold` is the smallest
1551    /// section a manager records; anything smaller is space the file leaks
1552    /// rather than tracks. Both are ignored for the two strategies that have
1553    /// no managers, exactly as `H5P__set_file_space_strategy` ignores them.
1554    ///
1555    /// Only [`create`](Self::create) reads this. A file that already exists
1556    /// declares its own strategy in its superblock extension, and this crate
1557    /// honours what it finds there.
1558    ///
1559    /// ```no_run
1560    /// use rust_hdf5::{FileSpaceStrategy, H5File};
1561    /// let file = H5File::options()
1562    ///     .file_space(FileSpaceStrategy::FsmAggr, true, 1)
1563    ///     .create("persisting.h5")
1564    ///     .unwrap();
1565    /// # let _ = file;
1566    /// ```
1567    pub fn file_space(
1568        mut self,
1569        strategy: FileSpaceStrategy,
1570        persist: bool,
1571        threshold: u64,
1572    ) -> Self {
1573        self.file_space = Some(FileSpaceConfig::new(strategy, persist, threshold));
1574        self
1575    }
1576
1577    /// `H5Pset_file_space_page_size`, h5py's `File(..., fs_page_size=...)`.
1578    ///
1579    /// The file-space page is the unit [`FileSpaceStrategy::Page`] allocates
1580    /// in: a request smaller than one page is packed into a page holding only
1581    /// that kind of data, and a larger one is page-aligned. `size` is between
1582    /// 512 (`H5F_FILE_SPACE_PAGE_SIZE_MIN`) and 1 GiB — no power of two
1583    /// required — and anything outside that is refused by
1584    /// [`create`](Self::create), as `H5Pset_file_space_page_size` refuses it.
1585    ///
1586    /// Setting it is enough on its own to give the file a file-space info
1587    /// message, because the page size is one of the four properties
1588    /// `H5F__super_init` compares against the library defaults. Under any
1589    /// other strategy that is all it does: the file records the size and
1590    /// allocates without it.
1591    ///
1592    /// Only [`create`](Self::create) reads this. A reopened file keeps the
1593    /// page size its own message carries.
1594    ///
1595    /// ```no_run
1596    /// use rust_hdf5::{FileSpaceStrategy, H5File};
1597    /// let file = H5File::options()
1598    ///     .file_space(FileSpaceStrategy::Page, true, 1)
1599    ///     .file_space_page_size(8192)
1600    ///     .create("paged.h5")
1601    ///     .unwrap();
1602    /// # let _ = file;
1603    /// ```
1604    pub fn file_space_page_size(mut self, size: u64) -> Self {
1605        self.file_space_page_size = Some(size);
1606        self
1607    }
1608
1609    /// The one [`FileSpaceConfig`] the two file-space builders describe
1610    /// between them.
1611    ///
1612    /// They are separate properties of one property list —
1613    /// `H5Pset_file_space_strategy` and `H5Pset_file_space_page_size` write
1614    /// different fcpl entries and neither reads the other — so each is
1615    /// recorded by whether it was called, and joining them here is what keeps
1616    /// either call order meaning the same thing.
1617    fn resolved_file_space(&self) -> FileSpaceConfig {
1618        let config = self.file_space.unwrap_or_default();
1619        match self.file_space_page_size {
1620            Some(size) => config.with_page_size(size),
1621            None => config,
1622        }
1623    }
1624
1625    fn resolved_locking(&self) -> FileLocking {
1626        match self.locking {
1627            Some(p) => p,
1628            None => FileLocking::from_env_or(FileLocking::default()),
1629        }
1630    }
1631
1632    /// Refuse an `open`/`open_rw` call that set an option only [`create`]
1633    /// reads — the fcpl/fapl split each of those setters' docs already
1634    /// describe: `track_order`, `track_times`, `libver`, `userblock` and
1635    /// `shared_messages` all bake into a file at creation, so an existing
1636    /// file's root group,
1637    /// superblock and shared-message table are already fixed by whatever
1638    /// created it. Silently ignoring the option, the previous behavior,
1639    /// hides a builder call that has no effect at all; one gate here checks
1640    /// every such field instead of a scattered check per opener.
1641    ///
1642    /// The `libver` arm tests `is_some`, not inequality against a default
1643    /// value: [`LibverBound::default`] is `Earliest`, so a gate written as
1644    /// `libver != default()` would let the one bound that asks for a whole
1645    /// classic file through unrefused. Whether the builder was *called* is
1646    /// the question, and `Option` is what records it.
1647    ///
1648    /// [`create`]: Self::create
1649    fn refuse_create_only_options(&self) -> Result<()> {
1650        let mut offending = Vec::new();
1651        if self.track_order {
1652            offending.push("track_order");
1653        }
1654        if self.track_times {
1655            offending.push("track_times");
1656        }
1657        if self.libver.is_some() {
1658            offending.push("libver");
1659        }
1660        if self.userblock != 0 {
1661            offending.push("userblock");
1662        }
1663        if self.shared_messages != SharedMessageConfig::default() {
1664            offending.push("shared_messages");
1665        }
1666        if self.file_space.is_some() {
1667            offending.push("file_space");
1668        }
1669        if self.file_space_page_size.is_some() {
1670            offending.push("file_space_page_size");
1671        }
1672        if offending.is_empty() {
1673            Ok(())
1674        } else {
1675            Err(Hdf5Error::InvalidState(format!(
1676                "these options only take effect when creating a file, not when \
1677                 opening an existing one: {}",
1678                offending.join(", ")
1679            )))
1680        }
1681    }
1682
1683    /// The mirror of [`refuse_create_only_options`](Self::refuse_create_only_options)
1684    /// for the options only a read-mode open can honour, refused by the two
1685    /// openers that produce a writer.
1686    ///
1687    /// [`elink_prefix`](Self::elink_prefix) is one because nothing on the
1688    /// write side traverses an external link, so a file opened for writing
1689    /// would silently never use it.
1690    fn refuse_read_only_options(&self) -> Result<()> {
1691        if self.elink_prefix.is_none() {
1692            return Ok(());
1693        }
1694        Err(Hdf5Error::InvalidState(
1695            "these options only take effect when opening a file for reading: \
1696             elink_prefix"
1697                .to_string(),
1698        ))
1699    }
1700
1701    /// Create a new HDF5 file at `path` with the configured options.
1702    pub fn create<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
1703        self.refuse_read_only_options()?;
1704        let writer = Hdf5Writer::create_with_options(
1705            path.as_ref(),
1706            crate::io::writer::FileCreateOptions {
1707                locking: self.resolved_locking(),
1708                track_order: self.track_order,
1709                track_times: self.track_times,
1710                libver: self.libver,
1711                userblock: self.userblock,
1712                shared_messages: self.shared_messages,
1713                file_space: self.resolved_file_space(),
1714            },
1715        )?;
1716        Ok(H5File {
1717            inner: new_shared(H5FileInner::Writer(Box::new(writer))),
1718        })
1719    }
1720
1721    /// Open an existing HDF5 file for reading with the configured options.
1722    pub fn open<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
1723        self.refuse_create_only_options()?;
1724        let mut reader = Hdf5Reader::open_with_locking(path.as_ref(), self.resolved_locking())?;
1725        reader.set_elink_prefix(self.elink_prefix);
1726        Ok(H5File {
1727            inner: new_shared(H5FileInner::Reader(Box::new(reader))),
1728        })
1729    }
1730
1731    /// Open an existing HDF5 file for read/write with the configured options.
1732    pub fn open_rw<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
1733        self.refuse_create_only_options()?;
1734        self.refuse_read_only_options()?;
1735        let writer = Hdf5Writer::open_append_with_locking(path.as_ref(), self.resolved_locking())?;
1736        Ok(H5File {
1737            inner: new_shared(H5FileInner::Writer(Box::new(writer))),
1738        })
1739    }
1740}
1741
1742#[cfg(test)]
1743fn unique_test_path(name: &str) -> std::path::PathBuf {
1744    // PID + atomic counter so each test invocation uses a distinct path,
1745    // preventing collisions across concurrent cargo runs and any
1746    // flock/LockFileEx race where a previous close()'d file's lock
1747    // remains briefly visible when reopening the same path.
1748    use std::sync::atomic::{AtomicU64, Ordering};
1749    static COUNTER: AtomicU64 = AtomicU64::new(0);
1750    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1751    std::env::temp_dir().join(format!(
1752        "rust_hdf5_test_{}_{}_{}.h5",
1753        name,
1754        std::process::id(),
1755        n
1756    ))
1757}
1758
1759#[cfg(test)]
1760mod tests {
1761    use super::*;
1762    use std::path::PathBuf;
1763
1764    fn temp_path(name: &str) -> PathBuf {
1765        super::unique_test_path(name)
1766    }
1767
1768    #[test]
1769    fn create_and_close_empty() {
1770        let path = temp_path("create_empty");
1771        let file = H5File::create(&path).unwrap();
1772        file.close().unwrap();
1773
1774        // Should be readable
1775        let file = H5File::open(&path).unwrap();
1776        file.close().unwrap();
1777
1778        std::fs::remove_file(&path).ok();
1779    }
1780
1781    #[test]
1782    fn create_and_drop_empty() {
1783        let path = temp_path("drop_empty");
1784        {
1785            let _file = H5File::create(&path).unwrap();
1786            // drop auto-finalizes
1787        }
1788        // Verify the file is valid by opening it
1789        let file = H5File::open(&path).unwrap();
1790        file.close().unwrap();
1791
1792        std::fs::remove_file(&path).ok();
1793    }
1794
1795    #[test]
1796    fn dataset_not_found() {
1797        let path = temp_path("ds_not_found");
1798        {
1799            let _file = H5File::create(&path).unwrap();
1800        }
1801        let file = H5File::open(&path).unwrap();
1802        let result = file.dataset("nonexistent");
1803        assert!(result.is_err());
1804
1805        std::fs::remove_file(&path).ok();
1806    }
1807
1808    #[test]
1809    fn write_and_read_roundtrip() {
1810        let path = temp_path("write_read_rt");
1811
1812        // Write
1813        {
1814            let file = H5File::create(&path).unwrap();
1815            let ds = file
1816                .new_dataset::<u8>()
1817                .shape([4, 4])
1818                .create("data")
1819                .unwrap();
1820            ds.write_raw(&[0u8; 16]).unwrap();
1821            file.close().unwrap();
1822        }
1823
1824        // Read
1825        {
1826            let file = H5File::open(&path).unwrap();
1827            let ds = file.dataset("data").unwrap();
1828            assert_eq!(ds.shape(), vec![4, 4]);
1829            let data = ds.read_raw::<u8>().unwrap();
1830            assert_eq!(data.len(), 16);
1831            assert!(data.iter().all(|&b| b == 0));
1832            file.close().unwrap();
1833        }
1834
1835        std::fs::remove_file(&path).ok();
1836    }
1837
1838    #[test]
1839    fn close_no_sync_produces_valid_readable_file() {
1840        let path = temp_path("close_no_sync_rt");
1841        let payload: Vec<u8> = (0u8..16).collect();
1842
1843        // Write and finalize WITHOUT the trailing fsync.
1844        {
1845            let file = H5File::create(&path).unwrap();
1846            let ds = file
1847                .new_dataset::<u8>()
1848                .shape([4, 4])
1849                .create("data")
1850                .unwrap();
1851            ds.write_raw(&payload).unwrap();
1852            // The only difference from `write_and_read_roundtrip`: no fsync.
1853            // The file must still be a complete, valid, readable HDF5 file.
1854            file.close_no_sync().unwrap();
1855        }
1856
1857        // Reopen and verify the full content survived (same-machine reader sees
1858        // the OS page cache regardless of whether fsync ran).
1859        {
1860            let file = H5File::open(&path).unwrap();
1861            let ds = file.dataset("data").unwrap();
1862            assert_eq!(ds.shape(), vec![4, 4]);
1863            let data = ds.read_raw::<u8>().unwrap();
1864            assert_eq!(data, payload);
1865            file.close().unwrap();
1866        }
1867
1868        std::fs::remove_file(&path).ok();
1869    }
1870
1871    #[test]
1872    fn create_over_existing_file_truncates() {
1873        // The create path skips the ftruncate on a brand-new empty file (it
1874        // arms ext4's auto_da_alloc and turns close(2) into an implicit
1875        // writeback, defeating close_no_sync). This pins the other side of
1876        // that guard: creating over an existing non-empty file must still
1877        // truncate it, so no stale content survives.
1878        let path = temp_path("create_truncates");
1879
1880        {
1881            let file = H5File::create(&path).unwrap();
1882            let ds = file
1883                .new_dataset::<u8>()
1884                .shape([4, 4])
1885                .create("old_data")
1886                .unwrap();
1887            ds.write_raw(&[7u8; 16]).unwrap();
1888            file.close().unwrap();
1889        }
1890        assert!(std::fs::metadata(&path).unwrap().len() > 0);
1891
1892        // Re-create over the non-empty file, write nothing.
1893        {
1894            let file = H5File::create(&path).unwrap();
1895            file.close().unwrap();
1896        }
1897
1898        // The old dataset must be gone.
1899        let file = H5File::open(&path).unwrap();
1900        assert!(file.dataset("old_data").is_err());
1901        file.close().unwrap();
1902
1903        std::fs::remove_file(&path).ok();
1904    }
1905
1906    #[test]
1907    fn close_no_sync_chunked_dataset_valid() {
1908        // Exercises flush_dataset_synced(sync=false): a chunked (EA-indexed)
1909        // dataset closed with close_no_sync must skip the per-dataset
1910        // sync_data yet still write valid index structures, so the reopened
1911        // file reconstructs every frame.
1912        let path = temp_path("close_no_sync_chunked");
1913
1914        {
1915            let file = H5File::create(&path).unwrap();
1916            let ds = file
1917                .new_dataset::<i32>()
1918                .shape([0usize, 3])
1919                .chunk(&[1, 3])
1920                .max_shape(&[None, Some(3)])
1921                .create("data")
1922                .unwrap();
1923            // 10 frames exceeds idx_blk_elmts=4, so data blocks are exercised.
1924            for frame in 0..10u64 {
1925                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1926                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1927                ds.write_chunk(frame as usize, &raw).unwrap();
1928            }
1929            ds.extend(&[10, 3]).unwrap();
1930            file.close_no_sync().unwrap();
1931        }
1932
1933        {
1934            let file = H5File::open(&path).unwrap();
1935            let ds = file.dataset("data").unwrap();
1936            assert_eq!(ds.shape(), vec![10, 3]);
1937            let data = ds.read_raw::<i32>().unwrap();
1938            let expected: Vec<i32> = (0..30).collect();
1939            assert_eq!(data, expected);
1940            file.close().unwrap();
1941        }
1942
1943        std::fs::remove_file(&path).ok();
1944    }
1945
1946    #[test]
1947    fn write_and_read_f64() {
1948        let path = temp_path("write_read_f64");
1949
1950        let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1951
1952        // Write
1953        {
1954            let file = H5File::create(&path).unwrap();
1955            let ds = file
1956                .new_dataset::<f64>()
1957                .shape([2, 3])
1958                .create("matrix")
1959                .unwrap();
1960            ds.write_raw(&values).unwrap();
1961            file.close().unwrap();
1962        }
1963
1964        // Read
1965        {
1966            let file = H5File::open(&path).unwrap();
1967            let ds = file.dataset("matrix").unwrap();
1968            assert_eq!(ds.shape(), vec![2, 3]);
1969            let readback = ds.read_raw::<f64>().unwrap();
1970            assert_eq!(readback, values);
1971        }
1972
1973        std::fs::remove_file(&path).ok();
1974    }
1975
1976    #[test]
1977    fn multiple_datasets() {
1978        let path = temp_path("multi_ds");
1979
1980        {
1981            let file = H5File::create(&path).unwrap();
1982            let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
1983            ds1.write_raw(&[10i32, 20, 30]).unwrap();
1984
1985            let ds2 = file
1986                .new_dataset::<f32>()
1987                .shape([2, 2])
1988                .create("floats")
1989                .unwrap();
1990            ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
1991
1992            file.close().unwrap();
1993        }
1994
1995        {
1996            let file = H5File::open(&path).unwrap();
1997
1998            let ds_ints = file.dataset("ints").unwrap();
1999            assert_eq!(ds_ints.shape(), vec![3]);
2000            let ints = ds_ints.read_raw::<i32>().unwrap();
2001            assert_eq!(ints, vec![10, 20, 30]);
2002
2003            let ds_floats = file.dataset("floats").unwrap();
2004            assert_eq!(ds_floats.shape(), vec![2, 2]);
2005            let floats = ds_floats.read_raw::<f32>().unwrap();
2006            assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
2007        }
2008
2009        std::fs::remove_file(&path).ok();
2010    }
2011
2012    #[test]
2013    fn close_is_idempotent() {
2014        let path = temp_path("close_idemp");
2015        let file = H5File::create(&path).unwrap();
2016        file.close().unwrap();
2017        // File is consumed by close(), so no double-close possible at the type level.
2018        std::fs::remove_file(&path).ok();
2019    }
2020}
2021
2022#[cfg(test)]
2023mod integration_tests {
2024    use super::*;
2025
2026    fn temp_path(name: &str) -> std::path::PathBuf {
2027        super::unique_test_path(name)
2028    }
2029
2030    #[test]
2031    fn write_file_for_h5dump() {
2032        let path = temp_path("integration");
2033        let file = H5File::create(&path).unwrap();
2034
2035        let ds = file
2036            .new_dataset::<u8>()
2037            .shape([4usize, 4])
2038            .create("data_u8")
2039            .unwrap();
2040        let data: Vec<u8> = (0..16).collect();
2041        ds.write_raw(&data).unwrap();
2042
2043        let ds2 = file
2044            .new_dataset::<f64>()
2045            .shape([3usize, 2])
2046            .create("data_f64")
2047            .unwrap();
2048        let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
2049        ds2.write_raw(&fdata).unwrap();
2050
2051        let ds3 = file
2052            .new_dataset::<i32>()
2053            .shape([5usize])
2054            .create("values")
2055            .unwrap();
2056        let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
2057        ds3.write_raw(&idata).unwrap();
2058
2059        file.close().unwrap();
2060
2061        // File exists
2062        assert!(path.exists());
2063    }
2064
2065    #[test]
2066    fn write_chunked_file_for_h5dump() {
2067        let path = temp_path("chunked");
2068        let file = H5File::create(&path).unwrap();
2069
2070        // Create a chunked dataset with unlimited first dimension
2071        let ds = file
2072            .new_dataset::<f64>()
2073            .shape([0usize, 4])
2074            .chunk(&[1, 4])
2075            .max_shape(&[None, Some(4)])
2076            .create("streaming_data")
2077            .unwrap();
2078
2079        // Write 5 frames of data
2080        for frame in 0..5u64 {
2081            let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
2082            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
2083            ds.write_chunk(frame as usize, &raw).unwrap();
2084        }
2085
2086        // Extend dimensions to reflect the 5 written frames
2087        ds.extend(&[5, 4]).unwrap();
2088        ds.flush().unwrap();
2089
2090        file.close().unwrap();
2091
2092        assert!(path.exists());
2093    }
2094
2095    #[test]
2096    fn write_chunked_many_frames_for_h5dump() {
2097        let path = temp_path("chunked_many");
2098        let file = H5File::create(&path).unwrap();
2099
2100        let ds = file
2101            .new_dataset::<i32>()
2102            .shape([0usize, 3])
2103            .chunk(&[1, 3])
2104            .max_shape(&[None, Some(3)])
2105            .create("data")
2106            .unwrap();
2107
2108        // Write 10 frames (exceeds idx_blk_elmts=4, uses data blocks)
2109        for frame in 0..10u64 {
2110            let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
2111            let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2112            ds.write_chunk(frame as usize, &raw).unwrap();
2113        }
2114        ds.extend(&[10, 3]).unwrap();
2115        file.close().unwrap();
2116
2117        assert!(path.exists());
2118    }
2119
2120    #[test]
2121    fn write_dataset_with_attributes() {
2122        use crate::types::VarLenUnicode;
2123
2124        let path = temp_path("attributes");
2125        let file = H5File::create(&path).unwrap();
2126
2127        let ds = file
2128            .new_dataset::<f32>()
2129            .shape([10usize])
2130            .create("temperature")
2131            .unwrap();
2132        let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
2133        ds.write_raw(&data).unwrap();
2134
2135        // Add string attributes
2136        let attr = ds
2137            .new_attr::<VarLenUnicode>()
2138            .shape(())
2139            .create("units")
2140            .unwrap();
2141        attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
2142            .unwrap();
2143
2144        let attr2 = ds
2145            .new_attr::<VarLenUnicode>()
2146            .shape(())
2147            .create("description")
2148            .unwrap();
2149        attr2
2150            .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
2151            .unwrap();
2152
2153        // Use write_string convenience method
2154        let attr3 = ds
2155            .new_attr::<VarLenUnicode>()
2156            .shape(())
2157            .create("source")
2158            .unwrap();
2159        attr3.write_string("sensor_01").unwrap();
2160
2161        // Also test parse -> write_scalar pattern
2162        let attr4 = ds
2163            .new_attr::<VarLenUnicode>()
2164            .shape(())
2165            .create("label")
2166            .unwrap();
2167        let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
2168        attr4.write_scalar(&s).unwrap();
2169
2170        file.close().unwrap();
2171
2172        assert!(path.exists());
2173    }
2174
2175    #[test]
2176    fn dataset_writer_reopens_for_attributes() {
2177        // Reopen a dataset by name in write mode (the original handle is gone)
2178        // and attach attributes to it — the close-time flush pattern.
2179        let path = temp_path("dataset_writer");
2180        {
2181            let file = H5File::create(&path).unwrap();
2182            {
2183                let ds = file
2184                    .new_dataset::<u16>()
2185                    .shape([8])
2186                    .create("image")
2187                    .unwrap();
2188                ds.write_raw(&[0u16; 8]).unwrap();
2189                // original handle dropped here
2190            }
2191
2192            // Reopen by name; dataset() would error in write mode.
2193            assert!(file.dataset("image").is_err());
2194            let ds = file.dataset_writer("image").unwrap();
2195            assert_eq!(ds.shape(), vec![8]);
2196            ds.new_attr::<i32>()
2197                .shape([3])
2198                .create("NDArrayDimOffset")
2199                .unwrap()
2200                .write_array(&[0i32, 4, 8])
2201                .unwrap();
2202            ds.new_attr::<i32>()
2203                .shape(())
2204                .create("NDUniqueId")
2205                .unwrap()
2206                .write_numeric(&42i32)
2207                .unwrap();
2208
2209            // Missing dataset is reported.
2210            assert!(matches!(
2211                file.dataset_writer("nope"),
2212                Err(crate::error::Hdf5Error::NotFound(_))
2213            ));
2214
2215            file.close().unwrap();
2216        }
2217        {
2218            let file = H5File::open(&path).unwrap();
2219            let ds = file.dataset("image").unwrap();
2220            let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
2221            let got: Vec<i32> = off
2222                .as_chunks::<4>()
2223                .0
2224                .iter()
2225                .map(|b| i32::from_le_bytes(*b))
2226                .collect();
2227            assert_eq!(got, vec![0, 4, 8]);
2228            let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
2229            assert_eq!(uid, 42);
2230        }
2231        std::fs::remove_file(&path).ok();
2232    }
2233
2234    #[test]
2235    fn chunked_write_read_roundtrip() {
2236        let path = temp_path("chunked_roundtrip");
2237
2238        // Write
2239        {
2240            let file = H5File::create(&path).unwrap();
2241            let ds = file
2242                .new_dataset::<i32>()
2243                .shape([0usize, 3])
2244                .chunk(&[1, 3])
2245                .max_shape(&[None, Some(3)])
2246                .create("table")
2247                .unwrap();
2248
2249            for frame in 0..8u64 {
2250                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
2251                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2252                ds.write_chunk(frame as usize, &raw).unwrap();
2253            }
2254            ds.extend(&[8, 3]).unwrap();
2255            file.close().unwrap();
2256        }
2257
2258        // Read
2259        {
2260            let file = H5File::open(&path).unwrap();
2261            let ds = file.dataset("table").unwrap();
2262            assert_eq!(ds.shape(), vec![8, 3]);
2263            let data = ds.read_raw::<i32>().unwrap();
2264            assert_eq!(data.len(), 24);
2265            for (i, val) in data.iter().enumerate() {
2266                assert_eq!(*val, i as i32);
2267            }
2268        }
2269
2270        std::fs::remove_file(&path).ok();
2271    }
2272
2273    #[test]
2274    #[cfg(feature = "deflate")]
2275    fn compressed_chunked_roundtrip() {
2276        let path = temp_path("compressed_roundtrip");
2277
2278        // Write compressed
2279        {
2280            let file = H5File::create(&path).unwrap();
2281            let ds = file
2282                .new_dataset::<f64>()
2283                .shape([0usize, 4])
2284                .chunk(&[1, 4])
2285                .max_shape(&[None, Some(4)])
2286                .deflate(6)
2287                .create("compressed")
2288                .unwrap();
2289
2290            for frame in 0..10u64 {
2291                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
2292                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2293                ds.write_chunk(frame as usize, &raw).unwrap();
2294            }
2295            ds.extend(&[10, 4]).unwrap();
2296            file.close().unwrap();
2297        }
2298
2299        // Read back and verify
2300        {
2301            let file = H5File::open(&path).unwrap();
2302            let ds = file.dataset("compressed").unwrap();
2303            assert_eq!(ds.shape(), vec![10, 4]);
2304            let data = ds.read_raw::<f64>().unwrap();
2305            assert_eq!(data.len(), 40);
2306            for (i, val) in data.iter().enumerate() {
2307                assert!(
2308                    (val - i as f64).abs() < 1e-10,
2309                    "mismatch at {}: {} != {}",
2310                    i,
2311                    val,
2312                    i
2313                );
2314            }
2315        }
2316
2317        std::fs::remove_file(&path).ok();
2318    }
2319
2320    #[test]
2321    #[cfg(feature = "deflate")]
2322    fn compressed_chunked_many_frames() {
2323        let path = temp_path("compressed_many");
2324
2325        {
2326            let file = H5File::create(&path).unwrap();
2327            let ds = file
2328                .new_dataset::<i32>()
2329                .shape([0usize, 3])
2330                .chunk(&[1, 3])
2331                .max_shape(&[None, Some(3)])
2332                .deflate(6)
2333                .create("stream")
2334                .unwrap();
2335
2336            for frame in 0..100u64 {
2337                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
2338                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2339                ds.write_chunk(frame as usize, &raw).unwrap();
2340            }
2341            ds.extend(&[100, 3]).unwrap();
2342            file.close().unwrap();
2343        }
2344
2345        {
2346            let file = H5File::open(&path).unwrap();
2347            let ds = file.dataset("stream").unwrap();
2348            assert_eq!(ds.shape(), vec![100, 3]);
2349            let data = ds.read_raw::<i32>().unwrap();
2350            assert_eq!(data.len(), 300);
2351            for (i, val) in data.iter().enumerate() {
2352                assert_eq!(*val, i as i32, "mismatch at {}", i);
2353            }
2354        }
2355
2356        std::fs::remove_file(&path).ok();
2357    }
2358    #[test]
2359    fn append_mode() {
2360        let path = temp_path("append");
2361
2362        // Create initial file
2363        {
2364            let file = H5File::create(&path).unwrap();
2365            let ds = file
2366                .new_dataset::<i32>()
2367                .shape([3usize])
2368                .create("first")
2369                .unwrap();
2370            ds.write_raw(&[1i32, 2, 3]).unwrap();
2371            file.close().unwrap();
2372        }
2373
2374        // Append new dataset
2375        {
2376            let file = H5File::open_rw(&path).unwrap();
2377            let ds = file
2378                .new_dataset::<f64>()
2379                .shape([2usize])
2380                .create("second")
2381                .unwrap();
2382            ds.write_raw(&[4.0f64, 5.0]).unwrap();
2383            file.close().unwrap();
2384        }
2385
2386        // Read back both
2387        {
2388            let file = H5File::open(&path).unwrap();
2389            let names = file.dataset_names();
2390            assert!(names.contains(&"first".to_string()));
2391            assert!(names.contains(&"second".to_string()));
2392
2393            let ds1 = file.dataset("first").unwrap();
2394            assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
2395
2396            let ds2 = file.dataset("second").unwrap();
2397            assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
2398        }
2399
2400        std::fs::remove_file(&path).ok();
2401    }
2402
2403    #[test]
2404    fn open_rw_set_attr_preserves_file() {
2405        let path = temp_path("open_rw_attr");
2406        // Create file with a dataset and an attribute
2407        {
2408            let file = H5File::create(&path).unwrap();
2409            let ds = file
2410                .new_dataset::<i32>()
2411                .shape([3usize])
2412                .create("data")
2413                .unwrap();
2414            ds.write_raw(&[10i32, 20, 30]).unwrap();
2415            file.set_attr_string("version", "1.0").unwrap();
2416            file.close().unwrap();
2417        }
2418        // Open rw and modify the attribute
2419        {
2420            let file = H5File::open_rw(&path).unwrap();
2421            file.set_attr_string("version", "2.0").unwrap();
2422            file.close().unwrap();
2423        }
2424        // Verify: dataset intact, attribute updated
2425        {
2426            let file = H5File::open(&path).unwrap();
2427            let ds = file.dataset("data").unwrap();
2428            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
2429            let ver = file.attr_string("version").unwrap();
2430            assert_eq!(ver, "2.0");
2431        }
2432        std::fs::remove_file(&path).ok();
2433    }
2434
2435    #[test]
2436    #[cfg(feature = "deflate")]
2437    fn open_rw_attr_with_compressed_dataset() {
2438        use crate::format::messages::filter::FilterPipeline;
2439        let path = temp_path("open_rw_compressed");
2440        let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
2441        // Create file with compressed vlen strings
2442        {
2443            let file = H5File::create(&path).unwrap();
2444            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
2445                .unwrap();
2446            file.set_attr_string("version", "1.0").unwrap();
2447            file.close().unwrap();
2448        }
2449        // Open rw and modify attribute only
2450        {
2451            let file = H5File::open_rw(&path).unwrap();
2452            file.set_attr_string("version", "2.0").unwrap();
2453            file.close().unwrap();
2454        }
2455        // Verify: compressed dataset still readable, attribute updated
2456        {
2457            let file = H5File::open(&path).unwrap();
2458            let ds = file.dataset("texts").unwrap();
2459            let strings = ds.read_vlen_strings().unwrap();
2460            assert_eq!(strings.len(), 50);
2461            assert_eq!(strings[0], "test string data");
2462            let ver = file.attr_string("version").unwrap();
2463            assert_eq!(ver, "2.0");
2464        }
2465        std::fs::remove_file(&path).ok();
2466    }
2467
2468    #[test]
2469    #[cfg(feature = "lz4")]
2470    fn append_vlen_strings_basic() {
2471        use crate::format::messages::filter::FilterPipeline;
2472        let path = temp_path("append_vlen");
2473        {
2474            let file = H5File::create(&path).unwrap();
2475            file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
2476                .unwrap();
2477            file.append_vlen_strings("names", &["alice", "bob", "charlie"])
2478                .unwrap();
2479            file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
2480            file.close().unwrap();
2481        }
2482        {
2483            let file = H5File::open(&path).unwrap();
2484            let ds = file.dataset("names").unwrap();
2485            let strings = ds.read_vlen_strings().unwrap();
2486            assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
2487        }
2488        std::fs::remove_file(&path).ok();
2489    }
2490
2491    #[test]
2492    #[cfg(feature = "lz4")]
2493    fn append_vlen_strings_large() {
2494        use crate::format::messages::filter::FilterPipeline;
2495        let path = temp_path("append_vlen_large");
2496        let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
2497        let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
2498        {
2499            let file = H5File::create(&path).unwrap();
2500            file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
2501                .unwrap();
2502            let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
2503            file.append_vlen_strings("data", &r1).unwrap();
2504            let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
2505            file.append_vlen_strings("data", &r2).unwrap();
2506            file.close().unwrap();
2507        }
2508        {
2509            let file = H5File::open(&path).unwrap();
2510            let ds = file.dataset("data").unwrap();
2511            let strings = ds.read_vlen_strings().unwrap();
2512            assert_eq!(strings.len(), 7189);
2513            assert_eq!(strings[0], "node-000000");
2514            assert_eq!(strings[7188], "node-007188");
2515        }
2516        std::fs::remove_file(&path).ok();
2517    }
2518
2519    #[test]
2520    fn append_vlen_strings_uncompressed() {
2521        let path = temp_path("append_vlen_unc");
2522        {
2523            let file = H5File::create(&path).unwrap();
2524            file.create_appendable_vlen_dataset("texts", 8, None)
2525                .unwrap();
2526            file.append_vlen_strings("texts", &["hello", "world"])
2527                .unwrap();
2528            file.append_vlen_strings("texts", &["foo", "bar", "baz"])
2529                .unwrap();
2530            file.close().unwrap();
2531        }
2532        {
2533            let file = H5File::open(&path).unwrap();
2534            let ds = file.dataset("texts").unwrap();
2535            let strings = ds.read_vlen_strings().unwrap();
2536            assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
2537        }
2538        std::fs::remove_file(&path).ok();
2539    }
2540
2541    #[test]
2542    fn delete_dataset_roundtrip() {
2543        let path = temp_path("delete_ds");
2544        {
2545            let file = H5File::create(&path).unwrap();
2546            file.write_vlen_strings("keep", &["a", "b"]).unwrap();
2547            file.write_vlen_strings("remove", &["x", "y"]).unwrap();
2548            file.delete_dataset("remove").unwrap();
2549            file.close().unwrap();
2550        }
2551        {
2552            let file = H5File::open(&path).unwrap();
2553            let names = file.dataset_names();
2554            assert!(names.contains(&"keep".to_string()));
2555            assert!(!names.contains(&"remove".to_string()));
2556            let ds = file.dataset("keep").unwrap();
2557            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
2558        }
2559        std::fs::remove_file(&path).ok();
2560    }
2561
2562    #[test]
2563    fn delete_group_roundtrip() {
2564        let path = temp_path("delete_grp");
2565        {
2566            let file = H5File::create(&path).unwrap();
2567            let g1 = file.create_group("keep").unwrap();
2568            g1.write_vlen_strings("data", &["a"]).unwrap();
2569            let g2 = file.create_group("remove").unwrap();
2570            g2.write_vlen_strings("data", &["x"]).unwrap();
2571            file.delete_group("remove").unwrap();
2572            file.close().unwrap();
2573        }
2574        {
2575            let file = H5File::open(&path).unwrap();
2576            let names = file.dataset_names();
2577            assert!(names.contains(&"keep/data".to_string()));
2578            assert!(!names.contains(&"remove/data".to_string()));
2579        }
2580        std::fs::remove_file(&path).ok();
2581    }
2582
2583    #[test]
2584    fn open_rw_delete_recreate_group() {
2585        let path = temp_path("rw_delete_recreate");
2586        // Step 1: create file with groups
2587        {
2588            let file = H5File::create(&path).unwrap();
2589            let n = file.create_group("nodes").unwrap();
2590            n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
2591            let e = file.create_group("edges").unwrap();
2592            e.write_vlen_strings("src", &["x", "y"]).unwrap();
2593            file.close().unwrap();
2594        }
2595        // Step 2: open_rw, delete one group, recreate with new data
2596        {
2597            let file = H5File::open_rw(&path).unwrap();
2598            file.delete_group("nodes").unwrap();
2599            let n = file.create_group("nodes").unwrap();
2600            n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
2601            file.close().unwrap();
2602        }
2603        // Step 3: verify
2604        {
2605            let file = H5File::open(&path).unwrap();
2606            let ds = file.dataset("nodes/id").unwrap();
2607            let s = ds.read_vlen_strings().unwrap();
2608            assert_eq!(s, vec!["new1", "new2"]);
2609            // edges should still be intact
2610            let ds = file.dataset("edges/src").unwrap();
2611            let s = ds.read_vlen_strings().unwrap();
2612            assert_eq!(s, vec!["x", "y"]);
2613        }
2614        std::fs::remove_file(&path).ok();
2615    }
2616
2617    #[test]
2618    fn delete_and_recreate_group() {
2619        let path = temp_path("delete_recreate");
2620        {
2621            let file = H5File::create(&path).unwrap();
2622            let g = file.create_group("nodes").unwrap();
2623            g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
2624            file.delete_group("nodes").unwrap();
2625            let g = file.create_group("nodes").unwrap();
2626            g.write_vlen_strings("id", &["new1", "new2", "new3"])
2627                .unwrap();
2628            file.close().unwrap();
2629        }
2630        {
2631            let file = H5File::open(&path).unwrap();
2632            let ds = file.dataset("nodes/id").unwrap();
2633            let strings = ds.read_vlen_strings().unwrap();
2634            assert_eq!(strings, vec!["new1", "new2", "new3"]);
2635        }
2636        std::fs::remove_file(&path).ok();
2637    }
2638
2639    #[test]
2640    #[cfg(feature = "deflate")]
2641    fn vlen_string_compressed_large_roundtrip() {
2642        use crate::format::messages::filter::FilterPipeline;
2643        let path = temp_path("vlen_large");
2644        // Simulate kodex scenario: 7189 strings, chunk_size 512
2645        let input: Vec<String> = (0..7189)
2646            .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
2647            .collect();
2648        let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
2649        {
2650            let file = H5File::create(&path).unwrap();
2651            file.create_group("nodes").unwrap();
2652            file.write_vlen_strings_compressed(
2653                "nodes/id",
2654                &input_refs,
2655                512,
2656                FilterPipeline::deflate(6),
2657            )
2658            .unwrap();
2659            file.close().unwrap();
2660        }
2661        // Read back
2662        {
2663            let file = H5File::open(&path).unwrap();
2664            let ds = file.dataset("nodes/id").unwrap();
2665            let strings = ds.read_vlen_strings().unwrap();
2666            assert_eq!(strings.len(), 7189);
2667            assert_eq!(strings[0], input[0]);
2668            assert_eq!(strings[7188], input[7188]);
2669        }
2670        // Also test open_rw then re-read
2671        {
2672            let file = H5File::open_rw(&path).unwrap();
2673            file.set_attr_string("version", "1.0").unwrap();
2674            file.close().unwrap();
2675        }
2676        {
2677            let file = H5File::open(&path).unwrap();
2678            let ds = file.dataset("nodes/id").unwrap();
2679            let strings = ds.read_vlen_strings().unwrap();
2680            assert_eq!(strings.len(), 7189);
2681            assert_eq!(strings[0], input[0]);
2682        }
2683        std::fs::remove_file(&path).ok();
2684    }
2685
2686    #[test]
2687    fn vlen_string_write_read() {
2688        let path = temp_path("vlen_wr");
2689        {
2690            let file = H5File::create(&path).unwrap();
2691            file.write_vlen_strings("names", &["alice", "bob", "charlie"])
2692                .unwrap();
2693            file.close().unwrap();
2694        }
2695        {
2696            let file = H5File::open(&path).unwrap();
2697            let ds = file.dataset("names").unwrap();
2698            let strings = ds.read_vlen_strings().unwrap();
2699            assert_eq!(strings, vec!["alice", "bob", "charlie"]);
2700        }
2701        std::fs::remove_file(&path).ok();
2702    }
2703
2704    /// The one-call writers declare the character set they are named for —
2705    /// `write_vlen_strings` UTF-8, `write_vlen_strings_ascii` ASCII — and the
2706    /// ASCII one refuses a string its declaration would misdescribe, before
2707    /// anything reaches the file.
2708    #[test]
2709    fn vlen_string_writers_declare_their_character_set() {
2710        use crate::format::messages::datatype::DatatypeMessage;
2711
2712        let path = temp_path("vlen_cset");
2713        let file = H5File::create(&path).unwrap();
2714        file.write_vlen_strings_ascii("ascii", &["alpha", "b", ""])
2715            .unwrap();
2716        file.write_vlen_strings("utf8", &["été", "日本"]).unwrap();
2717        let err = file
2718            .write_vlen_strings_ascii("rejected", &["ok", "안녕"])
2719            .err()
2720            .expect("a non-ASCII string was accepted under an ASCII datatype")
2721            .to_string();
2722        assert!(
2723            err.contains("string 1") && err.contains("is not ASCII"),
2724            "got: {err}"
2725        );
2726        file.close().unwrap();
2727
2728        let file = H5File::open(&path).unwrap();
2729        let ascii = file.dataset("ascii").unwrap();
2730        assert_eq!(
2731            ascii.datatype().unwrap(),
2732            DatatypeMessage::VarLenString {
2733                padding: 0,
2734                charset: 0,
2735            }
2736        );
2737        assert_eq!(ascii.read_strings().unwrap(), vec!["alpha", "b", ""]);
2738        let utf8 = file.dataset("utf8").unwrap();
2739        assert_eq!(
2740            utf8.datatype().unwrap(),
2741            DatatypeMessage::VarLenString {
2742                padding: 0,
2743                charset: 1,
2744            }
2745        );
2746        assert_eq!(utf8.read_strings().unwrap(), vec!["été", "日本"]);
2747        // The refused write left nothing behind.
2748        assert!(file.dataset("rejected").is_err());
2749        std::fs::remove_file(&path).ok();
2750    }
2751
2752    /// The group-level twin declares ASCII the same way the file-level one
2753    /// does, for a dataset inside the group.
2754    #[test]
2755    fn group_vlen_string_writer_declares_ascii() {
2756        use crate::format::messages::datatype::DatatypeMessage;
2757
2758        let path = temp_path("vlen_cset_group");
2759        let file = H5File::create(&path).unwrap();
2760        let g = file.create_group("entry").unwrap();
2761        g.write_vlen_strings_ascii("notes", &["alpha", "b"])
2762            .unwrap();
2763        let err = g
2764            .write_vlen_strings_ascii("rejected", &["안녕"])
2765            .err()
2766            .expect("a non-ASCII string was accepted under an ASCII datatype")
2767            .to_string();
2768        assert!(err.contains("is not ASCII"), "got: {err}");
2769        file.close().unwrap();
2770
2771        let file = H5File::open(&path).unwrap();
2772        let ds = file.dataset("entry/notes").unwrap();
2773        assert_eq!(
2774            ds.datatype().unwrap(),
2775            DatatypeMessage::VarLenString {
2776                padding: 0,
2777                charset: 0,
2778            }
2779        );
2780        assert_eq!(ds.read_strings().unwrap(), vec!["alpha", "b"]);
2781        std::fs::remove_file(&path).ok();
2782    }
2783
2784    #[test]
2785    fn vlen_bytes_write_read() {
2786        let path = temp_path("vlen_bytes_wr");
2787        let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
2788        {
2789            let file = H5File::create(&path).unwrap();
2790            file.write_vlen_bytes("blobs", &items).unwrap();
2791            file.close().unwrap();
2792        }
2793        {
2794            let file = H5File::open(&path).unwrap();
2795            let ds = file.dataset("blobs").unwrap();
2796            let got = ds.read_vlen_bytes().unwrap();
2797            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
2798            assert_eq!(got, expected);
2799        }
2800        std::fs::remove_file(&path).ok();
2801    }
2802
2803    /// A vlen sequence over a wider base stores element counts, not byte
2804    /// counts, in the `H5T_VLEN` length field, and the datatype names the
2805    /// base — so the file says what it holds for every width.
2806    #[test]
2807    fn vlen_numeric_write_read() {
2808        use crate::format::global_heap::decode_vlen_reference;
2809        use crate::format::messages::datatype::DatatypeMessage;
2810
2811        let path = temp_path("vlen_numeric_wr");
2812        let a: &[i32] = &[1, 2, 3];
2813        let b: &[i32] = &[];
2814        let c: &[i32] = &[-7];
2815        {
2816            let file = H5File::create(&path).unwrap();
2817            file.write_vlen_numeric("data", &[a, b, c]).unwrap();
2818            let f64s: &[f64] = &[1.5, -2.5];
2819            file.write_vlen_numeric("wide", &[f64s]).unwrap();
2820            file.close().unwrap();
2821        }
2822        let file = H5File::open(&path).unwrap();
2823        let ds = file.dataset("data").unwrap();
2824        assert_eq!(
2825            ds.datatype().unwrap(),
2826            DatatypeMessage::VarLenSequence {
2827                base: Box::new(DatatypeMessage::i32_type()),
2828            }
2829        );
2830        let decoded: Vec<Vec<i32>> = ds
2831            .read_vlen_bytes()
2832            .unwrap()
2833            .iter()
2834            .map(|item| {
2835                item.as_chunks::<4>()
2836                    .0
2837                    .iter()
2838                    .map(|w| i32::from_le_bytes(*w))
2839                    .collect()
2840            })
2841            .collect();
2842        assert_eq!(decoded, vec![a.to_vec(), b.to_vec(), c.to_vec()]);
2843
2844        // The length field counts elements: 3 i32s, not 12 bytes.
2845        let ctx = crate::format::FormatContext::default_v3();
2846        let raw = ds.read_raw_bytes().unwrap();
2847        let (seq_len, _, _) = decode_vlen_reference(&raw, &ctx).unwrap();
2848        assert_eq!(seq_len, 3);
2849
2850        let wide = file.dataset("wide").unwrap();
2851        assert_eq!(
2852            wide.datatype().unwrap(),
2853            DatatypeMessage::VarLenSequence {
2854                base: Box::new(DatatypeMessage::f64_type()),
2855            }
2856        );
2857        let (seq_len, _, _) = decode_vlen_reference(&wide.read_raw_bytes().unwrap(), &ctx).unwrap();
2858        assert_eq!(seq_len, 2);
2859        drop(file);
2860        std::fs::remove_file(&path).ok();
2861    }
2862
2863    /// `write_vlen_bytes` is the `u8` case of the same writer, so the byte
2864    /// datatype and the byte-per-element length field are unchanged.
2865    #[test]
2866    fn vlen_bytes_is_the_u8_case_of_vlen_numeric() {
2867        use crate::format::messages::datatype::DatatypeMessage;
2868
2869        let path = temp_path("vlen_bytes_u8");
2870        let items: [&[u8]; 2] = [b"abc", b""];
2871        {
2872            let file = H5File::create(&path).unwrap();
2873            file.write_vlen_bytes("blobs", &items).unwrap();
2874            file.close().unwrap();
2875        }
2876        let file = H5File::open(&path).unwrap();
2877        let ds = file.dataset("blobs").unwrap();
2878        assert_eq!(ds.datatype().unwrap(), DatatypeMessage::vlen_bytes());
2879        let ctx = crate::format::FormatContext::default_v3();
2880        let (seq_len, _, _) =
2881            crate::format::global_heap::decode_vlen_reference(&ds.read_raw_bytes().unwrap(), &ctx)
2882                .unwrap();
2883        assert_eq!(seq_len, 3);
2884        drop(file);
2885        std::fs::remove_file(&path).ok();
2886    }
2887
2888    #[test]
2889    fn vlen_bytes_in_group_with_attribute() {
2890        use crate::types::VarLenUnicode;
2891        let path = temp_path("vlen_bytes_grp");
2892        let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
2893        {
2894            let file = H5File::create(&path).unwrap();
2895            let grp = file.root_group().create_group("payloads").unwrap();
2896            let ds = grp.write_vlen_bytes("frames", &items).unwrap();
2897            ds.new_attr::<VarLenUnicode>()
2898                .shape(())
2899                .create("codec")
2900                .unwrap()
2901                .write_string("raw")
2902                .unwrap();
2903            file.close().unwrap();
2904        }
2905        {
2906            let file = H5File::open(&path).unwrap();
2907            let ds = file.dataset("payloads/frames").unwrap();
2908            let got = ds.read_vlen_bytes().unwrap();
2909            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
2910            assert_eq!(got, expected);
2911        }
2912        std::fs::remove_file(&path).ok();
2913    }
2914
2915    #[test]
2916    fn vlen_dataset_returns_handle_for_attributes() {
2917        use crate::types::VarLenUnicode;
2918        let path = temp_path("vlen_attr");
2919        {
2920            let file = H5File::create(&path).unwrap();
2921            let grp = file.root_group().create_group("ch").unwrap();
2922            // The vlen helper now returns the dataset handle, so attributes can
2923            // be attached directly — the issue the mdfr reporter hit.
2924            let ds = grp
2925                .write_vlen_strings("labels", &["a", "bb", "ccc"])
2926                .unwrap();
2927            ds.new_attr::<VarLenUnicode>()
2928                .shape(())
2929                .create("unit")
2930                .unwrap()
2931                .write_string("volt")
2932                .unwrap();
2933            // The same dataset can also be reopened by name within the group.
2934            let ds2 = grp.dataset_writer("labels").unwrap();
2935            ds2.new_attr::<VarLenUnicode>()
2936                .shape(())
2937                .create("desc")
2938                .unwrap()
2939                .write_string("channel labels")
2940                .unwrap();
2941            file.close().unwrap();
2942        }
2943        {
2944            let file = H5File::open(&path).unwrap();
2945            let ds = file.dataset("ch/labels").unwrap();
2946            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
2947            assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
2948            assert_eq!(
2949                ds.attr("desc").unwrap().read_string().unwrap(),
2950                "channel labels"
2951            );
2952        }
2953        std::fs::remove_file(&path).ok();
2954    }
2955
2956    #[test]
2957    #[cfg(feature = "deflate")]
2958    fn vlen_string_deflate_roundtrip() {
2959        use crate::format::messages::filter::FilterPipeline;
2960        let path = temp_path("vlen_deflate");
2961        let input: Vec<&str> = (0..100)
2962            .map(|i| match i % 3 {
2963                0 => "hello world",
2964                1 => "compressed vlen string test",
2965                _ => "rust-hdf5",
2966            })
2967            .collect();
2968        {
2969            let file = H5File::create(&path).unwrap();
2970            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
2971                .unwrap();
2972            file.close().unwrap();
2973        }
2974        {
2975            let file = H5File::open(&path).unwrap();
2976            let ds = file.dataset("texts").unwrap();
2977            let strings = ds.read_vlen_strings().unwrap();
2978            assert_eq!(strings.len(), 100);
2979            for (i, s) in strings.iter().enumerate() {
2980                assert_eq!(s, input[i]);
2981            }
2982        }
2983        std::fs::remove_file(&path).ok();
2984    }
2985
2986    #[test]
2987    #[cfg(feature = "zstd")]
2988    fn vlen_string_zstd_roundtrip() {
2989        use crate::format::messages::filter::FilterPipeline;
2990        let path = temp_path("vlen_zstd");
2991        let input: Vec<&str> = (0..200)
2992            .map(|i| match i % 4 {
2993                0 => "zstandard compression test",
2994                1 => "variable length string",
2995                2 => "rust-hdf5 chunked storage",
2996                _ => "hello zstd world",
2997            })
2998            .collect();
2999        {
3000            let file = H5File::create(&path).unwrap();
3001            file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
3002                .unwrap();
3003            file.close().unwrap();
3004        }
3005        {
3006            let file = H5File::open(&path).unwrap();
3007            let ds = file.dataset("data").unwrap();
3008            let strings = ds.read_vlen_strings().unwrap();
3009            assert_eq!(strings.len(), 200);
3010            for (i, s) in strings.iter().enumerate() {
3011                assert_eq!(s, input[i]);
3012            }
3013        }
3014        std::fs::remove_file(&path).ok();
3015    }
3016
3017    #[test]
3018    #[cfg(feature = "deflate")]
3019    fn shuffle_deflate_roundtrip() {
3020        let path = temp_path("shuf_defl");
3021        {
3022            let file = H5File::create(&path).unwrap();
3023            let ds = file
3024                .new_dataset::<f64>()
3025                .shape([0usize, 4])
3026                .chunk(&[1, 4])
3027                .max_shape(&[None, Some(4)])
3028                .shuffle_deflate(6)
3029                .create("data")
3030                .unwrap();
3031            for frame in 0..20u64 {
3032                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
3033                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3034                ds.write_chunk(frame as usize, &raw).unwrap();
3035            }
3036            ds.extend(&[20, 4]).unwrap();
3037            file.close().unwrap();
3038        }
3039        {
3040            let file = H5File::open(&path).unwrap();
3041            let ds = file.dataset("data").unwrap();
3042            assert_eq!(ds.shape(), vec![20, 4]);
3043            let data = ds.read_raw::<f64>().unwrap();
3044            assert_eq!(data.len(), 80);
3045            for (i, val) in data.iter().enumerate() {
3046                assert!((val - i as f64).abs() < 1e-10);
3047            }
3048        }
3049        std::fs::remove_file(&path).ok();
3050    }
3051
3052    #[test]
3053    fn file_level_attributes() {
3054        let path = temp_path("file_attr");
3055        {
3056            let file = H5File::create(&path).unwrap();
3057            file.set_attr_string("title", "Test File").unwrap();
3058            file.set_attr_numeric("version", &42i32).unwrap();
3059            let ds = file
3060                .new_dataset::<u8>()
3061                .shape([1usize])
3062                .create("dummy")
3063                .unwrap();
3064            ds.write_raw(&[0u8]).unwrap();
3065            file.close().unwrap();
3066        }
3067        {
3068            let file = H5File::open(&path).unwrap();
3069            assert!(file.dataset_names().contains(&"dummy".to_string()));
3070
3071            // Read file-level attributes
3072            let names = file.attr_names().unwrap();
3073            assert!(names.contains(&"title".to_string()));
3074
3075            let title = file.attr_string("title").unwrap();
3076            assert_eq!(title, "Test File");
3077        }
3078        std::fs::remove_file(&path).ok();
3079    }
3080
3081    #[test]
3082    fn scalar_dataset_roundtrip() {
3083        let path = temp_path("scalar");
3084        {
3085            let file = H5File::create(&path).unwrap();
3086            let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
3087            ds.write_raw(&[std::f64::consts::PI]).unwrap();
3088            file.close().unwrap();
3089        }
3090        {
3091            let file = H5File::open(&path).unwrap();
3092            let ds = file.dataset("pi").unwrap();
3093            assert_eq!(ds.shape(), Vec::<usize>::new());
3094            assert_eq!(ds.total_elements(), 1);
3095            let data = ds.read_raw::<f64>().unwrap();
3096            assert_eq!(data.len(), 1);
3097            assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
3098        }
3099        std::fs::remove_file(&path).ok();
3100    }
3101
3102    #[test]
3103    fn append_mode_extend_chunked() {
3104        let path = temp_path("append_extend");
3105
3106        // Create with 5 frames
3107        {
3108            let file = H5File::create(&path).unwrap();
3109            let ds = file
3110                .new_dataset::<i32>()
3111                .shape([0usize, 3])
3112                .chunk(&[1, 3])
3113                .max_shape(&[None, Some(3)])
3114                .create("stream")
3115                .unwrap();
3116            for i in 0..5u64 {
3117                let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
3118                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3119                ds.write_chunk(i as usize, &raw).unwrap();
3120            }
3121            ds.extend(&[5, 3]).unwrap();
3122            file.close().unwrap();
3123        }
3124
3125        // Reopen and add 5 more frames
3126        {
3127            let file = H5File::open_rw(&path).unwrap();
3128            // Find the stream dataset index (it's the first one)
3129            let names = file.dataset_names();
3130            assert!(names.contains(&"stream".to_string()));
3131
3132            // Write more chunks via the writer directly
3133            let mut inner = crate::file::borrow_inner_mut(&file.inner);
3134            if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
3135                let ds_idx = writer.dataset_index("stream").unwrap();
3136                for i in 5..10u64 {
3137                    let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
3138                    let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3139                    writer.write_chunk(ds_idx, i, &raw).unwrap();
3140                }
3141                writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
3142            }
3143            drop(inner);
3144            file.close().unwrap();
3145        }
3146
3147        // Read back all 10 frames
3148        {
3149            let file = H5File::open(&path).unwrap();
3150            let ds = file.dataset("stream").unwrap();
3151            assert_eq!(ds.shape(), vec![10, 3]);
3152            let data = ds.read_raw::<i32>().unwrap();
3153            assert_eq!(data.len(), 30);
3154            for (i, val) in data.iter().enumerate() {
3155                assert_eq!(*val, i as i32, "mismatch at {}", i);
3156            }
3157        }
3158
3159        std::fs::remove_file(&path).ok();
3160    }
3161
3162    #[test]
3163    fn group_hierarchy_roundtrip() {
3164        let path = temp_path("groups_rt");
3165
3166        {
3167            let file = H5File::create(&path).unwrap();
3168            let root = file.root_group();
3169
3170            // Create groups
3171            let det = root.create_group("detector").unwrap();
3172            let raw = det.create_group("raw").unwrap();
3173
3174            // Create datasets in groups
3175            let ds1 = det
3176                .new_dataset::<f32>()
3177                .shape([10usize])
3178                .create("temperature")
3179                .unwrap();
3180            ds1.write_raw(&[1.0f32; 10]).unwrap();
3181
3182            let ds2 = raw
3183                .new_dataset::<u16>()
3184                .shape([4usize, 4])
3185                .create("image")
3186                .unwrap();
3187            ds2.write_raw(&[42u16; 16]).unwrap();
3188
3189            // Root-level dataset
3190            let ds3 = file
3191                .new_dataset::<i32>()
3192                .shape([3usize])
3193                .create("version")
3194                .unwrap();
3195            ds3.write_raw(&[1i32, 0, 0]).unwrap();
3196
3197            file.close().unwrap();
3198        }
3199
3200        {
3201            let file = H5File::open(&path).unwrap();
3202            let names = file.dataset_names();
3203            assert!(names.contains(&"version".to_string()));
3204            assert!(names.contains(&"detector/temperature".to_string()));
3205            assert!(names.contains(&"detector/raw/image".to_string()));
3206
3207            // Read datasets
3208            let ds = file.dataset("version").unwrap();
3209            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
3210
3211            let ds = file.dataset("detector/temperature").unwrap();
3212            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
3213
3214            let ds = file.dataset("detector/raw/image").unwrap();
3215            assert_eq!(ds.shape(), vec![4, 4]);
3216            assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
3217
3218            // Group traversal
3219            let root = file.root_group();
3220            let group_names = root.group_names().unwrap();
3221            assert!(group_names.contains(&"detector".to_string()));
3222        }
3223
3224        std::fs::remove_file(&path).ok();
3225    }
3226
3227    #[test]
3228    fn nested_groups_via_file_create_group() {
3229        let path = temp_path("file_create_group");
3230
3231        {
3232            let file = H5File::create(&path).unwrap();
3233
3234            // Use the H5File::create_group convenience method
3235            let grp = file.create_group("sensors").unwrap();
3236            let sub = grp.create_group("accel").unwrap();
3237
3238            let ds = sub
3239                .new_dataset::<f64>()
3240                .shape([3usize])
3241                .create("xyz")
3242                .unwrap();
3243            ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
3244
3245            file.close().unwrap();
3246        }
3247
3248        {
3249            let file = H5File::open(&path).unwrap();
3250            let names = file.dataset_names();
3251            assert!(names.contains(&"sensors/accel/xyz".to_string()));
3252
3253            let ds = file.dataset("sensors/accel/xyz").unwrap();
3254            assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
3255
3256            // Open group in read mode
3257            let root = file.root_group();
3258            let sensors = root.group("sensors").unwrap();
3259            assert_eq!(sensors.name(), "/sensors");
3260
3261            let accel = sensors.group("accel").unwrap();
3262            assert_eq!(accel.name(), "/sensors/accel");
3263
3264            // list_groups from root
3265            let top_groups = root.group_names().unwrap();
3266            assert!(top_groups.contains(&"sensors".to_string()));
3267
3268            // list_groups from sensors
3269            let sub_groups = sensors.group_names().unwrap();
3270            assert!(sub_groups.contains(&"accel".to_string()));
3271        }
3272
3273        std::fs::remove_file(&path).ok();
3274    }
3275}
3276
3277#[cfg(test)]
3278mod h5py_compat_tests {
3279    use super::*;
3280
3281    fn temp_path(name: &str) -> std::path::PathBuf {
3282        super::unique_test_path(name)
3283    }
3284
3285    /// Verify our files can be read by h5dump (if available).
3286    #[test]
3287    #[cfg(feature = "deflate")]
3288    fn h5dump_validates_our_files() {
3289        // Check if h5dump is available
3290        let h5dump = std::process::Command::new("h5dump")
3291            .arg("--version")
3292            .output();
3293        if h5dump.is_err() {
3294            eprintln!("skipping: h5dump not found");
3295            return;
3296        }
3297
3298        let path = temp_path("h5dump_validate");
3299
3300        // Write a comprehensive test file
3301        {
3302            let file = H5File::create(&path).unwrap();
3303
3304            // Contiguous
3305            let ds = file
3306                .new_dataset::<f64>()
3307                .shape([3usize, 4])
3308                .create("matrix")
3309                .unwrap();
3310            let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
3311            ds.write_raw(&data).unwrap();
3312
3313            // Chunked + compressed
3314            let ds2 = file
3315                .new_dataset::<i32>()
3316                .shape([0usize, 2])
3317                .chunk(&[1, 2])
3318                .max_shape(&[None, Some(2)])
3319                .deflate(6)
3320                .create("stream")
3321                .unwrap();
3322            for i in 0..5u64 {
3323                let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
3324                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3325                ds2.write_chunk(i as usize, &raw).unwrap();
3326            }
3327            ds2.extend(&[5, 2]).unwrap();
3328
3329            // Group
3330            let grp = file.create_group("meta").unwrap();
3331            let ds3 = grp
3332                .new_dataset::<u8>()
3333                .shape([4usize])
3334                .create("flags")
3335                .unwrap();
3336            ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
3337
3338            // String attribute
3339            use crate::types::VarLenUnicode;
3340            let attr = ds
3341                .new_attr::<VarLenUnicode>()
3342                .shape(())
3343                .create("units")
3344                .unwrap();
3345            attr.write_string("meters").unwrap();
3346
3347            file.close().unwrap();
3348        }
3349
3350        // Run h5dump and verify exit code
3351        let output = std::process::Command::new("h5dump")
3352            .arg("-H") // header only (faster)
3353            .arg(path.to_str().unwrap())
3354            .output()
3355            .unwrap();
3356
3357        assert!(
3358            output.status.success(),
3359            "h5dump failed:\nstdout: {}\nstderr: {}",
3360            String::from_utf8_lossy(&output.stdout),
3361            String::from_utf8_lossy(&output.stderr),
3362        );
3363
3364        // Full dump (with data) should also work
3365        let output2 = std::process::Command::new("h5dump")
3366            .arg(path.to_str().unwrap())
3367            .output()
3368            .unwrap();
3369
3370        assert!(
3371            output2.status.success(),
3372            "h5dump (full) failed:\nstderr: {}",
3373            String::from_utf8_lossy(&output2.stderr),
3374        );
3375
3376        std::fs::remove_file(&path).ok();
3377    }
3378
3379    #[test]
3380    fn read_h5py_generated_file() {
3381        let path = "/tmp/test_h5py_default.h5";
3382        if !std::path::Path::new(path).exists() {
3383            eprintln!("skipping: h5py test file not found");
3384            return;
3385        }
3386        let file = H5File::open(path).unwrap();
3387
3388        let ds = file.dataset("data").unwrap();
3389        assert_eq!(ds.shape(), vec![4, 5]);
3390        let data = ds.read_raw::<f64>().unwrap();
3391        assert_eq!(data.len(), 20);
3392        assert!((data[0]).abs() < 1e-10);
3393        assert!((data[19] - 19.0).abs() < 1e-10);
3394
3395        let ds2 = file.dataset("images").unwrap();
3396        assert_eq!(ds2.shape(), vec![3, 64, 64]);
3397        let images = ds2.read_raw::<u16>().unwrap();
3398        assert_eq!(images.len(), 3 * 64 * 64);
3399    }
3400
3401    /// `track_order`, `libver`, `userblock` and `shared_messages` only take
3402    /// effect on [`H5FileOptions::create`]; setting any of them for
3403    /// [`H5FileOptions::open_rw`] on an already-created file must be
3404    /// refused, naming the option, rather than silently doing nothing.
3405    #[test]
3406    fn open_rw_refuses_every_create_only_option() {
3407        let path = temp_path("open_rw_refuses");
3408        H5File::create(&path).unwrap().close().unwrap();
3409
3410        let err = H5File::options()
3411            .track_order(true)
3412            .open_rw(&path)
3413            .err()
3414            .unwrap();
3415        assert!(err.to_string().contains("track_order"), "{err}");
3416
3417        let err = H5File::options()
3418            .libver(LibverBound::V110)
3419            .open_rw(&path)
3420            .err()
3421            .unwrap();
3422        assert!(err.to_string().contains("libver"), "{err}");
3423
3424        // Every bound, not just the ones that differ from `LibverBound`'s
3425        // own default. `Earliest` *is* that default and is the bound that
3426        // asks for a classic file, so a gate comparing against the default
3427        // value would pass this call through as if nothing had been set.
3428        for bound in [
3429            LibverBound::Earliest,
3430            LibverBound::V18,
3431            LibverBound::V112,
3432            LibverBound::V114,
3433            LibverBound::V200,
3434        ] {
3435            let err = H5File::options()
3436                .libver(bound)
3437                .open_rw(&path)
3438                .err()
3439                .unwrap();
3440            assert!(err.to_string().contains("libver"), "{bound:?}: {err}");
3441        }
3442
3443        let err = H5File::options()
3444            .userblock(512)
3445            .open_rw(&path)
3446            .err()
3447            .unwrap();
3448        assert!(err.to_string().contains("userblock"), "{err}");
3449
3450        let types = crate::format::sohm::type_flag(crate::format::messages::MSG_DATATYPE).unwrap();
3451        let err = H5File::options()
3452            .shared_messages(&[(types, 0)], 50, 40)
3453            .open_rw(&path)
3454            .err()
3455            .unwrap();
3456        assert!(err.to_string().contains("shared_messages"), "{err}");
3457
3458        // A default builder — no create-only option touched — still opens.
3459        H5File::options().open_rw(&path).unwrap().close().unwrap();
3460
3461        std::fs::remove_file(&path).ok();
3462    }
3463
3464    /// [`H5FileOptions::open`] shares the same gate as `open_rw` — it goes
3465    /// through the same `refuse_create_only_options` check.
3466    #[test]
3467    fn open_refuses_a_create_only_option() {
3468        let path = temp_path("open_refuses");
3469        H5File::create(&path).unwrap().close().unwrap();
3470
3471        let err = H5File::options()
3472            .track_order(true)
3473            .open(&path)
3474            .err()
3475            .unwrap();
3476        assert!(err.to_string().contains("track_order"), "{err}");
3477
3478        H5File::options().open(&path).unwrap().close().unwrap();
3479
3480        std::fs::remove_file(&path).ok();
3481    }
3482}