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