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 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 DictionariesConfig {
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!("Warning: Failed to load user config from {:?}: {}", user_config_path, e);
132 }
133 }
134 }
135 }
136
137 let local_config_path = std::path::Path::new("dictionaries.toml");
139 if local_config_path.exists() {
140 match Self::load_from_file(local_config_path) {
141 Ok(local_config) => {
142 config.merge(local_config);
143 }
144 Err(e) => {
145 eprintln!("Warning: Failed to load local config from {:?}: {}", local_config_path, e);
146 }
147 }
148 }
149
150 Ok(config)
151 }
152
153 pub fn merge(&mut self, other: DictionariesConfig) {
157 for (name, dictionary) in other.dictionaries {
158 self.dictionaries.insert(name, dictionary);
159 }
160 }
161
162 pub fn get_dictionary(&self, name: &str) -> Option<&DictionaryConfig> {
164 self.dictionaries.get(name)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_load_default_config() {
174 let config = DictionariesConfig::load_default().unwrap();
175 assert!(config.dictionaries.contains_key("cards"));
176 }
177
178 #[test]
179 fn test_cards_alphabet_length() {
180 let config = DictionariesConfig::load_default().unwrap();
181 let cards = config.get_dictionary("cards").unwrap();
182 assert_eq!(cards.chars.chars().count(), 52);
183 }
184
185 #[test]
186 fn test_base64_chunked_mode() {
187 let config = DictionariesConfig::load_default().unwrap();
188 let base64 = config.get_dictionary("base64").unwrap();
189 assert_eq!(base64.mode, EncodingMode::Chunked);
190 assert_eq!(base64.padding, Some("=".to_string()));
191 }
192
193 #[test]
194 fn test_base64_math_mode() {
195 let config = DictionariesConfig::load_default().unwrap();
196 let base64_math = config.get_dictionary("base64_math").unwrap();
197 assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
198 }
199
200 #[test]
201 fn test_merge_configs() {
202 let mut config1 = DictionariesConfig {
203 dictionaries: HashMap::new(),
204 compression: HashMap::new(),
205 settings: Settings::default(),
206 };
207 config1.dictionaries.insert("test1".to_string(), DictionaryConfig {
208 chars: "ABC".to_string(),
209 mode: EncodingMode::BaseConversion,
210 padding: None,
211 start_codepoint: None,
212 });
213
214 let mut config2 = DictionariesConfig {
215 dictionaries: HashMap::new(),
216 compression: HashMap::new(),
217 settings: Settings::default(),
218 };
219 config2.dictionaries.insert("test2".to_string(), DictionaryConfig {
220 chars: "XYZ".to_string(),
221 mode: EncodingMode::BaseConversion,
222 padding: None,
223 start_codepoint: None,
224 });
225 config2.dictionaries.insert("test1".to_string(), DictionaryConfig {
226 chars: "DEF".to_string(),
227 mode: EncodingMode::BaseConversion,
228 padding: None,
229 start_codepoint: None,
230 });
231
232 config1.merge(config2);
233
234 assert_eq!(config1.dictionaries.len(), 2);
235 assert_eq!(config1.get_dictionary("test1").unwrap().chars, "DEF");
236 assert_eq!(config1.get_dictionary("test2").unwrap().chars, "XYZ");
237 }
238
239 #[test]
240 fn test_load_from_toml_string() {
241 let toml_content = r#"
242[dictionaries.custom]
243chars = "0123456789"
244mode = "base_conversion"
245"#;
246 let config = DictionariesConfig::from_toml(toml_content).unwrap();
247 assert!(config.dictionaries.contains_key("custom"));
248 assert_eq!(config.get_dictionary("custom").unwrap().chars, "0123456789");
249 }
250}
251