config_lib/parsers/
properties_parser.rs

1use crate::error::{Error, Result};
2use crate::value::Value;
3use std::collections::BTreeMap;
4
5/// Parse Properties format configuration
6pub fn parse(source: &str) -> Result<Value> {
7    let mut parser = PropertiesParser::new(source.to_string());
8    parser.parse()
9}
10
11/// High-performance Java Properties format parser
12///
13/// Properties format specification:
14/// - Simple key=value pairs (one per line)
15/// - Comments start with # or !
16/// - Supports : as separator alternative to =
17/// - Backslash line continuation
18/// - Unicode escapes (\uXXXX)
19/// - Standard Java Properties format
20///
21/// Performance optimizations:
22/// - String-based parsing for maximum speed
23/// - Single pass parsing
24/// - Minimal allocations
25/// - Zero-copy where possible
26pub struct PropertiesParser {
27    input: String,
28    position: usize,
29    line: usize,
30    column: usize,
31}
32
33impl PropertiesParser {
34    /// Create a new Properties parser with the given input string
35    pub fn new(input: String) -> Self {
36        Self {
37            input,
38            position: 0,
39            line: 1,
40            column: 1,
41        }
42    }
43
44    /// Parse the input string as Java Properties format
45    pub fn parse(&mut self) -> Result<Value> {
46        let mut properties = BTreeMap::new();
47
48        while !self.at_end() {
49            self.skip_whitespace_and_comments();
50
51            if self.at_end() {
52                break;
53            }
54
55            let (key, value) = self.parse_property()?;
56            properties.insert(key, value);
57        }
58
59        Ok(Value::table(properties))
60    }
61
62    fn parse_property(&mut self) -> Result<(String, Value)> {
63        let key = self.parse_key()?;
64        self.skip_whitespace();
65
66        // Expect separator (= or :)
67        if self.current_char() != '=' && self.current_char() != ':' {
68            return Err(Error::Parse {
69                message: format!("Expected '=' or ':', found '{}'", self.current_char()),
70                line: self.line,
71                column: self.column,
72                file: None,
73            });
74        }
75
76        self.advance(); // Skip separator
77        self.skip_whitespace();
78
79        let value = self.parse_value()?;
80
81        Ok((key, value))
82    }
83
84    fn parse_key(&mut self) -> Result<String> {
85        let mut key = String::new();
86
87        while !self.at_end() {
88            let ch = self.current_char();
89
90            match ch {
91                '=' | ':' => break,
92                '\\' => {
93                    self.advance();
94                    if self.at_end() {
95                        return Err(Error::Parse {
96                            message: "Unexpected end of input in key".to_string(),
97                            line: self.line,
98                            column: self.column,
99                            file: None,
100                        });
101                    }
102
103                    let escaped = self.parse_escape()?;
104                    key.push_str(&escaped);
105                }
106                '\n' | '\r' => {
107                    return Err(Error::Parse {
108                        message: "Unexpected newline in key".to_string(),
109                        line: self.line,
110                        column: self.column,
111                        file: None,
112                    });
113                }
114                _ => {
115                    key.push(ch);
116                    self.advance();
117                }
118            }
119        }
120
121        if key.trim().is_empty() {
122            return Err(Error::Parse {
123                message: "Empty key name".to_string(),
124                line: self.line,
125                column: self.column,
126                file: None,
127            });
128        }
129
130        Ok(key.trim().to_string())
131    }
132
133    fn parse_value(&mut self) -> Result<Value> {
134        let mut value = String::new();
135
136        while !self.at_end() {
137            let ch = self.current_char();
138
139            match ch {
140                '\\' => {
141                    self.advance();
142                    if self.at_end() {
143                        break;
144                    }
145
146                    // Check for line continuation
147                    if self.current_char() == '\n' || self.current_char() == '\r' {
148                        self.skip_newline();
149                        self.skip_whitespace();
150                        continue;
151                    }
152
153                    let escaped = self.parse_escape()?;
154                    value.push_str(&escaped);
155                }
156                '\n' | '\r' => break,
157                _ => {
158                    value.push(ch);
159                    self.advance();
160                }
161            }
162        }
163
164        let trimmed = value.trim();
165        Ok(self.infer_value_type(trimmed))
166    }
167
168    fn parse_escape(&mut self) -> Result<String> {
169        let ch = self.current_char();
170        self.advance();
171
172        match ch {
173            'n' => Ok("\n".to_string()),
174            't' => Ok("\t".to_string()),
175            'r' => Ok("\r".to_string()),
176            '\\' => Ok("\\".to_string()),
177            '=' => Ok("=".to_string()),
178            ':' => Ok(":".to_string()),
179            ' ' => Ok(" ".to_string()),
180            'u' => self.parse_unicode_escape(),
181            _ => Ok(ch.to_string()),
182        }
183    }
184
185    fn parse_unicode_escape(&mut self) -> Result<String> {
186        let mut hex_digits = String::new();
187
188        for _ in 0..4 {
189            if self.at_end() {
190                return Err(Error::Parse {
191                    message: "Incomplete unicode escape".to_string(),
192                    line: self.line,
193                    column: self.column,
194                    file: None,
195                });
196            }
197
198            let ch = self.current_char();
199            if ch.is_ascii_hexdigit() {
200                hex_digits.push(ch);
201                self.advance();
202            } else {
203                return Err(Error::Parse {
204                    message: format!("Invalid hex digit in unicode escape: '{ch}'"),
205                    line: self.line,
206                    column: self.column,
207                    file: None,
208                });
209            }
210        }
211
212        let code_point = u32::from_str_radix(&hex_digits, 16).unwrap();
213        if let Some(unicode_char) = char::from_u32(code_point) {
214            Ok(unicode_char.to_string())
215        } else {
216            Err(Error::Parse {
217                message: format!("Invalid unicode code point: {code_point}"),
218                line: self.line,
219                column: self.column,
220                file: None,
221            })
222        }
223    }
224
225    fn infer_value_type(&self, value: &str) -> Value {
226        if value.is_empty() {
227            return Value::string(String::new());
228        }
229
230        // Boolean values (common in Java properties)
231        match value.to_lowercase().as_str() {
232            "true" => return Value::bool(true),
233            "false" => return Value::bool(false),
234            _ => {}
235        }
236
237        // Integer values
238        if let Ok(int_val) = value.parse::<i64>() {
239            return Value::integer(int_val);
240        }
241
242        // Float values
243        if let Ok(float_val) = value.parse::<f64>() {
244            return Value::float(float_val);
245        }
246
247        // Default to string
248        Value::string(value.to_string())
249    }
250
251    fn skip_whitespace_and_comments(&mut self) {
252        while !self.at_end() {
253            match self.current_char() {
254                ' ' | '\t' => self.advance(),
255                '\n' | '\r' => self.skip_newline(),
256                '#' | '!' => self.skip_comment(),
257                _ => break,
258            }
259        }
260    }
261
262    fn skip_whitespace(&mut self) {
263        while !self.at_end() && (self.current_char() == ' ' || self.current_char() == '\t') {
264            self.advance();
265        }
266    }
267
268    fn skip_comment(&mut self) {
269        while !self.at_end() && self.current_char() != '\n' && self.current_char() != '\r' {
270            self.advance();
271        }
272    }
273
274    fn skip_newline(&mut self) {
275        if !self.at_end() && self.current_char() == '\r' {
276            self.advance();
277        }
278        if !self.at_end() && self.current_char() == '\n' {
279            self.advance();
280        }
281    }
282
283    fn current_char(&self) -> char {
284        self.input.chars().nth(self.position).unwrap_or('\0')
285    }
286
287    fn at_end(&self) -> bool {
288        self.position >= self.input.len()
289    }
290
291    fn advance(&mut self) {
292        if !self.at_end() {
293            if self.current_char() == '\n' {
294                self.line += 1;
295                self.column = 1;
296            } else {
297                self.column += 1;
298            }
299            self.position += 1;
300        }
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_simple_properties() {
310        let input = "key1=value1\nkey2=123\nbool_key=true";
311        let mut parser = PropertiesParser::new(input.to_string());
312        let result = parser.parse().unwrap();
313
314        if let Value::Table(table) = result {
315            assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
316            assert_eq!(table.get("key2").unwrap().as_integer().unwrap(), 123);
317            assert!(table.get("bool_key").unwrap().as_bool().unwrap());
318        }
319    }
320
321    #[test]
322    fn test_comments() {
323        let input = "# This is a comment\nkey1=value1\n! Another comment\nkey2=value2";
324        let mut parser = PropertiesParser::new(input.to_string());
325        let result = parser.parse().unwrap();
326
327        if let Value::Table(table) = result {
328            assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
329            assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
330        }
331    }
332
333    #[test]
334    fn test_escape_sequences() {
335        let input = r"key1=line1\nline2\ttab";
336        let mut parser = PropertiesParser::new(input.to_string());
337        let result = parser.parse().unwrap();
338
339        if let Value::Table(table) = result {
340            assert_eq!(
341                table.get("key1").unwrap().as_string().unwrap(),
342                "line1\nline2\ttab"
343            );
344        }
345    }
346
347    #[test]
348    fn test_colon_separator() {
349        let input = "key1:value1\nkey2: value2";
350        let mut parser = PropertiesParser::new(input.to_string());
351        let result = parser.parse().unwrap();
352
353        if let Value::Table(table) = result {
354            assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
355            assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
356        }
357    }
358}