Skip to main content

matrix_rain/
charset.rs

1use crate::error::MatrixError;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub enum CharSet {
5    Matrix,
6    Ascii,
7    Hex,
8    Binary,
9    Custom(Vec<char>),
10}
11
12impl CharSet {
13    pub(crate) fn chars(&self) -> &[char] {
14        match self {
15            Self::Matrix => MATRIX_CHARS,
16            Self::Ascii => ASCII_CHARS,
17            Self::Hex => HEX_CHARS,
18            Self::Binary => BINARY_CHARS,
19            Self::Custom(v) => v.as_slice(),
20        }
21    }
22
23    pub(crate) fn validate(&self) -> Result<(), MatrixError> {
24        let chars = self.chars();
25        if chars.is_empty() {
26            return Err(MatrixError::EmptyCharset);
27        }
28        for c in chars {
29            if c.is_control() {
30                return Err(MatrixError::InvalidConfig(format!(
31                    "charset contains control character U+{:04X}",
32                    *c as u32
33                )));
34            }
35        }
36        Ok(())
37    }
38}
39
40const MATRIX_CHARS: &[char] = &[
41    'ヲ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ',
42    'ー', 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ',
43    'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ',
44    'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ',
45    'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ',
46    'リ', 'ル', 'レ', 'ロ', 'ワ', 'ン', '0', '1', '2', '3',
47    '4', '5', '6', '7', '8', '9',
48];
49
50const ASCII_CHARS: &[char] = &[
51    '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*',
52    '+', ',', '-', '.', '/', '0', '1', '2', '3', '4',
53    '5', '6', '7', '8', '9', ':', ';', '<', '=', '>',
54    '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
55    'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
56    'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',
57    ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f',
58    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
59    'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
60    '{', '|', '}', '~',
61];
62
63const HEX_CHARS: &[char] = &[
64    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
65];
66
67const BINARY_CHARS: &[char] = &['0', '1'];
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn matrix_chars_non_empty() {
75        assert!(!CharSet::Matrix.chars().is_empty());
76    }
77
78    #[test]
79    fn matrix_chars_include_all_digits() {
80        let chars = CharSet::Matrix.chars();
81        for d in '0'..='9' {
82            assert!(chars.contains(&d), "Matrix charset missing digit {d}");
83        }
84    }
85
86    #[test]
87    fn matrix_chars_include_katakana() {
88        let chars = CharSet::Matrix.chars();
89        assert!(chars.contains(&'ヲ'));
90        assert!(chars.contains(&'ン'));
91    }
92
93    #[test]
94    fn ascii_chars_exclude_space_and_control() {
95        let chars = CharSet::Ascii.chars();
96        assert!(!chars.is_empty());
97        assert!(!chars.contains(&' '));
98        assert!(!chars.contains(&'\n'));
99        assert!(!chars.contains(&'\t'));
100        assert!(chars.contains(&'!'));
101        assert!(chars.contains(&'~'));
102        assert!(chars.contains(&'A'));
103        assert!(chars.contains(&'z'));
104        assert!(chars.contains(&'0'));
105    }
106
107    #[test]
108    fn hex_chars_are_digits_and_lower_af() {
109        let chars = CharSet::Hex.chars();
110        assert_eq!(chars.len(), 16);
111        for d in '0'..='9' {
112            assert!(chars.contains(&d));
113        }
114        for d in 'a'..='f' {
115            assert!(chars.contains(&d));
116        }
117        // No uppercase per spec ("0–9 a–f").
118        assert!(!chars.contains(&'A'));
119    }
120
121    #[test]
122    fn binary_chars_are_zero_and_one() {
123        assert_eq!(CharSet::Binary.chars(), &['0', '1']);
124    }
125
126    #[test]
127    fn custom_passthrough() {
128        let cs = CharSet::Custom(vec!['a', 'b', 'c']);
129        assert_eq!(cs.chars(), &['a', 'b', 'c']);
130    }
131
132    #[test]
133    fn validate_passes_for_all_builtins() {
134        for cs in [CharSet::Matrix, CharSet::Ascii, CharSet::Hex, CharSet::Binary] {
135            assert!(cs.validate().is_ok(), "{cs:?} should validate");
136        }
137    }
138
139    #[test]
140    fn validate_rejects_empty_custom() {
141        let err = CharSet::Custom(vec![]).validate().unwrap_err();
142        assert!(matches!(err, MatrixError::EmptyCharset));
143    }
144
145    #[test]
146    fn validate_rejects_control_chars() {
147        for bad in ['\n', '\r', '\t', '\0', '\x07'] {
148            let err = CharSet::Custom(vec!['a', bad, 'b']).validate().unwrap_err();
149            assert!(
150                matches!(err, MatrixError::InvalidConfig(_)),
151                "control char {bad:?} should be rejected"
152            );
153        }
154    }
155
156    #[test]
157    fn validate_accepts_single_char_custom() {
158        assert!(CharSet::Custom(vec!['x']).validate().is_ok());
159    }
160
161    #[test]
162    fn validate_does_not_check_display_width() {
163        // Full-width / combining chars are NOT detected per spec §5.4 — caller's responsibility.
164        // Just confirm validation passes for one example so the test documents the policy.
165        assert!(CharSet::Custom(vec!['漢']).validate().is_ok());
166    }
167}