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)]
81 pub default_dictionary: Option<String>,
82 #[serde(default)]
84 pub xxhash: XxHashSettings,
85}
86
87impl DictionaryRegistry {
88 pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
90 toml::from_str(content)
91 }
92
93 pub fn load_default() -> Result<Self, Box<dyn std::error::Error>> {
97 let content = include_str!("../../dictionaries.toml");
98 Ok(Self::from_toml(content)?)
99 }
100
101 pub fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
103 let content = std::fs::read_to_string(path)?;
104 Ok(Self::from_toml(&content)?)
105 }
106
107 pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
116 let mut config = Self::load_default()?;
117
118 if let Some(config_dir) = dirs::config_dir() {
120 let user_config_path = config_dir.join("base-d").join("dictionaries.toml");
121 if user_config_path.exists() {
122 match Self::load_from_file(&user_config_path) {
123 Ok(user_config) => {
124 config.merge(user_config);
125 }
126 Err(e) => {
127 eprintln!(
128 "Warning: Failed to load user config from {:?}: {}",
129 user_config_path, e
130 );
131 }
132 }
133 }
134 }
135
136 let local_config_path = std::path::Path::new("dictionaries.toml");
138 if local_config_path.exists() {
139 match Self::load_from_file(local_config_path) {
140 Ok(local_config) => {
141 config.merge(local_config);
142 }
143 Err(e) => {
144 eprintln!(
145 "Warning: Failed to load local config from {:?}: {}",
146 local_config_path, e
147 );
148 }
149 }
150 }
151
152 Ok(config)
153 }
154
155 pub fn merge(&mut self, other: DictionaryRegistry) {
159 for (name, dictionary) in other.dictionaries {
160 self.dictionaries.insert(name, dictionary);
161 }
162 }
163
164 pub fn get_dictionary(&self, name: &str) -> Option<&DictionaryConfig> {
166 self.dictionaries.get(name)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_load_default_config() {
176 let config = DictionaryRegistry::load_default().unwrap();
177 assert!(config.dictionaries.contains_key("cards"));
178 }
179
180 #[test]
181 fn test_cards_alphabet_length() {
182 let config = DictionaryRegistry::load_default().unwrap();
183 let cards = config.get_dictionary("cards").unwrap();
184 assert_eq!(cards.chars.chars().count(), 52);
185 }
186
187 #[test]
188 fn test_base64_chunked_mode() {
189 let config = DictionaryRegistry::load_default().unwrap();
190 let base64 = config.get_dictionary("base64").unwrap();
191 assert_eq!(base64.mode, EncodingMode::Chunked);
192 assert_eq!(base64.padding, Some("=".to_string()));
193 }
194
195 #[test]
196 fn test_base64_math_mode() {
197 let config = DictionaryRegistry::load_default().unwrap();
198 let base64_math = config.get_dictionary("base64_math").unwrap();
199 assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
200 }
201
202 #[test]
203 fn test_merge_configs() {
204 let mut config1 = DictionaryRegistry {
205 dictionaries: HashMap::new(),
206 compression: HashMap::new(),
207 settings: Settings::default(),
208 };
209 config1.dictionaries.insert(
210 "test1".to_string(),
211 DictionaryConfig {
212 chars: "ABC".to_string(),
213 mode: EncodingMode::BaseConversion,
214 padding: None,
215 start_codepoint: None,
216 },
217 );
218
219 let mut config2 = DictionaryRegistry {
220 dictionaries: HashMap::new(),
221 compression: HashMap::new(),
222 settings: Settings::default(),
223 };
224 config2.dictionaries.insert(
225 "test2".to_string(),
226 DictionaryConfig {
227 chars: "XYZ".to_string(),
228 mode: EncodingMode::BaseConversion,
229 padding: None,
230 start_codepoint: None,
231 },
232 );
233 config2.dictionaries.insert(
234 "test1".to_string(),
235 DictionaryConfig {
236 chars: "DEF".to_string(),
237 mode: EncodingMode::BaseConversion,
238 padding: None,
239 start_codepoint: None,
240 },
241 );
242
243 config1.merge(config2);
244
245 assert_eq!(config1.dictionaries.len(), 2);
246 assert_eq!(config1.get_dictionary("test1").unwrap().chars, "DEF");
247 assert_eq!(config1.get_dictionary("test2").unwrap().chars, "XYZ");
248 }
249
250 #[test]
251 fn test_load_from_toml_string() {
252 let toml_content = r#"
253[dictionaries.custom]
254chars = "0123456789"
255mode = "base_conversion"
256"#;
257 let config = DictionaryRegistry::from_toml(toml_content).unwrap();
258 assert!(config.dictionaries.contains_key("custom"));
259 assert_eq!(config.get_dictionary("custom").unwrap().chars, "0123456789");
260 }
261}