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::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`).
1323    pub fn no_locking(self) -> Self {
1324        self.locking(FileLocking::Disabled)
1325    }
1326
1327    /// Try to acquire the lock but do not fail if the filesystem rejects it
1328    /// (equivalent to `HDF5_USE_FILE_LOCKING=BEST_EFFORT`).
1329    pub fn best_effort_locking(self) -> Self {
1330        self.locking(FileLocking::BestEffort)
1331    }
1332
1333    /// `H5Pset_elink_prefix` (H5Plapl.c:923): a directory the *target file*
1334    /// of an external link is looked for under, after `HDF5_EXT_PREFIX` and
1335    /// before the linking file's own directory — step 3 of
1336    /// `H5F_prefix_open_file`'s order (H5Fint.c:938-950).
1337    ///
1338    /// Unlike [`DatasetAccess::virtual_prefix`](crate::DatasetAccess), this
1339    /// one is *not* shadowed by its environment variable and gets no
1340    /// `${ORIGIN}` expansion: `H5L__extern_traverse` peeks the property
1341    /// verbatim and hands it straight to the search (H5Lexternal.c:210-215),
1342    /// with no `H5D__build_file_prefix` step in between. Measured against
1343    /// libhdf5 1.14.6 and 2.0.0: with `HDF5_EXT_PREFIX` naming a directory
1344    /// that has no target, this property still resolves it, while the same
1345    /// arrangement for a virtual source does not; and a `${ORIGIN}` written
1346    /// here stays a literal directory name.
1347    ///
1348    /// # Where this lives, and why not on the call
1349    ///
1350    /// libhdf5 keeps it in a *link access* property list, an argument every
1351    /// `H5*_by_name` call carries. Here it is a property of the open,
1352    /// because that is the narrowest scope this crate can honour: a reader
1353    /// resolves each external link's file name once and then holds that
1354    /// answer for its own life, so a prefix passed per call could not change
1355    /// a name another call had already resolved. Making it file-scoped also
1356    /// keeps it with the one other cross-file policy libhdf5 takes from a
1357    /// property list and applies to every file a path touches — the locking
1358    /// mode — and, like that one, it propagates down a chain of links, which
1359    /// is what a lapl does upstream (measured: a two-hop chain resolves its
1360    /// second hop under the prefix given at the first).
1361    ///
1362    /// Read-side only: nothing the writer does traverses an external link.
1363    pub fn elink_prefix(mut self, prefix: impl Into<String>) -> Self {
1364        self.elink_prefix = Some(prefix.into());
1365        self
1366    }
1367
1368    /// Create the file's root group with creation-order tracking, and make
1369    /// that the policy for objects created in it — h5py's
1370    /// `File(path, "w", track_order=True)`.
1371    ///
1372    /// Only [`create`](Self::create) reads this; opening an existing file
1373    /// takes the policy from the root group already on disk. Change it for
1374    /// later objects with [`H5File::set_track_order`].
1375    pub fn track_order(mut self, track: bool) -> Self {
1376        self.track_order = track;
1377        self
1378    }
1379
1380    /// Create the file's root group recording its times, and make that the
1381    /// policy for objects created in it — `H5Pset_obj_track_times`, h5py's
1382    /// `File(path, "w", track_times=True)`.
1383    ///
1384    /// An object recording times keeps the ones its header version can hold:
1385    /// four in a version-2 header's prefix, one modification time in a
1386    /// version-1 dataset's `H5O_MTIME_NEW` message, and none at all in a
1387    /// version-1 group or committed datatype, which have nowhere to put one.
1388    ///
1389    /// Off unless this says otherwise — h5py's default, not libhdf5's. h5py's
1390    /// high-level API passes `track_times=False` for every object it makes
1391    /// (`_hl/files.py:189`, `_hl/dataset.py:39`, `_hl/group.py:42`), while a
1392    /// bare creation property list leaves it on (`H5O_CRT_OHDR_FLAGS_DEF` is
1393    /// `H5O_HDR_STORE_TIMES`, H5Opkg.h:74), which is what `h5py.h5d.create`
1394    /// and libhdf5's own C API get.
1395    ///
1396    /// Only [`create`](Self::create) reads this; the root group of an existing
1397    /// file was made under whatever created it. Change it for later objects
1398    /// with [`H5File::set_track_times`].
1399    pub fn track_times(mut self, track: bool) -> Self {
1400        self.track_times = track;
1401        self
1402    }
1403
1404    /// Create the file under a library-version low bound — h5py's
1405    /// `File(path, "w", libver=("v108", "v108"))`, libhdf5's
1406    /// `H5Pset_libver_bounds` `low` argument.
1407    ///
1408    /// The bound decides the superblock version the file is written with
1409    /// ([`LibverBound::superblock_version`]) as well as the message versions
1410    /// of the objects created in it, so unlike
1411    /// [`H5File::set_libver_bound`] — which only reaches objects created
1412    /// after the call — it applies to the file itself.
1413    ///
1414    /// [`LibverBound::Earliest`] asks for the whole classic generation, the
1415    /// file libhdf5 writes at `H5F_LIBVER_EARLIEST`: a version-0 superblock,
1416    /// a symbol-table root group, version-1 object headers, symbol-table
1417    /// subgroups and the version-1 B-tree chunk index. Such a file is
1418    /// readable by libhdf5 1.6, and correspondingly gives up everything
1419    /// newer — SWMR ([`crate::swmr`]) and virtual datasets are refused in
1420    /// it, and a chunk larger than 4 GiB does not fit its index key.
1421    ///
1422    /// [`LibverBound::V18`] asks for the file libhdf5 writes at
1423    /// `H5F_LIBVER_V18`: a version-2 superblock over link-message groups and
1424    /// version-2 object headers, but still the version-3 data layout message
1425    /// and so still the version-1 B-tree chunk index — `H5O_layout_ver_bounds`
1426    /// does not reach version 4 until `V110`, and the v1.10 indexes live in
1427    /// nothing older. SWMR is refused in such a file: its status flags need a
1428    /// version-3 superblock, which this bound's row does not reach.
1429    ///
1430    /// Not calling this at all is *not* the same as asking for `Earliest`,
1431    /// nor for `V18`: the default file has the version-2 superblock and
1432    /// link-message groups of the v1.8 bound over the v1.10 chunk indexes,
1433    /// which no single bound describes.
1434    ///
1435    /// Only [`create`](Self::create) reads this; an existing file keeps the
1436    /// superblock it already has.
1437    ///
1438    /// ```no_run
1439    /// use rust_hdf5::{H5File, LibverBound};
1440    /// let file = H5File::options()
1441    ///     .libver(LibverBound::V110)
1442    ///     .create("v110.h5")
1443    ///     .unwrap();
1444    /// # let _ = file;
1445    /// ```
1446    pub fn libver(mut self, libver: LibverBound) -> Self {
1447        self.libver = Some(libver);
1448        self
1449    }
1450
1451    /// Reserve `size` bytes in front of the superblock for the application's
1452    /// own use — h5py's `File(path, "w", userblock_size=512)`, libhdf5's
1453    /// `H5Pset_userblock`.
1454    ///
1455    /// The block is the file's first `size` bytes and belongs to whoever
1456    /// writes it: an executable header, a checksum, a provenance record. HDF5
1457    /// itself only skips it — the superblock and every address in the file are
1458    /// based at `size`, and a reader finds the superblock by looking at offset
1459    /// 0 and then at [`MIN_USERBLOCK`](crate::MIN_USERBLOCK) doubled
1460    /// repeatedly, which is why the size must be zero (no block) or a power of
1461    /// two of at least that many bytes. [`create`](Self::create) reports any
1462    /// other size as an error; it is not rounded up.
1463    ///
1464    /// This crate writes the block zero-filled and never reads it back, so
1465    /// filling it is a plain write to the front of the file after
1466    /// [`H5File::close`].
1467    ///
1468    /// ```no_run
1469    /// use rust_hdf5::H5File;
1470    /// let file = H5File::options().userblock(512).create("prefixed.h5").unwrap();
1471    /// assert_eq!(file.userblock_size(), 512);
1472    /// ```
1473    pub fn userblock(mut self, size: u64) -> Self {
1474        self.userblock = size;
1475        self
1476    }
1477
1478    /// Create the file with shared object header messages — libhdf5's
1479    /// `H5Pset_shared_mesg_nindexes` + `H5Pset_shared_mesg_index` +
1480    /// `H5Pset_shared_mesg_phase_change`, which h5py exposes no binding for.
1481    ///
1482    /// A message class covered by an index is written once into a
1483    /// shared-message fractal heap, and every object header that would have
1484    /// held that exact body holds a pointer to it instead. `indexes` gives
1485    /// one `(message types, minimum message size)` pair per index, where the
1486    /// type mask is built from
1487    /// [`type_flag`](crate::format::sohm::type_flag); `list_max` and
1488    /// `btree_min` are the file-wide counts at which an index changes between
1489    /// list and v2 B-tree form.
1490    ///
1491    /// Only [`create`](Self::create) reads this, and it refuses a
1492    /// configuration libhdf5 would refuse: more than eight indexes, an index
1493    /// covering no type, or thresholds that overlap.
1494    ///
1495    /// ```no_run
1496    /// use rust_hdf5::{H5File, format::sohm::type_flag};
1497    /// use rust_hdf5::format::messages::{MSG_ATTRIBUTE, MSG_DATASPACE, MSG_DATATYPE};
1498    ///
1499    /// let types = type_flag(MSG_DATATYPE).unwrap()
1500    ///     | type_flag(MSG_DATASPACE).unwrap()
1501    ///     | type_flag(MSG_ATTRIBUTE).unwrap();
1502    /// let file = H5File::options()
1503    ///     .shared_messages(&[(types, 0)], 50, 40)
1504    ///     .create("sohm.h5")
1505    ///     .unwrap();
1506    /// # let _ = file;
1507    /// ```
1508    pub fn shared_messages(
1509        mut self,
1510        indexes: &[(u16, u32)],
1511        list_max: u16,
1512        btree_min: u16,
1513    ) -> Self {
1514        self.shared_messages = SharedMessageConfig::new(indexes, list_max, btree_min);
1515        self
1516    }
1517
1518    /// Create the file under a file-space handling strategy — libhdf5's
1519    /// `H5Pset_file_space_strategy`, h5py's `File(..., fs_strategy=...,
1520    /// fs_persist=..., fs_threshold=...)`.
1521    ///
1522    /// `strategy` picks how released space is reused:
1523    /// [`FileSpaceStrategy::FsmAggr`] keeps free-space managers and the
1524    /// metadata/raw-data aggregators (the library default),
1525    /// [`FileSpaceStrategy::Aggr`] the aggregators alone, and
1526    /// [`FileSpaceStrategy::None`] neither, so every allocation comes from the
1527    /// end of the file. [`FileSpaceStrategy::Page`] allocates on file-space
1528    /// page boundaries instead, packing everything smaller than a page into
1529    /// pages of its own kind; [`file_space_page_size`](Self::file_space_page_size)
1530    /// sets how big those pages are.
1531    ///
1532    /// `persist` writes the free-space managers into the file on close, so a
1533    /// later session — this crate or libhdf5 — finds the space this one
1534    /// released instead of appending past it. `threshold` is the smallest
1535    /// section a manager records; anything smaller is space the file leaks
1536    /// rather than tracks. Both are ignored for the two strategies that have
1537    /// no managers, exactly as `H5P__set_file_space_strategy` ignores them.
1538    ///
1539    /// Only [`create`](Self::create) reads this. A file that already exists
1540    /// declares its own strategy in its superblock extension, and this crate
1541    /// honours what it finds there.
1542    ///
1543    /// ```no_run
1544    /// use rust_hdf5::{FileSpaceStrategy, H5File};
1545    /// let file = H5File::options()
1546    ///     .file_space(FileSpaceStrategy::FsmAggr, true, 1)
1547    ///     .create("persisting.h5")
1548    ///     .unwrap();
1549    /// # let _ = file;
1550    /// ```
1551    pub fn file_space(
1552        mut self,
1553        strategy: FileSpaceStrategy,
1554        persist: bool,
1555        threshold: u64,
1556    ) -> Self {
1557        self.file_space = Some(FileSpaceConfig::new(strategy, persist, threshold));
1558        self
1559    }
1560
1561    /// `H5Pset_file_space_page_size`, h5py's `File(..., fs_page_size=...)`.
1562    ///
1563    /// The file-space page is the unit [`FileSpaceStrategy::Page`] allocates
1564    /// in: a request smaller than one page is packed into a page holding only
1565    /// that kind of data, and a larger one is page-aligned. `size` is between
1566    /// 512 (`H5F_FILE_SPACE_PAGE_SIZE_MIN`) and 1 GiB — no power of two
1567    /// required — and anything outside that is refused by
1568    /// [`create`](Self::create), as `H5Pset_file_space_page_size` refuses it.
1569    ///
1570    /// Setting it is enough on its own to give the file a file-space info
1571    /// message, because the page size is one of the four properties
1572    /// `H5F__super_init` compares against the library defaults. Under any
1573    /// other strategy that is all it does: the file records the size and
1574    /// allocates without it.
1575    ///
1576    /// Only [`create`](Self::create) reads this. A reopened file keeps the
1577    /// page size its own message carries.
1578    ///
1579    /// ```no_run
1580    /// use rust_hdf5::{FileSpaceStrategy, H5File};
1581    /// let file = H5File::options()
1582    ///     .file_space(FileSpaceStrategy::Page, true, 1)
1583    ///     .file_space_page_size(8192)
1584    ///     .create("paged.h5")
1585    ///     .unwrap();
1586    /// # let _ = file;
1587    /// ```
1588    pub fn file_space_page_size(mut self, size: u64) -> Self {
1589        self.file_space_page_size = Some(size);
1590        self
1591    }
1592
1593    /// The one [`FileSpaceConfig`] the two file-space builders describe
1594    /// between them.
1595    ///
1596    /// They are separate properties of one property list —
1597    /// `H5Pset_file_space_strategy` and `H5Pset_file_space_page_size` write
1598    /// different fcpl entries and neither reads the other — so each is
1599    /// recorded by whether it was called, and joining them here is what keeps
1600    /// either call order meaning the same thing.
1601    fn resolved_file_space(&self) -> FileSpaceConfig {
1602        let config = self.file_space.unwrap_or_default();
1603        match self.file_space_page_size {
1604            Some(size) => config.with_page_size(size),
1605            None => config,
1606        }
1607    }
1608
1609    fn resolved_locking(&self) -> FileLocking {
1610        match self.locking {
1611            Some(p) => p,
1612            None => FileLocking::from_env_or(FileLocking::default()),
1613        }
1614    }
1615
1616    /// Refuse an `open`/`open_rw` call that set an option only [`create`]
1617    /// reads — the fcpl/fapl split each of those setters' docs already
1618    /// describe: `track_order`, `track_times`, `libver`, `userblock` and
1619    /// `shared_messages` all bake into a file at creation, so an existing
1620    /// file's root group,
1621    /// superblock and shared-message table are already fixed by whatever
1622    /// created it. Silently ignoring the option, the previous behavior,
1623    /// hides a builder call that has no effect at all; one gate here checks
1624    /// every such field instead of a scattered check per opener.
1625    ///
1626    /// The `libver` arm tests `is_some`, not inequality against a default
1627    /// value: [`LibverBound::default`] is `Earliest`, so a gate written as
1628    /// `libver != default()` would let the one bound that asks for a whole
1629    /// classic file through unrefused. Whether the builder was *called* is
1630    /// the question, and `Option` is what records it.
1631    ///
1632    /// [`create`]: Self::create
1633    fn refuse_create_only_options(&self) -> Result<()> {
1634        let mut offending = Vec::new();
1635        if self.track_order {
1636            offending.push("track_order");
1637        }
1638        if self.track_times {
1639            offending.push("track_times");
1640        }
1641        if self.libver.is_some() {
1642            offending.push("libver");
1643        }
1644        if self.userblock != 0 {
1645            offending.push("userblock");
1646        }
1647        if self.shared_messages != SharedMessageConfig::default() {
1648            offending.push("shared_messages");
1649        }
1650        if self.file_space.is_some() {
1651            offending.push("file_space");
1652        }
1653        if self.file_space_page_size.is_some() {
1654            offending.push("file_space_page_size");
1655        }
1656        if offending.is_empty() {
1657            Ok(())
1658        } else {
1659            Err(Hdf5Error::InvalidState(format!(
1660                "these options only take effect when creating a file, not when \
1661                 opening an existing one: {}",
1662                offending.join(", ")
1663            )))
1664        }
1665    }
1666
1667    /// The mirror of [`refuse_create_only_options`](Self::refuse_create_only_options)
1668    /// for the options only a read-mode open can honour, refused by the two
1669    /// openers that produce a writer.
1670    ///
1671    /// [`elink_prefix`](Self::elink_prefix) is one because nothing on the
1672    /// write side traverses an external link, so a file opened for writing
1673    /// would silently never use it.
1674    fn refuse_read_only_options(&self) -> Result<()> {
1675        if self.elink_prefix.is_none() {
1676            return Ok(());
1677        }
1678        Err(Hdf5Error::InvalidState(
1679            "these options only take effect when opening a file for reading: \
1680             elink_prefix"
1681                .to_string(),
1682        ))
1683    }
1684
1685    /// Create a new HDF5 file at `path` with the configured options.
1686    pub fn create<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
1687        self.refuse_read_only_options()?;
1688        let writer = Hdf5Writer::create_with_options(
1689            path.as_ref(),
1690            crate::io::writer::FileCreateOptions {
1691                locking: self.resolved_locking(),
1692                track_order: self.track_order,
1693                track_times: self.track_times,
1694                libver: self.libver,
1695                userblock: self.userblock,
1696                shared_messages: self.shared_messages,
1697                file_space: self.resolved_file_space(),
1698            },
1699        )?;
1700        Ok(H5File {
1701            inner: new_shared(H5FileInner::Writer(Box::new(writer))),
1702        })
1703    }
1704
1705    /// Open an existing HDF5 file for reading with the configured options.
1706    pub fn open<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
1707        self.refuse_create_only_options()?;
1708        let mut reader = Hdf5Reader::open_with_locking(path.as_ref(), self.resolved_locking())?;
1709        reader.set_elink_prefix(self.elink_prefix);
1710        Ok(H5File {
1711            inner: new_shared(H5FileInner::Reader(Box::new(reader))),
1712        })
1713    }
1714
1715    /// Open an existing HDF5 file for read/write with the configured options.
1716    pub fn open_rw<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
1717        self.refuse_create_only_options()?;
1718        self.refuse_read_only_options()?;
1719        let writer = Hdf5Writer::open_append_with_locking(path.as_ref(), self.resolved_locking())?;
1720        Ok(H5File {
1721            inner: new_shared(H5FileInner::Writer(Box::new(writer))),
1722        })
1723    }
1724}
1725
1726#[cfg(test)]
1727fn unique_test_path(name: &str) -> std::path::PathBuf {
1728    // PID + atomic counter so each test invocation uses a distinct path,
1729    // preventing collisions across concurrent cargo runs and any
1730    // flock/LockFileEx race where a previous close()'d file's lock
1731    // remains briefly visible when reopening the same path.
1732    use std::sync::atomic::{AtomicU64, Ordering};
1733    static COUNTER: AtomicU64 = AtomicU64::new(0);
1734    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1735    std::env::temp_dir().join(format!(
1736        "rust_hdf5_test_{}_{}_{}.h5",
1737        name,
1738        std::process::id(),
1739        n
1740    ))
1741}
1742
1743#[cfg(test)]
1744mod tests {
1745    use super::*;
1746    use std::path::PathBuf;
1747
1748    fn temp_path(name: &str) -> PathBuf {
1749        super::unique_test_path(name)
1750    }
1751
1752    #[test]
1753    fn create_and_close_empty() {
1754        let path = temp_path("create_empty");
1755        let file = H5File::create(&path).unwrap();
1756        file.close().unwrap();
1757
1758        // Should be readable
1759        let file = H5File::open(&path).unwrap();
1760        file.close().unwrap();
1761
1762        std::fs::remove_file(&path).ok();
1763    }
1764
1765    #[test]
1766    fn create_and_drop_empty() {
1767        let path = temp_path("drop_empty");
1768        {
1769            let _file = H5File::create(&path).unwrap();
1770            // drop auto-finalizes
1771        }
1772        // Verify the file is valid by opening it
1773        let file = H5File::open(&path).unwrap();
1774        file.close().unwrap();
1775
1776        std::fs::remove_file(&path).ok();
1777    }
1778
1779    #[test]
1780    fn dataset_not_found() {
1781        let path = temp_path("ds_not_found");
1782        {
1783            let _file = H5File::create(&path).unwrap();
1784        }
1785        let file = H5File::open(&path).unwrap();
1786        let result = file.dataset("nonexistent");
1787        assert!(result.is_err());
1788
1789        std::fs::remove_file(&path).ok();
1790    }
1791
1792    #[test]
1793    fn write_and_read_roundtrip() {
1794        let path = temp_path("write_read_rt");
1795
1796        // Write
1797        {
1798            let file = H5File::create(&path).unwrap();
1799            let ds = file
1800                .new_dataset::<u8>()
1801                .shape([4, 4])
1802                .create("data")
1803                .unwrap();
1804            ds.write_raw(&[0u8; 16]).unwrap();
1805            file.close().unwrap();
1806        }
1807
1808        // Read
1809        {
1810            let file = H5File::open(&path).unwrap();
1811            let ds = file.dataset("data").unwrap();
1812            assert_eq!(ds.shape(), vec![4, 4]);
1813            let data = ds.read_raw::<u8>().unwrap();
1814            assert_eq!(data.len(), 16);
1815            assert!(data.iter().all(|&b| b == 0));
1816            file.close().unwrap();
1817        }
1818
1819        std::fs::remove_file(&path).ok();
1820    }
1821
1822    #[test]
1823    fn close_no_sync_produces_valid_readable_file() {
1824        let path = temp_path("close_no_sync_rt");
1825        let payload: Vec<u8> = (0u8..16).collect();
1826
1827        // Write and finalize WITHOUT the trailing fsync.
1828        {
1829            let file = H5File::create(&path).unwrap();
1830            let ds = file
1831                .new_dataset::<u8>()
1832                .shape([4, 4])
1833                .create("data")
1834                .unwrap();
1835            ds.write_raw(&payload).unwrap();
1836            // The only difference from `write_and_read_roundtrip`: no fsync.
1837            // The file must still be a complete, valid, readable HDF5 file.
1838            file.close_no_sync().unwrap();
1839        }
1840
1841        // Reopen and verify the full content survived (same-machine reader sees
1842        // the OS page cache regardless of whether fsync ran).
1843        {
1844            let file = H5File::open(&path).unwrap();
1845            let ds = file.dataset("data").unwrap();
1846            assert_eq!(ds.shape(), vec![4, 4]);
1847            let data = ds.read_raw::<u8>().unwrap();
1848            assert_eq!(data, payload);
1849            file.close().unwrap();
1850        }
1851
1852        std::fs::remove_file(&path).ok();
1853    }
1854
1855    #[test]
1856    fn create_over_existing_file_truncates() {
1857        // The create path skips the ftruncate on a brand-new empty file (it
1858        // arms ext4's auto_da_alloc and turns close(2) into an implicit
1859        // writeback, defeating close_no_sync). This pins the other side of
1860        // that guard: creating over an existing non-empty file must still
1861        // truncate it, so no stale content survives.
1862        let path = temp_path("create_truncates");
1863
1864        {
1865            let file = H5File::create(&path).unwrap();
1866            let ds = file
1867                .new_dataset::<u8>()
1868                .shape([4, 4])
1869                .create("old_data")
1870                .unwrap();
1871            ds.write_raw(&[7u8; 16]).unwrap();
1872            file.close().unwrap();
1873        }
1874        assert!(std::fs::metadata(&path).unwrap().len() > 0);
1875
1876        // Re-create over the non-empty file, write nothing.
1877        {
1878            let file = H5File::create(&path).unwrap();
1879            file.close().unwrap();
1880        }
1881
1882        // The old dataset must be gone.
1883        let file = H5File::open(&path).unwrap();
1884        assert!(file.dataset("old_data").is_err());
1885        file.close().unwrap();
1886
1887        std::fs::remove_file(&path).ok();
1888    }
1889
1890    #[test]
1891    fn close_no_sync_chunked_dataset_valid() {
1892        // Exercises flush_dataset_synced(sync=false): a chunked (EA-indexed)
1893        // dataset closed with close_no_sync must skip the per-dataset
1894        // sync_data yet still write valid index structures, so the reopened
1895        // file reconstructs every frame.
1896        let path = temp_path("close_no_sync_chunked");
1897
1898        {
1899            let file = H5File::create(&path).unwrap();
1900            let ds = file
1901                .new_dataset::<i32>()
1902                .shape([0usize, 3])
1903                .chunk(&[1, 3])
1904                .max_shape(&[None, Some(3)])
1905                .create("data")
1906                .unwrap();
1907            // 10 frames exceeds idx_blk_elmts=4, so data blocks are exercised.
1908            for frame in 0..10u64 {
1909                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1910                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1911                ds.write_chunk(frame as usize, &raw).unwrap();
1912            }
1913            ds.extend(&[10, 3]).unwrap();
1914            file.close_no_sync().unwrap();
1915        }
1916
1917        {
1918            let file = H5File::open(&path).unwrap();
1919            let ds = file.dataset("data").unwrap();
1920            assert_eq!(ds.shape(), vec![10, 3]);
1921            let data = ds.read_raw::<i32>().unwrap();
1922            let expected: Vec<i32> = (0..30).collect();
1923            assert_eq!(data, expected);
1924            file.close().unwrap();
1925        }
1926
1927        std::fs::remove_file(&path).ok();
1928    }
1929
1930    #[test]
1931    fn write_and_read_f64() {
1932        let path = temp_path("write_read_f64");
1933
1934        let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1935
1936        // Write
1937        {
1938            let file = H5File::create(&path).unwrap();
1939            let ds = file
1940                .new_dataset::<f64>()
1941                .shape([2, 3])
1942                .create("matrix")
1943                .unwrap();
1944            ds.write_raw(&values).unwrap();
1945            file.close().unwrap();
1946        }
1947
1948        // Read
1949        {
1950            let file = H5File::open(&path).unwrap();
1951            let ds = file.dataset("matrix").unwrap();
1952            assert_eq!(ds.shape(), vec![2, 3]);
1953            let readback = ds.read_raw::<f64>().unwrap();
1954            assert_eq!(readback, values);
1955        }
1956
1957        std::fs::remove_file(&path).ok();
1958    }
1959
1960    #[test]
1961    fn multiple_datasets() {
1962        let path = temp_path("multi_ds");
1963
1964        {
1965            let file = H5File::create(&path).unwrap();
1966            let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
1967            ds1.write_raw(&[10i32, 20, 30]).unwrap();
1968
1969            let ds2 = file
1970                .new_dataset::<f32>()
1971                .shape([2, 2])
1972                .create("floats")
1973                .unwrap();
1974            ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
1975
1976            file.close().unwrap();
1977        }
1978
1979        {
1980            let file = H5File::open(&path).unwrap();
1981
1982            let ds_ints = file.dataset("ints").unwrap();
1983            assert_eq!(ds_ints.shape(), vec![3]);
1984            let ints = ds_ints.read_raw::<i32>().unwrap();
1985            assert_eq!(ints, vec![10, 20, 30]);
1986
1987            let ds_floats = file.dataset("floats").unwrap();
1988            assert_eq!(ds_floats.shape(), vec![2, 2]);
1989            let floats = ds_floats.read_raw::<f32>().unwrap();
1990            assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
1991        }
1992
1993        std::fs::remove_file(&path).ok();
1994    }
1995
1996    #[test]
1997    fn close_is_idempotent() {
1998        let path = temp_path("close_idemp");
1999        let file = H5File::create(&path).unwrap();
2000        file.close().unwrap();
2001        // File is consumed by close(), so no double-close possible at the type level.
2002        std::fs::remove_file(&path).ok();
2003    }
2004}
2005
2006#[cfg(test)]
2007mod integration_tests {
2008    use super::*;
2009
2010    fn temp_path(name: &str) -> std::path::PathBuf {
2011        super::unique_test_path(name)
2012    }
2013
2014    #[test]
2015    fn write_file_for_h5dump() {
2016        let path = temp_path("integration");
2017        let file = H5File::create(&path).unwrap();
2018
2019        let ds = file
2020            .new_dataset::<u8>()
2021            .shape([4usize, 4])
2022            .create("data_u8")
2023            .unwrap();
2024        let data: Vec<u8> = (0..16).collect();
2025        ds.write_raw(&data).unwrap();
2026
2027        let ds2 = file
2028            .new_dataset::<f64>()
2029            .shape([3usize, 2])
2030            .create("data_f64")
2031            .unwrap();
2032        let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
2033        ds2.write_raw(&fdata).unwrap();
2034
2035        let ds3 = file
2036            .new_dataset::<i32>()
2037            .shape([5usize])
2038            .create("values")
2039            .unwrap();
2040        let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
2041        ds3.write_raw(&idata).unwrap();
2042
2043        file.close().unwrap();
2044
2045        // File exists
2046        assert!(path.exists());
2047    }
2048
2049    #[test]
2050    fn write_chunked_file_for_h5dump() {
2051        let path = temp_path("chunked");
2052        let file = H5File::create(&path).unwrap();
2053
2054        // Create a chunked dataset with unlimited first dimension
2055        let ds = file
2056            .new_dataset::<f64>()
2057            .shape([0usize, 4])
2058            .chunk(&[1, 4])
2059            .max_shape(&[None, Some(4)])
2060            .create("streaming_data")
2061            .unwrap();
2062
2063        // Write 5 frames of data
2064        for frame in 0..5u64 {
2065            let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
2066            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
2067            ds.write_chunk(frame as usize, &raw).unwrap();
2068        }
2069
2070        // Extend dimensions to reflect the 5 written frames
2071        ds.extend(&[5, 4]).unwrap();
2072        ds.flush().unwrap();
2073
2074        file.close().unwrap();
2075
2076        assert!(path.exists());
2077    }
2078
2079    #[test]
2080    fn write_chunked_many_frames_for_h5dump() {
2081        let path = temp_path("chunked_many");
2082        let file = H5File::create(&path).unwrap();
2083
2084        let ds = file
2085            .new_dataset::<i32>()
2086            .shape([0usize, 3])
2087            .chunk(&[1, 3])
2088            .max_shape(&[None, Some(3)])
2089            .create("data")
2090            .unwrap();
2091
2092        // Write 10 frames (exceeds idx_blk_elmts=4, uses data blocks)
2093        for frame in 0..10u64 {
2094            let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
2095            let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2096            ds.write_chunk(frame as usize, &raw).unwrap();
2097        }
2098        ds.extend(&[10, 3]).unwrap();
2099        file.close().unwrap();
2100
2101        assert!(path.exists());
2102    }
2103
2104    #[test]
2105    fn write_dataset_with_attributes() {
2106        use crate::types::VarLenUnicode;
2107
2108        let path = temp_path("attributes");
2109        let file = H5File::create(&path).unwrap();
2110
2111        let ds = file
2112            .new_dataset::<f32>()
2113            .shape([10usize])
2114            .create("temperature")
2115            .unwrap();
2116        let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
2117        ds.write_raw(&data).unwrap();
2118
2119        // Add string attributes
2120        let attr = ds
2121            .new_attr::<VarLenUnicode>()
2122            .shape(())
2123            .create("units")
2124            .unwrap();
2125        attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
2126            .unwrap();
2127
2128        let attr2 = ds
2129            .new_attr::<VarLenUnicode>()
2130            .shape(())
2131            .create("description")
2132            .unwrap();
2133        attr2
2134            .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
2135            .unwrap();
2136
2137        // Use write_string convenience method
2138        let attr3 = ds
2139            .new_attr::<VarLenUnicode>()
2140            .shape(())
2141            .create("source")
2142            .unwrap();
2143        attr3.write_string("sensor_01").unwrap();
2144
2145        // Also test parse -> write_scalar pattern
2146        let attr4 = ds
2147            .new_attr::<VarLenUnicode>()
2148            .shape(())
2149            .create("label")
2150            .unwrap();
2151        let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
2152        attr4.write_scalar(&s).unwrap();
2153
2154        file.close().unwrap();
2155
2156        assert!(path.exists());
2157    }
2158
2159    #[test]
2160    fn dataset_writer_reopens_for_attributes() {
2161        // Reopen a dataset by name in write mode (the original handle is gone)
2162        // and attach attributes to it — the close-time flush pattern.
2163        let path = temp_path("dataset_writer");
2164        {
2165            let file = H5File::create(&path).unwrap();
2166            {
2167                let ds = file
2168                    .new_dataset::<u16>()
2169                    .shape([8])
2170                    .create("image")
2171                    .unwrap();
2172                ds.write_raw(&[0u16; 8]).unwrap();
2173                // original handle dropped here
2174            }
2175
2176            // Reopen by name; dataset() would error in write mode.
2177            assert!(file.dataset("image").is_err());
2178            let ds = file.dataset_writer("image").unwrap();
2179            assert_eq!(ds.shape(), vec![8]);
2180            ds.new_attr::<i32>()
2181                .shape([3])
2182                .create("NDArrayDimOffset")
2183                .unwrap()
2184                .write_array(&[0i32, 4, 8])
2185                .unwrap();
2186            ds.new_attr::<i32>()
2187                .shape(())
2188                .create("NDUniqueId")
2189                .unwrap()
2190                .write_numeric(&42i32)
2191                .unwrap();
2192
2193            // Missing dataset is reported.
2194            assert!(matches!(
2195                file.dataset_writer("nope"),
2196                Err(crate::error::Hdf5Error::NotFound(_))
2197            ));
2198
2199            file.close().unwrap();
2200        }
2201        {
2202            let file = H5File::open(&path).unwrap();
2203            let ds = file.dataset("image").unwrap();
2204            let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
2205            let got: Vec<i32> = off
2206                .chunks_exact(4)
2207                .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
2208                .collect();
2209            assert_eq!(got, vec![0, 4, 8]);
2210            let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
2211            assert_eq!(uid, 42);
2212        }
2213        std::fs::remove_file(&path).ok();
2214    }
2215
2216    #[test]
2217    fn chunked_write_read_roundtrip() {
2218        let path = temp_path("chunked_roundtrip");
2219
2220        // Write
2221        {
2222            let file = H5File::create(&path).unwrap();
2223            let ds = file
2224                .new_dataset::<i32>()
2225                .shape([0usize, 3])
2226                .chunk(&[1, 3])
2227                .max_shape(&[None, Some(3)])
2228                .create("table")
2229                .unwrap();
2230
2231            for frame in 0..8u64 {
2232                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
2233                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2234                ds.write_chunk(frame as usize, &raw).unwrap();
2235            }
2236            ds.extend(&[8, 3]).unwrap();
2237            file.close().unwrap();
2238        }
2239
2240        // Read
2241        {
2242            let file = H5File::open(&path).unwrap();
2243            let ds = file.dataset("table").unwrap();
2244            assert_eq!(ds.shape(), vec![8, 3]);
2245            let data = ds.read_raw::<i32>().unwrap();
2246            assert_eq!(data.len(), 24);
2247            for (i, val) in data.iter().enumerate() {
2248                assert_eq!(*val, i as i32);
2249            }
2250        }
2251
2252        std::fs::remove_file(&path).ok();
2253    }
2254
2255    #[test]
2256    #[cfg(feature = "deflate")]
2257    fn compressed_chunked_roundtrip() {
2258        let path = temp_path("compressed_roundtrip");
2259
2260        // Write compressed
2261        {
2262            let file = H5File::create(&path).unwrap();
2263            let ds = file
2264                .new_dataset::<f64>()
2265                .shape([0usize, 4])
2266                .chunk(&[1, 4])
2267                .max_shape(&[None, Some(4)])
2268                .deflate(6)
2269                .create("compressed")
2270                .unwrap();
2271
2272            for frame in 0..10u64 {
2273                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
2274                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2275                ds.write_chunk(frame as usize, &raw).unwrap();
2276            }
2277            ds.extend(&[10, 4]).unwrap();
2278            file.close().unwrap();
2279        }
2280
2281        // Read back and verify
2282        {
2283            let file = H5File::open(&path).unwrap();
2284            let ds = file.dataset("compressed").unwrap();
2285            assert_eq!(ds.shape(), vec![10, 4]);
2286            let data = ds.read_raw::<f64>().unwrap();
2287            assert_eq!(data.len(), 40);
2288            for (i, val) in data.iter().enumerate() {
2289                assert!(
2290                    (val - i as f64).abs() < 1e-10,
2291                    "mismatch at {}: {} != {}",
2292                    i,
2293                    val,
2294                    i
2295                );
2296            }
2297        }
2298
2299        std::fs::remove_file(&path).ok();
2300    }
2301
2302    #[test]
2303    #[cfg(feature = "deflate")]
2304    fn compressed_chunked_many_frames() {
2305        let path = temp_path("compressed_many");
2306
2307        {
2308            let file = H5File::create(&path).unwrap();
2309            let ds = file
2310                .new_dataset::<i32>()
2311                .shape([0usize, 3])
2312                .chunk(&[1, 3])
2313                .max_shape(&[None, Some(3)])
2314                .deflate(6)
2315                .create("stream")
2316                .unwrap();
2317
2318            for frame in 0..100u64 {
2319                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
2320                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2321                ds.write_chunk(frame as usize, &raw).unwrap();
2322            }
2323            ds.extend(&[100, 3]).unwrap();
2324            file.close().unwrap();
2325        }
2326
2327        {
2328            let file = H5File::open(&path).unwrap();
2329            let ds = file.dataset("stream").unwrap();
2330            assert_eq!(ds.shape(), vec![100, 3]);
2331            let data = ds.read_raw::<i32>().unwrap();
2332            assert_eq!(data.len(), 300);
2333            for (i, val) in data.iter().enumerate() {
2334                assert_eq!(*val, i as i32, "mismatch at {}", i);
2335            }
2336        }
2337
2338        std::fs::remove_file(&path).ok();
2339    }
2340    #[test]
2341    fn append_mode() {
2342        let path = temp_path("append");
2343
2344        // Create initial file
2345        {
2346            let file = H5File::create(&path).unwrap();
2347            let ds = file
2348                .new_dataset::<i32>()
2349                .shape([3usize])
2350                .create("first")
2351                .unwrap();
2352            ds.write_raw(&[1i32, 2, 3]).unwrap();
2353            file.close().unwrap();
2354        }
2355
2356        // Append new dataset
2357        {
2358            let file = H5File::open_rw(&path).unwrap();
2359            let ds = file
2360                .new_dataset::<f64>()
2361                .shape([2usize])
2362                .create("second")
2363                .unwrap();
2364            ds.write_raw(&[4.0f64, 5.0]).unwrap();
2365            file.close().unwrap();
2366        }
2367
2368        // Read back both
2369        {
2370            let file = H5File::open(&path).unwrap();
2371            let names = file.dataset_names();
2372            assert!(names.contains(&"first".to_string()));
2373            assert!(names.contains(&"second".to_string()));
2374
2375            let ds1 = file.dataset("first").unwrap();
2376            assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
2377
2378            let ds2 = file.dataset("second").unwrap();
2379            assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
2380        }
2381
2382        std::fs::remove_file(&path).ok();
2383    }
2384
2385    #[test]
2386    fn open_rw_set_attr_preserves_file() {
2387        let path = temp_path("open_rw_attr");
2388        // Create file with a dataset and an attribute
2389        {
2390            let file = H5File::create(&path).unwrap();
2391            let ds = file
2392                .new_dataset::<i32>()
2393                .shape([3usize])
2394                .create("data")
2395                .unwrap();
2396            ds.write_raw(&[10i32, 20, 30]).unwrap();
2397            file.set_attr_string("version", "1.0").unwrap();
2398            file.close().unwrap();
2399        }
2400        // Open rw and modify the attribute
2401        {
2402            let file = H5File::open_rw(&path).unwrap();
2403            file.set_attr_string("version", "2.0").unwrap();
2404            file.close().unwrap();
2405        }
2406        // Verify: dataset intact, attribute updated
2407        {
2408            let file = H5File::open(&path).unwrap();
2409            let ds = file.dataset("data").unwrap();
2410            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
2411            let ver = file.attr_string("version").unwrap();
2412            assert_eq!(ver, "2.0");
2413        }
2414        std::fs::remove_file(&path).ok();
2415    }
2416
2417    #[test]
2418    #[cfg(feature = "deflate")]
2419    fn open_rw_attr_with_compressed_dataset() {
2420        use crate::format::messages::filter::FilterPipeline;
2421        let path = temp_path("open_rw_compressed");
2422        let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
2423        // Create file with compressed vlen strings
2424        {
2425            let file = H5File::create(&path).unwrap();
2426            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
2427                .unwrap();
2428            file.set_attr_string("version", "1.0").unwrap();
2429            file.close().unwrap();
2430        }
2431        // Open rw and modify attribute only
2432        {
2433            let file = H5File::open_rw(&path).unwrap();
2434            file.set_attr_string("version", "2.0").unwrap();
2435            file.close().unwrap();
2436        }
2437        // Verify: compressed dataset still readable, attribute updated
2438        {
2439            let file = H5File::open(&path).unwrap();
2440            let ds = file.dataset("texts").unwrap();
2441            let strings = ds.read_vlen_strings().unwrap();
2442            assert_eq!(strings.len(), 50);
2443            assert_eq!(strings[0], "test string data");
2444            let ver = file.attr_string("version").unwrap();
2445            assert_eq!(ver, "2.0");
2446        }
2447        std::fs::remove_file(&path).ok();
2448    }
2449
2450    #[test]
2451    #[cfg(feature = "lz4")]
2452    fn append_vlen_strings_basic() {
2453        use crate::format::messages::filter::FilterPipeline;
2454        let path = temp_path("append_vlen");
2455        {
2456            let file = H5File::create(&path).unwrap();
2457            file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
2458                .unwrap();
2459            file.append_vlen_strings("names", &["alice", "bob", "charlie"])
2460                .unwrap();
2461            file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
2462            file.close().unwrap();
2463        }
2464        {
2465            let file = H5File::open(&path).unwrap();
2466            let ds = file.dataset("names").unwrap();
2467            let strings = ds.read_vlen_strings().unwrap();
2468            assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
2469        }
2470        std::fs::remove_file(&path).ok();
2471    }
2472
2473    #[test]
2474    #[cfg(feature = "lz4")]
2475    fn append_vlen_strings_large() {
2476        use crate::format::messages::filter::FilterPipeline;
2477        let path = temp_path("append_vlen_large");
2478        let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
2479        let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
2480        {
2481            let file = H5File::create(&path).unwrap();
2482            file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
2483                .unwrap();
2484            let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
2485            file.append_vlen_strings("data", &r1).unwrap();
2486            let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
2487            file.append_vlen_strings("data", &r2).unwrap();
2488            file.close().unwrap();
2489        }
2490        {
2491            let file = H5File::open(&path).unwrap();
2492            let ds = file.dataset("data").unwrap();
2493            let strings = ds.read_vlen_strings().unwrap();
2494            assert_eq!(strings.len(), 7189);
2495            assert_eq!(strings[0], "node-000000");
2496            assert_eq!(strings[7188], "node-007188");
2497        }
2498        std::fs::remove_file(&path).ok();
2499    }
2500
2501    #[test]
2502    fn append_vlen_strings_uncompressed() {
2503        let path = temp_path("append_vlen_unc");
2504        {
2505            let file = H5File::create(&path).unwrap();
2506            file.create_appendable_vlen_dataset("texts", 8, None)
2507                .unwrap();
2508            file.append_vlen_strings("texts", &["hello", "world"])
2509                .unwrap();
2510            file.append_vlen_strings("texts", &["foo", "bar", "baz"])
2511                .unwrap();
2512            file.close().unwrap();
2513        }
2514        {
2515            let file = H5File::open(&path).unwrap();
2516            let ds = file.dataset("texts").unwrap();
2517            let strings = ds.read_vlen_strings().unwrap();
2518            assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
2519        }
2520        std::fs::remove_file(&path).ok();
2521    }
2522
2523    #[test]
2524    fn delete_dataset_roundtrip() {
2525        let path = temp_path("delete_ds");
2526        {
2527            let file = H5File::create(&path).unwrap();
2528            file.write_vlen_strings("keep", &["a", "b"]).unwrap();
2529            file.write_vlen_strings("remove", &["x", "y"]).unwrap();
2530            file.delete_dataset("remove").unwrap();
2531            file.close().unwrap();
2532        }
2533        {
2534            let file = H5File::open(&path).unwrap();
2535            let names = file.dataset_names();
2536            assert!(names.contains(&"keep".to_string()));
2537            assert!(!names.contains(&"remove".to_string()));
2538            let ds = file.dataset("keep").unwrap();
2539            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
2540        }
2541        std::fs::remove_file(&path).ok();
2542    }
2543
2544    #[test]
2545    fn delete_group_roundtrip() {
2546        let path = temp_path("delete_grp");
2547        {
2548            let file = H5File::create(&path).unwrap();
2549            let g1 = file.create_group("keep").unwrap();
2550            g1.write_vlen_strings("data", &["a"]).unwrap();
2551            let g2 = file.create_group("remove").unwrap();
2552            g2.write_vlen_strings("data", &["x"]).unwrap();
2553            file.delete_group("remove").unwrap();
2554            file.close().unwrap();
2555        }
2556        {
2557            let file = H5File::open(&path).unwrap();
2558            let names = file.dataset_names();
2559            assert!(names.contains(&"keep/data".to_string()));
2560            assert!(!names.contains(&"remove/data".to_string()));
2561        }
2562        std::fs::remove_file(&path).ok();
2563    }
2564
2565    #[test]
2566    fn open_rw_delete_recreate_group() {
2567        let path = temp_path("rw_delete_recreate");
2568        // Step 1: create file with groups
2569        {
2570            let file = H5File::create(&path).unwrap();
2571            let n = file.create_group("nodes").unwrap();
2572            n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
2573            let e = file.create_group("edges").unwrap();
2574            e.write_vlen_strings("src", &["x", "y"]).unwrap();
2575            file.close().unwrap();
2576        }
2577        // Step 2: open_rw, delete one group, recreate with new data
2578        {
2579            let file = H5File::open_rw(&path).unwrap();
2580            file.delete_group("nodes").unwrap();
2581            let n = file.create_group("nodes").unwrap();
2582            n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
2583            file.close().unwrap();
2584        }
2585        // Step 3: verify
2586        {
2587            let file = H5File::open(&path).unwrap();
2588            let ds = file.dataset("nodes/id").unwrap();
2589            let s = ds.read_vlen_strings().unwrap();
2590            assert_eq!(s, vec!["new1", "new2"]);
2591            // edges should still be intact
2592            let ds = file.dataset("edges/src").unwrap();
2593            let s = ds.read_vlen_strings().unwrap();
2594            assert_eq!(s, vec!["x", "y"]);
2595        }
2596        std::fs::remove_file(&path).ok();
2597    }
2598
2599    #[test]
2600    fn delete_and_recreate_group() {
2601        let path = temp_path("delete_recreate");
2602        {
2603            let file = H5File::create(&path).unwrap();
2604            let g = file.create_group("nodes").unwrap();
2605            g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
2606            file.delete_group("nodes").unwrap();
2607            let g = file.create_group("nodes").unwrap();
2608            g.write_vlen_strings("id", &["new1", "new2", "new3"])
2609                .unwrap();
2610            file.close().unwrap();
2611        }
2612        {
2613            let file = H5File::open(&path).unwrap();
2614            let ds = file.dataset("nodes/id").unwrap();
2615            let strings = ds.read_vlen_strings().unwrap();
2616            assert_eq!(strings, vec!["new1", "new2", "new3"]);
2617        }
2618        std::fs::remove_file(&path).ok();
2619    }
2620
2621    #[test]
2622    #[cfg(feature = "deflate")]
2623    fn vlen_string_compressed_large_roundtrip() {
2624        use crate::format::messages::filter::FilterPipeline;
2625        let path = temp_path("vlen_large");
2626        // Simulate kodex scenario: 7189 strings, chunk_size 512
2627        let input: Vec<String> = (0..7189)
2628            .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
2629            .collect();
2630        let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
2631        {
2632            let file = H5File::create(&path).unwrap();
2633            file.create_group("nodes").unwrap();
2634            file.write_vlen_strings_compressed(
2635                "nodes/id",
2636                &input_refs,
2637                512,
2638                FilterPipeline::deflate(6),
2639            )
2640            .unwrap();
2641            file.close().unwrap();
2642        }
2643        // Read back
2644        {
2645            let file = H5File::open(&path).unwrap();
2646            let ds = file.dataset("nodes/id").unwrap();
2647            let strings = ds.read_vlen_strings().unwrap();
2648            assert_eq!(strings.len(), 7189);
2649            assert_eq!(strings[0], input[0]);
2650            assert_eq!(strings[7188], input[7188]);
2651        }
2652        // Also test open_rw then re-read
2653        {
2654            let file = H5File::open_rw(&path).unwrap();
2655            file.set_attr_string("version", "1.0").unwrap();
2656            file.close().unwrap();
2657        }
2658        {
2659            let file = H5File::open(&path).unwrap();
2660            let ds = file.dataset("nodes/id").unwrap();
2661            let strings = ds.read_vlen_strings().unwrap();
2662            assert_eq!(strings.len(), 7189);
2663            assert_eq!(strings[0], input[0]);
2664        }
2665        std::fs::remove_file(&path).ok();
2666    }
2667
2668    #[test]
2669    fn vlen_string_write_read() {
2670        let path = temp_path("vlen_wr");
2671        {
2672            let file = H5File::create(&path).unwrap();
2673            file.write_vlen_strings("names", &["alice", "bob", "charlie"])
2674                .unwrap();
2675            file.close().unwrap();
2676        }
2677        {
2678            let file = H5File::open(&path).unwrap();
2679            let ds = file.dataset("names").unwrap();
2680            let strings = ds.read_vlen_strings().unwrap();
2681            assert_eq!(strings, vec!["alice", "bob", "charlie"]);
2682        }
2683        std::fs::remove_file(&path).ok();
2684    }
2685
2686    /// The one-call writers declare the character set they are named for —
2687    /// `write_vlen_strings` UTF-8, `write_vlen_strings_ascii` ASCII — and the
2688    /// ASCII one refuses a string its declaration would misdescribe, before
2689    /// anything reaches the file.
2690    #[test]
2691    fn vlen_string_writers_declare_their_character_set() {
2692        use crate::format::messages::datatype::DatatypeMessage;
2693
2694        let path = temp_path("vlen_cset");
2695        let file = H5File::create(&path).unwrap();
2696        file.write_vlen_strings_ascii("ascii", &["alpha", "b", ""])
2697            .unwrap();
2698        file.write_vlen_strings("utf8", &["été", "日本"]).unwrap();
2699        let err = file
2700            .write_vlen_strings_ascii("rejected", &["ok", "안녕"])
2701            .err()
2702            .expect("a non-ASCII string was accepted under an ASCII datatype")
2703            .to_string();
2704        assert!(
2705            err.contains("string 1") && err.contains("is not ASCII"),
2706            "got: {err}"
2707        );
2708        file.close().unwrap();
2709
2710        let file = H5File::open(&path).unwrap();
2711        let ascii = file.dataset("ascii").unwrap();
2712        assert_eq!(
2713            ascii.datatype().unwrap(),
2714            DatatypeMessage::VarLenString {
2715                padding: 0,
2716                charset: 0,
2717            }
2718        );
2719        assert_eq!(ascii.read_strings().unwrap(), vec!["alpha", "b", ""]);
2720        let utf8 = file.dataset("utf8").unwrap();
2721        assert_eq!(
2722            utf8.datatype().unwrap(),
2723            DatatypeMessage::VarLenString {
2724                padding: 0,
2725                charset: 1,
2726            }
2727        );
2728        assert_eq!(utf8.read_strings().unwrap(), vec!["été", "日本"]);
2729        // The refused write left nothing behind.
2730        assert!(file.dataset("rejected").is_err());
2731        std::fs::remove_file(&path).ok();
2732    }
2733
2734    /// The group-level twin declares ASCII the same way the file-level one
2735    /// does, for a dataset inside the group.
2736    #[test]
2737    fn group_vlen_string_writer_declares_ascii() {
2738        use crate::format::messages::datatype::DatatypeMessage;
2739
2740        let path = temp_path("vlen_cset_group");
2741        let file = H5File::create(&path).unwrap();
2742        let g = file.create_group("entry").unwrap();
2743        g.write_vlen_strings_ascii("notes", &["alpha", "b"])
2744            .unwrap();
2745        let err = g
2746            .write_vlen_strings_ascii("rejected", &["안녕"])
2747            .err()
2748            .expect("a non-ASCII string was accepted under an ASCII datatype")
2749            .to_string();
2750        assert!(err.contains("is not ASCII"), "got: {err}");
2751        file.close().unwrap();
2752
2753        let file = H5File::open(&path).unwrap();
2754        let ds = file.dataset("entry/notes").unwrap();
2755        assert_eq!(
2756            ds.datatype().unwrap(),
2757            DatatypeMessage::VarLenString {
2758                padding: 0,
2759                charset: 0,
2760            }
2761        );
2762        assert_eq!(ds.read_strings().unwrap(), vec!["alpha", "b"]);
2763        std::fs::remove_file(&path).ok();
2764    }
2765
2766    #[test]
2767    fn vlen_bytes_write_read() {
2768        let path = temp_path("vlen_bytes_wr");
2769        let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
2770        {
2771            let file = H5File::create(&path).unwrap();
2772            file.write_vlen_bytes("blobs", &items).unwrap();
2773            file.close().unwrap();
2774        }
2775        {
2776            let file = H5File::open(&path).unwrap();
2777            let ds = file.dataset("blobs").unwrap();
2778            let got = ds.read_vlen_bytes().unwrap();
2779            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
2780            assert_eq!(got, expected);
2781        }
2782        std::fs::remove_file(&path).ok();
2783    }
2784
2785    /// A vlen sequence over a wider base stores element counts, not byte
2786    /// counts, in the `H5T_VLEN` length field, and the datatype names the
2787    /// base — so the file says what it holds for every width.
2788    #[test]
2789    fn vlen_numeric_write_read() {
2790        use crate::format::global_heap::decode_vlen_reference;
2791        use crate::format::messages::datatype::DatatypeMessage;
2792
2793        let path = temp_path("vlen_numeric_wr");
2794        let a: &[i32] = &[1, 2, 3];
2795        let b: &[i32] = &[];
2796        let c: &[i32] = &[-7];
2797        {
2798            let file = H5File::create(&path).unwrap();
2799            file.write_vlen_numeric("data", &[a, b, c]).unwrap();
2800            let f64s: &[f64] = &[1.5, -2.5];
2801            file.write_vlen_numeric("wide", &[f64s]).unwrap();
2802            file.close().unwrap();
2803        }
2804        let file = H5File::open(&path).unwrap();
2805        let ds = file.dataset("data").unwrap();
2806        assert_eq!(
2807            ds.datatype().unwrap(),
2808            DatatypeMessage::VarLenSequence {
2809                base: Box::new(DatatypeMessage::i32_type()),
2810            }
2811        );
2812        let decoded: Vec<Vec<i32>> = ds
2813            .read_vlen_bytes()
2814            .unwrap()
2815            .iter()
2816            .map(|item| {
2817                item.chunks_exact(4)
2818                    .map(|w| i32::from_le_bytes(w.try_into().unwrap()))
2819                    .collect()
2820            })
2821            .collect();
2822        assert_eq!(decoded, vec![a.to_vec(), b.to_vec(), c.to_vec()]);
2823
2824        // The length field counts elements: 3 i32s, not 12 bytes.
2825        let ctx = crate::format::FormatContext::default_v3();
2826        let raw = ds.read_raw_bytes().unwrap();
2827        let (seq_len, _, _) = decode_vlen_reference(&raw, &ctx).unwrap();
2828        assert_eq!(seq_len, 3);
2829
2830        let wide = file.dataset("wide").unwrap();
2831        assert_eq!(
2832            wide.datatype().unwrap(),
2833            DatatypeMessage::VarLenSequence {
2834                base: Box::new(DatatypeMessage::f64_type()),
2835            }
2836        );
2837        let (seq_len, _, _) = decode_vlen_reference(&wide.read_raw_bytes().unwrap(), &ctx).unwrap();
2838        assert_eq!(seq_len, 2);
2839        drop(file);
2840        std::fs::remove_file(&path).ok();
2841    }
2842
2843    /// `write_vlen_bytes` is the `u8` case of the same writer, so the byte
2844    /// datatype and the byte-per-element length field are unchanged.
2845    #[test]
2846    fn vlen_bytes_is_the_u8_case_of_vlen_numeric() {
2847        use crate::format::messages::datatype::DatatypeMessage;
2848
2849        let path = temp_path("vlen_bytes_u8");
2850        let items: [&[u8]; 2] = [b"abc", b""];
2851        {
2852            let file = H5File::create(&path).unwrap();
2853            file.write_vlen_bytes("blobs", &items).unwrap();
2854            file.close().unwrap();
2855        }
2856        let file = H5File::open(&path).unwrap();
2857        let ds = file.dataset("blobs").unwrap();
2858        assert_eq!(ds.datatype().unwrap(), DatatypeMessage::vlen_bytes());
2859        let ctx = crate::format::FormatContext::default_v3();
2860        let (seq_len, _, _) =
2861            crate::format::global_heap::decode_vlen_reference(&ds.read_raw_bytes().unwrap(), &ctx)
2862                .unwrap();
2863        assert_eq!(seq_len, 3);
2864        drop(file);
2865        std::fs::remove_file(&path).ok();
2866    }
2867
2868    #[test]
2869    fn vlen_bytes_in_group_with_attribute() {
2870        use crate::types::VarLenUnicode;
2871        let path = temp_path("vlen_bytes_grp");
2872        let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
2873        {
2874            let file = H5File::create(&path).unwrap();
2875            let grp = file.root_group().create_group("payloads").unwrap();
2876            let ds = grp.write_vlen_bytes("frames", &items).unwrap();
2877            ds.new_attr::<VarLenUnicode>()
2878                .shape(())
2879                .create("codec")
2880                .unwrap()
2881                .write_string("raw")
2882                .unwrap();
2883            file.close().unwrap();
2884        }
2885        {
2886            let file = H5File::open(&path).unwrap();
2887            let ds = file.dataset("payloads/frames").unwrap();
2888            let got = ds.read_vlen_bytes().unwrap();
2889            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
2890            assert_eq!(got, expected);
2891        }
2892        std::fs::remove_file(&path).ok();
2893    }
2894
2895    #[test]
2896    fn vlen_dataset_returns_handle_for_attributes() {
2897        use crate::types::VarLenUnicode;
2898        let path = temp_path("vlen_attr");
2899        {
2900            let file = H5File::create(&path).unwrap();
2901            let grp = file.root_group().create_group("ch").unwrap();
2902            // The vlen helper now returns the dataset handle, so attributes can
2903            // be attached directly — the issue the mdfr reporter hit.
2904            let ds = grp
2905                .write_vlen_strings("labels", &["a", "bb", "ccc"])
2906                .unwrap();
2907            ds.new_attr::<VarLenUnicode>()
2908                .shape(())
2909                .create("unit")
2910                .unwrap()
2911                .write_string("volt")
2912                .unwrap();
2913            // The same dataset can also be reopened by name within the group.
2914            let ds2 = grp.dataset_writer("labels").unwrap();
2915            ds2.new_attr::<VarLenUnicode>()
2916                .shape(())
2917                .create("desc")
2918                .unwrap()
2919                .write_string("channel labels")
2920                .unwrap();
2921            file.close().unwrap();
2922        }
2923        {
2924            let file = H5File::open(&path).unwrap();
2925            let ds = file.dataset("ch/labels").unwrap();
2926            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
2927            assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
2928            assert_eq!(
2929                ds.attr("desc").unwrap().read_string().unwrap(),
2930                "channel labels"
2931            );
2932        }
2933        std::fs::remove_file(&path).ok();
2934    }
2935
2936    #[test]
2937    #[cfg(feature = "deflate")]
2938    fn vlen_string_deflate_roundtrip() {
2939        use crate::format::messages::filter::FilterPipeline;
2940        let path = temp_path("vlen_deflate");
2941        let input: Vec<&str> = (0..100)
2942            .map(|i| match i % 3 {
2943                0 => "hello world",
2944                1 => "compressed vlen string test",
2945                _ => "rust-hdf5",
2946            })
2947            .collect();
2948        {
2949            let file = H5File::create(&path).unwrap();
2950            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
2951                .unwrap();
2952            file.close().unwrap();
2953        }
2954        {
2955            let file = H5File::open(&path).unwrap();
2956            let ds = file.dataset("texts").unwrap();
2957            let strings = ds.read_vlen_strings().unwrap();
2958            assert_eq!(strings.len(), 100);
2959            for (i, s) in strings.iter().enumerate() {
2960                assert_eq!(s, input[i]);
2961            }
2962        }
2963        std::fs::remove_file(&path).ok();
2964    }
2965
2966    #[test]
2967    #[cfg(feature = "zstd")]
2968    fn vlen_string_zstd_roundtrip() {
2969        use crate::format::messages::filter::FilterPipeline;
2970        let path = temp_path("vlen_zstd");
2971        let input: Vec<&str> = (0..200)
2972            .map(|i| match i % 4 {
2973                0 => "zstandard compression test",
2974                1 => "variable length string",
2975                2 => "rust-hdf5 chunked storage",
2976                _ => "hello zstd world",
2977            })
2978            .collect();
2979        {
2980            let file = H5File::create(&path).unwrap();
2981            file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
2982                .unwrap();
2983            file.close().unwrap();
2984        }
2985        {
2986            let file = H5File::open(&path).unwrap();
2987            let ds = file.dataset("data").unwrap();
2988            let strings = ds.read_vlen_strings().unwrap();
2989            assert_eq!(strings.len(), 200);
2990            for (i, s) in strings.iter().enumerate() {
2991                assert_eq!(s, input[i]);
2992            }
2993        }
2994        std::fs::remove_file(&path).ok();
2995    }
2996
2997    #[test]
2998    #[cfg(feature = "deflate")]
2999    fn shuffle_deflate_roundtrip() {
3000        let path = temp_path("shuf_defl");
3001        {
3002            let file = H5File::create(&path).unwrap();
3003            let ds = file
3004                .new_dataset::<f64>()
3005                .shape([0usize, 4])
3006                .chunk(&[1, 4])
3007                .max_shape(&[None, Some(4)])
3008                .shuffle_deflate(6)
3009                .create("data")
3010                .unwrap();
3011            for frame in 0..20u64 {
3012                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
3013                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3014                ds.write_chunk(frame as usize, &raw).unwrap();
3015            }
3016            ds.extend(&[20, 4]).unwrap();
3017            file.close().unwrap();
3018        }
3019        {
3020            let file = H5File::open(&path).unwrap();
3021            let ds = file.dataset("data").unwrap();
3022            assert_eq!(ds.shape(), vec![20, 4]);
3023            let data = ds.read_raw::<f64>().unwrap();
3024            assert_eq!(data.len(), 80);
3025            for (i, val) in data.iter().enumerate() {
3026                assert!((val - i as f64).abs() < 1e-10);
3027            }
3028        }
3029        std::fs::remove_file(&path).ok();
3030    }
3031
3032    #[test]
3033    fn file_level_attributes() {
3034        let path = temp_path("file_attr");
3035        {
3036            let file = H5File::create(&path).unwrap();
3037            file.set_attr_string("title", "Test File").unwrap();
3038            file.set_attr_numeric("version", &42i32).unwrap();
3039            let ds = file
3040                .new_dataset::<u8>()
3041                .shape([1usize])
3042                .create("dummy")
3043                .unwrap();
3044            ds.write_raw(&[0u8]).unwrap();
3045            file.close().unwrap();
3046        }
3047        {
3048            let file = H5File::open(&path).unwrap();
3049            assert!(file.dataset_names().contains(&"dummy".to_string()));
3050
3051            // Read file-level attributes
3052            let names = file.attr_names().unwrap();
3053            assert!(names.contains(&"title".to_string()));
3054
3055            let title = file.attr_string("title").unwrap();
3056            assert_eq!(title, "Test File");
3057        }
3058        std::fs::remove_file(&path).ok();
3059    }
3060
3061    #[test]
3062    fn scalar_dataset_roundtrip() {
3063        let path = temp_path("scalar");
3064        {
3065            let file = H5File::create(&path).unwrap();
3066            let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
3067            ds.write_raw(&[std::f64::consts::PI]).unwrap();
3068            file.close().unwrap();
3069        }
3070        {
3071            let file = H5File::open(&path).unwrap();
3072            let ds = file.dataset("pi").unwrap();
3073            assert_eq!(ds.shape(), Vec::<usize>::new());
3074            assert_eq!(ds.total_elements(), 1);
3075            let data = ds.read_raw::<f64>().unwrap();
3076            assert_eq!(data.len(), 1);
3077            assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
3078        }
3079        std::fs::remove_file(&path).ok();
3080    }
3081
3082    #[test]
3083    fn append_mode_extend_chunked() {
3084        let path = temp_path("append_extend");
3085
3086        // Create with 5 frames
3087        {
3088            let file = H5File::create(&path).unwrap();
3089            let ds = file
3090                .new_dataset::<i32>()
3091                .shape([0usize, 3])
3092                .chunk(&[1, 3])
3093                .max_shape(&[None, Some(3)])
3094                .create("stream")
3095                .unwrap();
3096            for i in 0..5u64 {
3097                let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
3098                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3099                ds.write_chunk(i as usize, &raw).unwrap();
3100            }
3101            ds.extend(&[5, 3]).unwrap();
3102            file.close().unwrap();
3103        }
3104
3105        // Reopen and add 5 more frames
3106        {
3107            let file = H5File::open_rw(&path).unwrap();
3108            // Find the stream dataset index (it's the first one)
3109            let names = file.dataset_names();
3110            assert!(names.contains(&"stream".to_string()));
3111
3112            // Write more chunks via the writer directly
3113            let mut inner = crate::file::borrow_inner_mut(&file.inner);
3114            if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
3115                let ds_idx = writer.dataset_index("stream").unwrap();
3116                for i in 5..10u64 {
3117                    let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
3118                    let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3119                    writer.write_chunk(ds_idx, i, &raw).unwrap();
3120                }
3121                writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
3122            }
3123            drop(inner);
3124            file.close().unwrap();
3125        }
3126
3127        // Read back all 10 frames
3128        {
3129            let file = H5File::open(&path).unwrap();
3130            let ds = file.dataset("stream").unwrap();
3131            assert_eq!(ds.shape(), vec![10, 3]);
3132            let data = ds.read_raw::<i32>().unwrap();
3133            assert_eq!(data.len(), 30);
3134            for (i, val) in data.iter().enumerate() {
3135                assert_eq!(*val, i as i32, "mismatch at {}", i);
3136            }
3137        }
3138
3139        std::fs::remove_file(&path).ok();
3140    }
3141
3142    #[test]
3143    fn group_hierarchy_roundtrip() {
3144        let path = temp_path("groups_rt");
3145
3146        {
3147            let file = H5File::create(&path).unwrap();
3148            let root = file.root_group();
3149
3150            // Create groups
3151            let det = root.create_group("detector").unwrap();
3152            let raw = det.create_group("raw").unwrap();
3153
3154            // Create datasets in groups
3155            let ds1 = det
3156                .new_dataset::<f32>()
3157                .shape([10usize])
3158                .create("temperature")
3159                .unwrap();
3160            ds1.write_raw(&[1.0f32; 10]).unwrap();
3161
3162            let ds2 = raw
3163                .new_dataset::<u16>()
3164                .shape([4usize, 4])
3165                .create("image")
3166                .unwrap();
3167            ds2.write_raw(&[42u16; 16]).unwrap();
3168
3169            // Root-level dataset
3170            let ds3 = file
3171                .new_dataset::<i32>()
3172                .shape([3usize])
3173                .create("version")
3174                .unwrap();
3175            ds3.write_raw(&[1i32, 0, 0]).unwrap();
3176
3177            file.close().unwrap();
3178        }
3179
3180        {
3181            let file = H5File::open(&path).unwrap();
3182            let names = file.dataset_names();
3183            assert!(names.contains(&"version".to_string()));
3184            assert!(names.contains(&"detector/temperature".to_string()));
3185            assert!(names.contains(&"detector/raw/image".to_string()));
3186
3187            // Read datasets
3188            let ds = file.dataset("version").unwrap();
3189            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
3190
3191            let ds = file.dataset("detector/temperature").unwrap();
3192            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
3193
3194            let ds = file.dataset("detector/raw/image").unwrap();
3195            assert_eq!(ds.shape(), vec![4, 4]);
3196            assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
3197
3198            // Group traversal
3199            let root = file.root_group();
3200            let group_names = root.group_names().unwrap();
3201            assert!(group_names.contains(&"detector".to_string()));
3202        }
3203
3204        std::fs::remove_file(&path).ok();
3205    }
3206
3207    #[test]
3208    fn nested_groups_via_file_create_group() {
3209        let path = temp_path("file_create_group");
3210
3211        {
3212            let file = H5File::create(&path).unwrap();
3213
3214            // Use the H5File::create_group convenience method
3215            let grp = file.create_group("sensors").unwrap();
3216            let sub = grp.create_group("accel").unwrap();
3217
3218            let ds = sub
3219                .new_dataset::<f64>()
3220                .shape([3usize])
3221                .create("xyz")
3222                .unwrap();
3223            ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
3224
3225            file.close().unwrap();
3226        }
3227
3228        {
3229            let file = H5File::open(&path).unwrap();
3230            let names = file.dataset_names();
3231            assert!(names.contains(&"sensors/accel/xyz".to_string()));
3232
3233            let ds = file.dataset("sensors/accel/xyz").unwrap();
3234            assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
3235
3236            // Open group in read mode
3237            let root = file.root_group();
3238            let sensors = root.group("sensors").unwrap();
3239            assert_eq!(sensors.name(), "/sensors");
3240
3241            let accel = sensors.group("accel").unwrap();
3242            assert_eq!(accel.name(), "/sensors/accel");
3243
3244            // list_groups from root
3245            let top_groups = root.group_names().unwrap();
3246            assert!(top_groups.contains(&"sensors".to_string()));
3247
3248            // list_groups from sensors
3249            let sub_groups = sensors.group_names().unwrap();
3250            assert!(sub_groups.contains(&"accel".to_string()));
3251        }
3252
3253        std::fs::remove_file(&path).ok();
3254    }
3255}
3256
3257#[cfg(test)]
3258mod h5py_compat_tests {
3259    use super::*;
3260
3261    fn temp_path(name: &str) -> std::path::PathBuf {
3262        super::unique_test_path(name)
3263    }
3264
3265    /// Verify our files can be read by h5dump (if available).
3266    #[test]
3267    #[cfg(feature = "deflate")]
3268    fn h5dump_validates_our_files() {
3269        // Check if h5dump is available
3270        let h5dump = std::process::Command::new("h5dump")
3271            .arg("--version")
3272            .output();
3273        if h5dump.is_err() {
3274            eprintln!("skipping: h5dump not found");
3275            return;
3276        }
3277
3278        let path = temp_path("h5dump_validate");
3279
3280        // Write a comprehensive test file
3281        {
3282            let file = H5File::create(&path).unwrap();
3283
3284            // Contiguous
3285            let ds = file
3286                .new_dataset::<f64>()
3287                .shape([3usize, 4])
3288                .create("matrix")
3289                .unwrap();
3290            let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
3291            ds.write_raw(&data).unwrap();
3292
3293            // Chunked + compressed
3294            let ds2 = file
3295                .new_dataset::<i32>()
3296                .shape([0usize, 2])
3297                .chunk(&[1, 2])
3298                .max_shape(&[None, Some(2)])
3299                .deflate(6)
3300                .create("stream")
3301                .unwrap();
3302            for i in 0..5u64 {
3303                let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
3304                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
3305                ds2.write_chunk(i as usize, &raw).unwrap();
3306            }
3307            ds2.extend(&[5, 2]).unwrap();
3308
3309            // Group
3310            let grp = file.create_group("meta").unwrap();
3311            let ds3 = grp
3312                .new_dataset::<u8>()
3313                .shape([4usize])
3314                .create("flags")
3315                .unwrap();
3316            ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
3317
3318            // String attribute
3319            use crate::types::VarLenUnicode;
3320            let attr = ds
3321                .new_attr::<VarLenUnicode>()
3322                .shape(())
3323                .create("units")
3324                .unwrap();
3325            attr.write_string("meters").unwrap();
3326
3327            file.close().unwrap();
3328        }
3329
3330        // Run h5dump and verify exit code
3331        let output = std::process::Command::new("h5dump")
3332            .arg("-H") // header only (faster)
3333            .arg(path.to_str().unwrap())
3334            .output()
3335            .unwrap();
3336
3337        assert!(
3338            output.status.success(),
3339            "h5dump failed:\nstdout: {}\nstderr: {}",
3340            String::from_utf8_lossy(&output.stdout),
3341            String::from_utf8_lossy(&output.stderr),
3342        );
3343
3344        // Full dump (with data) should also work
3345        let output2 = std::process::Command::new("h5dump")
3346            .arg(path.to_str().unwrap())
3347            .output()
3348            .unwrap();
3349
3350        assert!(
3351            output2.status.success(),
3352            "h5dump (full) failed:\nstderr: {}",
3353            String::from_utf8_lossy(&output2.stderr),
3354        );
3355
3356        std::fs::remove_file(&path).ok();
3357    }
3358
3359    #[test]
3360    fn read_h5py_generated_file() {
3361        let path = "/tmp/test_h5py_default.h5";
3362        if !std::path::Path::new(path).exists() {
3363            eprintln!("skipping: h5py test file not found");
3364            return;
3365        }
3366        let file = H5File::open(path).unwrap();
3367
3368        let ds = file.dataset("data").unwrap();
3369        assert_eq!(ds.shape(), vec![4, 5]);
3370        let data = ds.read_raw::<f64>().unwrap();
3371        assert_eq!(data.len(), 20);
3372        assert!((data[0]).abs() < 1e-10);
3373        assert!((data[19] - 19.0).abs() < 1e-10);
3374
3375        let ds2 = file.dataset("images").unwrap();
3376        assert_eq!(ds2.shape(), vec![3, 64, 64]);
3377        let images = ds2.read_raw::<u16>().unwrap();
3378        assert_eq!(images.len(), 3 * 64 * 64);
3379    }
3380
3381    /// `track_order`, `libver`, `userblock` and `shared_messages` only take
3382    /// effect on [`H5FileOptions::create`]; setting any of them for
3383    /// [`H5FileOptions::open_rw`] on an already-created file must be
3384    /// refused, naming the option, rather than silently doing nothing.
3385    #[test]
3386    fn open_rw_refuses_every_create_only_option() {
3387        let path = temp_path("open_rw_refuses");
3388        H5File::create(&path).unwrap().close().unwrap();
3389
3390        let err = H5File::options()
3391            .track_order(true)
3392            .open_rw(&path)
3393            .err()
3394            .unwrap();
3395        assert!(err.to_string().contains("track_order"), "{err}");
3396
3397        let err = H5File::options()
3398            .libver(LibverBound::V110)
3399            .open_rw(&path)
3400            .err()
3401            .unwrap();
3402        assert!(err.to_string().contains("libver"), "{err}");
3403
3404        // Every bound, not just the ones that differ from `LibverBound`'s
3405        // own default. `Earliest` *is* that default and is the bound that
3406        // asks for a classic file, so a gate comparing against the default
3407        // value would pass this call through as if nothing had been set.
3408        for bound in [
3409            LibverBound::Earliest,
3410            LibverBound::V18,
3411            LibverBound::V112,
3412            LibverBound::V114,
3413            LibverBound::V200,
3414        ] {
3415            let err = H5File::options()
3416                .libver(bound)
3417                .open_rw(&path)
3418                .err()
3419                .unwrap();
3420            assert!(err.to_string().contains("libver"), "{bound:?}: {err}");
3421        }
3422
3423        let err = H5File::options()
3424            .userblock(512)
3425            .open_rw(&path)
3426            .err()
3427            .unwrap();
3428        assert!(err.to_string().contains("userblock"), "{err}");
3429
3430        let types = crate::format::sohm::type_flag(crate::format::messages::MSG_DATATYPE).unwrap();
3431        let err = H5File::options()
3432            .shared_messages(&[(types, 0)], 50, 40)
3433            .open_rw(&path)
3434            .err()
3435            .unwrap();
3436        assert!(err.to_string().contains("shared_messages"), "{err}");
3437
3438        // A default builder — no create-only option touched — still opens.
3439        H5File::options().open_rw(&path).unwrap().close().unwrap();
3440
3441        std::fs::remove_file(&path).ok();
3442    }
3443
3444    /// [`H5FileOptions::open`] shares the same gate as `open_rw` — it goes
3445    /// through the same `refuse_create_only_options` check.
3446    #[test]
3447    fn open_refuses_a_create_only_option() {
3448        let path = temp_path("open_refuses");
3449        H5File::create(&path).unwrap().close().unwrap();
3450
3451        let err = H5File::options()
3452            .track_order(true)
3453            .open(&path)
3454            .err()
3455            .unwrap();
3456        assert!(err.to_string().contains("track_order"), "{err}");
3457
3458        H5File::options().open(&path).unwrap().close().unwrap();
3459
3460        std::fs::remove_file(&path).ok();
3461    }
3462}