Skip to main content

hdf5_reader/
dataset.rs

1use std::mem::MaybeUninit;
2use std::num::NonZeroUsize;
3use std::sync::{Arc, OnceLock};
4
5use lru::LruCache;
6use ndarray::{ArrayD, IxDyn};
7use parking_lot::Mutex;
8#[cfg(feature = "rayon")]
9use rayon::prelude::*;
10use smallvec::SmallVec;
11
12use crate::attribute_api::{
13    collect_attribute_messages_storage, decode_string, decoded_vlen_strings_storage,
14    read_one_vlen_string_storage, resolve_vlen_bytes_storage, Attribute,
15};
16use crate::cache::{ChunkCache, ChunkCacheStats, ChunkKey};
17use crate::chunk_index;
18use crate::datatype_api::H5Type;
19use crate::error::{ByteOrder, Error, Result};
20use crate::filters::{self, FilterRegistry};
21use crate::io::Cursor;
22use crate::local_heap::LocalHeap;
23use crate::messages::attribute::AttributeMessage;
24use crate::messages::dataspace::{DataspaceMessage, DataspaceType};
25use crate::messages::datatype::{Datatype, StringSize, VarLenKind};
26use crate::messages::external_files::ExternalFilesMessage;
27use crate::messages::fill_value::{FillTime, FillValueMessage};
28use crate::messages::filter_pipeline::FilterPipelineMessage;
29use crate::messages::layout::{ChunkIndexing, DataLayout};
30use crate::messages::HdfMessage;
31use crate::object_header::ObjectHeader;
32use crate::storage::DynStorage;
33use crate::FileContext;
34
35const HOT_FULL_DATASET_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
36
37#[derive(Clone, Copy)]
38struct FlatBufferPtr {
39    ptr: *mut u8,
40    len: usize,
41}
42
43#[derive(Clone, Copy)]
44struct ChunkCopyLayout<'a> {
45    chunk_offsets: &'a [u64],
46    chunk_shape: &'a [u64],
47    dataset_shape: &'a [u64],
48    dataset_strides: &'a [usize],
49    chunk_strides: &'a [usize],
50    elem_size: usize,
51}
52
53#[derive(Clone, Copy)]
54struct UnitStrideCopyLayout<'a> {
55    chunk_offsets: &'a [u64],
56    chunk_shape: &'a [u64],
57    dataset_shape: &'a [u64],
58    resolved: &'a ResolvedSelection,
59    chunk_strides: &'a [usize],
60    result_strides: &'a [usize],
61    elem_size: usize,
62}
63
64#[derive(Clone, Copy)]
65struct ContiguousSliceDirectLayout<'a> {
66    dataset_strides: &'a [usize],
67    result_strides: &'a [usize],
68    elem_size: usize,
69    result_total_bytes: usize,
70}
71
72#[derive(Clone)]
73struct ResolvedExternalRawSlot {
74    logical_offset: u64,
75    storage: DynStorage,
76    file_offset: u64,
77    size: u64,
78}
79
80pub(crate) struct DatasetParseContext {
81    pub(crate) context: Arc<FileContext>,
82}
83
84#[derive(Clone, Copy)]
85struct ChunkEntrySelection<'a> {
86    shape: &'a [u64],
87    ndim: usize,
88    elem_size: usize,
89    chunk_bounds: Option<(&'a [u64], &'a [u64])>,
90}
91
92// SAFETY: `FlatBufferPtr` has no safe dereference operations. Callers of its
93// unsafe methods must keep the allocation alive and coordinate disjoint writes.
94unsafe impl Send for FlatBufferPtr {}
95
96// SAFETY: sharing the pointer does not itself access the allocation. Every
97// access remains unsafe and requires callers to guarantee disjoint writes.
98unsafe impl Sync for FlatBufferPtr {}
99
100impl FlatBufferPtr {
101    #[cfg(feature = "rayon")]
102    #[inline(always)]
103    unsafe fn copy_chunk(self, chunk_data: &[u8], layout: ChunkCopyLayout<'_>) -> Result<()> {
104        // SAFETY: the caller upholds this method's allocation-lifetime and
105        // disjoint-write requirements.
106        unsafe { copy_chunk_to_flat_with_strides_ptr(chunk_data, self, layout) }
107    }
108
109    #[cfg(feature = "rayon")]
110    #[inline(always)]
111    unsafe fn copy_selected(
112        self,
113        chunk_data: &[u8],
114        dim_indices: &[Vec<(usize, usize)>],
115        chunk_strides: &[usize],
116        result_strides: &[usize],
117        elem_size: usize,
118        ndim: usize,
119    ) -> Result<()> {
120        // SAFETY: the caller upholds this method's allocation-lifetime and
121        // disjoint-write requirements.
122        unsafe {
123            copy_selected_elements_ptr(
124                chunk_data,
125                self.ptr,
126                self.len,
127                dim_indices,
128                chunk_strides,
129                result_strides,
130                elem_size,
131                ndim,
132            )
133        }
134    }
135
136    #[cfg(feature = "rayon")]
137    #[inline(always)]
138    unsafe fn copy_unit_stride_chunk_overlap(
139        self,
140        chunk_data: &[u8],
141        layout: UnitStrideCopyLayout<'_>,
142    ) -> Result<()> {
143        // SAFETY: the caller upholds this method's allocation-lifetime and
144        // disjoint-write requirements.
145        unsafe { copy_unit_stride_chunk_overlap_ptr(chunk_data, self, layout) }
146    }
147}
148
149/// Hyperslab selection for reading slices of datasets.
150#[derive(Debug, Clone)]
151pub struct SliceInfo {
152    pub selections: Vec<SliceInfoElem>,
153}
154
155/// A single dimension's selection.
156#[derive(Debug, Clone)]
157pub enum SliceInfoElem {
158    /// Select a single index (reduces dimensionality).
159    Index(u64),
160    /// Select a range with optional step.
161    Slice { start: u64, end: u64, step: u64 },
162}
163
164#[derive(Clone, Debug)]
165struct ResolvedSelectionDim {
166    start: u64,
167    end: u64,
168    step: u64,
169    count: usize,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Hash)]
173struct ChunkEntryCacheKey {
174    index_address: u64,
175    first_chunk: SmallVec<[u64; 4]>,
176    last_chunk: SmallVec<[u64; 4]>,
177}
178
179impl ResolvedSelectionDim {
180    fn chunk_index_range(&self, chunk_extent: u64) -> Option<(u64, u64)> {
181        if self.count == 0 {
182            return None;
183        }
184
185        Some((self.start / chunk_extent, (self.end - 1) / chunk_extent))
186    }
187}
188
189#[derive(Clone, Debug)]
190struct ResolvedSelection {
191    dims: Vec<ResolvedSelectionDim>,
192    result_shape: Vec<usize>,
193    result_elements: usize,
194}
195
196impl ResolvedSelection {
197    fn result_dims_with_collapsed(&self) -> Vec<usize> {
198        self.dims.iter().map(|dim| dim.count).collect()
199    }
200
201    fn is_unit_stride(&self) -> bool {
202        self.dims.iter().all(|dim| dim.step == 1)
203    }
204}
205
206impl SliceInfo {
207    /// Create a selection that reads everything.
208    pub fn all(ndim: usize) -> Self {
209        SliceInfo {
210            selections: vec![
211                SliceInfoElem::Slice {
212                    start: 0,
213                    end: u64::MAX,
214                    step: 1,
215                };
216                ndim
217            ],
218        }
219    }
220}
221
222fn checked_usize(value: u64, context: &str) -> Result<usize> {
223    usize::try_from(value).map_err(|_| {
224        Error::InvalidData(format!(
225            "{context} value {value} exceeds platform usize capacity"
226        ))
227    })
228}
229
230fn checked_mul_usize(lhs: usize, rhs: usize, context: &str) -> Result<usize> {
231    lhs.checked_mul(rhs)
232        .ok_or_else(|| Error::InvalidData(format!("{context} exceeds platform usize capacity")))
233}
234
235fn checked_add_usize(lhs: usize, rhs: usize, context: &str) -> Result<usize> {
236    lhs.checked_add(rhs)
237        .ok_or_else(|| Error::InvalidData(format!("{context} exceeds platform usize capacity")))
238}
239
240fn checked_mul_u64(lhs: u64, rhs: u64, context: &str) -> Result<u64> {
241    lhs.checked_mul(rhs)
242        .ok_or_else(|| Error::InvalidData(format!("{context} exceeds u64 capacity")))
243}
244
245fn checked_add_u64(lhs: u64, rhs: u64, context: &str) -> Result<u64> {
246    lhs.checked_add(rhs)
247        .ok_or_else(|| Error::InvalidData(format!("{context} exceeds u64 capacity")))
248}
249
250fn checked_shape_elements_usize(shape: &[u64], context: &str) -> Result<usize> {
251    let mut total = 1usize;
252    for &dim in shape {
253        total = checked_mul_usize(total, checked_usize(dim, context)?, context)?;
254    }
255    Ok(total)
256}
257
258fn full_dataset_chunk_bounds(
259    shape: &[u64],
260    chunk_shape: &[u64],
261) -> Result<Option<(Vec<u64>, Vec<u64>)>> {
262    validate_chunk_shape(shape, chunk_shape)?;
263    if shape.contains(&0) {
264        return Ok(None);
265    }
266
267    let first_chunk = vec![0u64; shape.len()];
268    let last_chunk = shape
269        .iter()
270        .zip(chunk_shape.iter())
271        .map(|(&dim, &chunk)| dim.div_ceil(chunk) - 1)
272        .collect();
273    Ok(Some((first_chunk, last_chunk)))
274}
275
276fn validate_chunk_shape(shape: &[u64], chunk_shape: &[u64]) -> Result<()> {
277    if chunk_shape.len() != shape.len() {
278        return Err(Error::InvalidData(format!(
279            "chunk rank {} does not match dataset rank {}",
280            chunk_shape.len(),
281            shape.len()
282        )));
283    }
284    if let Some((dim, _)) = chunk_shape
285        .iter()
286        .enumerate()
287        .find(|(_, chunk)| **chunk == 0)
288    {
289        return Err(Error::InvalidData(format!(
290            "chunk dimension {dim} has zero extent"
291        )));
292    }
293    Ok(())
294}
295
296fn validate_decoded_chunk_len(
297    entry: &chunk_index::ChunkEntry,
298    chunk_shape: &[u64],
299    elem_size: usize,
300    actual_len: usize,
301) -> Result<()> {
302    let expected_len = decoded_chunk_expected_len(chunk_shape, elem_size)?;
303    if actual_len != expected_len {
304        return Err(Error::InvalidData(format!(
305            "chunk at offsets {:?} decoded to {} bytes, expected {} bytes",
306            entry.offsets, actual_len, expected_len
307        )));
308    }
309    Ok(())
310}
311
312fn decoded_chunk_expected_len(chunk_shape: &[u64], elem_size: usize) -> Result<usize> {
313    let chunk_elements = checked_shape_elements_usize(chunk_shape, "decoded chunk element count")?;
314    checked_mul_usize(chunk_elements, elem_size, "decoded chunk byte length")
315}
316
317fn validate_chunk_grid_coverage(
318    entries: &mut [chunk_index::ChunkEntry],
319    shape: &[u64],
320    chunk_shape: &[u64],
321    first_chunk: &[u64],
322    last_chunk: &[u64],
323) -> Result<bool> {
324    validate_chunk_shape(shape, chunk_shape)?;
325    if first_chunk.len() != shape.len() || last_chunk.len() != shape.len() {
326        return Err(Error::InvalidData(format!(
327            "chunk grid bounds rank does not match dataset rank {}",
328            shape.len()
329        )));
330    }
331
332    if shape.contains(&0) {
333        if entries.is_empty() {
334            return Ok(true);
335        }
336        return Err(Error::InvalidData(
337            "chunk index contains entries for an empty dataset".into(),
338        ));
339    }
340
341    for dim in 0..shape.len() {
342        if first_chunk[dim] > last_chunk[dim] {
343            return Err(Error::InvalidData(format!(
344                "invalid chunk grid bounds for dimension {dim}: {} > {}",
345                first_chunk[dim], last_chunk[dim]
346            )));
347        }
348    }
349
350    entries.sort_by(|a, b| a.offsets.cmp(&b.offsets));
351
352    for i in 0..entries.len() {
353        validate_chunk_entry_offsets(&entries[i], shape, chunk_shape, first_chunk, last_chunk)?;
354        if i > 0 && entries[i].offsets == entries[i - 1].offsets {
355            return Err(Error::InvalidData(format!(
356                "duplicate chunk output offsets {:?} (addresses {:#x} and {:#x})",
357                entries[i].offsets,
358                entries[i - 1].address,
359                entries[i].address
360            )));
361        }
362    }
363
364    let mut entry_idx = 0usize;
365    let mut expected = first_chunk.to_vec();
366    loop {
367        let expected_offsets: Vec<u64> = expected
368            .iter()
369            .enumerate()
370            .map(|(dim, chunk_index)| chunk_index * chunk_shape[dim])
371            .collect();
372
373        if entry_idx >= entries.len() || entries[entry_idx].offsets != expected_offsets {
374            return Ok(false);
375        }
376        entry_idx += 1;
377
378        if !advance_chunk_index(&mut expected, first_chunk, last_chunk) {
379            break;
380        }
381    }
382
383    Ok(entry_idx == entries.len())
384}
385
386fn validate_chunk_entry_offsets(
387    entry: &chunk_index::ChunkEntry,
388    shape: &[u64],
389    chunk_shape: &[u64],
390    first_chunk: &[u64],
391    last_chunk: &[u64],
392) -> Result<()> {
393    if entry.offsets.len() != shape.len() {
394        return Err(Error::InvalidData(format!(
395            "chunk at address {:#x} has rank {}, expected {}",
396            entry.address,
397            entry.offsets.len(),
398            shape.len()
399        )));
400    }
401
402    for dim in 0..shape.len() {
403        let offset = entry.offsets[dim];
404        if offset >= shape[dim] {
405            return Err(Error::InvalidData(format!(
406                "chunk at address {:#x} has out-of-bounds offset {} for dimension {} of size {}",
407                entry.address, offset, dim, shape[dim]
408            )));
409        }
410        if offset % chunk_shape[dim] != 0 {
411            return Err(Error::InvalidData(format!(
412                "chunk at address {:#x} has non-grid offset {} for dimension {} with chunk extent {}",
413                entry.address, offset, dim, chunk_shape[dim]
414            )));
415        }
416
417        let chunk_index = offset / chunk_shape[dim];
418        if chunk_index < first_chunk[dim] || chunk_index > last_chunk[dim] {
419            return Err(Error::InvalidData(format!(
420                "chunk at address {:#x} has offset {:?} outside requested chunk grid",
421                entry.address, entry.offsets
422            )));
423        }
424    }
425
426    Ok(())
427}
428
429fn advance_chunk_index(index: &mut [u64], first_chunk: &[u64], last_chunk: &[u64]) -> bool {
430    if index.is_empty() {
431        return false;
432    }
433
434    for dim in (0..index.len()).rev() {
435        if index[dim] < last_chunk[dim] {
436            index[dim] += 1;
437            if dim + 1 < index.len() {
438                index[(dim + 1)..].copy_from_slice(&first_chunk[(dim + 1)..]);
439            }
440            return true;
441        }
442    }
443
444    false
445}
446
447fn row_major_strides(shape: &[u64], context: &str) -> Result<Vec<usize>> {
448    let ndim = shape.len();
449    if ndim == 0 {
450        return Ok(Vec::new());
451    }
452
453    let mut strides = vec![1usize; ndim];
454    for i in (0..ndim - 1).rev() {
455        let next_extent = checked_usize(shape[i + 1], context)?;
456        strides[i] = checked_mul_usize(strides[i + 1], next_extent, context)?;
457    }
458    Ok(strides)
459}
460
461/// Convert an allocation of maybe-uninitialized bytes without copying it.
462///
463/// # Safety
464///
465/// Every element of `buffer` must have been initialized.
466unsafe fn assume_init_u8_vec(mut buffer: Vec<MaybeUninit<u8>>) -> Vec<u8> {
467    let ptr = buffer.as_mut_ptr() as *mut u8;
468    let len = buffer.len();
469    let capacity = buffer.capacity();
470    std::mem::forget(buffer);
471    // SAFETY: guaranteed by the caller; `u8` and `MaybeUninit<u8>` have the
472    // same layout, and ownership of the allocation was transferred by forget.
473    unsafe { Vec::from_raw_parts(ptr, len, capacity) }
474}
475
476/// Convert an allocation of maybe-uninitialized values without copying it.
477///
478/// # Safety
479///
480/// Every element of `buffer` must have been initialized.
481unsafe fn assume_init_vec<T>(mut buffer: Vec<MaybeUninit<T>>) -> Vec<T> {
482    let ptr = buffer.as_mut_ptr() as *mut T;
483    let len = buffer.len();
484    let capacity = buffer.capacity();
485    std::mem::forget(buffer);
486    // SAFETY: guaranteed by the caller; `T` and `MaybeUninit<T>` have the
487    // same layout, and ownership of the allocation was transferred by forget.
488    unsafe { Vec::from_raw_parts(ptr, len, capacity) }
489}
490
491fn normalize_selection(selection: &SliceInfo, shape: &[u64]) -> Result<ResolvedSelection> {
492    if selection.selections.len() != shape.len() {
493        return Err(Error::InvalidData(format!(
494            "slice has {} dimensions but dataset has {}",
495            selection.selections.len(),
496            shape.len()
497        )));
498    }
499
500    let mut dims = Vec::with_capacity(shape.len());
501    let mut result_shape = Vec::new();
502    let mut result_elements = 1usize;
503
504    for (i, sel) in selection.selections.iter().enumerate() {
505        let dim_size = shape[i];
506        match sel {
507            SliceInfoElem::Index(idx) => {
508                if *idx >= dim_size {
509                    return Err(Error::SliceOutOfBounds {
510                        dim: i,
511                        index: *idx,
512                        size: dim_size,
513                    });
514                }
515                dims.push(ResolvedSelectionDim {
516                    start: *idx,
517                    end: *idx + 1,
518                    step: 1,
519                    count: 1,
520                });
521            }
522            SliceInfoElem::Slice { start, end, step } => {
523                if *step == 0 {
524                    return Err(Error::InvalidData("slice step cannot be 0".into()));
525                }
526                if *start > dim_size {
527                    return Err(Error::SliceOutOfBounds {
528                        dim: i,
529                        index: *start,
530                        size: dim_size,
531                    });
532                }
533
534                let actual_end = if *end == u64::MAX {
535                    dim_size
536                } else {
537                    (*end).min(dim_size)
538                };
539                let count_u64 = if *start >= actual_end {
540                    0
541                } else {
542                    (actual_end - *start).div_ceil(*step)
543                };
544                let count = checked_usize(count_u64, "slice element count")?;
545
546                dims.push(ResolvedSelectionDim {
547                    start: *start,
548                    end: actual_end,
549                    step: *step,
550                    count,
551                });
552                result_shape.push(count);
553                result_elements =
554                    checked_mul_usize(result_elements, count, "slice result element count")?;
555            }
556        }
557    }
558
559    Ok(ResolvedSelection {
560        dims,
561        result_shape,
562        result_elements,
563    })
564}
565
566fn selection_dim_is_full_unit(dim: &ResolvedSelectionDim, dim_size: u64) -> bool {
567    dim.step == 1
568        && dim.start == 0
569        && dim.end == dim_size
570        && u64::try_from(dim.count).ok() == Some(dim_size)
571}
572
573fn selection_covers_full_dataset(resolved: &ResolvedSelection, shape: &[u64]) -> bool {
574    resolved.result_shape.len() == shape.len()
575        && resolved
576            .dims
577            .iter()
578            .zip(shape.iter())
579            .all(|(dim, &dim_size)| selection_dim_is_full_unit(dim, dim_size))
580}
581
582fn contiguous_slice_tail_start(shape: &[u64], resolved: &ResolvedSelection) -> usize {
583    let ndim = shape.len();
584    if ndim == 0 {
585        return 0;
586    }
587
588    let mut tail_start = if resolved.dims[ndim - 1].step == 1 {
589        ndim - 1
590    } else {
591        ndim
592    };
593
594    while tail_start > 0 {
595        let prev = tail_start - 1;
596        let later_dims_are_full =
597            (tail_start..ndim).all(|d| selection_dim_is_full_unit(&resolved.dims[d], shape[d]));
598        if resolved.dims[prev].step == 1 && later_dims_are_full {
599            tail_start = prev;
600        } else {
601            break;
602        }
603    }
604
605    tail_start
606}
607
608fn contiguous_slice_block_elements(
609    resolved: &ResolvedSelection,
610    tail_start: usize,
611) -> Result<usize> {
612    let mut elements = 1usize;
613    for dim in &resolved.dims[tail_start..] {
614        elements = checked_mul_usize(elements, dim.count, "contiguous slice block elements")?;
615    }
616    Ok(elements)
617}
618
619fn result_strides_for_dims(result_dims: &[usize]) -> Result<Vec<usize>> {
620    let ndim = result_dims.len();
621    let mut result_strides = vec![1usize; ndim];
622    for d in (0..ndim.saturating_sub(1)).rev() {
623        result_strides[d] =
624            checked_mul_usize(result_strides[d + 1], result_dims[d + 1], "result stride")?;
625    }
626    Ok(result_strides)
627}
628
629/// A dataset within an HDF5 file.
630#[derive(Clone)]
631pub struct Dataset {
632    pub(crate) context: Arc<FileContext>,
633    pub(crate) name: String,
634    pub(crate) data_address: u64,
635    pub(crate) dataspace: DataspaceMessage,
636    pub(crate) datatype: Datatype,
637    pub(crate) layout: DataLayout,
638    pub(crate) fill_value: Option<FillValueMessage>,
639    pub(crate) filters: Option<FilterPipelineMessage>,
640    pub(crate) external_files: Option<ExternalFilesMessage>,
641    pub(crate) attributes: Vec<AttributeMessage>,
642    pub(crate) chunk_cache: Arc<ChunkCache>,
643    chunk_entry_cache: Arc<Mutex<LruCache<ChunkEntryCacheKey, Arc<Vec<chunk_index::ChunkEntry>>>>>,
644    full_chunk_entries: Arc<OnceLock<Arc<Vec<chunk_index::ChunkEntry>>>>,
645    full_dataset_bytes: Arc<OnceLock<Arc<Vec<u8>>>>,
646    external_slots: Arc<OnceLock<Arc<Vec<ResolvedExternalRawSlot>>>>,
647    pub(crate) filter_registry: Arc<FilterRegistry>,
648}
649
650/// One decoded chunk from a chunked dataset.
651pub struct DatasetChunk {
652    offsets: Vec<u64>,
653    shape: Vec<u64>,
654    filter_mask: u32,
655    bytes: Arc<Vec<u8>>,
656}
657
658impl DatasetChunk {
659    /// Dataset coordinates of the first element in this chunk.
660    pub fn offsets(&self) -> &[u64] {
661        &self.offsets
662    }
663
664    /// Logical chunk shape from the dataset layout.
665    pub fn shape(&self) -> &[u64] {
666        &self.shape
667    }
668
669    /// HDF5 filter mask recorded for this chunk.
670    pub fn filter_mask(&self) -> u32 {
671        self.filter_mask
672    }
673
674    /// Decoded logical bytes for this chunk.
675    pub fn bytes(&self) -> &[u8] {
676        self.bytes.as_ref()
677    }
678}
679
680/// Iterator over decoded chunks in a chunked dataset.
681pub struct DatasetChunkIterator {
682    dataset: Dataset,
683    entries: Vec<chunk_index::ChunkEntry>,
684    index_address: u64,
685    chunk_shape: Vec<u64>,
686    elem_size: usize,
687    next: usize,
688}
689
690impl Iterator for DatasetChunkIterator {
691    type Item = Result<DatasetChunk>;
692
693    fn next(&mut self) -> Option<Self::Item> {
694        let entry = self.entries.get(self.next)?;
695        self.next += 1;
696
697        Some(
698            self.dataset
699                .load_exact_chunk_data(entry, self.index_address, &self.chunk_shape, self.elem_size)
700                .map(|bytes| DatasetChunk {
701                    offsets: entry.offsets.clone(),
702                    shape: self.chunk_shape.clone(),
703                    filter_mask: entry.filter_mask,
704                    bytes,
705                }),
706        )
707    }
708}
709
710pub(crate) struct DatasetTemplate {
711    name: String,
712    data_address: u64,
713    dataspace: DataspaceMessage,
714    datatype: Datatype,
715    layout: DataLayout,
716    fill_value: Option<FillValueMessage>,
717    filters: Option<FilterPipelineMessage>,
718    external_files: Option<ExternalFilesMessage>,
719    attributes: Vec<AttributeMessage>,
720    chunk_entry_cache: Arc<Mutex<LruCache<ChunkEntryCacheKey, Arc<Vec<chunk_index::ChunkEntry>>>>>,
721    full_chunk_entries: Arc<OnceLock<Arc<Vec<chunk_index::ChunkEntry>>>>,
722    full_dataset_bytes: Arc<OnceLock<Arc<Vec<u8>>>>,
723    external_slots: Arc<OnceLock<Arc<Vec<ResolvedExternalRawSlot>>>>,
724}
725
726impl Dataset {
727    pub(crate) fn from_template(context: Arc<FileContext>, template: Arc<DatasetTemplate>) -> Self {
728        Dataset {
729            chunk_cache: context.chunk_cache.clone(),
730            filter_registry: context.filter_registry.clone(),
731            context,
732            name: template.name.clone(),
733            data_address: template.data_address,
734            dataspace: template.dataspace.clone(),
735            datatype: template.datatype.clone(),
736            layout: template.layout.clone(),
737            fill_value: template.fill_value.clone(),
738            filters: template.filters.clone(),
739            external_files: template.external_files.clone(),
740            attributes: template.attributes.clone(),
741            chunk_entry_cache: template.chunk_entry_cache.clone(),
742            full_chunk_entries: template.full_chunk_entries.clone(),
743            full_dataset_bytes: template.full_dataset_bytes.clone(),
744            external_slots: template.external_slots.clone(),
745        }
746    }
747
748    pub(crate) fn template(&self) -> Arc<DatasetTemplate> {
749        Arc::new(DatasetTemplate {
750            name: self.name.clone(),
751            data_address: self.data_address,
752            dataspace: self.dataspace.clone(),
753            datatype: self.datatype.clone(),
754            layout: self.layout.clone(),
755            fill_value: self.fill_value.clone(),
756            filters: self.filters.clone(),
757            external_files: self.external_files.clone(),
758            attributes: self.attributes.clone(),
759            chunk_entry_cache: self.chunk_entry_cache.clone(),
760            full_chunk_entries: self.full_chunk_entries.clone(),
761            full_dataset_bytes: self.full_dataset_bytes.clone(),
762            external_slots: self.external_slots.clone(),
763        })
764    }
765
766    pub(crate) fn from_parsed_header(
767        context: DatasetParseContext,
768        address: u64,
769        name: String,
770        header: &ObjectHeader,
771    ) -> Result<Self> {
772        let mut dataspace: Option<DataspaceMessage> = None;
773        let mut datatype: Option<Datatype> = None;
774        let mut layout: Option<DataLayout> = None;
775        let mut fill_value: Option<FillValueMessage> = None;
776        let mut filter_pipeline: Option<FilterPipelineMessage> = None;
777        let mut external_files: Option<ExternalFilesMessage> = None;
778        let attributes = collect_attribute_messages_storage(
779            header,
780            context.context.storage.as_ref(),
781            context.context.superblock.offset_size,
782            context.context.superblock.length_size,
783            Some(context.context.filter_registry.as_ref()),
784        )?;
785
786        for msg in &header.messages {
787            match msg {
788                HdfMessage::Dataspace(ds) => dataspace = Some(ds.clone()),
789                HdfMessage::Datatype(dt) => datatype = Some(dt.datatype.clone()),
790                HdfMessage::DataLayout(dl) => layout = Some(dl.layout.clone()),
791                HdfMessage::FillValue(fv) => fill_value = Some(fv.clone()),
792                HdfMessage::FilterPipeline(fp) => filter_pipeline = Some(fp.clone()),
793                HdfMessage::ExternalFiles(ef) => external_files = Some(ef.clone()),
794                _ => {}
795            }
796        }
797
798        let dataspace =
799            dataspace.ok_or_else(|| Error::InvalidData("dataset missing dataspace".into()))?;
800        let dt = datatype.ok_or_else(|| Error::InvalidData("dataset missing datatype".into()))?;
801        let layout =
802            layout.ok_or_else(|| Error::InvalidData("dataset missing data layout".into()))?;
803        let layout = normalize_layout(layout, &dataspace);
804        let attr_fill_value = attributes
805            .iter()
806            .find(|attr| {
807                attr.name == "_FillValue" && matches!(attr.dataspace.num_elements(), Ok(1))
808            })
809            .map(|attr| FillValueMessage {
810                defined: !attr.raw_data.is_empty(),
811                fill_time: FillTime::IfSet,
812                value: Some(attr.raw_data.clone()),
813            });
814        let fill_value = match fill_value {
815            Some(existing) if existing.value.is_some() => Some(existing),
816            _ => attr_fill_value,
817        };
818
819        Ok(Dataset {
820            context: context.context.clone(),
821            name,
822            data_address: address,
823            dataspace,
824            datatype: dt,
825            layout,
826            fill_value,
827            filters: filter_pipeline,
828            external_files,
829            attributes,
830            chunk_cache: context.context.chunk_cache.clone(),
831            chunk_entry_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(32).unwrap()))),
832            full_chunk_entries: Arc::new(OnceLock::new()),
833            full_dataset_bytes: Arc::new(OnceLock::new()),
834            external_slots: Arc::new(OnceLock::new()),
835            filter_registry: context.context.filter_registry.clone(),
836        })
837    }
838
839    /// Dataset name.
840    pub fn name(&self) -> &str {
841        &self.name
842    }
843
844    /// The object header address used to parse this dataset.
845    /// Useful as an opaque identifier or for NC4 data_offset.
846    pub fn address(&self) -> u64 {
847        self.data_address
848    }
849
850    /// Shape of the dataset (dimensions).
851    pub fn shape(&self) -> &[u64] {
852        &self.dataspace.dims
853    }
854
855    /// Datatype of the dataset.
856    pub fn dtype(&self) -> &Datatype {
857        &self.datatype
858    }
859
860    /// Number of dimensions.
861    pub fn ndim(&self) -> usize {
862        self.dataspace.dims.len()
863    }
864
865    fn offset_size(&self) -> u8 {
866        self.context.superblock.offset_size
867    }
868
869    fn length_size(&self) -> u8 {
870        self.context.superblock.length_size
871    }
872
873    /// Maximum dimension sizes, if defined. `u64::MAX` indicates unlimited.
874    pub fn max_dims(&self) -> Option<&[u64]> {
875        self.dataspace.max_dims.as_deref()
876    }
877
878    /// Chunk dimensions, if the dataset is chunked.
879    pub fn chunks(&self) -> Option<Vec<u32>> {
880        match &self.layout {
881            DataLayout::Chunked { dims, .. } => Some(dims.clone()),
882            _ => None,
883        }
884    }
885
886    /// Iterate over decoded chunks for a chunked dataset.
887    pub fn iter_chunks(&self) -> Result<DatasetChunkIterator> {
888        let DataLayout::Chunked {
889            address,
890            dims,
891            chunk_indexing,
892            ..
893        } = &self.layout
894        else {
895            return Err(Error::InvalidData(format!(
896                "dataset '{}' is not chunked",
897                self.name
898            )));
899        };
900
901        if Cursor::is_undefined_offset(*address, self.offset_size()) {
902            return Ok(DatasetChunkIterator {
903                dataset: self.clone(),
904                entries: Vec::new(),
905                index_address: *address,
906                chunk_shape: dims.iter().map(|&d| d as u64).collect(),
907                elem_size: self.raw_element_size()?,
908                next: 0,
909            });
910        }
911
912        let ndim = self.ndim();
913        let shape = &self.dataspace.dims;
914        let elem_size = self.raw_element_size()?;
915        let chunk_shape: Vec<u64> = dims.iter().map(|&d| d as u64).collect();
916        validate_chunk_shape(shape, &chunk_shape)?;
917        let entries = self.collect_chunk_entries(
918            *address,
919            dims,
920            chunk_indexing.as_ref(),
921            ChunkEntrySelection {
922                shape,
923                ndim,
924                elem_size,
925                chunk_bounds: None,
926            },
927        )?;
928
929        Ok(DatasetChunkIterator {
930            dataset: self.clone(),
931            entries,
932            index_address: *address,
933            chunk_shape,
934            elem_size,
935            next: 0,
936        })
937    }
938
939    /// Return current chunk-cache statistics for this dataset's file.
940    pub fn chunk_cache_stats(&self) -> ChunkCacheStats {
941        self.chunk_cache.stats()
942    }
943
944    /// Fill value, if defined.
945    pub fn fill_value(&self) -> Option<&FillValueMessage> {
946        self.fill_value.as_ref()
947    }
948
949    /// Dataset attributes.
950    pub fn attributes(&self) -> Vec<Attribute> {
951        self.attributes
952            .iter()
953            .map(|a| attribute_from_message_storage(a, self.context.as_ref()))
954            .collect()
955    }
956
957    /// Find an attribute by name.
958    pub fn attribute(&self, name: &str) -> Result<Attribute> {
959        self.attributes
960            .iter()
961            .find(|a| a.name == name)
962            .map(|a| attribute_from_message_storage(a, self.context.as_ref()))
963            .ok_or_else(|| Error::AttributeNotFound(name.to_string()))
964    }
965
966    /// Read a scalar string dataset or single string element.
967    ///
968    /// Use [`Dataset::read_strings`] when the dataset contains multiple strings.
969    pub fn read_string(&self) -> Result<String> {
970        let mut strings = self.read_strings()?;
971        match strings.len() {
972            1 => Ok(strings.swap_remove(0)),
973            0 => Err(Error::InvalidData(format!(
974                "dataset '{}' contains no string elements",
975                self.name
976            ))),
977            count => Err(Error::InvalidData(format!(
978                "dataset '{}' contains {count} string elements; use read_strings()",
979                self.name
980            ))),
981        }
982    }
983
984    /// Read all string elements from a string-typed dataset.
985    pub fn read_strings(&self) -> Result<Vec<String>> {
986        match &self.datatype {
987            Datatype::String {
988                size: StringSize::Fixed(len),
989                encoding,
990                padding,
991            } => {
992                let raw = self.read_raw_bytes()?;
993                let elem_size = *len as usize;
994                let count = checked_usize(self.num_elements()?, "dataset string element count")?;
995                let expected_bytes =
996                    checked_mul_usize(count, elem_size, "dataset string byte size")?;
997                if raw.len() < expected_bytes {
998                    return Err(Error::InvalidData(format!(
999                        "dataset '{}' string data too short: need {} bytes, have {}",
1000                        self.name,
1001                        expected_bytes,
1002                        raw.len()
1003                    )));
1004                }
1005
1006                let mut strings = Vec::with_capacity(count);
1007                for i in 0..count {
1008                    let start = i * elem_size;
1009                    let end = start + elem_size;
1010                    strings.push(decode_string(&raw[start..end], *padding, *encoding)?);
1011                }
1012                Ok(strings)
1013            }
1014            Datatype::String {
1015                size: StringSize::Variable,
1016                encoding,
1017                padding,
1018            } => {
1019                let raw = self.read_raw_bytes()?;
1020                let count = checked_usize(self.num_elements()?, "dataset string element count")?;
1021                let ref_size = 4 + self.offset_size() as usize + 4;
1022                let expected_bytes =
1023                    checked_mul_usize(count, ref_size, "dataset string reference byte size")?;
1024                if raw.len() < expected_bytes {
1025                    return Err(Error::InvalidData(format!(
1026                        "dataset '{}' vlen string data too short: need {} bytes, have {}",
1027                        self.name,
1028                        expected_bytes,
1029                        raw.len()
1030                    )));
1031                }
1032
1033                let mut strings = Vec::with_capacity(count);
1034                for i in 0..count {
1035                    let offset = i * ref_size;
1036                    strings.push(read_one_vlen_string_storage(
1037                        &raw,
1038                        offset,
1039                        self.context.storage.as_ref(),
1040                        self.offset_size(),
1041                        self.length_size(),
1042                        *padding,
1043                        *encoding,
1044                    )?);
1045                }
1046                Ok(strings)
1047            }
1048            Datatype::VarLen {
1049                base,
1050                kind: VarLenKind::String,
1051                encoding,
1052                padding,
1053            } => {
1054                if !matches!(base.as_ref(), Datatype::FixedPoint { size: 1, .. }) {
1055                    return Err(Error::TypeMismatch {
1056                        expected: "String dataset".into(),
1057                        actual: format!("{:?}", self.datatype),
1058                    });
1059                }
1060
1061                let raw = self.read_raw_bytes()?;
1062                let count = checked_usize(self.num_elements()?, "dataset string element count")?;
1063                let ref_size = 4 + self.offset_size() as usize + 4;
1064                let expected_bytes =
1065                    checked_mul_usize(count, ref_size, "dataset string reference byte size")?;
1066                if raw.len() < expected_bytes {
1067                    return Err(Error::InvalidData(format!(
1068                        "dataset '{}' vlen byte string data too short: need {} bytes, have {}",
1069                        self.name,
1070                        expected_bytes,
1071                        raw.len()
1072                    )));
1073                }
1074
1075                let mut strings = Vec::with_capacity(count);
1076                for i in 0..count {
1077                    let offset = i * ref_size;
1078                    let ref_bytes = &raw[offset..offset + ref_size];
1079                    let value = resolve_vlen_bytes_storage(
1080                        ref_bytes,
1081                        self.context.storage.as_ref(),
1082                        self.offset_size(),
1083                        self.length_size(),
1084                    )
1085                    .unwrap_or_default();
1086                    strings.push(decode_string(&value, *padding, *encoding)?);
1087                }
1088                Ok(strings)
1089            }
1090            _ => Err(Error::TypeMismatch {
1091                expected: "String dataset".into(),
1092                actual: format!("{:?}", self.datatype),
1093            }),
1094        }
1095    }
1096
1097    /// Total number of elements in the dataset.
1098    pub fn num_elements(&self) -> Result<u64> {
1099        Ok(self.dataspace.num_elements()?)
1100    }
1101
1102    /// Read the entire dataset into an n-dimensional array.
1103    pub fn read_array<T: H5Type>(&self) -> Result<ArrayD<T>> {
1104        let result = match &self.layout {
1105            DataLayout::Compact { data } => self.read_compact::<T>(data),
1106            DataLayout::Contiguous { address, size } => self.read_contiguous::<T>(*address, *size),
1107            DataLayout::Chunked {
1108                address,
1109                dims,
1110                element_size,
1111                chunk_indexing,
1112            } => self.read_chunked::<T>(*address, dims, *element_size, chunk_indexing.as_ref()),
1113        };
1114        result.map_err(|e| e.with_context(&self.name))
1115    }
1116
1117    /// Read the entire dataset into a caller-provided typed buffer.
1118    pub fn read_into<T: H5Type>(&self, dst: &mut [T]) -> Result<()> {
1119        let result = (|| {
1120            let element_count = checked_usize(self.num_elements()?, "dataset element count")?;
1121            if dst.len() != element_count {
1122                return Err(Error::InvalidData(format!(
1123                    "destination has {} elements, dataset requires {}",
1124                    dst.len(),
1125                    element_count
1126                )));
1127            }
1128
1129            let elem_size = self.raw_element_size()?;
1130            if T::native_copy_compatible(&self.datatype) && std::mem::size_of::<T>() == elem_size {
1131                // SAFETY: `dst` is a live, contiguous mutable slice, and the
1132                // checked byte length exactly covers its initialized elements.
1133                let dst_bytes = unsafe {
1134                    std::slice::from_raw_parts_mut(
1135                        dst.as_mut_ptr() as *mut u8,
1136                        checked_mul_usize(dst.len(), elem_size, "destination byte length")?,
1137                    )
1138                };
1139                return self.read_raw_bytes_into_inner(dst_bytes);
1140            }
1141
1142            let array = self.read_array::<T>()?;
1143            let values = array.as_slice_memory_order().ok_or_else(|| {
1144                Error::InvalidData("decoded array is not contiguous in memory order".into())
1145            })?;
1146            dst.clone_from_slice(values);
1147            Ok(())
1148        })();
1149
1150        result.map_err(|e| e.with_context(&self.name))
1151    }
1152
1153    /// Read the entire dataset using internal chunk-level parallelism when possible.
1154    ///
1155    /// Non-chunked datasets fall back to `read_array`.
1156    #[cfg(feature = "rayon")]
1157    pub fn read_array_parallel<T: H5Type>(&self) -> Result<ArrayD<T>> {
1158        match &self.layout {
1159            DataLayout::Chunked {
1160                address,
1161                dims,
1162                element_size,
1163                chunk_indexing,
1164            } => self.read_chunked_parallel::<T>(
1165                *address,
1166                dims,
1167                *element_size,
1168                chunk_indexing.as_ref(),
1169            ),
1170            _ => self.read_array::<T>(),
1171        }
1172    }
1173
1174    /// Read the entire dataset using the provided Rayon thread pool.
1175    ///
1176    /// Non-chunked datasets fall back to `read_array`.
1177    #[cfg(feature = "rayon")]
1178    pub fn read_array_in_pool<T: H5Type>(&self, pool: &rayon::ThreadPool) -> Result<ArrayD<T>> {
1179        match &self.layout {
1180            DataLayout::Chunked {
1181                address,
1182                dims,
1183                element_size,
1184                chunk_indexing,
1185            } => pool.install(|| {
1186                self.read_chunked_parallel::<T>(
1187                    *address,
1188                    dims,
1189                    *element_size,
1190                    chunk_indexing.as_ref(),
1191                )
1192            }),
1193            _ => self.read_array::<T>(),
1194        }
1195    }
1196
1197    /// Read a hyperslab of the dataset using chunk-level parallelism when possible.
1198    ///
1199    /// Chunked datasets decompress overlapping chunks in parallel via Rayon.
1200    /// Non-chunked layouts fall back to `read_slice`.
1201    #[cfg(feature = "rayon")]
1202    pub fn read_slice_parallel<T: H5Type>(&self, selection: &SliceInfo) -> Result<ArrayD<T>> {
1203        let resolved = normalize_selection(selection, &self.dataspace.dims)?;
1204
1205        match &self.layout {
1206            DataLayout::Chunked {
1207                address,
1208                dims,
1209                element_size,
1210                chunk_indexing,
1211            } => self.read_chunked_slice_parallel::<T>(
1212                *address,
1213                dims,
1214                *element_size,
1215                chunk_indexing.as_ref(),
1216                selection,
1217                &resolved,
1218            ),
1219            _ => self.read_slice::<T>(selection),
1220        }
1221    }
1222
1223    /// Read a hyperslab of the dataset.
1224    pub fn read_slice<T: H5Type>(&self, selection: &SliceInfo) -> Result<ArrayD<T>> {
1225        let resolved = normalize_selection(selection, &self.dataspace.dims)?;
1226
1227        match &self.layout {
1228            DataLayout::Contiguous { address, size } => {
1229                self.read_contiguous_slice::<T>(*address, *size, &resolved)
1230            }
1231            DataLayout::Compact { data } => self.read_compact_slice::<T>(data, selection),
1232            DataLayout::Chunked {
1233                address,
1234                dims,
1235                element_size,
1236                chunk_indexing,
1237            } => self.read_chunked_slice::<T>(
1238                *address,
1239                dims,
1240                *element_size,
1241                chunk_indexing.as_ref(),
1242                selection,
1243                &resolved,
1244            ),
1245        }
1246    }
1247
1248    fn read_compact<T: H5Type>(&self, data: &[u8]) -> Result<ArrayD<T>> {
1249        self.validate_allocated_raw_data_len("compact", data.len())?;
1250        self.decode_raw_data::<T>(data)
1251    }
1252
1253    /// Read the dataset as logical element bytes.
1254    ///
1255    /// The returned bytes are after layout handling, chunk decompression, filter
1256    /// decoding, external raw-data resolution, and fill-value materialization.
1257    /// Numeric values remain in the dataset datatype's byte order.
1258    pub fn read_raw_bytes(&self) -> Result<Vec<u8>> {
1259        let result: Result<Vec<u8>> = (|| {
1260            let total_bytes = self.raw_byte_len()?;
1261            let mut output = vec![0u8; total_bytes];
1262            self.read_raw_bytes_into_inner(&mut output)?;
1263            Ok(output)
1264        })();
1265
1266        result.map_err(|e| e.with_context(&self.name))
1267    }
1268
1269    /// Number of logical raw bytes in the entire dataset.
1270    pub fn raw_byte_len(&self) -> Result<usize> {
1271        let elem_size = self.raw_element_size()?;
1272        let total_elements = checked_usize(self.num_elements()?, "dataset element count")?;
1273        checked_mul_usize(total_elements, elem_size, "dataset size in bytes")
1274    }
1275
1276    /// Read logical raw bytes into a caller-provided buffer.
1277    ///
1278    /// The destination length must match [`Dataset::raw_byte_len`]. Numeric
1279    /// values remain in the dataset datatype's byte order.
1280    pub fn read_raw_bytes_into(&self, dst: &mut [u8]) -> Result<()> {
1281        let result: Result<()> = (|| {
1282            let total_bytes = self.raw_byte_len()?;
1283            if dst.len() != total_bytes {
1284                return Err(Error::InvalidData(format!(
1285                    "destination has {} bytes, dataset requires {}",
1286                    dst.len(),
1287                    total_bytes
1288                )));
1289            }
1290            self.read_raw_bytes_into_inner(dst)
1291        })();
1292
1293        result.map_err(|e| e.with_context(&self.name))
1294    }
1295
1296    /// Read logical raw bytes converted to native-endian numeric fields.
1297    pub fn read_native_bytes(&self) -> Result<Vec<u8>> {
1298        let result: Result<Vec<u8>> = (|| {
1299            let total_bytes = self.raw_byte_len()?;
1300            let mut output = vec![0u8; total_bytes];
1301            self.read_raw_bytes_into_inner(&mut output)?;
1302            self.convert_to_native_endian(&mut output)?;
1303            Ok(output)
1304        })();
1305
1306        result.map_err(|e| e.with_context(&self.name))
1307    }
1308
1309    /// Read logical raw bytes into a buffer and convert numeric fields to native endian.
1310    pub fn read_native_bytes_into(&self, dst: &mut [u8]) -> Result<()> {
1311        let result: Result<()> = (|| {
1312            let total_bytes = self.raw_byte_len()?;
1313            if dst.len() != total_bytes {
1314                return Err(Error::InvalidData(format!(
1315                    "destination has {} bytes, dataset requires {}",
1316                    dst.len(),
1317                    total_bytes
1318                )));
1319            }
1320            self.read_raw_bytes_into_inner(dst)?;
1321            self.convert_to_native_endian(dst)
1322        })();
1323
1324        result.map_err(|e| e.with_context(&self.name))
1325    }
1326
1327    fn read_raw_bytes_into_inner(&self, dst: &mut [u8]) -> Result<()> {
1328        match &self.layout {
1329            DataLayout::Compact { data } => {
1330                self.validate_allocated_raw_data_len("compact", data.len())?;
1331                dst.copy_from_slice(data);
1332                Ok(())
1333            }
1334            DataLayout::Contiguous { address, size } => {
1335                self.read_contiguous_bytes_into(*address, *size, dst)
1336            }
1337            DataLayout::Chunked {
1338                address,
1339                dims,
1340                element_size: _,
1341                chunk_indexing,
1342            } => self.read_chunked_bytes_into(*address, dims, chunk_indexing.as_ref(), dst),
1343        }
1344    }
1345
1346    /// Size in bytes of one HDF5 variable-length reference in this file.
1347    pub fn vlen_reference_size(&self) -> usize {
1348        4 + self.offset_size() as usize + 4
1349    }
1350
1351    /// Size in bytes of one logical raw element in this file.
1352    ///
1353    /// Variable-length strings and sequences are stored as global-heap
1354    /// references whose width depends on the file offset size.
1355    pub fn raw_element_size(&self) -> Result<usize> {
1356        raw_element_size_for_datatype(&self.datatype, self.vlen_reference_size())
1357    }
1358
1359    /// Resolve one HDF5 variable-length reference into logical element bytes.
1360    ///
1361    /// `base_element_size` is the byte width of one value in the vlen sequence.
1362    /// The sequence length stored in the reference is interpreted as an element
1363    /// count and multiplied by this size.
1364    pub fn resolve_vlen_reference_bytes(
1365        &self,
1366        reference: &[u8],
1367        base_element_size: usize,
1368    ) -> Result<Vec<u8>> {
1369        if reference.len() < self.vlen_reference_size() {
1370            return Err(Error::InvalidData(format!(
1371                "dataset '{}' vlen reference too short: need {} bytes, have {}",
1372                self.name,
1373                self.vlen_reference_size(),
1374                reference.len()
1375            )));
1376        }
1377
1378        let mut cursor = Cursor::new(reference);
1379        let seq_len = cursor.read_u32_le()? as usize;
1380        let heap_addr = cursor.read_offset(self.offset_size())?;
1381        let obj_index = cursor.read_u32_le()? as u16;
1382
1383        if Cursor::is_undefined_offset(heap_addr, self.offset_size()) || obj_index == 0 {
1384            return Ok(Vec::new());
1385        }
1386
1387        let expected_bytes =
1388            checked_mul_usize(seq_len, base_element_size, "vlen sequence byte size")?;
1389        let collection = crate::global_heap::GlobalHeapCollection::parse_at_storage(
1390            self.context.storage.as_ref(),
1391            heap_addr,
1392            self.offset_size(),
1393            self.length_size(),
1394        )?;
1395        let object = collection.get_object(obj_index).ok_or_else(|| {
1396            Error::InvalidData(format!(
1397                "dataset '{}' references missing vlen heap object {}",
1398                self.name, obj_index
1399            ))
1400        })?;
1401        if object.data.len() < expected_bytes {
1402            return Err(Error::InvalidData(format!(
1403                "dataset '{}' vlen heap object too short: need {} bytes, have {}",
1404                self.name,
1405                expected_bytes,
1406                object.data.len()
1407            )));
1408        }
1409
1410        Ok(object.data[..expected_bytes].to_vec())
1411    }
1412
1413    fn read_contiguous<T: H5Type>(&self, address: u64, size: u64) -> Result<ArrayD<T>> {
1414        if self.external_files.is_some() {
1415            let elem_size = self.raw_element_size()?;
1416            let total_elements = checked_usize(self.num_elements()?, "dataset element count")?;
1417            let total_bytes =
1418                checked_mul_usize(total_elements, elem_size, "dataset size in bytes")?;
1419            let raw = self.read_external_range(0, total_bytes)?;
1420            return self.decode_raw_data::<T>(&raw);
1421        }
1422
1423        if Cursor::is_undefined_offset(address, self.offset_size()) || size == 0 {
1424            // Dataset with no data written — return fill values
1425            return self.make_fill_array::<T>();
1426        }
1427
1428        let sz = checked_usize(size, "contiguous dataset size")?;
1429        self.validate_allocated_raw_data_len("contiguous", sz)?;
1430        let raw = self.context.read_range(address, sz)?;
1431        self.decode_raw_data::<T>(raw.as_ref())
1432    }
1433
1434    fn read_contiguous_bytes_into(&self, address: u64, size: u64, dst: &mut [u8]) -> Result<()> {
1435        if self.external_files.is_some() {
1436            return self.read_external_range_into(0, dst);
1437        }
1438
1439        if Cursor::is_undefined_offset(address, self.offset_size()) || size == 0 {
1440            self.fill_output_buffer(dst);
1441            return Ok(());
1442        }
1443
1444        let sz = checked_usize(size, "contiguous dataset size")?;
1445        self.validate_allocated_raw_data_len("contiguous", sz)?;
1446        if dst.is_empty() {
1447            return Ok(());
1448        }
1449        let raw = self.context.read_range(address, sz)?;
1450        dst.copy_from_slice(raw.as_ref());
1451        Ok(())
1452    }
1453
1454    fn read_contiguous_logical_range(
1455        &self,
1456        address: u64,
1457        logical_offset: usize,
1458        len: usize,
1459    ) -> Result<Vec<u8>> {
1460        if self.external_files.is_some() {
1461            return self.read_external_range(logical_offset, len);
1462        }
1463
1464        let file_offset = checked_add_u64(
1465            address,
1466            u64::try_from(logical_offset).map_err(|_| {
1467                Error::InvalidData("contiguous logical offset exceeds u64 capacity".to_string())
1468            })?,
1469            "contiguous read file offset",
1470        )?;
1471        Ok(self.context.read_range(file_offset, len)?.to_vec())
1472    }
1473
1474    fn read_external_range(&self, logical_offset: usize, len: usize) -> Result<Vec<u8>> {
1475        let mut output = vec![0u8; len];
1476        self.read_external_range_into(logical_offset, &mut output)?;
1477        Ok(output)
1478    }
1479
1480    fn read_external_range_into(&self, logical_offset: usize, dst: &mut [u8]) -> Result<()> {
1481        self.fill_output_buffer(dst);
1482        if dst.is_empty() {
1483            return Ok(());
1484        }
1485
1486        let request_start = u64::try_from(logical_offset).map_err(|_| {
1487            Error::InvalidData("external dataset offset exceeds u64 capacity".to_string())
1488        })?;
1489        let request_len = u64::try_from(dst.len()).map_err(|_| {
1490            Error::InvalidData("external dataset length exceeds u64 capacity".to_string())
1491        })?;
1492        let request_end = request_start
1493            .checked_add(request_len)
1494            .ok_or_else(|| Error::InvalidData("external dataset range overflows".into()))?;
1495
1496        for slot in self.external_raw_slots()?.iter() {
1497            let slot_end = slot.logical_offset.saturating_add(slot.size);
1498            let overlap_start = request_start.max(slot.logical_offset);
1499            let overlap_end = request_end.min(slot_end);
1500            if overlap_start >= overlap_end {
1501                continue;
1502            }
1503
1504            let read_offset = slot
1505                .file_offset
1506                .checked_add(overlap_start - slot.logical_offset)
1507                .ok_or_else(|| Error::InvalidData("external file read offset overflows".into()))?;
1508            let read_len = checked_usize(overlap_end - overlap_start, "external read length")?;
1509            let dst_start = checked_usize(overlap_start - request_start, "external read dst")?;
1510            let dst_end = checked_add_usize(dst_start, read_len, "external read dst end")?;
1511            let bytes = slot.storage.read_range(read_offset, read_len)?;
1512            dst[dst_start..dst_end].copy_from_slice(bytes.as_ref());
1513        }
1514
1515        Ok(())
1516    }
1517
1518    fn external_raw_slots(&self) -> Result<Arc<Vec<ResolvedExternalRawSlot>>> {
1519        if let Some(slots) = self.external_slots.get() {
1520            return Ok(slots.clone());
1521        }
1522
1523        let slots = Arc::new(self.load_external_raw_slots()?);
1524        let _ = self.external_slots.set(slots.clone());
1525        Ok(self
1526            .external_slots
1527            .get()
1528            .expect("external slot cache must exist after initialization")
1529            .clone())
1530    }
1531
1532    fn load_external_raw_slots(&self) -> Result<Vec<ResolvedExternalRawSlot>> {
1533        let Some(external_files) = self.external_files.as_ref() else {
1534            return Ok(Vec::new());
1535        };
1536
1537        let heap = LocalHeap::parse_at_storage(
1538            self.context.storage.as_ref(),
1539            external_files.heap_address,
1540            self.offset_size(),
1541            self.length_size(),
1542        )?;
1543
1544        let mut logical_offset = 0u64;
1545        let mut slots = Vec::with_capacity(external_files.slots.len());
1546        for slot in &external_files.slots {
1547            let filename =
1548                heap.get_string_storage(slot.name_offset, self.context.storage.as_ref())?;
1549            let storage = self
1550                .context
1551                .resolve_external_file(&filename)?
1552                .ok_or_else(|| {
1553                    Error::Other(format!(
1554                        "external raw data file '{filename}' could not be resolved"
1555                    ))
1556                })?;
1557            let size = if Cursor::is_undefined_offset(slot.size, self.length_size()) {
1558                u64::MAX.saturating_sub(logical_offset)
1559            } else {
1560                slot.size
1561            };
1562
1563            slots.push(ResolvedExternalRawSlot {
1564                logical_offset,
1565                storage,
1566                file_offset: slot.offset,
1567                size,
1568            });
1569
1570            if Cursor::is_undefined_offset(slot.size, self.length_size()) {
1571                break;
1572            }
1573            logical_offset = logical_offset.checked_add(slot.size).ok_or_else(|| {
1574                Error::InvalidData("external raw data logical offset overflows".into())
1575            })?;
1576        }
1577
1578        Ok(slots)
1579    }
1580
1581    fn read_chunked<T: H5Type>(
1582        &self,
1583        index_address: u64,
1584        chunk_dims: &[u32],
1585        _element_size: u32,
1586        chunk_indexing: Option<&ChunkIndexing>,
1587    ) -> Result<ArrayD<T>> {
1588        if Cursor::is_undefined_offset(index_address, self.offset_size()) {
1589            return self.make_fill_array::<T>();
1590        }
1591
1592        let ndim = self.ndim();
1593        let shape = &self.dataspace.dims;
1594        let elem_size = self.raw_element_size()?;
1595        let total_elements = checked_usize(self.num_elements()?, "dataset element count")?;
1596        let total_bytes = checked_mul_usize(total_elements, elem_size, "dataset size in bytes")?;
1597
1598        if total_bytes <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1599            if let Some(cached_bytes) = self.full_dataset_bytes.get() {
1600                return self.decode_raw_data::<T>(cached_bytes);
1601            }
1602        }
1603
1604        let chunk_shape: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
1605        validate_chunk_shape(shape, &chunk_shape)?;
1606        let dataset_strides = row_major_strides(shape, "dataset stride")?;
1607        let chunk_strides = row_major_strides(&chunk_shape, "chunk stride")?;
1608
1609        let mut entries = self.collect_chunk_entries(
1610            index_address,
1611            chunk_dims,
1612            chunk_indexing,
1613            ChunkEntrySelection {
1614                shape,
1615                ndim,
1616                elem_size,
1617                chunk_bounds: None,
1618            },
1619        )?;
1620
1621        let full_chunk_coverage = match full_dataset_chunk_bounds(shape, &chunk_shape)? {
1622            Some((first_chunk, last_chunk)) => validate_chunk_grid_coverage(
1623                &mut entries,
1624                shape,
1625                &chunk_shape,
1626                &first_chunk,
1627                &last_chunk,
1628            )?,
1629            None if entries.is_empty() => true,
1630            None => {
1631                return Err(Error::InvalidData(
1632                    "chunk index contains entries for an empty dataset".into(),
1633                ))
1634            }
1635        };
1636        if full_chunk_coverage {
1637            if T::native_copy_compatible(&self.datatype) && std::mem::size_of::<T>() == elem_size {
1638                let mut result_values: Vec<MaybeUninit<T>> =
1639                    std::iter::repeat_with(MaybeUninit::<T>::uninit)
1640                        .take(total_elements)
1641                        .collect();
1642                let result_ptr = result_values.as_mut_ptr() as *mut u8;
1643                let result_len = checked_mul_usize(
1644                    result_values.len(),
1645                    std::mem::size_of::<T>(),
1646                    "typed dataset size in bytes",
1647                )?;
1648
1649                for entry in &entries {
1650                    let chunk_data =
1651                        self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
1652                    // SAFETY: the validated chunk grid has no duplicate
1653                    // offsets, the helper checks every range, and the output
1654                    // allocation remains live for the duration of the copy.
1655                    unsafe {
1656                        copy_chunk_to_flat_with_strides_ptr(
1657                            &chunk_data,
1658                            FlatBufferPtr {
1659                                ptr: result_ptr,
1660                                len: result_len,
1661                            },
1662                            ChunkCopyLayout {
1663                                chunk_offsets: &entry.offsets,
1664                                chunk_shape: &chunk_shape,
1665                                dataset_shape: shape,
1666                                dataset_strides: &dataset_strides,
1667                                chunk_strides: &chunk_strides,
1668                                elem_size,
1669                            },
1670                        )?;
1671                    }
1672                }
1673
1674                if total_bytes <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1675                    let mut cached_bytes = vec![0u8; total_bytes];
1676                    // SAFETY: full chunk coverage initialized all
1677                    // `total_bytes` at `result_ptr`, and both allocations are
1678                    // live and distinct.
1679                    unsafe {
1680                        std::ptr::copy_nonoverlapping(
1681                            result_ptr,
1682                            cached_bytes.as_mut_ptr(),
1683                            total_bytes,
1684                        );
1685                    }
1686                    let _ = self.full_dataset_bytes.set(Arc::new(cached_bytes));
1687                }
1688
1689                let mut result_shape = Vec::with_capacity(shape.len());
1690                for &dim in shape {
1691                    result_shape.push(checked_usize(dim, "dataset dimension")?);
1692                }
1693                // SAFETY: exact, duplicate-free chunk-grid coverage was
1694                // validated above and every chunk was copied successfully.
1695                let result_values = unsafe { assume_init_vec(result_values) };
1696                return ArrayD::from_shape_vec(IxDyn(&result_shape), result_values)
1697                    .map_err(|e| Error::InvalidData(format!("array shape error: {e}")));
1698            }
1699
1700            let mut flat_data = vec![MaybeUninit::<u8>::uninit(); total_bytes];
1701            let flat_ptr = flat_data.as_mut_ptr() as *mut u8;
1702            let flat_len = flat_data.len();
1703
1704            for entry in &entries {
1705                let chunk_data =
1706                    self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
1707                // SAFETY: the validated chunk grid has no duplicate offsets,
1708                // the helper checks every range, and the output allocation
1709                // remains live for the duration of the copy.
1710                unsafe {
1711                    copy_chunk_to_flat_with_strides_ptr(
1712                        &chunk_data,
1713                        FlatBufferPtr {
1714                            ptr: flat_ptr,
1715                            len: flat_len,
1716                        },
1717                        ChunkCopyLayout {
1718                            chunk_offsets: &entry.offsets,
1719                            chunk_shape: &chunk_shape,
1720                            dataset_shape: shape,
1721                            dataset_strides: &dataset_strides,
1722                            chunk_strides: &chunk_strides,
1723                            elem_size,
1724                        },
1725                    )?;
1726                }
1727            }
1728
1729            // SAFETY: exact, duplicate-free chunk-grid coverage was validated
1730            // above and every chunk was copied successfully.
1731            let flat_data = unsafe { assume_init_u8_vec(flat_data) };
1732            if total_bytes <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1733                let _ = self.full_dataset_bytes.set(Arc::new(flat_data.clone()));
1734            }
1735            return self.decode_raw_data::<T>(&flat_data);
1736        }
1737
1738        let mut flat_data = self.make_output_buffer(total_bytes);
1739        for entry in &entries {
1740            let chunk_data =
1741                self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
1742            copy_chunk_to_flat_with_strides(
1743                &chunk_data,
1744                &mut flat_data,
1745                ChunkCopyLayout {
1746                    chunk_offsets: &entry.offsets,
1747                    chunk_shape: &chunk_shape,
1748                    dataset_shape: shape,
1749                    dataset_strides: &dataset_strides,
1750                    chunk_strides: &chunk_strides,
1751                    elem_size,
1752                },
1753            )?;
1754        }
1755
1756        self.decode_raw_data::<T>(&flat_data)
1757    }
1758
1759    fn read_chunked_bytes_into(
1760        &self,
1761        index_address: u64,
1762        chunk_dims: &[u32],
1763        chunk_indexing: Option<&ChunkIndexing>,
1764        dst: &mut [u8],
1765    ) -> Result<()> {
1766        if Cursor::is_undefined_offset(index_address, self.offset_size()) {
1767            self.fill_output_buffer(dst);
1768            return Ok(());
1769        }
1770
1771        let ndim = self.ndim();
1772        let shape = &self.dataspace.dims;
1773        let elem_size = self.raw_element_size()?;
1774
1775        if dst.len() <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1776            if let Some(cached_bytes) = self.full_dataset_bytes.get() {
1777                if cached_bytes.len() == dst.len() {
1778                    dst.copy_from_slice(cached_bytes.as_slice());
1779                    return Ok(());
1780                }
1781            }
1782        }
1783
1784        let chunk_shape: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
1785        validate_chunk_shape(shape, &chunk_shape)?;
1786        let dataset_strides = row_major_strides(shape, "dataset stride")?;
1787        let chunk_strides = row_major_strides(&chunk_shape, "chunk stride")?;
1788
1789        let mut entries = self.collect_chunk_entries(
1790            index_address,
1791            chunk_dims,
1792            chunk_indexing,
1793            ChunkEntrySelection {
1794                shape,
1795                ndim,
1796                elem_size,
1797                chunk_bounds: None,
1798            },
1799        )?;
1800
1801        let full_chunk_coverage = match full_dataset_chunk_bounds(shape, &chunk_shape)? {
1802            Some((first_chunk, last_chunk)) => validate_chunk_grid_coverage(
1803                &mut entries,
1804                shape,
1805                &chunk_shape,
1806                &first_chunk,
1807                &last_chunk,
1808            )?,
1809            None if entries.is_empty() => true,
1810            None => {
1811                return Err(Error::InvalidData(
1812                    "chunk index contains entries for an empty dataset".into(),
1813                ))
1814            }
1815        };
1816
1817        self.fill_output_buffer(dst);
1818        for entry in &entries {
1819            let chunk_data =
1820                self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
1821            copy_chunk_to_flat_with_strides(
1822                &chunk_data,
1823                dst,
1824                ChunkCopyLayout {
1825                    chunk_offsets: &entry.offsets,
1826                    chunk_shape: &chunk_shape,
1827                    dataset_shape: shape,
1828                    dataset_strides: &dataset_strides,
1829                    chunk_strides: &chunk_strides,
1830                    elem_size,
1831                },
1832            )?;
1833        }
1834
1835        if full_chunk_coverage && dst.len() <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1836            let _ = self.full_dataset_bytes.set(Arc::new(dst.to_vec()));
1837        }
1838
1839        Ok(())
1840    }
1841
1842    #[cfg(feature = "rayon")]
1843    fn read_chunked_parallel<T: H5Type>(
1844        &self,
1845        index_address: u64,
1846        chunk_dims: &[u32],
1847        _element_size: u32,
1848        chunk_indexing: Option<&ChunkIndexing>,
1849    ) -> Result<ArrayD<T>> {
1850        if Cursor::is_undefined_offset(index_address, self.offset_size()) {
1851            return self.make_fill_array::<T>();
1852        }
1853
1854        let ndim = self.ndim();
1855        let shape = &self.dataspace.dims;
1856        let elem_size = self.raw_element_size()?;
1857        let total_elements = checked_usize(self.num_elements()?, "dataset element count")?;
1858        let total_bytes = checked_mul_usize(total_elements, elem_size, "dataset size in bytes")?;
1859
1860        if total_bytes <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1861            if let Some(cached_bytes) = self.full_dataset_bytes.get() {
1862                return self.decode_raw_data::<T>(cached_bytes);
1863            }
1864        }
1865
1866        let chunk_shape: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
1867        validate_chunk_shape(shape, &chunk_shape)?;
1868        let dataset_strides = row_major_strides(shape, "dataset stride")?;
1869        let chunk_strides = row_major_strides(&chunk_shape, "chunk stride")?;
1870
1871        let mut entries = self.collect_chunk_entries(
1872            index_address,
1873            chunk_dims,
1874            chunk_indexing,
1875            ChunkEntrySelection {
1876                shape,
1877                ndim,
1878                elem_size,
1879                chunk_bounds: None,
1880            },
1881        )?;
1882
1883        let full_chunk_coverage = match full_dataset_chunk_bounds(shape, &chunk_shape)? {
1884            Some((first_chunk, last_chunk)) => validate_chunk_grid_coverage(
1885                &mut entries,
1886                shape,
1887                &chunk_shape,
1888                &first_chunk,
1889                &last_chunk,
1890            )?,
1891            None if entries.is_empty() => true,
1892            None => {
1893                return Err(Error::InvalidData(
1894                    "chunk index contains entries for an empty dataset".into(),
1895                ))
1896            }
1897        };
1898        if full_chunk_coverage {
1899            if T::native_copy_compatible(&self.datatype) && std::mem::size_of::<T>() == elem_size {
1900                let mut result_values: Vec<MaybeUninit<T>> =
1901                    std::iter::repeat_with(MaybeUninit::<T>::uninit)
1902                        .take(total_elements)
1903                        .collect();
1904                let flat = FlatBufferPtr {
1905                    ptr: result_values.as_mut_ptr() as *mut u8,
1906                    len: checked_mul_usize(
1907                        result_values.len(),
1908                        std::mem::size_of::<T>(),
1909                        "typed dataset size in bytes",
1910                    )?,
1911                };
1912
1913                entries
1914                    .par_iter()
1915                    .map(|entry| {
1916                        self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)
1917                            .and_then(|data| {
1918                                // SAFETY: validated chunk offsets are unique,
1919                                // so parallel copies write disjoint ranges;
1920                                // `result_values` remains live through collect.
1921                                unsafe {
1922                                    flat.copy_chunk(
1923                                        &data,
1924                                        ChunkCopyLayout {
1925                                            chunk_offsets: &entry.offsets,
1926                                            chunk_shape: &chunk_shape,
1927                                            dataset_shape: shape,
1928                                            dataset_strides: &dataset_strides,
1929                                            chunk_strides: &chunk_strides,
1930                                            elem_size,
1931                                        },
1932                                    )
1933                                }
1934                            })
1935                    })
1936                    .collect::<std::result::Result<Vec<_>, Error>>()?;
1937
1938                let mut result_shape = Vec::with_capacity(shape.len());
1939                for &dim in shape {
1940                    result_shape.push(checked_usize(dim, "dataset dimension")?);
1941                }
1942                if total_bytes <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1943                    let mut cached_bytes = vec![0u8; total_bytes];
1944                    // SAFETY: full chunk coverage initialized all
1945                    // `total_bytes` at `flat.ptr`, and both allocations are
1946                    // live and distinct.
1947                    unsafe {
1948                        std::ptr::copy_nonoverlapping(
1949                            flat.ptr,
1950                            cached_bytes.as_mut_ptr(),
1951                            total_bytes,
1952                        );
1953                    }
1954                    let _ = self.full_dataset_bytes.set(Arc::new(cached_bytes));
1955                }
1956                // SAFETY: exact, duplicate-free chunk-grid coverage was
1957                // validated above and all parallel copies completed.
1958                let result_values = unsafe { assume_init_vec(result_values) };
1959                return ArrayD::from_shape_vec(IxDyn(&result_shape), result_values)
1960                    .map_err(|e| Error::InvalidData(format!("array shape error: {e}")));
1961            }
1962
1963            let mut flat_data = vec![MaybeUninit::<u8>::uninit(); total_bytes];
1964            let flat = FlatBufferPtr {
1965                ptr: flat_data.as_mut_ptr() as *mut u8,
1966                len: flat_data.len(),
1967            };
1968
1969            entries
1970                .par_iter()
1971                .map(|entry| {
1972                    self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)
1973                        .and_then(|data| {
1974                            // SAFETY: validated chunk offsets are unique, so
1975                            // parallel copies write disjoint ranges;
1976                            // `flat_data` remains live through collect.
1977                            unsafe {
1978                                flat.copy_chunk(
1979                                    &data,
1980                                    ChunkCopyLayout {
1981                                        chunk_offsets: &entry.offsets,
1982                                        chunk_shape: &chunk_shape,
1983                                        dataset_shape: shape,
1984                                        dataset_strides: &dataset_strides,
1985                                        chunk_strides: &chunk_strides,
1986                                        elem_size,
1987                                    },
1988                                )
1989                            }
1990                        })
1991                })
1992                .collect::<std::result::Result<Vec<_>, Error>>()?;
1993
1994            // SAFETY: exact, duplicate-free chunk-grid coverage was validated
1995            // above and all parallel copies completed.
1996            let flat_data = unsafe { assume_init_u8_vec(flat_data) };
1997            if total_bytes <= HOT_FULL_DATASET_CACHE_MAX_BYTES {
1998                let _ = self.full_dataset_bytes.set(Arc::new(flat_data.clone()));
1999            }
2000            return self.decode_raw_data::<T>(&flat_data);
2001        }
2002
2003        let mut flat_data = self.make_output_buffer(total_bytes);
2004        let flat = FlatBufferPtr {
2005            ptr: flat_data.as_mut_ptr(),
2006            len: flat_data.len(),
2007        };
2008
2009        entries
2010            .par_iter()
2011            .map(|entry| {
2012                self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)
2013                    .and_then(|data| {
2014                        // SAFETY: validated chunk offsets are unique, so
2015                        // parallel copies write disjoint ranges; `flat_data`
2016                        // remains live through collect.
2017                        unsafe {
2018                            flat.copy_chunk(
2019                                &data,
2020                                ChunkCopyLayout {
2021                                    chunk_offsets: &entry.offsets,
2022                                    chunk_shape: &chunk_shape,
2023                                    dataset_shape: shape,
2024                                    dataset_strides: &dataset_strides,
2025                                    chunk_strides: &chunk_strides,
2026                                    elem_size,
2027                                },
2028                            )
2029                        }
2030                    })
2031            })
2032            .collect::<std::result::Result<Vec<_>, Error>>()?;
2033
2034        self.decode_raw_data::<T>(&flat_data)
2035    }
2036
2037    /// Collect all chunk entries by dispatching on the chunk indexing type.
2038    ///
2039    /// Shared by `read_chunked` and `read_chunked_slice`.
2040    fn collect_chunk_entries(
2041        &self,
2042        index_address: u64,
2043        chunk_dims: &[u32],
2044        chunk_indexing: Option<&ChunkIndexing>,
2045        selection: ChunkEntrySelection<'_>,
2046    ) -> Result<Vec<chunk_index::ChunkEntry>> {
2047        if selection.chunk_bounds.is_none() {
2048            if let Some(cached) = self.full_chunk_entries.get() {
2049                return Ok((**cached).clone());
2050            }
2051        }
2052
2053        let cache_key =
2054            selection
2055                .chunk_bounds
2056                .map(|(first_chunk, last_chunk)| ChunkEntryCacheKey {
2057                    index_address,
2058                    first_chunk: SmallVec::from_slice(first_chunk),
2059                    last_chunk: SmallVec::from_slice(last_chunk),
2060                });
2061
2062        if let Some(ref key) = cache_key {
2063            let mut cache = self.chunk_entry_cache.lock();
2064            if let Some(cached) = cache.get(key) {
2065                return Ok((**cached).clone());
2066            }
2067        }
2068
2069        let entries = match chunk_indexing {
2070            None => {
2071                // V1-V3: B-tree v1 chunk indexing
2072                self.collect_btree_v1_entries(
2073                    index_address,
2074                    selection.ndim,
2075                    chunk_dims,
2076                    selection.chunk_bounds,
2077                )
2078            }
2079            Some(ChunkIndexing::SingleChunk {
2080                filtered_size,
2081                filters,
2082            }) => Ok(vec![chunk_index::single_chunk_entry(
2083                index_address,
2084                *filtered_size,
2085                *filters,
2086                selection.ndim,
2087            )]),
2088            Some(ChunkIndexing::BTreeV2) => chunk_index::collect_v2_chunk_entries_storage(
2089                self.context.storage.as_ref(),
2090                index_address,
2091                self.offset_size(),
2092                self.length_size(),
2093                selection.ndim as u32,
2094                chunk_dims,
2095                selection.chunk_bounds,
2096            ),
2097            Some(ChunkIndexing::Implicit) => chunk_index::collect_implicit_chunk_entries(
2098                index_address,
2099                selection.shape,
2100                chunk_dims,
2101                selection.elem_size,
2102                selection.chunk_bounds,
2103                self.context.storage.len(),
2104            ),
2105            Some(ChunkIndexing::FixedArray { .. }) => {
2106                crate::fixed_array::collect_fixed_array_chunk_entries_storage(
2107                    self.context.storage.as_ref(),
2108                    index_address,
2109                    self.offset_size(),
2110                    self.length_size(),
2111                    selection.shape,
2112                    chunk_dims,
2113                    selection.chunk_bounds,
2114                )
2115            }
2116            Some(ChunkIndexing::ExtensibleArray { .. }) => {
2117                crate::extensible_array::collect_extensible_array_chunk_entries_storage(
2118                    self.context.storage.as_ref(),
2119                    index_address,
2120                    self.offset_size(),
2121                    self.length_size(),
2122                    selection.shape,
2123                    chunk_dims,
2124                    selection.chunk_bounds,
2125                )
2126            }
2127        }?;
2128
2129        if let Some(key) = cache_key {
2130            let mut cache = self.chunk_entry_cache.lock();
2131            cache.put(key, Arc::new(entries.clone()));
2132        } else {
2133            let _ = self.full_chunk_entries.set(Arc::new(entries.clone()));
2134        }
2135
2136        Ok(entries)
2137    }
2138
2139    /// Collect chunk entries from a B-tree v1 index.
2140    fn collect_btree_v1_entries(
2141        &self,
2142        btree_address: u64,
2143        ndim: usize,
2144        chunk_dims: &[u32],
2145        chunk_bounds: Option<(&[u64], &[u64])>,
2146    ) -> Result<Vec<chunk_index::ChunkEntry>> {
2147        let leaves = crate::btree_v1::collect_btree_v1_leaves_storage(
2148            self.context.storage.as_ref(),
2149            btree_address,
2150            self.offset_size(),
2151            self.length_size(),
2152            Some(ndim as u32),
2153            chunk_dims,
2154            chunk_bounds,
2155        )?;
2156
2157        let mut entries = Vec::with_capacity(leaves.len());
2158        for (key, chunk_addr) in &leaves {
2159            match key {
2160                crate::btree_v1::BTreeV1Key::RawData {
2161                    chunk_size,
2162                    filter_mask,
2163                    offsets,
2164                } => {
2165                    entries.push(chunk_index::ChunkEntry {
2166                        address: *chunk_addr,
2167                        size: *chunk_size as u64,
2168                        filter_mask: *filter_mask,
2169                        offsets: offsets[..ndim].to_vec(),
2170                    });
2171                }
2172                _ => {
2173                    return Err(Error::InvalidData(
2174                        "expected raw data key in chunk B-tree".into(),
2175                    ))
2176                }
2177            }
2178        }
2179        Ok(entries)
2180    }
2181
2182    fn load_chunk_data(
2183        &self,
2184        entry: &chunk_index::ChunkEntry,
2185        dataset_addr: u64,
2186        chunk_shape: &[u64],
2187        elem_size: usize,
2188    ) -> Result<Arc<Vec<u8>>> {
2189        let cache_key = ChunkKey {
2190            dataset_addr,
2191            chunk_offsets: smallvec::SmallVec::from_slice(&entry.offsets),
2192        };
2193        let expected_len = decoded_chunk_expected_len(chunk_shape, elem_size)?;
2194        let filter_output_limit =
2195            checked_add_usize(expected_len, 1, "decoded chunk filter output limit")?;
2196
2197        self.chunk_cache.get_or_insert_with(cache_key, || {
2198            let size = if entry.size > 0 {
2199                checked_usize(entry.size, "encoded chunk size")?
2200            } else {
2201                expected_len
2202            };
2203            let raw = self.context.read_range(entry.address, size)?;
2204
2205            if let Some(ref pipeline) = self.filters {
2206                filters::apply_pipeline_with_limit(
2207                    raw.as_ref(),
2208                    &pipeline.filters,
2209                    entry.filter_mask,
2210                    elem_size,
2211                    Some(&self.filter_registry),
2212                    Some(filter_output_limit),
2213                )
2214            } else {
2215                Ok(raw.to_vec())
2216            }
2217        })
2218    }
2219
2220    fn load_exact_chunk_data(
2221        &self,
2222        entry: &chunk_index::ChunkEntry,
2223        dataset_addr: u64,
2224        chunk_shape: &[u64],
2225        elem_size: usize,
2226    ) -> Result<Arc<Vec<u8>>> {
2227        let data = self.load_chunk_data(entry, dataset_addr, chunk_shape, elem_size)?;
2228        validate_decoded_chunk_len(entry, chunk_shape, elem_size, data.len())?;
2229        Ok(data)
2230    }
2231
2232    /// Chunked slice: only read chunks that overlap the selection.
2233    ///
2234    /// Resolves each `SliceInfoElem` to concrete ranges, computes the chunk
2235    /// grid range per dimension, and only decompresses overlapping chunks.
2236    fn read_chunked_slice<T: H5Type>(
2237        &self,
2238        index_address: u64,
2239        chunk_dims: &[u32],
2240        _element_size: u32,
2241        chunk_indexing: Option<&ChunkIndexing>,
2242        _selection: &SliceInfo,
2243        resolved: &ResolvedSelection,
2244    ) -> Result<ArrayD<T>> {
2245        if resolved.result_elements == 0 {
2246            return self.make_fill_array_from_shape::<T>(0, &resolved.result_shape);
2247        }
2248
2249        if Cursor::is_undefined_offset(index_address, self.offset_size()) {
2250            return self
2251                .make_fill_array_from_shape::<T>(resolved.result_elements, &resolved.result_shape);
2252        }
2253
2254        let ndim = self.ndim();
2255        let shape = &self.dataspace.dims;
2256        let elem_size = self.raw_element_size()?;
2257
2258        if let Some(array) = self.try_read_single_chunk_unit_stride_2d::<T>(
2259            index_address,
2260            chunk_dims,
2261            chunk_indexing,
2262            shape,
2263            elem_size,
2264            resolved,
2265        )? {
2266            return Ok(array);
2267        }
2268
2269        let chunk_shape: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
2270        validate_chunk_shape(shape, &chunk_shape)?;
2271        let mut first_chunk = vec![0u64; ndim];
2272        let mut last_chunk = vec![0u64; ndim];
2273        for d in 0..ndim {
2274            let (first, last) = resolved.dims[d]
2275                .chunk_index_range(chunk_shape[d])
2276                .expect("zero-sized result handled above");
2277            first_chunk[d] = first;
2278            last_chunk[d] = last;
2279        }
2280
2281        // Collect all chunk entries.
2282        let mut overlapping = self.collect_chunk_entries(
2283            index_address,
2284            chunk_dims,
2285            chunk_indexing,
2286            ChunkEntrySelection {
2287                shape,
2288                ndim,
2289                elem_size,
2290                chunk_bounds: Some((&first_chunk, &last_chunk)),
2291            },
2292        )?;
2293        let fully_covered_grid = validate_chunk_grid_coverage(
2294            &mut overlapping,
2295            shape,
2296            &chunk_shape,
2297            &first_chunk,
2298            &last_chunk,
2299        )?;
2300
2301        let result_total_bytes = checked_mul_usize(
2302            resolved.result_elements,
2303            elem_size,
2304            "slice result size in bytes",
2305        )?;
2306        // Compute result strides (including collapsed dims — they have count=1).
2307        let result_dims = resolved.result_dims_with_collapsed();
2308        let mut result_strides = vec![1usize; ndim];
2309        for d in (0..ndim.saturating_sub(1)).rev() {
2310            result_strides[d] =
2311                checked_mul_usize(result_strides[d + 1], result_dims[d + 1], "result stride")?;
2312        }
2313        let mut chunk_strides = vec![1usize; ndim];
2314        for d in (0..ndim.saturating_sub(1)).rev() {
2315            chunk_strides[d] = checked_mul_usize(
2316                chunk_strides[d + 1],
2317                chunk_shape[d + 1] as usize,
2318                "chunk stride",
2319            )?;
2320        }
2321        let use_unit_stride_fast_path = resolved.is_unit_stride();
2322        let fully_covered_unit_stride = use_unit_stride_fast_path && fully_covered_grid;
2323
2324        if fully_covered_unit_stride {
2325            if T::native_copy_compatible(&self.datatype) && std::mem::size_of::<T>() == elem_size {
2326                let mut result_values: Vec<MaybeUninit<T>> =
2327                    std::iter::repeat_with(MaybeUninit::<T>::uninit)
2328                        .take(resolved.result_elements)
2329                        .collect();
2330                let result_ptr = result_values.as_mut_ptr() as *mut u8;
2331                let result_len = checked_mul_usize(
2332                    result_values.len(),
2333                    std::mem::size_of::<T>(),
2334                    "typed slice result size in bytes",
2335                )?;
2336
2337                for entry in &overlapping {
2338                    let chunk_data =
2339                        self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
2340
2341                    // SAFETY: validated chunk offsets are unique, the overlap
2342                    // helper checks every range, and the output allocation
2343                    // remains live for the duration of the copy.
2344                    unsafe {
2345                        copy_unit_stride_chunk_overlap_ptr(
2346                            &chunk_data,
2347                            FlatBufferPtr {
2348                                ptr: result_ptr,
2349                                len: result_len,
2350                            },
2351                            UnitStrideCopyLayout {
2352                                chunk_offsets: &entry.offsets,
2353                                chunk_shape: &chunk_shape,
2354                                dataset_shape: shape,
2355                                resolved,
2356                                chunk_strides: &chunk_strides,
2357                                result_strides: &result_strides,
2358                                elem_size,
2359                            },
2360                        )?;
2361                    }
2362                }
2363
2364                // SAFETY: the fully-covered selection gate proves that the
2365                // successful overlap copies initialized every result element.
2366                let result_values = unsafe { assume_init_vec(result_values) };
2367                return ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), result_values)
2368                    .map_err(|e| Error::InvalidData(format!("array shape error: {e}")));
2369            }
2370
2371            let mut result_buf = vec![MaybeUninit::<u8>::uninit(); result_total_bytes];
2372            let result_ptr = result_buf.as_mut_ptr() as *mut u8;
2373            let result_len = result_buf.len();
2374
2375            for entry in &overlapping {
2376                let chunk_data =
2377                    self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
2378
2379                // SAFETY: validated chunk offsets are unique, the overlap
2380                // helper checks every range, and the output allocation remains
2381                // live for the duration of the copy.
2382                unsafe {
2383                    copy_unit_stride_chunk_overlap_ptr(
2384                        &chunk_data,
2385                        FlatBufferPtr {
2386                            ptr: result_ptr,
2387                            len: result_len,
2388                        },
2389                        UnitStrideCopyLayout {
2390                            chunk_offsets: &entry.offsets,
2391                            chunk_shape: &chunk_shape,
2392                            dataset_shape: shape,
2393                            resolved,
2394                            chunk_strides: &chunk_strides,
2395                            result_strides: &result_strides,
2396                            elem_size,
2397                        },
2398                    )?;
2399                }
2400            }
2401
2402            // SAFETY: the fully-covered selection gate proves that the
2403            // successful overlap copies initialized every result byte.
2404            let result_buf = unsafe { assume_init_u8_vec(result_buf) };
2405            return self.decode_buffer_with_shape::<T>(
2406                &result_buf,
2407                resolved.result_elements,
2408                &resolved.result_shape,
2409            );
2410        }
2411
2412        let mut result_buf = self.make_output_buffer(result_total_bytes);
2413
2414        // For each overlapping chunk: decompress and copy matching elements.
2415        for entry in &overlapping {
2416            let chunk_data =
2417                self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
2418
2419            if use_unit_stride_fast_path {
2420                copy_unit_stride_chunk_overlap(
2421                    &chunk_data,
2422                    &mut result_buf,
2423                    UnitStrideCopyLayout {
2424                        chunk_offsets: &entry.offsets,
2425                        chunk_shape: &chunk_shape,
2426                        dataset_shape: shape,
2427                        resolved,
2428                        chunk_strides: &chunk_strides,
2429                        result_strides: &result_strides,
2430                        elem_size,
2431                    },
2432                )?;
2433                continue;
2434            }
2435
2436            // For each dimension, compute which elements within this chunk fall
2437            // within the selection.
2438            let mut dim_indices: Vec<Vec<(usize, usize)>> = Vec::with_capacity(ndim);
2439            for d in 0..ndim {
2440                let chunk_start = entry.offsets[d];
2441                let chunk_end = (chunk_start + chunk_shape[d]).min(shape[d]);
2442                let dim = &resolved.dims[d];
2443                let sel_start = dim.start;
2444                let sel_end = dim.end;
2445                let sel_step = dim.step;
2446                let mut indices = Vec::new();
2447
2448                // Find first selected index >= chunk_start
2449                let first_sel = if sel_start >= chunk_start {
2450                    sel_start
2451                } else {
2452                    let steps_to_skip = (chunk_start - sel_start).div_ceil(sel_step);
2453                    sel_start + steps_to_skip * sel_step
2454                };
2455
2456                let mut sel_idx = first_sel;
2457                while sel_idx < sel_end && sel_idx < chunk_end {
2458                    let chunk_local = checked_usize(sel_idx - chunk_start, "chunk-local index")?;
2459                    // Compute result-space index for this dimension.
2460                    let result_dim_idx =
2461                        checked_usize((sel_idx - dim.start) / sel_step, "result index")?;
2462                    indices.push((chunk_local, result_dim_idx));
2463                    sel_idx += sel_step;
2464                }
2465
2466                dim_indices.push(indices);
2467            }
2468
2469            // Iterate over the cartesian product of matching indices.
2470            copy_selected_elements(
2471                &chunk_data,
2472                &mut result_buf,
2473                &dim_indices,
2474                &chunk_strides,
2475                &result_strides,
2476                elem_size,
2477                ndim,
2478            )?;
2479        }
2480
2481        self.decode_buffer_with_shape::<T>(
2482            &result_buf,
2483            resolved.result_elements,
2484            &resolved.result_shape,
2485        )
2486    }
2487
2488    fn try_read_single_chunk_unit_stride_2d<T: H5Type>(
2489        &self,
2490        index_address: u64,
2491        chunk_dims: &[u32],
2492        chunk_indexing: Option<&ChunkIndexing>,
2493        shape: &[u64],
2494        elem_size: usize,
2495        resolved: &ResolvedSelection,
2496    ) -> Result<Option<ArrayD<T>>> {
2497        if shape.len() != 2
2498            || chunk_dims.len() != 2
2499            || resolved.dims.len() != 2
2500            || resolved.result_shape.len() != 2
2501            || !resolved.is_unit_stride()
2502            || std::mem::size_of::<T>() != elem_size
2503            || !T::native_copy_compatible(&self.datatype)
2504        {
2505            return Ok(None);
2506        }
2507
2508        let chunk_shape = [u64::from(chunk_dims[0]), u64::from(chunk_dims[1])];
2509        validate_chunk_shape(shape, &chunk_shape)?;
2510
2511        let row_dim = &resolved.dims[0];
2512        let col_dim = &resolved.dims[1];
2513        let Some((first_row_chunk, last_row_chunk)) = row_dim.chunk_index_range(chunk_shape[0])
2514        else {
2515            return Ok(None);
2516        };
2517        let Some((first_col_chunk, last_col_chunk)) = col_dim.chunk_index_range(chunk_shape[1])
2518        else {
2519            return Ok(None);
2520        };
2521        if first_row_chunk != last_row_chunk || first_col_chunk != last_col_chunk {
2522            return Ok(None);
2523        }
2524
2525        let first_chunk = [first_row_chunk, first_col_chunk];
2526        let last_chunk = first_chunk;
2527        let expected_offsets = [
2528            checked_mul_u64(first_row_chunk, chunk_shape[0], "single chunk row offset")?,
2529            checked_mul_u64(
2530                first_col_chunk,
2531                chunk_shape[1],
2532                "single chunk column offset",
2533            )?,
2534        ];
2535
2536        let entries = self.collect_chunk_entries(
2537            index_address,
2538            chunk_dims,
2539            chunk_indexing,
2540            ChunkEntrySelection {
2541                shape,
2542                ndim: 2,
2543                elem_size,
2544                chunk_bounds: Some((&first_chunk, &last_chunk)),
2545            },
2546        )?;
2547
2548        if entries.is_empty() {
2549            return self
2550                .make_fill_array_from_shape::<T>(resolved.result_elements, &resolved.result_shape)
2551                .map(Some);
2552        }
2553
2554        if entries.len() != 1 || entries[0].offsets.as_slice() != expected_offsets.as_slice() {
2555            return Ok(None);
2556        }
2557
2558        let entry = &entries[0];
2559        let chunk_data =
2560            self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
2561
2562        let row_start = checked_usize(
2563            row_dim.start - expected_offsets[0],
2564            "single chunk slice row start",
2565        )?;
2566        let col_start = checked_usize(
2567            col_dim.start - expected_offsets[1],
2568            "single chunk slice column start",
2569        )?;
2570        let row_count = row_dim.count;
2571        let col_count = col_dim.count;
2572        let chunk_cols = checked_usize(chunk_shape[1], "single chunk column extent")?;
2573        let row_bytes = checked_mul_usize(col_count, elem_size, "single chunk slice row bytes")?;
2574
2575        let mut result_values: Vec<MaybeUninit<T>> =
2576            std::iter::repeat_with(MaybeUninit::<T>::uninit)
2577                .take(resolved.result_elements)
2578                .collect();
2579        let result_ptr = result_values.as_mut_ptr() as *mut u8;
2580        let result_len = checked_mul_usize(
2581            result_values.len(),
2582            std::mem::size_of::<T>(),
2583            "typed slice result size in bytes",
2584        )?;
2585
2586        for row in 0..row_count {
2587            let chunk_row = checked_add_usize(row_start, row, "single chunk slice source row")?;
2588            let src_elem = checked_add_usize(
2589                checked_mul_usize(chunk_row, chunk_cols, "single chunk slice source row")?,
2590                col_start,
2591                "single chunk slice source element",
2592            )?;
2593            let dst_elem = checked_mul_usize(row, col_count, "single chunk slice destination row")?;
2594
2595            let src_start =
2596                checked_mul_usize(src_elem, elem_size, "single chunk slice source byte offset")?;
2597            let dst_start = checked_mul_usize(
2598                dst_elem,
2599                elem_size,
2600                "single chunk slice destination byte offset",
2601            )?;
2602            let src_end =
2603                checked_add_usize(src_start, row_bytes, "single chunk slice source byte end")?;
2604            let dst_end = checked_add_usize(
2605                dst_start,
2606                row_bytes,
2607                "single chunk slice destination byte end",
2608            )?;
2609
2610            if src_end > chunk_data.len() || dst_end > result_len {
2611                return Err(Error::InvalidData(format!(
2612                    "single chunk unit-stride slice out of bounds: source range {}..{} of {} bytes, destination range {}..{} of {} bytes",
2613                    src_start,
2614                    src_end,
2615                    chunk_data.len(),
2616                    dst_start,
2617                    dst_end,
2618                    result_len
2619                )));
2620            }
2621
2622            // SAFETY: the source and destination ranges were checked above;
2623            // their allocations are live and distinct.
2624            unsafe {
2625                std::ptr::copy_nonoverlapping(
2626                    chunk_data.as_ptr().add(src_start),
2627                    result_ptr.add(dst_start),
2628                    row_bytes,
2629                );
2630            }
2631        }
2632
2633        // SAFETY: this fast path covers one complete unit-stride selection and
2634        // the row loop initialized every checked destination range.
2635        let result_values = unsafe { assume_init_vec(result_values) };
2636        ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), result_values)
2637            .map(Some)
2638            .map_err(|e| Error::InvalidData(format!("array shape error: {e}")))
2639    }
2640
2641    /// Parallel variant of `read_chunked_slice`: decompresses overlapping chunks
2642    /// in parallel using Rayon, then copies selected elements into the result buffer.
2643    ///
2644    /// Each chunk writes to a disjoint region of the result buffer (chunks don't
2645    /// overlap in output space), so this is safe to parallelize.
2646    #[cfg(feature = "rayon")]
2647    fn read_chunked_slice_parallel<T: H5Type>(
2648        &self,
2649        index_address: u64,
2650        chunk_dims: &[u32],
2651        _element_size: u32,
2652        chunk_indexing: Option<&ChunkIndexing>,
2653        _selection: &SliceInfo,
2654        resolved: &ResolvedSelection,
2655    ) -> Result<ArrayD<T>> {
2656        if resolved.result_elements == 0 {
2657            return self.make_fill_array_from_shape::<T>(0, &resolved.result_shape);
2658        }
2659
2660        if Cursor::is_undefined_offset(index_address, self.offset_size()) {
2661            return self
2662                .make_fill_array_from_shape::<T>(resolved.result_elements, &resolved.result_shape);
2663        }
2664
2665        let ndim = self.ndim();
2666        let shape = &self.dataspace.dims;
2667        let elem_size = self.raw_element_size()?;
2668        let chunk_shape: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
2669        validate_chunk_shape(shape, &chunk_shape)?;
2670        let mut first_chunk = vec![0u64; ndim];
2671        let mut last_chunk = vec![0u64; ndim];
2672        for d in 0..ndim {
2673            let (first, last) = resolved.dims[d]
2674                .chunk_index_range(chunk_shape[d])
2675                .expect("zero-sized result handled above");
2676            first_chunk[d] = first;
2677            last_chunk[d] = last;
2678        }
2679
2680        // Collect all chunk entries.
2681        let mut overlapping = self.collect_chunk_entries(
2682            index_address,
2683            chunk_dims,
2684            chunk_indexing,
2685            ChunkEntrySelection {
2686                shape,
2687                ndim,
2688                elem_size,
2689                chunk_bounds: Some((&first_chunk, &last_chunk)),
2690            },
2691        )?;
2692        let fully_covered_grid = validate_chunk_grid_coverage(
2693            &mut overlapping,
2694            shape,
2695            &chunk_shape,
2696            &first_chunk,
2697            &last_chunk,
2698        )?;
2699
2700        // Allocate result buffer (raw bytes) initialized from fill value.
2701        let result_total_bytes = checked_mul_usize(
2702            resolved.result_elements,
2703            elem_size,
2704            "slice result size in bytes",
2705        )?;
2706        // Compute result strides (including collapsed dims — they have count=1).
2707        let result_dims = resolved.result_dims_with_collapsed();
2708        let mut result_strides = vec![1usize; ndim];
2709        for d in (0..ndim.saturating_sub(1)).rev() {
2710            result_strides[d] =
2711                checked_mul_usize(result_strides[d + 1], result_dims[d + 1], "result stride")?;
2712        }
2713        let mut chunk_strides = vec![1usize; ndim];
2714        for d in (0..ndim.saturating_sub(1)).rev() {
2715            chunk_strides[d] = checked_mul_usize(
2716                chunk_strides[d + 1],
2717                chunk_shape[d + 1] as usize,
2718                "chunk stride",
2719            )?;
2720        }
2721        let use_unit_stride_fast_path = resolved.is_unit_stride();
2722        let fully_covered_unit_stride = use_unit_stride_fast_path && fully_covered_grid;
2723
2724        if fully_covered_unit_stride {
2725            if T::native_copy_compatible(&self.datatype) && std::mem::size_of::<T>() == elem_size {
2726                let mut result_values: Vec<MaybeUninit<T>> =
2727                    std::iter::repeat_with(MaybeUninit::<T>::uninit)
2728                        .take(resolved.result_elements)
2729                        .collect();
2730                let flat = FlatBufferPtr {
2731                    ptr: result_values.as_mut_ptr() as *mut u8,
2732                    len: checked_mul_usize(
2733                        result_values.len(),
2734                        std::mem::size_of::<T>(),
2735                        "typed slice result size in bytes",
2736                    )?,
2737                };
2738
2739                overlapping
2740                    .par_iter()
2741                    .map(|entry| {
2742                        let chunk_data = self.load_exact_chunk_data(
2743                            entry,
2744                            index_address,
2745                            &chunk_shape,
2746                            elem_size,
2747                        )?;
2748
2749                        // SAFETY: validated chunk offsets are unique, so
2750                        // parallel overlap copies write disjoint ranges;
2751                        // `result_values` remains live through collect.
2752                        unsafe {
2753                            flat.copy_unit_stride_chunk_overlap(
2754                                &chunk_data,
2755                                UnitStrideCopyLayout {
2756                                    chunk_offsets: &entry.offsets,
2757                                    chunk_shape: &chunk_shape,
2758                                    dataset_shape: shape,
2759                                    resolved,
2760                                    chunk_strides: &chunk_strides,
2761                                    result_strides: &result_strides,
2762                                    elem_size,
2763                                },
2764                            )?;
2765                        }
2766
2767                        Ok(())
2768                    })
2769                    .collect::<std::result::Result<Vec<_>, Error>>()?;
2770
2771                // SAFETY: the fully-covered selection gate proves that all
2772                // successful parallel copies initialized the result.
2773                let result_values = unsafe { assume_init_vec(result_values) };
2774                return ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), result_values)
2775                    .map_err(|e| Error::InvalidData(format!("array shape error: {e}")));
2776            }
2777
2778            let mut result_buf = vec![MaybeUninit::<u8>::uninit(); result_total_bytes];
2779            let flat = FlatBufferPtr {
2780                ptr: result_buf.as_mut_ptr() as *mut u8,
2781                len: result_buf.len(),
2782            };
2783
2784            overlapping
2785                .par_iter()
2786                .map(|entry| {
2787                    let chunk_data =
2788                        self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
2789
2790                    // SAFETY: validated chunk offsets are unique, so parallel
2791                    // overlap copies write disjoint ranges; `result_buf`
2792                    // remains live through collect.
2793                    unsafe {
2794                        flat.copy_unit_stride_chunk_overlap(
2795                            &chunk_data,
2796                            UnitStrideCopyLayout {
2797                                chunk_offsets: &entry.offsets,
2798                                chunk_shape: &chunk_shape,
2799                                dataset_shape: shape,
2800                                resolved,
2801                                chunk_strides: &chunk_strides,
2802                                result_strides: &result_strides,
2803                                elem_size,
2804                            },
2805                        )?;
2806                    }
2807
2808                    Ok(())
2809                })
2810                .collect::<std::result::Result<Vec<_>, Error>>()?;
2811
2812            // SAFETY: the fully-covered selection gate proves that all
2813            // successful parallel copies initialized every result byte.
2814            let result_buf = unsafe { assume_init_u8_vec(result_buf) };
2815            return self.decode_buffer_with_shape::<T>(
2816                &result_buf,
2817                resolved.result_elements,
2818                &resolved.result_shape,
2819            );
2820        }
2821
2822        let mut result_buf = self.make_output_buffer(result_total_bytes);
2823
2824        let flat = FlatBufferPtr {
2825            ptr: result_buf.as_mut_ptr(),
2826            len: result_buf.len(),
2827        };
2828
2829        overlapping
2830            .par_iter()
2831            .map(|entry| {
2832                let chunk_data =
2833                    self.load_exact_chunk_data(entry, index_address, &chunk_shape, elem_size)?;
2834
2835                if use_unit_stride_fast_path {
2836                    // SAFETY: validated chunk offsets are unique, so parallel
2837                    // overlap copies write disjoint ranges; `result_buf`
2838                    // remains live through collect.
2839                    unsafe {
2840                        flat.copy_unit_stride_chunk_overlap(
2841                            &chunk_data,
2842                            UnitStrideCopyLayout {
2843                                chunk_offsets: &entry.offsets,
2844                                chunk_shape: &chunk_shape,
2845                                dataset_shape: shape,
2846                                resolved,
2847                                chunk_strides: &chunk_strides,
2848                                result_strides: &result_strides,
2849                                elem_size,
2850                            },
2851                        )?;
2852                    }
2853                    return Ok(());
2854                }
2855
2856                // For each dimension, compute which elements within this chunk fall
2857                // within the selection.
2858                let mut dim_indices: Vec<Vec<(usize, usize)>> = Vec::with_capacity(ndim);
2859                for d in 0..ndim {
2860                    let chunk_start = entry.offsets[d];
2861                    let chunk_end = (chunk_start + chunk_shape[d]).min(shape[d]);
2862                    let dim = &resolved.dims[d];
2863                    let sel_start = dim.start;
2864                    let sel_end = dim.end;
2865                    let sel_step = dim.step;
2866                    let mut indices = Vec::new();
2867
2868                    let first_sel = if sel_start >= chunk_start {
2869                        sel_start
2870                    } else {
2871                        let steps_to_skip = (chunk_start - sel_start).div_ceil(sel_step);
2872                        sel_start + steps_to_skip * sel_step
2873                    };
2874
2875                    let mut sel_idx = first_sel;
2876                    while sel_idx < sel_end && sel_idx < chunk_end {
2877                        let chunk_local =
2878                            checked_usize(sel_idx - chunk_start, "chunk-local index")?;
2879                        let result_dim_idx =
2880                            checked_usize((sel_idx - dim.start) / sel_step, "result index")?;
2881                        indices.push((chunk_local, result_dim_idx));
2882                        sel_idx += sel_step;
2883                    }
2884
2885                    dim_indices.push(indices);
2886                }
2887
2888                // SAFETY: each chunk writes to disjoint output positions because
2889                // chunks occupy non-overlapping regions of the dataset grid and
2890                // the selection maps each dataset coordinate to a unique result index.
2891                unsafe {
2892                    flat.copy_selected(
2893                        &chunk_data,
2894                        &dim_indices,
2895                        &chunk_strides,
2896                        &result_strides,
2897                        elem_size,
2898                        ndim,
2899                    )?;
2900                }
2901
2902                Ok(())
2903            })
2904            .collect::<std::result::Result<Vec<_>, Error>>()?;
2905
2906        self.decode_buffer_with_shape::<T>(
2907            &result_buf,
2908            resolved.result_elements,
2909            &resolved.result_shape,
2910        )
2911    }
2912
2913    fn read_contiguous_slice<T: H5Type>(
2914        &self,
2915        address: u64,
2916        size: u64,
2917        resolved: &ResolvedSelection,
2918    ) -> Result<ArrayD<T>> {
2919        if resolved.result_elements == 0 {
2920            return self.make_fill_array_from_shape::<T>(0, &resolved.result_shape);
2921        }
2922
2923        if self.external_files.is_none()
2924            && (Cursor::is_undefined_offset(address, self.offset_size()) || size == 0)
2925        {
2926            return self
2927                .make_fill_array_from_shape::<T>(resolved.result_elements, &resolved.result_shape);
2928        }
2929        if self.external_files.is_none() {
2930            self.validate_allocated_raw_data_len(
2931                "contiguous",
2932                checked_usize(size, "contiguous dataset size")?,
2933            )?;
2934        }
2935
2936        let shape = &self.dataspace.dims;
2937        if selection_covers_full_dataset(resolved, shape) {
2938            return self.read_contiguous::<T>(address, size);
2939        }
2940
2941        let elem_size = self.raw_element_size()?;
2942        let result_total_bytes = checked_mul_usize(
2943            resolved.result_elements,
2944            elem_size,
2945            "contiguous slice result size in bytes",
2946        )?;
2947        let dataset_strides = row_major_strides(shape, "contiguous dataset stride")?;
2948        let result_dims = resolved.result_dims_with_collapsed();
2949        let result_strides = result_strides_for_dims(&result_dims)?;
2950        let result_buf = self.read_contiguous_slice_bytes_direct(
2951            address,
2952            size,
2953            resolved,
2954            ContiguousSliceDirectLayout {
2955                dataset_strides: &dataset_strides,
2956                result_strides: &result_strides,
2957                elem_size,
2958                result_total_bytes,
2959            },
2960        )?;
2961
2962        self.decode_buffer_with_shape::<T>(
2963            &result_buf,
2964            resolved.result_elements,
2965            &resolved.result_shape,
2966        )
2967    }
2968
2969    fn read_contiguous_slice_bytes_direct(
2970        &self,
2971        address: u64,
2972        size: u64,
2973        resolved: &ResolvedSelection,
2974        layout: ContiguousSliceDirectLayout<'_>,
2975    ) -> Result<Vec<u8>> {
2976        let shape = &self.dataspace.dims;
2977        let ndim = shape.len();
2978        if resolved.dims.len() != ndim
2979            || layout.dataset_strides.len() != ndim
2980            || layout.result_strides.len() != ndim
2981        {
2982            return Err(Error::InvalidData(format!(
2983                "contiguous slice layout rank does not match dataset rank {ndim}"
2984            )));
2985        }
2986
2987        let storage_len = if self.external_files.is_some() {
2988            checked_mul_usize(
2989                checked_usize(self.num_elements()?, "dataset element count")?,
2990                layout.elem_size,
2991                "external dataset size",
2992            )?
2993        } else {
2994            checked_usize(size, "contiguous dataset size")?
2995        };
2996        let tail_start = contiguous_slice_tail_start(shape, resolved);
2997        let block_elements = contiguous_slice_block_elements(resolved, tail_start)?;
2998        let block_bytes = checked_mul_usize(
2999            block_elements,
3000            layout.elem_size,
3001            "contiguous slice block size in bytes",
3002        )?;
3003        let mut result_buf = self.make_output_buffer(layout.result_total_bytes);
3004
3005        let prefix_blocks =
3006            resolved.dims[..tail_start]
3007                .iter()
3008                .try_fold(1usize, |acc, dim| -> Result<usize> {
3009                    checked_mul_usize(acc, dim.count, "contiguous slice block count")
3010                })?;
3011        let mut counters = vec![0usize; tail_start];
3012
3013        for _ in 0..prefix_blocks {
3014            let mut source_elem = 0usize;
3015            let mut result_elem = 0usize;
3016
3017            for (d, &counter) in counters.iter().enumerate().take(tail_start) {
3018                let ordinal = u64::try_from(counter).map_err(|_| {
3019                    Error::InvalidData("contiguous slice ordinal exceeds u64".to_string())
3020                })?;
3021                let coord = checked_add_u64(
3022                    resolved.dims[d].start,
3023                    checked_mul_u64(
3024                        ordinal,
3025                        resolved.dims[d].step,
3026                        "contiguous slice coordinate",
3027                    )?,
3028                    "contiguous slice coordinate",
3029                )?;
3030                let coord = checked_usize(coord, "contiguous slice source index")?;
3031                let source_term =
3032                    checked_mul_usize(coord, layout.dataset_strides[d], "contiguous slice source")?;
3033                let result_term = checked_mul_usize(
3034                    counter,
3035                    layout.result_strides[d],
3036                    "contiguous slice result",
3037                )?;
3038                source_elem =
3039                    checked_add_usize(source_elem, source_term, "contiguous slice source")?;
3040                result_elem =
3041                    checked_add_usize(result_elem, result_term, "contiguous slice result")?;
3042            }
3043
3044            for (d, &dataset_stride) in layout
3045                .dataset_strides
3046                .iter()
3047                .enumerate()
3048                .take(ndim)
3049                .skip(tail_start)
3050            {
3051                let coord = checked_usize(resolved.dims[d].start, "contiguous slice source index")?;
3052                let source_term =
3053                    checked_mul_usize(coord, dataset_stride, "contiguous slice source")?;
3054                source_elem =
3055                    checked_add_usize(source_elem, source_term, "contiguous slice source")?;
3056            }
3057
3058            let source_start = checked_mul_usize(
3059                source_elem,
3060                layout.elem_size,
3061                "contiguous slice source byte offset",
3062            )?;
3063            let source_end = checked_add_usize(
3064                source_start,
3065                block_bytes,
3066                "contiguous slice source byte end",
3067            )?;
3068            if source_end > storage_len {
3069                return Err(Error::InvalidData(format!(
3070                    "contiguous slice range {}..{} exceeds dataset storage size {}",
3071                    source_start, source_end, storage_len
3072                )));
3073            }
3074
3075            let dst_start = checked_mul_usize(
3076                result_elem,
3077                layout.elem_size,
3078                "contiguous slice destination byte offset",
3079            )?;
3080            let dst_end = checked_add_usize(
3081                dst_start,
3082                block_bytes,
3083                "contiguous slice destination byte end",
3084            )?;
3085            if dst_end > result_buf.len() {
3086                return Err(Error::InvalidData(format!(
3087                    "contiguous slice destination range {}..{} exceeds result size {}",
3088                    dst_start,
3089                    dst_end,
3090                    result_buf.len()
3091                )));
3092            }
3093
3094            let block = self.read_contiguous_logical_range(address, source_start, block_bytes)?;
3095            if block.len() != block_bytes {
3096                return Err(Error::InvalidData(format!(
3097                    "contiguous slice read returned {} bytes, expected {}",
3098                    block.len(),
3099                    block_bytes
3100                )));
3101            }
3102            result_buf[dst_start..dst_end].copy_from_slice(&block);
3103
3104            let mut carry = true;
3105            for d in (0..tail_start).rev() {
3106                if carry {
3107                    counters[d] += 1;
3108                    if counters[d] < resolved.dims[d].count {
3109                        carry = false;
3110                    } else {
3111                        counters[d] = 0;
3112                    }
3113                }
3114            }
3115        }
3116
3117        Ok(result_buf)
3118    }
3119
3120    fn read_compact_slice<T: H5Type>(
3121        &self,
3122        data: &[u8],
3123        selection: &SliceInfo,
3124    ) -> Result<ArrayD<T>> {
3125        let full = self.read_compact::<T>(data)?;
3126        slice_array(&full, selection, &self.dataspace.dims)
3127    }
3128
3129    fn decode_buffer_with_shape<T: H5Type>(
3130        &self,
3131        raw: &[u8],
3132        n: usize,
3133        shape: &[usize],
3134    ) -> Result<ArrayD<T>> {
3135        let elem_size = self.raw_element_size()?;
3136        let expected_bytes = checked_mul_usize(n, elem_size, "decoded buffer byte length")?;
3137        if raw.len() != expected_bytes {
3138            return Err(Error::InvalidData(format!(
3139                "decoded buffer has {} bytes, expected {} bytes",
3140                raw.len(),
3141                expected_bytes
3142            )));
3143        }
3144
3145        if let Some(elements) = T::decode_vec(raw, &self.datatype, n) {
3146            let elements = elements?;
3147            return ArrayD::from_shape_vec(IxDyn(shape), elements)
3148                .map_err(|e| Error::InvalidData(format!("array shape error: {e}")));
3149        }
3150
3151        let mut elements = Vec::with_capacity(n);
3152        for i in 0..n {
3153            let start = checked_mul_usize(i, elem_size, "decoded element byte offset")?;
3154            let end = checked_mul_usize(i + 1, elem_size, "decoded element end offset")?;
3155            elements.push(T::from_bytes(&raw[start..end], &self.datatype)?);
3156        }
3157
3158        ArrayD::from_shape_vec(IxDyn(shape), elements)
3159            .map_err(|e| Error::InvalidData(format!("array shape error: {e}")))
3160    }
3161
3162    fn decode_raw_data<T: H5Type>(&self, raw: &[u8]) -> Result<ArrayD<T>> {
3163        let n = checked_usize(self.num_elements()?, "dataset element count")?;
3164        let mut shape = Vec::with_capacity(self.dataspace.dims.len());
3165        for &dim in &self.dataspace.dims {
3166            shape.push(checked_usize(dim, "dataset dimension")?);
3167        }
3168        self.decode_buffer_with_shape::<T>(raw, n, &shape)
3169    }
3170
3171    fn make_fill_array<T: H5Type>(&self) -> Result<ArrayD<T>> {
3172        let n = checked_usize(self.num_elements()?, "dataset element count")?;
3173        let mut shape = Vec::with_capacity(self.dataspace.dims.len());
3174        for &dim in &self.dataspace.dims {
3175            shape.push(checked_usize(dim, "dataset dimension")?);
3176        }
3177        self.make_fill_array_from_shape::<T>(n, &shape)
3178    }
3179
3180    fn make_fill_array_from_shape<T: H5Type>(
3181        &self,
3182        element_count: usize,
3183        shape: &[usize],
3184    ) -> Result<ArrayD<T>> {
3185        let elem_size = self.raw_element_size()?;
3186        let total_bytes = checked_mul_usize(element_count, elem_size, "fill result size in bytes")?;
3187        let fill = self.make_output_buffer(total_bytes);
3188        self.decode_buffer_with_shape::<T>(&fill, element_count, shape)
3189    }
3190
3191    fn make_output_buffer(&self, total_bytes: usize) -> Vec<u8> {
3192        let mut buf = vec![0u8; total_bytes];
3193        self.fill_output_buffer(&mut buf);
3194        buf
3195    }
3196
3197    fn fill_output_buffer(&self, buf: &mut [u8]) {
3198        buf.fill(0);
3199        if let Some(ref fv) = self.fill_value {
3200            if let Some(ref fill_bytes) = fv.value {
3201                if !fill_bytes.is_empty() {
3202                    for chunk in buf.chunks_exact_mut(fill_bytes.len()) {
3203                        chunk.copy_from_slice(fill_bytes);
3204                    }
3205                }
3206            }
3207        }
3208    }
3209
3210    fn validate_allocated_raw_data_len(&self, storage_kind: &str, actual_len: usize) -> Result<()> {
3211        let expected_len = self.raw_byte_len()?;
3212        if actual_len != expected_len {
3213            return Err(Error::InvalidData(format!(
3214                "{storage_kind} raw data has {actual_len} bytes, expected {expected_len} bytes"
3215            )));
3216        }
3217        Ok(())
3218    }
3219
3220    fn convert_to_native_endian(&self, bytes: &mut [u8]) -> Result<()> {
3221        let count = checked_usize(self.num_elements()?, "dataset element count")?;
3222        convert_datatype_to_native_endian(&self.datatype, self.vlen_reference_size(), bytes, count)
3223    }
3224}
3225
3226fn native_byte_order() -> ByteOrder {
3227    if cfg!(target_endian = "little") {
3228        ByteOrder::LittleEndian
3229    } else {
3230        ByteOrder::BigEndian
3231    }
3232}
3233
3234fn convert_datatype_to_native_endian(
3235    dtype: &Datatype,
3236    vlen_reference_size: usize,
3237    bytes: &mut [u8],
3238    count: usize,
3239) -> Result<()> {
3240    match dtype {
3241        Datatype::FixedPoint {
3242            size, byte_order, ..
3243        }
3244        | Datatype::FloatingPoint { size, byte_order }
3245        | Datatype::Bitfield { size, byte_order } => {
3246            swap_elements_to_native(bytes, count, *size as usize, *byte_order)
3247        }
3248        Datatype::Enum { base, .. } => {
3249            convert_datatype_to_native_endian(base, vlen_reference_size, bytes, count)
3250        }
3251        Datatype::Array { base, dims } => {
3252            let array_count = dims.iter().try_fold(1usize, |acc, &dim| {
3253                checked_mul_usize(
3254                    acc,
3255                    checked_usize(dim, "array datatype dimension")?,
3256                    "array datatype element count",
3257                )
3258            })?;
3259            let total_count =
3260                checked_mul_usize(count, array_count, "array datatype total element count")?;
3261            convert_datatype_to_native_endian(base, vlen_reference_size, bytes, total_count)
3262        }
3263        Datatype::Compound { size, fields } => {
3264            let record_size = *size as usize;
3265            let required = checked_mul_usize(count, record_size, "compound byte length")?;
3266            if bytes.len() < required {
3267                return Err(Error::InvalidData(format!(
3268                    "compound native-endian conversion needs {required} bytes, got {}",
3269                    bytes.len()
3270                )));
3271            }
3272
3273            for record in 0..count {
3274                let record_start =
3275                    checked_mul_usize(record, record_size, "compound record byte offset")?;
3276                for field in fields {
3277                    let field_offset = field.byte_offset as usize;
3278                    let field_size =
3279                        raw_element_size_for_datatype(&field.datatype, vlen_reference_size)?;
3280                    let field_start = checked_add_usize(
3281                        record_start,
3282                        field_offset,
3283                        "compound field byte offset",
3284                    )?;
3285                    let field_end =
3286                        checked_add_usize(field_start, field_size, "compound field byte end")?;
3287                    if field_end > bytes.len() || field_offset + field_size > record_size {
3288                        return Err(Error::InvalidData(format!(
3289                            "compound field '{}' range exceeds record size",
3290                            field.name
3291                        )));
3292                    }
3293                    convert_datatype_to_native_endian(
3294                        &field.datatype,
3295                        vlen_reference_size,
3296                        &mut bytes[field_start..field_end],
3297                        1,
3298                    )?;
3299                }
3300            }
3301            Ok(())
3302        }
3303        Datatype::String { .. }
3304        | Datatype::VarLen { .. }
3305        | Datatype::Opaque { .. }
3306        | Datatype::Reference { .. } => Ok(()),
3307    }
3308}
3309
3310fn swap_elements_to_native(
3311    bytes: &mut [u8],
3312    count: usize,
3313    elem_size: usize,
3314    byte_order: ByteOrder,
3315) -> Result<()> {
3316    let required = checked_mul_usize(count, elem_size, "native-endian byte length")?;
3317    if bytes.len() < required {
3318        return Err(Error::InvalidData(format!(
3319            "native-endian conversion needs {required} bytes, got {}",
3320            bytes.len()
3321        )));
3322    }
3323
3324    if elem_size <= 1 || byte_order == native_byte_order() {
3325        return Ok(());
3326    }
3327
3328    for chunk in bytes[..required].chunks_exact_mut(elem_size) {
3329        chunk.reverse();
3330    }
3331    Ok(())
3332}
3333
3334fn attribute_from_message_storage(message: &AttributeMessage, context: &FileContext) -> Attribute {
3335    let decoded_strings = message
3336        .dataspace
3337        .num_elements()
3338        .ok()
3339        .and_then(|element_count| {
3340            decoded_vlen_strings_storage(
3341                &message.datatype,
3342                &message.raw_data,
3343                context.storage.as_ref(),
3344                context.superblock.offset_size,
3345                context.superblock.length_size,
3346                element_count,
3347            )
3348        });
3349    let raw_data = match &message.datatype {
3350        Datatype::VarLen {
3351            base,
3352            kind: VarLenKind::String,
3353            ..
3354        } if matches!(base.as_ref(), Datatype::FixedPoint { size: 1, .. })
3355            && matches!(message.dataspace.num_elements(), Ok(1)) =>
3356        {
3357            resolve_vlen_bytes_storage(
3358                &message.raw_data,
3359                context.storage.as_ref(),
3360                context.superblock.offset_size,
3361                context.superblock.length_size,
3362            )
3363            .unwrap_or_else(|| message.raw_data.clone())
3364        }
3365        _ => message.raw_data.clone(),
3366    };
3367
3368    Attribute {
3369        name: message.name.clone(),
3370        datatype: message.datatype.clone(),
3371        shape: match message.dataspace.dataspace_type {
3372            DataspaceType::Scalar => vec![],
3373            DataspaceType::Null => vec![0],
3374            DataspaceType::Simple => message.dataspace.dims.clone(),
3375        },
3376        raw_data,
3377        decoded_strings,
3378    }
3379}
3380
3381fn normalize_layout(layout: DataLayout, dataspace: &DataspaceMessage) -> DataLayout {
3382    match layout {
3383        DataLayout::Chunked {
3384            address,
3385            mut dims,
3386            mut element_size,
3387            chunk_indexing,
3388        } if dims.len() == dataspace.dims.len() + 1 => {
3389            if let Some(legacy_element_size) = dims.pop() {
3390                if element_size == 0 {
3391                    element_size = legacy_element_size;
3392                }
3393            }
3394            DataLayout::Chunked {
3395                address,
3396                dims,
3397                element_size,
3398                chunk_indexing,
3399            }
3400        }
3401        other => other,
3402    }
3403}
3404
3405fn raw_element_size_for_datatype(dtype: &Datatype, vlen_reference_size: usize) -> Result<usize> {
3406    match dtype {
3407        Datatype::String {
3408            size: StringSize::Variable,
3409            ..
3410        }
3411        | Datatype::VarLen { .. } => Ok(vlen_reference_size),
3412        Datatype::Array { base, dims } => {
3413            let base_size = raw_element_size_for_datatype(base, vlen_reference_size)?;
3414            let count = dims.iter().try_fold(1usize, |acc, &dim| {
3415                let dim = checked_usize(dim, "array datatype dimension")?;
3416                checked_mul_usize(acc, dim, "array datatype element count")
3417            })?;
3418            checked_mul_usize(base_size, count, "array datatype byte size")
3419        }
3420        Datatype::Enum { base, .. } => raw_element_size_for_datatype(base, vlen_reference_size),
3421        Datatype::FixedPoint { size, .. }
3422        | Datatype::FloatingPoint { size, .. }
3423        | Datatype::Bitfield { size, .. }
3424        | Datatype::Reference { size, .. } => Ok(*size as usize),
3425        Datatype::String {
3426            size: StringSize::Fixed(len),
3427            ..
3428        } => Ok(*len as usize),
3429        Datatype::Compound { size, .. } | Datatype::Opaque { size, .. } => Ok(*size as usize),
3430    }
3431}
3432
3433#[cfg(test)]
3434/// Copy a chunk's data into the flat output buffer at the correct position.
3435fn copy_chunk_to_flat(
3436    chunk_data: &[u8],
3437    flat: &mut [u8],
3438    chunk_offsets: &[u64],
3439    chunk_shape: &[u64],
3440    dataset_shape: &[u64],
3441    elem_size: usize,
3442) -> Result<()> {
3443    let dataset_strides = row_major_strides(dataset_shape, "dataset stride")
3444        .expect("dataset strides should fit in usize");
3445    let chunk_strides =
3446        row_major_strides(chunk_shape, "chunk stride").expect("chunk strides should fit in usize");
3447    copy_chunk_to_flat_with_strides(
3448        chunk_data,
3449        flat,
3450        ChunkCopyLayout {
3451            chunk_offsets,
3452            chunk_shape,
3453            dataset_shape,
3454            dataset_strides: &dataset_strides,
3455            chunk_strides: &chunk_strides,
3456            elem_size,
3457        },
3458    )
3459}
3460
3461fn copy_chunk_to_flat_with_strides(
3462    chunk_data: &[u8],
3463    flat: &mut [u8],
3464    layout: ChunkCopyLayout<'_>,
3465) -> Result<()> {
3466    // SAFETY: the mutable slice supplies a live, exclusively borrowed output
3467    // allocation, and the pointer helper checks every source and destination
3468    // range.
3469    unsafe {
3470        copy_chunk_to_flat_with_strides_ptr(
3471            chunk_data,
3472            FlatBufferPtr {
3473                ptr: flat.as_mut_ptr(),
3474                len: flat.len(),
3475            },
3476            layout,
3477        )
3478    }
3479}
3480
3481#[inline(always)]
3482unsafe fn copy_chunk_to_flat_with_strides_ptr(
3483    chunk_data: &[u8],
3484    flat: FlatBufferPtr,
3485    layout: ChunkCopyLayout<'_>,
3486) -> Result<()> {
3487    let ndim = layout.dataset_shape.len();
3488    if layout.chunk_offsets.len() != ndim
3489        || layout.chunk_shape.len() != ndim
3490        || layout.dataset_strides.len() != ndim
3491        || layout.chunk_strides.len() != ndim
3492    {
3493        return Err(Error::InvalidData(format!(
3494            "chunk copy layout rank does not match dataset rank {ndim}"
3495        )));
3496    }
3497
3498    if ndim == 0 {
3499        if chunk_data.len() < layout.elem_size || flat.len < layout.elem_size {
3500            return Err(Error::InvalidData(format!(
3501                "scalar chunk copy requires {} bytes, got source {} and destination {}",
3502                layout.elem_size,
3503                chunk_data.len(),
3504                flat.len
3505            )));
3506        }
3507        // SAFETY: both ranges were checked immediately above, and the source
3508        // slice and caller-owned output allocation are distinct.
3509        unsafe {
3510            std::ptr::copy_nonoverlapping(chunk_data.as_ptr(), flat.ptr, layout.elem_size);
3511        }
3512        return Ok(());
3513    }
3514
3515    // Total elements in this chunk (clamped to dataset boundaries)
3516    let mut actual_chunk_shape = Vec::with_capacity(ndim);
3517    for i in 0..ndim {
3518        if layout.chunk_offsets[i] >= layout.dataset_shape[i] {
3519            return Err(Error::InvalidData(format!(
3520                "chunk offset {} is outside dimension {} of size {}",
3521                layout.chunk_offsets[i], i, layout.dataset_shape[i]
3522            )));
3523        }
3524        let remaining = layout.dataset_shape[i] - layout.chunk_offsets[i];
3525        actual_chunk_shape.push(checked_usize(
3526            remaining.min(layout.chunk_shape[i]),
3527            "actual chunk extent",
3528        )?);
3529    }
3530
3531    let row_elems = *actual_chunk_shape.last().unwrap_or(&1);
3532    let row_bytes = checked_mul_usize(row_elems, layout.elem_size, "chunk row bytes")?;
3533    let mut dataset_origin = 0usize;
3534    for (d, offset) in layout.chunk_offsets.iter().enumerate() {
3535        let offset = checked_usize(*offset, "chunk offset")?;
3536        let term = checked_mul_usize(offset, layout.dataset_strides[d], "chunk origin")?;
3537        dataset_origin = checked_add_usize(dataset_origin, term, "chunk origin")?;
3538    }
3539
3540    if ndim == 1 {
3541        let dst_start = checked_mul_usize(dataset_origin, layout.elem_size, "chunk dst offset")?;
3542        let dst_end = checked_add_usize(dst_start, row_bytes, "chunk dst end")?;
3543        if row_bytes > chunk_data.len() || dst_end > flat.len {
3544            return Err(Error::InvalidData(format!(
3545                "chunk copy out of bounds: source row needs {} bytes from {} bytes, destination range {}..{} exceeds {} bytes",
3546                row_bytes,
3547                chunk_data.len(),
3548                dst_start,
3549                dst_end,
3550                flat.len
3551            )));
3552        }
3553        // SAFETY: both ranges were checked immediately above, and the source
3554        // slice and caller-owned output allocation are distinct.
3555        unsafe {
3556            std::ptr::copy_nonoverlapping(chunk_data.as_ptr(), flat.ptr.add(dst_start), row_bytes);
3557        }
3558        return Ok(());
3559    }
3560
3561    let outer_dims = &actual_chunk_shape[..ndim - 1];
3562    let total_rows = checked_product_usize(outer_dims, "chunk row count")?;
3563    let mut outer_idx = vec![0usize; ndim - 1];
3564
3565    for _ in 0..total_rows {
3566        let mut chunk_row = 0usize;
3567        let mut dataset_row = dataset_origin;
3568        for (d, outer) in outer_idx.iter().copied().enumerate() {
3569            let chunk_term = checked_mul_usize(outer, layout.chunk_strides[d], "chunk row")?;
3570            let dataset_term = checked_mul_usize(outer, layout.dataset_strides[d], "dataset row")?;
3571            chunk_row = checked_add_usize(chunk_row, chunk_term, "chunk row")?;
3572            dataset_row = checked_add_usize(dataset_row, dataset_term, "dataset row")?;
3573        }
3574
3575        let src_start = checked_mul_usize(chunk_row, layout.elem_size, "chunk src offset")?;
3576        let dst_start = checked_mul_usize(dataset_row, layout.elem_size, "chunk dst offset")?;
3577        let src_end = checked_add_usize(src_start, row_bytes, "chunk src end")?;
3578        let dst_end = checked_add_usize(dst_start, row_bytes, "chunk dst end")?;
3579        if src_end > chunk_data.len() || dst_end > flat.len {
3580            return Err(Error::InvalidData(format!(
3581                "chunk copy out of bounds: source range {}..{} of {} bytes, destination range {}..{} of {} bytes",
3582                src_start,
3583                src_end,
3584                chunk_data.len(),
3585                dst_start,
3586                dst_end,
3587                flat.len
3588            )));
3589        }
3590        // SAFETY: both ranges were checked immediately above, and the source
3591        // slice and caller-owned output allocation are distinct.
3592        unsafe {
3593            std::ptr::copy_nonoverlapping(
3594                chunk_data.as_ptr().add(src_start),
3595                flat.ptr.add(dst_start),
3596                row_bytes,
3597            );
3598        }
3599
3600        let mut carry = true;
3601        for d in (0..outer_idx.len()).rev() {
3602            if carry {
3603                outer_idx[d] += 1;
3604                if outer_idx[d] < outer_dims[d] {
3605                    carry = false;
3606                } else {
3607                    outer_idx[d] = 0;
3608                }
3609            }
3610        }
3611    }
3612
3613    Ok(())
3614}
3615
3616fn checked_product_usize(values: &[usize], context: &str) -> Result<usize> {
3617    let mut product = 1usize;
3618    for &value in values {
3619        product = checked_mul_usize(product, value, context)?;
3620    }
3621    Ok(product)
3622}
3623
3624fn unit_stride_chunk_overlap_plan(
3625    chunk_offsets: &[u64],
3626    chunk_shape: &[u64],
3627    dataset_shape: &[u64],
3628    resolved: &ResolvedSelection,
3629) -> Result<(Vec<usize>, Vec<usize>, Vec<usize>)> {
3630    let ndim = dataset_shape.len();
3631    let mut overlap_counts = Vec::with_capacity(ndim);
3632    let mut chunk_local_start = Vec::with_capacity(ndim);
3633    let mut result_start = Vec::with_capacity(ndim);
3634
3635    for d in 0..ndim {
3636        let chunk_start = chunk_offsets[d];
3637        let chunk_end = (chunk_start + chunk_shape[d]).min(dataset_shape[d]);
3638        let dim = &resolved.dims[d];
3639        let overlap_start = chunk_start.max(dim.start);
3640        let overlap_end = chunk_end.min(dim.end);
3641        if overlap_start >= overlap_end {
3642            return Ok((Vec::new(), Vec::new(), Vec::new()));
3643        }
3644
3645        overlap_counts.push(checked_usize(
3646            overlap_end - overlap_start,
3647            "chunk overlap size",
3648        )?);
3649        chunk_local_start.push(checked_usize(
3650            overlap_start - chunk_start,
3651            "chunk overlap start",
3652        )?);
3653        result_start.push(checked_usize(
3654            overlap_start - dim.start,
3655            "slice result overlap start",
3656        )?);
3657    }
3658
3659    Ok((overlap_counts, chunk_local_start, result_start))
3660}
3661
3662#[inline(always)]
3663fn copy_unit_stride_chunk_overlap(
3664    chunk_data: &[u8],
3665    result_buf: &mut [u8],
3666    layout: UnitStrideCopyLayout<'_>,
3667) -> Result<()> {
3668    // SAFETY: the mutable slice supplies a live, exclusively borrowed output
3669    // allocation, and the pointer helper checks every source and destination
3670    // range.
3671    unsafe {
3672        copy_unit_stride_chunk_overlap_ptr(
3673            chunk_data,
3674            FlatBufferPtr {
3675                ptr: result_buf.as_mut_ptr(),
3676                len: result_buf.len(),
3677            },
3678            layout,
3679        )
3680    }
3681}
3682
3683/// Copy a unit-step rectangular overlap from a chunk into the result buffer.
3684///
3685/// This is the hot path for contiguous hyperslab reads over chunked datasets:
3686/// rather than copying one element at a time, it copies contiguous runs along
3687/// the innermost dimension with a single memcpy per output row.
3688///
3689/// # Safety
3690///
3691/// The caller must guarantee that `[result_ptr .. result_ptr + result_len)` is
3692/// valid for writes. Concurrent callers must write to disjoint byte ranges.
3693#[inline(always)]
3694unsafe fn copy_unit_stride_chunk_overlap_ptr(
3695    chunk_data: &[u8],
3696    result: FlatBufferPtr,
3697    layout: UnitStrideCopyLayout<'_>,
3698) -> Result<()> {
3699    let ndim = layout.dataset_shape.len();
3700    if layout.chunk_offsets.len() != ndim
3701        || layout.chunk_shape.len() != ndim
3702        || layout.resolved.dims.len() != ndim
3703        || layout.chunk_strides.len() != ndim
3704        || layout.result_strides.len() != ndim
3705    {
3706        return Err(Error::InvalidData(format!(
3707            "unit-stride copy layout rank does not match dataset rank {ndim}"
3708        )));
3709    }
3710
3711    if ndim == 0 {
3712        if chunk_data.len() < layout.elem_size || result.len < layout.elem_size {
3713            return Err(Error::InvalidData(format!(
3714                "scalar slice copy requires {} bytes, got source {} and destination {}",
3715                layout.elem_size,
3716                chunk_data.len(),
3717                result.len
3718            )));
3719        }
3720        // SAFETY: both ranges were checked immediately above, and the source
3721        // slice and caller-owned output allocation are distinct.
3722        unsafe {
3723            std::ptr::copy_nonoverlapping(chunk_data.as_ptr(), result.ptr, layout.elem_size);
3724        }
3725        return Ok(());
3726    }
3727
3728    let (overlap_counts, chunk_local_start, result_start) = unit_stride_chunk_overlap_plan(
3729        layout.chunk_offsets,
3730        layout.chunk_shape,
3731        layout.dataset_shape,
3732        layout.resolved,
3733    )?;
3734    if overlap_counts.is_empty() {
3735        return Ok(());
3736    }
3737
3738    let row_elems = *overlap_counts.last().unwrap_or(&1);
3739    let row_bytes = checked_mul_usize(row_elems, layout.elem_size, "unit-stride slice row bytes")?;
3740
3741    let mut chunk_origin = 0usize;
3742    let mut result_origin = 0usize;
3743    for d in 0..ndim {
3744        let chunk_term = checked_mul_usize(
3745            chunk_local_start[d],
3746            layout.chunk_strides[d],
3747            "chunk overlap origin",
3748        )?;
3749        let result_term = checked_mul_usize(
3750            result_start[d],
3751            layout.result_strides[d],
3752            "slice result origin",
3753        )?;
3754        chunk_origin = checked_add_usize(chunk_origin, chunk_term, "chunk overlap origin")?;
3755        result_origin = checked_add_usize(result_origin, result_term, "slice result origin")?;
3756    }
3757
3758    if ndim == 1 {
3759        let src_start = checked_mul_usize(chunk_origin, layout.elem_size, "slice src offset")?;
3760        let dst_start = checked_mul_usize(result_origin, layout.elem_size, "slice dst offset")?;
3761        let src_end = checked_add_usize(src_start, row_bytes, "slice src end")?;
3762        let dst_end = checked_add_usize(dst_start, row_bytes, "slice dst end")?;
3763        if src_end > chunk_data.len() || dst_end > result.len {
3764            return Err(Error::InvalidData(format!(
3765                "unit-stride slice copy out of bounds: source range {}..{} of {} bytes, destination range {}..{} of {} bytes",
3766                src_start,
3767                src_end,
3768                chunk_data.len(),
3769                dst_start,
3770                dst_end,
3771                result.len
3772            )));
3773        }
3774        // SAFETY: both ranges were checked immediately above, and the source
3775        // slice and caller-owned output allocation are distinct.
3776        unsafe {
3777            std::ptr::copy_nonoverlapping(
3778                chunk_data.as_ptr().add(src_start),
3779                result.ptr.add(dst_start),
3780                row_bytes,
3781            );
3782        }
3783        return Ok(());
3784    }
3785
3786    let outer_counts = &overlap_counts[..ndim - 1];
3787    let total_rows = checked_product_usize(outer_counts, "unit-stride slice row count")?;
3788    let mut outer_idx = vec![0usize; ndim - 1];
3789
3790    for _ in 0..total_rows {
3791        let mut chunk_row = chunk_origin;
3792        let mut result_row = result_origin;
3793        for (d, outer) in outer_idx.iter().copied().enumerate() {
3794            let chunk_term = checked_mul_usize(outer, layout.chunk_strides[d], "slice chunk row")?;
3795            let result_term =
3796                checked_mul_usize(outer, layout.result_strides[d], "slice result row")?;
3797            chunk_row = checked_add_usize(chunk_row, chunk_term, "slice chunk row")?;
3798            result_row = checked_add_usize(result_row, result_term, "slice result row")?;
3799        }
3800
3801        let src_start = checked_mul_usize(chunk_row, layout.elem_size, "slice src offset")?;
3802        let dst_start = checked_mul_usize(result_row, layout.elem_size, "slice dst offset")?;
3803        let src_end = checked_add_usize(src_start, row_bytes, "slice src end")?;
3804        let dst_end = checked_add_usize(dst_start, row_bytes, "slice dst end")?;
3805        if src_end > chunk_data.len() || dst_end > result.len {
3806            return Err(Error::InvalidData(format!(
3807                "unit-stride slice copy out of bounds: source range {}..{} of {} bytes, destination range {}..{} of {} bytes",
3808                src_start,
3809                src_end,
3810                chunk_data.len(),
3811                dst_start,
3812                dst_end,
3813                result.len
3814            )));
3815        }
3816        // SAFETY: both ranges were checked immediately above, and the source
3817        // slice and caller-owned output allocation are distinct.
3818        unsafe {
3819            std::ptr::copy_nonoverlapping(
3820                chunk_data.as_ptr().add(src_start),
3821                result.ptr.add(dst_start),
3822                row_bytes,
3823            );
3824        }
3825
3826        let mut carry = true;
3827        for d in (0..outer_idx.len()).rev() {
3828            if carry {
3829                outer_idx[d] += 1;
3830                if outer_idx[d] < outer_counts[d] {
3831                    carry = false;
3832                } else {
3833                    outer_idx[d] = 0;
3834                }
3835            }
3836        }
3837    }
3838
3839    Ok(())
3840}
3841
3842#[allow(clippy::too_many_arguments)]
3843/// Copy selected elements from a chunk into the result buffer.
3844///
3845/// `dim_indices[d]` is a list of `(chunk_local_idx, result_dim_idx)` pairs for dimension `d`.
3846#[inline(always)]
3847fn copy_selected_elements(
3848    chunk_data: &[u8],
3849    result_buf: &mut [u8],
3850    dim_indices: &[Vec<(usize, usize)>],
3851    chunk_strides: &[usize],
3852    result_strides: &[usize],
3853    elem_size: usize,
3854    ndim: usize,
3855) -> Result<()> {
3856    if dim_indices.len() != ndim || chunk_strides.len() != ndim || result_strides.len() != ndim {
3857        return Err(Error::InvalidData(format!(
3858            "selected-element copy layout rank does not match rank {ndim}"
3859        )));
3860    }
3861
3862    // Check for empty selection
3863    if dim_indices.iter().any(|v| v.is_empty()) {
3864        return Ok(());
3865    }
3866
3867    // Recursive cartesian-product iteration, but unrolled iteratively.
3868    let counts: Vec<usize> = dim_indices.iter().map(|v| v.len()).collect();
3869    let total = checked_product_usize(&counts, "selected-element copy count")?;
3870    let mut counters = vec![0usize; ndim];
3871
3872    for _ in 0..total {
3873        let mut chunk_flat = 0;
3874        let mut result_flat = 0;
3875        for d in 0..ndim {
3876            let (cl, ri) = dim_indices[d][counters[d]];
3877            let chunk_term = checked_mul_usize(cl, chunk_strides[d], "selected chunk offset")?;
3878            let result_term = checked_mul_usize(ri, result_strides[d], "selected result offset")?;
3879            chunk_flat = checked_add_usize(chunk_flat, chunk_term, "selected chunk offset")?;
3880            result_flat = checked_add_usize(result_flat, result_term, "selected result offset")?;
3881        }
3882
3883        let src_start = checked_mul_usize(chunk_flat, elem_size, "selected source byte offset")?;
3884        let dst_start =
3885            checked_mul_usize(result_flat, elem_size, "selected destination byte offset")?;
3886        let src_end = checked_add_usize(src_start, elem_size, "selected source byte end")?;
3887        let dst_end = checked_add_usize(dst_start, elem_size, "selected destination byte end")?;
3888
3889        if src_end > chunk_data.len() || dst_end > result_buf.len() {
3890            return Err(Error::InvalidData(format!(
3891                "selected-element copy out of bounds: source range {}..{} of {} bytes, destination range {}..{} of {} bytes",
3892                src_start,
3893                src_end,
3894                chunk_data.len(),
3895                dst_start,
3896                dst_end,
3897                result_buf.len()
3898            )));
3899        }
3900        result_buf[dst_start..dst_end].copy_from_slice(&chunk_data[src_start..src_end]);
3901
3902        // Increment counters (row-major)
3903        let mut carry = true;
3904        for d in (0..ndim).rev() {
3905            if carry {
3906                counters[d] += 1;
3907                if counters[d] < dim_indices[d].len() {
3908                    carry = false;
3909                } else {
3910                    counters[d] = 0;
3911                }
3912            }
3913        }
3914    }
3915
3916    Ok(())
3917}
3918
3919/// Copy selected elements from a chunk into a raw output pointer.
3920///
3921/// This is the pointer-based variant of `copy_selected_elements`, suitable for
3922/// parallel use where multiple threads write to disjoint regions of the same buffer.
3923///
3924/// # Safety
3925///
3926/// The caller must guarantee that no two concurrent calls write to the same
3927/// byte range within `[result_ptr .. result_ptr + result_len)`.
3928#[cfg(feature = "rayon")]
3929#[allow(clippy::too_many_arguments)]
3930#[inline(always)]
3931unsafe fn copy_selected_elements_ptr(
3932    chunk_data: &[u8],
3933    result_ptr: *mut u8,
3934    result_len: usize,
3935    dim_indices: &[Vec<(usize, usize)>],
3936    chunk_strides: &[usize],
3937    result_strides: &[usize],
3938    elem_size: usize,
3939    ndim: usize,
3940) -> Result<()> {
3941    if dim_indices.len() != ndim || chunk_strides.len() != ndim || result_strides.len() != ndim {
3942        return Err(Error::InvalidData(format!(
3943            "selected-element copy layout rank does not match rank {ndim}"
3944        )));
3945    }
3946
3947    if dim_indices.iter().any(|v| v.is_empty()) {
3948        return Ok(());
3949    }
3950
3951    let counts: Vec<usize> = dim_indices.iter().map(|v| v.len()).collect();
3952    let total = checked_product_usize(&counts, "selected-element copy count")?;
3953    let mut counters = vec![0usize; ndim];
3954
3955    for _ in 0..total {
3956        let mut chunk_flat = 0;
3957        let mut result_flat = 0;
3958        for d in 0..ndim {
3959            let (cl, ri) = dim_indices[d][counters[d]];
3960            let chunk_term = checked_mul_usize(cl, chunk_strides[d], "selected chunk offset")?;
3961            let result_term = checked_mul_usize(ri, result_strides[d], "selected result offset")?;
3962            chunk_flat = checked_add_usize(chunk_flat, chunk_term, "selected chunk offset")?;
3963            result_flat = checked_add_usize(result_flat, result_term, "selected result offset")?;
3964        }
3965
3966        let src_start = checked_mul_usize(chunk_flat, elem_size, "selected source byte offset")?;
3967        let dst_start =
3968            checked_mul_usize(result_flat, elem_size, "selected destination byte offset")?;
3969        let src_end = checked_add_usize(src_start, elem_size, "selected source byte end")?;
3970        let dst_end = checked_add_usize(dst_start, elem_size, "selected destination byte end")?;
3971
3972        if src_end > chunk_data.len() || dst_end > result_len {
3973            return Err(Error::InvalidData(format!(
3974                "selected-element copy out of bounds: source range {}..{} of {} bytes, destination range {}..{} of {} bytes",
3975                src_start,
3976                src_end,
3977                chunk_data.len(),
3978                dst_start,
3979                dst_end,
3980                result_len
3981            )));
3982        }
3983        // SAFETY: both ranges were checked immediately above, and the source
3984        // slice and caller-owned output allocation are distinct.
3985        unsafe {
3986            std::ptr::copy_nonoverlapping(
3987                chunk_data.as_ptr().add(src_start),
3988                result_ptr.add(dst_start),
3989                elem_size,
3990            );
3991        }
3992
3993        let mut carry = true;
3994        for d in (0..ndim).rev() {
3995            if carry {
3996                counters[d] += 1;
3997                if counters[d] < dim_indices[d].len() {
3998                    carry = false;
3999                } else {
4000                    counters[d] = 0;
4001                }
4002            }
4003        }
4004    }
4005
4006    Ok(())
4007}
4008
4009/// Slice an ndarray according to a SliceInfo selection.
4010fn slice_array<T: H5Type + Clone>(
4011    array: &ArrayD<T>,
4012    selection: &SliceInfo,
4013    shape: &[u64],
4014) -> Result<ArrayD<T>> {
4015    // Build result shape
4016    let mut result_shape = Vec::new();
4017
4018    for (i, sel) in selection.selections.iter().enumerate() {
4019        let dim_size = shape[i];
4020        match sel {
4021            SliceInfoElem::Index(idx) => {
4022                if *idx >= dim_size {
4023                    return Err(Error::SliceOutOfBounds {
4024                        dim: i,
4025                        index: *idx,
4026                        size: dim_size,
4027                    });
4028                }
4029                // Don't add to result_shape — this dimension is collapsed
4030            }
4031            SliceInfoElem::Slice { start, end, step } => {
4032                let dim_size = checked_usize(dim_size, "slice dimension size")?;
4033                let actual_end = if *end == u64::MAX {
4034                    dim_size
4035                } else {
4036                    checked_usize(*end, "slice end")?.min(dim_size)
4037                };
4038                let actual_start = checked_usize(*start, "slice start")?;
4039                let actual_step = checked_usize(*step, "slice step")?;
4040                if actual_step == 0 {
4041                    return Err(Error::InvalidData("slice step cannot be 0".into()));
4042                }
4043                if actual_start > dim_size {
4044                    return Err(Error::SliceOutOfBounds {
4045                        dim: i,
4046                        index: *start,
4047                        size: shape[i],
4048                    });
4049                }
4050                let n = (actual_end - actual_start).div_ceil(actual_step);
4051                result_shape.push(n);
4052            }
4053        }
4054    }
4055
4056    // Extract elements manually (ndarray's slicing API is complex with dynamic dims)
4057    let ndim = shape.len();
4058    let total = checked_product_usize(&result_shape, "slice result element count")?;
4059    let mut elements = Vec::with_capacity(total);
4060
4061    // Generate all indices in the result
4062    let mut result_idx = vec![0usize; result_shape.len()];
4063
4064    for _ in 0..total {
4065        // Map result index to source index
4066        let mut src_idx = Vec::with_capacity(ndim);
4067        let mut ri = 0;
4068        for sel in selection.selections.iter() {
4069            match sel {
4070                SliceInfoElem::Index(idx) => {
4071                    src_idx.push(checked_usize(*idx, "slice source index")?);
4072                }
4073                SliceInfoElem::Slice { start, step, .. } => {
4074                    let start = checked_usize(*start, "slice start")?;
4075                    let step = checked_usize(*step, "slice step")?;
4076                    let offset =
4077                        checked_mul_usize(result_idx[ri], step, "slice source index offset")?;
4078                    src_idx.push(checked_add_usize(start, offset, "slice source index")?);
4079                    ri += 1;
4080                }
4081            }
4082        }
4083
4084        elements.push(array[IxDyn(&src_idx)].clone());
4085
4086        // Increment result index
4087        if !result_shape.is_empty() {
4088            let mut carry = true;
4089            for d in (0..result_shape.len()).rev() {
4090                if carry {
4091                    result_idx[d] += 1;
4092                    if result_idx[d] < result_shape[d] {
4093                        carry = false;
4094                    } else {
4095                        result_idx[d] = 0;
4096                    }
4097                }
4098            }
4099        }
4100    }
4101
4102    ArrayD::from_shape_vec(IxDyn(&result_shape), elements)
4103        .map_err(|e| Error::InvalidData(format!("slice shape error: {e}")))
4104}
4105
4106#[cfg(test)]
4107mod tests {
4108    use super::*;
4109    use crate::storage::BytesStorage;
4110    use crate::superblock::Superblock;
4111    use std::collections::HashMap;
4112
4113    fn test_context_with_offset_size(bytes: Vec<u8>, offset_size: u8) -> Arc<FileContext> {
4114        let storage: DynStorage = Arc::new(BytesStorage::new(bytes));
4115        Arc::new(FileContext {
4116            storage,
4117            superblock: Superblock {
4118                version: 2,
4119                offset_size,
4120                length_size: 8,
4121                group_leaf_node_k: 0,
4122                group_internal_node_k: 0,
4123                indexed_storage_k: 0,
4124                consistency_flags: 0,
4125                base_address: 0,
4126                free_space_address: u64::MAX,
4127                eof_address: 0,
4128                driver_info_address: u64::MAX,
4129                root_symbol_table_entry: None,
4130                root_object_header_address: Some(0),
4131                extension_address: None,
4132            },
4133            chunk_cache: Arc::new(ChunkCache::new(1024, 8)),
4134            header_cache: Arc::new(Mutex::new(HashMap::new())),
4135            dataset_path_cache: Arc::new(Mutex::new(HashMap::new())),
4136            filter_registry: Arc::new(FilterRegistry::default()),
4137            external_file_resolver: None,
4138            external_link_resolver: None,
4139            external_file_cache: Mutex::new(HashMap::new()),
4140            sohm_table: OnceLock::new(),
4141            full_file_cache: OnceLock::new(),
4142        })
4143    }
4144
4145    fn test_context(bytes: Vec<u8>) -> Arc<FileContext> {
4146        test_context_with_offset_size(bytes, 8)
4147    }
4148
4149    fn fixed_u16_dataset(layout: DataLayout, storage_bytes: Vec<u8>) -> Dataset {
4150        let context = test_context(storage_bytes);
4151        Dataset {
4152            context: context.clone(),
4153            name: "short".to_string(),
4154            data_address: 0,
4155            dataspace: DataspaceMessage {
4156                rank: 1,
4157                dims: vec![3],
4158                max_dims: None,
4159                dataspace_type: DataspaceType::Simple,
4160            },
4161            datatype: Datatype::FixedPoint {
4162                size: 2,
4163                signed: false,
4164                byte_order: ByteOrder::LittleEndian,
4165            },
4166            layout,
4167            fill_value: None,
4168            filters: None,
4169            external_files: None,
4170            attributes: Vec::new(),
4171            chunk_cache: context.chunk_cache.clone(),
4172            chunk_entry_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(32).unwrap()))),
4173            full_chunk_entries: Arc::new(OnceLock::new()),
4174            full_dataset_bytes: Arc::new(OnceLock::new()),
4175            external_slots: Arc::new(OnceLock::new()),
4176            filter_registry: context.filter_registry.clone(),
4177        }
4178    }
4179
4180    fn vlen_string_dataset(
4181        layout: DataLayout,
4182        storage_bytes: Vec<u8>,
4183        offset_size: u8,
4184        fill_value: Option<FillValueMessage>,
4185    ) -> Dataset {
4186        let context = test_context_with_offset_size(storage_bytes, offset_size);
4187        Dataset {
4188            context: context.clone(),
4189            name: "vlen".to_string(),
4190            data_address: 0,
4191            dataspace: DataspaceMessage {
4192                rank: 1,
4193                dims: vec![2],
4194                max_dims: None,
4195                dataspace_type: DataspaceType::Simple,
4196            },
4197            datatype: Datatype::String {
4198                size: StringSize::Variable,
4199                encoding: crate::messages::datatype::StringEncoding::Ascii,
4200                padding: crate::messages::datatype::StringPadding::NullTerminate,
4201            },
4202            layout,
4203            fill_value,
4204            filters: None,
4205            external_files: None,
4206            attributes: Vec::new(),
4207            chunk_cache: context.chunk_cache.clone(),
4208            chunk_entry_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(32).unwrap()))),
4209            full_chunk_entries: Arc::new(OnceLock::new()),
4210            full_dataset_bytes: Arc::new(OnceLock::new()),
4211            external_slots: Arc::new(OnceLock::new()),
4212            filter_registry: context.filter_registry.clone(),
4213        }
4214    }
4215
4216    #[derive(Clone, Debug, PartialEq, Eq)]
4217    struct RawElement(Vec<u8>);
4218
4219    impl H5Type for RawElement {
4220        fn hdf5_type() -> Datatype {
4221            Datatype::Opaque {
4222                size: 0,
4223                tag: "raw".to_string(),
4224            }
4225        }
4226
4227        fn from_bytes(bytes: &[u8], _dtype: &Datatype) -> Result<Self> {
4228            Ok(Self(bytes.to_vec()))
4229        }
4230
4231        fn element_size(dtype: &Datatype) -> usize {
4232            crate::datatype_api::dtype_element_size(dtype).unwrap_or(0)
4233        }
4234    }
4235
4236    #[test]
4237    fn slice_info_all() {
4238        let s = SliceInfo::all(3);
4239        assert_eq!(s.selections.len(), 3);
4240    }
4241
4242    #[test]
4243    fn raw_element_size_uses_file_vlen_reference_width() {
4244        let dtype = Datatype::VarLen {
4245            base: Box::new(Datatype::FixedPoint {
4246                size: 1,
4247                signed: false,
4248                byte_order: crate::error::ByteOrder::LittleEndian,
4249            }),
4250            kind: VarLenKind::Sequence,
4251            encoding: crate::messages::datatype::StringEncoding::Ascii,
4252            padding: crate::messages::datatype::StringPadding::NullTerminate,
4253        };
4254
4255        assert_eq!(raw_element_size_for_datatype(&dtype, 12).unwrap(), 12);
4256        assert_eq!(
4257            raw_element_size_for_datatype(
4258                &Datatype::Array {
4259                    base: Box::new(dtype),
4260                    dims: vec![2, 3],
4261                },
4262                12,
4263            )
4264            .unwrap(),
4265            72
4266        );
4267    }
4268
4269    #[test]
4270    fn chunked_vlen_slice_uses_file_reference_width() {
4271        for (offset_size, ref_size) in [(2u8, 10usize), (4u8, 12usize)] {
4272            let chunk_bytes: Vec<u8> = (0..(2 * ref_size)).map(|i| (i + 1) as u8).collect();
4273            let dataset = vlen_string_dataset(
4274                DataLayout::Chunked {
4275                    address: 0,
4276                    dims: vec![2],
4277                    element_size: 16,
4278                    chunk_indexing: Some(ChunkIndexing::Implicit),
4279                },
4280                chunk_bytes.clone(),
4281                offset_size,
4282                None,
4283            );
4284
4285            let selection = SliceInfo {
4286                selections: vec![SliceInfoElem::Slice {
4287                    start: 0,
4288                    end: 2,
4289                    step: 1,
4290                }],
4291            };
4292            let array = dataset.read_slice::<RawElement>(&selection).unwrap();
4293            let values: Vec<_> = array.iter().cloned().collect();
4294
4295            assert_eq!(values[0].0, chunk_bytes[..ref_size]);
4296            assert_eq!(values[1].0, chunk_bytes[ref_size..2 * ref_size]);
4297        }
4298    }
4299
4300    #[cfg(feature = "rayon")]
4301    #[test]
4302    fn parallel_chunked_vlen_slice_uses_file_reference_width() {
4303        for (offset_size, ref_size) in [(2u8, 10usize), (4u8, 12usize)] {
4304            let chunk_bytes: Vec<u8> = (0..(2 * ref_size)).map(|i| (i + 1) as u8).collect();
4305            let dataset = vlen_string_dataset(
4306                DataLayout::Chunked {
4307                    address: 0,
4308                    dims: vec![2],
4309                    element_size: 16,
4310                    chunk_indexing: Some(ChunkIndexing::Implicit),
4311                },
4312                chunk_bytes.clone(),
4313                offset_size,
4314                None,
4315            );
4316
4317            let selection = SliceInfo {
4318                selections: vec![SliceInfoElem::Slice {
4319                    start: 0,
4320                    end: 2,
4321                    step: 1,
4322                }],
4323            };
4324            let array = dataset
4325                .read_slice_parallel::<RawElement>(&selection)
4326                .unwrap();
4327            let values: Vec<_> = array.iter().cloned().collect();
4328
4329            assert_eq!(values[0].0, chunk_bytes[..ref_size]);
4330            assert_eq!(values[1].0, chunk_bytes[ref_size..2 * ref_size]);
4331        }
4332    }
4333
4334    #[test]
4335    fn undefined_chunked_vlen_slice_fill_uses_file_reference_width() {
4336        for (offset_size, ref_size, undefined_address) in
4337            [(2u8, 10usize, 0xFFFF), (4u8, 12usize, 0xFFFF_FFFF)]
4338        {
4339            let fill_bytes = vec![0xA5; ref_size];
4340            let dataset = vlen_string_dataset(
4341                DataLayout::Chunked {
4342                    address: undefined_address,
4343                    dims: vec![2],
4344                    element_size: 16,
4345                    chunk_indexing: Some(ChunkIndexing::Implicit),
4346                },
4347                Vec::new(),
4348                offset_size,
4349                Some(FillValueMessage {
4350                    defined: true,
4351                    fill_time: FillTime::IfSet,
4352                    value: Some(fill_bytes.clone()),
4353                }),
4354            );
4355
4356            let selection = SliceInfo {
4357                selections: vec![SliceInfoElem::Slice {
4358                    start: 0,
4359                    end: 2,
4360                    step: 1,
4361                }],
4362            };
4363            let array = dataset.read_slice::<RawElement>(&selection).unwrap();
4364            let values: Vec<_> = array.iter().cloned().collect();
4365
4366            assert_eq!(values[0].0, fill_bytes);
4367            assert_eq!(values[1].0, fill_bytes);
4368        }
4369    }
4370
4371    #[test]
4372    fn array_raw_element_size_rejects_overflow() {
4373        let dtype = Datatype::Array {
4374            base: Box::new(Datatype::FixedPoint {
4375                size: 8,
4376                signed: false,
4377                byte_order: crate::error::ByteOrder::LittleEndian,
4378            }),
4379            dims: vec![u64::MAX, 2],
4380        };
4381
4382        let err = raw_element_size_for_datatype(&dtype, 12).unwrap_err();
4383        assert!(err.to_string().contains("array datatype"));
4384    }
4385
4386    #[test]
4387    fn dataset_num_elements_rejects_overflow() {
4388        let mut dataset = fixed_u16_dataset(DataLayout::Compact { data: vec![] }, Vec::new());
4389        dataset.dataspace.dims = vec![u64::MAX, 2];
4390
4391        let err = dataset.num_elements().unwrap_err();
4392        assert!(err.to_string().contains("element count"));
4393    }
4394
4395    #[test]
4396    fn compact_raw_data_requires_exact_logical_length() {
4397        let dataset = fixed_u16_dataset(
4398            DataLayout::Compact {
4399                data: vec![1, 0, 2, 0, 3],
4400            },
4401            Vec::new(),
4402        );
4403
4404        let err = dataset.read_array::<u16>().unwrap_err();
4405        assert!(
4406            matches!(err, Error::Context { .. })
4407                && err
4408                    .to_string()
4409                    .contains("compact raw data has 5 bytes, expected 6 bytes"),
4410            "expected compact raw length error, got: {err}"
4411        );
4412    }
4413
4414    #[test]
4415    fn contiguous_raw_data_requires_exact_logical_length() {
4416        let dataset = fixed_u16_dataset(
4417            DataLayout::Contiguous {
4418                address: 0,
4419                size: 5,
4420            },
4421            vec![1, 0, 2, 0, 3],
4422        );
4423
4424        let err = dataset.read_raw_bytes().unwrap_err();
4425        assert!(
4426            matches!(err, Error::Context { .. })
4427                && err
4428                    .to_string()
4429                    .contains("contiguous raw data has 5 bytes, expected 6 bytes"),
4430            "expected contiguous raw length error, got: {err}"
4431        );
4432    }
4433
4434    #[test]
4435    fn copy_chunk_1d() {
4436        let chunk_data = vec![1u8, 2, 3, 4]; // 4 elements of 1 byte each
4437        let mut flat = vec![0u8; 8];
4438        let chunk_offsets = vec![2u64]; // starts at index 2
4439        let chunk_shape = vec![4u64];
4440        let dataset_shape = vec![8u64];
4441
4442        copy_chunk_to_flat(
4443            &chunk_data,
4444            &mut flat,
4445            &chunk_offsets,
4446            &chunk_shape,
4447            &dataset_shape,
4448            1,
4449        )
4450        .unwrap();
4451        assert_eq!(flat, vec![0, 0, 1, 2, 3, 4, 0, 0]);
4452    }
4453
4454    #[test]
4455    fn copy_chunk_2d_rowwise() {
4456        let chunk_data = vec![1u8, 2, 3, 4, 5, 6];
4457        let mut flat = vec![0u8; 16];
4458        let chunk_offsets = vec![1u64, 1u64];
4459        let chunk_shape = vec![2u64, 3u64];
4460        let dataset_shape = vec![4u64, 4u64];
4461
4462        copy_chunk_to_flat(
4463            &chunk_data,
4464            &mut flat,
4465            &chunk_offsets,
4466            &chunk_shape,
4467            &dataset_shape,
4468            1,
4469        )
4470        .unwrap();
4471
4472        assert_eq!(flat, vec![0, 0, 0, 0, 0, 1, 2, 3, 0, 4, 5, 6, 0, 0, 0, 0,]);
4473    }
4474
4475    #[test]
4476    fn copy_unit_stride_chunk_overlap_2d_partial() {
4477        let chunk_data: Vec<u8> = (1..=16).collect();
4478        let mut result = vec![0u8; 6];
4479        let chunk_offsets = vec![0u64, 0u64];
4480        let chunk_shape = vec![4u64, 4u64];
4481        let dataset_shape = vec![4u64, 4u64];
4482        let resolved = ResolvedSelection {
4483            dims: vec![
4484                ResolvedSelectionDim {
4485                    start: 1,
4486                    end: 3,
4487                    step: 1,
4488                    count: 2,
4489                },
4490                ResolvedSelectionDim {
4491                    start: 1,
4492                    end: 4,
4493                    step: 1,
4494                    count: 3,
4495                },
4496            ],
4497            result_shape: vec![2, 3],
4498            result_elements: 6,
4499        };
4500        let chunk_strides = vec![4usize, 1usize];
4501        let result_strides = vec![3usize, 1usize];
4502
4503        copy_unit_stride_chunk_overlap(
4504            &chunk_data,
4505            &mut result,
4506            UnitStrideCopyLayout {
4507                chunk_offsets: &chunk_offsets,
4508                chunk_shape: &chunk_shape,
4509                dataset_shape: &dataset_shape,
4510                resolved: &resolved,
4511                chunk_strides: &chunk_strides,
4512                result_strides: &result_strides,
4513                elem_size: 1,
4514            },
4515        )
4516        .unwrap();
4517
4518        assert_eq!(result, vec![6, 7, 8, 10, 11, 12]);
4519    }
4520
4521    fn chunk_entry(offsets: &[u64], address: u64) -> chunk_index::ChunkEntry {
4522        chunk_index::ChunkEntry {
4523            address,
4524            size: 0,
4525            filter_mask: 0,
4526            offsets: offsets.to_vec(),
4527        }
4528    }
4529
4530    #[test]
4531    fn chunk_grid_coverage_detects_missing_chunk() {
4532        let mut entries = vec![
4533            chunk_entry(&[0, 0], 0x1000),
4534            chunk_entry(&[0, 2], 0x2000),
4535            chunk_entry(&[2, 0], 0x3000),
4536        ];
4537
4538        let complete =
4539            validate_chunk_grid_coverage(&mut entries, &[4, 4], &[2, 2], &[0, 0], &[1, 1]).unwrap();
4540
4541        assert!(!complete);
4542    }
4543
4544    #[test]
4545    fn chunk_grid_coverage_rejects_duplicate_offsets() {
4546        let mut entries = vec![
4547            chunk_entry(&[0, 0], 0x1000),
4548            chunk_entry(&[0, 0], 0x2000),
4549            chunk_entry(&[0, 2], 0x3000),
4550            chunk_entry(&[2, 0], 0x4000),
4551        ];
4552
4553        let err = validate_chunk_grid_coverage(&mut entries, &[4, 4], &[2, 2], &[0, 0], &[1, 1])
4554            .unwrap_err();
4555
4556        assert!(matches!(err, Error::InvalidData(_)));
4557    }
4558
4559    #[test]
4560    fn decoded_chunk_len_requires_exact_size() {
4561        let entry = chunk_entry(&[0, 0], 0x1000);
4562
4563        validate_decoded_chunk_len(&entry, &[2, 3], 4, 24).unwrap();
4564        let err = validate_decoded_chunk_len(&entry, &[2, 3], 4, 23).unwrap_err();
4565
4566        assert!(matches!(err, Error::InvalidData(_)));
4567    }
4568
4569    #[test]
4570    fn copy_chunk_errors_on_short_row() {
4571        let chunk_data = vec![1u8, 2, 3, 4, 5];
4572        let mut flat = vec![0u8; 16];
4573        let chunk_offsets = vec![1u64, 1u64];
4574        let chunk_shape = vec![2u64, 3u64];
4575        let dataset_shape = vec![4u64, 4u64];
4576
4577        let err = copy_chunk_to_flat(
4578            &chunk_data,
4579            &mut flat,
4580            &chunk_offsets,
4581            &chunk_shape,
4582            &dataset_shape,
4583            1,
4584        )
4585        .unwrap_err();
4586
4587        assert!(matches!(err, Error::InvalidData(_)));
4588    }
4589
4590    #[test]
4591    fn copy_unit_stride_chunk_overlap_errors_on_short_row() {
4592        let chunk_data: Vec<u8> = (1..=7).collect();
4593        let mut result = vec![0u8; 6];
4594        let chunk_offsets = vec![0u64, 0u64];
4595        let chunk_shape = vec![4u64, 4u64];
4596        let dataset_shape = vec![4u64, 4u64];
4597        let resolved = ResolvedSelection {
4598            dims: vec![
4599                ResolvedSelectionDim {
4600                    start: 1,
4601                    end: 3,
4602                    step: 1,
4603                    count: 2,
4604                },
4605                ResolvedSelectionDim {
4606                    start: 1,
4607                    end: 4,
4608                    step: 1,
4609                    count: 3,
4610                },
4611            ],
4612            result_shape: vec![2, 3],
4613            result_elements: 6,
4614        };
4615        let chunk_strides = vec![4usize, 1usize];
4616        let result_strides = vec![3usize, 1usize];
4617
4618        let err = copy_unit_stride_chunk_overlap(
4619            &chunk_data,
4620            &mut result,
4621            UnitStrideCopyLayout {
4622                chunk_offsets: &chunk_offsets,
4623                chunk_shape: &chunk_shape,
4624                dataset_shape: &dataset_shape,
4625                resolved: &resolved,
4626                chunk_strides: &chunk_strides,
4627                result_strides: &result_strides,
4628                elem_size: 1,
4629            },
4630        )
4631        .unwrap_err();
4632
4633        assert!(matches!(err, Error::InvalidData(_)));
4634    }
4635
4636    // ------------------------------------------------------------------
4637    // validate_chunk_grid_coverage: the gate for the uninitialized-output
4638    // fast path. It must return Ok(true) only for an exact, gap-free,
4639    // duplicate-free tiling; otherwise the caller falls back to a
4640    // zero-initialized buffer, so returning false (not true) on a missing
4641    // chunk is what keeps the assume_init path sound.
4642    // ------------------------------------------------------------------
4643
4644    #[test]
4645    fn grid_coverage_true_for_exact_tiling() {
4646        // 4x6 dataset, 2x3 chunks -> a full 2x2 chunk grid.
4647        let shape = [4u64, 6];
4648        let chunk_shape = [2u64, 3];
4649        let mut entries = vec![
4650            chunk_entry(&[0, 0], 0x100),
4651            chunk_entry(&[0, 3], 0x200),
4652            chunk_entry(&[2, 0], 0x300),
4653            chunk_entry(&[2, 3], 0x400),
4654        ];
4655        let covered =
4656            validate_chunk_grid_coverage(&mut entries, &shape, &chunk_shape, &[0, 0], &[1, 1])
4657                .unwrap();
4658        assert!(covered);
4659    }
4660
4661    #[test]
4662    fn grid_coverage_false_when_a_chunk_is_missing() {
4663        // Drop the last chunk: coverage is incomplete, so the fast path must
4664        // be declined (Ok(false)), never Ok(true).
4665        let shape = [4u64, 6];
4666        let chunk_shape = [2u64, 3];
4667        let mut entries = vec![
4668            chunk_entry(&[0, 0], 0x100),
4669            chunk_entry(&[0, 3], 0x200),
4670            chunk_entry(&[2, 0], 0x300),
4671        ];
4672        let covered =
4673            validate_chunk_grid_coverage(&mut entries, &shape, &chunk_shape, &[0, 0], &[1, 1])
4674                .unwrap();
4675        assert!(!covered);
4676    }
4677
4678    #[test]
4679    fn grid_coverage_rejects_duplicate_chunks() {
4680        let shape = [4u64, 6];
4681        let chunk_shape = [2u64, 3];
4682        let mut entries = vec![
4683            chunk_entry(&[0, 0], 0x100),
4684            chunk_entry(&[0, 0], 0x999),
4685            chunk_entry(&[0, 3], 0x200),
4686            chunk_entry(&[2, 0], 0x300),
4687            chunk_entry(&[2, 3], 0x400),
4688        ];
4689        let err =
4690            validate_chunk_grid_coverage(&mut entries, &shape, &chunk_shape, &[0, 0], &[1, 1])
4691                .unwrap_err();
4692        assert!(matches!(err, Error::InvalidData(msg) if msg.contains("duplicate chunk")));
4693    }
4694
4695    #[test]
4696    fn grid_coverage_rejects_out_of_grid_offsets() {
4697        // An offset not aligned to the chunk grid must be rejected outright.
4698        let shape = [4u64, 6];
4699        let chunk_shape = [2u64, 3];
4700        let mut entries = vec![
4701            chunk_entry(&[0, 0], 0x100),
4702            chunk_entry(&[1, 3], 0x200), // row offset 1 is not a multiple of 2
4703            chunk_entry(&[2, 0], 0x300),
4704            chunk_entry(&[2, 3], 0x400),
4705        ];
4706        let err =
4707            validate_chunk_grid_coverage(&mut entries, &shape, &chunk_shape, &[0, 0], &[1, 1])
4708                .unwrap_err();
4709        assert!(matches!(err, Error::InvalidData(_)));
4710    }
4711}