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