use smallvec::SmallVec;
use crate::engine::format::common::iso11783_decompose;
use crate::engine::format::plain::ParseError;
use crate::engine::frame::{FASTPACKET_MAX_SIZE, RawFrame};
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 (timestamp, rest) = line.split_once(" - ").ok_or(ParseError::BadHeader {
expected: 3,
found: 1,
})?;
let mut toks = rest.split_whitespace();
let _pgn_tok = toks.next().ok_or(ParseError::BadHeader {
expected: 3,
found: 2,
})?;
let canid_tok = toks.next().ok_or(ParseError::BadHeader {
expected: 3,
found: 3,
})?;
let canid = u32::from_str_radix(canid_tok, 16).map_err(|_| ParseError::BadInteger {
field: "canid",
value: canid_tok.to_string(),
offset: None,
})?;
let (prio, pgn, src, dst) = iso11783_decompose(canid);
let mut data: SmallVec<[u8; 8]> = SmallVec::new();
for raw in toks {
for tok in raw.split(',').filter(|s| !s.is_empty()) {
if data.len() >= FASTPACKET_MAX_SIZE {
break;
}
let b = u8::from_str_radix(tok, 16).map_err(|_| ParseError::BadHexByte {
index: data.len(),
value: tok.to_string(),
offset: None,
})?;
data.push(b);
}
}
Ok(RawFrame {
timestamp: Some(timestamp.to_string()),
prio,
pgn,
src,
dst,
data,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_airmar_line() {
let line = "20:11:00 - 130306 09FD020D FF 8B 72 FF FF FF FF FF";
let f = parse_line(line).unwrap();
assert_eq!(f.timestamp.as_deref(), Some("20:11:00"));
assert_eq!(f.prio, 2);
assert_eq!(f.pgn, 130306);
assert_eq!(f.src, 0x0d);
assert_eq!(f.dst, 0xff);
assert_eq!(
f.data.as_slice(),
&[0xff, 0x8b, 0x72, 0xff, 0xff, 0xff, 0xff, 0xff]
);
}
#[test]
fn the_can_id_wins_over_the_pgn_column() {
let f = parse_line("20:11:00 - 999999 09FD020D FF 8B").unwrap();
assert_eq!(f.pgn, 130306);
}
#[test]
fn single_digit_hex_bytes_parse() {
let f = parse_line("00:00:00 - 0 09FD020D 1 F 0A").unwrap();
assert_eq!(f.data.as_slice(), &[0x01, 0x0f, 0x0a]);
}
#[test]
fn mixed_space_and_comma_separators_parse() {
let f = parse_line("00:00:00 - 0 09FD020D FF,8B 72,FF").unwrap();
assert_eq!(f.data.as_slice(), &[0xff, 0x8b, 0x72, 0xff]);
}
#[test]
fn empty_tokens_are_skipped() {
let f = parse_line("00:00:00 - 0 09FD020D FF,,8B,").unwrap();
assert_eq!(f.data.as_slice(), &[0xff, 0x8b]);
}
#[test]
fn the_timestamp_is_taken_verbatim() {
let f = parse_line("2015-06-21 20:11:00.123 - 0 09FD020D FF").unwrap();
assert_eq!(f.timestamp.as_deref(), Some("2015-06-21 20:11:00.123"));
}
#[test]
fn a_line_with_no_data_bytes_parses() {
let f = parse_line("00:00:00 - 0 09FD020D").unwrap();
assert!(f.data.is_empty());
assert_eq!(f.pgn, 130306);
}
#[test]
fn missing_separator_is_rejected() {
assert!(matches!(
parse_line("20:11:00 130306 09FD020D FF"),
Err(ParseError::BadHeader { .. })
));
}
#[test]
fn non_hex_can_id_is_rejected() {
assert!(matches!(
parse_line("00:00:00 - 0 ZZZZZZZZ FF"),
Err(ParseError::BadInteger { field: "canid", .. })
));
}
#[test]
fn non_hex_data_token_is_rejected() {
assert!(matches!(
parse_line("00:00:00 - 0 09FD020D FF ZZ"),
Err(ParseError::BadHexByte { .. })
));
}
#[test]
fn data_is_clamped_to_the_fast_packet_ceiling() {
let bytes = vec!["AB"; FASTPACKET_MAX_SIZE + 5].join(" ");
let f = parse_line(&format!("00:00:00 - 0 09FD020D {bytes}")).unwrap();
assert_eq!(f.data.len(), FASTPACKET_MAX_SIZE);
}
#[test]
fn short_line_is_rejected() {
assert!(matches!(
parse_line("00:00:00 - 130306"),
Err(ParseError::BadHeader { .. })
));
}
#[test]
fn empty_line_is_empty() {
assert!(matches!(parse_line(""), Err(ParseError::Empty)));
assert!(matches!(parse_line("\r\n"), Err(ParseError::Empty)));
}
#[test]
fn accepts_comma_separated_data() {
let line = "00:00:00 - 0 18EEFF00 8E,F2,DD,E8,00,96,64,40";
let f = parse_line(line).unwrap();
assert_eq!(f.pgn, 0xee00);
assert_eq!(f.dst, 0xff);
assert_eq!(f.src, 0x00);
assert_eq!(f.data.len(), 8);
}
}