use clap::{Args, ValueEnum};
pub fn parse_offset(text: &str) -> Result<u64, String> {
let trimmed = text.trim();
if trimmed.is_empty() {
return Err("expected a byte offset, found an empty value".to_string());
}
let (digits, radix) = match trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
Some(rest) => (rest, 16),
None => (trimmed, 10),
};
let cleaned: String = digits.chars().filter(|c| *c != '_').collect();
if cleaned.is_empty() {
return Err(format!("{trimmed}: no digits after the radix prefix"));
}
if !cleaned.chars().all(|c| c.is_digit(radix)) {
let kind = if radix == 16 {
"hexadecimal"
} else {
"decimal"
};
return Err(format!("{trimmed}: not a {kind} byte offset"));
}
u64::from_str_radix(&cleaned, radix).map_err(|_| format!("{trimmed}: does not fit in 64 bits"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Endian {
Little,
Big,
}
impl Endian {
pub const fn suffix(self) -> &'static str {
match self {
Self::Little => "le",
Self::Big => "be",
}
}
}
#[derive(Debug, Clone, Args)]
pub struct EndianArgs {
#[arg(long, conflicts_with = "be")]
le: bool,
#[arg(long)]
be: bool,
}
impl EndianArgs {
pub const fn endian(&self) -> Endian {
if self.be {
Endian::Big
} else {
Endian::Little
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ScalarType {
U8,
I8,
U16,
I16,
U32,
I32,
U64,
I64,
F32,
F64,
}
impl ScalarType {
pub const fn width(self) -> usize {
match self {
Self::U8 | Self::I8 => 1,
Self::U16 | Self::I16 => 2,
Self::U32 | Self::I32 | Self::F32 => 4,
Self::U64 | Self::I64 | Self::F64 => 8,
}
}
pub const fn is_single_byte(self) -> bool {
self.width() == 1
}
pub const fn base_name(self) -> &'static str {
match self {
Self::U8 => "u8",
Self::I8 => "i8",
Self::U16 => "u16",
Self::I16 => "i16",
Self::U32 => "u32",
Self::I32 => "i32",
Self::U64 => "u64",
Self::I64 => "i64",
Self::F32 => "f32",
Self::F64 => "f64",
}
}
pub fn display_name(self, endian: Endian) -> String {
if self.is_single_byte() {
self.base_name().to_string()
} else {
format!("{}{}", self.base_name(), endian.suffix())
}
}
pub fn from_base_name(name: &str) -> Option<Self> {
[
Self::U8,
Self::I8,
Self::U16,
Self::I16,
Self::U32,
Self::I32,
Self::U64,
Self::I64,
Self::F32,
Self::F64,
]
.into_iter()
.find(|candidate| candidate.base_name() == name)
}
pub fn read(self, bytes: &[u8], endian: Endian) -> ScalarValue {
assert_eq!(
bytes.len(),
self.width(),
"scalar read needs an exact slice"
);
let mut raw = [0u8; 8];
raw[..bytes.len()].copy_from_slice(bytes);
if endian == Endian::Big {
raw[..bytes.len()].reverse();
}
let bits = u64::from_le_bytes(raw);
match self {
Self::U8 | Self::U16 | Self::U32 | Self::U64 => ScalarValue::Unsigned(bits),
Self::I8 | Self::I16 | Self::I32 | Self::I64 => {
let shift = 64 - self.width() * 8;
ScalarValue::Signed(((bits << shift) as i64) >> shift)
}
Self::F32 => ScalarValue::Float(f64::from(f32::from_bits(bits as u32))),
Self::F64 => ScalarValue::Float(f64::from_bits(bits)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScalarValue {
Unsigned(u64),
Signed(i64),
Float(f64),
}
impl ScalarValue {
pub fn decimal(self) -> String {
match self {
Self::Unsigned(value) => value.to_string(),
Self::Signed(value) => value.to_string(),
Self::Float(value) => format!("{value:?}"),
}
}
pub fn hex(self, ty: ScalarType) -> String {
let digits = ty.width() * 2;
let bits = match self {
Self::Unsigned(value) => value,
Self::Signed(value) => (value as u64) & mask(ty.width()),
Self::Float(value) => {
if ty == ScalarType::F32 {
u64::from((value as f32).to_bits())
} else {
value.to_bits()
}
}
};
format!("0x{bits:0digits$x}")
}
}
const fn mask(width: usize) -> u64 {
if width >= 8 {
u64::MAX
} else {
(1u64 << (width * 8)) - 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_offset_reads_both_radixes() {
assert_eq!(parse_offset("0"), Ok(0));
assert_eq!(parse_offset("4096"), Ok(4096));
assert_eq!(parse_offset("0x1000"), Ok(4096));
assert_eq!(parse_offset("0X1000"), Ok(4096));
assert_eq!(parse_offset(" 0x1_0000 "), Ok(65536));
assert_eq!(parse_offset("1_000"), Ok(1000));
}
#[test]
fn parse_offset_rejects_garbage() {
for bad in ["", " ", "0x", "-1", "+1", "12g", "0xzz", "1.5", "0b101"] {
assert!(parse_offset(bad).is_err(), "{bad} should not parse");
}
}
#[test]
fn parse_offset_rejects_overflow() {
assert_eq!(parse_offset("0xffffffffffffffff"), Ok(u64::MAX));
assert!(parse_offset("0x1_0000_0000_0000_0000").is_err());
}
#[test]
fn unsigned_reads_follow_byte_order() {
let bytes = [0x01, 0x02];
assert_eq!(
ScalarType::U16.read(&bytes, Endian::Big),
ScalarValue::Unsigned(258)
);
assert_eq!(
ScalarType::U16.read(&bytes, Endian::Little),
ScalarValue::Unsigned(513)
);
}
#[test]
fn signed_reads_sign_extend_at_each_width() {
assert_eq!(
ScalarType::I8.read(&[0xff], Endian::Little),
ScalarValue::Signed(-1)
);
assert_eq!(
ScalarType::I16.read(&[0x00, 0x80], Endian::Little),
ScalarValue::Signed(-32768)
);
assert_eq!(
ScalarType::I32.read(&[0xff, 0xff, 0xff, 0xff], Endian::Big),
ScalarValue::Signed(-1)
);
assert_eq!(
ScalarType::I64.read(&[0, 0, 0, 0, 0, 0, 0, 0x80], Endian::Little),
ScalarValue::Signed(i64::MIN)
);
}
#[test]
fn float_reads_match_hand_built_bit_patterns() {
let one_point_five = 0x3ff8_0000_0000_0000u64.to_le_bytes();
assert_eq!(
ScalarType::F64.read(&one_point_five, Endian::Little),
ScalarValue::Float(1.5)
);
let minus_two = 0xc000_0000u32.to_be_bytes();
assert_eq!(
ScalarType::F32.read(&minus_two, Endian::Big),
ScalarValue::Float(-2.0)
);
}
#[test]
fn hex_rendering_pads_to_the_type_width() {
assert_eq!(ScalarValue::Unsigned(1).hex(ScalarType::U32), "0x00000001");
assert_eq!(ScalarValue::Signed(-1).hex(ScalarType::I16), "0xffff");
assert_eq!(
ScalarValue::Signed(-1).hex(ScalarType::I64),
"0xffffffffffffffff"
);
assert_eq!(
ScalarValue::Float(1.5).hex(ScalarType::F64),
"0x3ff8000000000000"
);
assert_eq!(ScalarValue::Float(-2.0).hex(ScalarType::F32), "0xc0000000");
}
#[test]
fn display_names_carry_a_suffix_only_for_wide_types() {
assert_eq!(ScalarType::U8.display_name(Endian::Big), "u8");
assert_eq!(ScalarType::F64.display_name(Endian::Big), "f64be");
}
}