1use crate::attribute::AttrBuilder;
8use crate::error::{Hdf5Error, Result};
9use crate::file::{borrow_inner, borrow_inner_mut, clone_inner, H5FileInner, SharedInner};
10use crate::format::messages::datatype::DatatypeMessage;
11use crate::types::H5Type;
12
13pub struct DatasetBuilder<T: H5Type> {
30 file_inner: SharedInner,
31 shape: Option<Vec<usize>>,
32 chunk_dims: Option<Vec<usize>>,
33 max_shape: Option<Vec<Option<usize>>>,
34 deflate_level: Option<u32>,
35 shuffle_deflate_level: Option<u32>,
36 custom_pipeline: Option<crate::format::messages::filter::FilterPipeline>,
37 group_path: Option<String>,
38 fill_value: Option<Vec<u8>>,
39 datatype_override: Option<crate::format::messages::datatype::DatatypeMessage>,
40 _marker: std::marker::PhantomData<T>,
41}
42
43impl<T: H5Type> DatasetBuilder<T> {
44 pub(crate) fn new(file_inner: SharedInner) -> Self {
45 Self {
46 file_inner,
47 shape: None,
48 chunk_dims: None,
49 max_shape: None,
50 deflate_level: None,
51 shuffle_deflate_level: None,
52 custom_pipeline: None,
53 group_path: None,
54 fill_value: None,
55 datatype_override: None,
56 _marker: std::marker::PhantomData,
57 }
58 }
59
60 pub(crate) fn new_in_group(file_inner: SharedInner, group_path: String) -> Self {
61 Self {
62 file_inner,
63 shape: None,
64 chunk_dims: None,
65 max_shape: None,
66 deflate_level: None,
67 shuffle_deflate_level: None,
68 custom_pipeline: None,
69 group_path: Some(group_path),
70 fill_value: None,
71 datatype_override: None,
72 _marker: std::marker::PhantomData,
73 }
74 }
75
76 #[must_use]
81 pub fn shape<S: AsRef<[usize]>>(mut self, dims: S) -> Self {
82 self.shape = Some(dims.as_ref().to_vec());
83 self
84 }
85
86 #[must_use]
88 pub fn scalar(mut self) -> Self {
89 self.shape = Some(vec![]);
90 self
91 }
92
93 #[must_use]
99 pub fn chunk(mut self, chunk_dims: &[usize]) -> Self {
100 self.chunk_dims = Some(chunk_dims.to_vec());
101 self
102 }
103
104 #[must_use]
108 pub fn resizable(mut self) -> Self {
109 self.max_shape = Some(vec![None; self.shape.as_ref().map_or(0, |s| s.len())]);
110 self
111 }
112
113 #[must_use]
115 pub fn max_shape(mut self, max: &[Option<usize>]) -> Self {
116 self.max_shape = Some(max.to_vec());
117 self
118 }
119
120 #[must_use]
125 pub fn deflate(mut self, level: u32) -> Self {
126 self.deflate_level = Some(level);
127 self
128 }
129
130 #[must_use]
136 pub fn shuffle_deflate(mut self, level: u32) -> Self {
137 self.shuffle_deflate_level = Some(level);
138 self
139 }
140
141 #[must_use]
145 pub fn zstd(mut self, level: u32) -> Self {
146 self.custom_pipeline = Some(crate::format::messages::filter::FilterPipeline::zstd(level));
147 self
148 }
149
150 #[must_use]
155 pub fn filter_pipeline(
156 mut self,
157 pipeline: crate::format::messages::filter::FilterPipeline,
158 ) -> Self {
159 self.custom_pipeline = Some(pipeline);
160 self
161 }
162
163 #[must_use]
176 pub fn datatype(mut self, dt: crate::format::messages::datatype::DatatypeMessage) -> Self {
177 self.datatype_override = Some(dt);
178 self
179 }
180
181 #[must_use]
198 pub fn fill_value(mut self, value: T) -> Self {
199 let es = T::element_size();
200 let raw = unsafe { std::slice::from_raw_parts(&value as *const T as *const u8, es) };
204 self.fill_value = Some(raw.to_vec());
205 self
206 }
207
208 pub fn create(self, name: &str) -> Result<H5Dataset> {
213 let shape = self.shape.ok_or_else(|| {
214 Hdf5Error::InvalidState("shape must be set before calling create()".into())
215 })?;
216
217 let full_name = if let Some(ref gp) = self.group_path {
219 if gp == "/" {
220 name.to_string()
221 } else {
222 let trimmed = gp.trim_start_matches('/');
223 format!("{}/{}", trimmed, name)
224 }
225 } else {
226 name.to_string()
227 };
228 let group_path = self.group_path.clone();
229 let fill_value = self.fill_value.clone();
230
231 let dims_u64: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
232 let datatype = self.datatype_override.clone().unwrap_or_else(T::hdf5_type);
233 let element_size = T::element_size();
234
235 if let Some(ref chunk_dims) = self.chunk_dims {
236 let chunk_u64: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
238 let max_u64: Vec<u64> = if let Some(ref max) = self.max_shape {
239 max.iter()
240 .map(|m| m.map_or(u64::MAX, |v| v as u64))
241 .collect()
242 } else {
243 dims_u64.clone()
245 };
246
247 let n_unlimited = max_u64.iter().filter(|&&m| m == u64::MAX).count();
251 let is_btree2 = n_unlimited >= 2;
252 let is_fixed_array = n_unlimited == 0;
253 let wants_filter = self.custom_pipeline.is_some()
254 || self.shuffle_deflate_level.is_some()
255 || self.deflate_level.is_some();
256
257 let index = {
258 let mut inner = borrow_inner_mut(&self.file_inner);
259 match &mut *inner {
260 H5FileInner::Writer(writer) => {
261 let idx = if is_btree2 {
262 if wants_filter {
263 return Err(Hdf5Error::InvalidState(
264 "compression of v2 B-tree (multi-unlimited-dimension) \
265 datasets is not yet supported"
266 .into(),
267 ));
268 }
269 writer.create_btree_v2_dataset(
270 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
271 )?
272 } else if is_fixed_array {
273 if wants_filter {
279 let pipeline = if let Some(p) = self.custom_pipeline {
280 p
281 } else if let Some(level) = self.shuffle_deflate_level {
282 crate::format::messages::filter::FilterPipeline::shuffle_deflate(
283 T::element_size() as u32,
284 level,
285 )
286 } else {
287 crate::format::messages::filter::FilterPipeline::deflate(
289 self.deflate_level.unwrap(),
290 )
291 };
292 writer.create_fixed_array_dataset_with_pipeline(
293 &full_name, datatype, &dims_u64, &chunk_u64, pipeline,
294 )?
295 } else {
296 writer.create_fixed_array_dataset(
297 &full_name, datatype, &dims_u64, &chunk_u64,
298 )?
299 }
300 } else if let Some(pipeline) = self.custom_pipeline {
301 writer.create_chunked_dataset_with_pipeline(
302 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
303 )?
304 } else if let Some(level) = self.shuffle_deflate_level {
305 let pipeline =
306 crate::format::messages::filter::FilterPipeline::shuffle_deflate(
307 T::element_size() as u32,
308 level,
309 );
310 writer.create_chunked_dataset_with_pipeline(
311 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
312 )?
313 } else if let Some(level) = self.deflate_level {
314 writer.create_chunked_dataset_compressed(
315 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, level,
316 )?
317 } else {
318 writer.create_chunked_dataset(
319 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
320 )?
321 };
322 if let Some(ref gp) = group_path {
323 if gp != "/" {
324 writer.assign_dataset_to_group(gp, idx)?;
325 }
326 }
327 if let Some(ref fv) = fill_value {
328 writer.set_dataset_fill_value(idx, fv.clone())?;
329 }
330 idx
331 }
332 H5FileInner::Reader(_) => {
333 return Err(Hdf5Error::InvalidState(
334 "cannot create a dataset in read mode".into(),
335 ));
336 }
337 H5FileInner::Closed => {
338 return Err(Hdf5Error::InvalidState("file is closed".into()));
339 }
340 }
341 };
342
343 Ok(H5Dataset {
344 file_inner: clone_inner(&self.file_inner),
345 info: DatasetInfo::Writer {
346 index,
347 shape,
348 element_size,
349 chunked: true,
350 btree2: is_btree2,
351 fixed_array: is_fixed_array,
352 },
353 })
354 } else {
355 let index = {
357 let mut inner = borrow_inner_mut(&self.file_inner);
358 match &mut *inner {
359 H5FileInner::Writer(writer) => {
360 let idx = writer.create_dataset(&full_name, datatype, &dims_u64)?;
361 if let Some(ref gp) = group_path {
362 if gp != "/" {
363 writer.assign_dataset_to_group(gp, idx)?;
364 }
365 }
366 if let Some(ref fv) = fill_value {
367 writer.set_dataset_fill_value(idx, fv.clone())?;
368 }
369 idx
370 }
371 H5FileInner::Reader(_) => {
372 return Err(Hdf5Error::InvalidState(
373 "cannot create a dataset in read mode".into(),
374 ));
375 }
376 H5FileInner::Closed => {
377 return Err(Hdf5Error::InvalidState("file is closed".into()));
378 }
379 }
380 };
381
382 Ok(H5Dataset {
383 file_inner: clone_inner(&self.file_inner),
384 info: DatasetInfo::Writer {
385 index,
386 shape,
387 element_size,
388 chunked: false,
389 btree2: false,
390 fixed_array: false,
391 },
392 })
393 }
394 }
395}
396
397enum DatasetInfo {
403 Writer {
405 index: usize,
407 shape: Vec<usize>,
409 element_size: usize,
411 chunked: bool,
413 btree2: bool,
415 fixed_array: bool,
417 },
418 Reader {
420 name: String,
422 shape: Vec<usize>,
424 element_size: usize,
426 },
427}
428
429pub struct H5Dataset {
439 file_inner: SharedInner,
440 info: DatasetInfo,
441}
442
443impl H5Dataset {
444 pub(crate) fn new_reader(
446 file_inner: SharedInner,
447 name: String,
448 shape: Vec<usize>,
449 element_size: usize,
450 ) -> Self {
451 Self {
452 file_inner,
453 info: DatasetInfo::Reader {
454 name,
455 shape,
456 element_size,
457 },
458 }
459 }
460
461 pub(crate) fn new_writer(
467 file_inner: SharedInner,
468 index: usize,
469 shape: Vec<usize>,
470 element_size: usize,
471 chunked: bool,
472 btree2: bool,
473 fixed_array: bool,
474 ) -> Self {
475 Self {
476 file_inner,
477 info: DatasetInfo::Writer {
478 index,
479 shape,
480 element_size,
481 chunked,
482 btree2,
483 fixed_array,
484 },
485 }
486 }
487
488 pub fn shape(&self) -> Vec<usize> {
490 match &self.info {
491 DatasetInfo::Writer { shape, .. } => shape.clone(),
492 DatasetInfo::Reader { shape, .. } => shape.clone(),
493 }
494 }
495
496 pub fn ndims(&self) -> usize {
498 match &self.info {
499 DatasetInfo::Writer { shape, .. } => shape.len(),
500 DatasetInfo::Reader { shape, .. } => shape.len(),
501 }
502 }
503
504 pub fn total_elements(&self) -> usize {
506 match &self.info {
507 DatasetInfo::Writer { shape, .. } => shape.iter().product(),
508 DatasetInfo::Reader { shape, .. } => shape.iter().product(),
509 }
510 }
511
512 pub fn element_size(&self) -> usize {
514 match &self.info {
515 DatasetInfo::Writer { element_size, .. } => *element_size,
516 DatasetInfo::Reader { element_size, .. } => *element_size,
517 }
518 }
519
520 pub fn datatype(&self) -> Result<DatatypeMessage> {
551 match &self.info {
552 DatasetInfo::Reader { name, .. } => {
553 let inner = borrow_inner(&self.file_inner);
554 match &*inner {
555 H5FileInner::Reader(reader) => reader
556 .dataset_info(name)
557 .map(|info| info.datatype.clone())
558 .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
559 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
560 }
561 }
562 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
563 "datatype() is only available in read mode".into(),
564 )),
565 }
566 }
567
568 pub fn chunk_dims(&self) -> Option<Vec<usize>> {
570 match &self.info {
571 DatasetInfo::Reader { name, .. } => {
572 let inner = borrow_inner(&self.file_inner);
573 if let H5FileInner::Reader(reader) = &*inner {
574 if let Some(info) = reader.dataset_info(name) {
575 use crate::format::messages::data_layout::DataLayoutMessage;
576 let chunk_dims = match &info.layout {
577 DataLayoutMessage::ChunkedV4 { chunk_dims, .. }
578 | DataLayoutMessage::ChunkedV3 { chunk_dims, .. } => Some(chunk_dims),
579 _ => None,
580 };
581 if let Some(chunk_dims) = chunk_dims {
582 return Some(
584 chunk_dims[..chunk_dims.len() - 1]
585 .iter()
586 .map(|&d| d as usize)
587 .collect(),
588 );
589 }
590 }
591 }
592 None
593 }
594 DatasetInfo::Writer { .. } => None,
595 }
596 }
597
598 pub fn is_chunked(&self) -> bool {
600 match &self.info {
601 DatasetInfo::Writer { chunked, .. } => *chunked,
602 DatasetInfo::Reader { name, .. } => {
603 let inner = borrow_inner(&self.file_inner);
604 match &*inner {
605 H5FileInner::Reader(reader) => {
606 if let Some(info) = reader.dataset_info(name) {
607 use crate::format::messages::data_layout::DataLayoutMessage;
608 matches!(
609 info.layout,
610 DataLayoutMessage::ChunkedV4 { .. }
611 | DataLayoutMessage::ChunkedV3 { .. }
612 )
613 } else {
614 false
615 }
616 }
617 _ => false,
618 }
619 }
620 }
621 }
622
623 pub fn attr_names(&self) -> Result<Vec<String>> {
625 match &self.info {
626 DatasetInfo::Reader { name, .. } => {
627 let inner = borrow_inner(&self.file_inner);
628 match &*inner {
629 H5FileInner::Reader(reader) => Ok(reader.dataset_attr_names(name)?),
630 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
631 }
632 }
633 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
634 "attr_names not available in write mode".into(),
635 )),
636 }
637 }
638
639 pub fn attr(&self, attr_name: &str) -> Result<crate::attribute::H5Attribute> {
641 match &self.info {
642 DatasetInfo::Reader { name, .. } => {
643 let inner = borrow_inner(&self.file_inner);
644 match &*inner {
645 H5FileInner::Reader(reader) => {
646 let attr_msg = reader.dataset_attr(name, attr_name)?.clone();
647 Ok(crate::attribute::H5Attribute::new_reader(
648 clone_inner(&self.file_inner),
649 attr_msg,
650 ))
651 }
652 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
653 }
654 }
655 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
656 "attr() not available in write mode".into(),
657 )),
658 }
659 }
660
661 pub fn new_attr<T: 'static>(&self) -> AttrBuilder<'_, T> {
677 let ds_index = match &self.info {
678 DatasetInfo::Writer { index, .. } => *index,
679 DatasetInfo::Reader { .. } => {
680 usize::MAX
683 }
684 };
685 AttrBuilder::new(&self.file_inner, ds_index)
686 }
687
688 pub fn write_raw<T: H5Type>(&self, data: &[T]) -> Result<()> {
700 match &self.info {
701 DatasetInfo::Writer {
702 index,
703 shape,
704 element_size,
705 chunked,
706 btree2: _,
707 fixed_array: _,
708 } => {
709 if *chunked {
710 return Err(Hdf5Error::InvalidState(
711 "use write_chunk for chunked datasets".into(),
712 ));
713 }
714
715 let total_elements: usize = shape.iter().product();
716 if data.len() != total_elements {
717 return Err(Hdf5Error::InvalidState(format!(
718 "data length {} does not match dataset size {}",
719 data.len(),
720 total_elements,
721 )));
722 }
723
724 if T::element_size() != *element_size {
726 return Err(Hdf5Error::TypeMismatch(format!(
727 "write type has element size {} but dataset expects {}",
728 T::element_size(),
729 element_size,
730 )));
731 }
732
733 let byte_len = data.len() * T::element_size();
737 let raw =
738 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
739
740 let mut inner = borrow_inner_mut(&self.file_inner);
741 match &mut *inner {
742 H5FileInner::Writer(writer) => {
743 writer.write_dataset_raw(*index, raw)?;
744 Ok(())
745 }
746 _ => Err(Hdf5Error::InvalidState(
747 "file is no longer in write mode".into(),
748 )),
749 }
750 }
751 DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
752 "cannot write to a dataset opened in read mode".into(),
753 )),
754 }
755 }
756
757 pub fn write_chunk(&self, chunk_idx: usize, data: &[u8]) -> Result<()> {
765 match &self.info {
766 DatasetInfo::Writer {
767 index,
768 chunked,
769 btree2,
770 fixed_array,
771 ..
772 } => {
773 if !*chunked {
774 return Err(Hdf5Error::InvalidState(
775 "write_chunk is only for chunked datasets".into(),
776 ));
777 }
778 if *btree2 {
779 return Err(Hdf5Error::InvalidState(
780 "this dataset uses a v2 B-tree chunk index; use write_chunk_at \
781 with the chunk's grid coordinates"
782 .into(),
783 ));
784 }
785
786 let mut inner = borrow_inner_mut(&self.file_inner);
787 match &mut *inner {
788 H5FileInner::Writer(writer) => {
789 if *fixed_array {
790 let chunk_dims = writer
793 .dataset_chunk_dims(*index)
794 .ok_or_else(|| {
795 Hdf5Error::InvalidState("dataset has no chunk info".into())
796 })?
797 .to_vec();
798 let dims = writer.dataset_dims(*index).to_vec();
799 let mut grid = vec![0u64; dims.len()];
800 for d in 0..dims.len() {
801 grid[d] = if chunk_dims[d] > 0 {
802 dims[d].div_ceil(chunk_dims[d])
803 } else {
804 1
805 };
806 }
807 if grid.contains(&0) {
810 return Err(Hdf5Error::InvalidState(
811 "dataset has a zero-extent dimension and no chunks".into(),
812 ));
813 }
814 let mut rem = chunk_idx as u64;
815 let mut coords = vec![0u64; dims.len()];
816 for d in (0..dims.len()).rev() {
817 coords[d] = rem % grid[d];
818 rem /= grid[d];
819 }
820 if rem != 0 {
822 return Err(Hdf5Error::InvalidState(format!(
823 "chunk index {chunk_idx} is out of range for this dataset"
824 )));
825 }
826 writer.write_chunk_fixed_array(*index, &coords, data)?;
827 } else {
828 writer.write_chunk(*index, chunk_idx as u64, data)?;
829 }
830 Ok(())
831 }
832 _ => Err(Hdf5Error::InvalidState(
833 "file is no longer in write mode".into(),
834 )),
835 }
836 }
837 DatasetInfo::Reader { .. } => {
838 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
839 }
840 }
841 }
842
843 pub fn write_chunk_raw(&self, chunk_idx: usize, data: &[u8], filter_mask: u32) -> Result<()> {
869 match &self.info {
870 DatasetInfo::Writer {
871 index,
872 chunked,
873 btree2,
874 fixed_array,
875 ..
876 } => {
877 if !*chunked {
878 return Err(Hdf5Error::InvalidState(
879 "write_chunk_raw is only for chunked datasets".into(),
880 ));
881 }
882 if *btree2 {
883 return Err(Hdf5Error::InvalidState(
884 "direct chunk writes are not supported for v2-B-tree-indexed \
885 datasets (two or more unlimited dimensions)"
886 .into(),
887 ));
888 }
889
890 let mut inner = borrow_inner_mut(&self.file_inner);
891 match &mut *inner {
892 H5FileInner::Writer(writer) => {
893 if *fixed_array {
894 let chunk_dims = writer
897 .dataset_chunk_dims(*index)
898 .ok_or_else(|| {
899 Hdf5Error::InvalidState("dataset has no chunk info".into())
900 })?
901 .to_vec();
902 let dims = writer.dataset_dims(*index).to_vec();
903 let mut grid = vec![0u64; dims.len()];
904 for d in 0..dims.len() {
905 grid[d] = if chunk_dims[d] > 0 {
906 dims[d].div_ceil(chunk_dims[d])
907 } else {
908 1
909 };
910 }
911 if grid.contains(&0) {
914 return Err(Hdf5Error::InvalidState(
915 "dataset has a zero-extent dimension and no chunks".into(),
916 ));
917 }
918 let mut rem = chunk_idx as u64;
919 let mut coords = vec![0u64; dims.len()];
920 for d in (0..dims.len()).rev() {
921 coords[d] = rem % grid[d];
922 rem /= grid[d];
923 }
924 if rem != 0 {
926 return Err(Hdf5Error::InvalidState(format!(
927 "chunk index {chunk_idx} is out of range for this dataset"
928 )));
929 }
930 writer.write_compressed_chunk_fixed_array(
931 *index,
932 &coords,
933 data,
934 filter_mask,
935 )?;
936 } else {
937 writer.write_compressed_chunk(
938 *index,
939 chunk_idx as u64,
940 data,
941 filter_mask,
942 )?;
943 }
944 Ok(())
945 }
946 _ => Err(Hdf5Error::InvalidState(
947 "file is no longer in write mode".into(),
948 )),
949 }
950 }
951 DatasetInfo::Reader { .. } => {
952 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
953 }
954 }
955 }
956
957 pub fn write_chunk_at(&self, chunk_coords: &[usize], data: &[u8]) -> Result<()> {
978 match &self.info {
979 DatasetInfo::Writer {
980 index,
981 chunked,
982 btree2,
983 fixed_array,
984 ..
985 } => {
986 if !*chunked {
987 return Err(Hdf5Error::InvalidState(
988 "write_chunk_at is only for chunked datasets".into(),
989 ));
990 }
991 let coords: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
992 let btree2 = *btree2;
993 let fixed_array = *fixed_array;
994 let mut inner = borrow_inner_mut(&self.file_inner);
995 let writer = match &mut *inner {
996 H5FileInner::Writer(w) => w,
997 _ => {
998 return Err(Hdf5Error::InvalidState(
999 "file is no longer in write mode".into(),
1000 ))
1001 }
1002 };
1003 let chunk_dims = writer
1004 .dataset_chunk_dims(*index)
1005 .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1006 .to_vec();
1007 let dims = writer.dataset_dims(*index).to_vec();
1008 if coords.len() != dims.len() {
1009 return Err(Hdf5Error::InvalidState(format!(
1010 "chunk_coords has {} entries but the dataset has {} dimensions",
1011 coords.len(),
1012 dims.len()
1013 )));
1014 }
1015 if chunk_dims.len() != dims.len() {
1016 return Err(Hdf5Error::InvalidState(format!(
1017 "dataset chunk shape has {} dimensions but the dataspace has {}",
1018 chunk_dims.len(),
1019 dims.len()
1020 )));
1021 }
1022
1023 let mut new_dims = dims.clone();
1027 for d in 0..dims.len() {
1028 let needed = coords[d]
1029 .checked_add(1)
1030 .and_then(|c| c.checked_mul(chunk_dims[d]))
1031 .ok_or_else(|| {
1032 Hdf5Error::InvalidState(format!(
1033 "chunk coordinate {} in dimension {} is too large",
1034 coords[d], d
1035 ))
1036 })?;
1037 if needed > new_dims[d] {
1038 new_dims[d] = needed;
1039 }
1040 }
1041
1042 if fixed_array {
1043 writer.write_chunk_fixed_array(*index, &coords, data)?;
1045 return Ok(());
1046 }
1047
1048 if btree2 {
1049 writer.write_chunk_btree_v2(*index, &coords, data)?;
1050 } else {
1051 let mut linear = 0u64;
1054 for d in 0..dims.len() {
1055 let grid = if chunk_dims[d] > 0 {
1056 dims[d].div_ceil(chunk_dims[d])
1057 } else {
1058 1
1059 };
1060 linear = linear
1061 .checked_mul(grid)
1062 .and_then(|l| l.checked_add(coords[d]))
1063 .ok_or_else(|| {
1064 Hdf5Error::InvalidState(
1065 "chunk coordinates overflow the array index".into(),
1066 )
1067 })?;
1068 }
1069 writer.write_chunk(*index, linear, data)?;
1070 }
1071
1072 if new_dims != dims {
1073 writer.extend_dataset(*index, &new_dims)?;
1074 }
1075 Ok(())
1076 }
1077 DatasetInfo::Reader { .. } => {
1078 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1079 }
1080 }
1081 }
1082
1083 pub fn write_chunks_batch(&self, chunks: &[(usize, &[u8])]) -> Result<()> {
1089 match &self.info {
1090 DatasetInfo::Writer { index, chunked, .. } => {
1091 if !*chunked {
1092 return Err(Hdf5Error::InvalidState(
1093 "write_chunks_batch is only for chunked datasets".into(),
1094 ));
1095 }
1096 let pairs: Vec<(u64, &[u8])> = chunks
1097 .iter()
1098 .map(|(idx, data)| (*idx as u64, *data))
1099 .collect();
1100 let mut inner = borrow_inner_mut(&self.file_inner);
1101 match &mut *inner {
1102 H5FileInner::Writer(writer) => {
1103 writer.write_chunks_batch(*index, &pairs)?;
1104 Ok(())
1105 }
1106 _ => Err(Hdf5Error::InvalidState(
1107 "file is no longer in write mode".into(),
1108 )),
1109 }
1110 }
1111 DatasetInfo::Reader { .. } => {
1112 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1113 }
1114 }
1115 }
1116
1117 pub fn append<T: H5Type>(&self, data: &[T]) -> Result<()> {
1140 match &self.info {
1141 DatasetInfo::Writer {
1142 index,
1143 element_size,
1144 chunked,
1145 ..
1146 } => {
1147 if !*chunked {
1148 return Err(Hdf5Error::InvalidState(
1149 "append is only for chunked datasets".into(),
1150 ));
1151 }
1152 if T::element_size() != *element_size {
1153 return Err(Hdf5Error::TypeMismatch(format!(
1154 "append type has element size {} but dataset expects {}",
1155 T::element_size(),
1156 element_size,
1157 )));
1158 }
1159
1160 let ds_index = *index;
1161 let es = *element_size;
1162
1163 let mut inner = borrow_inner_mut(&self.file_inner);
1164 let writer = match &mut *inner {
1165 H5FileInner::Writer(w) => w,
1166 _ => {
1167 return Err(Hdf5Error::InvalidState(
1168 "file is no longer in write mode".into(),
1169 ))
1170 }
1171 };
1172
1173 let chunk_dims = writer
1174 .dataset_chunk_dims(ds_index)
1175 .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1176 .to_vec();
1177 let dims = writer.dataset_dims(ds_index).to_vec();
1178
1179 let frame_elems: usize = if dims.len() > 1 {
1181 dims[1..].iter().map(|&d| d as usize).product()
1182 } else {
1183 1
1184 };
1185
1186 if frame_elems == 0 {
1187 return Err(Hdf5Error::InvalidState(
1188 "cannot append to dataset with zero-size trailing dimensions".into(),
1189 ));
1190 }
1191
1192 if !data.len().is_multiple_of(frame_elems) {
1193 return Err(Hdf5Error::InvalidState(format!(
1194 "data length {} is not a multiple of frame size {}",
1195 data.len(),
1196 frame_elems,
1197 )));
1198 }
1199
1200 let n_new_frames = data.len() / frame_elems;
1201 let current_dim0 = dims[0] as usize;
1202
1203 let chunk_dim0 = chunk_dims[0] as usize;
1205 let frame_bytes = frame_elems * es;
1206
1207 let raw = unsafe {
1208 std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * es)
1209 };
1210
1211 let ds = &mut writer.datasets[ds_index];
1213 let buffered_frames = ds.append_buffered_frames as usize;
1214 let mut combined = std::mem::take(&mut ds.append_buffer);
1215 combined.extend_from_slice(raw);
1216 ds.append_buffered_frames = 0;
1217
1218 let total_frames = buffered_frames + n_new_frames;
1219 let total_bytes = combined.len();
1220
1221 let base_dim0 = current_dim0 - buffered_frames;
1223 let mut byte_pos = 0usize;
1224 let mut frame_pos = 0usize;
1225
1226 while frame_pos < total_frames {
1227 let abs_frame = base_dim0 + frame_pos;
1228 let chunk_idx = abs_frame / chunk_dim0;
1229 let remaining_frames = total_frames - frame_pos;
1230 let frames_to_fill = chunk_dim0 - (abs_frame % chunk_dim0);
1231
1232 if remaining_frames >= frames_to_fill {
1233 let end = byte_pos + frames_to_fill * frame_bytes;
1235 if frames_to_fill == chunk_dim0 {
1236 writer.write_chunk(
1237 ds_index,
1238 chunk_idx as u64,
1239 &combined[byte_pos..end],
1240 )?;
1241 } else {
1242 let offset_in_chunk = (abs_frame % chunk_dim0) * frame_bytes;
1248 let mut chunk_buf =
1249 match writer.read_chunk_if_present(ds_index, chunk_idx as u64)? {
1250 Some(existing) => existing,
1251 None => {
1252 return Err(Hdf5Error::InvalidState(format!(
1253 "cannot append into partially-written chunk {}: \
1254 its existing content was not found in the chunk \
1255 index (the file may be inconsistent)",
1256 chunk_idx
1257 )));
1258 }
1259 };
1260 chunk_buf
1261 [offset_in_chunk..offset_in_chunk + frames_to_fill * frame_bytes]
1262 .copy_from_slice(&combined[byte_pos..end]);
1263 writer.write_chunk(ds_index, chunk_idx as u64, &chunk_buf)?;
1264 }
1265 byte_pos = end;
1266 frame_pos += frames_to_fill;
1267 } else {
1268 let ds = &mut writer.datasets[ds_index];
1270 ds.append_buffer = combined[byte_pos..total_bytes].to_vec();
1271 ds.append_buffered_frames = remaining_frames as u64;
1272 frame_pos = total_frames;
1273 }
1274 }
1275
1276 let logical_dim0 = base_dim0 + total_frames;
1278 let mut new_dims: Vec<u64> = dims;
1279 new_dims[0] = logical_dim0 as u64;
1280 writer.extend_dataset(ds_index, &new_dims)?;
1281
1282 Ok(())
1283 }
1284 DatasetInfo::Reader { .. } => {
1285 Err(Hdf5Error::InvalidState("cannot append in read mode".into()))
1286 }
1287 }
1288 }
1289
1290 pub fn extend(&self, new_dims: &[usize]) -> Result<()> {
1292 match &self.info {
1293 DatasetInfo::Writer { index, chunked, .. } => {
1294 if !*chunked {
1295 return Err(Hdf5Error::InvalidState(
1296 "extend is only for chunked datasets".into(),
1297 ));
1298 }
1299
1300 let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1301 let mut inner = borrow_inner_mut(&self.file_inner);
1302 match &mut *inner {
1303 H5FileInner::Writer(writer) => {
1304 writer.extend_dataset(*index, &dims_u64)?;
1305 Ok(())
1306 }
1307 _ => Err(Hdf5Error::InvalidState(
1308 "file is no longer in write mode".into(),
1309 )),
1310 }
1311 }
1312 DatasetInfo::Reader { .. } => {
1313 Err(Hdf5Error::InvalidState("cannot extend in read mode".into()))
1314 }
1315 }
1316 }
1317
1318 pub fn set_extent(&self, new_dims: &[usize]) -> Result<()> {
1329 match &self.info {
1330 DatasetInfo::Writer { index, .. } => {
1331 let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1332 let mut inner = borrow_inner_mut(&self.file_inner);
1333 match &mut *inner {
1334 H5FileInner::Writer(writer) => {
1335 writer.set_dataset_extent(*index, &dims_u64)?;
1336 Ok(())
1337 }
1338 _ => Err(Hdf5Error::InvalidState(
1339 "file is no longer in write mode".into(),
1340 )),
1341 }
1342 }
1343 DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
1344 "cannot set extent in read mode".into(),
1345 )),
1346 }
1347 }
1348
1349 pub fn flush(&self) -> Result<()> {
1351 match &self.info {
1352 DatasetInfo::Writer { index, .. } => {
1353 let mut inner = borrow_inner_mut(&self.file_inner);
1354 match &mut *inner {
1355 H5FileInner::Writer(writer) => {
1356 writer.flush_dataset(*index)?;
1357 Ok(())
1358 }
1359 _ => Ok(()),
1360 }
1361 }
1362 DatasetInfo::Reader { .. } => Ok(()),
1363 }
1364 }
1365
1366 pub fn read_slice<T: H5Type>(&self, starts: &[usize], counts: &[usize]) -> Result<Vec<T>> {
1371 match &self.info {
1372 DatasetInfo::Reader {
1373 name, element_size, ..
1374 } => {
1375 if T::element_size() != *element_size {
1376 return Err(Hdf5Error::TypeMismatch(format!(
1377 "read type has element size {} but dataset has element size {}",
1378 T::element_size(),
1379 element_size,
1380 )));
1381 }
1382 let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1383 let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1384
1385 let raw = {
1386 let mut inner = borrow_inner_mut(&self.file_inner);
1387 match &mut *inner {
1388 H5FileInner::Reader(reader) => {
1389 reader.read_slice(name, &starts_u64, &counts_u64)?
1390 }
1391 _ => {
1392 return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
1393 }
1394 }
1395 };
1396
1397 if raw.len() % T::element_size() != 0 {
1398 return Err(Hdf5Error::TypeMismatch(format!(
1399 "raw data size {} is not a multiple of element size {}",
1400 raw.len(),
1401 T::element_size(),
1402 )));
1403 }
1404
1405 let count = raw.len() / T::element_size();
1406 let mut result = Vec::<T>::with_capacity(count);
1407 unsafe {
1408 std::ptr::copy_nonoverlapping(
1409 raw.as_ptr(),
1410 result.as_mut_ptr() as *mut u8,
1411 raw.len(),
1412 );
1413 result.set_len(count);
1414 }
1415 Ok(result)
1416 }
1417 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1418 "cannot read_slice from a dataset in write mode".into(),
1419 )),
1420 }
1421 }
1422
1423 pub fn write_slice<T: H5Type>(
1427 &self,
1428 starts: &[usize],
1429 counts: &[usize],
1430 data: &[T],
1431 ) -> Result<()> {
1432 match &self.info {
1433 DatasetInfo::Writer {
1434 index,
1435 element_size,
1436 chunked,
1437 ..
1438 } => {
1439 if *chunked {
1440 return Err(Hdf5Error::InvalidState(
1441 "write_slice is only for contiguous datasets".into(),
1442 ));
1443 }
1444 if T::element_size() != *element_size {
1445 return Err(Hdf5Error::TypeMismatch(format!(
1446 "write type has element size {} but dataset expects {}",
1447 T::element_size(),
1448 element_size,
1449 )));
1450 }
1451
1452 let expected: usize = counts.iter().product();
1453 if data.len() != expected {
1454 return Err(Hdf5Error::InvalidState(format!(
1455 "data length {} does not match slice size {}",
1456 data.len(),
1457 expected,
1458 )));
1459 }
1460
1461 let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1462 let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1463
1464 let byte_len = data.len() * T::element_size();
1465 let raw =
1466 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
1467
1468 let mut inner = borrow_inner_mut(&self.file_inner);
1469 match &mut *inner {
1470 H5FileInner::Writer(writer) => {
1471 writer.write_slice(*index, &starts_u64, &counts_u64, raw)?;
1472 Ok(())
1473 }
1474 _ => Err(Hdf5Error::InvalidState(
1475 "file is no longer in write mode".into(),
1476 )),
1477 }
1478 }
1479 DatasetInfo::Reader { .. } => {
1480 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1481 }
1482 }
1483 }
1484
1485 pub fn read_vlen_strings(&self) -> Result<Vec<String>> {
1490 match &self.info {
1491 DatasetInfo::Reader { name, .. } => {
1492 let mut inner = borrow_inner_mut(&self.file_inner);
1493 match &mut *inner {
1494 H5FileInner::Reader(reader) => Ok(reader.read_vlen_strings(name)?),
1495 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1496 }
1497 }
1498 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1499 "cannot read vlen strings from a dataset in write mode".into(),
1500 )),
1501 }
1502 }
1503
1504 pub fn read_raw<T: H5Type>(&self) -> Result<Vec<T>> {
1516 match &self.info {
1517 DatasetInfo::Reader {
1518 name, element_size, ..
1519 } => {
1520 if T::element_size() != *element_size {
1521 return Err(Hdf5Error::TypeMismatch(format!(
1522 "read type has element size {} but dataset has element size {}",
1523 T::element_size(),
1524 element_size,
1525 )));
1526 }
1527
1528 let raw = {
1529 let mut inner = borrow_inner_mut(&self.file_inner);
1530 match &mut *inner {
1531 H5FileInner::Reader(reader) => reader.read_dataset_raw(name)?,
1532 _ => {
1533 return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
1534 }
1535 }
1536 };
1537
1538 if raw.len() % T::element_size() != 0 {
1539 return Err(Hdf5Error::TypeMismatch(format!(
1540 "raw data size {} is not a multiple of element size {}",
1541 raw.len(),
1542 T::element_size(),
1543 )));
1544 }
1545
1546 let count = raw.len() / T::element_size();
1547 let mut result = Vec::<T>::with_capacity(count);
1548
1549 unsafe {
1556 std::ptr::copy_nonoverlapping(
1557 raw.as_ptr(),
1558 result.as_mut_ptr() as *mut u8,
1559 raw.len(),
1560 );
1561 result.set_len(count);
1562 }
1563
1564 Ok(result)
1565 }
1566 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1567 "cannot read from a dataset in write mode".into(),
1568 )),
1569 }
1570 }
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575 use crate::H5File;
1576 use std::path::PathBuf;
1577
1578 fn temp_path(name: &str) -> PathBuf {
1579 use std::sync::atomic::{AtomicU64, Ordering};
1583 static COUNTER: AtomicU64 = AtomicU64::new(0);
1584 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1585 std::env::temp_dir().join(format!(
1586 "hdf5_dataset_test_{}_{}_{}.h5",
1587 name,
1588 std::process::id(),
1589 n
1590 ))
1591 }
1592
1593 #[test]
1594 fn builder_requires_shape() {
1595 let path = temp_path("no_shape");
1596 let file = H5File::create(&path).unwrap();
1597 let result = file.new_dataset::<u8>().create("data");
1598 assert!(result.is_err());
1599 std::fs::remove_file(&path).ok();
1600 }
1601
1602 #[test]
1603 fn write_raw_size_mismatch() {
1604 let path = temp_path("size_mismatch");
1605 let file = H5File::create(&path).unwrap();
1606 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1607 let result = ds.write_raw(&[1u8, 2, 3]);
1609 assert!(result.is_err());
1610 std::fs::remove_file(&path).ok();
1611 }
1612
1613 #[test]
1614 fn roundtrip_u8_1d() {
1615 let path = temp_path("rt_u8_1d");
1616 let data: Vec<u8> = (0..10).collect();
1617
1618 {
1619 let file = H5File::create(&path).unwrap();
1620 let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
1621 ds.write_raw(&data).unwrap();
1622 file.close().unwrap();
1623 }
1624
1625 {
1626 let file = H5File::open(&path).unwrap();
1627 let ds = file.dataset("seq").unwrap();
1628 assert_eq!(ds.shape(), vec![10]);
1629 let readback = ds.read_raw::<u8>().unwrap();
1630 assert_eq!(readback, data);
1631 }
1632
1633 std::fs::remove_file(&path).ok();
1634 }
1635
1636 #[test]
1637 fn roundtrip_i32_2d() {
1638 let path = temp_path("rt_i32_2d");
1639 let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
1640
1641 {
1642 let file = H5File::create(&path).unwrap();
1643 let ds = file
1644 .new_dataset::<i32>()
1645 .shape([2, 3])
1646 .create("matrix")
1647 .unwrap();
1648 ds.write_raw(&data).unwrap();
1649 file.close().unwrap();
1650 }
1651
1652 {
1653 let file = H5File::open(&path).unwrap();
1654 let ds = file.dataset("matrix").unwrap();
1655 assert_eq!(ds.shape(), vec![2, 3]);
1656 let readback = ds.read_raw::<i32>().unwrap();
1657 assert_eq!(readback, data);
1658 }
1659
1660 std::fs::remove_file(&path).ok();
1661 }
1662
1663 #[test]
1664 fn roundtrip_f64_3d() {
1665 let path = temp_path("rt_f64_3d");
1666 let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
1667
1668 {
1669 let file = H5File::create(&path).unwrap();
1670 let ds = file
1671 .new_dataset::<f64>()
1672 .shape([2, 3, 4])
1673 .create("cube")
1674 .unwrap();
1675 ds.write_raw(&data).unwrap();
1676 file.close().unwrap();
1677 }
1678
1679 {
1680 let file = H5File::open(&path).unwrap();
1681 let ds = file.dataset("cube").unwrap();
1682 assert_eq!(ds.shape(), vec![2, 3, 4]);
1683 let readback = ds.read_raw::<f64>().unwrap();
1684 assert_eq!(readback, data);
1685 }
1686
1687 std::fs::remove_file(&path).ok();
1688 }
1689
1690 #[test]
1691 fn cannot_read_in_write_mode() {
1692 let path = temp_path("no_read_write");
1693 let file = H5File::create(&path).unwrap();
1694 let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
1695 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1696 let result = ds.read_raw::<u8>();
1697 assert!(result.is_err());
1698 std::fs::remove_file(&path).ok();
1699 }
1700
1701 #[test]
1702 fn cannot_write_in_read_mode() {
1703 let path = temp_path("no_write_read");
1704
1705 {
1706 let file = H5File::create(&path).unwrap();
1707 let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
1708 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1709 file.close().unwrap();
1710 }
1711
1712 {
1713 let file = H5File::open(&path).unwrap();
1714 let ds = file.dataset("x").unwrap();
1715 let result = ds.write_raw(&[5u8, 6, 7, 8]);
1716 assert!(result.is_err());
1717 }
1718
1719 std::fs::remove_file(&path).ok();
1720 }
1721
1722 #[test]
1723 fn numeric_attr_roundtrip() {
1724 let path = temp_path("num_attr");
1725 {
1726 let file = H5File::create(&path).unwrap();
1727 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1728 ds.write_raw(&[1.0f32; 4]).unwrap();
1729
1730 let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
1731 a1.write_numeric(&1.2345f64).unwrap();
1732
1733 let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
1734 a2.write_numeric(&42i32).unwrap();
1735
1736 file.close().unwrap();
1737 }
1738 {
1739 let file = H5File::open(&path).unwrap();
1740 let ds = file.dataset("data").unwrap();
1741
1742 let scale = ds.attr("scale").unwrap();
1743 let val: f64 = scale.read_numeric().unwrap();
1744 assert!((val - 1.2345).abs() < 1e-10);
1745
1746 let count = ds.attr("count").unwrap();
1747 let val: i32 = count.read_numeric().unwrap();
1748 assert_eq!(val, 42);
1749 }
1750 std::fs::remove_file(&path).ok();
1751 }
1752
1753 #[test]
1754 fn array_attr_roundtrip() {
1755 let path = temp_path("array_attr");
1756 let offsets = [10i32, -20, 30];
1757 {
1758 let file = H5File::create(&path).unwrap();
1759 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1760 ds.write_raw(&[1.0f32; 4]).unwrap();
1761
1762 let a = ds
1764 .new_attr::<i32>()
1765 .shape([3])
1766 .create("dim_offset")
1767 .unwrap();
1768 a.write_array(&offsets).unwrap();
1769
1770 let bad = ds.new_attr::<i32>().shape([3]).create("bad").unwrap();
1772 assert!(bad.write_array(&[1i32, 2]).is_err());
1773
1774 file.close().unwrap();
1775 }
1776 {
1777 let file = H5File::open(&path).unwrap();
1778 let ds = file.dataset("data").unwrap();
1779 let a = ds.attr("dim_offset").unwrap();
1780 let raw = a.read_raw().unwrap();
1781 assert_eq!(raw.len(), 3 * 4);
1782 let got: Vec<i32> = raw
1783 .chunks_exact(4)
1784 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1785 .collect();
1786 assert_eq!(got, offsets);
1787 }
1788 std::fs::remove_file(&path).ok();
1789 }
1790
1791 #[test]
1792 fn attr_datatype_exposes_class_and_sign() {
1793 use crate::format::messages::datatype::DatatypeMessage;
1797
1798 let path = temp_path("attr_datatype");
1799 {
1800 let file = H5File::create(&path).unwrap();
1801 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1802 ds.new_attr::<f64>()
1803 .shape(())
1804 .create("scale")
1805 .unwrap()
1806 .write_numeric(&1.5f64)
1807 .unwrap();
1808 ds.new_attr::<i32>()
1809 .shape(())
1810 .create("count")
1811 .unwrap()
1812 .write_numeric(&7i32)
1813 .unwrap();
1814 file.close().unwrap();
1815 }
1816 {
1817 let file = H5File::open(&path).unwrap();
1818 let ds = file.dataset("data").unwrap();
1819
1820 match ds.attr("scale").unwrap().datatype().unwrap() {
1821 DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
1822 other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
1823 }
1824
1825 match ds.attr("count").unwrap().datatype().unwrap() {
1826 DatatypeMessage::FixedPoint { size, signed, .. } => {
1827 assert_eq!(size, 4);
1828 assert!(signed, "i32 attr must be signed");
1829 }
1830 other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
1831 }
1832 }
1833 std::fs::remove_file(&path).ok();
1834 }
1835
1836 #[test]
1837 fn attr_datatype_in_write_mode_errors() {
1838 let path = temp_path("attr_datatype_write_mode");
1839 let file = H5File::create(&path).unwrap();
1840 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1841 let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
1842 assert!(attr.datatype().is_err());
1843 std::fs::remove_file(&path).ok();
1844 }
1845
1846 #[test]
1847 fn cannot_create_dataset_in_read_mode() {
1848 let path = temp_path("no_create_read");
1849
1850 {
1851 let _file = H5File::create(&path).unwrap();
1852 }
1853
1854 {
1855 let file = H5File::open(&path).unwrap();
1856 let result = file.new_dataset::<u8>().shape([4]).create("x");
1857 assert!(result.is_err());
1858 }
1859
1860 std::fs::remove_file(&path).ok();
1861 }
1862
1863 #[test]
1864 fn shape_accessor() {
1865 let path = temp_path("shape_acc");
1866
1867 let file = H5File::create(&path).unwrap();
1868 let ds = file
1869 .new_dataset::<f32>()
1870 .shape([5, 10, 3])
1871 .create("tensor")
1872 .unwrap();
1873 assert_eq!(ds.shape(), vec![5, 10, 3]);
1874
1875 std::fs::remove_file(&path).ok();
1876 }
1877
1878 #[test]
1879 fn slice_roundtrip_2d() {
1880 let path = temp_path("slice_2d");
1881
1882 let data: Vec<i32> = (0..20).collect();
1884 {
1885 let file = H5File::create(&path).unwrap();
1886 let ds = file
1887 .new_dataset::<i32>()
1888 .shape([4, 5])
1889 .create("mat")
1890 .unwrap();
1891 ds.write_raw(&data).unwrap();
1892 file.close().unwrap();
1893 }
1894 {
1895 let file = H5File::open(&path).unwrap();
1896 let ds = file.dataset("mat").unwrap();
1897 let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
1899 assert_eq!(slice, vec![7, 8, 12, 13]);
1902 }
1903
1904 std::fs::remove_file(&path).ok();
1905 }
1906
1907 #[test]
1908 fn write_slice_2d() {
1909 let path = temp_path("write_slice_2d");
1910
1911 {
1912 let file = H5File::create(&path).unwrap();
1913 let ds = file
1914 .new_dataset::<f32>()
1915 .shape([3, 4])
1916 .create("data")
1917 .unwrap();
1918 ds.write_raw(&[0.0f32; 12]).unwrap();
1919 ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
1921 .unwrap();
1922 file.close().unwrap();
1923 }
1924 {
1925 let file = H5File::open(&path).unwrap();
1926 let ds = file.dataset("data").unwrap();
1927 let full = ds.read_raw::<f32>().unwrap();
1928 assert_eq!(
1932 full,
1933 vec![0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 30.0, 40.0, 0.0,]
1934 );
1935 }
1936
1937 std::fs::remove_file(&path).ok();
1938 }
1939
1940 #[test]
1941 fn write_slice_out_of_bounds_rejected() {
1942 let path = temp_path("write_slice_oob");
1943 let file = H5File::create(&path).unwrap();
1944 let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
1945 ds.write_raw(&[0i32; 4]).unwrap();
1946 assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
1948 assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
1950 std::fs::remove_file(&path).ok();
1951 }
1952
1953 #[test]
1954 fn duplicate_dataset_name_rejected() {
1955 let path = temp_path("dup_name");
1956 let file = H5File::create(&path).unwrap();
1957 let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
1958 assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
1959 std::fs::remove_file(&path).ok();
1960 }
1961
1962 #[test]
1963 fn extend_cannot_shrink() {
1964 let path = temp_path("extend_shrink");
1965 let file = H5File::create(&path).unwrap();
1966 let ds = file
1967 .new_dataset::<i32>()
1968 .shape([0])
1969 .chunk(&[2])
1970 .max_shape(&[None])
1971 .create("d")
1972 .unwrap();
1973 ds.append(&[1i32, 2, 3, 4]).unwrap();
1974 assert!(ds.extend(&[2]).is_err());
1976 assert!(ds.extend(&[6]).is_ok());
1978 std::fs::remove_file(&path).ok();
1979 }
1980
1981 #[test]
1982 fn attr_read_roundtrip() {
1983 use crate::types::VarLenUnicode;
1984 let path = temp_path("attr_read");
1985
1986 {
1987 let file = H5File::create(&path).unwrap();
1988 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1989 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1990 let a1 = ds
1991 .new_attr::<VarLenUnicode>()
1992 .shape(())
1993 .create("units")
1994 .unwrap();
1995 a1.write_string("meters").unwrap();
1996 let a2 = ds
1997 .new_attr::<VarLenUnicode>()
1998 .shape(())
1999 .create("desc")
2000 .unwrap();
2001 a2.write_string("test data").unwrap();
2002 file.close().unwrap();
2003 }
2004 {
2005 let file = H5File::open(&path).unwrap();
2006 let ds = file.dataset("data").unwrap();
2007
2008 let names = ds.attr_names().unwrap();
2009 assert!(names.contains(&"units".to_string()));
2010 assert!(names.contains(&"desc".to_string()));
2011
2012 let units = ds.attr("units").unwrap();
2013 assert_eq!(units.read_string().unwrap(), "meters");
2014
2015 let desc = ds.attr("desc").unwrap();
2016 assert_eq!(desc.read_string().unwrap(), "test data");
2017 }
2018
2019 std::fs::remove_file(&path).ok();
2020 }
2021
2022 #[test]
2023 fn type_mismatch_element_size() {
2024 let path = temp_path("type_mismatch");
2025
2026 {
2027 let file = H5File::create(&path).unwrap();
2028 let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
2029 ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
2030 file.close().unwrap();
2031 }
2032
2033 {
2034 let file = H5File::open(&path).unwrap();
2035 let ds = file.dataset("data").unwrap();
2036 let result = ds.read_raw::<u8>();
2038 assert!(result.is_err());
2039 }
2040
2041 std::fs::remove_file(&path).ok();
2042 }
2043
2044 #[test]
2045 fn dataset_survives_file_move() {
2046 let path = temp_path("ds_survives");
2047
2048 let ds = {
2049 let file = H5File::create(&path).unwrap();
2050 file.new_dataset::<u8>().shape([4]).create("x").unwrap()
2051 };
2052 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2054 std::fs::remove_file(&path).ok();
2057 }
2058
2059 #[test]
2060 fn new_attr_scalar_string() {
2061 use crate::types::VarLenUnicode;
2062
2063 let path = temp_path("attr_scalar_string");
2064 {
2065 let file = H5File::create(&path).unwrap();
2066 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2067 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2068
2069 let attr = ds
2070 .new_attr::<VarLenUnicode>()
2071 .shape(())
2072 .create("name")
2073 .unwrap();
2074 attr.write_scalar(&VarLenUnicode("test_value".to_string()))
2075 .unwrap();
2076
2077 file.close().unwrap();
2078 }
2079
2080 {
2082 let file = H5File::open(&path).unwrap();
2083 let ds = file.dataset("data").unwrap();
2084 assert_eq!(ds.shape(), vec![4]);
2085 let readback = ds.read_raw::<u8>().unwrap();
2086 assert_eq!(readback, vec![1u8, 2, 3, 4]);
2087 }
2088
2089 std::fs::remove_file(&path).ok();
2090 }
2091
2092 #[test]
2093 fn all_numeric_types_roundtrip() {
2094 let path = temp_path("all_types");
2095
2096 {
2097 let file = H5File::create(&path).unwrap();
2098
2099 let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
2100 ds.write_raw(&[1u8, 2]).unwrap();
2101
2102 let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
2103 ds.write_raw(&[-1i8, 1]).unwrap();
2104
2105 let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
2106 ds.write_raw(&[100u16, 200]).unwrap();
2107
2108 let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
2109 ds.write_raw(&[-100i16, 100]).unwrap();
2110
2111 let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
2112 ds.write_raw(&[1000u32, 2000]).unwrap();
2113
2114 let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
2115 ds.write_raw(&[-1000i32, 1000]).unwrap();
2116
2117 let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
2118 ds.write_raw(&[10000u64, 20000]).unwrap();
2119
2120 let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
2121 ds.write_raw(&[-10000i64, 10000]).unwrap();
2122
2123 let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
2124 ds.write_raw(&[1.5f32, 2.5]).unwrap();
2125
2126 let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
2127 ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
2128
2129 file.close().unwrap();
2130 }
2131
2132 {
2133 let file = H5File::open(&path).unwrap();
2134
2135 assert_eq!(
2136 file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
2137 vec![1u8, 2]
2138 );
2139 assert_eq!(
2140 file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2141 vec![-1i8, 1]
2142 );
2143 assert_eq!(
2144 file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2145 vec![100u16, 200]
2146 );
2147 assert_eq!(
2148 file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2149 vec![-100i16, 100]
2150 );
2151 assert_eq!(
2152 file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2153 vec![1000u32, 2000]
2154 );
2155 assert_eq!(
2156 file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2157 vec![-1000i32, 1000]
2158 );
2159 assert_eq!(
2160 file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2161 vec![10000u64, 20000]
2162 );
2163 assert_eq!(
2164 file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2165 vec![-10000i64, 10000]
2166 );
2167 assert_eq!(
2168 file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2169 vec![1.5f32, 2.5]
2170 );
2171 assert_eq!(
2172 file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2173 vec![1.23456f64, 7.89012]
2174 );
2175 }
2176
2177 std::fs::remove_file(&path).ok();
2178 }
2179
2180 #[test]
2181 fn append_chunked_roundtrip() {
2182 let path = temp_path("append_chunked");
2183
2184 {
2185 let file = H5File::create(&path).unwrap();
2186 let ds = file
2187 .new_dataset::<f64>()
2188 .shape([0, 3])
2189 .chunk(&[1, 3])
2190 .max_shape(&[None, Some(3)])
2191 .create("data")
2192 .unwrap();
2193
2194 ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2196 ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2198
2199 file.close().unwrap();
2200 }
2201
2202 {
2203 let file = H5File::open(&path).unwrap();
2204 let ds = file.dataset("data").unwrap();
2205 assert_eq!(ds.shape(), vec![3, 3]);
2206 let all = ds.read_raw::<f64>().unwrap();
2207 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2208 }
2209
2210 std::fs::remove_file(&path).ok();
2211 }
2212
2213 #[test]
2214 fn append_1d_chunked() {
2215 let path = temp_path("append_1d");
2216
2217 {
2218 let file = H5File::create(&path).unwrap();
2219 let ds = file
2220 .new_dataset::<i32>()
2221 .shape([0])
2222 .chunk(&[4])
2223 .max_shape(&[None])
2224 .create("values")
2225 .unwrap();
2226
2227 ds.append(&[10i32, 20, 30]).unwrap(); ds.append(&[40i32]).unwrap(); ds.append(&[50i32, 60, 70, 80]).unwrap(); file.close().unwrap();
2232 }
2233
2234 {
2235 let file = H5File::open(&path).unwrap();
2236 let ds = file.dataset("values").unwrap();
2237 assert_eq!(ds.shape(), vec![8]);
2238 let all = ds.read_raw::<i32>().unwrap();
2239 assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2240 }
2241
2242 std::fs::remove_file(&path).ok();
2243 }
2244
2245 #[test]
2246 fn append_partial_chunk_flushed_on_close() {
2247 let path = temp_path("append_partial_close");
2248
2249 {
2250 let file = H5File::create(&path).unwrap();
2251 let ds = file
2252 .new_dataset::<f64>()
2253 .shape([0])
2254 .chunk(&[4])
2255 .max_shape(&[None])
2256 .create("vals")
2257 .unwrap();
2258
2259 ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
2261 file.close().unwrap();
2262 }
2263
2264 {
2265 let file = H5File::open(&path).unwrap();
2266 let ds = file.dataset("vals").unwrap();
2267 assert_eq!(ds.shape(), vec![5]);
2268 let all = ds.read_raw::<f64>().unwrap();
2269 assert_eq!(all.len(), 5);
2272 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2273 }
2274
2275 std::fs::remove_file(&path).ok();
2276 }
2277
2278 #[cfg(feature = "deflate")]
2279 #[test]
2280 fn vlen_append_after_reopen_filtered() {
2281 let path = temp_path("vlen_reopen_filtered");
2285 {
2286 let file = H5File::create(&path).unwrap();
2287 file.create_appendable_vlen_dataset(
2288 "strs",
2289 4,
2290 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2291 )
2292 .unwrap();
2293 file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
2294 .unwrap();
2295 file.close().unwrap();
2296 }
2297 {
2298 let file = H5File::open_rw(&path).unwrap();
2299 file.append_vlen_strings("strs", &["delta"]).unwrap();
2300 file.close().unwrap();
2301 }
2302 {
2303 let file = H5File::open(&path).unwrap();
2304 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2305 assert_eq!(
2306 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2307 vec!["alpha", "beta", "gamma", "delta"]
2308 );
2309 }
2310 std::fs::remove_file(&path).ok();
2311 }
2312
2313 #[test]
2314 fn vlen_append_after_reopen_data_block() {
2315 let path = temp_path("vlen_reopen_datablk");
2319 let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
2320 {
2321 let file = H5File::create(&path).unwrap();
2322 file.create_appendable_vlen_dataset("strs", 2, None)
2323 .unwrap();
2324 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2325 file.append_vlen_strings("strs", &refs).unwrap();
2326 file.close().unwrap();
2327 }
2328 {
2329 let file = H5File::open_rw(&path).unwrap();
2330 file.append_vlen_strings("strs", &["s9"]).unwrap();
2331 file.close().unwrap();
2332 }
2333 {
2334 let file = H5File::open(&path).unwrap();
2335 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2336 let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
2337 assert_eq!(got, want);
2338 }
2339 std::fs::remove_file(&path).ok();
2340 }
2341
2342 #[test]
2343 fn vlen_append_after_reopen_super_block() {
2344 let path = temp_path("vlen_reopen_super");
2352 let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
2355 {
2356 let file = H5File::create(&path).unwrap();
2357 file.create_appendable_vlen_dataset("strs", 2, None)
2358 .unwrap();
2359 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2360 file.append_vlen_strings("strs", &refs).unwrap();
2361 file.close().unwrap();
2362 }
2363 {
2364 let file = H5File::open_rw(&path).unwrap();
2365 file.append_vlen_strings("strs", &["v489"]).unwrap();
2366 file.close().unwrap();
2367 }
2368 {
2369 let file = H5File::open(&path).unwrap();
2370 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2371 let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
2372 assert_eq!(got, want);
2373 }
2374 std::fs::remove_file(&path).ok();
2375 }
2376
2377 #[cfg(feature = "deflate")]
2378 #[test]
2379 fn vlen_append_after_reopen_filtered_data_block() {
2380 let path = temp_path("vlen_reopen_filt_datablk");
2383 let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
2384 {
2385 let file = H5File::create(&path).unwrap();
2386 file.create_appendable_vlen_dataset(
2387 "strs",
2388 2,
2389 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2390 )
2391 .unwrap();
2392 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2393 file.append_vlen_strings("strs", &refs).unwrap();
2394 file.close().unwrap();
2395 }
2396 {
2397 let file = H5File::open_rw(&path).unwrap();
2398 file.append_vlen_strings("strs", &["item09"]).unwrap();
2399 file.close().unwrap();
2400 }
2401 {
2402 let file = H5File::open(&path).unwrap();
2403 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2404 let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
2405 assert_eq!(got, want);
2406 }
2407 std::fs::remove_file(&path).ok();
2408 }
2409
2410 #[test]
2411 fn group_nx_class_attribute_roundtrip() {
2412 let path = temp_path("group_nx_class");
2415 {
2416 let file = H5File::create(&path).unwrap();
2417 let entry = file.create_group("entry").unwrap();
2418 entry.set_attr_string("NX_class", "NXentry").unwrap();
2419 let det = entry.create_group("detector").unwrap();
2420 det.set_attr_string("NX_class", "NXdetector").unwrap();
2421 det.set_attr_numeric("frame_count", &7i32).unwrap();
2422 det.new_dataset::<f32>()
2423 .shape([4])
2424 .create("data")
2425 .unwrap()
2426 .write_raw(&[1.0f32; 4])
2427 .unwrap();
2428 file.close().unwrap();
2429 }
2430 {
2431 let file = H5File::open(&path).unwrap();
2432 let entry = file.root_group().group("entry").unwrap();
2433 assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
2434 let det = entry.group("detector").unwrap();
2435 assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
2436 let names = det.attr_names().unwrap();
2437 assert!(names.contains(&"NX_class".to_string()));
2438 assert!(names.contains(&"frame_count".to_string()));
2439 }
2440 std::fs::remove_file(&path).ok();
2441 }
2442
2443 #[test]
2444 fn ea_super_block_roundtrip() {
2445 let path = temp_path("ea_super_rt");
2448 {
2449 let file = H5File::create(&path).unwrap();
2450 let ds = file
2451 .new_dataset::<i32>()
2452 .shape([0])
2453 .chunk(&[1])
2454 .max_shape(&[None])
2455 .create("v")
2456 .unwrap();
2457 ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
2458 file.close().unwrap();
2459 }
2460 {
2461 let file = H5File::open(&path).unwrap();
2462 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2463 assert_eq!(v.len(), 2000);
2464 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2465 }
2466 std::fs::remove_file(&path).ok();
2467 }
2468
2469 #[cfg(feature = "deflate")]
2470 #[test]
2471 fn ea_filtered_super_block_roundtrip() {
2472 let path = temp_path("ea_filt_super");
2474 {
2475 let file = H5File::create(&path).unwrap();
2476 let ds = file
2477 .new_dataset::<i32>()
2478 .shape([0])
2479 .chunk(&[1])
2480 .max_shape(&[None])
2481 .deflate(4)
2482 .create("v")
2483 .unwrap();
2484 ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
2485 file.close().unwrap();
2486 }
2487 {
2488 let file = H5File::open(&path).unwrap();
2489 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2490 assert_eq!(v, (0..600).collect::<Vec<i32>>());
2491 }
2492 std::fs::remove_file(&path).ok();
2493 }
2494
2495 #[test]
2496 fn ea_super_block_open_append() {
2497 let path = temp_path("ea_super_append");
2499 {
2500 let file = H5File::create(&path).unwrap();
2501 let ds = file
2502 .new_dataset::<i32>()
2503 .shape([0])
2504 .chunk(&[1])
2505 .max_shape(&[None])
2506 .create("v")
2507 .unwrap();
2508 ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
2509 file.close().unwrap();
2510 }
2511 {
2512 let mut w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2513 let idx = w.dataset_index("v").unwrap();
2514 for c in 300..900u64 {
2515 w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
2516 }
2517 w.extend_dataset(idx, &[900]).unwrap();
2518 w.close().unwrap();
2519 }
2520 {
2521 let file = H5File::open(&path).unwrap();
2522 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2523 assert_eq!(v.len(), 900);
2524 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2525 }
2526 std::fs::remove_file(&path).ok();
2527 }
2528
2529 #[test]
2530 fn btree_v2_multi_unlimited_roundtrip() {
2531 let path = temp_path("bt2_multi");
2534 {
2535 let file = H5File::create(&path).unwrap();
2536 let ds = file
2537 .new_dataset::<i32>()
2538 .shape([0, 0])
2539 .chunk(&[2, 2])
2540 .max_shape(&[None, None])
2541 .create("grid")
2542 .unwrap();
2543 assert!(ds.is_chunked());
2544 for cr in 0..2usize {
2546 for cc in 0..2usize {
2547 let mut bytes = Vec::new();
2548 for i in 0..2usize {
2549 for j in 0..2usize {
2550 let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
2551 bytes.extend_from_slice(&v.to_le_bytes());
2552 }
2553 }
2554 ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
2555 }
2556 }
2557 file.close().unwrap();
2558 }
2559 {
2560 let file = H5File::open(&path).unwrap();
2561 let ds = file.dataset("grid").unwrap();
2562 assert_eq!(ds.shape(), vec![4, 4]);
2563 assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
2564 }
2565 std::fs::remove_file(&path).ok();
2566 }
2567
2568 #[test]
2569 fn subframe_chunking_roundtrip() {
2570 let path = temp_path("subframe");
2574 {
2575 let file = H5File::create(&path).unwrap();
2576 let ds = file
2577 .new_dataset::<i32>()
2578 .shape([0, 8, 8])
2579 .chunk(&[1, 4, 4])
2580 .max_shape(&[None, Some(8), Some(8)])
2581 .create("v")
2582 .unwrap();
2583 for f in 0..3usize {
2584 for cr in 0..2usize {
2585 for cc in 0..2usize {
2586 let mut bytes = Vec::new();
2587 for i in 0..4usize {
2588 for j in 0..4usize {
2589 let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
2590 bytes.extend_from_slice(&v.to_le_bytes());
2591 }
2592 }
2593 ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
2594 }
2595 }
2596 }
2597 file.close().unwrap();
2598 }
2599 {
2600 let file = H5File::open(&path).unwrap();
2601 let ds = file.dataset("v").unwrap();
2602 assert_eq!(ds.shape(), vec![3, 8, 8]);
2603 assert_eq!(
2604 ds.read_raw::<i32>().unwrap(),
2605 (0..192).collect::<Vec<i32>>()
2606 );
2607 }
2608 std::fs::remove_file(&path).ok();
2609 }
2610
2611 #[test]
2612 fn fill_value_contiguous_roundtrip() {
2613 let path = temp_path("fill_value_contig");
2614 {
2615 let file = H5File::create(&path).unwrap();
2616 let ds = file
2617 .new_dataset::<f32>()
2618 .shape([4])
2619 .fill_value(2.5f32)
2620 .create("data")
2621 .unwrap();
2622 ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
2623 file.close().unwrap();
2624 }
2625 {
2627 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2628 let idx = writer.dataset_index("data").unwrap();
2629 assert_eq!(
2630 writer.datasets[idx].fill_value,
2631 Some(2.5f32.to_le_bytes().to_vec())
2632 );
2633 }
2634 {
2636 let file = H5File::open(&path).unwrap();
2637 let ds = file.dataset("data").unwrap();
2638 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
2639 }
2640 std::fs::remove_file(&path).ok();
2641 }
2642
2643 #[test]
2644 fn fill_value_chunked_roundtrip() {
2645 let path = temp_path("fill_value_chunked");
2646 {
2647 let file = H5File::create(&path).unwrap();
2648 let ds = file
2649 .new_dataset::<i32>()
2650 .shape([0])
2651 .chunk(&[4])
2652 .max_shape(&[None])
2653 .fill_value(-7i32)
2654 .create("vals")
2655 .unwrap();
2656 ds.append(&[1i32, 2, 3, 4]).unwrap();
2657 file.close().unwrap();
2658 }
2659 {
2660 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2661 let idx = writer.dataset_index("vals").unwrap();
2662 assert_eq!(
2663 writer.datasets[idx].fill_value,
2664 Some((-7i32).to_le_bytes().to_vec())
2665 );
2666 }
2667 std::fs::remove_file(&path).ok();
2668 }
2669
2670 #[test]
2671 fn fill_value_read_missing_chunks() {
2672 fn i32_bytes(vals: &[i32]) -> Vec<u8> {
2675 vals.iter().flat_map(|v| v.to_le_bytes()).collect()
2676 }
2677 let path = temp_path("fill_value_read_missing");
2678 {
2679 let file = H5File::create(&path).unwrap();
2680 let ds = file
2681 .new_dataset::<i32>()
2682 .shape([0])
2683 .chunk(&[2])
2684 .max_shape(&[None])
2685 .fill_value(-1i32)
2686 .create("vals")
2687 .unwrap();
2688 ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
2690 ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
2691 ds.extend(&[6]).unwrap();
2692 file.close().unwrap();
2693 }
2694 {
2695 let file = H5File::open(&path).unwrap();
2696 let ds = file.dataset("vals").unwrap();
2697 let all = ds.read_raw::<i32>().unwrap();
2698 assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
2699 }
2700 std::fs::remove_file(&path).ok();
2701 }
2702
2703 #[test]
2704 fn fill_value_partial_chunk_padded_with_fill() {
2705 let path = temp_path("fill_value_partial_pad");
2709 {
2710 let file = H5File::create(&path).unwrap();
2711 let ds = file
2712 .new_dataset::<i32>()
2713 .shape([0])
2714 .chunk(&[4])
2715 .max_shape(&[None])
2716 .fill_value(-9i32)
2717 .create("vals")
2718 .unwrap();
2719 ds.append(&[1i32, 2, 3]).unwrap();
2721 file.close().unwrap();
2722 }
2723 let bytes = std::fs::read(&path).unwrap();
2724 let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2726 let pos = bytes
2727 .windows(needle.len())
2728 .position(|w| w == needle)
2729 .expect("chunk data [1,2,3] not found in file");
2730 let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
2731 assert_eq!(
2732 pad,
2733 &(-9i32).to_le_bytes(),
2734 "partial chunk tail must be padded with fill value -9, got {:?}",
2735 pad
2736 );
2737 std::fs::remove_file(&path).ok();
2738 }
2739
2740 #[test]
2741 fn vlen_append_after_reopen_preserves_existing() {
2742 let path = temp_path("vlen_append_reopen");
2745 {
2746 let file = H5File::create(&path).unwrap();
2747 file.create_appendable_vlen_dataset("strs", 4, None)
2748 .unwrap();
2749 file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
2751 file.close().unwrap();
2752 }
2753 {
2754 let file = H5File::open_rw(&path).unwrap();
2756 file.append_vlen_strings("strs", &["d"]).unwrap();
2757 file.close().unwrap();
2758 }
2759 {
2760 let file = H5File::open(&path).unwrap();
2761 let ds = file.dataset("strs").unwrap();
2762 let got = ds.read_vlen_strings().unwrap();
2763 assert_eq!(
2764 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2765 vec!["a", "b", "c", "d"]
2766 );
2767 }
2768 std::fs::remove_file(&path).ok();
2769 }
2770
2771 #[test]
2772 fn fill_value_size_mismatch_errors() {
2773 let path = temp_path("fill_value_mismatch");
2774 let mut writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
2775 let dt = <f64 as crate::types::H5Type>::hdf5_type();
2776 let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
2777 assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
2779 writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
2781 writer.close().unwrap();
2782 std::fs::remove_file(&path).ok();
2783 }
2784
2785 #[test]
2786 fn datatype_exposes_class_sign_and_byteorder() {
2787 use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
2791
2792 let path = temp_path("datatype_accessor");
2793 {
2794 let file = H5File::create(&path).unwrap();
2795 file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
2796 file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
2797 file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
2798 file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
2799 file.close().unwrap();
2800 }
2801
2802 let file = H5File::open(&path).unwrap();
2803
2804 match file.dataset("u8d").unwrap().datatype().unwrap() {
2805 DatatypeMessage::FixedPoint {
2806 size,
2807 signed,
2808 byte_order,
2809 ..
2810 } => {
2811 assert_eq!(size, 1);
2812 assert!(!signed, "u8 must be unsigned");
2813 assert_eq!(byte_order, ByteOrder::LittleEndian);
2814 }
2815 other => panic!("expected FixedPoint for u8, got {other:?}"),
2816 }
2817
2818 match file.dataset("i8d").unwrap().datatype().unwrap() {
2819 DatatypeMessage::FixedPoint { size, signed, .. } => {
2820 assert_eq!(size, 1);
2821 assert!(signed, "i8 must be signed");
2822 }
2823 other => panic!("expected FixedPoint for i8, got {other:?}"),
2824 }
2825
2826 match file.dataset("i32d").unwrap().datatype().unwrap() {
2827 DatatypeMessage::FixedPoint { size, signed, .. } => {
2828 assert_eq!(size, 4);
2829 assert!(signed, "i32 must be signed");
2830 }
2831 other => panic!("expected FixedPoint for i32, got {other:?}"),
2832 }
2833
2834 match file.dataset("f32d").unwrap().datatype().unwrap() {
2835 DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
2836 other => panic!("expected FloatingPoint for f32, got {other:?}"),
2837 }
2838
2839 std::fs::remove_file(&path).ok();
2840 }
2841
2842 #[test]
2843 fn datatype_in_write_mode_errors() {
2844 let path = temp_path("datatype_write_mode");
2845 let file = H5File::create(&path).unwrap();
2846 let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
2847 assert!(ds.datatype().is_err());
2848 std::fs::remove_file(&path).ok();
2849 }
2850
2851 #[cfg(feature = "deflate")]
2857 #[test]
2858 fn write_chunk_raw_ea_roundtrip_mask0() {
2859 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2860 let path = temp_path("wcr_ea_mask0");
2861 let original: Vec<i32> = (0..12).collect();
2862 {
2863 let file = H5File::create(&path).unwrap();
2864 let ds = file
2865 .new_dataset::<i32>()
2866 .shape([0])
2867 .chunk(&[4])
2868 .max_shape(&[None])
2869 .deflate(4)
2870 .create("v")
2871 .unwrap();
2872 assert!(ds.is_chunked());
2873 let pipeline = FilterPipeline::deflate(4);
2874 for c in 0..3usize {
2875 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2876 .iter()
2877 .flat_map(|v| v.to_le_bytes())
2878 .collect();
2879 let compressed = apply_filters(&pipeline, &raw).unwrap();
2880 ds.write_chunk_raw(c, &compressed, 0).unwrap();
2881 }
2882 ds.set_extent(&[12]).unwrap();
2883 file.close().unwrap();
2884 }
2885 {
2886 let file = H5File::open(&path).unwrap();
2887 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2888 assert_eq!(v, original);
2889 }
2890 std::fs::remove_file(&path).ok();
2891 }
2892
2893 #[cfg(feature = "deflate")]
2896 #[test]
2897 fn write_chunk_raw_fixed_array_roundtrip_mask0() {
2898 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2899 let path = temp_path("wcr_fa_mask0");
2900 let original: Vec<i32> = (0..12).collect();
2901 {
2902 let file = H5File::create(&path).unwrap();
2903 let ds = file
2904 .new_dataset::<i32>()
2905 .shape([12])
2906 .chunk(&[4])
2907 .deflate(4)
2908 .create("v")
2909 .unwrap();
2910 assert!(ds.is_chunked());
2911 let pipeline = FilterPipeline::deflate(4);
2912 for c in 0..3usize {
2913 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2914 .iter()
2915 .flat_map(|v| v.to_le_bytes())
2916 .collect();
2917 let compressed = apply_filters(&pipeline, &raw).unwrap();
2918 ds.write_chunk_raw(c, &compressed, 0).unwrap();
2919 }
2920 file.close().unwrap();
2921 }
2922 {
2923 let file = H5File::open(&path).unwrap();
2924 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2925 assert_eq!(v, original);
2926 }
2927 std::fs::remove_file(&path).ok();
2928 }
2929
2930 #[cfg(feature = "deflate")]
2936 #[test]
2937 fn write_chunk_raw_records_filter_mask() {
2938 let path = temp_path("wcr_records_mask");
2939 let raw: Vec<u8> = [10i32, 20, 30, 40]
2940 .iter()
2941 .flat_map(|v| v.to_le_bytes())
2942 .collect();
2943 assert_eq!(raw.len(), 16);
2944 {
2945 let file = H5File::create(&path).unwrap();
2946 let ds = file
2947 .new_dataset::<i32>()
2948 .shape([0])
2949 .chunk(&[4])
2950 .max_shape(&[None])
2951 .deflate(4)
2952 .create("v")
2953 .unwrap();
2954 ds.write_chunk_raw(0, &raw, 1).unwrap();
2957 ds.set_extent(&[4]).unwrap();
2958 file.close().unwrap();
2959 }
2960 {
2963 let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2964 let idx = w.dataset_index("v").unwrap();
2965 let entry = &w.datasets[idx]
2966 .chunked
2967 .as_ref()
2968 .unwrap()
2969 .filt_iblk
2970 .as_ref()
2971 .unwrap()
2972 .elements[0];
2973 assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
2974 assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
2975 }
2976 std::fs::remove_file(&path).ok();
2977 }
2978
2979 #[cfg(feature = "deflate")]
2984 #[test]
2985 fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
2986 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2987 let path = temp_path("wcr_ea_per_chunk_mask");
2988 let original: Vec<i32> = (0..8).collect();
2989 let pipeline = FilterPipeline::deflate(4);
2990 {
2991 let file = H5File::create(&path).unwrap();
2992 let ds = file
2993 .new_dataset::<i32>()
2994 .shape([0])
2995 .chunk(&[4])
2996 .max_shape(&[None])
2997 .deflate(4)
2998 .create("v")
2999 .unwrap();
3000 let raw0: Vec<u8> = original[0..4]
3001 .iter()
3002 .flat_map(|v| v.to_le_bytes())
3003 .collect();
3004 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3006 .unwrap();
3007 let raw1: Vec<u8> = original[4..8]
3008 .iter()
3009 .flat_map(|v| v.to_le_bytes())
3010 .collect();
3011 ds.write_chunk_raw(1, &raw1, 1).unwrap();
3013 ds.set_extent(&[8]).unwrap();
3014 file.close().unwrap();
3015 }
3016 {
3017 let file = H5File::open(&path).unwrap();
3018 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3019 assert_eq!(v, original);
3020 }
3021 std::fs::remove_file(&path).ok();
3022 }
3023
3024 #[cfg(feature = "deflate")]
3027 #[test]
3028 fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
3029 use crate::format::messages::filter::{apply_filters, FilterPipeline};
3030 let path = temp_path("wcr_fa_per_chunk_mask");
3031 let original: Vec<i32> = (0..8).collect();
3032 let pipeline = FilterPipeline::deflate(4);
3033 {
3034 let file = H5File::create(&path).unwrap();
3035 let ds = file
3036 .new_dataset::<i32>()
3037 .shape([8])
3038 .chunk(&[4])
3039 .deflate(4)
3040 .create("v")
3041 .unwrap();
3042 let raw0: Vec<u8> = original[0..4]
3043 .iter()
3044 .flat_map(|v| v.to_le_bytes())
3045 .collect();
3046 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3047 .unwrap();
3048 let raw1: Vec<u8> = original[4..8]
3049 .iter()
3050 .flat_map(|v| v.to_le_bytes())
3051 .collect();
3052 ds.write_chunk_raw(1, &raw1, 1).unwrap();
3053 file.close().unwrap();
3054 }
3055 {
3056 let file = H5File::open(&path).unwrap();
3057 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3058 assert_eq!(v, original);
3059 }
3060 std::fs::remove_file(&path).ok();
3061 }
3062
3063 #[test]
3066 fn write_chunk_raw_rejects_unfiltered() {
3067 let path = temp_path("wcr_unfiltered");
3068 let file = H5File::create(&path).unwrap();
3069 let ds = file
3070 .new_dataset::<i32>()
3071 .shape([0])
3072 .chunk(&[4])
3073 .max_shape(&[None])
3074 .create("v")
3075 .unwrap();
3076 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3077 assert!(
3078 err.to_string().contains("filtered dataset"),
3079 "expected a filtered-dataset error, got: {err}"
3080 );
3081 std::fs::remove_file(&path).ok();
3082 }
3083
3084 #[test]
3087 fn write_chunk_raw_rejects_btree_v2() {
3088 let path = temp_path("wcr_btree2");
3089 let file = H5File::create(&path).unwrap();
3090 let ds = file
3091 .new_dataset::<i32>()
3092 .shape([0, 0])
3093 .chunk(&[2, 2])
3094 .max_shape(&[None, None])
3095 .create("grid")
3096 .unwrap();
3097 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3098 assert!(
3099 err.to_string().contains("v2-B-tree"),
3100 "expected a v2-B-tree rejection, got: {err}"
3101 );
3102 std::fs::remove_file(&path).ok();
3103 }
3104
3105 #[cfg(feature = "deflate")]
3110 #[test]
3111 fn write_chunk_raw_rejects_oversized_chunk() {
3112 let path = temp_path("wcr_oversized");
3113 let file = H5File::create(&path).unwrap();
3114 let ds = file
3115 .new_dataset::<i32>()
3116 .shape([0])
3117 .chunk(&[1])
3118 .max_shape(&[None])
3119 .deflate(4)
3120 .create("v")
3121 .unwrap();
3122 let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
3123 assert!(
3124 err.to_string().contains("does not fit"),
3125 "expected a chunk-size-field overflow error, got: {err}"
3126 );
3127 std::fs::remove_file(&path).ok();
3128 }
3129}