diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-17 00:53:20 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-17 00:53:20 -0500 |
| commit | 06c396fd27f55929b40161e587cb9b596b549f3f (patch) | |
| tree | 40fdb465d2fe6e7d4487bbd237582868c8332459 /libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text | |
| parent | e583d6dda3cde9e8bccee313fe1ad69634593708 (diff) | |
Symlink saves, rank prefixes by Steam app type, ntsync safeguards
- Replace copy-in/copy-out save deployment with a direct symlink from the
prefix's __MO_Saves to the profile's saves/. Writes land in the profile
immediately; afterRun reverts the symlink + prefix INI so a vanilla
launch outside MO2 uses the default Saves dir.
- Parse Steam's binary appinfo.vdf (v41) via a new Rust FFI crate backed
by steam-vdf-parser. Rank detected games by common.type so real Games
beat Tools/Editors when multiple prefixes share a My Games folder name
(fixes Creation Kit shadowing Skyrim SE). Top-ranked candidate
overwrites stale symlinks from earlier runs.
- Switch bundled Steam Linux Runtime downloader from sniper (steamrt3)
to steamrt4 so Proton 11+ works out of the box. steamrt4 omits xrandr
so we inject it from the Debian x11-xserver-utils .deb into a
dedicated dir and prepend it to PATH via `env PATH=...` inside the
pressure-vessel container (PATH env doesn't propagate).
- Auto-kill stale wineboot/wineserver/pv-adverb processes still bound
to the prefix before starting a new wineboot -u; match by
/proc/<pid>/environ WINEPREFIX / STEAM_COMPAT_DATA_PATH since Wine
cmdlines are Windows-style. Same sweep runs in destroyPrefix so a
delete actually frees file handles.
- Blacklist Proton 11 (Steam-bundled) — its current Wine tree deadlocks
during wineboot -u on modern kernels via ntsync / futex_wait_multiple
races. Use "waitforexitandrun" (not "run") for prefix init so
use_sessions=1 Protons don't fork into a persistent session manager.
- Drop WINEDEBUG=-all during prefix init so wineboot progress is
visible. Suppress focus-stealing by setting WA_ShowWithoutActivating
on the PrefixSetupDialog and SLR download progress dialogs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text')
| -rw-r--r-- | libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/mod.rs | 7 | ||||
| -rw-r--r-- | libs/steam_appinfo_ffi/vendor/steam-vdf-parser/src/text/parser.rs | 408 |
2 files changed, 415 insertions, 0 deletions
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"); + } +} |
