use std::num::NonZeroUsize;
use super::numeric::{Endian, ScalarType, ScalarValue};
const HEX_DIGITS: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
pub(super) fn push_hex(out: &mut String, byte: u8) {
out.push(HEX_DIGITS[usize::from(byte >> 4)]);
out.push(HEX_DIGITS[usize::from(byte & 0x0f)]);
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub(in crate::inspect) enum LayoutError {
#[error("empty layout; expected fields such as `u32le:count,f64le:x`")]
EmptySpec,
#[error("layout field {index} is empty; remove the stray comma")]
EmptyField {
index: usize,
},
#[error("layout field {index} (`{token}`) has a `:` with no name after it")]
EmptyName {
index: usize,
token: String,
},
#[error("`{token}`: a field name cannot contain `:`")]
NameHasColon {
token: String,
},
#[error("`{token}`: `{base}` needs a byte-order suffix, as in `{base}le` or `{base}be`")]
MissingByteOrder {
token: String,
base: String,
},
#[error("`{token}`: `{base}` is one byte wide, so it takes no `{suffix}` suffix")]
ForbiddenByteOrder {
token: String,
base: String,
suffix: &'static str,
},
#[error(
"`{token}`: unknown field type `{type_text}`; expected u8/i8, \
u16/i16/u32/i32/u64/i64/f32/f64 with an `le` or `be` suffix, `bytesN`, or `padN`"
)]
UnknownType {
token: String,
type_text: String,
},
#[error("`{token}`: `{keyword}` needs a byte count, as in `{keyword}4`")]
MissingCount {
token: String,
keyword: &'static str,
},
#[error("`{token}`: `{digits}` is not a decimal byte count for `{keyword}`")]
BadCount {
token: String,
keyword: &'static str,
digits: String,
},
#[error("`{token}`: a `{keyword}` field must cover at least one byte")]
ZeroCount {
token: String,
keyword: &'static str,
},
#[error("`{token}`: a pad field is never printed, so it takes no name")]
NamedPad {
token: String,
},
#[error("`{token}`: record size overflows a machine word")]
SizeOverflow {
token: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FieldKind {
Scalar(ScalarType, Endian),
Bytes(NonZeroUsize),
}
impl FieldKind {
const fn width(self) -> usize {
match self {
Self::Scalar(ty, _) => ty.width(),
Self::Bytes(count) => count.get(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Field {
Named { name: String, kind: FieldKind },
Pad(NonZeroUsize),
}
impl Field {
fn width(&self) -> usize {
match self {
Self::Named { kind, .. } => kind.width(),
Self::Pad(count) => count.get(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Layout {
fields: Vec<Field>,
size: NonZeroUsize,
}
impl Layout {
pub(super) fn parse(spec: &str) -> Result<Self, LayoutError> {
if spec.trim().is_empty() {
return Err(LayoutError::EmptySpec);
}
let mut fields = Vec::new();
let mut size = 0usize;
for (index, raw_token) in spec.split(',').enumerate() {
let token = raw_token.trim();
if token.is_empty() {
return Err(LayoutError::EmptyField { index });
}
let (type_text, name) = split_name(token, index)?;
let field = if let Some(count) = type_text.strip_prefix("pad") {
let count = parse_count(count, token, "pad")?;
if name.is_some() {
return Err(LayoutError::NamedPad {
token: token.to_string(),
});
}
Field::Pad(count)
} else {
Field::Named {
kind: parse_kind(type_text, token)?,
name: name.unwrap_or_else(|| format!("f{index}")),
}
};
size = size
.checked_add(field.width())
.ok_or_else(|| LayoutError::SizeOverflow {
token: token.to_string(),
})?;
fields.push(field);
}
let Some(size) = NonZeroUsize::new(size) else {
return Err(LayoutError::EmptySpec);
};
Ok(Self { fields, size })
}
pub(super) const fn size(&self) -> NonZeroUsize {
self.size
}
pub(super) fn split<'a>(&'a self, bytes: &'a [u8]) -> impl Iterator<Item = Record<'a>> {
bytes.chunks_exact(self.size.get()).map(|bytes| Record {
layout: self,
bytes,
})
}
pub(super) fn names(&self) -> impl Iterator<Item = &str> {
self.fields.iter().filter_map(|field| match field {
Field::Named { name, .. } => Some(name.as_str()),
Field::Pad(_) => None,
})
}
fn fields_with_offsets(&self) -> impl Iterator<Item = (usize, &Field)> {
self.fields.iter().scan(0, |offset, field| {
let start = *offset;
*offset += field.width();
Some((start, field))
})
}
}
#[derive(Debug, Clone, Copy)]
pub(in crate::inspect) struct Record<'a> {
layout: &'a Layout,
bytes: &'a [u8],
}
impl<'a> Record<'a> {
pub(super) fn fields(&self) -> impl Iterator<Item = DecodedField<'a>> {
let bytes = self.bytes;
self.layout
.fields_with_offsets()
.filter_map(move |(offset, field)| {
let Field::Named { name, kind } = field else {
return None;
};
let value = match *kind {
FieldKind::Scalar(ty, endian) => {
let width = ty.width();
let mut raw = [0u8; ScalarType::MAX_WIDTH];
raw[..width].copy_from_slice(&bytes[offset..offset + width]);
DecodedValue::Scalar {
value: ty.window_of(&raw).read(endian),
endian,
}
}
FieldKind::Bytes(count) => DecodedValue::Bytes(RawBytes {
record: bytes,
offset,
count,
}),
};
Some(DecodedField {
name,
offset,
value,
})
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::inspect) struct RawBytes<'a> {
record: &'a [u8],
offset: usize,
count: NonZeroUsize,
}
impl<'a> RawBytes<'a> {
fn as_slice(self) -> &'a [u8] {
&self.record[self.offset..self.offset + self.count.get()]
}
const fn len(self) -> NonZeroUsize {
self.count
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum DecodedValue<'a> {
Scalar {
value: ScalarValue,
endian: Endian,
},
Bytes(RawBytes<'a>),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(in crate::inspect) struct DecodedField<'a> {
name: &'a str,
offset: usize,
value: DecodedValue<'a>,
}
impl<'a> DecodedField<'a> {
pub(super) const fn name(&self) -> &'a str {
self.name
}
pub(super) const fn offset(&self) -> usize {
self.offset
}
pub(super) const fn value(&self) -> DecodedValue<'a> {
self.value
}
pub(super) fn type_name(&self) -> String {
match self.value {
DecodedValue::Scalar { value, endian } => value.ty().display_name(endian),
DecodedValue::Bytes(raw) => format!("bytes{}", raw.len()),
}
}
pub(super) fn hex(&self) -> String {
match self.value {
DecodedValue::Scalar { value, .. } => value.hex(),
DecodedValue::Bytes(raw) => hex_bytes(raw.as_slice()),
}
}
}
fn split_name(token: &str, index: usize) -> Result<(&str, Option<String>), LayoutError> {
match token.split_once(':') {
None => Ok((token, None)),
Some((type_text, name)) => {
let name = name.trim();
if name.is_empty() {
return Err(LayoutError::EmptyName {
index,
token: token.to_string(),
});
}
if name.contains(':') {
return Err(LayoutError::NameHasColon {
token: token.to_string(),
});
}
Ok((type_text.trim(), Some(name.to_string())))
}
}
}
fn parse_kind(type_text: &str, token: &str) -> Result<FieldKind, LayoutError> {
if let Some(count) = type_text.strip_prefix("bytes") {
return parse_count(count, token, "bytes").map(FieldKind::Bytes);
}
if let Some(ty) = ScalarType::from_base_name(type_text) {
if ty.is_single_byte() {
return Ok(FieldKind::Scalar(ty, Endian::Le));
}
return Err(LayoutError::MissingByteOrder {
token: token.to_string(),
base: type_text.to_string(),
});
}
for (suffix, endian) in [("le", Endian::Le), ("be", Endian::Be)] {
let Some(base) = type_text.strip_suffix(suffix) else {
continue;
};
let Some(ty) = ScalarType::from_base_name(base) else {
continue;
};
if ty.is_single_byte() {
return Err(LayoutError::ForbiddenByteOrder {
token: token.to_string(),
base: base.to_string(),
suffix,
});
}
return Ok(FieldKind::Scalar(ty, endian));
}
Err(LayoutError::UnknownType {
token: token.to_string(),
type_text: type_text.to_string(),
})
}
fn parse_count(
digits: &str,
token: &str,
keyword: &'static str,
) -> Result<NonZeroUsize, LayoutError> {
if digits.is_empty() {
return Err(LayoutError::MissingCount {
token: token.to_string(),
keyword,
});
}
let count: usize = digits.parse().map_err(|_| LayoutError::BadCount {
token: token.to_string(),
keyword,
digits: digits.to_string(),
})?;
NonZeroUsize::new(count).ok_or_else(|| LayoutError::ZeroCount {
token: token.to_string(),
keyword,
})
}
fn hex_bytes(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 3);
for (index, byte) in bytes.iter().enumerate() {
if index > 0 {
out.push(' ');
}
push_hex(&mut out, *byte);
}
out
}
#[cfg(test)]
mod tests {
use super::super::numeric::{Endian, ScalarType, ScalarValue};
use super::{DecodedValue, Field, FieldKind, Layout, LayoutError};
#[test]
fn padding_does_not_widen_printed_name_column() {
let layout = Layout::parse("u8:x,pad4,u8:y").unwrap();
assert_eq!(layout.names().map(str::len).max(), Some(1));
assert_eq!(layout.names().collect::<Vec<_>>(), ["x", "y"]);
}
fn kinds(spec: &str) -> Vec<FieldKind> {
Layout::parse(spec)
.unwrap()
.fields
.into_iter()
.filter_map(|field| match field {
Field::Named { kind, .. } => Some(kind),
Field::Pad(_) => None,
})
.collect()
}
#[test]
fn parses_the_documented_example() {
let layout = Layout::parse("u32le:count,pad4,f64le:x,f64le:y,bytes4:tag").unwrap();
assert_eq!(layout.size().get(), 4 + 4 + 8 + 8 + 4);
let names: Vec<&str> = layout.names().collect();
assert_eq!(names, ["count", "x", "y", "tag"]);
let offsets: Vec<usize> = layout
.fields_with_offsets()
.map(|(offset, _)| offset)
.collect();
assert_eq!(offsets, [0, 4, 8, 16, 24]);
}
#[test]
fn accepts_every_scalar_width_and_both_byte_orders() {
assert_eq!(
kinds("u8,i8"),
[
FieldKind::Scalar(ScalarType::U8, Endian::Le),
FieldKind::Scalar(ScalarType::I8, Endian::Le),
]
);
assert_eq!(
kinds("u16be,i64le,f32be,f64le"),
[
FieldKind::Scalar(ScalarType::U16, Endian::Be),
FieldKind::Scalar(ScalarType::I64, Endian::Le),
FieldKind::Scalar(ScalarType::F32, Endian::Be),
FieldKind::Scalar(ScalarType::F64, Endian::Le),
]
);
}
#[test]
fn tolerates_whitespace_around_tokens_and_names() {
let layout = Layout::parse(" u32le : count , f64be:x ").unwrap();
assert_eq!(layout.size().get(), 12);
assert_eq!(layout.names().collect::<Vec<_>>(), ["count", "x"]);
}
#[track_caller]
fn error(spec: &str) -> LayoutError {
Layout::parse(spec).expect_err("spec should not parse")
}
#[test]
fn rejects_empty_specs_and_stray_commas() {
assert_eq!(error(""), LayoutError::EmptySpec);
assert_eq!(error(" "), LayoutError::EmptySpec);
assert_eq!(error(","), LayoutError::EmptyField { index: 0 });
assert_eq!(error(",u32le"), LayoutError::EmptyField { index: 0 });
assert_eq!(error("u32le,"), LayoutError::EmptyField { index: 1 });
assert_eq!(error("u32le,,f64le"), LayoutError::EmptyField { index: 1 });
}
#[test]
fn rejects_a_colon_with_no_name() {
assert_eq!(
error("u32le:"),
LayoutError::EmptyName {
index: 0,
token: "u32le:".to_string(),
}
);
assert_eq!(
error("f64le,u32le: "),
LayoutError::EmptyName {
index: 1,
token: "u32le:".to_string(),
}
);
assert_eq!(
error("u32le:a:b"),
LayoutError::NameHasColon {
token: "u32le:a:b".to_string(),
}
);
}
#[test]
fn rejects_missing_or_forbidden_byte_order_suffixes() {
assert_eq!(
error("u32"),
LayoutError::MissingByteOrder {
token: "u32".to_string(),
base: "u32".to_string(),
}
);
assert_eq!(
error("u8le"),
LayoutError::ForbiddenByteOrder {
token: "u8le".to_string(),
base: "u8".to_string(),
suffix: "le",
}
);
assert_eq!(
error("i8be"),
LayoutError::ForbiddenByteOrder {
token: "i8be".to_string(),
base: "i8".to_string(),
suffix: "be",
}
);
}
#[test]
fn rejects_unknown_types() {
for bad in ["u24le", "float64", "u32me"] {
assert!(
matches!(error(bad), LayoutError::UnknownType { .. }),
"{bad:?} should be an unknown type, got {:?}",
error(bad)
);
}
}
#[test]
fn rejects_bad_bytes_and_pad_counts() {
assert_eq!(
error("bytes"),
LayoutError::MissingCount {
token: "bytes".to_string(),
keyword: "bytes",
}
);
assert_eq!(
error("bytes0"),
LayoutError::ZeroCount {
token: "bytes0".to_string(),
keyword: "bytes",
}
);
assert_eq!(
error("pad0"),
LayoutError::ZeroCount {
token: "pad0".to_string(),
keyword: "pad",
}
);
assert_eq!(
error("padx"),
LayoutError::BadCount {
token: "padx".to_string(),
keyword: "pad",
digits: "x".to_string(),
}
);
assert!(matches!(error("bytes-1"), LayoutError::BadCount { .. }));
}
#[test]
fn rejects_a_named_pad() {
assert_eq!(
error("pad4:reserved"),
LayoutError::NamedPad {
token: "pad4:reserved".to_string(),
}
);
}
#[test]
fn decode_reads_hand_built_bytes_and_drops_pad() {
let layout = Layout::parse("u32le:count,pad2,i16be:delta,bytes2:tag").unwrap();
assert_eq!(layout.size().get(), 10);
let record = [0x2a, 0x00, 0x00, 0x00, 0xee, 0xee, 0xff, 0xfe, 0xab, 0xcd];
let records: Vec<_> = layout.split(&record).collect();
assert_eq!(records.len(), 1);
let decoded: Vec<_> = records[0].fields().collect();
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].name(), "count");
assert_eq!(decoded[0].type_name(), "u32le");
assert_eq!(
decoded[0].value(),
DecodedValue::Scalar {
value: ScalarValue::U32(42),
endian: Endian::Le,
}
);
assert_eq!(decoded[0].hex(), "0x0000002a");
assert_eq!(decoded[1].name(), "delta");
assert_eq!(
decoded[1].value(),
DecodedValue::Scalar {
value: ScalarValue::I16(-2),
endian: Endian::Be,
}
);
assert_eq!(decoded[1].hex(), "0xfffe");
assert_eq!(decoded[1].offset(), 6);
assert_eq!(decoded[2].type_name(), "bytes2");
assert_eq!(decoded[2].hex(), "ab cd");
let DecodedValue::Bytes(raw) = decoded[2].value() else {
panic!("the tag field is a raw byte run");
};
assert_eq!(raw.as_slice(), &[0xab, 0xcd]);
assert_eq!(raw.len().get(), 2);
}
#[test]
fn split_drops_a_trailing_partial_record() {
let layout = Layout::parse("u16le:a").unwrap();
let starts: Vec<usize> = layout
.split(&[1, 2, 3, 4, 5])
.map(|record| record.fields().next().expect("one named field").offset())
.collect();
assert_eq!(starts, [0, 0]);
assert_eq!(layout.split(&[1]).count(), 0);
}
}