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