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 _marker: std::marker::PhantomData<T>,
40}
41
42impl<T: H5Type> DatasetBuilder<T> {
43 pub(crate) fn new(file_inner: SharedInner) -> Self {
44 Self {
45 file_inner,
46 shape: None,
47 chunk_dims: None,
48 max_shape: None,
49 deflate_level: None,
50 shuffle_deflate_level: None,
51 custom_pipeline: None,
52 group_path: None,
53 fill_value: None,
54 _marker: std::marker::PhantomData,
55 }
56 }
57
58 pub(crate) fn new_in_group(file_inner: SharedInner, group_path: String) -> Self {
59 Self {
60 file_inner,
61 shape: None,
62 chunk_dims: None,
63 max_shape: None,
64 deflate_level: None,
65 shuffle_deflate_level: None,
66 custom_pipeline: None,
67 group_path: Some(group_path),
68 fill_value: None,
69 _marker: std::marker::PhantomData,
70 }
71 }
72
73 #[must_use]
78 pub fn shape<S: AsRef<[usize]>>(mut self, dims: S) -> Self {
79 self.shape = Some(dims.as_ref().to_vec());
80 self
81 }
82
83 #[must_use]
85 pub fn scalar(mut self) -> Self {
86 self.shape = Some(vec![]);
87 self
88 }
89
90 #[must_use]
96 pub fn chunk(mut self, chunk_dims: &[usize]) -> Self {
97 self.chunk_dims = Some(chunk_dims.to_vec());
98 self
99 }
100
101 #[must_use]
105 pub fn resizable(mut self) -> Self {
106 self.max_shape = Some(vec![None; self.shape.as_ref().map_or(0, |s| s.len())]);
107 self
108 }
109
110 #[must_use]
112 pub fn max_shape(mut self, max: &[Option<usize>]) -> Self {
113 self.max_shape = Some(max.to_vec());
114 self
115 }
116
117 #[must_use]
122 pub fn deflate(mut self, level: u32) -> Self {
123 self.deflate_level = Some(level);
124 self
125 }
126
127 #[must_use]
133 pub fn shuffle_deflate(mut self, level: u32) -> Self {
134 self.shuffle_deflate_level = Some(level);
135 self
136 }
137
138 #[must_use]
142 pub fn zstd(mut self, level: u32) -> Self {
143 self.custom_pipeline = Some(crate::format::messages::filter::FilterPipeline::zstd(level));
144 self
145 }
146
147 #[must_use]
152 pub fn filter_pipeline(
153 mut self,
154 pipeline: crate::format::messages::filter::FilterPipeline,
155 ) -> Self {
156 self.custom_pipeline = Some(pipeline);
157 self
158 }
159
160 #[must_use]
177 pub fn fill_value(mut self, value: T) -> Self {
178 let es = T::element_size();
179 let raw = unsafe { std::slice::from_raw_parts(&value as *const T as *const u8, es) };
183 self.fill_value = Some(raw.to_vec());
184 self
185 }
186
187 pub fn create(self, name: &str) -> Result<H5Dataset> {
192 let shape = self.shape.ok_or_else(|| {
193 Hdf5Error::InvalidState("shape must be set before calling create()".into())
194 })?;
195
196 let full_name = if let Some(ref gp) = self.group_path {
198 if gp == "/" {
199 name.to_string()
200 } else {
201 let trimmed = gp.trim_start_matches('/');
202 format!("{}/{}", trimmed, name)
203 }
204 } else {
205 name.to_string()
206 };
207 let group_path = self.group_path.clone();
208 let fill_value = self.fill_value.clone();
209
210 let dims_u64: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
211 let datatype = T::hdf5_type();
212 let element_size = T::element_size();
213
214 if let Some(ref chunk_dims) = self.chunk_dims {
215 let chunk_u64: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
217 let max_u64: Vec<u64> = if let Some(ref max) = self.max_shape {
218 max.iter()
219 .map(|m| m.map_or(u64::MAX, |v| v as u64))
220 .collect()
221 } else {
222 dims_u64.clone()
224 };
225
226 let n_unlimited = max_u64.iter().filter(|&&m| m == u64::MAX).count();
230 let is_btree2 = n_unlimited >= 2;
231 let is_fixed_array = n_unlimited == 0;
232 let wants_filter = self.custom_pipeline.is_some()
233 || self.shuffle_deflate_level.is_some()
234 || self.deflate_level.is_some();
235
236 let index = {
237 let mut inner = borrow_inner_mut(&self.file_inner);
238 match &mut *inner {
239 H5FileInner::Writer(writer) => {
240 let idx = if is_btree2 {
241 if wants_filter {
242 return Err(Hdf5Error::InvalidState(
243 "compression of v2 B-tree (multi-unlimited-dimension) \
244 datasets is not yet supported"
245 .into(),
246 ));
247 }
248 writer.create_btree_v2_dataset(
249 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
250 )?
251 } else if is_fixed_array {
252 if wants_filter {
258 let pipeline = if let Some(p) = self.custom_pipeline {
259 p
260 } else if let Some(level) = self.shuffle_deflate_level {
261 crate::format::messages::filter::FilterPipeline::shuffle_deflate(
262 T::element_size() as u32,
263 level,
264 )
265 } else {
266 crate::format::messages::filter::FilterPipeline::deflate(
268 self.deflate_level.unwrap(),
269 )
270 };
271 writer.create_fixed_array_dataset_with_pipeline(
272 &full_name, datatype, &dims_u64, &chunk_u64, pipeline,
273 )?
274 } else {
275 writer.create_fixed_array_dataset(
276 &full_name, datatype, &dims_u64, &chunk_u64,
277 )?
278 }
279 } else if let Some(pipeline) = self.custom_pipeline {
280 writer.create_chunked_dataset_with_pipeline(
281 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
282 )?
283 } else if let Some(level) = self.shuffle_deflate_level {
284 let pipeline =
285 crate::format::messages::filter::FilterPipeline::shuffle_deflate(
286 T::element_size() as u32,
287 level,
288 );
289 writer.create_chunked_dataset_with_pipeline(
290 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
291 )?
292 } else if let Some(level) = self.deflate_level {
293 writer.create_chunked_dataset_compressed(
294 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, level,
295 )?
296 } else {
297 writer.create_chunked_dataset(
298 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
299 )?
300 };
301 if let Some(ref gp) = group_path {
302 if gp != "/" {
303 writer.assign_dataset_to_group(gp, idx)?;
304 }
305 }
306 if let Some(ref fv) = fill_value {
307 writer.set_dataset_fill_value(idx, fv.clone())?;
308 }
309 idx
310 }
311 H5FileInner::Reader(_) => {
312 return Err(Hdf5Error::InvalidState(
313 "cannot create a dataset in read mode".into(),
314 ));
315 }
316 H5FileInner::Closed => {
317 return Err(Hdf5Error::InvalidState("file is closed".into()));
318 }
319 }
320 };
321
322 Ok(H5Dataset {
323 file_inner: clone_inner(&self.file_inner),
324 info: DatasetInfo::Writer {
325 index,
326 shape,
327 element_size,
328 chunked: true,
329 btree2: is_btree2,
330 fixed_array: is_fixed_array,
331 },
332 })
333 } else {
334 let index = {
336 let mut inner = borrow_inner_mut(&self.file_inner);
337 match &mut *inner {
338 H5FileInner::Writer(writer) => {
339 let idx = writer.create_dataset(&full_name, datatype, &dims_u64)?;
340 if let Some(ref gp) = group_path {
341 if gp != "/" {
342 writer.assign_dataset_to_group(gp, idx)?;
343 }
344 }
345 if let Some(ref fv) = fill_value {
346 writer.set_dataset_fill_value(idx, fv.clone())?;
347 }
348 idx
349 }
350 H5FileInner::Reader(_) => {
351 return Err(Hdf5Error::InvalidState(
352 "cannot create a dataset in read mode".into(),
353 ));
354 }
355 H5FileInner::Closed => {
356 return Err(Hdf5Error::InvalidState("file is closed".into()));
357 }
358 }
359 };
360
361 Ok(H5Dataset {
362 file_inner: clone_inner(&self.file_inner),
363 info: DatasetInfo::Writer {
364 index,
365 shape,
366 element_size,
367 chunked: false,
368 btree2: false,
369 fixed_array: false,
370 },
371 })
372 }
373 }
374}
375
376enum DatasetInfo {
382 Writer {
384 index: usize,
386 shape: Vec<usize>,
388 element_size: usize,
390 chunked: bool,
392 btree2: bool,
394 fixed_array: bool,
396 },
397 Reader {
399 name: String,
401 shape: Vec<usize>,
403 element_size: usize,
405 },
406}
407
408pub struct H5Dataset {
418 file_inner: SharedInner,
419 info: DatasetInfo,
420}
421
422impl H5Dataset {
423 pub(crate) fn new_reader(
425 file_inner: SharedInner,
426 name: String,
427 shape: Vec<usize>,
428 element_size: usize,
429 ) -> Self {
430 Self {
431 file_inner,
432 info: DatasetInfo::Reader {
433 name,
434 shape,
435 element_size,
436 },
437 }
438 }
439
440 pub fn shape(&self) -> Vec<usize> {
442 match &self.info {
443 DatasetInfo::Writer { shape, .. } => shape.clone(),
444 DatasetInfo::Reader { shape, .. } => shape.clone(),
445 }
446 }
447
448 pub fn ndims(&self) -> usize {
450 match &self.info {
451 DatasetInfo::Writer { shape, .. } => shape.len(),
452 DatasetInfo::Reader { shape, .. } => shape.len(),
453 }
454 }
455
456 pub fn total_elements(&self) -> usize {
458 match &self.info {
459 DatasetInfo::Writer { shape, .. } => shape.iter().product(),
460 DatasetInfo::Reader { shape, .. } => shape.iter().product(),
461 }
462 }
463
464 pub fn element_size(&self) -> usize {
466 match &self.info {
467 DatasetInfo::Writer { element_size, .. } => *element_size,
468 DatasetInfo::Reader { element_size, .. } => *element_size,
469 }
470 }
471
472 pub fn datatype(&self) -> Result<DatatypeMessage> {
503 match &self.info {
504 DatasetInfo::Reader { name, .. } => {
505 let inner = borrow_inner(&self.file_inner);
506 match &*inner {
507 H5FileInner::Reader(reader) => reader
508 .dataset_info(name)
509 .map(|info| info.datatype.clone())
510 .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
511 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
512 }
513 }
514 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
515 "datatype() is only available in read mode".into(),
516 )),
517 }
518 }
519
520 pub fn chunk_dims(&self) -> Option<Vec<usize>> {
522 match &self.info {
523 DatasetInfo::Reader { name, .. } => {
524 let inner = borrow_inner(&self.file_inner);
525 if let H5FileInner::Reader(reader) = &*inner {
526 if let Some(info) = reader.dataset_info(name) {
527 use crate::format::messages::data_layout::DataLayoutMessage;
528 let chunk_dims = match &info.layout {
529 DataLayoutMessage::ChunkedV4 { chunk_dims, .. }
530 | DataLayoutMessage::ChunkedV3 { chunk_dims, .. } => Some(chunk_dims),
531 _ => None,
532 };
533 if let Some(chunk_dims) = chunk_dims {
534 return Some(
536 chunk_dims[..chunk_dims.len() - 1]
537 .iter()
538 .map(|&d| d as usize)
539 .collect(),
540 );
541 }
542 }
543 }
544 None
545 }
546 DatasetInfo::Writer { .. } => None,
547 }
548 }
549
550 pub fn is_chunked(&self) -> bool {
552 match &self.info {
553 DatasetInfo::Writer { chunked, .. } => *chunked,
554 DatasetInfo::Reader { name, .. } => {
555 let inner = borrow_inner(&self.file_inner);
556 match &*inner {
557 H5FileInner::Reader(reader) => {
558 if let Some(info) = reader.dataset_info(name) {
559 use crate::format::messages::data_layout::DataLayoutMessage;
560 matches!(
561 info.layout,
562 DataLayoutMessage::ChunkedV4 { .. }
563 | DataLayoutMessage::ChunkedV3 { .. }
564 )
565 } else {
566 false
567 }
568 }
569 _ => false,
570 }
571 }
572 }
573 }
574
575 pub fn attr_names(&self) -> Result<Vec<String>> {
577 match &self.info {
578 DatasetInfo::Reader { name, .. } => {
579 let inner = borrow_inner(&self.file_inner);
580 match &*inner {
581 H5FileInner::Reader(reader) => Ok(reader.dataset_attr_names(name)?),
582 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
583 }
584 }
585 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
586 "attr_names not available in write mode".into(),
587 )),
588 }
589 }
590
591 pub fn attr(&self, attr_name: &str) -> Result<crate::attribute::H5Attribute> {
593 match &self.info {
594 DatasetInfo::Reader { name, .. } => {
595 let inner = borrow_inner(&self.file_inner);
596 match &*inner {
597 H5FileInner::Reader(reader) => {
598 let attr_msg = reader.dataset_attr(name, attr_name)?.clone();
599 Ok(crate::attribute::H5Attribute::new_reader(
600 clone_inner(&self.file_inner),
601 attr_msg,
602 ))
603 }
604 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
605 }
606 }
607 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
608 "attr() not available in write mode".into(),
609 )),
610 }
611 }
612
613 pub fn new_attr<T: 'static>(&self) -> AttrBuilder<'_, T> {
629 let ds_index = match &self.info {
630 DatasetInfo::Writer { index, .. } => *index,
631 DatasetInfo::Reader { .. } => {
632 usize::MAX
635 }
636 };
637 AttrBuilder::new(&self.file_inner, ds_index)
638 }
639
640 pub fn write_raw<T: H5Type>(&self, data: &[T]) -> Result<()> {
652 match &self.info {
653 DatasetInfo::Writer {
654 index,
655 shape,
656 element_size,
657 chunked,
658 btree2: _,
659 fixed_array: _,
660 } => {
661 if *chunked {
662 return Err(Hdf5Error::InvalidState(
663 "use write_chunk for chunked datasets".into(),
664 ));
665 }
666
667 let total_elements: usize = shape.iter().product();
668 if data.len() != total_elements {
669 return Err(Hdf5Error::InvalidState(format!(
670 "data length {} does not match dataset size {}",
671 data.len(),
672 total_elements,
673 )));
674 }
675
676 if T::element_size() != *element_size {
678 return Err(Hdf5Error::TypeMismatch(format!(
679 "write type has element size {} but dataset expects {}",
680 T::element_size(),
681 element_size,
682 )));
683 }
684
685 let byte_len = data.len() * T::element_size();
689 let raw =
690 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
691
692 let mut inner = borrow_inner_mut(&self.file_inner);
693 match &mut *inner {
694 H5FileInner::Writer(writer) => {
695 writer.write_dataset_raw(*index, raw)?;
696 Ok(())
697 }
698 _ => Err(Hdf5Error::InvalidState(
699 "file is no longer in write mode".into(),
700 )),
701 }
702 }
703 DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
704 "cannot write to a dataset opened in read mode".into(),
705 )),
706 }
707 }
708
709 pub fn write_chunk(&self, chunk_idx: usize, data: &[u8]) -> Result<()> {
717 match &self.info {
718 DatasetInfo::Writer {
719 index,
720 chunked,
721 btree2,
722 fixed_array,
723 ..
724 } => {
725 if !*chunked {
726 return Err(Hdf5Error::InvalidState(
727 "write_chunk is only for chunked datasets".into(),
728 ));
729 }
730 if *btree2 {
731 return Err(Hdf5Error::InvalidState(
732 "this dataset uses a v2 B-tree chunk index; use write_chunk_at \
733 with the chunk's grid coordinates"
734 .into(),
735 ));
736 }
737
738 let mut inner = borrow_inner_mut(&self.file_inner);
739 match &mut *inner {
740 H5FileInner::Writer(writer) => {
741 if *fixed_array {
742 let chunk_dims = writer
745 .dataset_chunk_dims(*index)
746 .ok_or_else(|| {
747 Hdf5Error::InvalidState("dataset has no chunk info".into())
748 })?
749 .to_vec();
750 let dims = writer.dataset_dims(*index).to_vec();
751 let mut grid = vec![0u64; dims.len()];
752 for d in 0..dims.len() {
753 grid[d] = if chunk_dims[d] > 0 {
754 dims[d].div_ceil(chunk_dims[d])
755 } else {
756 1
757 };
758 }
759 if grid.contains(&0) {
762 return Err(Hdf5Error::InvalidState(
763 "dataset has a zero-extent dimension and no chunks".into(),
764 ));
765 }
766 let mut rem = chunk_idx as u64;
767 let mut coords = vec![0u64; dims.len()];
768 for d in (0..dims.len()).rev() {
769 coords[d] = rem % grid[d];
770 rem /= grid[d];
771 }
772 if rem != 0 {
774 return Err(Hdf5Error::InvalidState(format!(
775 "chunk index {chunk_idx} is out of range for this dataset"
776 )));
777 }
778 writer.write_chunk_fixed_array(*index, &coords, data)?;
779 } else {
780 writer.write_chunk(*index, chunk_idx as u64, data)?;
781 }
782 Ok(())
783 }
784 _ => Err(Hdf5Error::InvalidState(
785 "file is no longer in write mode".into(),
786 )),
787 }
788 }
789 DatasetInfo::Reader { .. } => {
790 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
791 }
792 }
793 }
794
795 pub fn write_chunk_raw(&self, chunk_idx: usize, data: &[u8], filter_mask: u32) -> Result<()> {
821 match &self.info {
822 DatasetInfo::Writer {
823 index,
824 chunked,
825 btree2,
826 fixed_array,
827 ..
828 } => {
829 if !*chunked {
830 return Err(Hdf5Error::InvalidState(
831 "write_chunk_raw is only for chunked datasets".into(),
832 ));
833 }
834 if *btree2 {
835 return Err(Hdf5Error::InvalidState(
836 "direct chunk writes are not supported for v2-B-tree-indexed \
837 datasets (two or more unlimited dimensions)"
838 .into(),
839 ));
840 }
841
842 let mut inner = borrow_inner_mut(&self.file_inner);
843 match &mut *inner {
844 H5FileInner::Writer(writer) => {
845 if *fixed_array {
846 let chunk_dims = writer
849 .dataset_chunk_dims(*index)
850 .ok_or_else(|| {
851 Hdf5Error::InvalidState("dataset has no chunk info".into())
852 })?
853 .to_vec();
854 let dims = writer.dataset_dims(*index).to_vec();
855 let mut grid = vec![0u64; dims.len()];
856 for d in 0..dims.len() {
857 grid[d] = if chunk_dims[d] > 0 {
858 dims[d].div_ceil(chunk_dims[d])
859 } else {
860 1
861 };
862 }
863 if grid.contains(&0) {
866 return Err(Hdf5Error::InvalidState(
867 "dataset has a zero-extent dimension and no chunks".into(),
868 ));
869 }
870 let mut rem = chunk_idx as u64;
871 let mut coords = vec![0u64; dims.len()];
872 for d in (0..dims.len()).rev() {
873 coords[d] = rem % grid[d];
874 rem /= grid[d];
875 }
876 if rem != 0 {
878 return Err(Hdf5Error::InvalidState(format!(
879 "chunk index {chunk_idx} is out of range for this dataset"
880 )));
881 }
882 writer.write_compressed_chunk_fixed_array(
883 *index,
884 &coords,
885 data,
886 filter_mask,
887 )?;
888 } else {
889 writer.write_compressed_chunk(
890 *index,
891 chunk_idx as u64,
892 data,
893 filter_mask,
894 )?;
895 }
896 Ok(())
897 }
898 _ => Err(Hdf5Error::InvalidState(
899 "file is no longer in write mode".into(),
900 )),
901 }
902 }
903 DatasetInfo::Reader { .. } => {
904 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
905 }
906 }
907 }
908
909 pub fn write_chunk_at(&self, chunk_coords: &[usize], data: &[u8]) -> Result<()> {
930 match &self.info {
931 DatasetInfo::Writer {
932 index,
933 chunked,
934 btree2,
935 fixed_array,
936 ..
937 } => {
938 if !*chunked {
939 return Err(Hdf5Error::InvalidState(
940 "write_chunk_at is only for chunked datasets".into(),
941 ));
942 }
943 let coords: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
944 let btree2 = *btree2;
945 let fixed_array = *fixed_array;
946 let mut inner = borrow_inner_mut(&self.file_inner);
947 let writer = match &mut *inner {
948 H5FileInner::Writer(w) => w,
949 _ => {
950 return Err(Hdf5Error::InvalidState(
951 "file is no longer in write mode".into(),
952 ))
953 }
954 };
955 let chunk_dims = writer
956 .dataset_chunk_dims(*index)
957 .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
958 .to_vec();
959 let dims = writer.dataset_dims(*index).to_vec();
960 if coords.len() != dims.len() {
961 return Err(Hdf5Error::InvalidState(format!(
962 "chunk_coords has {} entries but the dataset has {} dimensions",
963 coords.len(),
964 dims.len()
965 )));
966 }
967 if chunk_dims.len() != dims.len() {
968 return Err(Hdf5Error::InvalidState(format!(
969 "dataset chunk shape has {} dimensions but the dataspace has {}",
970 chunk_dims.len(),
971 dims.len()
972 )));
973 }
974
975 let mut new_dims = dims.clone();
979 for d in 0..dims.len() {
980 let needed = coords[d]
981 .checked_add(1)
982 .and_then(|c| c.checked_mul(chunk_dims[d]))
983 .ok_or_else(|| {
984 Hdf5Error::InvalidState(format!(
985 "chunk coordinate {} in dimension {} is too large",
986 coords[d], d
987 ))
988 })?;
989 if needed > new_dims[d] {
990 new_dims[d] = needed;
991 }
992 }
993
994 if fixed_array {
995 writer.write_chunk_fixed_array(*index, &coords, data)?;
997 return Ok(());
998 }
999
1000 if btree2 {
1001 writer.write_chunk_btree_v2(*index, &coords, data)?;
1002 } else {
1003 let mut linear = 0u64;
1006 for d in 0..dims.len() {
1007 let grid = if chunk_dims[d] > 0 {
1008 dims[d].div_ceil(chunk_dims[d])
1009 } else {
1010 1
1011 };
1012 linear = linear
1013 .checked_mul(grid)
1014 .and_then(|l| l.checked_add(coords[d]))
1015 .ok_or_else(|| {
1016 Hdf5Error::InvalidState(
1017 "chunk coordinates overflow the array index".into(),
1018 )
1019 })?;
1020 }
1021 writer.write_chunk(*index, linear, data)?;
1022 }
1023
1024 if new_dims != dims {
1025 writer.extend_dataset(*index, &new_dims)?;
1026 }
1027 Ok(())
1028 }
1029 DatasetInfo::Reader { .. } => {
1030 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1031 }
1032 }
1033 }
1034
1035 pub fn write_chunks_batch(&self, chunks: &[(usize, &[u8])]) -> Result<()> {
1041 match &self.info {
1042 DatasetInfo::Writer { index, chunked, .. } => {
1043 if !*chunked {
1044 return Err(Hdf5Error::InvalidState(
1045 "write_chunks_batch is only for chunked datasets".into(),
1046 ));
1047 }
1048 let pairs: Vec<(u64, &[u8])> = chunks
1049 .iter()
1050 .map(|(idx, data)| (*idx as u64, *data))
1051 .collect();
1052 let mut inner = borrow_inner_mut(&self.file_inner);
1053 match &mut *inner {
1054 H5FileInner::Writer(writer) => {
1055 writer.write_chunks_batch(*index, &pairs)?;
1056 Ok(())
1057 }
1058 _ => Err(Hdf5Error::InvalidState(
1059 "file is no longer in write mode".into(),
1060 )),
1061 }
1062 }
1063 DatasetInfo::Reader { .. } => {
1064 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1065 }
1066 }
1067 }
1068
1069 pub fn append<T: H5Type>(&self, data: &[T]) -> Result<()> {
1092 match &self.info {
1093 DatasetInfo::Writer {
1094 index,
1095 element_size,
1096 chunked,
1097 ..
1098 } => {
1099 if !*chunked {
1100 return Err(Hdf5Error::InvalidState(
1101 "append is only for chunked datasets".into(),
1102 ));
1103 }
1104 if T::element_size() != *element_size {
1105 return Err(Hdf5Error::TypeMismatch(format!(
1106 "append type has element size {} but dataset expects {}",
1107 T::element_size(),
1108 element_size,
1109 )));
1110 }
1111
1112 let ds_index = *index;
1113 let es = *element_size;
1114
1115 let mut inner = borrow_inner_mut(&self.file_inner);
1116 let writer = match &mut *inner {
1117 H5FileInner::Writer(w) => w,
1118 _ => {
1119 return Err(Hdf5Error::InvalidState(
1120 "file is no longer in write mode".into(),
1121 ))
1122 }
1123 };
1124
1125 let chunk_dims = writer
1126 .dataset_chunk_dims(ds_index)
1127 .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1128 .to_vec();
1129 let dims = writer.dataset_dims(ds_index).to_vec();
1130
1131 let frame_elems: usize = if dims.len() > 1 {
1133 dims[1..].iter().map(|&d| d as usize).product()
1134 } else {
1135 1
1136 };
1137
1138 if frame_elems == 0 {
1139 return Err(Hdf5Error::InvalidState(
1140 "cannot append to dataset with zero-size trailing dimensions".into(),
1141 ));
1142 }
1143
1144 if !data.len().is_multiple_of(frame_elems) {
1145 return Err(Hdf5Error::InvalidState(format!(
1146 "data length {} is not a multiple of frame size {}",
1147 data.len(),
1148 frame_elems,
1149 )));
1150 }
1151
1152 let n_new_frames = data.len() / frame_elems;
1153 let current_dim0 = dims[0] as usize;
1154
1155 let chunk_dim0 = chunk_dims[0] as usize;
1157 let frame_bytes = frame_elems * es;
1158
1159 let raw = unsafe {
1160 std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * es)
1161 };
1162
1163 let ds = &mut writer.datasets[ds_index];
1165 let buffered_frames = ds.append_buffered_frames as usize;
1166 let mut combined = std::mem::take(&mut ds.append_buffer);
1167 combined.extend_from_slice(raw);
1168 ds.append_buffered_frames = 0;
1169
1170 let total_frames = buffered_frames + n_new_frames;
1171 let total_bytes = combined.len();
1172
1173 let base_dim0 = current_dim0 - buffered_frames;
1175 let mut byte_pos = 0usize;
1176 let mut frame_pos = 0usize;
1177
1178 while frame_pos < total_frames {
1179 let abs_frame = base_dim0 + frame_pos;
1180 let chunk_idx = abs_frame / chunk_dim0;
1181 let remaining_frames = total_frames - frame_pos;
1182 let frames_to_fill = chunk_dim0 - (abs_frame % chunk_dim0);
1183
1184 if remaining_frames >= frames_to_fill {
1185 let end = byte_pos + frames_to_fill * frame_bytes;
1187 if frames_to_fill == chunk_dim0 {
1188 writer.write_chunk(
1189 ds_index,
1190 chunk_idx as u64,
1191 &combined[byte_pos..end],
1192 )?;
1193 } else {
1194 let offset_in_chunk = (abs_frame % chunk_dim0) * frame_bytes;
1200 let mut chunk_buf =
1201 match writer.read_chunk_if_present(ds_index, chunk_idx as u64)? {
1202 Some(existing) => existing,
1203 None => {
1204 return Err(Hdf5Error::InvalidState(format!(
1205 "cannot append into partially-written chunk {}: \
1206 its existing content was not found in the chunk \
1207 index (the file may be inconsistent)",
1208 chunk_idx
1209 )));
1210 }
1211 };
1212 chunk_buf
1213 [offset_in_chunk..offset_in_chunk + frames_to_fill * frame_bytes]
1214 .copy_from_slice(&combined[byte_pos..end]);
1215 writer.write_chunk(ds_index, chunk_idx as u64, &chunk_buf)?;
1216 }
1217 byte_pos = end;
1218 frame_pos += frames_to_fill;
1219 } else {
1220 let ds = &mut writer.datasets[ds_index];
1222 ds.append_buffer = combined[byte_pos..total_bytes].to_vec();
1223 ds.append_buffered_frames = remaining_frames as u64;
1224 frame_pos = total_frames;
1225 }
1226 }
1227
1228 let logical_dim0 = base_dim0 + total_frames;
1230 let mut new_dims: Vec<u64> = dims;
1231 new_dims[0] = logical_dim0 as u64;
1232 writer.extend_dataset(ds_index, &new_dims)?;
1233
1234 Ok(())
1235 }
1236 DatasetInfo::Reader { .. } => {
1237 Err(Hdf5Error::InvalidState("cannot append in read mode".into()))
1238 }
1239 }
1240 }
1241
1242 pub fn extend(&self, new_dims: &[usize]) -> Result<()> {
1244 match &self.info {
1245 DatasetInfo::Writer { index, chunked, .. } => {
1246 if !*chunked {
1247 return Err(Hdf5Error::InvalidState(
1248 "extend is only for chunked datasets".into(),
1249 ));
1250 }
1251
1252 let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1253 let mut inner = borrow_inner_mut(&self.file_inner);
1254 match &mut *inner {
1255 H5FileInner::Writer(writer) => {
1256 writer.extend_dataset(*index, &dims_u64)?;
1257 Ok(())
1258 }
1259 _ => Err(Hdf5Error::InvalidState(
1260 "file is no longer in write mode".into(),
1261 )),
1262 }
1263 }
1264 DatasetInfo::Reader { .. } => {
1265 Err(Hdf5Error::InvalidState("cannot extend in read mode".into()))
1266 }
1267 }
1268 }
1269
1270 pub fn set_extent(&self, new_dims: &[usize]) -> Result<()> {
1281 match &self.info {
1282 DatasetInfo::Writer { index, .. } => {
1283 let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1284 let mut inner = borrow_inner_mut(&self.file_inner);
1285 match &mut *inner {
1286 H5FileInner::Writer(writer) => {
1287 writer.set_dataset_extent(*index, &dims_u64)?;
1288 Ok(())
1289 }
1290 _ => Err(Hdf5Error::InvalidState(
1291 "file is no longer in write mode".into(),
1292 )),
1293 }
1294 }
1295 DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
1296 "cannot set extent in read mode".into(),
1297 )),
1298 }
1299 }
1300
1301 pub fn flush(&self) -> Result<()> {
1303 match &self.info {
1304 DatasetInfo::Writer { index, .. } => {
1305 let mut inner = borrow_inner_mut(&self.file_inner);
1306 match &mut *inner {
1307 H5FileInner::Writer(writer) => {
1308 writer.flush_dataset(*index)?;
1309 Ok(())
1310 }
1311 _ => Ok(()),
1312 }
1313 }
1314 DatasetInfo::Reader { .. } => Ok(()),
1315 }
1316 }
1317
1318 pub fn read_slice<T: H5Type>(&self, starts: &[usize], counts: &[usize]) -> Result<Vec<T>> {
1323 match &self.info {
1324 DatasetInfo::Reader {
1325 name, element_size, ..
1326 } => {
1327 if T::element_size() != *element_size {
1328 return Err(Hdf5Error::TypeMismatch(format!(
1329 "read type has element size {} but dataset has element size {}",
1330 T::element_size(),
1331 element_size,
1332 )));
1333 }
1334 let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1335 let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1336
1337 let raw = {
1338 let mut inner = borrow_inner_mut(&self.file_inner);
1339 match &mut *inner {
1340 H5FileInner::Reader(reader) => {
1341 reader.read_slice(name, &starts_u64, &counts_u64)?
1342 }
1343 _ => {
1344 return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
1345 }
1346 }
1347 };
1348
1349 if raw.len() % T::element_size() != 0 {
1350 return Err(Hdf5Error::TypeMismatch(format!(
1351 "raw data size {} is not a multiple of element size {}",
1352 raw.len(),
1353 T::element_size(),
1354 )));
1355 }
1356
1357 let count = raw.len() / T::element_size();
1358 let mut result = Vec::<T>::with_capacity(count);
1359 unsafe {
1360 std::ptr::copy_nonoverlapping(
1361 raw.as_ptr(),
1362 result.as_mut_ptr() as *mut u8,
1363 raw.len(),
1364 );
1365 result.set_len(count);
1366 }
1367 Ok(result)
1368 }
1369 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1370 "cannot read_slice from a dataset in write mode".into(),
1371 )),
1372 }
1373 }
1374
1375 pub fn write_slice<T: H5Type>(
1379 &self,
1380 starts: &[usize],
1381 counts: &[usize],
1382 data: &[T],
1383 ) -> Result<()> {
1384 match &self.info {
1385 DatasetInfo::Writer {
1386 index,
1387 element_size,
1388 chunked,
1389 ..
1390 } => {
1391 if *chunked {
1392 return Err(Hdf5Error::InvalidState(
1393 "write_slice is only for contiguous datasets".into(),
1394 ));
1395 }
1396 if T::element_size() != *element_size {
1397 return Err(Hdf5Error::TypeMismatch(format!(
1398 "write type has element size {} but dataset expects {}",
1399 T::element_size(),
1400 element_size,
1401 )));
1402 }
1403
1404 let expected: usize = counts.iter().product();
1405 if data.len() != expected {
1406 return Err(Hdf5Error::InvalidState(format!(
1407 "data length {} does not match slice size {}",
1408 data.len(),
1409 expected,
1410 )));
1411 }
1412
1413 let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1414 let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1415
1416 let byte_len = data.len() * T::element_size();
1417 let raw =
1418 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
1419
1420 let mut inner = borrow_inner_mut(&self.file_inner);
1421 match &mut *inner {
1422 H5FileInner::Writer(writer) => {
1423 writer.write_slice(*index, &starts_u64, &counts_u64, raw)?;
1424 Ok(())
1425 }
1426 _ => Err(Hdf5Error::InvalidState(
1427 "file is no longer in write mode".into(),
1428 )),
1429 }
1430 }
1431 DatasetInfo::Reader { .. } => {
1432 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1433 }
1434 }
1435 }
1436
1437 pub fn read_vlen_strings(&self) -> Result<Vec<String>> {
1442 match &self.info {
1443 DatasetInfo::Reader { name, .. } => {
1444 let mut inner = borrow_inner_mut(&self.file_inner);
1445 match &mut *inner {
1446 H5FileInner::Reader(reader) => Ok(reader.read_vlen_strings(name)?),
1447 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1448 }
1449 }
1450 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1451 "cannot read vlen strings from a dataset in write mode".into(),
1452 )),
1453 }
1454 }
1455
1456 pub fn read_raw<T: H5Type>(&self) -> Result<Vec<T>> {
1468 match &self.info {
1469 DatasetInfo::Reader {
1470 name, element_size, ..
1471 } => {
1472 if T::element_size() != *element_size {
1473 return Err(Hdf5Error::TypeMismatch(format!(
1474 "read type has element size {} but dataset has element size {}",
1475 T::element_size(),
1476 element_size,
1477 )));
1478 }
1479
1480 let raw = {
1481 let mut inner = borrow_inner_mut(&self.file_inner);
1482 match &mut *inner {
1483 H5FileInner::Reader(reader) => reader.read_dataset_raw(name)?,
1484 _ => {
1485 return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
1486 }
1487 }
1488 };
1489
1490 if raw.len() % T::element_size() != 0 {
1491 return Err(Hdf5Error::TypeMismatch(format!(
1492 "raw data size {} is not a multiple of element size {}",
1493 raw.len(),
1494 T::element_size(),
1495 )));
1496 }
1497
1498 let count = raw.len() / T::element_size();
1499 let mut result = Vec::<T>::with_capacity(count);
1500
1501 unsafe {
1508 std::ptr::copy_nonoverlapping(
1509 raw.as_ptr(),
1510 result.as_mut_ptr() as *mut u8,
1511 raw.len(),
1512 );
1513 result.set_len(count);
1514 }
1515
1516 Ok(result)
1517 }
1518 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1519 "cannot read from a dataset in write mode".into(),
1520 )),
1521 }
1522 }
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527 use crate::H5File;
1528 use std::path::PathBuf;
1529
1530 fn temp_path(name: &str) -> PathBuf {
1531 use std::sync::atomic::{AtomicU64, Ordering};
1535 static COUNTER: AtomicU64 = AtomicU64::new(0);
1536 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1537 std::env::temp_dir().join(format!(
1538 "hdf5_dataset_test_{}_{}_{}.h5",
1539 name,
1540 std::process::id(),
1541 n
1542 ))
1543 }
1544
1545 #[test]
1546 fn builder_requires_shape() {
1547 let path = temp_path("no_shape");
1548 let file = H5File::create(&path).unwrap();
1549 let result = file.new_dataset::<u8>().create("data");
1550 assert!(result.is_err());
1551 std::fs::remove_file(&path).ok();
1552 }
1553
1554 #[test]
1555 fn write_raw_size_mismatch() {
1556 let path = temp_path("size_mismatch");
1557 let file = H5File::create(&path).unwrap();
1558 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1559 let result = ds.write_raw(&[1u8, 2, 3]);
1561 assert!(result.is_err());
1562 std::fs::remove_file(&path).ok();
1563 }
1564
1565 #[test]
1566 fn roundtrip_u8_1d() {
1567 let path = temp_path("rt_u8_1d");
1568 let data: Vec<u8> = (0..10).collect();
1569
1570 {
1571 let file = H5File::create(&path).unwrap();
1572 let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
1573 ds.write_raw(&data).unwrap();
1574 file.close().unwrap();
1575 }
1576
1577 {
1578 let file = H5File::open(&path).unwrap();
1579 let ds = file.dataset("seq").unwrap();
1580 assert_eq!(ds.shape(), vec![10]);
1581 let readback = ds.read_raw::<u8>().unwrap();
1582 assert_eq!(readback, data);
1583 }
1584
1585 std::fs::remove_file(&path).ok();
1586 }
1587
1588 #[test]
1589 fn roundtrip_i32_2d() {
1590 let path = temp_path("rt_i32_2d");
1591 let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
1592
1593 {
1594 let file = H5File::create(&path).unwrap();
1595 let ds = file
1596 .new_dataset::<i32>()
1597 .shape([2, 3])
1598 .create("matrix")
1599 .unwrap();
1600 ds.write_raw(&data).unwrap();
1601 file.close().unwrap();
1602 }
1603
1604 {
1605 let file = H5File::open(&path).unwrap();
1606 let ds = file.dataset("matrix").unwrap();
1607 assert_eq!(ds.shape(), vec![2, 3]);
1608 let readback = ds.read_raw::<i32>().unwrap();
1609 assert_eq!(readback, data);
1610 }
1611
1612 std::fs::remove_file(&path).ok();
1613 }
1614
1615 #[test]
1616 fn roundtrip_f64_3d() {
1617 let path = temp_path("rt_f64_3d");
1618 let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
1619
1620 {
1621 let file = H5File::create(&path).unwrap();
1622 let ds = file
1623 .new_dataset::<f64>()
1624 .shape([2, 3, 4])
1625 .create("cube")
1626 .unwrap();
1627 ds.write_raw(&data).unwrap();
1628 file.close().unwrap();
1629 }
1630
1631 {
1632 let file = H5File::open(&path).unwrap();
1633 let ds = file.dataset("cube").unwrap();
1634 assert_eq!(ds.shape(), vec![2, 3, 4]);
1635 let readback = ds.read_raw::<f64>().unwrap();
1636 assert_eq!(readback, data);
1637 }
1638
1639 std::fs::remove_file(&path).ok();
1640 }
1641
1642 #[test]
1643 fn cannot_read_in_write_mode() {
1644 let path = temp_path("no_read_write");
1645 let file = H5File::create(&path).unwrap();
1646 let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
1647 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1648 let result = ds.read_raw::<u8>();
1649 assert!(result.is_err());
1650 std::fs::remove_file(&path).ok();
1651 }
1652
1653 #[test]
1654 fn cannot_write_in_read_mode() {
1655 let path = temp_path("no_write_read");
1656
1657 {
1658 let file = H5File::create(&path).unwrap();
1659 let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
1660 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1661 file.close().unwrap();
1662 }
1663
1664 {
1665 let file = H5File::open(&path).unwrap();
1666 let ds = file.dataset("x").unwrap();
1667 let result = ds.write_raw(&[5u8, 6, 7, 8]);
1668 assert!(result.is_err());
1669 }
1670
1671 std::fs::remove_file(&path).ok();
1672 }
1673
1674 #[test]
1675 fn numeric_attr_roundtrip() {
1676 let path = temp_path("num_attr");
1677 {
1678 let file = H5File::create(&path).unwrap();
1679 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1680 ds.write_raw(&[1.0f32; 4]).unwrap();
1681
1682 let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
1683 a1.write_numeric(&1.2345f64).unwrap();
1684
1685 let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
1686 a2.write_numeric(&42i32).unwrap();
1687
1688 file.close().unwrap();
1689 }
1690 {
1691 let file = H5File::open(&path).unwrap();
1692 let ds = file.dataset("data").unwrap();
1693
1694 let scale = ds.attr("scale").unwrap();
1695 let val: f64 = scale.read_numeric().unwrap();
1696 assert!((val - 1.2345).abs() < 1e-10);
1697
1698 let count = ds.attr("count").unwrap();
1699 let val: i32 = count.read_numeric().unwrap();
1700 assert_eq!(val, 42);
1701 }
1702 std::fs::remove_file(&path).ok();
1703 }
1704
1705 #[test]
1706 fn attr_datatype_exposes_class_and_sign() {
1707 use crate::format::messages::datatype::DatatypeMessage;
1711
1712 let path = temp_path("attr_datatype");
1713 {
1714 let file = H5File::create(&path).unwrap();
1715 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1716 ds.new_attr::<f64>()
1717 .shape(())
1718 .create("scale")
1719 .unwrap()
1720 .write_numeric(&1.5f64)
1721 .unwrap();
1722 ds.new_attr::<i32>()
1723 .shape(())
1724 .create("count")
1725 .unwrap()
1726 .write_numeric(&7i32)
1727 .unwrap();
1728 file.close().unwrap();
1729 }
1730 {
1731 let file = H5File::open(&path).unwrap();
1732 let ds = file.dataset("data").unwrap();
1733
1734 match ds.attr("scale").unwrap().datatype().unwrap() {
1735 DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
1736 other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
1737 }
1738
1739 match ds.attr("count").unwrap().datatype().unwrap() {
1740 DatatypeMessage::FixedPoint { size, signed, .. } => {
1741 assert_eq!(size, 4);
1742 assert!(signed, "i32 attr must be signed");
1743 }
1744 other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
1745 }
1746 }
1747 std::fs::remove_file(&path).ok();
1748 }
1749
1750 #[test]
1751 fn attr_datatype_in_write_mode_errors() {
1752 let path = temp_path("attr_datatype_write_mode");
1753 let file = H5File::create(&path).unwrap();
1754 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1755 let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
1756 assert!(attr.datatype().is_err());
1757 std::fs::remove_file(&path).ok();
1758 }
1759
1760 #[test]
1761 fn cannot_create_dataset_in_read_mode() {
1762 let path = temp_path("no_create_read");
1763
1764 {
1765 let _file = H5File::create(&path).unwrap();
1766 }
1767
1768 {
1769 let file = H5File::open(&path).unwrap();
1770 let result = file.new_dataset::<u8>().shape([4]).create("x");
1771 assert!(result.is_err());
1772 }
1773
1774 std::fs::remove_file(&path).ok();
1775 }
1776
1777 #[test]
1778 fn shape_accessor() {
1779 let path = temp_path("shape_acc");
1780
1781 let file = H5File::create(&path).unwrap();
1782 let ds = file
1783 .new_dataset::<f32>()
1784 .shape([5, 10, 3])
1785 .create("tensor")
1786 .unwrap();
1787 assert_eq!(ds.shape(), vec![5, 10, 3]);
1788
1789 std::fs::remove_file(&path).ok();
1790 }
1791
1792 #[test]
1793 fn slice_roundtrip_2d() {
1794 let path = temp_path("slice_2d");
1795
1796 let data: Vec<i32> = (0..20).collect();
1798 {
1799 let file = H5File::create(&path).unwrap();
1800 let ds = file
1801 .new_dataset::<i32>()
1802 .shape([4, 5])
1803 .create("mat")
1804 .unwrap();
1805 ds.write_raw(&data).unwrap();
1806 file.close().unwrap();
1807 }
1808 {
1809 let file = H5File::open(&path).unwrap();
1810 let ds = file.dataset("mat").unwrap();
1811 let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
1813 assert_eq!(slice, vec![7, 8, 12, 13]);
1816 }
1817
1818 std::fs::remove_file(&path).ok();
1819 }
1820
1821 #[test]
1822 fn write_slice_2d() {
1823 let path = temp_path("write_slice_2d");
1824
1825 {
1826 let file = H5File::create(&path).unwrap();
1827 let ds = file
1828 .new_dataset::<f32>()
1829 .shape([3, 4])
1830 .create("data")
1831 .unwrap();
1832 ds.write_raw(&[0.0f32; 12]).unwrap();
1833 ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
1835 .unwrap();
1836 file.close().unwrap();
1837 }
1838 {
1839 let file = H5File::open(&path).unwrap();
1840 let ds = file.dataset("data").unwrap();
1841 let full = ds.read_raw::<f32>().unwrap();
1842 assert_eq!(
1846 full,
1847 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,]
1848 );
1849 }
1850
1851 std::fs::remove_file(&path).ok();
1852 }
1853
1854 #[test]
1855 fn write_slice_out_of_bounds_rejected() {
1856 let path = temp_path("write_slice_oob");
1857 let file = H5File::create(&path).unwrap();
1858 let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
1859 ds.write_raw(&[0i32; 4]).unwrap();
1860 assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
1862 assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
1864 std::fs::remove_file(&path).ok();
1865 }
1866
1867 #[test]
1868 fn duplicate_dataset_name_rejected() {
1869 let path = temp_path("dup_name");
1870 let file = H5File::create(&path).unwrap();
1871 let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
1872 assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
1873 std::fs::remove_file(&path).ok();
1874 }
1875
1876 #[test]
1877 fn extend_cannot_shrink() {
1878 let path = temp_path("extend_shrink");
1879 let file = H5File::create(&path).unwrap();
1880 let ds = file
1881 .new_dataset::<i32>()
1882 .shape([0])
1883 .chunk(&[2])
1884 .max_shape(&[None])
1885 .create("d")
1886 .unwrap();
1887 ds.append(&[1i32, 2, 3, 4]).unwrap();
1888 assert!(ds.extend(&[2]).is_err());
1890 assert!(ds.extend(&[6]).is_ok());
1892 std::fs::remove_file(&path).ok();
1893 }
1894
1895 #[test]
1896 fn attr_read_roundtrip() {
1897 use crate::types::VarLenUnicode;
1898 let path = temp_path("attr_read");
1899
1900 {
1901 let file = H5File::create(&path).unwrap();
1902 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1903 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1904 let a1 = ds
1905 .new_attr::<VarLenUnicode>()
1906 .shape(())
1907 .create("units")
1908 .unwrap();
1909 a1.write_string("meters").unwrap();
1910 let a2 = ds
1911 .new_attr::<VarLenUnicode>()
1912 .shape(())
1913 .create("desc")
1914 .unwrap();
1915 a2.write_string("test data").unwrap();
1916 file.close().unwrap();
1917 }
1918 {
1919 let file = H5File::open(&path).unwrap();
1920 let ds = file.dataset("data").unwrap();
1921
1922 let names = ds.attr_names().unwrap();
1923 assert!(names.contains(&"units".to_string()));
1924 assert!(names.contains(&"desc".to_string()));
1925
1926 let units = ds.attr("units").unwrap();
1927 assert_eq!(units.read_string().unwrap(), "meters");
1928
1929 let desc = ds.attr("desc").unwrap();
1930 assert_eq!(desc.read_string().unwrap(), "test data");
1931 }
1932
1933 std::fs::remove_file(&path).ok();
1934 }
1935
1936 #[test]
1937 fn type_mismatch_element_size() {
1938 let path = temp_path("type_mismatch");
1939
1940 {
1941 let file = H5File::create(&path).unwrap();
1942 let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
1943 ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
1944 file.close().unwrap();
1945 }
1946
1947 {
1948 let file = H5File::open(&path).unwrap();
1949 let ds = file.dataset("data").unwrap();
1950 let result = ds.read_raw::<u8>();
1952 assert!(result.is_err());
1953 }
1954
1955 std::fs::remove_file(&path).ok();
1956 }
1957
1958 #[test]
1959 fn dataset_survives_file_move() {
1960 let path = temp_path("ds_survives");
1961
1962 let ds = {
1963 let file = H5File::create(&path).unwrap();
1964 file.new_dataset::<u8>().shape([4]).create("x").unwrap()
1965 };
1966 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1968 std::fs::remove_file(&path).ok();
1971 }
1972
1973 #[test]
1974 fn new_attr_scalar_string() {
1975 use crate::types::VarLenUnicode;
1976
1977 let path = temp_path("attr_scalar_string");
1978 {
1979 let file = H5File::create(&path).unwrap();
1980 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1981 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1982
1983 let attr = ds
1984 .new_attr::<VarLenUnicode>()
1985 .shape(())
1986 .create("name")
1987 .unwrap();
1988 attr.write_scalar(&VarLenUnicode("test_value".to_string()))
1989 .unwrap();
1990
1991 file.close().unwrap();
1992 }
1993
1994 {
1996 let file = H5File::open(&path).unwrap();
1997 let ds = file.dataset("data").unwrap();
1998 assert_eq!(ds.shape(), vec![4]);
1999 let readback = ds.read_raw::<u8>().unwrap();
2000 assert_eq!(readback, vec![1u8, 2, 3, 4]);
2001 }
2002
2003 std::fs::remove_file(&path).ok();
2004 }
2005
2006 #[test]
2007 fn all_numeric_types_roundtrip() {
2008 let path = temp_path("all_types");
2009
2010 {
2011 let file = H5File::create(&path).unwrap();
2012
2013 let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
2014 ds.write_raw(&[1u8, 2]).unwrap();
2015
2016 let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
2017 ds.write_raw(&[-1i8, 1]).unwrap();
2018
2019 let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
2020 ds.write_raw(&[100u16, 200]).unwrap();
2021
2022 let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
2023 ds.write_raw(&[-100i16, 100]).unwrap();
2024
2025 let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
2026 ds.write_raw(&[1000u32, 2000]).unwrap();
2027
2028 let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
2029 ds.write_raw(&[-1000i32, 1000]).unwrap();
2030
2031 let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
2032 ds.write_raw(&[10000u64, 20000]).unwrap();
2033
2034 let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
2035 ds.write_raw(&[-10000i64, 10000]).unwrap();
2036
2037 let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
2038 ds.write_raw(&[1.5f32, 2.5]).unwrap();
2039
2040 let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
2041 ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
2042
2043 file.close().unwrap();
2044 }
2045
2046 {
2047 let file = H5File::open(&path).unwrap();
2048
2049 assert_eq!(
2050 file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
2051 vec![1u8, 2]
2052 );
2053 assert_eq!(
2054 file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2055 vec![-1i8, 1]
2056 );
2057 assert_eq!(
2058 file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2059 vec![100u16, 200]
2060 );
2061 assert_eq!(
2062 file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2063 vec![-100i16, 100]
2064 );
2065 assert_eq!(
2066 file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2067 vec![1000u32, 2000]
2068 );
2069 assert_eq!(
2070 file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2071 vec![-1000i32, 1000]
2072 );
2073 assert_eq!(
2074 file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2075 vec![10000u64, 20000]
2076 );
2077 assert_eq!(
2078 file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2079 vec![-10000i64, 10000]
2080 );
2081 assert_eq!(
2082 file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2083 vec![1.5f32, 2.5]
2084 );
2085 assert_eq!(
2086 file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2087 vec![1.23456f64, 7.89012]
2088 );
2089 }
2090
2091 std::fs::remove_file(&path).ok();
2092 }
2093
2094 #[test]
2095 fn append_chunked_roundtrip() {
2096 let path = temp_path("append_chunked");
2097
2098 {
2099 let file = H5File::create(&path).unwrap();
2100 let ds = file
2101 .new_dataset::<f64>()
2102 .shape([0, 3])
2103 .chunk(&[1, 3])
2104 .max_shape(&[None, Some(3)])
2105 .create("data")
2106 .unwrap();
2107
2108 ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2110 ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2112
2113 file.close().unwrap();
2114 }
2115
2116 {
2117 let file = H5File::open(&path).unwrap();
2118 let ds = file.dataset("data").unwrap();
2119 assert_eq!(ds.shape(), vec![3, 3]);
2120 let all = ds.read_raw::<f64>().unwrap();
2121 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2122 }
2123
2124 std::fs::remove_file(&path).ok();
2125 }
2126
2127 #[test]
2128 fn append_1d_chunked() {
2129 let path = temp_path("append_1d");
2130
2131 {
2132 let file = H5File::create(&path).unwrap();
2133 let ds = file
2134 .new_dataset::<i32>()
2135 .shape([0])
2136 .chunk(&[4])
2137 .max_shape(&[None])
2138 .create("values")
2139 .unwrap();
2140
2141 ds.append(&[10i32, 20, 30]).unwrap(); ds.append(&[40i32]).unwrap(); ds.append(&[50i32, 60, 70, 80]).unwrap(); file.close().unwrap();
2146 }
2147
2148 {
2149 let file = H5File::open(&path).unwrap();
2150 let ds = file.dataset("values").unwrap();
2151 assert_eq!(ds.shape(), vec![8]);
2152 let all = ds.read_raw::<i32>().unwrap();
2153 assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2154 }
2155
2156 std::fs::remove_file(&path).ok();
2157 }
2158
2159 #[test]
2160 fn append_partial_chunk_flushed_on_close() {
2161 let path = temp_path("append_partial_close");
2162
2163 {
2164 let file = H5File::create(&path).unwrap();
2165 let ds = file
2166 .new_dataset::<f64>()
2167 .shape([0])
2168 .chunk(&[4])
2169 .max_shape(&[None])
2170 .create("vals")
2171 .unwrap();
2172
2173 ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
2175 file.close().unwrap();
2176 }
2177
2178 {
2179 let file = H5File::open(&path).unwrap();
2180 let ds = file.dataset("vals").unwrap();
2181 assert_eq!(ds.shape(), vec![5]);
2182 let all = ds.read_raw::<f64>().unwrap();
2183 assert_eq!(all.len(), 5);
2186 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2187 }
2188
2189 std::fs::remove_file(&path).ok();
2190 }
2191
2192 #[cfg(feature = "deflate")]
2193 #[test]
2194 fn vlen_append_after_reopen_filtered() {
2195 let path = temp_path("vlen_reopen_filtered");
2199 {
2200 let file = H5File::create(&path).unwrap();
2201 file.create_appendable_vlen_dataset(
2202 "strs",
2203 4,
2204 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2205 )
2206 .unwrap();
2207 file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
2208 .unwrap();
2209 file.close().unwrap();
2210 }
2211 {
2212 let file = H5File::open_rw(&path).unwrap();
2213 file.append_vlen_strings("strs", &["delta"]).unwrap();
2214 file.close().unwrap();
2215 }
2216 {
2217 let file = H5File::open(&path).unwrap();
2218 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2219 assert_eq!(
2220 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2221 vec!["alpha", "beta", "gamma", "delta"]
2222 );
2223 }
2224 std::fs::remove_file(&path).ok();
2225 }
2226
2227 #[test]
2228 fn vlen_append_after_reopen_data_block() {
2229 let path = temp_path("vlen_reopen_datablk");
2233 let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
2234 {
2235 let file = H5File::create(&path).unwrap();
2236 file.create_appendable_vlen_dataset("strs", 2, None)
2237 .unwrap();
2238 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2239 file.append_vlen_strings("strs", &refs).unwrap();
2240 file.close().unwrap();
2241 }
2242 {
2243 let file = H5File::open_rw(&path).unwrap();
2244 file.append_vlen_strings("strs", &["s9"]).unwrap();
2245 file.close().unwrap();
2246 }
2247 {
2248 let file = H5File::open(&path).unwrap();
2249 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2250 let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
2251 assert_eq!(got, want);
2252 }
2253 std::fs::remove_file(&path).ok();
2254 }
2255
2256 #[test]
2257 fn vlen_append_after_reopen_super_block() {
2258 let path = temp_path("vlen_reopen_super");
2266 let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
2269 {
2270 let file = H5File::create(&path).unwrap();
2271 file.create_appendable_vlen_dataset("strs", 2, None)
2272 .unwrap();
2273 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2274 file.append_vlen_strings("strs", &refs).unwrap();
2275 file.close().unwrap();
2276 }
2277 {
2278 let file = H5File::open_rw(&path).unwrap();
2279 file.append_vlen_strings("strs", &["v489"]).unwrap();
2280 file.close().unwrap();
2281 }
2282 {
2283 let file = H5File::open(&path).unwrap();
2284 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2285 let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
2286 assert_eq!(got, want);
2287 }
2288 std::fs::remove_file(&path).ok();
2289 }
2290
2291 #[cfg(feature = "deflate")]
2292 #[test]
2293 fn vlen_append_after_reopen_filtered_data_block() {
2294 let path = temp_path("vlen_reopen_filt_datablk");
2297 let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
2298 {
2299 let file = H5File::create(&path).unwrap();
2300 file.create_appendable_vlen_dataset(
2301 "strs",
2302 2,
2303 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2304 )
2305 .unwrap();
2306 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2307 file.append_vlen_strings("strs", &refs).unwrap();
2308 file.close().unwrap();
2309 }
2310 {
2311 let file = H5File::open_rw(&path).unwrap();
2312 file.append_vlen_strings("strs", &["item09"]).unwrap();
2313 file.close().unwrap();
2314 }
2315 {
2316 let file = H5File::open(&path).unwrap();
2317 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2318 let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
2319 assert_eq!(got, want);
2320 }
2321 std::fs::remove_file(&path).ok();
2322 }
2323
2324 #[test]
2325 fn group_nx_class_attribute_roundtrip() {
2326 let path = temp_path("group_nx_class");
2329 {
2330 let file = H5File::create(&path).unwrap();
2331 let entry = file.create_group("entry").unwrap();
2332 entry.set_attr_string("NX_class", "NXentry").unwrap();
2333 let det = entry.create_group("detector").unwrap();
2334 det.set_attr_string("NX_class", "NXdetector").unwrap();
2335 det.set_attr_numeric("frame_count", &7i32).unwrap();
2336 det.new_dataset::<f32>()
2337 .shape([4])
2338 .create("data")
2339 .unwrap()
2340 .write_raw(&[1.0f32; 4])
2341 .unwrap();
2342 file.close().unwrap();
2343 }
2344 {
2345 let file = H5File::open(&path).unwrap();
2346 let entry = file.root_group().group("entry").unwrap();
2347 assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
2348 let det = entry.group("detector").unwrap();
2349 assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
2350 let names = det.attr_names().unwrap();
2351 assert!(names.contains(&"NX_class".to_string()));
2352 assert!(names.contains(&"frame_count".to_string()));
2353 }
2354 std::fs::remove_file(&path).ok();
2355 }
2356
2357 #[test]
2358 fn ea_super_block_roundtrip() {
2359 let path = temp_path("ea_super_rt");
2362 {
2363 let file = H5File::create(&path).unwrap();
2364 let ds = file
2365 .new_dataset::<i32>()
2366 .shape([0])
2367 .chunk(&[1])
2368 .max_shape(&[None])
2369 .create("v")
2370 .unwrap();
2371 ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
2372 file.close().unwrap();
2373 }
2374 {
2375 let file = H5File::open(&path).unwrap();
2376 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2377 assert_eq!(v.len(), 2000);
2378 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2379 }
2380 std::fs::remove_file(&path).ok();
2381 }
2382
2383 #[cfg(feature = "deflate")]
2384 #[test]
2385 fn ea_filtered_super_block_roundtrip() {
2386 let path = temp_path("ea_filt_super");
2388 {
2389 let file = H5File::create(&path).unwrap();
2390 let ds = file
2391 .new_dataset::<i32>()
2392 .shape([0])
2393 .chunk(&[1])
2394 .max_shape(&[None])
2395 .deflate(4)
2396 .create("v")
2397 .unwrap();
2398 ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
2399 file.close().unwrap();
2400 }
2401 {
2402 let file = H5File::open(&path).unwrap();
2403 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2404 assert_eq!(v, (0..600).collect::<Vec<i32>>());
2405 }
2406 std::fs::remove_file(&path).ok();
2407 }
2408
2409 #[test]
2410 fn ea_super_block_open_append() {
2411 let path = temp_path("ea_super_append");
2413 {
2414 let file = H5File::create(&path).unwrap();
2415 let ds = file
2416 .new_dataset::<i32>()
2417 .shape([0])
2418 .chunk(&[1])
2419 .max_shape(&[None])
2420 .create("v")
2421 .unwrap();
2422 ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
2423 file.close().unwrap();
2424 }
2425 {
2426 let mut w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2427 let idx = w.dataset_index("v").unwrap();
2428 for c in 300..900u64 {
2429 w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
2430 }
2431 w.extend_dataset(idx, &[900]).unwrap();
2432 w.close().unwrap();
2433 }
2434 {
2435 let file = H5File::open(&path).unwrap();
2436 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2437 assert_eq!(v.len(), 900);
2438 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2439 }
2440 std::fs::remove_file(&path).ok();
2441 }
2442
2443 #[test]
2444 fn btree_v2_multi_unlimited_roundtrip() {
2445 let path = temp_path("bt2_multi");
2448 {
2449 let file = H5File::create(&path).unwrap();
2450 let ds = file
2451 .new_dataset::<i32>()
2452 .shape([0, 0])
2453 .chunk(&[2, 2])
2454 .max_shape(&[None, None])
2455 .create("grid")
2456 .unwrap();
2457 assert!(ds.is_chunked());
2458 for cr in 0..2usize {
2460 for cc in 0..2usize {
2461 let mut bytes = Vec::new();
2462 for i in 0..2usize {
2463 for j in 0..2usize {
2464 let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
2465 bytes.extend_from_slice(&v.to_le_bytes());
2466 }
2467 }
2468 ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
2469 }
2470 }
2471 file.close().unwrap();
2472 }
2473 {
2474 let file = H5File::open(&path).unwrap();
2475 let ds = file.dataset("grid").unwrap();
2476 assert_eq!(ds.shape(), vec![4, 4]);
2477 assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
2478 }
2479 std::fs::remove_file(&path).ok();
2480 }
2481
2482 #[test]
2483 fn subframe_chunking_roundtrip() {
2484 let path = temp_path("subframe");
2488 {
2489 let file = H5File::create(&path).unwrap();
2490 let ds = file
2491 .new_dataset::<i32>()
2492 .shape([0, 8, 8])
2493 .chunk(&[1, 4, 4])
2494 .max_shape(&[None, Some(8), Some(8)])
2495 .create("v")
2496 .unwrap();
2497 for f in 0..3usize {
2498 for cr in 0..2usize {
2499 for cc in 0..2usize {
2500 let mut bytes = Vec::new();
2501 for i in 0..4usize {
2502 for j in 0..4usize {
2503 let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
2504 bytes.extend_from_slice(&v.to_le_bytes());
2505 }
2506 }
2507 ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
2508 }
2509 }
2510 }
2511 file.close().unwrap();
2512 }
2513 {
2514 let file = H5File::open(&path).unwrap();
2515 let ds = file.dataset("v").unwrap();
2516 assert_eq!(ds.shape(), vec![3, 8, 8]);
2517 assert_eq!(
2518 ds.read_raw::<i32>().unwrap(),
2519 (0..192).collect::<Vec<i32>>()
2520 );
2521 }
2522 std::fs::remove_file(&path).ok();
2523 }
2524
2525 #[test]
2526 fn fill_value_contiguous_roundtrip() {
2527 let path = temp_path("fill_value_contig");
2528 {
2529 let file = H5File::create(&path).unwrap();
2530 let ds = file
2531 .new_dataset::<f32>()
2532 .shape([4])
2533 .fill_value(2.5f32)
2534 .create("data")
2535 .unwrap();
2536 ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
2537 file.close().unwrap();
2538 }
2539 {
2541 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2542 let idx = writer.dataset_index("data").unwrap();
2543 assert_eq!(
2544 writer.datasets[idx].fill_value,
2545 Some(2.5f32.to_le_bytes().to_vec())
2546 );
2547 }
2548 {
2550 let file = H5File::open(&path).unwrap();
2551 let ds = file.dataset("data").unwrap();
2552 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
2553 }
2554 std::fs::remove_file(&path).ok();
2555 }
2556
2557 #[test]
2558 fn fill_value_chunked_roundtrip() {
2559 let path = temp_path("fill_value_chunked");
2560 {
2561 let file = H5File::create(&path).unwrap();
2562 let ds = file
2563 .new_dataset::<i32>()
2564 .shape([0])
2565 .chunk(&[4])
2566 .max_shape(&[None])
2567 .fill_value(-7i32)
2568 .create("vals")
2569 .unwrap();
2570 ds.append(&[1i32, 2, 3, 4]).unwrap();
2571 file.close().unwrap();
2572 }
2573 {
2574 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2575 let idx = writer.dataset_index("vals").unwrap();
2576 assert_eq!(
2577 writer.datasets[idx].fill_value,
2578 Some((-7i32).to_le_bytes().to_vec())
2579 );
2580 }
2581 std::fs::remove_file(&path).ok();
2582 }
2583
2584 #[test]
2585 fn fill_value_read_missing_chunks() {
2586 fn i32_bytes(vals: &[i32]) -> Vec<u8> {
2589 vals.iter().flat_map(|v| v.to_le_bytes()).collect()
2590 }
2591 let path = temp_path("fill_value_read_missing");
2592 {
2593 let file = H5File::create(&path).unwrap();
2594 let ds = file
2595 .new_dataset::<i32>()
2596 .shape([0])
2597 .chunk(&[2])
2598 .max_shape(&[None])
2599 .fill_value(-1i32)
2600 .create("vals")
2601 .unwrap();
2602 ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
2604 ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
2605 ds.extend(&[6]).unwrap();
2606 file.close().unwrap();
2607 }
2608 {
2609 let file = H5File::open(&path).unwrap();
2610 let ds = file.dataset("vals").unwrap();
2611 let all = ds.read_raw::<i32>().unwrap();
2612 assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
2613 }
2614 std::fs::remove_file(&path).ok();
2615 }
2616
2617 #[test]
2618 fn fill_value_partial_chunk_padded_with_fill() {
2619 let path = temp_path("fill_value_partial_pad");
2623 {
2624 let file = H5File::create(&path).unwrap();
2625 let ds = file
2626 .new_dataset::<i32>()
2627 .shape([0])
2628 .chunk(&[4])
2629 .max_shape(&[None])
2630 .fill_value(-9i32)
2631 .create("vals")
2632 .unwrap();
2633 ds.append(&[1i32, 2, 3]).unwrap();
2635 file.close().unwrap();
2636 }
2637 let bytes = std::fs::read(&path).unwrap();
2638 let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2640 let pos = bytes
2641 .windows(needle.len())
2642 .position(|w| w == needle)
2643 .expect("chunk data [1,2,3] not found in file");
2644 let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
2645 assert_eq!(
2646 pad,
2647 &(-9i32).to_le_bytes(),
2648 "partial chunk tail must be padded with fill value -9, got {:?}",
2649 pad
2650 );
2651 std::fs::remove_file(&path).ok();
2652 }
2653
2654 #[test]
2655 fn vlen_append_after_reopen_preserves_existing() {
2656 let path = temp_path("vlen_append_reopen");
2659 {
2660 let file = H5File::create(&path).unwrap();
2661 file.create_appendable_vlen_dataset("strs", 4, None)
2662 .unwrap();
2663 file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
2665 file.close().unwrap();
2666 }
2667 {
2668 let file = H5File::open_rw(&path).unwrap();
2670 file.append_vlen_strings("strs", &["d"]).unwrap();
2671 file.close().unwrap();
2672 }
2673 {
2674 let file = H5File::open(&path).unwrap();
2675 let ds = file.dataset("strs").unwrap();
2676 let got = ds.read_vlen_strings().unwrap();
2677 assert_eq!(
2678 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2679 vec!["a", "b", "c", "d"]
2680 );
2681 }
2682 std::fs::remove_file(&path).ok();
2683 }
2684
2685 #[test]
2686 fn fill_value_size_mismatch_errors() {
2687 let path = temp_path("fill_value_mismatch");
2688 let mut writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
2689 let dt = <f64 as crate::types::H5Type>::hdf5_type();
2690 let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
2691 assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
2693 writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
2695 writer.close().unwrap();
2696 std::fs::remove_file(&path).ok();
2697 }
2698
2699 #[test]
2700 fn datatype_exposes_class_sign_and_byteorder() {
2701 use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
2705
2706 let path = temp_path("datatype_accessor");
2707 {
2708 let file = H5File::create(&path).unwrap();
2709 file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
2710 file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
2711 file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
2712 file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
2713 file.close().unwrap();
2714 }
2715
2716 let file = H5File::open(&path).unwrap();
2717
2718 match file.dataset("u8d").unwrap().datatype().unwrap() {
2719 DatatypeMessage::FixedPoint {
2720 size,
2721 signed,
2722 byte_order,
2723 ..
2724 } => {
2725 assert_eq!(size, 1);
2726 assert!(!signed, "u8 must be unsigned");
2727 assert_eq!(byte_order, ByteOrder::LittleEndian);
2728 }
2729 other => panic!("expected FixedPoint for u8, got {other:?}"),
2730 }
2731
2732 match file.dataset("i8d").unwrap().datatype().unwrap() {
2733 DatatypeMessage::FixedPoint { size, signed, .. } => {
2734 assert_eq!(size, 1);
2735 assert!(signed, "i8 must be signed");
2736 }
2737 other => panic!("expected FixedPoint for i8, got {other:?}"),
2738 }
2739
2740 match file.dataset("i32d").unwrap().datatype().unwrap() {
2741 DatatypeMessage::FixedPoint { size, signed, .. } => {
2742 assert_eq!(size, 4);
2743 assert!(signed, "i32 must be signed");
2744 }
2745 other => panic!("expected FixedPoint for i32, got {other:?}"),
2746 }
2747
2748 match file.dataset("f32d").unwrap().datatype().unwrap() {
2749 DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
2750 other => panic!("expected FloatingPoint for f32, got {other:?}"),
2751 }
2752
2753 std::fs::remove_file(&path).ok();
2754 }
2755
2756 #[test]
2757 fn datatype_in_write_mode_errors() {
2758 let path = temp_path("datatype_write_mode");
2759 let file = H5File::create(&path).unwrap();
2760 let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
2761 assert!(ds.datatype().is_err());
2762 std::fs::remove_file(&path).ok();
2763 }
2764
2765 #[cfg(feature = "deflate")]
2771 #[test]
2772 fn write_chunk_raw_ea_roundtrip_mask0() {
2773 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2774 let path = temp_path("wcr_ea_mask0");
2775 let original: Vec<i32> = (0..12).collect();
2776 {
2777 let file = H5File::create(&path).unwrap();
2778 let ds = file
2779 .new_dataset::<i32>()
2780 .shape([0])
2781 .chunk(&[4])
2782 .max_shape(&[None])
2783 .deflate(4)
2784 .create("v")
2785 .unwrap();
2786 assert!(ds.is_chunked());
2787 let pipeline = FilterPipeline::deflate(4);
2788 for c in 0..3usize {
2789 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2790 .iter()
2791 .flat_map(|v| v.to_le_bytes())
2792 .collect();
2793 let compressed = apply_filters(&pipeline, &raw).unwrap();
2794 ds.write_chunk_raw(c, &compressed, 0).unwrap();
2795 }
2796 ds.set_extent(&[12]).unwrap();
2797 file.close().unwrap();
2798 }
2799 {
2800 let file = H5File::open(&path).unwrap();
2801 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2802 assert_eq!(v, original);
2803 }
2804 std::fs::remove_file(&path).ok();
2805 }
2806
2807 #[cfg(feature = "deflate")]
2810 #[test]
2811 fn write_chunk_raw_fixed_array_roundtrip_mask0() {
2812 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2813 let path = temp_path("wcr_fa_mask0");
2814 let original: Vec<i32> = (0..12).collect();
2815 {
2816 let file = H5File::create(&path).unwrap();
2817 let ds = file
2818 .new_dataset::<i32>()
2819 .shape([12])
2820 .chunk(&[4])
2821 .deflate(4)
2822 .create("v")
2823 .unwrap();
2824 assert!(ds.is_chunked());
2825 let pipeline = FilterPipeline::deflate(4);
2826 for c in 0..3usize {
2827 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2828 .iter()
2829 .flat_map(|v| v.to_le_bytes())
2830 .collect();
2831 let compressed = apply_filters(&pipeline, &raw).unwrap();
2832 ds.write_chunk_raw(c, &compressed, 0).unwrap();
2833 }
2834 file.close().unwrap();
2835 }
2836 {
2837 let file = H5File::open(&path).unwrap();
2838 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2839 assert_eq!(v, original);
2840 }
2841 std::fs::remove_file(&path).ok();
2842 }
2843
2844 #[cfg(feature = "deflate")]
2850 #[test]
2851 fn write_chunk_raw_records_filter_mask() {
2852 let path = temp_path("wcr_records_mask");
2853 let raw: Vec<u8> = [10i32, 20, 30, 40]
2854 .iter()
2855 .flat_map(|v| v.to_le_bytes())
2856 .collect();
2857 assert_eq!(raw.len(), 16);
2858 {
2859 let file = H5File::create(&path).unwrap();
2860 let ds = file
2861 .new_dataset::<i32>()
2862 .shape([0])
2863 .chunk(&[4])
2864 .max_shape(&[None])
2865 .deflate(4)
2866 .create("v")
2867 .unwrap();
2868 ds.write_chunk_raw(0, &raw, 1).unwrap();
2871 ds.set_extent(&[4]).unwrap();
2872 file.close().unwrap();
2873 }
2874 {
2877 let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2878 let idx = w.dataset_index("v").unwrap();
2879 let entry = &w.datasets[idx]
2880 .chunked
2881 .as_ref()
2882 .unwrap()
2883 .filt_iblk
2884 .as_ref()
2885 .unwrap()
2886 .elements[0];
2887 assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
2888 assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
2889 }
2890 std::fs::remove_file(&path).ok();
2891 }
2892
2893 #[cfg(feature = "deflate")]
2898 #[test]
2899 fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
2900 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2901 let path = temp_path("wcr_ea_per_chunk_mask");
2902 let original: Vec<i32> = (0..8).collect();
2903 let pipeline = FilterPipeline::deflate(4);
2904 {
2905 let file = H5File::create(&path).unwrap();
2906 let ds = file
2907 .new_dataset::<i32>()
2908 .shape([0])
2909 .chunk(&[4])
2910 .max_shape(&[None])
2911 .deflate(4)
2912 .create("v")
2913 .unwrap();
2914 let raw0: Vec<u8> = original[0..4]
2915 .iter()
2916 .flat_map(|v| v.to_le_bytes())
2917 .collect();
2918 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
2920 .unwrap();
2921 let raw1: Vec<u8> = original[4..8]
2922 .iter()
2923 .flat_map(|v| v.to_le_bytes())
2924 .collect();
2925 ds.write_chunk_raw(1, &raw1, 1).unwrap();
2927 ds.set_extent(&[8]).unwrap();
2928 file.close().unwrap();
2929 }
2930 {
2931 let file = H5File::open(&path).unwrap();
2932 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2933 assert_eq!(v, original);
2934 }
2935 std::fs::remove_file(&path).ok();
2936 }
2937
2938 #[cfg(feature = "deflate")]
2941 #[test]
2942 fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
2943 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2944 let path = temp_path("wcr_fa_per_chunk_mask");
2945 let original: Vec<i32> = (0..8).collect();
2946 let pipeline = FilterPipeline::deflate(4);
2947 {
2948 let file = H5File::create(&path).unwrap();
2949 let ds = file
2950 .new_dataset::<i32>()
2951 .shape([8])
2952 .chunk(&[4])
2953 .deflate(4)
2954 .create("v")
2955 .unwrap();
2956 let raw0: Vec<u8> = original[0..4]
2957 .iter()
2958 .flat_map(|v| v.to_le_bytes())
2959 .collect();
2960 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
2961 .unwrap();
2962 let raw1: Vec<u8> = original[4..8]
2963 .iter()
2964 .flat_map(|v| v.to_le_bytes())
2965 .collect();
2966 ds.write_chunk_raw(1, &raw1, 1).unwrap();
2967 file.close().unwrap();
2968 }
2969 {
2970 let file = H5File::open(&path).unwrap();
2971 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2972 assert_eq!(v, original);
2973 }
2974 std::fs::remove_file(&path).ok();
2975 }
2976
2977 #[test]
2980 fn write_chunk_raw_rejects_unfiltered() {
2981 let path = temp_path("wcr_unfiltered");
2982 let file = H5File::create(&path).unwrap();
2983 let ds = file
2984 .new_dataset::<i32>()
2985 .shape([0])
2986 .chunk(&[4])
2987 .max_shape(&[None])
2988 .create("v")
2989 .unwrap();
2990 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
2991 assert!(
2992 err.to_string().contains("filtered dataset"),
2993 "expected a filtered-dataset error, got: {err}"
2994 );
2995 std::fs::remove_file(&path).ok();
2996 }
2997
2998 #[test]
3001 fn write_chunk_raw_rejects_btree_v2() {
3002 let path = temp_path("wcr_btree2");
3003 let file = H5File::create(&path).unwrap();
3004 let ds = file
3005 .new_dataset::<i32>()
3006 .shape([0, 0])
3007 .chunk(&[2, 2])
3008 .max_shape(&[None, None])
3009 .create("grid")
3010 .unwrap();
3011 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3012 assert!(
3013 err.to_string().contains("v2-B-tree"),
3014 "expected a v2-B-tree rejection, got: {err}"
3015 );
3016 std::fs::remove_file(&path).ok();
3017 }
3018
3019 #[cfg(feature = "deflate")]
3024 #[test]
3025 fn write_chunk_raw_rejects_oversized_chunk() {
3026 let path = temp_path("wcr_oversized");
3027 let file = H5File::create(&path).unwrap();
3028 let ds = file
3029 .new_dataset::<i32>()
3030 .shape([0])
3031 .chunk(&[1])
3032 .max_shape(&[None])
3033 .deflate(4)
3034 .create("v")
3035 .unwrap();
3036 let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
3037 assert!(
3038 err.to_string().contains("does not fit"),
3039 "expected a chunk-size-field overflow error, got: {err}"
3040 );
3041 std::fs::remove_file(&path).ok();
3042 }
3043}