base_d/
config.rs

1use serde::Deserialize;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "snake_case")]
6pub enum EncodingMode {
7    BaseConversion,
8    Chunked,
9    ByteRange,
10}
11
12impl Default for EncodingMode {
13    fn default() -> Self {
14        EncodingMode::BaseConversion
15    }
16}
17
18#[derive(Debug, Deserialize, Clone)]
19pub struct AlphabetConfig {
20    #[serde(default)]
21    pub chars: String,
22    #[serde(default)]
23    pub mode: EncodingMode,
24    #[serde(default)]
25    pub padding: Option<String>,
26    #[serde(default)]
27    pub start_codepoint: Option<u32>,
28}
29
30#[derive(Debug, Deserialize)]
31pub struct AlphabetsConfig {
32    pub alphabets: HashMap<String, AlphabetConfig>,
33}
34
35impl AlphabetsConfig {
36    pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
37        toml::from_str(content)
38    }
39    
40    pub fn load_default() -> Result<Self, Box<dyn std::error::Error>> {
41        let content = include_str!("../alphabets.toml");
42        Ok(Self::from_toml(content)?)
43    }
44    
45    /// Load configuration from custom file path
46    pub fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
47        let content = std::fs::read_to_string(path)?;
48        Ok(Self::from_toml(&content)?)
49    }
50    
51    /// Load configuration with user overrides from standard locations
52    /// 1. Start with built-in alphabets
53    /// 2. Override with ~/.config/base-d/alphabets.toml if it exists
54    /// 3. Override with ./alphabets.toml if it exists in current directory
55    pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
56        let mut config = Self::load_default()?;
57        
58        // Try to load user config from ~/.config/base-d/alphabets.toml
59        if let Some(config_dir) = dirs::config_dir() {
60            let user_config_path = config_dir.join("base-d").join("alphabets.toml");
61            if user_config_path.exists() {
62                match Self::load_from_file(&user_config_path) {
63                    Ok(user_config) => {
64                        config.merge(user_config);
65                    }
66                    Err(e) => {
67                        eprintln!("Warning: Failed to load user config from {:?}: {}", user_config_path, e);
68                    }
69                }
70            }
71        }
72        
73        // Try to load local config from ./alphabets.toml
74        let local_config_path = std::path::Path::new("alphabets.toml");
75        if local_config_path.exists() {
76            match Self::load_from_file(local_config_path) {
77                Ok(local_config) => {
78                    config.merge(local_config);
79                }
80                Err(e) => {
81                    eprintln!("Warning: Failed to load local config from {:?}: {}", local_config_path, e);
82                }
83            }
84        }
85        
86        Ok(config)
87    }
88    
89    /// Merge another config into this one, overriding existing alphabets
90    pub fn merge(&mut self, other: AlphabetsConfig) {
91        for (name, alphabet) in other.alphabets {
92            self.alphabets.insert(name, alphabet);
93        }
94    }
95    
96    pub fn get_alphabet(&self, name: &str) -> Option<&AlphabetConfig> {
97        self.alphabets.get(name)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    
105    #[test]
106    fn test_load_default_config() {
107        let config = AlphabetsConfig::load_default().unwrap();
108        assert!(config.alphabets.contains_key("cards"));
109    }
110    
111    #[test]
112    fn test_cards_alphabet_length() {
113        let config = AlphabetsConfig::load_default().unwrap();
114        let cards = config.get_alphabet("cards").unwrap();
115        assert_eq!(cards.chars.chars().count(), 52);
116    }
117    
118    #[test]
119    fn test_base64_chunked_mode() {
120        let config = AlphabetsConfig::load_default().unwrap();
121        let base64 = config.get_alphabet("base64").unwrap();
122        assert_eq!(base64.mode, EncodingMode::Chunked);
123        assert_eq!(base64.padding, Some("=".to_string()));
124    }
125    
126    #[test]
127    fn test_base64_math_mode() {
128        let config = AlphabetsConfig::load_default().unwrap();
129        let base64_math = config.get_alphabet("base64_math").unwrap();
130        assert_eq!(base64_math.mode, EncodingMode::BaseConversion);
131    }
132    
133    #[test]
134    fn test_merge_configs() {
135        let mut config1 = AlphabetsConfig {
136            alphabets: HashMap::new(),
137        };
138        config1.alphabets.insert("test1".to_string(), AlphabetConfig {
139            chars: "ABC".to_string(),
140            mode: EncodingMode::BaseConversion,
141            padding: None,
142            start_codepoint: None,
143        });
144        
145        let mut config2 = AlphabetsConfig {
146            alphabets: HashMap::new(),
147        };
148        config2.alphabets.insert("test2".to_string(), AlphabetConfig {
149            chars: "XYZ".to_string(),
150            mode: EncodingMode::BaseConversion,
151            padding: None,
152            start_codepoint: None,
153        });
154        config2.alphabets.insert("test1".to_string(), AlphabetConfig {
155            chars: "DEF".to_string(),
156            mode: EncodingMode::BaseConversion,
157            padding: None,
158            start_codepoint: None,
159        });
160        
161        config1.merge(config2);
162        
163        assert_eq!(config1.alphabets.len(), 2);
164        assert_eq!(config1.get_alphabet("test1").unwrap().chars, "DEF");
165        assert_eq!(config1.get_alphabet("test2").unwrap().chars, "XYZ");
166    }
167    
168    #[test]
169    fn test_load_from_toml_string() {
170        let toml_content = r#"
171[alphabets.custom]
172chars = "0123456789"
173mode = "base_conversion"
174"#;
175        let config = AlphabetsConfig::from_toml(toml_content).unwrap();
176        assert!(config.alphabets.contains_key("custom"));
177        assert_eq!(config.get_alphabet("custom").unwrap().chars, "0123456789");
178    }
179}
180