use fs_err::File;
use log::{error, warn};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
#[cfg(unix)]
use std::path::PathBuf;
#[cfg(windows)]
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename = "libraryfolders")]
pub struct LibraryFolders(pub HashMap<usize, LibraryFolder>);
#[derive(Debug, Clone, Deserialize)]
pub struct LibraryFolder {
pub path: String,
pub label: String,
pub contentid: i64,
pub totalsize: usize,
pub update_clean_bytes_tally: Option<usize>,
pub time_last_update_corruption: Option<u64>,
pub apps: HashMap<u32, usize>,
}
#[derive(Debug, Clone, Deserialize)]
#[allow(non_snake_case)]
pub struct AppState {
pub appid: u64,
pub LauncherPath: Option<String>,
pub name: String,
pub StateFlags: u64,
pub installdir: String,
pub LastUpdated: Option<u64>,
pub SizeOnDisk: usize,
pub StagingSize: Option<usize>,
pub buildid: u64,
pub LastOwner: Option<u64>,
pub UpdateResult: Option<u64>,
pub BytesToDownload: Option<usize>,
pub BytesDownloaded: Option<usize>,
pub BytesToStage: Option<usize>,
pub BytesStaged: Option<usize>,
pub TargetBuildID: Option<usize>,
pub AutoUpdateBehavior: Option<u64>,
pub ScheduledAutoUpdate: Option<u64>,
pub InstalledDepots: HashMap<u64, InstalledDepot>,
pub SharedDepots: Option<HashMap<u64, u64>>,
#[serde(skip)]
pub library_path: String,
#[serde(skip)]
pub game_path: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct InstalledDepot {
pub manifest: u64,
pub size: usize,
}
#[cfg(not(any(windows, unix)))]
fn get_steam_path() -> anyhow::Result<String> {
anyhow::bail!("Not supported on this platform")
}
#[cfg(unix)]
fn get_steam_path() -> anyhow::Result<PathBuf> {
use anyhow::Context;
let home = std::env::var("HOME").context("HOME environment variable not set")?;
Ok(Path::new(&home).join(Path::new(".steam/steam")))
}
#[cfg(windows)]
fn get_steam_path_reg(root: winreg::HKEY) -> anyhow::Result<String> {
const STEAM_REGKEY_PATH: &str = "SOFTWARE\\Valve\\Steam";
let hkcu = winreg::RegKey::predef(root);
let steam_key = hkcu.open_subkey(STEAM_REGKEY_PATH)?;
let path: String = steam_key.get_value("SteamPath")?;
Ok(path)
}
#[cfg(windows)]
fn get_steam_path() -> anyhow::Result<PathBuf> {
use anyhow::Context;
use log::info;
let is_wine = std::fs::read_to_string("/proc/version")
.unwrap_or_default()
.contains("Linux");
if is_wine {
info!("Detected Wine, looking for Linux Steam directory instead");
let username = std::env::var("USERNAME").context("%USERNAME% is not set")?;
let home = format!("/home/{username}");
Ok(Path::new(&home).join(".steam/steam/"))
} else {
get_steam_path_reg(winreg::enums::HKEY_CURRENT_USER)
.or_else(|_| get_steam_path_reg(winreg::enums::HKEY_LOCAL_MACHINE))
.map(|p| Path::new(&p).into())
}
}
pub fn get_all_apps() -> anyhow::Result<Vec<AppState>> {
use anyhow::Context;
use log::debug;
let steam_path = get_steam_path().context("Failed to find Steam installation path")?;
debug!("Using Steam path: {}", steam_path.display());
let vdf_path = Path::new(&steam_path).join("config/libraryfolders.vdf");
let mut apps = vec![];
let folders: LibraryFolders = keyvalues_serde::from_reader(File::open(vdf_path)?)?;
for f in folders.0.values() {
let steamapps_path = Path::new(&f.path).join("steamapps");
for &app_id in f.apps.keys() {
let appmanifest_path = steamapps_path.join(format!("appmanifest_{app_id}.acf"));
match File::open(&appmanifest_path).map(keyvalues_serde::from_reader::<_, AppState>) {
Ok(a) => match a {
Ok(mut a) => {
a.library_path = f.path.clone();
a.game_path = steamapps_path
.join("common")
.join(&a.installdir)
.to_string_lossy()
.to_string();
apps.push(a);
}
Err(e) => {
error!(
"Failed to read appmanifest {}: {e}",
appmanifest_path.display()
);
}
},
Err(e) => {
warn!(
"Failed to open appmanifest {}: {e}",
appmanifest_path.display()
);
}
}
}
}
Ok(apps)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_appstate_serde() {
const MANIFEST_DATA: &str = r#"
"AppState"
{
"appid" "70"
"universe" "1"
"LauncherPath" "C:\\Program Files (x86)\\Steam\\steam.exe"
"name" "Half-Life"
"StateFlags" "6"
"installdir" "Half-Life"
"LastUpdated" "1703587250"
"SizeOnDisk" "589449723"
"StagingSize" "0"
"buildid" "13032868"
"LastOwner" "76561198166639473"
"UpdateResult" "0"
"BytesToDownload" "42478352"
"BytesDownloaded" "0"
"BytesToStage" "127625842"
"BytesStaged" "0"
"TargetBuildID" "13032868"
"AutoUpdateBehavior" "0"
"AllowOtherDownloadsWhileRunning" "0"
"ScheduledAutoUpdate" "1706853353"
"InstalledDepots"
{
"1"
{
"manifest" "6665583105370934040"
"size" "513399487"
}
"3"
{
"manifest" "6081070194444336449"
"size" "893958"
}
"71"
{
"manifest" "5133329123964362030"
"size" "16416909"
}
"96"
{
"manifest" "6298465564582633871"
"size" "9067684"
}
"2"
{
"manifest" "3124227209284380614"
"size" "49671685"
}
}
"SharedDepots"
{
"228988" "228980"
}
"UserConfig"
{
"language" "english"
}
"MountedConfig"
{
"language" "english"
}
}
"#;
let app_state: AppState =
keyvalues_serde::from_str(MANIFEST_DATA).expect("Failed to parse app manifest data");
assert_eq!(app_state.appid, 70);
assert_eq!(
app_state.LauncherPath,
Some("C:\\Program Files (x86)\\Steam\\steam.exe".to_string())
);
assert_eq!(app_state.name, "Half-Life");
assert_eq!(app_state.StateFlags, 6);
assert_eq!(app_state.installdir, "Half-Life");
assert_eq!(app_state.LastUpdated, Some(1703587250));
assert_eq!(app_state.SizeOnDisk, 589449723);
assert_eq!(app_state.StagingSize, Some(0));
assert_eq!(app_state.buildid, 13032868);
assert_eq!(app_state.LastOwner, Some(76561198166639473));
assert_eq!(app_state.UpdateResult, Some(0));
assert_eq!(app_state.BytesToDownload, Some(42478352));
assert_eq!(app_state.BytesDownloaded, Some(0));
assert_eq!(app_state.BytesToStage, Some(127625842));
assert_eq!(app_state.BytesStaged, Some(0));
assert_eq!(app_state.TargetBuildID, Some(13032868));
assert_eq!(app_state.AutoUpdateBehavior, Some(0));
assert_eq!(app_state.ScheduledAutoUpdate, Some(1706853353));
assert_eq!(app_state.InstalledDepots.len(), 5);
}
#[test]
fn test_libraryfolders_serde() {
const MANIFEST_DATA: &str = r#"
"libraryfolders"
{
"0"
{
"path" "C:\\Program Files (x86)\\Steam"
"label" ""
"contentid" "3328371409298419016"
"totalsize" "0"
"update_clean_bytes_tally" "131786642906"
"time_last_update_corruption" "0"
"apps"
{
"228980" "747619496"
"250820" "5464658003"
"365670" "1174137444"
"629730" "9384044754"
"992490" "85670250"
"1009850" "79179350"
"1068820" "825086515"
"1826330" "274110"
}
}
"1"
{
"path" "D:\\Steam"
"label" ""
"contentid" "1039182383252157525"
"totalsize" "2000397791232"
"update_clean_bytes_tally" "133903725466"
"time_last_update_corruption" "0"
"apps"
{
"70" "589449723"
"240" "4628887753"
"440" "28179155955"
"620" "12753876784"
}
}
}
"#;
let folders: LibraryFolders =
keyvalues_serde::from_str(MANIFEST_DATA).expect("Failed to parse app manifest data");
assert_eq!(folders.0.len(), 2);
}
}