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