1use serde::Deserialize;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10pub enum EncodingMode {
11 BaseConversion,
14 Chunked,
17 ByteRange,
20}
21
22impl Default for EncodingMode {
23 fn default() -> Self {
24 EncodingMode::BaseConversion
25 }
26}
27
28#[derive(Debug, Deserialize, Clone)]
30pub struct AlphabetConfig {
31 #[serde(default)]
33 pub chars: String,
34 #[serde(default)]
36 pub mode: EncodingMode,
37 #[serde(default)]
39 pub padding: Option<String>,
40 #[serde(default)]
42 pub start_codepoint: Option<u32>,
43}
44
45#[derive(Debug, Deserialize)]
47pub struct AlphabetsConfig {
48 pub alphabets: HashMap<String, AlphabetConfig>,
50}
51
52impl AlphabetsConfig {
53 pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
55 toml::from_str(content)
56 }
57
58 pub fn load_default() -> Result<Self, Box<dyn std::error::Error>> {
62 let content = include_str!("../alphabets.toml");
63 Ok(Self::from_toml(content)?)
64 }
65
66 pub fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
68 let content = std::fs::read_to_string(path)?;
69 Ok(Self::from_toml(&content)?)
70 }
71
72 pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
81 let mut config = Self::load_default()?;
82
83 if let Some(config_dir) = dirs::config_dir() {
85 let user_config_path = config_dir.join("base-d").join("alphabets.toml");
86 if user_config_path.exists() {
87 match Self::load_from_file(&user_config_path) {
88 Ok(user_config) => {
89 config.merge(user_config);
90 }
91 Err(e) => {
92 eprintln!("Warning: Failed to load user config from {:?}: {}", user_config_path, e);
93 }
94 }
95 }
96 }
97
98 let local_config_path = std::path::Path::new("alphabets.toml");
100 if local_config_path.exists() {
101 match Self::load_from_file(local_config_path) {
102 Ok(local_config) => {
103 config.merge(local_config);
104 }
105 Err(e) => {
106 eprintln!("Warning: Failed to load local config from {:?}: {}", local_config_path, e);
107 }
108 }
109 }
110
111 Ok(config)
112 }
113
114 pub fn merge(&mut self, other: AlphabetsConfig) {
118 for (name, alphabet) in other.alphabets {
119 self.alphabets.insert(name, alphabet);
120 }
121 }
122
123 pub fn get_alphabet(&self, name: &str) -> Option<&AlphabetConfig> {
125 self.alphabets.get(name)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_load_default_config() {
135 let config = AlphabetsConfig::load_default().unwrap();
136 assert!(config.alphabets.contains_key("cards"));
137 }
138
139 #[test]
140 fn test_cards_alphabet_length() {
141 let config = AlphabetsConfig::load_default().unwrap();
142 let cards = config.get_alphabet("cards").unwrap();
143 assert_eq!(cards.chars.chars().count(), 52);
144 }
145
146 #[test]
147 fn test_base64_chunked_mode() {
148 let config = AlphabetsConfig::load_default().unwrap();
149 let base64 = config.get_alphabet("base64").unwrap();
150 assert_eq!(base64.mode, EncodingMode::Chunked);
151 assert_eq!(base64.padding, Some("=".to_string()));
152 }
153
154 #[test]
155 fn test_base64_math_mode() {
156 let config = AlphabetsConfig::load_default().unwrap();
157 let base64_math = config.get_alphabet("base64_math").unwrap();
158 assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
159 }
160
161 #[test]
162 fn test_merge_configs() {
163 let mut config1 = AlphabetsConfig {
164 alphabets: HashMap::new(),
165 };
166 config1.alphabets.insert("test1".to_string(), AlphabetConfig {
167 chars: "ABC".to_string(),
168 mode: EncodingMode::BaseConversion,
169 padding: None,
170 start_codepoint: None,
171 });
172
173 let mut config2 = AlphabetsConfig {
174 alphabets: HashMap::new(),
175 };
176 config2.alphabets.insert("test2".to_string(), AlphabetConfig {
177 chars: "XYZ".to_string(),
178 mode: EncodingMode::BaseConversion,
179 padding: None,
180 start_codepoint: None,
181 });
182 config2.alphabets.insert("test1".to_string(), AlphabetConfig {
183 chars: "DEF".to_string(),
184 mode: EncodingMode::BaseConversion,
185 padding: None,
186 start_codepoint: None,
187 });
188
189 config1.merge(config2);
190
191 assert_eq!(config1.alphabets.len(), 2);
192 assert_eq!(config1.get_alphabet("test1").unwrap().chars, "DEF");
193 assert_eq!(config1.get_alphabet("test2").unwrap().chars, "XYZ");
194 }
195
196 #[test]
197 fn test_load_from_toml_string() {
198 let toml_content = r#"
199[alphabets.custom]
200chars = "0123456789"
201mode = "base_conversion"
202"#;
203 let config = AlphabetsConfig::from_toml(toml_content).unwrap();
204 assert!(config.alphabets.contains_key("custom"));
205 assert_eq!(config.get_alphabet("custom").unwrap().chars, "0123456789");
206 }
207}
208