Skip to content

Commit 052a6ae

Browse files
sonic2kkMatoking
authored andcommitted
Add deserialization support for appinfo.vdf V29
appinfo.vdf V29 was introduced in Steam beta. This new version introduces a space-saving optimization: instead of encoding each key name in the binary VDF segment directly, an int64 identifier is instead used for each key, with a table at the end of the 'appinfo.vdf' file providing the mapping to actual key names. Also see SteamDatabase/SteamAppInfo@56b1fec Fixes #462 Co-authored-by: Eamonn Rea <[email protected]>
1 parent 26166e0 commit 052a6ae

File tree

1 file changed

+45
-6
lines changed

1 file changed

+45
-6
lines changed

steam/utils/appcache.py

+45-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
>>> header, apps = parse_appinfo(open('/d/Steam/appcache/appinfo.vdf', 'rb'))
99
>>> header
10-
{'magic': b"(DV\\x07", 'universe': 1}
10+
{'magic': b")DV\\x07", 'universe': 1}
1111
>>> next(apps)
1212
{'appid': 5,
1313
'size': 79,
@@ -43,6 +43,7 @@
4343

4444
uint32 = struct.Struct('<I')
4545
uint64 = struct.Struct('<Q')
46+
int64 = struct.Struct('<q')
4647

4748
def parse_appinfo(fp):
4849
"""Parse appinfo.vdf from the Steam appcache folder
@@ -53,8 +54,9 @@ def parse_appinfo(fp):
5354
:return: (header, apps iterator)
5455
"""
5556
# format:
56-
# uint32 - MAGIC: "'DV\x07" or "(DV\x07"
57+
# uint32 - MAGIC: "'DV\x07" or "(DV\x07" or b")DV\x07"
5758
# uint32 - UNIVERSE: 1
59+
# int64 - OFFSET TO KEY TABLE (added in ")DV\x07")
5860
# ---- repeated app sections ----
5961
# uint32 - AppID
6062
# uint32 - size
@@ -63,17 +65,52 @@ def parse_appinfo(fp):
6365
# uint64 - accessToken
6466
# 20bytes - SHA1
6567
# uint32 - changeNumber
66-
# 20bytes - binary_vdf SHA1 (added in "(DV\x07"
68+
# 20bytes - binary_vdf SHA1 (added in "(DV\x07")
6769
# variable - binary_vdf
6870
# ---- end of section ---------
6971
# uint32 - EOF: 0
72+
#
73+
# ---- key table ----
74+
# uint32 - Count of keys
75+
# char[] - Null-terminated strings corresponding to field names
7076

7177
magic = fp.read(4)
72-
if magic not in (b"'DV\x07", b"(DV\x07"):
78+
if magic not in (b"'DV\x07", b"(DV\x07", b")DV\x07"):
7379
raise SyntaxError("Invalid magic, got %s" % repr(magic))
7480

7581
universe = uint32.unpack(fp.read(4))[0]
7682

83+
key_table = None
84+
85+
appinfo_version = magic[0]
86+
if appinfo_version >= 41: # b')'
87+
# appinfo.vdf V29 and newer store list of keys in separate table at the
88+
# end of the file to reduce size. Retrieve it and pass it to the VDF
89+
# parser later.
90+
key_table = []
91+
92+
key_table_offset = struct.unpack('q', fp.read(8))[0]
93+
offset = fp.tell()
94+
fp.seek(key_table_offset)
95+
key_count = uint32.unpack(fp.read(4))[0]
96+
97+
# Read all null-terminated strings into a list
98+
for _ in range(0, key_count):
99+
field_name = bytearray()
100+
while True:
101+
field_name += fp.read(1)
102+
103+
if field_name[-1] == 0:
104+
field_name = field_name[0:-1]
105+
field_name = field_name.decode('utf-8', 'replace')
106+
107+
key_table.append(field_name)
108+
break
109+
110+
# Rewind to the beginning of the file after the header:
111+
# we can now parse the rest of the file.
112+
fp.seek(offset)
113+
77114
def apps_iter():
78115
while True:
79116
appid = uint32.unpack(fp.read(4))[0]
@@ -91,10 +128,12 @@ def apps_iter():
91128
'change_number': uint32.unpack(fp.read(4))[0],
92129
}
93130

94-
if magic == b"(DV\x07":
131+
if magic != b"'DV\x07":
95132
app['data_sha1'] = fp.read(20)
96133

97-
app['data'] = binary_load(fp)
134+
# 'key_table' will be None for older 'appinfo.vdf' files which
135+
# use self-contained binary VDFs.
136+
app['data'] = binary_load(fp, key_table=key_table)
98137

99138
yield app
100139

0 commit comments

Comments
 (0)