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