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 DictionaryRegistry {
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 XxHashSettings {
68 #[serde(default)]
70 pub default_seed: u64,
71 #[serde(default)]
73 pub default_secret_file: Option<String>,
74}
75
76#[derive(Debug, Deserialize, Clone, Default)]
78pub struct Settings {
79 #[serde(default = "default_dictionary")]
81 pub default_dictionary: String,
82 #[serde(default)]
84 pub xxhash: XxHashSettings,
85}
86
87fn default_dictionary() -> String {
88 "base64".to_string()
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_alphabet_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 },
221 );
222
223 let mut config2 = DictionaryRegistry {
224 dictionaries: HashMap::new(),
225 compression: HashMap::new(),
226 settings: Settings::default(),
227 };
228 config2.dictionaries.insert(
229 "test2".to_string(),
230 DictionaryConfig {
231 chars: "XYZ".to_string(),
232 mode: EncodingMode::BaseConversion,
233 padding: None,
234 start_codepoint: None,
235 },
236 );
237 config2.dictionaries.insert(
238 "test1".to_string(),
239 DictionaryConfig {
240 chars: "DEF".to_string(),
241 mode: EncodingMode::BaseConversion,
242 padding: None,
243 start_codepoint: None,
244 },
245 );
246
247 config1.merge(config2);
248
249 assert_eq!(config1.dictionaries.len(), 2);
250 assert_eq!(config1.get_dictionary("test1").unwrap().chars, "DEF");
251 assert_eq!(config1.get_dictionary("test2").unwrap().chars, "XYZ");
252 }
253
254 #[test]
255 fn test_load_from_toml_string() {
256 let toml_content = r#"
257[dictionaries.custom]
258chars = "0123456789"
259mode = "base_conversion"
260"#;
261 let config = DictionaryRegistry::from_toml(toml_content).unwrap();
262 assert!(config.dictionaries.contains_key("custom"));
263 assert_eq!(config.get_dictionary("custom").unwrap().chars, "0123456789");
264 }
265}