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