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 close_no_sync_chunked_dataset_valid() {
913 let path = temp_path("close_no_sync_chunked");
918
919 {
920 let file = H5File::create(&path).unwrap();
921 let ds = file
922 .new_dataset::<i32>()
923 .shape([0usize, 3])
924 .chunk(&[1, 3])
925 .max_shape(&[None, Some(3)])
926 .create("data")
927 .unwrap();
928 for frame in 0..10u64 {
930 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
931 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
932 ds.write_chunk(frame as usize, &raw).unwrap();
933 }
934 ds.extend(&[10, 3]).unwrap();
935 file.close_no_sync().unwrap();
936 }
937
938 {
939 let file = H5File::open(&path).unwrap();
940 let ds = file.dataset("data").unwrap();
941 assert_eq!(ds.shape(), vec![10, 3]);
942 let data = ds.read_raw::<i32>().unwrap();
943 let expected: Vec<i32> = (0..30).collect();
944 assert_eq!(data, expected);
945 file.close().unwrap();
946 }
947
948 std::fs::remove_file(&path).ok();
949 }
950
951 #[test]
952 fn write_and_read_f64() {
953 let path = temp_path("write_read_f64");
954
955 let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
956
957 {
959 let file = H5File::create(&path).unwrap();
960 let ds = file
961 .new_dataset::<f64>()
962 .shape([2, 3])
963 .create("matrix")
964 .unwrap();
965 ds.write_raw(&values).unwrap();
966 file.close().unwrap();
967 }
968
969 {
971 let file = H5File::open(&path).unwrap();
972 let ds = file.dataset("matrix").unwrap();
973 assert_eq!(ds.shape(), vec![2, 3]);
974 let readback = ds.read_raw::<f64>().unwrap();
975 assert_eq!(readback, values);
976 }
977
978 std::fs::remove_file(&path).ok();
979 }
980
981 #[test]
982 fn multiple_datasets() {
983 let path = temp_path("multi_ds");
984
985 {
986 let file = H5File::create(&path).unwrap();
987 let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
988 ds1.write_raw(&[10i32, 20, 30]).unwrap();
989
990 let ds2 = file
991 .new_dataset::<f32>()
992 .shape([2, 2])
993 .create("floats")
994 .unwrap();
995 ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
996
997 file.close().unwrap();
998 }
999
1000 {
1001 let file = H5File::open(&path).unwrap();
1002
1003 let ds_ints = file.dataset("ints").unwrap();
1004 assert_eq!(ds_ints.shape(), vec![3]);
1005 let ints = ds_ints.read_raw::<i32>().unwrap();
1006 assert_eq!(ints, vec![10, 20, 30]);
1007
1008 let ds_floats = file.dataset("floats").unwrap();
1009 assert_eq!(ds_floats.shape(), vec![2, 2]);
1010 let floats = ds_floats.read_raw::<f32>().unwrap();
1011 assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
1012 }
1013
1014 std::fs::remove_file(&path).ok();
1015 }
1016
1017 #[test]
1018 fn close_is_idempotent() {
1019 let path = temp_path("close_idemp");
1020 let file = H5File::create(&path).unwrap();
1021 file.close().unwrap();
1022 std::fs::remove_file(&path).ok();
1024 }
1025}
1026
1027#[cfg(test)]
1028mod integration_tests {
1029 use super::*;
1030
1031 fn temp_path(name: &str) -> std::path::PathBuf {
1032 super::unique_test_path(name)
1033 }
1034
1035 #[test]
1036 fn write_file_for_h5dump() {
1037 let path = temp_path("integration");
1038 let file = H5File::create(&path).unwrap();
1039
1040 let ds = file
1041 .new_dataset::<u8>()
1042 .shape([4usize, 4])
1043 .create("data_u8")
1044 .unwrap();
1045 let data: Vec<u8> = (0..16).collect();
1046 ds.write_raw(&data).unwrap();
1047
1048 let ds2 = file
1049 .new_dataset::<f64>()
1050 .shape([3usize, 2])
1051 .create("data_f64")
1052 .unwrap();
1053 let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1054 ds2.write_raw(&fdata).unwrap();
1055
1056 let ds3 = file
1057 .new_dataset::<i32>()
1058 .shape([5usize])
1059 .create("values")
1060 .unwrap();
1061 let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
1062 ds3.write_raw(&idata).unwrap();
1063
1064 file.close().unwrap();
1065
1066 assert!(path.exists());
1068 }
1069
1070 #[test]
1071 fn write_chunked_file_for_h5dump() {
1072 let path = temp_path("chunked");
1073 let file = H5File::create(&path).unwrap();
1074
1075 let ds = file
1077 .new_dataset::<f64>()
1078 .shape([0usize, 4])
1079 .chunk(&[1, 4])
1080 .max_shape(&[None, Some(4)])
1081 .create("streaming_data")
1082 .unwrap();
1083
1084 for frame in 0..5u64 {
1086 let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1087 let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1088 ds.write_chunk(frame as usize, &raw).unwrap();
1089 }
1090
1091 ds.extend(&[5, 4]).unwrap();
1093 ds.flush().unwrap();
1094
1095 file.close().unwrap();
1096
1097 assert!(path.exists());
1098 }
1099
1100 #[test]
1101 fn write_chunked_many_frames_for_h5dump() {
1102 let path = temp_path("chunked_many");
1103 let file = H5File::create(&path).unwrap();
1104
1105 let ds = file
1106 .new_dataset::<i32>()
1107 .shape([0usize, 3])
1108 .chunk(&[1, 3])
1109 .max_shape(&[None, Some(3)])
1110 .create("data")
1111 .unwrap();
1112
1113 for frame in 0..10u64 {
1115 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1116 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1117 ds.write_chunk(frame as usize, &raw).unwrap();
1118 }
1119 ds.extend(&[10, 3]).unwrap();
1120 file.close().unwrap();
1121
1122 assert!(path.exists());
1123 }
1124
1125 #[test]
1126 fn write_dataset_with_attributes() {
1127 use crate::types::VarLenUnicode;
1128
1129 let path = temp_path("attributes");
1130 let file = H5File::create(&path).unwrap();
1131
1132 let ds = file
1133 .new_dataset::<f32>()
1134 .shape([10usize])
1135 .create("temperature")
1136 .unwrap();
1137 let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
1138 ds.write_raw(&data).unwrap();
1139
1140 let attr = ds
1142 .new_attr::<VarLenUnicode>()
1143 .shape(())
1144 .create("units")
1145 .unwrap();
1146 attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
1147 .unwrap();
1148
1149 let attr2 = ds
1150 .new_attr::<VarLenUnicode>()
1151 .shape(())
1152 .create("description")
1153 .unwrap();
1154 attr2
1155 .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
1156 .unwrap();
1157
1158 let attr3 = ds
1160 .new_attr::<VarLenUnicode>()
1161 .shape(())
1162 .create("source")
1163 .unwrap();
1164 attr3.write_string("sensor_01").unwrap();
1165
1166 let attr4 = ds
1168 .new_attr::<VarLenUnicode>()
1169 .shape(())
1170 .create("label")
1171 .unwrap();
1172 let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
1173 attr4.write_scalar(&s).unwrap();
1174
1175 file.close().unwrap();
1176
1177 assert!(path.exists());
1178 }
1179
1180 #[test]
1181 fn dataset_writer_reopens_for_attributes() {
1182 let path = temp_path("dataset_writer");
1185 {
1186 let file = H5File::create(&path).unwrap();
1187 {
1188 let ds = file
1189 .new_dataset::<u16>()
1190 .shape([8])
1191 .create("image")
1192 .unwrap();
1193 ds.write_raw(&[0u16; 8]).unwrap();
1194 }
1196
1197 assert!(file.dataset("image").is_err());
1199 let ds = file.dataset_writer("image").unwrap();
1200 assert_eq!(ds.shape(), vec![8]);
1201 ds.new_attr::<i32>()
1202 .shape([3])
1203 .create("NDArrayDimOffset")
1204 .unwrap()
1205 .write_array(&[0i32, 4, 8])
1206 .unwrap();
1207 ds.new_attr::<i32>()
1208 .shape(())
1209 .create("NDUniqueId")
1210 .unwrap()
1211 .write_numeric(&42i32)
1212 .unwrap();
1213
1214 assert!(matches!(
1216 file.dataset_writer("nope"),
1217 Err(crate::error::Hdf5Error::NotFound(_))
1218 ));
1219
1220 file.close().unwrap();
1221 }
1222 {
1223 let file = H5File::open(&path).unwrap();
1224 let ds = file.dataset("image").unwrap();
1225 let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
1226 let got: Vec<i32> = off
1227 .chunks_exact(4)
1228 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1229 .collect();
1230 assert_eq!(got, vec![0, 4, 8]);
1231 let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
1232 assert_eq!(uid, 42);
1233 }
1234 std::fs::remove_file(&path).ok();
1235 }
1236
1237 #[test]
1238 fn chunked_write_read_roundtrip() {
1239 let path = temp_path("chunked_roundtrip");
1240
1241 {
1243 let file = H5File::create(&path).unwrap();
1244 let ds = file
1245 .new_dataset::<i32>()
1246 .shape([0usize, 3])
1247 .chunk(&[1, 3])
1248 .max_shape(&[None, Some(3)])
1249 .create("table")
1250 .unwrap();
1251
1252 for frame in 0..8u64 {
1253 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1254 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1255 ds.write_chunk(frame as usize, &raw).unwrap();
1256 }
1257 ds.extend(&[8, 3]).unwrap();
1258 file.close().unwrap();
1259 }
1260
1261 {
1263 let file = H5File::open(&path).unwrap();
1264 let ds = file.dataset("table").unwrap();
1265 assert_eq!(ds.shape(), vec![8, 3]);
1266 let data = ds.read_raw::<i32>().unwrap();
1267 assert_eq!(data.len(), 24);
1268 for (i, val) in data.iter().enumerate() {
1269 assert_eq!(*val, i as i32);
1270 }
1271 }
1272
1273 std::fs::remove_file(&path).ok();
1274 }
1275
1276 #[test]
1277 #[cfg(feature = "deflate")]
1278 fn compressed_chunked_roundtrip() {
1279 let path = temp_path("compressed_roundtrip");
1280
1281 {
1283 let file = H5File::create(&path).unwrap();
1284 let ds = file
1285 .new_dataset::<f64>()
1286 .shape([0usize, 4])
1287 .chunk(&[1, 4])
1288 .max_shape(&[None, Some(4)])
1289 .deflate(6)
1290 .create("compressed")
1291 .unwrap();
1292
1293 for frame in 0..10u64 {
1294 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1295 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1296 ds.write_chunk(frame as usize, &raw).unwrap();
1297 }
1298 ds.extend(&[10, 4]).unwrap();
1299 file.close().unwrap();
1300 }
1301
1302 {
1304 let file = H5File::open(&path).unwrap();
1305 let ds = file.dataset("compressed").unwrap();
1306 assert_eq!(ds.shape(), vec![10, 4]);
1307 let data = ds.read_raw::<f64>().unwrap();
1308 assert_eq!(data.len(), 40);
1309 for (i, val) in data.iter().enumerate() {
1310 assert!(
1311 (val - i as f64).abs() < 1e-10,
1312 "mismatch at {}: {} != {}",
1313 i,
1314 val,
1315 i
1316 );
1317 }
1318 }
1319
1320 std::fs::remove_file(&path).ok();
1321 }
1322
1323 #[test]
1324 #[cfg(feature = "deflate")]
1325 fn compressed_chunked_many_frames() {
1326 let path = temp_path("compressed_many");
1327
1328 {
1329 let file = H5File::create(&path).unwrap();
1330 let ds = file
1331 .new_dataset::<i32>()
1332 .shape([0usize, 3])
1333 .chunk(&[1, 3])
1334 .max_shape(&[None, Some(3)])
1335 .deflate(6)
1336 .create("stream")
1337 .unwrap();
1338
1339 for frame in 0..100u64 {
1340 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1341 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1342 ds.write_chunk(frame as usize, &raw).unwrap();
1343 }
1344 ds.extend(&[100, 3]).unwrap();
1345 file.close().unwrap();
1346 }
1347
1348 {
1349 let file = H5File::open(&path).unwrap();
1350 let ds = file.dataset("stream").unwrap();
1351 assert_eq!(ds.shape(), vec![100, 3]);
1352 let data = ds.read_raw::<i32>().unwrap();
1353 assert_eq!(data.len(), 300);
1354 for (i, val) in data.iter().enumerate() {
1355 assert_eq!(*val, i as i32, "mismatch at {}", i);
1356 }
1357 }
1358
1359 std::fs::remove_file(&path).ok();
1360 }
1361 #[test]
1362 fn append_mode() {
1363 let path = temp_path("append");
1364
1365 {
1367 let file = H5File::create(&path).unwrap();
1368 let ds = file
1369 .new_dataset::<i32>()
1370 .shape([3usize])
1371 .create("first")
1372 .unwrap();
1373 ds.write_raw(&[1i32, 2, 3]).unwrap();
1374 file.close().unwrap();
1375 }
1376
1377 {
1379 let file = H5File::open_rw(&path).unwrap();
1380 let ds = file
1381 .new_dataset::<f64>()
1382 .shape([2usize])
1383 .create("second")
1384 .unwrap();
1385 ds.write_raw(&[4.0f64, 5.0]).unwrap();
1386 file.close().unwrap();
1387 }
1388
1389 {
1391 let file = H5File::open(&path).unwrap();
1392 let names = file.dataset_names();
1393 assert!(names.contains(&"first".to_string()));
1394 assert!(names.contains(&"second".to_string()));
1395
1396 let ds1 = file.dataset("first").unwrap();
1397 assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
1398
1399 let ds2 = file.dataset("second").unwrap();
1400 assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
1401 }
1402
1403 std::fs::remove_file(&path).ok();
1404 }
1405
1406 #[test]
1407 fn open_rw_set_attr_preserves_file() {
1408 let path = temp_path("open_rw_attr");
1409 {
1411 let file = H5File::create(&path).unwrap();
1412 let ds = file
1413 .new_dataset::<i32>()
1414 .shape([3usize])
1415 .create("data")
1416 .unwrap();
1417 ds.write_raw(&[10i32, 20, 30]).unwrap();
1418 file.set_attr_string("version", "1.0").unwrap();
1419 file.close().unwrap();
1420 }
1421 {
1423 let file = H5File::open_rw(&path).unwrap();
1424 file.set_attr_string("version", "2.0").unwrap();
1425 file.close().unwrap();
1426 }
1427 {
1429 let file = H5File::open(&path).unwrap();
1430 let ds = file.dataset("data").unwrap();
1431 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
1432 let ver = file.attr_string("version").unwrap();
1433 assert_eq!(ver, "2.0");
1434 }
1435 std::fs::remove_file(&path).ok();
1436 }
1437
1438 #[test]
1439 #[cfg(feature = "deflate")]
1440 fn open_rw_attr_with_compressed_dataset() {
1441 use crate::format::messages::filter::FilterPipeline;
1442 let path = temp_path("open_rw_compressed");
1443 let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
1444 {
1446 let file = H5File::create(&path).unwrap();
1447 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1448 .unwrap();
1449 file.set_attr_string("version", "1.0").unwrap();
1450 file.close().unwrap();
1451 }
1452 {
1454 let file = H5File::open_rw(&path).unwrap();
1455 file.set_attr_string("version", "2.0").unwrap();
1456 file.close().unwrap();
1457 }
1458 {
1460 let file = H5File::open(&path).unwrap();
1461 let ds = file.dataset("texts").unwrap();
1462 let strings = ds.read_vlen_strings().unwrap();
1463 assert_eq!(strings.len(), 50);
1464 assert_eq!(strings[0], "test string data");
1465 let ver = file.attr_string("version").unwrap();
1466 assert_eq!(ver, "2.0");
1467 }
1468 std::fs::remove_file(&path).ok();
1469 }
1470
1471 #[test]
1472 #[cfg(feature = "lz4")]
1473 fn append_vlen_strings_basic() {
1474 use crate::format::messages::filter::FilterPipeline;
1475 let path = temp_path("append_vlen");
1476 {
1477 let file = H5File::create(&path).unwrap();
1478 file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
1479 .unwrap();
1480 file.append_vlen_strings("names", &["alice", "bob", "charlie"])
1481 .unwrap();
1482 file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
1483 file.close().unwrap();
1484 }
1485 {
1486 let file = H5File::open(&path).unwrap();
1487 let ds = file.dataset("names").unwrap();
1488 let strings = ds.read_vlen_strings().unwrap();
1489 assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
1490 }
1491 std::fs::remove_file(&path).ok();
1492 }
1493
1494 #[test]
1495 #[cfg(feature = "lz4")]
1496 fn append_vlen_strings_large() {
1497 use crate::format::messages::filter::FilterPipeline;
1498 let path = temp_path("append_vlen_large");
1499 let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
1500 let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
1501 {
1502 let file = H5File::create(&path).unwrap();
1503 file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
1504 .unwrap();
1505 let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
1506 file.append_vlen_strings("data", &r1).unwrap();
1507 let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
1508 file.append_vlen_strings("data", &r2).unwrap();
1509 file.close().unwrap();
1510 }
1511 {
1512 let file = H5File::open(&path).unwrap();
1513 let ds = file.dataset("data").unwrap();
1514 let strings = ds.read_vlen_strings().unwrap();
1515 assert_eq!(strings.len(), 7189);
1516 assert_eq!(strings[0], "node-000000");
1517 assert_eq!(strings[7188], "node-007188");
1518 }
1519 std::fs::remove_file(&path).ok();
1520 }
1521
1522 #[test]
1523 fn append_vlen_strings_uncompressed() {
1524 let path = temp_path("append_vlen_unc");
1525 {
1526 let file = H5File::create(&path).unwrap();
1527 file.create_appendable_vlen_dataset("texts", 8, None)
1528 .unwrap();
1529 file.append_vlen_strings("texts", &["hello", "world"])
1530 .unwrap();
1531 file.append_vlen_strings("texts", &["foo", "bar", "baz"])
1532 .unwrap();
1533 file.close().unwrap();
1534 }
1535 {
1536 let file = H5File::open(&path).unwrap();
1537 let ds = file.dataset("texts").unwrap();
1538 let strings = ds.read_vlen_strings().unwrap();
1539 assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
1540 }
1541 std::fs::remove_file(&path).ok();
1542 }
1543
1544 #[test]
1545 fn delete_dataset_roundtrip() {
1546 let path = temp_path("delete_ds");
1547 {
1548 let file = H5File::create(&path).unwrap();
1549 file.write_vlen_strings("keep", &["a", "b"]).unwrap();
1550 file.write_vlen_strings("remove", &["x", "y"]).unwrap();
1551 file.delete_dataset("remove").unwrap();
1552 file.close().unwrap();
1553 }
1554 {
1555 let file = H5File::open(&path).unwrap();
1556 let names = file.dataset_names();
1557 assert!(names.contains(&"keep".to_string()));
1558 assert!(!names.contains(&"remove".to_string()));
1559 let ds = file.dataset("keep").unwrap();
1560 assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
1561 }
1562 std::fs::remove_file(&path).ok();
1563 }
1564
1565 #[test]
1566 fn delete_group_roundtrip() {
1567 let path = temp_path("delete_grp");
1568 {
1569 let file = H5File::create(&path).unwrap();
1570 let g1 = file.create_group("keep").unwrap();
1571 g1.write_vlen_strings("data", &["a"]).unwrap();
1572 let g2 = file.create_group("remove").unwrap();
1573 g2.write_vlen_strings("data", &["x"]).unwrap();
1574 file.delete_group("remove").unwrap();
1575 file.close().unwrap();
1576 }
1577 {
1578 let file = H5File::open(&path).unwrap();
1579 let names = file.dataset_names();
1580 assert!(names.contains(&"keep/data".to_string()));
1581 assert!(!names.contains(&"remove/data".to_string()));
1582 }
1583 std::fs::remove_file(&path).ok();
1584 }
1585
1586 #[test]
1587 fn open_rw_delete_recreate_group() {
1588 let path = temp_path("rw_delete_recreate");
1589 {
1591 let file = H5File::create(&path).unwrap();
1592 let n = file.create_group("nodes").unwrap();
1593 n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
1594 let e = file.create_group("edges").unwrap();
1595 e.write_vlen_strings("src", &["x", "y"]).unwrap();
1596 file.close().unwrap();
1597 }
1598 {
1600 let file = H5File::open_rw(&path).unwrap();
1601 file.delete_group("nodes").unwrap();
1602 let n = file.create_group("nodes").unwrap();
1603 n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
1604 file.close().unwrap();
1605 }
1606 {
1608 let file = H5File::open(&path).unwrap();
1609 let ds = file.dataset("nodes/id").unwrap();
1610 let s = ds.read_vlen_strings().unwrap();
1611 assert_eq!(s, vec!["new1", "new2"]);
1612 let ds = file.dataset("edges/src").unwrap();
1614 let s = ds.read_vlen_strings().unwrap();
1615 assert_eq!(s, vec!["x", "y"]);
1616 }
1617 std::fs::remove_file(&path).ok();
1618 }
1619
1620 #[test]
1621 fn delete_and_recreate_group() {
1622 let path = temp_path("delete_recreate");
1623 {
1624 let file = H5File::create(&path).unwrap();
1625 let g = file.create_group("nodes").unwrap();
1626 g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
1627 file.delete_group("nodes").unwrap();
1628 let g = file.create_group("nodes").unwrap();
1629 g.write_vlen_strings("id", &["new1", "new2", "new3"])
1630 .unwrap();
1631 file.close().unwrap();
1632 }
1633 {
1634 let file = H5File::open(&path).unwrap();
1635 let ds = file.dataset("nodes/id").unwrap();
1636 let strings = ds.read_vlen_strings().unwrap();
1637 assert_eq!(strings, vec!["new1", "new2", "new3"]);
1638 }
1639 std::fs::remove_file(&path).ok();
1640 }
1641
1642 #[test]
1643 #[cfg(feature = "deflate")]
1644 fn vlen_string_compressed_large_roundtrip() {
1645 use crate::format::messages::filter::FilterPipeline;
1646 let path = temp_path("vlen_large");
1647 let input: Vec<String> = (0..7189)
1649 .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
1650 .collect();
1651 let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
1652 {
1653 let file = H5File::create(&path).unwrap();
1654 file.create_group("nodes").unwrap();
1655 file.write_vlen_strings_compressed(
1656 "nodes/id",
1657 &input_refs,
1658 512,
1659 FilterPipeline::deflate(6),
1660 )
1661 .unwrap();
1662 file.close().unwrap();
1663 }
1664 {
1666 let file = H5File::open(&path).unwrap();
1667 let ds = file.dataset("nodes/id").unwrap();
1668 let strings = ds.read_vlen_strings().unwrap();
1669 assert_eq!(strings.len(), 7189);
1670 assert_eq!(strings[0], input[0]);
1671 assert_eq!(strings[7188], input[7188]);
1672 }
1673 {
1675 let file = H5File::open_rw(&path).unwrap();
1676 file.set_attr_string("version", "1.0").unwrap();
1677 file.close().unwrap();
1678 }
1679 {
1680 let file = H5File::open(&path).unwrap();
1681 let ds = file.dataset("nodes/id").unwrap();
1682 let strings = ds.read_vlen_strings().unwrap();
1683 assert_eq!(strings.len(), 7189);
1684 assert_eq!(strings[0], input[0]);
1685 }
1686 std::fs::remove_file(&path).ok();
1687 }
1688
1689 #[test]
1690 fn vlen_string_write_read() {
1691 let path = temp_path("vlen_wr");
1692 {
1693 let file = H5File::create(&path).unwrap();
1694 file.write_vlen_strings("names", &["alice", "bob", "charlie"])
1695 .unwrap();
1696 file.close().unwrap();
1697 }
1698 {
1699 let file = H5File::open(&path).unwrap();
1700 let ds = file.dataset("names").unwrap();
1701 let strings = ds.read_vlen_strings().unwrap();
1702 assert_eq!(strings, vec!["alice", "bob", "charlie"]);
1703 }
1704 std::fs::remove_file(&path).ok();
1705 }
1706
1707 #[test]
1708 fn vlen_bytes_write_read() {
1709 let path = temp_path("vlen_bytes_wr");
1710 let items: [&[u8]; 4] = [b"abc", b"", &[0u8, 1, 2, 255], b"hi"];
1711 {
1712 let file = H5File::create(&path).unwrap();
1713 file.write_vlen_bytes("blobs", &items).unwrap();
1714 file.close().unwrap();
1715 }
1716 {
1717 let file = H5File::open(&path).unwrap();
1718 let ds = file.dataset("blobs").unwrap();
1719 let got = ds.read_vlen_bytes().unwrap();
1720 let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1721 assert_eq!(got, expected);
1722 }
1723 std::fs::remove_file(&path).ok();
1724 }
1725
1726 #[test]
1727 fn vlen_bytes_in_group_with_attribute() {
1728 use crate::types::VarLenUnicode;
1729 let path = temp_path("vlen_bytes_grp");
1730 let items: [&[u8]; 2] = [&[1u8, 2, 3], &[9u8, 8, 7, 6]];
1731 {
1732 let file = H5File::create(&path).unwrap();
1733 let grp = file.root_group().create_group("payloads").unwrap();
1734 let ds = grp.write_vlen_bytes("frames", &items).unwrap();
1735 ds.new_attr::<VarLenUnicode>()
1736 .shape(())
1737 .create("codec")
1738 .unwrap()
1739 .write_string("raw")
1740 .unwrap();
1741 file.close().unwrap();
1742 }
1743 {
1744 let file = H5File::open(&path).unwrap();
1745 let ds = file.dataset("payloads/frames").unwrap();
1746 let got = ds.read_vlen_bytes().unwrap();
1747 let expected: Vec<Vec<u8>> = items.iter().map(|s| s.to_vec()).collect();
1748 assert_eq!(got, expected);
1749 }
1750 std::fs::remove_file(&path).ok();
1751 }
1752
1753 #[test]
1754 fn vlen_dataset_returns_handle_for_attributes() {
1755 use crate::types::VarLenUnicode;
1756 let path = temp_path("vlen_attr");
1757 {
1758 let file = H5File::create(&path).unwrap();
1759 let grp = file.root_group().create_group("ch").unwrap();
1760 let ds = grp
1763 .write_vlen_strings("labels", &["a", "bb", "ccc"])
1764 .unwrap();
1765 ds.new_attr::<VarLenUnicode>()
1766 .shape(())
1767 .create("unit")
1768 .unwrap()
1769 .write_string("volt")
1770 .unwrap();
1771 let ds2 = grp.dataset_writer("labels").unwrap();
1773 ds2.new_attr::<VarLenUnicode>()
1774 .shape(())
1775 .create("desc")
1776 .unwrap()
1777 .write_string("channel labels")
1778 .unwrap();
1779 file.close().unwrap();
1780 }
1781 {
1782 let file = H5File::open(&path).unwrap();
1783 let ds = file.dataset("ch/labels").unwrap();
1784 assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "bb", "ccc"]);
1785 assert_eq!(ds.attr("unit").unwrap().read_string().unwrap(), "volt");
1786 assert_eq!(
1787 ds.attr("desc").unwrap().read_string().unwrap(),
1788 "channel labels"
1789 );
1790 }
1791 std::fs::remove_file(&path).ok();
1792 }
1793
1794 #[test]
1795 #[cfg(feature = "deflate")]
1796 fn vlen_string_deflate_roundtrip() {
1797 use crate::format::messages::filter::FilterPipeline;
1798 let path = temp_path("vlen_deflate");
1799 let input: Vec<&str> = (0..100)
1800 .map(|i| match i % 3 {
1801 0 => "hello world",
1802 1 => "compressed vlen string test",
1803 _ => "rust-hdf5",
1804 })
1805 .collect();
1806 {
1807 let file = H5File::create(&path).unwrap();
1808 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1809 .unwrap();
1810 file.close().unwrap();
1811 }
1812 {
1813 let file = H5File::open(&path).unwrap();
1814 let ds = file.dataset("texts").unwrap();
1815 let strings = ds.read_vlen_strings().unwrap();
1816 assert_eq!(strings.len(), 100);
1817 for (i, s) in strings.iter().enumerate() {
1818 assert_eq!(s, input[i]);
1819 }
1820 }
1821 std::fs::remove_file(&path).ok();
1822 }
1823
1824 #[test]
1825 #[cfg(feature = "zstd")]
1826 fn vlen_string_zstd_roundtrip() {
1827 use crate::format::messages::filter::FilterPipeline;
1828 let path = temp_path("vlen_zstd");
1829 let input: Vec<&str> = (0..200)
1830 .map(|i| match i % 4 {
1831 0 => "zstandard compression test",
1832 1 => "variable length string",
1833 2 => "rust-hdf5 chunked storage",
1834 _ => "hello zstd world",
1835 })
1836 .collect();
1837 {
1838 let file = H5File::create(&path).unwrap();
1839 file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
1840 .unwrap();
1841 file.close().unwrap();
1842 }
1843 {
1844 let file = H5File::open(&path).unwrap();
1845 let ds = file.dataset("data").unwrap();
1846 let strings = ds.read_vlen_strings().unwrap();
1847 assert_eq!(strings.len(), 200);
1848 for (i, s) in strings.iter().enumerate() {
1849 assert_eq!(s, input[i]);
1850 }
1851 }
1852 std::fs::remove_file(&path).ok();
1853 }
1854
1855 #[test]
1856 #[cfg(feature = "deflate")]
1857 fn shuffle_deflate_roundtrip() {
1858 let path = temp_path("shuf_defl");
1859 {
1860 let file = H5File::create(&path).unwrap();
1861 let ds = file
1862 .new_dataset::<f64>()
1863 .shape([0usize, 4])
1864 .chunk(&[1, 4])
1865 .max_shape(&[None, Some(4)])
1866 .shuffle_deflate(6)
1867 .create("data")
1868 .unwrap();
1869 for frame in 0..20u64 {
1870 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1871 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1872 ds.write_chunk(frame as usize, &raw).unwrap();
1873 }
1874 ds.extend(&[20, 4]).unwrap();
1875 file.close().unwrap();
1876 }
1877 {
1878 let file = H5File::open(&path).unwrap();
1879 let ds = file.dataset("data").unwrap();
1880 assert_eq!(ds.shape(), vec![20, 4]);
1881 let data = ds.read_raw::<f64>().unwrap();
1882 assert_eq!(data.len(), 80);
1883 for (i, val) in data.iter().enumerate() {
1884 assert!((val - i as f64).abs() < 1e-10);
1885 }
1886 }
1887 std::fs::remove_file(&path).ok();
1888 }
1889
1890 #[test]
1891 fn file_level_attributes() {
1892 let path = temp_path("file_attr");
1893 {
1894 let file = H5File::create(&path).unwrap();
1895 file.set_attr_string("title", "Test File").unwrap();
1896 file.set_attr_numeric("version", &42i32).unwrap();
1897 let ds = file
1898 .new_dataset::<u8>()
1899 .shape([1usize])
1900 .create("dummy")
1901 .unwrap();
1902 ds.write_raw(&[0u8]).unwrap();
1903 file.close().unwrap();
1904 }
1905 {
1906 let file = H5File::open(&path).unwrap();
1907 assert!(file.dataset_names().contains(&"dummy".to_string()));
1908
1909 let names = file.attr_names().unwrap();
1911 assert!(names.contains(&"title".to_string()));
1912
1913 let title = file.attr_string("title").unwrap();
1914 assert_eq!(title, "Test File");
1915 }
1916 std::fs::remove_file(&path).ok();
1917 }
1918
1919 #[test]
1920 fn scalar_dataset_roundtrip() {
1921 let path = temp_path("scalar");
1922 {
1923 let file = H5File::create(&path).unwrap();
1924 let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
1925 ds.write_raw(&[std::f64::consts::PI]).unwrap();
1926 file.close().unwrap();
1927 }
1928 {
1929 let file = H5File::open(&path).unwrap();
1930 let ds = file.dataset("pi").unwrap();
1931 assert_eq!(ds.shape(), Vec::<usize>::new());
1932 assert_eq!(ds.total_elements(), 1);
1933 let data = ds.read_raw::<f64>().unwrap();
1934 assert_eq!(data.len(), 1);
1935 assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
1936 }
1937 std::fs::remove_file(&path).ok();
1938 }
1939
1940 #[test]
1941 fn append_mode_extend_chunked() {
1942 let path = temp_path("append_extend");
1943
1944 {
1946 let file = H5File::create(&path).unwrap();
1947 let ds = file
1948 .new_dataset::<i32>()
1949 .shape([0usize, 3])
1950 .chunk(&[1, 3])
1951 .max_shape(&[None, Some(3)])
1952 .create("stream")
1953 .unwrap();
1954 for i in 0..5u64 {
1955 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1956 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1957 ds.write_chunk(i as usize, &raw).unwrap();
1958 }
1959 ds.extend(&[5, 3]).unwrap();
1960 file.close().unwrap();
1961 }
1962
1963 {
1965 let file = H5File::open_rw(&path).unwrap();
1966 let names = file.dataset_names();
1968 assert!(names.contains(&"stream".to_string()));
1969
1970 let mut inner = crate::file::borrow_inner_mut(&file.inner);
1972 if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
1973 let ds_idx = writer.dataset_index("stream").unwrap();
1974 for i in 5..10u64 {
1975 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1976 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1977 writer.write_chunk(ds_idx, i, &raw).unwrap();
1978 }
1979 writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
1980 }
1981 drop(inner);
1982 file.close().unwrap();
1983 }
1984
1985 {
1987 let file = H5File::open(&path).unwrap();
1988 let ds = file.dataset("stream").unwrap();
1989 assert_eq!(ds.shape(), vec![10, 3]);
1990 let data = ds.read_raw::<i32>().unwrap();
1991 assert_eq!(data.len(), 30);
1992 for (i, val) in data.iter().enumerate() {
1993 assert_eq!(*val, i as i32, "mismatch at {}", i);
1994 }
1995 }
1996
1997 std::fs::remove_file(&path).ok();
1998 }
1999
2000 #[test]
2001 fn group_hierarchy_roundtrip() {
2002 let path = temp_path("groups_rt");
2003
2004 {
2005 let file = H5File::create(&path).unwrap();
2006 let root = file.root_group();
2007
2008 let det = root.create_group("detector").unwrap();
2010 let raw = det.create_group("raw").unwrap();
2011
2012 let ds1 = det
2014 .new_dataset::<f32>()
2015 .shape([10usize])
2016 .create("temperature")
2017 .unwrap();
2018 ds1.write_raw(&[1.0f32; 10]).unwrap();
2019
2020 let ds2 = raw
2021 .new_dataset::<u16>()
2022 .shape([4usize, 4])
2023 .create("image")
2024 .unwrap();
2025 ds2.write_raw(&[42u16; 16]).unwrap();
2026
2027 let ds3 = file
2029 .new_dataset::<i32>()
2030 .shape([3usize])
2031 .create("version")
2032 .unwrap();
2033 ds3.write_raw(&[1i32, 0, 0]).unwrap();
2034
2035 file.close().unwrap();
2036 }
2037
2038 {
2039 let file = H5File::open(&path).unwrap();
2040 let names = file.dataset_names();
2041 assert!(names.contains(&"version".to_string()));
2042 assert!(names.contains(&"detector/temperature".to_string()));
2043 assert!(names.contains(&"detector/raw/image".to_string()));
2044
2045 let ds = file.dataset("version").unwrap();
2047 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
2048
2049 let ds = file.dataset("detector/temperature").unwrap();
2050 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
2051
2052 let ds = file.dataset("detector/raw/image").unwrap();
2053 assert_eq!(ds.shape(), vec![4, 4]);
2054 assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
2055
2056 let root = file.root_group();
2058 let group_names = root.group_names().unwrap();
2059 assert!(group_names.contains(&"detector".to_string()));
2060 }
2061
2062 std::fs::remove_file(&path).ok();
2063 }
2064
2065 #[test]
2066 fn nested_groups_via_file_create_group() {
2067 let path = temp_path("file_create_group");
2068
2069 {
2070 let file = H5File::create(&path).unwrap();
2071
2072 let grp = file.create_group("sensors").unwrap();
2074 let sub = grp.create_group("accel").unwrap();
2075
2076 let ds = sub
2077 .new_dataset::<f64>()
2078 .shape([3usize])
2079 .create("xyz")
2080 .unwrap();
2081 ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
2082
2083 file.close().unwrap();
2084 }
2085
2086 {
2087 let file = H5File::open(&path).unwrap();
2088 let names = file.dataset_names();
2089 assert!(names.contains(&"sensors/accel/xyz".to_string()));
2090
2091 let ds = file.dataset("sensors/accel/xyz").unwrap();
2092 assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
2093
2094 let root = file.root_group();
2096 let sensors = root.group("sensors").unwrap();
2097 assert_eq!(sensors.name(), "/sensors");
2098
2099 let accel = sensors.group("accel").unwrap();
2100 assert_eq!(accel.name(), "/sensors/accel");
2101
2102 let top_groups = root.group_names().unwrap();
2104 assert!(top_groups.contains(&"sensors".to_string()));
2105
2106 let sub_groups = sensors.group_names().unwrap();
2108 assert!(sub_groups.contains(&"accel".to_string()));
2109 }
2110
2111 std::fs::remove_file(&path).ok();
2112 }
2113}
2114
2115#[cfg(test)]
2116mod h5py_compat_tests {
2117 use super::*;
2118
2119 #[cfg(feature = "deflate")]
2122 fn temp_path(name: &str) -> std::path::PathBuf {
2123 super::unique_test_path(name)
2124 }
2125
2126 #[test]
2128 #[cfg(feature = "deflate")]
2129 fn h5dump_validates_our_files() {
2130 let h5dump = std::process::Command::new("h5dump")
2132 .arg("--version")
2133 .output();
2134 if h5dump.is_err() {
2135 eprintln!("skipping: h5dump not found");
2136 return;
2137 }
2138
2139 let path = temp_path("h5dump_validate");
2140
2141 {
2143 let file = H5File::create(&path).unwrap();
2144
2145 let ds = file
2147 .new_dataset::<f64>()
2148 .shape([3usize, 4])
2149 .create("matrix")
2150 .unwrap();
2151 let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
2152 ds.write_raw(&data).unwrap();
2153
2154 let ds2 = file
2156 .new_dataset::<i32>()
2157 .shape([0usize, 2])
2158 .chunk(&[1, 2])
2159 .max_shape(&[None, Some(2)])
2160 .deflate(6)
2161 .create("stream")
2162 .unwrap();
2163 for i in 0..5u64 {
2164 let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
2165 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
2166 ds2.write_chunk(i as usize, &raw).unwrap();
2167 }
2168 ds2.extend(&[5, 2]).unwrap();
2169
2170 let grp = file.create_group("meta").unwrap();
2172 let ds3 = grp
2173 .new_dataset::<u8>()
2174 .shape([4usize])
2175 .create("flags")
2176 .unwrap();
2177 ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
2178
2179 use crate::types::VarLenUnicode;
2181 let attr = ds
2182 .new_attr::<VarLenUnicode>()
2183 .shape(())
2184 .create("units")
2185 .unwrap();
2186 attr.write_string("meters").unwrap();
2187
2188 file.close().unwrap();
2189 }
2190
2191 let output = std::process::Command::new("h5dump")
2193 .arg("-H") .arg(path.to_str().unwrap())
2195 .output()
2196 .unwrap();
2197
2198 assert!(
2199 output.status.success(),
2200 "h5dump failed:\nstdout: {}\nstderr: {}",
2201 String::from_utf8_lossy(&output.stdout),
2202 String::from_utf8_lossy(&output.stderr),
2203 );
2204
2205 let output2 = std::process::Command::new("h5dump")
2207 .arg(path.to_str().unwrap())
2208 .output()
2209 .unwrap();
2210
2211 assert!(
2212 output2.status.success(),
2213 "h5dump (full) failed:\nstderr: {}",
2214 String::from_utf8_lossy(&output2.stderr),
2215 );
2216
2217 std::fs::remove_file(&path).ok();
2218 }
2219
2220 #[test]
2221 fn read_h5py_generated_file() {
2222 let path = "/tmp/test_h5py_default.h5";
2223 if !std::path::Path::new(path).exists() {
2224 eprintln!("skipping: h5py test file not found");
2225 return;
2226 }
2227 let file = H5File::open(path).unwrap();
2228
2229 let ds = file.dataset("data").unwrap();
2230 assert_eq!(ds.shape(), vec![4, 5]);
2231 let data = ds.read_raw::<f64>().unwrap();
2232 assert_eq!(data.len(), 20);
2233 assert!((data[0]).abs() < 1e-10);
2234 assert!((data[19] - 19.0).abs() < 1e-10);
2235
2236 let ds2 = file.dataset("images").unwrap();
2237 assert_eq!(ds2.shape(), vec![3, 64, 64]);
2238 let images = ds2.read_raw::<u16>().unwrap();
2239 assert_eq!(images.len(), 3 * 64 * 64);
2240 }
2241}