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 cannot_create_dataset_in_read_mode() {
1707 let path = temp_path("no_create_read");
1708
1709 {
1710 let _file = H5File::create(&path).unwrap();
1711 }
1712
1713 {
1714 let file = H5File::open(&path).unwrap();
1715 let result = file.new_dataset::<u8>().shape([4]).create("x");
1716 assert!(result.is_err());
1717 }
1718
1719 std::fs::remove_file(&path).ok();
1720 }
1721
1722 #[test]
1723 fn shape_accessor() {
1724 let path = temp_path("shape_acc");
1725
1726 let file = H5File::create(&path).unwrap();
1727 let ds = file
1728 .new_dataset::<f32>()
1729 .shape([5, 10, 3])
1730 .create("tensor")
1731 .unwrap();
1732 assert_eq!(ds.shape(), vec![5, 10, 3]);
1733
1734 std::fs::remove_file(&path).ok();
1735 }
1736
1737 #[test]
1738 fn slice_roundtrip_2d() {
1739 let path = temp_path("slice_2d");
1740
1741 let data: Vec<i32> = (0..20).collect();
1743 {
1744 let file = H5File::create(&path).unwrap();
1745 let ds = file
1746 .new_dataset::<i32>()
1747 .shape([4, 5])
1748 .create("mat")
1749 .unwrap();
1750 ds.write_raw(&data).unwrap();
1751 file.close().unwrap();
1752 }
1753 {
1754 let file = H5File::open(&path).unwrap();
1755 let ds = file.dataset("mat").unwrap();
1756 let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
1758 assert_eq!(slice, vec![7, 8, 12, 13]);
1761 }
1762
1763 std::fs::remove_file(&path).ok();
1764 }
1765
1766 #[test]
1767 fn write_slice_2d() {
1768 let path = temp_path("write_slice_2d");
1769
1770 {
1771 let file = H5File::create(&path).unwrap();
1772 let ds = file
1773 .new_dataset::<f32>()
1774 .shape([3, 4])
1775 .create("data")
1776 .unwrap();
1777 ds.write_raw(&[0.0f32; 12]).unwrap();
1778 ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
1780 .unwrap();
1781 file.close().unwrap();
1782 }
1783 {
1784 let file = H5File::open(&path).unwrap();
1785 let ds = file.dataset("data").unwrap();
1786 let full = ds.read_raw::<f32>().unwrap();
1787 assert_eq!(
1791 full,
1792 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,]
1793 );
1794 }
1795
1796 std::fs::remove_file(&path).ok();
1797 }
1798
1799 #[test]
1800 fn write_slice_out_of_bounds_rejected() {
1801 let path = temp_path("write_slice_oob");
1802 let file = H5File::create(&path).unwrap();
1803 let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
1804 ds.write_raw(&[0i32; 4]).unwrap();
1805 assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
1807 assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
1809 std::fs::remove_file(&path).ok();
1810 }
1811
1812 #[test]
1813 fn duplicate_dataset_name_rejected() {
1814 let path = temp_path("dup_name");
1815 let file = H5File::create(&path).unwrap();
1816 let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
1817 assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
1818 std::fs::remove_file(&path).ok();
1819 }
1820
1821 #[test]
1822 fn extend_cannot_shrink() {
1823 let path = temp_path("extend_shrink");
1824 let file = H5File::create(&path).unwrap();
1825 let ds = file
1826 .new_dataset::<i32>()
1827 .shape([0])
1828 .chunk(&[2])
1829 .max_shape(&[None])
1830 .create("d")
1831 .unwrap();
1832 ds.append(&[1i32, 2, 3, 4]).unwrap();
1833 assert!(ds.extend(&[2]).is_err());
1835 assert!(ds.extend(&[6]).is_ok());
1837 std::fs::remove_file(&path).ok();
1838 }
1839
1840 #[test]
1841 fn attr_read_roundtrip() {
1842 use crate::types::VarLenUnicode;
1843 let path = temp_path("attr_read");
1844
1845 {
1846 let file = H5File::create(&path).unwrap();
1847 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1848 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1849 let a1 = ds
1850 .new_attr::<VarLenUnicode>()
1851 .shape(())
1852 .create("units")
1853 .unwrap();
1854 a1.write_string("meters").unwrap();
1855 let a2 = ds
1856 .new_attr::<VarLenUnicode>()
1857 .shape(())
1858 .create("desc")
1859 .unwrap();
1860 a2.write_string("test data").unwrap();
1861 file.close().unwrap();
1862 }
1863 {
1864 let file = H5File::open(&path).unwrap();
1865 let ds = file.dataset("data").unwrap();
1866
1867 let names = ds.attr_names().unwrap();
1868 assert!(names.contains(&"units".to_string()));
1869 assert!(names.contains(&"desc".to_string()));
1870
1871 let units = ds.attr("units").unwrap();
1872 assert_eq!(units.read_string().unwrap(), "meters");
1873
1874 let desc = ds.attr("desc").unwrap();
1875 assert_eq!(desc.read_string().unwrap(), "test data");
1876 }
1877
1878 std::fs::remove_file(&path).ok();
1879 }
1880
1881 #[test]
1882 fn type_mismatch_element_size() {
1883 let path = temp_path("type_mismatch");
1884
1885 {
1886 let file = H5File::create(&path).unwrap();
1887 let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
1888 ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
1889 file.close().unwrap();
1890 }
1891
1892 {
1893 let file = H5File::open(&path).unwrap();
1894 let ds = file.dataset("data").unwrap();
1895 let result = ds.read_raw::<u8>();
1897 assert!(result.is_err());
1898 }
1899
1900 std::fs::remove_file(&path).ok();
1901 }
1902
1903 #[test]
1904 fn dataset_survives_file_move() {
1905 let path = temp_path("ds_survives");
1906
1907 let ds = {
1908 let file = H5File::create(&path).unwrap();
1909 file.new_dataset::<u8>().shape([4]).create("x").unwrap()
1910 };
1911 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1913 std::fs::remove_file(&path).ok();
1916 }
1917
1918 #[test]
1919 fn new_attr_scalar_string() {
1920 use crate::types::VarLenUnicode;
1921
1922 let path = temp_path("attr_scalar_string");
1923 {
1924 let file = H5File::create(&path).unwrap();
1925 let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1926 ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1927
1928 let attr = ds
1929 .new_attr::<VarLenUnicode>()
1930 .shape(())
1931 .create("name")
1932 .unwrap();
1933 attr.write_scalar(&VarLenUnicode("test_value".to_string()))
1934 .unwrap();
1935
1936 file.close().unwrap();
1937 }
1938
1939 {
1941 let file = H5File::open(&path).unwrap();
1942 let ds = file.dataset("data").unwrap();
1943 assert_eq!(ds.shape(), vec![4]);
1944 let readback = ds.read_raw::<u8>().unwrap();
1945 assert_eq!(readback, vec![1u8, 2, 3, 4]);
1946 }
1947
1948 std::fs::remove_file(&path).ok();
1949 }
1950
1951 #[test]
1952 fn all_numeric_types_roundtrip() {
1953 let path = temp_path("all_types");
1954
1955 {
1956 let file = H5File::create(&path).unwrap();
1957
1958 let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
1959 ds.write_raw(&[1u8, 2]).unwrap();
1960
1961 let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
1962 ds.write_raw(&[-1i8, 1]).unwrap();
1963
1964 let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
1965 ds.write_raw(&[100u16, 200]).unwrap();
1966
1967 let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
1968 ds.write_raw(&[-100i16, 100]).unwrap();
1969
1970 let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
1971 ds.write_raw(&[1000u32, 2000]).unwrap();
1972
1973 let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
1974 ds.write_raw(&[-1000i32, 1000]).unwrap();
1975
1976 let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
1977 ds.write_raw(&[10000u64, 20000]).unwrap();
1978
1979 let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
1980 ds.write_raw(&[-10000i64, 10000]).unwrap();
1981
1982 let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
1983 ds.write_raw(&[1.5f32, 2.5]).unwrap();
1984
1985 let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
1986 ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
1987
1988 file.close().unwrap();
1989 }
1990
1991 {
1992 let file = H5File::open(&path).unwrap();
1993
1994 assert_eq!(
1995 file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
1996 vec![1u8, 2]
1997 );
1998 assert_eq!(
1999 file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2000 vec![-1i8, 1]
2001 );
2002 assert_eq!(
2003 file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2004 vec![100u16, 200]
2005 );
2006 assert_eq!(
2007 file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2008 vec![-100i16, 100]
2009 );
2010 assert_eq!(
2011 file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2012 vec![1000u32, 2000]
2013 );
2014 assert_eq!(
2015 file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2016 vec![-1000i32, 1000]
2017 );
2018 assert_eq!(
2019 file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2020 vec![10000u64, 20000]
2021 );
2022 assert_eq!(
2023 file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2024 vec![-10000i64, 10000]
2025 );
2026 assert_eq!(
2027 file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2028 vec![1.5f32, 2.5]
2029 );
2030 assert_eq!(
2031 file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2032 vec![1.23456f64, 7.89012]
2033 );
2034 }
2035
2036 std::fs::remove_file(&path).ok();
2037 }
2038
2039 #[test]
2040 fn append_chunked_roundtrip() {
2041 let path = temp_path("append_chunked");
2042
2043 {
2044 let file = H5File::create(&path).unwrap();
2045 let ds = file
2046 .new_dataset::<f64>()
2047 .shape([0, 3])
2048 .chunk(&[1, 3])
2049 .max_shape(&[None, Some(3)])
2050 .create("data")
2051 .unwrap();
2052
2053 ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2055 ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2057
2058 file.close().unwrap();
2059 }
2060
2061 {
2062 let file = H5File::open(&path).unwrap();
2063 let ds = file.dataset("data").unwrap();
2064 assert_eq!(ds.shape(), vec![3, 3]);
2065 let all = ds.read_raw::<f64>().unwrap();
2066 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2067 }
2068
2069 std::fs::remove_file(&path).ok();
2070 }
2071
2072 #[test]
2073 fn append_1d_chunked() {
2074 let path = temp_path("append_1d");
2075
2076 {
2077 let file = H5File::create(&path).unwrap();
2078 let ds = file
2079 .new_dataset::<i32>()
2080 .shape([0])
2081 .chunk(&[4])
2082 .max_shape(&[None])
2083 .create("values")
2084 .unwrap();
2085
2086 ds.append(&[10i32, 20, 30]).unwrap(); ds.append(&[40i32]).unwrap(); ds.append(&[50i32, 60, 70, 80]).unwrap(); file.close().unwrap();
2091 }
2092
2093 {
2094 let file = H5File::open(&path).unwrap();
2095 let ds = file.dataset("values").unwrap();
2096 assert_eq!(ds.shape(), vec![8]);
2097 let all = ds.read_raw::<i32>().unwrap();
2098 assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2099 }
2100
2101 std::fs::remove_file(&path).ok();
2102 }
2103
2104 #[test]
2105 fn append_partial_chunk_flushed_on_close() {
2106 let path = temp_path("append_partial_close");
2107
2108 {
2109 let file = H5File::create(&path).unwrap();
2110 let ds = file
2111 .new_dataset::<f64>()
2112 .shape([0])
2113 .chunk(&[4])
2114 .max_shape(&[None])
2115 .create("vals")
2116 .unwrap();
2117
2118 ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
2120 file.close().unwrap();
2121 }
2122
2123 {
2124 let file = H5File::open(&path).unwrap();
2125 let ds = file.dataset("vals").unwrap();
2126 assert_eq!(ds.shape(), vec![5]);
2127 let all = ds.read_raw::<f64>().unwrap();
2128 assert_eq!(all.len(), 5);
2131 assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2132 }
2133
2134 std::fs::remove_file(&path).ok();
2135 }
2136
2137 #[cfg(feature = "deflate")]
2138 #[test]
2139 fn vlen_append_after_reopen_filtered() {
2140 let path = temp_path("vlen_reopen_filtered");
2144 {
2145 let file = H5File::create(&path).unwrap();
2146 file.create_appendable_vlen_dataset(
2147 "strs",
2148 4,
2149 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2150 )
2151 .unwrap();
2152 file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
2153 .unwrap();
2154 file.close().unwrap();
2155 }
2156 {
2157 let file = H5File::open_rw(&path).unwrap();
2158 file.append_vlen_strings("strs", &["delta"]).unwrap();
2159 file.close().unwrap();
2160 }
2161 {
2162 let file = H5File::open(&path).unwrap();
2163 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2164 assert_eq!(
2165 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2166 vec!["alpha", "beta", "gamma", "delta"]
2167 );
2168 }
2169 std::fs::remove_file(&path).ok();
2170 }
2171
2172 #[test]
2173 fn vlen_append_after_reopen_data_block() {
2174 let path = temp_path("vlen_reopen_datablk");
2178 let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
2179 {
2180 let file = H5File::create(&path).unwrap();
2181 file.create_appendable_vlen_dataset("strs", 2, None)
2182 .unwrap();
2183 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2184 file.append_vlen_strings("strs", &refs).unwrap();
2185 file.close().unwrap();
2186 }
2187 {
2188 let file = H5File::open_rw(&path).unwrap();
2189 file.append_vlen_strings("strs", &["s9"]).unwrap();
2190 file.close().unwrap();
2191 }
2192 {
2193 let file = H5File::open(&path).unwrap();
2194 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2195 let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
2196 assert_eq!(got, want);
2197 }
2198 std::fs::remove_file(&path).ok();
2199 }
2200
2201 #[test]
2202 fn vlen_append_after_reopen_super_block() {
2203 let path = temp_path("vlen_reopen_super");
2211 let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
2214 {
2215 let file = H5File::create(&path).unwrap();
2216 file.create_appendable_vlen_dataset("strs", 2, None)
2217 .unwrap();
2218 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2219 file.append_vlen_strings("strs", &refs).unwrap();
2220 file.close().unwrap();
2221 }
2222 {
2223 let file = H5File::open_rw(&path).unwrap();
2224 file.append_vlen_strings("strs", &["v489"]).unwrap();
2225 file.close().unwrap();
2226 }
2227 {
2228 let file = H5File::open(&path).unwrap();
2229 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2230 let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
2231 assert_eq!(got, want);
2232 }
2233 std::fs::remove_file(&path).ok();
2234 }
2235
2236 #[cfg(feature = "deflate")]
2237 #[test]
2238 fn vlen_append_after_reopen_filtered_data_block() {
2239 let path = temp_path("vlen_reopen_filt_datablk");
2242 let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
2243 {
2244 let file = H5File::create(&path).unwrap();
2245 file.create_appendable_vlen_dataset(
2246 "strs",
2247 2,
2248 Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2249 )
2250 .unwrap();
2251 let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2252 file.append_vlen_strings("strs", &refs).unwrap();
2253 file.close().unwrap();
2254 }
2255 {
2256 let file = H5File::open_rw(&path).unwrap();
2257 file.append_vlen_strings("strs", &["item09"]).unwrap();
2258 file.close().unwrap();
2259 }
2260 {
2261 let file = H5File::open(&path).unwrap();
2262 let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2263 let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
2264 assert_eq!(got, want);
2265 }
2266 std::fs::remove_file(&path).ok();
2267 }
2268
2269 #[test]
2270 fn group_nx_class_attribute_roundtrip() {
2271 let path = temp_path("group_nx_class");
2274 {
2275 let file = H5File::create(&path).unwrap();
2276 let entry = file.create_group("entry").unwrap();
2277 entry.set_attr_string("NX_class", "NXentry").unwrap();
2278 let det = entry.create_group("detector").unwrap();
2279 det.set_attr_string("NX_class", "NXdetector").unwrap();
2280 det.set_attr_numeric("frame_count", &7i32).unwrap();
2281 det.new_dataset::<f32>()
2282 .shape([4])
2283 .create("data")
2284 .unwrap()
2285 .write_raw(&[1.0f32; 4])
2286 .unwrap();
2287 file.close().unwrap();
2288 }
2289 {
2290 let file = H5File::open(&path).unwrap();
2291 let entry = file.root_group().group("entry").unwrap();
2292 assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
2293 let det = entry.group("detector").unwrap();
2294 assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
2295 let names = det.attr_names().unwrap();
2296 assert!(names.contains(&"NX_class".to_string()));
2297 assert!(names.contains(&"frame_count".to_string()));
2298 }
2299 std::fs::remove_file(&path).ok();
2300 }
2301
2302 #[test]
2303 fn ea_super_block_roundtrip() {
2304 let path = temp_path("ea_super_rt");
2307 {
2308 let file = H5File::create(&path).unwrap();
2309 let ds = file
2310 .new_dataset::<i32>()
2311 .shape([0])
2312 .chunk(&[1])
2313 .max_shape(&[None])
2314 .create("v")
2315 .unwrap();
2316 ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
2317 file.close().unwrap();
2318 }
2319 {
2320 let file = H5File::open(&path).unwrap();
2321 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2322 assert_eq!(v.len(), 2000);
2323 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2324 }
2325 std::fs::remove_file(&path).ok();
2326 }
2327
2328 #[cfg(feature = "deflate")]
2329 #[test]
2330 fn ea_filtered_super_block_roundtrip() {
2331 let path = temp_path("ea_filt_super");
2333 {
2334 let file = H5File::create(&path).unwrap();
2335 let ds = file
2336 .new_dataset::<i32>()
2337 .shape([0])
2338 .chunk(&[1])
2339 .max_shape(&[None])
2340 .deflate(4)
2341 .create("v")
2342 .unwrap();
2343 ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
2344 file.close().unwrap();
2345 }
2346 {
2347 let file = H5File::open(&path).unwrap();
2348 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2349 assert_eq!(v, (0..600).collect::<Vec<i32>>());
2350 }
2351 std::fs::remove_file(&path).ok();
2352 }
2353
2354 #[test]
2355 fn ea_super_block_open_append() {
2356 let path = temp_path("ea_super_append");
2358 {
2359 let file = H5File::create(&path).unwrap();
2360 let ds = file
2361 .new_dataset::<i32>()
2362 .shape([0])
2363 .chunk(&[1])
2364 .max_shape(&[None])
2365 .create("v")
2366 .unwrap();
2367 ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
2368 file.close().unwrap();
2369 }
2370 {
2371 let mut w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2372 let idx = w.dataset_index("v").unwrap();
2373 for c in 300..900u64 {
2374 w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
2375 }
2376 w.extend_dataset(idx, &[900]).unwrap();
2377 w.close().unwrap();
2378 }
2379 {
2380 let file = H5File::open(&path).unwrap();
2381 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2382 assert_eq!(v.len(), 900);
2383 assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2384 }
2385 std::fs::remove_file(&path).ok();
2386 }
2387
2388 #[test]
2389 fn btree_v2_multi_unlimited_roundtrip() {
2390 let path = temp_path("bt2_multi");
2393 {
2394 let file = H5File::create(&path).unwrap();
2395 let ds = file
2396 .new_dataset::<i32>()
2397 .shape([0, 0])
2398 .chunk(&[2, 2])
2399 .max_shape(&[None, None])
2400 .create("grid")
2401 .unwrap();
2402 assert!(ds.is_chunked());
2403 for cr in 0..2usize {
2405 for cc in 0..2usize {
2406 let mut bytes = Vec::new();
2407 for i in 0..2usize {
2408 for j in 0..2usize {
2409 let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
2410 bytes.extend_from_slice(&v.to_le_bytes());
2411 }
2412 }
2413 ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
2414 }
2415 }
2416 file.close().unwrap();
2417 }
2418 {
2419 let file = H5File::open(&path).unwrap();
2420 let ds = file.dataset("grid").unwrap();
2421 assert_eq!(ds.shape(), vec![4, 4]);
2422 assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
2423 }
2424 std::fs::remove_file(&path).ok();
2425 }
2426
2427 #[test]
2428 fn subframe_chunking_roundtrip() {
2429 let path = temp_path("subframe");
2433 {
2434 let file = H5File::create(&path).unwrap();
2435 let ds = file
2436 .new_dataset::<i32>()
2437 .shape([0, 8, 8])
2438 .chunk(&[1, 4, 4])
2439 .max_shape(&[None, Some(8), Some(8)])
2440 .create("v")
2441 .unwrap();
2442 for f in 0..3usize {
2443 for cr in 0..2usize {
2444 for cc in 0..2usize {
2445 let mut bytes = Vec::new();
2446 for i in 0..4usize {
2447 for j in 0..4usize {
2448 let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
2449 bytes.extend_from_slice(&v.to_le_bytes());
2450 }
2451 }
2452 ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
2453 }
2454 }
2455 }
2456 file.close().unwrap();
2457 }
2458 {
2459 let file = H5File::open(&path).unwrap();
2460 let ds = file.dataset("v").unwrap();
2461 assert_eq!(ds.shape(), vec![3, 8, 8]);
2462 assert_eq!(
2463 ds.read_raw::<i32>().unwrap(),
2464 (0..192).collect::<Vec<i32>>()
2465 );
2466 }
2467 std::fs::remove_file(&path).ok();
2468 }
2469
2470 #[test]
2471 fn fill_value_contiguous_roundtrip() {
2472 let path = temp_path("fill_value_contig");
2473 {
2474 let file = H5File::create(&path).unwrap();
2475 let ds = file
2476 .new_dataset::<f32>()
2477 .shape([4])
2478 .fill_value(2.5f32)
2479 .create("data")
2480 .unwrap();
2481 ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
2482 file.close().unwrap();
2483 }
2484 {
2486 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2487 let idx = writer.dataset_index("data").unwrap();
2488 assert_eq!(
2489 writer.datasets[idx].fill_value,
2490 Some(2.5f32.to_le_bytes().to_vec())
2491 );
2492 }
2493 {
2495 let file = H5File::open(&path).unwrap();
2496 let ds = file.dataset("data").unwrap();
2497 assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
2498 }
2499 std::fs::remove_file(&path).ok();
2500 }
2501
2502 #[test]
2503 fn fill_value_chunked_roundtrip() {
2504 let path = temp_path("fill_value_chunked");
2505 {
2506 let file = H5File::create(&path).unwrap();
2507 let ds = file
2508 .new_dataset::<i32>()
2509 .shape([0])
2510 .chunk(&[4])
2511 .max_shape(&[None])
2512 .fill_value(-7i32)
2513 .create("vals")
2514 .unwrap();
2515 ds.append(&[1i32, 2, 3, 4]).unwrap();
2516 file.close().unwrap();
2517 }
2518 {
2519 let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2520 let idx = writer.dataset_index("vals").unwrap();
2521 assert_eq!(
2522 writer.datasets[idx].fill_value,
2523 Some((-7i32).to_le_bytes().to_vec())
2524 );
2525 }
2526 std::fs::remove_file(&path).ok();
2527 }
2528
2529 #[test]
2530 fn fill_value_read_missing_chunks() {
2531 fn i32_bytes(vals: &[i32]) -> Vec<u8> {
2534 vals.iter().flat_map(|v| v.to_le_bytes()).collect()
2535 }
2536 let path = temp_path("fill_value_read_missing");
2537 {
2538 let file = H5File::create(&path).unwrap();
2539 let ds = file
2540 .new_dataset::<i32>()
2541 .shape([0])
2542 .chunk(&[2])
2543 .max_shape(&[None])
2544 .fill_value(-1i32)
2545 .create("vals")
2546 .unwrap();
2547 ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
2549 ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
2550 ds.extend(&[6]).unwrap();
2551 file.close().unwrap();
2552 }
2553 {
2554 let file = H5File::open(&path).unwrap();
2555 let ds = file.dataset("vals").unwrap();
2556 let all = ds.read_raw::<i32>().unwrap();
2557 assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
2558 }
2559 std::fs::remove_file(&path).ok();
2560 }
2561
2562 #[test]
2563 fn fill_value_partial_chunk_padded_with_fill() {
2564 let path = temp_path("fill_value_partial_pad");
2568 {
2569 let file = H5File::create(&path).unwrap();
2570 let ds = file
2571 .new_dataset::<i32>()
2572 .shape([0])
2573 .chunk(&[4])
2574 .max_shape(&[None])
2575 .fill_value(-9i32)
2576 .create("vals")
2577 .unwrap();
2578 ds.append(&[1i32, 2, 3]).unwrap();
2580 file.close().unwrap();
2581 }
2582 let bytes = std::fs::read(&path).unwrap();
2583 let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2585 let pos = bytes
2586 .windows(needle.len())
2587 .position(|w| w == needle)
2588 .expect("chunk data [1,2,3] not found in file");
2589 let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
2590 assert_eq!(
2591 pad,
2592 &(-9i32).to_le_bytes(),
2593 "partial chunk tail must be padded with fill value -9, got {:?}",
2594 pad
2595 );
2596 std::fs::remove_file(&path).ok();
2597 }
2598
2599 #[test]
2600 fn vlen_append_after_reopen_preserves_existing() {
2601 let path = temp_path("vlen_append_reopen");
2604 {
2605 let file = H5File::create(&path).unwrap();
2606 file.create_appendable_vlen_dataset("strs", 4, None)
2607 .unwrap();
2608 file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
2610 file.close().unwrap();
2611 }
2612 {
2613 let file = H5File::open_rw(&path).unwrap();
2615 file.append_vlen_strings("strs", &["d"]).unwrap();
2616 file.close().unwrap();
2617 }
2618 {
2619 let file = H5File::open(&path).unwrap();
2620 let ds = file.dataset("strs").unwrap();
2621 let got = ds.read_vlen_strings().unwrap();
2622 assert_eq!(
2623 got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2624 vec!["a", "b", "c", "d"]
2625 );
2626 }
2627 std::fs::remove_file(&path).ok();
2628 }
2629
2630 #[test]
2631 fn fill_value_size_mismatch_errors() {
2632 let path = temp_path("fill_value_mismatch");
2633 let mut writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
2634 let dt = <f64 as crate::types::H5Type>::hdf5_type();
2635 let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
2636 assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
2638 writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
2640 writer.close().unwrap();
2641 std::fs::remove_file(&path).ok();
2642 }
2643
2644 #[test]
2645 fn datatype_exposes_class_sign_and_byteorder() {
2646 use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
2650
2651 let path = temp_path("datatype_accessor");
2652 {
2653 let file = H5File::create(&path).unwrap();
2654 file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
2655 file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
2656 file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
2657 file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
2658 file.close().unwrap();
2659 }
2660
2661 let file = H5File::open(&path).unwrap();
2662
2663 match file.dataset("u8d").unwrap().datatype().unwrap() {
2664 DatatypeMessage::FixedPoint {
2665 size,
2666 signed,
2667 byte_order,
2668 ..
2669 } => {
2670 assert_eq!(size, 1);
2671 assert!(!signed, "u8 must be unsigned");
2672 assert_eq!(byte_order, ByteOrder::LittleEndian);
2673 }
2674 other => panic!("expected FixedPoint for u8, got {other:?}"),
2675 }
2676
2677 match file.dataset("i8d").unwrap().datatype().unwrap() {
2678 DatatypeMessage::FixedPoint { size, signed, .. } => {
2679 assert_eq!(size, 1);
2680 assert!(signed, "i8 must be signed");
2681 }
2682 other => panic!("expected FixedPoint for i8, got {other:?}"),
2683 }
2684
2685 match file.dataset("i32d").unwrap().datatype().unwrap() {
2686 DatatypeMessage::FixedPoint { size, signed, .. } => {
2687 assert_eq!(size, 4);
2688 assert!(signed, "i32 must be signed");
2689 }
2690 other => panic!("expected FixedPoint for i32, got {other:?}"),
2691 }
2692
2693 match file.dataset("f32d").unwrap().datatype().unwrap() {
2694 DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
2695 other => panic!("expected FloatingPoint for f32, got {other:?}"),
2696 }
2697
2698 std::fs::remove_file(&path).ok();
2699 }
2700
2701 #[test]
2702 fn datatype_in_write_mode_errors() {
2703 let path = temp_path("datatype_write_mode");
2704 let file = H5File::create(&path).unwrap();
2705 let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
2706 assert!(ds.datatype().is_err());
2707 std::fs::remove_file(&path).ok();
2708 }
2709
2710 #[cfg(feature = "deflate")]
2716 #[test]
2717 fn write_chunk_raw_ea_roundtrip_mask0() {
2718 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2719 let path = temp_path("wcr_ea_mask0");
2720 let original: Vec<i32> = (0..12).collect();
2721 {
2722 let file = H5File::create(&path).unwrap();
2723 let ds = file
2724 .new_dataset::<i32>()
2725 .shape([0])
2726 .chunk(&[4])
2727 .max_shape(&[None])
2728 .deflate(4)
2729 .create("v")
2730 .unwrap();
2731 assert!(ds.is_chunked());
2732 let pipeline = FilterPipeline::deflate(4);
2733 for c in 0..3usize {
2734 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2735 .iter()
2736 .flat_map(|v| v.to_le_bytes())
2737 .collect();
2738 let compressed = apply_filters(&pipeline, &raw).unwrap();
2739 ds.write_chunk_raw(c, &compressed, 0).unwrap();
2740 }
2741 ds.set_extent(&[12]).unwrap();
2742 file.close().unwrap();
2743 }
2744 {
2745 let file = H5File::open(&path).unwrap();
2746 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2747 assert_eq!(v, original);
2748 }
2749 std::fs::remove_file(&path).ok();
2750 }
2751
2752 #[cfg(feature = "deflate")]
2755 #[test]
2756 fn write_chunk_raw_fixed_array_roundtrip_mask0() {
2757 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2758 let path = temp_path("wcr_fa_mask0");
2759 let original: Vec<i32> = (0..12).collect();
2760 {
2761 let file = H5File::create(&path).unwrap();
2762 let ds = file
2763 .new_dataset::<i32>()
2764 .shape([12])
2765 .chunk(&[4])
2766 .deflate(4)
2767 .create("v")
2768 .unwrap();
2769 assert!(ds.is_chunked());
2770 let pipeline = FilterPipeline::deflate(4);
2771 for c in 0..3usize {
2772 let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2773 .iter()
2774 .flat_map(|v| v.to_le_bytes())
2775 .collect();
2776 let compressed = apply_filters(&pipeline, &raw).unwrap();
2777 ds.write_chunk_raw(c, &compressed, 0).unwrap();
2778 }
2779 file.close().unwrap();
2780 }
2781 {
2782 let file = H5File::open(&path).unwrap();
2783 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2784 assert_eq!(v, original);
2785 }
2786 std::fs::remove_file(&path).ok();
2787 }
2788
2789 #[cfg(feature = "deflate")]
2795 #[test]
2796 fn write_chunk_raw_records_filter_mask() {
2797 let path = temp_path("wcr_records_mask");
2798 let raw: Vec<u8> = [10i32, 20, 30, 40]
2799 .iter()
2800 .flat_map(|v| v.to_le_bytes())
2801 .collect();
2802 assert_eq!(raw.len(), 16);
2803 {
2804 let file = H5File::create(&path).unwrap();
2805 let ds = file
2806 .new_dataset::<i32>()
2807 .shape([0])
2808 .chunk(&[4])
2809 .max_shape(&[None])
2810 .deflate(4)
2811 .create("v")
2812 .unwrap();
2813 ds.write_chunk_raw(0, &raw, 1).unwrap();
2816 ds.set_extent(&[4]).unwrap();
2817 file.close().unwrap();
2818 }
2819 {
2822 let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2823 let idx = w.dataset_index("v").unwrap();
2824 let entry = &w.datasets[idx]
2825 .chunked
2826 .as_ref()
2827 .unwrap()
2828 .filt_iblk
2829 .as_ref()
2830 .unwrap()
2831 .elements[0];
2832 assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
2833 assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
2834 }
2835 std::fs::remove_file(&path).ok();
2836 }
2837
2838 #[cfg(feature = "deflate")]
2843 #[test]
2844 fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
2845 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2846 let path = temp_path("wcr_ea_per_chunk_mask");
2847 let original: Vec<i32> = (0..8).collect();
2848 let pipeline = FilterPipeline::deflate(4);
2849 {
2850 let file = H5File::create(&path).unwrap();
2851 let ds = file
2852 .new_dataset::<i32>()
2853 .shape([0])
2854 .chunk(&[4])
2855 .max_shape(&[None])
2856 .deflate(4)
2857 .create("v")
2858 .unwrap();
2859 let raw0: Vec<u8> = original[0..4]
2860 .iter()
2861 .flat_map(|v| v.to_le_bytes())
2862 .collect();
2863 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
2865 .unwrap();
2866 let raw1: Vec<u8> = original[4..8]
2867 .iter()
2868 .flat_map(|v| v.to_le_bytes())
2869 .collect();
2870 ds.write_chunk_raw(1, &raw1, 1).unwrap();
2872 ds.set_extent(&[8]).unwrap();
2873 file.close().unwrap();
2874 }
2875 {
2876 let file = H5File::open(&path).unwrap();
2877 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2878 assert_eq!(v, original);
2879 }
2880 std::fs::remove_file(&path).ok();
2881 }
2882
2883 #[cfg(feature = "deflate")]
2886 #[test]
2887 fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
2888 use crate::format::messages::filter::{apply_filters, FilterPipeline};
2889 let path = temp_path("wcr_fa_per_chunk_mask");
2890 let original: Vec<i32> = (0..8).collect();
2891 let pipeline = FilterPipeline::deflate(4);
2892 {
2893 let file = H5File::create(&path).unwrap();
2894 let ds = file
2895 .new_dataset::<i32>()
2896 .shape([8])
2897 .chunk(&[4])
2898 .deflate(4)
2899 .create("v")
2900 .unwrap();
2901 let raw0: Vec<u8> = original[0..4]
2902 .iter()
2903 .flat_map(|v| v.to_le_bytes())
2904 .collect();
2905 ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
2906 .unwrap();
2907 let raw1: Vec<u8> = original[4..8]
2908 .iter()
2909 .flat_map(|v| v.to_le_bytes())
2910 .collect();
2911 ds.write_chunk_raw(1, &raw1, 1).unwrap();
2912 file.close().unwrap();
2913 }
2914 {
2915 let file = H5File::open(&path).unwrap();
2916 let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2917 assert_eq!(v, original);
2918 }
2919 std::fs::remove_file(&path).ok();
2920 }
2921
2922 #[test]
2925 fn write_chunk_raw_rejects_unfiltered() {
2926 let path = temp_path("wcr_unfiltered");
2927 let file = H5File::create(&path).unwrap();
2928 let ds = file
2929 .new_dataset::<i32>()
2930 .shape([0])
2931 .chunk(&[4])
2932 .max_shape(&[None])
2933 .create("v")
2934 .unwrap();
2935 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
2936 assert!(
2937 err.to_string().contains("filtered dataset"),
2938 "expected a filtered-dataset error, got: {err}"
2939 );
2940 std::fs::remove_file(&path).ok();
2941 }
2942
2943 #[test]
2946 fn write_chunk_raw_rejects_btree_v2() {
2947 let path = temp_path("wcr_btree2");
2948 let file = H5File::create(&path).unwrap();
2949 let ds = file
2950 .new_dataset::<i32>()
2951 .shape([0, 0])
2952 .chunk(&[2, 2])
2953 .max_shape(&[None, None])
2954 .create("grid")
2955 .unwrap();
2956 let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
2957 assert!(
2958 err.to_string().contains("v2-B-tree"),
2959 "expected a v2-B-tree rejection, got: {err}"
2960 );
2961 std::fs::remove_file(&path).ok();
2962 }
2963
2964 #[cfg(feature = "deflate")]
2969 #[test]
2970 fn write_chunk_raw_rejects_oversized_chunk() {
2971 let path = temp_path("wcr_oversized");
2972 let file = H5File::create(&path).unwrap();
2973 let ds = file
2974 .new_dataset::<i32>()
2975 .shape([0])
2976 .chunk(&[1])
2977 .max_shape(&[None])
2978 .deflate(4)
2979 .create("v")
2980 .unwrap();
2981 let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
2982 assert!(
2983 err.to_string().contains("does not fit"),
2984 "expected a chunk-size-field overflow error, got: {err}"
2985 );
2986 std::fs::remove_file(&path).ok();
2987 }
2988}