base_d/core/
config.rs

1use serde::Deserialize;
2use std::collections::HashMap;
3
4/// Encoding strategy for converting binary data to text.
5///
6/// Different modes offer different tradeoffs between efficiency, compatibility,
7/// and features.
8#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10#[derive(Default)]
11pub enum EncodingMode {
12    /// Mathematical base conversion treating data as a large number.
13    /// Works with any dictionary size. Output length varies with input.
14    #[default]
15    BaseConversion,
16    /// Fixed-size bit chunking per RFC 4648.
17    /// Requires power-of-two dictionary size. Supports padding.
18    Chunked,
19    /// Direct 1:1 byte-to-character mapping using Unicode codepoint ranges.
20    /// Zero encoding overhead. Always 256 characters.
21    ByteRange,
22}
23
24/// Configuration for a single dictionary loaded from TOML.
25#[derive(Debug, Deserialize, Clone)]
26pub struct DictionaryConfig {
27    /// The characters comprising the dictionary
28    #[serde(default)]
29    pub chars: String,
30    /// The encoding mode to use
31    #[serde(default)]
32    pub mode: EncodingMode,
33    /// Optional padding character (e.g., "=" for base64)
34    #[serde(default)]
35    pub padding: Option<String>,
36    /// Starting Unicode codepoint for ByteRange mode
37    #[serde(default)]
38    pub start_codepoint: Option<u32>,
39    /// Whether this dictionary renders consistently across platforms (default: true)
40    /// Dictionaries with common=false are excluded from random selection (--dejavu)
41    #[serde(default = "default_true")]
42    pub common: bool,
43}
44
45fn default_true() -> bool {
46    true
47}
48
49/// Collection of dictionary configurations loaded from TOML files.
50#[derive(Debug, Deserialize)]
51pub struct DictionaryRegistry {
52    /// Map of dictionary names to their configurations
53    pub dictionaries: HashMap<String, DictionaryConfig>,
54    /// Compression algorithm configurations
55    #[serde(default)]
56    pub compression: HashMap<String, CompressionConfig>,
57    /// Global settings
58    #[serde(default)]
59    pub settings: Settings,
60}
61
62/// Configuration for a compression algorithm.
63#[derive(Debug, Deserialize, Clone)]
64pub struct CompressionConfig {
65    /// Default compression level
66    pub default_level: u32,
67}
68
69/// xxHash-specific settings.
70#[derive(Debug, Deserialize, Clone, Default)]
71pub struct XxHashSettings {
72    /// Default seed for xxHash algorithms
73    #[serde(default)]
74    pub default_seed: u64,
75    /// Path to default secret file for XXH3 variants
76    #[serde(default)]
77    pub default_secret_file: Option<String>,
78}
79
80/// Global settings for base-d.
81#[derive(Debug, Deserialize, Clone, Default)]
82pub struct Settings {
83    /// Default dictionary - if not set, requires explicit -e or --dejavu
84    #[serde(default)]
85    pub default_dictionary: Option<String>,
86    /// xxHash configuration
87    #[serde(default)]
88    pub xxhash: XxHashSettings,
89}
90
91impl DictionaryRegistry {
92    /// Parses dictionary configurations from TOML content.
93    pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
94        toml::from_str(content)
95    }
96
97    /// Loads the built-in dictionary configurations.
98    ///
99    /// Returns the default dictionaries bundled with the library.
100    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    /// Loads configuration from a custom file path.
106    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    /// Loads configuration with user overrides from standard locations.
112    ///
113    /// Searches in priority order:
114    /// 1. Built-in dictionaries (from library)
115    /// 2. `~/.config/base-d/dictionaries.toml` (user overrides)
116    /// 3. `./dictionaries.toml` (project-local overrides)
117    ///
118    /// Later configurations override earlier ones for matching dictionary names.
119    pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
120        let mut config = Self::load_default()?;
121
122        // Try to load user config from ~/.config/base-d/dictionaries.toml
123        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        // Try to load local config from ./dictionaries.toml
141        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    /// Merges another configuration into this one.
160    ///
161    /// Dictionaries from `other` override dictionaries with the same name in `self`.
162    pub fn merge(&mut self, other: DictionaryRegistry) {
163        for (name, dictionary) in other.dictionaries {
164            self.dictionaries.insert(name, dictionary);
165        }
166    }
167
168    /// Retrieves an dictionary configuration by name.
169    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}