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