Skip to content

Commit

Permalink
Support appinfo.vdf V29
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Matoking committed Sep 16, 2024
1 parent f51826f commit 0fa5954
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `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.
- `protontricks` will now launch GUI if no arguments were provided

### Fixed
- Fix crash when parsing appinfo.vdf V29 in new Steam client version

## [1.11.1] - 2024-02-20
### Fixed
- Fix Protontricks crash when custom Proton has an invalid or empty `compatibilitytool.vdf` manifest
Expand Down
56 changes: 56 additions & 0 deletions src/protontricks/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ def find_legacy_steam_runtime_path(steam_root):

APPINFO_STRUCT_HEADER = "<4sL"
APPINFO_V28_STRUCT_SECTION = "<LLLLQ20sL20s"
APPINFO_V29_STRUCT_SECTION = "<LLLLQ20sL20s"


def iter_appinfo_sections(path):
Expand Down Expand Up @@ -518,6 +519,59 @@ def _iter_v28_appinfo(data, start):
if i == len(data) - 4:
return

def _iter_v29_appinfo(data, start):
"""
Parse and iterate appinfo.vdf version 29.
"""
i = start

# The header contains the offset to the key table
key_table_offset = struct.unpack("<q", data[i:i+8])[0]
key_table = []

key_count = struct.unpack(
"<i", data[key_table_offset:key_table_offset+4]
)[0]

table_i = key_table_offset + 4
for _ in range(0, key_count):
key = bytearray()
while True:
key.append(data[table_i])
table_i += 1

if key[-1] == 0:
key_table.append(
key[0:-1].decode("utf-8", errors="replace")
)
break

i += 8

section_size = struct.calcsize(APPINFO_V29_STRUCT_SECTION)
while True:
# We don't need any of the fields besides 'entry_size',
# which is used to determine the length of the variable-length VDF
# field.
# Still, here they are for posterity's sake.
(appid, entry_size, infostate, last_updated, access_token,
sha_hash, change_number, vdf_sha_hash) = struct.unpack(
APPINFO_V29_STRUCT_SECTION, data[i:i+section_size])
vdf_section_size = entry_size - (section_size - 8)

i += section_size

vdf_d = vdf.binary_loads(
data[i:i+vdf_section_size], key_table=key_table
)
vdf_d = lower_dict(vdf_d)
yield vdf_d

i += vdf_section_size

if i == key_table_offset - 4:
return

logger.debug("Loading appinfo.vdf in %s", path)

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

if magic == b'(DV\x07':
yield from _iter_v28_appinfo(data, i)
elif magic == b')DV\x07':
yield from _iter_v29_appinfo(data, i)
else:
raise SyntaxError(
"Invalid file magic number. The appinfo.vdf version might not be "
Expand Down

0 comments on commit 0fa5954

Please sign in to comment.