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 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 pub fn load_with_overrides() -> Result<Self, Box<dyn std::error::Error>> {
56 let mut config = Self::load_default()?;
57
58 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 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 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