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:
@@ -300,7 +300,7 @@ impl Storage for FileData {
|
||||
if len == 0 {
|
||||
return Ok(Cow::Borrowed(&[]));
|
||||
}
|
||||
let bytes = self.remote()?.read_at(self.base + offset, len)?;
|
||||
let bytes = cut_to(self.remote()?.read_at(self.base + offset, len)?, len);
|
||||
Ok(self.with_overlay(offset, bytes))
|
||||
}
|
||||
|
||||
@@ -323,8 +323,11 @@ impl Storage for FileData {
|
||||
let got = self.remote()?.read_ranges(&shifted)?;
|
||||
Ok(got
|
||||
.into_iter()
|
||||
.zip(ranges)
|
||||
.map(|(bytes, r)| self.with_overlay(r.start, bytes))
|
||||
.zip(ranges.iter().zip(&shifted))
|
||||
.map(|(bytes, (r, asked))| {
|
||||
let bytes = cut_to(bytes, (asked.end - asked.start) as usize);
|
||||
self.with_overlay(r.start, bytes)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -334,6 +337,23 @@ impl Storage for FileData {
|
||||
}
|
||||
}
|
||||
|
||||
/// `bytes`, a backend's answer to a read of `len` bytes, without anything
|
||||
/// past those `len` (a backend that returns more than asked breaks
|
||||
/// [`Storage::read_at`]'s contract; the extra bytes are not the file's). A
|
||||
/// short answer is passed on: the parsers' own checks refuse it.
|
||||
fn cut_to(bytes: Cow<'_, [u8]>, len: usize) -> Cow<'_, [u8]> {
|
||||
if bytes.len() <= len {
|
||||
return bytes;
|
||||
}
|
||||
match bytes {
|
||||
Cow::Borrowed(b) => Cow::Borrowed(&b[..len]),
|
||||
Cow::Owned(mut v) => {
|
||||
v.truncate(len);
|
||||
Cow::Owned(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -432,3 +432,50 @@ fn storage_backed_files_keep_their_zero_copy_views_only_in_memory() {
|
||||
"as_bytes over a range storage must not answer"
|
||||
);
|
||||
}
|
||||
|
||||
/// A storage that returns 37 junk bytes more than every read asked for.
|
||||
struct Overlong(Vec<u8>);
|
||||
|
||||
impl clawhdf5::Storage for Overlong {
|
||||
fn read_at(&self, offset: u64, len: usize) -> Result<std::borrow::Cow<'_, [u8]>, FormatError> {
|
||||
let mut v = clawhdf5::Storage::read_at(self.0.as_slice(), offset, len)?.into_owned();
|
||||
v.extend(std::iter::repeat_n(0xa5, 37));
|
||||
Ok(std::borrow::Cow::Owned(v))
|
||||
}
|
||||
|
||||
fn len(&self) -> u64 {
|
||||
self.0.len() as u64
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes a misbehaving storage returns past the range asked for are never
|
||||
/// read as the file's: every fixture reads through it as through
|
||||
/// `File::open`.
|
||||
#[test]
|
||||
fn overlong_storage_reads_identically() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let mut files = Vec::new();
|
||||
hdf5_files(&root.join("tests/fixtures"), &mut files);
|
||||
hdf5_files(&root.join("../clawhdf5-format/tests/fixtures"), &mut files);
|
||||
files.sort();
|
||||
let mut compared = 0;
|
||||
for path in &files {
|
||||
let Ok(bytes) = std::fs::read(path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(local) = File::open(path) else {
|
||||
continue;
|
||||
};
|
||||
let mut remote = File::open_storage(Arc::new(Overlong(bytes)))
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", path.display()));
|
||||
remote.set_vds_resolver(sibling_resolver(path.parent().map(Path::to_path_buf)));
|
||||
assert_eq!(
|
||||
transcript(&local),
|
||||
transcript(&remote),
|
||||
"{}",
|
||||
path.display()
|
||||
);
|
||||
compared += 1;
|
||||
}
|
||||
assert!(compared >= 40, "{compared}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user