use std::path::{Path, PathBuf};
pub(crate) const MTGA_APP_ID: u32 = 2_141_910;
pub(crate) fn steam_library_for_appid(appid: u32) -> Option<PathBuf> {
let home = std::env::var_os("HOME").map(PathBuf::from)?;
let default_steam = home.join(".local").join("share").join("Steam");
let vdf_path = default_steam.join("steamapps").join("libraryfolders.vdf");
if vdf_path.exists() {
if let Ok(contents) = std::fs::read_to_string(&vdf_path) {
if let Some(library) = parse_library_for_appid(&contents, appid) {
return Some(library);
}
}
}
let compat = default_compat_path(&default_steam, appid);
if compat.exists() {
return Some(default_steam);
}
None
}
pub(crate) fn parse_library_for_appid(vdf_content: &str, appid: u32) -> Option<PathBuf> {
let appid_str = appid.to_string();
let mut current_path: Option<String> = None;
let mut in_apps_block = false;
let mut brace_depth: u32 = 0;
let mut apps_depth: u32 = 0;
for raw_line in vdf_content.lines() {
let line = raw_line.trim();
if line == "{" {
brace_depth += 1;
continue;
}
if line == "}" {
if in_apps_block && brace_depth == apps_depth {
in_apps_block = false;
current_path = None;
}
brace_depth = brace_depth.saturating_sub(1);
continue;
}
if in_apps_block {
if let Some(found_id) = extract_first_quoted(line) {
if found_id == appid_str {
return current_path.map(PathBuf::from);
}
}
continue;
}
if extract_first_quoted(line) == Some("apps") && extract_key_value(line).is_none() {
in_apps_block = true;
apps_depth = brace_depth + 1;
} else if let Some((key, value)) = extract_key_value(line) {
if key == "path" {
current_path = Some(value.to_string());
}
}
}
None
}
fn extract_key_value(line: &str) -> Option<(&str, &str)> {
let (key, rest) = extract_quoted_and_rest(line)?;
let value = extract_first_quoted(rest.trim_start())?;
Some((key, value))
}
fn extract_first_quoted(s: &str) -> Option<&str> {
let start = s.find('"')? + 1;
let end = s[start..].find('"')? + start;
Some(&s[start..end])
}
fn extract_quoted_and_rest(s: &str) -> Option<(&str, &str)> {
let open = s.find('"')? + 1;
let close = s[open..].find('"')? + open;
Some((&s[open..close], &s[close + 1..]))
}
fn default_compat_path(steam_root: &Path, appid: u32) -> PathBuf {
steam_root
.join("steamapps")
.join("compatdata")
.join(appid.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_VDF_SINGLE: &str = r#"
"libraryfolders"
{
"0"
{
"path" "/home/user/.local/share/Steam"
"label" ""
"contentid" "123"
"totalsize" "0"
"update_clean_bytes_tally" "0"
"time_last_update_corruption" "0"
"apps"
{
"228980" "184139082"
"2141910" "12345678"
"730" "87654321"
}
}
}
"#;
const SAMPLE_VDF_MULTI: &str = r#"
"libraryfolders"
{
"0"
{
"path" "/home/user/.local/share/Steam"
"apps"
{
"730" "87654321"
}
}
"1"
{
"path" "/mnt/games/SteamLibrary"
"apps"
{
"2141910" "12345678"
"440" "99999999"
}
}
}
"#;
const SAMPLE_VDF_NO_MTGA: &str = r#"
"libraryfolders"
{
"0"
{
"path" "/home/user/.local/share/Steam"
"apps"
{
"730" "87654321"
}
}
}
"#;
#[test]
fn test_parse_library_single_library_contains_mtga() {
let result = parse_library_for_appid(SAMPLE_VDF_SINGLE, 2_141_910);
assert_eq!(result, Some(PathBuf::from("/home/user/.local/share/Steam")));
}
#[test]
fn test_parse_library_multi_library_mtga_on_second() {
let result = parse_library_for_appid(SAMPLE_VDF_MULTI, 2_141_910);
assert_eq!(result, Some(PathBuf::from("/mnt/games/SteamLibrary")));
}
#[test]
fn test_parse_library_other_app_on_first() {
let result = parse_library_for_appid(SAMPLE_VDF_MULTI, 730);
assert_eq!(result, Some(PathBuf::from("/home/user/.local/share/Steam")));
}
#[test]
fn test_parse_library_app_not_found_returns_none() {
let result = parse_library_for_appid(SAMPLE_VDF_NO_MTGA, 2_141_910);
assert_eq!(result, None);
}
#[test]
fn test_parse_library_empty_vdf_returns_none() {
let result = parse_library_for_appid("", 2_141_910);
assert_eq!(result, None);
}
#[test]
fn test_parse_library_malformed_vdf_returns_none() {
let result = parse_library_for_appid("not valid vdf content !!!", 2_141_910);
assert_eq!(result, None);
}
#[test]
fn test_parse_library_prefix_collision_does_not_match() {
const VDF_WITH_SIMILAR_ID: &str = r#"
"libraryfolders"
{
"0"
{
"path" "/home/user/.local/share/Steam"
"apps"
{
"21419100" "12345678"
}
}
}
"#;
let result = parse_library_for_appid(VDF_WITH_SIMILAR_ID, 2_141_910);
assert_eq!(result, None);
}
#[test]
fn test_extract_first_quoted_normal() {
assert_eq!(extract_first_quoted(r#""hello""#), Some("hello"));
}
#[test]
fn test_extract_first_quoted_with_whitespace() {
assert_eq!(extract_first_quoted(r#" "my value" "#), Some("my value"));
}
#[test]
fn test_extract_first_quoted_no_quotes_returns_none() {
assert_eq!(extract_first_quoted("no quotes here"), None);
}
#[test]
fn test_extract_key_value_normal() {
let result = extract_key_value(r#" "path" "/home/user/.local/share/Steam""#);
assert_eq!(result, Some(("path", "/home/user/.local/share/Steam")));
}
#[test]
fn test_extract_key_value_only_key_no_value_returns_none() {
let result = extract_key_value(r#" "apps""#);
assert_eq!(result, None);
}
}