Skip to main content

rustyhdf5_format/
chunked_read.rs

1//! Chunked dataset reading: B-tree v1 type 1 traversal and chunk assembly.
2
3#[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
25/// Decompress all chunks into cache-line-aligned buffers, using lane-partitioned
26/// parallel decompression when the `parallel` feature is enabled and the chunk
27/// count exceeds the threshold.
28fn 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                // Seed from the first chunk's address and count for determinism.
40                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, // auto-detect lane count
52                )?;
53                return Ok(data.into_iter().map(CacheAlignedBuffer::from_vec).collect());
54            }
55        }
56    }
57
58    // Sequential fallback — allocate into aligned buffers
59    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/// Decompress all chunks with lane-partitioned parallelism and return
86/// per-lane diagnostics.
87///
88/// This is the stats-returning variant for callers who want to inspect
89/// the partition balance.  Only available with the `parallel` feature.
90#[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/// Information about a single chunk in a chunked dataset.
112#[derive(Debug, Clone)]
113pub struct ChunkInfo {
114    /// Size of chunk data in the file (after compression).
115    pub chunk_size: u32,
116    /// Bitmask of filters that were NOT applied (0 = all applied).
117    pub filter_mask: u32,
118    /// N-dimensional offset of this chunk in dataset space.
119    pub offsets: Vec<u64>,
120    /// File address of the chunk data.
121    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
143/// Traverse B-tree v1 type 1 to collect all chunk locations.
144///
145/// `ndims` is the number of offset dimensions in each key, which equals
146/// `chunk_dimensions.len()` from the DataLayout::Chunked message (rank+1).
147pub 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    // Parse B-tree v1 header
158    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; // skip left/right sibling
180
181    // Key size: chunk_size(4) + filter_mask(4) + ndims * offset_size
182    let key_size = 4 + 4 + ndims * os;
183
184    if node_level == 0 {
185        // Leaf node: keys and children interleaved
186        // key[0], child[0], key[1], child[1], ..., key[N-1], child[N-1], key[N]
187        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            // Parse key
198            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            // Parse child address
219            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        // Skip final key
230        Ok(chunks)
231    } else {
232        // Internal node: recurse into children
233        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; // skip key
244            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
259/// Generate ChunkInfo entries for an implicit index (v4 index type 2).
260///
261/// Chunks are stored contiguously starting at `base_address`. No stored index;
262/// addresses are computed from the chunk position.
263pub 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
303/// Read a chunked dataset, decompressing chunks as needed.
304pub 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    // Both v3 and v4 include element size as last dim (rank+1)
338    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    // Collect chunks based on version and index type
356    let chunks = match (version, chunk_index_type) {
357        (3, _) => {
358            let ndims = chunk_dimensions.len(); // rank+1
359            collect_chunk_info(file_data, addr, ndims, offset_size, length_size)?
360        }
361        (4, Some(1)) => {
362            // Single chunk — one chunk covering the entire dataset
363            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            // Implicit index — use spatial chunk dims only
378            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            // Fixed Array — use spatial chunk dims only
388            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            // Extensible Array — use spatial chunk dims only
397            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    // Assemble output
414    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    // Decompress all chunks (parallel when beneficial, sequential otherwise)
432    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        // B-tree v1 (v3) offsets have rank+1 dims; v4 index offsets have rank dims
442        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
468/// Read a chunked dataset with caching support.
469///
470/// On the first call, scans the chunk index (B-tree / fixed array / etc.) once
471/// and populates the cache's hash index.  Subsequent calls skip the index scan
472/// entirely.  Decompressed chunk data is also cached with LRU eviction.
473pub 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    // Populate chunk index on first access
522    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    // Assemble output
573    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        // Try decompressed cache first
594        let decompressed = if let Some(cached) = cache.get_decompressed(&coord) {
595            cached
596        } else {
597            // Decompress from file
598            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
646/// Sweep context passed into `read_chunked_data_sweep` to enable adaptive
647/// prefetching based on detected access patterns.
648///
649/// The caller is responsible for maintaining the `SweepContext` across
650/// multiple reads on the same dataset. After each read, the context will
651/// contain updated sweep detection state and any predicted next-chunk
652/// coordinates.
653pub struct SweepContext {
654    /// Sliding window of recent chunk coordinates.
655    pub history: Vec<Vec<u64>>,
656    /// Maximum window size.
657    pub window_size: usize,
658    /// Currently detected sweep direction label.
659    pub direction: &'static str,
660    /// How many chunks ahead to predict.
661    pub prefetch_count: usize,
662    /// Predicted next chunk coordinates (populated after each read).
663    pub predicted_next: Vec<Vec<u64>>,
664}
665
666impl SweepContext {
667    /// Create a new sweep context with the given window size and prefetch count.
668    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    /// Create with default settings (window=12, prefetch=4).
679    pub fn with_defaults() -> Self {
680        Self::new(12, 4)
681    }
682
683    /// Record a chunk coordinate access and update predictions.
684    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        // Inline sweep detection matching the algorithm in rustyhdf5-io/sweep.rs
697        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        // Predict next chunks
745        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/// Read a chunked dataset with caching and sweep-aware prefetching.
784///
785/// Extends `read_chunked_data_cached` by feeding each chunk coordinate to a
786/// [`SweepContext`]. When a sweep pattern is detected, predicted next-chunk
787/// coordinates are pre-populated in the cache index via `prefetch_hint`.
788#[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    // Populate chunk index on first access
839    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    // Assemble output
890    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        // Feed coordinate to sweep detector
911        sweep.record(coord.clone(), rank);
912
913        // Issue prefetch hint for predicted next chunks
914        if !sweep.predicted_next.is_empty() {
915            cache.prefetch_hint(&sweep.predicted_next);
916            cache.set_sweep_direction(sweep.direction);
917        }
918
919        // Try decompressed cache first
920        let decompressed = if let Some(cached) = cache.get_decompressed(&coord) {
921            cached
922        } else {
923            // Decompress from file
924            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/// Copy chunk data into the output buffer at the correct N-D position.
973#[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    // Iterate over all elements in the chunk using a flat index
986    let chunk_total: usize = chunk_dims.iter().product();
987    for flat_idx in 0..chunk_total {
988        // Convert flat index to N-D chunk-local coordinates
989        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    /// Build a B-tree v1 type 1 leaf node with given chunk infos.
1032    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        // Header
1042        buf.extend_from_slice(b"TREE");
1043        buf.push(1); // node_type = 1 (raw data chunks)
1044        buf.push(0); // node_level = 0 (leaf)
1045        buf.extend_from_slice(&entries_used.to_le_bytes());
1046
1047        // Left/right sibling = undefined
1048        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        // Entries: key[i], child[i] pairs, then final key
1057        for chunk in chunks {
1058            // Key: chunk_size(4) + filter_mask(4) + ndims offsets
1059            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            // Child: address
1070            write_offset(&mut buf, chunk.address, offset_size);
1071        }
1072
1073        // Final key (dummy)
1074        buf.extend_from_slice(&0u32.to_le_bytes()); // chunk_size
1075        buf.extend_from_slice(&0u32.to_le_bytes()); // filter_mask
1076        for _ in 0..ndims {
1077            write_offset(&mut buf, u64::MAX, offset_size);
1078        }
1079
1080        buf
1081    }
1082
1083    // --- ChunkInfo collection tests ---
1084
1085    #[test]
1086    fn collect_two_chunks_from_leaf() {
1087        let ndims = 2; // rank+1 for 1D dataset
1088        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    // --- Chunked read tests (synthetic) ---
1168
1169    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    /// Build a synthetic file with a B-tree and chunk data for a 1D uncompressed dataset.
1201    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; // rank(1) + 1
1208        let total = values.len();
1209
1210        // Place chunk data starting at offset 0x2000
1211        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; // full chunk allocation
1219
1220            // Write chunk data (full chunk size, padding with zeros)
1221            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        // Build B-tree at offset 0x100
1239        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        // Verify values
1273        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        // 25 elements, chunk size 10 => 3 chunks, last has only 5 valid
1282        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; // some padding
1342        }
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        // 4x6 dataset with chunk size 2x3 => 4 chunks
1384        let os: u8 = 8;
1385        let elem_size = 4usize; // f32
1386        let ndims = 3; // rank(2) + 1
1387        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        // Generate chunks: (0,0), (0,3), (2,0), (2,3)
1396        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        // Build a type-0 B-tree and try to collect chunk info
1460        let mut buf = Vec::new();
1461        buf.extend_from_slice(b"TREE");
1462        buf.push(0); // type 0, not 1
1463        buf.push(0);
1464        buf.extend_from_slice(&0u16.to_le_bytes());
1465        buf.extend_from_slice(&[0xFF; 16]); // siblings
1466        // final key
1467        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    // --- Implicit chunk generation tests ---
1477
1478    #[test]
1479    fn implicit_chunks_1d_five_chunks() {
1480        let chunks = generate_implicit_chunks(
1481            0x1000,
1482            &[100],
1483            &[20],
1484            8, // f64
1485        );
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        // 10x6 dataset, 4x3 chunks => ceil(10/4)=3, ceil(6/3)=2 => 6 chunks
1499        let chunks = generate_implicit_chunks(
1500            0x2000,
1501            &[10, 6],
1502            &[4, 3],
1503            4, // f32
1504        );
1505        assert_eq!(chunks.len(), 6);
1506        let chunk_byte_size = 4 * 3 * 4;
1507        // Row-major: (0,0), (0,3), (4,0), (4,3), (8,0), (8,3)
1508        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        // 25 elements, chunk size 10 => 3 chunks (last partial)
1522        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    // --- V4 single chunk synthetic test ---
1530
1531    #[test]
1532    fn read_v4_single_chunk_synthetic() {
1533        // Build a synthetic v4 single chunk dataset (no filters)
1534        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    // --- Cached read tests ---
1571
1572    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        // First read — populates index + decompressed cache
1601        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        // Second read — should hit the decompressed cache
1608        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    // --- Sweep-aware read tests ---
1632
1633    #[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        // After reading 2 chunks (offsets [0] and [10]), history should be populated
1664        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        // We accessed 2 chunks; the second should be sequential to the first
1702        assert!(stats.sequential_count > 0 || stats.random_count > 0);
1703    }
1704}