Skip to main content

rust_hdf5/
file.rs

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