Skip to main content

steam_client/utils/
vdf.rs

1//! VDF (Valve Data Format) parser.
2//!
3//! Parses text-based VDF/KeyValue data used by Steam for app info.
4//! This is a simple recursive descent parser for the VDF format.
5
6use std::{collections::HashMap, fmt};
7
8/// Error during VDF parsing.
9#[derive(Debug)]
10pub enum VdfError {
11    /// Unexpected end of input.
12    UnexpectedEof,
13    /// Expected a specific character.
14    Expected(char),
15    /// Invalid escape sequence.
16    InvalidEscape(char),
17    /// Generic parse error.
18    ParseError(String),
19}
20
21impl fmt::Display for VdfError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            VdfError::UnexpectedEof => write!(f, "Unexpected end of input"),
25            VdfError::Expected(c) => write!(f, "Expected '{}'", c),
26            VdfError::InvalidEscape(c) => write!(f, "Invalid escape sequence: \\{}", c),
27            VdfError::ParseError(msg) => write!(f, "Parse error: {}", msg),
28        }
29    }
30}
31
32impl std::error::Error for VdfError {}
33
34/// A VDF value - either a string or an object (nested key-value pairs).
35#[derive(Debug, Clone, PartialEq)]
36pub enum VdfValue {
37    /// A string value.
38    String(String),
39    /// An object containing nested key-value pairs.
40    Object(HashMap<String, VdfValue>),
41    /// A list of values (when a key is duplicated).
42    Array(Vec<VdfValue>),
43}
44
45impl VdfValue {
46    /// Get as string if this is a string value.
47    pub fn as_str(&self) -> Option<&str> {
48        match self {
49            VdfValue::String(s) => Some(s),
50            _ => None,
51        }
52    }
53
54    /// Get as object if this is an object value.
55    pub fn as_object(&self) -> Option<&HashMap<String, VdfValue>> {
56        match self {
57            VdfValue::Object(obj) => Some(obj),
58            _ => None,
59        }
60    }
61
62    /// Get as array if this is an array value.
63    pub fn as_array(&self) -> Option<&Vec<VdfValue>> {
64        match self {
65            VdfValue::Array(arr) => Some(arr),
66            _ => None,
67        }
68    }
69
70    /// Get a nested value by key.
71    pub fn get(&self, key: &str) -> Option<&VdfValue> {
72        self.as_object().and_then(|obj| obj.get(key))
73    }
74
75    /// Get a nested string value by key.
76    pub fn get_str(&self, key: &str) -> Option<&str> {
77        self.get(key).and_then(|v| v.as_str())
78    }
79}
80
81/// VDF parser state.
82struct VdfParser<'a> {
83    input: &'a str,
84    pos: usize,
85}
86
87impl<'a> VdfParser<'a> {
88    fn new(input: &'a str) -> Self {
89        Self { input, pos: 0 }
90    }
91
92    fn peek(&self) -> Option<char> {
93        self.input[self.pos..].chars().next()
94    }
95
96    fn advance(&mut self) -> Option<char> {
97        if let Some(c) = self.peek() {
98            self.pos += c.len_utf8();
99            Some(c)
100        } else {
101            None
102        }
103    }
104
105    fn skip_whitespace(&mut self) {
106        while let Some(c) = self.peek() {
107            if c.is_whitespace() {
108                self.advance();
109            } else if c == '/' {
110                // Check for comment
111                let next_pos = self.pos + 1;
112                if next_pos < self.input.len() {
113                    let next_char = self.input[next_pos..].chars().next();
114                    if next_char == Some('/') {
115                        // Line comment - skip to end of line
116                        while let Some(c) = self.peek() {
117                            self.advance();
118                            if c == '\n' {
119                                break;
120                            }
121                        }
122                    } else {
123                        break;
124                    }
125                } else {
126                    break;
127                }
128            } else {
129                break;
130            }
131        }
132    }
133
134    fn skip_conditionals(&mut self) {
135        self.skip_whitespace();
136        while self.peek() == Some('[') {
137            while let Some(c) = self.advance() {
138                if c == ']' {
139                    break;
140                }
141            }
142            self.skip_whitespace();
143        }
144    }
145
146    fn parse_string(&mut self) -> Result<String, VdfError> {
147        self.skip_whitespace();
148
149        let quoted = self.peek() == Some('"');
150        if quoted {
151            self.advance(); // consume opening quote
152        }
153
154        let mut result = String::new();
155
156        loop {
157            match self.peek() {
158                None => {
159                    if quoted {
160                        return Err(VdfError::UnexpectedEof);
161                    }
162                    break;
163                }
164                Some('"') if quoted => {
165                    self.advance(); // consume closing quote
166                    break;
167                }
168                Some(c) if !quoted && (c.is_whitespace() || c == '{' || c == '}') => {
169                    break;
170                }
171                Some('\\') => {
172                    self.advance();
173                    match self.advance() {
174                        Some('n') => result.push('\n'),
175                        Some('t') => result.push('\t'),
176                        Some('\\') => result.push('\\'),
177                        Some('"') => result.push('"'),
178                        Some(c) => return Err(VdfError::InvalidEscape(c)),
179                        None => return Err(VdfError::UnexpectedEof),
180                    }
181                }
182                Some(c) => {
183                    self.advance();
184                    result.push(c);
185                }
186            }
187        }
188
189        Ok(result)
190    }
191
192    fn parse_value(&mut self) -> Result<VdfValue, VdfError> {
193        self.skip_whitespace();
194
195        if self.peek() == Some('{') {
196            self.parse_object()
197        } else {
198            Ok(VdfValue::String(self.parse_string()?))
199        }
200    }
201
202    fn insert_or_append(map: &mut HashMap<String, VdfValue>, key: String, value: VdfValue) {
203        if let Some(existing) = map.get_mut(&key) {
204            match existing {
205                VdfValue::Array(arr) => arr.push(value),
206                _ => {
207                    let old = existing.clone();
208                    *existing = VdfValue::Array(vec![old, value]);
209                }
210            }
211        } else {
212            map.insert(key, value);
213        }
214    }
215
216    fn parse_object(&mut self) -> Result<VdfValue, VdfError> {
217        self.skip_whitespace();
218
219        if self.peek() != Some('{') {
220            return Err(VdfError::Expected('{'));
221        }
222        self.advance(); // consume '{'
223
224        let mut map = HashMap::new();
225
226        loop {
227            self.skip_whitespace();
228
229            match self.peek() {
230                None => return Err(VdfError::UnexpectedEof),
231                Some('}') => {
232                    self.advance(); // consume '}'
233                    break;
234                }
235                _ => {
236                    let key = self.parse_string()?;
237                    let value = self.parse_value()?;
238                    self.skip_conditionals();
239                    Self::insert_or_append(&mut map, key, value);
240                }
241            }
242        }
243
244        Ok(VdfValue::Object(map))
245    }
246
247    fn parse_root(&mut self) -> Result<VdfValue, VdfError> {
248        self.skip_whitespace();
249
250        // Root can be either a single object or key-value pairs
251        if self.peek() == Some('{') {
252            self.parse_object()
253        } else {
254            // Parse as key-value pairs at root level
255            let mut map = HashMap::new();
256
257            loop {
258                self.skip_whitespace();
259
260                if self.peek().is_none() {
261                    break;
262                }
263
264                let key = self.parse_string()?;
265                let value = self.parse_value()?;
266                self.skip_conditionals();
267                Self::insert_or_append(&mut map, key, value);
268            }
269
270            Ok(VdfValue::Object(map))
271        }
272    }
273}
274
275/// Parse a VDF string into a VdfValue.
276///
277/// # Example
278/// ```rust
279/// use steam_client::utils::vdf::parse_vdf;
280///
281/// let vdf = r#"
282/// "appinfo"
283/// {
284///     "appid" "730"
285///     "name" "Counter-Strike 2"
286/// }
287/// "#;
288///
289/// let value = parse_vdf(vdf).expect("should not fail");
290/// ```
291pub fn parse_vdf(input: &str) -> Result<VdfValue, VdfError> {
292    let mut parser = VdfParser::new(input);
293    parser.parse_root()
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_simple_object() {
302        let vdf = r#"
303        "appinfo"
304        {
305            "appid" "730"
306            "name" "Counter-Strike 2"
307        }
308        "#;
309
310        let result = parse_vdf(vdf).expect("should not fail");
311        let appinfo = result.get("appinfo").expect("should not fail");
312        assert_eq!(appinfo.get_str("appid"), Some("730"));
313        assert_eq!(appinfo.get_str("name"), Some("Counter-Strike 2"));
314    }
315
316    #[test]
317    fn test_nested_object() {
318        let vdf = r#"
319        "appinfo"
320        {
321            "common"
322            {
323                "name" "Test Game"
324                "type" "Game"
325            }
326        }
327        "#;
328
329        let result = parse_vdf(vdf).expect("should not fail");
330        let common = result.get("appinfo").expect("should not fail").get("common").expect("should not fail");
331        assert_eq!(common.get_str("name"), Some("Test Game"));
332        assert_eq!(common.get_str("type"), Some("Game"));
333    }
334
335    #[test]
336    fn test_escape_sequences() {
337        let vdf = r#""key" "value with \"quotes\" and \\backslash""#;
338        let result = parse_vdf(vdf).expect("should not fail");
339        assert_eq!(result.get_str("key"), Some("value with \"quotes\" and \\backslash"));
340    }
341
342    #[test]
343    fn test_comments() {
344        let vdf = r#"
345        // This is a comment
346        "key" "value"
347        "#;
348        let result = parse_vdf(vdf).expect("should not fail");
349        assert_eq!(result.get_str("key"), Some("value"));
350    }
351}