Skip to main content

rust_hdf5/
file.rs

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