use super::constants::{BITS_PER_ROW, MAX_PACKET_SIZE};
#[derive(Debug, Clone)]
pub struct PacketBlock {
pub start: u32,
pub end: u32,
pub label: String,
}
#[derive(Debug, Default)]
pub struct PacketDiagram {
pub title: Option<String>,
pub words: Vec<Vec<PacketBlock>>,
}
pub fn parse(input: &str) -> crate::error::ParseResult<PacketDiagram> {
let mut title: Option<String> = None;
let mut last_bit: i64 = -1;
let mut current_word: Vec<PacketBlock> = Vec::new();
let mut row: u32 = 1;
let mut words: Vec<Vec<PacketBlock>> = Vec::new();
let mut in_yaml = false;
let mut found_keyword = false;
for raw in input.lines() {
let line = strip_comment(raw);
let trimmed = line.trim();
if trimmed == "---" {
in_yaml = !in_yaml;
continue;
}
if in_yaml {
continue;
}
if !found_keyword {
if trimmed == "packet"
|| trimmed == "packet-beta"
|| trimmed.starts_with("packet ")
|| trimmed.starts_with("packet\t")
|| trimmed.starts_with("packet-beta ")
|| trimmed.starts_with("packet-beta\t")
{
found_keyword = true;
}
continue;
}
if trimmed.is_empty() {
continue;
}
if let Some(rest) = trimmed
.strip_prefix("title ")
.or_else(|| trimmed.strip_prefix("title\t"))
{
title = Some(rest.trim().trim_matches('"').to_string());
continue;
}
if trimmed == "title" {
title = Some(String::new());
continue;
}
if trimmed.starts_with("accTitle") || trimmed.starts_with("accDescr") {
continue;
}
let Some((raw_start, raw_end, raw_bits, label)) = parse_block_line(trimmed) else {
continue;
};
if raw_bits == Some(0) {
continue;
}
let start = raw_start.unwrap_or((last_bit + 1) as u32);
if start as i64 != last_bit + 1 {
continue;
}
let end = raw_end.unwrap_or_else(|| {
let bits = raw_bits.unwrap_or(1);
start + bits - 1
});
if end < start {
continue;
}
last_bit = end as i64;
let mut cur_start = start;
let mut cur_end = end;
loop {
if words.len() >= MAX_PACKET_SIZE {
break;
}
let (block, maybe_next) =
get_next_fitting_block(cur_start, cur_end, &label, row, BITS_PER_ROW);
current_word.push(block);
if current_word.last().map(|b| b.end + 1) == Some(row * BITS_PER_ROW) {
words.push(std::mem::take(&mut current_word));
row += 1;
}
match maybe_next {
None => break,
Some((ns, ne)) => {
cur_start = ns;
cur_end = ne;
}
}
}
}
if !current_word.is_empty() {
words.push(current_word);
}
crate::error::ParseResult::ok(PacketDiagram { title, words })
}
fn get_next_fitting_block(
start: u32,
end: u32,
label: &str,
row: u32,
bits_per_row: u32,
) -> (PacketBlock, Option<(u32, u32)>) {
if end < row * bits_per_row {
(
PacketBlock {
start,
end,
label: label.to_string(),
},
None,
)
} else {
let row_end = row * bits_per_row - 1;
let row_start = row * bits_per_row;
(
PacketBlock {
start,
end: row_end,
label: label.to_string(),
},
Some((row_start, end)),
)
}
}
#[allow(clippy::type_complexity)]
fn parse_block_line(line: &str) -> Option<(Option<u32>, Option<u32>, Option<u32>, String)> {
if let Some(rest) = line.strip_prefix('+') {
let colon = rest.find(':')?;
let bits: u32 = rest[..colon].trim().parse().ok()?;
let label = parse_label(&rest[colon + 1..]);
if label.is_empty() {
return None;
}
return Some((None, None, Some(bits), label));
}
let colon = line.find(':')?;
let range_part = line[..colon].trim();
let label = parse_label(&line[colon + 1..]);
if let Some(dash) = range_part.find('-') {
let start: u32 = range_part[..dash].trim().parse().ok()?;
let end: u32 = range_part[dash + 1..].trim().parse().ok()?;
Some((Some(start), Some(end), None, label))
} else {
let start: u32 = range_part.trim().parse().ok()?;
Some((Some(start), None, None, label))
}
}
fn parse_label(s: &str) -> String {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
fn strip_comment(line: &str) -> &str {
if let Some(pos) = line.find("%%") {
&line[..pos]
} else {
line
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic_packet() {
let input =
"packet-beta\n 0-15: \"Source Port\"\n 16-31: \"Destination Port\"\n 32-63: \"Sequence Number\"\n 64-95: \"Acknowledgment Number\"";
let diag = parse(input).diagram;
assert_eq!(diag.words.len(), 3, "expected 3 rows");
assert_eq!(diag.words[0][0].start, 0);
assert_eq!(diag.words[0][0].end, 15);
assert_eq!(diag.words[0][0].label, "Source Port");
}
#[test]
fn parse_single_bit() {
let input = "packet-beta\n 0: \"Flag\"";
let diag = parse(input).diagram;
assert_eq!(diag.words.len(), 1);
assert_eq!(diag.words[0][0].start, 0);
assert_eq!(diag.words[0][0].end, 0);
}
#[test]
fn parse_packet_keyword_no_beta() {
let input = "packet\n 0-7: \"A\"\n 8-15: \"B\"";
let diag = parse(input).diagram;
assert_eq!(diag.words.len(), 1);
assert_eq!(diag.words[0].len(), 2);
}
#[test]
fn parse_yaml_frontmatter() {
let input = "---\ntitle: \"TCP Packet\"\n---\npacket-beta\n 0-15: \"Source Port\"\n 16-31: \"Destination Port\"";
let diag = parse(input).diagram;
assert_eq!(diag.words.len(), 1);
assert_eq!(diag.words[0][0].label, "Source Port");
}
#[test]
fn rejects_non_contiguous_blocks() {
let input = "packet-beta\n 0-99: \"A\"\n 106: \"URG\"";
let diag = parse(input).diagram;
let all: Vec<&PacketBlock> = diag.words.iter().flatten().collect();
assert!(
all.iter().all(|b| b.end <= 99),
"non-contiguous block should be absent"
);
assert!(
all.iter().any(|b| b.start == 0),
"first block should be rendered"
);
}
#[test]
fn relative_bits_form() {
let input = "packet-beta\n 0-7: \"A\"\n +8: \"B\"";
let diag = parse(input).diagram;
let all: Vec<&PacketBlock> = diag.words.iter().flatten().collect();
assert_eq!(all[1].start, 8);
assert_eq!(all[1].end, 15);
assert_eq!(all[1].label, "B");
}
#[test]
fn block_spans_row_boundary() {
let input = "packet-beta\n 0-23: \"A\"\n 24-39: \"B\"";
let diag = parse(input).diagram;
assert_eq!(diag.words.len(), 2, "expected 2 rows after split");
assert_eq!(diag.words[0].last().unwrap().end, 31);
assert_eq!(diag.words[1][0].start, 32);
assert_eq!(diag.words[1][0].end, 39);
}
}