1use crate::cp437;
6
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
8pub struct Sauce {
9 pub title: String,
10 pub author: String,
11 pub group: String,
12 pub date: String,
14 pub width: u16,
16 pub lines: u16,
18 pub ice: bool,
20 pub nine_dot: bool,
22 pub font: String,
24 pub comments: Vec<String>,
25}
26
27impl Sauce {
28 pub fn date_text(&self) -> Option<String> {
30 let d = self.date.trim();
31 (d.len() == 8 && d.bytes().all(|b| b.is_ascii_digit())).then(|| format!("{}-{}-{}", &d[..4], &d[4..6], &d[6..]))
32 }
33
34 pub fn cterm_font(&self) -> u8 {
37 let f = self.font.to_ascii_lowercase();
38 match () {
39 _ if f.contains("topaz") && f.contains('+') => 40,
40 _ if f.contains("topaz") => 42,
41 _ if f.contains("microknight") && f.contains('+') => 39,
42 _ if f.contains("microknight") => 41,
43 _ if f.contains("noodle") => 37,
44 _ if f.contains("mosoul") || f.contains("mo'soul") => 38,
45 _ => 0,
46 }
47 }
48}
49
50pub fn split(data: &[u8]) -> (&[u8], Option<Sauce>) {
53 let mut end = data.len();
54 let mut sauce = None;
55 if data.len() >= 128 && data[data.len() - 128..].starts_with(b"SAUCE00") {
56 let r = &data[data.len() - 128..];
57 end = data.len() - 128;
58 let field = |a: usize, b: usize| cp437::decode(&r[a..b]).trim_end_matches(['\0', ' ']).to_string();
59 let u16_at = |a: usize| u16::from_le_bytes([r[a], r[a + 1]]);
60 let (data_type, flags) = (r[94], r[105]);
61 let mut s = Sauce {
62 title: field(7, 42),
63 author: field(42, 62),
64 group: field(62, 82),
65 date: field(82, 90),
66 font: field(106, 128),
67 ..Sauce::default()
68 };
69 if matches!(data_type, 1 | 5) {
71 s.width = if data_type == 5 { u16::from(r[95]) * 2 } else { u16_at(96) };
72 s.lines = if data_type == 5 { 0 } else { u16_at(98) };
73 s.ice = flags & 1 != 0;
74 s.nine_dot = (flags >> 1) & 3 == 2;
75 }
76 let n = r[104] as usize;
78 let block = 5 + 64 * n;
79 if n > 0 && end >= block && data[end - block..].starts_with(b"COMNT") {
80 s.comments = data[end - block + 5..end]
81 .chunks(64)
82 .map(|c| cp437::decode(c).trim_end_matches(['\0', ' ']).to_string())
83 .collect();
84 end -= block;
85 }
86 sauce = Some(s);
87 }
88 if let Some(z) = data[..end].iter().position(|&b| b == 0x1A) {
90 end = z;
91 }
92 (&data[..end], sauce)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 pub fn sauce_record(title: &str, author: &str, width: u16, flags: u8, font: &str, comments: u8) -> Vec<u8> {
100 let mut r = vec![0u8; 128];
101 r[..7].copy_from_slice(b"SAUCE00");
102 r[7..7 + title.len()].copy_from_slice(title.as_bytes());
103 r[42..42 + author.len()].copy_from_slice(author.as_bytes());
104 r[82..90].copy_from_slice(b"19960314");
105 r[94] = 1;
106 r[95] = 1;
107 r[96..98].copy_from_slice(&width.to_le_bytes());
108 r[98..100].copy_from_slice(&25u16.to_le_bytes());
109 r[104] = comments;
110 r[105] = flags;
111 r[106..106 + font.len()].copy_from_slice(font.as_bytes());
112 r
113 }
114
115 #[test]
116 fn reads_the_record_and_comments() {
117 let mut file = b"\x1b[1;31mART\x1b[0m".to_vec();
118 let art_len = file.len();
119 file.push(0x1A);
120 file.extend_from_slice(b"COMNT");
121 let mut line = b"Greets to the scene".to_vec();
122 line.resize(64, b' ');
123 file.extend_from_slice(&line);
124 file.extend(sauce_record("Big Logo", "artist", 80, 1 | 4, "Amiga Topaz 2+", 1));
125 let (art, sauce) = split(&file);
126 assert_eq!(art.len(), art_len);
127 let s = sauce.unwrap();
128 assert_eq!((s.title.as_str(), s.author.as_str(), s.width, s.lines), ("Big Logo", "artist", 80, 25));
129 assert!(s.ice && s.nine_dot);
130 assert_eq!(s.cterm_font(), 40);
131 assert_eq!(s.date_text().as_deref(), Some("1996-03-14"));
132 assert_eq!(s.comments, vec!["Greets to the scene".to_string()]);
133 assert_eq!(split(b"plain"), (&b"plain"[..], None));
134 }
135}