format: raw data, VDS and VL data over Storage

Every raw-data path has a generic *_in core, with the &[u8] functions as
thin wrappers: data_read (read_raw_data*, read_raw_data_selection,
read_chunked_native), chunked_read (the v1 B-tree chunk index, list_chunks,
the full, cached, sweep and indexed reads), parallel_read, partial_read,
fill_value (read_full_with_fill, apply_to_unallocated_chunks; and
dataset_fill_value_from_storage is now generic), vds (the virtual file
through Storage, external sources still through the resolver),
vl_data (VlResolver<'a, S = [u8]>, read_vl_strings_in, read_vl_bytes_in),
AttributeMessage::read_vl_strings_in and provenance::verify_dataset_in.

With the whole file in memory nothing changes: chunks and contiguous data
are sliced from it as before. Otherwise a chunked read lists its chunks,
fetches their stored bytes with one Storage::read_ranges call per 64 MiB
batch (chunks the cache already holds are not fetched), then decodes as
today; a selection fetches only the chunks it overlaps, and a contiguous
selection only its runs. Each extent's bounds error is the one the slice
code gave, reported when that extent is reached, so errors keep their
order.

Tests: the equivalence harness now reads every dataset's values (whole,
fill-aware, cached, indexed, three selections, VDS, VL strings and
sequences) through the read_at-only storage and requires the slice
results (all 653 corpus files agree); a misbehaving storage (a failing
Nth read, short reads) only ever yields errors or the right values; and
chunked reads are checked to use one read_ranges call.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 16:28:01 -05:00
co-authored by Claude Opus 5.5
parent 42894bf93b
commit 3fa5ed1dda
13 changed files with 1803 additions and 279 deletions
+357 -80
View File
@@ -6,19 +6,20 @@ extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, vec, vec::Vec};
use crate::addr::to_usize;
use crate::addr::{checked_addr, to_usize};
#[cfg(feature = "std")]
use crate::chunk_cache::{CacheAlignedBuffer, ChunkCache};
use crate::data_layout::DataLayout;
use crate::dataspace::Dataspace;
use crate::datatype::Datatype;
use crate::error::FormatError;
use crate::extensible_array::{ExtensibleArrayHeader, read_extensible_array_chunks};
use crate::extensible_array::{ExtensibleArrayHeader, read_extensible_array_chunks_in};
use crate::filter_pipeline::FilterPipeline;
use crate::filters::{DecodeScratch, decompress_chunk_exact_with};
#[cfg(feature = "std")]
use crate::filters::{all_filters_skipped, decompress_chunk_exact};
use crate::fixed_array::{FixedArrayHeader, read_fixed_array_chunks};
use crate::fixed_array::{FixedArrayHeader, read_fixed_array_chunks_in};
use crate::storage::{ExtentBytes, Storage, Window, raw_batches, read_exact_at};
#[cfg(feature = "std")]
use std::sync::Arc;
@@ -238,22 +239,101 @@ type CacheUse<'a> = Option<&'a core::convert::Infallible>;
/// ([`parallel_read::run_with_helpers`]), so the caller never waits on a
/// busy pool. The error returned is the first failing chunk's, in `chunks`
/// order.
///
/// With the whole file in memory the chunks are sliced from it. Otherwise
/// their stored bytes are fetched first, with one
/// [`Storage::read_ranges`] call per batch of up to
/// [`crate::storage::RAW_BATCH_BYTES`] (all of them, for most datasets),
/// and then decoded as above; chunks the cache already holds are not
/// fetched.
#[allow(clippy::too_many_arguments)]
fn fill_from_chunks(
file_data: &[u8],
fn fill_from_chunks<S: Storage + ?Sized>(
file_data: &S,
chunks: &[ChunkInfo],
pipeline: Option<&FilterPipeline>,
placer: &ChunkPlacer,
chunk_total_bytes: usize,
cache: CacheUse<'_>,
output: &mut [u8],
) -> Result<(), FormatError> {
let contiguous = file_data.as_contiguous().is_some();
let out = OutBuf::new(output);
let batches = raw_batches(chunks.len(), contiguous, |i| chunks[i].chunk_size as usize);
for batch in batches {
fill_batch(
file_data,
&chunks[batch],
pipeline,
placer,
chunk_total_bytes,
cache,
&out,
)?;
}
Ok(())
}
/// Whether a full read looks chunk `c` up in the cache (see
/// [`fill_from_chunks`]): a filtered chunk of a dataset the cache keeps.
#[cfg(feature = "std")]
fn uses_cache(cache: CacheUse<'_>, pipeline: Option<&FilterPipeline>, c: &ChunkInfo) -> bool {
matches!(cache, Some((_, _, true)))
&& pipeline.is_some_and(|pl| !all_filters_skipped(pl, c.filter_mask))
}
/// [`fill_from_chunks`] for one batch of chunks.
#[allow(clippy::too_many_arguments)]
fn fill_batch<S: Storage + ?Sized>(
file_data: &S,
chunks: &[ChunkInfo],
pipeline: Option<&FilterPipeline>,
placer: &ChunkPlacer,
chunk_total_bytes: usize,
cache: CacheUse<'_>,
out: &OutBuf<'_>,
) -> Result<(), FormatError> {
let rank = placer.rank;
let elem_size = placer.elem_size as u32;
let out = OutBuf::new(output);
#[cfg(not(feature = "std"))]
let _ = cache;
// Over a backend without the whole file in memory, the decoded chunks
// the cache holds are taken now (so a chunk evicted before it is placed
// is not left without bytes), and only the others are fetched.
#[cfg(feature = "std")]
let hits: Vec<Option<Arc<CacheAlignedBuffer>>> = match cache {
Some((cache, key, true)) if file_data.as_contiguous().is_none() => chunks
.iter()
.map(|c| {
if c.offsets.len() >= rank && uses_cache(Some((cache, key, true)), pipeline, c) {
cache.get_decompressed_in(key, &c.offsets[..rank])
} else {
None
}
})
.collect(),
_ => Vec::new(),
};
let extents: Vec<(u64, usize, bool)> = if file_data.as_contiguous().is_some() {
Vec::new()
} else {
chunks
.iter()
.enumerate()
.map(|(i, c)| {
#[cfg(feature = "std")]
let wanted = hits.get(i).is_none_or(Option::is_none);
#[cfg(not(feature = "std"))]
let wanted = {
let _ = i;
true
};
(c.address, c.chunk_size as usize, wanted)
})
.collect()
};
let raw_bytes = ExtentBytes::fetch(file_data, &extents)?;
// Decode chunk `i` and place it. Its callers below run it either on one
// thread, or on several for chunks whose regions are pairwise disjoint,
// each chunk once: no two threads ever write the same bytes.
@@ -266,13 +346,18 @@ fn fill_from_chunks(
)));
}
let offsets = &c.offsets[..rank];
let c_addr = to_usize(c.address)?;
let size = c.chunk_size as usize;
ensure_len(file_data, c_addr, size)?;
let raw = &file_data[c_addr..c_addr + size];
#[cfg(feature = "std")]
if let Some(Some(hit)) = hits.get(i) {
raw_bytes.check(i, c.address, size)?;
// SAFETY: see above.
unsafe { placer.place(hit, offsets, out) };
return Ok(());
}
let raw = raw_bytes.get(i, c.address, size)?;
let Some(pl) = pipeline else {
// SAFETY: see above.
unsafe { placer.place(raw, offsets, &out) };
unsafe { placer.place(raw, offsets, out) };
return Ok(());
};
// A chunk stored as-is (every filter skipped) is checked and placed
@@ -296,7 +381,7 @@ fn fill_from_chunks(
}
};
// SAFETY: see above.
unsafe { placer.place(&cached, offsets, &out) };
unsafe { placer.place(&cached, offsets, out) };
return Ok(());
}
let data = decompress_chunk_exact_with(
@@ -309,7 +394,7 @@ fn fill_from_chunks(
scratch,
)?;
// SAFETY: see above.
unsafe { placer.place(data, offsets, &out) };
unsafe { placer.place(data, offsets, out) };
Ok(())
};
@@ -370,7 +455,29 @@ pub fn decompress_all_chunks_with_stats(
seed: u64,
num_lanes: Option<usize>,
) -> Result<(Vec<Vec<u8>>, PartitionStats), FormatError> {
parallel_read::decompress_chunks_lane_partitioned(
decompress_all_chunks_with_stats_in(
file_data,
chunks,
pipeline,
chunk_total_bytes,
element_size,
seed,
num_lanes,
)
}
/// [`decompress_all_chunks_with_stats`] over any [`Storage`].
#[cfg(feature = "parallel")]
pub fn decompress_all_chunks_with_stats_in<S: Storage + ?Sized>(
file_data: &S,
chunks: &[ChunkInfo],
pipeline: &FilterPipeline,
chunk_total_bytes: usize,
element_size: u32,
seed: u64,
num_lanes: Option<usize>,
) -> Result<(Vec<Vec<u8>>, PartitionStats), FormatError> {
parallel_read::decompress_chunks_lane_partitioned_in(
file_data,
chunks,
pipeline,
@@ -394,21 +501,6 @@ pub struct ChunkInfo {
pub address: u64,
}
/// Checks that `[offset, offset + needed)` fits within `data`, guarding the
/// addition against `usize` overflow from a crafted near-`usize::MAX` offset.
fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
if offset
.checked_add(needed)
.is_none_or(|end| end > data.len())
{
return Err(FormatError::UnexpectedEof {
expected: offset.saturating_add(needed),
available: data.len(),
});
}
Ok(())
}
/// `elements * elem_size` for sizes that come from the file. Dataspace and
/// chunk dimensions are untrusted 64-bit fields, so a crafted file can make
/// the plain product wrap to a small number (or to something enormous).
@@ -588,6 +680,17 @@ pub fn collect_chunk_info(
ndims: usize,
offset_size: u8,
length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
collect_chunk_info_in(file_data, btree_address, ndims, offset_size, length_size)
}
/// [`collect_chunk_info`] over any [`Storage`].
pub fn collect_chunk_info_in<S: Storage + ?Sized>(
file_data: &S,
btree_address: u64,
ndims: usize,
offset_size: u8,
length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let _ = length_size;
let mut chunks = Vec::new();
@@ -630,6 +733,23 @@ pub fn collect_chunk_info_checked(
chunk_dimensions: &[u32],
offset_size: u8,
length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
collect_chunk_info_checked_in(
file_data,
btree_address,
chunk_dimensions,
offset_size,
length_size,
)
}
/// [`collect_chunk_info_checked`] over any [`Storage`].
pub fn collect_chunk_info_checked_in<S: Storage + ?Sized>(
file_data: &S,
btree_address: u64,
chunk_dimensions: &[u32],
offset_size: u8,
length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let _ = length_size;
let ndims = chunk_dimensions.len();
@@ -813,8 +933,8 @@ const MAX_CHUNK_BTREE_DEPTH: usize = 64;
/// Parse the v1 B-tree chunk index node at `btree_address` and its
/// subtree, appending its chunks to `stored` in tree order.
fn parse_chunk_node(
file_data: &[u8],
fn parse_chunk_node<S: Storage + ?Sized>(
file_data: &S,
btree_address: u64,
ndims: usize,
chunk_dimensions: Option<&[u32]>,
@@ -831,21 +951,24 @@ fn parse_chunk_node(
// Parse B-tree v1 header
let header_size = 8 + os * 2;
ensure_len(file_data, offset, header_size)?;
let head = Window::read(file_data, offset as u64, header_size)?;
head.ensure(0, header_size)?;
let h = &head.bytes;
if &file_data[offset..offset + 4] != b"TREE" {
if &h[..4] != b"TREE" {
return Err(FormatError::InvalidBTreeSignature);
}
let node_type = file_data[offset + 4];
let node_type = h[4];
if node_type != 1 {
return Err(FormatError::InvalidBTreeNodeType(node_type));
}
let node_level = file_data[offset + 5];
let entries_used = u16::from_le_bytes([file_data[offset + 6], file_data[offset + 7]]) as usize;
let node_level = h[5];
let entries_used = u16::from_le_bytes([h[6], h[7]]) as usize;
let mut pos = offset + 8 + os * 2; // skip left/right sibling
// Positions below are relative to the node's start.
let mut pos = header_size; // skip left/right sibling
// Key: chunk_size(4) + filter_mask(4) + one offset per dimension. The
// offsets are always 8 bytes each — they are dataset coordinates, not file
@@ -858,28 +981,20 @@ fn parse_chunk_node(
// key[0], child[0], key[1], child[1], ..., key[N-1], child[N-1], key[N]
let needed = entries_used * (key_size + os) + key_size;
ensure_len(file_data, pos, needed)?;
let node = Window::read(file_data, offset as u64, header_size + needed)?;
node.ensure(pos, needed)?;
let d = &node.bytes;
let mut keys = Vec::with_capacity((entries_used + 1) * ndims);
let mut chunks = Vec::new();
let mut child_addrs = Vec::new();
for _ in 0..entries_used {
let chunk_size = u32::from_le_bytes([
file_data[pos],
file_data[pos + 1],
file_data[pos + 2],
file_data[pos + 3],
]);
let filter_mask = u32::from_le_bytes([
file_data[pos + 4],
file_data[pos + 5],
file_data[pos + 6],
file_data[pos + 7],
]);
let chunk_size = u32::from_le_bytes([d[pos], d[pos + 1], d[pos + 2], d[pos + 3]]);
let filter_mask = u32::from_le_bytes([d[pos + 4], d[pos + 5], d[pos + 6], d[pos + 7]]);
let k = keys.len();
read_key_offsets(file_data, pos, ndims, chunk_dimensions, &mut keys)?;
read_key_offsets(d, pos, ndims, chunk_dimensions, &mut keys)?;
pos += key_size;
let address = read_offset(file_data, pos, offset_size)?;
let address = read_offset(d, pos, offset_size)?;
pos += os;
if node_level == 0 {
chunks.push(stored.len());
@@ -894,7 +1009,7 @@ fn parse_chunk_node(
}
}
// The final key only bounds the node; libhdf5 still checks it.
read_key_offsets(file_data, pos, ndims, chunk_dimensions, &mut keys)?;
read_key_offsets(d, pos, ndims, chunk_dimensions, &mut keys)?;
let children = if node_level == 0 {
ChunkChildren::Chunks(chunks)
@@ -984,18 +1099,18 @@ const BT2_CHUNK_FILTERED: u8 = 11;
/// The width of the stored-size field depends on the largest possible chunk;
/// rather than re-derive the library's formula it is taken from the record
/// size the tree header declares, which is what actually governs the bytes.
fn read_btree_v2_chunks(
file_data: &[u8],
fn read_btree_v2_chunks<S: Storage + ?Sized>(
file_data: &S,
addr: u64,
chunk_dims: &[usize],
elem_size: usize,
offset_size: u8,
length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records_in};
let bad = |what: &str| FormatError::ChunkedReadError(format!("B-tree v2 chunk index: {what}"));
let header = BTreeV2Header::parse(file_data, to_usize(addr)?, offset_size, length_size)?;
let header = BTreeV2Header::parse_in(file_data, checked_addr(addr)?, offset_size, length_size)?;
let rank = chunk_dims.len();
let os = offset_size as usize;
let record_size = header.record_size as usize;
@@ -1022,7 +1137,7 @@ fn read_btree_v2_chunks(
let unfiltered_bytes =
u32::try_from(unfiltered_bytes).map_err(|_| bad("chunk larger than 4 GiB"))?;
let records = collect_btree_v2_records(file_data, &header, offset_size, length_size)?;
let records = collect_btree_v2_records_in(file_data, &header, offset_size, length_size)?;
let mut chunks = Vec::with_capacity(records.len());
for record in &records {
let data = record.data.as_slice();
@@ -1085,6 +1200,25 @@ pub fn list_chunks(
elem_size: usize,
offset_size: u8,
length_size: u8,
) -> Result<(Vec<ChunkInfo>, Vec<usize>), FormatError> {
list_chunks_in(
file_data,
layout,
dataspace,
elem_size,
offset_size,
length_size,
)
}
/// [`list_chunks`] over any [`Storage`].
pub fn list_chunks_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
elem_size: usize,
offset_size: u8,
length_size: u8,
) -> Result<(Vec<ChunkInfo>, Vec<usize>), FormatError> {
let (
chunk_dimensions,
@@ -1132,9 +1266,13 @@ pub fn list_chunks(
// Collect chunks based on version and index type
let mut chunks = match (version, chunk_index_type) {
(3, _) => {
collect_chunk_info_checked(file_data, addr, chunk_dimensions, offset_size, length_size)?
}
(3, _) => collect_chunk_info_checked_in(
file_data,
addr,
chunk_dimensions,
offset_size,
length_size,
)?,
(4, Some(1)) => {
// Single chunk — one chunk covering the entire dataset
let chunk_byte_size = checked_chunk_byte_len(&chunk_dims, elem_size)?;
@@ -1163,9 +1301,13 @@ pub fn list_chunks(
(4, Some(3)) => {
// Fixed Array — use spatial chunk dims only
let spatial_chunk_dims: &[u32] = &chunk_dimensions[..rank];
let header =
FixedArrayHeader::parse(file_data, to_usize(addr)?, offset_size, length_size)?;
read_fixed_array_chunks(
let header = FixedArrayHeader::parse_in(
file_data,
checked_addr(addr)?,
offset_size,
length_size,
)?;
read_fixed_array_chunks_in(
file_data,
&header,
&dataspace.dimensions,
@@ -1179,9 +1321,13 @@ pub fn list_chunks(
(4, Some(4)) => {
// Extensible Array — use spatial chunk dims only
let spatial_chunk_dims: &[u32] = &chunk_dimensions[..rank];
let header =
ExtensibleArrayHeader::parse(file_data, to_usize(addr)?, offset_size, length_size)?;
read_extensible_array_chunks(
let header = ExtensibleArrayHeader::parse_in(
file_data,
checked_addr(addr)?,
offset_size,
length_size,
)?;
read_extensible_array_chunks_in(
file_data,
&header,
&dataspace.dimensions,
@@ -1248,7 +1394,28 @@ pub fn list_chunks_for_read(
offset_size: u8,
length_size: u8,
) -> Result<(Vec<ChunkInfo>, Vec<usize>), FormatError> {
let (chunks, chunk_dims) = list_chunks(
list_chunks_for_read_in(
file_data,
layout,
dataspace,
elem_size,
pipeline,
offset_size,
length_size,
)
}
/// [`list_chunks_for_read`] over any [`Storage`].
pub fn list_chunks_for_read_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
elem_size: usize,
pipeline: Option<&FilterPipeline>,
offset_size: u8,
length_size: u8,
) -> Result<(Vec<ChunkInfo>, Vec<usize>), FormatError> {
let (chunks, chunk_dims) = list_chunks_in(
file_data,
layout,
dataspace,
@@ -1284,8 +1451,8 @@ pub(crate) type CacheRef<'a> = Option<&'a core::convert::Infallible>;
/// output with `alloc` (zeroed, `total_bytes` long, as bytes through
/// `bytes`), and decode every chunk straight into it.
#[allow(clippy::too_many_arguments)]
pub(crate) fn read_chunked_full<O>(
file_data: &[u8],
pub(crate) fn read_chunked_full<O, S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
@@ -1299,7 +1466,7 @@ pub(crate) fn read_chunked_full<O>(
check_chunk_element_size(layout, datatype, offset_size)?;
let elem_size = datatype.type_size() as usize;
let list = || {
list_chunks_for_read(
list_chunks_for_read_in(
file_data,
layout,
dataspace,
@@ -1390,6 +1557,27 @@ pub fn read_chunked_data(
pipeline: Option<&FilterPipeline>,
offset_size: u8,
length_size: u8,
) -> Result<Vec<u8>, FormatError> {
read_chunked_data_in(
file_data,
layout,
dataspace,
datatype,
pipeline,
offset_size,
length_size,
)
}
/// [`read_chunked_data`] over any [`Storage`].
pub fn read_chunked_data_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
pipeline: Option<&FilterPipeline>,
offset_size: u8,
length_size: u8,
) -> Result<Vec<u8>, FormatError> {
read_chunked_full(
file_data,
@@ -1422,6 +1610,31 @@ pub fn read_chunked_data_cached(
offset_size: u8,
length_size: u8,
cache: &ChunkCache,
) -> Result<Vec<u8>, FormatError> {
read_chunked_data_cached_in(
file_data,
layout,
dataspace,
datatype,
pipeline,
offset_size,
length_size,
cache,
)
}
/// [`read_chunked_data_cached`] over any [`Storage`].
#[cfg(feature = "std")]
#[allow(clippy::too_many_arguments)]
pub fn read_chunked_data_cached_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
pipeline: Option<&FilterPipeline>,
offset_size: u8,
length_size: u8,
cache: &ChunkCache,
) -> Result<Vec<u8>, FormatError> {
read_chunked_full(
file_data,
@@ -1592,6 +1805,33 @@ pub fn read_chunked_data_sweep(
length_size: u8,
cache: &ChunkCache,
sweep: &mut SweepContext,
) -> Result<Vec<u8>, FormatError> {
read_chunked_data_sweep_in(
file_data,
layout,
dataspace,
datatype,
pipeline,
offset_size,
length_size,
cache,
sweep,
)
}
/// [`read_chunked_data_sweep`] over any [`Storage`].
#[cfg(feature = "std")]
#[allow(clippy::too_many_arguments)]
pub fn read_chunked_data_sweep_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
pipeline: Option<&FilterPipeline>,
offset_size: u8,
length_size: u8,
cache: &ChunkCache,
sweep: &mut SweepContext,
) -> Result<Vec<u8>, FormatError> {
let (chunk_dimensions, version, addr_opt) = match layout {
DataLayout::Chunked {
@@ -1623,7 +1863,7 @@ pub fn read_chunked_data_sweep(
// lookup is keyed by this dataset's chunk-index address, so another
// dataset's index or chunks are never used for this read.
let chunks = cache.chunks_for(addr, rank, || {
list_chunks_for_read(
list_chunks_for_read_in(
file_data,
layout,
dataspace,
@@ -1675,8 +1915,8 @@ pub fn read_chunked_data_sweep(
// Decompress from file
let c_addr = to_usize(chunk_info.address)?;
let size = chunk_info.chunk_size as usize;
ensure_len(file_data, c_addr, size)?;
let raw_chunk = &file_data[c_addr..c_addr + size];
let raw_chunk = read_exact_at(file_data, c_addr as u64, size)?;
let raw_chunk = &*raw_chunk;
let dec = if let Some(pl) = pipeline {
decompress_chunk_exact(
raw_chunk,
@@ -1736,6 +1976,31 @@ pub fn read_chunked_data_indexed(
offset_size: u8,
length_size: u8,
cache: &ChunkCache,
) -> Result<Vec<u8>, FormatError> {
read_chunked_data_indexed_in(
file_data,
layout,
dataspace,
datatype,
pipeline,
offset_size,
length_size,
cache,
)
}
/// [`read_chunked_data_indexed`] over any [`Storage`].
#[cfg(feature = "std")]
#[allow(clippy::too_many_arguments)]
pub fn read_chunked_data_indexed_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
pipeline: Option<&FilterPipeline>,
offset_size: u8,
length_size: u8,
cache: &ChunkCache,
) -> Result<Vec<u8>, FormatError> {
let (chunk_dimensions, version, addr_opt) = match layout {
DataLayout::Chunked {
@@ -1769,7 +2034,7 @@ pub fn read_chunked_data_indexed(
addr,
rank,
|| {
list_chunks_for_read(
list_chunks_for_read_in(
file_data,
layout,
dataspace,
@@ -1786,18 +2051,30 @@ pub fn read_chunked_data_indexed(
)?;
let chunk_total_bytes = plan.chunk_total_bytes;
// The decoded chunks the cache holds, and the stored bytes of the
// others: fetched in one batch when the file is not in memory.
let hits: Vec<Option<Arc<CacheAlignedBuffer>>> = plan
.mappings
.iter()
.map(|m| cache.get_decompressed_in(addr, &m.coord))
.collect();
let extents: Vec<(u64, usize, bool)> = plan
.mappings
.iter()
.zip(&hits)
.map(|(m, hit)| (m.file_offset, m.file_size as usize, hit.is_none()))
.collect();
let raw_bytes = ExtentBytes::fetch(file_data, &extents)?;
// Decompress chunks (using LRU cache where possible)
let mut chunk_buffers: Vec<Arc<CacheAlignedBuffer>> = Vec::with_capacity(plan.mappings.len());
for m in &plan.mappings {
for (i, (m, hit)) in plan.mappings.iter().zip(hits).enumerate() {
let (coord, file_offset, file_size, filter_mask) =
(&m.coord, &m.file_offset, &m.file_size, &m.filter_mask);
if let Some(cached) = cache.get_decompressed_in(addr, coord) {
if let Some(cached) = hit {
chunk_buffers.push(cached);
} else {
let c_addr = to_usize(*file_offset)?;
let size = *file_size as usize;
ensure_len(file_data, c_addr, size)?;
let raw_chunk = &file_data[c_addr..c_addr + size];
let raw_chunk = raw_bytes.get(i, *file_offset, *file_size as usize)?;
let decompressed = if let Some(pl) = pipeline {
decompress_chunk_exact(
raw_chunk,