use crate::cp437;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Sauce {
pub title: String,
pub author: String,
pub group: String,
pub date: String,
pub width: u16,
pub lines: u16,
pub ice: bool,
pub nine_dot: bool,
pub font: String,
pub comments: Vec<String>,
}
impl Sauce {
pub fn date_text(&self) -> Option<String> {
let d = self.date.trim();
(d.len() == 8 && d.bytes().all(|b| b.is_ascii_digit())).then(|| format!("{}-{}-{}", &d[..4], &d[4..6], &d[6..]))
}
pub fn cterm_font(&self) -> u8 {
let f = self.font.to_ascii_lowercase();
match () {
_ if f.contains("topaz") && f.contains('+') => 40,
_ if f.contains("topaz") => 42,
_ if f.contains("microknight") && f.contains('+') => 39,
_ if f.contains("microknight") => 41,
_ if f.contains("noodle") => 37,
_ if f.contains("mosoul") || f.contains("mo'soul") => 38,
_ => 0,
}
}
}
pub fn split(data: &[u8]) -> (&[u8], Option<Sauce>) {
let mut end = data.len();
let mut sauce = None;
if data.len() >= 128 && data[data.len() - 128..].starts_with(b"SAUCE00") {
let r = &data[data.len() - 128..];
end = data.len() - 128;
let field = |a: usize, b: usize| cp437::decode(&r[a..b]).trim_end_matches(['\0', ' ']).to_string();
let u16_at = |a: usize| u16::from_le_bytes([r[a], r[a + 1]]);
let (data_type, flags) = (r[94], r[105]);
let mut s = Sauce {
title: field(7, 42),
author: field(42, 62),
group: field(62, 82),
date: field(82, 90),
font: field(106, 128),
..Sauce::default()
};
if matches!(data_type, 1 | 5) {
s.width = if data_type == 5 { u16::from(r[95]) * 2 } else { u16_at(96) };
s.lines = if data_type == 5 { 0 } else { u16_at(98) };
s.ice = flags & 1 != 0;
s.nine_dot = (flags >> 1) & 3 == 2;
}
let n = r[104] as usize;
let block = 5 + 64 * n;
if n > 0 && end >= block && data[end - block..].starts_with(b"COMNT") {
s.comments = data[end - block + 5..end]
.chunks(64)
.map(|c| cp437::decode(c).trim_end_matches(['\0', ' ']).to_string())
.collect();
end -= block;
}
sauce = Some(s);
}
if let Some(z) = data[..end].iter().position(|&b| b == 0x1A) {
end = z;
}
(&data[..end], sauce)
}
#[cfg(test)]
mod tests {
use super::*;
pub fn sauce_record(title: &str, author: &str, width: u16, flags: u8, font: &str, comments: u8) -> Vec<u8> {
let mut r = vec![0u8; 128];
r[..7].copy_from_slice(b"SAUCE00");
r[7..7 + title.len()].copy_from_slice(title.as_bytes());
r[42..42 + author.len()].copy_from_slice(author.as_bytes());
r[82..90].copy_from_slice(b"19960314");
r[94] = 1;
r[95] = 1;
r[96..98].copy_from_slice(&width.to_le_bytes());
r[98..100].copy_from_slice(&25u16.to_le_bytes());
r[104] = comments;
r[105] = flags;
r[106..106 + font.len()].copy_from_slice(font.as_bytes());
r
}
#[test]
fn reads_the_record_and_comments() {
let mut file = b"\x1b[1;31mART\x1b[0m".to_vec();
let art_len = file.len();
file.push(0x1A);
file.extend_from_slice(b"COMNT");
let mut line = b"Greets to the scene".to_vec();
line.resize(64, b' ');
file.extend_from_slice(&line);
file.extend(sauce_record("Big Logo", "artist", 80, 1 | 4, "Amiga Topaz 2+", 1));
let (art, sauce) = split(&file);
assert_eq!(art.len(), art_len);
let s = sauce.unwrap();
assert_eq!((s.title.as_str(), s.author.as_str(), s.width, s.lines), ("Big Logo", "artist", 80, 25));
assert!(s.ice && s.nine_dot);
assert_eq!(s.cterm_font(), 40);
assert_eq!(s.date_text().as_deref(), Some("1996-03-14"));
assert_eq!(s.comments, vec!["Greets to the scene".to_string()]);
assert_eq!(split(b"plain"), (&b"plain"[..], None));
}
}