Skip to main content

bare_config/
ini.rs

1//! INI format support for configuration content.
2//!
3//! This module provides the `IniContent` type for working
4//! with INI configuration files using a custom parser.
5
6use crate::ConfigContent;
7use crate::error::{ConfigError, ConfigResult};
8use crate::key::{Key, KeySegment};
9use crate::value::Value;
10use core::fmt;
11use core::str::FromStr;
12use std::collections::HashMap;
13
14/// INI configuration content.
15#[derive(Debug, Clone)]
16pub struct IniContent {
17    data: HashMap<String, HashMap<String, Option<String>>>,
18}
19
20impl PartialEq for IniContent {
21    fn eq(&self, other: &Self) -> bool {
22        // Compare internal data directly for efficiency
23        self.data == other.data
24    }
25}
26
27impl IniContent {
28    /// Creates a new IniContent from a HashMap.
29    pub fn from_map(data: HashMap<String, HashMap<String, Option<String>>>) -> Self {
30        Self { data }
31    }
32
33    /// Returns a reference to the underlying HashMap.
34    pub fn as_map(&self) -> &HashMap<String, HashMap<String, Option<String>>> {
35        &self.data
36    }
37
38    /// Converts to the underlying HashMap.
39    pub fn into_map(self) -> HashMap<String, HashMap<String, Option<String>>> {
40        self.data
41    }
42
43    fn parse_key(key: &Key) -> ConfigResult<(Option<String>, String)> {
44        let segments = key.segments();
45        if segments.is_empty() {
46            return Err(ConfigError::InvalidKey("Empty key".to_string()));
47        }
48
49        if segments.len() == 1 {
50            if let KeySegment::Key(s) = &segments[0] {
51                return Ok((None, s.clone()));
52            }
53        }
54
55        if segments.len() == 2 {
56            if let (KeySegment::Key(section), KeySegment::Key(k)) = (&segments[0], &segments[1]) {
57                return Ok((Some(section.clone()), k.clone()));
58            }
59        }
60
61        Err(ConfigError::InvalidKey(
62            "INI key must be .key or .section.key".to_string(),
63        ))
64    }
65
66    fn get_value_at_key(&self, key: &Key) -> ConfigResult<String> {
67        let (section, key_name) = Self::parse_key(key)?;
68        match section {
69            Some(sec) => self
70                .data
71                .get(&sec)
72                .and_then(|s| s.get(&key_name))
73                .cloned()
74                .ok_or_else(|| ConfigError::KeyNotFound(key.to_key_string()))
75                .and_then(|v| v.ok_or_else(|| ConfigError::KeyNotFound(key.to_key_string()))),
76            None => self
77                .data
78                .get("")
79                .and_then(|s| s.get(&key_name))
80                .cloned()
81                .ok_or_else(|| ConfigError::KeyNotFound(key.to_key_string()))
82                .and_then(|v| v.ok_or_else(|| ConfigError::KeyNotFound(key.to_key_string()))),
83        }
84    }
85
86    fn contains_key_at(&self, key: &Key) -> bool {
87        self.get_value_at_key(key).is_ok()
88    }
89
90    fn keys_at(&self) -> Vec<Key> {
91        let mut keys = Vec::new();
92
93        // General section (empty string key)
94        if let Some(section) = self.data.get("") {
95            for k in section.keys() {
96                if let Ok(key) = Key::from_str(&format!(".{k}")) {
97                    keys.push(key);
98                }
99            }
100        }
101
102        // Named sections
103        for (section_name, section) in &self.data {
104            if section_name.is_empty() {
105                continue;
106            }
107            for k in section.keys() {
108                if let Ok(key) = Key::from_str(&format!(".{section_name}.{k}")) {
109                    keys.push(key);
110                }
111            }
112        }
113
114        keys
115    }
116
117    #[allow(dead_code)]
118    fn len_at(&self) -> usize {
119        self.keys_at().len()
120    }
121
122    fn delete_at(&mut self, key: &Key) -> ConfigResult<()> {
123        let (section, key_name) = Self::parse_key(key)?;
124        let section_key = section.unwrap_or_default();
125        if let Some(properties) = self.data.get_mut(&section_key) {
126            if properties.remove(&key_name).is_some() {
127                return Ok(());
128            }
129        }
130        Err(ConfigError::KeyNotFound(key.to_key_string()))
131    }
132
133    fn upsert_at(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
134        let (section, key_name) = Self::parse_key(key)?;
135        let value_str = value.to_string_repr();
136
137        let section_name = section.unwrap_or_default();
138        let section_data = self.data.entry(section_name).or_default();
139        drop(section_data.insert(key_name, Some(value_str)));
140        Ok(())
141    }
142}
143
144impl fmt::Display for IniContent {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        let mut output = String::new();
147
148        // Handle general section (empty string key) first
149        if let Some(section) = self.data.get("") {
150            for (key, value) in section {
151                output.push_str(key);
152                output.push('=');
153                if let Some(v) = value {
154                    output.push_str(v);
155                }
156                output.push('\n');
157            }
158            if !section.is_empty() {
159                output.push('\n');
160            }
161        }
162
163        for (section_name, section) in &self.data {
164            if section_name.is_empty() {
165                continue;
166            }
167            if section.is_empty() {
168                continue;
169            }
170
171            output.push('[');
172            output.push_str(section_name);
173            output.push_str("]\n");
174
175            for (key, value) in section {
176                output.push_str(key);
177                output.push('=');
178                if let Some(v) = value {
179                    output.push_str(v);
180                }
181                output.push('\n');
182            }
183
184            output.push('\n');
185        }
186
187        write!(f, "{}", output.trim_end())
188    }
189}
190
191/// Parse INI content into HashMap
192/// Strip inline comments from a value, handling quoted strings properly
193fn strip_inline_comment(value: &str) -> &str {
194    let mut in_single_quote = false;
195    let mut in_double_quote = false;
196
197    for (i, c) in value.char_indices() {
198        match c {
199            '\'' if !in_double_quote => in_single_quote = !in_single_quote,
200            '"' if !in_single_quote => in_double_quote = !in_double_quote,
201            '#' | ';' if !in_single_quote && !in_double_quote => {
202                return value[..i].trim();
203            }
204            _ => {}
205        }
206    }
207    value
208}
209
210fn parse_ini_content(input: &str) -> HashMap<String, HashMap<String, Option<String>>> {
211    // Pre-allocate: estimate sections and keys
212    let estimated_sections = input
213        .lines()
214        .filter(|l| l.trim().starts_with('['))
215        .count()
216        .saturating_add(1); // +1 for default section
217    let _estimated_keys = input
218        .lines()
219        .filter(|l| {
220            let t = l.trim();
221            !t.is_empty() && !t.starts_with('[') && !t.starts_with('#') && !t.starts_with(';')
222        })
223        .count();
224
225    let mut data: HashMap<String, HashMap<String, Option<String>>> =
226        HashMap::with_capacity(estimated_sections);
227    let mut current_section = String::new();
228
229    // Initialize default section
230    let _ = data.entry(String::new()).or_default();
231
232    for line in input.lines() {
233        let line = line.trim();
234
235        // Skip empty lines
236        if line.is_empty() {
237            continue;
238        }
239
240        // Skip comments
241        if line.starts_with('#') || line.starts_with(';') || line.starts_with("//") {
242            continue;
243        }
244
245        // Parse section header [section_name]
246        if line.starts_with('[') {
247            if let Some(end) = line.find(']') {
248                let section = line[1..end].trim().to_string();
249                if !section.is_empty() {
250                    current_section = section;
251                    let _ = data.entry(current_section.clone()).or_default();
252                }
253                continue;
254            }
255        }
256
257        // Parse key=value
258        if let Some(eq_pos) = line.find('=') {
259            let key = line[..eq_pos].trim().to_string();
260            let value = line[eq_pos + 1..].trim();
261
262            // Remove inline comments (but not if inside quotes)
263            let value = strip_inline_comment(value);
264
265            if !key.is_empty() {
266                let section_data = data.entry(current_section.clone()).or_default();
267                drop(section_data.insert(key, Some(value.to_string())));
268            }
269        }
270    }
271
272    // Remove empty default section if there's nothing in it
273    if let Some(default_section) = data.get("") {
274        if default_section.is_empty() {
275            drop(data.remove(""));
276        }
277    }
278
279    data
280}
281
282impl FromStr for IniContent {
283    type Err = ConfigError;
284
285    fn from_str(s: &str) -> Result<Self, Self::Err> {
286        let data = parse_ini_content(s);
287        Ok(Self { data })
288    }
289}
290
291impl ConfigContent for IniContent {
292    fn select(&self, key: &Key) -> ConfigResult<Value> {
293        self.get_value_at_key(key).map(Value::string)
294    }
295
296    fn insert(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
297        if self.contains_key_at(key) {
298            return Err(ConfigError::KeyAlreadyExists(key.to_key_string()));
299        }
300        self.upsert_at(key, value)
301    }
302
303    fn update(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
304        if !self.contains_key_at(key) {
305            return Err(ConfigError::KeyDoesNotExist(key.to_key_string()));
306        }
307        self.upsert_at(key, value)
308    }
309
310    fn delete(&mut self, key: &Key) -> ConfigResult<()> {
311        self.delete_at(key)
312    }
313
314    fn upsert(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
315        self.upsert_at(key, value)
316    }
317
318    fn keys(&self) -> Vec<Key> {
319        self.keys_at()
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_ini_from_str() {
329        let content = IniContent::from_str("key=value").expect("test should succeed");
330        assert!(
331            content
332                .select(&Key::from_str(".key").expect("test should succeed"))
333                .is_ok()
334        );
335    }
336
337    #[test]
338    fn test_ini_select() {
339        let content = IniContent::from_str("name=test").expect("test should succeed");
340        let value = content
341            .select(&Key::from_str(".name").expect("test should succeed"))
342            .expect("test should succeed");
343        assert_eq!(value.as_str(), Some("test"));
344    }
345
346    #[test]
347    fn test_ini_section() {
348        let content =
349            IniContent::from_str("[database]\nurl=localhost").expect("test should succeed");
350        let value = content
351            .select(&Key::from_str(".database.url").expect("test should succeed"))
352            .expect("test should succeed");
353        assert_eq!(value.as_str(), Some("localhost"));
354    }
355
356    #[test]
357    fn test_ini_insert() {
358        let mut content = IniContent::from_str("").expect("test should succeed");
359        content
360            .insert(
361                &Key::from_str(".name").expect("test should succeed"),
362                &Value::string("test"),
363            )
364            .expect("test should succeed");
365        let value = content
366            .select(&Key::from_str(".name").expect("test should succeed"))
367            .expect("test should succeed");
368        assert_eq!(value.as_str(), Some("test"));
369    }
370
371    #[test]
372    fn test_ini_update() {
373        let mut content = IniContent::from_str("name=old").expect("test should succeed");
374        content
375            .update(
376                &Key::from_str(".name").expect("test should succeed"),
377                &Value::string("new"),
378            )
379            .expect("test should succeed");
380        let value = content
381            .select(&Key::from_str(".name").expect("test should succeed"))
382            .expect("test should succeed");
383        assert_eq!(value.as_str(), Some("new"));
384    }
385
386    #[test]
387    fn test_ini_delete() {
388        let mut content = IniContent::from_str("name=test").expect("test should succeed");
389        content
390            .delete(&Key::from_str(".name").expect("test should succeed"))
391            .expect("test should succeed");
392        assert!(
393            content
394                .select(&Key::from_str(".name").expect("test should succeed"))
395                .is_err()
396        );
397    }
398
399    #[test]
400    fn test_ini_upsert() {
401        let mut content = IniContent::from_str("").expect("test should succeed");
402        content
403            .upsert(
404                &Key::from_str(".name").expect("test should succeed"),
405                &Value::string("test"),
406            )
407            .expect("test should succeed");
408        let value = content
409            .select(&Key::from_str(".name").expect("test should succeed"))
410            .expect("test should succeed");
411        assert_eq!(value.as_str(), Some("test"));
412    }
413
414    #[test]
415    fn test_ini_keys() {
416        let content = IniContent::from_str("a=1\nb=2").expect("test should succeed");
417        let keys = content.keys();
418        assert_eq!(keys.len(), 2);
419    }
420
421    #[test]
422    fn test_ini_section_nested() {
423        let content =
424            IniContent::from_str("[database]\nhost=localhost").expect("test should succeed");
425        let value = content
426            .select(&Key::from_str(".database.host").expect("test should succeed"))
427            .expect("test should succeed");
428        assert_eq!(value.as_str(), Some("localhost"));
429    }
430
431    #[test]
432    fn test_ini_multiple_sections() {
433        let content = IniContent::from_str("[database]\nhost=localhost\n[server]\nport=8080")
434            .expect("test should succeed");
435        assert!(
436            content
437                .select(&Key::from_str(".database.host").expect("test should succeed"))
438                .is_ok()
439        );
440        assert!(
441            content
442                .select(&Key::from_str(".server.port").expect("test should succeed"))
443                .is_ok()
444        );
445    }
446
447    #[test]
448    fn test_ini_comment() {
449        let content =
450            IniContent::from_str("# This is a comment\nkey=value").expect("test should succeed");
451        let value = content
452            .select(&Key::from_str(".key").expect("test should succeed"))
453            .expect("test should succeed");
454        assert_eq!(value.as_str(), Some("value"));
455    }
456
457    #[test]
458    fn test_ini_inline_comment() {
459        let content = IniContent::from_str("key=value # comment").expect("test should succeed");
460        let value = content
461            .select(&Key::from_str(".key").expect("test should succeed"))
462            .expect("test should succeed");
463        assert_eq!(value.as_str(), Some("value"));
464    }
465
466    #[test]
467    fn test_ini_empty_value() {
468        let content = IniContent::from_str("key=").expect("test should succeed");
469        let value = content
470            .select(&Key::from_str(".key").expect("test should succeed"))
471            .expect("test should succeed");
472        assert_eq!(value.as_str(), Some(""));
473    }
474
475    #[test]
476    fn test_ini_update_section_value() {
477        let mut content =
478            IniContent::from_str("[database]\nhost=old").expect("test should succeed");
479        content
480            .update(
481                &Key::from_str(".database.host").expect("test should succeed"),
482                &Value::string("new"),
483            )
484            .expect("test should succeed");
485        let value = content
486            .select(&Key::from_str(".database.host").expect("test should succeed"))
487            .expect("test should succeed");
488        assert_eq!(value.as_str(), Some("new"));
489    }
490
491    #[test]
492    fn test_ini_delete_section_value() {
493        let mut content = IniContent::from_str("[database]\nhost=localhost\nport=5432")
494            .expect("test should succeed");
495        content
496            .delete(&Key::from_str(".database.port").expect("test should succeed"))
497            .expect("test should succeed");
498        assert!(
499            content
500                .select(&Key::from_str(".database.port").expect("test should succeed"))
501                .is_err()
502        );
503    }
504}