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 create_over_existing_file_truncates() {
913        // The create path skips the ftruncate on a brand-new empty file (it
914        // arms ext4's auto_da_alloc and turns close(2) into an implicit
915        // writeback, defeating close_no_sync). This pins the other side of
916        // that guard: creating over an existing non-empty file must still
917        // truncate it, so no stale content survives.
918        let path = temp_path("create_truncates");
919
920        {
921            let file = H5File::create(&path).unwrap();
922            let ds = file
923                .new_dataset::<u8>()
924                .shape([4, 4])
925                .create("old_data")
926                .unwrap();
927            ds.write_raw(&[7u8; 16]).unwrap();
928            file.close().unwrap();
929        }
930        assert!(std::fs::metadata(&path).unwrap().len() > 0);
931
932        // Re-create over the non-empty file, write nothing.
933        {
934            let file = H5File::create(&path).unwrap();
935            file.close().unwrap();
936        }
937
938        // The old dataset must be gone.
939        let file = H5File::open(&path).unwrap();
940        assert!(file.dataset("old_data").is_err());
941        file.close().unwrap();
942
943        std::fs::remove_file(&path).ok();
944    }
945
946    #[test]
947    fn close_no_sync_chunked_dataset_valid() {
948        // Exercises flush_dataset_synced(sync=false): a chunked (EA-indexed)
949        // dataset closed with close_no_sync must skip the per-dataset
950        // sync_data yet still write valid index structures, so the reopened
951        // file reconstructs every frame.
952        let path = temp_path("close_no_sync_chunked");
953
954        {
955            let file = H5File::create(&path).unwrap();
956            let ds = file
957                .new_dataset::<i32>()
958                .shape([0usize, 3])
959                .chunk(&[1, 3])
960                .max_shape(&[None, Some(3)])
961                .create("data")
962                .unwrap();
963            // 10 frames exceeds idx_blk_elmts=4, so data blocks are exercised.
964            for frame in 0..10u64 {
965                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
966                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
967                ds.write_chunk(frame as usize, &raw).unwrap();
968            }
969            ds.extend(&[10, 3]).unwrap();
970            file.close_no_sync().unwrap();
971        }
972
973        {
974            let file = H5File::open(&path).unwrap();
975            let ds = file.dataset("data").unwrap();
976            assert_eq!(ds.shape(), vec![10, 3]);
977            let data = ds.read_raw::<i32>().unwrap();
978            let expected: Vec<i32> = (0..30).collect();
979            assert_eq!(data, expected);
980            file.close().unwrap();
981        }
982
983        std::fs::remove_file(&path).ok();
984    }
985
986    #[test]
987    fn write_and_read_f64() {
988        let path = temp_path("write_read_f64");
989
990        let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
991
992        // Write
993        {
994            let file = H5File::create(&path).unwrap();
995            let ds = file
996                .new_dataset::<f64>()
997                .shape([2, 3])
998                .create("matrix")
999                .unwrap();
1000            ds.write_raw(&values).unwrap();
1001            file.close().unwrap();
1002        }
1003
1004        // Read
1005        {
1006            let file = H5File::open(&path).unwrap();
1007            let ds = file.dataset("matrix").unwrap();
1008            assert_eq!(ds.shape(), vec![2, 3]);
1009            let readback = ds.read_raw::<f64>().unwrap();
1010            assert_eq!(readback, values);
1011        }
1012
1013        std::fs::remove_file(&path).ok();
1014    }
1015
1016    #[test]
1017    fn multiple_datasets() {
1018        let path = temp_path("multi_ds");
1019
1020        {
1021            let file = H5File::create(&path).unwrap();
1022            let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
1023            ds1.write_raw(&[10i32, 20, 30]).unwrap();
1024
1025            let ds2 = file
1026                .new_dataset::<f32>()
1027                .shape([2, 2])
1028                .create("floats")
1029                .unwrap();
1030            ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
1031
1032            file.close().unwrap();
1033        }
1034
1035        {
1036            let file = H5File::open(&path).unwrap();
1037
1038            let ds_ints = file.dataset("ints").unwrap();
1039            assert_eq!(ds_ints.shape(), vec![3]);
1040            let ints = ds_ints.read_raw::<i32>().unwrap();
1041            assert_eq!(ints, vec![10, 20, 30]);
1042
1043            let ds_floats = file.dataset("floats").unwrap();
1044            assert_eq!(ds_floats.shape(), vec![2, 2]);
1045            let floats = ds_floats.read_raw::<f32>().unwrap();
1046            assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
1047        }
1048
1049        std::fs::remove_file(&path).ok();
1050    }
1051
1052    #[test]
1053    fn close_is_idempotent() {
1054        let path = temp_path("close_idemp");
1055        let file = H5File::create(&path).unwrap();
1056        file.close().unwrap();
1057        // File is consumed by close(), so no double-close possible at the type level.
1058        std::fs::remove_file(&path).ok();
1059    }
1060}
1061
1062#[cfg(test)]
1063mod integration_tests {
1064    use super::*;
1065
1066    fn temp_path(name: &str) -> std::path::PathBuf {
1067        super::unique_test_path(name)
1068    }
1069
1070    #[test]
1071    fn write_file_for_h5dump() {
1072        let path = temp_path("integration");
1073        let file = H5File::create(&path).unwrap();
1074
1075        let ds = file
1076            .new_dataset::<u8>()
1077            .shape([4usize, 4])
1078            .create("data_u8")
1079            .unwrap();
1080        let data: Vec<u8> = (0..16).collect();
1081        ds.write_raw(&data).unwrap();
1082
1083        let ds2 = file
1084            .new_dataset::<f64>()
1085            .shape([3usize, 2])
1086            .create("data_f64")
1087            .unwrap();
1088        let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1089        ds2.write_raw(&fdata).unwrap();
1090
1091        let ds3 = file
1092            .new_dataset::<i32>()
1093            .shape([5usize])
1094            .create("values")
1095            .unwrap();
1096        let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
1097        ds3.write_raw(&idata).unwrap();
1098
1099        file.close().unwrap();
1100
1101        // File exists
1102        assert!(path.exists());
1103    }
1104
1105    #[test]
1106    fn write_chunked_file_for_h5dump() {
1107        let path = temp_path("chunked");
1108        let file = H5File::create(&path).unwrap();
1109
1110        // Create a chunked dataset with unlimited first dimension
1111        let ds = file
1112            .new_dataset::<f64>()
1113            .shape([0usize, 4])
1114            .chunk(&[1, 4])
1115            .max_shape(&[None, Some(4)])
1116            .create("streaming_data")
1117            .unwrap();
1118
1119        // Write 5 frames of data
1120        for frame in 0..5u64 {
1121            let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1122            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1123            ds.write_chunk(frame as usize, &raw).unwrap();
1124        }
1125
1126        // Extend dimensions to reflect the 5 written frames
1127        ds.extend(&[5, 4]).unwrap();
1128        ds.flush().unwrap();
1129
1130        file.close().unwrap();
1131
1132        assert!(path.exists());
1133    }
1134
1135    #[test]
1136    fn write_chunked_many_frames_for_h5dump() {
1137        let path = temp_path("chunked_many");
1138        let file = H5File::create(&path).unwrap();
1139
1140        let ds = file
1141            .new_dataset::<i32>()
1142            .shape([0usize, 3])
1143            .chunk(&[1, 3])
1144            .max_shape(&[None, Some(3)])
1145            .create("data")
1146            .unwrap();
1147
1148        // Write 10 frames (exceeds idx_blk_elmts=4, uses data blocks)
1149        for frame in 0..10u64 {
1150            let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1151            let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1152            ds.write_chunk(frame as usize, &raw).unwrap();
1153        }
1154        ds.extend(&[10, 3]).unwrap();
1155        file.close().unwrap();
1156
1157        assert!(path.exists());
1158    }
1159
1160    #[test]
1161    fn write_dataset_with_attributes() {
1162        use crate::types::VarLenUnicode;
1163
1164        let path = temp_path("attributes");
1165        let file = H5File::create(&path).unwrap();
1166
1167        let ds = file
1168            .new_dataset::<f32>()
1169            .shape([10usize])
1170            .create("temperature")
1171            .unwrap();
1172        let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
1173        ds.write_raw(&data).unwrap();
1174
1175        // Add string attributes
1176        let attr = ds
1177            .new_attr::<VarLenUnicode>()
1178            .shape(())
1179            .create("units")
1180            .unwrap();
1181        attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
1182            .unwrap();
1183
1184        let attr2 = ds
1185            .new_attr::<VarLenUnicode>()
1186            .shape(())
1187            .create("description")
1188            .unwrap();
1189        attr2
1190            .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
1191            .unwrap();
1192
1193        // Use write_string convenience method
1194        let attr3 = ds
1195            .new_attr::<VarLenUnicode>()
1196            .shape(())
1197            .create("source")
1198            .unwrap();
1199        attr3.write_string("sensor_01").unwrap();
1200
1201        // Also test parse -> write_scalar pattern
1202        let attr4 = ds
1203            .new_attr::<VarLenUnicode>()
1204            .shape(())
1205            .create("label")
1206            .unwrap();
1207        let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
1208        attr4.write_scalar(&s).unwrap();
1209
1210        file.close().unwrap();
1211
1212        assert!(path.exists());
1213    }
1214
1215    #[test]
1216    fn dataset_writer_reopens_for_attributes() {
1217        // Reopen a dataset by name in write mode (the original handle is gone)
1218        // and attach attributes to it — the close-time flush pattern.
1219        let path = temp_path("dataset_writer");
1220        {
1221            let file = H5File::create(&path).unwrap();
1222            {
1223                let ds = file
1224                    .new_dataset::<u16>()
1225                    .shape([8])
1226                    .create("image")
1227                    .unwrap();
1228                ds.write_raw(&[0u16; 8]).unwrap();
1229                // original handle dropped here
1230            }
1231
1232            // Reopen by name; dataset() would error in write mode.
1233            assert!(file.dataset("image").is_err());
1234            let ds = file.dataset_writer("image").unwrap();
1235            assert_eq!(ds.shape(), vec![8]);
1236            ds.new_attr::<i32>()
1237                .shape([3])
1238                .create("NDArrayDimOffset")
1239                .unwrap()
1240                .write_array(&[0i32, 4, 8])
1241                .unwrap();
1242            ds.new_attr::<i32>()
1243                .shape(())
1244                .create("NDUniqueId")
1245                .unwrap()
1246                .write_numeric(&42i32)
1247                .unwrap();
1248
1249            // Missing dataset is reported.
1250            assert!(matches!(
1251                file.dataset_writer("nope"),
1252                Err(crate::error::Hdf5Error::NotFound(_))
1253            ));
1254
1255            file.close().unwrap();
1256        }
1257        {
1258            let file = H5File::open(&path).unwrap();
1259            let ds = file.dataset("image").unwrap();
1260            let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
1261            let got: Vec<i32> = off
1262                .chunks_exact(4)
1263                .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1264                .collect();
1265            assert_eq!(got, vec![0, 4, 8]);
1266            let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
1267            assert_eq!(uid, 42);
1268        }
1269        std::fs::remove_file(&path).ok();
1270    }
1271
1272    #[test]
1273    fn chunked_write_read_roundtrip() {
1274        let path = temp_path("chunked_roundtrip");
1275
1276        // Write
1277        {
1278            let file = H5File::create(&path).unwrap();
1279            let ds = file
1280                .new_dataset::<i32>()
1281                .shape([0usize, 3])
1282                .chunk(&[1, 3])
1283                .max_shape(&[None, Some(3)])
1284                .create("table")
1285                .unwrap();
1286
1287            for frame in 0..8u64 {
1288                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1289                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1290                ds.write_chunk(frame as usize, &raw).unwrap();
1291            }
1292            ds.extend(&[8, 3]).unwrap();
1293            file.close().unwrap();
1294        }
1295
1296        // Read
1297        {
1298            let file = H5File::open(&path).unwrap();
1299            let ds = file.dataset("table").unwrap();
1300            assert_eq!(ds.shape(), vec![8, 3]);
1301            let data = ds.read_raw::<i32>().unwrap();
1302            assert_eq!(data.len(), 24);
1303            for (i, val) in data.iter().enumerate() {
1304                assert_eq!(*val, i as i32);
1305            }
1306        }
1307
1308        std::fs::remove_file(&path).ok();
1309    }
1310
1311    #[test]
1312    #[cfg(feature = "deflate")]
1313    fn compressed_chunked_roundtrip() {
1314        let path = temp_path("compressed_roundtrip");
1315
1316        // Write compressed
1317        {
1318            let file = H5File::create(&path).unwrap();
1319            let ds = file
1320                .new_dataset::<f64>()
1321                .shape([0usize, 4])
1322                .chunk(&[1, 4])
1323                .max_shape(&[None, Some(4)])
1324                .deflate(6)
1325                .create("compressed")
1326                .unwrap();
1327
1328            for frame in 0..10u64 {
1329                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1330                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1331                ds.write_chunk(frame as usize, &raw).unwrap();
1332            }
1333            ds.extend(&[10, 4]).unwrap();
1334            file.close().unwrap();
1335        }
1336
1337        // Read back and verify
1338        {
1339            let file = H5File::open(&path).unwrap();
1340            let ds = file.dataset("compressed").unwrap();
1341            assert_eq!(ds.shape(), vec![10, 4]);
1342            let data = ds.read_raw::<f64>().unwrap();
1343            assert_eq!(data.len(), 40);
1344            for (i, val) in data.iter().enumerate() {
1345                assert!(
1346                    (val - i as f64).abs() < 1e-10,
1347                    "mismatch at {}: {} != {}",
1348                    i,
1349                    val,
1350                    i
1351                );
1352            }
1353        }
1354
1355        std::fs::remove_file(&path).ok();
1356    }
1357
1358    #[test]
1359    #[cfg(feature = "deflate")]
1360    fn compressed_chunked_many_frames() {
1361        let path = temp_path("compressed_many");
1362
1363        {
1364            let file = H5File::create(&path).unwrap();
1365            let ds = file
1366                .new_dataset::<i32>()
1367                .shape([0usize, 3])
1368                .chunk(&[1, 3])
1369                .max_shape(&[None, Some(3)])
1370                .deflate(6)
1371                .create("stream")
1372                .unwrap();
1373
1374            for frame in 0..100u64 {
1375                let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1376                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1377                ds.write_chunk(frame as usize, &raw).unwrap();
1378            }
1379            ds.extend(&[100, 3]).unwrap();
1380            file.close().unwrap();
1381        }
1382
1383        {
1384            let file = H5File::open(&path).unwrap();
1385            let ds = file.dataset("stream").unwrap();
1386            assert_eq!(ds.shape(), vec![100, 3]);
1387            let data = ds.read_raw::<i32>().unwrap();
1388            assert_eq!(data.len(), 300);
1389            for (i, val) in data.iter().enumerate() {
1390                assert_eq!(*val, i as i32, "mismatch at {}", i);
1391            }
1392        }
1393
1394        std::fs::remove_file(&path).ok();
1395    }
1396    #[test]
1397    fn append_mode() {
1398        let path = temp_path("append");
1399
1400        // Create initial file
1401        {
1402            let file = H5File::create(&path).unwrap();
1403            let ds = file
1404                .new_dataset::<i32>()
1405                .shape([3usize])
1406                .create("first")
1407                .unwrap();
1408            ds.write_raw(&[1i32, 2, 3]).unwrap();
1409            file.close().unwrap();
1410        }
1411
1412        // Append new dataset
1413        {
1414            let file = H5File::open_rw(&path).unwrap();
1415            let ds = file
1416                .new_dataset::<f64>()
1417                .shape([2usize])
1418                .create("second")
1419                .unwrap();
1420            ds.write_raw(&[4.0f64, 5.0]).unwrap();
1421            file.close().unwrap();
1422        }
1423
1424        // Read back both
1425        {
1426            let file = H5File::open(&path).unwrap();
1427            let names = file.dataset_names();
1428            assert!(names.contains(&"first".to_string()));
1429            assert!(names.contains(&"second".to_string()));
1430
1431            let ds1 = file.dataset("first").unwrap();
1432            assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
1433
1434            let ds2 = file.dataset("second").unwrap();
1435            assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
1436        }
1437
1438        std::fs::remove_file(&path).ok();
1439    }
1440
1441    #[test]
1442    fn open_rw_set_attr_preserves_file() {
1443        let path = temp_path("open_rw_attr");
1444        // Create file with a dataset and an attribute
1445        {
1446            let file = H5File::create(&path).unwrap();
1447            let ds = file
1448                .new_dataset::<i32>()
1449                .shape([3usize])
1450                .create("data")
1451                .unwrap();
1452            ds.write_raw(&[10i32, 20, 30]).unwrap();
1453            file.set_attr_string("version", "1.0").unwrap();
1454            file.close().unwrap();
1455        }
1456        // Open rw and modify the attribute
1457        {
1458            let file = H5File::open_rw(&path).unwrap();
1459            file.set_attr_string("version", "2.0").unwrap();
1460            file.close().unwrap();
1461        }
1462        // Verify: dataset intact, attribute updated
1463        {
1464            let file = H5File::open(&path).unwrap();
1465            let ds = file.dataset("data").unwrap();
1466            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
1467            let ver = file.attr_string("version").unwrap();
1468            assert_eq!(ver, "2.0");
1469        }
1470        std::fs::remove_file(&path).ok();
1471    }
1472
1473    #[test]
1474    #[cfg(feature = "deflate")]
1475    fn open_rw_attr_with_compressed_dataset() {
1476        use crate::format::messages::filter::FilterPipeline;
1477        let path = temp_path("open_rw_compressed");
1478        let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
1479        // Create file with compressed vlen strings
1480        {
1481            let file = H5File::create(&path).unwrap();
1482            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1483                .unwrap();
1484            file.set_attr_string("version", "1.0").unwrap();
1485            file.close().unwrap();
1486        }
1487        // Open rw and modify attribute only
1488        {
1489            let file = H5File::open_rw(&path).unwrap();
1490            file.set_attr_string("version", "2.0").unwrap();
1491            file.close().unwrap();
1492        }
1493        // Verify: compressed dataset still readable, attribute updated
1494        {
1495            let file = H5File::open(&path).unwrap();
1496            let ds = file.dataset("texts").unwrap();
1497            let strings = ds.read_vlen_strings().unwrap();
1498            assert_eq!(strings.len(), 50);
1499            assert_eq!(strings[0], "test string data");
1500            let ver = file.attr_string("version").unwrap();
1501            assert_eq!(ver, "2.0");
1502        }
1503        std::fs::remove_file(&path).ok();
1504    }
1505
1506    #[test]
1507    #[cfg(feature = "lz4")]
1508    fn append_vlen_strings_basic() {
1509        use crate::format::messages::filter::FilterPipeline;
1510        let path = temp_path("append_vlen");
1511        {
1512            let file = H5File::create(&path).unwrap();
1513            file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
1514                .unwrap();
1515            file.append_vlen_strings("names", &["alice", "bob", "charlie"])
1516                .unwrap();
1517            file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
1518            file.close().unwrap();
1519        }
1520        {
1521            let file = H5File::open(&path).unwrap();
1522            let ds = file.dataset("names").unwrap();
1523            let strings = ds.read_vlen_strings().unwrap();
1524            assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
1525        }
1526        std::fs::remove_file(&path).ok();
1527    }
1528
1529    #[test]
1530    #[cfg(feature = "lz4")]
1531    fn append_vlen_strings_large() {
1532        use crate::format::messages::filter::FilterPipeline;
1533        let path = temp_path("append_vlen_large");
1534        let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
1535        let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
1536        {
1537            let file = H5File::create(&path).unwrap();
1538            file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
1539                .unwrap();
1540            let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
1541            file.append_vlen_strings("data", &r1).unwrap();
1542            let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
1543            file.append_vlen_strings("data", &r2).unwrap();
1544            file.close().unwrap();
1545        }
1546        {
1547            let file = H5File::open(&path).unwrap();
1548            let ds = file.dataset("data").unwrap();
1549            let strings = ds.read_vlen_strings().unwrap();
1550            assert_eq!(strings.len(), 7189);
1551            assert_eq!(strings[0], "node-000000");
1552            assert_eq!(strings[7188], "node-007188");
1553        }
1554        std::fs::remove_file(&path).ok();
1555    }
1556
1557    #[test]
1558    fn append_vlen_strings_uncompressed() {
1559        let path = temp_path("append_vlen_unc");
1560        {
1561            let file = H5File::create(&path).unwrap();
1562            file.create_appendable_vlen_dataset("texts", 8, None)
1563                .unwrap();
1564            file.append_vlen_strings("texts", &["hello", "world"])
1565                .unwrap();
1566            file.append_vlen_strings("texts", &["foo", "bar", "baz"])
1567                .unwrap();
1568            file.close().unwrap();
1569        }
1570        {
1571            let file = H5File::open(&path).unwrap();
1572            let ds = file.dataset("texts").unwrap();
1573            let strings = ds.read_vlen_strings().unwrap();
1574            assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
1575        }
1576        std::fs::remove_file(&path).ok();
1577    }
1578
1579    #[test]
1580    fn delete_dataset_roundtrip() {
1581        let path = temp_path("delete_ds");
1582        {
1583            let file = H5File::create(&path).unwrap();
1584            file.write_vlen_strings("keep", &["a", "b"]).unwrap();
1585            file.write_vlen_strings("remove", &["x", "y"]).unwrap();
1586            file.delete_dataset("remove").unwrap();
1587            file.close().unwrap();
1588        }
1589        {
1590            let file = H5File::open(&path).unwrap();
1591            let names = file.dataset_names();
1592            assert!(names.contains(&"keep".to_string()));
1593            assert!(!names.contains(&"remove".to_string()));
1594            let ds = file.dataset("keep").unwrap();
1595            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
1596        }
1597        std::fs::remove_file(&path).ok();
1598    }
1599
1600    #[test]
1601    fn delete_group_roundtrip() {
1602        let path = temp_path("delete_grp");
1603        {
1604            let file = H5File::create(&path).unwrap();
1605            let g1 = file.create_group("keep").unwrap();
1606            g1.write_vlen_strings("data", &["a"]).unwrap();
1607            let g2 = file.create_group("remove").unwrap();
1608            g2.write_vlen_strings("data", &["x"]).unwrap();
1609            file.delete_group("remove").unwrap();
1610            file.close().unwrap();
1611        }
1612        {
1613            let file = H5File::open(&path).unwrap();
1614            let names = file.dataset_names();
1615            assert!(names.contains(&"keep/data".to_string()));
1616            assert!(!names.contains(&"remove/data".to_string()));
1617        }
1618        std::fs::remove_file(&path).ok();
1619    }
1620
1621    #[test]
1622    fn open_rw_delete_recreate_group() {
1623        let path = temp_path("rw_delete_recreate");
1624        // Step 1: create file with groups
1625        {
1626            let file = H5File::create(&path).unwrap();
1627            let n = file.create_group("nodes").unwrap();
1628            n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
1629            let e = file.create_group("edges").unwrap();
1630            e.write_vlen_strings("src", &["x", "y"]).unwrap();
1631            file.close().unwrap();
1632        }
1633        // Step 2: open_rw, delete one group, recreate with new data
1634        {
1635            let file = H5File::open_rw(&path).unwrap();
1636            file.delete_group("nodes").unwrap();
1637            let n = file.create_group("nodes").unwrap();
1638            n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
1639            file.close().unwrap();
1640        }
1641        // Step 3: verify
1642        {
1643            let file = H5File::open(&path).unwrap();
1644            let ds = file.dataset("nodes/id").unwrap();
1645            let s = ds.read_vlen_strings().unwrap();
1646            assert_eq!(s, vec!["new1", "new2"]);
1647            // edges should still be intact
1648            let ds = file.dataset("edges/src").unwrap();
1649            let s = ds.read_vlen_strings().unwrap();
1650            assert_eq!(s, vec!["x", "y"]);
1651        }
1652        std::fs::remove_file(&path).ok();
1653    }
1654
1655    #[test]
1656    fn delete_and_recreate_group() {
1657        let path = temp_path("delete_recreate");
1658        {
1659            let file = H5File::create(&path).unwrap();
1660            let g = file.create_group("nodes").unwrap();
1661            g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
1662            file.delete_group("nodes").unwrap();
1663            let g = file.create_group("nodes").unwrap();
1664            g.write_vlen_strings("id", &["new1", "new2", "new3"])
1665                .unwrap();
1666            file.close().unwrap();
1667        }
1668        {
1669            let file = H5File::open(&path).unwrap();
1670            let ds = file.dataset("nodes/id").unwrap();
1671            let strings = ds.read_vlen_strings().unwrap();
1672            assert_eq!(strings, vec!["new1", "new2", "new3"]);
1673        }
1674        std::fs::remove_file(&path).ok();
1675    }
1676
1677    #[test]
1678    #[cfg(feature = "deflate")]
1679    fn vlen_string_compressed_large_roundtrip() {
1680        use crate::format::messages::filter::FilterPipeline;
1681        let path = temp_path("vlen_large");
1682        // Simulate kodex scenario: 7189 strings, chunk_size 512
1683        let input: Vec<String> = (0..7189)
1684            .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
1685            .collect();
1686        let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
1687        {
1688            let file = H5File::create(&path).unwrap();
1689            file.create_group("nodes").unwrap();
1690            file.write_vlen_strings_compressed(
1691                "nodes/id",
1692                &input_refs,
1693                512,
1694                FilterPipeline::deflate(6),
1695            )
1696            .unwrap();
1697            file.close().unwrap();
1698        }
1699        // Read back
1700        {
1701            let file = H5File::open(&path).unwrap();
1702            let ds = file.dataset("nodes/id").unwrap();
1703            let strings = ds.read_vlen_strings().unwrap();
1704            assert_eq!(strings.len(), 7189);
1705            assert_eq!(strings[0], input[0]);
1706            assert_eq!(strings[7188], input[7188]);
1707        }
1708        // Also test open_rw then re-read
1709        {
1710            let file = H5File::open_rw(&path).unwrap();
1711            file.set_attr_string("version", "1.0").unwrap();
1712            file.close().unwrap();
1713        }
1714        {
1715            let file = H5File::open(&path).unwrap();
1716            let ds = file.dataset("nodes/id").unwrap();
1717            let strings = ds.read_vlen_strings().unwrap();
1718            assert_eq!(strings.len(), 7189);
1719            assert_eq!(strings[0], input[0]);
1720        }
1721        std::fs::remove_file(&path).ok();
1722    }
1723
1724    #[test]
1725    fn vlen_string_write_read() {
1726        let path = temp_path("vlen_wr");
1727        {
1728            let file = H5File::create(&path).unwrap();
1729            file.write_vlen_strings("names", &["alice", "bob", "charlie"])
1730                .unwrap();
1731            file.close().unwrap();
1732        }
1733        {
1734            let file = H5File::open(&path).unwrap();
1735            let ds = file.dataset("names").unwrap();
1736            let strings = ds.read_vlen_strings().unwrap();
1737            assert_eq!(strings, vec!["alice", "bob", "charlie"]);
1738        }
1739        std::fs::remove_file(&path).ok();
1740    }
1741
1742    #[test]
1743    fn vlen_bytes_write_read() {
1744        let path = temp_path("vlen_bytes_wr");
1745        let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
1746        {
1747            let file = H5File::create(&path).unwrap();
1748            file.write_vlen_bytes("blobs", &items).unwrap();
1749            file.close().unwrap();
1750        }
1751        {
1752            let file = H5File::open(&path).unwrap();
1753            let ds = file.dataset("blobs").unwrap();
1754            let got = ds.read_vlen_bytes().unwrap();
1755            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1756            assert_eq!(got, expected);
1757        }
1758        std::fs::remove_file(&path).ok();
1759    }
1760
1761    #[test]
1762    fn vlen_bytes_in_group_with_attribute() {
1763        use crate::types::VarLenUnicode;
1764        let path = temp_path("vlen_bytes_grp");
1765        let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
1766        {
1767            let file = H5File::create(&path).unwrap();
1768            let grp = file.root_group().create_group("payloads").unwrap();
1769            let ds = grp.write_vlen_bytes("frames", &items).unwrap();
1770            ds.new_attr::<VarLenUnicode>()
1771                .shape(())
1772                .create("codec")
1773                .unwrap()
1774                .write_string("raw")
1775                .unwrap();
1776            file.close().unwrap();
1777        }
1778        {
1779            let file = H5File::open(&path).unwrap();
1780            let ds = file.dataset("payloads/frames").unwrap();
1781            let got = ds.read_vlen_bytes().unwrap();
1782            let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1783            assert_eq!(got, expected);
1784        }
1785        std::fs::remove_file(&path).ok();
1786    }
1787
1788    #[test]
1789    fn vlen_dataset_returns_handle_for_attributes() {
1790        use crate::types::VarLenUnicode;
1791        let path = temp_path("vlen_attr");
1792        {
1793            let file = H5File::create(&path).unwrap();
1794            let grp = file.root_group().create_group("ch").unwrap();
1795            // The vlen helper now returns the dataset handle, so attributes can
1796            // be attached directly — the issue the mdfr reporter hit.
1797            let ds = grp
1798                .write_vlen_strings("labels", &["a", "bb", "ccc"])
1799                .unwrap();
1800            ds.new_attr::<VarLenUnicode>()
1801                .shape(())
1802                .create("unit")
1803                .unwrap()
1804                .write_string("volt")
1805                .unwrap();
1806            // The same dataset can also be reopened by name within the group.
1807            let ds2 = grp.dataset_writer("labels").unwrap();
1808            ds2.new_attr::<VarLenUnicode>()
1809                .shape(())
1810                .create("desc")
1811                .unwrap()
1812                .write_string("channel labels")
1813                .unwrap();
1814            file.close().unwrap();
1815        }
1816        {
1817            let file = H5File::open(&path).unwrap();
1818            let ds = file.dataset("ch/labels").unwrap();
1819            assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
1820            assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
1821            assert_eq!(
1822                ds.attr("desc").unwrap().read_string().unwrap(),
1823                "channel labels"
1824            );
1825        }
1826        std::fs::remove_file(&path).ok();
1827    }
1828
1829    #[test]
1830    #[cfg(feature = "deflate")]
1831    fn vlen_string_deflate_roundtrip() {
1832        use crate::format::messages::filter::FilterPipeline;
1833        let path = temp_path("vlen_deflate");
1834        let input: Vec<&str> = (0..100)
1835            .map(|i| match i % 3 {
1836                0 => "hello world",
1837                1 => "compressed vlen string test",
1838                _ => "rust-hdf5",
1839            })
1840            .collect();
1841        {
1842            let file = H5File::create(&path).unwrap();
1843            file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1844                .unwrap();
1845            file.close().unwrap();
1846        }
1847        {
1848            let file = H5File::open(&path).unwrap();
1849            let ds = file.dataset("texts").unwrap();
1850            let strings = ds.read_vlen_strings().unwrap();
1851            assert_eq!(strings.len(), 100);
1852            for (i, s) in strings.iter().enumerate() {
1853                assert_eq!(s, input[i]);
1854            }
1855        }
1856        std::fs::remove_file(&path).ok();
1857    }
1858
1859    #[test]
1860    #[cfg(feature = "zstd")]
1861    fn vlen_string_zstd_roundtrip() {
1862        use crate::format::messages::filter::FilterPipeline;
1863        let path = temp_path("vlen_zstd");
1864        let input: Vec<&str> = (0..200)
1865            .map(|i| match i % 4 {
1866                0 => "zstandard compression test",
1867                1 => "variable length string",
1868                2 => "rust-hdf5 chunked storage",
1869                _ => "hello zstd world",
1870            })
1871            .collect();
1872        {
1873            let file = H5File::create(&path).unwrap();
1874            file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
1875                .unwrap();
1876            file.close().unwrap();
1877        }
1878        {
1879            let file = H5File::open(&path).unwrap();
1880            let ds = file.dataset("data").unwrap();
1881            let strings = ds.read_vlen_strings().unwrap();
1882            assert_eq!(strings.len(), 200);
1883            for (i, s) in strings.iter().enumerate() {
1884                assert_eq!(s, input[i]);
1885            }
1886        }
1887        std::fs::remove_file(&path).ok();
1888    }
1889
1890    #[test]
1891    #[cfg(feature = "deflate")]
1892    fn shuffle_deflate_roundtrip() {
1893        let path = temp_path("shuf_defl");
1894        {
1895            let file = H5File::create(&path).unwrap();
1896            let ds = file
1897                .new_dataset::<f64>()
1898                .shape([0usize, 4])
1899                .chunk(&[1, 4])
1900                .max_shape(&[None, Some(4)])
1901                .shuffle_deflate(6)
1902                .create("data")
1903                .unwrap();
1904            for frame in 0..20u64 {
1905                let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1906                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1907                ds.write_chunk(frame as usize, &raw).unwrap();
1908            }
1909            ds.extend(&[20, 4]).unwrap();
1910            file.close().unwrap();
1911        }
1912        {
1913            let file = H5File::open(&path).unwrap();
1914            let ds = file.dataset("data").unwrap();
1915            assert_eq!(ds.shape(), vec![20, 4]);
1916            let data = ds.read_raw::<f64>().unwrap();
1917            assert_eq!(data.len(), 80);
1918            for (i, val) in data.iter().enumerate() {
1919                assert!((val - i as f64).abs() < 1e-10);
1920            }
1921        }
1922        std::fs::remove_file(&path).ok();
1923    }
1924
1925    #[test]
1926    fn file_level_attributes() {
1927        let path = temp_path("file_attr");
1928        {
1929            let file = H5File::create(&path).unwrap();
1930            file.set_attr_string("title", "Test File").unwrap();
1931            file.set_attr_numeric("version", &42i32).unwrap();
1932            let ds = file
1933                .new_dataset::<u8>()
1934                .shape([1usize])
1935                .create("dummy")
1936                .unwrap();
1937            ds.write_raw(&[0u8]).unwrap();
1938            file.close().unwrap();
1939        }
1940        {
1941            let file = H5File::open(&path).unwrap();
1942            assert!(file.dataset_names().contains(&"dummy".to_string()));
1943
1944            // Read file-level attributes
1945            let names = file.attr_names().unwrap();
1946            assert!(names.contains(&"title".to_string()));
1947
1948            let title = file.attr_string("title").unwrap();
1949            assert_eq!(title, "Test File");
1950        }
1951        std::fs::remove_file(&path).ok();
1952    }
1953
1954    #[test]
1955    fn scalar_dataset_roundtrip() {
1956        let path = temp_path("scalar");
1957        {
1958            let file = H5File::create(&path).unwrap();
1959            let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
1960            ds.write_raw(&[std::f64::consts::PI]).unwrap();
1961            file.close().unwrap();
1962        }
1963        {
1964            let file = H5File::open(&path).unwrap();
1965            let ds = file.dataset("pi").unwrap();
1966            assert_eq!(ds.shape(), Vec::<usize>::new());
1967            assert_eq!(ds.total_elements(), 1);
1968            let data = ds.read_raw::<f64>().unwrap();
1969            assert_eq!(data.len(), 1);
1970            assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
1971        }
1972        std::fs::remove_file(&path).ok();
1973    }
1974
1975    #[test]
1976    fn append_mode_extend_chunked() {
1977        let path = temp_path("append_extend");
1978
1979        // Create with 5 frames
1980        {
1981            let file = H5File::create(&path).unwrap();
1982            let ds = file
1983                .new_dataset::<i32>()
1984                .shape([0usize, 3])
1985                .chunk(&[1, 3])
1986                .max_shape(&[None, Some(3)])
1987                .create("stream")
1988                .unwrap();
1989            for i in 0..5u64 {
1990                let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1991                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1992                ds.write_chunk(i as usize, &raw).unwrap();
1993            }
1994            ds.extend(&[5, 3]).unwrap();
1995            file.close().unwrap();
1996        }
1997
1998        // Reopen and add 5 more frames
1999        {
2000            let file = H5File::open_rw(&path).unwrap();
2001            // Find the stream dataset index (it's the first one)
2002            let names = file.dataset_names();
2003            assert!(names.contains(&"stream".to_string()));
2004
2005            // Write more chunks via the writer directly
2006            let mut inner = crate::file::borrow_inner_mut(&file.inner);
2007            if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
2008                let ds_idx = writer.dataset_index("stream").unwrap();
2009                for i in 5..10u64 {
2010                    let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
2011                    let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2012                    writer.write_chunk(ds_idx, i, &raw).unwrap();
2013                }
2014                writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
2015            }
2016            drop(inner);
2017            file.close().unwrap();
2018        }
2019
2020        // Read back all 10 frames
2021        {
2022            let file = H5File::open(&path).unwrap();
2023            let ds = file.dataset("stream").unwrap();
2024            assert_eq!(ds.shape(), vec![10, 3]);
2025            let data = ds.read_raw::<i32>().unwrap();
2026            assert_eq!(data.len(), 30);
2027            for (i, val) in data.iter().enumerate() {
2028                assert_eq!(*val, i as i32, "mismatch at {}", i);
2029            }
2030        }
2031
2032        std::fs::remove_file(&path).ok();
2033    }
2034
2035    #[test]
2036    fn group_hierarchy_roundtrip() {
2037        let path = temp_path("groups_rt");
2038
2039        {
2040            let file = H5File::create(&path).unwrap();
2041            let root = file.root_group();
2042
2043            // Create groups
2044            let det = root.create_group("detector").unwrap();
2045            let raw = det.create_group("raw").unwrap();
2046
2047            // Create datasets in groups
2048            let ds1 = det
2049                .new_dataset::<f32>()
2050                .shape([10usize])
2051                .create("temperature")
2052                .unwrap();
2053            ds1.write_raw(&[1.0f32; 10]).unwrap();
2054
2055            let ds2 = raw
2056                .new_dataset::<u16>()
2057                .shape([4usize, 4])
2058                .create("image")
2059                .unwrap();
2060            ds2.write_raw(&[42u16; 16]).unwrap();
2061
2062            // Root-level dataset
2063            let ds3 = file
2064                .new_dataset::<i32>()
2065                .shape([3usize])
2066                .create("version")
2067                .unwrap();
2068            ds3.write_raw(&[1i32, 0, 0]).unwrap();
2069
2070            file.close().unwrap();
2071        }
2072
2073        {
2074            let file = H5File::open(&path).unwrap();
2075            let names = file.dataset_names();
2076            assert!(names.contains(&"version".to_string()));
2077            assert!(names.contains(&"detector/temperature".to_string()));
2078            assert!(names.contains(&"detector/raw/image".to_string()));
2079
2080            // Read datasets
2081            let ds = file.dataset("version").unwrap();
2082            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
2083
2084            let ds = file.dataset("detector/temperature").unwrap();
2085            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
2086
2087            let ds = file.dataset("detector/raw/image").unwrap();
2088            assert_eq!(ds.shape(), vec![4, 4]);
2089            assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
2090
2091            // Group traversal
2092            let root = file.root_group();
2093            let group_names = root.group_names().unwrap();
2094            assert!(group_names.contains(&"detector".to_string()));
2095        }
2096
2097        std::fs::remove_file(&path).ok();
2098    }
2099
2100    #[test]
2101    fn nested_groups_via_file_create_group() {
2102        let path = temp_path("file_create_group");
2103
2104        {
2105            let file = H5File::create(&path).unwrap();
2106
2107            // Use the H5File::create_group convenience method
2108            let grp = file.create_group("sensors").unwrap();
2109            let sub = grp.create_group("accel").unwrap();
2110
2111            let ds = sub
2112                .new_dataset::<f64>()
2113                .shape([3usize])
2114                .create("xyz")
2115                .unwrap();
2116            ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
2117
2118            file.close().unwrap();
2119        }
2120
2121        {
2122            let file = H5File::open(&path).unwrap();
2123            let names = file.dataset_names();
2124            assert!(names.contains(&"sensors/accel/xyz".to_string()));
2125
2126            let ds = file.dataset("sensors/accel/xyz").unwrap();
2127            assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
2128
2129            // Open group in read mode
2130            let root = file.root_group();
2131            let sensors = root.group("sensors").unwrap();
2132            assert_eq!(sensors.name(), "/sensors");
2133
2134            let accel = sensors.group("accel").unwrap();
2135            assert_eq!(accel.name(), "/sensors/accel");
2136
2137            // list_groups from root
2138            let top_groups = root.group_names().unwrap();
2139            assert!(top_groups.contains(&"sensors".to_string()));
2140
2141            // list_groups from sensors
2142            let sub_groups = sensors.group_names().unwrap();
2143            assert!(sub_groups.contains(&"accel".to_string()));
2144        }
2145
2146        std::fs::remove_file(&path).ok();
2147    }
2148}
2149
2150#[cfg(test)]
2151mod h5py_compat_tests {
2152    use super::*;
2153
2154    // Only the deflate-gated h5dump test creates files; without that
2155    // feature this helper has no callers.
2156    #[cfg(feature = "deflate")]
2157    fn temp_path(name: &str) -> std::path::PathBuf {
2158        super::unique_test_path(name)
2159    }
2160
2161    /// Verify our files can be read by h5dump (if available).
2162    #[test]
2163    #[cfg(feature = "deflate")]
2164    fn h5dump_validates_our_files() {
2165        // Check if h5dump is available
2166        let h5dump = std::process::Command::new("h5dump")
2167            .arg("--version")
2168            .output();
2169        if h5dump.is_err() {
2170            eprintln!("skipping: h5dump not found");
2171            return;
2172        }
2173
2174        let path = temp_path("h5dump_validate");
2175
2176        // Write a comprehensive test file
2177        {
2178            let file = H5File::create(&path).unwrap();
2179
2180            // Contiguous
2181            let ds = file
2182                .new_dataset::<f64>()
2183                .shape([3usize, 4])
2184                .create("matrix")
2185                .unwrap();
2186            let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
2187            ds.write_raw(&data).unwrap();
2188
2189            // Chunked + compressed
2190            let ds2 = file
2191                .new_dataset::<i32>()
2192                .shape([0usize, 2])
2193                .chunk(&[1, 2])
2194                .max_shape(&[None, Some(2)])
2195                .deflate(6)
2196                .create("stream")
2197                .unwrap();
2198            for i in 0..5u64 {
2199                let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
2200                let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2201                ds2.write_chunk(i as usize, &raw).unwrap();
2202            }
2203            ds2.extend(&[5, 2]).unwrap();
2204
2205            // Group
2206            let grp = file.create_group("meta").unwrap();
2207            let ds3 = grp
2208                .new_dataset::<u8>()
2209                .shape([4usize])
2210                .create("flags")
2211                .unwrap();
2212            ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
2213
2214            // String attribute
2215            use crate::types::VarLenUnicode;
2216            let attr = ds
2217                .new_attr::<VarLenUnicode>()
2218                .shape(())
2219                .create("units")
2220                .unwrap();
2221            attr.write_string("meters").unwrap();
2222
2223            file.close().unwrap();
2224        }
2225
2226        // Run h5dump and verify exit code
2227        let output = std::process::Command::new("h5dump")
2228            .arg("-H") // header only (faster)
2229            .arg(path.to_str().unwrap())
2230            .output()
2231            .unwrap();
2232
2233        assert!(
2234            output.status.success(),
2235            "h5dump failed:\nstdout: {}\nstderr: {}",
2236            String::from_utf8_lossy(&output.stdout),
2237            String::from_utf8_lossy(&output.stderr),
2238        );
2239
2240        // Full dump (with data) should also work
2241        let output2 = std::process::Command::new("h5dump")
2242            .arg(path.to_str().unwrap())
2243            .output()
2244            .unwrap();
2245
2246        assert!(
2247            output2.status.success(),
2248            "h5dump (full) failed:\nstderr: {}",
2249            String::from_utf8_lossy(&output2.stderr),
2250        );
2251
2252        std::fs::remove_file(&path).ok();
2253    }
2254
2255    #[test]
2256    fn read_h5py_generated_file() {
2257        let path = "/tmp/test_h5py_default.h5";
2258        if !std::path::Path::new(path).exists() {
2259            eprintln!("skipping: h5py test file not found");
2260            return;
2261        }
2262        let file = H5File::open(path).unwrap();
2263
2264        let ds = file.dataset("data").unwrap();
2265        assert_eq!(ds.shape(), vec![4, 5]);
2266        let data = ds.read_raw::<f64>().unwrap();
2267        assert_eq!(data.len(), 20);
2268        assert!((data[0]).abs() < 1e-10);
2269        assert!((data[19] - 19.0).abs() < 1e-10);
2270
2271        let ds2 = file.dataset("images").unwrap();
2272        assert_eq!(ds2.shape(), vec![3, 64, 64]);
2273        let images = ds2.read_raw::<u16>().unwrap();
2274        assert_eq!(images.len(), 3 * 64 * 64);
2275    }
2276}