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    /// Close the file without a final `fsync` (write mode only).
676    ///
677    /// Like [`close`](Self::close), this finalizes the file — object headers
678    /// and superblock are written, so on return it is a complete, valid HDF5
679    /// file readable by any process — but the trailing `sync_all` (fsync) is
680    /// skipped. The bytes are handed to the OS but are not guaranteed durable
681    /// against power loss or an OS crash until the OS flushes its page cache.
682    ///
683    /// This trades durability for speed (the fsync typically dominates close
684    /// latency); use it for bulk output that can be regenerated. Prefer
685    /// [`close`](Self::close) when durability matters. Dropping the file
686    /// without calling either finalizes durably.
687    ///
688    /// For a reader or an already-closed file this is a no-op, matching
689    /// [`close`](Self::close).
690    pub fn close_no_sync(self) -> Result<()> {
691        let old = {
692            let mut inner = borrow_inner_mut(&self.inner);
693            std::mem::replace(&mut *inner, H5FileInner::Closed)
694        };
695        match old {
696            H5FileInner::Writer(writer) => {
697                writer.close_no_sync()?;
698                Ok(())
699            }
700            H5FileInner::Reader(_) => Ok(()),
701            H5FileInner::Closed => Ok(()),
702        }
703    }
704
705    /// Flush the file to disk. Only meaningful in write mode.
706    pub fn flush(&self) -> Result<()> {
707        // The underlying writer does not expose a standalone flush; data is
708        // written to disk immediately via pwrite. This is a compatibility
709        // method that does nothing for now.
710        Ok(())
711    }
712}
713
714/// Builder controlling how an [`H5File`] is opened.
715///
716/// The default policy follows the HDF5 C library: an exclusive lock is
717/// acquired for write-mode opens and a shared lock for read-mode opens,
718/// honoring the `HDF5_USE_FILE_LOCKING` environment variable. Calling
719/// [`Self::locking`] overrides the env-var value.
720#[derive(Debug, Default, Clone)]
721pub struct H5FileOptions {
722    locking: Option<FileLocking>,
723}
724
725impl H5FileOptions {
726    /// Construct a fresh options builder with default settings.
727    pub fn new() -> Self {
728        Self::default()
729    }
730
731    /// Override the locking policy. Bypasses the `HDF5_USE_FILE_LOCKING`
732    /// environment variable for the resulting open call.
733    pub fn locking(mut self, policy: FileLocking) -> Self {
734        self.locking = Some(policy);
735        self
736    }
737
738    /// Disable OS-level file locking entirely (equivalent to
739    /// `HDF5_USE_FILE_LOCKING=FALSE`).
740    pub fn no_locking(self) -> Self {
741        self.locking(FileLocking::Disabled)
742    }
743
744    /// Try to acquire the lock but do not fail if the filesystem rejects it
745    /// (equivalent to `HDF5_USE_FILE_LOCKING=BEST_EFFORT`).
746    pub fn best_effort_locking(self) -> Self {
747        self.locking(FileLocking::BestEffort)
748    }
749
750    fn resolved_locking(&self) -> FileLocking {
751        match self.locking {
752            Some(p) => p,
753            None => FileLocking::from_env_or(FileLocking::default()),
754        }
755    }
756
757    /// Create a new HDF5 file at `path` with the configured options.
758    pub fn create<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
759        let writer = Hdf5Writer::create_with_locking(path.as_ref(), self.resolved_locking())?;
760        Ok(H5File {
761            inner: new_shared(H5FileInner::Writer(writer)),
762        })
763    }
764
765    /// Open an existing HDF5 file for reading with the configured options.
766    pub fn open<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
767        let reader = Hdf5Reader::open_with_locking(path.as_ref(), self.resolved_locking())?;
768        Ok(H5File {
769            inner: new_shared(H5FileInner::Reader(reader)),
770        })
771    }
772
773    /// Open an existing HDF5 file for read/write with the configured options.
774    pub fn open_rw<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
775        let writer = Hdf5Writer::open_append_with_locking(path.as_ref(), self.resolved_locking())?;
776        Ok(H5File {
777            inner: new_shared(H5FileInner::Writer(writer)),
778        })
779    }
780}
781
782#[cfg(test)]
783fn unique_test_path(name: &str) -> std::path::PathBuf {
784    // PID + atomic counter so each test invocation uses a distinct path,
785    // preventing collisions across concurrent cargo runs and any
786    // flock/LockFileEx race where a previous close()'d file's lock
787    // remains briefly visible when reopening the same path.
788    use std::sync::atomic::{AtomicU64, Ordering};
789    static COUNTER: AtomicU64 = AtomicU64::new(0);
790    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
791    std::env::temp_dir().join(format!(
792        "rust_hdf5_test_{}_{}_{}.h5",
793        name,
794        std::process::id(),
795        n
796    ))
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802    use std::path::PathBuf;
803
804    fn temp_path(name: &str) -> PathBuf {
805        super::unique_test_path(name)
806    }
807
808    #[test]
809    fn create_and_close_empty() {
810        let path = temp_path("create_empty");
811        let file = H5File::create(&path).unwrap();
812        file.close().unwrap();
813
814        // Should be readable
815        let file = H5File::open(&path).unwrap();
816        file.close().unwrap();
817
818        std::fs::remove_file(&path).ok();
819    }
820
821    #[test]
822    fn create_and_drop_empty() {
823        let path = temp_path("drop_empty");
824        {
825            let _file = H5File::create(&path).unwrap();
826            // drop auto-finalizes
827        }
828        // Verify the file is valid by opening it
829        let file = H5File::open(&path).unwrap();
830        file.close().unwrap();
831
832        std::fs::remove_file(&path).ok();
833    }
834
835    #[test]
836    fn dataset_not_found() {
837        let path = temp_path("ds_not_found");
838        {
839            let _file = H5File::create(&path).unwrap();
840        }
841        let file = H5File::open(&path).unwrap();
842        let result = file.dataset("nonexistent");
843        assert!(result.is_err());
844
845        std::fs::remove_file(&path).ok();
846    }
847
848    #[test]
849    fn write_and_read_roundtrip() {
850        let path = temp_path("write_read_rt");
851
852        // Write
853        {
854            let file = H5File::create(&path).unwrap();
855            let ds = file
856                .new_dataset::<u8>()
857                .shape([4, 4])
858                .create("data")
859                .unwrap();
860            ds.write_raw(&[0u8; 16]).unwrap();
861            file.close().unwrap();
862        }
863
864        // Read
865        {
866            let file = H5File::open(&path).unwrap();
867            let ds = file.dataset("data").unwrap();
868            assert_eq!(ds.shape(), vec![4, 4]);
869            let data = ds.read_raw::<u8>().unwrap();
870            assert_eq!(data.len(), 16);
871            assert!(data.iter().all(|&b| b == 0));
872            file.close().unwrap();
873        }
874
875        std::fs::remove_file(&path).ok();
876    }
877
878    #[test]
879    fn close_no_sync_produces_valid_readable_file() {
880        let path = temp_path("close_no_sync_rt");
881        let payload: Vec<u8> = (0u8..16).collect();
882
883        // Write and finalize WITHOUT the trailing fsync.
884        {
885            let file = H5File::create(&path).unwrap();
886            let ds = file
887                .new_dataset::<u8>()
888                .shape([4, 4])
889                .create("data")
890                .unwrap();
891            ds.write_raw(&payload).unwrap();
892            // The only difference from `write_and_read_roundtrip`: no fsync.
893            // The file must still be a complete, valid, readable HDF5 file.
894            file.close_no_sync().unwrap();
895        }
896
897        // Reopen and verify the full content survived (same-machine reader sees
898        // the OS page cache regardless of whether fsync ran).
899        {
900            let file = H5File::open(&path).unwrap();
901            let ds = file.dataset("data").unwrap();
902            assert_eq!(ds.shape(), vec![4, 4]);
903            let data = ds.read_raw::<u8>().unwrap();
904            assert_eq!(data, payload);
905            file.close().unwrap();
906        }
907
908        std::fs::remove_file(&path).ok();
909    }
910
911    #[test]
912    fn close_no_sync_chunked_dataset_valid() {
913        // Exercises flush_dataset_synced(sync=false): a chunked (EA-indexed)
914        // dataset closed with close_no_sync must skip the per-dataset
915        // sync_data yet still write valid index structures, so the reopened
916        // file reconstructs every frame.
917        let path = temp_path("close_no_sync_chunked");
918
919        {
920            let file = H5File::create(&path).unwrap();
921            let ds = file
922                .new_dataset::<i32>()
923                .shape([0usize, 3])
924                .chunk(&[1, 3])
925                .max_shape(&[None, Some(3)])
926                .create("data")
927                .unwrap();
928            // 10 frames exceeds idx_blk_elmts=4, so data blocks are exercised.
929            for frame in 0..10u64 {
930                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
931                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
932                ds.write_chunk(frame as usize, &raw).unwrap();
933            }
934            ds.extend(&[10, 3]).unwrap();
935            file.close_no_sync().unwrap();
936        }
937
938        {
939            let file = H5File::open(&path).unwrap();
940            let ds = file.dataset("data").unwrap();
941            assert_eq!(ds.shape(), vec![10, 3]);
942            let data = ds.read_raw::<i32>().unwrap();
943            let expected: Vec<i32> = (0..30).collect();
944            assert_eq!(data, expected);
945            file.close().unwrap();
946        }
947
948        std::fs::remove_file(&path).ok();
949    }
950
951    #[test]
952    fn write_and_read_f64() {
953        let path = temp_path("write_read_f64");
954
955        let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
956
957        // Write
958        {
959            let file = H5File::create(&path).unwrap();
960            let ds = file
961                .new_dataset::<f64>()
962                .shape([2, 3])
963                .create("matrix")
964                .unwrap();
965            ds.write_raw(&values).unwrap();
966            file.close().unwrap();
967        }
968
969        // Read
970        {
971            let file = H5File::open(&path).unwrap();
972            let ds = file.dataset("matrix").unwrap();
973            assert_eq!(ds.shape(), vec![2, 3]);
974            let readback = ds.read_raw::<f64>().unwrap();
975            assert_eq!(readback, values);
976        }
977
978        std::fs::remove_file(&path).ok();
979    }
980
981    #[test]
982    fn multiple_datasets() {
983        let path = temp_path("multi_ds");
984
985        {
986            let file = H5File::create(&path).unwrap();
987            let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
988            ds1.write_raw(&[10i32, 20, 30]).unwrap();
989
990            let ds2 = file
991                .new_dataset::<f32>()
992                .shape([2, 2])
993                .create("floats")
994                .unwrap();
995            ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
996
997            file.close().unwrap();
998        }
999
1000        {
1001            let file = H5File::open(&path).unwrap();
1002
1003            let ds_ints = file.dataset("ints").unwrap();
1004            assert_eq!(ds_ints.shape(), vec![3]);
1005            let ints = ds_ints.read_raw::<i32>().unwrap();
1006            assert_eq!(ints, vec![10, 20, 30]);
1007
1008            let ds_floats = file.dataset("floats").unwrap();
1009            assert_eq!(ds_floats.shape(), vec![2, 2]);
1010            let floats = ds_floats.read_raw::<f32>().unwrap();
1011            assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
1012        }
1013
1014        std::fs::remove_file(&path).ok();
1015    }
1016
1017    #[test]
1018    fn close_is_idempotent() {
1019        let path = temp_path("close_idemp");
1020        let file = H5File::create(&path).unwrap();
1021        file.close().unwrap();
1022        // File is consumed by close(), so no double-close possible at the type level.
1023        std::fs::remove_file(&path).ok();
1024    }
1025}
1026
1027#[cfg(test)]
1028mod integration_tests {
1029    use super::*;
1030
1031    fn temp_path(name: &str) -> std::path::PathBuf {
1032        super::unique_test_path(name)
1033    }
1034
1035    #[test]
1036    fn write_file_for_h5dump() {
1037        let path = temp_path("integration");
1038        let file = H5File::create(&path).unwrap();
1039
1040        let ds = file
1041            .new_dataset::<u8>()
1042            .shape([4usize, 4])
1043            .create("data_u8")
1044            .unwrap();
1045        let data: Vec<u8> = (0..16).collect();
1046        ds.write_raw(&data).unwrap();
1047
1048        let ds2 = file
1049            .new_dataset::<f64>()
1050            .shape([3usize, 2])
1051            .create("data_f64")
1052            .unwrap();
1053        let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1054        ds2.write_raw(&fdata).unwrap();
1055
1056        let ds3 = file
1057            .new_dataset::<i32>()
1058            .shape([5usize])
1059            .create("values")
1060            .unwrap();
1061        let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
1062        ds3.write_raw(&idata).unwrap();
1063
1064        file.close().unwrap();
1065
1066        // File exists
1067        assert!(path.exists());
1068    }
1069
1070    #[test]
1071    fn write_chunked_file_for_h5dump() {
1072        let path = temp_path("chunked");
1073        let file = H5File::create(&path).unwrap();
1074
1075        // Create a chunked dataset with unlimited first dimension
1076        let ds = file
1077            .new_dataset::<f64>()
1078            .shape([0usize, 4])
1079            .chunk(&[1, 4])
1080            .max_shape(&[None, Some(4)])
1081            .create("streaming_data")
1082            .unwrap();
1083
1084        // Write 5 frames of data
1085        for frame in 0..5u64 {
1086            let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1087            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1088            ds.write_chunk(frame as usize, &raw).unwrap();
1089        }
1090
1091        // Extend dimensions to reflect the 5 written frames
1092        ds.extend(&[5, 4]).unwrap();
1093        ds.flush().unwrap();
1094
1095        file.close().unwrap();
1096
1097        assert!(path.exists());
1098    }
1099
1100    #[test]
1101    fn write_chunked_many_frames_for_h5dump() {
1102        let path = temp_path("chunked_many");
1103        let file = H5File::create(&path).unwrap();
1104
1105        let ds = file
1106            .new_dataset::<i32>()
1107            .shape([0usize, 3])
1108            .chunk(&[1, 3])
1109            .max_shape(&[None, Some(3)])
1110            .create("data")
1111            .unwrap();
1112
1113        // Write 10 frames (exceeds idx_blk_elmts=4, uses data blocks)
1114        for frame in 0..10u64 {
1115            let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1116            let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1117            ds.write_chunk(frame as usize, &raw).unwrap();
1118        }
1119        ds.extend(&[10, 3]).unwrap();
1120        file.close().unwrap();
1121
1122        assert!(path.exists());
1123    }
1124
1125    #[test]
1126    fn write_dataset_with_attributes() {
1127        use crate::types::VarLenUnicode;
1128
1129        let path = temp_path("attributes");
1130        let file = H5File::create(&path).unwrap();
1131
1132        let ds = file
1133            .new_dataset::<f32>()
1134            .shape([10usize])
1135            .create("temperature")
1136            .unwrap();
1137        let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
1138        ds.write_raw(&data).unwrap();
1139
1140        // Add string attributes
1141        let attr = ds
1142            .new_attr::<VarLenUnicode>()
1143            .shape(())
1144            .create("units")
1145            .unwrap();
1146        attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
1147            .unwrap();
1148
1149        let attr2 = ds
1150            .new_attr::<VarLenUnicode>()
1151            .shape(())
1152            .create("description")
1153            .unwrap();
1154        attr2
1155            .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
1156            .unwrap();
1157
1158        // Use write_string convenience method
1159        let attr3 = ds
1160            .new_attr::<VarLenUnicode>()
1161            .shape(())
1162            .create("source")
1163            .unwrap();
1164        attr3.write_string("sensor_01").unwrap();
1165
1166        // Also test parse -> write_scalar pattern
1167        let attr4 = ds
1168            .new_attr::<VarLenUnicode>()
1169            .shape(())
1170            .create("label")
1171            .unwrap();
1172        let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
1173        attr4.write_scalar(&s).unwrap();
1174
1175        file.close().unwrap();
1176
1177        assert!(path.exists());
1178    }
1179
1180    #[test]
1181    fn dataset_writer_reopens_for_attributes() {
1182        // Reopen a dataset by name in write mode (the original handle is gone)
1183        // and attach attributes to it — the close-time flush pattern.
1184        let path = temp_path("dataset_writer");
1185        {
1186            let file = H5File::create(&path).unwrap();
1187            {
1188                let ds = file
1189                    .new_dataset::<u16>()
1190                    .shape([8])
1191                    .create("image")
1192                    .unwrap();
1193                ds.write_raw(&[0u16; 8]).unwrap();
1194                // original handle dropped here
1195            }
1196
1197            // Reopen by name; dataset() would error in write mode.
1198            assert!(file.dataset("image").is_err());
1199            let ds = file.dataset_writer("image").unwrap();
1200            assert_eq!(ds.shape(), vec![8]);
1201            ds.new_attr::<i32>()
1202                .shape([3])
1203                .create("NDArrayDimOffset")
1204                .unwrap()
1205                .write_array(&[0i32, 4, 8])
1206                .unwrap();
1207            ds.new_attr::<i32>()
1208                .shape(())
1209                .create("NDUniqueId")
1210                .unwrap()
1211                .write_numeric(&42i32)
1212                .unwrap();
1213
1214            // Missing dataset is reported.
1215            assert!(matches!(
1216                file.dataset_writer("nope"),
1217                Err(crate::error::Hdf5Error::NotFound(_))
1218            ));
1219
1220            file.close().unwrap();
1221        }
1222        {
1223            let file = H5File::open(&path).unwrap();
1224            let ds = file.dataset("image").unwrap();
1225            let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
1226            let got: Vec<i32> = off
1227                .chunks_exact(4)
1228                .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1229                .collect();
1230            assert_eq!(got, vec![0, 4, 8]);
1231            let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
1232            assert_eq!(uid, 42);
1233        }
1234        std::fs::remove_file(&path).ok();
1235    }
1236
1237    #[test]
1238    fn chunked_write_read_roundtrip() {
1239        let path = temp_path("chunked_roundtrip");
1240
1241        // Write
1242        {
1243            let file = H5File::create(&path).unwrap();
1244            let ds = file
1245                .new_dataset::<i32>()
1246                .shape([0usize, 3])
1247                .chunk(&[1, 3])
1248                .max_shape(&[None, Some(3)])
1249                .create("table")
1250                .unwrap();
1251
1252            for frame in 0..8u64 {
1253                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1254                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1255                ds.write_chunk(frame as usize, &raw).unwrap();
1256            }
1257            ds.extend(&[8, 3]).unwrap();
1258            file.close().unwrap();
1259        }
1260
1261        // Read
1262        {
1263            let file = H5File::open(&path).unwrap();
1264            let ds = file.dataset("table").unwrap();
1265            assert_eq!(ds.shape(), vec![8, 3]);
1266            let data = ds.read_raw::<i32>().unwrap();
1267            assert_eq!(data.len(), 24);
1268            for (i, val) in data.iter().enumerate() {
1269                assert_eq!(*val, i as i32);
1270            }
1271        }
1272
1273        std::fs::remove_file(&path).ok();
1274    }
1275
1276    #[test]
1277    #[cfg(feature = "deflate")]
1278    fn compressed_chunked_roundtrip() {
1279        let path = temp_path("compressed_roundtrip");
1280
1281        // Write compressed
1282        {
1283            let file = H5File::create(&path).unwrap();
1284            let ds = file
1285                .new_dataset::<f64>()
1286                .shape([0usize, 4])
1287                .chunk(&[1, 4])
1288                .max_shape(&[None, Some(4)])
1289                .deflate(6)
1290                .create("compressed")
1291                .unwrap();
1292
1293            for frame in 0..10u64 {
1294                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1295                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1296                ds.write_chunk(frame as usize, &raw).unwrap();
1297            }
1298            ds.extend(&[10, 4]).unwrap();
1299            file.close().unwrap();
1300        }
1301
1302        // Read back and verify
1303        {
1304            let file = H5File::open(&path).unwrap();
1305            let ds = file.dataset("compressed").unwrap();
1306            assert_eq!(ds.shape(), vec![10, 4]);
1307            let data = ds.read_raw::<f64>().unwrap();
1308            assert_eq!(data.len(), 40);
1309            for (i, val) in data.iter().enumerate() {
1310                assert!(
1311                    (val - i as f64).abs() < 1e-10,
1312                    "mismatch at {}: {} != {}",
1313                    i,
1314                    val,
1315                    i
1316                );
1317            }
1318        }
1319
1320        std::fs::remove_file(&path).ok();
1321    }
1322
1323    #[test]
1324    #[cfg(feature = "deflate")]
1325    fn compressed_chunked_many_frames() {
1326        let path = temp_path("compressed_many");
1327
1328        {
1329            let file = H5File::create(&path).unwrap();
1330            let ds = file
1331                .new_dataset::<i32>()
1332                .shape([0usize, 3])
1333                .chunk(&[1, 3])
1334                .max_shape(&[None, Some(3)])
1335                .deflate(6)
1336                .create("stream")
1337                .unwrap();
1338
1339            for frame in 0..100u64 {
1340                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1341                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1342                ds.write_chunk(frame as usize, &raw).unwrap();
1343            }
1344            ds.extend(&[100, 3]).unwrap();
1345            file.close().unwrap();
1346        }
1347
1348        {
1349            let file = H5File::open(&path).unwrap();
1350            let ds = file.dataset("stream").unwrap();
1351            assert_eq!(ds.shape(), vec![100, 3]);
1352            let data = ds.read_raw::<i32>().unwrap();
1353            assert_eq!(data.len(), 300);
1354            for (i, val) in data.iter().enumerate() {
1355                assert_eq!(*val, i as i32, "mismatch at {}", i);
1356            }
1357        }
1358
1359        std::fs::remove_file(&path).ok();
1360    }
1361    #[test]
1362    fn append_mode() {
1363        let path = temp_path("append");
1364
1365        // Create initial file
1366        {
1367            let file = H5File::create(&path).unwrap();
1368            let ds = file
1369                .new_dataset::<i32>()
1370                .shape([3usize])
1371                .create("first")
1372                .unwrap();
1373            ds.write_raw(&[1i32, 2, 3]).unwrap();
1374            file.close().unwrap();
1375        }
1376
1377        // Append new dataset
1378        {
1379            let file = H5File::open_rw(&path).unwrap();
1380            let ds = file
1381                .new_dataset::<f64>()
1382                .shape([2usize])
1383                .create("second")
1384                .unwrap();
1385            ds.write_raw(&[4.0f64, 5.0]).unwrap();
1386            file.close().unwrap();
1387        }
1388
1389        // Read back both
1390        {
1391            let file = H5File::open(&path).unwrap();
1392            let names = file.dataset_names();
1393            assert!(names.contains(&"first".to_string()));
1394            assert!(names.contains(&"second".to_string()));
1395
1396            let ds1 = file.dataset("first").unwrap();
1397            assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
1398
1399            let ds2 = file.dataset("second").unwrap();
1400            assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
1401        }
1402
1403        std::fs::remove_file(&path).ok();
1404    }
1405
1406    #[test]
1407    fn open_rw_set_attr_preserves_file() {
1408        let path = temp_path("open_rw_attr");
1409        // Create file with a dataset and an attribute
1410        {
1411            let file = H5File::create(&path).unwrap();
1412            let ds = file
1413                .new_dataset::<i32>()
1414                .shape([3usize])
1415                .create("data")
1416                .unwrap();
1417            ds.write_raw(&[10i32, 20, 30]).unwrap();
1418            file.set_attr_string("version", "1.0").unwrap();
1419            file.close().unwrap();
1420        }
1421        // Open rw and modify the attribute
1422        {
1423            let file = H5File::open_rw(&path).unwrap();
1424            file.set_attr_string("version", "2.0").unwrap();
1425            file.close().unwrap();
1426        }
1427        // Verify: dataset intact, attribute updated
1428        {
1429            let file = H5File::open(&path).unwrap();
1430            let ds = file.dataset("data").unwrap();
1431            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
1432            let ver = file.attr_string("version").unwrap();
1433            assert_eq!(ver, "2.0");
1434        }
1435        std::fs::remove_file(&path).ok();
1436    }
1437
1438    #[test]
1439    #[cfg(feature = "deflate")]
1440    fn open_rw_attr_with_compressed_dataset() {
1441        use crate::format::messages::filter::FilterPipeline;
1442        let path = temp_path("open_rw_compressed");
1443        let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
1444        // Create file with compressed vlen strings
1445        {
1446            let file = H5File::create(&path).unwrap();
1447            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1448                .unwrap();
1449            file.set_attr_string("version", "1.0").unwrap();
1450            file.close().unwrap();
1451        }
1452        // Open rw and modify attribute only
1453        {
1454            let file = H5File::open_rw(&path).unwrap();
1455            file.set_attr_string("version", "2.0").unwrap();
1456            file.close().unwrap();
1457        }
1458        // Verify: compressed dataset still readable, attribute updated
1459        {
1460            let file = H5File::open(&path).unwrap();
1461            let ds = file.dataset("texts").unwrap();
1462            let strings = ds.read_vlen_strings().unwrap();
1463            assert_eq!(strings.len(), 50);
1464            assert_eq!(strings[0], "test string data");
1465            let ver = file.attr_string("version").unwrap();
1466            assert_eq!(ver, "2.0");
1467        }
1468        std::fs::remove_file(&path).ok();
1469    }
1470
1471    #[test]
1472    #[cfg(feature = "lz4")]
1473    fn append_vlen_strings_basic() {
1474        use crate::format::messages::filter::FilterPipeline;
1475        let path = temp_path("append_vlen");
1476        {
1477            let file = H5File::create(&path).unwrap();
1478            file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
1479                .unwrap();
1480            file.append_vlen_strings("names", &["alice", "bob", "charlie"])
1481                .unwrap();
1482            file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
1483            file.close().unwrap();
1484        }
1485        {
1486            let file = H5File::open(&path).unwrap();
1487            let ds = file.dataset("names").unwrap();
1488            let strings = ds.read_vlen_strings().unwrap();
1489            assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
1490        }
1491        std::fs::remove_file(&path).ok();
1492    }
1493
1494    #[test]
1495    #[cfg(feature = "lz4")]
1496    fn append_vlen_strings_large() {
1497        use crate::format::messages::filter::FilterPipeline;
1498        let path = temp_path("append_vlen_large");
1499        let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
1500        let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
1501        {
1502            let file = H5File::create(&path).unwrap();
1503            file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
1504                .unwrap();
1505            let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
1506            file.append_vlen_strings("data", &r1).unwrap();
1507            let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
1508            file.append_vlen_strings("data", &r2).unwrap();
1509            file.close().unwrap();
1510        }
1511        {
1512            let file = H5File::open(&path).unwrap();
1513            let ds = file.dataset("data").unwrap();
1514            let strings = ds.read_vlen_strings().unwrap();
1515            assert_eq!(strings.len(), 7189);
1516            assert_eq!(strings[0], "node-000000");
1517            assert_eq!(strings[7188], "node-007188");
1518        }
1519        std::fs::remove_file(&path).ok();
1520    }
1521
1522    #[test]
1523    fn append_vlen_strings_uncompressed() {
1524        let path = temp_path("append_vlen_unc");
1525        {
1526            let file = H5File::create(&path).unwrap();
1527            file.create_appendable_vlen_dataset("texts", 8, None)
1528                .unwrap();
1529            file.append_vlen_strings("texts", &["hello", "world"])
1530                .unwrap();
1531            file.append_vlen_strings("texts", &["foo", "bar", "baz"])
1532                .unwrap();
1533            file.close().unwrap();
1534        }
1535        {
1536            let file = H5File::open(&path).unwrap();
1537            let ds = file.dataset("texts").unwrap();
1538            let strings = ds.read_vlen_strings().unwrap();
1539            assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
1540        }
1541        std::fs::remove_file(&path).ok();
1542    }
1543
1544    #[test]
1545    fn delete_dataset_roundtrip() {
1546        let path = temp_path("delete_ds");
1547        {
1548            let file = H5File::create(&path).unwrap();
1549            file.write_vlen_strings("keep", &["a", "b"]).unwrap();
1550            file.write_vlen_strings("remove", &["x", "y"]).unwrap();
1551            file.delete_dataset("remove").unwrap();
1552            file.close().unwrap();
1553        }
1554        {
1555            let file = H5File::open(&path).unwrap();
1556            let names = file.dataset_names();
1557            assert!(names.contains(&"keep".to_string()));
1558            assert!(!names.contains(&"remove".to_string()));
1559            let ds = file.dataset("keep").unwrap();
1560            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
1561        }
1562        std::fs::remove_file(&path).ok();
1563    }
1564
1565    #[test]
1566    fn delete_group_roundtrip() {
1567        let path = temp_path("delete_grp");
1568        {
1569            let file = H5File::create(&path).unwrap();
1570            let g1 = file.create_group("keep").unwrap();
1571            g1.write_vlen_strings("data", &["a"]).unwrap();
1572            let g2 = file.create_group("remove").unwrap();
1573            g2.write_vlen_strings("data", &["x"]).unwrap();
1574            file.delete_group("remove").unwrap();
1575            file.close().unwrap();
1576        }
1577        {
1578            let file = H5File::open(&path).unwrap();
1579            let names = file.dataset_names();
1580            assert!(names.contains(&"keep/data".to_string()));
1581            assert!(!names.contains(&"remove/data".to_string()));
1582        }
1583        std::fs::remove_file(&path).ok();
1584    }
1585
1586    #[test]
1587    fn open_rw_delete_recreate_group() {
1588        let path = temp_path("rw_delete_recreate");
1589        // Step 1: create file with groups
1590        {
1591            let file = H5File::create(&path).unwrap();
1592            let n = file.create_group("nodes").unwrap();
1593            n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
1594            let e = file.create_group("edges").unwrap();
1595            e.write_vlen_strings("src", &["x", "y"]).unwrap();
1596            file.close().unwrap();
1597        }
1598        // Step 2: open_rw, delete one group, recreate with new data
1599        {
1600            let file = H5File::open_rw(&path).unwrap();
1601            file.delete_group("nodes").unwrap();
1602            let n = file.create_group("nodes").unwrap();
1603            n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
1604            file.close().unwrap();
1605        }
1606        // Step 3: verify
1607        {
1608            let file = H5File::open(&path).unwrap();
1609            let ds = file.dataset("nodes/id").unwrap();
1610            let s = ds.read_vlen_strings().unwrap();
1611            assert_eq!(s, vec!["new1", "new2"]);
1612            // edges should still be intact
1613            let ds = file.dataset("edges/src").unwrap();
1614            let s = ds.read_vlen_strings().unwrap();
1615            assert_eq!(s, vec!["x", "y"]);
1616        }
1617        std::fs::remove_file(&path).ok();
1618    }
1619
1620    #[test]
1621    fn delete_and_recreate_group() {
1622        let path = temp_path("delete_recreate");
1623        {
1624            let file = H5File::create(&path).unwrap();
1625            let g = file.create_group("nodes").unwrap();
1626            g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
1627            file.delete_group("nodes").unwrap();
1628            let g = file.create_group("nodes").unwrap();
1629            g.write_vlen_strings("id", &["new1", "new2", "new3"])
1630                .unwrap();
1631            file.close().unwrap();
1632        }
1633        {
1634            let file = H5File::open(&path).unwrap();
1635            let ds = file.dataset("nodes/id").unwrap();
1636            let strings = ds.read_vlen_strings().unwrap();
1637            assert_eq!(strings, vec!["new1", "new2", "new3"]);
1638        }
1639        std::fs::remove_file(&path).ok();
1640    }
1641
1642    #[test]
1643    #[cfg(feature = "deflate")]
1644    fn vlen_string_compressed_large_roundtrip() {
1645        use crate::format::messages::filter::FilterPipeline;
1646        let path = temp_path("vlen_large");
1647        // Simulate kodex scenario: 7189 strings, chunk_size 512
1648        let input: Vec<String> = (0..7189)
1649            .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
1650            .collect();
1651        let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
1652        {
1653            let file = H5File::create(&path).unwrap();
1654            file.create_group("nodes").unwrap();
1655            file.write_vlen_strings_compressed(
1656                "nodes/id",
1657                &input_refs,
1658                512,
1659                FilterPipeline::deflate(6),
1660            )
1661            .unwrap();
1662            file.close().unwrap();
1663        }
1664        // Read back
1665        {
1666            let file = H5File::open(&path).unwrap();
1667            let ds = file.dataset("nodes/id").unwrap();
1668            let strings = ds.read_vlen_strings().unwrap();
1669            assert_eq!(strings.len(), 7189);
1670            assert_eq!(strings[0], input[0]);
1671            assert_eq!(strings[7188], input[7188]);
1672        }
1673        // Also test open_rw then re-read
1674        {
1675            let file = H5File::open_rw(&path).unwrap();
1676            file.set_attr_string("version", "1.0").unwrap();
1677            file.close().unwrap();
1678        }
1679        {
1680            let file = H5File::open(&path).unwrap();
1681            let ds = file.dataset("nodes/id").unwrap();
1682            let strings = ds.read_vlen_strings().unwrap();
1683            assert_eq!(strings.len(), 7189);
1684            assert_eq!(strings[0], input[0]);
1685        }
1686        std::fs::remove_file(&path).ok();
1687    }
1688
1689    #[test]
1690    fn vlen_string_write_read() {
1691        let path = temp_path("vlen_wr");
1692        {
1693            let file = H5File::create(&path).unwrap();
1694            file.write_vlen_strings("names", &["alice", "bob", "charlie"])
1695                .unwrap();
1696            file.close().unwrap();
1697        }
1698        {
1699            let file = H5File::open(&path).unwrap();
1700            let ds = file.dataset("names").unwrap();
1701            let strings = ds.read_vlen_strings().unwrap();
1702            assert_eq!(strings, vec!["alice", "bob", "charlie"]);
1703        }
1704        std::fs::remove_file(&path).ok();
1705    }
1706
1707    #[test]
1708    fn vlen_bytes_write_read() {
1709        let path = temp_path("vlen_bytes_wr");
1710        let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
1711        {
1712            let file = H5File::create(&path).unwrap();
1713            file.write_vlen_bytes("blobs", &items).unwrap();
1714            file.close().unwrap();
1715        }
1716        {
1717            let file = H5File::open(&path).unwrap();
1718            let ds = file.dataset("blobs").unwrap();
1719            let got = ds.read_vlen_bytes().unwrap();
1720            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1721            assert_eq!(got, expected);
1722        }
1723        std::fs::remove_file(&path).ok();
1724    }
1725
1726    #[test]
1727    fn vlen_bytes_in_group_with_attribute() {
1728        use crate::types::VarLenUnicode;
1729        let path = temp_path("vlen_bytes_grp");
1730        let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
1731        {
1732            let file = H5File::create(&path).unwrap();
1733            let grp = file.root_group().create_group("payloads").unwrap();
1734            let ds = grp.write_vlen_bytes("frames", &items).unwrap();
1735            ds.new_attr::<VarLenUnicode>()
1736                .shape(())
1737                .create("codec")
1738                .unwrap()
1739                .write_string("raw")
1740                .unwrap();
1741            file.close().unwrap();
1742        }
1743        {
1744            let file = H5File::open(&path).unwrap();
1745            let ds = file.dataset("payloads/frames").unwrap();
1746            let got = ds.read_vlen_bytes().unwrap();
1747            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1748            assert_eq!(got, expected);
1749        }
1750        std::fs::remove_file(&path).ok();
1751    }
1752
1753    #[test]
1754    fn vlen_dataset_returns_handle_for_attributes() {
1755        use crate::types::VarLenUnicode;
1756        let path = temp_path("vlen_attr");
1757        {
1758            let file = H5File::create(&path).unwrap();
1759            let grp = file.root_group().create_group("ch").unwrap();
1760            // The vlen helper now returns the dataset handle, so attributes can
1761            // be attached directly — the issue the mdfr reporter hit.
1762            let ds = grp
1763                .write_vlen_strings("labels", &["a", "bb", "ccc"])
1764                .unwrap();
1765            ds.new_attr::<VarLenUnicode>()
1766                .shape(())
1767                .create("unit")
1768                .unwrap()
1769                .write_string("volt")
1770                .unwrap();
1771            // The same dataset can also be reopened by name within the group.
1772            let ds2 = grp.dataset_writer("labels").unwrap();
1773            ds2.new_attr::<VarLenUnicode>()
1774                .shape(())
1775                .create("desc")
1776                .unwrap()
1777                .write_string("channel labels")
1778                .unwrap();
1779            file.close().unwrap();
1780        }
1781        {
1782            let file = H5File::open(&path).unwrap();
1783            let ds = file.dataset("ch/labels").unwrap();
1784            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
1785            assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
1786            assert_eq!(
1787                ds.attr("desc").unwrap().read_string().unwrap(),
1788                "channel labels"
1789            );
1790        }
1791        std::fs::remove_file(&path).ok();
1792    }
1793
1794    #[test]
1795    #[cfg(feature = "deflate")]
1796    fn vlen_string_deflate_roundtrip() {
1797        use crate::format::messages::filter::FilterPipeline;
1798        let path = temp_path("vlen_deflate");
1799        let input: Vec<&str> = (0..100)
1800            .map(|i| match i % 3 {
1801                0 => "hello world",
1802                1 => "compressed vlen string test",
1803                _ => "rust-hdf5",
1804            })
1805            .collect();
1806        {
1807            let file = H5File::create(&path).unwrap();
1808            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1809                .unwrap();
1810            file.close().unwrap();
1811        }
1812        {
1813            let file = H5File::open(&path).unwrap();
1814            let ds = file.dataset("texts").unwrap();
1815            let strings = ds.read_vlen_strings().unwrap();
1816            assert_eq!(strings.len(), 100);
1817            for (i, s) in strings.iter().enumerate() {
1818                assert_eq!(s, input[i]);
1819            }
1820        }
1821        std::fs::remove_file(&path).ok();
1822    }
1823
1824    #[test]
1825    #[cfg(feature = "zstd")]
1826    fn vlen_string_zstd_roundtrip() {
1827        use crate::format::messages::filter::FilterPipeline;
1828        let path = temp_path("vlen_zstd");
1829        let input: Vec<&str> = (0..200)
1830            .map(|i| match i % 4 {
1831                0 => "zstandard compression test",
1832                1 => "variable length string",
1833                2 => "rust-hdf5 chunked storage",
1834                _ => "hello zstd world",
1835            })
1836            .collect();
1837        {
1838            let file = H5File::create(&path).unwrap();
1839            file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
1840                .unwrap();
1841            file.close().unwrap();
1842        }
1843        {
1844            let file = H5File::open(&path).unwrap();
1845            let ds = file.dataset("data").unwrap();
1846            let strings = ds.read_vlen_strings().unwrap();
1847            assert_eq!(strings.len(), 200);
1848            for (i, s) in strings.iter().enumerate() {
1849                assert_eq!(s, input[i]);
1850            }
1851        }
1852        std::fs::remove_file(&path).ok();
1853    }
1854
1855    #[test]
1856    #[cfg(feature = "deflate")]
1857    fn shuffle_deflate_roundtrip() {
1858        let path = temp_path("shuf_defl");
1859        {
1860            let file = H5File::create(&path).unwrap();
1861            let ds = file
1862                .new_dataset::<f64>()
1863                .shape([0usize, 4])
1864                .chunk(&[1, 4])
1865                .max_shape(&[None, Some(4)])
1866                .shuffle_deflate(6)
1867                .create("data")
1868                .unwrap();
1869            for frame in 0..20u64 {
1870                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1871                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1872                ds.write_chunk(frame as usize, &raw).unwrap();
1873            }
1874            ds.extend(&[20, 4]).unwrap();
1875            file.close().unwrap();
1876        }
1877        {
1878            let file = H5File::open(&path).unwrap();
1879            let ds = file.dataset("data").unwrap();
1880            assert_eq!(ds.shape(), vec![20, 4]);
1881            let data = ds.read_raw::<f64>().unwrap();
1882            assert_eq!(data.len(), 80);
1883            for (i, val) in data.iter().enumerate() {
1884                assert!((val - i as f64).abs() < 1e-10);
1885            }
1886        }
1887        std::fs::remove_file(&path).ok();
1888    }
1889
1890    #[test]
1891    fn file_level_attributes() {
1892        let path = temp_path("file_attr");
1893        {
1894            let file = H5File::create(&path).unwrap();
1895            file.set_attr_string("title", "Test File").unwrap();
1896            file.set_attr_numeric("version", &42i32).unwrap();
1897            let ds = file
1898                .new_dataset::<u8>()
1899                .shape([1usize])
1900                .create("dummy")
1901                .unwrap();
1902            ds.write_raw(&[0u8]).unwrap();
1903            file.close().unwrap();
1904        }
1905        {
1906            let file = H5File::open(&path).unwrap();
1907            assert!(file.dataset_names().contains(&"dummy".to_string()));
1908
1909            // Read file-level attributes
1910            let names = file.attr_names().unwrap();
1911            assert!(names.contains(&"title".to_string()));
1912
1913            let title = file.attr_string("title").unwrap();
1914            assert_eq!(title, "Test File");
1915        }
1916        std::fs::remove_file(&path).ok();
1917    }
1918
1919    #[test]
1920    fn scalar_dataset_roundtrip() {
1921        let path = temp_path("scalar");
1922        {
1923            let file = H5File::create(&path).unwrap();
1924            let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
1925            ds.write_raw(&[std::f64::consts::PI]).unwrap();
1926            file.close().unwrap();
1927        }
1928        {
1929            let file = H5File::open(&path).unwrap();
1930            let ds = file.dataset("pi").unwrap();
1931            assert_eq!(ds.shape(), Vec::<usize>::new());
1932            assert_eq!(ds.total_elements(), 1);
1933            let data = ds.read_raw::<f64>().unwrap();
1934            assert_eq!(data.len(), 1);
1935            assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
1936        }
1937        std::fs::remove_file(&path).ok();
1938    }
1939
1940    #[test]
1941    fn append_mode_extend_chunked() {
1942        let path = temp_path("append_extend");
1943
1944        // Create with 5 frames
1945        {
1946            let file = H5File::create(&path).unwrap();
1947            let ds = file
1948                .new_dataset::<i32>()
1949                .shape([0usize, 3])
1950                .chunk(&[1, 3])
1951                .max_shape(&[None, Some(3)])
1952                .create("stream")
1953                .unwrap();
1954            for i in 0..5u64 {
1955                let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1956                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1957                ds.write_chunk(i as usize, &raw).unwrap();
1958            }
1959            ds.extend(&[5, 3]).unwrap();
1960            file.close().unwrap();
1961        }
1962
1963        // Reopen and add 5 more frames
1964        {
1965            let file = H5File::open_rw(&path).unwrap();
1966            // Find the stream dataset index (it's the first one)
1967            let names = file.dataset_names();
1968            assert!(names.contains(&"stream".to_string()));
1969
1970            // Write more chunks via the writer directly
1971            let mut inner = crate::file::borrow_inner_mut(&file.inner);
1972            if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
1973                let ds_idx = writer.dataset_index("stream").unwrap();
1974                for i in 5..10u64 {
1975                    let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1976                    let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1977                    writer.write_chunk(ds_idx, i, &raw).unwrap();
1978                }
1979                writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
1980            }
1981            drop(inner);
1982            file.close().unwrap();
1983        }
1984
1985        // Read back all 10 frames
1986        {
1987            let file = H5File::open(&path).unwrap();
1988            let ds = file.dataset("stream").unwrap();
1989            assert_eq!(ds.shape(), vec![10, 3]);
1990            let data = ds.read_raw::<i32>().unwrap();
1991            assert_eq!(data.len(), 30);
1992            for (i, val) in data.iter().enumerate() {
1993                assert_eq!(*val, i as i32, "mismatch at {}", i);
1994            }
1995        }
1996
1997        std::fs::remove_file(&path).ok();
1998    }
1999
2000    #[test]
2001    fn group_hierarchy_roundtrip() {
2002        let path = temp_path("groups_rt");
2003
2004        {
2005            let file = H5File::create(&path).unwrap();
2006            let root = file.root_group();
2007
2008            // Create groups
2009            let det = root.create_group("detector").unwrap();
2010            let raw = det.create_group("raw").unwrap();
2011
2012            // Create datasets in groups
2013            let ds1 = det
2014                .new_dataset::<f32>()
2015                .shape([10usize])
2016                .create("temperature")
2017                .unwrap();
2018            ds1.write_raw(&[1.0f32; 10]).unwrap();
2019
2020            let ds2 = raw
2021                .new_dataset::<u16>()
2022                .shape([4usize, 4])
2023                .create("image")
2024                .unwrap();
2025            ds2.write_raw(&[42u16; 16]).unwrap();
2026
2027            // Root-level dataset
2028            let ds3 = file
2029                .new_dataset::<i32>()
2030                .shape([3usize])
2031                .create("version")
2032                .unwrap();
2033            ds3.write_raw(&[1i32, 0, 0]).unwrap();
2034
2035            file.close().unwrap();
2036        }
2037
2038        {
2039            let file = H5File::open(&path).unwrap();
2040            let names = file.dataset_names();
2041            assert!(names.contains(&"version".to_string()));
2042            assert!(names.contains(&"detector/temperature".to_string()));
2043            assert!(names.contains(&"detector/raw/image".to_string()));
2044
2045            // Read datasets
2046            let ds = file.dataset("version").unwrap();
2047            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
2048
2049            let ds = file.dataset("detector/temperature").unwrap();
2050            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
2051
2052            let ds = file.dataset("detector/raw/image").unwrap();
2053            assert_eq!(ds.shape(), vec![4, 4]);
2054            assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
2055
2056            // Group traversal
2057            let root = file.root_group();
2058            let group_names = root.group_names().unwrap();
2059            assert!(group_names.contains(&"detector".to_string()));
2060        }
2061
2062        std::fs::remove_file(&path).ok();
2063    }
2064
2065    #[test]
2066    fn nested_groups_via_file_create_group() {
2067        let path = temp_path("file_create_group");
2068
2069        {
2070            let file = H5File::create(&path).unwrap();
2071
2072            // Use the H5File::create_group convenience method
2073            let grp = file.create_group("sensors").unwrap();
2074            let sub = grp.create_group("accel").unwrap();
2075
2076            let ds = sub
2077                .new_dataset::<f64>()
2078                .shape([3usize])
2079                .create("xyz")
2080                .unwrap();
2081            ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
2082
2083            file.close().unwrap();
2084        }
2085
2086        {
2087            let file = H5File::open(&path).unwrap();
2088            let names = file.dataset_names();
2089            assert!(names.contains(&"sensors/accel/xyz".to_string()));
2090
2091            let ds = file.dataset("sensors/accel/xyz").unwrap();
2092            assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
2093
2094            // Open group in read mode
2095            let root = file.root_group();
2096            let sensors = root.group("sensors").unwrap();
2097            assert_eq!(sensors.name(), "/sensors");
2098
2099            let accel = sensors.group("accel").unwrap();
2100            assert_eq!(accel.name(), "/sensors/accel");
2101
2102            // list_groups from root
2103            let top_groups = root.group_names().unwrap();
2104            assert!(top_groups.contains(&"sensors".to_string()));
2105
2106            // list_groups from sensors
2107            let sub_groups = sensors.group_names().unwrap();
2108            assert!(sub_groups.contains(&"accel".to_string()));
2109        }
2110
2111        std::fs::remove_file(&path).ok();
2112    }
2113}
2114
2115#[cfg(test)]
2116mod h5py_compat_tests {
2117    use super::*;
2118
2119    // Only the deflate-gated h5dump test creates files; without that
2120    // feature this helper has no callers.
2121    #[cfg(feature = "deflate")]
2122    fn temp_path(name: &str) -> std::path::PathBuf {
2123        super::unique_test_path(name)
2124    }
2125
2126    /// Verify our files can be read by h5dump (if available).
2127    #[test]
2128    #[cfg(feature = "deflate")]
2129    fn h5dump_validates_our_files() {
2130        // Check if h5dump is available
2131        let h5dump = std::process::Command::new("h5dump")
2132            .arg("--version")
2133            .output();
2134        if h5dump.is_err() {
2135            eprintln!("skipping: h5dump not found");
2136            return;
2137        }
2138
2139        let path = temp_path("h5dump_validate");
2140
2141        // Write a comprehensive test file
2142        {
2143            let file = H5File::create(&path).unwrap();
2144
2145            // Contiguous
2146            let ds = file
2147                .new_dataset::<f64>()
2148                .shape([3usize, 4])
2149                .create("matrix")
2150                .unwrap();
2151            let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
2152            ds.write_raw(&data).unwrap();
2153
2154            // Chunked + compressed
2155            let ds2 = file
2156                .new_dataset::<i32>()
2157                .shape([0usize, 2])
2158                .chunk(&[1, 2])
2159                .max_shape(&[None, Some(2)])
2160                .deflate(6)
2161                .create("stream")
2162                .unwrap();
2163            for i in 0..5u64 {
2164                let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
2165                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2166                ds2.write_chunk(i as usize, &raw).unwrap();
2167            }
2168            ds2.extend(&[5, 2]).unwrap();
2169
2170            // Group
2171            let grp = file.create_group("meta").unwrap();
2172            let ds3 = grp
2173                .new_dataset::<u8>()
2174                .shape([4usize])
2175                .create("flags")
2176                .unwrap();
2177            ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
2178
2179            // String attribute
2180            use crate::types::VarLenUnicode;
2181            let attr = ds
2182                .new_attr::<VarLenUnicode>()
2183                .shape(())
2184                .create("units")
2185                .unwrap();
2186            attr.write_string("meters").unwrap();
2187
2188            file.close().unwrap();
2189        }
2190
2191        // Run h5dump and verify exit code
2192        let output = std::process::Command::new("h5dump")
2193            .arg("-H") // header only (faster)
2194            .arg(path.to_str().unwrap())
2195            .output()
2196            .unwrap();
2197
2198        assert!(
2199            output.status.success(),
2200            "h5dump failed:\nstdout: {}\nstderr: {}",
2201            String::from_utf8_lossy(&output.stdout),
2202            String::from_utf8_lossy(&output.stderr),
2203        );
2204
2205        // Full dump (with data) should also work
2206        let output2 = std::process::Command::new("h5dump")
2207            .arg(path.to_str().unwrap())
2208            .output()
2209            .unwrap();
2210
2211        assert!(
2212            output2.status.success(),
2213            "h5dump (full) failed:\nstderr: {}",
2214            String::from_utf8_lossy(&output2.stderr),
2215        );
2216
2217        std::fs::remove_file(&path).ok();
2218    }
2219
2220    #[test]
2221    fn read_h5py_generated_file() {
2222        let path = "/tmp/test_h5py_default.h5";
2223        if !std::path::Path::new(path).exists() {
2224            eprintln!("skipping: h5py test file not found");
2225            return;
2226        }
2227        let file = H5File::open(path).unwrap();
2228
2229        let ds = file.dataset("data").unwrap();
2230        assert_eq!(ds.shape(), vec![4, 5]);
2231        let data = ds.read_raw::<f64>().unwrap();
2232        assert_eq!(data.len(), 20);
2233        assert!((data[0]).abs() < 1e-10);
2234        assert!((data[19] - 19.0).abs() < 1e-10);
2235
2236        let ds2 = file.dataset("images").unwrap();
2237        assert_eq!(ds2.shape(), vec![3, 64, 64]);
2238        let images = ds2.read_raw::<u16>().unwrap();
2239        assert_eq!(images.len(), 3 * 64 * 64);
2240    }
2241}