Rust · 7034 bytes Raw Blame History
1 //! VDF (Valve Data Format) file parsing
2 //!
3 //! Steam uses VDF files for configuration (libraryfolders.vdf, appmanifest_*.acf, etc.)
4
5 use crate::error::{Result, WandaError};
6 use std::collections::HashMap;
7 use std::path::Path;
8
9 /// Parsed VDF value - can be a string or a nested object
10 #[derive(Debug, Clone)]
11 pub enum VdfValue {
12 String(String),
13 Object(HashMap<String, VdfValue>),
14 }
15
16 impl VdfValue {
17 /// Get as string if this is a string value
18 pub fn as_str(&self) -> Option<&str> {
19 match self {
20 VdfValue::String(s) => Some(s),
21 VdfValue::Object(_) => None,
22 }
23 }
24
25 /// Get as object if this is an object value
26 pub fn as_object(&self) -> Option<&HashMap<String, VdfValue>> {
27 match self {
28 VdfValue::String(_) => None,
29 VdfValue::Object(obj) => Some(obj),
30 }
31 }
32
33 /// Get a nested value by key
34 pub fn get(&self, key: &str) -> Option<&VdfValue> {
35 self.as_object()?.get(key)
36 }
37
38 /// Get a string value by key
39 pub fn get_str(&self, key: &str) -> Option<&str> {
40 self.get(key)?.as_str()
41 }
42 }
43
44 /// Parse a VDF file from disk
45 pub fn parse_vdf_file(path: &Path) -> Result<VdfValue> {
46 let content = std::fs::read_to_string(path).map_err(|e| WandaError::VdfParseError {
47 path: path.to_path_buf(),
48 reason: e.to_string(),
49 })?;
50
51 parse_vdf(&content).map_err(|e| WandaError::VdfParseError {
52 path: path.to_path_buf(),
53 reason: e,
54 })
55 }
56
57 /// Parse VDF content from a string
58 pub fn parse_vdf(content: &str) -> std::result::Result<VdfValue, String> {
59 let mut parser = VdfParser::new(content);
60 parser.parse_root()
61 }
62
63 struct VdfParser<'a> {
64 chars: std::iter::Peekable<std::str::Chars<'a>>,
65 }
66
67 impl<'a> VdfParser<'a> {
68 fn new(content: &'a str) -> Self {
69 Self {
70 chars: content.chars().peekable(),
71 }
72 }
73
74 fn parse_root(&mut self) -> std::result::Result<VdfValue, String> {
75 self.skip_whitespace();
76
77 // Root is typically a single key-value pair where value is an object
78 let mut root = HashMap::new();
79
80 while self.chars.peek().is_some() {
81 self.skip_whitespace();
82 if self.chars.peek().is_none() {
83 break;
84 }
85
86 let key = self.parse_string()?;
87 self.skip_whitespace();
88 let value = self.parse_value()?;
89 root.insert(key, value);
90 self.skip_whitespace();
91 }
92
93 Ok(VdfValue::Object(root))
94 }
95
96 fn parse_value(&mut self) -> std::result::Result<VdfValue, String> {
97 self.skip_whitespace();
98
99 match self.chars.peek() {
100 Some('{') => self.parse_object(),
101 Some('"') => Ok(VdfValue::String(self.parse_string()?)),
102 Some(c) => Err(format!("Unexpected character: {}", c)),
103 None => Err("Unexpected end of input".to_string()),
104 }
105 }
106
107 fn parse_object(&mut self) -> std::result::Result<VdfValue, String> {
108 self.expect_char('{')?;
109 let mut obj = HashMap::new();
110
111 loop {
112 self.skip_whitespace();
113 match self.chars.peek() {
114 Some('}') => {
115 self.chars.next();
116 break;
117 }
118 Some('"') => {
119 let key = self.parse_string()?;
120 self.skip_whitespace();
121 let value = self.parse_value()?;
122 obj.insert(key, value);
123 }
124 Some(c) => return Err(format!("Expected '\"' or '}}', got '{}'", c)),
125 None => return Err("Unexpected end of input in object".to_string()),
126 }
127 }
128
129 Ok(VdfValue::Object(obj))
130 }
131
132 fn parse_string(&mut self) -> std::result::Result<String, String> {
133 self.expect_char('"')?;
134 let mut result = String::new();
135
136 loop {
137 match self.chars.next() {
138 Some('"') => break,
139 Some('\\') => {
140 // Handle escape sequences
141 match self.chars.next() {
142 Some('n') => result.push('\n'),
143 Some('t') => result.push('\t'),
144 Some('\\') => result.push('\\'),
145 Some('"') => result.push('"'),
146 Some(c) => {
147 result.push('\\');
148 result.push(c);
149 }
150 None => return Err("Unexpected end of input in escape sequence".to_string()),
151 }
152 }
153 Some(c) => result.push(c),
154 None => return Err("Unexpected end of input in string".to_string()),
155 }
156 }
157
158 Ok(result)
159 }
160
161 fn skip_whitespace(&mut self) {
162 while let Some(&c) = self.chars.peek() {
163 if c.is_whitespace() {
164 self.chars.next();
165 } else if c == '/' {
166 // Handle // comments
167 self.chars.next();
168 if self.chars.peek() == Some(&'/') {
169 // Skip until end of line
170 while let Some(&c) = self.chars.peek() {
171 self.chars.next();
172 if c == '\n' {
173 break;
174 }
175 }
176 }
177 } else {
178 break;
179 }
180 }
181 }
182
183 fn expect_char(&mut self, expected: char) -> std::result::Result<(), String> {
184 match self.chars.next() {
185 Some(c) if c == expected => Ok(()),
186 Some(c) => Err(format!("Expected '{}', got '{}'", expected, c)),
187 None => Err(format!("Expected '{}', got end of input", expected)),
188 }
189 }
190 }
191
192 #[cfg(test)]
193 mod tests {
194 use super::*;
195
196 #[test]
197 fn test_parse_simple_vdf() {
198 let vdf = r#"
199 "libraryfolders"
200 {
201 "0"
202 {
203 "path" "/home/user/.local/share/Steam"
204 "label" ""
205 }
206 }
207 "#;
208
209 let result = parse_vdf(vdf).unwrap();
210 let folders = result.get("libraryfolders").unwrap();
211 let folder0 = folders.get("0").unwrap();
212 assert_eq!(
213 folder0.get_str("path"),
214 Some("/home/user/.local/share/Steam")
215 );
216 }
217
218 #[test]
219 fn test_parse_app_manifest() {
220 let acf = r#"
221 "AppState"
222 {
223 "appid" "292030"
224 "name" "The Witcher 3: Wild Hunt"
225 "installdir" "The Witcher 3 Wild Hunt"
226 "SizeOnDisk" "50000000000"
227 }
228 "#;
229
230 let result = parse_vdf(acf).unwrap();
231 let app_state = result.get("AppState").unwrap();
232 assert_eq!(app_state.get_str("appid"), Some("292030"));
233 assert_eq!(
234 app_state.get_str("name"),
235 Some("The Witcher 3: Wild Hunt")
236 );
237 }
238 }
239