use std::fmt;
use smallvec::SmallVec;
use crate::engine::frame::{RAWFRAME_MAX_SIZE, RawFrame};
const HEX_NIBBLE: [u8; 256] = {
let mut t = [0xffu8; 256];
let mut i = 0u8;
while i < 10 {
t[(b'0' + i) as usize] = i;
i += 1;
}
let mut i = 0u8;
while i < 6 {
t[(b'a' + i) as usize] = 10 + i;
t[(b'A' + i) as usize] = 10 + i;
i += 1;
}
t
};
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
#[error("line is empty")]
Empty,
#[error("missing timestamp separator")]
MissingTimestamp,
#[error("expected {expected} comma-separated header fields, found {found}")]
#[non_exhaustive]
BadHeader { expected: usize, found: usize },
#[error("malformed integer in field {field}: {value:?}")]
#[non_exhaustive]
BadInteger {
field: &'static str,
value: String,
offset: Option<usize>,
},
#[error("declared length {len} exceeds max {max}", max = RAWFRAME_MAX_SIZE)]
#[non_exhaustive]
LengthTooLarge { len: usize },
#[error("expected {expected} hex bytes, found {found}")]
#[non_exhaustive]
BadPayloadCount { expected: usize, found: usize },
#[error("malformed hex byte {value:?} at index {index}")]
#[non_exhaustive]
BadHexByte {
index: usize,
value: String,
offset: Option<usize>,
},
#[error("line ends after the length field; no payload section")]
MissingPayload,
}
impl ParseError {
pub fn byte_offset(&self) -> Option<usize> {
match self {
ParseError::BadInteger { offset, .. } | ParseError::BadHexByte { offset, .. } => {
*offset
}
_ => None,
}
}
}
pub fn parse_line(line: &str) -> Result<RawFrame, ParseError> {
let line = line.trim_end_matches(['\r', '\n']);
if line.is_empty() {
return Err(ParseError::Empty);
}
let (ts, rest) = line.split_once(',').ok_or(ParseError::MissingTimestamp)?;
let timestamp = if ts.is_empty() {
None
} else {
Some(ts.to_string())
};
let rest_base = ts.len() + 1;
let rest_bytes = rest.as_bytes();
let mut cursor = 0usize;
let prio = take_uint_until_comma::<u8>(rest_bytes, &mut cursor, rest_base, "prio")?;
let pgn = take_uint_until_comma::<u32>(rest_bytes, &mut cursor, rest_base, "pgn")?;
let src = take_uint_until_comma::<u8>(rest_bytes, &mut cursor, rest_base, "src")?;
let dst = take_uint_until_comma::<u8>(rest_bytes, &mut cursor, rest_base, "dst")?;
let len = take_uint_until_comma::<usize>(rest_bytes, &mut cursor, rest_base, "len")?;
if len > RAWFRAME_MAX_SIZE {
return Err(ParseError::LengthTooLarge { len });
}
if cursor == 0 || rest_bytes[cursor - 1] != b',' {
return Err(ParseError::MissingPayload);
}
let payload_start = cursor;
let payload_bytes = &rest_bytes[cursor..];
let mut data: SmallVec<[u8; 8]> = SmallVec::with_capacity(len);
let mut i = 0usize;
let mut decoded = 0usize;
while decoded < len && i < payload_bytes.len() {
while i < payload_bytes.len() && payload_bytes[i] == b' ' {
i += 1;
}
if i + 2 > payload_bytes.len() {
return Err(ParseError::BadHexByte {
index: decoded,
value: String::from_utf8_lossy(&payload_bytes[i..]).into_owned(),
offset: Some(rest_base + payload_start + i),
});
}
let hi = HEX_NIBBLE[payload_bytes[i] as usize];
let lo = HEX_NIBBLE[payload_bytes[i + 1] as usize];
if hi == 0xff || lo == 0xff {
return Err(ParseError::BadHexByte {
index: decoded,
value: String::from_utf8_lossy(&payload_bytes[i..i + 2]).into_owned(),
offset: Some(rest_base + payload_start + i),
});
}
data.push((hi << 4) | lo);
decoded += 1;
i += 2;
while i < payload_bytes.len() && payload_bytes[i] == b' ' {
i += 1;
}
if i < payload_bytes.len() && payload_bytes[i] == b',' {
i += 1;
}
}
if data.len() != len {
return Err(ParseError::BadPayloadCount {
expected: len,
found: data.len(),
});
}
Ok(RawFrame {
timestamp,
prio,
pgn,
src,
dst,
data,
})
}
fn take_uint_until_comma<T>(
bytes: &[u8],
cursor: &mut usize,
base: usize,
field: &'static str,
) -> Result<T, ParseError>
where
T: TryFrom<u64>,
{
while *cursor < bytes.len() && bytes[*cursor] == b' ' {
*cursor += 1;
}
let start = *cursor;
let mut acc: u64 = 0;
let mut saw_digit = false;
while *cursor < bytes.len() {
let b = bytes[*cursor];
if b.is_ascii_digit() {
acc = acc * 10 + (b - b'0') as u64;
saw_digit = true;
*cursor += 1;
continue;
}
break;
}
if !saw_digit {
return Err(ParseError::BadInteger {
field,
value: String::from_utf8_lossy(&bytes[start..*cursor]).into_owned(),
offset: Some(base + start),
});
}
while *cursor < bytes.len() && bytes[*cursor] == b' ' {
*cursor += 1;
}
if *cursor < bytes.len() && bytes[*cursor] == b',' {
*cursor += 1;
}
T::try_from(acc).map_err(|_| ParseError::BadInteger {
field,
value: acc.to_string(),
offset: Some(base + start),
})
}
pub fn write_line<W: fmt::Write>(w: &mut W, frame: &RawFrame) -> fmt::Result {
if let Some(ts) = &frame.timestamp {
w.write_str(&super::normalize_timestamp(ts))?;
}
write!(
w,
",{prio},{pgn},{src},{dst},{len}",
prio = frame.prio,
pgn = frame.pgn,
src = frame.src,
dst = frame.dst,
len = frame.data.len(),
)?;
for b in &frame.data {
write!(w, ",{:02x}", b)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_plain_8byte_frame() {
let line = "2011-04-25-06:25:03.603,3,129029,36,255,8,e6,f1,3a,80,9c,c6,0d,b3";
let f = parse_line(line).unwrap();
assert_eq!(f.timestamp.as_deref(), Some("2011-04-25-06:25:03.603"));
assert_eq!(f.prio, 3);
assert_eq!(f.pgn, 129029);
assert_eq!(f.src, 36);
assert_eq!(f.dst, 255);
assert_eq!(
&f.data[..],
&[0xe6, 0xf1, 0x3a, 0x80, 0x9c, 0xc6, 0x0d, 0xb3]
);
}
#[test]
fn parses_fast_43byte_payload() {
let mut line = String::from("2011-04-25-06:25:03.603,3,129029,36,255,43");
for i in 0..43 {
line.push_str(&format!(",{:02x}", i as u8));
}
let f = parse_line(&line).unwrap();
assert_eq!(f.data.len(), 43);
assert_eq!(f.data[0], 0);
assert_eq!(f.data[42], 42);
}
#[test]
fn writes_round_trip() {
let frame = RawFrame::new(
Some("2011-04-25T06:25:03.603Z".into()),
3,
129029,
36,
255,
vec![0xe6, 0xf1, 0x3a, 0x80, 0x9c, 0xc6, 0x0d, 0xb3],
);
let mut out = String::new();
write_line(&mut out, &frame).unwrap();
let again = parse_line(&out).unwrap();
assert_eq!(again, frame);
}
#[test]
fn write_canonicalises_classic_timestamp() {
let frame = RawFrame::new(
Some("2011-04-25-06:25:03.603".into()),
3,
129029,
36,
255,
vec![0xe6, 0xf1, 0x3a, 0x80, 0x9c, 0xc6, 0x0d, 0xb3],
);
let mut out = String::new();
write_line(&mut out, &frame).unwrap();
assert!(out.starts_with("2011-04-25T06:25:03.603Z,"), "got {out}");
}
#[test]
fn rejects_empty_line() {
assert!(matches!(parse_line(""), Err(ParseError::Empty)));
}
#[test]
fn rejects_oversized_len() {
let line = "ts,3,129029,36,255,1786";
assert!(matches!(
parse_line(line),
Err(ParseError::LengthTooLarge { .. })
));
let line = "ts,3,129029,36,255,250";
assert!(matches!(parse_line(line), Err(ParseError::MissingPayload)));
}
#[test]
fn rejects_a_line_that_stops_after_the_length() {
let line = "2020-08-22T13:52:36.981Z,0,16712965,255,0,0";
assert!(matches!(parse_line(line), Err(ParseError::MissingPayload)));
let ok = parse_line("2020-08-22T13:52:36.981Z,0,127250,255,0,0,").expect("parses");
assert!(ok.data.is_empty());
}
#[test]
fn rejects_short_payload() {
let line = "ts,3,129029,36,255,8,01,02,03";
assert!(matches!(
parse_line(line),
Err(ParseError::BadPayloadCount {
expected: 8,
found: 3
})
));
}
#[test]
fn empty_timestamp_field() {
let line = ",3,129029,36,255,1,ff";
let f = parse_line(line).unwrap();
assert!(f.timestamp.is_none());
assert_eq!(&f.data[..], &[0xff]);
}
}