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 set_libver_latest(&self, latest: bool) -> Result<()> {
185 let mut inner = borrow_inner_mut(&self.inner);
186 match &mut *inner {
187 H5FileInner::Writer(writer) => {
188 writer.set_libver_latest(latest);
189 Ok(())
190 }
191 _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
192 }
193 }
194
195 pub fn root_group(&self) -> H5Group {
199 H5Group::new(clone_inner(&self.inner), "/".to_string())
200 }
201
202 pub fn create_group(&self, name: &str) -> Result<H5Group> {
210 self.root_group().create_group(name)
211 }
212
213 pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
224 DatasetBuilder::new(clone_inner(&self.inner))
225 }
226
227 pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
232 let inner = borrow_inner(&self.inner);
233 match &*inner {
234 H5FileInner::Writer(writer) => {
235 writer.set_vlen_string_attribute(
236 crate::io::writer::AttrTarget::Root,
237 name,
238 value,
239 )?;
240 Ok(())
241 }
242 _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
243 }
244 }
245
246 pub fn set_attr_numeric<T: crate::types::H5Type>(&self, name: &str, value: &T) -> Result<()> {
248 use crate::format::messages::attribute::AttributeMessage;
249 let es = T::element_size();
250 let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
251 let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), raw.to_vec());
252 let inner = borrow_inner(&self.inner);
253 match &*inner {
254 H5FileInner::Writer(writer) => {
255 writer.add_root_attribute(attr)?;
256 Ok(())
257 }
258 _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
259 }
260 }
261
262 pub fn set_attr_array_numeric<T: crate::types::H5Type>(
270 &self,
271 name: &str,
272 values: &[T],
273 ) -> Result<()> {
274 self.set_attr_array_numeric_nd(name, values, &[values.len()])
275 }
276
277 pub fn set_attr_array_numeric_nd<T: crate::types::H5Type>(
286 &self,
287 name: &str,
288 values: &[T],
289 shape: &[usize],
290 ) -> Result<()> {
291 use crate::format::messages::attribute::AttributeMessage;
292 let n: usize = shape.iter().product();
293 if values.len() != n {
294 return Err(Hdf5Error::InvalidState(format!(
295 "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
296 values.len()
297 )));
298 }
299 let es = T::element_size();
300 let raw =
302 unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
303 let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
304 let attr = AttributeMessage::array_numeric(name, T::hdf5_type(), &dims, raw.to_vec());
305 let mut inner = borrow_inner_mut(&self.inner);
306 match &mut *inner {
307 H5FileInner::Writer(writer) => {
308 writer.add_root_attribute(attr)?;
309 Ok(())
310 }
311 _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
312 }
313 }
314
315 pub fn set_attr_string_array(&self, name: &str, values: &[&str]) -> Result<()> {
320 self.set_attr_string_array_nd(name, values, &[values.len()])
321 }
322
323 pub fn set_attr_string_array_nd(
333 &self,
334 name: &str,
335 values: &[&str],
336 shape: &[usize],
337 ) -> Result<()> {
338 let n: usize = shape.iter().product();
339 if values.len() != n {
340 return Err(Hdf5Error::InvalidState(format!(
341 "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
342 values.len()
343 )));
344 }
345 let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
346 let mut inner = borrow_inner_mut(&self.inner);
347 match &mut *inner {
348 H5FileInner::Writer(writer) => {
349 writer.set_vlen_string_array_attribute(
350 crate::io::writer::AttrTarget::Root,
351 name,
352 values,
353 &dims,
354 )?;
355 Ok(())
356 }
357 _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
358 }
359 }
360
361 pub fn attr_names(&self) -> Result<Vec<String>> {
363 let inner = borrow_inner(&self.inner);
364 match &*inner {
365 H5FileInner::Reader(reader) => Ok(reader.root_attr_names()),
366 _ => Ok(vec![]),
367 }
368 }
369
370 pub fn attr_string(&self, name: &str) -> Result<String> {
372 let mut inner = borrow_inner_mut(&self.inner);
373 match &mut *inner {
374 H5FileInner::Reader(reader) => {
375 let attr = reader
376 .root_attr(name)
377 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
378 .clone();
379 Ok(reader.attr_string_value(&attr)?)
380 }
381 _ => Err(Hdf5Error::InvalidState("not in read mode".into())),
382 }
383 }
384
385 pub fn is_writable(&self) -> bool {
387 let inner = borrow_inner(&self.inner);
388 matches!(&*inner, H5FileInner::Writer(_))
389 }
390
391 pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
396 let inner = borrow_inner(&self.inner);
397 match &*inner {
398 H5FileInner::Writer(writer) => {
399 let idx = writer.create_vlen_string_dataset(name, strings)?;
400 if let Some(slash_pos) = name.rfind('/') {
402 let group_path = &name[..slash_pos];
403 let abs_group_path = if group_path.starts_with('/') {
404 group_path.to_string()
405 } else {
406 format!("/{}", group_path)
407 };
408 writer.assign_dataset_to_group(&abs_group_path, idx)?;
409 }
410 let (shape, element_size, chunked, btree2, fixed_array) =
411 writer.dataset_handle_parts(idx);
412 Ok(H5Dataset::new_writer(
413 clone_inner(&self.inner),
414 idx,
415 shape,
416 element_size,
417 chunked,
418 btree2,
419 fixed_array,
420 ))
421 }
422 H5FileInner::Reader(_) => {
423 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
424 }
425 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
426 }
427 }
428
429 pub fn write_vlen_bytes(&self, name: &str, items: &[&[u8]]) -> Result<H5Dataset> {
436 let inner = borrow_inner(&self.inner);
437 match &*inner {
438 H5FileInner::Writer(writer) => {
439 let idx = writer.create_vlen_bytes_dataset(name, items)?;
440 if let Some(slash_pos) = name.rfind('/') {
442 let group_path = &name[..slash_pos];
443 let abs_group_path = if group_path.starts_with('/') {
444 group_path.to_string()
445 } else {
446 format!("/{}", group_path)
447 };
448 writer.assign_dataset_to_group(&abs_group_path, idx)?;
449 }
450 let (shape, element_size, chunked, btree2, fixed_array) =
451 writer.dataset_handle_parts(idx);
452 Ok(H5Dataset::new_writer(
453 clone_inner(&self.inner),
454 idx,
455 shape,
456 element_size,
457 chunked,
458 btree2,
459 fixed_array,
460 ))
461 }
462 H5FileInner::Reader(_) => {
463 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
464 }
465 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
466 }
467 }
468
469 pub fn write_vlen_strings_compressed(
476 &self,
477 name: &str,
478 strings: &[&str],
479 chunk_size: usize,
480 pipeline: FilterPipeline,
481 ) -> Result<H5Dataset> {
482 let inner = borrow_inner(&self.inner);
483 match &*inner {
484 H5FileInner::Writer(writer) => {
485 let idx = writer
486 .create_vlen_string_dataset_compressed(name, strings, chunk_size, pipeline)?;
487 if let Some(slash_pos) = name.rfind('/') {
488 let group_path = &name[..slash_pos];
489 let abs_group_path = if group_path.starts_with('/') {
490 group_path.to_string()
491 } else {
492 format!("/{}", group_path)
493 };
494 writer.assign_dataset_to_group(&abs_group_path, idx)?;
495 }
496 let (shape, element_size, chunked, btree2, fixed_array) =
497 writer.dataset_handle_parts(idx);
498 Ok(H5Dataset::new_writer(
499 clone_inner(&self.inner),
500 idx,
501 shape,
502 element_size,
503 chunked,
504 btree2,
505 fixed_array,
506 ))
507 }
508 H5FileInner::Reader(_) => {
509 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
510 }
511 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
512 }
513 }
514
515 pub fn create_appendable_vlen_dataset(
520 &self,
521 name: &str,
522 chunk_size: usize,
523 pipeline: Option<FilterPipeline>,
524 ) -> Result<H5Dataset> {
525 let inner = borrow_inner(&self.inner);
526 match &*inner {
527 H5FileInner::Writer(writer) => {
528 let idx =
529 writer.create_appendable_vlen_string_dataset(name, chunk_size, pipeline)?;
530 if let Some(slash_pos) = name.rfind('/') {
531 let group_path = &name[..slash_pos];
532 let abs_group_path = if group_path.starts_with('/') {
533 group_path.to_string()
534 } else {
535 format!("/{}", group_path)
536 };
537 writer.assign_dataset_to_group(&abs_group_path, idx)?;
538 }
539 let (shape, element_size, chunked, btree2, fixed_array) =
540 writer.dataset_handle_parts(idx);
541 Ok(H5Dataset::new_writer(
542 clone_inner(&self.inner),
543 idx,
544 shape,
545 element_size,
546 chunked,
547 btree2,
548 fixed_array,
549 ))
550 }
551 H5FileInner::Reader(_) => {
552 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
553 }
554 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
555 }
556 }
557
558 pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
560 let inner = borrow_inner(&self.inner);
561 match &*inner {
562 H5FileInner::Writer(writer) => {
563 let ds_index = writer
564 .dataset_index(name)
565 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
566 writer.append_vlen_strings(ds_index, strings)?;
567 Ok(())
568 }
569 H5FileInner::Reader(_) => {
570 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
571 }
572 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
573 }
574 }
575
576 pub fn delete_dataset(&self, name: &str) -> Result<()> {
585 let inner = borrow_inner(&self.inner);
586 match &*inner {
587 H5FileInner::Writer(writer) => {
588 writer.delete_dataset(name)?;
589 Ok(())
590 }
591 _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
592 }
593 }
594
595 pub fn delete_group(&self, name: &str) -> Result<()> {
604 let inner = borrow_inner(&self.inner);
605 match &*inner {
606 H5FileInner::Writer(writer) => {
607 writer.delete_group(name)?;
608 Ok(())
609 }
610 _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
611 }
612 }
613
614 pub fn dataset(&self, name: &str) -> Result<H5Dataset> {
616 let inner = borrow_inner(&self.inner);
617 match &*inner {
618 H5FileInner::Reader(reader) => {
619 let info = reader
620 .dataset_info(name)
621 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
622 let shape: Vec<usize> = info.dataspace.dims.iter().map(|&d| d as usize).collect();
623 let element_size = info.datatype.element_size() as usize;
624 Ok(H5Dataset::new_reader(
625 clone_inner(&self.inner),
626 name.to_string(),
627 shape,
628 element_size,
629 ))
630 }
631 H5FileInner::Writer(_) => Err(Hdf5Error::InvalidState(
632 "cannot open a dataset by name in write mode; use new_dataset() instead"
633 .to_string(),
634 )),
635 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
636 }
637 }
638
639 pub fn dataset_writer(&self, name: &str) -> Result<H5Dataset> {
653 let inner = borrow_inner(&self.inner);
654 match &*inner {
655 H5FileInner::Writer(writer) => {
656 let index = writer
657 .dataset_index(name)
658 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
659 let (shape, element_size, chunked, btree2, fixed_array) =
660 writer.dataset_handle_parts(index);
661 Ok(H5Dataset::new_writer(
662 clone_inner(&self.inner),
663 index,
664 shape,
665 element_size,
666 chunked,
667 btree2,
668 fixed_array,
669 ))
670 }
671 H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
672 "cannot open a dataset_writer in read mode; use dataset() instead".to_string(),
673 )),
674 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
675 }
676 }
677
678 pub fn dataset_names(&self) -> Vec<String> {
684 let inner = borrow_inner(&self.inner);
685 match &*inner {
686 H5FileInner::Reader(reader) => reader
687 .dataset_names()
688 .iter()
689 .map(|s| s.to_string())
690 .collect(),
691 H5FileInner::Writer(writer) => writer
692 .dataset_names()
693 .iter()
694 .map(|s| s.to_string())
695 .collect(),
696 H5FileInner::Closed => Vec::new(),
697 }
698 }
699
700 pub fn close(self) -> Result<()> {
706 let old = {
707 let mut inner = borrow_inner_mut(&self.inner);
708 std::mem::replace(&mut *inner, H5FileInner::Closed)
709 };
710 match old {
711 H5FileInner::Writer(writer) => {
712 writer.close()?;
713 Ok(())
714 }
715 H5FileInner::Reader(_) => Ok(()),
716 H5FileInner::Closed => Ok(()),
717 }
718 }
719
720 pub fn close_no_sync(self) -> Result<()> {
736 let old = {
737 let mut inner = borrow_inner_mut(&self.inner);
738 std::mem::replace(&mut *inner, H5FileInner::Closed)
739 };
740 match old {
741 H5FileInner::Writer(writer) => {
742 writer.close_no_sync()?;
743 Ok(())
744 }
745 H5FileInner::Reader(_) => Ok(()),
746 H5FileInner::Closed => Ok(()),
747 }
748 }
749
750 pub fn flush(&self) -> Result<()> {
752 Ok(())
756 }
757}
758
759#[derive(Debug, Default, Clone)]
766pub struct H5FileOptions {
767 locking: Option<FileLocking>,
768}
769
770impl H5FileOptions {
771 pub fn new() -> Self {
773 Self::default()
774 }
775
776 pub fn locking(mut self, policy: FileLocking) -> Self {
779 self.locking = Some(policy);
780 self
781 }
782
783 pub fn no_locking(self) -> Self {
786 self.locking(FileLocking::Disabled)
787 }
788
789 pub fn best_effort_locking(self) -> Self {
792 self.locking(FileLocking::BestEffort)
793 }
794
795 fn resolved_locking(&self) -> FileLocking {
796 match self.locking {
797 Some(p) => p,
798 None => FileLocking::from_env_or(FileLocking::default()),
799 }
800 }
801
802 pub fn create<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
804 let writer = Hdf5Writer::create_with_locking(path.as_ref(), self.resolved_locking())?;
805 Ok(H5File {
806 inner: new_shared(H5FileInner::Writer(writer)),
807 })
808 }
809
810 pub fn open<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
812 let reader = Hdf5Reader::open_with_locking(path.as_ref(), self.resolved_locking())?;
813 Ok(H5File {
814 inner: new_shared(H5FileInner::Reader(reader)),
815 })
816 }
817
818 pub fn open_rw<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
820 let writer = Hdf5Writer::open_append_with_locking(path.as_ref(), self.resolved_locking())?;
821 Ok(H5File {
822 inner: new_shared(H5FileInner::Writer(writer)),
823 })
824 }
825}
826
827#[cfg(test)]
828fn unique_test_path(name: &str) -> std::path::PathBuf {
829 use std::sync::atomic::{AtomicU64, Ordering};
834 static COUNTER: AtomicU64 = AtomicU64::new(0);
835 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
836 std::env::temp_dir().join(format!(
837 "rust_hdf5_test_{}_{}_{}.h5",
838 name,
839 std::process::id(),
840 n
841 ))
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847 use std::path::PathBuf;
848
849 fn temp_path(name: &str) -> PathBuf {
850 super::unique_test_path(name)
851 }
852
853 #[test]
854 fn create_and_close_empty() {
855 let path = temp_path("create_empty");
856 let file = H5File::create(&path).unwrap();
857 file.close().unwrap();
858
859 let file = H5File::open(&path).unwrap();
861 file.close().unwrap();
862
863 std::fs::remove_file(&path).ok();
864 }
865
866 #[test]
867 fn create_and_drop_empty() {
868 let path = temp_path("drop_empty");
869 {
870 let _file = H5File::create(&path).unwrap();
871 }
873 let file = H5File::open(&path).unwrap();
875 file.close().unwrap();
876
877 std::fs::remove_file(&path).ok();
878 }
879
880 #[test]
881 fn dataset_not_found() {
882 let path = temp_path("ds_not_found");
883 {
884 let _file = H5File::create(&path).unwrap();
885 }
886 let file = H5File::open(&path).unwrap();
887 let result = file.dataset("nonexistent");
888 assert!(result.is_err());
889
890 std::fs::remove_file(&path).ok();
891 }
892
893 #[test]
894 fn write_and_read_roundtrip() {
895 let path = temp_path("write_read_rt");
896
897 {
899 let file = H5File::create(&path).unwrap();
900 let ds = file
901 .new_dataset::<u8>()
902 .shape([4, 4])
903 .create("data")
904 .unwrap();
905 ds.write_raw(&[0u8; 16]).unwrap();
906 file.close().unwrap();
907 }
908
909 {
911 let file = H5File::open(&path).unwrap();
912 let ds = file.dataset("data").unwrap();
913 assert_eq!(ds.shape(), vec![4, 4]);
914 let data = ds.read_raw::<u8>().unwrap();
915 assert_eq!(data.len(), 16);
916 assert!(data.iter().all(|&b| b == 0));
917 file.close().unwrap();
918 }
919
920 std::fs::remove_file(&path).ok();
921 }
922
923 #[test]
924 fn close_no_sync_produces_valid_readable_file() {
925 let path = temp_path("close_no_sync_rt");
926 let payload: Vec<u8> = (0u8..16).collect();
927
928 {
930 let file = H5File::create(&path).unwrap();
931 let ds = file
932 .new_dataset::<u8>()
933 .shape([4, 4])
934 .create("data")
935 .unwrap();
936 ds.write_raw(&payload).unwrap();
937 file.close_no_sync().unwrap();
940 }
941
942 {
945 let file = H5File::open(&path).unwrap();
946 let ds = file.dataset("data").unwrap();
947 assert_eq!(ds.shape(), vec![4, 4]);
948 let data = ds.read_raw::<u8>().unwrap();
949 assert_eq!(data, payload);
950 file.close().unwrap();
951 }
952
953 std::fs::remove_file(&path).ok();
954 }
955
956 #[test]
957 fn create_over_existing_file_truncates() {
958 let path = temp_path("create_truncates");
964
965 {
966 let file = H5File::create(&path).unwrap();
967 let ds = file
968 .new_dataset::<u8>()
969 .shape([4, 4])
970 .create("old_data")
971 .unwrap();
972 ds.write_raw(&[7u8; 16]).unwrap();
973 file.close().unwrap();
974 }
975 assert!(std::fs::metadata(&path).unwrap().len() > 0);
976
977 {
979 let file = H5File::create(&path).unwrap();
980 file.close().unwrap();
981 }
982
983 let file = H5File::open(&path).unwrap();
985 assert!(file.dataset("old_data").is_err());
986 file.close().unwrap();
987
988 std::fs::remove_file(&path).ok();
989 }
990
991 #[test]
992 fn close_no_sync_chunked_dataset_valid() {
993 let path = temp_path("close_no_sync_chunked");
998
999 {
1000 let file = H5File::create(&path).unwrap();
1001 let ds = file
1002 .new_dataset::<i32>()
1003 .shape([0usize, 3])
1004 .chunk(&[1, 3])
1005 .max_shape(&[None, Some(3)])
1006 .create("data")
1007 .unwrap();
1008 for frame in 0..10u64 {
1010 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1011 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1012 ds.write_chunk(frame as usize, &raw).unwrap();
1013 }
1014 ds.extend(&[10, 3]).unwrap();
1015 file.close_no_sync().unwrap();
1016 }
1017
1018 {
1019 let file = H5File::open(&path).unwrap();
1020 let ds = file.dataset("data").unwrap();
1021 assert_eq!(ds.shape(), vec![10, 3]);
1022 let data = ds.read_raw::<i32>().unwrap();
1023 let expected: Vec<i32> = (0..30).collect();
1024 assert_eq!(data, expected);
1025 file.close().unwrap();
1026 }
1027
1028 std::fs::remove_file(&path).ok();
1029 }
1030
1031 #[test]
1032 fn write_and_read_f64() {
1033 let path = temp_path("write_read_f64");
1034
1035 let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1036
1037 {
1039 let file = H5File::create(&path).unwrap();
1040 let ds = file
1041 .new_dataset::<f64>()
1042 .shape([2, 3])
1043 .create("matrix")
1044 .unwrap();
1045 ds.write_raw(&values).unwrap();
1046 file.close().unwrap();
1047 }
1048
1049 {
1051 let file = H5File::open(&path).unwrap();
1052 let ds = file.dataset("matrix").unwrap();
1053 assert_eq!(ds.shape(), vec![2, 3]);
1054 let readback = ds.read_raw::<f64>().unwrap();
1055 assert_eq!(readback, values);
1056 }
1057
1058 std::fs::remove_file(&path).ok();
1059 }
1060
1061 #[test]
1062 fn multiple_datasets() {
1063 let path = temp_path("multi_ds");
1064
1065 {
1066 let file = H5File::create(&path).unwrap();
1067 let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
1068 ds1.write_raw(&[10i32, 20, 30]).unwrap();
1069
1070 let ds2 = file
1071 .new_dataset::<f32>()
1072 .shape([2, 2])
1073 .create("floats")
1074 .unwrap();
1075 ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
1076
1077 file.close().unwrap();
1078 }
1079
1080 {
1081 let file = H5File::open(&path).unwrap();
1082
1083 let ds_ints = file.dataset("ints").unwrap();
1084 assert_eq!(ds_ints.shape(), vec![3]);
1085 let ints = ds_ints.read_raw::<i32>().unwrap();
1086 assert_eq!(ints, vec![10, 20, 30]);
1087
1088 let ds_floats = file.dataset("floats").unwrap();
1089 assert_eq!(ds_floats.shape(), vec![2, 2]);
1090 let floats = ds_floats.read_raw::<f32>().unwrap();
1091 assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
1092 }
1093
1094 std::fs::remove_file(&path).ok();
1095 }
1096
1097 #[test]
1098 fn close_is_idempotent() {
1099 let path = temp_path("close_idemp");
1100 let file = H5File::create(&path).unwrap();
1101 file.close().unwrap();
1102 std::fs::remove_file(&path).ok();
1104 }
1105}
1106
1107#[cfg(test)]
1108mod integration_tests {
1109 use super::*;
1110
1111 fn temp_path(name: &str) -> std::path::PathBuf {
1112 super::unique_test_path(name)
1113 }
1114
1115 #[test]
1116 fn write_file_for_h5dump() {
1117 let path = temp_path("integration");
1118 let file = H5File::create(&path).unwrap();
1119
1120 let ds = file
1121 .new_dataset::<u8>()
1122 .shape([4usize, 4])
1123 .create("data_u8")
1124 .unwrap();
1125 let data: Vec<u8> = (0..16).collect();
1126 ds.write_raw(&data).unwrap();
1127
1128 let ds2 = file
1129 .new_dataset::<f64>()
1130 .shape([3usize, 2])
1131 .create("data_f64")
1132 .unwrap();
1133 let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1134 ds2.write_raw(&fdata).unwrap();
1135
1136 let ds3 = file
1137 .new_dataset::<i32>()
1138 .shape([5usize])
1139 .create("values")
1140 .unwrap();
1141 let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
1142 ds3.write_raw(&idata).unwrap();
1143
1144 file.close().unwrap();
1145
1146 assert!(path.exists());
1148 }
1149
1150 #[test]
1151 fn write_chunked_file_for_h5dump() {
1152 let path = temp_path("chunked");
1153 let file = H5File::create(&path).unwrap();
1154
1155 let ds = file
1157 .new_dataset::<f64>()
1158 .shape([0usize, 4])
1159 .chunk(&[1, 4])
1160 .max_shape(&[None, Some(4)])
1161 .create("streaming_data")
1162 .unwrap();
1163
1164 for frame in 0..5u64 {
1166 let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1167 let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1168 ds.write_chunk(frame as usize, &raw).unwrap();
1169 }
1170
1171 ds.extend(&[5, 4]).unwrap();
1173 ds.flush().unwrap();
1174
1175 file.close().unwrap();
1176
1177 assert!(path.exists());
1178 }
1179
1180 #[test]
1181 fn write_chunked_many_frames_for_h5dump() {
1182 let path = temp_path("chunked_many");
1183 let file = H5File::create(&path).unwrap();
1184
1185 let ds = file
1186 .new_dataset::<i32>()
1187 .shape([0usize, 3])
1188 .chunk(&[1, 3])
1189 .max_shape(&[None, Some(3)])
1190 .create("data")
1191 .unwrap();
1192
1193 for frame in 0..10u64 {
1195 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1196 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1197 ds.write_chunk(frame as usize, &raw).unwrap();
1198 }
1199 ds.extend(&[10, 3]).unwrap();
1200 file.close().unwrap();
1201
1202 assert!(path.exists());
1203 }
1204
1205 #[test]
1206 fn write_dataset_with_attributes() {
1207 use crate::types::VarLenUnicode;
1208
1209 let path = temp_path("attributes");
1210 let file = H5File::create(&path).unwrap();
1211
1212 let ds = file
1213 .new_dataset::<f32>()
1214 .shape([10usize])
1215 .create("temperature")
1216 .unwrap();
1217 let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
1218 ds.write_raw(&data).unwrap();
1219
1220 let attr = ds
1222 .new_attr::<VarLenUnicode>()
1223 .shape(())
1224 .create("units")
1225 .unwrap();
1226 attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
1227 .unwrap();
1228
1229 let attr2 = ds
1230 .new_attr::<VarLenUnicode>()
1231 .shape(())
1232 .create("description")
1233 .unwrap();
1234 attr2
1235 .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
1236 .unwrap();
1237
1238 let attr3 = ds
1240 .new_attr::<VarLenUnicode>()
1241 .shape(())
1242 .create("source")
1243 .unwrap();
1244 attr3.write_string("sensor_01").unwrap();
1245
1246 let attr4 = ds
1248 .new_attr::<VarLenUnicode>()
1249 .shape(())
1250 .create("label")
1251 .unwrap();
1252 let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
1253 attr4.write_scalar(&s).unwrap();
1254
1255 file.close().unwrap();
1256
1257 assert!(path.exists());
1258 }
1259
1260 #[test]
1261 fn dataset_writer_reopens_for_attributes() {
1262 let path = temp_path("dataset_writer");
1265 {
1266 let file = H5File::create(&path).unwrap();
1267 {
1268 let ds = file
1269 .new_dataset::<u16>()
1270 .shape([8])
1271 .create("image")
1272 .unwrap();
1273 ds.write_raw(&[0u16; 8]).unwrap();
1274 }
1276
1277 assert!(file.dataset("image").is_err());
1279 let ds = file.dataset_writer("image").unwrap();
1280 assert_eq!(ds.shape(), vec![8]);
1281 ds.new_attr::<i32>()
1282 .shape([3])
1283 .create("NDArrayDimOffset")
1284 .unwrap()
1285 .write_array(&[0i32, 4, 8])
1286 .unwrap();
1287 ds.new_attr::<i32>()
1288 .shape(())
1289 .create("NDUniqueId")
1290 .unwrap()
1291 .write_numeric(&42i32)
1292 .unwrap();
1293
1294 assert!(matches!(
1296 file.dataset_writer("nope"),
1297 Err(crate::error::Hdf5Error::NotFound(_))
1298 ));
1299
1300 file.close().unwrap();
1301 }
1302 {
1303 let file = H5File::open(&path).unwrap();
1304 let ds = file.dataset("image").unwrap();
1305 let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
1306 let got: Vec<i32> = off
1307 .chunks_exact(4)
1308 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1309 .collect();
1310 assert_eq!(got, vec![0, 4, 8]);
1311 let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
1312 assert_eq!(uid, 42);
1313 }
1314 std::fs::remove_file(&path).ok();
1315 }
1316
1317 #[test]
1318 fn chunked_write_read_roundtrip() {
1319 let path = temp_path("chunked_roundtrip");
1320
1321 {
1323 let file = H5File::create(&path).unwrap();
1324 let ds = file
1325 .new_dataset::<i32>()
1326 .shape([0usize, 3])
1327 .chunk(&[1, 3])
1328 .max_shape(&[None, Some(3)])
1329 .create("table")
1330 .unwrap();
1331
1332 for frame in 0..8u64 {
1333 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1334 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1335 ds.write_chunk(frame as usize, &raw).unwrap();
1336 }
1337 ds.extend(&[8, 3]).unwrap();
1338 file.close().unwrap();
1339 }
1340
1341 {
1343 let file = H5File::open(&path).unwrap();
1344 let ds = file.dataset("table").unwrap();
1345 assert_eq!(ds.shape(), vec![8, 3]);
1346 let data = ds.read_raw::<i32>().unwrap();
1347 assert_eq!(data.len(), 24);
1348 for (i, val) in data.iter().enumerate() {
1349 assert_eq!(*val, i as i32);
1350 }
1351 }
1352
1353 std::fs::remove_file(&path).ok();
1354 }
1355
1356 #[test]
1357 #[cfg(feature = "deflate")]
1358 fn compressed_chunked_roundtrip() {
1359 let path = temp_path("compressed_roundtrip");
1360
1361 {
1363 let file = H5File::create(&path).unwrap();
1364 let ds = file
1365 .new_dataset::<f64>()
1366 .shape([0usize, 4])
1367 .chunk(&[1, 4])
1368 .max_shape(&[None, Some(4)])
1369 .deflate(6)
1370 .create("compressed")
1371 .unwrap();
1372
1373 for frame in 0..10u64 {
1374 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1375 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1376 ds.write_chunk(frame as usize, &raw).unwrap();
1377 }
1378 ds.extend(&[10, 4]).unwrap();
1379 file.close().unwrap();
1380 }
1381
1382 {
1384 let file = H5File::open(&path).unwrap();
1385 let ds = file.dataset("compressed").unwrap();
1386 assert_eq!(ds.shape(), vec![10, 4]);
1387 let data = ds.read_raw::<f64>().unwrap();
1388 assert_eq!(data.len(), 40);
1389 for (i, val) in data.iter().enumerate() {
1390 assert!(
1391 (val - i as f64).abs() < 1e-10,
1392 "mismatch at {}: {} != {}",
1393 i,
1394 val,
1395 i
1396 );
1397 }
1398 }
1399
1400 std::fs::remove_file(&path).ok();
1401 }
1402
1403 #[test]
1404 #[cfg(feature = "deflate")]
1405 fn compressed_chunked_many_frames() {
1406 let path = temp_path("compressed_many");
1407
1408 {
1409 let file = H5File::create(&path).unwrap();
1410 let ds = file
1411 .new_dataset::<i32>()
1412 .shape([0usize, 3])
1413 .chunk(&[1, 3])
1414 .max_shape(&[None, Some(3)])
1415 .deflate(6)
1416 .create("stream")
1417 .unwrap();
1418
1419 for frame in 0..100u64 {
1420 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1421 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1422 ds.write_chunk(frame as usize, &raw).unwrap();
1423 }
1424 ds.extend(&[100, 3]).unwrap();
1425 file.close().unwrap();
1426 }
1427
1428 {
1429 let file = H5File::open(&path).unwrap();
1430 let ds = file.dataset("stream").unwrap();
1431 assert_eq!(ds.shape(), vec![100, 3]);
1432 let data = ds.read_raw::<i32>().unwrap();
1433 assert_eq!(data.len(), 300);
1434 for (i, val) in data.iter().enumerate() {
1435 assert_eq!(*val, i as i32, "mismatch at {}", i);
1436 }
1437 }
1438
1439 std::fs::remove_file(&path).ok();
1440 }
1441 #[test]
1442 fn append_mode() {
1443 let path = temp_path("append");
1444
1445 {
1447 let file = H5File::create(&path).unwrap();
1448 let ds = file
1449 .new_dataset::<i32>()
1450 .shape([3usize])
1451 .create("first")
1452 .unwrap();
1453 ds.write_raw(&[1i32, 2, 3]).unwrap();
1454 file.close().unwrap();
1455 }
1456
1457 {
1459 let file = H5File::open_rw(&path).unwrap();
1460 let ds = file
1461 .new_dataset::<f64>()
1462 .shape([2usize])
1463 .create("second")
1464 .unwrap();
1465 ds.write_raw(&[4.0f64, 5.0]).unwrap();
1466 file.close().unwrap();
1467 }
1468
1469 {
1471 let file = H5File::open(&path).unwrap();
1472 let names = file.dataset_names();
1473 assert!(names.contains(&"first".to_string()));
1474 assert!(names.contains(&"second".to_string()));
1475
1476 let ds1 = file.dataset("first").unwrap();
1477 assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
1478
1479 let ds2 = file.dataset("second").unwrap();
1480 assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
1481 }
1482
1483 std::fs::remove_file(&path).ok();
1484 }
1485
1486 #[test]
1487 fn open_rw_set_attr_preserves_file() {
1488 let path = temp_path("open_rw_attr");
1489 {
1491 let file = H5File::create(&path).unwrap();
1492 let ds = file
1493 .new_dataset::<i32>()
1494 .shape([3usize])
1495 .create("data")
1496 .unwrap();
1497 ds.write_raw(&[10i32, 20, 30]).unwrap();
1498 file.set_attr_string("version", "1.0").unwrap();
1499 file.close().unwrap();
1500 }
1501 {
1503 let file = H5File::open_rw(&path).unwrap();
1504 file.set_attr_string("version", "2.0").unwrap();
1505 file.close().unwrap();
1506 }
1507 {
1509 let file = H5File::open(&path).unwrap();
1510 let ds = file.dataset("data").unwrap();
1511 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
1512 let ver = file.attr_string("version").unwrap();
1513 assert_eq!(ver, "2.0");
1514 }
1515 std::fs::remove_file(&path).ok();
1516 }
1517
1518 #[test]
1519 #[cfg(feature = "deflate")]
1520 fn open_rw_attr_with_compressed_dataset() {
1521 use crate::format::messages::filter::FilterPipeline;
1522 let path = temp_path("open_rw_compressed");
1523 let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
1524 {
1526 let file = H5File::create(&path).unwrap();
1527 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1528 .unwrap();
1529 file.set_attr_string("version", "1.0").unwrap();
1530 file.close().unwrap();
1531 }
1532 {
1534 let file = H5File::open_rw(&path).unwrap();
1535 file.set_attr_string("version", "2.0").unwrap();
1536 file.close().unwrap();
1537 }
1538 {
1540 let file = H5File::open(&path).unwrap();
1541 let ds = file.dataset("texts").unwrap();
1542 let strings = ds.read_vlen_strings().unwrap();
1543 assert_eq!(strings.len(), 50);
1544 assert_eq!(strings[0], "test string data");
1545 let ver = file.attr_string("version").unwrap();
1546 assert_eq!(ver, "2.0");
1547 }
1548 std::fs::remove_file(&path).ok();
1549 }
1550
1551 #[test]
1552 #[cfg(feature = "lz4")]
1553 fn append_vlen_strings_basic() {
1554 use crate::format::messages::filter::FilterPipeline;
1555 let path = temp_path("append_vlen");
1556 {
1557 let file = H5File::create(&path).unwrap();
1558 file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
1559 .unwrap();
1560 file.append_vlen_strings("names", &["alice", "bob", "charlie"])
1561 .unwrap();
1562 file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
1563 file.close().unwrap();
1564 }
1565 {
1566 let file = H5File::open(&path).unwrap();
1567 let ds = file.dataset("names").unwrap();
1568 let strings = ds.read_vlen_strings().unwrap();
1569 assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
1570 }
1571 std::fs::remove_file(&path).ok();
1572 }
1573
1574 #[test]
1575 #[cfg(feature = "lz4")]
1576 fn append_vlen_strings_large() {
1577 use crate::format::messages::filter::FilterPipeline;
1578 let path = temp_path("append_vlen_large");
1579 let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
1580 let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
1581 {
1582 let file = H5File::create(&path).unwrap();
1583 file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
1584 .unwrap();
1585 let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
1586 file.append_vlen_strings("data", &r1).unwrap();
1587 let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
1588 file.append_vlen_strings("data", &r2).unwrap();
1589 file.close().unwrap();
1590 }
1591 {
1592 let file = H5File::open(&path).unwrap();
1593 let ds = file.dataset("data").unwrap();
1594 let strings = ds.read_vlen_strings().unwrap();
1595 assert_eq!(strings.len(), 7189);
1596 assert_eq!(strings[0], "node-000000");
1597 assert_eq!(strings[7188], "node-007188");
1598 }
1599 std::fs::remove_file(&path).ok();
1600 }
1601
1602 #[test]
1603 fn append_vlen_strings_uncompressed() {
1604 let path = temp_path("append_vlen_unc");
1605 {
1606 let file = H5File::create(&path).unwrap();
1607 file.create_appendable_vlen_dataset("texts", 8, None)
1608 .unwrap();
1609 file.append_vlen_strings("texts", &["hello", "world"])
1610 .unwrap();
1611 file.append_vlen_strings("texts", &["foo", "bar", "baz"])
1612 .unwrap();
1613 file.close().unwrap();
1614 }
1615 {
1616 let file = H5File::open(&path).unwrap();
1617 let ds = file.dataset("texts").unwrap();
1618 let strings = ds.read_vlen_strings().unwrap();
1619 assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
1620 }
1621 std::fs::remove_file(&path).ok();
1622 }
1623
1624 #[test]
1625 fn delete_dataset_roundtrip() {
1626 let path = temp_path("delete_ds");
1627 {
1628 let file = H5File::create(&path).unwrap();
1629 file.write_vlen_strings("keep", &["a", "b"]).unwrap();
1630 file.write_vlen_strings("remove", &["x", "y"]).unwrap();
1631 file.delete_dataset("remove").unwrap();
1632 file.close().unwrap();
1633 }
1634 {
1635 let file = H5File::open(&path).unwrap();
1636 let names = file.dataset_names();
1637 assert!(names.contains(&"keep".to_string()));
1638 assert!(!names.contains(&"remove".to_string()));
1639 let ds = file.dataset("keep").unwrap();
1640 assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
1641 }
1642 std::fs::remove_file(&path).ok();
1643 }
1644
1645 #[test]
1646 fn delete_group_roundtrip() {
1647 let path = temp_path("delete_grp");
1648 {
1649 let file = H5File::create(&path).unwrap();
1650 let g1 = file.create_group("keep").unwrap();
1651 g1.write_vlen_strings("data", &["a"]).unwrap();
1652 let g2 = file.create_group("remove").unwrap();
1653 g2.write_vlen_strings("data", &["x"]).unwrap();
1654 file.delete_group("remove").unwrap();
1655 file.close().unwrap();
1656 }
1657 {
1658 let file = H5File::open(&path).unwrap();
1659 let names = file.dataset_names();
1660 assert!(names.contains(&"keep/data".to_string()));
1661 assert!(!names.contains(&"remove/data".to_string()));
1662 }
1663 std::fs::remove_file(&path).ok();
1664 }
1665
1666 #[test]
1667 fn open_rw_delete_recreate_group() {
1668 let path = temp_path("rw_delete_recreate");
1669 {
1671 let file = H5File::create(&path).unwrap();
1672 let n = file.create_group("nodes").unwrap();
1673 n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
1674 let e = file.create_group("edges").unwrap();
1675 e.write_vlen_strings("src", &["x", "y"]).unwrap();
1676 file.close().unwrap();
1677 }
1678 {
1680 let file = H5File::open_rw(&path).unwrap();
1681 file.delete_group("nodes").unwrap();
1682 let n = file.create_group("nodes").unwrap();
1683 n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
1684 file.close().unwrap();
1685 }
1686 {
1688 let file = H5File::open(&path).unwrap();
1689 let ds = file.dataset("nodes/id").unwrap();
1690 let s = ds.read_vlen_strings().unwrap();
1691 assert_eq!(s, vec!["new1", "new2"]);
1692 let ds = file.dataset("edges/src").unwrap();
1694 let s = ds.read_vlen_strings().unwrap();
1695 assert_eq!(s, vec!["x", "y"]);
1696 }
1697 std::fs::remove_file(&path).ok();
1698 }
1699
1700 #[test]
1701 fn delete_and_recreate_group() {
1702 let path = temp_path("delete_recreate");
1703 {
1704 let file = H5File::create(&path).unwrap();
1705 let g = file.create_group("nodes").unwrap();
1706 g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
1707 file.delete_group("nodes").unwrap();
1708 let g = file.create_group("nodes").unwrap();
1709 g.write_vlen_strings("id", &["new1", "new2", "new3"])
1710 .unwrap();
1711 file.close().unwrap();
1712 }
1713 {
1714 let file = H5File::open(&path).unwrap();
1715 let ds = file.dataset("nodes/id").unwrap();
1716 let strings = ds.read_vlen_strings().unwrap();
1717 assert_eq!(strings, vec!["new1", "new2", "new3"]);
1718 }
1719 std::fs::remove_file(&path).ok();
1720 }
1721
1722 #[test]
1723 #[cfg(feature = "deflate")]
1724 fn vlen_string_compressed_large_roundtrip() {
1725 use crate::format::messages::filter::FilterPipeline;
1726 let path = temp_path("vlen_large");
1727 let input: Vec<String> = (0..7189)
1729 .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
1730 .collect();
1731 let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
1732 {
1733 let file = H5File::create(&path).unwrap();
1734 file.create_group("nodes").unwrap();
1735 file.write_vlen_strings_compressed(
1736 "nodes/id",
1737 &input_refs,
1738 512,
1739 FilterPipeline::deflate(6),
1740 )
1741 .unwrap();
1742 file.close().unwrap();
1743 }
1744 {
1746 let file = H5File::open(&path).unwrap();
1747 let ds = file.dataset("nodes/id").unwrap();
1748 let strings = ds.read_vlen_strings().unwrap();
1749 assert_eq!(strings.len(), 7189);
1750 assert_eq!(strings[0], input[0]);
1751 assert_eq!(strings[7188], input[7188]);
1752 }
1753 {
1755 let file = H5File::open_rw(&path).unwrap();
1756 file.set_attr_string("version", "1.0").unwrap();
1757 file.close().unwrap();
1758 }
1759 {
1760 let file = H5File::open(&path).unwrap();
1761 let ds = file.dataset("nodes/id").unwrap();
1762 let strings = ds.read_vlen_strings().unwrap();
1763 assert_eq!(strings.len(), 7189);
1764 assert_eq!(strings[0], input[0]);
1765 }
1766 std::fs::remove_file(&path).ok();
1767 }
1768
1769 #[test]
1770 fn vlen_string_write_read() {
1771 let path = temp_path("vlen_wr");
1772 {
1773 let file = H5File::create(&path).unwrap();
1774 file.write_vlen_strings("names", &["alice", "bob", "charlie"])
1775 .unwrap();
1776 file.close().unwrap();
1777 }
1778 {
1779 let file = H5File::open(&path).unwrap();
1780 let ds = file.dataset("names").unwrap();
1781 let strings = ds.read_vlen_strings().unwrap();
1782 assert_eq!(strings, vec!["alice", "bob", "charlie"]);
1783 }
1784 std::fs::remove_file(&path).ok();
1785 }
1786
1787 #[test]
1788 fn vlen_bytes_write_read() {
1789 let path = temp_path("vlen_bytes_wr");
1790 let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
1791 {
1792 let file = H5File::create(&path).unwrap();
1793 file.write_vlen_bytes("blobs", &items).unwrap();
1794 file.close().unwrap();
1795 }
1796 {
1797 let file = H5File::open(&path).unwrap();
1798 let ds = file.dataset("blobs").unwrap();
1799 let got = ds.read_vlen_bytes().unwrap();
1800 let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1801 assert_eq!(got, expected);
1802 }
1803 std::fs::remove_file(&path).ok();
1804 }
1805
1806 #[test]
1807 fn vlen_bytes_in_group_with_attribute() {
1808 use crate::types::VarLenUnicode;
1809 let path = temp_path("vlen_bytes_grp");
1810 let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
1811 {
1812 let file = H5File::create(&path).unwrap();
1813 let grp = file.root_group().create_group("payloads").unwrap();
1814 let ds = grp.write_vlen_bytes("frames", &items).unwrap();
1815 ds.new_attr::<VarLenUnicode>()
1816 .shape(())
1817 .create("codec")
1818 .unwrap()
1819 .write_string("raw")
1820 .unwrap();
1821 file.close().unwrap();
1822 }
1823 {
1824 let file = H5File::open(&path).unwrap();
1825 let ds = file.dataset("payloads/frames").unwrap();
1826 let got = ds.read_vlen_bytes().unwrap();
1827 let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1828 assert_eq!(got, expected);
1829 }
1830 std::fs::remove_file(&path).ok();
1831 }
1832
1833 #[test]
1834 fn vlen_dataset_returns_handle_for_attributes() {
1835 use crate::types::VarLenUnicode;
1836 let path = temp_path("vlen_attr");
1837 {
1838 let file = H5File::create(&path).unwrap();
1839 let grp = file.root_group().create_group("ch").unwrap();
1840 let ds = grp
1843 .write_vlen_strings("labels", &["a", "bb", "ccc"])
1844 .unwrap();
1845 ds.new_attr::<VarLenUnicode>()
1846 .shape(())
1847 .create("unit")
1848 .unwrap()
1849 .write_string("volt")
1850 .unwrap();
1851 let ds2 = grp.dataset_writer("labels").unwrap();
1853 ds2.new_attr::<VarLenUnicode>()
1854 .shape(())
1855 .create("desc")
1856 .unwrap()
1857 .write_string("channel labels")
1858 .unwrap();
1859 file.close().unwrap();
1860 }
1861 {
1862 let file = H5File::open(&path).unwrap();
1863 let ds = file.dataset("ch/labels").unwrap();
1864 assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
1865 assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
1866 assert_eq!(
1867 ds.attr("desc").unwrap().read_string().unwrap(),
1868 "channel labels"
1869 );
1870 }
1871 std::fs::remove_file(&path).ok();
1872 }
1873
1874 #[test]
1875 #[cfg(feature = "deflate")]
1876 fn vlen_string_deflate_roundtrip() {
1877 use crate::format::messages::filter::FilterPipeline;
1878 let path = temp_path("vlen_deflate");
1879 let input: Vec<&str> = (0..100)
1880 .map(|i| match i % 3 {
1881 0 => "hello world",
1882 1 => "compressed vlen string test",
1883 _ => "rust-hdf5",
1884 })
1885 .collect();
1886 {
1887 let file = H5File::create(&path).unwrap();
1888 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1889 .unwrap();
1890 file.close().unwrap();
1891 }
1892 {
1893 let file = H5File::open(&path).unwrap();
1894 let ds = file.dataset("texts").unwrap();
1895 let strings = ds.read_vlen_strings().unwrap();
1896 assert_eq!(strings.len(), 100);
1897 for (i, s) in strings.iter().enumerate() {
1898 assert_eq!(s, input[i]);
1899 }
1900 }
1901 std::fs::remove_file(&path).ok();
1902 }
1903
1904 #[test]
1905 #[cfg(feature = "zstd")]
1906 fn vlen_string_zstd_roundtrip() {
1907 use crate::format::messages::filter::FilterPipeline;
1908 let path = temp_path("vlen_zstd");
1909 let input: Vec<&str> = (0..200)
1910 .map(|i| match i % 4 {
1911 0 => "zstandard compression test",
1912 1 => "variable length string",
1913 2 => "rust-hdf5 chunked storage",
1914 _ => "hello zstd world",
1915 })
1916 .collect();
1917 {
1918 let file = H5File::create(&path).unwrap();
1919 file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
1920 .unwrap();
1921 file.close().unwrap();
1922 }
1923 {
1924 let file = H5File::open(&path).unwrap();
1925 let ds = file.dataset("data").unwrap();
1926 let strings = ds.read_vlen_strings().unwrap();
1927 assert_eq!(strings.len(), 200);
1928 for (i, s) in strings.iter().enumerate() {
1929 assert_eq!(s, input[i]);
1930 }
1931 }
1932 std::fs::remove_file(&path).ok();
1933 }
1934
1935 #[test]
1936 #[cfg(feature = "deflate")]
1937 fn shuffle_deflate_roundtrip() {
1938 let path = temp_path("shuf_defl");
1939 {
1940 let file = H5File::create(&path).unwrap();
1941 let ds = file
1942 .new_dataset::<f64>()
1943 .shape([0usize, 4])
1944 .chunk(&[1, 4])
1945 .max_shape(&[None, Some(4)])
1946 .shuffle_deflate(6)
1947 .create("data")
1948 .unwrap();
1949 for frame in 0..20u64 {
1950 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1951 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1952 ds.write_chunk(frame as usize, &raw).unwrap();
1953 }
1954 ds.extend(&[20, 4]).unwrap();
1955 file.close().unwrap();
1956 }
1957 {
1958 let file = H5File::open(&path).unwrap();
1959 let ds = file.dataset("data").unwrap();
1960 assert_eq!(ds.shape(), vec![20, 4]);
1961 let data = ds.read_raw::<f64>().unwrap();
1962 assert_eq!(data.len(), 80);
1963 for (i, val) in data.iter().enumerate() {
1964 assert!((val - i as f64).abs() < 1e-10);
1965 }
1966 }
1967 std::fs::remove_file(&path).ok();
1968 }
1969
1970 #[test]
1971 fn file_level_attributes() {
1972 let path = temp_path("file_attr");
1973 {
1974 let file = H5File::create(&path).unwrap();
1975 file.set_attr_string("title", "Test File").unwrap();
1976 file.set_attr_numeric("version", &42i32).unwrap();
1977 let ds = file
1978 .new_dataset::<u8>()
1979 .shape([1usize])
1980 .create("dummy")
1981 .unwrap();
1982 ds.write_raw(&[0u8]).unwrap();
1983 file.close().unwrap();
1984 }
1985 {
1986 let file = H5File::open(&path).unwrap();
1987 assert!(file.dataset_names().contains(&"dummy".to_string()));
1988
1989 let names = file.attr_names().unwrap();
1991 assert!(names.contains(&"title".to_string()));
1992
1993 let title = file.attr_string("title").unwrap();
1994 assert_eq!(title, "Test File");
1995 }
1996 std::fs::remove_file(&path).ok();
1997 }
1998
1999 #[test]
2000 fn scalar_dataset_roundtrip() {
2001 let path = temp_path("scalar");
2002 {
2003 let file = H5File::create(&path).unwrap();
2004 let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
2005 ds.write_raw(&[std::f64::consts::PI]).unwrap();
2006 file.close().unwrap();
2007 }
2008 {
2009 let file = H5File::open(&path).unwrap();
2010 let ds = file.dataset("pi").unwrap();
2011 assert_eq!(ds.shape(), Vec::<usize>::new());
2012 assert_eq!(ds.total_elements(), 1);
2013 let data = ds.read_raw::<f64>().unwrap();
2014 assert_eq!(data.len(), 1);
2015 assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
2016 }
2017 std::fs::remove_file(&path).ok();
2018 }
2019
2020 #[test]
2021 fn append_mode_extend_chunked() {
2022 let path = temp_path("append_extend");
2023
2024 {
2026 let file = H5File::create(&path).unwrap();
2027 let ds = file
2028 .new_dataset::<i32>()
2029 .shape([0usize, 3])
2030 .chunk(&[1, 3])
2031 .max_shape(&[None, Some(3)])
2032 .create("stream")
2033 .unwrap();
2034 for i in 0..5u64 {
2035 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
2036 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2037 ds.write_chunk(i as usize, &raw).unwrap();
2038 }
2039 ds.extend(&[5, 3]).unwrap();
2040 file.close().unwrap();
2041 }
2042
2043 {
2045 let file = H5File::open_rw(&path).unwrap();
2046 let names = file.dataset_names();
2048 assert!(names.contains(&"stream".to_string()));
2049
2050 let mut inner = crate::file::borrow_inner_mut(&file.inner);
2052 if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
2053 let ds_idx = writer.dataset_index("stream").unwrap();
2054 for i in 5..10u64 {
2055 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
2056 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2057 writer.write_chunk(ds_idx, i, &raw).unwrap();
2058 }
2059 writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
2060 }
2061 drop(inner);
2062 file.close().unwrap();
2063 }
2064
2065 {
2067 let file = H5File::open(&path).unwrap();
2068 let ds = file.dataset("stream").unwrap();
2069 assert_eq!(ds.shape(), vec![10, 3]);
2070 let data = ds.read_raw::<i32>().unwrap();
2071 assert_eq!(data.len(), 30);
2072 for (i, val) in data.iter().enumerate() {
2073 assert_eq!(*val, i as i32, "mismatch at {}", i);
2074 }
2075 }
2076
2077 std::fs::remove_file(&path).ok();
2078 }
2079
2080 #[test]
2081 fn group_hierarchy_roundtrip() {
2082 let path = temp_path("groups_rt");
2083
2084 {
2085 let file = H5File::create(&path).unwrap();
2086 let root = file.root_group();
2087
2088 let det = root.create_group("detector").unwrap();
2090 let raw = det.create_group("raw").unwrap();
2091
2092 let ds1 = det
2094 .new_dataset::<f32>()
2095 .shape([10usize])
2096 .create("temperature")
2097 .unwrap();
2098 ds1.write_raw(&[1.0f32; 10]).unwrap();
2099
2100 let ds2 = raw
2101 .new_dataset::<u16>()
2102 .shape([4usize, 4])
2103 .create("image")
2104 .unwrap();
2105 ds2.write_raw(&[42u16; 16]).unwrap();
2106
2107 let ds3 = file
2109 .new_dataset::<i32>()
2110 .shape([3usize])
2111 .create("version")
2112 .unwrap();
2113 ds3.write_raw(&[1i32, 0, 0]).unwrap();
2114
2115 file.close().unwrap();
2116 }
2117
2118 {
2119 let file = H5File::open(&path).unwrap();
2120 let names = file.dataset_names();
2121 assert!(names.contains(&"version".to_string()));
2122 assert!(names.contains(&"detector/temperature".to_string()));
2123 assert!(names.contains(&"detector/raw/image".to_string()));
2124
2125 let ds = file.dataset("version").unwrap();
2127 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
2128
2129 let ds = file.dataset("detector/temperature").unwrap();
2130 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
2131
2132 let ds = file.dataset("detector/raw/image").unwrap();
2133 assert_eq!(ds.shape(), vec![4, 4]);
2134 assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
2135
2136 let root = file.root_group();
2138 let group_names = root.group_names().unwrap();
2139 assert!(group_names.contains(&"detector".to_string()));
2140 }
2141
2142 std::fs::remove_file(&path).ok();
2143 }
2144
2145 #[test]
2146 fn nested_groups_via_file_create_group() {
2147 let path = temp_path("file_create_group");
2148
2149 {
2150 let file = H5File::create(&path).unwrap();
2151
2152 let grp = file.create_group("sensors").unwrap();
2154 let sub = grp.create_group("accel").unwrap();
2155
2156 let ds = sub
2157 .new_dataset::<f64>()
2158 .shape([3usize])
2159 .create("xyz")
2160 .unwrap();
2161 ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
2162
2163 file.close().unwrap();
2164 }
2165
2166 {
2167 let file = H5File::open(&path).unwrap();
2168 let names = file.dataset_names();
2169 assert!(names.contains(&"sensors/accel/xyz".to_string()));
2170
2171 let ds = file.dataset("sensors/accel/xyz").unwrap();
2172 assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
2173
2174 let root = file.root_group();
2176 let sensors = root.group("sensors").unwrap();
2177 assert_eq!(sensors.name(), "/sensors");
2178
2179 let accel = sensors.group("accel").unwrap();
2180 assert_eq!(accel.name(), "/sensors/accel");
2181
2182 let top_groups = root.group_names().unwrap();
2184 assert!(top_groups.contains(&"sensors".to_string()));
2185
2186 let sub_groups = sensors.group_names().unwrap();
2188 assert!(sub_groups.contains(&"accel".to_string()));
2189 }
2190
2191 std::fs::remove_file(&path).ok();
2192 }
2193}
2194
2195#[cfg(test)]
2196mod h5py_compat_tests {
2197 use super::*;
2198
2199 #[cfg(feature = "deflate")]
2202 fn temp_path(name: &str) -> std::path::PathBuf {
2203 super::unique_test_path(name)
2204 }
2205
2206 #[test]
2208 #[cfg(feature = "deflate")]
2209 fn h5dump_validates_our_files() {
2210 let h5dump = std::process::Command::new("h5dump")
2212 .arg("--version")
2213 .output();
2214 if h5dump.is_err() {
2215 eprintln!("skipping: h5dump not found");
2216 return;
2217 }
2218
2219 let path = temp_path("h5dump_validate");
2220
2221 {
2223 let file = H5File::create(&path).unwrap();
2224
2225 let ds = file
2227 .new_dataset::<f64>()
2228 .shape([3usize, 4])
2229 .create("matrix")
2230 .unwrap();
2231 let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
2232 ds.write_raw(&data).unwrap();
2233
2234 let ds2 = file
2236 .new_dataset::<i32>()
2237 .shape([0usize, 2])
2238 .chunk(&[1, 2])
2239 .max_shape(&[None, Some(2)])
2240 .deflate(6)
2241 .create("stream")
2242 .unwrap();
2243 for i in 0..5u64 {
2244 let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
2245 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2246 ds2.write_chunk(i as usize, &raw).unwrap();
2247 }
2248 ds2.extend(&[5, 2]).unwrap();
2249
2250 let grp = file.create_group("meta").unwrap();
2252 let ds3 = grp
2253 .new_dataset::<u8>()
2254 .shape([4usize])
2255 .create("flags")
2256 .unwrap();
2257 ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
2258
2259 use crate::types::VarLenUnicode;
2261 let attr = ds
2262 .new_attr::<VarLenUnicode>()
2263 .shape(())
2264 .create("units")
2265 .unwrap();
2266 attr.write_string("meters").unwrap();
2267
2268 file.close().unwrap();
2269 }
2270
2271 let output = std::process::Command::new("h5dump")
2273 .arg("-H") .arg(path.to_str().unwrap())
2275 .output()
2276 .unwrap();
2277
2278 assert!(
2279 output.status.success(),
2280 "h5dump failed:\nstdout: {}\nstderr: {}",
2281 String::from_utf8_lossy(&output.stdout),
2282 String::from_utf8_lossy(&output.stderr),
2283 );
2284
2285 let output2 = std::process::Command::new("h5dump")
2287 .arg(path.to_str().unwrap())
2288 .output()
2289 .unwrap();
2290
2291 assert!(
2292 output2.status.success(),
2293 "h5dump (full) failed:\nstderr: {}",
2294 String::from_utf8_lossy(&output2.stderr),
2295 );
2296
2297 std::fs::remove_file(&path).ok();
2298 }
2299
2300 #[test]
2301 fn read_h5py_generated_file() {
2302 let path = "/tmp/test_h5py_default.h5";
2303 if !std::path::Path::new(path).exists() {
2304 eprintln!("skipping: h5py test file not found");
2305 return;
2306 }
2307 let file = H5File::open(path).unwrap();
2308
2309 let ds = file.dataset("data").unwrap();
2310 assert_eq!(ds.shape(), vec![4, 5]);
2311 let data = ds.read_raw::<f64>().unwrap();
2312 assert_eq!(data.len(), 20);
2313 assert!((data[0]).abs() < 1e-10);
2314 assert!((data[19] - 19.0).abs() < 1e-10);
2315
2316 let ds2 = file.dataset("images").unwrap();
2317 assert_eq!(ds2.shape(), vec![3, 64, 64]);
2318 let images = ds2.read_raw::<u16>().unwrap();
2319 assert_eq!(images.len(), 3 * 64 * 64);
2320 }
2321}