steam_vdf_parser/text/
parser.rs

1//! Text VDF parser powered by winnow.
2
3use alloc::borrow::Cow;
4use alloc::string::String;
5
6use winnow::ascii::{line_ending, multispace1};
7use winnow::combinator::{alt, delimited, preceded, repeat};
8use winnow::error::{ContextError, StrContext};
9use winnow::prelude::*;
10use winnow::token::{one_of, take_till};
11
12use crate::error::{Result, parse_error};
13use crate::value::{Obj, Value, Vdf};
14
15/// Parse a VDF document from text format.
16///
17/// # Example
18///
19/// ```
20/// use steam_vdf_parser::parse_text;
21///
22/// let input = r#""root"
23/// {
24///     "key" "value"
25/// }"#;
26/// let vdf = parse_text(input).unwrap();
27/// assert_eq!(vdf.key(), "root");
28/// ```
29pub fn parse(input: &str) -> Result<Vdf<'_>> {
30    let mut input = input.trim_start();
31
32    let key = token
33        .parse_next(&mut input)
34        .map_err(|_| parse_error(input, 0, "expected root key"))?;
35
36    let obj = object
37        .parse_next(&mut input)
38        .map_err(|_| parse_error(input, 0, "expected root object"))?;
39
40    Ok(Vdf::new(Cow::Borrowed(key), Value::Obj(obj)))
41}
42
43/// Parse a token (either quoted or unquoted).
44fn token<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
45    preceded(whitespace, alt((quoted_string, unquoted_string))).parse_next(input)
46}
47
48/// Parse a quoted string, returning a Cow (borrowed if no escapes, owned if escapes processed).
49///
50/// Handles escape sequences: \n, \t, \r, \\, \"
51fn quoted_string_cow<'i>(input: &mut &'i str) -> ModalResult<Cow<'i, str>> {
52    // Parse opening quote
53    '"'.parse_next(input)?;
54
55    // Check if there are any escape sequences
56    let content_end = input.find(['\\', '"']).unwrap_or(input.len());
57
58    if content_end < input.len() && input[content_end..].starts_with('\\') {
59        // Has escape sequences - need to process them
60        let mut result = String::from(&input[..content_end]);
61        *input = &input[content_end..];
62
63        loop {
64            // Check for closing quote
65            if let Some(c) = input.chars().next() {
66                if c == '"' {
67                    *input = &input[c.len_utf8()..];
68                    return Ok(Cow::Owned(result));
69                }
70                if c == '\\' {
71                    // Escape sequence - consume backslash
72                    *input = &input[c.len_utf8()..];
73
74                    // Get escaped character
75                    let escaped = one_of(('n', 't', 'r', '\\', '"'))
76                        .map(|c| match c {
77                            'n' => '\n',
78                            't' => '\t',
79                            'r' => '\r',
80                            '\\' => '\\',
81                            '"' => '"',
82                            _ => unreachable!(),
83                        })
84                        .parse_next(input)?;
85                    result.push(escaped);
86                } else {
87                    result.push(c);
88                    *input = &input[c.len_utf8()..];
89                }
90            } else {
91                // EOF before closing quote - fail
92                return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
93            }
94        }
95    } else {
96        // No escapes - zero copy path
97        let content = &input[..content_end];
98        *input = &input[content_end..];
99
100        // Parse closing quote
101        '"'.parse_next(input)?;
102
103        Ok(Cow::Borrowed(content))
104    }
105}
106
107/// Parse a quoted string (borrowed version for key parsing).
108/// Keys with escapes will fail - use quoted_string_cow for values that may have escapes.
109fn quoted_string<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
110    '"'.parse_next(input)?;
111
112    // Find closing quote, checking for escapes
113    let mut end = 0;
114    let mut chars = input.char_indices();
115    while let Some((idx, c)) = chars.next() {
116        if c == '"' {
117            end = idx;
118            break;
119        }
120        if c == '\\' {
121            // Skip escaped character
122            chars.next();
123        }
124    }
125
126    if end == 0 {
127        return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
128    }
129
130    let result = &input[..end];
131    *input = &input[end + '"'.len_utf8()..];
132
133    Ok(result)
134}
135
136/// Parse an unquoted string.
137///
138/// Unquoted strings end at whitespace, `{`, `}`, or `"`.
139fn unquoted_string<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
140    take_till(1.., |c: char| {
141        c.is_whitespace() || c == '{' || c == '}' || c == '"'
142    })
143    .context(StrContext::Label("token"))
144    .parse_next(input)
145}
146
147/// Parse an object (recursive block of key-value pairs).
148fn object<'i>(input: &mut &'i str) -> ModalResult<Obj<'i>> {
149    preceded(
150        whitespace,
151        delimited('{', object_body, preceded(whitespace, '}')),
152    )
153    .context(StrContext::Label("object"))
154    .parse_next(input)
155}
156
157/// Parse the body of an object (key-value pairs until closing brace).
158fn object_body<'i>(input: &mut &'i str) -> ModalResult<Obj<'i>> {
159    let mut obj = Obj::new();
160
161    loop {
162        // Skip whitespace
163        whitespace.parse_next(input)?;
164
165        // Check for closing brace
166        if input.starts_with('}') {
167            break;
168        }
169
170        // Parse a key-value pair
171        let (key, value) = kv_pair.parse_next(input)?;
172        obj.insert(Cow::Borrowed(key), value);
173    }
174
175    Ok(obj)
176}
177
178/// Parse a key-value pair.
179fn kv_pair<'i>(input: &mut &'i str) -> ModalResult<(&'i str, Value<'i>)> {
180    let key = token.parse_next(input)?;
181
182    // Skip whitespace before value
183    whitespace.parse_next(input)?;
184
185    // Parse the value
186    let value = if let Some(c) = input.chars().next() {
187        match c {
188            '{' => object.map(Value::Obj).parse_next(input)?,
189            '"' => quoted_string_cow.map(Value::Str).parse_next(input)?,
190            _ => unquoted_string
191                .map(|s| Value::Str(Cow::Borrowed(s)))
192                .parse_next(input)?,
193        }
194    } else {
195        return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
196    };
197
198    Ok((key, value))
199}
200
201/// Skip whitespace and line comments.
202fn whitespace(input: &mut &str) -> ModalResult<()> {
203    repeat(0.., alt((multispace1.void(), line_comment.void()))).parse_next(input)
204}
205
206/// Parse a line comment (// to newline).
207fn line_comment(input: &mut &str) -> ModalResult<()> {
208    preceded(
209        "//",
210        alt((line_ending.void(), take_till(0.., ['\r', '\n']).void())),
211    )
212    .parse_next(input)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use alloc::format;
219
220    #[test]
221    fn test_parse_simple_kv() {
222        let input = r#""root"
223        {
224            "key" "value"
225        }"#;
226        let vdf = parse(input).unwrap();
227        assert_eq!(vdf.key(), "root");
228
229        let obj = vdf.as_obj().unwrap();
230        let value = obj.get("key").and_then(|v| v.as_str());
231        assert_eq!(value, Some("value"));
232    }
233
234    #[test]
235    fn test_parse_nested_objects() {
236        let input = r#""outer"
237        {
238            "inner"
239            {
240                "key" "value"
241            }
242        }"#;
243        let vdf = parse(input).unwrap();
244        assert_eq!(vdf.key(), "outer");
245
246        let obj = vdf.as_obj().unwrap();
247        let inner = obj.get("inner").and_then(|v| v.as_obj()).unwrap();
248        let value = inner.get("key").and_then(|v| v.as_str());
249        assert_eq!(value, Some("value"));
250    }
251
252    #[test]
253    fn test_parse_unquoted_tokens() {
254        let input = r#"root
255        {
256            key value
257        }"#;
258        let vdf = parse(input).unwrap();
259        assert_eq!(vdf.key(), "root");
260
261        let obj = vdf.as_obj().unwrap();
262        let value = obj.get("key").and_then(|v| v.as_str());
263        assert_eq!(value, Some("value"));
264    }
265
266    #[test]
267    fn test_parse_with_comments() {
268        let input = r#""root"
269        {
270            // This is a comment
271            "key" "value"
272            // Another comment
273        }"#;
274        let vdf = parse(input).unwrap();
275
276        let obj = vdf.as_obj().unwrap();
277        let value = obj.get("key").and_then(|v| v.as_str());
278        assert_eq!(value, Some("value"));
279    }
280
281    #[test]
282    fn test_parse_multiple_keys() {
283        let input = r#""settings"
284        {
285            "name" "test"
286            "count" "42"
287        }"#;
288        let vdf = parse(input).unwrap();
289
290        let obj = vdf.as_obj().unwrap();
291        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("test"));
292        assert_eq!(obj.get("count").and_then(|v| v.as_str()), Some("42"));
293    }
294
295    #[test]
296    fn test_escape_sequences() {
297        let test_cases: &[(&str, &str)] = &[
298            (r#""test\nline""#, "test\nline"),
299            (r#""test\ttab""#, "test\ttab"),
300            (r#""test\\backslash""#, "test\\backslash"),
301            (r#""test\"quote""#, "test\"quote"),
302            (r#""test\rreturn""#, "test\rreturn"),
303        ];
304
305        for (input, expected) in test_cases {
306            let full_input = format!(r#""root"{{"key" {}}}"#, input);
307            let vdf = parse(&full_input).unwrap();
308            let obj = vdf.as_obj().unwrap();
309            let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
310            assert_eq!(value, *expected, "Failed for input: {}", input);
311        }
312    }
313
314    #[test]
315    fn test_escape_sequences_in_nested_objects() {
316        let input = r#""root"
317        {
318            "outer"
319            {
320                "key" "value\nwith\nnewlines"
321            }
322        }"#;
323        let vdf = parse(input).unwrap();
324        let outer = vdf
325            .as_obj()
326            .unwrap()
327            .get("outer")
328            .and_then(|v| v.as_obj())
329            .unwrap();
330        let value = outer.get("key").and_then(|v| v.as_str()).unwrap();
331        assert_eq!(value, "value\nwith\nnewlines");
332    }
333
334    #[test]
335    fn test_mixed_escape_sequences() {
336        let input = r#""root"{"key" "line1\nline2\ttab\\slash\"quote"}"#;
337        let vdf = parse(input).unwrap();
338        let obj = vdf.as_obj().unwrap();
339        let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
340        assert_eq!(value, "line1\nline2\ttab\\slash\"quote");
341    }
342
343    #[test]
344    fn test_unquoted_token_no_escape_processing() {
345        let input = r#"root{key value\nnotescaped}"#;
346        let vdf = parse(input).unwrap();
347        let obj = vdf.as_obj().unwrap();
348        let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
349        // Unquoted tokens should have literal backslash-n
350        assert_eq!(value, r#"value\nnotescaped"#);
351    }
352
353    #[test]
354    fn test_quoted_string_without_escapes_zero_copy() {
355        let input = r#""root"{"key" "value"}"#;
356        let vdf = parse(input).unwrap();
357        let obj = vdf.as_obj().unwrap();
358        let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
359        // Without escapes, value should be parsed correctly (zero-copy internally)
360        assert_eq!(value, "value");
361    }
362
363    #[test]
364    fn test_quoted_string_with_escapes_owned() {
365        let input = r#""root"{"key" "value\nwith\nescape"}"#;
366        let vdf = parse(input).unwrap();
367        let obj = vdf.as_obj().unwrap();
368        let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
369        // With escapes, value should be parsed correctly (owned internally)
370        assert_eq!(value, "value\nwith\nescape");
371    }
372
373    #[test]
374    fn test_empty_object() {
375        let input = r#""root"{}"#;
376        let vdf = parse(input).unwrap();
377        let obj = vdf.as_obj().unwrap();
378        assert!(obj.is_empty());
379    }
380
381    #[test]
382    fn test_deeply_nested_objects() {
383        let input = r#""root"
384        {
385            "level1"
386            {
387                "level2"
388                {
389                    "level3"
390                    {
391                        "key" "value"
392                    }
393                }
394            }
395        }"#;
396        let vdf = parse(input).unwrap();
397        let level1 = vdf
398            .as_obj()
399            .unwrap()
400            .get("level1")
401            .and_then(|v| v.as_obj())
402            .unwrap();
403        let level2 = level1.get("level2").and_then(|v| v.as_obj()).unwrap();
404        let level3 = level2.get("level3").and_then(|v| v.as_obj()).unwrap();
405        let value = level3.get("key").and_then(|v| v.as_str()).unwrap();
406        assert_eq!(value, "value");
407    }
408}