format, clawhdf5: cut every Storage read to the range asked for

ExtentBytes and read_exact_at/read_upto rejected short results but passed
longer-than-asked ones through, and FileData forwarded them too, so a
Storage that broke read_at's contract by returning extra bytes had them
decoded or returned as data (a contiguous dataset read gained 37 junk
bytes). gather_storage alone trimmed.

- storage::exact_len (new, pub): a read of len bytes as exactly len — cut
  when longer, an error when short. read_exact_at, read_upto and
  ExtentBytes (so chunk fetches and selection gathers) go through it.
- FileData cuts a backend's answer to what it asked for before laying the
  cache image over it.
- Tests: over a storage that appends 37 junk bytes to every read, every
  format-crate fixture reads exactly as from the slice
  (overlong_reads_are_cut_to_the_range_asked_for), and every facade
  fixture opens and reads through File::open_storage as through File::open
  (overlong_storage_reads_identically). Both failed before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 18:43:05 -05:00
co-authored by Claude Opus 5.5
parent 89e7977943
commit 6185874f9c
5 changed files with 225 additions and 90 deletions
@@ -753,6 +753,73 @@ fn corpus_parses_identically_through_storage() {
assert!(tally.files > 0);
}
/// The whole read of every object reachable from the root group of `sb`'s
/// file — each dataset whole (fill-aware) and half of it through a
/// selection, and each group's listing — as one result to compare.
fn read_everything(file: &dyn Storage, sb: &Superblock) -> Result<String, FormatError> {
let (os, ls) = (sb.offset_size, sb.length_size);
let mut out = String::new();
let mut queue = VecDeque::from([sb.root_group_address]);
let mut seen = HashSet::new();
while let Some(addr) = queue.pop_front() {
if seen.len() > 200 || !seen.insert(addr) {
continue;
}
let header = ObjectHeader::parse_in(file, addr, os, ls)?;
let find = |t: MessageType| {
header
.messages
.iter()
.find(|m| m.msg_type == t)
.map(|m| message_data_with_sohm_in(file, m, os, ls))
.transpose()
};
if let (Some(dt), Some(ds), Some(dl)) = (
find(MessageType::Datatype)?,
find(MessageType::Dataspace)?,
find(MessageType::DataLayout)?,
) {
let dt = Datatype::parse(&dt)?.0;
let ds = Dataspace::parse(&ds, ls)?;
let dl = DataLayout::parse(&dl, os, ls)?;
let pl = find(MessageType::FilterPipeline)?
.map(|p| FilterPipeline::parse(&p))
.transpose()?;
let data = read_full_with_fill_in(
&header.messages,
file,
&dl,
&ds,
dt.type_size() as usize,
os,
ls,
|| read_raw_data_full_in(file, &dl, &ds, &dt, pl.as_ref(), os, ls),
);
out.push_str(&format!("{addr}: {data:?}\n"));
if let Some(&d0) = ds.dimensions.first() {
let rank = ds.dimensions.len();
let sel = Selection::Hyperslab {
start: vec![0; rank],
stride: vec![1; rank],
count: std::iter::once(d0.div_ceil(2))
.chain(ds.dimensions[1..].iter().copied())
.collect(),
block: vec![1; rank],
};
let part =
read_raw_data_selection_in(file, &dl, &ds, &dt, pl.as_ref(), os, ls, &sel);
out.push_str(&format!("{addr} half: {part:?}\n"));
}
}
let children = group_v2::resolve_group_children_in(file, sb, addr);
out.push_str(&format!("{addr} children: {children:?}\n"));
if let Ok(c) = children {
queue.extend(c.iter().map(|c| c.object_header_address));
}
}
Ok(out)
}
/// A storage that misbehaves: fails its `fail_at`-th read (1-based, `0`
/// never), and, with `short`, serves one byte less than asked for inside
/// the file (a truncated response).
@@ -808,78 +875,7 @@ fn misbehaving_storage_never_returns_wrong_data() {
let Ok(sb) = Superblock::parse(hdf5, 0) else {
continue;
};
// The whole read of every object, as one result to compare.
let everything = |file: &dyn Storage| -> Result<String, FormatError> {
let (os, ls) = (sb.offset_size, sb.length_size);
let mut out = String::new();
let mut queue = VecDeque::from([sb.root_group_address]);
let mut seen = HashSet::new();
while let Some(addr) = queue.pop_front() {
if seen.len() > 200 || !seen.insert(addr) {
continue;
}
let header = ObjectHeader::parse_in(file, addr, os, ls)?;
let find = |t: MessageType| {
header
.messages
.iter()
.find(|m| m.msg_type == t)
.map(|m| message_data_with_sohm_in(file, m, os, ls))
.transpose()
};
if let (Some(dt), Some(ds), Some(dl)) = (
find(MessageType::Datatype)?,
find(MessageType::Dataspace)?,
find(MessageType::DataLayout)?,
) {
let dt = Datatype::parse(&dt)?.0;
let ds = Dataspace::parse(&ds, ls)?;
let dl = DataLayout::parse(&dl, os, ls)?;
let pl = find(MessageType::FilterPipeline)?
.map(|p| FilterPipeline::parse(&p))
.transpose()?;
let data = read_full_with_fill_in(
&header.messages,
file,
&dl,
&ds,
dt.type_size() as usize,
os,
ls,
|| read_raw_data_full_in(file, &dl, &ds, &dt, pl.as_ref(), os, ls),
);
out.push_str(&format!("{addr}: {data:?}\n"));
if let Some(&d0) = ds.dimensions.first() {
let rank = ds.dimensions.len();
let sel = Selection::Hyperslab {
start: vec![0; rank],
stride: vec![1; rank],
count: std::iter::once(d0.div_ceil(2))
.chain(ds.dimensions[1..].iter().copied())
.collect(),
block: vec![1; rank],
};
let part = read_raw_data_selection_in(
file,
&dl,
&ds,
&dt,
pl.as_ref(),
os,
ls,
&sel,
);
out.push_str(&format!("{addr} half: {part:?}\n"));
}
}
let children = group_v2::resolve_group_children_in(file, &sb, addr);
out.push_str(&format!("{addr} children: {children:?}\n"));
if let Ok(c) = children {
queue.extend(c.iter().map(|c| c.object_header_address));
}
}
Ok(out)
};
let everything = |file: &dyn Storage| read_everything(file, &sb);
let want = everything(&hdf5);
let counting = CountingStorage::new(hdf5.to_vec());
assert_eq!(
@@ -947,6 +943,61 @@ fn misbehaving_storage_never_returns_wrong_data() {
);
}
/// A storage that breaks `read_at`'s contract the other way: every read
/// comes back with 37 bytes more than asked for (junk past the range).
struct Overlong {
data: Vec<u8>,
}
impl Storage for Overlong {
fn read_at(&self, offset: u64, len: usize) -> Result<std::borrow::Cow<'_, [u8]>, FormatError> {
let mut v = self.data.as_slice().read_at(offset, len)?.into_owned();
v.extend(std::iter::repeat_n(0xa5, 37));
Ok(std::borrow::Cow::Owned(v))
}
fn len(&self) -> u64 {
self.data.len() as u64
}
}
/// Bytes past the range asked for are never used: every read through a
/// storage that returns more than asked gives exactly the in-memory result.
#[test]
fn overlong_reads_are_cut_to_the_range_asked_for() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let mut files = Vec::new();
hdf5_files(&dir, &mut files);
files.sort();
let mut compared = 0;
for path in &files {
let Ok(bytes) = std::fs::read(path) else {
continue;
};
let Ok((_, hdf5)) = split_user_block(&bytes) else {
continue;
};
let Ok(sb) = Superblock::parse(hdf5, 0) else {
continue;
};
let want = read_everything(&hdf5, &sb);
let got = read_everything(
&Overlong {
data: hdf5.to_vec(),
},
&sb,
);
assert_eq!(
format!("{got:?}"),
format!("{want:?}"),
"{}",
path.display()
);
compared += 1;
}
assert!(compared > 40, "{compared}");
}
/// A read_at-only storage that also counts `read_ranges` calls and ranges.
struct BatchCounting {
inner: CountingStorage,