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 #[serde(default = "default_true")]
46 pub common: bool,
47}
48
49fn default_true() -> bool {
50 true
51}
52
53#[derive(Debug, Deserialize)]
55pub struct DictionaryRegistry {
56 pub dictionaries: HashMap<String, DictionaryConfig>,
58 #[serde(default)]
60 pub compression: HashMap<String, CompressionConfig>,
61 #[serde(default)]
63 pub settings: Settings,
64}
65
66#[derive(Debug, Deserialize, Clone)]
68pub struct CompressionConfig {
69 pub default_level: u32,
71}
72
73#[derive(Debug, Deserialize, Clone, Default)]
75pub struct XxHashSettings {
76 #[serde(default)]
78 pub default_seed: u64,
79 #[serde(default)]
81 pub default_secret_file: Option<String>,
82}
83
84#[derive(Debug, Deserialize, Clone, Default)]
86pub struct Settings {
87 #[serde(default)]
89 pub default_dictionary: Option<String>,
90 #[serde(default)]
92 pub xxhash: XxHashSettings,
93}
94
95impl DictionaryRegistry {
96 pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
98 toml::from_str(content)
99 }
100
101 pub fn load_default() -> Result<Self, Box<dyn std::error::Error>> {
105 let content = include_str!("../../dictionaries.toml");
106 Ok(Self::from_toml(content)?)
107 }
108
109 pub fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
111 let content = std::fs::read_to_string(path)?;
112 Ok(Self::from_toml(&content)?)
113 }
114
115 pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
124 let mut config = Self::load_default()?;
125
126 if let Some(config_dir) = dirs::config_dir() {
128 let user_config_path = config_dir.join("base-d").join("dictionaries.toml");
129 if user_config_path.exists() {
130 match Self::load_from_file(&user_config_path) {
131 Ok(user_config) => {
132 config.merge(user_config);
133 }
134 Err(e) => {
135 eprintln!(
136 "Warning: Failed to load user config from {:?}: {}",
137 user_config_path, e
138 );
139 }
140 }
141 }
142 }
143
144 let local_config_path = std::path::Path::new("dictionaries.toml");
146 if local_config_path.exists() {
147 match Self::load_from_file(local_config_path) {
148 Ok(local_config) => {
149 config.merge(local_config);
150 }
151 Err(e) => {
152 eprintln!(
153 "Warning: Failed to load local config from {:?}: {}",
154 local_config_path, e
155 );
156 }
157 }
158 }
159
160 Ok(config)
161 }
162
163 pub fn merge(&mut self, other: DictionaryRegistry) {
167 for (name, dictionary) in other.dictionaries {
168 self.dictionaries.insert(name, dictionary);
169 }
170 }
171
172 pub fn get_dictionary(&self, name: &str) -> Option<&DictionaryConfig> {
174 self.dictionaries.get(name)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_load_default_config() {
184 let config = DictionaryRegistry::load_default().unwrap();
185 assert!(config.dictionaries.contains_key("cards"));
186 }
187
188 #[test]
189 fn test_cards_dictionary_length() {
190 let config = DictionaryRegistry::load_default().unwrap();
191 let cards = config.get_dictionary("cards").unwrap();
192 assert_eq!(cards.chars.chars().count(), 52);
193 }
194
195 #[test]
196 fn test_base64_chunked_mode() {
197 let config = DictionaryRegistry::load_default().unwrap();
198 let base64 = config.get_dictionary("base64").unwrap();
199 assert_eq!(base64.mode, EncodingMode::Chunked);
200 assert_eq!(base64.padding, Some("=".to_string()));
201 }
202
203 #[test]
204 fn test_base64_math_mode() {
205 let config = DictionaryRegistry::load_default().unwrap();
206 let base64_math = config.get_dictionary("base64_math").unwrap();
207 assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
208 }
209
210 #[test]
211 fn test_merge_configs() {
212 let mut config1 = DictionaryRegistry {
213 dictionaries: HashMap::new(),
214 compression: HashMap::new(),
215 settings: Settings::default(),
216 };
217 config1.dictionaries.insert(
218 "test1".to_string(),
219 DictionaryConfig {
220 chars: "ABC".to_string(),
221 mode: EncodingMode::BaseConversion,
222 padding: None,
223 start_codepoint: None,
224 common: true,
225 },
226 );
227
228 let mut config2 = DictionaryRegistry {
229 dictionaries: HashMap::new(),
230 compression: HashMap::new(),
231 settings: Settings::default(),
232 };
233 config2.dictionaries.insert(
234 "test2".to_string(),
235 DictionaryConfig {
236 chars: "XYZ".to_string(),
237 mode: EncodingMode::BaseConversion,
238 padding: None,
239 start_codepoint: None,
240 common: true,
241 },
242 );
243 config2.dictionaries.insert(
244 "test1".to_string(),
245 DictionaryConfig {
246 chars: "DEF".to_string(),
247 mode: EncodingMode::BaseConversion,
248 padding: None,
249 start_codepoint: None,
250 common: true,
251 },
252 );
253
254 config1.merge(config2);
255
256 assert_eq!(config1.dictionaries.len(), 2);
257 assert_eq!(config1.get_dictionary("test1").unwrap().chars, "DEF");
258 assert_eq!(config1.get_dictionary("test2").unwrap().chars, "XYZ");
259 }
260
261 #[test]
262 fn test_load_from_toml_string() {
263 let toml_content = r#"
264[dictionaries.custom]
265chars = "0123456789"
266mode = "base_conversion"
267"#;
268 let config = DictionaryRegistry::from_toml(toml_content).unwrap();
269 assert!(config.dictionaries.contains_key("custom"));
270 assert_eq!(config.get_dictionary("custom").unwrap().chars, "0123456789");
271 }
272}