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 DictionaryConfig {
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 DictionariesConfig {
48 pub dictionaries: HashMap<String, DictionaryConfig>,
50 #[serde(default)]
52 pub compression: HashMap<String, CompressionConfig>,
53 #[serde(default)]
55 pub settings: Settings,
56}
57
58#[derive(Debug, Deserialize, Clone)]
60pub struct CompressionConfig {
61 pub default_level: u32,
63}
64
65#[derive(Debug, Deserialize, Clone, Default)]
67pub struct Settings {
68 #[serde(default = "default_dictionary")]
70 pub default_dictionary: String,
71}
72
73fn default_dictionary() -> String {
74 "base64".to_string()
75}
76
77impl DictionariesConfig {
78 pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
80 toml::from_str(content)
81 }
82
83 pub fn load_default() -> Result<Self, Box<dyn std::error::Error>> {
87 let content = include_str!("../../dictionaries.toml");
88 Ok(Self::from_toml(content)?)
89 }
90
91 pub fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
93 let content = std::fs::read_to_string(path)?;
94 Ok(Self::from_toml(&content)?)
95 }
96
97 pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
106 let mut config = Self::load_default()?;
107
108 if let Some(config_dir) = dirs::config_dir() {
110 let user_config_path = config_dir.join("base-d").join("dictionaries.toml");
111 if user_config_path.exists() {
112 match Self::load_from_file(&user_config_path) {
113 Ok(user_config) => {
114 config.merge(user_config);
115 }
116 Err(e) => {
117 eprintln!("Warning: Failed to load user config from {:?}: {}", user_config_path, e);
118 }
119 }
120 }
121 }
122
123 let local_config_path = std::path::Path::new("dictionaries.toml");
125 if local_config_path.exists() {
126 match Self::load_from_file(local_config_path) {
127 Ok(local_config) => {
128 config.merge(local_config);
129 }
130 Err(e) => {
131 eprintln!("Warning: Failed to load local config from {:?}: {}", local_config_path, e);
132 }
133 }
134 }
135
136 Ok(config)
137 }
138
139 pub fn merge(&mut self, other: DictionariesConfig) {
143 for (name, dictionary) in other.dictionaries {
144 self.dictionaries.insert(name, dictionary);
145 }
146 }
147
148 pub fn get_dictionary(&self, name: &str) -> Option<&DictionaryConfig> {
150 self.dictionaries.get(name)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_load_default_config() {
160 let config = DictionariesConfig::load_default().unwrap();
161 assert!(config.dictionaries.contains_key("cards"));
162 }
163
164 #[test]
165 fn test_cards_alphabet_length() {
166 let config = DictionariesConfig::load_default().unwrap();
167 let cards = config.get_dictionary("cards").unwrap();
168 assert_eq!(cards.chars.chars().count(), 52);
169 }
170
171 #[test]
172 fn test_base64_chunked_mode() {
173 let config = DictionariesConfig::load_default().unwrap();
174 let base64 = config.get_dictionary("base64").unwrap();
175 assert_eq!(base64.mode, EncodingMode::Chunked);
176 assert_eq!(base64.padding, Some("=".to_string()));
177 }
178
179 #[test]
180 fn test_base64_math_mode() {
181 let config = DictionariesConfig::load_default().unwrap();
182 let base64_math = config.get_dictionary("base64_math").unwrap();
183 assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
184 }
185
186 #[test]
187 fn test_merge_configs() {
188 let mut config1 = DictionariesConfig {
189 dictionaries: HashMap::new(),
190 compression: HashMap::new(),
191 settings: Settings::default(),
192 };
193 config1.dictionaries.insert("test1".to_string(), DictionaryConfig {
194 chars: "ABC".to_string(),
195 mode: EncodingMode::BaseConversion,
196 padding: None,
197 start_codepoint: None,
198 });
199
200 let mut config2 = DictionariesConfig {
201 dictionaries: HashMap::new(),
202 compression: HashMap::new(),
203 settings: Settings::default(),
204 };
205 config2.dictionaries.insert("test2".to_string(), DictionaryConfig {
206 chars: "XYZ".to_string(),
207 mode: EncodingMode::BaseConversion,
208 padding: None,
209 start_codepoint: None,
210 });
211 config2.dictionaries.insert("test1".to_string(), DictionaryConfig {
212 chars: "DEF".to_string(),
213 mode: EncodingMode::BaseConversion,
214 padding: None,
215 start_codepoint: None,
216 });
217
218 config1.merge(config2);
219
220 assert_eq!(config1.dictionaries.len(), 2);
221 assert_eq!(config1.get_dictionary("test1").unwrap().chars, "DEF");
222 assert_eq!(config1.get_dictionary("test2").unwrap().chars, "XYZ");
223 }
224
225 #[test]
226 fn test_load_from_toml_string() {
227 let toml_content = r#"
228[dictionaries.custom]
229chars = "0123456789"
230mode = "base_conversion"
231"#;
232 let config = DictionariesConfig::from_toml(toml_content).unwrap();
233 assert!(config.dictionaries.contains_key("custom"));
234 assert_eq!(config.get_dictionary("custom").unwrap().chars, "0123456789");
235 }
236}
237