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::io::locking::FileLocking;
22use crate::io::{Hdf5Reader, Hdf5Writer};
23
24use crate::dataset::{DatasetBuilder, H5Dataset};
25use crate::error::{Hdf5Error, Result};
26use crate::format::messages::filter::FilterPipeline;
27use crate::group::H5Group;
28use crate::types::H5Type;
29
30// ---------------------------------------------------------------------------
31// Thread-safety: choose between Rc<RefCell<>> and Arc<Mutex<>> based on
32// the `threadsafe` feature flag.
33// ---------------------------------------------------------------------------
34
35#[cfg(not(feature = "threadsafe"))]
36pub(crate) type SharedInner = std::rc::Rc<std::cell::RefCell<H5FileInner>>;
37
38#[cfg(feature = "threadsafe")]
39pub(crate) type SharedInner = std::sync::Arc<std::sync::RwLock<H5FileInner>>;
40
41/// Helper to borrow/lock the inner state immutably.
42#[cfg(not(feature = "threadsafe"))]
43pub(crate) fn borrow_inner(inner: &SharedInner) -> std::cell::Ref<'_, H5FileInner> {
44    inner.borrow()
45}
46
47/// Helper to borrow/lock the inner state mutably.
48#[cfg(not(feature = "threadsafe"))]
49pub(crate) fn borrow_inner_mut(inner: &SharedInner) -> std::cell::RefMut<'_, H5FileInner> {
50    inner.borrow_mut()
51}
52
53/// Helper to clone a SharedInner.
54#[cfg(not(feature = "threadsafe"))]
55pub(crate) fn clone_inner(inner: &SharedInner) -> SharedInner {
56    std::rc::Rc::clone(inner)
57}
58
59/// Helper to wrap an H5FileInner in SharedInner.
60#[cfg(not(feature = "threadsafe"))]
61pub(crate) fn new_shared(inner: H5FileInner) -> SharedInner {
62    std::rc::Rc::new(std::cell::RefCell::new(inner))
63}
64
65/// Acquire a shared (read) lock on the inner state. The fine-grained writer
66/// (atomic allocator, positioned handle, per-dataset `Slot` mutexes) is safe to
67/// drive through `&H5FileInner`, so the non-extending chunk-write path takes
68/// this read guard and lets writes to different datasets proceed concurrently.
69#[cfg(feature = "threadsafe")]
70pub(crate) fn borrow_inner(inner: &SharedInner) -> std::sync::RwLockReadGuard<'_, H5FileInner> {
71    inner.read().unwrap()
72}
73
74/// Acquire an exclusive (write) lock on the inner state. Required by paths that
75/// mutate shared writer state directly — create, extend/`set_extent`, append,
76/// metadata, finalize/close.
77#[cfg(feature = "threadsafe")]
78pub(crate) fn borrow_inner_mut(
79    inner: &SharedInner,
80) -> std::sync::RwLockWriteGuard<'_, H5FileInner> {
81    inner.write().unwrap()
82}
83
84#[cfg(feature = "threadsafe")]
85pub(crate) fn clone_inner(inner: &SharedInner) -> SharedInner {
86    std::sync::Arc::clone(inner)
87}
88
89#[cfg(feature = "threadsafe")]
90pub(crate) fn new_shared(inner: H5FileInner) -> SharedInner {
91    std::sync::Arc::new(std::sync::RwLock::new(inner))
92}
93
94/// The inner state of an HDF5 file, shared with datasets via reference counting.
95///
96/// By default, this uses `Rc<RefCell<>>` for zero-overhead single-threaded use.
97/// Enable the `threadsafe` feature to use `Arc<Mutex<>>` instead, making
98/// `H5File` `Send + Sync`.
99pub(crate) enum H5FileInner {
100    Writer(Hdf5Writer),
101    Reader(Hdf5Reader),
102    /// Sentinel value used during `close()` to take ownership of the writer.
103    Closed,
104}
105
106/// An HDF5 file opened for reading or writing.
107///
108/// Datasets created from this file hold a shared reference to the underlying
109/// I/O handle, so the file does not need to outlive its datasets (they share
110/// ownership via reference counting).
111pub struct H5File {
112    pub(crate) inner: SharedInner,
113}
114
115impl H5File {
116    /// Create a new HDF5 file at `path`. Truncates if the file already exists.
117    pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
118        let writer = Hdf5Writer::create(path.as_ref())?;
119        Ok(Self {
120            inner: new_shared(H5FileInner::Writer(writer)),
121        })
122    }
123
124    /// Open an existing HDF5 file for reading.
125    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
126        let reader = Hdf5Reader::open(path.as_ref())?;
127        Ok(Self {
128            inner: new_shared(H5FileInner::Reader(reader)),
129        })
130    }
131
132    /// Open an existing HDF5 file for appending new datasets.
133    ///
134    /// Existing datasets are preserved. New datasets can be added and will
135    /// be written after the current end of file. Existing chunked datasets
136    /// can be extended with `write_chunk` and `extend_dataset`.
137    ///
138    /// ```no_run
139    /// use rust_hdf5::H5File;
140    /// let file = H5File::open_rw("existing.h5").unwrap();
141    /// let ds = file.new_dataset::<f64>().shape(&[100]).create("new_data").unwrap();
142    /// ds.write_raw(&vec![0.0f64; 100]).unwrap();
143    /// file.close().unwrap();
144    /// ```
145    pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
146        let writer = Hdf5Writer::open_append(path.as_ref())?;
147        Ok(Self {
148            inner: new_shared(H5FileInner::Writer(writer)),
149        })
150    }
151
152    /// Start building open options for an HDF5 file.
153    ///
154    /// Use this to control file-locking behavior explicitly:
155    ///
156    /// ```no_run
157    /// use rust_hdf5::{H5File, FileLocking};
158    /// // Open with locking disabled (e.g. on NFS without lock support).
159    /// let file = H5File::options()
160    ///     .locking(FileLocking::Disabled)
161    ///     .open_rw("existing.h5")
162    ///     .unwrap();
163    /// # let _ = file;
164    /// ```
165    pub fn options() -> H5FileOptions {
166        H5FileOptions::default()
167    }
168
169    /// Opt in to the latest file format for datasets created after this call —
170    /// the equivalent of libhdf5's `H5Pset_libver_bounds(low = H5F_LIBVER_V200)`.
171    ///
172    /// With `latest` set, filtered chunked datasets get a version-5 data layout
173    /// message, whose chunk indexes store on-disk chunk sizes in fixed-width
174    /// (`sizeof_size`, i.e. 8-byte) fields instead of fields sized from the
175    /// uncompressed chunk size. That removes the overflow risk when a filter
176    /// *expands* a chunk, but the file is only readable by libhdf5 ≥ 2.0
177    /// (h5py bundling hdf5 1.14 rejects it with "bad version number").
178    ///
179    /// Off by default; unfiltered and contiguous datasets are unaffected.
180    /// Independent of this setting, a chunk larger than 4 GiB forces version 5
181    /// because version 4 cannot represent its size field, matching libhdf5.
182    ///
183    /// Errors in read mode.
184    pub fn set_libver_latest(&self, latest: bool) -> Result<()> {
185        let mut inner = borrow_inner_mut(&self.inner);
186        match &mut *inner {
187            H5FileInner::Writer(writer) => {
188                writer.set_libver_latest(latest);
189                Ok(())
190            }
191            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
192        }
193    }
194
195    /// Return a handle to the root group.
196    ///
197    /// The root group can be used to create datasets and sub-groups.
198    pub fn root_group(&self) -> H5Group {
199        H5Group::new(clone_inner(&self.inner), "/".to_string())
200    }
201
202    /// Create a group in the root of the file.
203    ///
204    /// ```no_run
205    /// use rust_hdf5::H5File;
206    /// let file = H5File::create("groups.h5").unwrap();
207    /// let grp = file.create_group("detector").unwrap();
208    /// ```
209    pub fn create_group(&self, name: &str) -> Result<H5Group> {
210        self.root_group().create_group(name)
211    }
212
213    /// Start building a new dataset with the given element type.
214    ///
215    /// This returns a fluent builder. Call `.shape(...)` to set dimensions and
216    /// `.create("name")` to finalize.
217    ///
218    /// ```no_run
219    /// # use rust_hdf5::H5File;
220    /// let file = H5File::create("build.h5").unwrap();
221    /// let ds = file.new_dataset::<f64>().shape(&[3, 4]).create("matrix").unwrap();
222    /// ```
223    pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
224        DatasetBuilder::new(clone_inner(&self.inner))
225    }
226
227    /// Add a string attribute to the file (root group).
228    ///
229    /// The value is stored as a variable-length UTF-8 string (read back as a
230    /// Python `str` by h5py), not a fixed-length string.
231    pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
232        let inner = borrow_inner(&self.inner);
233        match &*inner {
234            H5FileInner::Writer(writer) => {
235                writer.set_vlen_string_attribute(
236                    crate::io::writer::AttrTarget::Root,
237                    name,
238                    value,
239                )?;
240                Ok(())
241            }
242            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
243        }
244    }
245
246    /// Add a numeric attribute to the file (root group).
247    pub fn set_attr_numeric<T: crate::types::H5Type>(&self, name: &str, value: &T) -> Result<()> {
248        use crate::format::messages::attribute::AttributeMessage;
249        let es = T::element_size();
250        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
251        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), raw.to_vec());
252        let inner = borrow_inner(&self.inner);
253        match &*inner {
254            H5FileInner::Writer(writer) => {
255                writer.add_root_attribute(attr)?;
256                Ok(())
257            }
258            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
259        }
260    }
261
262    /// Add a numeric (or bool) **array** attribute to the file (root group).
263    ///
264    /// The values are written as a 1-D HDF5 array attribute (simple dataspace
265    /// `[values.len()]`, on-disk type `T::hdf5_type()`), read back by h5py as a
266    /// numpy array — the array counterpart of [`set_attr_numeric`](Self::set_attr_numeric).
267    /// For a multi-dimensional shape use
268    /// [`set_attr_array_numeric_nd`](Self::set_attr_array_numeric_nd).
269    pub fn set_attr_array_numeric<T: crate::types::H5Type>(
270        &self,
271        name: &str,
272        values: &[T],
273    ) -> Result<()> {
274        self.set_attr_array_numeric_nd(name, values, &[values.len()])
275    }
276
277    /// Add a numeric (or bool) **N-dimensional array** attribute to the file
278    /// (root group).
279    ///
280    /// `shape` gives the dataspace dimensions; `values` is the row-major data
281    /// and its length must equal the product of `shape` (an empty `shape` is a
282    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
283    /// of that shape. [`set_attr_array_numeric`](Self::set_attr_array_numeric)
284    /// is the 1-D convenience form.
285    pub fn set_attr_array_numeric_nd<T: crate::types::H5Type>(
286        &self,
287        name: &str,
288        values: &[T],
289        shape: &[usize],
290    ) -> Result<()> {
291        use crate::format::messages::attribute::AttributeMessage;
292        let n: usize = shape.iter().product();
293        if values.len() != n {
294            return Err(Hdf5Error::InvalidState(format!(
295                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
296                values.len()
297            )));
298        }
299        let es = T::element_size();
300        // Safety: `T: H5Type` is a `Copy` POD numeric whose byte width is `es`.
301        let raw =
302            unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
303        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
304        let attr = AttributeMessage::array_numeric(name, T::hdf5_type(), &dims, raw.to_vec());
305        let mut inner = borrow_inner_mut(&self.inner);
306        match &mut *inner {
307            H5FileInner::Writer(writer) => {
308                writer.add_root_attribute(attr)?;
309                Ok(())
310            }
311            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
312        }
313    }
314
315    /// Add a variable-length UTF-8 string **array** attribute to the file (root
316    /// group), read back by h5py as a 1-D array of `str` — the array counterpart
317    /// of [`set_attr_string`](Self::set_attr_string). For a multi-dimensional
318    /// shape use [`set_attr_string_array_nd`](Self::set_attr_string_array_nd).
319    pub fn set_attr_string_array(&self, name: &str, values: &[&str]) -> Result<()> {
320        self.set_attr_string_array_nd(name, values, &[values.len()])
321    }
322
323    /// Add a variable-length UTF-8 string **N-dimensional array** attribute to
324    /// the file (root group).
325    ///
326    /// `shape` gives the dataspace dimensions; `values` is the row-major data
327    /// and its length must equal the product of `shape` (an empty `shape` is a
328    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
329    /// of Python `str` with that shape.
330    /// [`set_attr_string_array`](Self::set_attr_string_array) is the 1-D
331    /// convenience form.
332    pub fn set_attr_string_array_nd(
333        &self,
334        name: &str,
335        values: &[&str],
336        shape: &[usize],
337    ) -> Result<()> {
338        let n: usize = shape.iter().product();
339        if values.len() != n {
340            return Err(Hdf5Error::InvalidState(format!(
341                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
342                values.len()
343            )));
344        }
345        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
346        let mut inner = borrow_inner_mut(&self.inner);
347        match &mut *inner {
348            H5FileInner::Writer(writer) => {
349                writer.set_vlen_string_array_attribute(
350                    crate::io::writer::AttrTarget::Root,
351                    name,
352                    values,
353                    &dims,
354                )?;
355                Ok(())
356            }
357            _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
358        }
359    }
360
361    /// Return the names of file-level (root group) attributes.
362    pub fn attr_names(&self) -> Result<Vec<String>> {
363        let inner = borrow_inner(&self.inner);
364        match &*inner {
365            H5FileInner::Reader(reader) => Ok(reader.root_attr_names()),
366            _ => Ok(vec![]),
367        }
368    }
369
370    /// Read a file-level string attribute.
371    pub fn attr_string(&self, name: &str) -> Result<String> {
372        let mut inner = borrow_inner_mut(&self.inner);
373        match &mut *inner {
374            H5FileInner::Reader(reader) => {
375                let attr = reader
376                    .root_attr(name)
377                    .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
378                    .clone();
379                Ok(reader.attr_string_value(&attr)?)
380            }
381            _ => Err(Hdf5Error::InvalidState("not in read mode".into())),
382        }
383    }
384
385    /// Check if the file is in write/append mode.
386    pub fn is_writable(&self) -> bool {
387        let inner = borrow_inner(&self.inner);
388        matches!(&*inner, H5FileInner::Writer(_))
389    }
390
391    /// Create a variable-length string dataset and write data.
392    ///
393    /// This is a convenience method for writing h5py-compatible vlen string
394    /// datasets using global heap storage.
395    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
396        let inner = borrow_inner(&self.inner);
397        match &*inner {
398            H5FileInner::Writer(writer) => {
399                let idx = writer.create_vlen_string_dataset(name, strings)?;
400                // If the name contains '/', assign the dataset to its parent group
401                if let Some(slash_pos) = name.rfind('/') {
402                    let group_path = &name[..slash_pos];
403                    let abs_group_path = if group_path.starts_with('/') {
404                        group_path.to_string()
405                    } else {
406                        format!("/{}", group_path)
407                    };
408                    writer.assign_dataset_to_group(&abs_group_path, idx)?;
409                }
410                let (shape, element_size, chunked, btree2, fixed_array) =
411                    writer.dataset_handle_parts(idx);
412                Ok(H5Dataset::new_writer(
413                    clone_inner(&self.inner),
414                    idx,
415                    shape,
416                    element_size,
417                    chunked,
418                    btree2,
419                    fixed_array,
420                ))
421            }
422            H5FileInner::Reader(_) => {
423                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
424            }
425            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
426        }
427    }
428
429    /// Create a variable-length byte-array dataset and write data.
430    ///
431    /// Each `&[u8]` becomes one element of variable length, stored as a vlen
432    /// sequence of `u8` in global heap storage. h5py reads it back as an array
433    /// of `uint8` arrays. Returns a writer-mode handle so attributes can be
434    /// attached, like [`write_vlen_strings`](Self::write_vlen_strings).
435    pub fn write_vlen_bytes(&self, name: &str, items: &[&[u8]]) -> Result<H5Dataset> {
436        let inner = borrow_inner(&self.inner);
437        match &*inner {
438            H5FileInner::Writer(writer) => {
439                let idx = writer.create_vlen_bytes_dataset(name, items)?;
440                // If the name contains '/', assign the dataset to its parent group
441                if let Some(slash_pos) = name.rfind('/') {
442                    let group_path = &name[..slash_pos];
443                    let abs_group_path = if group_path.starts_with('/') {
444                        group_path.to_string()
445                    } else {
446                        format!("/{}", group_path)
447                    };
448                    writer.assign_dataset_to_group(&abs_group_path, idx)?;
449                }
450                let (shape, element_size, chunked, btree2, fixed_array) =
451                    writer.dataset_handle_parts(idx);
452                Ok(H5Dataset::new_writer(
453                    clone_inner(&self.inner),
454                    idx,
455                    shape,
456                    element_size,
457                    chunked,
458                    btree2,
459                    fixed_array,
460                ))
461            }
462            H5FileInner::Reader(_) => {
463                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
464            }
465            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
466        }
467    }
468
469    /// Create a chunked, compressed variable-length string dataset.
470    ///
471    /// Like `write_vlen_strings`, but stores the vlen references in chunked
472    /// layout with the given filter pipeline (e.g., `FilterPipeline::deflate(6)`
473    /// or `FilterPipeline::zstd(3)`). `chunk_size` is the number of strings
474    /// per chunk.
475    pub fn write_vlen_strings_compressed(
476        &self,
477        name: &str,
478        strings: &[&str],
479        chunk_size: usize,
480        pipeline: FilterPipeline,
481    ) -> Result<H5Dataset> {
482        let inner = borrow_inner(&self.inner);
483        match &*inner {
484            H5FileInner::Writer(writer) => {
485                let idx = writer
486                    .create_vlen_string_dataset_compressed(name, strings, chunk_size, pipeline)?;
487                if let Some(slash_pos) = name.rfind('/') {
488                    let group_path = &name[..slash_pos];
489                    let abs_group_path = if group_path.starts_with('/') {
490                        group_path.to_string()
491                    } else {
492                        format!("/{}", group_path)
493                    };
494                    writer.assign_dataset_to_group(&abs_group_path, idx)?;
495                }
496                let (shape, element_size, chunked, btree2, fixed_array) =
497                    writer.dataset_handle_parts(idx);
498                Ok(H5Dataset::new_writer(
499                    clone_inner(&self.inner),
500                    idx,
501                    shape,
502                    element_size,
503                    chunked,
504                    btree2,
505                    fixed_array,
506                ))
507            }
508            H5FileInner::Reader(_) => {
509                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
510            }
511            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
512        }
513    }
514
515    /// Create an empty chunked vlen string dataset ready for incremental appends.
516    ///
517    /// Use `append_vlen_strings` to add data. If `pipeline` is `Some`, chunks
518    /// are compressed (e.g., `Some(FilterPipeline::lz4())`).
519    pub fn create_appendable_vlen_dataset(
520        &self,
521        name: &str,
522        chunk_size: usize,
523        pipeline: Option<FilterPipeline>,
524    ) -> Result<H5Dataset> {
525        let inner = borrow_inner(&self.inner);
526        match &*inner {
527            H5FileInner::Writer(writer) => {
528                let idx =
529                    writer.create_appendable_vlen_string_dataset(name, chunk_size, pipeline)?;
530                if let Some(slash_pos) = name.rfind('/') {
531                    let group_path = &name[..slash_pos];
532                    let abs_group_path = if group_path.starts_with('/') {
533                        group_path.to_string()
534                    } else {
535                        format!("/{}", group_path)
536                    };
537                    writer.assign_dataset_to_group(&abs_group_path, idx)?;
538                }
539                let (shape, element_size, chunked, btree2, fixed_array) =
540                    writer.dataset_handle_parts(idx);
541                Ok(H5Dataset::new_writer(
542                    clone_inner(&self.inner),
543                    idx,
544                    shape,
545                    element_size,
546                    chunked,
547                    btree2,
548                    fixed_array,
549                ))
550            }
551            H5FileInner::Reader(_) => {
552                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
553            }
554            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
555        }
556    }
557
558    /// Append variable-length strings to an existing chunked vlen string dataset.
559    pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
560        let inner = borrow_inner(&self.inner);
561        match &*inner {
562            H5FileInner::Writer(writer) => {
563                let ds_index = writer
564                    .dataset_index(name)
565                    .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
566                writer.append_vlen_strings(ds_index, strings)?;
567                Ok(())
568            }
569            H5FileInner::Reader(_) => {
570                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
571            }
572            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
573        }
574    }
575
576    /// Delete a dataset name, with libhdf5's `H5Ldelete` semantics: a
577    /// path naming a hard link removes just that link, and if a hard link
578    /// still names the object whose tree name is deleted, the dataset
579    /// lives on under the link. Deleting the last name unlinks the
580    /// dataset on close and the file space it owned — data blocks,
581    /// chunk-index structures, and the global-heap objects of
582    /// variable-length values — is freed for reuse by later writes in this
583    /// session (the file itself does not shrink).
584    pub fn delete_dataset(&self, name: &str) -> Result<()> {
585        let inner = borrow_inner(&self.inner);
586        match &*inner {
587            H5FileInner::Writer(writer) => {
588                writer.delete_dataset(name)?;
589                Ok(())
590            }
591            _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
592        }
593    }
594
595    /// Delete a group and all its child datasets/sub-groups, freeing their
596    /// file space the way [`delete_dataset`](Self::delete_dataset) does.
597    ///
598    /// Hard links reaching in from outside the deleted subtree keep their
599    /// targets alive: a dataset or group named by such a link survives
600    /// under the link's path (a group brings its whole subtree with it),
601    /// and a `name` that is itself a hard link's path removes just that
602    /// link.
603    pub fn delete_group(&self, name: &str) -> Result<()> {
604        let inner = borrow_inner(&self.inner);
605        match &*inner {
606            H5FileInner::Writer(writer) => {
607                writer.delete_group(name)?;
608                Ok(())
609            }
610            _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
611        }
612    }
613
614    /// Open an existing dataset by name (read mode).
615    pub fn dataset(&self, name: &str) -> Result<H5Dataset> {
616        let inner = borrow_inner(&self.inner);
617        match &*inner {
618            H5FileInner::Reader(reader) => {
619                let info = reader
620                    .dataset_info(name)
621                    .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
622                let shape: Vec<usize> = info.dataspace.dims.iter().map(|&d| d as usize).collect();
623                let element_size = info.datatype.element_size() as usize;
624                Ok(H5Dataset::new_reader(
625                    clone_inner(&self.inner),
626                    name.to_string(),
627                    shape,
628                    element_size,
629                ))
630            }
631            H5FileInner::Writer(_) => Err(Hdf5Error::InvalidState(
632                "cannot open a dataset by name in write mode; use new_dataset() instead"
633                    .to_string(),
634            )),
635            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
636        }
637    }
638
639    /// Reopen an existing dataset by name in write mode.
640    ///
641    /// [`dataset`](Self::dataset) only works in read mode; in write mode a
642    /// dataset is normally created via [`new_dataset`](Self::new_dataset).
643    /// This returns a write-mode handle to a dataset created earlier in the
644    /// same session, so you can attach attributes or append chunks to it
645    /// without keeping the original handle around — e.g. to flush cached
646    /// first/last values onto a dataset at file-close time.
647    ///
648    /// # Errors
649    ///
650    /// Returns [`Hdf5Error::NotFound`] if no live dataset with that name
651    /// exists, and an error in read mode (use [`dataset`](Self::dataset)).
652    pub fn dataset_writer(&self, name: &str) -> Result<H5Dataset> {
653        let inner = borrow_inner(&self.inner);
654        match &*inner {
655            H5FileInner::Writer(writer) => {
656                let index = writer
657                    .dataset_index(name)
658                    .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
659                let (shape, element_size, chunked, btree2, fixed_array) =
660                    writer.dataset_handle_parts(index);
661                Ok(H5Dataset::new_writer(
662                    clone_inner(&self.inner),
663                    index,
664                    shape,
665                    element_size,
666                    chunked,
667                    btree2,
668                    fixed_array,
669                ))
670            }
671            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
672                "cannot open a dataset_writer in read mode; use dataset() instead".to_string(),
673            )),
674            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
675        }
676    }
677
678    /// Return the names of all datasets in the root group.
679    ///
680    /// Works in both read and write mode: in write mode, returns the names of
681    /// datasets created so far; in read mode, returns the names discovered
682    /// during file open.
683    pub fn dataset_names(&self) -> Vec<String> {
684        let inner = borrow_inner(&self.inner);
685        match &*inner {
686            H5FileInner::Reader(reader) => reader
687                .dataset_names()
688                .iter()
689                .map(|s| s.to_string())
690                .collect(),
691            H5FileInner::Writer(writer) => writer
692                .dataset_names()
693                .iter()
694                .map(|s| s.to_string())
695                .collect(),
696            H5FileInner::Closed => Vec::new(),
697        }
698    }
699
700    /// Explicitly close the file. For a writer, this finalizes the file
701    /// (writes superblock, headers, etc.). For a reader, this is a no-op.
702    ///
703    /// The file is also auto-finalized on drop, but calling `close()` lets
704    /// you handle errors.
705    pub fn close(self) -> Result<()> {
706        let old = {
707            let mut inner = borrow_inner_mut(&self.inner);
708            std::mem::replace(&mut *inner, H5FileInner::Closed)
709        };
710        match old {
711            H5FileInner::Writer(writer) => {
712                writer.close()?;
713                Ok(())
714            }
715            H5FileInner::Reader(_) => Ok(()),
716            H5FileInner::Closed => Ok(()),
717        }
718    }
719
720    /// Close the file without a final `fsync` (write mode only).
721    ///
722    /// Like [`close`](Self::close), this finalizes the file — object headers
723    /// and superblock are written, so on return it is a complete, valid HDF5
724    /// file readable by any process — but the trailing `sync_all` (fsync) is
725    /// skipped. The bytes are handed to the OS but are not guaranteed durable
726    /// against power loss or an OS crash until the OS flushes its page cache.
727    ///
728    /// This trades durability for speed (the fsync typically dominates close
729    /// latency); use it for bulk output that can be regenerated. Prefer
730    /// [`close`](Self::close) when durability matters. Dropping the file
731    /// without calling either finalizes durably.
732    ///
733    /// For a reader or an already-closed file this is a no-op, matching
734    /// [`close`](Self::close).
735    pub fn close_no_sync(self) -> Result<()> {
736        let old = {
737            let mut inner = borrow_inner_mut(&self.inner);
738            std::mem::replace(&mut *inner, H5FileInner::Closed)
739        };
740        match old {
741            H5FileInner::Writer(writer) => {
742                writer.close_no_sync()?;
743                Ok(())
744            }
745            H5FileInner::Reader(_) => Ok(()),
746            H5FileInner::Closed => Ok(()),
747        }
748    }
749
750    /// Flush the file to disk. Only meaningful in write mode.
751    pub fn flush(&self) -> Result<()> {
752        // The underlying writer does not expose a standalone flush; data is
753        // written to disk immediately via pwrite. This is a compatibility
754        // method that does nothing for now.
755        Ok(())
756    }
757}
758
759/// Builder controlling how an [`H5File`] is opened.
760///
761/// The default policy follows the HDF5 C library: an exclusive lock is
762/// acquired for write-mode opens and a shared lock for read-mode opens,
763/// honoring the `HDF5_USE_FILE_LOCKING` environment variable. Calling
764/// [`Self::locking`] overrides the env-var value.
765#[derive(Debug, Default, Clone)]
766pub struct H5FileOptions {
767    locking: Option<FileLocking>,
768}
769
770impl H5FileOptions {
771    /// Construct a fresh options builder with default settings.
772    pub fn new() -> Self {
773        Self::default()
774    }
775
776    /// Override the locking policy. Bypasses the `HDF5_USE_FILE_LOCKING`
777    /// environment variable for the resulting open call.
778    pub fn locking(mut self, policy: FileLocking) -> Self {
779        self.locking = Some(policy);
780        self
781    }
782
783    /// Disable OS-level file locking entirely (equivalent to
784    /// `HDF5_USE_FILE_LOCKING=FALSE`).
785    pub fn no_locking(self) -> Self {
786        self.locking(FileLocking::Disabled)
787    }
788
789    /// Try to acquire the lock but do not fail if the filesystem rejects it
790    /// (equivalent to `HDF5_USE_FILE_LOCKING=BEST_EFFORT`).
791    pub fn best_effort_locking(self) -> Self {
792        self.locking(FileLocking::BestEffort)
793    }
794
795    fn resolved_locking(&self) -> FileLocking {
796        match self.locking {
797            Some(p) => p,
798            None => FileLocking::from_env_or(FileLocking::default()),
799        }
800    }
801
802    /// Create a new HDF5 file at `path` with the configured options.
803    pub fn create<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
804        let writer = Hdf5Writer::create_with_locking(path.as_ref(), self.resolved_locking())?;
805        Ok(H5File {
806            inner: new_shared(H5FileInner::Writer(writer)),
807        })
808    }
809
810    /// Open an existing HDF5 file for reading with the configured options.
811    pub fn open<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
812        let reader = Hdf5Reader::open_with_locking(path.as_ref(), self.resolved_locking())?;
813        Ok(H5File {
814            inner: new_shared(H5FileInner::Reader(reader)),
815        })
816    }
817
818    /// Open an existing HDF5 file for read/write with the configured options.
819    pub fn open_rw<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
820        let writer = Hdf5Writer::open_append_with_locking(path.as_ref(), self.resolved_locking())?;
821        Ok(H5File {
822            inner: new_shared(H5FileInner::Writer(writer)),
823        })
824    }
825}
826
827#[cfg(test)]
828fn unique_test_path(name: &str) -> std::path::PathBuf {
829    // PID + atomic counter so each test invocation uses a distinct path,
830    // preventing collisions across concurrent cargo runs and any
831    // flock/LockFileEx race where a previous close()'d file's lock
832    // remains briefly visible when reopening the same path.
833    use std::sync::atomic::{AtomicU64, Ordering};
834    static COUNTER: AtomicU64 = AtomicU64::new(0);
835    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
836    std::env::temp_dir().join(format!(
837        "rust_hdf5_test_{}_{}_{}.h5",
838        name,
839        std::process::id(),
840        n
841    ))
842}
843
844#[cfg(test)]
845mod tests {
846    use super::*;
847    use std::path::PathBuf;
848
849    fn temp_path(name: &str) -> PathBuf {
850        super::unique_test_path(name)
851    }
852
853    #[test]
854    fn create_and_close_empty() {
855        let path = temp_path("create_empty");
856        let file = H5File::create(&path).unwrap();
857        file.close().unwrap();
858
859        // Should be readable
860        let file = H5File::open(&path).unwrap();
861        file.close().unwrap();
862
863        std::fs::remove_file(&path).ok();
864    }
865
866    #[test]
867    fn create_and_drop_empty() {
868        let path = temp_path("drop_empty");
869        {
870            let _file = H5File::create(&path).unwrap();
871            // drop auto-finalizes
872        }
873        // Verify the file is valid by opening it
874        let file = H5File::open(&path).unwrap();
875        file.close().unwrap();
876
877        std::fs::remove_file(&path).ok();
878    }
879
880    #[test]
881    fn dataset_not_found() {
882        let path = temp_path("ds_not_found");
883        {
884            let _file = H5File::create(&path).unwrap();
885        }
886        let file = H5File::open(&path).unwrap();
887        let result = file.dataset("nonexistent");
888        assert!(result.is_err());
889
890        std::fs::remove_file(&path).ok();
891    }
892
893    #[test]
894    fn write_and_read_roundtrip() {
895        let path = temp_path("write_read_rt");
896
897        // Write
898        {
899            let file = H5File::create(&path).unwrap();
900            let ds = file
901                .new_dataset::<u8>()
902                .shape([4, 4])
903                .create("data")
904                .unwrap();
905            ds.write_raw(&[0u8; 16]).unwrap();
906            file.close().unwrap();
907        }
908
909        // Read
910        {
911            let file = H5File::open(&path).unwrap();
912            let ds = file.dataset("data").unwrap();
913            assert_eq!(ds.shape(), vec![4, 4]);
914            let data = ds.read_raw::<u8>().unwrap();
915            assert_eq!(data.len(), 16);
916            assert!(data.iter().all(|&b| b == 0));
917            file.close().unwrap();
918        }
919
920        std::fs::remove_file(&path).ok();
921    }
922
923    #[test]
924    fn close_no_sync_produces_valid_readable_file() {
925        let path = temp_path("close_no_sync_rt");
926        let payload: Vec<u8> = (0u8..16).collect();
927
928        // Write and finalize WITHOUT the trailing fsync.
929        {
930            let file = H5File::create(&path).unwrap();
931            let ds = file
932                .new_dataset::<u8>()
933                .shape([4, 4])
934                .create("data")
935                .unwrap();
936            ds.write_raw(&payload).unwrap();
937            // The only difference from `write_and_read_roundtrip`: no fsync.
938            // The file must still be a complete, valid, readable HDF5 file.
939            file.close_no_sync().unwrap();
940        }
941
942        // Reopen and verify the full content survived (same-machine reader sees
943        // the OS page cache regardless of whether fsync ran).
944        {
945            let file = H5File::open(&path).unwrap();
946            let ds = file.dataset("data").unwrap();
947            assert_eq!(ds.shape(), vec![4, 4]);
948            let data = ds.read_raw::<u8>().unwrap();
949            assert_eq!(data, payload);
950            file.close().unwrap();
951        }
952
953        std::fs::remove_file(&path).ok();
954    }
955
956    #[test]
957    fn create_over_existing_file_truncates() {
958        // The create path skips the ftruncate on a brand-new empty file (it
959        // arms ext4's auto_da_alloc and turns close(2) into an implicit
960        // writeback, defeating close_no_sync). This pins the other side of
961        // that guard: creating over an existing non-empty file must still
962        // truncate it, so no stale content survives.
963        let path = temp_path("create_truncates");
964
965        {
966            let file = H5File::create(&path).unwrap();
967            let ds = file
968                .new_dataset::<u8>()
969                .shape([4, 4])
970                .create("old_data")
971                .unwrap();
972            ds.write_raw(&[7u8; 16]).unwrap();
973            file.close().unwrap();
974        }
975        assert!(std::fs::metadata(&path).unwrap().len() > 0);
976
977        // Re-create over the non-empty file, write nothing.
978        {
979            let file = H5File::create(&path).unwrap();
980            file.close().unwrap();
981        }
982
983        // The old dataset must be gone.
984        let file = H5File::open(&path).unwrap();
985        assert!(file.dataset("old_data").is_err());
986        file.close().unwrap();
987
988        std::fs::remove_file(&path).ok();
989    }
990
991    #[test]
992    fn close_no_sync_chunked_dataset_valid() {
993        // Exercises flush_dataset_synced(sync=false): a chunked (EA-indexed)
994        // dataset closed with close_no_sync must skip the per-dataset
995        // sync_data yet still write valid index structures, so the reopened
996        // file reconstructs every frame.
997        let path = temp_path("close_no_sync_chunked");
998
999        {
1000            let file = H5File::create(&path).unwrap();
1001            let ds = file
1002                .new_dataset::<i32>()
1003                .shape([0usize, 3])
1004                .chunk(&[1, 3])
1005                .max_shape(&[None, Some(3)])
1006                .create("data")
1007                .unwrap();
1008            // 10 frames exceeds idx_blk_elmts=4, so data blocks are exercised.
1009            for frame in 0..10u64 {
1010                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1011                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1012                ds.write_chunk(frame as usize, &raw).unwrap();
1013            }
1014            ds.extend(&[10, 3]).unwrap();
1015            file.close_no_sync().unwrap();
1016        }
1017
1018        {
1019            let file = H5File::open(&path).unwrap();
1020            let ds = file.dataset("data").unwrap();
1021            assert_eq!(ds.shape(), vec![10, 3]);
1022            let data = ds.read_raw::<i32>().unwrap();
1023            let expected: Vec<i32> = (0..30).collect();
1024            assert_eq!(data, expected);
1025            file.close().unwrap();
1026        }
1027
1028        std::fs::remove_file(&path).ok();
1029    }
1030
1031    #[test]
1032    fn write_and_read_f64() {
1033        let path = temp_path("write_read_f64");
1034
1035        let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1036
1037        // Write
1038        {
1039            let file = H5File::create(&path).unwrap();
1040            let ds = file
1041                .new_dataset::<f64>()
1042                .shape([2, 3])
1043                .create("matrix")
1044                .unwrap();
1045            ds.write_raw(&values).unwrap();
1046            file.close().unwrap();
1047        }
1048
1049        // Read
1050        {
1051            let file = H5File::open(&path).unwrap();
1052            let ds = file.dataset("matrix").unwrap();
1053            assert_eq!(ds.shape(), vec![2, 3]);
1054            let readback = ds.read_raw::<f64>().unwrap();
1055            assert_eq!(readback, values);
1056        }
1057
1058        std::fs::remove_file(&path).ok();
1059    }
1060
1061    #[test]
1062    fn multiple_datasets() {
1063        let path = temp_path("multi_ds");
1064
1065        {
1066            let file = H5File::create(&path).unwrap();
1067            let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
1068            ds1.write_raw(&[10i32, 20, 30]).unwrap();
1069
1070            let ds2 = file
1071                .new_dataset::<f32>()
1072                .shape([2, 2])
1073                .create("floats")
1074                .unwrap();
1075            ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
1076
1077            file.close().unwrap();
1078        }
1079
1080        {
1081            let file = H5File::open(&path).unwrap();
1082
1083            let ds_ints = file.dataset("ints").unwrap();
1084            assert_eq!(ds_ints.shape(), vec![3]);
1085            let ints = ds_ints.read_raw::<i32>().unwrap();
1086            assert_eq!(ints, vec![10, 20, 30]);
1087
1088            let ds_floats = file.dataset("floats").unwrap();
1089            assert_eq!(ds_floats.shape(), vec![2, 2]);
1090            let floats = ds_floats.read_raw::<f32>().unwrap();
1091            assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
1092        }
1093
1094        std::fs::remove_file(&path).ok();
1095    }
1096
1097    #[test]
1098    fn close_is_idempotent() {
1099        let path = temp_path("close_idemp");
1100        let file = H5File::create(&path).unwrap();
1101        file.close().unwrap();
1102        // File is consumed by close(), so no double-close possible at the type level.
1103        std::fs::remove_file(&path).ok();
1104    }
1105}
1106
1107#[cfg(test)]
1108mod integration_tests {
1109    use super::*;
1110
1111    fn temp_path(name: &str) -> std::path::PathBuf {
1112        super::unique_test_path(name)
1113    }
1114
1115    #[test]
1116    fn write_file_for_h5dump() {
1117        let path = temp_path("integration");
1118        let file = H5File::create(&path).unwrap();
1119
1120        let ds = file
1121            .new_dataset::<u8>()
1122            .shape([4usize, 4])
1123            .create("data_u8")
1124            .unwrap();
1125        let data: Vec<u8> = (0..16).collect();
1126        ds.write_raw(&data).unwrap();
1127
1128        let ds2 = file
1129            .new_dataset::<f64>()
1130            .shape([3usize, 2])
1131            .create("data_f64")
1132            .unwrap();
1133        let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1134        ds2.write_raw(&fdata).unwrap();
1135
1136        let ds3 = file
1137            .new_dataset::<i32>()
1138            .shape([5usize])
1139            .create("values")
1140            .unwrap();
1141        let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
1142        ds3.write_raw(&idata).unwrap();
1143
1144        file.close().unwrap();
1145
1146        // File exists
1147        assert!(path.exists());
1148    }
1149
1150    #[test]
1151    fn write_chunked_file_for_h5dump() {
1152        let path = temp_path("chunked");
1153        let file = H5File::create(&path).unwrap();
1154
1155        // Create a chunked dataset with unlimited first dimension
1156        let ds = file
1157            .new_dataset::<f64>()
1158            .shape([0usize, 4])
1159            .chunk(&[1, 4])
1160            .max_shape(&[None, Some(4)])
1161            .create("streaming_data")
1162            .unwrap();
1163
1164        // Write 5 frames of data
1165        for frame in 0..5u64 {
1166            let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1167            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1168            ds.write_chunk(frame as usize, &raw).unwrap();
1169        }
1170
1171        // Extend dimensions to reflect the 5 written frames
1172        ds.extend(&[5, 4]).unwrap();
1173        ds.flush().unwrap();
1174
1175        file.close().unwrap();
1176
1177        assert!(path.exists());
1178    }
1179
1180    #[test]
1181    fn write_chunked_many_frames_for_h5dump() {
1182        let path = temp_path("chunked_many");
1183        let file = H5File::create(&path).unwrap();
1184
1185        let ds = file
1186            .new_dataset::<i32>()
1187            .shape([0usize, 3])
1188            .chunk(&[1, 3])
1189            .max_shape(&[None, Some(3)])
1190            .create("data")
1191            .unwrap();
1192
1193        // Write 10 frames (exceeds idx_blk_elmts=4, uses data blocks)
1194        for frame in 0..10u64 {
1195            let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1196            let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1197            ds.write_chunk(frame as usize, &raw).unwrap();
1198        }
1199        ds.extend(&[10, 3]).unwrap();
1200        file.close().unwrap();
1201
1202        assert!(path.exists());
1203    }
1204
1205    #[test]
1206    fn write_dataset_with_attributes() {
1207        use crate::types::VarLenUnicode;
1208
1209        let path = temp_path("attributes");
1210        let file = H5File::create(&path).unwrap();
1211
1212        let ds = file
1213            .new_dataset::<f32>()
1214            .shape([10usize])
1215            .create("temperature")
1216            .unwrap();
1217        let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
1218        ds.write_raw(&data).unwrap();
1219
1220        // Add string attributes
1221        let attr = ds
1222            .new_attr::<VarLenUnicode>()
1223            .shape(())
1224            .create("units")
1225            .unwrap();
1226        attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
1227            .unwrap();
1228
1229        let attr2 = ds
1230            .new_attr::<VarLenUnicode>()
1231            .shape(())
1232            .create("description")
1233            .unwrap();
1234        attr2
1235            .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
1236            .unwrap();
1237
1238        // Use write_string convenience method
1239        let attr3 = ds
1240            .new_attr::<VarLenUnicode>()
1241            .shape(())
1242            .create("source")
1243            .unwrap();
1244        attr3.write_string("sensor_01").unwrap();
1245
1246        // Also test parse -> write_scalar pattern
1247        let attr4 = ds
1248            .new_attr::<VarLenUnicode>()
1249            .shape(())
1250            .create("label")
1251            .unwrap();
1252        let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
1253        attr4.write_scalar(&s).unwrap();
1254
1255        file.close().unwrap();
1256
1257        assert!(path.exists());
1258    }
1259
1260    #[test]
1261    fn dataset_writer_reopens_for_attributes() {
1262        // Reopen a dataset by name in write mode (the original handle is gone)
1263        // and attach attributes to it — the close-time flush pattern.
1264        let path = temp_path("dataset_writer");
1265        {
1266            let file = H5File::create(&path).unwrap();
1267            {
1268                let ds = file
1269                    .new_dataset::<u16>()
1270                    .shape([8])
1271                    .create("image")
1272                    .unwrap();
1273                ds.write_raw(&[0u16; 8]).unwrap();
1274                // original handle dropped here
1275            }
1276
1277            // Reopen by name; dataset() would error in write mode.
1278            assert!(file.dataset("image").is_err());
1279            let ds = file.dataset_writer("image").unwrap();
1280            assert_eq!(ds.shape(), vec![8]);
1281            ds.new_attr::<i32>()
1282                .shape([3])
1283                .create("NDArrayDimOffset")
1284                .unwrap()
1285                .write_array(&[0i32, 4, 8])
1286                .unwrap();
1287            ds.new_attr::<i32>()
1288                .shape(())
1289                .create("NDUniqueId")
1290                .unwrap()
1291                .write_numeric(&42i32)
1292                .unwrap();
1293
1294            // Missing dataset is reported.
1295            assert!(matches!(
1296                file.dataset_writer("nope"),
1297                Err(crate::error::Hdf5Error::NotFound(_))
1298            ));
1299
1300            file.close().unwrap();
1301        }
1302        {
1303            let file = H5File::open(&path).unwrap();
1304            let ds = file.dataset("image").unwrap();
1305            let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
1306            let got: Vec<i32> = off
1307                .chunks_exact(4)
1308                .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1309                .collect();
1310            assert_eq!(got, vec![0, 4, 8]);
1311            let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
1312            assert_eq!(uid, 42);
1313        }
1314        std::fs::remove_file(&path).ok();
1315    }
1316
1317    #[test]
1318    fn chunked_write_read_roundtrip() {
1319        let path = temp_path("chunked_roundtrip");
1320
1321        // Write
1322        {
1323            let file = H5File::create(&path).unwrap();
1324            let ds = file
1325                .new_dataset::<i32>()
1326                .shape([0usize, 3])
1327                .chunk(&[1, 3])
1328                .max_shape(&[None, Some(3)])
1329                .create("table")
1330                .unwrap();
1331
1332            for frame in 0..8u64 {
1333                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1334                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1335                ds.write_chunk(frame as usize, &raw).unwrap();
1336            }
1337            ds.extend(&[8, 3]).unwrap();
1338            file.close().unwrap();
1339        }
1340
1341        // Read
1342        {
1343            let file = H5File::open(&path).unwrap();
1344            let ds = file.dataset("table").unwrap();
1345            assert_eq!(ds.shape(), vec![8, 3]);
1346            let data = ds.read_raw::<i32>().unwrap();
1347            assert_eq!(data.len(), 24);
1348            for (i, val) in data.iter().enumerate() {
1349                assert_eq!(*val, i as i32);
1350            }
1351        }
1352
1353        std::fs::remove_file(&path).ok();
1354    }
1355
1356    #[test]
1357    #[cfg(feature = "deflate")]
1358    fn compressed_chunked_roundtrip() {
1359        let path = temp_path("compressed_roundtrip");
1360
1361        // Write compressed
1362        {
1363            let file = H5File::create(&path).unwrap();
1364            let ds = file
1365                .new_dataset::<f64>()
1366                .shape([0usize, 4])
1367                .chunk(&[1, 4])
1368                .max_shape(&[None, Some(4)])
1369                .deflate(6)
1370                .create("compressed")
1371                .unwrap();
1372
1373            for frame in 0..10u64 {
1374                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1375                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1376                ds.write_chunk(frame as usize, &raw).unwrap();
1377            }
1378            ds.extend(&[10, 4]).unwrap();
1379            file.close().unwrap();
1380        }
1381
1382        // Read back and verify
1383        {
1384            let file = H5File::open(&path).unwrap();
1385            let ds = file.dataset("compressed").unwrap();
1386            assert_eq!(ds.shape(), vec![10, 4]);
1387            let data = ds.read_raw::<f64>().unwrap();
1388            assert_eq!(data.len(), 40);
1389            for (i, val) in data.iter().enumerate() {
1390                assert!(
1391                    (val - i as f64).abs() < 1e-10,
1392                    "mismatch at {}: {} != {}",
1393                    i,
1394                    val,
1395                    i
1396                );
1397            }
1398        }
1399
1400        std::fs::remove_file(&path).ok();
1401    }
1402
1403    #[test]
1404    #[cfg(feature = "deflate")]
1405    fn compressed_chunked_many_frames() {
1406        let path = temp_path("compressed_many");
1407
1408        {
1409            let file = H5File::create(&path).unwrap();
1410            let ds = file
1411                .new_dataset::<i32>()
1412                .shape([0usize, 3])
1413                .chunk(&[1, 3])
1414                .max_shape(&[None, Some(3)])
1415                .deflate(6)
1416                .create("stream")
1417                .unwrap();
1418
1419            for frame in 0..100u64 {
1420                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1421                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1422                ds.write_chunk(frame as usize, &raw).unwrap();
1423            }
1424            ds.extend(&[100, 3]).unwrap();
1425            file.close().unwrap();
1426        }
1427
1428        {
1429            let file = H5File::open(&path).unwrap();
1430            let ds = file.dataset("stream").unwrap();
1431            assert_eq!(ds.shape(), vec![100, 3]);
1432            let data = ds.read_raw::<i32>().unwrap();
1433            assert_eq!(data.len(), 300);
1434            for (i, val) in data.iter().enumerate() {
1435                assert_eq!(*val, i as i32, "mismatch at {}", i);
1436            }
1437        }
1438
1439        std::fs::remove_file(&path).ok();
1440    }
1441    #[test]
1442    fn append_mode() {
1443        let path = temp_path("append");
1444
1445        // Create initial file
1446        {
1447            let file = H5File::create(&path).unwrap();
1448            let ds = file
1449                .new_dataset::<i32>()
1450                .shape([3usize])
1451                .create("first")
1452                .unwrap();
1453            ds.write_raw(&[1i32, 2, 3]).unwrap();
1454            file.close().unwrap();
1455        }
1456
1457        // Append new dataset
1458        {
1459            let file = H5File::open_rw(&path).unwrap();
1460            let ds = file
1461                .new_dataset::<f64>()
1462                .shape([2usize])
1463                .create("second")
1464                .unwrap();
1465            ds.write_raw(&[4.0f64, 5.0]).unwrap();
1466            file.close().unwrap();
1467        }
1468
1469        // Read back both
1470        {
1471            let file = H5File::open(&path).unwrap();
1472            let names = file.dataset_names();
1473            assert!(names.contains(&"first".to_string()));
1474            assert!(names.contains(&"second".to_string()));
1475
1476            let ds1 = file.dataset("first").unwrap();
1477            assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
1478
1479            let ds2 = file.dataset("second").unwrap();
1480            assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
1481        }
1482
1483        std::fs::remove_file(&path).ok();
1484    }
1485
1486    #[test]
1487    fn open_rw_set_attr_preserves_file() {
1488        let path = temp_path("open_rw_attr");
1489        // Create file with a dataset and an attribute
1490        {
1491            let file = H5File::create(&path).unwrap();
1492            let ds = file
1493                .new_dataset::<i32>()
1494                .shape([3usize])
1495                .create("data")
1496                .unwrap();
1497            ds.write_raw(&[10i32, 20, 30]).unwrap();
1498            file.set_attr_string("version", "1.0").unwrap();
1499            file.close().unwrap();
1500        }
1501        // Open rw and modify the attribute
1502        {
1503            let file = H5File::open_rw(&path).unwrap();
1504            file.set_attr_string("version", "2.0").unwrap();
1505            file.close().unwrap();
1506        }
1507        // Verify: dataset intact, attribute updated
1508        {
1509            let file = H5File::open(&path).unwrap();
1510            let ds = file.dataset("data").unwrap();
1511            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
1512            let ver = file.attr_string("version").unwrap();
1513            assert_eq!(ver, "2.0");
1514        }
1515        std::fs::remove_file(&path).ok();
1516    }
1517
1518    #[test]
1519    #[cfg(feature = "deflate")]
1520    fn open_rw_attr_with_compressed_dataset() {
1521        use crate::format::messages::filter::FilterPipeline;
1522        let path = temp_path("open_rw_compressed");
1523        let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
1524        // Create file with compressed vlen strings
1525        {
1526            let file = H5File::create(&path).unwrap();
1527            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1528                .unwrap();
1529            file.set_attr_string("version", "1.0").unwrap();
1530            file.close().unwrap();
1531        }
1532        // Open rw and modify attribute only
1533        {
1534            let file = H5File::open_rw(&path).unwrap();
1535            file.set_attr_string("version", "2.0").unwrap();
1536            file.close().unwrap();
1537        }
1538        // Verify: compressed dataset still readable, attribute updated
1539        {
1540            let file = H5File::open(&path).unwrap();
1541            let ds = file.dataset("texts").unwrap();
1542            let strings = ds.read_vlen_strings().unwrap();
1543            assert_eq!(strings.len(), 50);
1544            assert_eq!(strings[0], "test string data");
1545            let ver = file.attr_string("version").unwrap();
1546            assert_eq!(ver, "2.0");
1547        }
1548        std::fs::remove_file(&path).ok();
1549    }
1550
1551    #[test]
1552    #[cfg(feature = "lz4")]
1553    fn append_vlen_strings_basic() {
1554        use crate::format::messages::filter::FilterPipeline;
1555        let path = temp_path("append_vlen");
1556        {
1557            let file = H5File::create(&path).unwrap();
1558            file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
1559                .unwrap();
1560            file.append_vlen_strings("names", &["alice", "bob", "charlie"])
1561                .unwrap();
1562            file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
1563            file.close().unwrap();
1564        }
1565        {
1566            let file = H5File::open(&path).unwrap();
1567            let ds = file.dataset("names").unwrap();
1568            let strings = ds.read_vlen_strings().unwrap();
1569            assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
1570        }
1571        std::fs::remove_file(&path).ok();
1572    }
1573
1574    #[test]
1575    #[cfg(feature = "lz4")]
1576    fn append_vlen_strings_large() {
1577        use crate::format::messages::filter::FilterPipeline;
1578        let path = temp_path("append_vlen_large");
1579        let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
1580        let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
1581        {
1582            let file = H5File::create(&path).unwrap();
1583            file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
1584                .unwrap();
1585            let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
1586            file.append_vlen_strings("data", &r1).unwrap();
1587            let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
1588            file.append_vlen_strings("data", &r2).unwrap();
1589            file.close().unwrap();
1590        }
1591        {
1592            let file = H5File::open(&path).unwrap();
1593            let ds = file.dataset("data").unwrap();
1594            let strings = ds.read_vlen_strings().unwrap();
1595            assert_eq!(strings.len(), 7189);
1596            assert_eq!(strings[0], "node-000000");
1597            assert_eq!(strings[7188], "node-007188");
1598        }
1599        std::fs::remove_file(&path).ok();
1600    }
1601
1602    #[test]
1603    fn append_vlen_strings_uncompressed() {
1604        let path = temp_path("append_vlen_unc");
1605        {
1606            let file = H5File::create(&path).unwrap();
1607            file.create_appendable_vlen_dataset("texts", 8, None)
1608                .unwrap();
1609            file.append_vlen_strings("texts", &["hello", "world"])
1610                .unwrap();
1611            file.append_vlen_strings("texts", &["foo", "bar", "baz"])
1612                .unwrap();
1613            file.close().unwrap();
1614        }
1615        {
1616            let file = H5File::open(&path).unwrap();
1617            let ds = file.dataset("texts").unwrap();
1618            let strings = ds.read_vlen_strings().unwrap();
1619            assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
1620        }
1621        std::fs::remove_file(&path).ok();
1622    }
1623
1624    #[test]
1625    fn delete_dataset_roundtrip() {
1626        let path = temp_path("delete_ds");
1627        {
1628            let file = H5File::create(&path).unwrap();
1629            file.write_vlen_strings("keep", &["a", "b"]).unwrap();
1630            file.write_vlen_strings("remove", &["x", "y"]).unwrap();
1631            file.delete_dataset("remove").unwrap();
1632            file.close().unwrap();
1633        }
1634        {
1635            let file = H5File::open(&path).unwrap();
1636            let names = file.dataset_names();
1637            assert!(names.contains(&"keep".to_string()));
1638            assert!(!names.contains(&"remove".to_string()));
1639            let ds = file.dataset("keep").unwrap();
1640            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
1641        }
1642        std::fs::remove_file(&path).ok();
1643    }
1644
1645    #[test]
1646    fn delete_group_roundtrip() {
1647        let path = temp_path("delete_grp");
1648        {
1649            let file = H5File::create(&path).unwrap();
1650            let g1 = file.create_group("keep").unwrap();
1651            g1.write_vlen_strings("data", &["a"]).unwrap();
1652            let g2 = file.create_group("remove").unwrap();
1653            g2.write_vlen_strings("data", &["x"]).unwrap();
1654            file.delete_group("remove").unwrap();
1655            file.close().unwrap();
1656        }
1657        {
1658            let file = H5File::open(&path).unwrap();
1659            let names = file.dataset_names();
1660            assert!(names.contains(&"keep/data".to_string()));
1661            assert!(!names.contains(&"remove/data".to_string()));
1662        }
1663        std::fs::remove_file(&path).ok();
1664    }
1665
1666    #[test]
1667    fn open_rw_delete_recreate_group() {
1668        let path = temp_path("rw_delete_recreate");
1669        // Step 1: create file with groups
1670        {
1671            let file = H5File::create(&path).unwrap();
1672            let n = file.create_group("nodes").unwrap();
1673            n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
1674            let e = file.create_group("edges").unwrap();
1675            e.write_vlen_strings("src", &["x", "y"]).unwrap();
1676            file.close().unwrap();
1677        }
1678        // Step 2: open_rw, delete one group, recreate with new data
1679        {
1680            let file = H5File::open_rw(&path).unwrap();
1681            file.delete_group("nodes").unwrap();
1682            let n = file.create_group("nodes").unwrap();
1683            n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
1684            file.close().unwrap();
1685        }
1686        // Step 3: verify
1687        {
1688            let file = H5File::open(&path).unwrap();
1689            let ds = file.dataset("nodes/id").unwrap();
1690            let s = ds.read_vlen_strings().unwrap();
1691            assert_eq!(s, vec!["new1", "new2"]);
1692            // edges should still be intact
1693            let ds = file.dataset("edges/src").unwrap();
1694            let s = ds.read_vlen_strings().unwrap();
1695            assert_eq!(s, vec!["x", "y"]);
1696        }
1697        std::fs::remove_file(&path).ok();
1698    }
1699
1700    #[test]
1701    fn delete_and_recreate_group() {
1702        let path = temp_path("delete_recreate");
1703        {
1704            let file = H5File::create(&path).unwrap();
1705            let g = file.create_group("nodes").unwrap();
1706            g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
1707            file.delete_group("nodes").unwrap();
1708            let g = file.create_group("nodes").unwrap();
1709            g.write_vlen_strings("id", &["new1", "new2", "new3"])
1710                .unwrap();
1711            file.close().unwrap();
1712        }
1713        {
1714            let file = H5File::open(&path).unwrap();
1715            let ds = file.dataset("nodes/id").unwrap();
1716            let strings = ds.read_vlen_strings().unwrap();
1717            assert_eq!(strings, vec!["new1", "new2", "new3"]);
1718        }
1719        std::fs::remove_file(&path).ok();
1720    }
1721
1722    #[test]
1723    #[cfg(feature = "deflate")]
1724    fn vlen_string_compressed_large_roundtrip() {
1725        use crate::format::messages::filter::FilterPipeline;
1726        let path = temp_path("vlen_large");
1727        // Simulate kodex scenario: 7189 strings, chunk_size 512
1728        let input: Vec<String> = (0..7189)
1729            .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
1730            .collect();
1731        let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
1732        {
1733            let file = H5File::create(&path).unwrap();
1734            file.create_group("nodes").unwrap();
1735            file.write_vlen_strings_compressed(
1736                "nodes/id",
1737                &input_refs,
1738                512,
1739                FilterPipeline::deflate(6),
1740            )
1741            .unwrap();
1742            file.close().unwrap();
1743        }
1744        // Read back
1745        {
1746            let file = H5File::open(&path).unwrap();
1747            let ds = file.dataset("nodes/id").unwrap();
1748            let strings = ds.read_vlen_strings().unwrap();
1749            assert_eq!(strings.len(), 7189);
1750            assert_eq!(strings[0], input[0]);
1751            assert_eq!(strings[7188], input[7188]);
1752        }
1753        // Also test open_rw then re-read
1754        {
1755            let file = H5File::open_rw(&path).unwrap();
1756            file.set_attr_string("version", "1.0").unwrap();
1757            file.close().unwrap();
1758        }
1759        {
1760            let file = H5File::open(&path).unwrap();
1761            let ds = file.dataset("nodes/id").unwrap();
1762            let strings = ds.read_vlen_strings().unwrap();
1763            assert_eq!(strings.len(), 7189);
1764            assert_eq!(strings[0], input[0]);
1765        }
1766        std::fs::remove_file(&path).ok();
1767    }
1768
1769    #[test]
1770    fn vlen_string_write_read() {
1771        let path = temp_path("vlen_wr");
1772        {
1773            let file = H5File::create(&path).unwrap();
1774            file.write_vlen_strings("names", &["alice", "bob", "charlie"])
1775                .unwrap();
1776            file.close().unwrap();
1777        }
1778        {
1779            let file = H5File::open(&path).unwrap();
1780            let ds = file.dataset("names").unwrap();
1781            let strings = ds.read_vlen_strings().unwrap();
1782            assert_eq!(strings, vec!["alice", "bob", "charlie"]);
1783        }
1784        std::fs::remove_file(&path).ok();
1785    }
1786
1787    #[test]
1788    fn vlen_bytes_write_read() {
1789        let path = temp_path("vlen_bytes_wr");
1790        let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
1791        {
1792            let file = H5File::create(&path).unwrap();
1793            file.write_vlen_bytes("blobs", &items).unwrap();
1794            file.close().unwrap();
1795        }
1796        {
1797            let file = H5File::open(&path).unwrap();
1798            let ds = file.dataset("blobs").unwrap();
1799            let got = ds.read_vlen_bytes().unwrap();
1800            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1801            assert_eq!(got, expected);
1802        }
1803        std::fs::remove_file(&path).ok();
1804    }
1805
1806    #[test]
1807    fn vlen_bytes_in_group_with_attribute() {
1808        use crate::types::VarLenUnicode;
1809        let path = temp_path("vlen_bytes_grp");
1810        let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
1811        {
1812            let file = H5File::create(&path).unwrap();
1813            let grp = file.root_group().create_group("payloads").unwrap();
1814            let ds = grp.write_vlen_bytes("frames", &items).unwrap();
1815            ds.new_attr::<VarLenUnicode>()
1816                .shape(())
1817                .create("codec")
1818                .unwrap()
1819                .write_string("raw")
1820                .unwrap();
1821            file.close().unwrap();
1822        }
1823        {
1824            let file = H5File::open(&path).unwrap();
1825            let ds = file.dataset("payloads/frames").unwrap();
1826            let got = ds.read_vlen_bytes().unwrap();
1827            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1828            assert_eq!(got, expected);
1829        }
1830        std::fs::remove_file(&path).ok();
1831    }
1832
1833    #[test]
1834    fn vlen_dataset_returns_handle_for_attributes() {
1835        use crate::types::VarLenUnicode;
1836        let path = temp_path("vlen_attr");
1837        {
1838            let file = H5File::create(&path).unwrap();
1839            let grp = file.root_group().create_group("ch").unwrap();
1840            // The vlen helper now returns the dataset handle, so attributes can
1841            // be attached directly — the issue the mdfr reporter hit.
1842            let ds = grp
1843                .write_vlen_strings("labels", &["a", "bb", "ccc"])
1844                .unwrap();
1845            ds.new_attr::<VarLenUnicode>()
1846                .shape(())
1847                .create("unit")
1848                .unwrap()
1849                .write_string("volt")
1850                .unwrap();
1851            // The same dataset can also be reopened by name within the group.
1852            let ds2 = grp.dataset_writer("labels").unwrap();
1853            ds2.new_attr::<VarLenUnicode>()
1854                .shape(())
1855                .create("desc")
1856                .unwrap()
1857                .write_string("channel labels")
1858                .unwrap();
1859            file.close().unwrap();
1860        }
1861        {
1862            let file = H5File::open(&path).unwrap();
1863            let ds = file.dataset("ch/labels").unwrap();
1864            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
1865            assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
1866            assert_eq!(
1867                ds.attr("desc").unwrap().read_string().unwrap(),
1868                "channel labels"
1869            );
1870        }
1871        std::fs::remove_file(&path).ok();
1872    }
1873
1874    #[test]
1875    #[cfg(feature = "deflate")]
1876    fn vlen_string_deflate_roundtrip() {
1877        use crate::format::messages::filter::FilterPipeline;
1878        let path = temp_path("vlen_deflate");
1879        let input: Vec<&str> = (0..100)
1880            .map(|i| match i % 3 {
1881                0 => "hello world",
1882                1 => "compressed vlen string test",
1883                _ => "rust-hdf5",
1884            })
1885            .collect();
1886        {
1887            let file = H5File::create(&path).unwrap();
1888            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1889                .unwrap();
1890            file.close().unwrap();
1891        }
1892        {
1893            let file = H5File::open(&path).unwrap();
1894            let ds = file.dataset("texts").unwrap();
1895            let strings = ds.read_vlen_strings().unwrap();
1896            assert_eq!(strings.len(), 100);
1897            for (i, s) in strings.iter().enumerate() {
1898                assert_eq!(s, input[i]);
1899            }
1900        }
1901        std::fs::remove_file(&path).ok();
1902    }
1903
1904    #[test]
1905    #[cfg(feature = "zstd")]
1906    fn vlen_string_zstd_roundtrip() {
1907        use crate::format::messages::filter::FilterPipeline;
1908        let path = temp_path("vlen_zstd");
1909        let input: Vec<&str> = (0..200)
1910            .map(|i| match i % 4 {
1911                0 => "zstandard compression test",
1912                1 => "variable length string",
1913                2 => "rust-hdf5 chunked storage",
1914                _ => "hello zstd world",
1915            })
1916            .collect();
1917        {
1918            let file = H5File::create(&path).unwrap();
1919            file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
1920                .unwrap();
1921            file.close().unwrap();
1922        }
1923        {
1924            let file = H5File::open(&path).unwrap();
1925            let ds = file.dataset("data").unwrap();
1926            let strings = ds.read_vlen_strings().unwrap();
1927            assert_eq!(strings.len(), 200);
1928            for (i, s) in strings.iter().enumerate() {
1929                assert_eq!(s, input[i]);
1930            }
1931        }
1932        std::fs::remove_file(&path).ok();
1933    }
1934
1935    #[test]
1936    #[cfg(feature = "deflate")]
1937    fn shuffle_deflate_roundtrip() {
1938        let path = temp_path("shuf_defl");
1939        {
1940            let file = H5File::create(&path).unwrap();
1941            let ds = file
1942                .new_dataset::<f64>()
1943                .shape([0usize, 4])
1944                .chunk(&[1, 4])
1945                .max_shape(&[None, Some(4)])
1946                .shuffle_deflate(6)
1947                .create("data")
1948                .unwrap();
1949            for frame in 0..20u64 {
1950                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1951                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1952                ds.write_chunk(frame as usize, &raw).unwrap();
1953            }
1954            ds.extend(&[20, 4]).unwrap();
1955            file.close().unwrap();
1956        }
1957        {
1958            let file = H5File::open(&path).unwrap();
1959            let ds = file.dataset("data").unwrap();
1960            assert_eq!(ds.shape(), vec![20, 4]);
1961            let data = ds.read_raw::<f64>().unwrap();
1962            assert_eq!(data.len(), 80);
1963            for (i, val) in data.iter().enumerate() {
1964                assert!((val - i as f64).abs() < 1e-10);
1965            }
1966        }
1967        std::fs::remove_file(&path).ok();
1968    }
1969
1970    #[test]
1971    fn file_level_attributes() {
1972        let path = temp_path("file_attr");
1973        {
1974            let file = H5File::create(&path).unwrap();
1975            file.set_attr_string("title", "Test File").unwrap();
1976            file.set_attr_numeric("version", &42i32).unwrap();
1977            let ds = file
1978                .new_dataset::<u8>()
1979                .shape([1usize])
1980                .create("dummy")
1981                .unwrap();
1982            ds.write_raw(&[0u8]).unwrap();
1983            file.close().unwrap();
1984        }
1985        {
1986            let file = H5File::open(&path).unwrap();
1987            assert!(file.dataset_names().contains(&"dummy".to_string()));
1988
1989            // Read file-level attributes
1990            let names = file.attr_names().unwrap();
1991            assert!(names.contains(&"title".to_string()));
1992
1993            let title = file.attr_string("title").unwrap();
1994            assert_eq!(title, "Test File");
1995        }
1996        std::fs::remove_file(&path).ok();
1997    }
1998
1999    #[test]
2000    fn scalar_dataset_roundtrip() {
2001        let path = temp_path("scalar");
2002        {
2003            let file = H5File::create(&path).unwrap();
2004            let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
2005            ds.write_raw(&[std::f64::consts::PI]).unwrap();
2006            file.close().unwrap();
2007        }
2008        {
2009            let file = H5File::open(&path).unwrap();
2010            let ds = file.dataset("pi").unwrap();
2011            assert_eq!(ds.shape(), Vec::<usize>::new());
2012            assert_eq!(ds.total_elements(), 1);
2013            let data = ds.read_raw::<f64>().unwrap();
2014            assert_eq!(data.len(), 1);
2015            assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
2016        }
2017        std::fs::remove_file(&path).ok();
2018    }
2019
2020    #[test]
2021    fn append_mode_extend_chunked() {
2022        let path = temp_path("append_extend");
2023
2024        // Create with 5 frames
2025        {
2026            let file = H5File::create(&path).unwrap();
2027            let ds = file
2028                .new_dataset::<i32>()
2029                .shape([0usize, 3])
2030                .chunk(&[1, 3])
2031                .max_shape(&[None, Some(3)])
2032                .create("stream")
2033                .unwrap();
2034            for i in 0..5u64 {
2035                let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
2036                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2037                ds.write_chunk(i as usize, &raw).unwrap();
2038            }
2039            ds.extend(&[5, 3]).unwrap();
2040            file.close().unwrap();
2041        }
2042
2043        // Reopen and add 5 more frames
2044        {
2045            let file = H5File::open_rw(&path).unwrap();
2046            // Find the stream dataset index (it's the first one)
2047            let names = file.dataset_names();
2048            assert!(names.contains(&"stream".to_string()));
2049
2050            // Write more chunks via the writer directly
2051            let mut inner = crate::file::borrow_inner_mut(&file.inner);
2052            if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
2053                let ds_idx = writer.dataset_index("stream").unwrap();
2054                for i in 5..10u64 {
2055                    let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
2056                    let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2057                    writer.write_chunk(ds_idx, i, &raw).unwrap();
2058                }
2059                writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
2060            }
2061            drop(inner);
2062            file.close().unwrap();
2063        }
2064
2065        // Read back all 10 frames
2066        {
2067            let file = H5File::open(&path).unwrap();
2068            let ds = file.dataset("stream").unwrap();
2069            assert_eq!(ds.shape(), vec![10, 3]);
2070            let data = ds.read_raw::<i32>().unwrap();
2071            assert_eq!(data.len(), 30);
2072            for (i, val) in data.iter().enumerate() {
2073                assert_eq!(*val, i as i32, "mismatch at {}", i);
2074            }
2075        }
2076
2077        std::fs::remove_file(&path).ok();
2078    }
2079
2080    #[test]
2081    fn group_hierarchy_roundtrip() {
2082        let path = temp_path("groups_rt");
2083
2084        {
2085            let file = H5File::create(&path).unwrap();
2086            let root = file.root_group();
2087
2088            // Create groups
2089            let det = root.create_group("detector").unwrap();
2090            let raw = det.create_group("raw").unwrap();
2091
2092            // Create datasets in groups
2093            let ds1 = det
2094                .new_dataset::<f32>()
2095                .shape([10usize])
2096                .create("temperature")
2097                .unwrap();
2098            ds1.write_raw(&[1.0f32; 10]).unwrap();
2099
2100            let ds2 = raw
2101                .new_dataset::<u16>()
2102                .shape([4usize, 4])
2103                .create("image")
2104                .unwrap();
2105            ds2.write_raw(&[42u16; 16]).unwrap();
2106
2107            // Root-level dataset
2108            let ds3 = file
2109                .new_dataset::<i32>()
2110                .shape([3usize])
2111                .create("version")
2112                .unwrap();
2113            ds3.write_raw(&[1i32, 0, 0]).unwrap();
2114
2115            file.close().unwrap();
2116        }
2117
2118        {
2119            let file = H5File::open(&path).unwrap();
2120            let names = file.dataset_names();
2121            assert!(names.contains(&"version".to_string()));
2122            assert!(names.contains(&"detector/temperature".to_string()));
2123            assert!(names.contains(&"detector/raw/image".to_string()));
2124
2125            // Read datasets
2126            let ds = file.dataset("version").unwrap();
2127            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
2128
2129            let ds = file.dataset("detector/temperature").unwrap();
2130            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
2131
2132            let ds = file.dataset("detector/raw/image").unwrap();
2133            assert_eq!(ds.shape(), vec![4, 4]);
2134            assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
2135
2136            // Group traversal
2137            let root = file.root_group();
2138            let group_names = root.group_names().unwrap();
2139            assert!(group_names.contains(&"detector".to_string()));
2140        }
2141
2142        std::fs::remove_file(&path).ok();
2143    }
2144
2145    #[test]
2146    fn nested_groups_via_file_create_group() {
2147        let path = temp_path("file_create_group");
2148
2149        {
2150            let file = H5File::create(&path).unwrap();
2151
2152            // Use the H5File::create_group convenience method
2153            let grp = file.create_group("sensors").unwrap();
2154            let sub = grp.create_group("accel").unwrap();
2155
2156            let ds = sub
2157                .new_dataset::<f64>()
2158                .shape([3usize])
2159                .create("xyz")
2160                .unwrap();
2161            ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
2162
2163            file.close().unwrap();
2164        }
2165
2166        {
2167            let file = H5File::open(&path).unwrap();
2168            let names = file.dataset_names();
2169            assert!(names.contains(&"sensors/accel/xyz".to_string()));
2170
2171            let ds = file.dataset("sensors/accel/xyz").unwrap();
2172            assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
2173
2174            // Open group in read mode
2175            let root = file.root_group();
2176            let sensors = root.group("sensors").unwrap();
2177            assert_eq!(sensors.name(), "/sensors");
2178
2179            let accel = sensors.group("accel").unwrap();
2180            assert_eq!(accel.name(), "/sensors/accel");
2181
2182            // list_groups from root
2183            let top_groups = root.group_names().unwrap();
2184            assert!(top_groups.contains(&"sensors".to_string()));
2185
2186            // list_groups from sensors
2187            let sub_groups = sensors.group_names().unwrap();
2188            assert!(sub_groups.contains(&"accel".to_string()));
2189        }
2190
2191        std::fs::remove_file(&path).ok();
2192    }
2193}
2194
2195#[cfg(test)]
2196mod h5py_compat_tests {
2197    use super::*;
2198
2199    // Only the deflate-gated h5dump test creates files; without that
2200    // feature this helper has no callers.
2201    #[cfg(feature = "deflate")]
2202    fn temp_path(name: &str) -> std::path::PathBuf {
2203        super::unique_test_path(name)
2204    }
2205
2206    /// Verify our files can be read by h5dump (if available).
2207    #[test]
2208    #[cfg(feature = "deflate")]
2209    fn h5dump_validates_our_files() {
2210        // Check if h5dump is available
2211        let h5dump = std::process::Command::new("h5dump")
2212            .arg("--version")
2213            .output();
2214        if h5dump.is_err() {
2215            eprintln!("skipping: h5dump not found");
2216            return;
2217        }
2218
2219        let path = temp_path("h5dump_validate");
2220
2221        // Write a comprehensive test file
2222        {
2223            let file = H5File::create(&path).unwrap();
2224
2225            // Contiguous
2226            let ds = file
2227                .new_dataset::<f64>()
2228                .shape([3usize, 4])
2229                .create("matrix")
2230                .unwrap();
2231            let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
2232            ds.write_raw(&data).unwrap();
2233
2234            // Chunked + compressed
2235            let ds2 = file
2236                .new_dataset::<i32>()
2237                .shape([0usize, 2])
2238                .chunk(&[1, 2])
2239                .max_shape(&[None, Some(2)])
2240                .deflate(6)
2241                .create("stream")
2242                .unwrap();
2243            for i in 0..5u64 {
2244                let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
2245                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2246                ds2.write_chunk(i as usize, &raw).unwrap();
2247            }
2248            ds2.extend(&[5, 2]).unwrap();
2249
2250            // Group
2251            let grp = file.create_group("meta").unwrap();
2252            let ds3 = grp
2253                .new_dataset::<u8>()
2254                .shape([4usize])
2255                .create("flags")
2256                .unwrap();
2257            ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
2258
2259            // String attribute
2260            use crate::types::VarLenUnicode;
2261            let attr = ds
2262                .new_attr::<VarLenUnicode>()
2263                .shape(())
2264                .create("units")
2265                .unwrap();
2266            attr.write_string("meters").unwrap();
2267
2268            file.close().unwrap();
2269        }
2270
2271        // Run h5dump and verify exit code
2272        let output = std::process::Command::new("h5dump")
2273            .arg("-H") // header only (faster)
2274            .arg(path.to_str().unwrap())
2275            .output()
2276            .unwrap();
2277
2278        assert!(
2279            output.status.success(),
2280            "h5dump failed:\nstdout: {}\nstderr: {}",
2281            String::from_utf8_lossy(&output.stdout),
2282            String::from_utf8_lossy(&output.stderr),
2283        );
2284
2285        // Full dump (with data) should also work
2286        let output2 = std::process::Command::new("h5dump")
2287            .arg(path.to_str().unwrap())
2288            .output()
2289            .unwrap();
2290
2291        assert!(
2292            output2.status.success(),
2293            "h5dump (full) failed:\nstderr: {}",
2294            String::from_utf8_lossy(&output2.stderr),
2295        );
2296
2297        std::fs::remove_file(&path).ok();
2298    }
2299
2300    #[test]
2301    fn read_h5py_generated_file() {
2302        let path = "/tmp/test_h5py_default.h5";
2303        if !std::path::Path::new(path).exists() {
2304            eprintln!("skipping: h5py test file not found");
2305            return;
2306        }
2307        let file = H5File::open(path).unwrap();
2308
2309        let ds = file.dataset("data").unwrap();
2310        assert_eq!(ds.shape(), vec![4, 5]);
2311        let data = ds.read_raw::<f64>().unwrap();
2312        assert_eq!(data.len(), 20);
2313        assert!((data[0]).abs() < 1e-10);
2314        assert!((data[19] - 19.0).abs() < 1e-10);
2315
2316        let ds2 = file.dataset("images").unwrap();
2317        assert_eq!(ds2.shape(), vec![3, 64, 64]);
2318        let images = ds2.read_raw::<u16>().unwrap();
2319        assert_eq!(images.len(), 3 * 64 * 64);
2320    }
2321}