steam_client/utils/
vdf.rs1use std::{collections::HashMap, fmt};
7
8#[derive(Debug)]
10pub enum VdfError {
11 UnexpectedEof,
13 Expected(char),
15 InvalidEscape(char),
17 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#[derive(Debug, Clone, PartialEq)]
36pub enum VdfValue {
37 String(String),
39 Object(HashMap<String, VdfValue>),
41 Array(Vec<VdfValue>),
43}
44
45impl VdfValue {
46 pub fn as_str(&self) -> Option<&str> {
48 match self {
49 VdfValue::String(s) => Some(s),
50 _ => None,
51 }
52 }
53
54 pub fn as_object(&self) -> Option<&HashMap<String, VdfValue>> {
56 match self {
57 VdfValue::Object(obj) => Some(obj),
58 _ => None,
59 }
60 }
61
62 pub fn as_array(&self) -> Option<&Vec<VdfValue>> {
64 match self {
65 VdfValue::Array(arr) => Some(arr),
66 _ => None,
67 }
68 }
69
70 pub fn get(&self, key: &str) -> Option<&VdfValue> {
72 self.as_object().and_then(|obj| obj.get(key))
73 }
74
75 pub fn get_str(&self, key: &str) -> Option<&str> {
77 self.get(key).and_then(|v| v.as_str())
78 }
79}
80
81struct 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 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 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(); }
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(); 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(); 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(); 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 if self.peek() == Some('{') {
252 self.parse_object()
253 } else {
254 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
275pub 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}