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