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