Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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::*;
Expand Down
210 changes: 210 additions & 0 deletions src/ops/byte_slice.rs
Original file line number Diff line number Diff line change
@@ -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!(<u32 as FromByteSlice>::from_be_slice(&[0x12, 0x34]), Ok(0x1234));
/// assert_eq!(<u32 as FromByteSlice>::from_be_slice(&[1, 2, 3, 4]), Ok(0x0102_0304));
/// assert!(<u16 as FromByteSlice>::from_be_slice(&[1, 2, 3]).is_err()); // too wide
/// assert!(<u32 as FromByteSlice>::from_be_slice(&[]).is_err()); // empty
/// ```
fn from_be_slice(bytes: &[u8]) -> Result<Self, ByteSliceError>;

/// Parses from a little-endian byte slice.
///
/// ```
/// use const_num_traits::FromByteSlice;
///
/// assert_eq!(<u32 as FromByteSlice>::from_le_slice(&[0x34, 0x12]), Ok(0x1234));
/// assert_eq!(<u32 as FromByteSlice>::from_le_slice(&[1, 2, 3, 4]), Ok(0x0403_0201));
/// ```
fn from_le_slice(bytes: &[u8]) -> Result<Self, ByteSliceError>;
}
}

macro_rules! from_byte_slice_impl {
($($t:ty)*) => {$(
c0nst::c0nst! {
c0nst impl FromByteSlice for $t {
fn from_be_slice(bytes: &[u8]) -> Result<Self, ByteSliceError> {
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<Self, ByteSliceError> {
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!(
<u32 as FromByteSlice>::from_be_slice(&[1, 2, 3, 4]),
Ok(0x0102_0304)
);
assert_eq!(
<u32 as FromByteSlice>::from_le_slice(&[1, 2, 3, 4]),
Ok(0x0403_0201)
);
assert_eq!(<u8 as FromByteSlice>::from_be_slice(&[0xff]), Ok(0xff));
assert_eq!(<u8 as FromByteSlice>::from_le_slice(&[0xff]), Ok(0xff));
}

#[test]
fn zero_extends_short_input() {
// BE: fewer bytes are the least-significant end
assert_eq!(
<u32 as FromByteSlice>::from_be_slice(&[0x12, 0x34]),
Ok(0x0000_1234)
);
// LE: fewer bytes are the least-significant end too
assert_eq!(
<u32 as FromByteSlice>::from_le_slice(&[0x34, 0x12]),
Ok(0x0000_1234)
);
assert_eq!(<u128 as FromByteSlice>::from_be_slice(&[1]), Ok(1));
}

#[test]
fn round_trips_be_le() {
let v = 0x0102_0304u32;
assert_eq!(
<u32 as FromByteSlice>::from_be_slice(&v.to_be_bytes()),
Ok(v)
);
assert_eq!(
<u32 as FromByteSlice>::from_le_slice(&v.to_le_bytes()),
Ok(v)
);
}

#[test]
fn empty_is_error_not_zero() {
use ByteSliceErrorKind::*;
assert_eq!(
<u32 as FromByteSlice>::from_be_slice(&[]).unwrap_err().kind,
Empty
);
assert_eq!(
<u32 as FromByteSlice>::from_le_slice(&[]).unwrap_err().kind,
Empty
);
}

#[test]
fn too_wide_is_overflow() {
use ByteSliceErrorKind::*;
assert_eq!(
<u16 as FromByteSlice>::from_be_slice(&[1, 2, 3])
.unwrap_err()
.kind,
Overflow
);
// over-width even when the surplus high bytes are zero: length-based.
assert_eq!(
<u16 as FromByteSlice>::from_be_slice(&[0, 1, 2])
.unwrap_err()
.kind,
Overflow
);
assert_eq!(
<u8 as FromByteSlice>::from_le_slice(&[1, 2])
.unwrap_err()
.kind,
Overflow
);
}
}
1 change: 1 addition & 0 deletions src/ops/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod bits;
pub mod byte_slice;
pub mod bytes;
pub mod carrying;
pub mod checked;
Expand Down
9 changes: 8 additions & 1 deletion tests/const_nightly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -159,13 +161,18 @@ fn ascii_and_floats_in_const() {
Ok(v) => v,
Err(_) => panic!("parse failed"),
};
const SLICE: u32 = match <u32 as FromByteSlice>::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);
const MAX: f64 = Maximum::maximum(1.5f64, 2.5);
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));
Expand Down
Loading