1#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6#[cfg(not(feature = "std"))]
7use alloc::{format, vec, vec::Vec};
8
9use crate::chunk_cache::{ChunkCache, CacheAlignedBuffer};
10use crate::data_layout::DataLayout;
11use crate::dataspace::Dataspace;
12use crate::datatype::Datatype;
13use crate::error::FormatError;
14use crate::filter_pipeline::FilterPipeline;
15use crate::filters::decompress_chunk;
16use crate::extensible_array::{ExtensibleArrayHeader, read_extensible_array_chunks};
17use crate::fixed_array::{FixedArrayHeader, read_fixed_array_chunks};
18
19#[cfg(feature = "parallel")]
20use crate::parallel_read;
21
22#[cfg(feature = "parallel")]
23use crate::lane_partition::PartitionStats;
24
25fn decompress_all_chunks(
29 file_data: &[u8],
30 chunks: &[ChunkInfo],
31 pipeline: Option<&FilterPipeline>,
32 chunk_total_bytes: usize,
33 element_size: u32,
34) -> Result<Vec<CacheAlignedBuffer>, FormatError> {
35 #[cfg(feature = "parallel")]
36 {
37 if let Some(pl) = pipeline {
38 if parallel_read::should_use_parallel(chunks.len()) {
39 let seed = chunks.first()
41 .map(|c| c.address)
42 .unwrap_or(0)
43 ^ (chunks.len() as u64);
44 let (data, _stats) = parallel_read::decompress_chunks_lane_partitioned(
45 file_data,
46 chunks,
47 pl,
48 chunk_total_bytes,
49 element_size,
50 seed,
51 None, )?;
53 return Ok(data.into_iter().map(CacheAlignedBuffer::from_vec).collect());
54 }
55 }
56 }
57
58 let mut result = Vec::with_capacity(chunks.len());
60 for chunk_info in chunks {
61 let c_addr = chunk_info.address as usize;
62 let size = chunk_info.chunk_size as usize;
63 if c_addr + size > file_data.len() {
64 return Err(FormatError::UnexpectedEof {
65 expected: c_addr + size,
66 available: file_data.len(),
67 });
68 }
69 let raw_chunk = &file_data[c_addr..c_addr + size];
70
71 let decompressed = if let Some(pl) = pipeline {
72 if chunk_info.filter_mask == 0 {
73 decompress_chunk(raw_chunk, pl, chunk_total_bytes, element_size)?
74 } else {
75 raw_chunk.to_vec()
76 }
77 } else {
78 raw_chunk.to_vec()
79 };
80 result.push(CacheAlignedBuffer::from_vec(decompressed));
81 }
82 Ok(result)
83}
84
85#[cfg(feature = "parallel")]
91pub fn decompress_all_chunks_with_stats(
92 file_data: &[u8],
93 chunks: &[ChunkInfo],
94 pipeline: &FilterPipeline,
95 chunk_total_bytes: usize,
96 element_size: u32,
97 seed: u64,
98 num_lanes: Option<usize>,
99) -> Result<(Vec<Vec<u8>>, PartitionStats), FormatError> {
100 parallel_read::decompress_chunks_lane_partitioned(
101 file_data,
102 chunks,
103 pipeline,
104 chunk_total_bytes,
105 element_size,
106 seed,
107 num_lanes,
108 )
109}
110
111#[derive(Debug, Clone)]
113pub struct ChunkInfo {
114 pub chunk_size: u32,
116 pub filter_mask: u32,
118 pub offsets: Vec<u64>,
120 pub address: u64,
122}
123
124fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
125 let s = size as usize;
126 if pos + s > data.len() {
127 return Err(FormatError::UnexpectedEof {
128 expected: pos + s,
129 available: data.len(),
130 });
131 }
132 let slice = &data[pos..pos + s];
133 Ok(match size {
134 2 => u16::from_le_bytes([slice[0], slice[1]]) as u64,
135 4 => u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as u64,
136 8 => u64::from_le_bytes([
137 slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
138 ]),
139 _ => return Err(FormatError::InvalidOffsetSize(size)),
140 })
141}
142
143pub fn collect_chunk_info(
148 file_data: &[u8],
149 btree_address: u64,
150 ndims: usize,
151 offset_size: u8,
152 _length_size: u8,
153) -> Result<Vec<ChunkInfo>, FormatError> {
154 let offset = btree_address as usize;
155 let os = offset_size as usize;
156
157 let header_size = 8 + os * 2;
159 if offset + header_size > file_data.len() {
160 return Err(FormatError::UnexpectedEof {
161 expected: offset + header_size,
162 available: file_data.len(),
163 });
164 }
165
166 if &file_data[offset..offset + 4] != b"TREE" {
167 return Err(FormatError::InvalidBTreeSignature);
168 }
169
170 let node_type = file_data[offset + 4];
171 if node_type != 1 {
172 return Err(FormatError::InvalidBTreeNodeType(node_type));
173 }
174
175 let node_level = file_data[offset + 5];
176 let entries_used =
177 u16::from_le_bytes([file_data[offset + 6], file_data[offset + 7]]) as usize;
178
179 let mut pos = offset + 8 + os * 2; let key_size = 4 + 4 + ndims * os;
183
184 if node_level == 0 {
185 let needed = entries_used * (key_size + os) + key_size;
188 if pos + needed > file_data.len() {
189 return Err(FormatError::UnexpectedEof {
190 expected: pos + needed,
191 available: file_data.len(),
192 });
193 }
194
195 let mut chunks = Vec::with_capacity(entries_used);
196 for _ in 0..entries_used {
197 let chunk_size = u32::from_le_bytes([
199 file_data[pos],
200 file_data[pos + 1],
201 file_data[pos + 2],
202 file_data[pos + 3],
203 ]);
204 let filter_mask = u32::from_le_bytes([
205 file_data[pos + 4],
206 file_data[pos + 5],
207 file_data[pos + 6],
208 file_data[pos + 7],
209 ]);
210 let mut offsets = Vec::with_capacity(ndims);
211 let mut kp = pos + 8;
212 for _ in 0..ndims {
213 offsets.push(read_offset(file_data, kp, offset_size)?);
214 kp += os;
215 }
216 pos += key_size;
217
218 let address = read_offset(file_data, pos, offset_size)?;
220 pos += os;
221
222 chunks.push(ChunkInfo {
223 chunk_size,
224 filter_mask,
225 offsets,
226 address,
227 });
228 }
229 Ok(chunks)
231 } else {
232 let needed = entries_used * (key_size + os) + key_size;
234 if pos + needed > file_data.len() {
235 return Err(FormatError::UnexpectedEof {
236 expected: pos + needed,
237 available: file_data.len(),
238 });
239 }
240
241 let mut child_addrs = Vec::with_capacity(entries_used);
242 for _ in 0..entries_used {
243 pos += key_size; let child_addr = read_offset(file_data, pos, offset_size)?;
245 child_addrs.push(child_addr);
246 pos += os;
247 }
248
249 let mut all_chunks = Vec::new();
250 for child_addr in child_addrs {
251 let child_chunks =
252 collect_chunk_info(file_data, child_addr, ndims, offset_size, _length_size)?;
253 all_chunks.extend(child_chunks);
254 }
255 Ok(all_chunks)
256 }
257}
258
259pub fn generate_implicit_chunks(
264 base_address: u64,
265 dataset_dims: &[u64],
266 chunk_dimensions: &[u32],
267 element_size: u32,
268) -> Vec<ChunkInfo> {
269 let rank = chunk_dimensions.len();
270 let chunk_byte_size: u64 = chunk_dimensions.iter().map(|&d| d as u64).product::<u64>()
271 * element_size as u64;
272
273 let mut num_chunks_per_dim = Vec::with_capacity(rank);
274 for d in 0..rank {
275 let ds = dataset_dims[d];
276 let ch = chunk_dimensions[d] as u64;
277 num_chunks_per_dim.push(ds.div_ceil(ch));
278 }
279 let total_chunks: u64 = num_chunks_per_dim.iter().product();
280
281 let mut chunks = Vec::with_capacity(total_chunks as usize);
282 for linear_idx in 0..total_chunks {
283 let mut offsets = vec![0u64; rank];
284 let mut remaining = linear_idx;
285 for d in (0..rank).rev() {
286 let nchunks = num_chunks_per_dim[d];
287 let chunk_idx = remaining % nchunks;
288 remaining /= nchunks;
289 offsets[d] = chunk_idx * chunk_dimensions[d] as u64;
290 }
291
292 chunks.push(ChunkInfo {
293 chunk_size: chunk_byte_size as u32,
294 filter_mask: 0,
295 offsets,
296 address: base_address + linear_idx * chunk_byte_size,
297 });
298 }
299
300 chunks
301}
302
303pub fn read_chunked_data(
305 file_data: &[u8],
306 layout: &DataLayout,
307 dataspace: &Dataspace,
308 datatype: &Datatype,
309 pipeline: Option<&FilterPipeline>,
310 offset_size: u8,
311 length_size: u8,
312) -> Result<Vec<u8>, FormatError> {
313 let (chunk_dimensions, version, chunk_index_type, addr_opt,
314 single_filtered_size, single_filter_mask) = match layout {
315 DataLayout::Chunked {
316 chunk_dimensions,
317 btree_address,
318 version,
319 chunk_index_type,
320 single_chunk_filtered_size,
321 single_chunk_filter_mask,
322 } => (chunk_dimensions, *version, *chunk_index_type, *btree_address,
323 *single_chunk_filtered_size, *single_chunk_filter_mask),
324 _ => {
325 return Err(FormatError::ChunkedReadError(
326 "expected chunked layout".into(),
327 ))
328 }
329 };
330
331 let addr = addr_opt.ok_or_else(|| {
332 FormatError::ChunkedReadError("no address for chunked layout".into())
333 })?;
334
335 let elem_size = datatype.type_size() as usize;
336
337 let ndims = chunk_dimensions.len();
339 let rank = ndims - 1;
340 let chunk_dims: Vec<usize> = chunk_dimensions[..rank]
341 .iter()
342 .map(|&d| d as usize)
343 .collect();
344
345 let ds_dims: Vec<usize> = dataspace.dimensions.iter().map(|&d| d as usize).collect();
346 if ds_dims.len() != rank {
347 return Err(FormatError::ChunkedReadError(format!(
348 "rank mismatch: dataspace has {} dims, layout has {} chunk dims (rank={})",
349 ds_dims.len(),
350 chunk_dimensions.len(),
351 rank
352 )));
353 }
354
355 let chunks = match (version, chunk_index_type) {
357 (3, _) => {
358 let ndims = chunk_dimensions.len(); collect_chunk_info(file_data, addr, ndims, offset_size, length_size)?
360 }
361 (4, Some(1)) => {
362 let chunk_byte_size: usize = chunk_dims.iter().product::<usize>() * elem_size;
364 let (csize, fmask) = if let Some(fs) = single_filtered_size {
365 (fs as u32, single_filter_mask.unwrap_or(0))
366 } else {
367 (chunk_byte_size as u32, 0)
368 };
369 vec![ChunkInfo {
370 chunk_size: csize,
371 filter_mask: fmask,
372 offsets: vec![0u64; rank],
373 address: addr,
374 }]
375 }
376 (4, Some(2)) => {
377 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
379 generate_implicit_chunks(
380 addr,
381 &dataspace.dimensions,
382 &spatial_chunk_dims,
383 elem_size as u32,
384 )
385 }
386 (4, Some(3)) => {
387 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
389 let header = FixedArrayHeader::parse(file_data, addr as usize, offset_size, length_size)?;
390 read_fixed_array_chunks(
391 file_data, &header, &dataspace.dimensions, &spatial_chunk_dims,
392 elem_size as u32, offset_size, length_size,
393 )?
394 }
395 (4, Some(4)) => {
396 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
398 let header = ExtensibleArrayHeader::parse(
399 file_data, addr as usize, offset_size, length_size,
400 )?;
401 read_extensible_array_chunks(
402 file_data, &header, &dataspace.dimensions, &spatial_chunk_dims,
403 elem_size as u32, offset_size, length_size,
404 )?
405 }
406 (v, idx) => {
407 return Err(FormatError::ChunkedReadError(format!(
408 "unsupported chunked layout version={v}, index_type={idx:?}"
409 )))
410 }
411 };
412
413 let total_elements = dataspace.num_elements() as usize;
415 let total_bytes = total_elements * elem_size;
416 let mut output = vec![0u8; total_bytes];
417
418 let mut ds_strides = vec![1usize; rank];
419 for i in (0..rank.saturating_sub(1)).rev() {
420 ds_strides[i] = ds_strides[i + 1] * ds_dims[i + 1];
421 }
422
423 let mut chunk_strides = vec![1usize; rank];
424 for i in (0..rank.saturating_sub(1)).rev() {
425 chunk_strides[i] = chunk_strides[i + 1] * chunk_dims[i + 1];
426 }
427
428 let chunk_total_elements: usize = chunk_dims.iter().product();
429 let chunk_total_bytes = chunk_total_elements * elem_size;
430
431 let decompressed_chunks = decompress_all_chunks(
433 file_data,
434 &chunks,
435 pipeline,
436 chunk_total_bytes,
437 elem_size as u32,
438 )?;
439
440 for (chunk_info, decompressed) in chunks.iter().zip(decompressed_chunks.iter()) {
441 let chunk_offsets: Vec<usize> = chunk_info.offsets.iter()
443 .take(rank)
444 .map(|&o| o as usize)
445 .collect();
446
447 if rank == 0 {
448 let copy_len = decompressed.len().min(output.len());
449 output[..copy_len].copy_from_slice(&decompressed[..copy_len]);
450 } else {
451 copy_chunk_to_output(
452 decompressed,
453 &mut output,
454 &chunk_offsets,
455 &chunk_dims,
456 &ds_dims,
457 &ds_strides,
458 &chunk_strides,
459 elem_size,
460 rank,
461 );
462 }
463 }
464
465 Ok(output)
466}
467
468pub fn read_chunked_data_cached(
474 file_data: &[u8],
475 layout: &DataLayout,
476 dataspace: &Dataspace,
477 datatype: &Datatype,
478 pipeline: Option<&FilterPipeline>,
479 offset_size: u8,
480 length_size: u8,
481 cache: &ChunkCache,
482) -> Result<Vec<u8>, FormatError> {
483 let (chunk_dimensions, version, chunk_index_type, addr_opt,
484 single_filtered_size, single_filter_mask) = match layout {
485 DataLayout::Chunked {
486 chunk_dimensions,
487 btree_address,
488 version,
489 chunk_index_type,
490 single_chunk_filtered_size,
491 single_chunk_filter_mask,
492 } => (chunk_dimensions, *version, *chunk_index_type, *btree_address,
493 *single_chunk_filtered_size, *single_chunk_filter_mask),
494 _ => {
495 return Err(FormatError::ChunkedReadError(
496 "expected chunked layout".into(),
497 ))
498 }
499 };
500
501 let addr = addr_opt.ok_or_else(|| {
502 FormatError::ChunkedReadError("no address for chunked layout".into())
503 })?;
504
505 let elem_size = datatype.type_size() as usize;
506 let ndims = chunk_dimensions.len();
507 let rank = ndims - 1;
508 let chunk_dims: Vec<usize> = chunk_dimensions[..rank]
509 .iter()
510 .map(|&d| d as usize)
511 .collect();
512
513 let ds_dims: Vec<usize> = dataspace.dimensions.iter().map(|&d| d as usize).collect();
514 if ds_dims.len() != rank {
515 return Err(FormatError::ChunkedReadError(format!(
516 "rank mismatch: dataspace has {} dims, layout has {} chunk dims (rank={})",
517 ds_dims.len(), chunk_dimensions.len(), rank
518 )));
519 }
520
521 if !cache.has_index() {
523 let chunks = match (version, chunk_index_type) {
524 (3, _) => {
525 collect_chunk_info(file_data, addr, ndims, offset_size, length_size)?
526 }
527 (4, Some(1)) => {
528 let chunk_byte_size: usize = chunk_dims.iter().product::<usize>() * elem_size;
529 let (csize, fmask) = if let Some(fs) = single_filtered_size {
530 (fs as u32, single_filter_mask.unwrap_or(0))
531 } else {
532 (chunk_byte_size as u32, 0)
533 };
534 vec![ChunkInfo {
535 chunk_size: csize,
536 filter_mask: fmask,
537 offsets: vec![0u64; rank],
538 address: addr,
539 }]
540 }
541 (4, Some(2)) => {
542 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
543 generate_implicit_chunks(addr, &dataspace.dimensions, &spatial_chunk_dims, elem_size as u32)
544 }
545 (4, Some(3)) => {
546 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
547 let header = FixedArrayHeader::parse(file_data, addr as usize, offset_size, length_size)?;
548 read_fixed_array_chunks(
549 file_data, &header, &dataspace.dimensions, &spatial_chunk_dims,
550 elem_size as u32, offset_size, length_size,
551 )?
552 }
553 (4, Some(4)) => {
554 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
555 let header = ExtensibleArrayHeader::parse(file_data, addr as usize, offset_size, length_size)?;
556 read_extensible_array_chunks(
557 file_data, &header, &dataspace.dimensions, &spatial_chunk_dims,
558 elem_size as u32, offset_size, length_size,
559 )?
560 }
561 (v, idx) => {
562 return Err(FormatError::ChunkedReadError(format!(
563 "unsupported chunked layout version={v}, index_type={idx:?}"
564 )))
565 }
566 };
567 cache.populate_index(&chunks, rank);
568 }
569
570 let chunks = cache.all_indexed_chunks().unwrap_or_default();
571
572 let total_elements = dataspace.num_elements() as usize;
574 let total_bytes = total_elements * elem_size;
575 let mut output = vec![0u8; total_bytes];
576
577 let mut ds_strides = vec![1usize; rank];
578 for i in (0..rank.saturating_sub(1)).rev() {
579 ds_strides[i] = ds_strides[i + 1] * ds_dims[i + 1];
580 }
581
582 let mut chunk_strides = vec![1usize; rank];
583 for i in (0..rank.saturating_sub(1)).rev() {
584 chunk_strides[i] = chunk_strides[i + 1] * chunk_dims[i + 1];
585 }
586
587 let chunk_total_elements: usize = chunk_dims.iter().product();
588 let chunk_total_bytes = chunk_total_elements * elem_size;
589
590 for chunk_info in &chunks {
591 let coord: Vec<u64> = chunk_info.offsets.iter().take(rank).copied().collect();
592
593 let decompressed = if let Some(cached) = cache.get_decompressed(&coord) {
595 cached
596 } else {
597 let c_addr = chunk_info.address as usize;
599 let size = chunk_info.chunk_size as usize;
600 if c_addr + size > file_data.len() {
601 return Err(FormatError::UnexpectedEof {
602 expected: c_addr + size,
603 available: file_data.len(),
604 });
605 }
606 let raw_chunk = &file_data[c_addr..c_addr + size];
607 let dec = if let Some(pl) = pipeline {
608 if chunk_info.filter_mask == 0 {
609 decompress_chunk(raw_chunk, pl, chunk_total_bytes, elem_size as u32)?
610 } else {
611 raw_chunk.to_vec()
612 }
613 } else {
614 raw_chunk.to_vec()
615 };
616 cache.put_decompressed(coord, dec.clone());
617 dec
618 };
619
620 let chunk_offsets: Vec<usize> = chunk_info.offsets.iter()
621 .take(rank)
622 .map(|&o| o as usize)
623 .collect();
624
625 if rank == 0 {
626 let copy_len = decompressed.len().min(output.len());
627 output[..copy_len].copy_from_slice(&decompressed[..copy_len]);
628 } else {
629 copy_chunk_to_output(
630 &decompressed,
631 &mut output,
632 &chunk_offsets,
633 &chunk_dims,
634 &ds_dims,
635 &ds_strides,
636 &chunk_strides,
637 elem_size,
638 rank,
639 );
640 }
641 }
642
643 Ok(output)
644}
645
646pub struct SweepContext {
654 pub history: Vec<Vec<u64>>,
656 pub window_size: usize,
658 pub direction: &'static str,
660 pub prefetch_count: usize,
662 pub predicted_next: Vec<Vec<u64>>,
664}
665
666impl SweepContext {
667 pub fn new(window_size: usize, prefetch_count: usize) -> Self {
669 Self {
670 history: Vec::with_capacity(window_size),
671 window_size,
672 direction: "random",
673 prefetch_count,
674 predicted_next: Vec::new(),
675 }
676 }
677
678 pub fn with_defaults() -> Self {
680 Self::new(12, 4)
681 }
682
683 fn record(&mut self, coord: Vec<u64>, ndims: usize) {
685 if self.history.len() >= self.window_size {
686 self.history.remove(0);
687 }
688 self.history.push(coord);
689
690 if self.history.len() < 3 || ndims == 0 {
691 self.direction = "random";
692 self.predicted_next.clear();
693 return;
694 }
695
696 let num_deltas = self.history.len() - 1;
698 let mut changing = vec![0usize; ndims];
699 for i in 0..num_deltas {
700 let prev = &self.history[i];
701 let curr = &self.history[i + 1];
702 if prev.len() < ndims || curr.len() < ndims {
703 self.direction = "random";
704 self.predicted_next.clear();
705 return;
706 }
707 for d in 0..ndims {
708 if curr[d] != prev[d] {
709 changing[d] += 1;
710 }
711 }
712 }
713
714 let threshold = (num_deltas + 1) / 2;
715 let (max_dim, max_changes) = changing.iter().enumerate()
716 .max_by_key(|(_, c)| *c).unwrap();
717
718 if *max_changes < threshold {
719 self.direction = "random";
720 self.predicted_next.clear();
721 return;
722 }
723
724 let others_max = changing.iter().enumerate()
725 .filter(|(d, _)| *d != max_dim)
726 .map(|(_, c)| *c)
727 .max()
728 .unwrap_or(0);
729
730 if others_max > 0 && *max_changes < others_max * 2 {
731 self.direction = "random";
732 self.predicted_next.clear();
733 return;
734 }
735
736 self.direction = if max_dim == ndims - 1 {
737 "row_major"
738 } else if max_dim == 0 {
739 "column_major"
740 } else {
741 "slice_major"
742 };
743
744 let sweep_dim = max_dim;
746 let mut total_step: i64 = 0;
747 let mut step_count: usize = 0;
748 for i in 1..self.history.len() {
749 let prev = self.history[i - 1][sweep_dim] as i64;
750 let curr = self.history[i][sweep_dim] as i64;
751 let diff = curr - prev;
752 if diff != 0 {
753 total_step += diff;
754 step_count += 1;
755 }
756 }
757
758 if step_count == 0 {
759 self.predicted_next.clear();
760 return;
761 }
762
763 let avg_step = total_step / step_count as i64;
764 if avg_step == 0 {
765 self.predicted_next.clear();
766 return;
767 }
768
769 let last = self.history.last().unwrap();
770 self.predicted_next.clear();
771 for i in 1..=self.prefetch_count {
772 let mut pred = last.clone();
773 let new_val = last[sweep_dim] as i64 + avg_step * i as i64;
774 if new_val < 0 {
775 break;
776 }
777 pred[sweep_dim] = new_val as u64;
778 self.predicted_next.push(pred);
779 }
780 }
781}
782
783#[allow(clippy::too_many_arguments)]
789pub fn read_chunked_data_sweep(
790 file_data: &[u8],
791 layout: &DataLayout,
792 dataspace: &Dataspace,
793 datatype: &Datatype,
794 pipeline: Option<&FilterPipeline>,
795 offset_size: u8,
796 length_size: u8,
797 cache: &ChunkCache,
798 sweep: &mut SweepContext,
799) -> Result<Vec<u8>, FormatError> {
800 let (chunk_dimensions, version, chunk_index_type, addr_opt,
801 single_filtered_size, single_filter_mask) = match layout {
802 DataLayout::Chunked {
803 chunk_dimensions,
804 btree_address,
805 version,
806 chunk_index_type,
807 single_chunk_filtered_size,
808 single_chunk_filter_mask,
809 } => (chunk_dimensions, *version, *chunk_index_type, *btree_address,
810 *single_chunk_filtered_size, *single_chunk_filter_mask),
811 _ => {
812 return Err(FormatError::ChunkedReadError(
813 "expected chunked layout".into(),
814 ))
815 }
816 };
817
818 let addr = addr_opt.ok_or_else(|| {
819 FormatError::ChunkedReadError("no address for chunked layout".into())
820 })?;
821
822 let elem_size = datatype.type_size() as usize;
823 let ndims = chunk_dimensions.len();
824 let rank = ndims - 1;
825 let chunk_dims: Vec<usize> = chunk_dimensions[..rank]
826 .iter()
827 .map(|&d| d as usize)
828 .collect();
829
830 let ds_dims: Vec<usize> = dataspace.dimensions.iter().map(|&d| d as usize).collect();
831 if ds_dims.len() != rank {
832 return Err(FormatError::ChunkedReadError(format!(
833 "rank mismatch: dataspace has {} dims, layout has {} chunk dims (rank={})",
834 ds_dims.len(), chunk_dimensions.len(), rank
835 )));
836 }
837
838 if !cache.has_index() {
840 let chunks = match (version, chunk_index_type) {
841 (3, _) => {
842 collect_chunk_info(file_data, addr, ndims, offset_size, length_size)?
843 }
844 (4, Some(1)) => {
845 let chunk_byte_size: usize = chunk_dims.iter().product::<usize>() * elem_size;
846 let (csize, fmask) = if let Some(fs) = single_filtered_size {
847 (fs as u32, single_filter_mask.unwrap_or(0))
848 } else {
849 (chunk_byte_size as u32, 0)
850 };
851 vec![ChunkInfo {
852 chunk_size: csize,
853 filter_mask: fmask,
854 offsets: vec![0u64; rank],
855 address: addr,
856 }]
857 }
858 (4, Some(2)) => {
859 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
860 generate_implicit_chunks(addr, &dataspace.dimensions, &spatial_chunk_dims, elem_size as u32)
861 }
862 (4, Some(3)) => {
863 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
864 let header = FixedArrayHeader::parse(file_data, addr as usize, offset_size, length_size)?;
865 read_fixed_array_chunks(
866 file_data, &header, &dataspace.dimensions, &spatial_chunk_dims,
867 elem_size as u32, offset_size, length_size,
868 )?
869 }
870 (4, Some(4)) => {
871 let spatial_chunk_dims: Vec<u32> = chunk_dimensions[..rank].to_vec();
872 let header = ExtensibleArrayHeader::parse(file_data, addr as usize, offset_size, length_size)?;
873 read_extensible_array_chunks(
874 file_data, &header, &dataspace.dimensions, &spatial_chunk_dims,
875 elem_size as u32, offset_size, length_size,
876 )?
877 }
878 (v, idx) => {
879 return Err(FormatError::ChunkedReadError(format!(
880 "unsupported chunked layout version={v}, index_type={idx:?}"
881 )))
882 }
883 };
884 cache.populate_index(&chunks, rank);
885 }
886
887 let chunks = cache.all_indexed_chunks().unwrap_or_default();
888
889 let total_elements = dataspace.num_elements() as usize;
891 let total_bytes = total_elements * elem_size;
892 let mut output = vec![0u8; total_bytes];
893
894 let mut ds_strides = vec![1usize; rank];
895 for i in (0..rank.saturating_sub(1)).rev() {
896 ds_strides[i] = ds_strides[i + 1] * ds_dims[i + 1];
897 }
898
899 let mut chunk_strides = vec![1usize; rank];
900 for i in (0..rank.saturating_sub(1)).rev() {
901 chunk_strides[i] = chunk_strides[i + 1] * chunk_dims[i + 1];
902 }
903
904 let chunk_total_elements: usize = chunk_dims.iter().product();
905 let chunk_total_bytes = chunk_total_elements * elem_size;
906
907 for chunk_info in &chunks {
908 let coord: Vec<u64> = chunk_info.offsets.iter().take(rank).copied().collect();
909
910 sweep.record(coord.clone(), rank);
912
913 if !sweep.predicted_next.is_empty() {
915 cache.prefetch_hint(&sweep.predicted_next);
916 cache.set_sweep_direction(sweep.direction);
917 }
918
919 let decompressed = if let Some(cached) = cache.get_decompressed(&coord) {
921 cached
922 } else {
923 let c_addr = chunk_info.address as usize;
925 let size = chunk_info.chunk_size as usize;
926 if c_addr + size > file_data.len() {
927 return Err(FormatError::UnexpectedEof {
928 expected: c_addr + size,
929 available: file_data.len(),
930 });
931 }
932 let raw_chunk = &file_data[c_addr..c_addr + size];
933 let dec = if let Some(pl) = pipeline {
934 if chunk_info.filter_mask == 0 {
935 decompress_chunk(raw_chunk, pl, chunk_total_bytes, elem_size as u32)?
936 } else {
937 raw_chunk.to_vec()
938 }
939 } else {
940 raw_chunk.to_vec()
941 };
942 cache.put_decompressed(coord, dec.clone());
943 dec
944 };
945
946 let chunk_offsets: Vec<usize> = chunk_info.offsets.iter()
947 .take(rank)
948 .map(|&o| o as usize)
949 .collect();
950
951 if rank == 0 {
952 let copy_len = decompressed.len().min(output.len());
953 output[..copy_len].copy_from_slice(&decompressed[..copy_len]);
954 } else {
955 copy_chunk_to_output(
956 &decompressed,
957 &mut output,
958 &chunk_offsets,
959 &chunk_dims,
960 &ds_dims,
961 &ds_strides,
962 &chunk_strides,
963 elem_size,
964 rank,
965 );
966 }
967 }
968
969 Ok(output)
970}
971
972#[allow(clippy::too_many_arguments)]
974fn copy_chunk_to_output(
975 chunk_data: &[u8],
976 output: &mut [u8],
977 chunk_offsets: &[usize],
978 chunk_dims: &[usize],
979 ds_dims: &[usize],
980 ds_strides: &[usize],
981 chunk_strides: &[usize],
982 elem_size: usize,
983 rank: usize,
984) {
985 let chunk_total: usize = chunk_dims.iter().product();
987 for flat_idx in 0..chunk_total {
988 let mut remaining = flat_idx;
990 let mut ds_flat = 0usize;
991 let mut out_of_bounds = false;
992
993 for d in 0..rank {
994 let coord_in_chunk = remaining / chunk_strides[d];
995 remaining %= chunk_strides[d];
996
997 let global_coord = chunk_offsets[d] + coord_in_chunk;
998 if global_coord >= ds_dims[d] {
999 out_of_bounds = true;
1000 break;
1001 }
1002 ds_flat += global_coord * ds_strides[d];
1003 }
1004
1005 if out_of_bounds {
1006 continue;
1007 }
1008
1009 let src_start = flat_idx * elem_size;
1010 let dst_start = ds_flat * elem_size;
1011
1012 if src_start + elem_size <= chunk_data.len() && dst_start + elem_size <= output.len() {
1013 output[dst_start..dst_start + elem_size]
1014 .copy_from_slice(&chunk_data[src_start..src_start + elem_size]);
1015 }
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022
1023 fn write_offset(buf: &mut Vec<u8>, val: u64, size: u8) {
1024 match size {
1025 4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
1026 8 => buf.extend_from_slice(&val.to_le_bytes()),
1027 _ => panic!("unsupported offset size in test"),
1028 }
1029 }
1030
1031 fn build_chunk_btree_leaf(
1033 chunks: &[ChunkInfo],
1034 ndims: usize,
1035 offset_size: u8,
1036 ) -> Vec<u8> {
1037 let _os = offset_size as usize;
1038 let entries_used = chunks.len() as u16;
1039 let mut buf = Vec::new();
1040
1041 buf.extend_from_slice(b"TREE");
1043 buf.push(1); buf.push(0); buf.extend_from_slice(&entries_used.to_le_bytes());
1046
1047 let undef: u64 = if offset_size == 4 {
1049 0xFFFFFFFF
1050 } else {
1051 0xFFFFFFFFFFFFFFFF
1052 };
1053 write_offset(&mut buf, undef, offset_size);
1054 write_offset(&mut buf, undef, offset_size);
1055
1056 for chunk in chunks {
1058 buf.extend_from_slice(&chunk.chunk_size.to_le_bytes());
1060 buf.extend_from_slice(&chunk.filter_mask.to_le_bytes());
1061 for d in 0..ndims {
1062 let off = if d < chunk.offsets.len() {
1063 chunk.offsets[d]
1064 } else {
1065 0
1066 };
1067 write_offset(&mut buf, off, offset_size);
1068 }
1069 write_offset(&mut buf, chunk.address, offset_size);
1071 }
1072
1073 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); for _ in 0..ndims {
1077 write_offset(&mut buf, u64::MAX, offset_size);
1078 }
1079
1080 buf
1081 }
1082
1083 #[test]
1086 fn collect_two_chunks_from_leaf() {
1087 let ndims = 2; let os: u8 = 8;
1089
1090 let chunks = vec![
1091 ChunkInfo {
1092 chunk_size: 80,
1093 filter_mask: 0,
1094 offsets: vec![0, 0],
1095 address: 0x1000,
1096 },
1097 ChunkInfo {
1098 chunk_size: 80,
1099 filter_mask: 0,
1100 offsets: vec![10, 0],
1101 address: 0x2000,
1102 },
1103 ];
1104
1105 let btree = build_chunk_btree_leaf(&chunks, ndims, os);
1106 let mut file_data = vec![0u8; 0x3000];
1107 file_data[..btree.len()].copy_from_slice(&btree);
1108
1109 let result = collect_chunk_info(&file_data, 0, ndims, os, os).unwrap();
1110 assert_eq!(result.len(), 2);
1111 assert_eq!(result[0].address, 0x1000);
1112 assert_eq!(result[0].offsets, vec![0, 0]);
1113 assert_eq!(result[0].chunk_size, 80);
1114 assert_eq!(result[1].address, 0x2000);
1115 assert_eq!(result[1].offsets, vec![10, 0]);
1116 }
1117
1118 #[test]
1119 fn collect_three_chunks() {
1120 let ndims = 2;
1121 let os: u8 = 8;
1122
1123 let chunks = vec![
1124 ChunkInfo {
1125 chunk_size: 40,
1126 filter_mask: 0,
1127 offsets: vec![0, 0],
1128 address: 0x100,
1129 },
1130 ChunkInfo {
1131 chunk_size: 40,
1132 filter_mask: 0,
1133 offsets: vec![5, 0],
1134 address: 0x200,
1135 },
1136 ChunkInfo {
1137 chunk_size: 40,
1138 filter_mask: 0,
1139 offsets: vec![10, 0],
1140 address: 0x300,
1141 },
1142 ];
1143
1144 let btree = build_chunk_btree_leaf(&chunks, ndims, os);
1145 let mut file_data = vec![0u8; 0x1000];
1146 file_data[..btree.len()].copy_from_slice(&btree);
1147
1148 let result = collect_chunk_info(&file_data, 0, ndims, os, os).unwrap();
1149 assert_eq!(result.len(), 3);
1150 assert_eq!(result[0].address, 0x100);
1151 assert_eq!(result[1].address, 0x200);
1152 assert_eq!(result[2].address, 0x300);
1153 }
1154
1155 #[test]
1156 fn collect_empty_btree() {
1157 let ndims = 2;
1158 let os: u8 = 8;
1159 let btree = build_chunk_btree_leaf(&[], ndims, os);
1160 let mut file_data = vec![0u8; 0x1000];
1161 file_data[..btree.len()].copy_from_slice(&btree);
1162
1163 let result = collect_chunk_info(&file_data, 0, ndims, os, os).unwrap();
1164 assert_eq!(result.len(), 0);
1165 }
1166
1167 use crate::dataspace::{Dataspace, DataspaceType};
1170 use crate::datatype::{Datatype, DatatypeByteOrder};
1171
1172 fn make_f64_type() -> Datatype {
1173 Datatype::FloatingPoint {
1174 size: 8,
1175 byte_order: DatatypeByteOrder::LittleEndian,
1176 bit_offset: 0,
1177 bit_precision: 64,
1178 exponent_location: 52,
1179 exponent_size: 11,
1180 mantissa_location: 0,
1181 mantissa_size: 52,
1182 exponent_bias: 1023,
1183 }
1184 }
1185
1186 fn make_f32_type() -> Datatype {
1187 Datatype::FloatingPoint {
1188 size: 4,
1189 byte_order: DatatypeByteOrder::LittleEndian,
1190 bit_offset: 0,
1191 bit_precision: 32,
1192 exponent_location: 23,
1193 exponent_size: 8,
1194 mantissa_location: 0,
1195 mantissa_size: 23,
1196 exponent_bias: 127,
1197 }
1198 }
1199
1200 fn build_1d_chunked_file(
1202 values: &[f64],
1203 chunk_size_elems: usize,
1204 ) -> (Vec<u8>, DataLayout, Dataspace) {
1205 let os: u8 = 8;
1206 let elem_size = 8usize;
1207 let ndims = 2; let total = values.len();
1209
1210 let mut file_data = vec![0u8; 0x10000];
1212 let mut chunk_infos = Vec::new();
1213 let mut data_offset = 0x2000usize;
1214
1215 let mut start = 0;
1216 while start < total {
1217 let end = (start + chunk_size_elems).min(total);
1218 let chunk_bytes = chunk_size_elems * elem_size; for i in start..end {
1222 let byte_offset = data_offset + (i - start) * elem_size;
1223 file_data[byte_offset..byte_offset + 8]
1224 .copy_from_slice(&values[i].to_le_bytes());
1225 }
1226
1227 chunk_infos.push(ChunkInfo {
1228 chunk_size: chunk_bytes as u32,
1229 filter_mask: 0,
1230 offsets: vec![start as u64, 0],
1231 address: data_offset as u64,
1232 });
1233
1234 data_offset += chunk_bytes;
1235 start += chunk_size_elems;
1236 }
1237
1238 let btree = build_chunk_btree_leaf(&chunk_infos, ndims, os);
1240 let btree_addr = 0x100usize;
1241 file_data[btree_addr..btree_addr + btree.len()].copy_from_slice(&btree);
1242
1243 let layout = DataLayout::Chunked {
1244 chunk_dimensions: vec![chunk_size_elems as u32, elem_size as u32],
1245 btree_address: Some(btree_addr as u64),
1246 version: 3,
1247 chunk_index_type: None,
1248 single_chunk_filtered_size: None,
1249 single_chunk_filter_mask: None,
1250 };
1251
1252 let dataspace = Dataspace {
1253 space_type: DataspaceType::Simple,
1254 rank: 1,
1255 dimensions: vec![total as u64],
1256 max_dimensions: None,
1257 };
1258
1259 (file_data, layout, dataspace)
1260 }
1261
1262 #[test]
1263 fn read_1d_two_chunks_no_compression() {
1264 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1265 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1266 let datatype = make_f64_type();
1267
1268 let raw = read_chunked_data(&file_data, &layout, &dataspace, &datatype, None, 8, 8)
1269 .unwrap();
1270 assert_eq!(raw.len(), 20 * 8);
1271
1272 for i in 0..20 {
1274 let val = f64::from_le_bytes(raw[i * 8..(i + 1) * 8].try_into().unwrap());
1275 assert_eq!(val, i as f64);
1276 }
1277 }
1278
1279 #[test]
1280 fn read_1d_three_chunks_partial_last() {
1281 let values: Vec<f64> = (0..25).map(|i| i as f64).collect();
1283 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1284 let datatype = make_f64_type();
1285
1286 let raw = read_chunked_data(&file_data, &layout, &dataspace, &datatype, None, 8, 8)
1287 .unwrap();
1288 assert_eq!(raw.len(), 25 * 8);
1289
1290 for i in 0..25 {
1291 let val = f64::from_le_bytes(raw[i * 8..(i + 1) * 8].try_into().unwrap());
1292 assert_eq!(val, i as f64, "mismatch at index {i}");
1293 }
1294 }
1295
1296 #[cfg(feature = "deflate")]
1297 #[test]
1298 fn read_1d_two_chunks_with_deflate() {
1299 use crate::filter_pipeline::{FilterDescription, FilterPipeline, FILTER_DEFLATE};
1300 use crate::filters::compress_chunk;
1301
1302 let os: u8 = 8;
1303 let elem_size = 8usize;
1304 let ndims = 2;
1305 let chunk_elems = 10usize;
1306 let total = 20usize;
1307
1308 let pipeline = FilterPipeline {
1309 version: 2,
1310 filters: vec![FilterDescription {
1311 filter_id: FILTER_DEFLATE,
1312 name: None,
1313 flags: 0,
1314 client_data: vec![6],
1315 }],
1316 };
1317
1318 let values: Vec<f64> = (0..total).map(|i| i as f64).collect();
1319 let mut file_data = vec![0u8; 0x10000];
1320 let mut chunk_infos = Vec::new();
1321 let mut data_offset = 0x2000usize;
1322
1323 for chunk_idx in 0..2 {
1324 let start = chunk_idx * chunk_elems;
1325 let mut chunk_bytes = Vec::new();
1326 for i in start..start + chunk_elems {
1327 chunk_bytes.extend_from_slice(&values[i].to_le_bytes());
1328 }
1329 let compressed = compress_chunk(&chunk_bytes, &pipeline, elem_size as u32).unwrap();
1330
1331 file_data[data_offset..data_offset + compressed.len()]
1332 .copy_from_slice(&compressed);
1333
1334 chunk_infos.push(ChunkInfo {
1335 chunk_size: compressed.len() as u32,
1336 filter_mask: 0,
1337 offsets: vec![start as u64, 0],
1338 address: data_offset as u64,
1339 });
1340
1341 data_offset += compressed.len() + 16; }
1343
1344 let btree = build_chunk_btree_leaf(&chunk_infos, ndims, os);
1345 let btree_addr = 0x100usize;
1346 file_data[btree_addr..btree_addr + btree.len()].copy_from_slice(&btree);
1347
1348 let layout = DataLayout::Chunked {
1349 chunk_dimensions: vec![chunk_elems as u32, elem_size as u32],
1350 btree_address: Some(btree_addr as u64),
1351 version: 3,
1352 chunk_index_type: None,
1353 single_chunk_filtered_size: None,
1354 single_chunk_filter_mask: None,
1355 };
1356 let dataspace = Dataspace {
1357 space_type: DataspaceType::Simple,
1358 rank: 1,
1359 dimensions: vec![total as u64],
1360 max_dimensions: None,
1361 };
1362 let datatype = make_f64_type();
1363
1364 let raw = read_chunked_data(
1365 &file_data,
1366 &layout,
1367 &dataspace,
1368 &datatype,
1369 Some(&pipeline),
1370 8,
1371 8,
1372 )
1373 .unwrap();
1374
1375 for i in 0..total {
1376 let val = f64::from_le_bytes(raw[i * 8..(i + 1) * 8].try_into().unwrap());
1377 assert_eq!(val, i as f64, "mismatch at index {i}");
1378 }
1379 }
1380
1381 #[test]
1382 fn read_2d_four_chunks() {
1383 let os: u8 = 8;
1385 let elem_size = 4usize; let ndims = 3; let ds_dims = [4usize, 6];
1388 let chunk_dims = [2usize, 3];
1389
1390 let values: Vec<f32> = (0..24).map(|i| i as f32).collect();
1391 let mut file_data = vec![0u8; 0x10000];
1392 let mut chunk_infos = Vec::new();
1393 let mut data_offset = 0x2000usize;
1394
1395 for row_start in (0..ds_dims[0]).step_by(chunk_dims[0]) {
1397 for col_start in (0..ds_dims[1]).step_by(chunk_dims[1]) {
1398 let mut chunk_bytes = Vec::new();
1399 for r in 0..chunk_dims[0] {
1400 for c in 0..chunk_dims[1] {
1401 let gr = row_start + r;
1402 let gc = col_start + c;
1403 let val = if gr < ds_dims[0] && gc < ds_dims[1] {
1404 values[gr * ds_dims[1] + gc]
1405 } else {
1406 0.0
1407 };
1408 chunk_bytes.extend_from_slice(&val.to_le_bytes());
1409 }
1410 }
1411
1412 let chunk_size = chunk_bytes.len();
1413 file_data[data_offset..data_offset + chunk_size]
1414 .copy_from_slice(&chunk_bytes);
1415
1416 chunk_infos.push(ChunkInfo {
1417 chunk_size: chunk_size as u32,
1418 filter_mask: 0,
1419 offsets: vec![row_start as u64, col_start as u64, 0],
1420 address: data_offset as u64,
1421 });
1422
1423 data_offset += chunk_size + 8;
1424 }
1425 }
1426
1427 let btree = build_chunk_btree_leaf(&chunk_infos, ndims, os);
1428 let btree_addr = 0x100usize;
1429 file_data[btree_addr..btree_addr + btree.len()].copy_from_slice(&btree);
1430
1431 let layout = DataLayout::Chunked {
1432 chunk_dimensions: vec![chunk_dims[0] as u32, chunk_dims[1] as u32, elem_size as u32],
1433 btree_address: Some(btree_addr as u64),
1434 version: 3,
1435 chunk_index_type: None,
1436 single_chunk_filtered_size: None,
1437 single_chunk_filter_mask: None,
1438 };
1439 let dataspace = Dataspace {
1440 space_type: DataspaceType::Simple,
1441 rank: 2,
1442 dimensions: vec![ds_dims[0] as u64, ds_dims[1] as u64],
1443 max_dimensions: None,
1444 };
1445 let datatype = make_f32_type();
1446
1447 let raw = read_chunked_data(&file_data, &layout, &dataspace, &datatype, None, 8, 8)
1448 .unwrap();
1449 assert_eq!(raw.len(), 24 * 4);
1450
1451 for i in 0..24 {
1452 let val = f32::from_le_bytes(raw[i * 4..(i + 1) * 4].try_into().unwrap());
1453 assert_eq!(val, i as f32, "mismatch at element {i}");
1454 }
1455 }
1456
1457 #[test]
1458 fn wrong_node_type_error() {
1459 let mut buf = Vec::new();
1461 buf.extend_from_slice(b"TREE");
1462 buf.push(0); buf.push(0);
1464 buf.extend_from_slice(&0u16.to_le_bytes());
1465 buf.extend_from_slice(&[0xFF; 16]); buf.extend_from_slice(&[0u8; 24]);
1468
1469 let mut file_data = vec![0u8; 512];
1470 file_data[..buf.len()].copy_from_slice(&buf);
1471
1472 let err = collect_chunk_info(&file_data, 0, 2, 8, 8).unwrap_err();
1473 assert_eq!(err, FormatError::InvalidBTreeNodeType(0));
1474 }
1475
1476 #[test]
1479 fn implicit_chunks_1d_five_chunks() {
1480 let chunks = generate_implicit_chunks(
1481 0x1000,
1482 &[100],
1483 &[20],
1484 8, );
1486 assert_eq!(chunks.len(), 5);
1487 let chunk_byte_size = 20 * 8;
1488 for (i, c) in chunks.iter().enumerate() {
1489 assert_eq!(c.address, 0x1000 + i as u64 * chunk_byte_size as u64);
1490 assert_eq!(c.offsets, vec![i as u64 * 20]);
1491 assert_eq!(c.filter_mask, 0);
1492 assert_eq!(c.chunk_size, chunk_byte_size as u32);
1493 }
1494 }
1495
1496 #[test]
1497 fn implicit_chunks_2d() {
1498 let chunks = generate_implicit_chunks(
1500 0x2000,
1501 &[10, 6],
1502 &[4, 3],
1503 4, );
1505 assert_eq!(chunks.len(), 6);
1506 let chunk_byte_size = 4 * 3 * 4;
1507 assert_eq!(chunks[0].offsets, vec![0, 0]);
1509 assert_eq!(chunks[1].offsets, vec![0, 3]);
1510 assert_eq!(chunks[2].offsets, vec![4, 0]);
1511 assert_eq!(chunks[3].offsets, vec![4, 3]);
1512 assert_eq!(chunks[4].offsets, vec![8, 0]);
1513 assert_eq!(chunks[5].offsets, vec![8, 3]);
1514 for (i, c) in chunks.iter().enumerate() {
1515 assert_eq!(c.address, 0x2000 + i as u64 * chunk_byte_size as u64);
1516 }
1517 }
1518
1519 #[test]
1520 fn implicit_chunks_partial_last() {
1521 let chunks = generate_implicit_chunks(0x0, &[25], &[10], 8);
1523 assert_eq!(chunks.len(), 3);
1524 assert_eq!(chunks[0].offsets, vec![0]);
1525 assert_eq!(chunks[1].offsets, vec![10]);
1526 assert_eq!(chunks[2].offsets, vec![20]);
1527 }
1528
1529 #[test]
1532 fn read_v4_single_chunk_synthetic() {
1533 let values: Vec<f64> = vec![10.0, 20.0, 30.0];
1535 let elem_size = 8usize;
1536 let chunk_elems = 3usize;
1537
1538 let mut file_data = vec![0u8; 0x2000];
1539 let data_addr = 0x1000usize;
1540 for (i, &v) in values.iter().enumerate() {
1541 file_data[data_addr + i * elem_size..data_addr + (i + 1) * elem_size]
1542 .copy_from_slice(&v.to_le_bytes());
1543 }
1544
1545 let layout = DataLayout::Chunked {
1546 chunk_dimensions: vec![chunk_elems as u32, elem_size as u32],
1547 btree_address: Some(data_addr as u64),
1548 version: 4,
1549 chunk_index_type: Some(1),
1550 single_chunk_filtered_size: None,
1551 single_chunk_filter_mask: None,
1552 };
1553 let dataspace = Dataspace {
1554 space_type: DataspaceType::Simple,
1555 rank: 1,
1556 dimensions: vec![3],
1557 max_dimensions: None,
1558 };
1559 let datatype = make_f64_type();
1560
1561 let raw = read_chunked_data(&file_data, &layout, &dataspace, &datatype, None, 8, 8)
1562 .unwrap();
1563 assert_eq!(raw.len(), 24);
1564 for i in 0..3 {
1565 let val = f64::from_le_bytes(raw[i * 8..(i + 1) * 8].try_into().unwrap());
1566 assert_eq!(val, values[i]);
1567 }
1568 }
1569
1570 use crate::chunk_cache::ChunkCache;
1573
1574 #[test]
1575 fn cached_read_populates_index_and_returns_correct_data() {
1576 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1577 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1578 let datatype = make_f64_type();
1579 let cache = ChunkCache::new();
1580
1581 assert!(!cache.has_index());
1582 let raw = read_chunked_data_cached(
1583 &file_data, &layout, &dataspace, &datatype, None, 8, 8, &cache,
1584 ).unwrap();
1585 assert!(cache.has_index());
1586 assert_eq!(raw.len(), 20 * 8);
1587 for i in 0..20 {
1588 let val = f64::from_le_bytes(raw[i * 8..(i + 1) * 8].try_into().unwrap());
1589 assert_eq!(val, i as f64);
1590 }
1591 }
1592
1593 #[test]
1594 fn cached_read_second_call_uses_cache() {
1595 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1596 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1597 let datatype = make_f64_type();
1598 let cache = ChunkCache::new();
1599
1600 let raw1 = read_chunked_data_cached(
1602 &file_data, &layout, &dataspace, &datatype, None, 8, 8, &cache,
1603 ).unwrap();
1604 assert!(cache.has_index());
1605 assert!(cache.cached_chunk_count() > 0);
1606
1607 let raw2 = read_chunked_data_cached(
1609 &file_data, &layout, &dataspace, &datatype, None, 8, 8, &cache,
1610 ).unwrap();
1611 assert_eq!(raw1, raw2);
1612 }
1613
1614 #[test]
1615 fn cached_read_with_partial_last_chunk() {
1616 let values: Vec<f64> = (0..25).map(|i| i as f64).collect();
1617 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1618 let datatype = make_f64_type();
1619 let cache = ChunkCache::new();
1620
1621 let raw = read_chunked_data_cached(
1622 &file_data, &layout, &dataspace, &datatype, None, 8, 8, &cache,
1623 ).unwrap();
1624 assert_eq!(raw.len(), 25 * 8);
1625 for i in 0..25 {
1626 let val = f64::from_le_bytes(raw[i * 8..(i + 1) * 8].try_into().unwrap());
1627 assert_eq!(val, i as f64, "mismatch at index {i}");
1628 }
1629 }
1630
1631 #[test]
1634 fn sweep_read_returns_correct_data() {
1635 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1636 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1637 let datatype = make_f64_type();
1638 let cache = ChunkCache::new();
1639 let mut sweep = SweepContext::with_defaults();
1640
1641 let raw = read_chunked_data_sweep(
1642 &file_data, &layout, &dataspace, &datatype, None, 8, 8, &cache, &mut sweep,
1643 ).unwrap();
1644 assert_eq!(raw.len(), 20 * 8);
1645 for i in 0..20 {
1646 let val = f64::from_le_bytes(raw[i * 8..(i + 1) * 8].try_into().unwrap());
1647 assert_eq!(val, i as f64);
1648 }
1649 }
1650
1651 #[test]
1652 fn sweep_read_populates_sweep_context() {
1653 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1654 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1655 let datatype = make_f64_type();
1656 let cache = ChunkCache::new();
1657 let mut sweep = SweepContext::with_defaults();
1658
1659 read_chunked_data_sweep(
1660 &file_data, &layout, &dataspace, &datatype, None, 8, 8, &cache, &mut sweep,
1661 ).unwrap();
1662
1663 assert!(!sweep.history.is_empty());
1665 }
1666
1667 #[test]
1668 fn sweep_context_unit_test() {
1669 let mut ctx = SweepContext::with_defaults();
1670 ctx.record(vec![0, 0], 2);
1671 ctx.record(vec![0, 10], 2);
1672 ctx.record(vec![0, 20], 2);
1673 assert_eq!(ctx.direction, "row_major");
1674 assert!(!ctx.predicted_next.is_empty());
1675 assert_eq!(ctx.predicted_next[0], vec![0, 30]);
1676 }
1677
1678 #[test]
1679 fn sweep_context_random() {
1680 let mut ctx = SweepContext::with_defaults();
1681 ctx.record(vec![0, 0], 2);
1682 ctx.record(vec![30, 20], 2);
1683 ctx.record(vec![10, 0], 2);
1684 assert_eq!(ctx.direction, "random");
1685 assert!(ctx.predicted_next.is_empty());
1686 }
1687
1688 #[test]
1689 fn sweep_read_access_stats() {
1690 let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1691 let (file_data, layout, dataspace) = build_1d_chunked_file(&values, 10);
1692 let datatype = make_f64_type();
1693 let cache = ChunkCache::new();
1694 let mut sweep = SweepContext::with_defaults();
1695
1696 read_chunked_data_sweep(
1697 &file_data, &layout, &dataspace, &datatype, None, 8, 8, &cache, &mut sweep,
1698 ).unwrap();
1699
1700 let stats = cache.access_stats();
1701 assert!(stats.sequential_count > 0 || stats.random_count > 0);
1703 }
1704}