use std::fmt::Write as _;
use super::numeric::{Endian, ScalarType};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub 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)]
pub enum FieldKind {
Scalar(ScalarType, Endian),
Bytes(usize),
Pad(usize),
}
impl FieldKind {
pub const fn width(self) -> usize {
match self {
Self::Scalar(ty, _) => ty.width(),
Self::Bytes(count) | Self::Pad(count) => count,
}
}
pub fn type_name(self) -> String {
match self {
Self::Scalar(ty, endian) => ty.display_name(endian),
Self::Bytes(count) => format!("bytes{count}"),
Self::Pad(count) => format!("pad{count}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub name: String,
pub kind: FieldKind,
pub offset: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Layout {
pub fields: Vec<Field>,
pub size: usize,
}
impl Layout {
pub fn parse(spec: &str) -> Result<Self, LayoutError> {
if spec.trim().is_empty() {
return Err(LayoutError::EmptySpec);
}
let mut fields = Vec::new();
let mut offset: usize = 0;
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 kind = parse_kind(type_text, token)?;
if matches!(kind, FieldKind::Pad(_)) && name.is_some() {
return Err(LayoutError::NamedPad {
token: token.to_string(),
});
}
let name = name.unwrap_or_else(|| format!("f{index}"));
fields.push(Field { name, kind, offset });
offset = offset
.checked_add(kind.width())
.ok_or_else(|| LayoutError::SizeOverflow {
token: token.to_string(),
})?;
}
Ok(Self {
fields,
size: offset,
})
}
pub fn decode(&self, record: &[u8]) -> Vec<DecodedField> {
assert_eq!(
record.len(),
self.size,
"record slice must match the layout"
);
self.fields
.iter()
.filter(|field| !matches!(field.kind, FieldKind::Pad(_)))
.map(|field| {
let bytes = &record[field.offset..field.offset + field.kind.width()];
let (decimal, hex) = match field.kind {
FieldKind::Scalar(ty, endian) => {
let value = ty.read(bytes, endian);
(value.decimal(), value.hex(ty))
}
FieldKind::Bytes(_) => (String::new(), hex_bytes(bytes)),
FieldKind::Pad(_) => unreachable!("pad fields are filtered out"),
};
DecodedField {
name: field.name.clone(),
type_name: field.kind.type_name(),
offset: field.offset,
decimal,
hex,
}
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedField {
pub name: String,
pub type_name: String,
pub offset: usize,
pub decimal: String,
pub hex: String,
}
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(count) = type_text.strip_prefix("pad") {
return parse_count(count, token, "pad").map(FieldKind::Pad);
}
if let Some(ty) = ScalarType::from_base_name(type_text) {
if ty.is_single_byte() {
return Ok(FieldKind::Scalar(ty, Endian::Little));
}
return Err(LayoutError::MissingByteOrder {
token: token.to_string(),
base: type_text.to_string(),
});
}
for (suffix, endian) in [("le", Endian::Little), ("be", Endian::Big)] {
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<usize, 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(),
})?;
if count == 0 {
return Err(LayoutError::ZeroCount {
token: token.to_string(),
keyword,
});
}
Ok(count)
}
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(' ');
}
let _ = write!(out, "{byte:02x}");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(spec: &str) -> Vec<FieldKind> {
Layout::parse(spec)
.unwrap()
.fields
.into_iter()
.map(|field| field.kind)
.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, 4 + 4 + 8 + 8 + 4);
let names: Vec<&str> = layout
.fields
.iter()
.map(|field| field.name.as_str())
.collect();
assert_eq!(names, ["count", "f1", "x", "y", "tag"]);
let offsets: Vec<usize> = layout.fields.iter().map(|field| field.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::Little),
FieldKind::Scalar(ScalarType::I8, Endian::Little),
]
);
assert_eq!(
kinds("u16be,i64le,f32be,f64le"),
[
FieldKind::Scalar(ScalarType::U16, Endian::Big),
FieldKind::Scalar(ScalarType::I64, Endian::Little),
FieldKind::Scalar(ScalarType::F32, Endian::Big),
FieldKind::Scalar(ScalarType::F64, Endian::Little),
]
);
}
#[test]
fn tolerates_whitespace_around_tokens_and_names() {
let layout = Layout::parse(" u32le : count , f64be:x ").unwrap();
assert_eq!(layout.size, 12);
assert_eq!(layout.fields[0].name, "count");
assert_eq!(layout.fields[1].name, "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, 10);
let record = [0x2a, 0x00, 0x00, 0x00, 0xee, 0xee, 0xff, 0xfe, 0xab, 0xcd];
let decoded = layout.decode(&record);
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].name, "count");
assert_eq!(decoded[0].type_name, "u32le");
assert_eq!(decoded[0].decimal, "42");
assert_eq!(decoded[0].hex, "0x0000002a");
assert_eq!(decoded[1].name, "delta");
assert_eq!(decoded[1].decimal, "-2");
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");
assert_eq!(decoded[2].decimal, "");
}
}