Initial commit

This commit is contained in:
snow flurry 2023-07-09 20:27:28 -07:00
commit 08103a08fc
9 changed files with 990 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.DS_Store

338
Cargo.lock generated Normal file
View file

@ -0,0 +1,338 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "argh"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab257697eb9496bf75526f0217b5ed64636a9cfafa78b8365c71bd283fcef93e"
dependencies = [
"argh_derive",
"argh_shared",
]
[[package]]
name = "argh_derive"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b382dbd3288e053331f03399e1db106c9fb0d8562ad62cb04859ae926f324fa6"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "argh_shared"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f"
[[package]]
name = "array-init"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
[[package]]
name = "binrw"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab81d22cbd2d745852348b2138f3db2103afa8ce043117a374581926a523e267"
dependencies = [
"array-init",
"binrw_derive",
"bytemuck",
]
[[package]]
name = "binrw_derive"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b019a3efebe7f453612083202887b6f1ace59e20d010672e336eea4ed5be97"
dependencies = [
"either",
"owo-colors",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytemuck"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "getrandom"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "nom"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "overcast"
version = "0.1.0"
dependencies = [
"argh",
"binrw",
"crc32fast",
"dirs",
"itertools",
"steamy-vdf",
]
[[package]]
name = "owo-colors"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "proc-macro2"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"redox_syscall",
"thiserror",
]
[[package]]
name = "steamy-vdf"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533127ad49314bfe71c3d3fd36b3ebac3d24f40618092e70e1cfe8362c7fac79"
dependencies = [
"nom",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
name = "unicode-ident"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "overcast"
description = "CLI tool for managing non-Steam games in Steam"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
binrw = "0.11.2"
crc32fast = "1.3.2"
argh = "0.1.10"
dirs = "5.0.1"
steamy-vdf = "0.2.0"
itertools = "0.11.0"

41
README.md Normal file
View file

@ -0,0 +1,41 @@
# overcast
**overcast** is a tool for managing non-Steam games, known internally as "shortcuts", via the command-line. This tool is intended to be used with the Steam Deck, and support on other platforms should not be expected.
## How to Use
The end goal is for overcast to be managed via a GUI. In the meantime, the info provided by `overcast --help` should get you started:
```
Usage: overcast <command> [<args>]
Manage non-Steam games
Options:
--help display usage information
Commands:
add add a new shortcut
list list all shortcuts
```
### Adding a new shortcut
When adding a new game, you are required to provide its name as will be shown in your Steam library, and the command to launch it.
```
overcast add -n "My Cool Game" \
/home/flurry/Games/my_cool_game/launch.sh --fullscreen
```
You may also want to set the starting directory, since most Linux launcher scripts expect to be in the same directory.
```
overcast add -n "My Cool Game" \
--start-dir /home/flurry/Games/my_cool_game \
/home/flurry/Games/my_cool_game/launch.sh --fullscreen
```
### Listing shortcuts
`overcast list` provides a quick list of the App IDs and names for all shortcuts you have. If you want more info, use the `--verbose` flag.

7
TODO Normal file
View file

@ -0,0 +1,7 @@
* Send commands to running Steam instance:
* Reload shortcuts.vdf on change (steam://resetcollections?)
* Set Steam Play compat tool (+app_change_compat_tool "Proton X.Y")
* Bonus: Get list of installed compat tools so we aren't
blindly guessing
* Add images other than icon
* holy grail: documentation

152
src/game.rs Normal file
View file

@ -0,0 +1,152 @@
use crate::vdf::Dictionary;
pub struct NonSteamGame {
/// UNIX timestamp of the last time the game was played
pub last_play_time: u32,
/// Location of the shortcut (e.g., .desktop file on Linux) the game entry was generated from
pub shortcut_path: String,
/// Executable path for the game
pub executable: String,
/// Whether to allow Steam overlay over the game
pub allow_overlay: bool,
/// Whether the game is hidden from the Steam library(?)
pub is_hidden: bool,
/// Whether the game supports OpenVR
pub open_vr: bool,
/// Unknown
pub devkit_game_id: String,
/// App name as it appears in Steam
pub app_name: String,
/// Starting directory for the game. Default is "./", for the current working directory.
pub start_dir: String,
/// What options to launch the game with. Used with `executable`.
pub launch_options: String,
/// The app ID of the game. Default is to use a randomly-generated integer, to avoid conflict with other installed games.
pub app_id: u32,
/// Unknown. Possibly means this was created by the SteamOS Devkit Client?
pub devkit: bool,
/// If the game is a Flatpak image, the Flatpak app ID of the game
pub flatpak_app_id: String,
/// If the game has a launcher, allows a Steam controller to use the desktop config to interact with it
pub allow_desktop_config: bool,
/// List of tags used for the game
pub tags: Vec<String>,
/// Path to the icon for the game. Both .ico and .png seem to be supported
pub icon: String,
/// Unknown. More Devkit stuff
pub devkit_override_app_id: bool,
}
impl NonSteamGame {
pub fn new<S: AsRef<str> + Default>(
name: S,
executable: S,
launch_options: Option<S>,
icon: Option<S>,
) -> Self {
let app_id = {
let input = format!("{}{}", executable.as_ref(), name.as_ref());
crc32fast::hash(input.as_bytes()) | 0x80000000
};
NonSteamGame {
app_name: name.as_ref().to_owned(),
executable: executable.as_ref().to_owned(),
app_id,
launch_options: launch_options.unwrap_or_default().as_ref().to_owned(),
icon: icon.unwrap_or_default().as_ref().to_owned(),
..Default::default()
}
}
}
macro_rules! append_kv {
($d:expr, $k:literal, $v:expr) => {
$d.insert(String::from($k), crate::vdf::VdfValue::from($v))
};
}
impl Default for NonSteamGame {
fn default() -> Self {
NonSteamGame {
last_play_time: 0,
shortcut_path: String::new(),
executable: String::new(),
allow_overlay: true,
is_hidden: false,
open_vr: false,
devkit_game_id: String::new(),
app_name: String::new(),
start_dir: String::from("./"),
launch_options: String::new(),
app_id: 0,
devkit: false,
flatpak_app_id: String::new(),
allow_desktop_config: true,
tags: Vec::new(),
icon: String::new(),
devkit_override_app_id: false,
}
}
}
impl From<NonSteamGame> for Dictionary {
fn from(value: NonSteamGame) -> Self {
let mut dict = Dictionary::new();
append_kv!(dict, "LastPlayTime", value.last_play_time);
// TODO: .desktop path is stored here; why does steam use this?
append_kv!(dict, "ShortcutPath", &value.shortcut_path);
// quoted string
append_kv!(dict, "Exe", &value.executable);
append_kv!(dict, "AllowOverlay", value.allow_overlay);
append_kv!(dict, "IsHidden", value.is_hidden);
append_kv!(dict, "OpenVR", value.open_vr);
append_kv!(dict, "DevkitGameID", &value.devkit_game_id);
append_kv!(dict, "AppName", &value.app_name);
append_kv!(dict, "StartDir", &value.start_dir);
append_kv!(dict, "LaunchOptions", &value.launch_options);
append_kv!(dict, "appid", value.app_id);
append_kv!(dict, "Devkit", value.devkit);
append_kv!(dict, "FlatpakAppID", &value.flatpak_app_id);
append_kv!(dict, "AllowDesktopConfig", value.allow_desktop_config);
append_kv!(dict, "tags", crate::vdf::VdfValue::from(value.tags));
append_kv!(dict, "icon", &value.icon);
append_kv!(dict, "DevkitOverrideAppID", value.devkit_override_app_id);
dict
}
}
macro_rules! map_to_struct {
($dict:expr, $st:tt { $($ent:tt => $key:literal),+ } ) => {
$st {
$($ent: $dict[$key].clone().try_into()?),+
}
};
}
impl TryFrom<Dictionary> for NonSteamGame {
type Error = Box<dyn std::error::Error>;
// TODO: do we care if this panics? maybe we should care
fn try_from(value: Dictionary) -> Result<Self, Self::Error> {
Ok(map_to_struct!(value, NonSteamGame {
last_play_time => "LastPlayTime",
shortcut_path => "ShortcutPath",
executable => "Exe",
allow_overlay => "AllowOverlay",
is_hidden => "IsHidden",
open_vr => "OpenVR",
devkit_game_id => "DevkitGameID",
app_name => "AppName",
start_dir => "StartDir",
launch_options => "LaunchOptions",
app_id => "appid",
devkit => "Devkit",
flatpak_app_id => "FlatpakAppID",
allow_desktop_config => "AllowDesktopConfig",
tags => "tags",
icon => "icon",
devkit_override_app_id => "DevkitOverrideAppID"
}))
}
}

171
src/main.rs Normal file
View file

@ -0,0 +1,171 @@
use std::path::PathBuf;
use argh::FromArgs;
use game::NonSteamGame;
use itertools::Itertools;
use vdf::{Dictionary, VdfValue};
mod game;
mod user;
mod vdf;
#[derive(Debug, FromArgs)]
/// Manage non-Steam games
struct Args {
#[argh(subcommand)]
cmd: Commands,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand)]
enum Commands {
Add(AddArgs),
/// list all shortcuts
List(ListArgs),
}
/// add a new shortcut
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "add")]
struct AddArgs {
/// starting directory
#[argh(option, short = 'd')]
start_dir: Option<String>,
/// path to the game icon
#[argh(option, short = 'i')]
icon: Option<PathBuf>,
/// name of the app as it'll show in Steam
#[argh(option, short = 'n')]
app_name: String,
/// what command and args to run to execute the app
#[argh(positional, greedy)]
command: Vec<String>,
}
/// list all shortcuts
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "list")]
struct ListArgs {
/// print too much info about apps
#[argh(switch, short = 'v')]
verbose: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Args = argh::from_env();
let shortcuts_path = {
let mut dir = user::get_userdir()?;
dir.push("config/shortcuts.vdf");
dir
};
match args.cmd {
Commands::Add(args) => add_shortcut(shortcuts_path, args),
Commands::List(args) => list_shortcuts(shortcuts_path, args),
}
}
fn list_shortcuts(
shortcuts_path: PathBuf,
args: ListArgs,
) -> Result<(), Box<dyn std::error::Error>> {
if shortcuts_path.exists() {
for shortcut in get_shortcuts(shortcuts_path)? {
if args.verbose {
println!("App {} (ID {}):", shortcut.app_name, shortcut.app_id);
println!(" Start from: {}", shortcut.start_dir);
println!(" Target: {}", shortcut.executable);
if !shortcut.launch_options.is_empty() {
println!(" Launch Args: {}", shortcut.launch_options);
}
if !shortcut.tags.is_empty() {
println!(" Tags: {}", shortcut.tags.join(", "));
}
if !shortcut.icon.is_empty() {
println!(" Icon Path: {}", shortcut.icon);
}
} else {
println!("App ID {}: {}", shortcut.app_id, shortcut.app_name);
}
}
} else {
println!("shortcuts.vdf doesn't exist!");
}
Ok(())
}
fn add_shortcut(shortcuts_path: PathBuf, args: AddArgs) -> Result<(), Box<dyn std::error::Error>> {
let mut shortcuts = if shortcuts_path.exists() {
get_shortcuts(shortcuts_path.clone())?
} else {
Vec::new()
};
for sc in &shortcuts {
println!("App {}: {}, {}", sc.app_id, sc.app_name, sc.executable);
}
let executable = prepare_arg(&args.command[0]);
let exe_opts: Option<String> = if args.command.len() > 1 {
Some(args.command[1..].to_vec().iter().map(prepare_arg).join(" "))
} else {
None
};
let mut new_game = NonSteamGame::new(args.app_name, executable, exe_opts, None);
if let Some(start_dir) = &args.start_dir {
new_game.start_dir = format!("\"{}\"", start_dir);
}
if let Some(icon) = &args.icon {
new_game.icon = icon.display().to_string();
}
shortcuts.push(new_game);
let vdf_out: vdf::Dictionary = {
let mut map = Dictionary::new();
let arr: vdf::Dictionary = shortcuts
.into_iter()
.enumerate()
.map(|(idx, game)| (idx.to_string(), VdfValue::Dict(game.into())))
.collect();
map.insert("shortcuts".to_owned(), VdfValue::Dict(arr));
map
};
vdf::write_file(vdf_out, shortcuts_path)?;
Ok(())
}
fn get_shortcuts(path: PathBuf) -> Result<Vec<NonSteamGame>, String> {
let vdf_map =
vdf::read_file(&path).map_err(|x| format!("Error opening {}: {}", &path.display(), x))?;
let vec: Result<Vec<NonSteamGame>, String> = vdf_map["shortcuts"]
.clone()
.into_vec()
.into_iter()
.map(|x| {
if let vdf::VdfValue::Dict(dict) = x {
Ok(NonSteamGame::try_from(dict).unwrap())
} else {
Err("Couldn't parse shortcuts.vdf".to_owned())
}
})
.collect();
vec
}
fn prepare_arg<S: AsRef<str>>(input: S) -> String {
let input = input.as_ref();
if input.contains(char::is_whitespace) {
format!("\"{}\"", input)
} else {
input.to_owned()
}
}

