| 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 |