1use serde::Deserialize;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10#[derive(Default)]
11pub enum EncodingMode {
12 #[default]
15 BaseConversion,
16 Chunked,
19 ByteRange,
22}
23
24#[derive(Debug, Deserialize, Clone)]
26pub struct DictionaryConfig {
27 #[serde(default)]
29 pub chars: String,
30 #[serde(default)]
32 pub mode: EncodingMode,
33 #[serde(default)]
35 pub padding: Option<String>,
36 #[serde(default)]
38 pub start_codepoint: Option<u32>,
39 #[serde(default = "default_true")]
42 pub common: bool,
43}
44
45fn default_true() -> bool {
46 true
47}
48
49#[derive(Debug, Deserialize)]
51pub struct DictionaryRegistry {
52 pub dictionaries: HashMap<String, DictionaryConfig>,
54 #[serde(default)]
56 pub compression: HashMap<String, CompressionConfig>,
57 #[serde(default)]
59 pub settings: Settings,
60}
61
62#[derive(Debug, Deserialize, Clone)]
64pub struct CompressionConfig {
65 pub default_level: u32,
67}
68
69#[derive(Debug, Deserialize, Clone, Default)]
71pub struct XxHashSettings {
72 #[serde(default)]
74 pub default_seed: u64,
75 #[serde(default)]
77 pub default_secret_file: Option<String>,
78}
79
80#[derive(Debug, Deserialize, Clone, Default)]
82pub struct Settings {
83 #[serde(default)]
85 pub default_dictionary: Option<String>,
86 #[serde(default)]
88 pub xxhash: XxHashSettings,
89}
90
91impl DictionaryRegistry {
92 pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
94 toml::from_str(content)
95 }
96
97 pub fn load_default() -> Result<Self, Box<dyn std::error::Error>> {
101 let content = include_str!("../../dictionaries.toml");
102 Ok(Self::from_toml(content)?)
103 }
104
105 pub fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
107 let content = std::fs::read_to_string(path)?;
108 Ok(Self::from_toml(&content)?)
109 }
110
111 pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
120 let mut config = Self::load_default()?;
121
122 if let Some(config_dir) = dirs::config_dir() {
124 let user_config_path = config_dir.join("base-d").join("dictionaries.toml");
125 if user_config_path.exists() {
126 match Self::load_from_file(&user_config_path) {
127 Ok(user_config) => {
128 config.merge(user_config);
129 }
130 Err(e) => {
131 eprintln!(
132 "Warning: Failed to load user config from {:?}: {}",
133 user_config_path, e
134 );
135 }
136 }
137 }
138 }
139
140 let local_config_path = std::path::Path::new("dictionaries.toml");
142 if local_config_path.exists() {
143 match Self::load_from_file(local_config_path) {
144 Ok(local_config) => {
145 config.merge(local_config);
146 }
147 Err(e) => {
148 eprintln!(
149 "Warning: Failed to load local config from {:?}: {}",
150 local_config_path, e
151 );
152 }
153 }
154 }
155
156 Ok(config)
157 }
158
159 pub fn merge(&mut self, other: DictionaryRegistry) {
163 for (name, dictionary) in other.dictionaries {
164 self.dictionaries.insert(name, dictionary);
165 }
166 }
167
168 pub fn get_dictionary(&self, name: &str) -> Option<&DictionaryConfig> {
170 self.dictionaries.get(name)
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_load_default_config() {
180 let config = DictionaryRegistry::load_default().unwrap();
181 assert!(config.dictionaries.contains_key("cards"));
182 }
183
184 #[test]
185 fn test_cards_dictionary_length() {
186 let config = DictionaryRegistry::load_default().unwrap();
187 let cards = config.get_dictionary("cards").unwrap();
188 assert_eq!(cards.chars.chars().count(), 52);
189 }
190
191 #[test]
192 fn test_base64_chunked_mode() {
193 let config = DictionaryRegistry::load_default().unwrap();
194 let base64 = config.get_dictionary("base64").unwrap();
195 assert_eq!(base64.mode, EncodingMode::Chunked);
196 assert_eq!(base64.padding, Some("=".to_string()));
197 }
198
199 #[test]
200 fn test_base64_math_mode() {
201 let config = DictionaryRegistry::load_default().unwrap();
202 let base64_math = config.get_dictionary("base64_math").unwrap();
203 assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
204 }
205
206 #[test]
207 fn test_merge_configs() {
208 let mut config1 = DictionaryRegistry {
209 dictionaries: HashMap::new(),
210 compression: HashMap::new(),
211 settings: Settings::default(),
212 };
213 config1.dictionaries.insert(
214 "test1".to_string(),
215 DictionaryConfig {
216 chars: "ABC".to_string(),
217 mode: EncodingMode::BaseConversion,
218 padding: None,
219 start_codepoint: None,
220 common: true,
221 },
222 );
223
224 let mut config2 = DictionaryRegistry {
225 dictionaries: HashMap::new(),
226 compression: HashMap::new(),
227 settings: Settings::default(),
228 };
229 config2.dictionaries.insert(
230 "test2".to_string(),
231 DictionaryConfig {
232 chars: "XYZ".to_string(),
233 mode: EncodingMode::BaseConversion,
234 padding: None,
235 start_codepoint: None,
236 common: true,
237 },
238 );
239 config2.dictionaries.insert(
240 "test1".to_string(),
241 DictionaryConfig {
242 chars: "DEF".to_string(),
243 mode: EncodingMode::BaseConversion,
244 padding: None,
245 start_codepoint: None,
246 common: true,
247 },
248 );
249
250 config1.merge(config2);
251
252 assert_eq!(config1.dictionaries.len(), 2);
253 assert_eq!(config1.get_dictionary("test1").unwrap().chars, "DEF");
254 assert_eq!(config1.get_dictionary("test2").unwrap().chars, "XYZ");
255 }
256
257 #[test]
258 fn test_load_from_toml_string() {
259 let toml_content = r#"
260[dictionaries.custom]
261chars = "0123456789"
262mode = "base_conversion"
263"#;
264 let config = DictionaryRegistry::from_toml(toml_content).unwrap();
265 assert!(config.dictionaries.contains_key("custom"));
266 assert_eq!(config.get_dictionary("custom").unwrap().chars, "0123456789");
267 }
268}