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}
44
45/// Collection of dictionary configurations loaded from TOML files.
46#[derive(Debug, Deserialize)]
47pub struct DictionaryRegistry {
48    /// Map of dictionary names to their configurations
49    pub dictionaries: HashMap<String, DictionaryConfig>,
50    /// Compression algorithm configurations
51    #[serde(default)]
52    pub compression: HashMap<String, CompressionConfig>,
53    /// Global settings
54    #[serde(default)]
55    pub settings: Settings,
56}
57
58/// Configuration for a compression algorithm.
59#[derive(Debug, Deserialize, Clone)]
60pub struct CompressionConfig {
61    /// Default compression level
62    pub default_level: u32,
63}
64
65/// xxHash-specific settings.
66#[derive(Debug, Deserialize, Clone, Default)]
67pub struct XxHashSettings {
68    /// Default seed for xxHash algorithms
69    #[serde(default)]
70    pub default_seed: u64,
71    /// Path to default secret file for XXH3 variants
72    #[serde(default)]
73    pub default_secret_file: Option<String>,
74}
75
76/// Global settings for base-d.
77#[derive(Debug, Deserialize, Clone, Default)]
78pub struct Settings {
79    /// Default dictionary - if not set, requires explicit -e or --dejavu
80    #[serde(default)]
81    pub default_dictionary: Option<String>,
82    /// xxHash configuration
83    #[serde(default)]
84    pub xxhash: XxHashSettings,
85}
86
87impl DictionaryRegistry {
88    /// Parses dictionary configurations from TOML content.
89    pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
90        toml::from_str(content)
91    }
92
93    /// Loads the built-in dictionary configurations.
94    ///
95    /// Returns the default alphabets bundled with the library.
96    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    /// Loads configuration from a custom file path.
102    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    /// Loads configuration with user overrides from standard locations.
108    ///
109    /// Searches in priority order:
110    /// 1. Built-in dictionaries (from library)
111    /// 2. `~/.config/base-d/dictionaries.toml` (user overrides)
112    /// 3. `./dictionaries.toml` (project-local overrides)
113    ///
114    /// Later configurations override earlier ones for matching dictionary names.
115    pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
116        let mut config = Self::load_default()?;
117
118        // Try to load user config from ~/.config/base-d/dictionaries.toml
119        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        // Try to load local config from ./dictionaries.toml
137        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    /// Merges another configuration into this one.
156    ///
157    /// Alphabets from `other` override alphabets with the same name in `self`.
158    pub fn merge(&mut self, other: DictionaryRegistry) {
159        for (name, dictionary) in other.dictionaries {
160            self.dictionaries.insert(name, dictionary);
161        }
162    }
163
164    /// Retrieves an dictionary configuration by name.
165    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}