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 = datatype.element_size() as usize;
240
241 let wants_filter = self.custom_pipeline.is_some()
248 || self.shuffle_deflate_level.is_some()
249 || self.deflate_level.is_some();
250 let auto_chunk: Option<Vec<usize>> =
251 if self.chunk_dims.is_none() && wants_filter && !shape.is_empty() {
252 Some(shape.iter().map(|&d| d.max(1)).collect())
253 } else {
254 None
255 };
256
257 if let Some(chunk_dims) = self.chunk_dims.as_ref().or(auto_chunk.as_ref()) {
258 let chunk_u64: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
260 let max_u64: Vec<u64> = if let Some(ref max) = self.max_shape {
261 max.iter()
262 .map(|m| m.map_or(u64::MAX, |v| v as u64))
263 .collect()
264 } else {
265 dims_u64.clone()
267 };
268
269 let n_unlimited = max_u64.iter().filter(|&&m| m == u64::MAX).count();
273 let is_btree2 = n_unlimited >= 2;
274 let is_fixed_array = n_unlimited == 0;
275
276 let index = {
277 let inner = borrow_inner(&self.file_inner);
278 match &*inner {
279 H5FileInner::Writer(writer) => {
280 let idx = if is_btree2 {
281 if wants_filter {
282 return Err(Hdf5Error::InvalidState(
283 "compression of v2 B-tree (multi-unlimited-dimension) \
284 datasets is not yet supported"
285 .into(),
286 ));
287 }
288 writer.create_btree_v2_dataset(
289 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
290 )?
291 } else if is_fixed_array {
292 if wants_filter {
298 let pipeline = if let Some(p) = self.custom_pipeline {
299 p
300 } else if let Some(level) = self.shuffle_deflate_level {
301 crate::format::messages::filter::FilterPipeline::shuffle_deflate(
302 T::element_size() as u32,
303 level,
304 )
305 } else {
306 crate::format::messages::filter::FilterPipeline::deflate(
308 self.deflate_level.unwrap(),
309 )
310 };
311 writer.create_fixed_array_dataset_with_pipeline(
312 &full_name, datatype, &dims_u64, &chunk_u64, pipeline,
313 )?
314 } else {
315 writer.create_fixed_array_dataset(
316 &full_name, datatype, &dims_u64, &chunk_u64,
317 )?
318 }
319 } else if let Some(pipeline) = self.custom_pipeline {
320 writer.create_chunked_dataset_with_pipeline(
321 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
322 )?
323 } else if let Some(level) = self.shuffle_deflate_level {
324 let pipeline =
325 crate::format::messages::filter::FilterPipeline::shuffle_deflate(
326 T::element_size() as u32,
327 level,
328 );
329 writer.create_chunked_dataset_with_pipeline(
330 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
331 )?
332 } else if let Some(level) = self.deflate_level {
333 writer.create_chunked_dataset_compressed(
334 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, level,
335 )?
336 } else {
337 writer.create_chunked_dataset(
338 &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
339 )?
340 };
341 if let Some(ref gp) = group_path {
342 if gp != "/" {
343 writer.assign_dataset_to_group(gp, idx)?;
344 }
345 }
346 if let Some(ref fv) = fill_value {
347 writer.set_dataset_fill_value(idx, fv.clone())?;
348 }
349 idx
350 }
351 H5FileInner::Reader(_) => {
352 return Err(Hdf5Error::InvalidState(
353 "cannot create a dataset in read mode".into(),
354 ));
355 }
356 H5FileInner::Closed => {
357 return Err(Hdf5Error::InvalidState("file is closed".into()));
358 }
359 }
360 };
361
362 Ok(H5Dataset {
363 file_inner: clone_inner(&self.file_inner),
364 info: DatasetInfo::Writer {
365 index,
366 shape,
367 element_size,
368 chunked: true,
369 btree2: is_btree2,
370 fixed_array: is_fixed_array,
371 },
372 })
373 } else {
374 let index = {
376 let inner = borrow_inner(&self.file_inner);
377 match &*inner {
378 H5FileInner::Writer(writer) => {
379 let idx = writer.create_dataset(&full_name, datatype, &dims_u64)?;
380 if let Some(ref gp) = group_path {
381 if gp != "/" {
382 writer.assign_dataset_to_group(gp, idx)?;
383 }
384 }
385 if let Some(ref fv) = fill_value {
386 writer.set_dataset_fill_value(idx, fv.clone())?;
387 }
388 idx
389 }
390 H5FileInner::Reader(_) => {
391 return Err(Hdf5Error::InvalidState(
392 "cannot create a dataset in read mode".into(),
393 ));
394 }
395 H5FileInner::Closed => {
396 return Err(Hdf5Error::InvalidState("file is closed".into()));
397 }
398 }
399 };
400
401 Ok(H5Dataset {
402 file_inner: clone_inner(&self.file_inner),
403 info: DatasetInfo::Writer {
404 index,
405 shape,
406 element_size,
407 chunked: false,
408 btree2: false,
409 fixed_array: false,
410 },
411 })
412 }
413 }
414}
415
416enum DatasetInfo {
422 Writer {
424 index: usize,
426 shape: Vec<usize>,
428 element_size: usize,
430 chunked: bool,
432 btree2: bool,
434 fixed_array: bool,
436 },
437 Reader {
439 name: String,
441 shape: Vec<usize>,
443 element_size: usize,
445 },
446}
447
448pub struct H5Dataset {
458 file_inner: SharedInner,
459 info: DatasetInfo,
460}
461
462impl H5Dataset {
463 pub(crate) fn new_reader(
465 file_inner: SharedInner,
466 name: String,
467 shape: Vec<usize>,
468 element_size: usize,
469 ) -> Self {
470 Self {
471 file_inner,
472 info: DatasetInfo::Reader {
473 name,
474 shape,
475 element_size,
476 },
477 }
478 }
479
480 pub(crate) fn new_writer(
486 file_inner: SharedInner,
487 index: usize,
488 shape: Vec<usize>,
489 element_size: usize,
490 chunked: bool,
491 btree2: bool,
492 fixed_array: bool,
493 ) -> Self {
494 Self {
495 file_inner,
496 info: DatasetInfo::Writer {
497 index,
498 shape,
499 element_size,
500 chunked,
501 btree2,
502 fixed_array,
503 },
504 }
505 }
506
507 pub fn shape(&self) -> Vec<usize> {
509 match &self.info {
510 DatasetInfo::Writer { shape, .. } => shape.clone(),
511 DatasetInfo::Reader { shape, .. } => shape.clone(),
512 }
513 }
514
515 pub fn ndims(&self) -> usize {
517 match &self.info {
518 DatasetInfo::Writer { shape, .. } => shape.len(),
519 DatasetInfo::Reader { shape, .. } => shape.len(),
520 }
521 }
522
523 pub fn total_elements(&self) -> usize {
525 match &self.info {
526 DatasetInfo::Writer { shape, .. } => shape.iter().product(),
527 DatasetInfo::Reader { shape, .. } => shape.iter().product(),
528 }
529 }
530
531 pub fn element_size(&self) -> usize {
533 match &self.info {
534 DatasetInfo::Writer { element_size, .. } => *element_size,
535 DatasetInfo::Reader { element_size, .. } => *element_size,
536 }
537 }
538
539 pub fn datatype(&self) -> Result<DatatypeMessage> {
570 match &self.info {
571 DatasetInfo::Reader { name, .. } => {
572 let inner = borrow_inner(&self.file_inner);
573 match &*inner {
574 H5FileInner::Reader(reader) => reader
575 .dataset_info(name)
576 .map(|info| info.datatype.clone())
577 .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
578 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
579 }
580 }
581 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
582 "datatype() is only available in read mode".into(),
583 )),
584 }
585 }
586
587 pub fn chunk_dims(&self) -> Option<Vec<usize>> {
589 match &self.info {
590 DatasetInfo::Reader { name, .. } => {
591 let inner = borrow_inner(&self.file_inner);
592 if let H5FileInner::Reader(reader) = &*inner {
593 if let Some(info) = reader.dataset_info(name) {
594 use crate::format::messages::data_layout::DataLayoutMessage;
595 let chunk_dims = match &info.layout {
596 DataLayoutMessage::ChunkedV4 { chunk_dims, .. }
597 | DataLayoutMessage::ChunkedV3 { chunk_dims, .. } => Some(chunk_dims),
598 _ => None,
599 };
600 if let Some(chunk_dims) = chunk_dims {
601 return Some(
603 chunk_dims[..chunk_dims.len() - 1]
604 .iter()
605 .map(|&d| d as usize)
606 .collect(),
607 );
608 }
609 }
610 }
611 None
612 }
613 DatasetInfo::Writer { .. } => None,
614 }
615 }
616
617 pub fn is_chunked(&self) -> bool {
619 match &self.info {
620 DatasetInfo::Writer { chunked, .. } => *chunked,
621 DatasetInfo::Reader { name, .. } => {
622 let inner = borrow_inner(&self.file_inner);
623 match &*inner {
624 H5FileInner::Reader(reader) => {
625 if let Some(info) = reader.dataset_info(name) {
626 use crate::format::messages::data_layout::DataLayoutMessage;
627 matches!(
628 info.layout,
629 DataLayoutMessage::ChunkedV4 { .. }
630 | DataLayoutMessage::ChunkedV3 { .. }
631 )
632 } else {
633 false
634 }
635 }
636 _ => false,
637 }
638 }
639 }
640 }
641
642 pub fn attr_names(&self) -> Result<Vec<String>> {
644 match &self.info {
645 DatasetInfo::Reader { name, .. } => {
646 let inner = borrow_inner(&self.file_inner);
647 match &*inner {
648 H5FileInner::Reader(reader) => Ok(reader.dataset_attr_names(name)?),
649 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
650 }
651 }
652 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
653 "attr_names not available in write mode".into(),
654 )),
655 }
656 }
657
658 pub fn attr(&self, attr_name: &str) -> Result<crate::attribute::H5Attribute> {
660 match &self.info {
661 DatasetInfo::Reader { name, .. } => {
662 let inner = borrow_inner(&self.file_inner);
663 match &*inner {
664 H5FileInner::Reader(reader) => {
665 let attr_msg = reader.dataset_attr(name, attr_name)?.clone();
666 Ok(crate::attribute::H5Attribute::new_reader(
667 clone_inner(&self.file_inner),
668 attr_msg,
669 ))
670 }
671 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
672 }
673 }
674 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
675 "attr() not available in write mode".into(),
676 )),
677 }
678 }
679
680 pub fn new_attr<T: 'static>(&self) -> AttrBuilder<'_, T> {
696 let ds_index = match &self.info {
697 DatasetInfo::Writer { index, .. } => *index,
698 DatasetInfo::Reader { .. } => {
699 usize::MAX
702 }
703 };
704 AttrBuilder::new(&self.file_inner, ds_index)
705 }
706
707 pub fn write_raw<T: H5Type>(&self, data: &[T]) -> Result<()> {
719 match &self.info {
720 DatasetInfo::Writer {
721 index,
722 shape,
723 element_size,
724 chunked,
725 btree2,
726 fixed_array,
727 } => {
728 let total_elements: usize = shape.iter().product();
729 if data.len() != total_elements {
730 return Err(Hdf5Error::InvalidState(format!(
731 "data length {} does not match dataset size {}",
732 data.len(),
733 total_elements,
734 )));
735 }
736
737 if T::element_size() != *element_size {
739 return Err(Hdf5Error::TypeMismatch(format!(
740 "write type has element size {} but dataset expects {}",
741 T::element_size(),
742 element_size,
743 )));
744 }
745
746 let byte_len = data.len() * T::element_size();
750 let raw =
751 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
752
753 if *chunked {
754 return self.write_full_image_chunked(
758 *index,
759 *btree2,
760 *fixed_array,
761 raw,
762 *element_size,
763 );
764 }
765
766 let inner = borrow_inner(&self.file_inner);
767 match &*inner {
768 H5FileInner::Writer(writer) => {
769 writer.write_dataset_raw(*index, raw)?;
770 Ok(())
771 }
772 _ => Err(Hdf5Error::InvalidState(
773 "file is no longer in write mode".into(),
774 )),
775 }
776 }
777 DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
778 "cannot write to a dataset opened in read mode".into(),
779 )),
780 }
781 }
782
783 pub fn write_raw_bytes(&self, bytes: &[u8]) -> Result<()> {
818 match &self.info {
819 DatasetInfo::Writer {
820 index,
821 shape,
822 element_size,
823 chunked,
824 btree2,
825 fixed_array,
826 } => {
827 let expected: usize = shape.iter().product::<usize>() * *element_size;
828 if bytes.len() != expected {
829 return Err(Hdf5Error::InvalidState(format!(
830 "raw byte length {} does not match dataset size {} \
831 (product(shape) * element_size {})",
832 bytes.len(),
833 expected,
834 element_size,
835 )));
836 }
837 if *chunked {
838 return self.write_full_image_chunked(
841 *index,
842 *btree2,
843 *fixed_array,
844 bytes,
845 *element_size,
846 );
847 }
848 let inner = borrow_inner(&self.file_inner);
849 match &*inner {
850 H5FileInner::Writer(writer) => {
851 writer.write_dataset_raw(*index, bytes)?;
852 Ok(())
853 }
854 _ => Err(Hdf5Error::InvalidState(
855 "file is no longer in write mode".into(),
856 )),
857 }
858 }
859 DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
860 "cannot write to a dataset opened in read mode".into(),
861 )),
862 }
863 }
864
865 fn write_full_image_chunked(
875 &self,
876 index: usize,
877 btree2: bool,
878 fixed_array: bool,
879 bytes: &[u8],
880 element_size: usize,
881 ) -> Result<()> {
882 let inner = borrow_inner(&self.file_inner);
883 let writer = match &*inner {
884 H5FileInner::Writer(w) => w,
885 _ => {
886 return Err(Hdf5Error::InvalidState(
887 "file is no longer in write mode".into(),
888 ))
889 }
890 };
891 let chunk_dims = writer
892 .dataset_chunk_dims(index)
893 .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
894 .to_vec();
895 let dims = writer.dataset_dims(index).to_vec();
896 let rank = dims.len();
897
898 let mut grid = vec![0u64; rank];
900 for d in 0..rank {
901 grid[d] = if chunk_dims[d] > 0 {
902 dims[d].div_ceil(chunk_dims[d])
903 } else {
904 0
905 };
906 }
907 let total_chunks: u64 = grid.iter().product();
908
909 for linear in 0..total_chunks {
910 let mut rem = linear;
912 let mut coords = vec![0u64; rank];
913 for d in (0..rank).rev() {
914 coords[d] = rem % grid[d];
915 rem /= grid[d];
916 }
917 let chunk_buf = Self::gather_chunk(bytes, &dims, &chunk_dims, &coords, element_size);
918 if fixed_array {
919 writer.write_chunk_fixed_array(index, &coords, &chunk_buf)?;
920 } else if btree2 {
921 writer.write_chunk_btree_v2(index, &coords, &chunk_buf)?;
922 } else {
923 writer.write_chunk(index, linear, &chunk_buf)?;
926 }
927 }
928 Ok(())
929 }
930
931 fn gather_chunk(
937 source: &[u8],
938 dims: &[u64],
939 chunk_dims: &[u64],
940 coords: &[u64],
941 element_size: usize,
942 ) -> Vec<u8> {
943 let rank = dims.len();
944 let chunk_elems: u64 = chunk_dims.iter().product();
945 let mut out = vec![0u8; chunk_elems as usize * element_size];
946 if rank == 0 {
947 if source.len() >= element_size {
949 out[..element_size].copy_from_slice(&source[..element_size]);
950 }
951 return out;
952 }
953
954 let mut extent = vec![0u64; rank];
957 for d in 0..rank {
958 let start = coords[d] * chunk_dims[d];
959 let end = ((coords[d] + 1) * chunk_dims[d]).min(dims[d]);
960 extent[d] = end.saturating_sub(start);
961 }
962 if extent.contains(&0) {
963 return out; }
965
966 let mut src_stride = vec![1u64; rank];
969 let mut dst_stride = vec![1u64; rank];
970 for d in (0..rank - 1).rev() {
971 src_stride[d] = src_stride[d + 1] * dims[d + 1];
972 dst_stride[d] = dst_stride[d + 1] * chunk_dims[d + 1];
973 }
974
975 let last = rank - 1;
977 let run = extent[last] as usize * element_size;
978 let outer: u64 = extent[..last].iter().product::<u64>().max(1);
979 let mut idx = vec![0u64; rank]; for _ in 0..outer {
981 let mut src_off = 0u64;
982 let mut dst_off = 0u64;
983 for d in 0..rank {
984 let global = coords[d] * chunk_dims[d] + idx[d];
985 src_off += global * src_stride[d];
986 dst_off += idx[d] * dst_stride[d];
987 }
988 let s = src_off as usize * element_size;
989 let dpos = dst_off as usize * element_size;
990 out[dpos..dpos + run].copy_from_slice(&source[s..s + run]);
991
992 let mut d = last;
995 while d > 0 {
996 d -= 1;
997 idx[d] += 1;
998 if idx[d] < extent[d] {
999 break;
1000 }
1001 idx[d] = 0;
1002 }
1003 }
1004 out
1005 }
1006
1007 pub fn write_chunk(&self, chunk_idx: usize, data: &[u8]) -> Result<()> {
1015 match &self.info {
1016 DatasetInfo::Writer {
1017 index,
1018 chunked,
1019 btree2,
1020 fixed_array,
1021 ..
1022 } => {
1023 if !*chunked {
1024 return Err(Hdf5Error::InvalidState(
1025 "write_chunk is only for chunked datasets".into(),
1026 ));
1027 }
1028 if *btree2 {
1029 return Err(Hdf5Error::InvalidState(
1030 "this dataset uses a v2 B-tree chunk index; use write_chunk_at \
1031 with the chunk's grid coordinates"
1032 .into(),
1033 ));
1034 }
1035
1036 let inner = borrow_inner(&self.file_inner);
1037 match &*inner {
1038 H5FileInner::Writer(writer) => {
1039 if *fixed_array {
1040 let chunk_dims = writer
1043 .dataset_chunk_dims(*index)
1044 .ok_or_else(|| {
1045 Hdf5Error::InvalidState("dataset has no chunk info".into())
1046 })?
1047 .to_vec();
1048 let dims = writer.dataset_dims(*index).to_vec();
1049 let mut grid = vec![0u64; dims.len()];
1050 for d in 0..dims.len() {
1051 grid[d] = if chunk_dims[d] > 0 {
1052 dims[d].div_ceil(chunk_dims[d])
1053 } else {
1054 1
1055 };
1056 }
1057 if grid.contains(&0) {
1060 return Err(Hdf5Error::InvalidState(
1061 "dataset has a zero-extent dimension and no chunks".into(),
1062 ));
1063 }
1064 let mut rem = chunk_idx as u64;
1065 let mut coords = vec![0u64; dims.len()];
1066 for d in (0..dims.len()).rev() {
1067 coords[d] = rem % grid[d];
1068 rem /= grid[d];
1069 }
1070 if rem != 0 {
1072 return Err(Hdf5Error::InvalidState(format!(
1073 "chunk index {chunk_idx} is out of range for this dataset"
1074 )));
1075 }
1076 writer.write_chunk_fixed_array(*index, &coords, data)?;
1077 } else {
1078 writer.write_chunk(*index, chunk_idx as u64, data)?;
1079 }
1080 Ok(())
1081 }
1082 _ => Err(Hdf5Error::InvalidState(
1083 "file is no longer in write mode".into(),
1084 )),
1085 }
1086 }
1087 DatasetInfo::Reader { .. } => {
1088 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1089 }
1090 }
1091 }
1092
1093 pub fn write_chunk_raw(&self, chunk_idx: usize, data: &[u8], filter_mask: u32) -> Result<()> {
1119 match &self.info {
1120 DatasetInfo::Writer {
1121 index,
1122 chunked,
1123 btree2,
1124 fixed_array,
1125 ..
1126 } => {
1127 if !*chunked {
1128 return Err(Hdf5Error::InvalidState(
1129 "write_chunk_raw is only for chunked datasets".into(),
1130 ));
1131 }
1132 if *btree2 {
1133 return Err(Hdf5Error::InvalidState(
1134 "direct chunk writes are not supported for v2-B-tree-indexed \
1135 datasets (two or more unlimited dimensions)"
1136 .into(),
1137 ));
1138 }
1139
1140 let inner = borrow_inner(&self.file_inner);
1141 match &*inner {
1142 H5FileInner::Writer(writer) => {
1143 if *fixed_array {
1144 let chunk_dims = writer
1147 .dataset_chunk_dims(*index)
1148 .ok_or_else(|| {
1149 Hdf5Error::InvalidState("dataset has no chunk info".into())
1150 })?
1151 .to_vec();
1152 let dims = writer.dataset_dims(*index).to_vec();
1153 let mut grid = vec![0u64; dims.len()];
1154 for d in 0..dims.len() {
1155 grid[d] = if chunk_dims[d] > 0 {
1156 dims[d].div_ceil(chunk_dims[d])
1157 } else {
1158 1
1159 };
1160 }
1161 if grid.contains(&0) {
1164 return Err(Hdf5Error::InvalidState(
1165 "dataset has a zero-extent dimension and no chunks".into(),
1166 ));
1167 }
1168 let mut rem = chunk_idx as u64;
1169 let mut coords = vec![0u64; dims.len()];
1170 for d in (0..dims.len()).rev() {
1171 coords[d] = rem % grid[d];
1172 rem /= grid[d];
1173 }
1174 if rem != 0 {
1176 return Err(Hdf5Error::InvalidState(format!(
1177 "chunk index {chunk_idx} is out of range for this dataset"
1178 )));
1179 }
1180 writer.write_compressed_chunk_fixed_array(
1181 *index,
1182 &coords,
1183 data,
1184 filter_mask,
1185 )?;
1186 } else {
1187 writer.write_compressed_chunk(
1188 *index,
1189 chunk_idx as u64,
1190 data,
1191 filter_mask,
1192 )?;
1193 }
1194 Ok(())
1195 }
1196 _ => Err(Hdf5Error::InvalidState(
1197 "file is no longer in write mode".into(),
1198 )),
1199 }
1200 }
1201 DatasetInfo::Reader { .. } => {
1202 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1203 }
1204 }
1205 }
1206
1207 pub fn write_chunk_at(&self, chunk_coords: &[usize], data: &[u8]) -> Result<()> {
1228 match &self.info {
1229 DatasetInfo::Writer {
1230 index,
1231 chunked,
1232 btree2,
1233 fixed_array,
1234 ..
1235 } => {
1236 if !*chunked {
1237 return Err(Hdf5Error::InvalidState(
1238 "write_chunk_at is only for chunked datasets".into(),
1239 ));
1240 }
1241 let coords: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
1242 let btree2 = *btree2;
1243 let fixed_array = *fixed_array;
1244 let inner = borrow_inner(&self.file_inner);
1245 let writer = match &*inner {
1246 H5FileInner::Writer(w) => w,
1247 _ => {
1248 return Err(Hdf5Error::InvalidState(
1249 "file is no longer in write mode".into(),
1250 ))
1251 }
1252 };
1253 let chunk_dims = writer
1254 .dataset_chunk_dims(*index)
1255 .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1256 .to_vec();
1257 let dims = writer.dataset_dims(*index).to_vec();
1258 if coords.len() != dims.len() {
1259 return Err(Hdf5Error::InvalidState(format!(
1260 "chunk_coords has {} entries but the dataset has {} dimensions",
1261 coords.len(),
1262 dims.len()
1263 )));
1264 }
1265 if chunk_dims.len() != dims.len() {
1266 return Err(Hdf5Error::InvalidState(format!(
1267 "dataset chunk shape has {} dimensions but the dataspace has {}",
1268 chunk_dims.len(),
1269 dims.len()
1270 )));
1271 }
1272
1273 let mut new_dims = dims.clone();
1277 for d in 0..dims.len() {
1278 let needed = coords[d]
1279 .checked_add(1)
1280 .and_then(|c| c.checked_mul(chunk_dims[d]))
1281 .ok_or_else(|| {
1282 Hdf5Error::InvalidState(format!(
1283 "chunk coordinate {} in dimension {} is too large",
1284 coords[d], d
1285 ))
1286 })?;
1287 if needed > new_dims[d] {
1288 new_dims[d] = needed;
1289 }
1290 }
1291
1292 if fixed_array {
1293 writer.write_chunk_fixed_array(*index, &coords, data)?;
1295 return Ok(());
1296 }
1297
1298 if btree2 {
1299 writer.write_chunk_btree_v2(*index, &coords, data)?;
1300 } else {
1301 let mut linear = 0u64;
1304 for d in 0..dims.len() {
1305 let grid = if chunk_dims[d] > 0 {
1306 dims[d].div_ceil(chunk_dims[d])
1307 } else {
1308 1
1309 };
1310 linear = linear
1311 .checked_mul(grid)
1312 .and_then(|l| l.checked_add(coords[d]))
1313 .ok_or_else(|| {
1314 Hdf5Error::InvalidState(
1315 "chunk coordinates overflow the array index".into(),
1316 )
1317 })?;
1318 }
1319 writer.write_chunk(*index, linear, data)?;
1320 }
1321
1322 if new_dims != dims {
1323 writer.extend_dataset(*index, &new_dims)?;
1324 }
1325 Ok(())
1326 }
1327 DatasetInfo::Reader { .. } => {
1328 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1329 }
1330 }
1331 }
1332
1333 pub fn write_chunks_batch(&self, chunks: &[(usize, &[u8])]) -> Result<()> {
1339 match &self.info {
1340 DatasetInfo::Writer { index, chunked, .. } => {
1341 if !*chunked {
1342 return Err(Hdf5Error::InvalidState(
1343 "write_chunks_batch is only for chunked datasets".into(),
1344 ));
1345 }
1346 let pairs: Vec<(u64, &[u8])> = chunks
1347 .iter()
1348 .map(|(idx, data)| (*idx as u64, *data))
1349 .collect();
1350 let inner = borrow_inner(&self.file_inner);
1351 match &*inner {
1352 H5FileInner::Writer(writer) => {
1353 writer.write_chunks_batch(*index, &pairs)?;
1354 Ok(())
1355 }
1356 _ => Err(Hdf5Error::InvalidState(
1357 "file is no longer in write mode".into(),
1358 )),
1359 }
1360 }
1361 DatasetInfo::Reader { .. } => {
1362 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1363 }
1364 }
1365 }
1366
1367 pub fn append<T: H5Type>(&self, data: &[T]) -> Result<()> {
1390 match &self.info {
1391 DatasetInfo::Writer {
1392 index,
1393 element_size,
1394 chunked,
1395 ..
1396 } => {
1397 if !*chunked {
1398 return Err(Hdf5Error::InvalidState(
1399 "append is only for chunked datasets".into(),
1400 ));
1401 }
1402 if T::element_size() != *element_size {
1403 return Err(Hdf5Error::TypeMismatch(format!(
1404 "append type has element size {} but dataset expects {}",
1405 T::element_size(),
1406 element_size,
1407 )));
1408 }
1409
1410 let ds_index = *index;
1411 let es = *element_size;
1412
1413 let inner = borrow_inner(&self.file_inner);
1414 let writer = match &*inner {
1415 H5FileInner::Writer(w) => w,
1416 _ => {
1417 return Err(Hdf5Error::InvalidState(
1418 "file is no longer in write mode".into(),
1419 ))
1420 }
1421 };
1422
1423 let chunk_dims = writer
1424 .dataset_chunk_dims(ds_index)
1425 .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1426 .to_vec();
1427 let dims = writer.dataset_dims(ds_index).to_vec();
1428
1429 let frame_elems: usize = if dims.len() > 1 {
1431 dims[1..].iter().map(|&d| d as usize).product()
1432 } else {
1433 1
1434 };
1435
1436 if frame_elems == 0 {
1437 return Err(Hdf5Error::InvalidState(
1438 "cannot append to dataset with zero-size trailing dimensions".into(),
1439 ));
1440 }
1441
1442 if !data.len().is_multiple_of(frame_elems) {
1443 return Err(Hdf5Error::InvalidState(format!(
1444 "data length {} is not a multiple of frame size {}",
1445 data.len(),
1446 frame_elems,
1447 )));
1448 }
1449
1450 let n_new_frames = data.len() / frame_elems;
1451 let current_dim0 = dims[0] as usize;
1452
1453 let chunk_dim0 = chunk_dims[0] as usize;
1455 let frame_bytes = frame_elems * es;
1456
1457 let raw = unsafe {
1458 std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * es)
1459 };
1460
1461 let (buffered_frames, mut combined) = {
1464 let ds = writer.ds(ds_index);
1465 let mut m = ds.lock();
1466 let buffered_frames = m.append_buffered_frames as usize;
1467 let combined = std::mem::take(&mut m.append_buffer);
1468 m.append_buffered_frames = 0;
1469 (buffered_frames, combined)
1470 };
1471 combined.extend_from_slice(raw);
1472
1473 let total_frames = buffered_frames + n_new_frames;
1474 let total_bytes = combined.len();
1475
1476 let base_dim0 = current_dim0 - buffered_frames;
1478 let mut byte_pos = 0usize;
1479 let mut frame_pos = 0usize;
1480
1481 while frame_pos < total_frames {
1482 let abs_frame = base_dim0 + frame_pos;
1483 let chunk_idx = abs_frame / chunk_dim0;
1484 let remaining_frames = total_frames - frame_pos;
1485 let frames_to_fill = chunk_dim0 - (abs_frame % chunk_dim0);
1486
1487 if remaining_frames >= frames_to_fill {
1488 let end = byte_pos + frames_to_fill * frame_bytes;
1490 if frames_to_fill == chunk_dim0 {
1491 writer.write_chunk(
1492 ds_index,
1493 chunk_idx as u64,
1494 &combined[byte_pos..end],
1495 )?;
1496 } else {
1497 let offset_in_chunk = (abs_frame % chunk_dim0) * frame_bytes;
1503 let mut chunk_buf =
1504 match writer.read_chunk_if_present(ds_index, chunk_idx as u64)? {
1505 Some(existing) => existing,
1506 None => {
1507 return Err(Hdf5Error::InvalidState(format!(
1508 "cannot append into partially-written chunk {}: \
1509 its existing content was not found in the chunk \
1510 index (the file may be inconsistent)",
1511 chunk_idx
1512 )));
1513 }
1514 };
1515 chunk_buf
1516 [offset_in_chunk..offset_in_chunk + frames_to_fill * frame_bytes]
1517 .copy_from_slice(&combined[byte_pos..end]);
1518 writer.write_chunk(ds_index, chunk_idx as u64, &chunk_buf)?;
1519 }
1520 byte_pos = end;
1521 frame_pos += frames_to_fill;
1522 } else {
1523 let ds = writer.ds(ds_index);
1525 let mut m = ds.lock();
1526 m.append_buffer = combined[byte_pos..total_bytes].to_vec();
1527 m.append_buffered_frames = remaining_frames as u64;
1528 frame_pos = total_frames;
1529 }
1530 }
1531
1532 let logical_dim0 = base_dim0 + total_frames;
1534 let mut new_dims: Vec<u64> = dims;
1535 new_dims[0] = logical_dim0 as u64;
1536 writer.extend_dataset(ds_index, &new_dims)?;
1537
1538 Ok(())
1539 }
1540 DatasetInfo::Reader { .. } => {
1541 Err(Hdf5Error::InvalidState("cannot append in read mode".into()))
1542 }
1543 }
1544 }
1545
1546 pub fn extend(&self, new_dims: &[usize]) -> Result<()> {
1548 match &self.info {
1549 DatasetInfo::Writer { index, chunked, .. } => {
1550 if !*chunked {
1551 return Err(Hdf5Error::InvalidState(
1552 "extend is only for chunked datasets".into(),
1553 ));
1554 }
1555
1556 let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1557 let inner = borrow_inner(&self.file_inner);
1558 match &*inner {
1559 H5FileInner::Writer(writer) => {
1560 writer.extend_dataset(*index, &dims_u64)?;
1561 Ok(())
1562 }
1563 _ => Err(Hdf5Error::InvalidState(
1564 "file is no longer in write mode".into(),
1565 )),
1566 }
1567 }
1568 DatasetInfo::Reader { .. } => {
1569 Err(Hdf5Error::InvalidState("cannot extend in read mode".into()))
1570 }
1571 }
1572 }
1573
1574 pub fn set_extent(&self, new_dims: &[usize]) -> Result<()> {
1585 match &self.info {
1586 DatasetInfo::Writer { index, .. } => {
1587 let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1588 let inner = borrow_inner(&self.file_inner);
1589 match &*inner {
1590 H5FileInner::Writer(writer) => {
1591 writer.set_dataset_extent(*index, &dims_u64)?;
1592 Ok(())
1593 }
1594 _ => Err(Hdf5Error::InvalidState(
1595 "file is no longer in write mode".into(),
1596 )),
1597 }
1598 }
1599 DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
1600 "cannot set extent in read mode".into(),
1601 )),
1602 }
1603 }
1604
1605 pub fn flush(&self) -> Result<()> {
1607 match &self.info {
1608 DatasetInfo::Writer { index, .. } => {
1609 let inner = borrow_inner(&self.file_inner);
1610 match &*inner {
1611 H5FileInner::Writer(writer) => {
1612 writer.flush_dataset(*index)?;
1613 Ok(())
1614 }
1615 _ => Ok(()),
1616 }
1617 }
1618 DatasetInfo::Reader { .. } => Ok(()),
1619 }
1620 }
1621
1622 pub fn read_slice<T: H5Type>(&self, starts: &[usize], counts: &[usize]) -> Result<Vec<T>> {
1627 match &self.info {
1628 DatasetInfo::Reader {
1629 name, element_size, ..
1630 } => {
1631 if T::element_size() != *element_size {
1632 return Err(Hdf5Error::TypeMismatch(format!(
1633 "read type has element size {} but dataset has element size {}",
1634 T::element_size(),
1635 element_size,
1636 )));
1637 }
1638 let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1639 let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1640
1641 let raw = {
1642 let mut inner = borrow_inner_mut(&self.file_inner);
1643 match &mut *inner {
1644 H5FileInner::Reader(reader) => {
1645 reader.read_slice(name, &starts_u64, &counts_u64)?
1646 }
1647 _ => {
1648 return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
1649 }
1650 }
1651 };
1652
1653 if raw.len() % T::element_size() != 0 {
1654 return Err(Hdf5Error::TypeMismatch(format!(
1655 "raw data size {} is not a multiple of element size {}",
1656 raw.len(),
1657 T::element_size(),
1658 )));
1659 }
1660
1661 let count = raw.len() / T::element_size();
1662 let mut result = Vec::<T>::with_capacity(count);
1663 unsafe {
1664 std::ptr::copy_nonoverlapping(
1665 raw.as_ptr(),
1666 result.as_mut_ptr() as *mut u8,
1667 raw.len(),
1668 );
1669 result.set_len(count);
1670 }
1671 Ok(result)
1672 }
1673 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1674 "cannot read_slice from a dataset in write mode".into(),
1675 )),
1676 }
1677 }
1678
1679 pub fn write_slice<T: H5Type>(
1683 &self,
1684 starts: &[usize],
1685 counts: &[usize],
1686 data: &[T],
1687 ) -> Result<()> {
1688 match &self.info {
1689 DatasetInfo::Writer {
1690 index,
1691 element_size,
1692 chunked,
1693 ..
1694 } => {
1695 if *chunked {
1696 return Err(Hdf5Error::InvalidState(
1697 "write_slice is only for contiguous datasets".into(),
1698 ));
1699 }
1700 if T::element_size() != *element_size {
1701 return Err(Hdf5Error::TypeMismatch(format!(
1702 "write type has element size {} but dataset expects {}",
1703 T::element_size(),
1704 element_size,
1705 )));
1706 }
1707
1708 let expected: usize = counts.iter().product();
1709 if data.len() != expected {
1710 return Err(Hdf5Error::InvalidState(format!(
1711 "data length {} does not match slice size {}",
1712 data.len(),
1713 expected,
1714 )));
1715 }
1716
1717 let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1718 let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1719
1720 let byte_len = data.len() * T::element_size();
1721 let raw =
1722 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
1723
1724 let inner = borrow_inner(&self.file_inner);
1725 match &*inner {
1726 H5FileInner::Writer(writer) => {
1727 writer.write_slice(*index, &starts_u64, &counts_u64, raw)?;
1728 Ok(())
1729 }
1730 _ => Err(Hdf5Error::InvalidState(
1731 "file is no longer in write mode".into(),
1732 )),
1733 }
1734 }
1735 DatasetInfo::Reader { .. } => {
1736 Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1737 }
1738 }
1739 }
1740
1741 pub fn read_vlen_strings(&self) -> Result<Vec<String>> {
1746 match &self.info {
1747 DatasetInfo::Reader { name, .. } => {
1748 let mut inner = borrow_inner_mut(&self.file_inner);
1749 match &mut *inner {
1750 H5FileInner::Reader(reader) => Ok(reader.read_vlen_strings(name)?),
1751 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1752 }
1753 }
1754 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1755 "cannot read vlen strings from a dataset in write mode".into(),
1756 )),
1757 }
1758 }
1759
1760 pub fn read_vlen_bytes(&self) -> Result<Vec<Vec<u8>>> {
1767 match &self.info {
1768 DatasetInfo::Reader { name, .. } => {
1769 let mut inner = borrow_inner_mut(&self.file_inner);
1770 match &mut *inner {
1771 H5FileInner::Reader(reader) => Ok(reader.read_vlen_bytes(name)?),
1772 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1773 }
1774 }
1775 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1776 "cannot read vlen bytes from a dataset in write mode".into(),
1777 )),
1778 }
1779 }
1780
1781 pub fn read_raw<T: H5Type>(&self) -> Result<Vec<T>> {
1793 match &self.info {
1794 DatasetInfo::Reader {
1795 name, element_size, ..
1796 } => {
1797 if T::element_size() != *element_size {
1798 return Err(Hdf5Error::TypeMismatch(format!(
1799 "read type has element size {} but dataset has element size {}",
1800 T::element_size(),
1801 element_size,
1802 )));
1803 }
1804
1805 let raw = {
1806 let mut inner = borrow_inner_mut(&self.file_inner);
1807 match &mut *inner {
1808 H5FileInner::Reader(reader) => reader.read_dataset_raw(name)?,
1809 _ => {
1810 return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
1811 }
1812 }
1813 };
1814
1815 if raw.len() % T::element_size() != 0 {
1816 return Err(Hdf5Error::TypeMismatch(format!(
1817 "raw data size {} is not a multiple of element size {}",
1818 raw.len(),
1819 T::element_size(),
1820 )));
1821 }
1822
1823 let count = raw.len() / T::element_size();
1824 let mut result = Vec::<T>::with_capacity(count);
1825
1826 unsafe {
1833 std::ptr::copy_nonoverlapping(
1834 raw.as_ptr(),
1835 result.as_mut_ptr() as *mut u8,
1836 raw.len(),
1837 );
1838 result.set_len(count);
1839 }
1840
1841 Ok(result)
1842 }
1843 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1844 "cannot read from a dataset in write mode".into(),
1845 )),
1846 }
1847 }
1848
1849 pub fn read_raw_bytes(&self) -> Result<Vec<u8>> {
1856 match &self.info {
1857 DatasetInfo::Reader { name, .. } => {
1858 let mut inner = borrow_inner_mut(&self.file_inner);
1859 match &mut *inner {
1860 H5FileInner::Reader(reader) => Ok(reader.read_dataset_raw(name)?),
1861 _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1862 }
1863 }
1864 DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1865 "cannot read from a dataset in write mode".into(),
1866 )),
1867 }
1868 }
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873 use crate::H5File;
1874 use std::path::PathBuf;
1875
1876 fn temp_path(name: &str) -> PathBuf {
1877 use std::sync::atomic::{AtomicU64, Ordering};
1881 static COUNTER: AtomicU64 = AtomicU64::new(0);
1882 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1883 std::env::temp_dir().join(format!(
1884 "hdf5_dataset_test_{}_{}_{}.h5",
1885 name,
1886 std::process::id(),
1887 n
1888 ))
1889 }
1890
1891 #[test]
1892 fn runtime_compound_via_datatype_override_and_raw_bytes() {
1893 use crate::format::messages::datatype::DatatypeMessage;
1894 use crate::types::{CompoundType, H5Type};
1895
1896 let path = temp_path("compound_raw");
1897 let ct = CompoundType {
1901 members: vec![
1902 ("id".to_string(), i32::hdf5_type(), 0),
1903 ("val".to_string(), f64::hdf5_type(), 4),
1904 ],
1905 total_size: 12,
1906 };
1907 let recs: [(i32, f64); 3] = [(1, 2.5), (2, 3.5), (3, -4.0)];
1908 let mut bytes = Vec::new();
1909 for (id, val) in recs {
1910 bytes.extend_from_slice(&id.to_le_bytes());
1911 bytes.extend_from_slice(&val.to_le_bytes());
1912 }
1913
1914 {
1915 let file = H5File::create(&path).unwrap();
1916 let ds = file
1917 .new_dataset::<u8>()
1918 .datatype(ct.to_datatype())
1919 .shape([recs.len()])
1920 .create("records")
1921 .unwrap();
1922 ds.write_raw_bytes(&bytes).unwrap();
1923 file.close().unwrap();
1924 }
1925 {
1926 let file = H5File::open(&path).unwrap();
1927 let ds = file.dataset("records").unwrap();
1928 match ds.datatype().unwrap() {
1931 DatatypeMessage::Compound { size, members } => {
1932 assert_eq!(size, 12);
1933 assert_eq!(members.len(), 2);
1934 assert_eq!(members[0].name, "id");
1935 assert_eq!(members[0].offset, 0);
1936 assert_eq!(members[1].name, "val");
1937 assert_eq!(members[1].offset, 4);
1938 }
1939 other => panic!("expected compound datatype, got {other:?}"),
1940 }
1941 assert_eq!(ds.read_raw_bytes().unwrap(), bytes);
1942 }
1943 std::fs::remove_file(&path).ok();
1944 }
1945
1946 #[test]
1947 fn builder_requires_shape() {
1948 let path = temp_path("no_shape");
1949 let file = H5File::create(&path).unwrap();
1950 let result = file.new_dataset::<u8>().create("data");
1951 assert!(result.is_err());
1952 std::fs::remove_file(&path).ok();
1953 }
1954
1955 #[test]
1956 fn write_raw_size_mismatch() {
1957 let path = temp_path("size_mismatch");
1958 let file = H5File::create(&path).unwrap();
1959 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1960 let result = ds.write_raw(&[1u8, 2, 3]);
1962 assert!(result.is_err());
1963 std::fs::remove_file(&path).ok();
1964 }
1965
1966 #[cfg(feature = "deflate")]
1970 #[test]
1971 fn filter_without_chunk_autochunks_and_roundtrips() {
1972 let path = temp_path("autochunk_filter");
1973 let data: Vec<i32> = (0..8).collect();
1974 {
1975 let file = H5File::create(&path).unwrap();
1976 let ds = file
1977 .new_dataset::<i32>()
1978 .deflate(6)
1979 .shape([8])
1980 .create("seq")
1981 .unwrap();
1982 ds.write_raw(&data).unwrap();
1983 file.close().unwrap();
1984 }
1985 {
1986 let file = H5File::open(&path).unwrap();
1987 let ds = file.dataset("seq").unwrap();
1988 assert!(
1990 ds.is_chunked(),
1991 "auto-chunk did not produce chunked storage"
1992 );
1993 assert_eq!(ds.chunk_dims(), Some(vec![8]));
1994 assert_eq!(ds.read_raw::<i32>().unwrap(), data);
1995 }
1996 std::fs::remove_file(&path).ok();
1997 }
1998
1999 #[cfg(feature = "deflate")]
2003 #[test]
2004 fn write_raw_multichunk_edge_roundtrips() {
2005 let path = temp_path("multichunk_edge");
2006 let data: Vec<i32> = (0..35).collect(); {
2008 let file = H5File::create(&path).unwrap();
2009 let ds = file
2010 .new_dataset::<i32>()
2011 .shape([7, 5])
2012 .chunk(&[3, 2])
2013 .deflate(4)
2014 .create("grid")
2015 .unwrap();
2016 ds.write_raw(&data).unwrap();
2017 file.close().unwrap();
2018 }
2019 {
2020 let file = H5File::open(&path).unwrap();
2021 let ds = file.dataset("grid").unwrap();
2022 assert_eq!(ds.shape(), vec![7, 5]);
2023 assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
2024 assert_eq!(ds.read_raw::<i32>().unwrap(), data);
2025 }
2026 std::fs::remove_file(&path).ok();
2027 }
2028
2029 #[test]
2032 fn write_raw_unfiltered_chunked_roundtrips() {
2033 let path = temp_path("chunked_unfiltered");
2034 let data: Vec<f64> = (0..12).map(|i| i as f64 * 1.5).collect(); {
2036 let file = H5File::create(&path).unwrap();
2037 let ds = file
2038 .new_dataset::<f64>()
2039 .shape([4, 3])
2040 .chunk(&[2, 2])
2041 .create("m")
2042 .unwrap();
2043 ds.write_raw(&data).unwrap();
2044 file.close().unwrap();
2045 }
2046 {
2047 let file = H5File::open(&path).unwrap();
2048 let ds = file.dataset("m").unwrap();
2049 assert_eq!(ds.chunk_dims(), Some(vec![2, 2]));
2050 assert_eq!(ds.read_raw::<f64>().unwrap(), data);
2051 }
2052 std::fs::remove_file(&path).ok();
2053 }
2054
2055 #[test]
2056 fn roundtrip_u8_1d() {
2057 let path = temp_path("rt_u8_1d");
2058 let data: Vec<u8> = (0..10).collect();
2059
2060 {
2061 let file = H5File::create(&path).unwrap();
2062 let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
2063 ds.write_raw(&data).unwrap();
2064 file.close().unwrap();
2065 }
2066
2067 {
2068 let file = H5File::open(&path).unwrap();
2069 let ds = file.dataset("seq").unwrap();
2070 assert_eq!(ds.shape(), vec![10]);
2071 let readback = ds.read_raw::<u8>().unwrap();
2072 assert_eq!(readback, data);
2073 }
2074
2075 std::fs::remove_file(&path).ok();
2076 }
2077
2078 #[test]
2079 fn roundtrip_i32_2d() {
2080 let path = temp_path("rt_i32_2d");
2081 let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
2082
2083 {
2084 let file = H5File::create(&path).unwrap();
2085 let ds = file
2086 .new_dataset::<i32>()
2087 .shape([2, 3])
2088 .create("matrix")
2089 .unwrap();
2090 ds.write_raw(&data).unwrap();
2091 file.close().unwrap();
2092 }
2093
2094 {
2095 let file = H5File::open(&path).unwrap();
2096 let ds = file.dataset("matrix").unwrap();
2097 assert_eq!(ds.shape(), vec![2, 3]);
2098 let readback = ds.read_raw::<i32>().unwrap();
2099 assert_eq!(readback, data);
2100 }
2101
2102 std::fs::remove_file(&path).ok();
2103 }
2104
2105 #[test]
2106 fn roundtrip_f64_3d() {
2107 let path = temp_path("rt_f64_3d");
2108 let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
2109
2110 {
2111 let file = H5File::create(&path).unwrap();
2112 let ds = file
2113 .new_dataset::<f64>()
2114 .shape([2, 3, 4])
2115 .create("cube")
2116 .unwrap();
2117 ds.write_raw(&data).unwrap();
2118 file.close().unwrap();
2119 }
2120
2121 {
2122 let file = H5File::open(&path).unwrap();
2123 let ds = file.dataset("cube").unwrap();
2124 assert_eq!(ds.shape(), vec![2, 3, 4]);
2125 let readback = ds.read_raw::<f64>().unwrap();
2126 assert_eq!(readback, data);
2127 }
2128
2129 std::fs::remove_file(&path).ok();
2130 }
2131
2132 #[test]
2133 fn cannot_read_in_write_mode() {
2134 let path = temp_path("no_read_write");
2135 let file = H5File::create(&path).unwrap();
2136 let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
2137 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2138 let result = ds.read_raw::<u8>();
2139 assert!(result.is_err());
2140 std::fs::remove_file(&path).ok();
2141 }
2142
2143 #[test]
2144 fn cannot_write_in_read_mode() {
2145 let path = temp_path("no_write_read");
2146
2147 {
2148 let file = H5File::create(&path).unwrap();
2149 let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
2150 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2151 file.close().unwrap();
2152 }
2153
2154 {
2155 let file = H5File::open(&path).unwrap();
2156 let ds = file.dataset("x").unwrap();
2157 let result = ds.write_raw(&[5u8, 6, 7, 8]);
2158 assert!(result.is_err());
2159 }
2160
2161 std::fs::remove_file(&path).ok();
2162 }
2163
2164 #[test]
2165 fn numeric_attr_roundtrip() {
2166 let path = temp_path("num_attr");
2167 {
2168 let file = H5File::create(&path).unwrap();
2169 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2170 ds.write_raw(&[1.0f32; 4]).unwrap();
2171
2172 let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
2173 a1.write_numeric(&1.2345f64).unwrap();
2174
2175 let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
2176 a2.write_numeric(&42i32).unwrap();
2177
2178 file.close().unwrap();
2179 }
2180 {
2181 let file = H5File::open(&path).unwrap();
2182 let ds = file.dataset("data").unwrap();
2183
2184 let scale = ds.attr("scale").unwrap();
2185 let val: f64 = scale.read_numeric().unwrap();
2186 assert!((val - 1.2345).abs() < 1e-10);
2187
2188 let count = ds.attr("count").unwrap();
2189 let val: i32 = count.read_numeric().unwrap();
2190 assert_eq!(val, 42);
2191 }
2192 std::fs::remove_file(&path).ok();
2193 }
2194
2195 #[test]
2196 fn array_attr_roundtrip() {
2197 let path = temp_path("array_attr");
2198 let offsets = [10i32, -20, 30];
2199 {
2200 let file = H5File::create(&path).unwrap();
2201 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2202 ds.write_raw(&[1.0f32; 4]).unwrap();
2203
2204 let a = ds
2206 .new_attr::<i32>()
2207 .shape([3])
2208 .create("dim_offset")
2209 .unwrap();
2210 a.write_array(&offsets).unwrap();
2211
2212 let bad = ds.new_attr::<i32>().shape([3]).create("bad").unwrap();
2214 assert!(bad.write_array(&[1i32, 2]).is_err());
2215
2216 file.close().unwrap();
2217 }
2218 {
2219 let file = H5File::open(&path).unwrap();
2220 let ds = file.dataset("data").unwrap();
2221 let a = ds.attr("dim_offset").unwrap();
2222 let raw = a.read_raw().unwrap();
2223 assert_eq!(raw.len(), 3 * 4);
2224 let got: Vec<i32> = raw
2225 .chunks_exact(4)
2226 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
2227 .collect();
2228 assert_eq!(got, offsets);
2229 }
2230 std::fs::remove_file(&path).ok();
2231 }
2232
2233 #[test]
2234 fn attr_datatype_exposes_class_and_sign() {
2235 use crate::format::messages::datatype::DatatypeMessage;
2239
2240 let path = temp_path("attr_datatype");
2241 {
2242 let file = H5File::create(&path).unwrap();
2243 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2244 ds.new_attr::<f64>()
2245 .shape(())
2246 .create("scale")
2247 .unwrap()
2248 .write_numeric(&1.5f64)
2249 .unwrap();
2250 ds.new_attr::<i32>()
2251 .shape(())
2252 .create("count")
2253 .unwrap()
2254 .write_numeric(&7i32)
2255 .unwrap();
2256 file.close().unwrap();
2257 }
2258 {
2259 let file = H5File::open(&path).unwrap();
2260 let ds = file.dataset("data").unwrap();
2261
2262 match ds.attr("scale").unwrap().datatype().unwrap() {
2263 DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
2264 other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
2265 }
2266
2267 match ds.attr("count").unwrap().datatype().unwrap() {
2268 DatatypeMessage::FixedPoint { size, signed, .. } => {
2269 assert_eq!(size, 4);
2270 assert!(signed, "i32 attr must be signed");
2271 }
2272 other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
2273 }
2274 }
2275 std::fs::remove_file(&path).ok();
2276 }
2277
2278 #[test]
2279 fn attr_datatype_in_write_mode_errors() {
2280 let path = temp_path("attr_datatype_write_mode");
2281 let file = H5File::create(&path).unwrap();
2282 let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2283 let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
2284 assert!(attr.datatype().is_err());
2285 std::fs::remove_file(&path).ok();
2286 }
2287
2288 #[test]
2289 fn cannot_create_dataset_in_read_mode() {
2290 let path = temp_path("no_create_read");
2291
2292 {
2293 let _file = H5File::create(&path).unwrap();
2294 }
2295
2296 {
2297 let file = H5File::open(&path).unwrap();
2298 let result = file.new_dataset::<u8>().shape([4]).create("x");
2299 assert!(result.is_err());
2300 }
2301
2302 std::fs::remove_file(&path).ok();
2303 }
2304
2305 #[test]
2306 fn shape_accessor() {
2307 let path = temp_path("shape_acc");
2308
2309 let file = H5File::create(&path).unwrap();
2310 let ds = file
2311 .new_dataset::<f32>()
2312 .shape([5, 10, 3])
2313 .create("tensor")
2314 .unwrap();
2315 assert_eq!(ds.shape(), vec![5, 10, 3]);
2316
2317 std::fs::remove_file(&path).ok();
2318 }
2319
2320 #[test]
2321 fn slice_roundtrip_2d() {
2322 let path = temp_path("slice_2d");
2323
2324 let data: Vec<i32> = (0..20).collect();
2326 {
2327 let file = H5File::create(&path).unwrap();
2328 let ds = file
2329 .new_dataset::<i32>()
2330 .shape([4, 5])
2331 .create("mat")
2332 .unwrap();
2333 ds.write_raw(&data).unwrap();
2334 file.close().unwrap();
2335 }
2336 {
2337 let file = H5File::open(&path).unwrap();
2338 let ds = file.dataset("mat").unwrap();
2339 let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
2341 assert_eq!(slice, vec![7, 8, 12, 13]);
2344 }
2345
2346 std::fs::remove_file(&path).ok();
2347 }
2348
2349 #[test]
2350 fn write_slice_2d() {
2351 let path = temp_path("write_slice_2d");
2352
2353 {
2354 let file = H5File::create(&path).unwrap();
2355 let ds = file
2356 .new_dataset::<f32>()
2357 .shape([3, 4])
2358 .create("data")
2359 .unwrap();
2360 ds.write_raw(&[0.0f32; 12]).unwrap();
2361 ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
2363 .unwrap();
2364 file.close().unwrap();
2365 }
2366 {
2367 let file = H5File::open(&path).unwrap();
2368 let ds = file.dataset("data").unwrap();
2369 let full = ds.read_raw::<f32>().unwrap();
2370 assert_eq!(
2374 full,
2375 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,]
2376 );
2377 }
2378
2379 std::fs::remove_file(&path).ok();
2380 }
2381
2382 #[test]
2383 fn write_slice_out_of_bounds_rejected() {
2384 let path = temp_path("write_slice_oob");
2385 let file = H5File::create(&path).unwrap();
2386 let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
2387 ds.write_raw(&[0i32; 4]).unwrap();
2388 assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
2390 assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
2392 std::fs::remove_file(&path).ok();
2393 }
2394
2395 #[test]
2396 fn duplicate_dataset_name_rejected() {
2397 let path = temp_path("dup_name");
2398 let file = H5File::create(&path).unwrap();
2399 let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
2400 assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
2401 std::fs::remove_file(&path).ok();
2402 }
2403
2404 #[test]
2405 fn extend_cannot_shrink() {
2406 let path = temp_path("extend_shrink");
2407 let file = H5File::create(&path).unwrap();
2408 let ds = file
2409 .new_dataset::<i32>()
2410 .shape([0])
2411 .chunk(&[2])
2412 .max_shape(&[None])
2413 .create("d")
2414 .unwrap();
2415 ds.append(&[1i32, 2, 3, 4]).unwrap();
2416 assert!(ds.extend(&[2]).is_err());
2418 assert!(ds.extend(&[6]).is_ok());
2420 std::fs::remove_file(&path).ok();
2421 }
2422
2423 #[test]
2424 fn attr_read_roundtrip() {
2425 use crate::types::VarLenUnicode;
2426 let path = temp_path("attr_read");
2427
2428 {
2429 let file = H5File::create(&path).unwrap();
2430 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2431 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2432 let a1 = ds
2433 .new_attr::<VarLenUnicode>()
2434 .shape(())
2435 .create("units")
2436 .unwrap();
2437 a1.write_string("meters").unwrap();
2438 let a2 = ds
2439 .new_attr::<VarLenUnicode>()
2440 .shape(())
2441 .create("desc")
2442 .unwrap();
2443 a2.write_string("test data").unwrap();
2444 file.close().unwrap();
2445 }
2446 {
2447 let file = H5File::open(&path).unwrap();
2448 let ds = file.dataset("data").unwrap();
2449
2450 let names = ds.attr_names().unwrap();
2451 assert!(names.contains(&"units".to_string()));
2452 assert!(names.contains(&"desc".to_string()));
2453
2454 let units = ds.attr("units").unwrap();
2455 assert_eq!(units.read_string().unwrap(), "meters");
2456
2457 let desc = ds.attr("desc").unwrap();
2458 assert_eq!(desc.read_string().unwrap(), "test data");
2459 }
2460
2461 std::fs::remove_file(&path).ok();
2462 }
2463
2464 #[test]
2465 fn type_mismatch_element_size() {
2466 let path = temp_path("type_mismatch");
2467
2468 {
2469 let file = H5File::create(&path).unwrap();
2470 let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
2471 ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
2472 file.close().unwrap();
2473 }
2474
2475 {
2476 let file = H5File::open(&path).unwrap();
2477 let ds = file.dataset("data").unwrap();
2478 let result = ds.read_raw::<u8>();
2480 assert!(result.is_err());
2481 }
2482
2483 std::fs::remove_file(&path).ok();
2484 }
2485
2486 #[test]
2487 fn dataset_survives_file_move() {
2488 let path = temp_path("ds_survives");
2489
2490 let ds = {
2491 let file = H5File::create(&path).unwrap();
2492 file.new_dataset::<u8>().shape([4]).create("x").unwrap()
2493 };
2494 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2496 std::fs::remove_file(&path).ok();
2499 }
2500
2501 #[test]
2502 fn new_attr_scalar_string() {
2503 use crate::types::VarLenUnicode;
2504
2505 let path = temp_path("attr_scalar_string");
2506 {
2507 let file = H5File::create(&path).unwrap();
2508 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2509 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2510
2511 let attr = ds
2512 .new_attr::<VarLenUnicode>()
2513 .shape(())
2514 .create("name")
2515 .unwrap();
2516 attr.write_scalar(&VarLenUnicode("test_value".to_string()))
2517 .unwrap();
2518
2519 file.close().unwrap();
2520 }
2521
2522 {
2524 use crate::format::messages::datatype::DatatypeMessage;
2525 let file = H5File::open(&path).unwrap();
2526 let ds = file.dataset("data").unwrap();
2527 assert_eq!(ds.shape(), vec![4]);
2528 let readback = ds.read_raw::<u8>().unwrap();
2529 assert_eq!(readback, vec![1u8, 2, 3, 4]);
2530
2531 let attr = ds.attr("name").unwrap();
2534 assert!(
2535 matches!(
2536 attr.datatype().unwrap(),
2537 DatatypeMessage::VarLenString { .. }
2538 ),
2539 "string attribute should have a variable-length string datatype"
2540 );
2541 assert_eq!(attr.read_string().unwrap(), "test_value");
2542 }
2543
2544 std::fs::remove_file(&path).ok();
2545 }
2546
2547 #[test]
2548 fn all_numeric_types_roundtrip() {
2549 let path = temp_path("all_types");
2550
2551 {
2552 let file = H5File::create(&path).unwrap();
2553
2554 let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
2555 ds.write_raw(&[1u8, 2]).unwrap();
2556
2557 let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
2558 ds.write_raw(&[-1i8, 1]).unwrap();
2559
2560 let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
2561 ds.write_raw(&[100u16, 200]).unwrap();
2562
2563 let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
2564 ds.write_raw(&[-100i16, 100]).unwrap();
2565
2566 let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
2567 ds.write_raw(&[1000u32, 2000]).unwrap();
2568
2569 let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
2570 ds.write_raw(&[-1000i32, 1000]).unwrap();
2571
2572 let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
2573 ds.write_raw(&[10000u64, 20000]).unwrap();
2574
2575 let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
2576 ds.write_raw(&[-10000i64, 10000]).unwrap();
2577
2578 let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
2579 ds.write_raw(&[1.5f32, 2.5]).unwrap();
2580
2581 let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
2582 ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
2583
2584 file.close().unwrap();
2585 }
2586
2587 {
2588 let file = H5File::open(&path).unwrap();
2589
2590 assert_eq!(
2591 file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
2592 vec![1u8, 2]
2593 );
2594 assert_eq!(
2595 file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2596 vec![-1i8, 1]
2597 );
2598 assert_eq!(
2599 file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2600 vec![100u16, 200]
2601 );
2602 assert_eq!(
2603 file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2604 vec![-100i16, 100]
2605 );
2606 assert_eq!(
2607 file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2608 vec![1000u32, 2000]
2609 );
2610 assert_eq!(
2611 file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2612 vec![-1000i32, 1000]
2613 );
2614 assert_eq!(
2615 file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2616 vec![10000u64, 20000]
2617 );
2618 assert_eq!(
2619 file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2620 vec![-10000i64, 10000]
2621 );
2622 assert_eq!(
2623 file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2624 vec![1.5f32, 2.5]
2625 );
2626 assert_eq!(
2627 file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2628 vec![1.23456f64, 7.89012]
2629 );
2630 }
2631
2632 std::fs::remove_file(&path).ok();
2633 }
2634
2635 #[test]
2636 fn append_chunked_roundtrip() {
2637 let path = temp_path("append_chunked");
2638
2639 {
2640 let file = H5File::create(&path).unwrap();
2641 let ds = file
2642 .new_dataset::<f64>()
2643 .shape([0, 3])
2644 .chunk(&[1, 3])
2645 .max_shape(&[None, Some(3)])
2646 .create("data")
2647 .unwrap();
2648
2649 ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2651 ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2653
2654 file.close().unwrap();
2655 }
2656
2657 {
2658 let file = H5File::open(&path).unwrap();
2659 let ds = file.dataset("data").unwrap();
2660 assert_eq!(ds.shape(), vec![3, 3]);
2661 let all = ds.read_raw::<f64>().unwrap();
2662 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2663 }
2664
2665 std::fs::remove_file(&path).ok();
2666 }
2667
2668 #[test]
2669 fn append_1d_chunked() {
2670 let path = temp_path("append_1d");
2671
2672 {
2673 let file = H5File::create(&path).unwrap();
2674 let ds = file
2675 .new_dataset::<i32>()
2676 .shape([0])
2677 .chunk(&[4])
2678 .max_shape(&[None])
2679 .create("values")
2680 .unwrap();
2681
2682 ds.append(&[10i32, 20, 30]).unwrap(); ds.append(&[40i32]).unwrap(); ds.append(&[50i32, 60, 70, 80]).unwrap(); file.close().unwrap();
2687 }
2688
2689 {
2690 let file = H5File::open(&path).unwrap();
2691 let ds = file.dataset("values").unwrap();
2692 assert_eq!(ds.shape(), vec![8]);
2693 let all = ds.read_raw::<i32>().unwrap();
2694 assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2695 }
2696
2697 std::fs::remove_file(&path).ok();
2698 }
2699
2700 #[test]
2701 fn append_partial_chunk_flushed_on_close() {
2702 let path = temp_path("append_partial_close");
2703
2704 {
2705 let file = H5File::create(&path).unwrap();
2706 let ds = file
2707 .new_dataset::<f64>()
2708 .shape([0])
2709 .chunk(&[4])
2710 .max_shape(&[None])
2711 .create("vals")
2712 .unwrap();
2713
2714 ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
2716 file.close().unwrap();
2717 }
2718
2719 {
2720 let file = H5File::open(&path).unwrap();
2721 let ds = file.dataset("vals").unwrap();
2722 assert_eq!(ds.shape(), vec![5]);
2723 let all = ds.read_raw::<f64>().unwrap();
2724 assert_eq!(all.len(), 5);
2727 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2728 }
2729
2730 std::fs::remove_file(&path).ok();
2731 }
2732
2733 #[cfg(feature = "deflate")]
2734 #[test]
2735 fn vlen_append_after_reopen_filtered() {
2736 let path = temp_path("vlen_reopen_filtered");
2740 {
2741 let file = H5File::create(&path).unwrap();
2742 file.create_appendable_vlen_dataset(
2743 "strs",
2744 4,
2745 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2746 )
2747 .unwrap();
2748 file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
2749 .unwrap();
2750 file.close().unwrap();
2751 }
2752 {
2753 let file = H5File::open_rw(&path).unwrap();
2754 file.append_vlen_strings("strs", &["delta"]).unwrap();
2755 file.close().unwrap();
2756 }
2757 {
2758 let file = H5File::open(&path).unwrap();
2759 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2760 assert_eq!(
2761 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2762 vec!["alpha", "beta", "gamma", "delta"]
2763 );
2764 }
2765 std::fs::remove_file(&path).ok();
2766 }
2767
2768 #[test]
2769 fn vlen_append_after_reopen_data_block() {
2770 let path = temp_path("vlen_reopen_datablk");
2774 let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
2775 {
2776 let file = H5File::create(&path).unwrap();
2777 file.create_appendable_vlen_dataset("strs", 2, None)
2778 .unwrap();
2779 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2780 file.append_vlen_strings("strs", &refs).unwrap();
2781 file.close().unwrap();
2782 }
2783 {
2784 let file = H5File::open_rw(&path).unwrap();
2785 file.append_vlen_strings("strs", &["s9"]).unwrap();
2786 file.close().unwrap();
2787 }
2788 {
2789 let file = H5File::open(&path).unwrap();
2790 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2791 let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
2792 assert_eq!(got, want);
2793 }
2794 std::fs::remove_file(&path).ok();
2795 }
2796
2797 #[test]
2798 fn vlen_append_after_reopen_super_block() {
2799 let path = temp_path("vlen_reopen_super");
2807 let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
2810 {
2811 let file = H5File::create(&path).unwrap();
2812 file.create_appendable_vlen_dataset("strs", 2, None)
2813 .unwrap();
2814 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2815 file.append_vlen_strings("strs", &refs).unwrap();
2816 file.close().unwrap();
2817 }
2818 {
2819 let file = H5File::open_rw(&path).unwrap();
2820 file.append_vlen_strings("strs", &["v489"]).unwrap();
2821 file.close().unwrap();
2822 }
2823 {
2824 let file = H5File::open(&path).unwrap();
2825 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2826 let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
2827 assert_eq!(got, want);
2828 }
2829 std::fs::remove_file(&path).ok();
2830 }
2831
2832 #[cfg(feature = "deflate")]
2833 #[test]
2834 fn vlen_append_after_reopen_filtered_data_block() {
2835 let path = temp_path("vlen_reopen_filt_datablk");
2838 let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
2839 {
2840 let file = H5File::create(&path).unwrap();
2841 file.create_appendable_vlen_dataset(
2842 "strs",
2843 2,
2844 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2845 )
2846 .unwrap();
2847 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2848 file.append_vlen_strings("strs", &refs).unwrap();
2849 file.close().unwrap();
2850 }
2851 {
2852 let file = H5File::open_rw(&path).unwrap();
2853 file.append_vlen_strings("strs", &["item09"]).unwrap();
2854 file.close().unwrap();
2855 }
2856 {
2857 let file = H5File::open(&path).unwrap();
2858 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2859 let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
2860 assert_eq!(got, want);
2861 }
2862 std::fs::remove_file(&path).ok();
2863 }
2864
2865 #[test]
2866 fn group_nx_class_attribute_roundtrip() {
2867 let path = temp_path("group_nx_class");
2870 {
2871 let file = H5File::create(&path).unwrap();
2872 let entry = file.create_group("entry").unwrap();
2873 entry.set_attr_string("NX_class", "NXentry").unwrap();
2874 let det = entry.create_group("detector").unwrap();
2875 det.set_attr_string("NX_class", "NXdetector").unwrap();
2876 det.set_attr_numeric("frame_count", &7i32).unwrap();
2877 det.new_dataset::<f32>()
2878 .shape([4])
2879 .create("data")
2880 .unwrap()
2881 .write_raw(&[1.0f32; 4])
2882 .unwrap();
2883 file.close().unwrap();
2884 }
2885 {
2886 let file = H5File::open(&path).unwrap();
2887 let entry = file.root_group().group("entry").unwrap();
2888 assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
2889 let det = entry.group("detector").unwrap();
2890 assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
2891 let names = det.attr_names().unwrap();
2892 assert!(names.contains(&"NX_class".to_string()));
2893 assert!(names.contains(&"frame_count".to_string()));
2894 }
2895 std::fs::remove_file(&path).ok();
2896 }
2897
2898 #[test]
2899 fn ea_super_block_roundtrip() {
2900 let path = temp_path("ea_super_rt");
2903 {
2904 let file = H5File::create(&path).unwrap();
2905 let ds = file
2906 .new_dataset::<i32>()
2907 .shape([0])
2908 .chunk(&[1])
2909 .max_shape(&[None])
2910 .create("v")
2911 .unwrap();
2912 ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
2913 file.close().unwrap();
2914 }
2915 {
2916 let file = H5File::open(&path).unwrap();
2917 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2918 assert_eq!(v.len(), 2000);
2919 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2920 }
2921 std::fs::remove_file(&path).ok();
2922 }
2923
2924 #[cfg(feature = "deflate")]
2925 #[test]
2926 fn ea_filtered_super_block_roundtrip() {
2927 let path = temp_path("ea_filt_super");
2929 {
2930 let file = H5File::create(&path).unwrap();
2931 let ds = file
2932 .new_dataset::<i32>()
2933 .shape([0])
2934 .chunk(&[1])
2935 .max_shape(&[None])
2936 .deflate(4)
2937 .create("v")
2938 .unwrap();
2939 ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
2940 file.close().unwrap();
2941 }
2942 {
2943 let file = H5File::open(&path).unwrap();
2944 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2945 assert_eq!(v, (0..600).collect::<Vec<i32>>());
2946 }
2947 std::fs::remove_file(&path).ok();
2948 }
2949
2950 #[test]
2951 fn ea_super_block_open_append() {
2952 let path = temp_path("ea_super_append");
2954 {
2955 let file = H5File::create(&path).unwrap();
2956 let ds = file
2957 .new_dataset::<i32>()
2958 .shape([0])
2959 .chunk(&[1])
2960 .max_shape(&[None])
2961 .create("v")
2962 .unwrap();
2963 ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
2964 file.close().unwrap();
2965 }
2966 {
2967 let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2968 let idx = w.dataset_index("v").unwrap();
2969 for c in 300..900u64 {
2970 w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
2971 }
2972 w.extend_dataset(idx, &[900]).unwrap();
2973 w.close().unwrap();
2974 }
2975 {
2976 let file = H5File::open(&path).unwrap();
2977 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2978 assert_eq!(v.len(), 900);
2979 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2980 }
2981 std::fs::remove_file(&path).ok();
2982 }
2983
2984 #[test]
2985 fn btree_v2_multi_unlimited_roundtrip() {
2986 let path = temp_path("bt2_multi");
2989 {
2990 let file = H5File::create(&path).unwrap();
2991 let ds = file
2992 .new_dataset::<i32>()
2993 .shape([0, 0])
2994 .chunk(&[2, 2])
2995 .max_shape(&[None, None])
2996 .create("grid")
2997 .unwrap();
2998 assert!(ds.is_chunked());
2999 for cr in 0..2usize {
3001 for cc in 0..2usize {
3002 let mut bytes = Vec::new();
3003 for i in 0..2usize {
3004 for j in 0..2usize {
3005 let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
3006 bytes.extend_from_slice(&v.to_le_bytes());
3007 }
3008 }
3009 ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
3010 }
3011 }
3012 file.close().unwrap();
3013 }
3014 {
3015 let file = H5File::open(&path).unwrap();
3016 let ds = file.dataset("grid").unwrap();
3017 assert_eq!(ds.shape(), vec![4, 4]);
3018 assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
3019 }
3020 std::fs::remove_file(&path).ok();
3021 }
3022
3023 #[test]
3024 fn subframe_chunking_roundtrip() {
3025 let path = temp_path("subframe");
3029 {
3030 let file = H5File::create(&path).unwrap();
3031 let ds = file
3032 .new_dataset::<i32>()
3033 .shape([0, 8, 8])
3034 .chunk(&[1, 4, 4])
3035 .max_shape(&[None, Some(8), Some(8)])
3036 .create("v")
3037 .unwrap();
3038 for f in 0..3usize {
3039 for cr in 0..2usize {
3040 for cc in 0..2usize {
3041 let mut bytes = Vec::new();
3042 for i in 0..4usize {
3043 for j in 0..4usize {
3044 let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
3045 bytes.extend_from_slice(&v.to_le_bytes());
3046 }
3047 }
3048 ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
3049 }
3050 }
3051 }
3052 file.close().unwrap();
3053 }
3054 {
3055 let file = H5File::open(&path).unwrap();
3056 let ds = file.dataset("v").unwrap();
3057 assert_eq!(ds.shape(), vec![3, 8, 8]);
3058 assert_eq!(
3059 ds.read_raw::<i32>().unwrap(),
3060 (0..192).collect::<Vec<i32>>()
3061 );
3062 }
3063 std::fs::remove_file(&path).ok();
3064 }
3065
3066 #[test]
3067 fn fill_value_contiguous_roundtrip() {
3068 let path = temp_path("fill_value_contig");
3069 {
3070 let file = H5File::create(&path).unwrap();
3071 let ds = file
3072 .new_dataset::<f32>()
3073 .shape([4])
3074 .fill_value(2.5f32)
3075 .create("data")
3076 .unwrap();
3077 ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
3078 file.close().unwrap();
3079 }
3080 {
3082 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3083 let idx = writer.dataset_index("data").unwrap();
3084 assert_eq!(
3085 writer.ds(idx).lock().fill_value,
3086 Some(2.5f32.to_le_bytes().to_vec())
3087 );
3088 }
3089 {
3091 let file = H5File::open(&path).unwrap();
3092 let ds = file.dataset("data").unwrap();
3093 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
3094 }
3095 std::fs::remove_file(&path).ok();
3096 }
3097
3098 #[test]
3099 fn fill_value_chunked_roundtrip() {
3100 let path = temp_path("fill_value_chunked");
3101 {
3102 let file = H5File::create(&path).unwrap();
3103 let ds = file
3104 .new_dataset::<i32>()
3105 .shape([0])
3106 .chunk(&[4])
3107 .max_shape(&[None])
3108 .fill_value(-7i32)
3109 .create("vals")
3110 .unwrap();
3111 ds.append(&[1i32, 2, 3, 4]).unwrap();
3112 file.close().unwrap();
3113 }
3114 {
3115 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3116 let idx = writer.dataset_index("vals").unwrap();
3117 assert_eq!(
3118 writer.ds(idx).lock().fill_value,
3119 Some((-7i32).to_le_bytes().to_vec())
3120 );
3121 }
3122 std::fs::remove_file(&path).ok();
3123 }
3124
3125 #[test]
3126 fn fill_value_read_missing_chunks() {
3127 fn i32_bytes(vals: &[i32]) -> Vec<u8> {
3130 vals.iter().flat_map(|v| v.to_le_bytes()).collect()
3131 }
3132 let path = temp_path("fill_value_read_missing");
3133 {
3134 let file = H5File::create(&path).unwrap();
3135 let ds = file
3136 .new_dataset::<i32>()
3137 .shape([0])
3138 .chunk(&[2])
3139 .max_shape(&[None])
3140 .fill_value(-1i32)
3141 .create("vals")
3142 .unwrap();
3143 ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
3145 ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
3146 ds.extend(&[6]).unwrap();
3147 file.close().unwrap();
3148 }
3149 {
3150 let file = H5File::open(&path).unwrap();
3151 let ds = file.dataset("vals").unwrap();
3152 let all = ds.read_raw::<i32>().unwrap();
3153 assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
3154 }
3155 std::fs::remove_file(&path).ok();
3156 }
3157
3158 #[test]
3159 fn fill_value_partial_chunk_padded_with_fill() {
3160 let path = temp_path("fill_value_partial_pad");
3164 {
3165 let file = H5File::create(&path).unwrap();
3166 let ds = file
3167 .new_dataset::<i32>()
3168 .shape([0])
3169 .chunk(&[4])
3170 .max_shape(&[None])
3171 .fill_value(-9i32)
3172 .create("vals")
3173 .unwrap();
3174 ds.append(&[1i32, 2, 3]).unwrap();
3176 file.close().unwrap();
3177 }
3178 let bytes = std::fs::read(&path).unwrap();
3179 let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
3181 let pos = bytes
3182 .windows(needle.len())
3183 .position(|w| w == needle)
3184 .expect("chunk data [1,2,3] not found in file");
3185 let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
3186 assert_eq!(
3187 pad,
3188 &(-9i32).to_le_bytes(),
3189 "partial chunk tail must be padded with fill value -9, got {:?}",
3190 pad
3191 );
3192 std::fs::remove_file(&path).ok();
3193 }
3194
3195 #[test]
3196 fn vlen_append_after_reopen_preserves_existing() {
3197 let path = temp_path("vlen_append_reopen");
3200 {
3201 let file = H5File::create(&path).unwrap();
3202 file.create_appendable_vlen_dataset("strs", 4, None)
3203 .unwrap();
3204 file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
3206 file.close().unwrap();
3207 }
3208 {
3209 let file = H5File::open_rw(&path).unwrap();
3211 file.append_vlen_strings("strs", &["d"]).unwrap();
3212 file.close().unwrap();
3213 }
3214 {
3215 let file = H5File::open(&path).unwrap();
3216 let ds = file.dataset("strs").unwrap();
3217 let got = ds.read_vlen_strings().unwrap();
3218 assert_eq!(
3219 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
3220 vec!["a", "b", "c", "d"]
3221 );
3222 }
3223 std::fs::remove_file(&path).ok();
3224 }
3225
3226 #[test]
3227 fn fill_value_size_mismatch_errors() {
3228 let path = temp_path("fill_value_mismatch");
3229 let writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
3230 let dt = <f64 as crate::types::H5Type>::hdf5_type();
3231 let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
3232 assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
3234 writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
3236 writer.close().unwrap();
3237 std::fs::remove_file(&path).ok();
3238 }
3239
3240 #[test]
3241 fn datatype_exposes_class_sign_and_byteorder() {
3242 use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
3246
3247 let path = temp_path("datatype_accessor");
3248 {
3249 let file = H5File::create(&path).unwrap();
3250 file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
3251 file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
3252 file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
3253 file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
3254 file.close().unwrap();
3255 }
3256
3257 let file = H5File::open(&path).unwrap();
3258
3259 match file.dataset("u8d").unwrap().datatype().unwrap() {
3260 DatatypeMessage::FixedPoint {
3261 size,
3262 signed,
3263 byte_order,
3264 ..
3265 } => {
3266 assert_eq!(size, 1);
3267 assert!(!signed, "u8 must be unsigned");
3268 assert_eq!(byte_order, ByteOrder::LittleEndian);
3269 }
3270 other => panic!("expected FixedPoint for u8, got {other:?}"),
3271 }
3272
3273 match file.dataset("i8d").unwrap().datatype().unwrap() {
3274 DatatypeMessage::FixedPoint { size, signed, .. } => {
3275 assert_eq!(size, 1);
3276 assert!(signed, "i8 must be signed");
3277 }
3278 other => panic!("expected FixedPoint for i8, got {other:?}"),
3279 }
3280
3281 match file.dataset("i32d").unwrap().datatype().unwrap() {
3282 DatatypeMessage::FixedPoint { size, signed, .. } => {
3283 assert_eq!(size, 4);
3284 assert!(signed, "i32 must be signed");
3285 }
3286 other => panic!("expected FixedPoint for i32, got {other:?}"),
3287 }
3288
3289 match file.dataset("f32d").unwrap().datatype().unwrap() {
3290 DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
3291 other => panic!("expected FloatingPoint for f32, got {other:?}"),
3292 }
3293
3294 std::fs::remove_file(&path).ok();
3295 }
3296
3297 #[test]
3298 fn datatype_in_write_mode_errors() {
3299 let path = temp_path("datatype_write_mode");
3300 let file = H5File::create(&path).unwrap();
3301 let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
3302 assert!(ds.datatype().is_err());
3303 std::fs::remove_file(&path).ok();
3304 }
3305
3306 #[cfg(feature = "deflate")]
3312 #[test]
3313 fn write_chunk_raw_ea_roundtrip_mask0() {
3314 use crate::format::messages::filter::{apply_filters, FilterPipeline};
3315 let path = temp_path("wcr_ea_mask0");
3316 let original: Vec<i32> = (0..12).collect();
3317 {
3318 let file = H5File::create(&path).unwrap();
3319 let ds = file
3320 .new_dataset::<i32>()
3321 .shape([0])
3322 .chunk(&[4])
3323 .max_shape(&[None])
3324 .deflate(4)
3325 .create("v")
3326 .unwrap();
3327 assert!(ds.is_chunked());
3328 let pipeline = FilterPipeline::deflate(4);
3329 for c in 0..3usize {
3330 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
3331 .iter()
3332 .flat_map(|v| v.to_le_bytes())
3333 .collect();
3334 let compressed = apply_filters(&pipeline, &raw).unwrap();
3335 ds.write_chunk_raw(c, &compressed, 0).unwrap();
3336 }
3337 ds.set_extent(&[12]).unwrap();
3338 file.close().unwrap();
3339 }
3340 {
3341 let file = H5File::open(&path).unwrap();
3342 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3343 assert_eq!(v, original);
3344 }
3345 std::fs::remove_file(&path).ok();
3346 }
3347
3348 #[cfg(feature = "deflate")]
3351 #[test]
3352 fn write_chunk_raw_fixed_array_roundtrip_mask0() {
3353 use crate::format::messages::filter::{apply_filters, FilterPipeline};
3354 let path = temp_path("wcr_fa_mask0");
3355 let original: Vec<i32> = (0..12).collect();
3356 {
3357 let file = H5File::create(&path).unwrap();
3358 let ds = file
3359 .new_dataset::<i32>()
3360 .shape([12])
3361 .chunk(&[4])
3362 .deflate(4)
3363 .create("v")
3364 .unwrap();
3365 assert!(ds.is_chunked());
3366 let pipeline = FilterPipeline::deflate(4);
3367 for c in 0..3usize {
3368 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
3369 .iter()
3370 .flat_map(|v| v.to_le_bytes())
3371 .collect();
3372 let compressed = apply_filters(&pipeline, &raw).unwrap();
3373 ds.write_chunk_raw(c, &compressed, 0).unwrap();
3374 }
3375 file.close().unwrap();
3376 }
3377 {
3378 let file = H5File::open(&path).unwrap();
3379 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3380 assert_eq!(v, original);
3381 }
3382 std::fs::remove_file(&path).ok();
3383 }
3384
3385 #[cfg(feature = "deflate")]
3391 #[test]
3392 fn write_chunk_raw_records_filter_mask() {
3393 let path = temp_path("wcr_records_mask");
3394 let raw: Vec<u8> = [10i32, 20, 30, 40]
3395 .iter()
3396 .flat_map(|v| v.to_le_bytes())
3397 .collect();
3398 assert_eq!(raw.len(), 16);
3399 {
3400 let file = H5File::create(&path).unwrap();
3401 let ds = file
3402 .new_dataset::<i32>()
3403 .shape([0])
3404 .chunk(&[4])
3405 .max_shape(&[None])
3406 .deflate(4)
3407 .create("v")
3408 .unwrap();
3409 ds.write_chunk_raw(0, &raw, 1).unwrap();
3412 ds.set_extent(&[4]).unwrap();
3413 file.close().unwrap();
3414 }
3415 {
3418 let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3419 let idx = w.dataset_index("v").unwrap();
3420 let ds = w.ds(idx);
3421 let m = ds.lock();
3422 let entry = &m
3423 .chunked
3424 .as_ref()
3425 .unwrap()
3426 .filt_iblk
3427 .as_ref()
3428 .unwrap()
3429 .elements[0];
3430 assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
3431 assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
3432 }
3433 std::fs::remove_file(&path).ok();
3434 }
3435
3436 #[cfg(feature = "deflate")]
3441 #[test]
3442 fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
3443 use crate::format::messages::filter::{apply_filters, FilterPipeline};
3444 let path = temp_path("wcr_ea_per_chunk_mask");
3445 let original: Vec<i32> = (0..8).collect();
3446 let pipeline = FilterPipeline::deflate(4);
3447 {
3448 let file = H5File::create(&path).unwrap();
3449 let ds = file
3450 .new_dataset::<i32>()
3451 .shape([0])
3452 .chunk(&[4])
3453 .max_shape(&[None])
3454 .deflate(4)
3455 .create("v")
3456 .unwrap();
3457 let raw0: Vec<u8> = original[0..4]
3458 .iter()
3459 .flat_map(|v| v.to_le_bytes())
3460 .collect();
3461 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3463 .unwrap();
3464 let raw1: Vec<u8> = original[4..8]
3465 .iter()
3466 .flat_map(|v| v.to_le_bytes())
3467 .collect();
3468 ds.write_chunk_raw(1, &raw1, 1).unwrap();
3470 ds.set_extent(&[8]).unwrap();
3471 file.close().unwrap();
3472 }
3473 {
3474 let file = H5File::open(&path).unwrap();
3475 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3476 assert_eq!(v, original);
3477 }
3478 std::fs::remove_file(&path).ok();
3479 }
3480
3481 #[cfg(feature = "deflate")]
3484 #[test]
3485 fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
3486 use crate::format::messages::filter::{apply_filters, FilterPipeline};
3487 let path = temp_path("wcr_fa_per_chunk_mask");
3488 let original: Vec<i32> = (0..8).collect();
3489 let pipeline = FilterPipeline::deflate(4);
3490 {
3491 let file = H5File::create(&path).unwrap();
3492 let ds = file
3493 .new_dataset::<i32>()
3494 .shape([8])
3495 .chunk(&[4])
3496 .deflate(4)
3497 .create("v")
3498 .unwrap();
3499 let raw0: Vec<u8> = original[0..4]
3500 .iter()
3501 .flat_map(|v| v.to_le_bytes())
3502 .collect();
3503 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3504 .unwrap();
3505 let raw1: Vec<u8> = original[4..8]
3506 .iter()
3507 .flat_map(|v| v.to_le_bytes())
3508 .collect();
3509 ds.write_chunk_raw(1, &raw1, 1).unwrap();
3510 file.close().unwrap();
3511 }
3512 {
3513 let file = H5File::open(&path).unwrap();
3514 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3515 assert_eq!(v, original);
3516 }
3517 std::fs::remove_file(&path).ok();
3518 }
3519
3520 #[test]
3523 fn write_chunk_raw_rejects_unfiltered() {
3524 let path = temp_path("wcr_unfiltered");
3525 let file = H5File::create(&path).unwrap();
3526 let ds = file
3527 .new_dataset::<i32>()
3528 .shape([0])
3529 .chunk(&[4])
3530 .max_shape(&[None])
3531 .create("v")
3532 .unwrap();
3533 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3534 assert!(
3535 err.to_string().contains("filtered dataset"),
3536 "expected a filtered-dataset error, got: {err}"
3537 );
3538 std::fs::remove_file(&path).ok();
3539 }
3540
3541 #[test]
3544 fn write_chunk_raw_rejects_btree_v2() {
3545 let path = temp_path("wcr_btree2");
3546 let file = H5File::create(&path).unwrap();
3547 let ds = file
3548 .new_dataset::<i32>()
3549 .shape([0, 0])
3550 .chunk(&[2, 2])
3551 .max_shape(&[None, None])
3552 .create("grid")
3553 .unwrap();
3554 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3555 assert!(
3556 err.to_string().contains("v2-B-tree"),
3557 "expected a v2-B-tree rejection, got: {err}"
3558 );
3559 std::fs::remove_file(&path).ok();
3560 }
3561
3562 #[cfg(feature = "deflate")]
3567 #[test]
3568 fn write_chunk_raw_rejects_oversized_chunk() {
3569 let path = temp_path("wcr_oversized");
3570 let file = H5File::create(&path).unwrap();
3571 let ds = file
3572 .new_dataset::<i32>()
3573 .shape([0])
3574 .chunk(&[1])
3575 .max_shape(&[None])
3576 .deflate(4)
3577 .create("v")
3578 .unwrap();
3579 let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
3580 assert!(
3581 err.to_string().contains("does not fit"),
3582 "expected a chunk-size-field overflow error, got: {err}"
3583 );
3584 std::fs::remove_file(&path).ok();
3585 }
3586}