aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-17 00:53:20 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-17 00:53:20 -0500
commit06c396fd27f55929b40161e587cb9b596b549f3f (patch)
tree40fdb465d2fe6e7d4487bbd237582868c8332459 /libs
parente583d6dda3cde9e8bccee313fe1ad69634593708 (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')
-rw-r--r--libs/steam_appinfo_ffi/CMakeLists.txt35
-rw-r--r--libs/steam_appinfo_ffi/Cargo.lock76
-rw-r--r--libs/steam_appinfo_ffi/Cargo.toml11
-rw-r--r--libs/steam_appinfo_ffi/include/steam_appinfo_ffi.h24
-rw-r--r--libs/steam_appinfo_ffi/src/lib.rs89
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.github/workflows/ci.yml55
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.gitignore1
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.lock69
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.toml19
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-APACHE190
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-MIT19
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/README.md288
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/appid_to_name.rs103
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/compactify_vdf.rs242
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/dump_vdf.rs106
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/print.rs59
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/test_parse_real.rs41
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/examples/verify_compactified.rs45
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/byte_reader.rs138
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/mod.rs13
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/parser.rs1439
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/types.rs70
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/error.rs410
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/lib.rs220
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/mod.rs7
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/parser.rs408
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/hasher.rs127
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/impls.rs222
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/mod.rs10
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/tests.rs802
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/types.rs346
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/appinfo_10.vdfbin0 -> 44443 bytes
-rwxr-xr-xlibs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/localconfig.vdf100
-rwxr-xr-xlibs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/packageinfo.vdfbin0 -> 31612 bytes
-rw-r--r--libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/real_files.rs123
-rw-r--r--libs/uibase/src/CMakeLists.txt5
36 files changed, 5912 insertions, 0 deletions
diff --git a/libs/steam_appinfo_ffi/CMakeLists.txt b/libs/steam_appinfo_ffi/CMakeLists.txt
new file mode 100644
index 0000000..46d117e
--- /dev/null
+++ b/libs/steam_appinfo_ffi/CMakeLists.txt
@@ -0,0 +1,35 @@
+cmake_minimum_required(VERSION 3.16)
+project(steam_appinfo_ffi)
+
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ set(CARGO_BUILD_TYPE "debug")
+ set(CARGO_BUILD_FLAGS "")
+else()
+ set(CARGO_BUILD_TYPE "release")
+ set(CARGO_BUILD_FLAGS "--release")
+endif()
+
+set(SAI_FFI_DIR ${CMAKE_CURRENT_SOURCE_DIR})
+set(SAI_FFI_LIB ${SAI_FFI_DIR}/target/${CARGO_BUILD_TYPE}/libsteam_appinfo_ffi.so)
+
+add_custom_command(
+ OUTPUT ${SAI_FFI_LIB}
+ COMMAND cargo build ${CARGO_BUILD_FLAGS}
+ WORKING_DIRECTORY ${SAI_FFI_DIR}
+ COMMENT "Building Steam appinfo FFI library (Rust)"
+ DEPENDS
+ ${SAI_FFI_DIR}/Cargo.toml
+ ${SAI_FFI_DIR}/src/lib.rs
+ ${SAI_FFI_DIR}/vendor/steam-vdf-parser/src/binary/parser.rs
+)
+
+add_custom_target(steam_appinfo_ffi_build DEPENDS ${SAI_FFI_LIB})
+
+add_library(steam_appinfo_ffi_wrapper INTERFACE)
+target_link_libraries(steam_appinfo_ffi_wrapper INTERFACE ${SAI_FFI_LIB})
+target_include_directories(steam_appinfo_ffi_wrapper INTERFACE ${SAI_FFI_DIR}/include)
+add_dependencies(steam_appinfo_ffi_wrapper steam_appinfo_ffi_build)
+add_library(mo2::steam_appinfo_ffi ALIAS steam_appinfo_ffi_wrapper)
+
+install(FILES ${SAI_FFI_LIB} DESTINATION lib)
+install(DIRECTORY ${SAI_FFI_DIR}/include/ DESTINATION include)
diff --git a/libs/steam_appinfo_ffi/Cargo.lock b/libs/steam_appinfo_ffi/Cargo.lock
new file mode 100644
index 0000000..7854128
--- /dev/null
+++ b/libs/steam_appinfo_ffi/Cargo.lock
@@ -0,0 +1,76 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mo2_steam_appinfo_ffi"
+version = "0.1.0"
+dependencies = [
+ "steam-vdf-parser",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "steam-vdf-parser"
+version = "0.1.1"
+dependencies = [
+ "foldhash",
+ "hex",
+ "indexmap",
+ "static_assertions",
+ "winnow",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
diff --git a/libs/steam_appinfo_ffi/Cargo.toml b/libs/steam_appinfo_ffi/Cargo.toml
new file mode 100644
index 0000000..f97f9db
--- /dev/null
+++ b/libs/steam_appinfo_ffi/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "mo2_steam_appinfo_ffi"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "steam_appinfo_ffi"
+crate-type = ["cdylib"]
+
+[dependencies]
+steam-vdf-parser = { path = "vendor/steam-vdf-parser" }
diff --git a/libs/steam_appinfo_ffi/include/steam_appinfo_ffi.h b/libs/steam_appinfo_ffi/include/steam_appinfo_ffi.h
new file mode 100644
index 0000000..be53999
--- /dev/null
+++ b/libs/steam_appinfo_ffi/include/steam_appinfo_ffi.h
@@ -0,0 +1,24 @@
+#ifndef MO2_STEAM_APPINFO_FFI_H
+#define MO2_STEAM_APPINFO_FFI_H
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/// Callback invoked once per app in appinfo.vdf. Strings are valid for the
+/// duration of the call only — copy if you want to keep them.
+typedef void (*SteamAppInfoCallback)(void* user, uint32_t appid,
+ const char* type, const char* name);
+
+/// Parse Steam's appinfo.vdf at `path` and invoke `cb` for every app.
+/// Returns 0 on success, negative on error.
+int32_t steam_appinfo_parse(const char* path, void* user,
+ SteamAppInfoCallback cb);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // MO2_STEAM_APPINFO_FFI_H
diff --git a/libs/steam_appinfo_ffi/src/lib.rs b/libs/steam_appinfo_ffi/src/lib.rs
new file mode 100644
index 0000000..c134b97
--- /dev/null
+++ b/libs/steam_appinfo_ffi/src/lib.rs
@@ -0,0 +1,89 @@
+//! C FFI bridge to steam-vdf-parser for reading Steam's appinfo.vdf (binary,
+//! including v41 / magic 0x07564429 with string table).
+//!
+//! Exposes a single function that streams `(appid, type, name)` tuples to a
+//! C callback so the C++ side can build its own data structures without
+//! needing to know the parser's internals.
+
+use std::ffi::{CStr, CString, c_char, c_void};
+use std::fs;
+
+use steam_vdf_parser::parse_appinfo;
+
+/// Callback invoked once per app. `user` is passed through unchanged.
+/// Strings are valid for the duration of the call only — the C++ side must
+/// copy whatever it wants to keep.
+pub type SteamAppInfoCallback =
+ extern "C" fn(user: *mut c_void, appid: u32, type_: *const c_char, name: *const c_char);
+
+/// Parse the appinfo.vdf at `path_c` and call `cb(user, appid, type, name)`
+/// for every app whose `common` section is readable.
+///
+/// Returns 0 on success, negative on error:
+/// -1 = path is null / not valid UTF-8
+/// -2 = file read error
+/// -3 = parse error (unsupported magic, truncated, etc.)
+///
+/// # Safety
+/// `path_c` must be a valid null-terminated C string. `cb` must be non-null.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn steam_appinfo_parse(
+ path_c: *const c_char,
+ user: *mut c_void,
+ cb: SteamAppInfoCallback,
+) -> i32 {
+ if path_c.is_null() {
+ return -1;
+ }
+ let path = match unsafe { CStr::from_ptr(path_c) }.to_str() {
+ Ok(s) => s,
+ Err(_) => return -1,
+ };
+
+ let data = match fs::read(path) {
+ Ok(d) => d,
+ Err(_) => return -2,
+ };
+
+ let vdf = match parse_appinfo(&data) {
+ Ok(v) => v.into_owned(),
+ Err(_) => return -3,
+ };
+
+ let root = match vdf.as_obj() {
+ Some(o) => o,
+ None => return -3,
+ };
+
+ for (app_id_str, app_value) in root.iter() {
+ let appid = match app_id_str.parse::<u32>() {
+ Ok(v) => v,
+ Err(_) => continue,
+ };
+
+ let app_obj = match app_value.as_obj() {
+ Some(o) => o,
+ None => continue,
+ };
+
+ let common = app_obj
+ .get("appinfo")
+ .and_then(|v| v.as_obj())
+ .and_then(|appinfo| appinfo.get("common"))
+ .and_then(|v| v.as_obj());
+
+ let common = match common {
+ Some(c) => c,
+ None => continue,
+ };
+
+ let type_ = common.get("type").and_then(|v| v.as_str()).unwrap_or("");
+ let name = common.get("name").and_then(|v| v.as_str()).unwrap_or("");
+
+ let type_c = CString::new(type_).unwrap_or_default();
+ let name_c = CString::new(name).unwrap_or_default();
+ cb(user, appid, type_c.as_ptr(), name_c.as_ptr());
+ }
+
+ 0
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.github/workflows/ci.yml b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.github/workflows/ci.yml
new file mode 100644
index 0000000..4bd43a0
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.github/workflows/ci.yml
@@ -0,0 +1,55 @@
+name: CI
+
+on:
+ push:
+ branches: [master, main]
+ pull_request:
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ test:
+ name: Test (${{ matrix.rust }})
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ rust: [stable, beta, "1.88"]
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: ${{ matrix.rust }}
+ - run: cargo test --all-features
+
+ fmt:
+ name: Formatting
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: stable
+ components: rustfmt
+ - run: cargo fmt --all --check
+
+ clippy:
+ name: Clippy
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: stable
+ components: clippy
+ - run: cargo clippy --all-targets --all-features -- -D warnings
+
+ docs:
+ name: Documentation
+ runs-on: ubuntu-latest
+ env:
+ RUSTDOCFLAGS: -D warnings
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ - run: cargo doc --no-deps --all-features
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.gitignore b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.lock b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.lock
new file mode 100644
index 0000000..1c1268f
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.lock
@@ -0,0 +1,69 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "steam-vdf-parser"
+version = "0.1.1"
+dependencies = [
+ "foldhash",
+ "hex",
+ "indexmap",
+ "static_assertions",
+ "winnow",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.toml b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.toml
new file mode 100644
index 0000000..2bb0a0c
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "steam-vdf-parser"
+version = "0.1.1"
+edition = "2024"
+rust-version = "1.88"
+license = "MIT OR Apache-2.0"
+description = "Zero-copy parser for Steam's VDF (Valve Data Format) files"
+readme = "README.md"
+keywords = ["vdf", "steam", "parser", "binary", "valve"]
+categories = ["parser-implementations", "data-structures"]
+repository = "https://github.com/mexus/steam-vdf-parser"
+documentation = "https://docs.rs/steam-vdf-parser"
+
+[dependencies]
+hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
+winnow = { version = "0.7.14", default-features = false, features = ["alloc", "simd"] }
+indexmap = { version = "2.13.0", default-features = false }
+foldhash = { version = "0.2.0", default-features = false }
+static_assertions = "1.1.0"
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-APACHE b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-APACHE
new file mode 100644
index 0000000..77242f2
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-APACHE
@@ -0,0 +1,190 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to the Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+Copyright 2026 steam-vdf-parser contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-MIT b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-MIT
new file mode 100644
index 0000000..c816fbd
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2026 steam-vdf-parser contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/README.md b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/README.md
new file mode 100644
index 0000000..8108b7a
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/README.md
@@ -0,0 +1,288 @@
+# steam-vdf-parser
+
+[![Crates.io](https://img.shields.io/crates/v/steam-vdf-parser.svg)](https://crates.io/crates/steam-vdf-parser)
+[![Documentation](https://docs.rs/steam-vdf-parser/badge.svg)](https://docs.rs/steam-vdf-parser)
+[![License](https://img.shields.io/crates/l/steam-vdf-parser.svg)](https://github.com/mexus/steam-vdf-parser#license)
+[![CI](https://github.com/mexus/steam-vdf-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/mexus/steam-vdf-parser/actions/workflows/ci.yml)
+
+A blazing fast, zero-copy parser for Steam's VDF (Valve Data Format) files in Rust.
+
+Supports both text and binary formats used by Steam, including `shortcuts.vdf`, `appinfo.vdf`, and `packageinfo.vdf`.
+
+## Features
+
+- **`no_std` compatible** — works without the standard library, requires only `alloc`
+- **Zero-copy parsing** — text format returns borrowed strings when possible (no escape sequences)
+- **Binary format support** — parses all Steam binary VDF variants
+- **Version-aware** — handles `appinfo.vdf` v40 (null-terminated keys) and v41 (string table)
+- **`serde`-free** — simple, direct data structures with no hidden allocations
+
+## Usage
+
+### Text Format
+
+```rust
+use steam_vdf_parser::parse_text;
+
+let input = r#""root"
+{
+ "key" "value"
+ "nested"
+ {
+ "subkey" "subvalue"
+ }
+}"#;
+
+let vdf = parse_text(input)?;
+assert_eq!(vdf.key(), "root");
+
+// Access nested values
+let obj = vdf.as_obj().unwrap();
+let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
+assert_eq!(value, "value");
+
+// Path-based access
+let subvalue = vdf.get_str(&["nested", "subkey"]).unwrap();
+assert_eq!(subvalue, "subvalue");
+```
+
+### Binary Format (auto-detect)
+
+```rust
+use steam_vdf_parser::parse_binary;
+
+let data = read_vdf_data_somehow()?;
+let vdf = parse_binary(&data)?;
+
+// For data that needs to outlive the input:
+let owned = vdf.into_owned();
+```
+
+### Specific Formats
+
+```rust
+use steam_vdf_parser::{parse_appinfo, parse_packageinfo};
+
+// appinfo.vdf (auto-detects v40/v41)
+let data = read_vdf_data_somehow()?;
+let vdf = parse_appinfo(&data)?;
+
+// packageinfo.vdf
+let data = read_vdf_data_somehow()?;
+let vdf = parse_packageinfo(&data)?;
+```
+
+## `no_std` Support
+
+This library is `no_std` compatible and only requires the `alloc` crate. Enable it in your `Cargo.toml`:
+
+```toml
+[dependencies]
+steam-vdf-parser = "0.1"
+```
+
+For `no_std` environments, make sure to have a global allocator configured:
+
+```rust
+extern crate alloc;
+use steam_vdf_parser::parse_text;
+
+// Your parsing code here
+```
+
+## Data Structures
+
+### `Vdf<'text>`
+
+The top-level VDF document. A VDF document is essentially a single key-value pair at the root level.
+
+```rust
+let vdf = parse_text(input)?;
+
+// Access root key and value
+let key: &str = vdf.key();
+let value: &Value = vdf.value();
+
+// Check if root is an object
+if vdf.is_obj() {
+ let obj = vdf.as_obj().unwrap();
+}
+
+// Direct nested access
+let nested = vdf.get("key"); // Option<&Value>
+
+// Path-based traversal
+let deep = vdf.get_path(&["nested", "deep", "value"]);
+let name = vdf.get_str(&["config", "name"]);
+let count = vdf.get_i32(&["settings", "count"]);
+```
+
+### `Value<'text>` Enum
+
+| Variant | Rust Type | Type Check | Accessor |
+|---------|-----------|------------|----------|
+| `Str` | `Cow<'text, str>` | `is_str()` | `as_str()` |
+| `Obj` | `Obj<'text>` | `is_obj()` | `as_obj()`, `as_obj_mut()` |
+| `I32` | `i32` | `is_i32()` | `as_i32()` |
+| `U64` | `u64` | `is_u64()` | `as_u64()` |
+| `Float` | `f32` | `is_float()` | `as_float()` |
+| `Pointer` | `u32` | `is_pointer()` | `as_pointer()` |
+| `Color` | `[u8; 4]` | `is_color()` | `as_color()` |
+
+Path-based access methods are also available on `Value`:
+
+```rust
+// Traverse nested objects
+let deep = value.get_path(&["nested", "key"]);
+
+// Typed path access
+let name = value.get_str(&["config", "name"]);
+let obj = value.get_obj(&["settings"]);
+let count = value.get_i32(&["stats", "count"]);
+let id = value.get_u64(&["user", "id"]);
+let ratio = value.get_float(&["metrics", "ratio"]);
+```
+
+### `Obj<'text>`
+
+A `HashMap`-backed object (using `hashbrown` for `no_std` compatibility) with O(1) lookup:
+
+```rust
+let obj = vdf.as_obj().unwrap();
+
+// Basic access
+let len = obj.len();
+let is_empty = obj.is_empty();
+let value = obj.get("key"); // Option<&Value>
+let exists = obj.contains_key("key"); // bool
+
+// Iteration
+for (key, value) in obj.iter() {
+ println!("{}: {}", key, value);
+}
+for key in obj.keys() {
+ println!("Key: {}", key);
+}
+for value in obj.values() {
+ println!("Value: {}", value);
+}
+
+// Mutation
+let mut obj = Obj::new();
+obj.insert("key", Value::Str("value".into()));
+obj.get_mut("key"); // Option<&mut Value>
+obj.remove("key"); // Option<Value>
+```
+
+### Lifetime and Ownership
+
+Parsing functions return `Vdf<'_>` with strings borrowed from input where possible. Use `.into_owned()` to convert to `Vdf<'static>`:
+
+```rust
+let borrowed: Vdf<'_> = parse_text(input)?;
+let owned: Vdf<'static> = borrowed.into_owned();
+```
+
+## Binary Format Reference
+
+### Type Bytes
+
+All binary VDF formats use type byte prefixes:
+
+| Byte | Name | Description |
+|------|------|-------------|
+| `0x00` | None/Object | Start of an object (followed by key) |
+| `0x01` | String | Null-terminated UTF-8 string value |
+| `0x02` | Int32 | 4-byte little-endian signed integer |
+| `0x03` | Float | 4-byte little-endian IEEE-754 float |
+| `0x04` | Pointer | 4-byte pointer value (stored as `u32`) |
+| `0x05` | WString | Null-terminated UTF-16LE string (`0x00 0x00` terminator) |
+| `0x06` | Color | 4 bytes RGBA |
+| `0x07` | UInt64 | 8-byte little-endian unsigned integer |
+| `0x08` | ObjectEnd | End of current object |
+
+### Entry Format
+
+`[TypeByte] [Key] [Value...]`
+
+- **Key encoding** varies by format (null-terminated or string table index)
+- **String values** are always inline null-terminated UTF-8 (never from string table)
+
+### shortcuts.vdf
+
+Simple binary format. Keys are null-terminated UTF-8 strings. Ends at EOF.
+
+### appinfo.vdf
+
+#### File Header
+
+| Offset | Size | Field | Description |
+|--------|------|-------|-------------|
+| `0x00` | 4 | Magic | `0x07564428` (v40) or `0x07564429` (v41) |
+| `0x04` | 4 | Universe | Always `1` (public) |
+| `0x08` | 8 | String Table Offset | Present only in v41 |
+
+#### App Entry Header (68 bytes)
+
+| Offset | Size | Field | Description |
+|--------|------|-------|-------------|
+| `0x00` | 4 | App ID | Steam application ID |
+| `0x04` | 4 | Size | Size of remaining data (60 bytes + VDF payload) |
+| `0x08` | 4 | Info State | Flags (e.g., `2` = available) |
+| `0x0C` | 4 | Last Updated | Unix timestamp |
+| `0x10` | 8 | PICS Token | Access token for PICS API |
+| `0x18` | 20 | SHA1 | Hash of VDF payload |
+| `0x2C` | 4 | Change Number | Sequence number |
+| `0x30` | 20 | Binary SHA1 | Hash of binary VDF data |
+
+VDF data starts at offset `0x44` (68 bytes). Length = `Size - 60`.
+
+#### Key Encoding
+
+- **v40**: Keys are null-terminated UTF-8 strings
+- **v41**: Keys are `u32` indices into the string table
+- **All versions**: String *values* are always inline null-terminated UTF-8
+
+#### String Table (v41 only)
+
+Located at `String Table Offset`:
+
+| Offset | Size | Field | Description |
+|--------|------|-------|-------------|
+| `0x00` | 4 | String Count | Number of strings (little-endian) |
+| `0x04` | - | Strings | Null-terminated UTF-8 strings |
+
+### packageinfo.vdf
+
+#### File Header
+
+| Offset | Size | Field | Description |
+|--------|------|-------|-------------|
+| `0x00` | 4 | Magic | Upper 3 bytes: `0x065655`, lower byte: version (`27` = v39, `28` = v40) |
+| `0x04` | 4 | Universe | Always `1` (public) |
+
+#### Package Entry Header
+
+| Offset | Size | Field | Description |
+|--------|------|-------|-------------|
+| `0x00` | 4 | Package ID | `0xFFFFFFFF` marks end of file |
+| `0x04` | 20 | SHA-1 | Hash of VDF payload |
+| `0x18` | 4 | Change Number | Sequence number |
+| `0x1C` | 8 | PICS Token | Present only in v40 |
+
+Followed by binary VDF blob (null-terminated keys, like shortcuts.vdf).
+
+## Performance
+
+Text parsing is zero-copy when strings contain no escape sequences — `Cow::Borrowed` refers directly into the input. Escape sequences and wide strings (UTF-16) cause allocation (`Cow::Owned`).
+
+Binary parsing is also zero-copy for string values. In appinfo v41, root keys (app IDs) are owned due to integer-to-string conversion, but nested values remain borrowed from the string table or input buffer.
+
+## License
+
+Licensed under either of:
+
+- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
+- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+
+at your option.
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);
+ }
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/byte_reader.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/byte_reader.rs
new file mode 100644
index 0000000..26306ce
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/byte_reader.rs
@@ -0,0 +1,138 @@
+//! Utilities for reading little-endian values from byte slices.
+
+/// Reads a little-endian u32 from a byte slice.
+///
+/// Returns `None` if the slice doesn't have enough bytes.
+///
+/// # Examples
+/// ```
+/// use steam_vdf_parser::binary::read_u32_le;
+///
+/// let data = [0x01, 0x02, 0x03, 0x04];
+/// assert_eq!(read_u32_le(&data), Some(0x04030201));
+/// assert_eq!(read_u32_le(&[0x01, 0x02]), None);
+/// ```
+#[inline]
+pub fn read_u32_le(input: &[u8]) -> Option<u32> {
+ input.get(..4).and_then(|bytes| {
+ let arr: [u8; 4] = bytes.try_into().ok()?;
+ Some(u32::from_le_bytes(arr))
+ })
+}
+
+/// Reads a little-endian u64 from a byte slice.
+///
+/// Returns `None` if the slice doesn't have enough bytes.
+///
+/// # Examples
+/// ```
+/// use steam_vdf_parser::binary::read_u64_le;
+///
+/// let data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
+/// assert_eq!(read_u64_le(&data), Some(0x0807060504030201));
+/// assert_eq!(read_u64_le(&[0x01, 0x02]), None);
+/// ```
+#[inline]
+pub fn read_u64_le(input: &[u8]) -> Option<u64> {
+ input.get(..8).and_then(|bytes| {
+ let arr: [u8; 8] = bytes.try_into().ok()?;
+ Some(u64::from_le_bytes(arr))
+ })
+}
+
+/// Reads a little-endian u32 from a byte slice at a specific offset.
+///
+/// Returns `None` if the slice doesn't have enough bytes from the offset.
+///
+/// # Examples
+/// ```
+/// use steam_vdf_parser::binary::read_u32_le_at;
+///
+/// let data = [0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04];
+/// assert_eq!(read_u32_le_at(&data, 2), Some(0x04030201));
+/// assert_eq!(read_u32_le_at(&data, 4), None);
+/// ```
+#[inline]
+pub fn read_u32_le_at(input: &[u8], offset: usize) -> Option<u32> {
+ input.get(offset..offset + 4).and_then(|bytes| {
+ let arr: [u8; 4] = bytes.try_into().ok()?;
+ Some(u32::from_le_bytes(arr))
+ })
+}
+
+/// Reads a little-endian u64 from a byte slice at a specific offset.
+///
+/// Returns `None` if the slice doesn't have enough bytes from the offset.
+///
+/// # Examples
+/// ```
+/// use steam_vdf_parser::binary::read_u64_le_at;
+///
+/// let data = [0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
+/// assert_eq!(read_u64_le_at(&data, 4), Some(0x0807060504030201));
+/// assert_eq!(read_u64_le_at(&data, 8), None);
+/// ```
+#[inline]
+pub fn read_u64_le_at(input: &[u8], offset: usize) -> Option<u64> {
+ input.get(offset..offset + 8).and_then(|bytes| {
+ let arr: [u8; 8] = bytes.try_into().ok()?;
+ Some(u64::from_le_bytes(arr))
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_read_u32_le() {
+ let data = [0x01, 0x02, 0x03, 0x04];
+ assert_eq!(read_u32_le(&data), Some(0x04030201));
+ }
+
+ #[test]
+ fn test_read_u32_le_short() {
+ assert_eq!(read_u32_le(&[0x01, 0x02]), None);
+ assert_eq!(read_u32_le(&[]), None);
+ }
+
+ #[test]
+ fn test_read_u64_le() {
+ let data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
+ assert_eq!(read_u64_le(&data), Some(0x0807060504030201));
+ }
+
+ #[test]
+ fn test_read_u64_le_short() {
+ assert_eq!(read_u64_le(&[0x01, 0x02, 0x03, 0x04]), None);
+ assert_eq!(read_u64_le(&[]), None);
+ }
+
+ #[test]
+ fn test_read_u32_le_at() {
+ let data = [0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04];
+ assert_eq!(read_u32_le_at(&data, 0), Some(0x0201FFFF));
+ assert_eq!(read_u32_le_at(&data, 2), Some(0x04030201));
+ }
+
+ #[test]
+ fn test_read_u32_le_at_out_of_bounds() {
+ let data = [0x01, 0x02, 0x03, 0x04];
+ assert_eq!(read_u32_le_at(&data, 1), None);
+ assert_eq!(read_u32_le_at(&data, 4), None);
+ }
+
+ #[test]
+ fn test_read_u64_le_at() {
+ let data = [0xFF; 12];
+ assert_eq!(read_u64_le_at(&data, 0), Some(0xFFFFFFFFFFFFFFFF));
+ assert_eq!(read_u64_le_at(&data, 4), Some(0xFFFFFFFFFFFFFFFF));
+ }
+
+ #[test]
+ fn test_read_u64_le_at_out_of_bounds() {
+ let data = [0x01; 8];
+ assert_eq!(read_u64_le_at(&data, 1), None);
+ assert_eq!(read_u64_le_at(&data, 8), None);
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/mod.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/mod.rs
new file mode 100644
index 0000000..ac73961
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/mod.rs
@@ -0,0 +1,13 @@
+//! Binary VDF format parser.
+//!
+//! Supports Steam's binary VDF formats:
+//! - shortcuts.vdf format (simple binary)
+//! - appinfo.vdf format (with optional string table)
+
+mod byte_reader;
+mod parser;
+mod types;
+
+pub use byte_reader::{read_u32_le, read_u32_le_at, read_u64_le, read_u64_le_at};
+pub use parser::{parse, parse_appinfo, parse_packageinfo, parse_shortcuts};
+pub use types::{APPINFO_MAGIC_40, APPINFO_MAGIC_41, BinaryType};
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/parser.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/parser.rs
new file mode 100644
index 0000000..9ea1c0e
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/parser.rs
@@ -0,0 +1,1439 @@
+//! Binary VDF parser implementation.
+//!
+//! Supports Steam's binary VDF formats:
+//! - shortcuts.vdf (simple binary format)
+//! - appinfo.vdf (with optional string table)
+
+use alloc::borrow::Cow;
+use alloc::format;
+use alloc::string::{String, ToString};
+use alloc::vec::Vec;
+use core::str;
+
+use crate::binary::byte_reader::{read_u32_le, read_u64_le};
+use crate::binary::types::{
+ APPINFO_MAGIC_40, APPINFO_MAGIC_41, BinaryType, PACKAGEINFO_MAGIC_39, PACKAGEINFO_MAGIC_40,
+ PACKAGEINFO_MAGIC_BASE,
+};
+use crate::error::{Error, Result, with_offset};
+use crate::value::{Obj, Value, Vdf};
+
+// ===== Appinfo Header Constants =====
+
+/// Size of the appinfo entry header (up to and including the size field).
+const APPINFO_HEADER_SIZE: usize = 8;
+
+/// Size of the header after the size field (60 bytes).
+const APPINFO_HEADER_AFTER_SIZE: usize = 60;
+
+/// Total size of the appinfo entry header.
+const APPINFO_ENTRY_HEADER_SIZE: usize = APPINFO_HEADER_SIZE + APPINFO_HEADER_AFTER_SIZE;
+
+/// Offset where VDF data starts within an appinfo entry.
+const APPINFO_VDF_DATA_OFFSET: usize = APPINFO_ENTRY_HEADER_SIZE;
+
+// ===== Helper Functions =====
+
+/// Read a little-endian u32 from the start of a slice, returning an error if too small.
+fn ensure_read_u32_le(input: &[u8]) -> Result<(&[u8], u32)> {
+ read_u32_le(input)
+ .map(|value| (&input[4..], value))
+ .ok_or(Error::UnexpectedEndOfInput {
+ context: "reading u32",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ })
+}
+
+/// Parse configuration for binary VDF formats.
+///
+/// Encapsulates the differences between shortcuts.vdf and appinfo.vdf formats.
+#[derive(Clone, Copy, Debug, PartialEq, Default)]
+struct ParseConfig<'input, 'table> {
+ /// Strategy for parsing keys
+ key_mode: KeyMode<'input, 'table>,
+}
+
+/// Key parsing strategy for binary VDF formats.
+#[derive(Clone, Copy, Debug, PartialEq, Default)]
+enum KeyMode<'input, 'table> {
+ /// Parse keys as null-terminated UTF-8 strings (v40, shortcuts)
+ #[default]
+ NullTerminated,
+ /// Parse keys as u32 indices into string table (v41)
+ StringTableIndex {
+ string_table: &'table StringTable<'input>,
+ },
+}
+
+/// String table for v41 appinfo format.
+///
+/// Encapsulates pre-extracted strings from the string table section,
+/// enabling O(1) lookups by index.
+#[derive(Clone, Debug, PartialEq)]
+struct StringTable<'a> {
+ strings: Vec<&'a str>,
+}
+
+impl<'a> StringTable<'a> {
+ /// Get a string by index.
+ fn get(&self, index: usize) -> Result<&'a str> {
+ self.strings
+ .get(index)
+ .copied()
+ .ok_or(Error::InvalidStringIndex {
+ index,
+ max: self.strings.len(),
+ })
+ }
+}
+
+impl<'a> KeyMode<'a, '_> {
+ /// Parse a key from input according to this mode.
+ fn parse_key(&self, input: &'a [u8]) -> Result<(&'a [u8], Cow<'a, str>)> {
+ match self {
+ KeyMode::NullTerminated => {
+ let (rest, s) = parse_null_terminated_string_borrowed(input)?;
+ Ok((rest, Cow::Borrowed(s)))
+ }
+ KeyMode::StringTableIndex { string_table } => {
+ let (rest, index) = ensure_read_u32_le(input)?;
+ let s = string_table.get(index as usize)?;
+ Ok((rest, Cow::Borrowed(s)))
+ }
+ }
+ }
+}
+
+/// Parse binary VDF data (autodetects format).
+///
+/// Attempts to parse as appinfo.vdf first, then falls back to shortcuts.vdf format.
+/// For shortcuts format, returns zero-copy data borrowed from input.
+/// For appinfo format, returns mixed data: root key and app ID keys are owned,
+/// but actual parsed values (including string table entries) are borrowed.
+/// For packageinfo format, returns mixed data similar to appinfo.
+pub fn parse(input: &[u8]) -> Result<Vdf<'_>> {
+ // Check if this looks like appinfo or packageinfo format (starts with magic)
+ if let Some(magic) = read_u32_le(input) {
+ if magic == APPINFO_MAGIC_40 || magic == APPINFO_MAGIC_41 {
+ return parse_appinfo(input);
+ }
+ if magic == PACKAGEINFO_MAGIC_39 || magic == PACKAGEINFO_MAGIC_40 {
+ return parse_packageinfo(input);
+ }
+ }
+
+ // Otherwise, parse as shortcuts format (zero-copy)
+ parse_shortcuts(input)
+}
+
+/// Parse shortcuts.vdf format binary data.
+///
+/// This is the simpler binary format used by Steam for shortcuts and other data.
+///
+/// This function returns zero-copy data - strings are borrowed from the input buffer.
+///
+/// Format:
+/// - Each entry starts with a type byte
+/// - Type 0x00: Object start (key is the object name)
+/// - Type 0x01: String value
+/// - Type 0x02: Int32 value
+/// - Type 0x08: Object end
+///
+/// All strings are null-terminated.
+pub fn parse_shortcuts(input: &[u8]) -> Result<Vdf<'_>> {
+ let config = ParseConfig::default();
+ let (_rest, obj) = parse_object(input, &config)?;
+
+ Ok(Vdf::new("root", Value::Obj(obj)))
+}
+
+/// Parse appinfo.vdf format binary data.
+///
+/// This function returns zero-copy data where possible - strings are borrowed from
+/// the input buffer (including string table entries in v41 format).
+///
+/// Format:
+/// - 4 bytes: Magic number (0x07564428 or 0x07564429)
+/// - 4 bytes: Universe
+/// - If magic == 0x07564429: 8 bytes: String table offset
+/// - Apps continue until EOF (or string table for v41)
+/// - For each app:
+/// - 4 bytes: App ID
+/// - 4 bytes: Size (remaining data size for this entry)
+/// - 4 bytes: InfoState
+/// - 4 bytes: LastUpdated (Unix timestamp)
+/// - 8 bytes: AccessToken
+/// - 20 bytes: SHA1 of text data
+/// - 4 bytes: ChangeNumber
+/// - 20 bytes: SHA1 of binary data
+/// - Then the VDF data for the app (starts with 0x00)
+/// - String table (if magic == 0x07564429, at string_table_offset)
+///
+/// App entry header is `APPINFO_ENTRY_HEADER_SIZE` (68) bytes.
+pub fn parse_appinfo(input: &[u8]) -> Result<Vdf<'_>> {
+ if input.len() < 16 {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading appinfo header",
+ offset: input.len(),
+ expected: 16,
+ actual: input.len(),
+ });
+ }
+
+ let Some(magic) = read_u32_le(input) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading magic number",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ });
+ };
+ let Some(universe) = read_u32_le(&input[4..]) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading universe",
+ offset: 4,
+ expected: 4,
+ actual: input.len() - 4,
+ });
+ };
+
+ let (string_table_offset, mut rest) = match magic {
+ APPINFO_MAGIC_40 => (None, &input[8..]),
+ APPINFO_MAGIC_41 => {
+ let Some(offset) = read_u64_le(&input[8..]) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading string table offset",
+ offset: 8,
+ expected: 8,
+ actual: input.len() - 8,
+ });
+ };
+ (Some(offset as usize), &input[16..])
+ }
+ _ => {
+ return Err(Error::InvalidMagic {
+ found: magic,
+ expected: &[APPINFO_MAGIC_40, APPINFO_MAGIC_41],
+ });
+ }
+ };
+
+ // Parse the string table if present
+ let string_table = if let Some(offset) = string_table_offset {
+ if offset >= input.len() {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading string table",
+ offset,
+ expected: 4,
+ actual: input.len() - offset,
+ });
+ }
+ Some(parse_string_table(&input[offset..]).map_err(with_offset(offset))?)
+ } else {
+ None
+ };
+
+ let mut obj = Obj::new();
+
+ // Calculate where apps end (at string table for v41, or EOF for v40)
+ let apps_end_offset = string_table_offset.unwrap_or(input.len());
+
+ // Use v41 format (string table) if string_table_offset is Some
+ let config = ParseConfig {
+ key_mode: if let Some(string_table) = &string_table {
+ KeyMode::StringTableIndex { string_table }
+ } else {
+ KeyMode::NullTerminated
+ },
+ };
+
+ loop {
+ // Check if we've reached the end of apps section
+ let current_offset = input.len() - rest.len();
+ if current_offset >= apps_end_offset {
+ break;
+ }
+
+ // Not enough data for an app entry header.
+ if rest.len() < APPINFO_ENTRY_HEADER_SIZE {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading app entry header",
+ offset: current_offset,
+ expected: APPINFO_ENTRY_HEADER_SIZE,
+ actual: rest.len(),
+ });
+ }
+
+ // App ID (offset 0)
+ let Some(app_id) = read_u32_le(rest) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading app id",
+ offset: current_offset,
+ expected: 4,
+ actual: rest.len(),
+ });
+ };
+ if app_id == 0 {
+ break;
+ }
+
+ // Size (offset 4) - includes everything AFTER this field (APPINFO_HEADER_AFTER_SIZE bytes + VDF data)
+ let Some(size) = read_u32_le(&rest[4..]) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading entry size",
+ offset: current_offset + 4,
+ expected: 4,
+ actual: rest.len() - 4,
+ });
+ };
+ let size = size as usize;
+
+ // VDF data starts after the header
+ let vdf_size = size - APPINFO_HEADER_AFTER_SIZE;
+ let vdf_end = APPINFO_VDF_DATA_OFFSET + vdf_size;
+
+ if vdf_end > rest.len() {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading VDF data",
+ offset: current_offset + vdf_end,
+ expected: vdf_end,
+ actual: rest.len(),
+ });
+ }
+
+ let vdf_data = &rest[APPINFO_VDF_DATA_OFFSET..vdf_end];
+ let vdf_offset = current_offset + APPINFO_VDF_DATA_OFFSET;
+
+ let (_vdf_rest, app_obj) =
+ parse_object(vdf_data, &config).map_err(with_offset(vdf_offset))?;
+
+ // Insert with app ID as key
+ obj.insert(Cow::Owned(app_id.to_string()), Value::Obj(app_obj));
+ rest = &rest[vdf_end..];
+ }
+
+ Ok(Vdf::new(
+ format!("appinfo_universe_{}", universe),
+ Value::Obj(obj),
+ ))
+}
+
+/// Parses an object from binary VDF data.
+///
+/// This function implements a state machine that:
+/// 1. Reads a type byte to determine the entry type
+/// 2. Parses a key (format depends on `config.key_mode`)
+/// 3. Parses the value based on the type byte
+/// 4. Inserts the key-value pair into the object
+/// 5. Returns on `ObjectEnd` (0x08) marker
+///
+/// # Parameters
+/// - `input`: Binary data to parse
+/// - `config`: Parse configuration including string table reference
+///
+/// # Returns
+/// A tuple of remaining input and the parsed object.
+fn parse_object<'a>(input: &'a [u8], config: &ParseConfig<'a, '_>) -> Result<(&'a [u8], Obj<'a>)> {
+ let mut obj = Obj::new();
+ let mut rest = input;
+
+ loop {
+ match rest {
+ [] => {
+ // At root level, EOF is acceptable - file may end without trailing 0x08
+ break Ok((rest, obj));
+ }
+ [type_byte, remainder @ ..] => {
+ let type_byte = *type_byte;
+ let typ = BinaryType::from_byte(type_byte);
+ let offset = input.len() - remainder.len();
+ rest = remainder;
+
+ match typ {
+ Some(BinaryType::ObjectEnd) => {
+ // Consume the end marker and return
+ return Ok((rest, obj));
+ }
+ Some(BinaryType::None) => {
+ // Map entry: 0x00 [key] { ... entries ... }
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let (new_rest, nested_obj) = parse_object(new_rest, config)?;
+ obj.insert(key, Value::Obj(nested_obj));
+ rest = new_rest;
+ }
+ Some(BinaryType::String) => {
+ // String entry: 0x01 [key] [value]
+ // VALUE is ALWAYS inline null-terminated string (never from string table!)
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let value_offset = input.len() - new_rest.len();
+ let (new_rest, value) = parse_null_terminated_string_borrowed(new_rest)
+ .map_err(with_offset(value_offset))?;
+ obj.insert(key, Value::Str(Cow::Borrowed(value)));
+ rest = new_rest;
+ }
+ Some(BinaryType::Int32) => {
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let value_offset = input.len() - new_rest.len();
+ let (new_rest, value) =
+ parse_value_int32(new_rest).map_err(with_offset(value_offset))?;
+ obj.insert(key, value);
+ rest = new_rest;
+ }
+ Some(BinaryType::UInt64) => {
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let value_offset = input.len() - new_rest.len();
+ let (new_rest, value) =
+ parse_value_uint64(new_rest).map_err(with_offset(value_offset))?;
+ obj.insert(key, value);
+ rest = new_rest;
+ }
+ Some(BinaryType::Float) => {
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let value_offset = input.len() - new_rest.len();
+ let (new_rest, value) =
+ parse_value_float(new_rest).map_err(with_offset(value_offset))?;
+ obj.insert(key, value);
+ rest = new_rest;
+ }
+ Some(BinaryType::Ptr) => {
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let value_offset = input.len() - new_rest.len();
+ let (new_rest, value) =
+ parse_value_ptr(new_rest).map_err(with_offset(value_offset))?;
+ obj.insert(key, value);
+ rest = new_rest;
+ }
+ Some(BinaryType::WString) => {
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let value_offset = input.len() - new_rest.len();
+ let (new_rest, value) =
+ parse_value_wstring(new_rest).map_err(with_offset(value_offset))?;
+ obj.insert(key, value);
+ rest = new_rest;
+ }
+ Some(BinaryType::Color) => {
+ let key_offset = input.len() - rest.len();
+ let (new_rest, key) = config
+ .key_mode
+ .parse_key(rest)
+ .map_err(with_offset(key_offset))?;
+ let value_offset = input.len() - new_rest.len();
+ let (new_rest, value) =
+ parse_value_color(new_rest).map_err(with_offset(value_offset))?;
+ obj.insert(key, value);
+ rest = new_rest;
+ }
+ None => {
+ // Unknown type byte
+ return Err(Error::UnknownType { type_byte, offset });
+ }
+ }
+ }
+ }
+ }
+}
+
+// ===== Value Parser Functions =====
+
+/// Parse an Int32 value (4 bytes, little-endian).
+fn parse_value_int32<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
+ let arr = <[u8; 4]>::try_from(input.get(..4).ok_or(Error::UnexpectedEndOfInput {
+ context: "reading int32",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ })?)
+ .map_err(|_| Error::UnexpectedEndOfInput {
+ context: "reading int32",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ })?;
+ let value = i32::from_le_bytes(arr);
+ Ok((&input[4..], Value::I32(value)))
+}
+
+/// Parse a UInt64 value (8 bytes, little-endian).
+fn parse_value_uint64<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
+ let arr = <[u8; 8]>::try_from(input.get(..8).ok_or(Error::UnexpectedEndOfInput {
+ context: "reading uint64",
+ offset: 0,
+ expected: 8,
+ actual: input.len(),
+ })?)
+ .map_err(|_| Error::UnexpectedEndOfInput {
+ context: "reading uint64",
+ offset: 0,
+ expected: 8,
+ actual: input.len(),
+ })?;
+ let value = u64::from_le_bytes(arr);
+ Ok((&input[8..], Value::U64(value)))
+}
+
+/// Parse a Float value (4 bytes, little-endian).
+fn parse_value_float<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
+ let arr = <[u8; 4]>::try_from(input.get(..4).ok_or(Error::UnexpectedEndOfInput {
+ context: "reading float",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ })?)
+ .map_err(|_| Error::UnexpectedEndOfInput {
+ context: "reading float",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ })?;
+ let value = f32::from_le_bytes(arr);
+ Ok((&input[4..], Value::Float(value)))
+}
+
+/// Parse a Pointer value (4 bytes, little-endian).
+fn parse_value_ptr<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
+ let (rest, value) = ensure_read_u32_le(input)?;
+ Ok((rest, Value::Pointer(value)))
+}
+
+/// Parse a WideString value (UTF-16LE, null-terminated).
+fn parse_value_wstring<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
+ let (rest, string) = parse_null_terminated_wstring(input)?;
+ Ok((rest, Value::Str(Cow::Owned(string))))
+}
+
+/// Parse a Color value (4 bytes RGBA).
+fn parse_value_color<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
+ let arr = <[u8; 4]>::try_from(input.get(..4).ok_or(Error::UnexpectedEndOfInput {
+ context: "reading color",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ })?)
+ .map_err(|_| Error::UnexpectedEndOfInput {
+ context: "reading color",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ })?;
+ Ok((&input[4..], Value::Color(arr)))
+}
+
+// ===== String Parsing Functions =====
+
+/// Parse a null-terminated string (UTF-8), returning a borrowed slice.
+///
+/// This is the zero-copy version that borrows from the input when possible.
+fn parse_null_terminated_string_borrowed(input: &[u8]) -> Result<(&[u8], &str)> {
+ let null_pos = input
+ .iter()
+ .position(|&b| b == 0)
+ .ok_or(Error::UnexpectedEndOfInput {
+ context: "reading null-terminated string",
+ offset: 0,
+ expected: 1,
+ actual: input.len(),
+ })?;
+
+ let bytes = &input[..null_pos];
+ // Some apps have binary blobs where strings are expected (SHA1 hashes
+ // stored as "strings" in common blocks). Treat invalid-UTF-8 as empty
+ // so the parser keeps going rather than failing the whole file.
+ let string = core::str::from_utf8(bytes).unwrap_or("");
+
+ Ok((&input[null_pos + 1..], string))
+}
+
+/// Parse a null-terminated wide string (UTF-16LE).
+///
+/// WideString is terminated by two zero bytes (0x00 0x00).
+/// Note: This allocates due to UTF-16 to UTF-8 conversion.
+fn parse_null_terminated_wstring(input: &[u8]) -> Result<(&[u8], String)> {
+ // Find the double-null terminator
+ let mut i = 0;
+ while i + 1 < input.len() {
+ if input[i] == 0 && input[i + 1] == 0 {
+ break;
+ }
+ i += 2;
+ }
+
+ if i + 1 >= input.len() {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading null-terminated wide string",
+ offset: i,
+ expected: 2,
+ actual: input.len().saturating_sub(i),
+ });
+ }
+
+ // Convert UTF-16LE to u16 code units
+ let utf16_units = input[..i]
+ .chunks_exact(2)
+ .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]));
+
+ // Decode UTF-16 to char and then to String
+ let string: String = char::decode_utf16(utf16_units)
+ .enumerate()
+ .map(|(pos, r)| {
+ r.map_err(|_| Error::InvalidUtf16 {
+ offset: pos * 2,
+ position: pos,
+ })
+ })
+ .collect::<core::result::Result<_, _>>()?;
+
+ Ok((&input[i + 2..], string))
+}
+
+/// Parse the string table section (v41 format).
+///
+/// Returns a `StringTable` containing pre-extracted strings for O(1) lookups.
+///
+/// Format:
+/// - 4 bytes: string_count (little-endian u32)
+/// - Then string_count null-terminated UTF-8 strings
+fn parse_string_table(input: &[u8]) -> Result<StringTable<'_>> {
+ let (mut rest, string_count) = ensure_read_u32_le(input)?;
+ let string_count = string_count as usize;
+
+ let mut strings = Vec::with_capacity(string_count);
+
+ // Extract each null-terminated string
+ for _ in 0..string_count {
+ if rest.is_empty() {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading string table entry",
+ offset: input.len() - rest.len(),
+ expected: 1,
+ actual: 0,
+ });
+ }
+ let (new_rest, string) = parse_null_terminated_string_borrowed(rest)?;
+ strings.push(string);
+ rest = new_rest;
+ }
+
+ Ok(StringTable { strings })
+}
+
+/// Header size for packageinfo entries (package_id + hash + change_number + token).
+const PACKAGEINFO_ENTRY_HEADER_SIZE_V39: usize = 4 + 20 + 4; // package_id + hash + change_number
+const PACKAGEINFO_ENTRY_HEADER_SIZE_V40: usize = 4 + 20 + 4 + 8; // + token
+
+/// Parse packageinfo.vdf format binary data.
+///
+/// This function returns zero-copy data where possible - strings are borrowed from
+/// the input buffer.
+///
+/// Format:
+/// - 4 bytes: Magic number + version (0x06565527 for v39, 0x06565528 for v40)
+/// - Upper 3 bytes: 0x065655 (magic)
+/// - Lower 1 byte: version (27 = 39, 28 = 40)
+/// - 4 bytes: Universe
+/// - Repeated package entries until package_id == 0xFFFFFFFF:
+/// - 4 bytes: Package ID (uint32)
+/// - 20 bytes: SHA-1 hash
+/// - 4 bytes: Change number (uint32)
+/// - 8 bytes: PICS token (uint64, only in v40+)
+/// - Binary VDF blob (KeyValues1 binary) with package metadata
+pub fn parse_packageinfo(input: &[u8]) -> Result<Vdf<'_>> {
+ if input.len() < 8 {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading packageinfo header",
+ offset: input.len(),
+ expected: 8,
+ actual: input.len(),
+ });
+ }
+
+ let Some(magic) = read_u32_le(input) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading magic number",
+ offset: 0,
+ expected: 4,
+ actual: input.len(),
+ });
+ };
+
+ // Extract version from lower byte and magic from upper 3 bytes
+ let version = magic & 0xFF;
+ let magic_base = magic >> 8;
+
+ if magic_base != PACKAGEINFO_MAGIC_BASE {
+ return Err(Error::InvalidMagic {
+ found: magic,
+ expected: &[PACKAGEINFO_MAGIC_39, PACKAGEINFO_MAGIC_40],
+ });
+ }
+
+ if version != 39 && version != 40 {
+ return Err(Error::InvalidMagic {
+ found: magic,
+ expected: &[PACKAGEINFO_MAGIC_39, PACKAGEINFO_MAGIC_40],
+ });
+ }
+
+ let Some(universe) = read_u32_le(&input[4..]) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading universe",
+ offset: 4,
+ expected: 4,
+ actual: input.len() - 4,
+ });
+ };
+
+ let has_token = version >= 40;
+ let header_size = if has_token {
+ PACKAGEINFO_ENTRY_HEADER_SIZE_V40
+ } else {
+ PACKAGEINFO_ENTRY_HEADER_SIZE_V39
+ };
+
+ let mut rest = &input[8..];
+ let mut obj = Obj::new();
+
+ loop {
+ // Check if we have at least 4 bytes for the package ID
+ if rest.len() < 4 {
+ // At EOF or termination marker, exit gracefully
+ break;
+ }
+
+ // Read package ID
+ let Some(package_id) = read_u32_le(rest) else {
+ break;
+ };
+
+ // Check for termination marker
+ if package_id == 0xFFFFFFFF {
+ break;
+ }
+
+ // Now ensure we have enough data for the full header
+ if rest.len() < header_size {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading package entry header",
+ offset: input.len() - rest.len(),
+ expected: header_size,
+ actual: rest.len(),
+ });
+ }
+
+ // Skip hash (20 bytes), read change number
+ let hash_offset = 4;
+ let change_number_offset = hash_offset + 20;
+
+ let Some(change_number) = read_u32_le(&rest[change_number_offset..]) else {
+ return Err(Error::UnexpectedEndOfInput {
+ context: "reading change number",
+ offset: input.len() - rest.len() + change_number_offset,
+ expected: 4,
+ actual: rest.len() - change_number_offset,
+ });
+ };
+
+ // Skip token if present (8 bytes after change_number)
+ let vdf_data_offset = if has_token {
+ change_number_offset + 4 + 8
+ } else {
+ change_number_offset + 4
+ };
+
+ // Parse the VDF data for this package
+ let vdf_data = &rest[vdf_data_offset..];
+
+ let config = ParseConfig::default(); // Uses null-terminated keys like shortcuts
+
+ let (_vdf_rest, package_obj) =
+ parse_object(vdf_data, &config).map_err(with_offset(input.len() - vdf_data.len()))?;
+
+ // Create metadata object for this package
+ let mut package_with_meta = Obj::new();
+
+ // Add metadata fields
+ package_with_meta.insert(Cow::Borrowed("packageid"), Value::I32(package_id as i32));
+ package_with_meta.insert(
+ Cow::Borrowed("change_number"),
+ Value::U64(change_number as u64),
+ );
+ package_with_meta.insert(
+ Cow::Borrowed("sha1"),
+ Value::Str(Cow::Owned(hex::encode(
+ &rest[hash_offset..hash_offset + 20],
+ ))),
+ );
+
+ // Merge the parsed VDF data
+ for (key, value) in package_obj.iter() {
+ package_with_meta.insert(key.clone(), value.clone());
+ }
+
+ // Insert with package ID as key
+ obj.insert(
+ Cow::Owned(package_id.to_string()),
+ Value::Obj(package_with_meta),
+ );
+
+ // Find the end of this VDF object to move to the next entry
+ // _vdf_rest from the first parse_object call above tells us where VDF data ended
+ let vdf_end = vdf_data.len() - _vdf_rest.len();
+ rest = &rest[vdf_data_offset + vdf_end..];
+ }
+
+ Ok(Vdf::new(
+ format!("packageinfo_universe_{}", universe),
+ Value::Obj(obj),
+ ))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_simple_object() {
+ // Simple binary VDF: "test" { "key" "value" }
+ let data: &[u8] = &[
+ 0x00, // Object start
+ b't', b'e', b's', b't', 0x00, // Key "test"
+ 0x01, // String type
+ b'k', b'e', b'y', 0x00, // Key "key"
+ b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
+ 0x08, // Object end
+ ];
+
+ let result = parse_shortcuts(data);
+ assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
+
+ let vdf = result.unwrap();
+ assert_eq!(vdf.key(), "root");
+
+ let obj = vdf.as_obj().unwrap();
+ let test_obj = obj.get("test").and_then(|v| v.as_obj());
+ assert!(test_obj.is_some());
+
+ let test_obj = test_obj.unwrap();
+ let value = test_obj.get("key").and_then(|v| v.as_str());
+ assert_eq!(value, Some("value"));
+ }
+
+ #[test]
+ fn test_parse_nested_objects() {
+ // Nested objects: "outer" { "inner" { "key" "value" } }
+ let data: &[u8] = &[
+ 0x00, // Object start
+ b'o', b'u', b't', b'e', b'r', 0x00, // Key "outer"
+ 0x00, // Nested object start
+ b'i', b'n', b'n', b'e', b'r', 0x00, // Key "inner"
+ 0x01, // String type
+ b'k', b'e', b'y', 0x00, // Key "key"
+ b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
+ 0x08, // End inner object
+ 0x08, // End outer object
+ ];
+
+ let result = parse_shortcuts(data);
+ assert!(result.is_ok());
+
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let outer = obj.get("outer").and_then(|v| v.as_obj()).unwrap();
+ let inner = outer.get("inner").and_then(|v| v.as_obj()).unwrap();
+ let value = inner.get("key").and_then(|v| v.as_str());
+ assert_eq!(value, Some("value"));
+ }
+
+ #[test]
+ fn test_parse_int32_value() {
+ // Int32 value: "root" { "number" "42" }
+ let data: &[u8] = &[
+ 0x00, // Object start
+ b'r', b'o', b'o', b't', 0x00, // Key "root"
+ 0x02, // Int32 type
+ b'n', b'u', b'm', b'b', b'e', b'r', 0x00, // Key "number"
+ 42, 0, 0, 0, // Value 42 (little-endian)
+ 0x08, // Object end
+ ];
+
+ let result = parse_shortcuts(data);
+ assert!(result.is_ok());
+
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
+ let value = root.get("number").and_then(|v| v.as_i32());
+ assert_eq!(value, Some(42));
+ }
+
+ #[test]
+ fn test_parse_uint64_value() {
+ // UInt64 value
+ let data: &[u8] = &[
+ 0x00, // Object start
+ b'r', b'o', b'o', b't', 0x00, // Key "root"
+ 0x07, // UInt64 type
+ b'n', b'u', b'm', b'b', b'e', b'r', 0x00, // Key "number"
+ 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, // Value u32::MAX as u64
+ 0x08, // Object end
+ ];
+
+ let result = parse_shortcuts(data);
+ assert!(result.is_ok());
+
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
+ let value = root.get("number").and_then(|v| v.as_u64());
+ assert_eq!(value, Some(4294967295));
+ }
+
+ #[test]
+ fn test_parse_float_value() {
+ // Float value
+ let data: &[u8] = &[
+ 0x00, // Object start
+ b'r', b'o', b'o', b't', 0x00, // Key "root"
+ 0x03, // Float type
+ b'v', b'a', b'l', 0x00, // Key "val"
+ 0x00, 0x00, 0x80, 0x3F, // Value 1.0 (little-endian)
+ 0x08, // Object end
+ ];
+
+ let result = parse_shortcuts(data);
+ assert!(result.is_ok());
+
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
+ let value = root.get("val").and_then(|v| v.as_float());
+ assert_eq!(value, Some(1.0));
+ }
+
+ #[test]
+ fn test_parse_ptr_value() {
+ // Pointer value
+ let data: &[u8] = &[
+ 0x00, // Object start
+ b'r', b'o', b'o', b't', 0x00, // Key "root"
+ 0x04, // Ptr type
+ b'p', b't', b'r', 0x00, // Key "ptr"
+ 0xAB, 0xCD, 0xEF, 0x12, // Value 0x12EFCDAB
+ 0x08, // Object end
+ ];
+
+ let result = parse_shortcuts(data);
+ assert!(result.is_ok());
+
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
+ let value = root.get("ptr").and_then(|v| v.as_pointer());
+ assert_eq!(value, Some(0x12efcdab));
+ }
+
+ #[test]
+ fn test_parse_color_value() {
+ // Color value: RGBA (255, 0, 0, 255) = "25500255"
+ let data: &[u8] = &[
+ 0x00, // Object start
+ b'r', b'o', b'o', b't', 0x00, // Key "root"
+ 0x06, // Color type
+ b'c', b'o', b'l', 0x00, // Key "col"
+ 0xFF, 0x00, 0x00, 0xFF, // RGBA: red, opaque
+ 0x08, // Object end
+ ];
+
+ let result = parse_shortcuts(data);
+ assert!(result.is_ok());
+
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
+ let value = root.get("col").and_then(|v| v.as_color());
+ assert_eq!(value, Some([255, 0, 0, 255]));
+ }
+
+ // ===== Error Path Tests =====
+
+ #[test]
+ fn test_parse_unknown_type_byte() {
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0xFF, // Invalid type byte
+ b'k', b'e', b'y', 0x00,
+ ];
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::UnknownType {
+ type_byte: 0xFF,
+ ..
+ })
+ ));
+ }
+
+ #[test]
+ fn test_parse_truncated_object_start() {
+ let data: &[u8] = &[0x00]; // Incomplete object start
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_truncated_string_value() {
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0x01, // String type
+ b'k', b'e', b'y', 0x00,
+ // Missing null terminator
+ ];
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_invalid_utf8_string() {
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0x01, // String type
+ b'k', b'e', b'y', 0x00, 0xFF, 0xFF, 0x00, // Invalid UTF-8 followed by null
+ ];
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::InvalidUtf8 { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_truncated_int32_value() {
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0x02, // Int32 type
+ b'k', b'e', b'y', 0x00, 0x01, 0x02, // Only 2 bytes instead of 4
+ ];
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_truncated_uint64_value() {
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0x07, // UInt64 type
+ b'k', b'e', b'y', 0x00, 0x01, 0x02, 0x03, 0x04, // Only 4 bytes instead of 8
+ ];
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_truncated_float_value() {
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0x03, // Float type
+ b'k', b'e', b'y', 0x00, 0x01, 0x02, // Only 2 bytes instead of 4
+ ];
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_truncated_color_value() {
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0x06, // Color type
+ b'k', b'e', b'y', 0x00, 0xFF, 0x00, // Only 2 bytes instead of 4
+ ];
+ assert!(matches!(
+ parse_shortcuts(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_wstring_unpaired_surrogate() {
+ // WideString with unpaired surrogate - Rust's decode_utf16 replaces with
+ // replacement character rather than erroring, so this should parse successfully
+ let data: &[u8] = &[
+ 0x00, b't', b'e', b's', b't', 0x00, 0x05, // WideString type
+ b'k', b'e', b'y', 0x00, 0xD8, 0x00, 0x00,
+ 0x00, // Unpaired surrogate (UTF-16) - gets replaced
+ ];
+ assert!(parse_shortcuts(data).is_ok());
+ }
+
+ #[test]
+ fn test_parse_appinfo_invalid_magic() {
+ let data: &[u8] = &[
+ 0xDE, 0xAD, 0xBE, 0xEF, // Invalid magic
+ 0x00, 0x00, 0x00, 0x00, // universe
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding to meet minimum size
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ ];
+ assert!(matches!(
+ parse_appinfo(data),
+ Err(Error::InvalidMagic { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_appinfo_truncated_header() {
+ let data: &[u8] = &[
+ 0x28, 0x44, 0x56, 0x07, // APPINFO_MAGIC_41 first byte
+ ];
+ assert!(matches!(
+ parse_appinfo(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_appinfo_v41_invalid_string_table_offset() {
+ // v41 with string table offset beyond file length
+ let data: &[u8] = &[
+ 0x28, 0x44, 0x56, 0x07, // APPINFO_MAGIC_41
+ 0x00, 0x00, 0x00, 0x00, // universe
+ 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, // string table offset (255, beyond EOF)
+ ];
+ assert!(matches!(
+ parse_appinfo(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_invalid_magic() {
+ let data: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::InvalidMagic { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_truncated_header() {
+ let data: &[u8] = &[0x27]; // Partial magic
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_appinfo_with_terminator() {
+ // v40 format with immediate app_id terminator (no apps)
+ // Need 68 bytes minimum (APPINFO_ENTRY_HEADER_SIZE) to pass the size check
+ let data: &[u8] = &[
+ 0x28, 0x44, 0x56, 0x07, // APPINFO_MAGIC_40
+ 0x00, 0x00, 0x00, 0x00, // universe
+ 0x00, 0x00, 0x00, 0x00, // app_id = 0 (terminator)
+ // Padding to meet APPINFO_ENTRY_HEADER_SIZE (68 bytes total)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ ];
+ let result = parse_appinfo(data);
+ if let Err(e) = &result {
+ panic!("parse_appinfo failed with: {:?}", e);
+ }
+ assert!(
+ result.is_ok(),
+ "Appinfo with terminator should parse successfully"
+ );
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ assert_eq!(obj.len(), 0, "Should have no apps");
+ }
+
+ #[test]
+ fn test_parse_autodetect_fallback_to_shortcuts() {
+ // Data that doesn't look like appinfo should be parsed as shortcuts
+ let data: &[u8] = &[
+ 0x00, // Object start (not appinfo magic)
+ b't', b'e', b's', b't', 0x00, 0x01, // String type
+ b'k', b'e', b'y', 0x00, b'v', b'a', b'l', b'u', b'e', 0x00, 0x08, // Object end
+ ];
+ let result = parse(data);
+ assert!(result.is_ok());
+ }
+
+ // ===== packageinfo tests =====
+
+ #[test]
+ fn test_parse_packageinfo_v39_invalid_magic_base() {
+ // Correct version (39 = 0x27) but wrong magic base
+ let data: &[u8] = &[
+ 0x27, 0xBE, 0xBA, 0xFE, // Wrong magic base (0xFEBAFE instead of 0x065655)
+ 0x00, 0x00, 0x00, 0x00, // universe
+ ];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::InvalidMagic { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_invalid_version() {
+ // Correct magic base but wrong version (38 instead of 39/40)
+ // 0x06565526 = version 38
+ let data: &[u8] = &[
+ 0x26, 0x55, 0x56, 0x06, // magic with version 38
+ 0x00, 0x00, 0x00, 0x00, // universe
+ ];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::InvalidMagic { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_v39_truncated_universe() {
+ // v39 magic but missing universe bytes
+ let data: &[u8] = &[
+ 0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
+ 0x00, 0x00, // incomplete universe (only 2 bytes)
+ ];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_v39_with_terminator() {
+ // v39 format with immediate termination marker (no packages)
+ let data: &[u8] = &[
+ 0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39 (v39)
+ 0x01, 0x00, 0x00, 0x00, // universe = 1
+ 0xFF, 0xFF, 0xFF, 0xFF, // package_id = 0xFFFFFFFF (terminator)
+ ];
+ let result = parse_packageinfo(data);
+ assert!(
+ result.is_ok(),
+ "parse_packageinfo failed: {:?}",
+ result.err()
+ );
+ let vdf = result.unwrap();
+ assert_eq!(vdf.key(), "packageinfo_universe_1");
+ let obj = vdf.as_obj().unwrap();
+ assert_eq!(obj.len(), 0, "Should have no packages");
+ }
+
+ #[test]
+ fn test_parse_packageinfo_v40_with_terminator() {
+ // v40 format with immediate termination marker (no packages)
+ let data: &[u8] = &[
+ 0x28, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_40 (v40)
+ 0x01, 0x00, 0x00, 0x00, // universe = 1
+ 0xFF, 0xFF, 0xFF, 0xFF, // package_id = 0xFFFFFFFF (terminator)
+ ];
+ let result = parse_packageinfo(data);
+ assert!(
+ result.is_ok(),
+ "parse_packageinfo failed: {:?}",
+ result.err()
+ );
+ let vdf = result.unwrap();
+ assert_eq!(vdf.key(), "packageinfo_universe_1");
+ let obj = vdf.as_obj().unwrap();
+ assert_eq!(obj.len(), 0, "Should have no packages");
+ }
+
+ #[test]
+ fn test_parse_packageinfo_v39_truncated_entry_header() {
+ // v39 format with package_id but incomplete header
+ // Header size for v39 is 4 + 20 + 4 = 28 bytes (package_id + hash + change_number)
+ let data: &[u8] = &[
+ 0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
+ 0x00, 0x00, 0x00, 0x00, // universe
+ 0x01, 0x00, 0x00, 0x00, // package_id = 1
+ // Only 10 bytes of hash (need 20), missing change_number
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
+ ];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::UnexpectedEndOfInput { context, .. }) if context == "reading package entry header"
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_v40_truncated_entry_header() {
+ // v40 format with package_id but incomplete header
+ // Header size for v40 is 4 + 20 + 4 + 8 = 36 bytes (+ token)
+ let data: &[u8] = &[
+ 0x28, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_40
+ 0x00, 0x00, 0x00, 0x00, // universe
+ 0x01, 0x00, 0x00, 0x00, // package_id = 1
+ // Only hash (20 bytes), missing change_number and token
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x01, 0x02, 0x03, 0x04,
+ 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
+ ];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::UnexpectedEndOfInput { context, .. }) if context == "reading package entry header"
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_v39_with_minimal_vdf() {
+ // v39 format with minimal VDF that tests basic parsing
+ let data: &[u8] = &[
+ 0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
+ 0x00, 0x00, 0x00, 0x00, // universe = 0
+ // Package entry
+ 0x01, 0x00, 0x00, 0x00, // package_id = 1
+ // SHA-1 hash (20 bytes)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, // change_number = 42
+ // VDF: simple object with one string entry { "k": "value" }
+ 0x01, // String type
+ b'k', 0x00, // Key "k"
+ b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
+ 0x08, // Object end
+ // Termination marker
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ ];
+ let result = parse_packageinfo(data);
+ assert!(
+ result.is_ok(),
+ "parse_packageinfo failed: {:?}",
+ result.err()
+ );
+ let vdf = result.unwrap();
+ assert_eq!(vdf.key(), "packageinfo_universe_0");
+
+ let obj = vdf.as_obj().unwrap();
+ assert_eq!(obj.len(), 1);
+ let package = obj.get("1").and_then(|v| v.as_obj()).unwrap();
+ assert_eq!(package.get("k").and_then(|v| v.as_str()), Some("value"));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_v40_with_minimal_vdf() {
+ // v40 format with minimal VDF
+ let data: &[u8] = &[
+ 0x28, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_40
+ 0x00, 0x00, 0x00, 0x00, // universe = 0
+ // Package entry
+ 0x01, 0x00, 0x00, 0x00, // package_id = 1
+ // SHA-1 hash (20 bytes)
+ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
+ 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0x2A, 0x00, 0x00, 0x00, // change_number = 42
+ // PICS token (8 bytes)
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ // VDF: simple object with one int32 entry { "x": 5 }
+ 0x02, // Int32 type
+ b'x', 0x00, // Key "x"
+ 0x05, 0x00, 0x00, 0x00, // Value 5
+ 0x08, // Object end
+ // Termination marker
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ ];
+ let result = parse_packageinfo(data);
+ assert!(
+ result.is_ok(),
+ "parse_packageinfo failed: {:?}",
+ result.err()
+ );
+ let vdf = result.unwrap();
+ assert_eq!(vdf.key(), "packageinfo_universe_0");
+
+ let obj = vdf.as_obj().unwrap();
+ assert_eq!(obj.len(), 1);
+ let package = obj.get("1").and_then(|v| v.as_obj()).unwrap();
+ assert_eq!(package.get("x").and_then(|v| v.as_i32()), Some(5));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_multiple_packages() {
+ // v39 format with multiple packages
+ let data: &[u8] = &[
+ 0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
+ 0x00, 0x00, 0x00, 0x00, // universe = 0
+ // First package
+ 0x01, 0x00, 0x00, 0x00, // package_id = 1
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash
+ 0x01, 0x00, 0x00, 0x00, // change_number = 1
+ // VDF: { "x": 1 }
+ 0x02, 0x01, 0x00, 0x00, 0x00, b'x', 0x00, 0x08, // Second package
+ 0x02, 0x00, 0x00, 0x00, // package_id = 2
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash
+ 0x02, 0x00, 0x00, 0x00, // change_number = 2
+ // VDF: { "a": 2 }
+ 0x02, 0x02, 0x00, 0x00, 0x00, b'a', 0x00, 0x08, // Termination marker
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ ];
+ let result = parse_packageinfo(data);
+ assert!(
+ result.is_ok(),
+ "parse_packageinfo failed: {:?}",
+ result.err()
+ );
+ let vdf = result.unwrap();
+ let obj = vdf.as_obj().unwrap();
+ assert_eq!(obj.len(), 2);
+ assert!(obj.get("1").is_some());
+ assert!(obj.get("2").is_some());
+ }
+
+ #[test]
+ fn test_parse_packageinfo_empty_input() {
+ // Completely empty input
+ let data: &[u8] = &[];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::UnexpectedEndOfInput { context, .. }) if context == "reading packageinfo header"
+ ));
+ }
+
+ #[test]
+ fn test_parse_packageinfo_only_magic() {
+ // Only magic bytes, no universe
+ let data: &[u8] = &[
+ 0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
+ ];
+ assert!(matches!(
+ parse_packageinfo(data),
+ Err(Error::UnexpectedEndOfInput { .. })
+ ));
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/types.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/types.rs
new file mode 100644
index 0000000..4f39a8e
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/binary/types.rs
@@ -0,0 +1,70 @@
+//! Type definitions for binary VDF format.
+
+/// Binary type byte values used in Steam's binary VDF format.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum BinaryType {
+ /// Nested object start marker.
+ ///
+ /// Named `None` to match Steam SDK's `TYPE_NONE`, which indicates a subsection
+ /// with child keys rather than a leaf value. In binary VDF, `0x00` followed by
+ /// a key name starts a new nested object that continues until `ObjectEnd` (0x08).
+ None = 0x00,
+ /// String value (null-terminated).
+ String = 0x01,
+ /// 32-bit integer value.
+ Int32 = 0x02,
+ /// 32-bit float value.
+ Float = 0x03,
+ /// Pointer value.
+ Ptr = 0x04,
+ /// Wide string value (UTF-16).
+ WString = 0x05,
+ /// Color value (RGBA).
+ Color = 0x06,
+ /// 64-bit unsigned integer value.
+ UInt64 = 0x07,
+ /// End of object marker.
+ ObjectEnd = 0x08,
+}
+
+impl BinaryType {
+ /// Attempts to convert a byte to a `BinaryType`.
+ ///
+ /// Returns `None` if the byte doesn't correspond to a known type.
+ #[inline]
+ pub fn from_byte(b: u8) -> Option<Self> {
+ match b {
+ 0x00 => Some(BinaryType::None),
+ 0x01 => Some(BinaryType::String),
+ 0x02 => Some(BinaryType::Int32),
+ 0x03 => Some(BinaryType::Float),
+ 0x04 => Some(BinaryType::Ptr),
+ 0x05 => Some(BinaryType::WString),
+ 0x06 => Some(BinaryType::Color),
+ 0x07 => Some(BinaryType::UInt64),
+ 0x08 => Some(BinaryType::ObjectEnd),
+ _ => None,
+ }
+ }
+}
+
+/// Magic number for appinfo.vdf format version 40.
+///
+/// This format uses null-terminated UTF-8 keys.
+pub const APPINFO_MAGIC_40: u32 = 0x07564428;
+
+/// Magic number for appinfo.vdf format version 41 (with string table).
+///
+/// This format uses u32 indices into a string table for keys, enabling O(1) lookups.
+pub const APPINFO_MAGIC_41: u32 = 0x07564429;
+
+/// Magic base for packageinfo.vdf format (upper 3 bytes).
+pub const PACKAGEINFO_MAGIC_BASE: u32 = 0x065655;
+
+/// Magic number for packageinfo.vdf format version 39.
+pub const PACKAGEINFO_MAGIC_39: u32 = 0x06565527;
+
+/// Magic number for packageinfo.vdf format version 40 (with PICS token).
+pub const PACKAGEINFO_MAGIC_40: u32 = 0x06565528;
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/error.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/error.rs
new file mode 100644
index 0000000..f2a8713
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/error.rs
@@ -0,0 +1,410 @@
+//! Error types for VDF parsing.
+
+use alloc::format;
+use alloc::string::String;
+use alloc::vec::Vec;
+use core::fmt;
+
+/// Result type for VDF operations.
+pub type Result<T> = core::result::Result<T, Error>;
+
+/// Create a parse error with truncated input snippet (max 50 chars).
+///
+/// The snippet is limited to 50 characters to keep error messages manageable.
+///
+/// # Parameters
+/// - `input`: The input string being parsed
+/// - `offset`: Byte offset where the error occurred
+/// - `context`: Description of what was expected
+pub fn parse_error(input: &str, offset: usize, context: impl Into<String>) -> Error {
+ let snippet = input.chars().take(50).collect::<String>();
+ Error::ParseError {
+ input: snippet,
+ offset,
+ context: context.into(),
+ }
+}
+
+/// Errors that can occur during VDF parsing.
+#[derive(Debug)]
+pub enum Error {
+ /// Binary format errors
+ /// ---------------------
+
+ /// Invalid magic number in binary VDF header.
+ InvalidMagic {
+ /// The magic number that was found.
+ found: u32,
+ /// Expected magic numbers for this format.
+ expected: &'static [u32],
+ },
+
+ /// Unknown type byte encountered.
+ UnknownType {
+ /// The type byte that was found.
+ type_byte: u8,
+ /// Offset in the input where this occurred.
+ offset: usize,
+ },
+
+ /// Invalid string index into the string table.
+ InvalidStringIndex {
+ /// The index that was requested.
+ index: usize,
+ /// The maximum valid index.
+ max: usize,
+ },
+
+ /// Unexpected end of input while parsing.
+ UnexpectedEndOfInput {
+ /// Description of what was being read.
+ context: &'static str,
+ /// Offset in the input where this occurred.
+ offset: usize,
+ /// Expected minimum number of bytes.
+ expected: usize,
+ /// Actual number of bytes available.
+ actual: usize,
+ },
+
+ /// Invalid UTF-8 sequence in binary data.
+ InvalidUtf8 {
+ /// Offset where the error occurred.
+ offset: usize,
+ },
+
+ /// Invalid UTF-16 sequence in binary data.
+ InvalidUtf16 {
+ /// Offset where the error occurred.
+ offset: usize,
+ /// Position of the unpaired surrogate.
+ position: usize,
+ },
+
+ /// Text format errors
+ /// ------------------
+
+ /// Parse error with context.
+ ParseError {
+ /// A snippet of the input near the error.
+ input: String,
+ /// Offset in the input where this occurred.
+ offset: usize,
+ /// Context describing what was expected.
+ context: String,
+ },
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Error::InvalidMagic { found, expected } => {
+ let expected_str: String = expected
+ .iter()
+ .map(|v| format!("0x{:08x}", v))
+ .collect::<Vec<_>>()
+ .join(" or ");
+ write!(
+ f,
+ "Invalid magic number: expected {}, found 0x{:08x}",
+ expected_str, found
+ )
+ }
+ Error::UnknownType { type_byte, offset } => {
+ write!(
+ f,
+ "Unknown type byte 0x{:02x} at offset {}",
+ type_byte, offset
+ )
+ }
+ Error::InvalidStringIndex { index, max } => {
+ if *max == 0 {
+ write!(f, "Invalid string index {}: string table is empty", index)
+ } else {
+ write!(
+ f,
+ "Invalid string index {}: string table has {} entries (valid range: 0..{})",
+ index, max, max
+ )
+ }
+ }
+ Error::UnexpectedEndOfInput {
+ context,
+ offset,
+ expected,
+ actual,
+ } => {
+ write!(
+ f,
+ "Unexpected end of input at offset {} while {}: expected {} bytes, found {}",
+ offset, context, expected, actual
+ )
+ }
+ Error::InvalidUtf8 { offset } => {
+ write!(f, "Invalid UTF-8 sequence at offset {}", offset)
+ }
+ Error::InvalidUtf16 { offset, position } => {
+ write!(
+ f,
+ "Invalid UTF-16 sequence at offset {} (surrogate position {})",
+ offset, position
+ )
+ }
+ Error::ParseError {
+ input,
+ offset,
+ context,
+ } => {
+ let snippet = if input.len() > 50 {
+ format!("{}...", &input[..50])
+ } else {
+ input.clone()
+ };
+ write!(
+ f,
+ "Parse error at offset {}: {} (near: \"{}\")",
+ offset, context, snippet
+ )
+ }
+ }
+ }
+}
+
+impl core::error::Error for Error {}
+
+impl Error {
+ /// Adjusts the offset in error variants that contain position information.
+ ///
+ /// This is used to add a base offset when parsing from a sub-slice,
+ /// converting relative offsets to absolute offsets in the original input.
+ fn with_offset(mut self, base: usize) -> Self {
+ match &mut self {
+ Error::UnexpectedEndOfInput { offset, .. } => *offset += base,
+ Error::InvalidUtf8 { offset } => *offset += base,
+ Error::InvalidUtf16 { offset, .. } => *offset += base,
+ Error::UnknownType { offset, .. } => *offset += base,
+ Error::ParseError { offset, .. } => *offset += base,
+ // Other variants don't have offsets to adjust
+ Error::InvalidMagic { .. } | Error::InvalidStringIndex { .. } => {}
+ }
+ self
+ }
+}
+
+/// Returns a closure that adds an offset to an error.
+///
+/// This is used with `.map_err()` to adjust error offsets when parsing from sub-slices.
+pub(crate) fn with_offset(base: usize) -> impl Fn(Error) -> Error {
+ move |err| err.with_offset(base)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::binary::{APPINFO_MAGIC_40, APPINFO_MAGIC_41};
+ use alloc::format;
+ use alloc::string::ToString;
+
+ #[test]
+ fn test_error_display_invalid_magic() {
+ let err = Error::InvalidMagic {
+ found: 0xDEADBEEF,
+ expected: &[APPINFO_MAGIC_40, APPINFO_MAGIC_41],
+ };
+ let msg = format!("{}", err);
+ assert!(msg.contains("0xdeadbeef"));
+ assert!(msg.contains("0x07564428"));
+ }
+
+ #[test]
+ fn test_error_display_unknown_type() {
+ let err = Error::UnknownType {
+ type_byte: 0xFF,
+ offset: 42,
+ };
+ let msg = format!("{}", err);
+ assert!(msg.contains("0xff"));
+ assert!(msg.contains("42"));
+ }
+
+ #[test]
+ fn test_error_display_invalid_string_index() {
+ let err = Error::InvalidStringIndex { index: 5, max: 3 };
+ let msg = format!("{}", err);
+ assert!(msg.contains("5"));
+ assert!(msg.contains("3"));
+ }
+
+ #[test]
+ fn test_error_display_invalid_string_index_empty() {
+ let err = Error::InvalidStringIndex { index: 0, max: 0 };
+ let msg = format!("{}", err);
+ assert!(msg.contains("empty"));
+ }
+
+ #[test]
+ fn test_error_display_unexpected_end_of_input() {
+ let err = Error::UnexpectedEndOfInput {
+ context: "reading string",
+ offset: 10,
+ expected: 4,
+ actual: 2,
+ };
+ let msg = format!("{}", err);
+ assert!(msg.contains("reading string"));
+ assert!(msg.contains("10"));
+ assert!(msg.contains("expected 4"));
+ assert!(msg.contains("found 2"));
+ }
+
+ #[test]
+ fn test_error_display_invalid_utf8() {
+ let err = Error::InvalidUtf8 { offset: 15 };
+ let msg = format!("{}", err);
+ assert!(msg.contains("15"));
+ }
+
+ #[test]
+ fn test_error_display_invalid_utf16() {
+ let err = Error::InvalidUtf16 {
+ offset: 20,
+ position: 3,
+ };
+ let msg = format!("{}", err);
+ assert!(msg.contains("20"));
+ assert!(msg.contains("3"));
+ }
+
+ #[test]
+ fn test_error_display_parse_error() {
+ let err = Error::ParseError {
+ input: "some very long input that should be truncated in the message".to_string(),
+ offset: 5,
+ context: "expected quote".to_string(),
+ };
+ let msg = format!("{}", err);
+ assert!(msg.contains("5"));
+ assert!(msg.contains("expected quote"));
+ // The input should be truncated to 50 chars, so the message shouldn't be too long
+ assert!(msg.len() < 150);
+ }
+
+ #[test]
+ fn test_error_with_offset_unexpected_end() {
+ let err = Error::UnexpectedEndOfInput {
+ context: "test",
+ offset: 10,
+ expected: 4,
+ actual: 2,
+ };
+ let adjusted = err.with_offset(100);
+ match adjusted {
+ Error::UnexpectedEndOfInput { offset, .. } => {
+ assert_eq!(offset, 110);
+ }
+ _ => panic!("Unexpected error type"),
+ }
+ }
+
+ #[test]
+ fn test_error_with_offset_invalid_utf8() {
+ let err = Error::InvalidUtf8 { offset: 5 };
+ let adjusted = err.with_offset(100);
+ match adjusted {
+ Error::InvalidUtf8 { offset } => {
+ assert_eq!(offset, 105);
+ }
+ _ => panic!("Unexpected error type"),
+ }
+ }
+
+ #[test]
+ fn test_error_with_offset_invalid_utf16() {
+ let err = Error::InvalidUtf16 {
+ offset: 10,
+ position: 2,
+ };
+ let adjusted = err.with_offset(100);
+ match adjusted {
+ Error::InvalidUtf16 { offset, position } => {
+ assert_eq!(offset, 110);
+ assert_eq!(position, 2);
+ }
+ _ => panic!("Unexpected error type"),
+ }
+ }
+
+ #[test]
+ fn test_error_with_offset_unknown_type() {
+ let err = Error::UnknownType {
+ type_byte: 0x42,
+ offset: 7,
+ };
+ let adjusted = err.with_offset(100);
+ match adjusted {
+ Error::UnknownType { type_byte, offset } => {
+ assert_eq!(type_byte, 0x42);
+ assert_eq!(offset, 107);
+ }
+ _ => panic!("Unexpected error type"),
+ }
+ }
+
+ #[test]
+ fn test_error_with_offset_parse_error() {
+ let err = Error::ParseError {
+ input: "test".to_string(),
+ offset: 3,
+ context: "context".to_string(),
+ };
+ let adjusted = err.with_offset(100);
+ match adjusted {
+ Error::ParseError { offset, .. } => {
+ assert_eq!(offset, 103);
+ }
+ _ => panic!("Unexpected error type"),
+ }
+ }
+
+ #[test]
+ fn test_error_with_offset_no_change_for_non_offset_variants() {
+ let err = Error::InvalidMagic {
+ found: 0x12345678,
+ expected: &[APPINFO_MAGIC_40],
+ };
+ let adjusted = err.with_offset(100);
+ // InvalidMagic doesn't have an offset field, so it should be unchanged
+ match adjusted {
+ Error::InvalidMagic { found, .. } => {
+ assert_eq!(found, 0x12345678);
+ }
+ _ => panic!("Unexpected error type"),
+ }
+ }
+
+ #[test]
+ fn test_parse_error_truncates_long_input() {
+ let long_input = "a".repeat(100);
+ let err = parse_error(&long_input, 0, "test context");
+ match err {
+ Error::ParseError { input, .. } => {
+ assert!(input.len() <= 50, "Input should be truncated to 50 chars");
+ }
+ _ => panic!("Expected ParseError variant"),
+ }
+ }
+
+ #[test]
+ fn test_with_offset_closure() {
+ let base_offset = 100;
+ let f = with_offset(base_offset);
+ let err = Error::InvalidUtf8 { offset: 5 };
+ let adjusted = f(err);
+ match adjusted {
+ Error::InvalidUtf8 { offset } => {
+ assert_eq!(offset, 105);
+ }
+ _ => panic!("Unexpected error type"),
+ }
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/lib.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/lib.rs
new file mode 100644
index 0000000..edf4307
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/lib.rs
@@ -0,0 +1,220 @@
+//! Blazing fast VDF (Valve Data Format) parser.
+//!
+//! This library provides parsers for both text and binary VDF formats used by Steam and
+//! other Valve Software products.
+//!
+//! # Features
+//!
+//! - **`no_std` compatible** — works without the standard library, requires only `alloc`
+//! - **Zero-copy parsing** for text format when possible (no escape sequences)
+//! - **Binary format support** for Steam's appinfo.vdf, shortcuts.vdf, and packageinfo.vdf
+//! - **Winnow-powered** text parser for maximum performance
+//!
+//! # Example
+//!
+//! ```
+//! use steam_vdf_parser::parse_text;
+//!
+//! let input = r#""root"
+//! {
+//! "key" "value"
+//! "nested"
+//! {
+//! "subkey" "subvalue"
+//! }
+//! }"#;
+//!
+//! let vdf = parse_text(input).unwrap();
+//! ```
+
+#![no_std]
+#![warn(missing_docs)]
+
+extern crate alloc;
+
+use alloc::borrow::Cow;
+use alloc::string::ToString;
+
+pub mod binary;
+pub mod error;
+pub mod text;
+pub mod value;
+
+pub use error::{Error, Result};
+pub use value::{Obj, Value, Vdf};
+
+// Re-export commonly used functions
+pub use text::parse_text;
+
+/// Parse VDF from binary format (autodetects shortcuts or appinfo format).
+///
+/// This function returns zero-copy data where possible - strings are borrowed
+/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
+pub fn parse_binary(input: &[u8]) -> Result<Vdf<'_>> {
+ binary::parse(input)
+}
+
+/// Parse a shortcuts.vdf format binary file.
+///
+/// This function returns zero-copy data where possible - strings are borrowed
+/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
+pub fn parse_shortcuts(input: &[u8]) -> Result<Vdf<'_>> {
+ binary::parse_shortcuts(input)
+}
+
+/// Parse an appinfo.vdf format binary file.
+///
+/// This function returns zero-copy data where possible - strings are borrowed
+/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
+pub fn parse_appinfo(input: &[u8]) -> Result<Vdf<'_>> {
+ binary::parse_appinfo(input)
+}
+
+/// Parse a packageinfo.vdf format binary file.
+///
+/// This function returns zero-copy data where possible - strings are borrowed
+/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
+pub fn parse_packageinfo(input: &[u8]) -> Result<Vdf<'_>> {
+ binary::parse_packageinfo(input)
+}
+
+// Convert from borrowed to owned
+impl Vdf<'_> {
+ /// Convert to an owned version (with 'static lifetime).
+ ///
+ /// This creates a new `Vdf<'static>` with all strings owned, allowing the
+ /// data to outlive the original input.
+ pub fn into_owned(self) -> Vdf<'static> {
+ let (key, value) = self.into_parts();
+ let owned_key: Cow<'static, str> = match key {
+ Cow::Borrowed(s) => Cow::Owned(s.to_string()),
+ Cow::Owned(s) => Cow::Owned(s),
+ };
+ Vdf::new(owned_key, value.into_owned())
+ }
+}
+
+impl Value<'_> {
+ /// Convert to an owned version (with 'static lifetime).
+ pub fn into_owned(self) -> Value<'static> {
+ match self {
+ Value::Str(s) => Value::Str(match s {
+ Cow::Borrowed(b) => b.to_string().into(),
+ Cow::Owned(o) => o.into(),
+ }),
+ Value::Obj(obj) => Value::Obj(obj.into_owned()),
+ Value::I32(n) => Value::I32(n),
+ Value::U64(n) => Value::U64(n),
+ Value::Float(n) => Value::Float(n),
+ Value::Pointer(n) => Value::Pointer(n),
+ Value::Color(c) => Value::Color(c),
+ }
+ }
+}
+
+impl Obj<'_> {
+ /// Convert to an owned version (with 'static lifetime).
+ pub fn into_owned(self) -> Obj<'static> {
+ let mut new = Obj::new();
+ for (k, v) in self.iter() {
+ let owned_key: Cow<'static, str> = match k {
+ Cow::Borrowed(b) => Cow::Owned(b.to_string()),
+ Cow::Owned(o) => Cow::Owned(o.clone()),
+ };
+ // Clone the value since we're iterating
+ new.insert(owned_key, v.clone().into_owned());
+ }
+ new
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloc::vec;
+
+ // Simple shortcuts.vdf format test data (object start, key, string value, object end)
+ const SHORTCUTS_VDF: &[u8] = &[
+ 0x00, // Object start
+ b't', b'e', b's', b't', 0x00, // Key "test"
+ 0x01, // String type
+ b'k', b'e', b'y', 0x00, // Nested key "key"
+ b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
+ 0x08, // Object end
+ ];
+
+ #[test]
+ fn test_parse_binary() {
+ let vdf = parse_binary(SHORTCUTS_VDF).unwrap();
+ assert!(vdf.as_obj().is_some());
+ assert_eq!(vdf.key(), "root");
+ let obj = vdf.as_obj().unwrap();
+ let test_obj = obj.get("test").and_then(|v| v.as_obj()).unwrap();
+ assert_eq!(test_obj.get("key").and_then(|v| v.as_str()), Some("value"));
+ }
+
+ #[test]
+ fn test_parse_shortcuts() {
+ let vdf = parse_shortcuts(SHORTCUTS_VDF).unwrap();
+ assert!(vdf.as_obj().is_some());
+ assert_eq!(vdf.key(), "root");
+ let obj = vdf.as_obj().unwrap();
+ let test_obj = obj.get("test").and_then(|v| v.as_obj()).unwrap();
+ assert_eq!(test_obj.get("key").and_then(|v| v.as_str()), Some("value"));
+ }
+
+ #[test]
+ fn test_parse_appinfo() {
+ // appinfo v40 magic + terminator (no apps)
+ // Need 8 bytes (magic + universe) + 68 bytes (APPINFO_ENTRY_HEADER_SIZE) = 76 bytes total
+ let mut input = vec![
+ 0x28, 0x44, 0x56, 0x07, // magic: 0x07564428 (APPINFO_MAGIC_40)
+ 0x20, 0x00, 0x00, 0x00, // universe: 32
+ 0x00, 0x00, 0x00, 0x00, // app_id = 0 (terminator)
+ ];
+ // Pad to 76 bytes total (8 + APPINFO_ENTRY_HEADER_SIZE)
+ input.resize(76, 0);
+ let result = parse_appinfo(&input);
+ if let Err(e) = &result {
+ panic!("parse_appinfo failed: {:?}", e);
+ }
+ assert!(result.is_ok());
+ let vdf = result.unwrap();
+ assert!(vdf.key().starts_with("appinfo_universe_"));
+ }
+
+ #[test]
+ fn test_into_owned_vdf() {
+ let input = r#""root"
+ {
+ "key" "value"
+ }"#;
+ let borrowed = parse_text(input).unwrap();
+ let owned = borrowed.into_owned();
+ assert_eq!(owned.key(), "root");
+ }
+
+ #[test]
+ fn test_into_owned_value_str() {
+ let borrowed = Value::Str("test".into());
+ let owned = borrowed.into_owned();
+ assert!(matches!(owned, Value::Str(Cow::Owned(_))));
+ }
+
+ #[test]
+ fn test_into_owned_value_obj() {
+ let mut obj = Obj::new();
+ obj.insert("key", Value::Str("value".into()));
+ let borrowed = Value::Obj(obj);
+ let owned = borrowed.into_owned();
+ assert!(matches!(owned, Value::Obj(_)));
+ }
+
+ #[test]
+ fn test_into_owned_obj() {
+ let mut obj = Obj::new();
+ obj.insert("key", Value::Str("value".into()));
+ let owned = obj.into_owned();
+ assert!(owned.get("key").is_some());
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/mod.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/mod.rs
new file mode 100644
index 0000000..2e091a5
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/mod.rs
@@ -0,0 +1,7 @@
+//! Text VDF format parser.
+//!
+//! Parses human-readable VDF text format using winnow parser combinators.
+
+pub mod parser;
+
+pub use parser::parse as parse_text;
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/parser.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/parser.rs
new file mode 100644
index 0000000..730a8bb
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/parser.rs
@@ -0,0 +1,408 @@
+//! Text VDF parser powered by winnow.
+
+use alloc::borrow::Cow;
+use alloc::string::String;
+
+use winnow::ascii::{line_ending, multispace1};
+use winnow::combinator::{alt, delimited, preceded, repeat};
+use winnow::error::{ContextError, StrContext};
+use winnow::prelude::*;
+use winnow::token::{one_of, take_till};
+
+use crate::error::{Result, parse_error};
+use crate::value::{Obj, Value, Vdf};
+
+/// Parse a VDF document from text format.
+///
+/// # Example
+///
+/// ```
+/// use steam_vdf_parser::parse_text;
+///
+/// let input = r#""root"
+/// {
+/// "key" "value"
+/// }"#;
+/// let vdf = parse_text(input).unwrap();
+/// assert_eq!(vdf.key(), "root");
+/// ```
+pub fn parse(input: &str) -> Result<Vdf<'_>> {
+ let mut input = input.trim_start();
+
+ let key = token
+ .parse_next(&mut input)
+ .map_err(|_| parse_error(input, 0, "expected root key"))?;
+
+ let obj = object
+ .parse_next(&mut input)
+ .map_err(|_| parse_error(input, 0, "expected root object"))?;
+
+ Ok(Vdf::new(Cow::Borrowed(key), Value::Obj(obj)))
+}
+
+/// Parse a token (either quoted or unquoted).
+fn token<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
+ preceded(whitespace, alt((quoted_string, unquoted_string))).parse_next(input)
+}
+
+/// Parse a quoted string, returning a Cow (borrowed if no escapes, owned if escapes processed).
+///
+/// Handles escape sequences: \n, \t, \r, \\, \"
+fn quoted_string_cow<'i>(input: &mut &'i str) -> ModalResult<Cow<'i, str>> {
+ // Parse opening quote
+ '"'.parse_next(input)?;
+
+ // Check if there are any escape sequences
+ let content_end = input.find(['\\', '"']).unwrap_or(input.len());
+
+ if content_end < input.len() && input[content_end..].starts_with('\\') {
+ // Has escape sequences - need to process them
+ let mut result = String::from(&input[..content_end]);
+ *input = &input[content_end..];
+
+ loop {
+ // Check for closing quote
+ if let Some(c) = input.chars().next() {
+ if c == '"' {
+ *input = &input[c.len_utf8()..];
+ return Ok(Cow::Owned(result));
+ }
+ if c == '\\' {
+ // Escape sequence - consume backslash
+ *input = &input[c.len_utf8()..];
+
+ // Get escaped character
+ let escaped = one_of(('n', 't', 'r', '\\', '"'))
+ .map(|c| match c {
+ 'n' => '\n',
+ 't' => '\t',
+ 'r' => '\r',
+ '\\' => '\\',
+ '"' => '"',
+ _ => unreachable!(),
+ })
+ .parse_next(input)?;
+ result.push(escaped);
+ } else {
+ result.push(c);
+ *input = &input[c.len_utf8()..];
+ }
+ } else {
+ // EOF before closing quote - fail
+ return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
+ }
+ }
+ } else {
+ // No escapes - zero copy path
+ let content = &input[..content_end];
+ *input = &input[content_end..];
+
+ // Parse closing quote
+ '"'.parse_next(input)?;
+
+ Ok(Cow::Borrowed(content))
+ }
+}
+
+/// Parse a quoted string (borrowed version for key parsing).
+/// Keys with escapes will fail - use quoted_string_cow for values that may have escapes.
+fn quoted_string<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
+ '"'.parse_next(input)?;
+
+ // Find closing quote, checking for escapes
+ let mut end = 0;
+ let mut chars = input.char_indices();
+ while let Some((idx, c)) = chars.next() {
+ if c == '"' {
+ end = idx;
+ break;
+ }
+ if c == '\\' {
+ // Skip escaped character
+ chars.next();
+ }
+ }
+
+ if end == 0 {
+ return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
+ }
+
+ let result = &input[..end];
+ *input = &input[end + '"'.len_utf8()..];
+
+ Ok(result)
+}
+
+/// Parse an unquoted string.
+///
+/// Unquoted strings end at whitespace, `{`, `}`, or `"`.
+fn unquoted_string<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
+ take_till(1.., |c: char| {
+ c.is_whitespace() || c == '{' || c == '}' || c == '"'
+ })
+ .context(StrContext::Label("token"))
+ .parse_next(input)
+}
+
+/// Parse an object (recursive block of key-value pairs).
+fn object<'i>(input: &mut &'i str) -> ModalResult<Obj<'i>> {
+ preceded(
+ whitespace,
+ delimited('{', object_body, preceded(whitespace, '}')),
+ )
+ .context(StrContext::Label("object"))
+ .parse_next(input)
+}
+
+/// Parse the body of an object (key-value pairs until closing brace).
+fn object_body<'i>(input: &mut &'i str) -> ModalResult<Obj<'i>> {
+ let mut obj = Obj::new();
+
+ loop {
+ // Skip whitespace
+ whitespace.parse_next(input)?;
+
+ // Check for closing brace
+ if input.starts_with('}') {
+ break;
+ }
+
+ // Parse a key-value pair
+ let (key, value) = kv_pair.parse_next(input)?;
+ obj.insert(Cow::Borrowed(key), value);
+ }
+
+ Ok(obj)
+}
+
+/// Parse a key-value pair.
+fn kv_pair<'i>(input: &mut &'i str) -> ModalResult<(&'i str, Value<'i>)> {
+ let key = token.parse_next(input)?;
+
+ // Skip whitespace before value
+ whitespace.parse_next(input)?;
+
+ // Parse the value
+ let value = if let Some(c) = input.chars().next() {
+ match c {
+ '{' => object.map(Value::Obj).parse_next(input)?,
+ '"' => quoted_string_cow.map(Value::Str).parse_next(input)?,
+ _ => unquoted_string
+ .map(|s| Value::Str(Cow::Borrowed(s)))
+ .parse_next(input)?,
+ }
+ } else {
+ return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
+ };
+
+ Ok((key, value))
+}
+
+/// Skip whitespace and line comments.
+fn whitespace(input: &mut &str) -> ModalResult<()> {
+ repeat(0.., alt((multispace1.void(), line_comment.void()))).parse_next(input)
+}
+
+/// Parse a line comment (// to newline).
+fn line_comment(input: &mut &str) -> ModalResult<()> {
+ preceded(
+ "//",
+ alt((line_ending.void(), take_till(0.., ['\r', '\n']).void())),
+ )
+ .parse_next(input)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloc::format;
+
+ #[test]
+ fn test_parse_simple_kv() {
+ let input = r#""root"
+ {
+ "key" "value"
+ }"#;
+ let vdf = parse(input).unwrap();
+ assert_eq!(vdf.key(), "root");
+
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str());
+ assert_eq!(value, Some("value"));
+ }
+
+ #[test]
+ fn test_parse_nested_objects() {
+ let input = r#""outer"
+ {
+ "inner"
+ {
+ "key" "value"
+ }
+ }"#;
+ let vdf = parse(input).unwrap();
+ assert_eq!(vdf.key(), "outer");
+
+ let obj = vdf.as_obj().unwrap();
+ let inner = obj.get("inner").and_then(|v| v.as_obj()).unwrap();
+ let value = inner.get("key").and_then(|v| v.as_str());
+ assert_eq!(value, Some("value"));
+ }
+
+ #[test]
+ fn test_parse_unquoted_tokens() {
+ let input = r#"root
+ {
+ key value
+ }"#;
+ let vdf = parse(input).unwrap();
+ assert_eq!(vdf.key(), "root");
+
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str());
+ assert_eq!(value, Some("value"));
+ }
+
+ #[test]
+ fn test_parse_with_comments() {
+ let input = r#""root"
+ {
+ // This is a comment
+ "key" "value"
+ // Another comment
+ }"#;
+ let vdf = parse(input).unwrap();
+
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str());
+ assert_eq!(value, Some("value"));
+ }
+
+ #[test]
+ fn test_parse_multiple_keys() {
+ let input = r#""settings"
+ {
+ "name" "test"
+ "count" "42"
+ }"#;
+ let vdf = parse(input).unwrap();
+
+ let obj = vdf.as_obj().unwrap();
+ assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("test"));
+ assert_eq!(obj.get("count").and_then(|v| v.as_str()), Some("42"));
+ }
+
+ #[test]
+ fn test_escape_sequences() {
+ let test_cases: &[(&str, &str)] = &[
+ (r#""test\nline""#, "test\nline"),
+ (r#""test\ttab""#, "test\ttab"),
+ (r#""test\\backslash""#, "test\\backslash"),
+ (r#""test\"quote""#, "test\"quote"),
+ (r#""test\rreturn""#, "test\rreturn"),
+ ];
+
+ for (input, expected) in test_cases {
+ let full_input = format!(r#""root"{{"key" {}}}"#, input);
+ let vdf = parse(&full_input).unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
+ assert_eq!(value, *expected, "Failed for input: {}", input);
+ }
+ }
+
+ #[test]
+ fn test_escape_sequences_in_nested_objects() {
+ let input = r#""root"
+ {
+ "outer"
+ {
+ "key" "value\nwith\nnewlines"
+ }
+ }"#;
+ let vdf = parse(input).unwrap();
+ let outer = vdf
+ .as_obj()
+ .unwrap()
+ .get("outer")
+ .and_then(|v| v.as_obj())
+ .unwrap();
+ let value = outer.get("key").and_then(|v| v.as_str()).unwrap();
+ assert_eq!(value, "value\nwith\nnewlines");
+ }
+
+ #[test]
+ fn test_mixed_escape_sequences() {
+ let input = r#""root"{"key" "line1\nline2\ttab\\slash\"quote"}"#;
+ let vdf = parse(input).unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
+ assert_eq!(value, "line1\nline2\ttab\\slash\"quote");
+ }
+
+ #[test]
+ fn test_unquoted_token_no_escape_processing() {
+ let input = r#"root{key value\nnotescaped}"#;
+ let vdf = parse(input).unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
+ // Unquoted tokens should have literal backslash-n
+ assert_eq!(value, r#"value\nnotescaped"#);
+ }
+
+ #[test]
+ fn test_quoted_string_without_escapes_zero_copy() {
+ let input = r#""root"{"key" "value"}"#;
+ let vdf = parse(input).unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
+ // Without escapes, value should be parsed correctly (zero-copy internally)
+ assert_eq!(value, "value");
+ }
+
+ #[test]
+ fn test_quoted_string_with_escapes_owned() {
+ let input = r#""root"{"key" "value\nwith\nescape"}"#;
+ let vdf = parse(input).unwrap();
+ let obj = vdf.as_obj().unwrap();
+ let value = obj.get("key").and_then(|v| v.as_str()).unwrap();
+ // With escapes, value should be parsed correctly (owned internally)
+ assert_eq!(value, "value\nwith\nescape");
+ }
+
+ #[test]
+ fn test_empty_object() {
+ let input = r#""root"{}"#;
+ let vdf = parse(input).unwrap();
+ let obj = vdf.as_obj().unwrap();
+ assert!(obj.is_empty());
+ }
+
+ #[test]
+ fn test_deeply_nested_objects() {
+ let input = r#""root"
+ {
+ "level1"
+ {
+ "level2"
+ {
+ "level3"
+ {
+ "key" "value"
+ }
+ }
+ }
+ }"#;
+ let vdf = parse(input).unwrap();
+ let level1 = vdf
+ .as_obj()
+ .unwrap()
+ .get("level1")
+ .and_then(|v| v.as_obj())
+ .unwrap();
+ let level2 = level1.get("level2").and_then(|v| v.as_obj()).unwrap();
+ let level3 = level2.get("level3").and_then(|v| v.as_obj()).unwrap();
+ let value = level3.get("key").and_then(|v| v.as_str()).unwrap();
+ assert_eq!(value, "value");
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/hasher.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/hasher.rs
new file mode 100644
index 0000000..126ea86
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/hasher.rs
@@ -0,0 +1,127 @@
+//! Default hasher for IndexMap.
+
+use core::fmt::Debug;
+use core::hash::{BuildHasher, Hasher};
+use foldhash::fast::RandomState;
+use static_assertions::assert_impl_all;
+
+/// Default hash builder for IndexMap.
+#[derive(Clone, Debug, Default)]
+pub struct DefaultHashBuilder {
+ inner: RandomState,
+}
+
+impl BuildHasher for DefaultHashBuilder {
+ type Hasher = DefaultHasher;
+
+ #[inline(always)]
+ fn build_hasher(&self) -> Self::Hasher {
+ DefaultHasher {
+ inner: self.inner.build_hasher(),
+ }
+ }
+}
+
+/// Default hasher.
+#[derive(Clone)]
+pub struct DefaultHasher {
+ inner: <RandomState as BuildHasher>::Hasher,
+}
+
+impl Hasher for DefaultHasher {
+ #[inline(always)]
+ fn write(&mut self, bytes: &[u8]) {
+ self.inner.write(bytes);
+ }
+
+ #[inline(always)]
+ fn write_u8(&mut self, i: u8) {
+ self.inner.write_u8(i);
+ }
+
+ #[inline(always)]
+ fn write_u16(&mut self, i: u16) {
+ self.inner.write_u16(i);
+ }
+
+ #[inline(always)]
+ fn write_u32(&mut self, i: u32) {
+ self.inner.write_u32(i);
+ }
+
+ #[inline(always)]
+ fn write_u64(&mut self, i: u64) {
+ self.inner.write_u64(i);
+ }
+
+ #[inline(always)]
+ fn write_u128(&mut self, i: u128) {
+ self.inner.write_u128(i);
+ }
+
+ #[inline(always)]
+ fn write_usize(&mut self, i: usize) {
+ self.inner.write_usize(i);
+ }
+
+ #[inline(always)]
+ fn write_i8(&mut self, i: i8) {
+ self.inner.write_i8(i);
+ }
+
+ #[inline(always)]
+ fn write_i16(&mut self, i: i16) {
+ self.inner.write_i16(i);
+ }
+
+ #[inline(always)]
+ fn write_i32(&mut self, i: i32) {
+ self.inner.write_i32(i);
+ }
+
+ #[inline(always)]
+ fn write_i64(&mut self, i: i64) {
+ self.inner.write_i64(i);
+ }
+
+ #[inline(always)]
+ fn write_i128(&mut self, i: i128) {
+ self.inner.write_i128(i);
+ }
+
+ #[inline(always)]
+ fn write_isize(&mut self, i: isize) {
+ self.inner.write_isize(i);
+ }
+
+ #[inline(always)]
+ fn finish(&self) -> u64 {
+ self.inner.finish()
+ }
+}
+
+assert_impl_all!(DefaultHashBuilder: Clone, Debug, Default, BuildHasher);
+assert_impl_all!(DefaultHasher: Clone, Hasher);
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use core::hash::{BuildHasher, Hasher};
+
+ #[test]
+ fn write_order_affects_hash() {
+ let builder = DefaultHashBuilder::default();
+ let hasher = builder.build_hasher();
+
+ let mut h1 = hasher.clone();
+ let mut h2 = hasher.clone();
+
+ h1.write(b"a");
+ h1.write(b"b");
+
+ h2.write(b"b");
+ h2.write(b"a");
+
+ assert_ne!(h1.finish(), h2.finish());
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/impls.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/impls.rs
new file mode 100644
index 0000000..422c6f7
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/impls.rs
@@ -0,0 +1,222 @@
+//! Trait implementations for VDF types.
+
+use alloc::borrow::Cow;
+use alloc::string::String;
+
+use super::types::{Key, Obj, Value, Vdf};
+use core::fmt;
+use core::fmt::Write as _;
+use core::ops::Index;
+
+// ============================================================================
+// Pretty-print helper functions
+// ============================================================================
+
+/// Write a quoted and escaped string to the formatter.
+///
+/// Escapes special characters: `\n`, `\t`, `\r`, `\\`, `"`
+fn write_quoted_str(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
+ f.write_char('"')?;
+ for c in s.chars() {
+ match c {
+ '\n' => f.write_str("\\n")?,
+ '\t' => f.write_str("\\t")?,
+ '\r' => f.write_str("\\r")?,
+ '\\' => f.write_str("\\\\")?,
+ '"' => f.write_str("\\\"")?,
+ c => f.write_char(c)?,
+ }
+ }
+ f.write_char('"')
+}
+
+/// Write indentation (tabs) to the formatter.
+fn write_indent(f: &mut fmt::Formatter<'_>, level: usize) -> fmt::Result {
+ for _ in 0..level {
+ f.write_char('\t')?;
+ }
+ Ok(())
+}
+
+/// Helper struct for pretty-printing an Obj with a specific indent level.
+struct PrettyObj<'a, 'text> {
+ obj: &'a Obj<'text>,
+ indent: usize,
+}
+
+impl fmt::Display for PrettyObj<'_, '_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ writeln!(f, "{{")?;
+ for (key, value) in self.obj.inner.iter() {
+ write_indent(f, self.indent + 1)?;
+ write_quoted_str(f, key)?;
+ if let Value::Obj(inner_obj) = value {
+ // Object value: key on one line, value (with braces) on next lines
+ writeln!(f)?;
+ write_indent(f, self.indent + 1)?;
+ write!(
+ f,
+ "{}",
+ PrettyObj {
+ obj: inner_obj,
+ indent: self.indent + 1
+ }
+ )?;
+ writeln!(f)?;
+ } else {
+ // Scalar value: key<tab>value on same line
+ write!(f, "\t")?;
+ write!(f, "{:#}", value)?;
+ writeln!(f)?;
+ }
+ }
+ write_indent(f, self.indent)?;
+ write!(f, "}}")
+ }
+}
+
+// ============================================================================
+// From implementations for Value
+// ============================================================================
+
+impl<'text> From<&'text str> for Value<'text> {
+ fn from(s: &'text str) -> Self {
+ Value::Str(Cow::Borrowed(s))
+ }
+}
+
+impl From<String> for Value<'static> {
+ fn from(s: String) -> Self {
+ Value::Str(Cow::Owned(s))
+ }
+}
+
+impl From<i32> for Value<'static> {
+ fn from(n: i32) -> Self {
+ Value::I32(n)
+ }
+}
+
+impl From<u64> for Value<'static> {
+ fn from(n: u64) -> Self {
+ Value::U64(n)
+ }
+}
+
+impl From<f32> for Value<'static> {
+ fn from(n: f32) -> Self {
+ Value::Float(n)
+ }
+}
+
+impl From<u32> for Value<'static> {
+ fn from(n: u32) -> Self {
+ Value::Pointer(n)
+ }
+}
+
+impl From<[u8; 4]> for Value<'static> {
+ fn from(color: [u8; 4]) -> Self {
+ Value::Color(color)
+ }
+}
+
+impl<'text> From<Obj<'text>> for Value<'text> {
+ fn from(obj: Obj<'text>) -> Self {
+ Value::Obj(obj)
+ }
+}
+
+impl<'text> fmt::Display for Value<'text> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Value::Str(s) => write_quoted_str(f, s),
+ Value::Obj(obj) => write!(f, "{}", obj),
+ Value::I32(n) => write!(f, "\"{}\"", n),
+ Value::U64(n) => write!(f, "\"{}\"", n),
+ Value::Float(n) => write!(f, "\"{}\"", n),
+ Value::Pointer(n) => write!(f, "\"0x{:08x}\"", n),
+ Value::Color(c) => write!(f, "\"{} {} {} {}\"", c[0], c[1], c[2], c[3]),
+ }
+ }
+}
+
+impl<'text> Default for Obj<'text> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<'text> fmt::Display for Obj<'text> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ PrettyObj {
+ obj: self,
+ indent: 0
+ }
+ )
+ }
+}
+
+impl<'text> fmt::Display for Vdf<'text> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write_quoted_str(f, self.key())?;
+ writeln!(f)?;
+ write!(f, "{}", self.value())
+ }
+}
+
+// ============================================================================
+// IntoIterator implementations for Obj
+// ============================================================================
+
+impl<'text> IntoIterator for Obj<'text> {
+ type Item = (Key<'text>, Value<'text>);
+ type IntoIter = indexmap::map::IntoIter<Key<'text>, Value<'text>>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.inner.into_iter()
+ }
+}
+
+impl<'a, 'text> IntoIterator for &'a Obj<'text> {
+ type Item = (&'a Key<'text>, &'a Value<'text>);
+ type IntoIter = indexmap::map::Iter<'a, Key<'text>, Value<'text>>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.inner.iter()
+ }
+}
+
+// ============================================================================
+// Index implementations for Value and Obj
+// ============================================================================
+
+impl<'text> Index<&str> for Value<'text> {
+ type Output = Value<'text>;
+
+ /// Returns a reference to the value at the given key.
+ ///
+ /// # Panics
+ ///
+ /// Panics if this is not an object or if the key doesn't exist.
+ /// Use `get()` for non-panicking access.
+ fn index(&self, key: &str) -> &Self::Output {
+ self.get(key).expect("key not found in Value")
+ }
+}
+
+impl<'text> Index<&str> for Obj<'text> {
+ type Output = Value<'text>;
+
+ /// Returns a reference to the value at the given key.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the key doesn't exist. Use `get()` for non-panicking access.
+ fn index(&self, key: &str) -> &Self::Output {
+ self.get(key).expect("key not found in Obj")
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/mod.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/mod.rs
new file mode 100644
index 0000000..2518957
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/mod.rs
@@ -0,0 +1,10 @@
+//! Core data structures for VDF representation.
+
+mod hasher;
+mod impls;
+mod types;
+
+#[cfg(test)]
+mod tests;
+
+pub use types::{Obj, Value, Vdf};
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/tests.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/tests.rs
new file mode 100644
index 0000000..5dda1e9
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/tests.rs
@@ -0,0 +1,802 @@
+//! Unit tests for VDF types.
+
+use super::types::{Obj, Value, Vdf};
+use alloc::borrow::Cow;
+use alloc::format;
+use alloc::string::{String, ToString};
+
+// ============================================================================
+// From implementation tests
+// ============================================================================
+
+#[test]
+fn test_value_from_str() {
+ let value: Value = "hello".into();
+ assert!(value.is_str());
+ assert_eq!(value.as_str(), Some("hello"));
+}
+
+#[test]
+fn test_value_from_string() {
+ let value: Value = String::from("hello").into();
+ assert!(value.is_str());
+ assert_eq!(value.as_str(), Some("hello"));
+}
+
+#[test]
+fn test_value_from_i32() {
+ let value: Value = 42i32.into();
+ assert!(value.is_i32());
+ assert_eq!(value.as_i32(), Some(42));
+}
+
+#[test]
+fn test_value_from_u64() {
+ let value: Value = 123u64.into();
+ assert!(value.is_u64());
+ assert_eq!(value.as_u64(), Some(123));
+}
+
+#[test]
+fn test_value_from_f32() {
+ let value: Value = 2.5f32.into();
+ assert!(value.is_float());
+ assert_eq!(value.as_float(), Some(2.5));
+}
+
+#[test]
+fn test_value_from_u32() {
+ let value: Value = 0x12345678u32.into();
+ assert!(value.is_pointer());
+ assert_eq!(value.as_pointer(), Some(0x12345678));
+}
+
+#[test]
+fn test_value_from_color() {
+ let value: Value = [255u8, 0, 128, 64].into();
+ assert!(value.is_color());
+ assert_eq!(value.as_color(), Some([255, 0, 128, 64]));
+}
+
+#[test]
+fn test_value_from_obj() {
+ let obj = Obj::new();
+ let value: Value = obj.into();
+ assert!(value.is_obj());
+}
+
+// ============================================================================
+// Type check tests
+// ============================================================================
+
+#[test]
+fn test_value_is_methods() {
+ let v = Value::I32(42);
+ assert!(v.is_i32());
+ assert!(!v.is_str());
+ assert!(!v.is_obj());
+}
+
+#[test]
+fn test_value_is_str() {
+ let v = Value::Str(Cow::Borrowed("test"));
+ assert!(v.is_str());
+ assert!(!v.is_i32());
+}
+
+#[test]
+fn test_value_is_obj() {
+ let v = Value::Obj(Obj::new());
+ assert!(v.is_obj());
+ assert!(!v.is_str());
+}
+
+#[test]
+fn test_value_is_float() {
+ let v = Value::Float(1.0);
+ assert!(v.is_float());
+ assert!(!v.is_i32());
+}
+
+#[test]
+fn test_value_is_u64() {
+ let v = Value::U64(100);
+ assert!(v.is_u64());
+ assert!(!v.is_i32());
+}
+
+#[test]
+fn test_value_is_pointer() {
+ let v = Value::Pointer(0x12345678);
+ assert!(v.is_pointer());
+ assert!(!v.is_i32());
+}
+
+#[test]
+fn test_value_is_color() {
+ let v = Value::Color([255, 0, 0, 255]);
+ assert!(v.is_color());
+ assert!(!v.is_i32());
+}
+
+#[test]
+fn test_value_as_methods() {
+ let v = Value::I32(42);
+ assert_eq!(v.as_i32(), Some(42));
+ assert_eq!(v.as_str(), None);
+ assert_eq!(v.as_obj(), None);
+}
+
+#[test]
+fn test_value_as_str() {
+ let v = Value::Str(Cow::Borrowed("test"));
+ assert_eq!(v.as_str(), Some("test"));
+}
+
+#[test]
+fn test_value_as_obj() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let v = Value::Obj(obj);
+ assert!(v.as_obj().is_some());
+}
+
+#[test]
+fn test_value_as_float() {
+ let v = Value::Float(1.5);
+ assert_eq!(v.as_float(), Some(1.5));
+}
+
+#[test]
+fn test_value_as_u64() {
+ let v = Value::U64(123456789);
+ assert_eq!(v.as_u64(), Some(123456789));
+}
+
+#[test]
+fn test_value_as_pointer() {
+ let v = Value::Pointer(0xABCDEF01);
+ assert_eq!(v.as_pointer(), Some(0xABCDEF01));
+}
+
+#[test]
+fn test_value_as_color() {
+ let v = Value::Color([255, 128, 64, 32]);
+ assert_eq!(v.as_color(), Some([255, 128, 64, 32]));
+}
+
+#[test]
+fn test_value_display_i32() {
+ assert_eq!(format!("{}", Value::I32(42)), "\"42\"");
+ assert_eq!(format!("{}", Value::I32(-42)), "\"-42\"");
+}
+
+#[test]
+fn test_value_display_u64() {
+ assert_eq!(format!("{}", Value::U64(100)), "\"100\"");
+}
+
+#[test]
+fn test_value_display_str() {
+ assert_eq!(format!("{}", Value::Str(Cow::Borrowed("test"))), "\"test\"");
+}
+
+#[test]
+fn test_value_display_obj() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let v = Value::Obj(obj);
+ assert!(format!("{}", v).contains("\"key\""));
+ assert!(format!("{}", v).contains("\"42\""));
+}
+
+#[test]
+fn test_value_display_float() {
+ let v = Value::Float(1.5);
+ assert_eq!(format!("{}", v), "\"1.5\"");
+}
+
+#[test]
+fn test_value_display_pointer() {
+ assert_eq!(format!("{}", Value::Pointer(0x12345678)), "\"0x12345678\"");
+}
+
+#[test]
+fn test_value_display_color() {
+ assert_eq!(
+ format!("{}", Value::Color([255, 0, 0, 255])),
+ "\"255 0 0 255\""
+ );
+}
+
+#[test]
+fn test_obj_new_is_empty() {
+ let obj = Obj::new();
+ assert!(obj.is_empty());
+ assert_eq!(obj.len(), 0);
+}
+
+#[test]
+fn test_obj_get() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ assert_eq!(obj.get("key").and_then(|v| v.as_i32()), Some(42));
+ assert_eq!(obj.get("missing"), None);
+}
+
+#[test]
+fn test_obj_len() {
+ let mut obj = Obj::new();
+ assert_eq!(obj.len(), 0);
+ obj.insert(Cow::Borrowed("key1"), Value::I32(1));
+ assert_eq!(obj.len(), 1);
+ obj.insert(Cow::Borrowed("key2"), Value::I32(2));
+ assert_eq!(obj.len(), 2);
+}
+
+#[test]
+fn test_obj_iter() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key1"), Value::I32(1));
+ obj.insert(Cow::Borrowed("key2"), Value::I32(2));
+ let mut iter = obj.iter();
+ assert!(iter.next().is_some());
+ assert!(iter.next().is_some());
+ assert!(iter.next().is_none());
+}
+
+#[test]
+fn test_obj_default() {
+ let obj = Obj::default();
+ assert!(obj.is_empty());
+}
+
+#[test]
+fn test_vdf_new() {
+ let vdf = Vdf::new("root", Value::Obj(Obj::new()));
+ assert_eq!(vdf.key(), "root");
+ assert!(vdf.is_obj());
+}
+
+#[test]
+fn test_vdf_is_obj() {
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(Obj::new()));
+ assert!(vdf.is_obj());
+ let vdf2 = Vdf::new(Cow::Borrowed("root"), Value::I32(42));
+ assert!(!vdf2.is_obj());
+}
+
+#[test]
+fn test_vdf_as_obj() {
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(Obj::new()));
+ assert!(vdf.as_obj().is_some());
+ let vdf2 = Vdf::new(Cow::Borrowed("root"), Value::I32(42));
+ assert!(vdf2.as_obj().is_none());
+}
+
+#[test]
+fn test_vdf_display() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(obj));
+ let s = format!("{}", vdf);
+ assert!(s.contains("root"));
+}
+
+#[test]
+fn test_into_owned_value_str() {
+ let value = Value::Str(Cow::Borrowed("test"));
+ let owned = value.into_owned();
+ assert_eq!(owned, Value::Str(Cow::Owned("test".to_string())));
+}
+
+#[test]
+fn test_into_owned_value_str_already_owned() {
+ let value = Value::Str(Cow::Owned("test".to_string()));
+ let owned = value.into_owned();
+ assert_eq!(owned, Value::Str(Cow::Owned("test".to_string())));
+}
+
+#[test]
+fn test_into_owned_value_obj() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let value = Value::Obj(obj);
+ let owned = value.into_owned();
+ assert!(owned.is_obj());
+}
+
+#[test]
+fn test_into_owned_value_numeric() {
+ assert_eq!(Value::I32(42).into_owned(), Value::I32(42));
+ assert_eq!(Value::U64(100).into_owned(), Value::U64(100));
+ assert_eq!(Value::Float(1.5).into_owned(), Value::Float(1.5));
+ assert_eq!(Value::Pointer(123).into_owned(), Value::Pointer(123));
+ assert_eq!(
+ Value::Color([1, 2, 3, 4]).into_owned(),
+ Value::Color([1, 2, 3, 4])
+ );
+}
+
+#[test]
+fn test_into_owned_obj() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let owned = obj.into_owned();
+ assert_eq!(owned.len(), 1);
+ assert_eq!(owned.get("key").and_then(|v| v.as_i32()), Some(42));
+}
+
+#[test]
+fn test_into_owned_obj_nested() {
+ let mut inner = Obj::new();
+ inner.insert(
+ Cow::Borrowed("inner_key"),
+ Value::Str(Cow::Borrowed("value")),
+ );
+ let mut outer = Obj::new();
+ outer.insert(Cow::Borrowed("outer_key"), Value::Obj(inner));
+ let owned = outer.into_owned();
+ let inner_obj = owned.get("outer_key").and_then(|v| v.as_obj()).unwrap();
+ assert_eq!(
+ inner_obj.get("inner_key").and_then(|v| v.as_str()),
+ Some("value")
+ );
+}
+
+#[test]
+fn test_into_owned_vdf() {
+ let vdf = Vdf::new("root", Value::I32(42));
+ let owned = vdf.into_owned();
+ assert_eq!(owned.key(), "root");
+ assert_eq!(owned.value(), &Value::I32(42));
+}
+
+// Tests for Value::get and get_path methods
+
+#[test]
+fn test_value_get_on_obj() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let value = Value::Obj(obj);
+ assert_eq!(value.get("key").and_then(|v| v.as_i32()), Some(42));
+ assert_eq!(value.get("missing"), None);
+}
+
+#[test]
+fn test_value_get_on_non_obj() {
+ let value = Value::I32(42);
+ assert_eq!(value.get("key"), None);
+}
+
+#[test]
+fn test_value_get_path_single_key() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let value = Value::Obj(obj);
+ assert_eq!(value.get_path(&["key"]).and_then(|v| v.as_i32()), Some(42));
+}
+
+#[test]
+fn test_value_get_path_empty() {
+ let value = Value::I32(42);
+ assert_eq!(value.get_path(&[]).and_then(|v| v.as_i32()), Some(42));
+}
+
+#[test]
+fn test_value_get_path_nested() {
+ let mut inner = Obj::new();
+ inner.insert(Cow::Borrowed("c"), Value::Str(Cow::Borrowed("found")));
+ let mut middle = Obj::new();
+ middle.insert(Cow::Borrowed("b"), Value::Obj(inner));
+ let mut outer = Obj::new();
+ outer.insert(Cow::Borrowed("a"), Value::Obj(middle));
+ let value = Value::Obj(outer);
+
+ assert_eq!(
+ value.get_path(&["a", "b", "c"]).and_then(|v| v.as_str()),
+ Some("found")
+ );
+}
+
+#[test]
+fn test_value_get_path_missing_intermediate() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("a"), Value::I32(42));
+ let value = Value::Obj(obj);
+ assert_eq!(value.get_path(&["a", "b"]), None);
+}
+
+#[test]
+fn test_value_get_path_non_obj_intermediate() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("a"), Value::I32(42));
+ let value = Value::Obj(obj);
+ assert_eq!(value.get_path(&["a", "b"]), None);
+}
+
+#[test]
+fn test_value_get_str() {
+ let mut inner = Obj::new();
+ inner.insert(Cow::Borrowed("name"), Value::Str(Cow::Borrowed("test")));
+ let mut outer = Obj::new();
+ outer.insert(Cow::Borrowed("data"), Value::Obj(inner));
+ let value = Value::Obj(outer);
+
+ assert_eq!(value.get_str(&["data", "name"]), Some("test"));
+ assert_eq!(value.get_str(&["data", "missing"]), None);
+}
+
+#[test]
+fn test_value_get_obj() {
+ let mut inner = Obj::new();
+ inner.insert(Cow::Borrowed("key"), Value::I32(1));
+ let mut outer = Obj::new();
+ outer.insert(Cow::Borrowed("nested"), Value::Obj(inner));
+ let value = Value::Obj(outer);
+
+ let obj = value.get_obj(&["nested"]).unwrap();
+ assert_eq!(obj.len(), 1);
+}
+
+#[test]
+fn test_value_get_i32() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("num"), Value::I32(123));
+ let value = Value::Obj(obj);
+
+ assert_eq!(value.get_i32(&["num"]), Some(123));
+ assert_eq!(value.get_i32(&["missing"]), None);
+}
+
+#[test]
+fn test_value_get_u64() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("big"), Value::U64(9999999999));
+ let value = Value::Obj(obj);
+
+ assert_eq!(value.get_u64(&["big"]), Some(9999999999));
+}
+
+#[test]
+fn test_value_get_float() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("ratio"), Value::Float(2.5));
+ let value = Value::Obj(obj);
+
+ assert_eq!(value.get_float(&["ratio"]), Some(2.5));
+}
+
+// Tests for Vdf delegation methods
+
+#[test]
+fn test_vdf_get() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("key"), Value::I32(42));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(obj));
+
+ assert_eq!(vdf.get("key").and_then(|v| v.as_i32()), Some(42));
+ assert_eq!(vdf.get("missing"), None);
+}
+
+#[test]
+fn test_vdf_get_path() {
+ let mut inner = Obj::new();
+ inner.insert(Cow::Borrowed("value"), Value::Str(Cow::Borrowed("found")));
+ let mut outer = Obj::new();
+ outer.insert(Cow::Borrowed("nested"), Value::Obj(inner));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(outer));
+
+ assert_eq!(
+ vdf.get_path(&["nested", "value"]).and_then(|v| v.as_str()),
+ Some("found")
+ );
+}
+
+#[test]
+fn test_vdf_get_str() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("name"), Value::Str(Cow::Borrowed("test")));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(obj));
+
+ assert_eq!(vdf.get_str(&["name"]), Some("test"));
+}
+
+#[test]
+fn test_vdf_get_obj() {
+ let mut inner = Obj::new();
+ inner.insert(Cow::Borrowed("k"), Value::I32(1));
+ let mut outer = Obj::new();
+ outer.insert(Cow::Borrowed("inner"), Value::Obj(inner));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(outer));
+
+ assert!(vdf.get_obj(&["inner"]).is_some());
+}
+
+#[test]
+fn test_vdf_get_i32() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("num"), Value::I32(42));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(obj));
+
+ assert_eq!(vdf.get_i32(&["num"]), Some(42));
+}
+
+#[test]
+fn test_vdf_get_u64() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("big"), Value::U64(12345678901234));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(obj));
+
+ assert_eq!(vdf.get_u64(&["big"]), Some(12345678901234));
+}
+
+#[test]
+fn test_vdf_get_float() {
+ let mut obj = Obj::new();
+ obj.insert(Cow::Borrowed("f"), Value::Float(2.5));
+ let vdf = Vdf::new(Cow::Borrowed("root"), Value::Obj(obj));
+
+ assert_eq!(vdf.get_float(&["f"]), Some(2.5));
+}
+
+// ============================================================================
+// IntoIterator tests
+// ============================================================================
+
+#[test]
+fn test_obj_into_iter_owned() {
+ let mut obj = Obj::new();
+ obj.insert("key1", "value1".into());
+ obj.insert("key2", "value2".into());
+
+ let mut count = 0;
+ for (key, value) in obj {
+ count += 1;
+ assert!(key == "key1" || key == "key2");
+ assert!(value.is_str());
+ }
+ assert_eq!(count, 2);
+}
+
+#[test]
+fn test_obj_into_iter_ref() {
+ let mut obj = Obj::new();
+ obj.insert("key1", "value1".into());
+ obj.insert("key2", "value2".into());
+
+ let mut count = 0;
+ for (key, value) in &obj {
+ count += 1;
+ assert!(key.as_ref() == "key1" || key.as_ref() == "key2");
+ assert!(value.is_str());
+ }
+ assert_eq!(count, 2);
+ // obj is still usable after iterating by reference
+ assert_eq!(obj.len(), 2);
+}
+
+// ============================================================================
+// Index trait tests
+// ============================================================================
+
+#[test]
+fn test_value_index_existing_key() {
+ let mut obj = Obj::new();
+ obj.insert("name", "Alice".into());
+ let value = Value::Obj(obj);
+
+ assert_eq!(value["name"].as_str(), Some("Alice"));
+}
+
+#[test]
+#[should_panic(expected = "key not found in Value")]
+fn test_value_index_missing_key() {
+ let mut obj = Obj::new();
+ obj.insert("name", "Alice".into());
+ let value = Value::Obj(obj);
+
+ // This should panic because the key doesn't exist
+ let _ = &value["nonexistent"];
+}
+
+#[test]
+#[should_panic(expected = "key not found in Value")]
+fn test_value_index_on_non_obj() {
+ let value = Value::Str("not an object".into());
+
+ // This should panic because indexing a non-object fails
+ let _ = &value["any"];
+}
+
+#[test]
+fn test_obj_index_existing_key() {
+ let mut obj = Obj::new();
+ obj.insert("count", 42i32.into());
+
+ assert_eq!(obj["count"].as_i32(), Some(42));
+}
+
+#[test]
+#[should_panic(expected = "key not found in Obj")]
+fn test_obj_index_missing_key() {
+ let obj = Obj::new();
+
+ // This should panic because the key doesn't exist
+ let _ = &obj["nonexistent"];
+}
+
+#[test]
+fn test_index_chained_access() {
+ let mut inner = Obj::new();
+ inner.insert("value", "found".into());
+
+ let mut outer = Obj::new();
+ outer.insert("inner", Value::Obj(inner));
+
+ let value = Value::Obj(outer);
+
+ // Chained indexing works when keys exist
+ assert_eq!(value["inner"]["value"].as_str(), Some("found"));
+}
+
+#[test]
+#[should_panic(expected = "key not found in Value")]
+fn test_index_chained_access_missing_key() {
+ let mut inner = Obj::new();
+ inner.insert("value", "found".into());
+
+ let mut outer = Obj::new();
+ outer.insert("inner", Value::Obj(inner));
+
+ let value = Value::Obj(outer);
+
+ // This should panic because "missing" doesn't exist
+ let _ = &value["missing"]["anything"];
+}
+
+// ============================================================================
+// Pretty-print Display tests
+// ============================================================================
+
+#[test]
+fn test_value_pretty_str() {
+ let value = Value::Str(Cow::Borrowed("hello"));
+ assert_eq!(format!("{:#}", value), "\"hello\"");
+}
+
+#[test]
+fn test_value_pretty_str_with_escapes() {
+ let value = Value::Str(Cow::Borrowed("line1\nline2\ttab\\backslash\"quote"));
+ assert_eq!(
+ format!("{:#}", value),
+ "\"line1\\nline2\\ttab\\\\backslash\\\"quote\""
+ );
+}
+
+#[test]
+fn test_value_pretty_i32() {
+ let value = Value::I32(42);
+ assert_eq!(format!("{:#}", value), "\"42\"");
+}
+
+#[test]
+fn test_value_pretty_u64() {
+ let value = Value::U64(123456789);
+ assert_eq!(format!("{:#}", value), "\"123456789\"");
+}
+
+#[test]
+fn test_value_pretty_float() {
+ let value = Value::Float(1.5);
+ assert_eq!(format!("{:#}", value), "\"1.5\"");
+}
+
+#[test]
+fn test_value_pretty_pointer() {
+ let value = Value::Pointer(0x12345678);
+ assert_eq!(format!("{:#}", value), "\"0x12345678\"");
+}
+
+#[test]
+fn test_value_pretty_color() {
+ let value = Value::Color([255, 0, 128, 64]);
+ assert_eq!(format!("{:#}", value), "\"255 0 128 64\"");
+}
+
+#[test]
+fn test_obj_pretty_simple() {
+ let mut obj = Obj::new();
+ obj.insert("key", "value".into());
+ let output = format!("{:#}", obj);
+ // Should contain proper VDF structure
+ assert!(output.starts_with("{\n"));
+ assert!(output.ends_with("}"));
+ assert!(output.contains("\"key\""));
+ assert!(output.contains("\"value\""));
+}
+
+#[test]
+fn test_obj_pretty_nested() {
+ let mut inner = Obj::new();
+ inner.insert("inner_key", "inner_value".into());
+ let mut outer = Obj::new();
+ outer.insert("nested", Value::Obj(inner));
+
+ let output = format!("{:#}", outer);
+ // Should have nested structure with proper indentation
+ assert!(output.contains("\"nested\""));
+ assert!(output.contains("\"inner_key\""));
+ assert!(output.contains("\"inner_value\""));
+ // Check for proper indentation (tabs)
+ assert!(output.contains("\t\"nested\""));
+ assert!(output.contains("\t\t\"inner_key\""));
+}
+
+#[test]
+fn test_vdf_pretty() {
+ let mut obj = Obj::new();
+ obj.insert("key", "value".into());
+ let vdf = Vdf::new("root", Value::Obj(obj));
+
+ let output = format!("{:#}", vdf);
+ // Should start with quoted key followed by newline and brace
+ assert!(output.starts_with("\"root\"\n{"));
+ assert!(output.contains("\"key\"\t\"value\""));
+}
+
+#[test]
+fn test_vdf_pretty_complex() {
+ let mut inner = Obj::new();
+ inner.insert("value", "found".into());
+ inner.insert("number", Value::I32(42));
+
+ let mut outer = Obj::new();
+ outer.insert("nested", Value::Obj(inner));
+ outer.insert("simple", "string".into());
+
+ let vdf = Vdf::new("TestVdf", Value::Obj(outer));
+
+ let output = format!("{:#}", vdf);
+ // Verify structure
+ assert!(output.starts_with("\"TestVdf\"\n{"), "output: {}", output);
+ // The closing brace may or may not have a trailing newline depending on
+ // whether the last element is a nested object, so just check it ends with }
+ assert!(output.trim_end().ends_with("}"), "output: {}", output);
+ // Verify content is present (order may vary due to HashMap)
+ assert!(output.contains("\"nested\""), "output: {}", output);
+ assert!(
+ output.contains("\"simple\"\t\"string\""),
+ "output: {}",
+ output
+ );
+ assert!(
+ output.contains("\"value\"\t\"found\""),
+ "output: {}",
+ output
+ );
+ assert!(output.contains("\"number\"\t\"42\""), "output: {}", output);
+}
+
+#[test]
+fn test_value_display_produces_vdf_format() {
+ // Display always produces valid VDF text format
+ let value = Value::Str(Cow::Borrowed("test"));
+ assert_eq!(format!("{}", value), "\"test\"");
+ // Alternate format is the same
+ assert_eq!(format!("{:#}", value), "\"test\"");
+}
+
+#[test]
+fn test_obj_display_produces_vdf_format() {
+ let mut obj = Obj::new();
+ obj.insert("k", "v".into());
+
+ // Display produces VDF format with newlines and proper quoting
+ let output = format!("{}", obj);
+ assert!(output.contains('\n'));
+ assert!(output.contains("\"k\""));
+ assert!(output.contains("\"v\""));
+ // Alternate format is the same
+ assert_eq!(format!("{:#}", obj), output);
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/types.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/types.rs
new file mode 100644
index 0000000..2b3a77f
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/value/types.rs
@@ -0,0 +1,346 @@
+//! Type definitions for VDF values.
+
+use alloc::borrow::Cow;
+use indexmap::IndexMap;
+
+use super::hasher::DefaultHashBuilder;
+
+/// A key in VDF - zero-copy when possible
+pub(crate) type Key<'text> = Cow<'text, str>;
+
+/// VDF Value - can be a string, number, object, or other types
+#[derive(Clone, Debug, PartialEq)]
+pub enum Value<'text> {
+ /// A string value (text format and WideString from binary)
+ Str(Cow<'text, str>),
+ /// An object containing nested key-value pairs
+ Obj(Obj<'text>),
+ /// A 32-bit signed integer (binary Int32 type)
+ I32(i32),
+ /// A 64-bit unsigned integer (binary UInt64 type)
+ U64(u64),
+ /// A 32-bit float (binary Float type)
+ Float(f32),
+ /// A pointer value (binary Ptr type, stored as u32)
+ Pointer(u32),
+ /// A color value (binary Color type, RGBA)
+ Color([u8; 4]),
+}
+
+impl<'text> Value<'text> {
+ /// Returns `true` if this value is a string.
+ pub fn is_str(&self) -> bool {
+ matches!(self, Value::Str(_))
+ }
+
+ /// Returns `true` if this value is an object.
+ pub fn is_obj(&self) -> bool {
+ matches!(self, Value::Obj(_))
+ }
+
+ /// Returns `true` if this value is an i32.
+ pub fn is_i32(&self) -> bool {
+ matches!(self, Value::I32(_))
+ }
+
+ /// Returns `true` if this value is a u64.
+ pub fn is_u64(&self) -> bool {
+ matches!(self, Value::U64(_))
+ }
+
+ /// Returns `true` if this value is a float.
+ pub fn is_float(&self) -> bool {
+ matches!(self, Value::Float(_))
+ }
+
+ /// Returns `true` if this value is a pointer.
+ pub fn is_pointer(&self) -> bool {
+ matches!(self, Value::Pointer(_))
+ }
+
+ /// Returns `true` if this value is a color.
+ pub fn is_color(&self) -> bool {
+ matches!(self, Value::Color(_))
+ }
+
+ /// Returns a reference to the string value if this is a string.
+ pub fn as_str(&self) -> Option<&str> {
+ match self {
+ Value::Str(s) => Some(s.as_ref()),
+ _ => None,
+ }
+ }
+
+ /// Returns a reference to the object if this is an object.
+ pub fn as_obj(&self) -> Option<&Obj<'text>> {
+ match self {
+ Value::Obj(obj) => Some(obj),
+ _ => None,
+ }
+ }
+
+ /// Returns a mutable reference to the object if this is an object.
+ pub fn as_obj_mut(&mut self) -> Option<&mut Obj<'text>> {
+ match self {
+ Value::Obj(obj) => Some(obj),
+ _ => None,
+ }
+ }
+
+ /// Returns the i32 value if this is an i32.
+ pub fn as_i32(&self) -> Option<i32> {
+ match self {
+ Value::I32(n) => Some(*n),
+ _ => None,
+ }
+ }
+
+ /// Returns the u64 value if this is a u64.
+ pub fn as_u64(&self) -> Option<u64> {
+ match self {
+ Value::U64(n) => Some(*n),
+ _ => None,
+ }
+ }
+
+ /// Returns the float value if this is a float.
+ pub fn as_float(&self) -> Option<f32> {
+ match self {
+ Value::Float(n) => Some(*n),
+ _ => None,
+ }
+ }
+
+ /// Returns the pointer value if this is a pointer.
+ pub fn as_pointer(&self) -> Option<u32> {
+ match self {
+ Value::Pointer(n) => Some(*n),
+ _ => None,
+ }
+ }
+
+ /// Returns the color value if this is a color.
+ pub fn as_color(&self) -> Option<[u8; 4]> {
+ match self {
+ Value::Color(c) => Some(*c),
+ _ => None,
+ }
+ }
+
+ /// Returns a reference to a nested value by key.
+ ///
+ /// Shorthand for `self.as_obj()?.get(key)`.
+ pub fn get(&self, key: &str) -> Option<&Value<'text>> {
+ self.as_obj()?.get(key)
+ }
+
+ /// Traverse nested objects by path.
+ ///
+ /// Returns `None` if any segment doesn't exist or isn't an object.
+ pub fn get_path(&self, path: &[&str]) -> Option<&Value<'text>> {
+ let mut current = self;
+ for key in path {
+ current = current.get(key)?;
+ }
+ Some(current)
+ }
+
+ /// Get a string at the given path.
+ pub fn get_str(&self, path: &[&str]) -> Option<&str> {
+ self.get_path(path)?.as_str()
+ }
+
+ /// Get an object at the given path.
+ pub fn get_obj(&self, path: &[&str]) -> Option<&Obj<'text>> {
+ self.get_path(path)?.as_obj()
+ }
+
+ /// Get an i32 at the given path.
+ pub fn get_i32(&self, path: &[&str]) -> Option<i32> {
+ self.get_path(path)?.as_i32()
+ }
+
+ /// Get a u64 at the given path.
+ pub fn get_u64(&self, path: &[&str]) -> Option<u64> {
+ self.get_path(path)?.as_u64()
+ }
+
+ /// Get a float at the given path.
+ pub fn get_float(&self, path: &[&str]) -> Option<f32> {
+ self.get_path(path)?.as_float()
+ }
+}
+
+/// Object - map from keys to values
+///
+/// Uses `IndexMap` for O(1) lookup while preserving insertion order.
+/// Binary VDF doesn't have duplicate keys, and for text VDF we use
+/// "last value wins" semantics.
+#[derive(Clone, Debug, PartialEq)]
+pub struct Obj<'text> {
+ pub(crate) inner: IndexMap<Key<'text>, Value<'text>, DefaultHashBuilder>,
+}
+
+impl<'text> Obj<'text> {
+ /// Creates a new empty VDF object.
+ pub fn new() -> Self {
+ Self {
+ inner: IndexMap::with_hasher(DefaultHashBuilder::default()),
+ }
+ }
+
+ /// Returns the number of key-value pairs in the object.
+ pub fn len(&self) -> usize {
+ self.inner.len()
+ }
+
+ /// Returns `true` if the object contains no key-value pairs.
+ pub fn is_empty(&self) -> bool {
+ self.inner.is_empty()
+ }
+
+ /// Returns a reference to the value corresponding to the key.
+ pub fn get(&self, key: &str) -> Option<&Value<'text>> {
+ self.inner.get(key)
+ }
+
+ /// Returns an iterator over the key-value pairs.
+ pub fn iter(&self) -> impl Iterator<Item = (&Key<'text>, &Value<'text>)> {
+ self.inner.iter()
+ }
+
+ /// Returns an iterator over the keys.
+ pub fn keys(&self) -> impl Iterator<Item = &str> {
+ self.inner.keys().map(|k| k.as_ref())
+ }
+
+ /// Returns an iterator over the values.
+ pub fn values(&self) -> impl Iterator<Item = &Value<'text>> {
+ self.inner.values()
+ }
+
+ /// Returns `true` if the object contains the given key.
+ pub fn contains_key(&self, key: &str) -> bool {
+ self.inner.contains_key(key)
+ }
+
+ /// Returns a mutable reference to the value corresponding to the key.
+ pub fn get_mut(&mut self, key: &str) -> Option<&mut Value<'text>> {
+ self.inner.get_mut(key)
+ }
+
+ /// Inserts a key-value pair into the object.
+ ///
+ /// Returns the previous value if one existed for this key.
+ pub fn insert(
+ &mut self,
+ key: impl Into<Key<'text>>,
+ value: Value<'text>,
+ ) -> Option<Value<'text>> {
+ self.inner.insert(key.into(), value)
+ }
+
+ /// Removes a key from the object, preserving insertion order.
+ ///
+ /// This is O(n) as it shifts subsequent elements. Use [`swap_remove`](Self::swap_remove)
+ /// for O(1) removal when order doesn't matter.
+ ///
+ /// Returns the value if the key was present.
+ pub fn remove(&mut self, key: &str) -> Option<Value<'text>> {
+ self.inner.shift_remove(key)
+ }
+
+ /// Removes a key from the object by swapping with the last element.
+ ///
+ /// This is O(1) but does not preserve insertion order.
+ /// Use [`remove`](Self::remove) if order preservation is needed.
+ ///
+ /// Returns the value if the key was present.
+ pub fn swap_remove(&mut self, key: &str) -> Option<Value<'text>> {
+ self.inner.swap_remove(key)
+ }
+}
+
+/// Top-level VDF document
+///
+/// A VDF document is essentially a single key-value pair at the root level.
+#[derive(Clone, Debug, PartialEq)]
+pub struct Vdf<'text> {
+ key: Key<'text>,
+ value: Value<'text>,
+}
+
+impl<'text> Vdf<'text> {
+ /// Creates a new VDF document.
+ pub fn new(key: impl Into<Key<'text>>, value: Value<'text>) -> Self {
+ Self {
+ key: key.into(),
+ value,
+ }
+ }
+
+ /// Returns the root key.
+ pub fn key(&self) -> &str {
+ &self.key
+ }
+
+ /// Returns a reference to the root value.
+ pub fn value(&self) -> &Value<'text> {
+ &self.value
+ }
+
+ /// Returns a mutable reference to the root value.
+ pub fn value_mut(&mut self) -> &mut Value<'text> {
+ &mut self.value
+ }
+
+ /// Consumes the Vdf and returns its parts.
+ pub fn into_parts(self) -> (Cow<'text, str>, Value<'text>) {
+ (self.key, self.value)
+ }
+
+ /// Returns `true` if the root value is an object.
+ pub fn is_obj(&self) -> bool {
+ self.value.is_obj()
+ }
+
+ /// Returns a reference to the root object if it is one.
+ pub fn as_obj(&self) -> Option<&Obj<'text>> {
+ self.value.as_obj()
+ }
+
+ /// Returns a reference to a nested value by key.
+ pub fn get(&self, key: &str) -> Option<&Value<'text>> {
+ self.value.get(key)
+ }
+
+ /// Traverse nested objects by path from the root value.
+ pub fn get_path(&self, path: &[&str]) -> Option<&Value<'text>> {
+ self.value.get_path(path)
+ }
+
+ /// Get a string at the given path.
+ pub fn get_str(&self, path: &[&str]) -> Option<&str> {
+ self.value.get_str(path)
+ }
+
+ /// Get an object at the given path.
+ pub fn get_obj(&self, path: &[&str]) -> Option<&Obj<'text>> {
+ self.value.get_obj(path)
+ }
+
+ /// Get an i32 at the given path.
+ pub fn get_i32(&self, path: &[&str]) -> Option<i32> {
+ self.value.get_i32(path)
+ }
+
+ /// Get a u64 at the given path.
+ pub fn get_u64(&self, path: &[&str]) -> Option<u64> {
+ self.value.get_u64(path)
+ }
+
+ /// Get a float at the given path.
+ pub fn get_float(&self, path: &[&str]) -> Option<f32> {
+ self.value.get_float(path)
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/appinfo_10.vdf b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/appinfo_10.vdf
new file mode 100644
index 0000000..39e5137
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/appinfo_10.vdf
Binary files differ
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/localconfig.vdf b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/localconfig.vdf
new file mode 100755
index 0000000..c239422
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/localconfig.vdf
@@ -0,0 +1,100 @@
+"UserLocalConfigStore"
+{
+ "Broadcast"
+ {
+ "Permissions" "1"
+ }
+ "streaming_v2"
+ {
+ "EnableStreaming" "1"
+ "UnlockedH264Feature" "1"
+ }
+ "friends"
+ {
+ "10000001"
+ {
+ "NameHistory"
+ {
+ "0" "TestUser01"
+ }
+ "avatar" "0000000000000000000000000000000000000000"
+ "name" "TestUser01"
+ }
+ "PersonaName" "TestPersona"
+ "communitypreferences" "1899cc9284062000280330013800"
+ "contentdescriptorpreferences" ""
+ "10000002"
+ {
+ "name" "TestUser02"
+ "NameHistory"
+ {
+ "0" "TestUser02"
+ }
+ "avatar" "0000000000000000000000000000000000000000"
+ }
+ "10000003"
+ {
+ "name" "TestUser03"
+ "NameHistory"
+ {
+ "0" "TestUser03"
+ }
+ "avatar" "0000000000000000000000000000000000000000"
+ }
+ "Notifications_ShowIngame" "1"
+ "Notifications_ShowOnline" "0"
+ "Notifications_ShowMessage" "1"
+ "Notifications_EventsAndAnnouncements" "1"
+ "Sounds_PlayIngame" "0"
+ "Sounds_PlayOnline" "0"
+ "Sounds_PlayMessage" "1"
+ "Sounds_EventsAndAnnouncements" "0"
+ "ChatFlashMode" "0"
+ "DoNotDisturb" "0"
+ "SignIntoFriends" "0"
+ }
+ "ParentalSettings"
+ {
+ "settings" "09edc39b07000000004800"
+ "Signature" "30c818bcbcaad3ccf7597dadeabc2d03f71ce5070700dac271143a8cc9eda2fb32f7317a12e6e0adcda622d4dfc9d46f650ead2a5091c66565f975478091bcf7a510a858d988e41e5dec2fe0fd6365ea7a3be2bb43ed364896e521193cd41379260cc4be9b35f4df382990a545c3c55af8f70aa1f7740ecc8c2880dd9e9b572482690fae060f798237bfb1132bec9307ed8bedeb52de09b1b1e41e19f476811919bb301a99d1fa8c05a91c7a160ee3454064225978ef12fac54640ecb7266b14cb3b02fa79208b644a301b2d0aca1c654d1ce3335d42733e38dbab190dd16eb87ab8bc8d75c0b0efc128e8b18032639c5813895f7110864936e67f7da954562b"
+ }
+ "SharedAuth"
+ {
+ "id" "1234567890"
+ "AuthData" "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ "Offline"
+ {
+ "Ticket" "00000000000000000"
+ "Signature" "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ "EncryptedTicket" "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ "EncryptedSignature" "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ }
+ "apptickets"
+ {
+ "7" "3200000004000000edc39b0701001001070000005fff515d2301a8c0000000005aa45b69da537769010000000000000000003e6f542da8eb4b0d9dd8290178b4c5a2d0a20b857f60ad18ea51a922c53c7017a0cc352564ebe078da357fdca604dd748439254abd1b23766c424405fa545ae90c8bb944506316543c49a39133b4eff547b53d420969fb2ff892f5299d3f45d0c7e5723adb3ec12daf2532032619db48cb9ee82ba7420747634f3c5a160bb222"
+ "1282730" "3200000004000000edc39b0701001001aa9213005fff515d2301a8c0000000001a56cb679a05e767010011d20600000000002b6b8cb43dc4fce94043ca172a0cc9cd1f4a6acb14a1c268970cfdf4164b5d156e1cae9631cb326ee4cc30d033f358f6ff680ff85437008059f4bda89fb035cf178febe8b3668a4f668fda95df4c85a5b9598493c0361bdd054df4912b5f88d159d05a117e7534252676a12949aabe9bee25e5c2078861d97a71e8939458d0c8"
+ }
+ "AppInfoChangeNumber" "12345678"
+ "Software"
+ {
+ "Valve"
+ {
+ "Steam"
+ {
+ "BetaPassword" ""
+ "running"
+ {
+ "id" "0"
+ "Steam2Tickets"
+ {
+ }
+ }
+ }
+ }
+ }
+ "WebStorage"
+ {
+ "keyboard_trackpding_typing_trigger_as_click" "true"
+ }
+}
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/packageinfo.vdf b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/packageinfo.vdf
new file mode 100755
index 0000000..67de2ae
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/fixtures/packageinfo.vdf
Binary files differ
diff --git a/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/real_files.rs b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/real_files.rs
new file mode 100644
index 0000000..6f36aec
--- /dev/null
+++ b/libs/steam_appinfo_ffi/vendor/steam-vdf-parser/tests/real_files.rs
@@ -0,0 +1,123 @@
+//! Integration tests against real Steam VDF files.
+
+use std::path::Path;
+use steam_vdf_parser::{parse_binary, parse_packageinfo, parse_text};
+
+#[test]
+fn test_parse_real_localconfig_text() {
+ let path = Path::new("tests/fixtures/localconfig.vdf");
+ let content = std::fs::read_to_string(path).expect("Failed to read localconfig.vdf");
+
+ let result = parse_text(&content);
+ assert!(
+ result.is_ok(),
+ "Failed to parse localconfig.vdf: {:?}",
+ result.err()
+ );
+
+ let vdf = result.unwrap();
+ assert_eq!(vdf.key(), "UserLocalConfigStore");
+
+ let obj = vdf.as_obj().expect("Root should be an object");
+ assert!(!obj.is_empty(), "Root should have keys");
+
+ // Check for known keys that should exist in localconfig
+ assert!(obj.get("Broadcast").is_some());
+ assert!(obj.get("friends").is_some());
+}
+
+#[test]
+fn test_parse_real_appinfo_binary() {
+ let path = Path::new("tests/fixtures/appinfo_10.vdf");
+ let data = std::fs::read(path).expect("Failed to read appinfo_10.vdf");
+
+ let result = parse_binary(&data);
+ assert!(
+ result.is_ok(),
+ "Failed to parse appinfo_10.vdf: {:?}",
+ result.err()
+ );
+
+ let vdf = result.unwrap();
+ assert!(vdf.key().starts_with("appinfo_universe_"));
+
+ let obj = vdf.as_obj().expect("Root should be an object");
+ assert!(!obj.is_empty(), "Root should have keys");
+
+ // appinfo_10.vdf should contain 10 apps (numeric keys as strings)
+ assert_eq!(
+ obj.len(),
+ 10,
+ "Should have exactly 10 apps in appinfo_10.vdf"
+ );
+}
+
+#[test]
+fn test_parse_real_packageinfo_binary() {
+ let path = Path::new("tests/fixtures/packageinfo.vdf");
+ let data = std::fs::read(path).expect("Failed to read packageinfo.vdf");
+
+ let result = parse_packageinfo(&data);
+ assert!(
+ result.is_ok(),
+ "Failed to parse packageinfo.vdf: {:?}",
+ result.err()
+ );
+
+ let vdf = result.unwrap();
+ assert!(vdf.key().starts_with("packageinfo_universe_"));
+
+ let obj = vdf.as_obj().expect("Root should be an object");
+ assert!(!obj.is_empty(), "Root should have keys");
+
+ // packageinfo.vdf should contain packages
+ // Check that we have at least some entries
+ assert!(
+ obj.len() > 50,
+ "Should have many packages in packageinfo.vdf"
+ );
+
+ // Check that package 0 exists and has the expected metadata
+ let pkg0 = obj.get("0").expect("Package 0 should exist");
+ let pkg0_obj = pkg0.as_obj().expect("Package 0 should be an object");
+
+ // Check metadata fields
+ assert_eq!(
+ pkg0_obj.get("packageid").and_then(|v| v.as_i32()),
+ Some(0),
+ "Package 0 should have packageid = 0"
+ );
+
+ assert!(
+ pkg0_obj
+ .get("change_number")
+ .and_then(|v| v.as_u64())
+ .is_some(),
+ "Package 0 should have change_number"
+ );
+
+ assert!(
+ pkg0_obj.get("sha1").and_then(|v| v.as_str()).is_some(),
+ "Package 0 should have sha1 hash"
+ );
+
+ // Check that the VDF data is present under the "0" key
+ let vdf_data = pkg0_obj
+ .get("0")
+ .expect("Package 0 should have VDF data under '0' key");
+ let vdf_obj = vdf_data.as_obj().expect("VDF data should be an object");
+
+ // Check for known fields in the VDF data
+ assert!(
+ vdf_obj.get("packageid").is_some(),
+ "VDF data should contain packageid"
+ );
+ assert!(
+ vdf_obj.get("billingtype").is_some(),
+ "VDF data should contain billingtype"
+ );
+ assert!(
+ vdf_obj.get("licensetype").is_some(),
+ "VDF data should contain licensetype"
+ );
+}
diff --git a/libs/uibase/src/CMakeLists.txt b/libs/uibase/src/CMakeLists.txt
index 5660289..c823290 100644
--- a/libs/uibase/src/CMakeLists.txt
+++ b/libs/uibase/src/CMakeLists.txt
@@ -188,6 +188,10 @@ target_link_libraries(uibase
PRIVATE spdlog::spdlog_header_only Qt6::Qml Qt6::Quick)
if(NOT WIN32)
+ target_link_libraries(uibase PRIVATE mo2::steam_appinfo_ffi)
+endif()
+
+if(NOT WIN32)
# Native C++ ports of game detection, Steam/Proton detection, SLR management,
# icon extraction, and prefix symlinks (formerly in Rust nak_ffi).
# Use BUILD_INTERFACE to avoid "prefixed in the source directory" CMake error.
@@ -200,6 +204,7 @@ if(NOT WIN32)
${CMAKE_SOURCE_DIR}/src/src/iconextractor.cpp
${CMAKE_SOURCE_DIR}/src/src/prefixsymlinks.cpp
${CMAKE_SOURCE_DIR}/src/src/slrmanager.cpp
+ ${CMAKE_SOURCE_DIR}/src/src/steamappinfo.cpp
)
endif()