From a9a5be9c0e0008411aef7917f41fc9841115851c Mon Sep 17 00:00:00 2001 From: CosmicHorror Date: Thu, 14 Dec 2023 21:56:21 -0700 Subject: [PATCH] Prepare another alpha (#54) * Bump version to v2.0.0-alpha.1 * Make `App` plain data --- Cargo.toml | 4 +- src/app.rs | 277 ++++++------------ src/error.rs | 10 - src/library.rs | 9 +- .../steamlocate__app__tests__more_sanity.snap | 56 ++-- .../steamlocate__app__tests__sanity.snap | 54 ++-- 6 files changed, 160 insertions(+), 250 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7866d3d..7b89134 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "steamlocate" -version = "2.0.0-alpha.0" +version = "2.0.0-alpha.1" authors = ["William Venner "] edition = "2018" repository = "https://github.com/WilliamVenner/steamlocate-rs" @@ -30,5 +30,5 @@ locate_backend = { package = "winreg", version = "0.51", optional = true } locate_backend = { package = "dirs", version = "5", optional = true } [dev-dependencies] -insta = { version = "1.34.0", features = ["redactions", "ron"] } +insta = { version = "1.34.0", features = ["ron"] } tempfile = "3.8.1" diff --git a/src/app.rs b/src/app.rs index b38c169..d83c0bc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,7 @@ use crate::{ Error, Library, Result, }; -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; pub struct Iter<'library> { library: &'library Library, @@ -57,45 +57,65 @@ impl<'library> Iterator for Iter<'library> { /// last_user: Some(u64: 76561198040894045) // This will be a steamid_ng::SteamID if the "steamid_ng" feature is enabled /// ) /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq)] #[cfg_attr(test, derive(serde::Serialize))] #[non_exhaustive] +#[serde(rename_all = "PascalCase")] pub struct App { /// The app ID of this Steam app. + #[serde(rename = "appid")] pub app_id: u32, - /// The path to the installation directory of this Steam app. + /// The name of the installation directory of this Steam app. + /// + /// Example: `"GarrysMod"` /// - /// Example: `C:\Program Files (x86)\Steam\steamapps\common\GarrysMod` - pub path: PathBuf, + /// This can be resolved to the actual path off of the library + /// + /// ```rust,ignore + /// let app_dir = library.resolve_app_dir(&app); + /// ``` + #[serde(rename = "installdir")] + pub install_dir: String, /// The store name of the Steam app. + #[serde(rename = "name")] pub name: Option, pub universe: Option, pub launcher_path: Option, pub state_flags: Option, + // TODO: Need to handle this for serializing too before `App` can `impl Serialize` + #[serde(deserialize_with = "de_time_as_secs_from_unix_epoch")] pub last_updated: Option, // Can't find anything on what these values mean. I've seen 0, 2, 4, 6, and 7 pub update_result: Option, pub size_on_disk: Option, + #[serde(rename = "buildid")] pub build_id: Option, pub bytes_to_download: Option, pub bytes_downloaded: Option, pub bytes_to_stage: Option, pub bytes_staged: Option, pub staging_size: Option, + #[serde(rename = "TargetBuildID")] pub target_build_id: Option, - pub auto_update_behavior: AutoUpdateBehavior, - pub allow_other_downloads_while_running: AllowOtherDownloadsWhileRunning, - pub scheduled_auto_update: Option, - pub full_validate_before_next_update: bool, - pub full_validate_after_next_update: bool, + pub auto_update_behavior: Option, + pub allow_other_downloads_while_running: Option, + pub scheduled_auto_update: Option, + pub full_validate_before_next_update: Option, + pub full_validate_after_next_update: Option, + #[serde(default)] pub installed_depots: BTreeMap, + #[serde(default)] pub staged_depots: BTreeMap, + #[serde(default)] pub user_config: BTreeMap, + #[serde(default)] pub mounted_config: BTreeMap, + #[serde(default)] pub install_scripts: BTreeMap, + #[serde(default)] pub shared_depots: BTreeMap, /// The SteamID64 of the last Steam user that played this game on the filesystem. @@ -103,118 +123,30 @@ pub struct App { /// This crate supports [steamid-ng](https://docs.rs/steamid-ng) and can automatically convert this to a [SteamID](https://docs.rs/steamid-ng/*/steamid_ng/struct.SteamID.html) for you. /// /// To enable this support, [use the `steamid_ng` Cargo.toml feature](https://docs.rs/steamlocate/*/steamlocate#using-steamlocate). + #[serde(rename = "LastOwner")] pub last_user: Option, } impl App { - pub(crate) fn new(library_path: &Path, manifest: &Path) -> Result { + pub(crate) fn new(manifest: &Path) -> Result { let contents = fs::read_to_string(manifest).map_err(|io| Error::io(io, manifest))?; - let internal = keyvalues_serde::from_str(&contents).map_err(|err| { - Error::parse(ParseErrorKind::App, ParseError::from_serde(err), manifest) - })?; - let app = Self::from_internal_steam_app(internal, library_path); - - // Check if the installation path exists and is a valid directory - // TODO: this one check really shapes a lot of the API (in terms of how the data for the - // `App` is resolved. Maybe move this to something like - // ```rust - // library.resolve_install_dir(&app)?; - // ``` - if app.path.is_dir() { - Ok(app) - } else { - Err(Error::MissingAppInstall { - app_id: app.app_id, - path: app.path, - }) - } + keyvalues_serde::from_str(&contents) + .map_err(|err| Error::parse(ParseErrorKind::App, ParseError::from_serde(err), manifest)) } +} - pub(crate) fn from_internal_steam_app(internal: InternalApp, library_path: &Path) -> Self { - let InternalApp { - app_id, - universe, - launcher_path, - name, - state_flags, - install_dir, - last_updated, - update_result, - size_on_disk, - build_id, - last_user, - bytes_to_download, - bytes_downloaded, - bytes_to_stage, - bytes_staged, - staging_size, - target_build_id, - auto_update_behavior, - allow_other_downloads_while_running, - scheduled_auto_update, - full_validate_before_next_update, - full_validate_after_next_update, - installed_depots, - staged_depots, - user_config, - mounted_config, - install_scripts, - shared_depots, - } = internal; - - let path = library_path - .join("steamapps") - .join("common") - .join(install_dir); - - let universe = universe.map(Universe::from); - let state_flags = state_flags.map(StateFlags); - let last_updated = last_updated.and_then(time_as_secs_from_unix_epoch); - let scheduled_auto_update = if scheduled_auto_update == Some(0) { - None - } else { - scheduled_auto_update.and_then(time_as_secs_from_unix_epoch) - }; - let allow_other_downloads_while_running = allow_other_downloads_while_running - .map(AllowOtherDownloadsWhileRunning::from) - .unwrap_or_default(); - let auto_update_behavior = auto_update_behavior - .map(AutoUpdateBehavior::from) - .unwrap_or_default(); - let full_validate_before_next_update = full_validate_before_next_update.unwrap_or_default(); - let full_validate_after_next_update = full_validate_after_next_update.unwrap_or_default(); - - Self { - app_id, - universe, - launcher_path, - name, - state_flags, - path, - last_updated, - update_result, - size_on_disk, - build_id, - last_user, - bytes_to_download, - bytes_downloaded, - bytes_to_stage, - bytes_staged, - staging_size, - target_build_id, - auto_update_behavior, - allow_other_downloads_while_running, - scheduled_auto_update, - full_validate_before_next_update, - full_validate_after_next_update, - installed_depots, - staged_depots, - user_config, - mounted_config, - install_scripts, - shared_depots, +macro_rules! impl_deserialize_from_u64 { + ( $ty_name:ty ) => { + impl<'de> Deserialize<'de> for $ty_name { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let value = u64::deserialize(deserializer)?; + Ok(Self::from(value)) + } } - } + }; } #[derive(Debug, Clone, Copy, PartialEq)] @@ -243,7 +175,9 @@ impl From for Universe { } } -#[derive(Clone, Copy, Debug, PartialEq)] +impl_deserialize_from_u64!(Universe); + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[cfg_attr(test, derive(serde::Serialize))] pub struct StateFlags(pub u64); @@ -391,6 +325,17 @@ impl StateFlag { } } +fn de_time_as_secs_from_unix_epoch<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + let maybe_time = + >::deserialize(deserializer)?.and_then(time_as_secs_from_unix_epoch); + Ok(maybe_time) +} + fn time_as_secs_from_unix_epoch(secs: u64) -> Option { let offset = time::Duration::from_secs(secs); time::SystemTime::UNIX_EPOCH.checked_add(offset) @@ -422,6 +367,8 @@ impl Default for AllowOtherDownloadsWhileRunning { } } +impl_deserialize_from_u64!(AllowOtherDownloadsWhileRunning); + #[derive(Debug, Clone, PartialEq)] #[cfg_attr(test, derive(serde::Serialize))] pub enum AutoUpdateBehavior { @@ -442,12 +389,39 @@ impl From for AutoUpdateBehavior { } } +// TODO: Maybe don't have these defaults? impl Default for AutoUpdateBehavior { fn default() -> Self { Self::KeepUpToDate } } +impl_deserialize_from_u64!(AutoUpdateBehavior); + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(test, derive(serde::Serialize))] +pub enum ScheduledAutoUpdate { + Zero, + Time(time::SystemTime), +} + +impl<'de> Deserialize<'de> for ScheduledAutoUpdate { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let sched_auto_upd = match u64::deserialize(deserializer)? { + 0 => Self::Zero, + secs => { + let time = time_as_secs_from_unix_epoch(secs) + .ok_or_else(|| serde::de::Error::custom("Exceeded max time"))?; + Self::Time(time) + } + }; + Ok(sched_auto_upd) + } +} + #[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[cfg_attr(test, derive(serde::Serialize))] #[non_exhaustive] @@ -458,87 +432,26 @@ pub struct Depot { pub dlc_app_id: Option, } -#[derive(Debug, Deserialize)] -pub(crate) struct InternalApp { - #[serde(rename = "appid")] - app_id: u32, - #[serde(rename = "installdir")] - install_dir: String, - #[serde(rename = "Universe")] - universe: Option, - #[serde(rename = "LauncherPath")] - launcher_path: Option, - name: Option, - #[serde(rename = "StateFlags")] - state_flags: Option, - #[serde(rename = "LastUpdated")] - last_updated: Option, - #[serde(rename = "UpdateResult")] - update_result: Option, - #[serde(rename = "SizeOnDisk")] - size_on_disk: Option, - #[serde(rename = "buildid")] - build_id: Option, - #[serde(rename = "LastOwner")] - last_user: Option, - #[serde(rename = "BytesToDownload")] - bytes_to_download: Option, - #[serde(rename = "BytesDownloaded")] - bytes_downloaded: Option, - #[serde(rename = "BytesToStage")] - bytes_to_stage: Option, - #[serde(rename = "BytesStaged")] - bytes_staged: Option, - #[serde(rename = "StagingSize")] - staging_size: Option, - #[serde(rename = "TargetBuildID")] - target_build_id: Option, - #[serde(rename = "AutoUpdateBehavior")] - auto_update_behavior: Option, - #[serde(rename = "AllowOtherDownloadsWhileRunning")] - allow_other_downloads_while_running: Option, - #[serde(rename = "ScheduledAutoUpdate")] - scheduled_auto_update: Option, - #[serde(rename = "FullValidateBeforeNextUpdate")] - full_validate_before_next_update: Option, - #[serde(rename = "FullValidateAfterNextUpdate")] - full_validate_after_next_update: Option, - #[serde(rename = "InstalledDepots")] - installed_depots: BTreeMap, - #[serde(default, rename = "StagedDepots")] - staged_depots: BTreeMap, - #[serde(default, rename = "SharedDepots")] - shared_depots: BTreeMap, - #[serde(rename = "UserConfig")] - user_config: BTreeMap, - #[serde(default, rename = "MountedConfig")] - mounted_config: BTreeMap, - #[serde(default, rename = "InstallScripts")] - install_scripts: BTreeMap, -} - #[cfg(test)] mod tests { use super::*; - fn app_from_manifest_str(s: &str, library_path: &Path) -> App { - let internal: InternalApp = keyvalues_serde::from_str(s).unwrap(); - App::from_internal_steam_app(internal, library_path) + fn app_from_manifest_str(s: &str) -> App { + keyvalues_serde::from_str(s).unwrap() } #[test] fn sanity() { let manifest = include_str!("../tests/assets/appmanifest_230410.acf"); - let app = app_from_manifest_str(manifest, Path::new("C:\\redact\\me")); - // Redact the path because the path separator used is not cross-platform - insta::assert_ron_snapshot!(app, { ".path" => "[path]" }); + let app = app_from_manifest_str(manifest); + insta::assert_ron_snapshot!(app); } #[test] fn more_sanity() { let manifest = include_str!("../tests/assets/appmanifest_599140.acf"); - let app = app_from_manifest_str(manifest, Path::new("/redact/me")); - insta::assert_ron_snapshot!(app, { ".path" => "[path]" }); + let app = app_from_manifest_str(manifest); + insta::assert_ron_snapshot!(app); } #[test] diff --git a/src/error.rs b/src/error.rs index f10aeea..882b7f2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,10 +22,6 @@ pub enum Error { MissingExpectedApp { app_id: u32, }, - MissingAppInstall { - app_id: u32, - path: PathBuf, - }, } impl fmt::Display for Error { @@ -50,12 +46,6 @@ impl fmt::Display for Error { Self::MissingExpectedApp { app_id } => { write!(f, "Missing expected app with id: {}", app_id) } - Self::MissingAppInstall { app_id, path } => write!( - f, - "Missing expected app installation with id: {} at {}", - app_id, - path.display(), - ), } } } diff --git a/src/library.rs b/src/library.rs index 5e3a438..9c7a3aa 100644 --- a/src/library.rs +++ b/src/library.rs @@ -146,11 +146,18 @@ impl Library { .path() .join("steamapps") .join(format!("appmanifest_{}.acf", id)); - App::new(&self.path, &manifest_path) + App::new(&manifest_path) }) } pub fn apps(&self) -> app::Iter { app::Iter::new(self) } + + pub fn resolve_app_dir(&self, app: &App) -> PathBuf { + self.path + .join("steamapps") + .join("common") + .join(&app.install_dir) + } } diff --git a/src/snapshots/steamlocate__app__tests__more_sanity.snap b/src/snapshots/steamlocate__app__tests__more_sanity.snap index ba72d9b..187a72f 100644 --- a/src/snapshots/steamlocate__app__tests__more_sanity.snap +++ b/src/snapshots/steamlocate__app__tests__more_sanity.snap @@ -3,48 +3,48 @@ source: src/app.rs expression: app --- App( - app_id: 599140, - path: "[path]", + appid: 599140, + installdir: "Graveyard Keeper", name: Some("Graveyard Keeper"), - universe: Some(Public), - launcher_path: None, - state_flags: Some(StateFlags(6)), - last_updated: Some(SystemTime( + Universe: Some(Public), + LauncherPath: None, + StateFlags: Some(StateFlags(6)), + LastUpdated: Some(SystemTime( secs_since_epoch: 1672176869, nanos_since_epoch: 0, )), - update_result: Some(0), - size_on_disk: Some(1805798572), - build_id: Some(8559806), - bytes_to_download: Some(24348080), - bytes_downloaded: Some(0), - bytes_to_stage: Some(1284862702), - bytes_staged: Some(0), - staging_size: Some(0), - target_build_id: Some(8559806), - auto_update_behavior: OnlyUpdateOnLaunch, - allow_other_downloads_while_running: Allow, - scheduled_auto_update: Some(SystemTime( + UpdateResult: Some(0), + SizeOnDisk: Some(1805798572), + buildid: Some(8559806), + BytesToDownload: Some(24348080), + BytesDownloaded: Some(0), + BytesToStage: Some(1284862702), + BytesStaged: Some(0), + StagingSize: Some(0), + TargetBuildID: Some(8559806), + AutoUpdateBehavior: Some(OnlyUpdateOnLaunch), + AllowOtherDownloadsWhileRunning: Some(Allow), + ScheduledAutoUpdate: Some(Time(SystemTime( secs_since_epoch: 1678457806, nanos_since_epoch: 0, - )), - full_validate_before_next_update: false, - full_validate_after_next_update: false, - installed_depots: { + ))), + FullValidateBeforeNextUpdate: None, + FullValidateAfterNextUpdate: None, + InstalledDepots: { 599143: Depot( manifest: 8776335556818666951, size: 1805798572, dlcappid: None, ), }, - staged_depots: {}, - user_config: { + StagedDepots: {}, + UserConfig: { "language": "english", }, - mounted_config: { + MountedConfig: { "language": "english", }, - install_scripts: {}, - shared_depots: {}, - last_user: Some(12312312312312312), + InstallScripts: {}, + SharedDepots: {}, + LastOwner: Some(12312312312312312), ) diff --git a/src/snapshots/steamlocate__app__tests__sanity.snap b/src/snapshots/steamlocate__app__tests__sanity.snap index d082c00..9beddf9 100644 --- a/src/snapshots/steamlocate__app__tests__sanity.snap +++ b/src/snapshots/steamlocate__app__tests__sanity.snap @@ -3,45 +3,45 @@ source: src/app.rs expression: app --- App( - app_id: 230410, - path: "[path]", + appid: 230410, + installdir: "Warframe", name: Some("Warframe"), - universe: Some(Public), - launcher_path: Some("C:\\Program Files (x86)\\Steam\\steam.exe"), - state_flags: Some(StateFlags(4)), - last_updated: Some(SystemTime( + Universe: Some(Public), + LauncherPath: Some("C:\\Program Files (x86)\\Steam\\steam.exe"), + StateFlags: Some(StateFlags(4)), + LastUpdated: Some(SystemTime( secs_since_epoch: 1630871495, nanos_since_epoch: 0, )), - update_result: Some(2), - size_on_disk: Some(29070834580), - build_id: Some(6988007), - bytes_to_download: Some(28490671360), - bytes_downloaded: Some(28490671360), - bytes_to_stage: Some(29070834580), - bytes_staged: Some(29070834580), - staging_size: None, - target_build_id: None, - auto_update_behavior: KeepUpToDate, - allow_other_downloads_while_running: UseGlobalSetting, - scheduled_auto_update: None, - full_validate_before_next_update: false, - full_validate_after_next_update: false, - installed_depots: { + UpdateResult: Some(2), + SizeOnDisk: Some(29070834580), + buildid: Some(6988007), + BytesToDownload: Some(28490671360), + BytesDownloaded: Some(28490671360), + BytesToStage: Some(29070834580), + BytesStaged: Some(29070834580), + StagingSize: None, + TargetBuildID: None, + AutoUpdateBehavior: Some(KeepUpToDate), + AllowOtherDownloadsWhileRunning: Some(UseGlobalSetting), + ScheduledAutoUpdate: Some(Zero), + FullValidateBeforeNextUpdate: None, + FullValidateAfterNextUpdate: None, + InstalledDepots: { 230411: Depot( manifest: 1659398175797234554, size: 29070834580, dlcappid: None, ), }, - staged_depots: {}, - user_config: { + StagedDepots: {}, + UserConfig: { "language": "english", }, - mounted_config: {}, - install_scripts: { + MountedConfig: {}, + InstallScripts: { 230411: "installscript.vdf", }, - shared_depots: {}, - last_user: Some(12312312312312312), + SharedDepots: {}, + LastOwner: Some(12312312312312312), )