//! File address and length → in-memory index conversion. //! //! HDF5 addresses and lengths are 64-bit; the file is parsed through a //! `&[u8]` indexed by `usize`. On a 64-bit target every `u64` fits, but on a //! 32-bit one (`wasm32`, `i686`, `thumbv7em`) an address past `usize::MAX` //! used to be truncated by an `as usize` cast — silently pointing at another //! part of the file — or to panic. [`to_usize`] is the one conversion the //! parsers use instead: such an address is a clean //! [`FormatError::Overflow`]. It cannot be inside the data anyway: no slice //! is longer than `isize::MAX` bytes. #[cfg(not(feature = "std"))] use alloc::format; use crate::error::FormatError; /// A file address, offset or length from the file as a `usize` index. /// /// Fails with [`FormatError::Overflow`] when the value does not fit this /// platform's `usize` (only possible on targets narrower than 64 bits). #[inline] pub fn to_usize(value: u64) -> Result { to_index::(value) } /// A file address for a [`crate::storage::Storage`] read, checked as /// [`to_usize`] checks it: the parsers read through 64-bit offsets, but an /// address that could not index an in-memory file on this platform is the /// same [`FormatError::Overflow`] the slice parsers gave for it. #[inline] pub fn checked_addr(value: u64) -> Result { to_usize(value).map(|_| value) } /// [`to_usize`] for an index type of any width. `usize` is 64 bits wide on /// the hosts CI tests on, where the error path cannot be reached through /// `usize`; tests run the same code with `u32` in its place, as on a 32-bit /// target. #[inline] fn to_index>(value: u64) -> Result { T::try_from(value).map_err(|_| too_large(value)) } /// A count or offset into an in-memory buffer (a codec's progress counter, /// a size the writer computed from data it holds) as a `usize`, saturating /// at `usize::MAX` instead of truncating. /// /// For values that are bounded by the length of something in memory, so /// always fit; if one ever did not, a saturated index fails its bounds check /// or allocation instead of silently addressing the wrong bytes. A value /// read from the file uses [`to_usize`]. #[inline] pub fn saturating_usize(value: u64) -> usize { saturating_index(value, usize::MAX) } /// [`saturating_usize`] for an index type of any width, whose largest /// value is `max` (see [`to_index`]). #[inline] fn saturating_index>(value: u64, max: T) -> T { T::try_from(value).unwrap_or(max) } #[cold] #[inline(never)] fn too_large(value: u64) -> FormatError { FormatError::Overflow(format!( "file address or length {value:#x} exceeds this platform's address space" )) } #[cfg(test)] mod tests { use super::*; #[test] fn values_that_fit_convert_exactly() { assert_eq!(to_usize(0), Ok(0)); assert_eq!(to_usize(0x1234), Ok(0x1234)); assert_eq!(to_usize(usize::MAX as u64), Ok(usize::MAX)); } #[test] fn saturating_conversion_never_wraps() { assert_eq!(saturating_usize(0), 0); assert_eq!(saturating_usize(0x1234), 0x1234); assert_eq!(saturating_usize(usize::MAX as u64), usize::MAX); // Past usize::MAX (32-bit targets) or at u64::MAX: saturates. assert_eq!(saturating_usize(u64::MAX), usize::MAX); } #[test] fn values_past_usize_max_are_an_error_not_truncated() { // Reachable through `usize` only where it is narrower than u64 (no // such target runs tests in CI), so the same conversion is run with // u32 standing in for a 32-bit usize. let max = u64::from(u32::MAX); assert_eq!(to_index::(max), Ok(u32::MAX)); for past in [max + 1, max + 0x10, 0x1_0000_1234, u64::MAX] { let err = to_index::(past).unwrap_err(); assert!( matches!(err, FormatError::Overflow(_)), "{past:#x}: {err:?}" ); } // Where an `as` cast would have wrapped to a small, valid-looking // index, it is not returned. assert_eq!(0x1_0000_1234_u64 as u32, 0x1234); assert!(to_index::(0x1_0000_1234).is_err()); assert_eq!(saturating_index(max + 1, u32::MAX), u32::MAX); assert_eq!(saturating_index(0x1_0000_1234, u32::MAX), u32::MAX); assert_eq!(saturating_index(0x1234, u32::MAX), 0x1234); // And through `usize` itself, whichever width it has here. match (usize::MAX as u64).checked_add(1) { Some(past) => assert!(matches!(to_usize(past), Err(FormatError::Overflow(_)))), None => assert_eq!(to_usize(u64::MAX), Ok(usize::MAX)), } } }