1use 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#[doc(alias = "Sauce")]
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct Meta {
24 pub title: String,
26 pub author: String,
28 #[doc(alias = "team")]
30 pub group: String,
31 pub date: String,
33 pub size: u32,
35 pub r#type: (u8, u8),
45 pub width: u16,
47 pub height: u16,
49 #[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 pub font: String,
66 #[doc(alias = "comments")]
68 pub notes: Vec<String>,
69}
70
71impl Default for Meta {
76 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 #[must_use]
100 pub fn title(&self) -> Option<&String> {
101 return if self.title.is_empty() { None } else { Some(&self.title) };
102 }
103
104 #[must_use]
109 pub fn author(&self) -> Option<&String> {
110 return if self.author.is_empty() { None } else { Some(&self.author) };
111 }
112
113 #[must_use]
118 pub fn group(&self) -> Option<&String> {
119 return if self.group.is_empty() { None } else { Some(&self.group) };
120 }
121
122 #[must_use]
127 pub fn date(&self) -> Option<&String> {
128 return if self.date.is_empty() { None } else { Some(&self.date) };
129 }
130
131 #[inline]
136 #[must_use]
137 pub fn size(&self) -> u32 {
138 return self.size;
139 }
140
141 #[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 #[must_use]
159 pub fn width(&self) -> u16 {
160 return if self.width > 0 { self.width } else { Meta::default().width };
161 }
162
163 #[must_use]
170 pub fn height(&self) -> u16 {
171 return if self.height > 0 { self.height } else { Meta::default().height };
172 }
173
174 #[inline]
181 #[must_use]
182 pub fn dimensions(&self) -> (u16, u16) {
183 return (self.width(), self.height());
184 }
185
186 #[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 #[must_use]
200 pub fn font(&self) -> Option<&String> {
201 return if self.font.is_empty() { None } else { Some(&self.font) };
202 }
203
204 #[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 #[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 #[inline]
227 #[must_use]
228 pub fn notes(&self) -> &Vec<String> {
229 return &self.notes;
230 }
231
232 #[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 #[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 #[must_use]
263 pub fn font_width(&self) -> u8 {
264 return if self.flags().1 == 0b01 { 8 } else { 9 };
265 }
266
267 #[inline]
269 #[must_use]
270 pub fn font_height(&self) -> u8 {
271 return 16;
272 }
273
274 #[inline]
281 #[must_use]
282 pub fn font_size(&self) -> (u8, u8) {
283 return (self.font_width(), self.font_height());
284 }
285}
286
287#[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
302pub 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#[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#[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#[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#[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#[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#[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#[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#[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 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#[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 return Err(format!("Font is unsupported ({})", m.font));
511 }
512 }
513
514 return Ok(());
515}
516
517#[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#[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()) .chain(format!("{:<20}", "AUTHOR").bytes()) .chain(format!("{:<20}", "GROUP").bytes()) .chain(b"19700101".iter().cloned()) .chain(416u32.to_le_bytes()) .chain([1u8, 1u8]) .chain(32u16.to_le_bytes()) .chain(8u16.to_le_bytes()) .chain(0u32.to_le_bytes())
730 .chain([0u8]) .chain([0x01u8]) .chain(format!("{:\0<22}", "IBM VGA").bytes()) .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()) .chain(format!("{:<64}", "ipsum").bytes()) .chain(format!("{:<64}", "dolor").bytes()) .chain(format!("{:<64}", "sit").bytes()) .chain(format!("{:<64}", "amet").bytes()) .chain(b"SAUCE00".iter().cloned())
754 .chain(format!("{:<35}", "TITLE").bytes()) .chain(format!("{:<20}", "AUTHOR").bytes()) .chain(format!("{:<20}", "GROUP").bytes()) .chain(b"19700101".iter().cloned()) .chain(416u32.to_le_bytes()) .chain([1u8, 1u8]) .chain(32u16.to_le_bytes()) .chain(8u16.to_le_bytes()) .chain(0u32.to_le_bytes())
763 .chain([5u8]) .chain([0x01u8]) .chain(format!("{:\0<22}", "IBM VGA").bytes()) .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()) .chain(format!("{:<20}", "AUTHOR").bytes()) .chain(format!("{:<20}", "GROUP").bytes()) .chain(b"19700101".iter().cloned()) .chain(0u32.to_le_bytes()) .chain([1u8, 1u8]) .chain(32u16.to_le_bytes()) .chain(8u16.to_le_bytes()) .chain(0u32.to_le_bytes())
798 .chain([0u8]) .chain([0x01u8]) .chain(format!("{:\0<22}", "IBM VGA").bytes()) .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}