use clap::ValueEnum;
pub(super) 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, ValueEnum)]
pub(super) enum Endian {
Le,
Be,
}
impl Endian {
const fn suffix(self) -> &'static str {
match self {
Self::Le => "le",
Self::Be => "be",
}
}
}
#[derive(Clone)]
pub(super) struct ScalarTypeParser;
impl clap::builder::TypedValueParser for ScalarTypeParser {
type Value = ScalarType;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<ScalarType, clap::Error> {
let redirect = match value.to_str().map(str::to_ascii_lowercase).as_deref() {
Some("ascii" | "string" | "str" | "text" | "utf8") => Some(
"--type takes a fixed-width scalar; for text use `cadmpeg inspect strings`, \
or `cadmpeg inspect find FILE --encoding ascii TEXT` to locate it",
),
Some("hex" | "bytes") => {
Some("--type takes a fixed-width scalar; for a hex dump use `cadmpeg inspect hex`")
}
_ => None,
};
if let Some(message) = redirect {
return Err(clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
format!("{message}\n"),
)
.with_cmd(cmd));
}
clap::builder::EnumValueParser::<ScalarType>::new().parse_ref(cmd, arg, value)
}
fn possible_values(
&self,
) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
Some(Box::new(
clap::ValueEnum::value_variants()
.iter()
.filter_map(|variant: &ScalarType| clap::ValueEnum::to_possible_value(variant)),
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(super) enum ScalarType {
U8,
I8,
U16,
I16,
U32,
I32,
U64,
I64,
F32,
F64,
}
impl ScalarType {
pub(super) const MAX_WIDTH: usize = 8;
pub(super) 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(super) const fn is_single_byte(self) -> bool {
self.width() == 1
}
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(super) 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(super) 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(super) fn window_of(self, buffer: &[u8; ScalarType::MAX_WIDTH]) -> ScalarWindow<'_> {
ScalarWindow {
ty: self,
bytes: &buffer[..self.width()],
}
}
}
#[derive(Debug, Clone, Copy)]
pub(in crate::inspect) struct ScalarWindow<'a> {
ty: ScalarType,
bytes: &'a [u8],
}
impl ScalarWindow<'_> {
pub(super) fn read(self, endian: Endian) -> ScalarValue {
let bytes = self.bytes;
let mut raw = [0u8; ScalarType::MAX_WIDTH];
raw[..bytes.len()].copy_from_slice(bytes);
if endian == Endian::Be {
raw[..bytes.len()].reverse();
}
match self.ty {
ScalarType::U8 => ScalarValue::U8(raw[0]),
ScalarType::I8 => ScalarValue::I8(i8::from_le_bytes([raw[0]])),
ScalarType::U16 => ScalarValue::U16(u16::from_le_bytes([raw[0], raw[1]])),
ScalarType::I16 => ScalarValue::I16(i16::from_le_bytes([raw[0], raw[1]])),
ScalarType::U32 => {
ScalarValue::U32(u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
}
ScalarType::I32 => {
ScalarValue::I32(i32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
}
ScalarType::U64 => ScalarValue::U64(u64::from_le_bytes(raw)),
ScalarType::I64 => ScalarValue::I64(i64::from_le_bytes(raw)),
ScalarType::F32 => {
ScalarValue::F32(f32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
}
ScalarType::F64 => ScalarValue::F64(f64::from_le_bytes(raw)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum ScalarValue {
U8(u8),
I8(i8),
U16(u16),
I16(i16),
U32(u32),
I32(i32),
U64(u64),
I64(i64),
F32(f32),
F64(f64),
}
impl ScalarValue {
pub(super) const fn ty(self) -> ScalarType {
match self {
Self::U8(_) => ScalarType::U8,
Self::I8(_) => ScalarType::I8,
Self::U16(_) => ScalarType::U16,
Self::I16(_) => ScalarType::I16,
Self::U32(_) => ScalarType::U32,
Self::I32(_) => ScalarType::I32,
Self::U64(_) => ScalarType::U64,
Self::I64(_) => ScalarType::I64,
Self::F32(_) => ScalarType::F32,
Self::F64(_) => ScalarType::F64,
}
}
pub(super) fn decimal(self) -> String {
match self {
Self::U8(value) => value.to_string(),
Self::I8(value) => value.to_string(),
Self::U16(value) => value.to_string(),
Self::I16(value) => value.to_string(),
Self::U32(value) => value.to_string(),
Self::I32(value) => value.to_string(),
Self::U64(value) => value.to_string(),
Self::I64(value) => value.to_string(),
Self::F32(value) => format!("{:?}", f64::from(value)),
Self::F64(value) => format!("{value:?}"),
}
}
pub(super) fn hex(self) -> String {
let bits = match self {
Self::U8(value) => u64::from(value),
Self::I8(value) => u64::from(value.cast_unsigned()),
Self::U16(value) => u64::from(value),
Self::I16(value) => u64::from(value.cast_unsigned()),
Self::U32(value) => u64::from(value),
Self::I32(value) => u64::from(value.cast_unsigned()),
Self::U64(value) => value,
Self::I64(value) => value.cast_unsigned(),
Self::F32(value) => u64::from(value.to_bits()),
Self::F64(value) => value.to_bits(),
};
let digits = match self {
Self::U8(_) | Self::I8(_) => 2,
Self::U16(_) | Self::I16(_) => 4,
Self::U32(_) | Self::I32(_) | Self::F32(_) => 8,
Self::U64(_) | Self::I64(_) | Self::F64(_) => 16,
};
format!("0x{bits:0digits$x}")
}
}
#[cfg(test)]
mod tests {
use super::{parse_offset, Endian, ScalarType, ScalarValue};
#[track_caller]
fn read(ty: ScalarType, bytes: &[u8], endian: Endian) -> ScalarValue {
let mut raw = [0u8; ScalarType::MAX_WIDTH];
raw[..bytes.len()].copy_from_slice(bytes);
ty.window_of(&raw).read(endian)
}
#[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!(
read(ScalarType::U16, &bytes, Endian::Be),
ScalarValue::U16(258)
);
assert_eq!(
read(ScalarType::U16, &bytes, Endian::Le),
ScalarValue::U16(513)
);
}
#[test]
fn signed_reads_sign_extend_at_each_width() {
assert_eq!(
read(ScalarType::I8, &[0xff], Endian::Le),
ScalarValue::I8(-1)
);
assert_eq!(
read(ScalarType::I16, &[0x00, 0x80], Endian::Le),
ScalarValue::I16(-32768)
);
assert_eq!(
read(ScalarType::I32, &[0xff, 0xff, 0xff, 0xff], Endian::Be),
ScalarValue::I32(-1)
);
assert_eq!(
read(ScalarType::I64, &[0, 0, 0, 0, 0, 0, 0, 0x80], Endian::Le),
ScalarValue::I64(i64::MIN)
);
}
#[test]
fn float_reads_match_hand_built_bit_patterns() {
let one_point_five = 0x3ff8_0000_0000_0000u64.to_le_bytes();
assert_eq!(
read(ScalarType::F64, &one_point_five, Endian::Le),
ScalarValue::F64(1.5)
);
let minus_two = 0xc000_0000u32.to_be_bytes();
assert_eq!(
read(ScalarType::F32, &minus_two, Endian::Be),
ScalarValue::F32(-2.0)
);
}
#[test]
fn hex_rendering_pads_to_the_type_width() {
assert_eq!(ScalarValue::U32(1).hex(), "0x00000001");
assert_eq!(ScalarValue::I16(-1).hex(), "0xffff");
assert_eq!(ScalarValue::I64(-1).hex(), "0xffffffffffffffff");
assert_eq!(ScalarValue::F64(1.5).hex(), "0x3ff8000000000000");
assert_eq!(ScalarValue::F32(-2.0).hex(), "0xc0000000");
}
#[test]
fn display_names_carry_a_suffix_only_for_wide_types() {
assert_eq!(ScalarType::U8.display_name(Endian::Be), "u8");
assert_eq!(ScalarType::F64.display_name(Endian::Be), "f64be");
}
}