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