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::Mutex<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")]
66pub(crate) fn borrow_inner(inner: &SharedInner) -> std::sync::MutexGuard<'_, H5FileInner> {
67 inner.lock().unwrap()
68}
69
70#[cfg(feature = "threadsafe")]
71pub(crate) fn borrow_inner_mut(inner: &SharedInner) -> std::sync::MutexGuard<'_, H5FileInner> {
72 inner.lock().unwrap()
73}
74
75#[cfg(feature = "threadsafe")]
76pub(crate) fn clone_inner(inner: &SharedInner) -> SharedInner {
77 std::sync::Arc::clone(inner)
78}
79
80#[cfg(feature = "threadsafe")]
81pub(crate) fn new_shared(inner: H5FileInner) -> SharedInner {
82 std::sync::Arc::new(std::sync::Mutex::new(inner))
83}
84
85pub(crate) enum H5FileInner {
91 Writer(Hdf5Writer),
92 Reader(Hdf5Reader),
93 Closed,
95}
96
97pub struct H5File {
103 pub(crate) inner: SharedInner,
104}
105
106impl H5File {
107 pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
109 let writer = Hdf5Writer::create(path.as_ref())?;
110 Ok(Self {
111 inner: new_shared(H5FileInner::Writer(writer)),
112 })
113 }
114
115 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
117 let reader = Hdf5Reader::open(path.as_ref())?;
118 Ok(Self {
119 inner: new_shared(H5FileInner::Reader(reader)),
120 })
121 }
122
123 pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
137 let writer = Hdf5Writer::open_append(path.as_ref())?;
138 Ok(Self {
139 inner: new_shared(H5FileInner::Writer(writer)),
140 })
141 }
142
143 pub fn options() -> H5FileOptions {
157 H5FileOptions::default()
158 }
159
160 pub fn root_group(&self) -> H5Group {
164 H5Group::new(clone_inner(&self.inner), "/".to_string())
165 }
166
167 pub fn create_group(&self, name: &str) -> Result<H5Group> {
175 self.root_group().create_group(name)
176 }
177
178 pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
189 DatasetBuilder::new(clone_inner(&self.inner))
190 }
191
192 pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
194 use crate::format::messages::attribute::AttributeMessage;
195 let attr = AttributeMessage::scalar_string(name, value);
196 let mut inner = borrow_inner_mut(&self.inner);
197 match &mut *inner {
198 H5FileInner::Writer(writer) => {
199 writer.add_root_attribute(attr);
200 Ok(())
201 }
202 _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
203 }
204 }
205
206 pub fn set_attr_numeric<T: crate::types::H5Type>(&self, name: &str, value: &T) -> Result<()> {
208 use crate::format::messages::attribute::AttributeMessage;
209 let es = T::element_size();
210 let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
211 let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), raw.to_vec());
212 let mut inner = borrow_inner_mut(&self.inner);
213 match &mut *inner {
214 H5FileInner::Writer(writer) => {
215 writer.add_root_attribute(attr);
216 Ok(())
217 }
218 _ => Err(Hdf5Error::InvalidState("cannot write in read mode".into())),
219 }
220 }
221
222 pub fn attr_names(&self) -> Result<Vec<String>> {
224 let inner = borrow_inner(&self.inner);
225 match &*inner {
226 H5FileInner::Reader(reader) => Ok(reader.root_attr_names()),
227 _ => Ok(vec![]),
228 }
229 }
230
231 pub fn attr_string(&self, name: &str) -> Result<String> {
233 let mut inner = borrow_inner_mut(&self.inner);
234 match &mut *inner {
235 H5FileInner::Reader(reader) => {
236 let attr = reader
237 .root_attr(name)
238 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
239 .clone();
240 Ok(reader.attr_string_value(&attr)?)
241 }
242 _ => Err(Hdf5Error::InvalidState("not in read mode".into())),
243 }
244 }
245
246 pub fn is_writable(&self) -> bool {
248 let inner = borrow_inner(&self.inner);
249 matches!(&*inner, H5FileInner::Writer(_))
250 }
251
252 pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
257 let mut inner = borrow_inner_mut(&self.inner);
258 match &mut *inner {
259 H5FileInner::Writer(writer) => {
260 let idx = writer.create_vlen_string_dataset(name, strings)?;
261 if let Some(slash_pos) = name.rfind('/') {
263 let group_path = &name[..slash_pos];
264 let abs_group_path = if group_path.starts_with('/') {
265 group_path.to_string()
266 } else {
267 format!("/{}", group_path)
268 };
269 writer.assign_dataset_to_group(&abs_group_path, idx)?;
270 }
271 Ok(())
272 }
273 H5FileInner::Reader(_) => {
274 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
275 }
276 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
277 }
278 }
279
280 pub fn write_vlen_strings_compressed(
287 &self,
288 name: &str,
289 strings: &[&str],
290 chunk_size: usize,
291 pipeline: FilterPipeline,
292 ) -> Result<()> {
293 let mut inner = borrow_inner_mut(&self.inner);
294 match &mut *inner {
295 H5FileInner::Writer(writer) => {
296 let idx = writer
297 .create_vlen_string_dataset_compressed(name, strings, chunk_size, pipeline)?;
298 if let Some(slash_pos) = name.rfind('/') {
299 let group_path = &name[..slash_pos];
300 let abs_group_path = if group_path.starts_with('/') {
301 group_path.to_string()
302 } else {
303 format!("/{}", group_path)
304 };
305 writer.assign_dataset_to_group(&abs_group_path, idx)?;
306 }
307 Ok(())
308 }
309 H5FileInner::Reader(_) => {
310 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
311 }
312 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
313 }
314 }
315
316 pub fn create_appendable_vlen_dataset(
321 &self,
322 name: &str,
323 chunk_size: usize,
324 pipeline: Option<FilterPipeline>,
325 ) -> Result<()> {
326 let mut inner = borrow_inner_mut(&self.inner);
327 match &mut *inner {
328 H5FileInner::Writer(writer) => {
329 let idx =
330 writer.create_appendable_vlen_string_dataset(name, chunk_size, pipeline)?;
331 if let Some(slash_pos) = name.rfind('/') {
332 let group_path = &name[..slash_pos];
333 let abs_group_path = if group_path.starts_with('/') {
334 group_path.to_string()
335 } else {
336 format!("/{}", group_path)
337 };
338 writer.assign_dataset_to_group(&abs_group_path, idx)?;
339 }
340 Ok(())
341 }
342 H5FileInner::Reader(_) => {
343 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
344 }
345 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
346 }
347 }
348
349 pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
351 let mut inner = borrow_inner_mut(&self.inner);
352 match &mut *inner {
353 H5FileInner::Writer(writer) => {
354 let ds_index = writer
355 .dataset_index(name)
356 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
357 writer.append_vlen_strings(ds_index, strings)?;
358 Ok(())
359 }
360 H5FileInner::Reader(_) => {
361 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
362 }
363 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
364 }
365 }
366
367 pub fn delete_dataset(&self, name: &str) -> Result<()> {
370 let mut inner = borrow_inner_mut(&self.inner);
371 match &mut *inner {
372 H5FileInner::Writer(writer) => {
373 writer.delete_dataset(name)?;
374 Ok(())
375 }
376 _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
377 }
378 }
379
380 pub fn delete_group(&self, name: &str) -> Result<()> {
383 let mut inner = borrow_inner_mut(&self.inner);
384 match &mut *inner {
385 H5FileInner::Writer(writer) => {
386 writer.delete_group(name)?;
387 Ok(())
388 }
389 _ => Err(Hdf5Error::InvalidState("cannot delete in read mode".into())),
390 }
391 }
392
393 pub fn dataset(&self, name: &str) -> Result<H5Dataset> {
395 let inner = borrow_inner(&self.inner);
396 match &*inner {
397 H5FileInner::Reader(reader) => {
398 let info = reader
399 .dataset_info(name)
400 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
401 let shape: Vec<usize> = info.dataspace.dims.iter().map(|&d| d as usize).collect();
402 let element_size = info.datatype.element_size() as usize;
403 Ok(H5Dataset::new_reader(
404 clone_inner(&self.inner),
405 name.to_string(),
406 shape,
407 element_size,
408 ))
409 }
410 H5FileInner::Writer(_) => Err(Hdf5Error::InvalidState(
411 "cannot open a dataset by name in write mode; use new_dataset() instead"
412 .to_string(),
413 )),
414 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
415 }
416 }
417
418 pub fn dataset_writer(&self, name: &str) -> Result<H5Dataset> {
432 let inner = borrow_inner(&self.inner);
433 match &*inner {
434 H5FileInner::Writer(writer) => {
435 let index = writer
436 .dataset_index(name)
437 .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?;
438 let ds = &writer.datasets[index];
439 let shape: Vec<usize> = ds.dataspace.dims.iter().map(|&d| d as usize).collect();
440 let element_size = ds.datatype.element_size() as usize;
441 let fixed_array = ds.fixed_array.is_some();
442 let btree2 = ds.btree_v2.is_some();
443 let chunked = ds.chunked.is_some() || fixed_array || btree2;
444 Ok(H5Dataset::new_writer(
445 clone_inner(&self.inner),
446 index,
447 shape,
448 element_size,
449 chunked,
450 btree2,
451 fixed_array,
452 ))
453 }
454 H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
455 "cannot open a dataset_writer in read mode; use dataset() instead".to_string(),
456 )),
457 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".to_string())),
458 }
459 }
460
461 pub fn dataset_names(&self) -> Vec<String> {
467 let inner = borrow_inner(&self.inner);
468 match &*inner {
469 H5FileInner::Reader(reader) => reader
470 .dataset_names()
471 .iter()
472 .map(|s| s.to_string())
473 .collect(),
474 H5FileInner::Writer(writer) => writer
475 .dataset_names()
476 .iter()
477 .map(|s| s.to_string())
478 .collect(),
479 H5FileInner::Closed => Vec::new(),
480 }
481 }
482
483 pub fn close(self) -> Result<()> {
489 let old = {
490 let mut inner = borrow_inner_mut(&self.inner);
491 std::mem::replace(&mut *inner, H5FileInner::Closed)
492 };
493 match old {
494 H5FileInner::Writer(writer) => {
495 writer.close()?;
496 Ok(())
497 }
498 H5FileInner::Reader(_) => Ok(()),
499 H5FileInner::Closed => Ok(()),
500 }
501 }
502
503 pub fn flush(&self) -> Result<()> {
505 Ok(())
509 }
510}
511
512#[derive(Debug, Default, Clone)]
519pub struct H5FileOptions {
520 locking: Option<FileLocking>,
521}
522
523impl H5FileOptions {
524 pub fn new() -> Self {
526 Self::default()
527 }
528
529 pub fn locking(mut self, policy: FileLocking) -> Self {
532 self.locking = Some(policy);
533 self
534 }
535
536 pub fn no_locking(self) -> Self {
539 self.locking(FileLocking::Disabled)
540 }
541
542 pub fn best_effort_locking(self) -> Self {
545 self.locking(FileLocking::BestEffort)
546 }
547
548 fn resolved_locking(&self) -> FileLocking {
549 match self.locking {
550 Some(p) => p,
551 None => FileLocking::from_env_or(FileLocking::default()),
552 }
553 }
554
555 pub fn create<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
557 let writer = Hdf5Writer::create_with_locking(path.as_ref(), self.resolved_locking())?;
558 Ok(H5File {
559 inner: new_shared(H5FileInner::Writer(writer)),
560 })
561 }
562
563 pub fn open<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
565 let reader = Hdf5Reader::open_with_locking(path.as_ref(), self.resolved_locking())?;
566 Ok(H5File {
567 inner: new_shared(H5FileInner::Reader(reader)),
568 })
569 }
570
571 pub fn open_rw<P: AsRef<Path>>(self, path: P) -> Result<H5File> {
573 let writer = Hdf5Writer::open_append_with_locking(path.as_ref(), self.resolved_locking())?;
574 Ok(H5File {
575 inner: new_shared(H5FileInner::Writer(writer)),
576 })
577 }
578}
579
580#[cfg(test)]
581fn unique_test_path(name: &str) -> std::path::PathBuf {
582 use std::sync::atomic::{AtomicU64, Ordering};
587 static COUNTER: AtomicU64 = AtomicU64::new(0);
588 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
589 std::env::temp_dir().join(format!(
590 "rust_hdf5_test_{}_{}_{}.h5",
591 name,
592 std::process::id(),
593 n
594 ))
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use std::path::PathBuf;
601
602 fn temp_path(name: &str) -> PathBuf {
603 super::unique_test_path(name)
604 }
605
606 #[test]
607 fn create_and_close_empty() {
608 let path = temp_path("create_empty");
609 let file = H5File::create(&path).unwrap();
610 file.close().unwrap();
611
612 let file = H5File::open(&path).unwrap();
614 file.close().unwrap();
615
616 std::fs::remove_file(&path).ok();
617 }
618
619 #[test]
620 fn create_and_drop_empty() {
621 let path = temp_path("drop_empty");
622 {
623 let _file = H5File::create(&path).unwrap();
624 }
626 let file = H5File::open(&path).unwrap();
628 file.close().unwrap();
629
630 std::fs::remove_file(&path).ok();
631 }
632
633 #[test]
634 fn dataset_not_found() {
635 let path = temp_path("ds_not_found");
636 {
637 let _file = H5File::create(&path).unwrap();
638 }
639 let file = H5File::open(&path).unwrap();
640 let result = file.dataset("nonexistent");
641 assert!(result.is_err());
642
643 std::fs::remove_file(&path).ok();
644 }
645
646 #[test]
647 fn write_and_read_roundtrip() {
648 let path = temp_path("write_read_rt");
649
650 {
652 let file = H5File::create(&path).unwrap();
653 let ds = file
654 .new_dataset::<u8>()
655 .shape([4, 4])
656 .create("data")
657 .unwrap();
658 ds.write_raw(&[0u8; 16]).unwrap();
659 file.close().unwrap();
660 }
661
662 {
664 let file = H5File::open(&path).unwrap();
665 let ds = file.dataset("data").unwrap();
666 assert_eq!(ds.shape(), vec![4, 4]);
667 let data = ds.read_raw::<u8>().unwrap();
668 assert_eq!(data.len(), 16);
669 assert!(data.iter().all(|&b| b == 0));
670 file.close().unwrap();
671 }
672
673 std::fs::remove_file(&path).ok();
674 }
675
676 #[test]
677 fn write_and_read_f64() {
678 let path = temp_path("write_read_f64");
679
680 let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
681
682 {
684 let file = H5File::create(&path).unwrap();
685 let ds = file
686 .new_dataset::<f64>()
687 .shape([2, 3])
688 .create("matrix")
689 .unwrap();
690 ds.write_raw(&values).unwrap();
691 file.close().unwrap();
692 }
693
694 {
696 let file = H5File::open(&path).unwrap();
697 let ds = file.dataset("matrix").unwrap();
698 assert_eq!(ds.shape(), vec![2, 3]);
699 let readback = ds.read_raw::<f64>().unwrap();
700 assert_eq!(readback, values);
701 }
702
703 std::fs::remove_file(&path).ok();
704 }
705
706 #[test]
707 fn multiple_datasets() {
708 let path = temp_path("multi_ds");
709
710 {
711 let file = H5File::create(&path).unwrap();
712 let ds1 = file.new_dataset::<i32>().shape([3]).create("ints").unwrap();
713 ds1.write_raw(&[10i32, 20, 30]).unwrap();
714
715 let ds2 = file
716 .new_dataset::<f32>()
717 .shape([2, 2])
718 .create("floats")
719 .unwrap();
720 ds2.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
721
722 file.close().unwrap();
723 }
724
725 {
726 let file = H5File::open(&path).unwrap();
727
728 let ds_ints = file.dataset("ints").unwrap();
729 assert_eq!(ds_ints.shape(), vec![3]);
730 let ints = ds_ints.read_raw::<i32>().unwrap();
731 assert_eq!(ints, vec![10, 20, 30]);
732
733 let ds_floats = file.dataset("floats").unwrap();
734 assert_eq!(ds_floats.shape(), vec![2, 2]);
735 let floats = ds_floats.read_raw::<f32>().unwrap();
736 assert_eq!(floats, vec![1.0f32, 2.0, 3.0, 4.0]);
737 }
738
739 std::fs::remove_file(&path).ok();
740 }
741
742 #[test]
743 fn close_is_idempotent() {
744 let path = temp_path("close_idemp");
745 let file = H5File::create(&path).unwrap();
746 file.close().unwrap();
747 std::fs::remove_file(&path).ok();
749 }
750}
751
752#[cfg(test)]
753mod integration_tests {
754 use super::*;
755
756 fn temp_path(name: &str) -> std::path::PathBuf {
757 super::unique_test_path(name)
758 }
759
760 #[test]
761 fn write_file_for_h5dump() {
762 let path = temp_path("integration");
763 let file = H5File::create(&path).unwrap();
764
765 let ds = file
766 .new_dataset::<u8>()
767 .shape([4usize, 4])
768 .create("data_u8")
769 .unwrap();
770 let data: Vec<u8> = (0..16).collect();
771 ds.write_raw(&data).unwrap();
772
773 let ds2 = file
774 .new_dataset::<f64>()
775 .shape([3usize, 2])
776 .create("data_f64")
777 .unwrap();
778 let fdata: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
779 ds2.write_raw(&fdata).unwrap();
780
781 let ds3 = file
782 .new_dataset::<i32>()
783 .shape([5usize])
784 .create("values")
785 .unwrap();
786 let idata: Vec<i32> = vec![-10, -5, 0, 5, 10];
787 ds3.write_raw(&idata).unwrap();
788
789 file.close().unwrap();
790
791 assert!(path.exists());
793 }
794
795 #[test]
796 fn write_chunked_file_for_h5dump() {
797 let path = temp_path("chunked");
798 let file = H5File::create(&path).unwrap();
799
800 let ds = file
802 .new_dataset::<f64>()
803 .shape([0usize, 4])
804 .chunk(&[1, 4])
805 .max_shape(&[None, Some(4)])
806 .create("streaming_data")
807 .unwrap();
808
809 for frame in 0..5u64 {
811 let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
812 let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
813 ds.write_chunk(frame as usize, &raw).unwrap();
814 }
815
816 ds.extend(&[5, 4]).unwrap();
818 ds.flush().unwrap();
819
820 file.close().unwrap();
821
822 assert!(path.exists());
823 }
824
825 #[test]
826 fn write_chunked_many_frames_for_h5dump() {
827 let path = temp_path("chunked_many");
828 let file = H5File::create(&path).unwrap();
829
830 let ds = file
831 .new_dataset::<i32>()
832 .shape([0usize, 3])
833 .chunk(&[1, 3])
834 .max_shape(&[None, Some(3)])
835 .create("data")
836 .unwrap();
837
838 for frame in 0..10u64 {
840 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
841 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
842 ds.write_chunk(frame as usize, &raw).unwrap();
843 }
844 ds.extend(&[10, 3]).unwrap();
845 file.close().unwrap();
846
847 assert!(path.exists());
848 }
849
850 #[test]
851 fn write_dataset_with_attributes() {
852 use crate::types::VarLenUnicode;
853
854 let path = temp_path("attributes");
855 let file = H5File::create(&path).unwrap();
856
857 let ds = file
858 .new_dataset::<f32>()
859 .shape([10usize])
860 .create("temperature")
861 .unwrap();
862 let data: Vec<f32> = (0..10).map(|i| i as f32 * 1.5).collect();
863 ds.write_raw(&data).unwrap();
864
865 let attr = ds
867 .new_attr::<VarLenUnicode>()
868 .shape(())
869 .create("units")
870 .unwrap();
871 attr.write_scalar(&VarLenUnicode("kelvin".to_string()))
872 .unwrap();
873
874 let attr2 = ds
875 .new_attr::<VarLenUnicode>()
876 .shape(())
877 .create("description")
878 .unwrap();
879 attr2
880 .write_scalar(&VarLenUnicode("Temperature measurements".to_string()))
881 .unwrap();
882
883 let attr3 = ds
885 .new_attr::<VarLenUnicode>()
886 .shape(())
887 .create("source")
888 .unwrap();
889 attr3.write_string("sensor_01").unwrap();
890
891 let attr4 = ds
893 .new_attr::<VarLenUnicode>()
894 .shape(())
895 .create("label")
896 .unwrap();
897 let s: VarLenUnicode = "test_label".parse().unwrap_or_default();
898 attr4.write_scalar(&s).unwrap();
899
900 file.close().unwrap();
901
902 assert!(path.exists());
903 }
904
905 #[test]
906 fn dataset_writer_reopens_for_attributes() {
907 let path = temp_path("dataset_writer");
910 {
911 let file = H5File::create(&path).unwrap();
912 {
913 let ds = file
914 .new_dataset::<u16>()
915 .shape([8])
916 .create("image")
917 .unwrap();
918 ds.write_raw(&[0u16; 8]).unwrap();
919 }
921
922 assert!(file.dataset("image").is_err());
924 let ds = file.dataset_writer("image").unwrap();
925 assert_eq!(ds.shape(), vec![8]);
926 ds.new_attr::<i32>()
927 .shape([3])
928 .create("NDArrayDimOffset")
929 .unwrap()
930 .write_array(&[0i32, 4, 8])
931 .unwrap();
932 ds.new_attr::<i32>()
933 .shape(())
934 .create("NDUniqueId")
935 .unwrap()
936 .write_numeric(&42i32)
937 .unwrap();
938
939 assert!(matches!(
941 file.dataset_writer("nope"),
942 Err(crate::error::Hdf5Error::NotFound(_))
943 ));
944
945 file.close().unwrap();
946 }
947 {
948 let file = H5File::open(&path).unwrap();
949 let ds = file.dataset("image").unwrap();
950 let off = ds.attr("NDArrayDimOffset").unwrap().read_raw().unwrap();
951 let got: Vec<i32> = off
952 .chunks_exact(4)
953 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
954 .collect();
955 assert_eq!(got, vec![0, 4, 8]);
956 let uid: i32 = ds.attr("NDUniqueId").unwrap().read_numeric().unwrap();
957 assert_eq!(uid, 42);
958 }
959 std::fs::remove_file(&path).ok();
960 }
961
962 #[test]
963 fn chunked_write_read_roundtrip() {
964 let path = temp_path("chunked_roundtrip");
965
966 {
968 let file = H5File::create(&path).unwrap();
969 let ds = file
970 .new_dataset::<i32>()
971 .shape([0usize, 3])
972 .chunk(&[1, 3])
973 .max_shape(&[None, Some(3)])
974 .create("table")
975 .unwrap();
976
977 for frame in 0..8u64 {
978 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
979 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
980 ds.write_chunk(frame as usize, &raw).unwrap();
981 }
982 ds.extend(&[8, 3]).unwrap();
983 file.close().unwrap();
984 }
985
986 {
988 let file = H5File::open(&path).unwrap();
989 let ds = file.dataset("table").unwrap();
990 assert_eq!(ds.shape(), vec![8, 3]);
991 let data = ds.read_raw::<i32>().unwrap();
992 assert_eq!(data.len(), 24);
993 for (i, val) in data.iter().enumerate() {
994 assert_eq!(*val, i as i32);
995 }
996 }
997
998 std::fs::remove_file(&path).ok();
999 }
1000
1001 #[test]
1002 #[cfg(feature = "deflate")]
1003 fn compressed_chunked_roundtrip() {
1004 let path = temp_path("compressed_roundtrip");
1005
1006 {
1008 let file = H5File::create(&path).unwrap();
1009 let ds = file
1010 .new_dataset::<f64>()
1011 .shape([0usize, 4])
1012 .chunk(&[1, 4])
1013 .max_shape(&[None, Some(4)])
1014 .deflate(6)
1015 .create("compressed")
1016 .unwrap();
1017
1018 for frame in 0..10u64 {
1019 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1020 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1021 ds.write_chunk(frame as usize, &raw).unwrap();
1022 }
1023 ds.extend(&[10, 4]).unwrap();
1024 file.close().unwrap();
1025 }
1026
1027 {
1029 let file = H5File::open(&path).unwrap();
1030 let ds = file.dataset("compressed").unwrap();
1031 assert_eq!(ds.shape(), vec![10, 4]);
1032 let data = ds.read_raw::<f64>().unwrap();
1033 assert_eq!(data.len(), 40);
1034 for (i, val) in data.iter().enumerate() {
1035 assert!(
1036 (val - i as f64).abs() < 1e-10,
1037 "mismatch at {}: {} != {}",
1038 i,
1039 val,
1040 i
1041 );
1042 }
1043 }
1044
1045 std::fs::remove_file(&path).ok();
1046 }
1047
1048 #[test]
1049 #[cfg(feature = "deflate")]
1050 fn compressed_chunked_many_frames() {
1051 let path = temp_path("compressed_many");
1052
1053 {
1054 let file = H5File::create(&path).unwrap();
1055 let ds = file
1056 .new_dataset::<i32>()
1057 .shape([0usize, 3])
1058 .chunk(&[1, 3])
1059 .max_shape(&[None, Some(3)])
1060 .deflate(6)
1061 .create("stream")
1062 .unwrap();
1063
1064 for frame in 0..100u64 {
1065 let vals: Vec<i32> = (0..3).map(|i| (frame * 3 + i) as i32).collect();
1066 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1067 ds.write_chunk(frame as usize, &raw).unwrap();
1068 }
1069 ds.extend(&[100, 3]).unwrap();
1070 file.close().unwrap();
1071 }
1072
1073 {
1074 let file = H5File::open(&path).unwrap();
1075 let ds = file.dataset("stream").unwrap();
1076 assert_eq!(ds.shape(), vec![100, 3]);
1077 let data = ds.read_raw::<i32>().unwrap();
1078 assert_eq!(data.len(), 300);
1079 for (i, val) in data.iter().enumerate() {
1080 assert_eq!(*val, i as i32, "mismatch at {}", i);
1081 }
1082 }
1083
1084 std::fs::remove_file(&path).ok();
1085 }
1086 #[test]
1087 fn append_mode() {
1088 let path = temp_path("append");
1089
1090 {
1092 let file = H5File::create(&path).unwrap();
1093 let ds = file
1094 .new_dataset::<i32>()
1095 .shape([3usize])
1096 .create("first")
1097 .unwrap();
1098 ds.write_raw(&[1i32, 2, 3]).unwrap();
1099 file.close().unwrap();
1100 }
1101
1102 {
1104 let file = H5File::open_rw(&path).unwrap();
1105 let ds = file
1106 .new_dataset::<f64>()
1107 .shape([2usize])
1108 .create("second")
1109 .unwrap();
1110 ds.write_raw(&[4.0f64, 5.0]).unwrap();
1111 file.close().unwrap();
1112 }
1113
1114 {
1116 let file = H5File::open(&path).unwrap();
1117 let names = file.dataset_names();
1118 assert!(names.contains(&"first".to_string()));
1119 assert!(names.contains(&"second".to_string()));
1120
1121 let ds1 = file.dataset("first").unwrap();
1122 assert_eq!(ds1.read_raw::<i32>().unwrap(), vec![1, 2, 3]);
1123
1124 let ds2 = file.dataset("second").unwrap();
1125 assert_eq!(ds2.read_raw::<f64>().unwrap(), vec![4.0, 5.0]);
1126 }
1127
1128 std::fs::remove_file(&path).ok();
1129 }
1130
1131 #[test]
1132 fn open_rw_set_attr_preserves_file() {
1133 let path = temp_path("open_rw_attr");
1134 {
1136 let file = H5File::create(&path).unwrap();
1137 let ds = file
1138 .new_dataset::<i32>()
1139 .shape([3usize])
1140 .create("data")
1141 .unwrap();
1142 ds.write_raw(&[10i32, 20, 30]).unwrap();
1143 file.set_attr_string("version", "1.0").unwrap();
1144 file.close().unwrap();
1145 }
1146 {
1148 let file = H5File::open_rw(&path).unwrap();
1149 file.set_attr_string("version", "2.0").unwrap();
1150 file.close().unwrap();
1151 }
1152 {
1154 let file = H5File::open(&path).unwrap();
1155 let ds = file.dataset("data").unwrap();
1156 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 20, 30]);
1157 let ver = file.attr_string("version").unwrap();
1158 assert_eq!(ver, "2.0");
1159 }
1160 std::fs::remove_file(&path).ok();
1161 }
1162
1163 #[test]
1164 #[cfg(feature = "deflate")]
1165 fn open_rw_attr_with_compressed_dataset() {
1166 use crate::format::messages::filter::FilterPipeline;
1167 let path = temp_path("open_rw_compressed");
1168 let input: Vec<&str> = (0..50).map(|_| "test string data").collect();
1169 {
1171 let file = H5File::create(&path).unwrap();
1172 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1173 .unwrap();
1174 file.set_attr_string("version", "1.0").unwrap();
1175 file.close().unwrap();
1176 }
1177 {
1179 let file = H5File::open_rw(&path).unwrap();
1180 file.set_attr_string("version", "2.0").unwrap();
1181 file.close().unwrap();
1182 }
1183 {
1185 let file = H5File::open(&path).unwrap();
1186 let ds = file.dataset("texts").unwrap();
1187 let strings = ds.read_vlen_strings().unwrap();
1188 assert_eq!(strings.len(), 50);
1189 assert_eq!(strings[0], "test string data");
1190 let ver = file.attr_string("version").unwrap();
1191 assert_eq!(ver, "2.0");
1192 }
1193 std::fs::remove_file(&path).ok();
1194 }
1195
1196 #[test]
1197 #[cfg(feature = "lz4")]
1198 fn append_vlen_strings_basic() {
1199 use crate::format::messages::filter::FilterPipeline;
1200 let path = temp_path("append_vlen");
1201 {
1202 let file = H5File::create(&path).unwrap();
1203 file.create_appendable_vlen_dataset("names", 4, Some(FilterPipeline::lz4()))
1204 .unwrap();
1205 file.append_vlen_strings("names", &["alice", "bob", "charlie"])
1206 .unwrap();
1207 file.append_vlen_strings("names", &["dave", "eve"]).unwrap();
1208 file.close().unwrap();
1209 }
1210 {
1211 let file = H5File::open(&path).unwrap();
1212 let ds = file.dataset("names").unwrap();
1213 let strings = ds.read_vlen_strings().unwrap();
1214 assert_eq!(strings, vec!["alice", "bob", "charlie", "dave", "eve"]);
1215 }
1216 std::fs::remove_file(&path).ok();
1217 }
1218
1219 #[test]
1220 #[cfg(feature = "lz4")]
1221 fn append_vlen_strings_large() {
1222 use crate::format::messages::filter::FilterPipeline;
1223 let path = temp_path("append_vlen_large");
1224 let batch1: Vec<String> = (0..5000).map(|i| format!("node-{:06}", i)).collect();
1225 let batch2: Vec<String> = (5000..7189).map(|i| format!("node-{:06}", i)).collect();
1226 {
1227 let file = H5File::create(&path).unwrap();
1228 file.create_appendable_vlen_dataset("data", 512, Some(FilterPipeline::lz4()))
1229 .unwrap();
1230 let r1: Vec<&str> = batch1.iter().map(|s| s.as_str()).collect();
1231 file.append_vlen_strings("data", &r1).unwrap();
1232 let r2: Vec<&str> = batch2.iter().map(|s| s.as_str()).collect();
1233 file.append_vlen_strings("data", &r2).unwrap();
1234 file.close().unwrap();
1235 }
1236 {
1237 let file = H5File::open(&path).unwrap();
1238 let ds = file.dataset("data").unwrap();
1239 let strings = ds.read_vlen_strings().unwrap();
1240 assert_eq!(strings.len(), 7189);
1241 assert_eq!(strings[0], "node-000000");
1242 assert_eq!(strings[7188], "node-007188");
1243 }
1244 std::fs::remove_file(&path).ok();
1245 }
1246
1247 #[test]
1248 fn append_vlen_strings_uncompressed() {
1249 let path = temp_path("append_vlen_unc");
1250 {
1251 let file = H5File::create(&path).unwrap();
1252 file.create_appendable_vlen_dataset("texts", 8, None)
1253 .unwrap();
1254 file.append_vlen_strings("texts", &["hello", "world"])
1255 .unwrap();
1256 file.append_vlen_strings("texts", &["foo", "bar", "baz"])
1257 .unwrap();
1258 file.close().unwrap();
1259 }
1260 {
1261 let file = H5File::open(&path).unwrap();
1262 let ds = file.dataset("texts").unwrap();
1263 let strings = ds.read_vlen_strings().unwrap();
1264 assert_eq!(strings, vec!["hello", "world", "foo", "bar", "baz"]);
1265 }
1266 std::fs::remove_file(&path).ok();
1267 }
1268
1269 #[test]
1270 fn delete_dataset_roundtrip() {
1271 let path = temp_path("delete_ds");
1272 {
1273 let file = H5File::create(&path).unwrap();
1274 file.write_vlen_strings("keep", &["a", "b"]).unwrap();
1275 file.write_vlen_strings("remove", &["x", "y"]).unwrap();
1276 file.delete_dataset("remove").unwrap();
1277 file.close().unwrap();
1278 }
1279 {
1280 let file = H5File::open(&path).unwrap();
1281 let names = file.dataset_names();
1282 assert!(names.contains(&"keep".to_string()));
1283 assert!(!names.contains(&"remove".to_string()));
1284 let ds = file.dataset("keep").unwrap();
1285 assert_eq!(ds.read_vlen_strings().unwrap(), vec!["a", "b"]);
1286 }
1287 std::fs::remove_file(&path).ok();
1288 }
1289
1290 #[test]
1291 fn delete_group_roundtrip() {
1292 let path = temp_path("delete_grp");
1293 {
1294 let file = H5File::create(&path).unwrap();
1295 let g1 = file.create_group("keep").unwrap();
1296 g1.write_vlen_strings("data", &["a"]).unwrap();
1297 let g2 = file.create_group("remove").unwrap();
1298 g2.write_vlen_strings("data", &["x"]).unwrap();
1299 file.delete_group("remove").unwrap();
1300 file.close().unwrap();
1301 }
1302 {
1303 let file = H5File::open(&path).unwrap();
1304 let names = file.dataset_names();
1305 assert!(names.contains(&"keep/data".to_string()));
1306 assert!(!names.contains(&"remove/data".to_string()));
1307 }
1308 std::fs::remove_file(&path).ok();
1309 }
1310
1311 #[test]
1312 fn open_rw_delete_recreate_group() {
1313 let path = temp_path("rw_delete_recreate");
1314 {
1316 let file = H5File::create(&path).unwrap();
1317 let n = file.create_group("nodes").unwrap();
1318 n.write_vlen_strings("id", &["a", "b", "c"]).unwrap();
1319 let e = file.create_group("edges").unwrap();
1320 e.write_vlen_strings("src", &["x", "y"]).unwrap();
1321 file.close().unwrap();
1322 }
1323 {
1325 let file = H5File::open_rw(&path).unwrap();
1326 file.delete_group("nodes").unwrap();
1327 let n = file.create_group("nodes").unwrap();
1328 n.write_vlen_strings("id", &["new1", "new2"]).unwrap();
1329 file.close().unwrap();
1330 }
1331 {
1333 let file = H5File::open(&path).unwrap();
1334 let ds = file.dataset("nodes/id").unwrap();
1335 let s = ds.read_vlen_strings().unwrap();
1336 assert_eq!(s, vec!["new1", "new2"]);
1337 let ds = file.dataset("edges/src").unwrap();
1339 let s = ds.read_vlen_strings().unwrap();
1340 assert_eq!(s, vec!["x", "y"]);
1341 }
1342 std::fs::remove_file(&path).ok();
1343 }
1344
1345 #[test]
1346 fn delete_and_recreate_group() {
1347 let path = temp_path("delete_recreate");
1348 {
1349 let file = H5File::create(&path).unwrap();
1350 let g = file.create_group("nodes").unwrap();
1351 g.write_vlen_strings("id", &["old1", "old2"]).unwrap();
1352 file.delete_group("nodes").unwrap();
1353 let g = file.create_group("nodes").unwrap();
1354 g.write_vlen_strings("id", &["new1", "new2", "new3"])
1355 .unwrap();
1356 file.close().unwrap();
1357 }
1358 {
1359 let file = H5File::open(&path).unwrap();
1360 let ds = file.dataset("nodes/id").unwrap();
1361 let strings = ds.read_vlen_strings().unwrap();
1362 assert_eq!(strings, vec!["new1", "new2", "new3"]);
1363 }
1364 std::fs::remove_file(&path).ok();
1365 }
1366
1367 #[test]
1368 #[cfg(feature = "deflate")]
1369 fn vlen_string_compressed_large_roundtrip() {
1370 use crate::format::messages::filter::FilterPipeline;
1371 let path = temp_path("vlen_large");
1372 let input: Vec<String> = (0..7189)
1374 .map(|i| format!("node-{:08x}-{}", i, "a".repeat(20 + (i % 30))))
1375 .collect();
1376 let input_refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
1377 {
1378 let file = H5File::create(&path).unwrap();
1379 file.create_group("nodes").unwrap();
1380 file.write_vlen_strings_compressed(
1381 "nodes/id",
1382 &input_refs,
1383 512,
1384 FilterPipeline::deflate(6),
1385 )
1386 .unwrap();
1387 file.close().unwrap();
1388 }
1389 {
1391 let file = H5File::open(&path).unwrap();
1392 let ds = file.dataset("nodes/id").unwrap();
1393 let strings = ds.read_vlen_strings().unwrap();
1394 assert_eq!(strings.len(), 7189);
1395 assert_eq!(strings[0], input[0]);
1396 assert_eq!(strings[7188], input[7188]);
1397 }
1398 {
1400 let file = H5File::open_rw(&path).unwrap();
1401 file.set_attr_string("version", "1.0").unwrap();
1402 file.close().unwrap();
1403 }
1404 {
1405 let file = H5File::open(&path).unwrap();
1406 let ds = file.dataset("nodes/id").unwrap();
1407 let strings = ds.read_vlen_strings().unwrap();
1408 assert_eq!(strings.len(), 7189);
1409 assert_eq!(strings[0], input[0]);
1410 }
1411 std::fs::remove_file(&path).ok();
1412 }
1413
1414 #[test]
1415 fn vlen_string_write_read() {
1416 let path = temp_path("vlen_wr");
1417 {
1418 let file = H5File::create(&path).unwrap();
1419 file.write_vlen_strings("names", &["alice", "bob", "charlie"])
1420 .unwrap();
1421 file.close().unwrap();
1422 }
1423 {
1424 let file = H5File::open(&path).unwrap();
1425 let ds = file.dataset("names").unwrap();
1426 let strings = ds.read_vlen_strings().unwrap();
1427 assert_eq!(strings, vec!["alice", "bob", "charlie"]);
1428 }
1429 std::fs::remove_file(&path).ok();
1430 }
1431
1432 #[test]
1433 #[cfg(feature = "deflate")]
1434 fn vlen_string_deflate_roundtrip() {
1435 use crate::format::messages::filter::FilterPipeline;
1436 let path = temp_path("vlen_deflate");
1437 let input: Vec<&str> = (0..100)
1438 .map(|i| match i % 3 {
1439 0 => "hello world",
1440 1 => "compressed vlen string test",
1441 _ => "rust-hdf5",
1442 })
1443 .collect();
1444 {
1445 let file = H5File::create(&path).unwrap();
1446 file.write_vlen_strings_compressed("texts", &input, 16, FilterPipeline::deflate(6))
1447 .unwrap();
1448 file.close().unwrap();
1449 }
1450 {
1451 let file = H5File::open(&path).unwrap();
1452 let ds = file.dataset("texts").unwrap();
1453 let strings = ds.read_vlen_strings().unwrap();
1454 assert_eq!(strings.len(), 100);
1455 for (i, s) in strings.iter().enumerate() {
1456 assert_eq!(s, input[i]);
1457 }
1458 }
1459 std::fs::remove_file(&path).ok();
1460 }
1461
1462 #[test]
1463 #[cfg(feature = "zstd")]
1464 fn vlen_string_zstd_roundtrip() {
1465 use crate::format::messages::filter::FilterPipeline;
1466 let path = temp_path("vlen_zstd");
1467 let input: Vec<&str> = (0..200)
1468 .map(|i| match i % 4 {
1469 0 => "zstandard compression test",
1470 1 => "variable length string",
1471 2 => "rust-hdf5 chunked storage",
1472 _ => "hello zstd world",
1473 })
1474 .collect();
1475 {
1476 let file = H5File::create(&path).unwrap();
1477 file.write_vlen_strings_compressed("data", &input, 32, FilterPipeline::zstd(3))
1478 .unwrap();
1479 file.close().unwrap();
1480 }
1481 {
1482 let file = H5File::open(&path).unwrap();
1483 let ds = file.dataset("data").unwrap();
1484 let strings = ds.read_vlen_strings().unwrap();
1485 assert_eq!(strings.len(), 200);
1486 for (i, s) in strings.iter().enumerate() {
1487 assert_eq!(s, input[i]);
1488 }
1489 }
1490 std::fs::remove_file(&path).ok();
1491 }
1492
1493 #[test]
1494 #[cfg(feature = "deflate")]
1495 fn shuffle_deflate_roundtrip() {
1496 let path = temp_path("shuf_defl");
1497 {
1498 let file = H5File::create(&path).unwrap();
1499 let ds = file
1500 .new_dataset::<f64>()
1501 .shape([0usize, 4])
1502 .chunk(&[1, 4])
1503 .max_shape(&[None, Some(4)])
1504 .shuffle_deflate(6)
1505 .create("data")
1506 .unwrap();
1507 for frame in 0..20u64 {
1508 let vals: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
1509 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1510 ds.write_chunk(frame as usize, &raw).unwrap();
1511 }
1512 ds.extend(&[20, 4]).unwrap();
1513 file.close().unwrap();
1514 }
1515 {
1516 let file = H5File::open(&path).unwrap();
1517 let ds = file.dataset("data").unwrap();
1518 assert_eq!(ds.shape(), vec![20, 4]);
1519 let data = ds.read_raw::<f64>().unwrap();
1520 assert_eq!(data.len(), 80);
1521 for (i, val) in data.iter().enumerate() {
1522 assert!((val - i as f64).abs() < 1e-10);
1523 }
1524 }
1525 std::fs::remove_file(&path).ok();
1526 }
1527
1528 #[test]
1529 fn file_level_attributes() {
1530 let path = temp_path("file_attr");
1531 {
1532 let file = H5File::create(&path).unwrap();
1533 file.set_attr_string("title", "Test File").unwrap();
1534 file.set_attr_numeric("version", &42i32).unwrap();
1535 let ds = file
1536 .new_dataset::<u8>()
1537 .shape([1usize])
1538 .create("dummy")
1539 .unwrap();
1540 ds.write_raw(&[0u8]).unwrap();
1541 file.close().unwrap();
1542 }
1543 {
1544 let file = H5File::open(&path).unwrap();
1545 assert!(file.dataset_names().contains(&"dummy".to_string()));
1546
1547 let names = file.attr_names().unwrap();
1549 assert!(names.contains(&"title".to_string()));
1550
1551 let title = file.attr_string("title").unwrap();
1552 assert_eq!(title, "Test File");
1553 }
1554 std::fs::remove_file(&path).ok();
1555 }
1556
1557 #[test]
1558 fn scalar_dataset_roundtrip() {
1559 let path = temp_path("scalar");
1560 {
1561 let file = H5File::create(&path).unwrap();
1562 let ds = file.new_dataset::<f64>().scalar().create("pi").unwrap();
1563 ds.write_raw(&[std::f64::consts::PI]).unwrap();
1564 file.close().unwrap();
1565 }
1566 {
1567 let file = H5File::open(&path).unwrap();
1568 let ds = file.dataset("pi").unwrap();
1569 assert_eq!(ds.shape(), Vec::<usize>::new());
1570 assert_eq!(ds.total_elements(), 1);
1571 let data = ds.read_raw::<f64>().unwrap();
1572 assert_eq!(data.len(), 1);
1573 assert!((data[0] - std::f64::consts::PI).abs() < 1e-15);
1574 }
1575 std::fs::remove_file(&path).ok();
1576 }
1577
1578 #[test]
1579 fn append_mode_extend_chunked() {
1580 let path = temp_path("append_extend");
1581
1582 {
1584 let file = H5File::create(&path).unwrap();
1585 let ds = file
1586 .new_dataset::<i32>()
1587 .shape([0usize, 3])
1588 .chunk(&[1, 3])
1589 .max_shape(&[None, Some(3)])
1590 .create("stream")
1591 .unwrap();
1592 for i in 0..5u64 {
1593 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1594 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1595 ds.write_chunk(i as usize, &raw).unwrap();
1596 }
1597 ds.extend(&[5, 3]).unwrap();
1598 file.close().unwrap();
1599 }
1600
1601 {
1603 let file = H5File::open_rw(&path).unwrap();
1604 let names = file.dataset_names();
1606 assert!(names.contains(&"stream".to_string()));
1607
1608 let mut inner = crate::file::borrow_inner_mut(&file.inner);
1610 if let crate::file::H5FileInner::Writer(writer) = &mut *inner {
1611 let ds_idx = writer.dataset_index("stream").unwrap();
1612 for i in 5..10u64 {
1613 let vals: Vec<i32> = (0..3).map(|j| (i * 3 + j) as i32).collect();
1614 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1615 writer.write_chunk(ds_idx, i, &raw).unwrap();
1616 }
1617 writer.extend_dataset(ds_idx, &[10, 3]).unwrap();
1618 }
1619 drop(inner);
1620 file.close().unwrap();
1621 }
1622
1623 {
1625 let file = H5File::open(&path).unwrap();
1626 let ds = file.dataset("stream").unwrap();
1627 assert_eq!(ds.shape(), vec![10, 3]);
1628 let data = ds.read_raw::<i32>().unwrap();
1629 assert_eq!(data.len(), 30);
1630 for (i, val) in data.iter().enumerate() {
1631 assert_eq!(*val, i as i32, "mismatch at {}", i);
1632 }
1633 }
1634
1635 std::fs::remove_file(&path).ok();
1636 }
1637
1638 #[test]
1639 fn group_hierarchy_roundtrip() {
1640 let path = temp_path("groups_rt");
1641
1642 {
1643 let file = H5File::create(&path).unwrap();
1644 let root = file.root_group();
1645
1646 let det = root.create_group("detector").unwrap();
1648 let raw = det.create_group("raw").unwrap();
1649
1650 let ds1 = det
1652 .new_dataset::<f32>()
1653 .shape([10usize])
1654 .create("temperature")
1655 .unwrap();
1656 ds1.write_raw(&[1.0f32; 10]).unwrap();
1657
1658 let ds2 = raw
1659 .new_dataset::<u16>()
1660 .shape([4usize, 4])
1661 .create("image")
1662 .unwrap();
1663 ds2.write_raw(&[42u16; 16]).unwrap();
1664
1665 let ds3 = file
1667 .new_dataset::<i32>()
1668 .shape([3usize])
1669 .create("version")
1670 .unwrap();
1671 ds3.write_raw(&[1i32, 0, 0]).unwrap();
1672
1673 file.close().unwrap();
1674 }
1675
1676 {
1677 let file = H5File::open(&path).unwrap();
1678 let names = file.dataset_names();
1679 assert!(names.contains(&"version".to_string()));
1680 assert!(names.contains(&"detector/temperature".to_string()));
1681 assert!(names.contains(&"detector/raw/image".to_string()));
1682
1683 let ds = file.dataset("version").unwrap();
1685 assert_eq!(ds.read_raw::<i32>().unwrap(), vec![1, 0, 0]);
1686
1687 let ds = file.dataset("detector/temperature").unwrap();
1688 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0f32; 10]);
1689
1690 let ds = file.dataset("detector/raw/image").unwrap();
1691 assert_eq!(ds.shape(), vec![4, 4]);
1692 assert_eq!(ds.read_raw::<u16>().unwrap(), vec![42u16; 16]);
1693
1694 let root = file.root_group();
1696 let group_names = root.group_names().unwrap();
1697 assert!(group_names.contains(&"detector".to_string()));
1698 }
1699
1700 std::fs::remove_file(&path).ok();
1701 }
1702
1703 #[test]
1704 fn nested_groups_via_file_create_group() {
1705 let path = temp_path("file_create_group");
1706
1707 {
1708 let file = H5File::create(&path).unwrap();
1709
1710 let grp = file.create_group("sensors").unwrap();
1712 let sub = grp.create_group("accel").unwrap();
1713
1714 let ds = sub
1715 .new_dataset::<f64>()
1716 .shape([3usize])
1717 .create("xyz")
1718 .unwrap();
1719 ds.write_raw(&[1.0f64, 2.0, 3.0]).unwrap();
1720
1721 file.close().unwrap();
1722 }
1723
1724 {
1725 let file = H5File::open(&path).unwrap();
1726 let names = file.dataset_names();
1727 assert!(names.contains(&"sensors/accel/xyz".to_string()));
1728
1729 let ds = file.dataset("sensors/accel/xyz").unwrap();
1730 assert_eq!(ds.read_raw::<f64>().unwrap(), vec![1.0, 2.0, 3.0]);
1731
1732 let root = file.root_group();
1734 let sensors = root.group("sensors").unwrap();
1735 assert_eq!(sensors.name(), "/sensors");
1736
1737 let accel = sensors.group("accel").unwrap();
1738 assert_eq!(accel.name(), "/sensors/accel");
1739
1740 let top_groups = root.group_names().unwrap();
1742 assert!(top_groups.contains(&"sensors".to_string()));
1743
1744 let sub_groups = sensors.group_names().unwrap();
1746 assert!(sub_groups.contains(&"accel".to_string()));
1747 }
1748
1749 std::fs::remove_file(&path).ok();
1750 }
1751}
1752
1753#[cfg(test)]
1754mod h5py_compat_tests {
1755 use super::*;
1756
1757 #[cfg(feature = "deflate")]
1760 fn temp_path(name: &str) -> std::path::PathBuf {
1761 super::unique_test_path(name)
1762 }
1763
1764 #[test]
1766 #[cfg(feature = "deflate")]
1767 fn h5dump_validates_our_files() {
1768 let h5dump = std::process::Command::new("h5dump")
1770 .arg("--version")
1771 .output();
1772 if h5dump.is_err() {
1773 eprintln!("skipping: h5dump not found");
1774 return;
1775 }
1776
1777 let path = temp_path("h5dump_validate");
1778
1779 {
1781 let file = H5File::create(&path).unwrap();
1782
1783 let ds = file
1785 .new_dataset::<f64>()
1786 .shape([3usize, 4])
1787 .create("matrix")
1788 .unwrap();
1789 let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
1790 ds.write_raw(&data).unwrap();
1791
1792 let ds2 = file
1794 .new_dataset::<i32>()
1795 .shape([0usize, 2])
1796 .chunk(&[1, 2])
1797 .max_shape(&[None, Some(2)])
1798 .deflate(6)
1799 .create("stream")
1800 .unwrap();
1801 for i in 0..5u64 {
1802 let vals: Vec<i32> = vec![i as i32 * 2, i as i32 * 2 + 1];
1803 let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1804 ds2.write_chunk(i as usize, &raw).unwrap();
1805 }
1806 ds2.extend(&[5, 2]).unwrap();
1807
1808 let grp = file.create_group("meta").unwrap();
1810 let ds3 = grp
1811 .new_dataset::<u8>()
1812 .shape([4usize])
1813 .create("flags")
1814 .unwrap();
1815 ds3.write_raw(&[1u8, 0, 1, 0]).unwrap();
1816
1817 use crate::types::VarLenUnicode;
1819 let attr = ds
1820 .new_attr::<VarLenUnicode>()
1821 .shape(())
1822 .create("units")
1823 .unwrap();
1824 attr.write_string("meters").unwrap();
1825
1826 file.close().unwrap();
1827 }
1828
1829 let output = std::process::Command::new("h5dump")
1831 .arg("-H") .arg(path.to_str().unwrap())
1833 .output()
1834 .unwrap();
1835
1836 assert!(
1837 output.status.success(),
1838 "h5dump failed:\nstdout: {}\nstderr: {}",
1839 String::from_utf8_lossy(&output.stdout),
1840 String::from_utf8_lossy(&output.stderr),
1841 );
1842
1843 let output2 = std::process::Command::new("h5dump")
1845 .arg(path.to_str().unwrap())
1846 .output()
1847 .unwrap();
1848
1849 assert!(
1850 output2.status.success(),
1851 "h5dump (full) failed:\nstderr: {}",
1852 String::from_utf8_lossy(&output2.stderr),
1853 );
1854
1855 std::fs::remove_file(&path).ok();
1856 }
1857
1858 #[test]
1859 fn read_h5py_generated_file() {
1860 let path = "/tmp/test_h5py_default.h5";
1861 if !std::path::Path::new(path).exists() {
1862 eprintln!("skipping: h5py test file not found");
1863 return;
1864 }
1865 let file = H5File::open(path).unwrap();
1866
1867 let ds = file.dataset("data").unwrap();
1868 assert_eq!(ds.shape(), vec![4, 5]);
1869 let data = ds.read_raw::<f64>().unwrap();
1870 assert_eq!(data.len(), 20);
1871 assert!((data[0]).abs() < 1e-10);
1872 assert!((data[19] - 19.0).abs() < 1e-10);
1873
1874 let ds2 = file.dataset("images").unwrap();
1875 assert_eq!(ds2.shape(), vec![3, 64, 64]);
1876 let images = ds2.read_raw::<u16>().unwrap();
1877 assert_eq!(images.len(), 3 * 64 * 64);
1878 }
1879}