diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-17 00:53:20 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-17 00:53:20 -0500 |
| commit | 06c396fd27f55929b40161e587cb9b596b549f3f (patch) | |
| tree | 40fdb465d2fe6e7d4487bbd237582868c8332459 /libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples | |
| parent | e583d6dda3cde9e8bccee313fe1ad69634593708 (diff) | |
Symlink saves, rank prefixes by Steam app type, ntsync safeguards
- Replace copy-in/copy-out save deployment with a direct symlink from the
prefix's __MO_Saves to the profile's saves/. Writes land in the profile
immediately; afterRun reverts the symlink + prefix INI so a vanilla
launch outside MO2 uses the default Saves dir.
- Parse Steam's binary appinfo.vdf (v41) via a new Rust FFI crate backed
by steam-vdf-parser. Rank detected games by common.type so real Games
beat Tools/Editors when multiple prefixes share a My Games folder name
(fixes Creation Kit shadowing Skyrim SE). Top-ranked candidate
overwrites stale symlinks from earlier runs.
- Switch bundled Steam Linux Runtime downloader from sniper (steamrt3)
to steamrt4 so Proton 11+ works out of the box. steamrt4 omits xrandr
so we inject it from the Debian x11-xserver-utils .deb into a
dedicated dir and prepend it to PATH via `env PATH=...` inside the
pressure-vessel container (PATH env doesn't propagate).
- Auto-kill stale wineboot/wineserver/pv-adverb processes still bound
to the prefix before starting a new wineboot -u; match by
/proc/<pid>/environ WINEPREFIX / STEAM_COMPAT_DATA_PATH since Wine
cmdlines are Windows-style. Same sweep runs in destroyPrefix so a
delete actually frees file handles.
- Blacklist Proton 11 (Steam-bundled) — its current Wine tree deadlocks
during wineboot -u on modern kernels via ntsync / futex_wait_multiple
races. Use "waitforexitandrun" (not "run") for prefix init so
use_sessions=1 Protons don't fork into a persistent session manager.
- Drop WINEDEBUG=-all during prefix init so wineboot progress is
visible. Suppress focus-stealing by setting WA_ShowWithoutActivating
on the PrefixSetupDialog and SLR download progress dialogs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples')
6 files changed, 596 insertions, 0 deletions
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/appid_to_name.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/appid_to_name.rs new file mode 100644 index 0000000..6393869 --- /dev/null +++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/appid_to_name.rs @@ -0,0 +1,103 @@ +// Example: Extract AppId to game name mapping from Steam's appinfo.vdf +// +// Usage: +// cargo run --example appid_to_name -- path/to/appinfo.vdf +// +// The appinfo.vdf file is typically located at: +// - Windows: C:\Program Files (x86)\Steam\appcache\appinfo.vdf +// - Linux: ~/.steam/steam/appcache/appinfo.vdf +// - macOS: ~/Library/Application Support/Steam/appcache/appinfo.vdf + +use std::env; +use std::fs; +use std::process::ExitCode; + +use steam_vdf_parser::parse_appinfo; + +fn main() -> ExitCode { + let args: Vec<String> = env::args().collect(); + + if args.len() < 2 { + eprintln!("Usage: {} <path/to/appinfo.vdf>", args[0]); + eprintln!(); + eprintln!("Extracts AppId to game name mappings from Steam's appinfo.vdf file."); + eprintln!(); + eprintln!("The appinfo.vdf file is typically located at:"); + eprintln!(" Windows: C:\\Program Files (x86)\\Steam\\appcache\\appinfo.vdf"); + eprintln!(" Linux: ~/.steam/steam/appcache/appinfo.vdf"); + eprintln!(" macOS: ~/Library/Application Support/Steam/appcache/appinfo.vdf"); + return ExitCode::FAILURE; + } + + let path = &args[1]; + + // Read the file + let data = match fs::read(path) { + Ok(data) => data, + Err(e) => { + eprintln!("Error reading file '{}': {}", path, e); + return ExitCode::FAILURE; + } + }; + + // Parse the appinfo.vdf file + let vdf = match parse_appinfo(&data) { + Ok(vdf) => vdf.into_owned(), + Err(e) => { + eprintln!("Error parsing appinfo.vdf: {}", e); + return ExitCode::FAILURE; + } + }; + + // Get the root object containing all apps + let root = match vdf.as_obj() { + Some(obj) => obj, + None => { + eprintln!("Error: root is not an object"); + return ExitCode::FAILURE; + } + }; + + // Iterate through all apps (keyed by AppID as string) + let mut apps = Vec::new(); + + for (app_id_str, app_value) in root.iter() { + // Skip non-numeric keys (metadata entries) + if app_id_str.parse::<u32>().is_err() { + continue; + } + + let app_obj = match app_value.as_obj() { + Some(obj) => obj, + None => continue, + }; + + // Navigate the nested structure: appinfo -> common -> name + let name = app_obj + .get("appinfo") + .and_then(|v| v.as_obj()) + .and_then(|appinfo| appinfo.get("common")) + .and_then(|common| common.as_obj()) + .and_then(|common| common.get("name")) + .and_then(|v| v.as_str()); + + if let (Some(name), Ok(app_id)) = (name, app_id_str.parse::<u32>()) { + apps.push((app_id, name.to_string())); + } + } + + // Sort by AppID + apps.sort_by_key(|(id, _)| *id); + + // Print the results + println!("AppId\tName"); + println!("------\t{}", "-".repeat(80)); + for (app_id, name) in &apps { + println!("{}\t{}", app_id, name); + } + + println!(); + println!("Total games: {}", apps.len()); + + ExitCode::SUCCESS +} diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/compactify_vdf.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/compactify_vdf.rs new file mode 100644 index 0000000..64a94ee --- /dev/null +++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/compactify_vdf.rs @@ -0,0 +1,242 @@ +//! Compactify an appinfo.vdf file by keeping only the first N apps. +//! +//! Usage: +//! cargo run --example compactify_vdf <input.vdf> <output.vdf> [--count N] +//! +//! Default count is 5. + +use std::env; +use std::fs; +use std::path::Path; + +use steam_vdf_parser::binary::{ + APPINFO_MAGIC_40, APPINFO_MAGIC_41, read_u32_le_at, read_u64_le_at, +}; + +// App entry header size +const APPINFO_ENTRY_HEADER_SIZE: usize = 68; + +fn main() { + let args: Vec<String> = env::args().collect(); + + if args.len() < 3 { + eprintln!("Usage: {} <input.vdf> <output.vdf> [--count N]", args[0]); + eprintln!(" --count N: Keep only the first N apps (default: 5)"); + std::process::exit(1); + } + + let input_path = Path::new(&args[1]); + let output_path = Path::new(&args[2]); + + // Parse count argument + let mut count: usize = 5; + if args.len() >= 4 { + if args[3] == "--count" { + if args.len() < 5 { + eprintln!("Error: --count requires a number"); + std::process::exit(1); + } + count = match args[4].parse::<usize>() { + Ok(n) if n > 0 => n, + _ => { + eprintln!("Error: count must be a positive integer"); + std::process::exit(1); + } + }; + } else { + eprintln!("Error: unknown argument {}", args[3]); + std::process::exit(1); + } + } + + // Read input file + let data = match fs::read(input_path) { + Ok(d) => d, + Err(e) => { + eprintln!("Error reading input file: {}", e); + std::process::exit(1); + } + }; + + // Parse header + if data.len() < 16 { + eprintln!("Error: file too small to be a valid appinfo.vdf"); + std::process::exit(1); + } + + let magic = match read_u32_le_at(&data, 0) { + Some(m) => m, + None => { + eprintln!("Error: cannot read magic number"); + std::process::exit(1); + } + }; + + let universe = match read_u32_le_at(&data, 4) { + Some(u) => u, + None => { + eprintln!("Error: cannot read universe"); + std::process::exit(1); + } + }; + + let (is_v41, string_table_offset) = match magic { + APPINFO_MAGIC_40 => (false, None), + APPINFO_MAGIC_41 => { + let offset = read_u64_le_at(&data, 8); + (true, offset.map(|o| o as usize)) + } + _ => { + eprintln!( + "Error: invalid magic number {:08x}, expected appinfo.vdf format", + magic + ); + std::process::exit(1); + } + }; + + if is_v41 && string_table_offset.is_none() { + eprintln!("Error: cannot read string table offset for v41 format"); + std::process::exit(1); + } + + println!( + "Detected appinfo.vdf version: {}", + if is_v41 { 41 } else { 40 } + ); + println!("Universe: {}", universe); + if let Some(offset) = string_table_offset { + println!("String table offset: {}", offset); + } + + // Find app entries to keep + // App entries start at offset 16 (after magic + universe + optional string table offset) + const HEADER_SIZE: usize = 16; + + let mut apps_end = data.len(); + if let Some(offset) = string_table_offset { + apps_end = offset; + } + + let mut current_offset = HEADER_SIZE; + let mut selected_apps: Vec<(usize, usize)> = Vec::new(); // (start, size) for each app + + for _ in 0..count { + if current_offset >= apps_end { + break; + } + + // Check we have enough data for the entry header + if current_offset + APPINFO_ENTRY_HEADER_SIZE > data.len() { + eprintln!("Warning: incomplete app entry at offset {}", current_offset); + break; + } + + // Read app ID + let app_id = match read_u32_le_at(&data, current_offset) { + Some(id) => id, + None => { + eprintln!("Error: cannot read app_id at offset {}", current_offset); + std::process::exit(1); + } + }; + + // Check for terminator + if app_id == 0 { + println!( + "Reached terminator (app_id == 0) at offset {}", + current_offset + ); + break; + } + + // Read size field (at offset 4) + let entry_size = match read_u32_le_at(&data, current_offset + 4) { + Some(s) => s as usize, + None => { + eprintln!("Error: cannot read size at offset {}", current_offset + 4); + std::process::exit(1); + } + }; + + // Total size of this app entry = header (8) + size field value + // The size field includes APPINFO_HEADER_AFTER_SIZE (60) + VDF data + let total_entry_size = 8 + entry_size; + + // Verify we have enough data + if current_offset + total_entry_size > apps_end { + eprintln!( + "Warning: app entry extends past string table/EOF at offset {}", + current_offset + ); + break; + } + + println!( + "Selecting app_id {} at offset {}, size {}", + app_id, current_offset, total_entry_size + ); + + selected_apps.push((current_offset, total_entry_size)); + current_offset += total_entry_size; + } + + if selected_apps.is_empty() { + eprintln!("Error: no app entries found in file"); + std::process::exit(1); + } + + println!("Selected {} app entries", selected_apps.len()); + + // Build output file + let mut output = Vec::new(); + + // Write header + output.extend_from_slice(&data[0..HEADER_SIZE]); + + // Update string table offset for v41 (will be calculated later) + let string_table_offset_placeholder = if is_v41 { Some(output.len() - 8) } else { None }; + + // Write selected app entries + for (offset, size) in &selected_apps { + output.extend_from_slice(&data[*offset..*offset + *size]); + } + + // For v41: copy string table + // For v40: add terminator (4 bytes of 0x00) + if is_v41 { + let string_table_offset = string_table_offset.unwrap(); + let string_table_data = &data[string_table_offset..]; + + // Update string table offset in header + let new_offset = output.len() as u64; + let offset_bytes = new_offset.to_le_bytes(); + if let Some(pos) = string_table_offset_placeholder { + output[pos..pos + 8].copy_from_slice(&offset_bytes); + } + + println!( + "String table at original offset {}, new offset {}", + string_table_offset, new_offset + ); + + // Copy string table + output.extend_from_slice(string_table_data); + } else { + // v40: add terminator + output.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); + } + + // Write output file + if let Err(e) = fs::write(output_path, &output) { + eprintln!("Error writing output file: {}", e); + std::process::exit(1); + } + + println!( + "Wrote {} bytes (original: {} bytes, reduction: {}%)", + output.len(), + data.len(), + (data.len() - output.len()) * 100 / data.len() + ); +} diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/dump_vdf.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/dump_vdf.rs new file mode 100644 index 0000000..d0ee2e4 --- /dev/null +++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/dump_vdf.rs @@ -0,0 +1,106 @@ +use std::env; +use std::path::Path; +use steam_vdf_parser::{binary, parse_binary, parse_text}; + +fn dump_value(value: &steam_vdf_parser::Value, indent: usize) -> String { + let indent_str = " ".repeat(indent); + match value { + steam_vdf_parser::Value::Str(s) => format!("{}\"{}\"", indent_str, s), + steam_vdf_parser::Value::I32(n) => format!("{}{}", indent_str, n), + steam_vdf_parser::Value::U64(n) => format!("{}{}", indent_str, n), + steam_vdf_parser::Value::Float(n) => format!("{}{}", indent_str, n), + steam_vdf_parser::Value::Pointer(n) => format!("{}(pointer: {})", indent_str, n), + steam_vdf_parser::Value::Color(c) => format!( + "{}(color: #{:02x}{:02x}{:02x}{:02x})", + indent_str, c[0], c[1], c[2], c[3] + ), + steam_vdf_parser::Value::Obj(obj) => { + let mut out = format!("{}{{\n", indent_str); + for (k, v) in obj.iter() { + out.push_str(&format!("{}\"\"{}\"\": ", indent_str, k)); + match v { + steam_vdf_parser::Value::Obj(_) => { + out.push_str(&dump_value(v, indent + 1)); + } + steam_vdf_parser::Value::Str(s) => out.push_str(&format!("\"{}\"\n", s)), + steam_vdf_parser::Value::I32(n) => out.push_str(&format!("{}\n", n)), + steam_vdf_parser::Value::U64(n) => out.push_str(&format!("{}\n", n)), + steam_vdf_parser::Value::Float(n) => out.push_str(&format!("{}\n", n)), + steam_vdf_parser::Value::Pointer(n) => { + out.push_str(&format!("(pointer: {})\n", n)) + } + steam_vdf_parser::Value::Color(c) => out.push_str(&format!( + "(color: #{:02x}{:02x}{:02x}{:02x})\n", + c[0], c[1], c[2], c[3] + )), + } + } + out.push_str(&format!("{}}}\n", indent_str)); + out + } + } +} + +fn main() { + let args: Vec<String> = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} <vdf_file> [--text]", args[0]); + eprintln!( + " --text: force text format parsing (default: auto-detect based on file extension)" + ); + std::process::exit(1); + } + + let path = Path::new(&args[1]); + let force_text = args.len() > 2 && args[2] == "--text"; + + let result = if force_text || path.extension().is_some_and(|e| e == "vdf") { + // Try text first for .vdf files + let content = std::fs::read_to_string(path); + if let Ok(content) = content { + parse_text(&content).map(|v| v.into_owned()) + } else { + // Fall back to binary + let data = std::fs::read(path).expect("Failed to read file"); + if path + .file_name() + .is_some_and(|n| n.to_str().is_some_and(|s| s.contains("packageinfo"))) + { + binary::parse_packageinfo(&data).map(|v| v.into_owned()) + } else if path + .file_name() + .is_some_and(|n| n.to_str().is_some_and(|s| s.contains("appinfo"))) + { + binary::parse_appinfo(&data).map(|v| v.into_owned()) + } else { + parse_binary(&data).map(|v| v.into_owned()) + } + } + } else { + // Binary parsing + let data = std::fs::read(path).expect("Failed to read file"); + if path + .file_name() + .is_some_and(|n| n.to_str().is_some_and(|s| s.contains("packageinfo"))) + { + binary::parse_packageinfo(&data).map(|v| v.into_owned()) + } else if path + .file_name() + .is_some_and(|n| n.to_str().is_some_and(|s| s.contains("appinfo"))) + { + binary::parse_appinfo(&data).map(|v| v.into_owned()) + } else { + parse_binary(&data).map(|v| v.into_owned()) + } + }; + + match result { + Ok(vdf) => { + println!("\"{}\" {}", vdf.key(), dump_value(vdf.value(), 0)); + } + Err(e) => { + eprintln!("Error parsing VDF: {:?}", e); + std::process::exit(1); + } + } +} diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/print.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/print.rs new file mode 100644 index 0000000..b022a1f --- /dev/null +++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/print.rs @@ -0,0 +1,59 @@ +//! Pretty-print a VDF file using the `{:#}` Display format. +//! +//! Usage: cargo run --example pretty_print <vdf_file> +//! +//! This example demonstrates the pretty-print Display implementation that +//! outputs valid VDF text format with proper indentation and escaping. + +use std::env; +use std::path::Path; +use steam_vdf_parser::{binary, parse_binary, parse_text}; + +fn main() { + let args: Vec<String> = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} <vdf_file>", args[0]); + eprintln!(); + eprintln!("Pretty-prints a VDF file to stdout in valid VDF text format."); + eprintln!( + "Supports both text (.vdf) and binary (appinfo.vdf, packageinfo.vdf, shortcuts.vdf) formats." + ); + std::process::exit(1); + } + + let path = Path::new(&args[1]); + let data = std::fs::read(path).expect("Failed to read file"); + + // Try to detect format and parse accordingly + let result = if let Ok(text) = std::str::from_utf8(&data) { + // Looks like text, try text parser first + parse_text(text) + .map(|v| v.into_owned()) + .or_else(|_| parse_binary(&data).map(|v| v.into_owned())) + } else { + // Binary data - detect format from filename + let filename = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default(); + + if filename.contains("packageinfo") { + binary::parse_packageinfo(&data).map(|v| v.into_owned()) + } else if filename.contains("appinfo") { + binary::parse_appinfo(&data).map(|v| v.into_owned()) + } else { + parse_binary(&data).map(|v| v.into_owned()) + } + }; + + match result { + Ok(vdf) => { + // Use the pretty-print Display implementation + println!("{:#}", vdf); + } + Err(e) => { + eprintln!("Error parsing VDF: {:?}", e); + std::process::exit(1); + } + } +} diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/test_parse_real.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/test_parse_real.rs new file mode 100644 index 0000000..97752d4 --- /dev/null +++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/test_parse_real.rs @@ -0,0 +1,41 @@ +use std::fs; +use steam_vdf_parser::{parse_binary, parse_text}; + +fn main() { + // Test localconfig.vdf (text format) + println!("=== Parsing localconfig.vdf (text format) ==="); + let localconfig = fs::read_to_string( + "/home/mexus/.local/share/Steam/userdata/127648749/config/localconfig.vdf", + ); + match localconfig { + Ok(content) => match parse_text(&content) { + Ok(vdf) => { + println!("Success!"); + println!("Root key: {}", vdf.key()); + let obj = vdf.as_obj().unwrap(); + println!("Root has {} keys", obj.len()); + } + Err(e) => { + println!("Parse error: {:?}", e); + } + }, + Err(e) => println!("Error reading: {}", e), + } + + println!("\n=== Parsing appinfo.vdf (binary format) ==="); + let appinfo = fs::read("/home/mexus/.local/share/Steam/appcache/appinfo.vdf"); + match appinfo { + Ok(data) => match parse_binary(&data) { + Ok(vdf) => { + println!("Success!"); + println!("Root key: {}", vdf.key()); + let obj = vdf.as_obj().unwrap(); + println!("Root has {} keys", obj.len()); + } + Err(e) => { + println!("Parse error: {:?}", e); + } + }, + Err(e) => println!("Error reading: {}", e), + } +} diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/verify_compactified.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/verify_compactified.rs new file mode 100644 index 0000000..c36f917 --- /dev/null +++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/verify_compactified.rs @@ -0,0 +1,45 @@ +//! Verify that a compactified appinfo.vdf file parses correctly. + +use std::env; +use steam_vdf_parser::binary::parse_appinfo; + +fn main() { + let args: Vec<String> = env::args().collect(); + + if args.len() < 2 { + eprintln!("Usage: {} <vdf_file>", args[0]); + std::process::exit(1); + } + + let path = &args[1]; + + // Read the file + let data = match std::fs::read(path) { + Ok(d) => d, + Err(e) => { + eprintln!("Error reading file: {}", e); + std::process::exit(1); + } + }; + + // Parse it + match parse_appinfo(&data) { + Ok(vdf) => { + println!("Successfully parsed {}", path); + println!("Root key: {}", vdf.key()); + if let Some(obj) = vdf.as_obj() { + println!("Number of apps: {}", obj.len()); + for (key, _) in obj.iter().take(5) { + println!(" - app_id: {}", key); + } + if obj.len() > 5 { + println!(" ... and {} more", obj.len() - 5); + } + } + } + Err(e) => { + eprintln!("Error parsing file: {:?}", e); + std::process::exit(1); + } + } +} |
