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