base_d/
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 alphabet size. Output length varies with input.
13    BaseConversion,
14    /// Fixed-size bit chunking per RFC 4648.
15    /// Requires power-of-two alphabet 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 alphabet loaded from TOML.
29#[derive(Debug, Deserialize, Clone)]
30pub struct AlphabetConfig {
31    /// The characters comprising the alphabet
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 alphabet configurations loaded from TOML files.
46#[derive(Debug, Deserialize)]
47pub struct AlphabetsConfig {
48    /// Map of alphabet names to their configurations
49    pub alphabets: HashMap<String, AlphabetConfig>,
50}
51
52impl AlphabetsConfig {
53    /// Parses alphabet configurations from TOML content.
54    pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
55        toml::from_str(content)
56    }
57    
58    /// Loads the built-in alphabet configurations.
59    ///
60    /// Returns the default alphabets bundled with the library.
61    pub fn load_default() -> Result<Self, Box<dyn std::error::Error>> {
62        let content = include_str!("../alphabets.toml");
63        Ok(Self::from_toml(content)?)
64    }
65    
66    /// Loads configuration from a custom file path.
67    pub fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
68        let content = std::fs::read_to_string(path)?;
69        Ok(Self::from_toml(&content)?)
70    }
71    
72    /// Loads configuration with user overrides from standard locations.
73    ///
74    /// Searches in priority order:
75    /// 1. Built-in alphabets (from library)
76    /// 2. `~/.config/base-d/alphabets.toml` (user overrides)
77    /// 3. `./alphabets.toml` (project-local overrides)
78    ///
79    /// Later configurations override earlier ones for matching alphabet names.
80    pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
81        let mut config = Self::load_default()?;
82        
83        // Try to load user config from ~/.config/base-d/alphabets.toml
84        if let Some(config_dir) = dirs::config_dir() {
85            let user_config_path = config_dir.join("base-d").join("alphabets.toml");
86            if user_config_path.exists() {
87                match Self::load_from_file(&user_config_path) {
88                    Ok(user_config) => {
89                        config.merge(user_config);
90                    }
91                    Err(e) => {
92                        eprintln!("Warning: Failed to load user config from {:?}: {}", user_config_path, e);
93                    }
94                }
95            }
96        }
97        
98        // Try to load local config from ./alphabets.toml
99        let local_config_path = std::path::Path::new("alphabets.toml");
100        if local_config_path.exists() {
101            match Self::load_from_file(local_config_path) {
102                Ok(local_config) => {
103                    config.merge(local_config);
104                }
105                Err(e) => {
106                    eprintln!("Warning: Failed to load local config from {:?}: {}", local_config_path, e);
107                }
108            }
109        }
110        
111        Ok(config)
112    }
113    
114    /// Merges another configuration into this one.
115    ///
116    /// Alphabets from `other` override alphabets with the same name in `self`.
117    pub fn merge(&mut self, other: AlphabetsConfig) {
118        for (name, alphabet) in other.alphabets {
119            self.alphabets.insert(name, alphabet);
120        }
121    }
122    
123    /// Retrieves an alphabet configuration by name.
124    pub fn get_alphabet(&self, name: &str) -> Option<&AlphabetConfig> {
125        self.alphabets.get(name)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    
133    #[test]
134    fn test_load_default_config() {
135        let config = AlphabetsConfig::load_default().unwrap();
136        assert!(config.alphabets.contains_key("cards"));
137    }
138    
139    #[test]
140    fn test_cards_alphabet_length() {
141        let config = AlphabetsConfig::load_default().unwrap();
142        let cards = config.get_alphabet("cards").unwrap();
143        assert_eq!(cards.chars.chars().count(), 52);
144    }
145    
146    #[test]
147    fn test_base64_chunked_mode() {
148        let config = AlphabetsConfig::load_default().unwrap();
149        let base64 = config.get_alphabet("base64").unwrap();
150        assert_eq!(base64.mode, EncodingMode::Chunked);
151        assert_eq!(base64.padding, Some("=".to_string()));
152    }
153    
154    #[test]
155    fn test_base64_math_mode() {
156        let config = AlphabetsConfig::load_default().unwrap();
157        let base64_math = config.get_alphabet("base64_math").unwrap();
158        assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
159    }
160    
161    #[test]
162    fn test_merge_configs() {
163        let mut config1 = AlphabetsConfig {
164            alphabets: HashMap::new(),
165        };
166        config1.alphabets.insert("test1".to_string(), AlphabetConfig {
167            chars: "ABC".to_string(),
168            mode: EncodingMode::BaseConversion,
169            padding: None,
170            start_codepoint: None,
171        });
172        
173        let mut config2 = AlphabetsConfig {
174            alphabets: HashMap::new(),
175        };
176        config2.alphabets.insert("test2".to_string(), AlphabetConfig {
177            chars: "XYZ".to_string(),
178            mode: EncodingMode::BaseConversion,
179            padding: None,
180            start_codepoint: None,
181        });
182        config2.alphabets.insert("test1".to_string(), AlphabetConfig {
183            chars: "DEF".to_string(),
184            mode: EncodingMode::BaseConversion,
185            padding: None,
186            start_codepoint: None,
187        });
188        
189        config1.merge(config2);
190        
191        assert_eq!(config1.alphabets.len(), 2);
192        assert_eq!(config1.get_alphabet("test1").unwrap().chars, "DEF");
193        assert_eq!(config1.get_alphabet("test2").unwrap().chars, "XYZ");
194    }
195    
196    #[test]
197    fn test_load_from_toml_string() {
198        let toml_content = r#"
199[alphabets.custom]
200chars = "0123456789"
201mode = "base_conversion"
202"#;
203        let config = AlphabetsConfig::from_toml(toml_content).unwrap();
204        assert!(config.alphabets.contains_key("custom"));
205        assert_eq!(config.get_alphabet("custom").unwrap().chars, "0123456789");
206    }
207}
208