68
src/user.rs Normal file
View file

@ -0,0 +1,68 @@
use std::{fmt, ops::Deref, path::PathBuf};
#[derive(Debug)]
pub struct HeuristicError {
message: String,
}
impl fmt::Display for HeuristicError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", &self.message)
}
}
impl std::error::Error for HeuristicError {}
impl HeuristicError {
fn new<S: AsRef<str>>(mesg: S) -> Self {
Self {
message: mesg.as_ref().to_owned(),
}
}
}
pub fn get_steamdir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut dir = dirs::data_local_dir().unwrap_or_else(|| {
let mut dir = dirs::home_dir().unwrap();
dir.push(".local/share");
dir
});
dir.push("Steam");
Ok(dir)
}
/// Attempt to get the userdata dir via loginusers.vdf
pub fn get_userdir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let steam_dir = get_steamdir()?;
let users_path = {
let mut dir = steam_dir.clone();
dir.push("config/loginusers.vdf");
dir
};
if let steamy_vdf::Entry::Table(table) = steamy_vdf::load(users_path)? {
if let steamy_vdf::Entry::Table(users) = &table["users"] {
let mut last_id: Option<u64> = None;
for (uid, data) in users.iter() {
let data = data.as_table().unwrap();
if let Some(steamy_vdf::Entry::Value(most_recent)) = data.get("MostRecent") {
if most_recent.deref() == "1" {
last_id = Some(uid.parse()?);
break;
}
}
}
if let Some(last_id) = last_id {
let mut userdir = steam_dir;
userdir.push(format!("userdata/{}", last_id & 0xffffffff));
return Ok(userdir);
}
}
} else {
return Err(HeuristicError::new(
"steamy_vdf::load gave us something other than Table ???",
))?;
}
Err(HeuristicError::new("Couldn't guess your user ID"))?
}

