use super::{find_header, first_word, read_quoted, Envelope, ParseError, Preamble, TitleSyntax};
pub const KEYWORDS: &[&str] = &["packet-beta", "packet"];
pub const KEYWORD: &str = "packet";
pub const BITS_PER_ROW: u32 = 32;
pub const MAX_FIELDS: usize = 10_000;
pub const MAX_ROWS: u32 = 128;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub start: u32,
pub end: u32,
pub label: String,
}
impl Field {
pub fn bits(&self) -> u32 {
self.end - self.start + 1
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Packet {
pub preamble: Preamble,
pub rows: Vec<Vec<Field>>,
}
pub fn is_packet(src: &str) -> bool {
find_header(src, KEYWORDS).is_some()
}
pub fn parse(src: &str) -> Result<Packet, ParseError> {
let Some(source) = find_header(src, KEYWORDS) else {
return Err(ParseError::NotThisChart {
expected: KEYWORD,
header: first_word(src),
});
};
let mut env = Envelope::new(source.front_matter_title);
let mut blocks: Vec<Field> = Vec::new();
let mut last_bit: i64 = -1;
let mut feed: Vec<(usize, String)> = Vec::new();
if !source.header_rest.is_empty() {
feed.push((source.header_index + 1, source.header_rest.clone()));
}
for (i, line) in source
.lines
.iter()
.enumerate()
.skip(source.header_index + 1)
{
feed.push((i + 1, line.clone()));
}
for (number, line) in feed {
if line.trim().is_empty() {
continue;
}
if env.read(&line, TitleSyntax::Terminal) {
continue;
}
let t = line.trim();
let Some((head, tail)) = t.split_once(':') else {
return Err(ParseError::Unexpected {
kind: KEYWORD,
line: number,
text: t.to_string(),
});
};
let chars: Vec<char> = tail.trim().chars().collect();
let Some((label, used)) = read_quoted(&chars, 0) else {
return Err(ParseError::Invalid {
line: number,
message: "a field's label must be quoted".to_string(),
});
};
if !chars[used..].iter().collect::<String>().trim().is_empty() {
return Err(ParseError::Unexpected {
kind: KEYWORD,
line: number,
text: chars[used..].iter().collect::<String>().trim().to_string(),
});
}
let head = head.trim();
let (start, end) = if let Some(bits) = head.strip_prefix('+') {
let bits = integer(bits.trim(), number)?;
if bits == 0 {
return Err(ParseError::Invalid {
line: number,
message: format!("packet block {} cannot be a zero bit field", last_bit + 1),
});
}
let start = (last_bit + 1) as u32;
(start, start + bits - 1)
} else {
match head.split_once('-') {
Some((a, b)) => {
let start = integer(a.trim(), number)?;
let end = integer(b.trim(), number)?;
if end < start {
return Err(ParseError::Invalid {
line: number,
message: format!(
"packet block {start} - {end} is invalid; end must not be before start"
),
});
}
(start, end)
}
None => {
let start = integer(head, number)?;
(start, start)
}
}
};
if end / BITS_PER_ROW >= MAX_ROWS {
return Err(ParseError::Invalid {
line: number,
message: format!(
"bit {end} is past the {} bits a packet may be drawn across",
MAX_ROWS * BITS_PER_ROW
),
});
}
if start as i64 != last_bit + 1 {
return Err(ParseError::Invalid {
line: number,
message: format!(
"packet block {start} - {end} is not contiguous; it should start at {}",
last_bit + 1
),
});
}
last_bit = end as i64;
blocks.push(Field { start, end, label });
if blocks.len() > MAX_FIELDS {
return Err(ParseError::Invalid {
line: number,
message: format!("a packet may hold at most {MAX_FIELDS} fields"),
});
}
}
let mut packet = Packet {
preamble: env.preamble,
rows: Vec::new(),
};
if blocks.is_empty() {
return Err(ParseError::NoData {
kind: KEYWORD,
wanted: "field",
});
}
for block in blocks {
let mut start = block.start;
while start <= block.end {
let row = (start / BITS_PER_ROW) as usize;
let row_end = (row as u32 + 1) * BITS_PER_ROW - 1;
let end = block.end.min(row_end);
while packet.rows.len() <= row {
packet.rows.push(Vec::new());
}
packet.rows[row].push(Field {
start,
end,
label: block.label.clone(),
});
start = end + 1;
}
}
Ok(packet)
}
fn integer(text: &str, line: usize) -> Result<u32, ParseError> {
if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::BadNumber {
line,
text: text.to_string(),
why: "a bit index must be a whole number",
});
}
if text.len() > 1 && text.starts_with('0') {
return Err(ParseError::BadNumber {
line,
text: text.to_string(),
why: "a bit index must not have a leading zero",
});
}
text.parse::<u32>().map_err(|_| ParseError::BadNumber {
line,
text: text.to_string(),
why: "that bit index is too large",
})
}