steam_vdf_parser/text/
parser.rs1use 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
15pub 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
43fn token<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
45 preceded(whitespace, alt((quoted_string, unquoted_string))).parse_next(input)
46}
47
48fn quoted_string_cow<'i>(input: &mut &'i str) -> ModalResult<Cow<'i, str>> {
52 '"'.parse_next(input)?;
54
55 let content_end = input.find(['\\', '"']).unwrap_or(input.len());
57
58 if content_end < input.len() && input[content_end..].starts_with('\\') {
59 let mut result = String::from(&input[..content_end]);
61 *input = &input[content_end..];
62
63 loop {
64 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 *input = &input[c.len_utf8()..];
73
74 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 return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
93 }
94 }
95 } else {
96 let content = &input[..content_end];
98 *input = &input[content_end..];
99
100 '"'.parse_next(input)?;
102
103 Ok(Cow::Borrowed(content))
104 }
105}
106
107fn quoted_string<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
110 '"'.parse_next(input)?;
111
112 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 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
136fn 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
147fn 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
157fn object_body<'i>(input: &mut &'i str) -> ModalResult<Obj<'i>> {
159 let mut obj = Obj::new();
160
161 loop {
162 whitespace.parse_next(input)?;
164
165 if input.starts_with('}') {
167 break;
168 }
169
170 let (key, value) = kv_pair.parse_next(input)?;
172 obj.insert(Cow::Borrowed(key), value);
173 }
174
175 Ok(obj)
176}
177
178fn kv_pair<'i>(input: &mut &'i str) -> ModalResult<(&'i str, Value<'i>)> {
180 let key = token.parse_next(input)?;
181
182 whitespace.parse_next(input)?;
184
185 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
201fn whitespace(input: &mut &str) -> ModalResult<()> {
203 repeat(0.., alt((multispace1.void(), line_comment.void()))).parse_next(input)
204}
205
206fn 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 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 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 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}