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