diff --git a/Cargo.toml b/Cargo.toml index 3d7862f7..bce0826a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ categories = ["algorithms", "science", "no-std"] license = "MIT OR Apache-2.0" repository = "https://github.com/kaidokert/num-traits" name = "const-num-traits" -version = "0.1.1" +version = "0.1.2" readme = "README.md" # Include-list of the files that ship. Anything not listed here is never packaged. include = [ diff --git a/src/lib.rs b/src/lib.rs index 391cda40..f2df7a9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,7 @@ pub use crate::ops::bits::{ BitWidth, DepositBits, ExtractBits, FunnelShl, FunnelShr, HighestOne, IsolateHighestOne, IsolateLowestOne, LowestOne, ShlExact, ShrExact, UnboundedShl, UnboundedShr, }; +pub use crate::ops::byte_slice::{ByteSliceError, ByteSliceErrorKind, FromByteSlice}; pub use crate::ops::bytes::{FromBytes, ToBytes}; pub use crate::ops::carrying::{BorrowingSub, CarryingAdd, CarryingMul, WideningMul}; pub use crate::ops::checked::{ @@ -177,6 +178,7 @@ pub mod prelude { pub use crate::identities::*; pub use crate::int::*; pub use crate::ops::bits::*; + pub use crate::ops::byte_slice::*; pub use crate::ops::bytes::*; pub use crate::ops::carrying::*; pub use crate::ops::checked::*; diff --git a/src/ops/byte_slice.rs b/src/ops/byte_slice.rs new file mode 100644 index 00000000..eef47d03 --- /dev/null +++ b/src/ops/byte_slice.rs @@ -0,0 +1,210 @@ +//! Parsing an unsigned integer from a byte slice of *arbitrary* length. +//! +//! The input `&[u8]` need not match the target's byte width: shorter input +//! zero-extends, equal is exact, wider is rejected rather than truncated. That +//! reject case is why the parse returns a `Result` — an over-long slice has no +//! lossless reading. +//! +//! Unsigned-only: zero-extension is unambiguous for magnitudes but has no +//! sign-extension analogue, so signed targets are deliberately excluded. +//! +//! **CT tier A (structure-only)** for a fixed-length input: control flow +//! branches only on `bytes.len()` (emptiness, over-width), never on byte +//! *values*, so a constant-length caller is constant-time. + +use core::fmt; + +/// The reason a [`FromByteSlice`] parse failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ByteSliceErrorKind { + /// The input slice was empty. An empty slice is rejected rather than read + /// as `0`, so a truncated or missing buffer can't masquerade as a valid + /// zero value. + Empty, + /// The input had more bytes than the target type can hold. The value is + /// never silently truncated. + Overflow, +} + +/// Error parsing an integer from a byte slice. +/// +/// Crate-owned so it stays constructible on stable — the standard library's +/// integer-parse error is opaque and can't be built outside `core`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ByteSliceError { + /// What went wrong. + pub kind: ByteSliceErrorKind, +} + +impl fmt::Display for ByteSliceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let description = match self.kind { + ByteSliceErrorKind::Empty => "cannot parse integer from empty byte slice", + ByteSliceErrorKind::Overflow => "byte slice too long for target type", + }; + description.fmt(f) + } +} + +c0nst::c0nst! { +/// Parses an unsigned integer from a byte slice of arbitrary length. +/// +/// Input shorter than the target width is zero-extended (leading zeros for +/// big-endian, trailing for little-endian); input wider than the target is +/// rejected with [`ByteSliceErrorKind::Overflow`] — never truncated. An empty +/// slice is [`ByteSliceErrorKind::Empty`], not `0`. +pub c0nst trait FromByteSlice: Sized { + /// Parses from a big-endian byte slice. + /// + /// ``` + /// use const_num_traits::FromByteSlice; + /// + /// assert_eq!(::from_be_slice(&[0x12, 0x34]), Ok(0x1234)); + /// assert_eq!(::from_be_slice(&[1, 2, 3, 4]), Ok(0x0102_0304)); + /// assert!(::from_be_slice(&[1, 2, 3]).is_err()); // too wide + /// assert!(::from_be_slice(&[]).is_err()); // empty + /// ``` + fn from_be_slice(bytes: &[u8]) -> Result; + + /// Parses from a little-endian byte slice. + /// + /// ``` + /// use const_num_traits::FromByteSlice; + /// + /// assert_eq!(::from_le_slice(&[0x34, 0x12]), Ok(0x1234)); + /// assert_eq!(::from_le_slice(&[1, 2, 3, 4]), Ok(0x0403_0201)); + /// ``` + fn from_le_slice(bytes: &[u8]) -> Result; +} +} + +macro_rules! from_byte_slice_impl { + ($($t:ty)*) => {$( + c0nst::c0nst! { + c0nst impl FromByteSlice for $t { + fn from_be_slice(bytes: &[u8]) -> Result { + if bytes.len() == 0 { + return Err(ByteSliceError { kind: ByteSliceErrorKind::Empty }); + } + let width = core::mem::size_of::<$t>(); + if bytes.len() > width { + return Err(ByteSliceError { kind: ByteSliceErrorKind::Overflow }); + } + // right-align into a zero buffer, then reinterpret — avoids the + // `<< 8` shift that would overflow a single-byte accumulator. + let mut buf = [0u8; core::mem::size_of::<$t>()]; + let off = width - bytes.len(); + let mut i = 0; + while i < bytes.len() { + buf[off + i] = bytes[i]; + i += 1; + } + Ok(<$t>::from_be_bytes(buf)) + } + + fn from_le_slice(bytes: &[u8]) -> Result { + if bytes.len() == 0 { + return Err(ByteSliceError { kind: ByteSliceErrorKind::Empty }); + } + let width = core::mem::size_of::<$t>(); + if bytes.len() > width { + return Err(ByteSliceError { kind: ByteSliceErrorKind::Overflow }); + } + let mut buf = [0u8; core::mem::size_of::<$t>()]; + let mut i = 0; + while i < bytes.len() { + buf[i] = bytes[i]; + i += 1; + } + Ok(<$t>::from_le_bytes(buf)) + } + } + } + )*}; +} + +from_byte_slice_impl!(usize u8 u16 u32 u64 u128); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exact_width() { + assert_eq!( + ::from_be_slice(&[1, 2, 3, 4]), + Ok(0x0102_0304) + ); + assert_eq!( + ::from_le_slice(&[1, 2, 3, 4]), + Ok(0x0403_0201) + ); + assert_eq!(::from_be_slice(&[0xff]), Ok(0xff)); + assert_eq!(::from_le_slice(&[0xff]), Ok(0xff)); + } + + #[test] + fn zero_extends_short_input() { + // BE: fewer bytes are the least-significant end + assert_eq!( + ::from_be_slice(&[0x12, 0x34]), + Ok(0x0000_1234) + ); + // LE: fewer bytes are the least-significant end too + assert_eq!( + ::from_le_slice(&[0x34, 0x12]), + Ok(0x0000_1234) + ); + assert_eq!(::from_be_slice(&[1]), Ok(1)); + } + + #[test] + fn round_trips_be_le() { + let v = 0x0102_0304u32; + assert_eq!( + ::from_be_slice(&v.to_be_bytes()), + Ok(v) + ); + assert_eq!( + ::from_le_slice(&v.to_le_bytes()), + Ok(v) + ); + } + + #[test] + fn empty_is_error_not_zero() { + use ByteSliceErrorKind::*; + assert_eq!( + ::from_be_slice(&[]).unwrap_err().kind, + Empty + ); + assert_eq!( + ::from_le_slice(&[]).unwrap_err().kind, + Empty + ); + } + + #[test] + fn too_wide_is_overflow() { + use ByteSliceErrorKind::*; + assert_eq!( + ::from_be_slice(&[1, 2, 3]) + .unwrap_err() + .kind, + Overflow + ); + // over-width even when the surplus high bytes are zero: length-based. + assert_eq!( + ::from_be_slice(&[0, 1, 2]) + .unwrap_err() + .kind, + Overflow + ); + assert_eq!( + ::from_le_slice(&[1, 2]) + .unwrap_err() + .kind, + Overflow + ); + } +} diff --git a/src/ops/mod.rs b/src/ops/mod.rs index c5970c8b..538d4cb5 100644 --- a/src/ops/mod.rs +++ b/src/ops/mod.rs @@ -1,4 +1,5 @@ pub mod bits; +pub mod byte_slice; pub mod bytes; pub mod carrying; pub mod checked; diff --git a/tests/const_nightly.rs b/tests/const_nightly.rs index 7c808385..cb5bc0f5 100644 --- a/tests/const_nightly.rs +++ b/tests/const_nightly.rs @@ -16,7 +16,9 @@ use const_num_traits::{ ShlExact, StrictAdd, StrictEuclid, Truncate, UnboundedShr, UnsignedAbs, Widen, WideningMul, WrappingPow, }; -use const_num_traits::{Algebraic, FloatBits, FromAscii, FromBytes, Maximum, NextUp, ToBytes}; +use const_num_traits::{ + Algebraic, FloatBits, FromAscii, FromByteSlice, FromBytes, Maximum, NextUp, ToBytes, +}; #[test] fn bytes_in_const() { @@ -159,6 +161,10 @@ fn ascii_and_floats_in_const() { Ok(v) => v, Err(_) => panic!("parse failed"), }; + const SLICE: u32 = match ::from_be_slice(&[0x12, 0x34]) { + Ok(v) => v, + Err(_) => panic!("parse failed"), + }; const BITS: u32 = FloatBits::to_bits(1.0f32); const ONE: f32 = FloatBits::from_bits(0x3F80_0000u32); const UP: f32 = NextUp::next_up(1.0f32); @@ -166,6 +172,7 @@ fn ascii_and_floats_in_const() { const ALG: f64 = Algebraic::algebraic_mul(3.0f64, 0.5); assert_eq!(HEX, 0xdead_beef); assert_eq!(NEG, -1234); + assert_eq!(SLICE, 0x1234); assert_eq!(BITS, 0x3F80_0000); assert_eq!(ONE, 1.0); assert_eq!(UP, f32::from_bits(0x3F80_0001));