use crate::error::FigletError;
#[derive(Debug, Clone)]
pub(crate) struct NumericHeader {
pub hardblank: char,
pub height: u32,
pub baseline: u32,
pub max_length: u32,
pub old_layout: i32,
pub comment_lines: u32,
pub print_direction: u32,
pub full_layout: u32,
pub codetag_count: u32,
}
#[derive(Debug)]
pub(crate) struct HeaderReader<'a> {
rest: &'a str,
line_no: u32,
}
impl<'a> HeaderReader<'a> {
pub(crate) fn new(header_line: &'a str, magic_len: usize, line_no: u32) -> Self {
let rest = if header_line.len() > magic_len {
&header_line[magic_len..]
} else {
""
};
Self { rest, line_no }
}
pub(crate) fn line_no(&self) -> u32 {
self.line_no
}
}
pub(crate) fn read_numeric_header(
bytes: &[u8],
magic_len: usize,
) -> Result<NumericHeader, FigletError> {
let text: String = bytes.iter().map(|&b| b as char).collect();
let header_line = text
.split('\n')
.next()
.ok_or_else(|| parse_err("empty input", 1))?
.trim_end_matches('\r');
parse_header_line(header_line, magic_len, 1)
}
pub(crate) fn parse_header_line(
line: &str,
magic_len: usize,
line_no: u32,
) -> Result<NumericHeader, FigletError> {
if line.len() < magic_len {
return Err(parse_err("truncated header: missing magic", line_no));
}
let rest = &line[magic_len..];
let mut chars = rest.chars();
let hardblank = chars
.next()
.ok_or_else(|| parse_err("truncated header: missing hardblank", line_no))?;
let tail: String = chars.collect();
let mut tokens = tail.split_whitespace();
let height = next_u32(&mut tokens, "height", line_no)?;
let baseline = next_u32(&mut tokens, "baseline", line_no)?;
let max_length = next_u32(&mut tokens, "max_length", line_no)?;
let old_layout = next_i32(&mut tokens, "old_layout", line_no)?;
if !(-1..=63).contains(&old_layout) {
return Err(parse_err(
&format!("old_layout out of -1..=63 range: {old_layout}"),
line_no,
));
}
let comment_lines = next_u32(&mut tokens, "comment_lines", line_no)?;
let print_direction = next_u32_opt(&mut tokens).unwrap_or(0);
let derived_full_layout = if old_layout < 0 { 0 } else { old_layout as u32 };
let full_layout = next_u32_opt(&mut tokens).unwrap_or(derived_full_layout);
let codetag_count = next_u32_opt(&mut tokens).unwrap_or(0);
Ok(NumericHeader {
hardblank,
height,
baseline,
max_length,
old_layout,
comment_lines,
print_direction,
full_layout,
codetag_count,
})
}
fn next_u32(
tokens: &mut std::str::SplitWhitespace<'_>,
field: &str,
line_no: u32,
) -> Result<u32, FigletError> {
let tok = tokens
.next()
.ok_or_else(|| parse_err(&format!("truncated header: missing {field}"), line_no))?;
tok.parse::<u32>().map_err(|_| {
parse_err(
&format!("truncated header: {field} not a u32 ({tok})"),
line_no,
)
})
}
fn next_i32(
tokens: &mut std::str::SplitWhitespace<'_>,
field: &str,
line_no: u32,
) -> Result<i32, FigletError> {
let tok = tokens
.next()
.ok_or_else(|| parse_err(&format!("truncated header: missing {field}"), line_no))?;
tok.parse::<i32>().map_err(|_| {
parse_err(
&format!("truncated header: {field} not an i32 ({tok})"),
line_no,
)
})
}
fn next_u32_opt(tokens: &mut std::str::SplitWhitespace<'_>) -> Option<u32> {
tokens.next().and_then(|tok| tok.parse::<u32>().ok())
}
fn parse_err(reason: &str, line: u32) -> FigletError {
FigletError::FontParse {
reason: reason.to_owned(),
line,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_flf_header() {
let h = read_numeric_header(b"flf2a$ 1 1 8 0 0\n", 5).expect("parses");
assert_eq!(h.hardblank, '$');
assert_eq!(h.height, 1);
assert_eq!(h.baseline, 1);
assert_eq!(h.max_length, 8);
assert_eq!(h.old_layout, 0);
assert_eq!(h.comment_lines, 0);
assert_eq!(h.print_direction, 0);
assert_eq!(h.full_layout, 0);
assert_eq!(h.codetag_count, 0);
}
#[test]
fn parses_full_flf_header_with_all_optional_fields() {
let h = read_numeric_header(b"flf2a$ 4 3 8 0 16 0 64 0\nother stuff\n", 5).expect("parses");
assert_eq!(h.height, 4);
assert_eq!(h.baseline, 3);
assert_eq!(h.max_length, 8);
assert_eq!(h.comment_lines, 16);
assert_eq!(h.print_direction, 0);
assert_eq!(h.full_layout, 64);
assert_eq!(h.codetag_count, 0);
}
#[test]
fn parses_minimal_tlf_header() {
let h = read_numeric_header(b"tlf2a$ 1 1 8 0 0\n", 5).expect("parses");
assert_eq!(h.hardblank, '$');
assert_eq!(h.height, 1);
}
#[test]
fn rejects_empty_input() {
let err = read_numeric_header(b"", 5).unwrap_err();
match err {
FigletError::FontParse { reason, line } => {
assert!(reason.contains("truncated"), "{reason}");
assert_eq!(line, 1);
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn rejects_truncated_header_missing_fields() {
let err = read_numeric_header(b"flf2a$ 1\n", 5).unwrap_err();
match err {
FigletError::FontParse { reason, line } => {
assert!(reason.contains("truncated"), "{reason}");
assert_eq!(line, 1);
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn rejects_truncated_header_missing_hardblank() {
let err = read_numeric_header(b"flf2a", 5).unwrap_err();
match err {
FigletError::FontParse { reason, line } => {
assert!(
reason.contains("hardblank") || reason.contains("truncated"),
"{reason}"
);
assert_eq!(line, 1);
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn rejects_old_layout_out_of_range_high() {
let err = read_numeric_header(b"flf2a$ 1 1 8 64 0\n", 5).unwrap_err();
match err {
FigletError::FontParse { reason, .. } => {
assert!(reason.contains("old_layout"), "{reason}");
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn rejects_old_layout_out_of_range_low() {
let err = read_numeric_header(b"flf2a$ 1 1 8 -2 0\n", 5).unwrap_err();
match err {
FigletError::FontParse { reason, .. } => {
assert!(reason.contains("old_layout"), "{reason}");
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn rejects_oversized_numeric_field() {
let err = read_numeric_header(b"flf2a$ 4294967296 1 8 0 0\n", 5).unwrap_err();
match err {
FigletError::FontParse { reason, .. } => {
assert!(reason.contains("height"), "{reason}");
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn rejects_non_numeric_height() {
let err = read_numeric_header(b"flf2a$ abc 1 8 0 0\n", 5).unwrap_err();
match err {
FigletError::FontParse { reason, .. } => {
assert!(reason.contains("height"), "{reason}");
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn negative_height_rejected() {
let err = read_numeric_header(b"flf2a$ -1 1 8 0 0\n", 5).unwrap_err();
match err {
FigletError::FontParse { reason, .. } => {
assert!(reason.contains("height"), "{reason}");
}
other => panic!("expected FontParse, got {other:?}"),
}
}
#[test]
fn tolerates_latin1_bytes_in_header_comment() {
let h = read_numeric_header(b"flf2a\xC4 1 1 8 0 0\n", 5).expect("parses");
assert_eq!(h.hardblank, '\u{00C4}');
}
#[test]
fn full_layout_defaults_from_old_layout_when_omitted() {
let h = read_numeric_header(b"flf2a$ 1 1 8 15 0\n", 5).expect("parses");
assert_eq!(h.full_layout, 15);
}
#[test]
fn full_layout_defaults_to_zero_when_old_layout_negative() {
let h = read_numeric_header(b"flf2a$ 1 1 8 -1 0\n", 5).expect("parses");
assert_eq!(h.full_layout, 0);
}
#[test]
fn ignores_bytes_past_first_newline() {
let h = read_numeric_header(b"flf2a$ 1 1 8 0 0\nGARBAGE GARBAGE\n", 5).expect("parses");
assert_eq!(h.height, 1);
}
#[test]
fn header_reader_skips_magic() {
let r = HeaderReader::new("flf2a$ 1 1 8 0 0", 5, 1);
assert_eq!(r.line_no(), 1);
assert!(r.rest.starts_with('$'));
}
}