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 close_no_sync(self) -> Result<()> {
691 let old = {
692 let mut inner = borrow_inner_mut(&self.inner);
693 std::mem::replace(&mut *inner, H5FileInner::Closed)
694 };
695 match old {
696 H5FileInner::Writer(writer) => {
697 writer.close_no_sync()?;
698 Ok(())
699 }
700 H5FileInner::Reader(_) => Ok(()),
701 H5FileInner::Closed => Ok(()),
702 }
703 }
704
705 pub fn flush(&self) -> Result<()> {
707 Ok(())
711 }
712}
713
714#[derive(Debug, Default, Clone)]
721pub struct H5FileOptions {
722 locking: Option<FileLocking>,
723}
724
725impl H5FileOptions {
726 pub fn new() -> Self {
728 Self::default()
729 }
730
731 pub fn locking(mut self, policy: FileLocking) -> Self {
734 self.locking = Some(policy);
735 self
736 }
737
738 pub fn no_locking(self) -> Self {
741 self.locking(FileLocking::Disabled)
742 }
743
744 pub fn best_effort_locking(self) -> Self {
747 self.locking(FileLocking::BestEffort)
748 }
749
750 fn resolved_locking(&self) -> FileLocking {
751 match self.locking {
752 Some(p) => p,
753 None => FileLocking::from_env_or(FileLocking::default()),
754 }
755 }
756
757 pub fn create<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
759 let writer = Hdf5Writer::create_with_locking(path.as_ref(), self.resolved_locking())?;
760 Ok(H5File {
761 inner: new_shared(H5FileInner::Writer(writer)),
762 })
763 }
764
765 pub fn open<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
767 let reader = Hdf5Reader::open_with_locking(path.as_ref(), self.resolved_locking())?;
768 Ok(H5File {
769 inner: new_shared(H5FileInner::Reader(reader)),
770 })
771 }
772
773 pub fn open_rw<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
775 let writer = Hdf5Writer::open_append_with_locking(path.as_ref(), self.resolved_locking())?;
776 Ok(H5File {
777 inner: new_shared(H5FileInner::Writer(writer)),
778 })
779 }
780}
781
782#[cfg(test)]
783fn unique_test_path(name: &str) -> std::path::PathBuf {
784 use std::sync::atomic::{AtomicU64, Ordering};
789 static COUNTER: AtomicU64 = AtomicU64::new(0);
790 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
791 std::env::temp_dir().join(format!(
792 "rust_hdf5_test_{}_{}_{}.h5",
793 name,
794 std::process::id(),
795 n
796 ))
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use std::path::PathBuf;
803
804 fn temp_path(name: &str) -> PathBuf {
805 super::unique_test_path(name)
806 }
807
808 #[test]
809 fn create_and_close_empty() {
810 let path = temp_path("create_empty");
811 let file = H5File::create(&path).unwrap();
812 file.close().unwrap();
813
814 let file = H5File::open(&path).unwrap();
816 file.close().unwrap();
817
818 std::fs::remove_file(&path).ok();
819 }
820
821 #[test]
822 fn create_and_drop_empty() {
823 let path = temp_path("drop_empty");
824 {
825 let _file = H5File::create(&path).unwrap();
826 }
828 let file = H5File::open(&path).unwrap();
830 file.close().unwrap();
831
832 std::fs::remove_file(&path).ok();
833 }
834
835 #[test]
836 fn dataset_not_found() {
837 let path = temp_path("ds_not_found");
838 {
839 let _file = H5File::create(&path).unwrap();
840 }
841 let file = H5File::open(&path).unwrap();
842 let result = file.dataset("nonexistent");
843 assert!(result.is_err());
844
845 std::fs::remove_file(&path).ok();
846 }
847
848 #[test]
849 fn write_and_read_roundtrip() {
850 let path = temp_path("write_read_rt");
851
852 {
854 let file = H5File::create(&path).unwrap();
855 let ds = file
856 .new_dataset::<u8>()
857 .shape([4, 4])
858 .create("data")
859 .unwrap();
860 ds.write_raw(&[0u8; 16]).unwrap();
861 file.close().unwrap();
862 }
863
864 {
866 let file = H5File::open(&path).unwrap();
867 let ds = file.dataset("data").unwrap();
868 assert_eq!(ds.shape(), vec![4, 4]);
869 let data = ds.read_raw::<u8>().unwrap();
870 assert_eq!(data.len(), 16);
871 assert!(data.iter().all(|&b| b == 0));
872 file.close().unwrap();
873 }
874
875 std::fs::remove_file(&path).ok();
876 }
877
878 #[test]
879 fn close_no_sync_produces_valid_readable_file() {
880 let path = temp_path("close_no_sync_rt");
881 let payload: Vec<u8> = (0u8..16).collect();
882
883 {
885 let file = H5File::create(&path).unwrap();
886 let ds = file
887 .new_dataset::<u8>()
888 .shape([4, 4])
889 .create("data")
890 .unwrap();
891 ds.write_raw(&payload).unwrap();
892 file.close_no_sync().unwrap();
895 }
896
897 {
900 let file = H5File::open(&path).unwrap();
901 let ds = file.dataset("data").unwrap();
902 assert_eq!(ds.shape(), vec![4, 4]);
903 let data = ds.read_raw::<u8>().unwrap();
904 assert_eq!(data, payload);
905 file.close().unwrap();
906 }
907
908 std::fs::remove_file(&path).ok();
909 }
910
911 #[test]
912 fn create_over_existing_file_truncates() {
913 let path = temp_path("create_truncates");
919
920 {
921 let file = H5File::create(&path).unwrap();
922 let ds = file
923 .new_dataset::<u8>()
924 .shape([4, 4])
925 .create("old_data")
926 .unwrap();
927 ds.write_raw(&[7u8; 16]).unwrap();
928 file.close().unwrap();
929 }
930 assert!(std::fs::metadata(&path).unwrap().len() > 0);
931
932 {
934 let file = H5File::create(&path).unwrap();
935 file.close().unwrap();
936 }
937
938 let file = H5File::open(&path).unwrap();
940 assert!(file.dataset("old_data").is_err());
941 file.close().unwrap();
942
943 std::fs::remove_file(&path).ok();
944 }
945
946 #[test]
947 fn close_no_sync_chunked_dataset_valid() {
948 let path = temp_path("close_no_sync_chunked");
953
954 {
955 let file = H5File::create(&path).unwrap();
956 let ds = file
957 .new_dataset::<i32>()
958 .shape([0usize, 3])
959 .chunk(&[1, 3])
960 .max_shape(&[None, Some(3)])
961 .create("data")
962 .unwrap();
963 for frame in 0..10u64 {
965 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
966 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
967 ds.write_chunk(frame as usize, &raw).unwrap();
968 }
969 ds.extend(&[10, 3]).unwrap();
970 file.close_no_sync().unwrap();
971 }
972
973 {
974 let file = H5File::open(&path).unwrap();
975 let ds = file.dataset("data").unwrap();
976 assert_eq!(ds.shape(), vec![10, 3]);
977 let data = ds.read_raw::<i32>().unwrap();
978 let expected: Vec<i32> = (0..30).collect();
979 assert_eq!(data, expected);
980 file.close().unwrap();
981 }
982
983 std::fs::remove_file(&path).ok();
984 }
985
986 #[test]
987 fn write_and_read_f64() {
988 let path = temp_path("write_read_f64");
989
990 let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
991
992 {
994 let file = H5File::create(&path).unwrap();
995 let ds = file
996 .new_dataset::<f64>()
997 .shape([2, 3])
998 .create("matrix")
999 .unwrap();
1000 ds.write_raw(&values).unwrap();
1001 file.close().unwrap();
1002 }
1003
1004 {
1006 let file = H5File::open(&path).unwrap();
1007 let ds = file.dataset("matrix").unwrap();
1008 assert_eq!(ds.shape(), vec![2, 3]);
1009 let readback = ds.read_raw::<f64>().unwrap();
1010 assert_eq!(readback, values);
1011 }
1012
1013 std::fs::remove_file(&path).ok();
1014 }
1015
1016 #[test]
1017 fn multiple_datasets() {
1018 let path = temp_path("multi_ds");
1019
1020 {
1021 let file = H5File::create(&path).unwrap();
1022 let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
1023 ds1.write_raw(&[10i32, 20, 30]).unwrap();
1024
1025 let ds2 = file
1026 .new_dataset::<f32>()
1027 .shape([2, 2])
1028 .create("floats")
1029 .unwrap();
1030 ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
1031
1032 file.close().unwrap();
1033 }
1034
1035 {
1036 let file = H5File::open(&path).unwrap();
1037
1038 let ds_ints = file.dataset("ints").unwrap();
1039 assert_eq!(ds_ints.shape(), vec![3]);
1040 let ints = ds_ints.read_raw::<i32>().unwrap();
1041 assert_eq!(ints, vec![10, 20, 30]);
1042
1043 let ds_floats = file.dataset("floats").unwrap();
1044 assert_eq!(ds_floats.shape(), vec![2, 2]);
1045 let floats = ds_floats.read_raw::<f32>().unwrap();
1046 assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
1047 }
1048
1049 std::fs::remove_file(&path).ok();
1050 }
1051
1052 #[test]
1053 fn close_is_idempotent() {
1054 let path = temp_path("close_idemp");
1055 let file = H5File::create(&path).unwrap();
1056 file.close().unwrap();
1057 std::fs::remove_file(&path).ok();
1059 }
1060}
1061
1062#[cfg(test)]
1063mod integration_tests {
1064 use super::*;
1065
1066 fn temp_path(name: &str) -> std::path::PathBuf {
1067 super::unique_test_path(name)
1068 }
1069
1070 #[test]
1071 fn write_file_for_h5dump() {
1072 let path = temp_path("integration");
1073 let file = H5File::create(&path).unwrap();
1074
1075 let ds = file
1076 .new_dataset::<u8>()
1077 .shape([4usize, 4])
1078 .create("data_u8")
1079 .unwrap();
1080 let data: Vec<u8> = (0..16).collect();
1081 ds.write_raw(&data).unwrap();
1082
1083 let ds2 = file
1084 .new_dataset::<f64>()
1085 .shape([3usize, 2])
1086 .create("data_f64")
1087 .unwrap();
1088 let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1089 ds2.write_raw(&fdata).unwrap();
1090
1091 let ds3 = file
1092 .new_dataset::<i32>()
1093 .shape([5usize])
1094 .create("values")
1095 .unwrap();
1096 let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
1097 ds3.write_raw(&idata).unwrap();
1098
1099 file.close().unwrap();
1100
1101 assert!(path.exists());
1103 }
1104
1105 #[test]
1106 fn write_chunked_file_for_h5dump() {
1107 let path = temp_path("chunked");
1108 let file = H5File::create(&path).unwrap();
1109
1110 let ds = file
1112 .new_dataset::<f64>()
1113 .shape([0usize, 4])
1114 .chunk(&[1, 4])
1115 .max_shape(&[None, Some(4)])
1116 .create("streaming_data")
1117 .unwrap();
1118
1119 for frame in 0..5u64 {
1121 let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1122 let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1123 ds.write_chunk(frame as usize, &raw).unwrap();
1124 }
1125
1126 ds.extend(&[5, 4]).unwrap();
1128 ds.flush().unwrap();
1129
1130 file.close().unwrap();
1131
1132 assert!(path.exists());
1133 }
1134
1135 #[test]
1136 fn write_chunked_many_frames_for_h5dump() {
1137 let path = temp_path("chunked_many");
1138 let file = H5File::create(&path).unwrap();
1139
1140 let ds = file
1141 .new_dataset::<i32>()
1142 .shape([0usize, 3])
1143 .chunk(&[1, 3])
1144 .max_shape(&[None, Some(3)])
1145 .create("data")
1146 .unwrap();
1147
1148 for frame in 0..10u64 {
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(&[10, 3]).unwrap();
1155 file.close().unwrap();
1156
1157 assert!(path.exists());
1158 }
1159
1160 #[test]
1161 fn write_dataset_with_attributes() {
1162 use crate::types::VarLenUnicode;
1163
1164 let path = temp_path("attributes");
1165 let file = H5File::create(&path).unwrap();
1166
1167 let ds = file
1168 .new_dataset::<f32>()
1169 .shape([10usize])
1170 .create("temperature")
1171 .unwrap();
1172 let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
1173 ds.write_raw(&data).unwrap();
1174
1175 let attr = ds
1177 .new_attr::<VarLenUnicode>()
1178 .shape(())
1179 .create("units")
1180 .unwrap();
1181 attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
1182 .unwrap();
1183
1184 let attr2 = ds
1185 .new_attr::<VarLenUnicode>()
1186 .shape(())
1187 .create("description")
1188 .unwrap();
1189 attr2
1190 .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
1191 .unwrap();
1192
1193 let attr3 = ds
1195 .new_attr::<VarLenUnicode>()
1196 .shape(())
1197 .create("source")
1198 .unwrap();
1199 attr3.write_string("sensor_01").unwrap();
1200
1201 let attr4 = ds
1203 .new_attr::<VarLenUnicode>()
1204 .shape(())
1205 .create("label")
1206 .unwrap();
1207 let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
1208 attr4.write_scalar(&s).unwrap();
1209
1210 file.close().unwrap();
1211
1212 assert!(path.exists());
1213 }
1214
1215 #[test]
1216 fn dataset_writer_reopens_for_attributes() {
1217 let path = temp_path("dataset_writer");
1220 {
1221 let file = H5File::create(&path).unwrap();
1222 {
1223 let ds = file
1224 .new_dataset::<u16>()
1225 .shape([8])
1226 .create("image")
1227 .unwrap();
1228 ds.write_raw(&[0u16; 8]).unwrap();
1229 }
1231
1232 assert!(file.dataset("image").is_err());
1234 let ds = file.dataset_writer("image").unwrap();
1235 assert_eq!(ds.shape(), vec![8]);
1236 ds.new_attr::<i32>()
1237 .shape([3])
1238 .create("NDArrayDimOffset")
1239 .unwrap()
1240 .write_array(&[0i32, 4, 8])
1241 .unwrap();
1242 ds.new_attr::<i32>()
1243 .shape(())
1244 .create("NDUniqueId")
1245 .unwrap()
1246 .write_numeric(&42i32)
1247 .unwrap();
1248
1249 assert!(matches!(
1251 file.dataset_writer("nope"),
1252 Err(crate::error::Hdf5Error::NotFound(_))
1253 ));
1254
1255 file.close().unwrap();
1256 }
1257 {
1258 let file = H5File::open(&path).unwrap();
1259 let ds = file.dataset("image").unwrap();
1260 let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
1261 let got: Vec<i32> = off
1262 .chunks_exact(4)
1263 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1264 .collect();
1265 assert_eq!(got, vec![0, 4, 8]);
1266 let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
1267 assert_eq!(uid, 42);
1268 }
1269 std::fs::remove_file(&path).ok();
1270 }
1271
1272 #[test]
1273 fn chunked_write_read_roundtrip() {
1274 let path = temp_path("chunked_roundtrip");
1275
1276 {
1278 let file = H5File::create(&path).unwrap();
1279 let ds = file
1280 .new_dataset::<i32>()
1281 .shape([0usize, 3])
1282 .chunk(&[1, 3])
1283 .max_shape(&[None, Some(3)])
1284 .create("table")
1285 .unwrap();
1286
1287 for frame in 0..8u64 {
1288 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1289 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1290 ds.write_chunk(frame as usize, &raw).unwrap();
1291 }
1292 ds.extend(&[8, 3]).unwrap();
1293 file.close().unwrap();
1294 }
1295
1296 {
1298 let file = H5File::open(&path).unwrap();
1299 let ds = file.dataset("table").unwrap();
1300 assert_eq!(ds.shape(), vec![8, 3]);
1301 let data = ds.read_raw::<i32>().unwrap();
1302 assert_eq!(data.len(), 24);
1303 for (i, val) in data.iter().enumerate() {
1304 assert_eq!(*val, i as i32);
1305 }
1306 }
1307
1308 std::fs::remove_file(&path).ok();
1309 }
1310
1311 #[test]
1312 #[cfg(feature = "deflate")]
1313 fn compressed_chunked_roundtrip() {
1314 let path = temp_path("compressed_roundtrip");
1315
1316 {
1318 let file = H5File::create(&path).unwrap();
1319 let ds = file
1320 .new_dataset::<f64>()
1321 .shape([0usize, 4])
1322 .chunk(&[1, 4])
1323 .max_shape(&[None, Some(4)])
1324 .deflate(6)
1325 .create("compressed")
1326 .unwrap();
1327
1328 for frame in 0..10u64 {
1329 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1330 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1331 ds.write_chunk(frame as usize, &raw).unwrap();
1332 }
1333 ds.extend(&[10, 4]).unwrap();
1334 file.close().unwrap();
1335 }
1336
1337 {
1339 let file = H5File::open(&path).unwrap();
1340 let ds = file.dataset("compressed").unwrap();
1341 assert_eq!(ds.shape(), vec![10, 4]);
1342 let data = ds.read_raw::<f64>().unwrap();
1343 assert_eq!(data.len(), 40);
1344 for (i, val) in data.iter().enumerate() {
1345 assert!(
1346 (val - i as f64).abs() < 1e-10,
1347 "mismatch at {}: {} != {}",
1348 i,
1349 val,
1350 i
1351 );
1352 }
1353 }
1354
1355 std::fs::remove_file(&path).ok();
1356 }
1357
1358 #[test]
1359 #[cfg(feature = "deflate")]
1360 fn compressed_chunked_many_frames() {
1361 let path = temp_path("compressed_many");
1362
1363 {
1364 let file = H5File::create(&path).unwrap();
1365 let ds = file
1366 .new_dataset::<i32>()
1367 .shape([0usize, 3])
1368 .chunk(&[1, 3])
1369 .max_shape(&[None, Some(3)])
1370 .deflate(6)
1371 .create("stream")
1372 .unwrap();
1373
1374 for frame in 0..100u64 {
1375 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1376 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1377 ds.write_chunk(frame as usize, &raw).unwrap();
1378 }
1379 ds.extend(&[100, 3]).unwrap();
1380 file.close().unwrap();
1381 }
1382
1383 {
1384 let file = H5File::open(&path).unwrap();
1385 let ds = file.dataset("stream").unwrap();
1386 assert_eq!(ds.shape(), vec![100, 3]);
1387 let data = ds.read_raw::<i32>().unwrap();
1388 assert_eq!(data.len(), 300);
1389 for (i, val) in data.iter().enumerate() {
1390 assert_eq!(*val, i as i32, "mismatch at {}", i);
1391 }
1392 }
1393
1394 std::fs::remove_file(&path).ok();
1395 }
1396 #[test]
1397 fn append_mode() {
1398 let path = temp_path("append");
1399
1400 {
1402 let file = H5File::create(&path).unwrap();
1403 let ds = file
1404 .new_dataset::<i32>()
1405 .shape([3usize])
1406 .create("first")
1407 .unwrap();
1408 ds.write_raw(&[1i32, 2, 3]).unwrap();
1409 file.close().unwrap();
1410 }
1411
1412 {
1414 let file = H5File::open_rw(&path).unwrap();
1415 let ds = file
1416 .new_dataset::<f64>()
1417 .shape([2usize])
1418 .create("second")
1419 .unwrap();
1420 ds.write_raw(&[4.0f64, 5.0]).unwrap();
1421 file.close().unwrap();
1422 }
1423
1424 {
1426 let file = H5File::open(&path).unwrap();
1427 let names = file.dataset_names();
1428 assert!(names.contains(&"first".to_string()));
1429 assert!(names.contains(&"second".to_string()));
1430
1431 let ds1 = file.dataset("first").unwrap();
1432 assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
1433
1434 let ds2 = file.dataset("second").unwrap();
1435 assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
1436 }
1437
1438 std::fs::remove_file(&path).ok();
1439 }
1440
1441 #[test]
1442 fn open_rw_set_attr_preserves_file() {
1443 let path = temp_path("open_rw_attr");
1444 {
1446 let file = H5File::create(&path).unwrap();
1447 let ds = file
1448 .new_dataset::<i32>()
1449 .shape([3usize])
1450 .create("data")
1451 .unwrap();
1452 ds.write_raw(&[10i32, 20, 30]).unwrap();
1453 file.set_attr_string("version", "1.0").unwrap();
1454 file.close().unwrap();
1455 }
1456 {
1458 let file = H5File::open_rw(&path).unwrap();
1459 file.set_attr_string("version", "2.0").unwrap();
1460 file.close().unwrap();
1461 }
1462 {
1464 let file = H5File::open(&path).unwrap();
1465 let ds = file.dataset("data").unwrap();
1466 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
1467 let ver = file.attr_string("version").unwrap();
1468 assert_eq!(ver, "2.0");
1469 }
1470 std::fs::remove_file(&path).ok();
1471 }
1472
1473 #[test]
1474 #[cfg(feature = "deflate")]
1475 fn open_rw_attr_with_compressed_dataset() {
1476 use crate::format::messages::filter::FilterPipeline;
1477 let path = temp_path("open_rw_compressed");
1478 let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
1479 {
1481 let file = H5File::create(&path).unwrap();
1482 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1483 .unwrap();
1484 file.set_attr_string("version", "1.0").unwrap();
1485 file.close().unwrap();
1486 }
1487 {
1489 let file = H5File::open_rw(&path).unwrap();
1490 file.set_attr_string("version", "2.0").unwrap();
1491 file.close().unwrap();
1492 }
1493 {
1495 let file = H5File::open(&path).unwrap();
1496 let ds = file.dataset("texts").unwrap();
1497 let strings = ds.read_vlen_strings().unwrap();
1498 assert_eq!(strings.len(), 50);
1499 assert_eq!(strings[0], "test string data");
1500 let ver = file.attr_string("version").unwrap();
1501 assert_eq!(ver, "2.0");
1502 }
1503 std::fs::remove_file(&path).ok();
1504 }
1505
1506 #[test]
1507 #[cfg(feature = "lz4")]
1508 fn append_vlen_strings_basic() {
1509 use crate::format::messages::filter::FilterPipeline;
1510 let path = temp_path("append_vlen");
1511 {
1512 let file = H5File::create(&path).unwrap();
1513 file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
1514 .unwrap();
1515 file.append_vlen_strings("names", &["alice", "bob", "charlie"])
1516 .unwrap();
1517 file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
1518 file.close().unwrap();
1519 }
1520 {
1521 let file = H5File::open(&path).unwrap();
1522 let ds = file.dataset("names").unwrap();
1523 let strings = ds.read_vlen_strings().unwrap();
1524 assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
1525 }
1526 std::fs::remove_file(&path).ok();
1527 }
1528
1529 #[test]
1530 #[cfg(feature = "lz4")]
1531 fn append_vlen_strings_large() {
1532 use crate::format::messages::filter::FilterPipeline;
1533 let path = temp_path("append_vlen_large");
1534 let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
1535 let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
1536 {
1537 let file = H5File::create(&path).unwrap();
1538 file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
1539 .unwrap();
1540 let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
1541 file.append_vlen_strings("data", &r1).unwrap();
1542 let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
1543 file.append_vlen_strings("data", &r2).unwrap();
1544 file.close().unwrap();
1545 }
1546 {
1547 let file = H5File::open(&path).unwrap();
1548 let ds = file.dataset("data").unwrap();
1549 let strings = ds.read_vlen_strings().unwrap();
1550 assert_eq!(strings.len(), 7189);
1551 assert_eq!(strings[0], "node-000000");
1552 assert_eq!(strings[7188], "node-007188");
1553 }
1554 std::fs::remove_file(&path).ok();
1555 }
1556
1557 #[test]
1558 fn append_vlen_strings_uncompressed() {
1559 let path = temp_path("append_vlen_unc");
1560 {
1561 let file = H5File::create(&path).unwrap();
1562 file.create_appendable_vlen_dataset("texts", 8, None)
1563 .unwrap();
1564 file.append_vlen_strings("texts", &["hello", "world"])
1565 .unwrap();
1566 file.append_vlen_strings("texts", &["foo", "bar", "baz"])
1567 .unwrap();
1568 file.close().unwrap();
1569 }
1570 {
1571 let file = H5File::open(&path).unwrap();
1572 let ds = file.dataset("texts").unwrap();
1573 let strings = ds.read_vlen_strings().unwrap();
1574 assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
1575 }
1576 std::fs::remove_file(&path).ok();
1577 }
1578
1579 #[test]
1580 fn delete_dataset_roundtrip() {
1581 let path = temp_path("delete_ds");
1582 {
1583 let file = H5File::create(&path).unwrap();
1584 file.write_vlen_strings("keep", &["a", "b"]).unwrap();
1585 file.write_vlen_strings("remove", &["x", "y"]).unwrap();
1586 file.delete_dataset("remove").unwrap();
1587 file.close().unwrap();
1588 }
1589 {
1590 let file = H5File::open(&path).unwrap();
1591 let names = file.dataset_names();
1592 assert!(names.contains(&"keep".to_string()));
1593 assert!(!names.contains(&"remove".to_string()));
1594 let ds = file.dataset("keep").unwrap();
1595 assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
1596 }
1597 std::fs::remove_file(&path).ok();
1598 }
1599
1600 #[test]
1601 fn delete_group_roundtrip() {
1602 let path = temp_path("delete_grp");
1603 {
1604 let file = H5File::create(&path).unwrap();
1605 let g1 = file.create_group("keep").unwrap();
1606 g1.write_vlen_strings("data", &["a"]).unwrap();
1607 let g2 = file.create_group("remove").unwrap();
1608 g2.write_vlen_strings("data", &["x"]).unwrap();
1609 file.delete_group("remove").unwrap();
1610 file.close().unwrap();
1611 }
1612 {
1613 let file = H5File::open(&path).unwrap();
1614 let names = file.dataset_names();
1615 assert!(names.contains(&"keep/data".to_string()));
1616 assert!(!names.contains(&"remove/data".to_string()));
1617 }
1618 std::fs::remove_file(&path).ok();
1619 }
1620
1621 #[test]
1622 fn open_rw_delete_recreate_group() {
1623 let path = temp_path("rw_delete_recreate");
1624 {
1626 let file = H5File::create(&path).unwrap();
1627 let n = file.create_group("nodes").unwrap();
1628 n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
1629 let e = file.create_group("edges").unwrap();
1630 e.write_vlen_strings("src", &["x", "y"]).unwrap();
1631 file.close().unwrap();
1632 }
1633 {
1635 let file = H5File::open_rw(&path).unwrap();
1636 file.delete_group("nodes").unwrap();
1637 let n = file.create_group("nodes").unwrap();
1638 n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
1639 file.close().unwrap();
1640 }
1641 {
1643 let file = H5File::open(&path).unwrap();
1644 let ds = file.dataset("nodes/id").unwrap();
1645 let s = ds.read_vlen_strings().unwrap();
1646 assert_eq!(s, vec!["new1", "new2"]);
1647 let ds = file.dataset("edges/src").unwrap();
1649 let s = ds.read_vlen_strings().unwrap();
1650 assert_eq!(s, vec!["x", "y"]);
1651 }
1652 std::fs::remove_file(&path).ok();
1653 }
1654
1655 #[test]
1656 fn delete_and_recreate_group() {
1657 let path = temp_path("delete_recreate");
1658 {
1659 let file = H5File::create(&path).unwrap();
1660 let g = file.create_group("nodes").unwrap();
1661 g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
1662 file.delete_group("nodes").unwrap();
1663 let g = file.create_group("nodes").unwrap();
1664 g.write_vlen_strings("id", &["new1", "new2", "new3"])
1665 .unwrap();
1666 file.close().unwrap();
1667 }
1668 {
1669 let file = H5File::open(&path).unwrap();
1670 let ds = file.dataset("nodes/id").unwrap();
1671 let strings = ds.read_vlen_strings().unwrap();
1672 assert_eq!(strings, vec!["new1", "new2", "new3"]);
1673 }
1674 std::fs::remove_file(&path).ok();
1675 }
1676
1677 #[test]
1678 #[cfg(feature = "deflate")]
1679 fn vlen_string_compressed_large_roundtrip() {
1680 use crate::format::messages::filter::FilterPipeline;
1681 let path = temp_path("vlen_large");
1682 let input: Vec<String> = (0..7189)
1684 .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
1685 .collect();
1686 let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
1687 {
1688 let file = H5File::create(&path).unwrap();
1689 file.create_group("nodes").unwrap();
1690 file.write_vlen_strings_compressed(
1691 "nodes/id",
1692 &input_refs,
1693 512,
1694 FilterPipeline::deflate(6),
1695 )
1696 .unwrap();
1697 file.close().unwrap();
1698 }
1699 {
1701 let file = H5File::open(&path).unwrap();
1702 let ds = file.dataset("nodes/id").unwrap();
1703 let strings = ds.read_vlen_strings().unwrap();
1704 assert_eq!(strings.len(), 7189);
1705 assert_eq!(strings[0], input[0]);
1706 assert_eq!(strings[7188], input[7188]);
1707 }
1708 {
1710 let file = H5File::open_rw(&path).unwrap();
1711 file.set_attr_string("version", "1.0").unwrap();
1712 file.close().unwrap();
1713 }
1714 {
1715 let file = H5File::open(&path).unwrap();
1716 let ds = file.dataset("nodes/id").unwrap();
1717 let strings = ds.read_vlen_strings().unwrap();
1718 assert_eq!(strings.len(), 7189);
1719 assert_eq!(strings[0], input[0]);
1720 }
1721 std::fs::remove_file(&path).ok();
1722 }
1723
1724 #[test]
1725 fn vlen_string_write_read() {
1726 let path = temp_path("vlen_wr");
1727 {
1728 let file = H5File::create(&path).unwrap();
1729 file.write_vlen_strings("names", &["alice", "bob", "charlie"])
1730 .unwrap();
1731 file.close().unwrap();
1732 }
1733 {
1734 let file = H5File::open(&path).unwrap();
1735 let ds = file.dataset("names").unwrap();
1736 let strings = ds.read_vlen_strings().unwrap();
1737 assert_eq!(strings, vec!["alice", "bob", "charlie"]);
1738 }
1739 std::fs::remove_file(&path).ok();
1740 }
1741
1742 #[test]
1743 fn vlen_bytes_write_read() {
1744 let path = temp_path("vlen_bytes_wr");
1745 let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
1746 {
1747 let file = H5File::create(&path).unwrap();
1748 file.write_vlen_bytes("blobs", &items).unwrap();
1749 file.close().unwrap();
1750 }
1751 {
1752 let file = H5File::open(&path).unwrap();
1753 let ds = file.dataset("blobs").unwrap();
1754 let got = ds.read_vlen_bytes().unwrap();
1755 let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1756 assert_eq!(got, expected);
1757 }
1758 std::fs::remove_file(&path).ok();
1759 }
1760
1761 #[test]
1762 fn vlen_bytes_in_group_with_attribute() {
1763 use crate::types::VarLenUnicode;
1764 let path = temp_path("vlen_bytes_grp");
1765 let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
1766 {
1767 let file = H5File::create(&path).unwrap();
1768 let grp = file.root_group().create_group("payloads").unwrap();
1769 let ds = grp.write_vlen_bytes("frames", &items).unwrap();
1770 ds.new_attr::<VarLenUnicode>()
1771 .shape(())
1772 .create("codec")
1773 .unwrap()
1774 .write_string("raw")
1775 .unwrap();
1776 file.close().unwrap();
1777 }
1778 {
1779 let file = H5File::open(&path).unwrap();
1780 let ds = file.dataset("payloads/frames").unwrap();
1781 let got = ds.read_vlen_bytes().unwrap();
1782 let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1783 assert_eq!(got, expected);
1784 }
1785 std::fs::remove_file(&path).ok();
1786 }
1787
1788 #[test]
1789 fn vlen_dataset_returns_handle_for_attributes() {
1790 use crate::types::VarLenUnicode;
1791 let path = temp_path("vlen_attr");
1792 {
1793 let file = H5File::create(&path).unwrap();
1794 let grp = file.root_group().create_group("ch").unwrap();
1795 let ds = grp
1798 .write_vlen_strings("labels", &["a", "bb", "ccc"])
1799 .unwrap();
1800 ds.new_attr::<VarLenUnicode>()
1801 .shape(())
1802 .create("unit")
1803 .unwrap()
1804 .write_string("volt")
1805 .unwrap();
1806 let ds2 = grp.dataset_writer("labels").unwrap();
1808 ds2.new_attr::<VarLenUnicode>()
1809 .shape(())
1810 .create("desc")
1811 .unwrap()
1812 .write_string("channel labels")
1813 .unwrap();
1814 file.close().unwrap();
1815 }
1816 {
1817 let file = H5File::open(&path).unwrap();
1818 let ds = file.dataset("ch/labels").unwrap();
1819 assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
1820 assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
1821 assert_eq!(
1822 ds.attr("desc").unwrap().read_string().unwrap(),
1823 "channel labels"
1824 );
1825 }
1826 std::fs::remove_file(&path).ok();
1827 }
1828
1829 #[test]
1830 #[cfg(feature = "deflate")]
1831 fn vlen_string_deflate_roundtrip() {
1832 use crate::format::messages::filter::FilterPipeline;
1833 let path = temp_path("vlen_deflate");
1834 let input: Vec<&str> = (0..100)
1835 .map(|i| match i % 3 {
1836 0 => "hello world",
1837 1 => "compressed vlen string test",
1838 _ => "rust-hdf5",
1839 })
1840 .collect();
1841 {
1842 let file = H5File::create(&path).unwrap();
1843 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1844 .unwrap();
1845 file.close().unwrap();
1846 }
1847 {
1848 let file = H5File::open(&path).unwrap();
1849 let ds = file.dataset("texts").unwrap();
1850 let strings = ds.read_vlen_strings().unwrap();
1851 assert_eq!(strings.len(), 100);
1852 for (i, s) in strings.iter().enumerate() {
1853 assert_eq!(s, input[i]);
1854 }
1855 }
1856 std::fs::remove_file(&path).ok();
1857 }
1858
1859 #[test]
1860 #[cfg(feature = "zstd")]
1861 fn vlen_string_zstd_roundtrip() {
1862 use crate::format::messages::filter::FilterPipeline;
1863 let path = temp_path("vlen_zstd");
1864 let input: Vec<&str> = (0..200)
1865 .map(|i| match i % 4 {
1866 0 => "zstandard compression test",
1867 1 => "variable length string",
1868 2 => "rust-hdf5 chunked storage",
1869 _ => "hello zstd world",
1870 })
1871 .collect();
1872 {
1873 let file = H5File::create(&path).unwrap();
1874 file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
1875 .unwrap();
1876 file.close().unwrap();
1877 }
1878 {
1879 let file = H5File::open(&path).unwrap();
1880 let ds = file.dataset("data").unwrap();
1881 let strings = ds.read_vlen_strings().unwrap();
1882 assert_eq!(strings.len(), 200);
1883 for (i, s) in strings.iter().enumerate() {
1884 assert_eq!(s, input[i]);
1885 }
1886 }
1887 std::fs::remove_file(&path).ok();
1888 }
1889
1890 #[test]
1891 #[cfg(feature = "deflate")]
1892 fn shuffle_deflate_roundtrip() {
1893 let path = temp_path("shuf_defl");
1894 {
1895 let file = H5File::create(&path).unwrap();
1896 let ds = file
1897 .new_dataset::<f64>()
1898 .shape([0usize, 4])
1899 .chunk(&[1, 4])
1900 .max_shape(&[None, Some(4)])
1901 .shuffle_deflate(6)
1902 .create("data")
1903 .unwrap();
1904 for frame in 0..20u64 {
1905 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1906 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1907 ds.write_chunk(frame as usize, &raw).unwrap();
1908 }
1909 ds.extend(&[20, 4]).unwrap();
1910 file.close().unwrap();
1911 }
1912 {
1913 let file = H5File::open(&path).unwrap();
1914 let ds = file.dataset("data").unwrap();
1915 assert_eq!(ds.shape(), vec![20, 4]);
1916 let data = ds.read_raw::<f64>().unwrap();
1917 assert_eq!(data.len(), 80);
1918 for (i, val) in data.iter().enumerate() {
1919 assert!((val - i as f64).abs() < 1e-10);
1920 }
1921 }
1922 std::fs::remove_file(&path).ok();
1923 }
1924
1925 #[test]
1926 fn file_level_attributes() {
1927 let path = temp_path("file_attr");
1928 {
1929 let file = H5File::create(&path).unwrap();
1930 file.set_attr_string("title", "Test File").unwrap();
1931 file.set_attr_numeric("version", &42i32).unwrap();
1932 let ds = file
1933 .new_dataset::<u8>()
1934 .shape([1usize])
1935 .create("dummy")
1936 .unwrap();
1937 ds.write_raw(&[0u8]).unwrap();
1938 file.close().unwrap();
1939 }
1940 {
1941 let file = H5File::open(&path).unwrap();
1942 assert!(file.dataset_names().contains(&"dummy".to_string()));
1943
1944 let names = file.attr_names().unwrap();
1946 assert!(names.contains(&"title".to_string()));
1947
1948 let title = file.attr_string("title").unwrap();
1949 assert_eq!(title, "Test File");
1950 }
1951 std::fs::remove_file(&path).ok();
1952 }
1953
1954 #[test]
1955 fn scalar_dataset_roundtrip() {
1956 let path = temp_path("scalar");
1957 {
1958 let file = H5File::create(&path).unwrap();
1959 let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
1960 ds.write_raw(&[std::f64::consts::PI]).unwrap();
1961 file.close().unwrap();
1962 }
1963 {
1964 let file = H5File::open(&path).unwrap();
1965 let ds = file.dataset("pi").unwrap();
1966 assert_eq!(ds.shape(), Vec::<usize>::new());
1967 assert_eq!(ds.total_elements(), 1);
1968 let data = ds.read_raw::<f64>().unwrap();
1969 assert_eq!(data.len(), 1);
1970 assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
1971 }
1972 std::fs::remove_file(&path).ok();
1973 }
1974
1975 #[test]
1976 fn append_mode_extend_chunked() {
1977 let path = temp_path("append_extend");
1978
1979 {
1981 let file = H5File::create(&path).unwrap();
1982 let ds = file
1983 .new_dataset::<i32>()
1984 .shape([0usize, 3])
1985 .chunk(&[1, 3])
1986 .max_shape(&[None, Some(3)])
1987 .create("stream")
1988 .unwrap();
1989 for i in 0..5u64 {
1990 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1991 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1992 ds.write_chunk(i as usize, &raw).unwrap();
1993 }
1994 ds.extend(&[5, 3]).unwrap();
1995 file.close().unwrap();
1996 }
1997
1998 {
2000 let file = H5File::open_rw(&path).unwrap();
2001 let names = file.dataset_names();
2003 assert!(names.contains(&"stream".to_string()));
2004
2005 let mut inner = crate::file::borrow_inner_mut(&file.inner);
2007 if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
2008 let ds_idx = writer.dataset_index("stream").unwrap();
2009 for i in 5..10u64 {
2010 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
2011 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2012 writer.write_chunk(ds_idx, i, &raw).unwrap();
2013 }
2014 writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
2015 }
2016 drop(inner);
2017 file.close().unwrap();
2018 }
2019
2020 {
2022 let file = H5File::open(&path).unwrap();
2023 let ds = file.dataset("stream").unwrap();
2024 assert_eq!(ds.shape(), vec![10, 3]);
2025 let data = ds.read_raw::<i32>().unwrap();
2026 assert_eq!(data.len(), 30);
2027 for (i, val) in data.iter().enumerate() {
2028 assert_eq!(*val, i as i32, "mismatch at {}", i);
2029 }
2030 }
2031
2032 std::fs::remove_file(&path).ok();
2033 }
2034
2035 #[test]
2036 fn group_hierarchy_roundtrip() {
2037 let path = temp_path("groups_rt");
2038
2039 {
2040 let file = H5File::create(&path).unwrap();
2041 let root = file.root_group();
2042
2043 let det = root.create_group("detector").unwrap();
2045 let raw = det.create_group("raw").unwrap();
2046
2047 let ds1 = det
2049 .new_dataset::<f32>()
2050 .shape([10usize])
2051 .create("temperature")
2052 .unwrap();
2053 ds1.write_raw(&[1.0f32; 10]).unwrap();
2054
2055 let ds2 = raw
2056 .new_dataset::<u16>()
2057 .shape([4usize, 4])
2058 .create("image")
2059 .unwrap();
2060 ds2.write_raw(&[42u16; 16]).unwrap();
2061
2062 let ds3 = file
2064 .new_dataset::<i32>()
2065 .shape([3usize])
2066 .create("version")
2067 .unwrap();
2068 ds3.write_raw(&[1i32, 0, 0]).unwrap();
2069
2070 file.close().unwrap();
2071 }
2072
2073 {
2074 let file = H5File::open(&path).unwrap();
2075 let names = file.dataset_names();
2076 assert!(names.contains(&"version".to_string()));
2077 assert!(names.contains(&"detector/temperature".to_string()));
2078 assert!(names.contains(&"detector/raw/image".to_string()));
2079
2080 let ds = file.dataset("version").unwrap();
2082 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
2083
2084 let ds = file.dataset("detector/temperature").unwrap();
2085 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
2086
2087 let ds = file.dataset("detector/raw/image").unwrap();
2088 assert_eq!(ds.shape(), vec![4, 4]);
2089 assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
2090
2091 let root = file.root_group();
2093 let group_names = root.group_names().unwrap();
2094 assert!(group_names.contains(&"detector".to_string()));
2095 }
2096
2097 std::fs::remove_file(&path).ok();
2098 }
2099
2100 #[test]
2101 fn nested_groups_via_file_create_group() {
2102 let path = temp_path("file_create_group");
2103
2104 {
2105 let file = H5File::create(&path).unwrap();
2106
2107 let grp = file.create_group("sensors").unwrap();
2109 let sub = grp.create_group("accel").unwrap();
2110
2111 let ds = sub
2112 .new_dataset::<f64>()
2113 .shape([3usize])
2114 .create("xyz")
2115 .unwrap();
2116 ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
2117
2118 file.close().unwrap();
2119 }
2120
2121 {
2122 let file = H5File::open(&path).unwrap();
2123 let names = file.dataset_names();
2124 assert!(names.contains(&"sensors/accel/xyz".to_string()));
2125
2126 let ds = file.dataset("sensors/accel/xyz").unwrap();
2127 assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
2128
2129 let root = file.root_group();
2131 let sensors = root.group("sensors").unwrap();
2132 assert_eq!(sensors.name(), "/sensors");
2133
2134 let accel = sensors.group("accel").unwrap();
2135 assert_eq!(accel.name(), "/sensors/accel");
2136
2137 let top_groups = root.group_names().unwrap();
2139 assert!(top_groups.contains(&"sensors".to_string()));
2140
2141 let sub_groups = sensors.group_names().unwrap();
2143 assert!(sub_groups.contains(&"accel".to_string()));
2144 }
2145
2146 std::fs::remove_file(&path).ok();
2147 }
2148}
2149
2150#[cfg(test)]
2151mod h5py_compat_tests {
2152 use super::*;
2153
2154 #[cfg(feature = "deflate")]
2157 fn temp_path(name: &str) -> std::path::PathBuf {
2158 super::unique_test_path(name)
2159 }
2160
2161 #[test]
2163 #[cfg(feature = "deflate")]
2164 fn h5dump_validates_our_files() {
2165 let h5dump = std::process::Command::new("h5dump")
2167 .arg("--version")
2168 .output();
2169 if h5dump.is_err() {
2170 eprintln!("skipping: h5dump not found");
2171 return;
2172 }
2173
2174 let path = temp_path("h5dump_validate");
2175
2176 {
2178 let file = H5File::create(&path).unwrap();
2179
2180 let ds = file
2182 .new_dataset::<f64>()
2183 .shape([3usize, 4])
2184 .create("matrix")
2185 .unwrap();
2186 let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
2187 ds.write_raw(&data).unwrap();
2188
2189 let ds2 = file
2191 .new_dataset::<i32>()
2192 .shape([0usize, 2])
2193 .chunk(&[1, 2])
2194 .max_shape(&[None, Some(2)])
2195 .deflate(6)
2196 .create("stream")
2197 .unwrap();
2198 for i in 0..5u64 {
2199 let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
2200 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2201 ds2.write_chunk(i as usize, &raw).unwrap();
2202 }
2203 ds2.extend(&[5, 2]).unwrap();
2204
2205 let grp = file.create_group("meta").unwrap();
2207 let ds3 = grp
2208 .new_dataset::<u8>()
2209 .shape([4usize])
2210 .create("flags")
2211 .unwrap();
2212 ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
2213
2214 use crate::types::VarLenUnicode;
2216 let attr = ds
2217 .new_attr::<VarLenUnicode>()
2218 .shape(())
2219 .create("units")
2220 .unwrap();
2221 attr.write_string("meters").unwrap();
2222
2223 file.close().unwrap();
2224 }
2225
2226 let output = std::process::Command::new("h5dump")
2228 .arg("-H") .arg(path.to_str().unwrap())
2230 .output()
2231 .unwrap();
2232
2233 assert!(
2234 output.status.success(),
2235 "h5dump failed:\nstdout: {}\nstderr: {}",
2236 String::from_utf8_lossy(&output.stdout),
2237 String::from_utf8_lossy(&output.stderr),
2238 );
2239
2240 let output2 = std::process::Command::new("h5dump")
2242 .arg(path.to_str().unwrap())
2243 .output()
2244 .unwrap();
2245
2246 assert!(
2247 output2.status.success(),
2248 "h5dump (full) failed:\nstderr: {}",
2249 String::from_utf8_lossy(&output2.stderr),
2250 );
2251
2252 std::fs::remove_file(&path).ok();
2253 }
2254
2255 #[test]
2256 fn read_h5py_generated_file() {
2257 let path = "/tmp/test_h5py_default.h5";
2258 if !std::path::Path::new(path).exists() {
2259 eprintln!("skipping: h5py test file not found");
2260 return;
2261 }
2262 let file = H5File::open(path).unwrap();
2263
2264 let ds = file.dataset("data").unwrap();
2265 assert_eq!(ds.shape(), vec![4, 5]);
2266 let data = ds.read_raw::<f64>().unwrap();
2267 assert_eq!(data.len(), 20);
2268 assert!((data[0]).abs() < 1e-10);
2269 assert!((data[19] - 19.0).abs() < 1e-10);
2270
2271 let ds2 = file.dataset("images").unwrap();
2272 assert_eq!(ds2.shape(), vec![3, 64, 64]);
2273 let images = ds2.read_raw::<u16>().unwrap();
2274 assert_eq!(images.len(), 3 * 64 * 64);
2275 }
2276}