1use fs_err::File;
2use log::{error, warn};
3use serde::Deserialize;
4use std::collections::HashMap;
5use std::path::Path;
6#[cfg(unix)]
7use std::path::PathBuf;
8#[cfg(windows)]
9use std::path::PathBuf;
10
11#[derive(Debug, Clone, Deserialize)]
12#[serde(rename = "libraryfolders")]
13pub struct LibraryFolders(pub HashMap<usize, LibraryFolder>);
14
15#[derive(Debug, Clone, Deserialize)]
16pub struct LibraryFolder {
17 pub path: String,
18 pub label: String,
19 pub contentid: i64,
20 pub totalsize: usize,
21 pub update_clean_bytes_tally: Option<usize>,
22 pub time_last_update_corruption: Option<u64>,
23 pub apps: HashMap<u32, usize>,
24}
25
26#[derive(Debug, Clone, Deserialize)]
28#[allow(non_snake_case)]
29pub struct AppState {
30 pub appid: u64,
31 pub LauncherPath: Option<String>,
33 pub name: String,
34 pub StateFlags: u64,
35 pub installdir: String,
36 pub LastUpdated: Option<u64>,
37 pub SizeOnDisk: usize,
38 pub StagingSize: Option<usize>,
39 pub buildid: u64,
40 pub LastOwner: Option<u64>,
41 pub UpdateResult: Option<u64>,
42 pub BytesToDownload: Option<usize>,
43 pub BytesDownloaded: Option<usize>,
44 pub BytesToStage: Option<usize>,
45 pub BytesStaged: Option<usize>,
46 pub TargetBuildID: Option<usize>,
47 pub AutoUpdateBehavior: Option<u64>,
48 pub ScheduledAutoUpdate: Option<u64>,
50 pub InstalledDepots: HashMap<u64, InstalledDepot>,
51 pub SharedDepots: Option<HashMap<u64, u64>>,
52 #[serde(skip)]
56 pub library_path: String,
57
58 #[serde(skip)]
60 pub game_path: String,
61}
62
63#[derive(Debug, Clone, Deserialize)]
64pub struct InstalledDepot {
65 pub manifest: u64,
66 pub size: usize,
67}
68
69#[cfg(not(any(windows, unix)))]
75fn get_steam_path() -> anyhow::Result<String> {
76 anyhow::bail!("Not supported on this platform")
77}
78
79#[cfg(unix)]
80fn get_steam_path() -> anyhow::Result<PathBuf> {
81 use anyhow::Context;
82
83 let home = std::env::var("HOME").context("HOME environment variable not set")?;
84 Ok(Path::new(&home).join(Path::new(".steam/steam")))
85}
86
87#[cfg(windows)]
88fn get_steam_path_reg(root: winreg::HKEY) -> anyhow::Result<String> {
89 const STEAM_REGKEY_PATH: &str = "SOFTWARE\\Valve\\Steam";
90
91 let hkcu = winreg::RegKey::predef(root);
92 let steam_key = hkcu.open_subkey(STEAM_REGKEY_PATH)?;
93 let path: String = steam_key.get_value("SteamPath")?;
94 Ok(path)
95}
96
97#[cfg(windows)]
98fn get_steam_path() -> anyhow::Result<PathBuf> {
99 use anyhow::Context;
100 use log::info;
101
102 let is_wine = std::fs::read_to_string("/proc/version")
103 .unwrap_or_default()
104 .contains("Linux");
105
106 if is_wine {
107 info!("Detected Wine, looking for Linux Steam directory instead");
108 let username = std::env::var("USERNAME").context("%USERNAME% is not set")?;
109
110 let home = format!("/home/{username}");
111 Ok(Path::new(&home).join(".steam/steam/"))
112 } else {
113 get_steam_path_reg(winreg::enums::HKEY_CURRENT_USER)
114 .or_else(|_| get_steam_path_reg(winreg::enums::HKEY_LOCAL_MACHINE))
115 .map(|p| Path::new(&p).into())
116 }
117}
118
119pub fn get_all_apps() -> anyhow::Result<Vec<AppState>> {
120 use anyhow::Context;
121 use log::debug;
122
123 let steam_path = get_steam_path().context("Failed to find Steam installation path")?;
124
125 debug!("Using Steam path: {}", steam_path.display());
126
127 let vdf_path = Path::new(&steam_path).join("config/libraryfolders.vdf");
128
129 let mut apps = vec![];
130 let folders: LibraryFolders = keyvalues_serde::from_reader(File::open(vdf_path)?)?;
131 for f in folders.0.values() {
132 let steamapps_path = Path::new(&f.path).join("steamapps");
133 for &app_id in f.apps.keys() {
134 let appmanifest_path = steamapps_path.join(format!("appmanifest_{app_id}.acf"));
135 match File::open(&appmanifest_path).map(keyvalues_serde::from_reader::<_, AppState>) {
136 Ok(a) => match a {
137 Ok(mut a) => {
138 a.library_path = f.path.clone();
139 a.game_path = steamapps_path
140 .join("common")
141 .join(&a.installdir)
142 .to_string_lossy()
143 .to_string();
144 apps.push(a);
145 }
146 Err(e) => {
147 error!(
148 "Failed to read appmanifest {}: {e}",
149 appmanifest_path.display()
150 );
151 }
152 },
153 Err(e) => {
154 warn!(
156 "Failed to open appmanifest {}: {e}",
157 appmanifest_path.display()
158 );
159 }
160 }
161 }
162 }
163
164 Ok(apps)
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_appstate_serde() {
173 const MANIFEST_DATA: &str = r#"
174"AppState"
175{
176 "appid" "70"
177 "universe" "1"
178 "LauncherPath" "C:\\Program Files (x86)\\Steam\\steam.exe"
179 "name" "Half-Life"
180 "StateFlags" "6"
181 "installdir" "Half-Life"
182 "LastUpdated" "1703587250"
183 "SizeOnDisk" "589449723"
184 "StagingSize" "0"
185 "buildid" "13032868"
186 "LastOwner" "76561198166639473"
187 "UpdateResult" "0"
188 "BytesToDownload" "42478352"
189 "BytesDownloaded" "0"
190 "BytesToStage" "127625842"
191 "BytesStaged" "0"
192 "TargetBuildID" "13032868"
193 "AutoUpdateBehavior" "0"
194 "AllowOtherDownloadsWhileRunning" "0"
195 "ScheduledAutoUpdate" "1706853353"
196 "InstalledDepots"
197 {
198 "1"
199 {
200 "manifest" "6665583105370934040"
201 "size" "513399487"
202 }
203 "3"
204 {
205 "manifest" "6081070194444336449"
206 "size" "893958"
207 }
208 "71"
209 {
210 "manifest" "5133329123964362030"
211 "size" "16416909"
212 }
213 "96"
214 {
215 "manifest" "6298465564582633871"
216 "size" "9067684"
217 }
218 "2"
219 {
220 "manifest" "3124227209284380614"
221 "size" "49671685"
222 }
223 }
224 "SharedDepots"
225 {
226 "228988" "228980"
227 }
228 "UserConfig"
229 {
230 "language" "english"
231 }
232 "MountedConfig"
233 {
234 "language" "english"
235 }
236}
237"#;
238
239 let app_state: AppState =
240 keyvalues_serde::from_str(MANIFEST_DATA).expect("Failed to parse app manifest data");
241
242 assert_eq!(app_state.appid, 70);
243 assert_eq!(
245 app_state.LauncherPath,
246 Some("C:\\Program Files (x86)\\Steam\\steam.exe".to_string())
247 );
248 assert_eq!(app_state.name, "Half-Life");
249 assert_eq!(app_state.StateFlags, 6);
250 assert_eq!(app_state.installdir, "Half-Life");
251 assert_eq!(app_state.LastUpdated, Some(1703587250));
252 assert_eq!(app_state.SizeOnDisk, 589449723);
253 assert_eq!(app_state.StagingSize, Some(0));
254 assert_eq!(app_state.buildid, 13032868);
255 assert_eq!(app_state.LastOwner, Some(76561198166639473));
256 assert_eq!(app_state.UpdateResult, Some(0));
257 assert_eq!(app_state.BytesToDownload, Some(42478352));
258 assert_eq!(app_state.BytesDownloaded, Some(0));
259 assert_eq!(app_state.BytesToStage, Some(127625842));
260 assert_eq!(app_state.BytesStaged, Some(0));
261 assert_eq!(app_state.TargetBuildID, Some(13032868));
262 assert_eq!(app_state.AutoUpdateBehavior, Some(0));
263 assert_eq!(app_state.ScheduledAutoUpdate, Some(1706853353));
265
266 assert_eq!(app_state.InstalledDepots.len(), 5);
267 }
269
270 #[test]
271 fn test_libraryfolders_serde() {
272 const MANIFEST_DATA: &str = r#"
273"libraryfolders"
274{
275 "0"
276 {
277 "path" "C:\\Program Files (x86)\\Steam"
278 "label" ""
279 "contentid" "3328371409298419016"
280 "totalsize" "0"
281 "update_clean_bytes_tally" "131786642906"
282 "time_last_update_corruption" "0"
283 "apps"
284 {
285 "228980" "747619496"
286 "250820" "5464658003"
287 "365670" "1174137444"
288 "629730" "9384044754"
289 "992490" "85670250"
290 "1009850" "79179350"
291 "1068820" "825086515"
292 "1826330" "274110"
293 }
294 }
295 "1"
296 {
297 "path" "D:\\Steam"
298 "label" ""
299 "contentid" "1039182383252157525"
300 "totalsize" "2000397791232"
301 "update_clean_bytes_tally" "133903725466"
302 "time_last_update_corruption" "0"
303 "apps"
304 {
305 "70" "589449723"
306 "240" "4628887753"
307 "440" "28179155955"
308 "620" "12753876784"
309 }
310 }
311}
312"#;
313
314 let folders: LibraryFolders =
315 keyvalues_serde::from_str(MANIFEST_DATA).expect("Failed to parse app manifest data");
316
317 assert_eq!(folders.0.len(), 2);
318 }
319}