format: no truncating u64 -> usize casts

Every `u64 as usize` cast in clawhdf5-format (115 on wasm32) now goes
through addr::to_usize for values read from the file — addresses, lengths,
counts, dimensions: FormatError::Overflow where the value does not fit
instead of wrapping onto another part of the file on a 32-bit target — or
addr::saturating_usize for counts bounded by something in memory (codec
progress counters, writer sizes), which fail a bounds check or allocation
rather than wrap. A chunk whose offset does not fit lies outside the
dataset and is skipped; partial reads treat such an offset as out of the
buffers. On 64-bit targets nothing changes.

scripts/check-32bit-casts.sh (run by ci-test.sh) lints the wasm32 build
with clippy's cast_possible_truncation and fails on any u64 -> usize
finding; before this commit it listed 115.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 13:33:24 -05:00
co-authored by Claude Opus 5.5
parent 02e89c1d2d
commit b41583113a
27 changed files with 236 additions and 99 deletions
+11 -9
View File
@@ -3,6 +3,7 @@
//! Produces valid HDF5 files with v3 superblock, v2 object headers,
//! link messages, contiguous datasets, inline and dense attributes.
use crate::addr::saturating_usize;
#[cfg(not(feature = "std"))]
use alloc::{format, vec, vec::Vec};
@@ -336,7 +337,7 @@ pub(crate) fn build_single_block_fractal_heap(
// An object must fit one direct block: the writer has no huge-object
// path, and libhdf5 cannot read an object that overruns its block.
let max_managed = max_direct_block_size as usize - dblock_header_size;
let max_managed = saturating_usize(max_direct_block_size) - dblock_header_size;
if let Some(big) = serialized.iter().find(|s| s.len() > max_managed) {
return Err(FormatError::SerializationError(format!(
"a {}-byte message cannot go in dense storage: a fractal heap \
@@ -392,7 +393,7 @@ pub(crate) fn build_single_block_fractal_heap(
let dblock_addr = frhp_addr + frhp_size as u64;
let btree_addr = dblock_addr + starting_block_size;
let data_space = starting_block_size as usize - dblock_header_size;
let data_space = saturating_usize(starting_block_size) - dblock_header_size;
let free_space = data_space - total_data_size;
// Build fractal heap header
@@ -428,7 +429,7 @@ pub(crate) fn build_single_block_fractal_heap(
debug_assert_eq!(frhp.len(), frhp_size);
// Build direct block: header (with checksum) + data + padding
let mut dblock = Vec::with_capacity(starting_block_size as usize);
let mut dblock = Vec::with_capacity(saturating_usize(starting_block_size));
dblock.extend_from_slice(b"FHDB");
dblock.push(0); // version
write_offset(&mut dblock, frhp_addr, OFFSET_SIZE);
@@ -446,12 +447,12 @@ pub(crate) fn build_single_block_fractal_heap(
}
// Pad to full block size
dblock.resize(starting_block_size as usize, 0);
dblock.resize(saturating_usize(starting_block_size), 0);
// Checksum: computed over entire block with checksum field zeroed
let dblock_checksum = crate::checksum::jenkins_lookup3(&dblock);
dblock[cksum_pos..cksum_pos + 4].copy_from_slice(&dblock_checksum.to_le_bytes());
debug_assert_eq!(dblock.len(), starting_block_size as usize);
debug_assert_eq!(dblock.len(), saturating_usize(starting_block_size));
// Build heap IDs
let heap_ids: Vec<Vec<u8>> = obj_offsets
@@ -706,7 +707,7 @@ impl HeapIndirectBlock {
let cksum_pos = out.len();
out.extend_from_slice(&[0u8; 4]); // checksum placeholder
out.extend_from_slice(&b.data);
out.resize(d + b.size as usize, 0);
out.resize(d + saturating_usize(b.size), 0);
let cksum = crate::checksum::jenkins_lookup3(&out[d..]);
out[cksum_pos..cksum_pos + 4].copy_from_slice(&cksum.to_le_bytes());
child += b.size;
@@ -740,7 +741,7 @@ impl HeapPacker<'_> {
nrows: Option<usize>,
) -> Result<HeapIndirectBlock, FormatError> {
let geom = self.geom;
let width = geom.width as usize;
let width = saturating_usize(geom.width);
let mut slots = Vec::new();
let mut off = heap_offset;
let mut row = 0usize;
@@ -763,7 +764,8 @@ impl HeapPacker<'_> {
// A child whose biggest direct block cannot hold the
// next object is skipped whole, not walked.
let biggest = geom.row_size(child_rows.min(geom.max_direct_rows()) - 1);
if self.objects[self.next].len() > (biggest as usize - geom.dblock_header_size)
if self.objects[self.next].len()
> (saturating_usize(biggest) - geom.dblock_header_size)
{
slots.push(HeapSlot::Empty);
off += size;
@@ -794,7 +796,7 @@ impl HeapPacker<'_> {
/// objects as fit; leave it unallocated if not even the next one does.
fn fill_direct(&mut self, heap_offset: u64, size: u64) -> HeapSlot {
let header = self.geom.dblock_header_size;
let capacity = size as usize - header;
let capacity = saturating_usize(size) - header;
let mut data = Vec::new();
while let Some(obj) = self.objects.get(self.next) {
if data.len() + obj.len() > capacity {