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