Skip to content

Commit 0fa5954

Browse files
committed
Support appinfo.vdf V29
Steam beta introduced a new version of appinfo.vdf with a space-saving optimization. Field keys are stored in a separate table at the end of the file, with the actual VDF segments having to be parsed using the table to map the indices to actual field names. `vdf` library does not support serializing appinfo.vdf using this format, at least yet, so just use appinfo.vdf V28 in tests for the time being. This might need to be fixed in the future once appinfo.vdf V28 is phased out. Fixes #304
1 parent f51826f commit 0fa5954

File tree

2 files changed

+59
-0
lines changed

2 files changed

+59
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1313
- `protontricks -c` and `protontricks-launch` now use the current working directory instead of the game's installation directory. `--cwd-app` can be used to restore old behavior. Scripts can also `$STEAM_APP_PATH` environment variable to determine the game's installation directory; this has been supported (albeit undocumented) since 1.8.0.
1414
- `protontricks` will now launch GUI if no arguments were provided
1515

16+
### Fixed
17+
- Fix crash when parsing appinfo.vdf V29 in new Steam client version
18+
1619
## [1.11.1] - 2024-02-20
1720
### Fixed
1821
- Fix Protontricks crash when custom Proton has an invalid or empty `compatibilitytool.vdf` manifest

src/protontricks/steam.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ def find_legacy_steam_runtime_path(steam_root):
483483

484484
APPINFO_STRUCT_HEADER = "<4sL"
485485
APPINFO_V28_STRUCT_SECTION = "<LLLLQ20sL20s"
486+
APPINFO_V29_STRUCT_SECTION = "<LLLLQ20sL20s"
486487

487488

488489
def iter_appinfo_sections(path):
@@ -518,6 +519,59 @@ def _iter_v28_appinfo(data, start):
518519
if i == len(data) - 4:
519520
return
520521

522+
def _iter_v29_appinfo(data, start):
523+
"""
524+
Parse and iterate appinfo.vdf version 29.
525+
"""
526+
i = start
527+
528+
# The header contains the offset to the key table
529+
key_table_offset = struct.unpack("<q", data[i:i+8])[0]
530+
key_table = []
531+
532+
key_count = struct.unpack(
533+
"<i", data[key_table_offset:key_table_offset+4]
534+
)[0]
535+
536+
table_i = key_table_offset + 4
537+
for _ in range(0, key_count):
538+
key = bytearray()
539+
while True:
540+
key.append(data[table_i])
541+
table_i += 1
542+
543+
if key[-1] == 0:
544+
key_table.append(
545+
key[0:-1].decode("utf-8", errors="replace")
546+
)
547+
break
548+
549+
i += 8
550+
551+
section_size = struct.calcsize(APPINFO_V29_STRUCT_SECTION)
552+
while True:
553+
# We don't need any of the fields besides 'entry_size',
554+
# which is used to determine the length of the variable-length VDF
555+
# field.
556+
# Still, here they are for posterity's sake.
557+
(appid, entry_size, infostate, last_updated, access_token,
558+
sha_hash, change_number, vdf_sha_hash) = struct.unpack(
559+
APPINFO_V29_STRUCT_SECTION, data[i:i+section_size])
560+
vdf_section_size = entry_size - (section_size - 8)
561+
562+
i += section_size
563+
564+
vdf_d = vdf.binary_loads(
565+
data[i:i+vdf_section_size], key_table=key_table
566+
)
567+
vdf_d = lower_dict(vdf_d)
568+
yield vdf_d
569+
570+
i += vdf_section_size
571+
572+
if i == key_table_offset - 4:
573+
return
574+
521575
logger.debug("Loading appinfo.vdf in %s", path)
522576

523577
# appinfo.vdf is not actually a (binary) VDF file, but a binary file
@@ -539,6 +593,8 @@ def _iter_v28_appinfo(data, start):
539593

540594
if magic == b'(DV\x07':
541595
yield from _iter_v28_appinfo(data, i)
596+
elif magic == b')DV\x07':
597+
yield from _iter_v29_appinfo(data, i)
542598
else:
543599
raise SyntaxError(
544600
"Invalid file magic number. The appinfo.vdf version might not be "

0 commit comments

Comments
 (0)