196
src/vdf.rs Normal file
View file

@ -0,0 +1,196 @@
use binrw::{until, BinRead, BinWrite, NullString};
use std::{borrow::Cow, collections::HashMap, fmt, fs::File, path::Path};
#[derive(BinRead, BinWrite, Clone, PartialEq)]
#[brw(little)]
enum VdfBlock {
#[brw(magic(0u8))]
Dict(NullString, VdfDict),
#[brw(magic(1u8))]
Str(NullString, NullString),
#[brw(magic(2u8))]
Int(NullString, u32),
#[brw(magic(8u8))]
EndDict,
}
#[derive(BinRead, BinWrite, Clone, PartialEq)]
#[brw(little)]
struct VdfDict(
#[br(parse_with(until(|block: &VdfBlock| block == &VdfBlock::EndDict)))] Vec<VdfBlock>,
);
#[derive(Clone)]
pub enum VdfValue {
Dict(Dictionary),
Str(String),
Int(u32),
}
impl VdfValue {
pub fn into_vec(self) -> Vec<VdfValue> {
if let VdfValue::Dict(dict) = self {
// lazily assuming this is an array of strings
dict.into_values().collect()
} else {
panic!("Not a Dict! (maybe this should be less brutal of error mgmt!)");
}
}
}
pub type Dictionary = HashMap<String, VdfValue>;
macro_rules! as_str {
($var:expr) => {{
use std::borrow::Borrow;
String::from_utf8_lossy($var.borrow()).to_string()
}};
}
// === VdfValue Conversions ===
// Conversion error type for TryFrom
#[derive(Debug)]
pub enum VdfError {
WrongType,
}
impl fmt::Display for VdfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VdfError::WrongType => write!(f, "Attempt to convert value to wrong type"),
}
}
}
impl std::error::Error for VdfError {}
// macros
macro_rules! impl_convert_to_vdfvalue {
($t:ty, $e:tt, from_only) => {
impl From<$t> for VdfValue {
fn from(value: $t) -> Self {
VdfValue::$e(value.into())
}
}
};
($t:ty, $e:tt) => {
impl_convert_to_vdfvalue!($t, $e, from_only);
impl TryFrom<VdfValue> for $t {
type Error = VdfError;
fn try_from(value: VdfValue) -> Result<Self, Self::Error> {
if let VdfValue::$e(x) = value {
Ok(x)
} else {
Err(VdfError::WrongType)
}
}
}
};
}
impl_convert_to_vdfvalue!(u32, Int);
impl_convert_to_vdfvalue!(String, Str);
impl_convert_to_vdfvalue!(&String, Str, from_only);
impl_convert_to_vdfvalue!(&str, Str, from_only);
impl_convert_to_vdfvalue!(bool, Int, from_only);
impl_convert_to_vdfvalue!(Dictionary, Dict);
impl_convert_to_vdfvalue!(Cow<'_, str>, Str, from_only);
// bool requires a special TryFrom<VdfValue>
impl TryFrom<VdfValue> for bool {
type Error = VdfError;
fn try_from(value: VdfValue) -> Result<Self, Self::Error> {
if let VdfValue::Int(x) = value {
Ok(!matches!(x, 0))
} else {
Err(VdfError::WrongType)
}
}
}
// Arrays also require special TryFrom
impl<T> TryFrom<VdfValue> for Vec<T>
where
T: TryFrom<VdfValue>,
{
type Error = VdfError;
fn try_from(value: VdfValue) -> Result<Self, Self::Error> {
if let VdfValue::Dict(dict) = value {
// lazily assuming this is an array of strings
let results: Result<Vec<T>, T::Error> = dict.into_values().map(T::try_from).collect();
results.map_err(|_| VdfError::WrongType)
} else {
Err(VdfError::WrongType)
}
}
}
impl<T> From<Vec<T>> for VdfValue
where
T: Into<VdfValue>,
{
fn from(value: Vec<T>) -> Self {
let dict: HashMap<String, VdfValue> = value
.into_iter()
.enumerate()
.map(|(idx, val)| (idx.to_string(), val.into()))
.collect();
VdfValue::Dict(dict)
}
}
/// === VdfDict (raw) to Dictionary Conversions ===
impl From<VdfDict> for Dictionary {
fn from(value: VdfDict) -> Self {
let map: Dictionary = value
.0
.iter()
.filter_map(|block| match block.clone() {
VdfBlock::Dict(k, v) => Some((as_str!(k), VdfValue::Dict(v.into()))),
VdfBlock::Str(k, v) => Some((as_str!(k), VdfValue::Str(as_str!(v)))),
VdfBlock::Int(k, v) => Some((as_str!(k), VdfValue::Int(v))),
VdfBlock::EndDict => None,
})
.collect();
map
}
}
impl From<Dictionary> for VdfDict {
fn from(value: Dictionary) -> Self {
let mut dict: Vec<VdfBlock> = value
.into_iter()
.map(|ent| {
let key = NullString::from(ent.0.as_str());
match ent.1 {
VdfValue::Dict(v) => VdfBlock::Dict(key, v.into()),
VdfValue::Int(v) => VdfBlock::Int(key, v),
VdfValue::Str(v) => VdfBlock::Str(key, NullString::from(v.as_str())),
}
})
.collect();
dict.push(VdfBlock::EndDict);
VdfDict(dict)
}
}
// === Exported Functions ===
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<Dictionary, Box<dyn std::error::Error>> {
let mut fd = File::open(path)?;
Ok(VdfDict::read(&mut fd)?.into())
}
pub fn write_file<P: AsRef<Path>>(
dict: Dictionary,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
let mut fd = File::create(path)?;
VdfDict::from(dict).write(&mut fd)?;
Ok(())
}