cp437_tools/libs/public/
meta.rs

1//! Handling of file's metadata
2//!
3//! See <https://web.archive.org/web/20250427042053id_/https://www.acid.org/info/sauce/sauce.htm>
4//!
5
6use chrono::NaiveDate;
7use std::{
8    array::TryFromSliceError,
9    fs::File,
10    io::{Read as _, Seek as _, SeekFrom},
11    str,
12};
13use ttf_parser::Face;
14
15use crate::{
16    fonts,
17    prelude::{to_utf8, CP437_TO_UTF8},
18};
19
20/// A structure representing a file's metadata.
21#[doc(alias = "Sauce")]
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct Meta {
24    /// The image's title.
25    pub title: String,
26    /// The image's author.
27    pub author: String,
28    /// The image author's team or group.
29    #[doc(alias = "team")]
30    pub group: String,
31    /// The image creation date, in the YYYYMMDD format.
32    pub date: String,
33    /// The size of the file, sans this metadata.
34    pub size: u32,
35    /// The type of this file.
36    ///
37    /// Only supported values are:
38    /// * `(0, 0)` → `None` (effectively, `Character/ANSI`)
39    /// * `(1, 0)` → `Character/ASCII`
40    /// * `(1, 1)` → `Character/ANSI`
41    ///
42    /// See <https://web.archive.org/web/20250427042053id_/https://www.acid.org/info/sauce/sauce.htm#FileType>
43    ///
44    pub r#type: (u8, u8),
45    /// Width of the image.
46    pub width: u16,
47    /// Height of the image.
48    pub height: u16,
49    /// A bitfield of flags that define how to process an image.
50    ///
51    /// See <https://web.archive.org/web/20250427042053id_/https://www.acid.org/info/sauce/sauce.htm#ANSiFlags>
52    ///
53    #[doc(alias = "AR")]
54    #[doc(alias = "aspect ratio")]
55    #[doc(alias = "LS")]
56    #[doc(alias = "letter spacing")]
57    #[doc(alias = "B")]
58    #[doc(alias = "iCE colour")]
59    #[doc(alias = "non-blink mode")]
60    pub flags: u8,
61    /// The name of the font this image uses.
62    ///
63    /// Only IBM VGA is supported.
64    ///
65    pub font: String,
66    /// A list of comments on this image.
67    #[doc(alias = "comments")]
68    pub notes: Vec<String>,
69}
70
71/// A minimal meta.
72///
73/// Sets all defaults as interpreted when undefined.
74///
75impl Default for Meta {
76    /// Get default meta values.
77    fn default() -> Meta {
78        return Meta {
79            title: String::new(),
80            author: String::new(),
81            group: String::new(),
82            date: String::new(),
83            size: 0,
84            r#type: (1, 1),
85            width: 80,
86            height: 25,
87            flags: 0x0D,
88            font: String::from("IBM VGA"),
89            notes: vec![],
90        };
91    }
92}
93
94impl Meta {
95    /// Wrap the title in an [`Option`].
96    ///
97    /// See [`title` field](#structfield.title)
98    ///
99    #[must_use]
100    pub fn title(&self) -> Option<&String> {
101        return if self.title.is_empty() { None } else { Some(&self.title) };
102    }
103
104    /// Wrap the author in an [`Option`].
105    ///
106    /// See [`author` field](#structfield.author)
107    ///
108    #[must_use]
109    pub fn author(&self) -> Option<&String> {
110        return if self.author.is_empty() { None } else { Some(&self.author) };
111    }
112
113    /// Wrap the group in an [`Option`].
114    ///
115    /// See [`group` field](#structfield.group)
116    ///
117    #[must_use]
118    pub fn group(&self) -> Option<&String> {
119        return if self.group.is_empty() { None } else { Some(&self.group) };
120    }
121
122    /// Wrap the date in an [`Option`].
123    ///
124    /// See [`date` field](#structfield.date)
125    ///
126    #[must_use]
127    pub fn date(&self) -> Option<&String> {
128        return if self.date.is_empty() { None } else { Some(&self.date) };
129    }
130
131    /// Fetch the size.
132    ///
133    /// See [`size` field](#structfield.size)
134    ///
135    #[inline]
136    #[must_use]
137    pub fn size(&self) -> u32 {
138        return self.size;
139    }
140
141    /// Fetch the type if `type != (0, 0)`, otherwise the default.
142    ///
143    /// See [`type` field](#structfield.type)
144    ///
145    /// See [`Meta::default`]
146    ///
147    #[must_use]
148    pub fn r#type(&self) -> (u8, u8) {
149        return if self.r#type == (0, 0) { Meta::default().r#type } else { self.r#type };
150    }
151
152    /// Fetch the width if `width > 0`, otherwise the default.
153    ///
154    /// See [`width` field](#structfield.width)
155    ///
156    /// See [`Meta::default`]
157    ///
158    #[must_use]
159    pub fn width(&self) -> u16 {
160        return if self.width > 0 { self.width } else { Meta::default().width };
161    }
162
163    /// Fetch the height if `height > 0`, otherwise the default.
164    ///
165    /// See [`height` field](#structfield.height)
166    ///
167    /// See [`Meta::default`]
168    ///
169    #[must_use]
170    pub fn height(&self) -> u16 {
171        return if self.height > 0 { self.height } else { Meta::default().height };
172    }
173
174    /// Get both the width and the height.
175    ///
176    /// See [`width` method](#method.width)
177    ///
178    /// See [`height` method](#method.height)
179    ///
180    #[inline]
181    #[must_use]
182    pub fn dimensions(&self) -> (u16, u16) {
183        return (self.width(), self.height());
184    }
185
186    /// Fetch the flags, split into `(AR, LS, B)`.
187    ///
188    /// See [`flags` field](#structfield.flags)
189    ///
190    #[must_use]
191    pub fn flags(&self) -> (u8, u8, u8) {
192        return ((self.flags >> 3) & 3, (self.flags >> 1) & 3, self.flags & 1);
193    }
194
195    /// Fetch the font if `font != ""`, otherwise the default.
196    ///
197    /// See [`font` field](#structfield.font)
198    ///
199    #[must_use]
200    pub fn font(&self) -> Option<&String> {
201        return if self.font.is_empty() { None } else { Some(&self.font) };
202    }
203
204    /// Font face, in OTB format.
205    ///
206    /// See [`font` field](#structfield.font)
207    ///
208    #[must_use]
209    pub fn font_face_otb(&self) -> &Face<'_> {
210        return if self.font_width() == 8 { &fonts::VGA_8X16 as &Face } else { &fonts::VGA_9X16 as &Face };
211    }
212
213    /// Font face, in WOFF format.
214    ///
215    /// See [`font` field](#structfield.font)
216    ///
217    #[must_use]
218    pub fn font_face_woff(&self) -> &[u8] {
219        return if self.font_width() == 8 { &fonts::VGA_8X16_WOFF } else { &fonts::VGA_9X16_WOFF };
220    }
221
222    /// Fetch the notes.
223    ///
224    /// See [`notes` field](#structfield.notes)
225    ///
226    #[inline]
227    #[must_use]
228    pub fn notes(&self) -> &Vec<String> {
229        return &self.notes;
230    }
231
232    /// Compute the stretch required for a given aspect ratio.
233    ///
234    /// See [`aspect_ratio` method](#method.aspect_ratio)
235    ///
236    #[inline]
237    #[must_use]
238    pub fn stretch(&self) -> f64 {
239        let ar = self.aspect_ratio();
240        return f64::from(ar.1) / f64::from(ar.0);
241    }
242
243    /// Compute the aspect ratio.
244    ///
245    /// See [`flags` field](#structfield.flags)
246    ///
247    #[must_use]
248    pub fn aspect_ratio(&self) -> (u8, u8) {
249        return if self.flags().0 == 0b10 {
250            (1, 1)
251        } else if self.flags().1 == 0b01 {
252            (5, 6)
253        } else {
254            (20, 27)
255        };
256    }
257
258    /// Font width.
259    ///
260    /// See [`flags` field](#structfield.flags)
261    ///
262    #[must_use]
263    pub fn font_width(&self) -> u8 {
264        return if self.flags().1 == 0b01 { 8 } else { 9 };
265    }
266
267    /// Font height.
268    #[inline]
269    #[must_use]
270    pub fn font_height(&self) -> u8 {
271        return 16;
272    }
273
274    /// Font dimensions.
275    ///
276    /// See [`font_width` method](#method.font_width)
277    ///
278    /// See [`font_height` method](#method.font_height)
279    ///
280    #[inline]
281    #[must_use]
282    pub fn font_size(&self) -> (u8, u8) {
283        return (self.font_width(), self.font_height());
284    }
285}
286
287/// Get a file's metadata via its path.
288///
289/// # Arguments
290///
291/// * `path`: Path pointing to file. Can be relative to cwd.
292///
293/// # Errors
294///
295/// Fails when there's problems reading the file.
296///
297#[inline]
298pub fn get(path: &str) -> Result<Option<Meta>, String> {
299    return read(&mut File::open(path).map_err(|err| return err.to_string())?);
300}
301
302/// Get a file's metadata via a file reference.
303///
304/// # Arguments
305///
306/// * `file`: File to read.
307///
308/// # Errors
309///
310/// Fails when there's problems reading the file.
311///
312pub fn read(file: &mut File) -> Result<Option<Meta>, String> {
313    return read_raw(file).map(|maybe_raw| {
314        return maybe_raw
315            .map(|raw| {
316                return Ok(Meta {
317                    title: to_utf8(&(raw[raw.len() - 121..raw.len() - 86])).trim_matches('\x20').to_string(),
318                    author: to_utf8(&(raw[raw.len() - 86..raw.len() - 66])).trim_matches('\x20').to_string(),
319                    group: to_utf8(&(raw[raw.len() - 66..raw.len() - 46])).trim_matches('\x20').to_string(),
320                    date: to_utf8(&(raw[raw.len() - 46..raw.len() - 38])).trim_matches('\x20').to_string(),
321                    size: u32::from_le_bytes(
322                        raw[raw.len() - 38..raw.len() - 34]
323                            .try_into()
324                            .map_err(|err: TryFromSliceError| return err.to_string())?,
325                    ),
326                    r#type: (raw[raw.len() - 34], raw[raw.len() - 33]),
327                    width: u16::from_le_bytes(
328                        raw[raw.len() - 32..raw.len() - 30]
329                            .try_into()
330                            .map_err(|err: TryFromSliceError| return err.to_string())?,
331                    ),
332                    height: u16::from_le_bytes(
333                        raw[raw.len() - 30..raw.len() - 28]
334                            .try_into()
335                            .map_err(|err: TryFromSliceError| return err.to_string())?,
336                    ),
337                    flags: raw[raw.len() - 23],
338                    font: to_utf8(&(raw[raw.len() - 22..])).trim_matches('\x00').to_string(),
339                    notes: (0..raw[raw.len() - 24] as usize)
340                        .rev()
341                        .map(|i| {
342                            let offset = raw.len() - (i + 3) * 64;
343                            return to_utf8(&(raw[offset..offset + 64])).trim_matches('\x20').to_string();
344                        })
345                        .collect(),
346                });
347            })
348            .transpose();
349    })?;
350}
351
352/// Get a human readable type name.
353///
354/// # Arguments
355///
356/// * `type`: The type to get the name for.
357///
358#[must_use]
359pub fn type_name(r#type: (u8, u8)) -> String {
360    return match r#type {
361        (0, _) => String::from("None"),
362        (1, 0) => String::from("Character/ASCII"),
363        (1, 1) => String::from("Character/ANSi"),
364        (1, 2) => String::from("Character/ANSiMation"),
365        (1, 3) => String::from("Character/RIPScript"),
366        (1, 4) => String::from("Character/PCBoard"),
367        (1, 5) => String::from("Character/Avatar"),
368        (1, 6) => String::from("Character/HTML"),
369        (1, 7) => String::from("Character/Source"),
370        (1, 8) => String::from("Character/TundraDraw"),
371        (1, _) => format!("Character/Unknown {}", r#type.1),
372        (2, _) => String::from("Bitmap"),
373        (3, _) => String::from("Vector"),
374        (4, _) => String::from("Audio"),
375        (5, _) => String::from("BinaryText"),
376        (6, _) => String::from("XBin"),
377        (7, _) => String::from("Archive"),
378        (8, _) => String::from("Executable"),
379        _ => format!("Unknown {}/Unknown {}", r#type.0, r#type.1),
380    };
381}
382
383/// Check that a given file's metadata is valid and supported.
384///
385/// # Arguments
386///
387/// * `meta`: The metadata to check.
388///
389#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
390pub fn check(meta: Option<&Meta>) -> Result<(), String> {
391    check_title(meta)?;
392    check_author(meta)?;
393    check_group(meta)?;
394    check_date(meta)?;
395    check_type(meta)?;
396    check_flags(meta)?;
397    check_font(meta)?;
398    check_notes(meta)?;
399
400    return Ok(());
401}
402
403/// Check that the title is valid.
404///
405/// # Arguments
406///
407/// * `meta`: The metadata to check.
408///
409#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
410pub fn check_title(meta: Option<&Meta>) -> Result<(), String> {
411    return meta.as_ref().map_or(Ok(()), |m| return check_str(&m.title, "Title", 35));
412}
413
414/// Check that the author is valid.
415///
416/// # Arguments
417///
418/// * `meta`: The metadata to check.
419///
420#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
421pub fn check_author(meta: Option<&Meta>) -> Result<(), String> {
422    return meta.as_ref().map_or(Ok(()), |m| return check_str(&m.author, "Author", 20));
423}
424
425/// Check that the group is valid.
426///
427/// # Arguments
428///
429/// * `meta`: The metadata to check.
430///
431#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
432pub fn check_group(meta: Option<&Meta>) -> Result<(), String> {
433    return meta.as_ref().map_or(Ok(()), |m| return check_str(&m.group, "Group", 20));
434}
435
436/// Check that the date is valid.
437///
438/// # Arguments
439///
440/// * `meta`: The metadata to check.
441///
442#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
443pub fn check_date(meta: Option<&Meta>) -> Result<(), String> {
444    if let Some(m) = meta {
445        if !m.date.is_empty() {
446            if m.date.len() != 8 {
447                return Err(format!("Date length is wrong (expected =8, got {})", m.date.len()));
448            } else if let Err(err) = NaiveDate::parse_from_str(&m.date, "%Y%m%d") {
449                return Err(format!("Date format is wrong ({err})"));
450            }
451        }
452    }
453
454    return Ok(());
455}
456
457/// Check that the type is valid and supported.
458///
459/// # Arguments
460///
461/// * `meta`: The metadata to check.
462///
463#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
464pub fn check_type(meta: Option<&Meta>) -> Result<(), String> {
465    if let Some(m) = meta {
466        if ![0, 1].contains(&m.r#type.0) || ![0, 1].contains(&m.r#type.1) {
467            return Err(format!("Type is unsupported ({})", type_name(m.r#type)));
468        }
469    }
470
471    return Ok(());
472}
473
474/// Check that the flags are valid.
475///
476/// # Arguments
477///
478/// * `meta`: The metadata to check.
479///
480#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
481pub fn check_flags(meta: Option<&Meta>) -> Result<(), String> {
482    if let Some(m) = meta {
483        if m.flags & 0x01 == 0x00 {
484            // Only intended to support iCE colours
485            return Err(String::from("Blink mode is unsupported"));
486        } else if m.flags & 0x06 == 0x06 {
487            return Err(String::from("Invalid letter spacing"));
488        } else if m.flags & 0x18 == 0x18 {
489            return Err(String::from("Invalid aspect ratio"));
490        } else if m.flags > 0x1F {
491            return Err(String::from("Invalid flags"));
492        }
493    }
494
495    return Ok(());
496}
497
498/// Check that the font is valid and supported.
499///
500/// # Arguments
501///
502/// * `meta`: The metadata to check.
503///
504#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
505pub fn check_font(meta: Option<&Meta>) -> Result<(), String> {
506    if let Some(m) = meta {
507        if !["IBM VGA", "IBM VGA 437", ""].contains(&m.font.as_str()) {
508            // IBM VGA is by far the most common font, haven't even tried to
509            // support any others.
510            return Err(format!("Font is unsupported ({})", m.font));
511        }
512    }
513
514    return Ok(());
515}
516
517/// Check that the notes are valid.
518///
519/// # Arguments
520///
521/// * `meta`: The metadata to check.
522///
523#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
524pub fn check_notes(meta: Option<&Meta>) -> Result<(), String> {
525    if let Some(m) = meta {
526        if m.notes.len() > 255 {
527            return Err(format!("Too many notes (expected <= 255, got {})", m.notes.len()));
528        }
529
530        for i in 0..m.notes.len() {
531            check_note(meta, i)?;
532        }
533    }
534
535    return Ok(());
536}
537
538/// Check that a single note is valid.
539///
540/// # Arguments
541///
542/// * `meta`: The metadata to check.
543/// * `i`: The index of the note.
544///
545#[expect(clippy::cast_possible_truncation, reason = "Range is [0,3]")]
546#[expect(clippy::cast_sign_loss, reason = "Range is [0,3]")]
547#[expect(clippy::cast_precision_loss, reason = "Range is [0,3]")]
548#[expect(clippy::missing_errors_doc, reason = "That's like the whole purpose of this function")]
549pub fn check_note(meta: Option<&Meta>, i: usize) -> Result<(), String> {
550    if let Some(m) = meta {
551        check_str(
552            &m.notes[i],
553            &format!("Notes[{:0width$}]", i, width = (m.notes.len() as f32).log10().ceil() as usize),
554            64,
555        )?;
556    }
557
558    return Ok(());
559}
560
561fn check_str(string: &str, name: &str, max_length: usize) -> Result<(), String> {
562    if string.len() > max_length {
563        return Err(format!("{} is too long (expected <={}, got {})", name, max_length, string.len()));
564    }
565
566    return string.chars().try_for_each(|r#char| {
567        return check_char(r#char).map_err(|msg| return format!("{name} contains illegal characters ({msg})"));
568    });
569}
570
571fn check_char(r#char: char) -> Result<(), String> {
572    if ['\x00', '\x0A', '\x0D', '\x1A', '\x1B'].contains(&r#char) {
573        return Err(format!("0x{:02X} is a control character", r#char as u8));
574    } else if !CP437_TO_UTF8.contains(&r#char) {
575        return Err(format!("{} (U+{:X}) is not a valid CP437 character", r#char, r#char as u32));
576    }
577
578    return Ok(());
579}
580
581fn read_raw(file: &mut File) -> Result<Option<Vec<u8>>, String> {
582    if file.metadata().map_err(|err| return err.to_string())?.len() < 129 {
583        return Ok(None);
584    }
585
586    let mut sauce = vec![0; 128];
587    file.seek(SeekFrom::End(-128)).map_err(|err| return err.to_string())?;
588    file.read_exact(&mut sauce).map_err(|err| return err.to_string())?;
589
590    if &sauce[..7] != "SAUCE00".as_bytes() {
591        return Ok(None);
592    }
593
594    let offset = sauce[104] as usize * 64 + (if sauce[104] > 0 { 134 } else { 129 });
595    #[expect(clippy::cast_possible_wrap, reason = "Range is [0,16454]")]
596    file.seek(SeekFrom::End(-(offset as i64))).map_err(|err| return err.to_string())?;
597    let mut raw = vec![0; offset];
598    file.read_exact(&mut raw).map_err(|err| return err.to_string())?;
599    if raw[0] != 0x1A || (offset > 129 && &raw[1..6] != "COMNT".as_bytes()) {
600        return Ok(None);
601    }
602
603    return Ok(Some(raw));
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    use pretty_assertions::assert_eq;
611
612    #[test]
613    fn default() {
614        let meta = Meta::default();
615        assert_eq!(meta.title(), None);
616        assert_eq!(meta.author(), None);
617        assert_eq!(meta.group(), None);
618        assert_eq!(meta.date(), None);
619        assert_eq!(meta.size(), 0);
620        assert_eq!(meta.r#type(), (1, 1));
621        assert_eq!(meta.dimensions(), (80, 25));
622        assert_eq!(meta.flags(), (0b01, 0b10, 0b1));
623        assert_eq!(meta.font(), Some(&String::from("IBM VGA")));
624        assert_eq!(meta.notes(), &Vec::<String>::new());
625    }
626
627    #[test]
628    fn none() -> Result<(), String> {
629        let meta = get("res/test/simple.ans")?;
630        assert!(meta.is_none());
631
632        return Ok(());
633    }
634
635    #[test]
636    fn some() -> Result<(), String> {
637        let meta = get("res/test/meta.ans")?;
638        assert!(meta.is_some());
639        let meta = meta.unwrap();
640        assert_eq!(meta.title(), Some(&String::from("TITLE")));
641        assert_eq!(meta.author(), Some(&String::from("AUTHOR")));
642        assert_eq!(meta.group(), Some(&String::from("GROUP")));
643        assert_eq!(meta.date(), Some(&String::from("19700101")));
644        assert_eq!(meta.size(), 416);
645        assert_eq!(meta.r#type(), (1, 1));
646        assert_eq!(meta.dimensions(), (32, 8));
647        assert_eq!(meta.flags(), (0, 0, 1));
648        assert_eq!(meta.font(), Some(&String::from("IBM VGA")));
649        assert_eq!(meta.notes(), &Vec::<String>::new());
650
651        return Ok(());
652    }
653
654    #[test]
655    fn notes() -> Result<(), String> {
656        let meta = get("res/test/comments.ans")?;
657        assert!(meta.is_some());
658        let meta = meta.unwrap();
659        assert_eq!(meta.title(), Some(&String::from("TITLE")));
660        assert_eq!(meta.author(), Some(&String::from("AUTHOR")));
661        assert_eq!(meta.group(), Some(&String::from("GROUP")));
662        assert_eq!(meta.date(), Some(&String::from("19700101")));
663        assert_eq!(meta.size(), 416);
664        assert_eq!(meta.r#type(), (1, 1));
665        assert_eq!(meta.dimensions(), (32, 8));
666        assert_eq!(meta.flags(), (0, 0, 1));
667        assert_eq!(meta.font(), Some(&String::from("IBM VGA")));
668        assert_eq!(meta.notes(), &vec!["Lorem", "ipsum", "dolor", "sit", "amet"]);
669
670        return Ok(());
671    }
672
673    #[test]
674    fn empty() -> Result<(), String> {
675        let meta = get("res/test/empty.ans")?;
676        assert!(meta.is_none());
677
678        return Ok(());
679    }
680
681    #[test]
682    fn no_data() -> Result<(), String> {
683        let meta = get("res/test/no_data.ans")?;
684        assert!(meta.is_some());
685        let meta = meta.unwrap();
686        assert_eq!(meta.size(), 0);
687
688        return Ok(());
689    }
690
691    #[test]
692    fn one_hundred_twenty_eight_bytes() -> Result<(), String> {
693        let meta = get("res/test/128_bytes.ans")?;
694        assert!(meta.is_none());
695
696        return Ok(());
697    }
698
699    mod raw {
700        use super::*;
701
702        use pretty_assertions::assert_eq;
703
704        #[test]
705        fn none() -> Result<(), String> {
706            let meta = read_raw(&mut File::open("res/test/simple.ans").map_err(|err| return err.to_string())?)?;
707            assert!(meta.is_none());
708
709            return Ok(());
710        }
711
712        #[test]
713        fn some() -> Result<(), String> {
714            let meta = read_raw(&mut File::open("res/test/meta.ans").map_err(|err| return err.to_string())?)?;
715            assert!(meta.is_some());
716            assert_eq!(
717                meta.unwrap(),
718                b"\x1ASAUCE00"
719                    .iter()
720                    .cloned()
721                    .chain(format!("{:<35}", "TITLE").bytes()) // Title
722                    .chain(format!("{:<20}", "AUTHOR").bytes()) // Author
723                    .chain(format!("{:<20}", "GROUP").bytes()) // Group
724                    .chain(b"19700101".iter().cloned()) // Date
725                    .chain(416u32.to_le_bytes()) // Size
726                    .chain([1u8, 1u8]) // Type
727                    .chain(32u16.to_le_bytes()) // Width
728                    .chain(8u16.to_le_bytes()) // Height
729                    .chain(0u32.to_le_bytes())
730                    .chain([0u8]) // Notes
731                    .chain([0x01u8]) // Flags
732                    .chain(format!("{:\0<22}", "IBM VGA").bytes()) // Font
733                    .collect::<Vec<u8>>(),
734            );
735
736            return Ok(());
737        }
738
739        #[test]
740        fn notes() -> Result<(), String> {
741            let meta = read_raw(&mut File::open("res/test/comments.ans").map_err(|err| return err.to_string())?)?;
742            assert!(meta.is_some());
743            assert_eq!(
744                meta.unwrap(),
745                b"\x1ACOMNT"
746                    .iter()
747                    .cloned()
748                    .chain(format!("{:<64}", "Lorem").bytes()) // Comment
749                    .chain(format!("{:<64}", "ipsum").bytes()) // Comment
750                    .chain(format!("{:<64}", "dolor").bytes()) // Comment
751                    .chain(format!("{:<64}", "sit").bytes()) // Comment
752                    .chain(format!("{:<64}", "amet").bytes()) // Comment
753                    .chain(b"SAUCE00".iter().cloned())
754                    .chain(format!("{:<35}", "TITLE").bytes()) // Title
755                    .chain(format!("{:<20}", "AUTHOR").bytes()) // Author
756                    .chain(format!("{:<20}", "GROUP").bytes()) // Group
757                    .chain(b"19700101".iter().cloned()) // Date
758                    .chain(416u32.to_le_bytes()) // Size
759                    .chain([1u8, 1u8]) // Type
760                    .chain(32u16.to_le_bytes()) // Width
761                    .chain(8u16.to_le_bytes()) // Height
762                    .chain(0u32.to_le_bytes())
763                    .chain([5u8]) // Notes
764                    .chain([0x01u8]) // Flags
765                    .chain(format!("{:\0<22}", "IBM VGA").bytes()) // Font
766                    .collect::<Vec<u8>>(),
767            );
768
769            return Ok(());
770        }
771
772        #[test]
773        fn empty() -> Result<(), String> {
774            let meta = read_raw(&mut File::open("res/test/empty.ans").map_err(|err| return err.to_string())?)?;
775            assert!(meta.is_none());
776
777            return Ok(());
778        }
779
780        #[test]
781        fn no_data() -> Result<(), String> {
782            let meta = read_raw(&mut File::open("res/test/no_data.ans").map_err(|err| return err.to_string())?)?;
783            assert!(meta.is_some());
784            assert_eq!(
785                meta.unwrap(),
786                b"\x1ASAUCE00"
787                    .iter()
788                    .cloned()
789                    .chain(format!("{:<35}", "TITLE").bytes()) // Title
790                    .chain(format!("{:<20}", "AUTHOR").bytes()) // Author
791                    .chain(format!("{:<20}", "GROUP").bytes()) // Group
792                    .chain(b"19700101".iter().cloned()) // Date
793                    .chain(0u32.to_le_bytes()) // Size
794                    .chain([1u8, 1u8]) // Type
795                    .chain(32u16.to_le_bytes()) // Width
796                    .chain(8u16.to_le_bytes()) // Height
797                    .chain(0u32.to_le_bytes())
798                    .chain([0u8]) // Notes
799                    .chain([0x01u8]) // Flags
800                    .chain(format!("{:\0<22}", "IBM VGA").bytes()) // Font
801                    .collect::<Vec<u8>>(),
802            );
803
804            return Ok(());
805        }
806
807        #[test]
808        fn one_hundred_twenty_eight_bytes() -> Result<(), String> {
809            let meta = read_raw(&mut File::open("res/test/128_bytes.ans").map_err(|err| return err.to_string())?)?;
810            assert!(meta.is_none());
811
812            return Ok(());
813        }
814    }
815
816    mod check {
817        use super::*;
818
819        mod meta {
820            use super::*;
821
822            #[test]
823            fn none() -> Result<(), String> {
824                return check(None);
825            }
826
827            #[test]
828            fn some() -> Result<(), String> {
829                return check(Some(&Meta::default()));
830            }
831        }
832
833        mod date {
834            use super::*;
835
836            #[test]
837            fn valid() -> Result<(), String> {
838                return check_date(Some(&Meta { date: String::from("19700101"), ..Default::default() }));
839            }
840
841            #[test]
842            fn invalid() {
843                assert!(check_date(Some(&Meta { date: String::from("X"), ..Default::default() })).is_err());
844            }
845
846            #[test]
847            fn illegal() {
848                assert!(check_date(Some(&Meta { date: String::from("19700230"), ..Default::default() })).is_err());
849            }
850        }
851
852        mod flags {
853            use super::*;
854
855            use pretty_assertions::assert_eq;
856
857            #[test]
858            fn b_0() {
859                assert!(check_flags(Some(&Meta { flags: 0x00, ..Default::default() })).is_err());
860            }
861
862            #[test]
863            fn ls_00() -> Result<(), String> {
864                return check_flags(Some(&Meta { flags: 0x01, ..Default::default() }));
865            }
866
867            #[test]
868            fn ls_01() -> Result<(), String> {
869                return check_flags(Some(&Meta { flags: 0x03, ..Default::default() }));
870            }
871
872            #[test]
873            fn ls_10() -> Result<(), String> {
874                return check_flags(Some(&Meta { flags: 0x05, ..Default::default() }));
875            }
876
877            #[test]
878            fn ls_11() {
879                assert!(check_flags(Some(&Meta { flags: 0x07, ..Default::default() })).is_err());
880            }
881
882            #[test]
883            fn ar_00() -> Result<(), String> {
884                return check_flags(Some(&Meta { flags: 0x01, ..Default::default() }));
885            }
886
887            #[test]
888            fn ar_01() -> Result<(), String> {
889                return check_flags(Some(&Meta { flags: 0x09, ..Default::default() }));
890            }
891
892            #[test]
893            fn ar_10() -> Result<(), String> {
894                return check_flags(Some(&Meta { flags: 0x11, ..Default::default() }));
895            }
896
897            #[test]
898            fn ar_11() {
899                assert!(check_flags(Some(&Meta { flags: 0x19, ..Default::default() })).is_err());
900            }
901
902            #[test]
903            fn invalid() {
904                assert!(check_flags(Some(&Meta { flags: 0x21, ..Default::default() })).is_err());
905            }
906
907            #[test]
908            fn ratio_1_00() {
909                assert_eq!((Meta { flags: 0x11, ..Default::default() }).stretch(), 1.00);
910            }
911
912            #[test]
913            fn ratio_1_20() {
914                assert_eq!((Meta { flags: 0x03, ..Default::default() }).stretch(), 1.20);
915            }
916
917            #[test]
918            fn ratio_1_35() {
919                assert_eq!((Meta { flags: 0x01, ..Default::default() }).stretch(), 1.35);
920            }
921
922            #[test]
923            fn font_size_8x16() {
924                assert_eq!((Meta { flags: 0x03, ..Default::default() }).font_size(), (8, 16));
925            }
926
927            #[test]
928            fn font_size_9x16() {
929                assert_eq!((Meta { flags: 0x01, ..Default::default() }).font_size(), (9, 16));
930            }
931        }
932
933        mod font {
934            use super::*;
935
936            use pretty_assertions::assert_eq;
937
938            #[test]
939            fn valid() -> Result<(), String> {
940                return check_font(Some(&Meta { font: String::from("IBM VGA"), ..Default::default() }));
941            }
942
943            #[test]
944            fn invalid() {
945                assert!(check_font(Some(&Meta { font: String::from("X"), ..Default::default() })).is_err());
946            }
947
948            #[test]
949            fn font_face_8x16() {
950                assert_eq!(
951                    (Meta { flags: 0x03, ..Default::default() }).font_face_otb().raw_face().data,
952                    fonts::VGA_8X16.raw_face().data,
953                );
954            }
955
956            #[test]
957            fn font_face_9x16() {
958                assert_eq!(
959                    (Meta { flags: 0x01, ..Default::default() }).font_face_otb().raw_face().data,
960                    fonts::VGA_9X16.raw_face().data,
961                );
962            }
963        }
964
965        mod notes {
966            use super::*;
967
968            #[test]
969            fn empty() -> Result<(), String> {
970                return check_notes(Some(&Meta { notes: vec![], ..Default::default() }));
971            }
972
973            #[test]
974            fn not_empty() -> Result<(), String> {
975                return check_notes(Some(&Meta { notes: vec![String::new()], ..Default::default() }));
976            }
977
978            #[test]
979            fn too_many() {
980                assert!(check_notes(Some(&Meta { notes: vec![String::new(); 256], ..Default::default() })).is_err());
981            }
982        }
983
984        mod str {
985            use super::*;
986
987            use pretty_assertions::assert_eq;
988
989            #[test]
990            fn valid() -> Result<(), String> {
991                return check_str(&String::from("string"), "name", 99);
992            }
993
994            #[test]
995            fn valid_non_ascii() -> Result<(), String> {
996                return check_str(&String::from("░"), "name", 99);
997            }
998
999            #[test]
1000            fn long() {
1001                let result = check_str(&String::from("string"), "name", 0);
1002                assert!(result.is_err());
1003                assert_eq!(result.unwrap_err(), "name is too long (expected <=0, got 6)");
1004            }
1005
1006            #[test]
1007            fn control() {
1008                let result = check_str(&String::from("\0"), "name", 99);
1009                assert!(result.is_err());
1010                assert_eq!(result.unwrap_err(), "name contains illegal characters (0x00 is a control character)");
1011            }
1012
1013            #[test]
1014            fn invalid() {
1015                let result = check_str(&String::from("🚫"), "name", 99);
1016                assert!(result.is_err());
1017                assert_eq!(
1018                    result.unwrap_err(),
1019                    "name contains illegal characters (🚫 (U+1F6AB) is not a valid CP437 character)",
1020                );
1021            }
1022        }
1023
1024        mod r#type {
1025            use super::*;
1026
1027            #[test]
1028            fn none() -> Result<(), String> {
1029                return check_type(Some(&Meta { r#type: (0, 0), ..Default::default() }));
1030            }
1031
1032            #[test]
1033            fn ascii() -> Result<(), String> {
1034                return check_type(Some(&Meta { r#type: (1, 0), ..Default::default() }));
1035            }
1036
1037            #[test]
1038            fn ansi() -> Result<(), String> {
1039                return check_type(Some(&Meta { r#type: (1, 1), ..Default::default() }));
1040            }
1041
1042            #[test]
1043            fn bitmap() {
1044                assert!(check_type(Some(&Meta { r#type: (2, 0), ..Default::default() })).is_err());
1045            }
1046
1047            #[test]
1048            fn vector() {
1049                assert!(check_type(Some(&Meta { r#type: (3, 0), ..Default::default() })).is_err());
1050            }
1051
1052            #[test]
1053            fn audio() {
1054                assert!(check_type(Some(&Meta { r#type: (4, 0), ..Default::default() })).is_err());
1055            }
1056
1057            #[test]
1058            fn binary_test() {
1059                assert!(check_type(Some(&Meta { r#type: (5, 0), ..Default::default() })).is_err());
1060            }
1061
1062            #[test]
1063            fn xbin() {
1064                assert!(check_type(Some(&Meta { r#type: (6, 0), ..Default::default() })).is_err());
1065            }
1066
1067            #[test]
1068            fn archive() {
1069                assert!(check_type(Some(&Meta { r#type: (7, 0), ..Default::default() })).is_err());
1070            }
1071
1072            #[test]
1073            fn executable() {
1074                assert!(check_type(Some(&Meta { r#type: (8, 0), ..Default::default() })).is_err());
1075            }
1076
1077            #[test]
1078            fn ansimation() {
1079                assert!(check_type(Some(&Meta { r#type: (1, 2), ..Default::default() })).is_err());
1080            }
1081
1082            #[test]
1083            fn rip_script() {
1084                assert!(check_type(Some(&Meta { r#type: (1, 3), ..Default::default() })).is_err());
1085            }
1086
1087            #[test]
1088            fn pcboard() {
1089                assert!(check_type(Some(&Meta { r#type: (1, 4), ..Default::default() })).is_err());
1090            }
1091
1092            #[test]
1093            fn avatar() {
1094                assert!(check_type(Some(&Meta { r#type: (1, 5), ..Default::default() })).is_err());
1095            }
1096
1097            #[test]
1098            fn html() {
1099                assert!(check_type(Some(&Meta { r#type: (1, 6), ..Default::default() })).is_err());
1100            }
1101
1102            #[test]
1103            fn source() {
1104                assert!(check_type(Some(&Meta { r#type: (1, 7), ..Default::default() })).is_err());
1105            }
1106
1107            #[test]
1108            fn tundra_draw() {
1109                assert!(check_type(Some(&Meta { r#type: (1, 8), ..Default::default() })).is_err());
1110            }
1111        }
1112    }
1113}