config_lib/parsers/
properties_parser.rs

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