1#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6#[cfg(not(feature = "std"))]
7use alloc::{vec, vec::Vec};
8
9use crate::checksum::jenkins_lookup3;
10use crate::chunk_cache::{CACHE_LINE_SIZE, align_to_cache_line};
11use crate::error::FormatError;
12use crate::filter_pipeline::{
13 FilterDescription, FilterPipeline, FILTER_DEFLATE, FILTER_FLETCHER32, FILTER_SHUFFLE,
14};
15use crate::filters::compress_chunk;
16
17#[inline]
22pub fn align_chunk_offset(offset: u64) -> u64 {
23 let align = CACHE_LINE_SIZE as u64;
24 (offset + align - 1) & !(align - 1)
25}
26
27#[derive(Debug, Clone, Default)]
29pub struct ChunkOptions {
30 pub chunk_dims: Option<Vec<u64>>,
32 pub deflate_level: Option<u32>,
34 pub shuffle: bool,
36 pub fletcher32: bool,
38}
39
40impl ChunkOptions {
41 pub fn is_chunked(&self) -> bool {
43 self.chunk_dims.is_some()
44 || self.deflate_level.is_some()
45 || self.shuffle
46 || self.fletcher32
47 }
48
49 pub fn build_pipeline(&self, element_size: u32) -> Option<FilterPipeline> {
51 let mut filters = Vec::new();
52
53 if self.shuffle {
54 filters.push(FilterDescription {
55 filter_id: FILTER_SHUFFLE,
56 name: None,
57 flags: 0,
58 client_data: vec![element_size],
59 });
60 }
61
62 if let Some(level) = self.deflate_level {
63 filters.push(FilterDescription {
64 filter_id: FILTER_DEFLATE,
65 name: None,
66 flags: 0,
67 client_data: vec![level],
68 });
69 }
70
71 if self.fletcher32 {
72 filters.push(FilterDescription {
73 filter_id: FILTER_FLETCHER32,
74 name: None,
75 flags: 0,
76 client_data: vec![],
77 });
78 }
79
80 if filters.is_empty() {
84 None
85 } else {
86 Some(FilterPipeline {
87 version: 2,
88 filters,
89 })
90 }
91 }
92
93 pub fn resolve_chunk_dims(&self, shape: &[u64]) -> Vec<u64> {
95 if let Some(ref dims) = self.chunk_dims {
96 dims.clone()
97 } else {
98 shape.to_vec()
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct WrittenChunk {
107 pub address: u64,
109 pub compressed_size: u64,
111 pub raw_size: u64,
113 pub filter_mask: u32,
115}
116
117pub struct ChunkedDataResult {
119 pub data_bytes: Vec<u8>,
121 pub layout_message: Vec<u8>,
123 pub pipeline_message: Option<Vec<u8>>,
125}
126
127pub fn split_into_chunks(
130 raw_data: &[u8],
131 shape: &[u64],
132 chunk_dims: &[u64],
133 element_size: usize,
134) -> Vec<(Vec<u64>, Vec<u8>)> {
135 let rank = shape.len();
136 if rank == 0 {
137 return vec![(vec![], raw_data.to_vec())];
138 }
139
140 let mut num_chunks_per_dim = Vec::with_capacity(rank);
142 for d in 0..rank {
143 num_chunks_per_dim.push(shape[d].div_ceil(chunk_dims[d]));
144 }
145 let total_chunks: u64 = num_chunks_per_dim.iter().product();
146
147 let mut ds_strides = vec![1usize; rank];
149 for i in (0..rank.saturating_sub(1)).rev() {
150 ds_strides[i] = ds_strides[i + 1] * shape[i + 1] as usize;
151 }
152
153 let mut chunk_strides = vec![1usize; rank];
155 for i in (0..rank.saturating_sub(1)).rev() {
156 chunk_strides[i] = chunk_strides[i + 1] * chunk_dims[i + 1] as usize;
157 }
158
159 let chunk_total_elements: usize = chunk_dims.iter().map(|&d| d as usize).product();
160
161 let mut result = Vec::with_capacity(total_chunks as usize);
162
163 for linear_idx in 0..total_chunks {
164 let mut chunk_grid_coords = vec![0u64; rank];
166 let mut remaining = linear_idx;
167 for d in (0..rank).rev() {
168 chunk_grid_coords[d] = remaining % num_chunks_per_dim[d];
169 remaining /= num_chunks_per_dim[d];
170 }
171
172 let offsets: Vec<u64> = (0..rank)
174 .map(|d| chunk_grid_coords[d] * chunk_dims[d])
175 .collect();
176
177 let mut chunk_bytes = vec![0u8; chunk_total_elements * element_size];
179
180 for flat_idx in 0..chunk_total_elements {
181 let mut remaining_idx = flat_idx;
182 let mut ds_flat = 0usize;
183 let mut out_of_bounds = false;
184
185 for d in 0..rank {
186 let coord_in_chunk = remaining_idx / chunk_strides[d];
187 remaining_idx %= chunk_strides[d];
188
189 let global_coord = offsets[d] as usize + coord_in_chunk;
190 if global_coord >= shape[d] as usize {
191 out_of_bounds = true;
192 break;
193 }
194 ds_flat += global_coord * ds_strides[d];
195 }
196
197 if out_of_bounds {
198 continue;
200 }
201
202 let src_start = ds_flat * element_size;
203 let dst_start = flat_idx * element_size;
204
205 if src_start + element_size <= raw_data.len() {
206 chunk_bytes[dst_start..dst_start + element_size]
207 .copy_from_slice(&raw_data[src_start..src_start + element_size]);
208 }
209 }
210
211 result.push((offsets, chunk_bytes));
212 }
213
214 result
215}
216
217pub fn serialize_v4_single_chunk_pub(
221 chunk_dims: &[u32],
222 chunk_address: u64,
223 filtered_size: Option<u64>,
224 filter_mask: Option<u32>,
225 offset_size: u8,
226 element_size: u32,
227) -> Vec<u8> {
228 serialize_v4_single_chunk(
229 chunk_dims, chunk_address, filtered_size, filter_mask, offset_size, element_size,
230 )
231}
232
233fn serialize_v4_single_chunk(
235 chunk_dims: &[u32],
236 chunk_address: u64,
237 filtered_size: Option<u64>,
238 filter_mask: Option<u32>,
239 offset_size: u8,
240 element_size: u32,
241) -> Vec<u8> {
242 let mut buf = Vec::new();
243 buf.push(4); buf.push(2); let flags: u8 = if filtered_size.is_some() { 0x02 } else { 0x00 };
248 buf.push(flags);
249
250 let ndims = chunk_dims.len() as u8 + 1;
252 buf.push(ndims);
253
254 let max_dim = chunk_dims
257 .iter()
258 .map(|&d| d as u64)
259 .chain(core::iter::once(element_size as u64))
260 .max()
261 .unwrap_or(1);
262 let dim_encoded_len: u8 = if max_dim <= 0xFF {
263 1
264 } else if max_dim <= 0xFFFF {
265 2
266 } else {
267 4
268 };
269 buf.push(dim_encoded_len);
270
271 for &d in chunk_dims {
273 match dim_encoded_len {
274 1 => buf.push(d as u8),
275 2 => buf.extend_from_slice(&(d as u16).to_le_bytes()),
276 4 => buf.extend_from_slice(&d.to_le_bytes()),
277 _ => {}
278 }
279 }
280 match dim_encoded_len {
282 1 => buf.push(element_size as u8),
283 2 => buf.extend_from_slice(&(element_size as u16).to_le_bytes()),
284 4 => buf.extend_from_slice(&element_size.to_le_bytes()),
285 _ => {}
286 }
287
288 buf.push(1);
290
291 if let (Some(fs), Some(fm)) = (filtered_size, filter_mask) {
293 buf.extend_from_slice(&fs.to_le_bytes()); buf.extend_from_slice(&fm.to_le_bytes()); }
297
298 match offset_size {
300 4 => buf.extend_from_slice(&(chunk_address as u32).to_le_bytes()),
301 8 => buf.extend_from_slice(&chunk_address.to_le_bytes()),
302 _ => {}
303 }
304
305 buf
306}
307
308fn serialize_v4_fixed_array(
310 chunk_dims: &[u32],
311 fixed_array_address: u64,
312 offset_size: u8,
313 element_size: u32,
314 max_bits: u8,
315) -> Vec<u8> {
316 let mut buf = Vec::new();
317 buf.push(4); buf.push(2); let flags: u8 = 0x00;
321 buf.push(flags);
322
323 let ndims = chunk_dims.len() as u8 + 1;
324 buf.push(ndims);
325
326 let max_dim = chunk_dims
327 .iter()
328 .map(|&d| d as u64)
329 .chain(core::iter::once(element_size as u64))
330 .max()
331 .unwrap_or(1);
332 let dim_encoded_len: u8 = if max_dim <= 0xFF {
333 1
334 } else if max_dim <= 0xFFFF {
335 2
336 } else {
337 4
338 };
339 buf.push(dim_encoded_len);
340
341 for &d in chunk_dims {
342 match dim_encoded_len {
343 1 => buf.push(d as u8),
344 2 => buf.extend_from_slice(&(d as u16).to_le_bytes()),
345 4 => buf.extend_from_slice(&d.to_le_bytes()),
346 _ => {}
347 }
348 }
349 match dim_encoded_len {
350 1 => buf.push(element_size as u8),
351 2 => buf.extend_from_slice(&(element_size as u16).to_le_bytes()),
352 4 => buf.extend_from_slice(&element_size.to_le_bytes()),
353 _ => {}
354 }
355
356 buf.push(3);
358
359 buf.push(max_bits);
361
362 match offset_size {
364 4 => buf.extend_from_slice(&(fixed_array_address as u32).to_le_bytes()),
365 8 => buf.extend_from_slice(&fixed_array_address.to_le_bytes()),
366 _ => {}
367 }
368
369 buf
370}
371
372pub fn build_fixed_array_at(
374 chunks: &[WrittenChunk],
375 offset_size: u8,
376 length_size: u8,
377 has_filters: bool,
378 fa_base_address: u64,
379) -> Vec<u8> {
380 let os = offset_size as usize;
381 let num_elements = chunks.len();
382
383 let chunk_size_bytes: usize = if has_filters {
388 let max_raw = chunks.iter().map(|c| c.raw_size).max().unwrap_or(1);
389 let log2_val = if max_raw <= 1 { 0 } else { 63 - max_raw.leading_zeros() };
390 let len = 1 + ((log2_val + 8) / 8) as usize;
391 len.min(8)
392 } else {
393 0
394 };
395
396 let elem_size = if has_filters {
397 os + chunk_size_bytes + 4
398 } else {
399 os
400 };
401
402 let client_id: u8 = if has_filters { 1 } else { 0 };
403
404 let nelmts_field_size = length_size as usize;
406 let fahd_total_size = 4 + 1 + 1 + 1 + 1 + nelmts_field_size + os + 4;
407 let fadb_address = fa_base_address + fahd_total_size as u64;
408
409 let mut fahd = Vec::with_capacity(fahd_total_size);
411 fahd.extend_from_slice(b"FAHD");
412 fahd.push(0); fahd.push(client_id);
414 fahd.push(elem_size as u8);
415
416 let max_bits: u8 = 10;
418 fahd.push(max_bits);
419
420 match length_size {
421 4 => fahd.extend_from_slice(&(num_elements as u32).to_le_bytes()),
422 8 => fahd.extend_from_slice(&(num_elements as u64).to_le_bytes()),
423 _ => fahd.extend_from_slice(&(num_elements as u64).to_le_bytes()),
424 }
425
426 match offset_size {
427 4 => fahd.extend_from_slice(&(fadb_address as u32).to_le_bytes()),
428 8 => fahd.extend_from_slice(&fadb_address.to_le_bytes()),
429 _ => fahd.extend_from_slice(&fadb_address.to_le_bytes()),
430 }
431
432 let checksum = jenkins_lookup3(&fahd);
434 fahd.extend_from_slice(&checksum.to_le_bytes());
435
436 assert_eq!(fahd.len(), fahd_total_size);
437
438 let mut fadb = Vec::new();
440 fadb.extend_from_slice(b"FADB");
441 fadb.push(0); fadb.push(client_id);
443
444 match offset_size {
446 4 => fadb.extend_from_slice(&(fa_base_address as u32).to_le_bytes()),
447 8 => fadb.extend_from_slice(&fa_base_address.to_le_bytes()),
448 _ => fadb.extend_from_slice(&fa_base_address.to_le_bytes()),
449 }
450
451 for chunk in chunks {
453 match offset_size {
454 4 => fadb.extend_from_slice(&(chunk.address as u32).to_le_bytes()),
455 8 => fadb.extend_from_slice(&chunk.address.to_le_bytes()),
456 _ => fadb.extend_from_slice(&chunk.address.to_le_bytes()),
457 }
458 if has_filters {
459 let cs_bytes = chunk.compressed_size.to_le_bytes();
461 fadb.extend_from_slice(&cs_bytes[..chunk_size_bytes]);
462 fadb.extend_from_slice(&chunk.filter_mask.to_le_bytes());
463 }
464 }
465
466 let fadb_checksum = jenkins_lookup3(&fadb);
468 fadb.extend_from_slice(&fadb_checksum.to_le_bytes());
469
470 let mut combined = fahd;
471 combined.extend_from_slice(&fadb);
472 combined
473}
474
475fn serialize_v4_extensible_array(
477 chunk_dims: &[u32],
478 ea_address: u64,
479 offset_size: u8,
480 element_size: u32,
481) -> Vec<u8> {
482 let mut buf = Vec::new();
483 buf.push(4); buf.push(2); buf.push(0x00); let ndims = chunk_dims.len() as u8 + 1;
488 buf.push(ndims);
489
490 let max_dim = chunk_dims
491 .iter()
492 .map(|&d| d as u64)
493 .chain(core::iter::once(element_size as u64))
494 .max()
495 .unwrap_or(1);
496 let dim_encoded_len: u8 = if max_dim <= 0xFF {
497 1
498 } else if max_dim <= 0xFFFF {
499 2
500 } else {
501 4
502 };
503 buf.push(dim_encoded_len);
504
505 for &d in chunk_dims {
506 match dim_encoded_len {
507 1 => buf.push(d as u8),
508 2 => buf.extend_from_slice(&(d as u16).to_le_bytes()),
509 4 => buf.extend_from_slice(&d.to_le_bytes()),
510 _ => {}
511 }
512 }
513 match dim_encoded_len {
514 1 => buf.push(element_size as u8),
515 2 => buf.extend_from_slice(&(element_size as u16).to_le_bytes()),
516 4 => buf.extend_from_slice(&element_size.to_le_bytes()),
517 _ => {}
518 }
519
520 buf.push(4);
522
523 buf.push(32); buf.push(4); buf.push(4); buf.push(16); buf.push(10); match offset_size {
532 4 => buf.extend_from_slice(&(ea_address as u32).to_le_bytes()),
533 8 => buf.extend_from_slice(&ea_address.to_le_bytes()),
534 _ => {}
535 }
536
537 buf
538}
539
540pub fn build_extensible_array_at(
546 chunks: &[WrittenChunk],
547 offset_size: u8,
548 length_size: u8,
549 has_filters: bool,
550 ea_base_address: u64,
551) -> Vec<u8> {
552 let os = offset_size as usize;
553 let num_elements = chunks.len();
554
555 let chunk_size_bytes: usize = if has_filters {
557 let max_raw = chunks.iter().map(|c| c.raw_size).max().unwrap_or(1);
558 let log2_val = if max_raw <= 1 {
559 0
560 } else {
561 63 - max_raw.leading_zeros()
562 };
563 let len = 1 + ((log2_val + 8) / 8) as usize;
564 len.min(8)
565 } else {
566 0
567 };
568
569 let elem_size = if has_filters {
570 os + chunk_size_bytes + 4
571 } else {
572 os
573 };
574
575 let client_id: u8 = if has_filters { 1 } else { 0 };
576
577 let max_nelmts_bits: u8 = 32;
579 let idx_blk_elmts: u8 = 4;
580 let min_dblk_nelmts: u8 = 16;
581 let super_blk_min_nelmts: u8 = 4;
582 let max_dblk_nelmts_bits: u8 = 10;
583
584 let aehd_size = 4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
586 + 6 * length_size as usize + os + 4;
587 let aeib_address = ea_base_address + aehd_size as u64;
588
589 let n_inline = (idx_blk_elmts as usize).min(num_elements);
591 let remaining_after_inline = num_elements.saturating_sub(n_inline);
592
593 let sblk_min = super_blk_min_nelmts as usize; let log2_dblk_min = if min_dblk_nelmts <= 1 { 0 } else { (min_dblk_nelmts as u32).trailing_zeros() as usize };
604 let nsblks = (max_nelmts_bits as usize).saturating_sub(log2_dblk_min) + 1;
605
606 let mut dblk_sizes: Vec<usize> = Vec::new();
608 for sblk_idx in 0..sblk_min.min(nsblks) {
609 let ndblks = 1usize << (sblk_idx / 2);
610 let dblk_nelmts = (min_dblk_nelmts as usize) * (1 << sblk_idx.div_ceil(2));
611 for _ in 0..ndblks {
612 dblk_sizes.push(dblk_nelmts);
613 }
614 }
615 let n_direct_dblks = dblk_sizes.len();
616
617 let n_sblk_addrs = nsblks.saturating_sub(sblk_min);
619
620 let aeib_size = 4 + 1 + 1 + os + idx_blk_elmts as usize * elem_size + n_direct_dblks * os + n_sblk_addrs * os + 4; let mut aehd = Vec::with_capacity(aehd_size);
629 aehd.extend_from_slice(b"EAHD");
630 aehd.push(0); aehd.push(client_id);
632 aehd.push(elem_size as u8);
633 aehd.push(max_nelmts_bits);
634 aehd.push(idx_blk_elmts);
635 aehd.push(min_dblk_nelmts);
636 aehd.push(super_blk_min_nelmts);
637 aehd.push(max_dblk_nelmts_bits);
638
639 let n_active_dblks: u64 = if remaining_after_inline > 0 {
647 let mut count = 0u64;
648 let mut ci = n_inline;
649 for &sz in &dblk_sizes {
650 if ci < num_elements {
651 count += 1;
652 ci += sz;
653 }
654 }
655 count
656 } else {
657 0
658 };
659 let aedb_header_overhead = 4 + 1 + 1 + os + 4;
662 let data_blk_total_size: u64 = if remaining_after_inline > 0 {
663 let mut total = 0u64;
664 let mut ci = n_inline;
665 for &sz in &dblk_sizes {
666 if ci < num_elements {
667 total += (aedb_header_overhead + sz * elem_size) as u64;
668 ci += sz;
669 }
670 }
671 total
672 } else {
673 0
674 };
675 let max_idx_set: u64 = if remaining_after_inline > 0 {
677 let mut max_set = idx_blk_elmts as u64;
678 let mut ci = n_inline;
679 for &sz in &dblk_sizes {
680 if ci < num_elements {
681 max_set += sz as u64;
682 ci += sz;
683 }
684 }
685 max_set
686 } else {
687 idx_blk_elmts as u64
688 };
689
690 let write_length = |buf: &mut Vec<u8>, val: u64| {
691 match length_size {
692 4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
693 _ => buf.extend_from_slice(&val.to_le_bytes()),
694 }
695 };
696 let write_addr = |buf: &mut Vec<u8>, val: u64| {
697 match offset_size {
698 4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
699 _ => buf.extend_from_slice(&val.to_le_bytes()),
700 }
701 };
702
703 write_length(&mut aehd, 0); write_length(&mut aehd, 0); write_length(&mut aehd, n_active_dblks); write_length(&mut aehd, data_blk_total_size); write_length(&mut aehd, num_elements as u64); write_length(&mut aehd, max_idx_set); write_addr(&mut aehd, aeib_address);
711
712 let aehd_checksum = jenkins_lookup3(&aehd);
713 aehd.extend_from_slice(&aehd_checksum.to_le_bytes());
714 debug_assert_eq!(aehd.len(), aehd_size);
715
716 let mut aeib = Vec::with_capacity(aeib_size);
718 aeib.extend_from_slice(b"EAIB");
719 aeib.push(0); aeib.push(client_id);
721
722 match offset_size {
724 4 => aeib.extend_from_slice(&(ea_base_address as u32).to_le_bytes()),
725 8 => aeib.extend_from_slice(&ea_base_address.to_le_bytes()),
726 _ => aeib.extend_from_slice(&ea_base_address.to_le_bytes()),
727 }
728
729 #[allow(clippy::needless_range_loop)]
731 for i in 0..idx_blk_elmts as usize {
732 if i < n_inline {
733 write_chunk_element(&mut aeib, &chunks[i], offset_size, has_filters, chunk_size_bytes);
734 } else {
735 write_undefined_element(&mut aeib, offset_size, has_filters, chunk_size_bytes);
736 }
737 }
738
739 let mut data_blocks_buf = Vec::new();
741 let dblks_base = aeib_address + aeib_size as u64;
742 let mut dblk_cursor = dblks_base;
743 let mut chunk_idx = n_inline;
744
745 for &nelmts in &dblk_sizes {
746 if chunk_idx >= num_elements {
747 match offset_size {
749 4 => aeib.extend_from_slice(&u32::MAX.to_le_bytes()),
750 8 => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
751 _ => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
752 }
753 continue;
754 }
755
756 match offset_size {
758 4 => aeib.extend_from_slice(&(dblk_cursor as u32).to_le_bytes()),
759 8 => aeib.extend_from_slice(&dblk_cursor.to_le_bytes()),
760 _ => aeib.extend_from_slice(&dblk_cursor.to_le_bytes()),
761 }
762
763 let mut aedb = Vec::new();
765 aedb.extend_from_slice(b"EADB");
766 aedb.push(0); aedb.push(client_id);
768 match offset_size {
769 4 => aedb.extend_from_slice(&(ea_base_address as u32).to_le_bytes()),
770 8 => aedb.extend_from_slice(&ea_base_address.to_le_bytes()),
771 _ => aedb.extend_from_slice(&ea_base_address.to_le_bytes()),
772 }
773
774 let blk_off_size = (max_nelmts_bits as usize).div_ceil(8);
777 let blk_off_val = (chunk_idx - n_inline) as u64;
778 aedb.extend_from_slice(&blk_off_val.to_le_bytes()[..blk_off_size]);
779
780 for slot in 0..nelmts {
782 if chunk_idx + slot < num_elements {
783 write_chunk_element(
784 &mut aedb,
785 &chunks[chunk_idx + slot],
786 offset_size,
787 has_filters,
788 chunk_size_bytes,
789 );
790 } else {
791 write_undefined_element(&mut aedb, offset_size, has_filters, chunk_size_bytes);
793 }
794 }
795
796 let aedb_checksum = jenkins_lookup3(&aedb);
797 aedb.extend_from_slice(&aedb_checksum.to_le_bytes());
798
799 dblk_cursor += aedb.len() as u64;
800 data_blocks_buf.extend_from_slice(&aedb);
801 chunk_idx += nelmts;
802 }
803
804 for _ in 0..n_sblk_addrs {
806 match offset_size {
807 4 => aeib.extend_from_slice(&u32::MAX.to_le_bytes()),
808 8 => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
809 _ => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
810 }
811 }
812
813 let aeib_checksum = jenkins_lookup3(&aeib);
815 aeib.extend_from_slice(&aeib_checksum.to_le_bytes());
816 debug_assert_eq!(aeib.len(), aeib_size);
817
818 let mut combined = aehd;
819 combined.extend_from_slice(&aeib);
820 combined.extend_from_slice(&data_blocks_buf);
821 combined
822}
823
824fn write_chunk_element(
825 buf: &mut Vec<u8>,
826 chunk: &WrittenChunk,
827 offset_size: u8,
828 has_filters: bool,
829 chunk_size_bytes: usize,
830) {
831 match offset_size {
832 4 => buf.extend_from_slice(&(chunk.address as u32).to_le_bytes()),
833 8 => buf.extend_from_slice(&chunk.address.to_le_bytes()),
834 _ => buf.extend_from_slice(&chunk.address.to_le_bytes()),
835 }
836 if has_filters {
837 let cs_bytes = chunk.compressed_size.to_le_bytes();
838 buf.extend_from_slice(&cs_bytes[..chunk_size_bytes]);
839 buf.extend_from_slice(&chunk.filter_mask.to_le_bytes());
840 }
841}
842
843fn write_undefined_element(
844 buf: &mut Vec<u8>,
845 offset_size: u8,
846 has_filters: bool,
847 chunk_size_bytes: usize,
848) {
849 let os = offset_size as usize;
850 buf.extend_from_slice(&vec![0xFF; os]);
851 if has_filters {
852 buf.extend_from_slice(&vec![0x00; chunk_size_bytes]);
853 buf.extend_from_slice(&0u32.to_le_bytes());
854 }
855}
856
857pub fn build_chunked_data_at(
860 raw_data: &[u8],
861 shape: &[u64],
862 chunk_dims: &[u64],
863 element_size: usize,
864 options: &ChunkOptions,
865 base_address: u64,
866) -> Result<ChunkedDataResult, FormatError> {
867 build_chunked_data_at_ext(raw_data, shape, chunk_dims, element_size, options, base_address, None)
868}
869
870pub fn build_chunked_data_at_ext(
872 raw_data: &[u8],
873 shape: &[u64],
874 chunk_dims: &[u64],
875 element_size: usize,
876 options: &ChunkOptions,
877 base_address: u64,
878 maxshape: Option<&[u64]>,
879) -> Result<ChunkedDataResult, FormatError> {
880 let pipeline = options.build_pipeline(element_size as u32);
881
882 let chunks = split_into_chunks(raw_data, shape, chunk_dims, element_size);
883 let num_chunks = chunks.len();
884 let has_filters = pipeline.is_some();
885
886 let mut data_buf = Vec::new();
888 let mut written_chunks = Vec::with_capacity(num_chunks);
889
890 for (_offsets, chunk_bytes) in &chunks {
891 let compressed = if let Some(ref pl) = pipeline {
892 compress_chunk(chunk_bytes, pl, element_size as u32)?
893 } else {
894 chunk_bytes.clone()
895 };
896
897 let aligned_offset = align_to_cache_line(data_buf.len());
899 if aligned_offset > data_buf.len() {
900 data_buf.resize(aligned_offset, 0u8);
901 }
902
903 let address = base_address + data_buf.len() as u64;
904 let compressed_size = compressed.len() as u64;
905 let raw_size = chunk_bytes.len() as u64;
906
907 data_buf.extend_from_slice(&compressed);
908
909 written_chunks.push(WrittenChunk {
910 address,
911 compressed_size,
912 raw_size,
913 filter_mask: 0,
914 });
915 }
916
917 let chunk_dims_u32: Vec<u32> = chunk_dims.iter().map(|&d| d as u32).collect();
918 let offset_size: u8 = 8;
919 let length_size: u8 = 8;
920
921 let use_extensible = maxshape.is_some_and(|ms| ms.contains(&u64::MAX));
923
924 let aligned_idx = align_to_cache_line(data_buf.len());
926 if aligned_idx > data_buf.len() {
927 data_buf.resize(aligned_idx, 0u8);
928 }
929
930 let layout_message = if use_extensible {
931 let ea_address = base_address + data_buf.len() as u64;
932
933 let ea_bytes = build_extensible_array_at(
934 &written_chunks,
935 offset_size,
936 length_size,
937 has_filters,
938 ea_address,
939 );
940 data_buf.extend_from_slice(&ea_bytes);
941
942 serialize_v4_extensible_array(
943 &chunk_dims_u32,
944 ea_address,
945 offset_size,
946 element_size as u32,
947 )
948 } else if num_chunks == 1 {
949 let chunk_addr = written_chunks[0].address;
950 let filtered_size = if has_filters {
951 Some(written_chunks[0].compressed_size)
952 } else {
953 None
954 };
955 let filter_mask = if has_filters { Some(0u32) } else { None };
956 serialize_v4_single_chunk(
957 &chunk_dims_u32,
958 chunk_addr,
959 filtered_size,
960 filter_mask,
961 offset_size,
962 element_size as u32,
963 )
964 } else {
965 let fa_address = base_address + data_buf.len() as u64;
966 let max_bits: u8 = 10;
967
968 let fa_bytes = build_fixed_array_at(
969 &written_chunks,
970 offset_size,
971 length_size,
972 has_filters,
973 fa_address,
974 );
975 data_buf.extend_from_slice(&fa_bytes);
976
977 serialize_v4_fixed_array(
978 &chunk_dims_u32,
979 fa_address,
980 offset_size,
981 element_size as u32,
982 max_bits,
983 )
984 };
985
986 let pipeline_message = pipeline.as_ref().map(|pl| pl.serialize());
987
988 Ok(ChunkedDataResult {
989 data_bytes: data_buf,
990 layout_message,
991 pipeline_message,
992 })
993}
994
995#[cfg(test)]
996mod tests {
997 use super::*;
998 use crate::chunked_read::read_chunked_data;
999 use crate::data_layout::DataLayout;
1000 use crate::dataspace::{Dataspace, DataspaceType};
1001 use crate::datatype::{Datatype, DatatypeByteOrder};
1002
1003 fn make_f64_type() -> Datatype {
1004 Datatype::FloatingPoint {
1005 size: 8,
1006 byte_order: DatatypeByteOrder::LittleEndian,
1007 bit_offset: 0,
1008 bit_precision: 64,
1009 exponent_location: 52,
1010 exponent_size: 11,
1011 mantissa_location: 0,
1012 mantissa_size: 52,
1013 exponent_bias: 1023,
1014 }
1015 }
1016
1017 fn f64_to_bytes(data: &[f64]) -> Vec<u8> {
1018 let mut b = Vec::with_capacity(data.len() * 8);
1019 for &v in data {
1020 b.extend_from_slice(&v.to_le_bytes());
1021 }
1022 b
1023 }
1024
1025 fn bytes_to_f64(data: &[u8]) -> Vec<f64> {
1026 data.chunks(8)
1027 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
1028 .collect()
1029 }
1030
1031 fn roundtrip_chunked(
1033 values: &[f64],
1034 shape: &[u64],
1035 chunk_dims: &[u64],
1036 options: &ChunkOptions,
1037 ) -> Vec<f64> {
1038 let raw = f64_to_bytes(values);
1039 let base_address = 0x1000u64;
1040 let result =
1041 build_chunked_data_at(&raw, shape, chunk_dims, 8, options, base_address).unwrap();
1042
1043 let file_size = base_address as usize + result.data_bytes.len();
1045 let mut file_data = vec![0u8; file_size];
1046 file_data[base_address as usize..].copy_from_slice(&result.data_bytes);
1047
1048 let layout = DataLayout::parse(&result.layout_message, 8, 8).unwrap();
1050 let dataspace = Dataspace {
1051 space_type: DataspaceType::Simple,
1052 rank: shape.len() as u8,
1053 dimensions: shape.to_vec(),
1054 max_dimensions: None,
1055 };
1056 let datatype = make_f64_type();
1057
1058 let pipeline = result
1060 .pipeline_message
1061 .as_ref()
1062 .map(|pm| crate::filter_pipeline::FilterPipeline::parse(pm).unwrap());
1063
1064 let output = read_chunked_data(
1065 &file_data,
1066 &layout,
1067 &dataspace,
1068 &datatype,
1069 pipeline.as_ref(),
1070 8,
1071 8,
1072 )
1073 .unwrap();
1074
1075 bytes_to_f64(&output)
1076 }
1077
1078 #[test]
1079 fn split_1d_single_chunk() {
1080 let data = f64_to_bytes(&[1.0, 2.0, 3.0]);
1081 let result = split_into_chunks(&data, &[3], &[3], 8);
1082 assert_eq!(result.len(), 1);
1083 assert_eq!(result[0].0, vec![0]);
1084 assert_eq!(bytes_to_f64(&result[0].1), vec![1.0, 2.0, 3.0]);
1085 }
1086
1087 #[test]
1088 fn split_1d_multiple_chunks() {
1089 let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1090 let data = f64_to_bytes(&values);
1091 let result = split_into_chunks(&data, &[10], &[4], 8);
1092 assert_eq!(result.len(), 3); assert_eq!(result[0].0, vec![0]);
1094 assert_eq!(result[1].0, vec![4]);
1095 assert_eq!(result[2].0, vec![8]);
1096 assert_eq!(bytes_to_f64(&result[0].1), vec![0.0, 1.0, 2.0, 3.0]);
1097 assert_eq!(bytes_to_f64(&result[1].1), vec![4.0, 5.0, 6.0, 7.0]);
1098 assert_eq!(bytes_to_f64(&result[2].1), vec![8.0, 9.0, 0.0, 0.0]);
1100 }
1101
1102 #[test]
1103 fn split_2d_chunks() {
1104 let values: Vec<f64> = (0..16).map(|i| i as f64).collect();
1106 let data = f64_to_bytes(&values);
1107 let result = split_into_chunks(&data, &[4, 4], &[2, 2], 8);
1108 assert_eq!(result.len(), 4);
1109 assert_eq!(result[0].0, vec![0, 0]);
1110 assert_eq!(result[1].0, vec![0, 2]);
1111 assert_eq!(result[2].0, vec![2, 0]);
1112 assert_eq!(result[3].0, vec![2, 2]);
1113 assert_eq!(bytes_to_f64(&result[0].1), vec![0.0, 1.0, 4.0, 5.0]);
1115 assert_eq!(bytes_to_f64(&result[1].1), vec![2.0, 3.0, 6.0, 7.0]);
1117 }
1118
1119 #[test]
1120 fn roundtrip_1d_single_chunk_no_compression() {
1121 let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1122 let options = ChunkOptions {
1123 chunk_dims: Some(vec![10]),
1124 ..Default::default()
1125 };
1126 let result = roundtrip_chunked(&values, &[10], &[10], &options);
1127 assert_eq!(result, values);
1128 }
1129
1130 #[cfg(feature = "deflate")]
1131 #[test]
1132 fn roundtrip_1d_single_chunk_deflate() {
1133 let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1134 let options = ChunkOptions {
1135 chunk_dims: Some(vec![100]),
1136 deflate_level: Some(6),
1137 ..Default::default()
1138 };
1139 let result = roundtrip_chunked(&values, &[100], &[100], &options);
1140 assert_eq!(result, values);
1141 }
1142
1143 #[test]
1144 fn roundtrip_1d_multi_chunk_no_compression() {
1145 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1146 let options = ChunkOptions {
1147 chunk_dims: Some(vec![8]),
1148 ..Default::default()
1149 };
1150 let result = roundtrip_chunked(&values, &[20], &[8], &options);
1151 assert_eq!(result, values);
1152 }
1153
1154 #[cfg(feature = "deflate")]
1155 #[test]
1156 fn roundtrip_1d_multi_chunk_deflate() {
1157 let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1158 let options = ChunkOptions {
1159 chunk_dims: Some(vec![20]),
1160 deflate_level: Some(6),
1161 ..Default::default()
1162 };
1163 let result = roundtrip_chunked(&values, &[100], &[20], &options);
1164 assert_eq!(result, values);
1165 }
1166
1167 #[cfg(feature = "deflate")]
1168 #[test]
1169 fn roundtrip_1d_shuffle_deflate() {
1170 let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1171 let options = ChunkOptions {
1172 chunk_dims: Some(vec![50]),
1173 deflate_level: Some(6),
1174 shuffle: true,
1175 ..Default::default()
1176 };
1177 let result = roundtrip_chunked(&values, &[100], &[50], &options);
1178 assert_eq!(result, values);
1179 }
1180
1181 #[test]
1182 fn roundtrip_2d_chunks() {
1183 let values: Vec<f64> = (0..24).map(|i| i as f64).collect();
1185 let options = ChunkOptions {
1186 chunk_dims: Some(vec![3, 2]),
1187 ..Default::default()
1188 };
1189 let result = roundtrip_chunked(&values, &[6, 4], &[3, 2], &options);
1190 assert_eq!(result, values);
1191 }
1192
1193 #[test]
1194 fn align_chunk_offset_values() {
1195 use super::align_chunk_offset;
1196 use super::CACHE_LINE_SIZE;
1197 let cl = CACHE_LINE_SIZE as u64;
1198 assert_eq!(align_chunk_offset(0), 0);
1199 assert_eq!(align_chunk_offset(1), cl);
1200 assert_eq!(align_chunk_offset(cl), cl);
1201 assert_eq!(align_chunk_offset(cl + 1), cl * 2);
1202 assert_eq!(align_chunk_offset(cl * 10), cl * 10);
1203 }
1204
1205 #[test]
1206 fn chunk_addresses_are_cache_aligned() {
1207 use super::{CACHE_LINE_SIZE, align_chunk_offset};
1208 let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1209 let raw = f64_to_bytes(&values);
1210 let base_address = 0x1000u64;
1211 let base_address = align_chunk_offset(base_address);
1213 let options = ChunkOptions {
1214 chunk_dims: Some(vec![20]),
1215 ..Default::default()
1216 };
1217 let result =
1218 build_chunked_data_at(&raw, &[100], &[20], 8, &options, base_address).unwrap();
1219
1220 let file_size = base_address as usize + result.data_bytes.len();
1222 let mut file_data = vec![0u8; file_size];
1223 file_data[base_address as usize..].copy_from_slice(&result.data_bytes);
1224
1225 let layout = DataLayout::parse(&result.layout_message, 8, 8).unwrap();
1226 let dataspace = Dataspace {
1227 space_type: DataspaceType::Simple,
1228 rank: 1,
1229 dimensions: vec![100],
1230 max_dimensions: None,
1231 };
1232 let datatype = make_f64_type();
1233
1234 let output = read_chunked_data(
1236 &file_data, &layout, &dataspace, &datatype, None, 8, 8,
1237 ).unwrap();
1238 assert_eq!(bytes_to_f64(&output), values);
1239 }
1240
1241 #[test]
1242 fn chunk_options_auto_dims() {
1243 let options = ChunkOptions {
1244 chunk_dims: None,
1245 deflate_level: Some(6),
1246 ..Default::default()
1247 };
1248 let dims = options.resolve_chunk_dims(&[100, 50]);
1249 assert_eq!(dims, vec![100, 50]);
1250 }
1251
1252 #[test]
1253 fn chunk_options_pipeline_deflate() {
1254 let options = ChunkOptions {
1255 deflate_level: Some(6),
1256 ..Default::default()
1257 };
1258 let pl = options.build_pipeline(8).unwrap();
1259 assert_eq!(pl.filters.len(), 1);
1260 assert_eq!(pl.filters[0].filter_id, FILTER_DEFLATE);
1261 }
1262
1263 #[test]
1264 fn chunk_options_pipeline_shuffle_deflate_fletcher32() {
1265 let options = ChunkOptions {
1266 deflate_level: Some(6),
1267 shuffle: true,
1268 fletcher32: true,
1269 ..Default::default()
1270 };
1271 let pl = options.build_pipeline(8).unwrap();
1272 assert_eq!(pl.filters.len(), 3);
1273 assert_eq!(pl.filters[0].filter_id, FILTER_SHUFFLE);
1274 assert_eq!(pl.filters[1].filter_id, FILTER_DEFLATE);
1275 assert_eq!(pl.filters[2].filter_id, FILTER_FLETCHER32);
1276 }
1277
1278 #[test]
1279 fn serialize_v4_single_chunk_no_filters_roundtrip() {
1280 let msg = serialize_v4_single_chunk(&[20], 0x1000, None, None, 8, 8);
1281 let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1282 match layout {
1283 DataLayout::Chunked {
1284 chunk_dimensions,
1285 btree_address,
1286 version,
1287 chunk_index_type,
1288 single_chunk_filtered_size,
1289 single_chunk_filter_mask,
1290 } => {
1291 assert_eq!(version, 4);
1292 assert_eq!(chunk_index_type, Some(1));
1293 assert_eq!(chunk_dimensions, vec![20, 8]);
1294 assert_eq!(btree_address, Some(0x1000));
1295 assert_eq!(single_chunk_filtered_size, None);
1296 assert_eq!(single_chunk_filter_mask, None);
1297 }
1298 _ => panic!("expected chunked layout"),
1299 }
1300 }
1301
1302 #[test]
1303 fn serialize_v4_single_chunk_with_filters_roundtrip() {
1304 let msg = serialize_v4_single_chunk(&[100], 0x2000, Some(500), Some(0), 8, 8);
1305 let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1306 match layout {
1307 DataLayout::Chunked {
1308 btree_address,
1309 single_chunk_filtered_size,
1310 single_chunk_filter_mask,
1311 ..
1312 } => {
1313 assert_eq!(btree_address, Some(0x2000));
1314 assert_eq!(single_chunk_filtered_size, Some(500));
1315 assert_eq!(single_chunk_filter_mask, Some(0));
1316 }
1317 _ => panic!("expected chunked layout"),
1318 }
1319 }
1320
1321 #[test]
1322 fn serialize_v4_fixed_array_roundtrip() {
1323 let msg = serialize_v4_fixed_array(&[20], 0x3000, 8, 8, 4);
1324 let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1325 match layout {
1326 DataLayout::Chunked {
1327 version,
1328 chunk_index_type,
1329 btree_address,
1330 chunk_dimensions,
1331 ..
1332 } => {
1333 assert_eq!(version, 4);
1334 assert_eq!(chunk_index_type, Some(3));
1335 assert_eq!(btree_address, Some(0x3000));
1336 assert_eq!(chunk_dimensions, vec![20, 8]);
1337 }
1338 _ => panic!("expected chunked layout"),
1339 }
1340 }
1341
1342 #[test]
1343 fn build_fixed_array_valid_structure() {
1344 let chunks = vec![
1345 WrittenChunk {
1346 address: 0x1000,
1347 compressed_size: 160,
1348 raw_size: 160,
1349 filter_mask: 0,
1350 },
1351 WrittenChunk {
1352 address: 0x10A0,
1353 compressed_size: 160,
1354 raw_size: 160,
1355 filter_mask: 0,
1356 },
1357 ];
1358 let fa = build_fixed_array_at(&chunks, 8, 8, false, 0x2000);
1359 assert_eq!(&fa[0..4], b"FAHD");
1361 assert_eq!(&fa[28..32], b"FADB");
1364 }
1365
1366 #[test]
1369 fn serialize_v4_extensible_array_roundtrip() {
1370 let msg = serialize_v4_extensible_array(&[10], 0x4000, 8, 8);
1371 let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1372 match layout {
1373 DataLayout::Chunked {
1374 version,
1375 chunk_index_type,
1376 btree_address,
1377 chunk_dimensions,
1378 ..
1379 } => {
1380 assert_eq!(version, 4);
1381 assert_eq!(chunk_index_type, Some(4));
1382 assert_eq!(btree_address, Some(0x4000));
1383 assert_eq!(chunk_dimensions, vec![10, 8]);
1384 }
1385 _ => panic!("expected chunked layout"),
1386 }
1387 }
1388
1389 #[test]
1390 fn build_extensible_array_valid_structure() {
1391 let chunks = vec![
1392 WrittenChunk {
1393 address: 0x1000,
1394 compressed_size: 80,
1395 raw_size: 80,
1396 filter_mask: 0,
1397 },
1398 WrittenChunk {
1399 address: 0x1050,
1400 compressed_size: 80,
1401 raw_size: 80,
1402 filter_mask: 0,
1403 },
1404 ];
1405 let ea = build_extensible_array_at(&chunks, 8, 8, false, 0x2000);
1406 assert_eq!(&ea[0..4], b"EAHD");
1407 let aehd_size = 4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * 8 + 8 + 4;
1409 assert_eq!(&ea[aehd_size..aehd_size + 4], b"EAIB");
1410 }
1411
1412 fn roundtrip_ea(
1414 values: &[f64],
1415 shape: &[u64],
1416 chunk_dims: &[u64],
1417 maxshape: &[u64],
1418 ) -> Vec<f64> {
1419 let raw = f64_to_bytes(values);
1420 let base_address = 0x1000u64;
1421 let options = ChunkOptions {
1422 chunk_dims: Some(chunk_dims.to_vec()),
1423 ..Default::default()
1424 };
1425 let result = build_chunked_data_at_ext(
1426 &raw, shape, chunk_dims, 8, &options, base_address, Some(maxshape),
1427 )
1428 .unwrap();
1429
1430 let file_size = base_address as usize + result.data_bytes.len();
1431 let mut file_data = vec![0u8; file_size];
1432 file_data[base_address as usize..].copy_from_slice(&result.data_bytes);
1433
1434 let layout = DataLayout::parse(&result.layout_message, 8, 8).unwrap();
1435 match &layout {
1437 DataLayout::Chunked { chunk_index_type, .. } => {
1438 assert_eq!(*chunk_index_type, Some(4), "expected EA index type");
1439 }
1440 _ => panic!("expected chunked layout"),
1441 }
1442
1443 let dataspace = Dataspace {
1444 space_type: DataspaceType::Simple,
1445 rank: shape.len() as u8,
1446 dimensions: shape.to_vec(),
1447 max_dimensions: Some(maxshape.to_vec()),
1448 };
1449 let datatype = make_f64_type();
1450
1451 let output = read_chunked_data(&file_data, &layout, &dataspace, &datatype, None, 8, 8)
1452 .unwrap();
1453
1454 bytes_to_f64(&output)
1455 }
1456
1457 #[test]
1458 fn ea_roundtrip_1d_inline_only() {
1459 let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1460 let result = roundtrip_ea(&values, &[10], &[10], &[u64::MAX]);
1461 assert_eq!(result, values);
1462 }
1463
1464 #[test]
1465 fn ea_roundtrip_1d_multi_chunks() {
1466 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1467 let result = roundtrip_ea(&values, &[20], &[5], &[u64::MAX]);
1468 assert_eq!(result, values);
1469 }
1470
1471 #[test]
1472 fn ea_roundtrip_1d_many_chunks() {
1473 let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1474 let result = roundtrip_ea(&values, &[100], &[10], &[u64::MAX]);
1475 assert_eq!(result, values);
1476 }
1477
1478 #[cfg(feature = "std")]
1481 fn h5py_run(script: &str) -> String {
1482 let o = std::process::Command::new("python3").args(["-c", script]).output().expect("python3");
1483 if !o.status.success() { panic!("h5py: {}", String::from_utf8_lossy(&o.stderr)); }
1484 String::from_utf8(o.stdout).unwrap().trim().to_string()
1485 }
1486
1487 #[cfg(feature = "std")]
1488 #[test]
1489 fn h5py_reads_multiple_chunked_datasets() {
1490 use crate::file_writer::{FileWriter, AttrValue};
1491 let mut fw = FileWriter::new();
1492 let data1: Vec<f64> = (0..50).map(|i| i as f64).collect();
1493 let data2: Vec<f64> = (0..30).map(|i| (i * 10) as f64).collect();
1494 fw.create_dataset("a").with_f64_data(&data1).with_shape(&[50]).with_chunks(&[25]);
1495 fw.create_dataset("b").with_f64_data(&data2).with_shape(&[30]).with_chunks(&[10]);
1496 let bytes = fw.finish().unwrap();
1497 let path = std::env::temp_dir().join("rustyhdf5_chunked_multi.h5");
1498 std::fs::write(&path, &bytes).unwrap();
1499 let script = format!(
1500 "import h5py,json; f=h5py.File('{}','r'); print(json.dumps({{'a':f['a'][:].tolist(),'b':f['b'][:].tolist()}}))",
1501 path.display()
1502 );
1503 let v: serde_json::Value = serde_json::from_str(&h5py_run(&script)).unwrap();
1504 let va: Vec<f64> = serde_json::from_value(v["a"].clone()).unwrap();
1505 let vb: Vec<f64> = serde_json::from_value(v["b"].clone()).unwrap();
1506 assert_eq!(va, data1);
1507 assert_eq!(vb, data2);
1508 }
1509
1510 #[cfg(feature = "std")]
1511 #[test]
1512 fn h5py_reads_chunked_with_attrs() {
1513 use crate::file_writer::{FileWriter, AttrValue};
1514 let mut fw = FileWriter::new();
1515 let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
1516 fw.create_dataset("data").with_f64_data(&data).with_shape(&[50]).with_chunks(&[25])
1517 .set_attr("units", AttrValue::String("meters".to_string()));
1518 let bytes = fw.finish().unwrap();
1519 let path = std::env::temp_dir().join("rustyhdf5_chunked_attrs.h5");
1520 std::fs::write(&path, &bytes).unwrap();
1521 let script = format!(
1522 "import h5py,json; f=h5py.File('{}','r'); d=f['data']; print(json.dumps({{'values':d[:].tolist(),'units':d.attrs['units'].decode() if isinstance(d.attrs['units'],bytes) else str(d.attrs['units'])}}))",
1523 path.display()
1524 );
1525 let v: serde_json::Value = serde_json::from_str(&h5py_run(&script)).unwrap();
1526 let values: Vec<f64> = serde_json::from_value(v["values"].clone()).unwrap();
1527 assert_eq!(values, data);
1528 assert_eq!(v["units"], serde_json::json!("meters"));
1529 }
1530}