From 4cc117b45bb6ddf76f8506804054df68c444ee2a Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:05:04 -0500 Subject: [PATCH] fix: read DepotDownloader's compressed protobuf depot.config --- .../usr/local/bin/droiddeck-steam-games | 56 +++++++++++++++++-- tools/tests/test_steam_games.py | 15 +++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games index edc349a90..573465caf 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games @@ -11,6 +11,7 @@ import time import unicodedata import urllib.parse import urllib.request +import zlib STATE = ".droiddeck-games.json" TRIED = ".droiddeck-import-tried.json" @@ -301,17 +302,62 @@ def installed_build(folder, appid, installdir): if not (folder / ".download_complete").is_file(): return None try: - ids = json.loads((folder / ".DepotDownloader/depot.config").read_text())["installedManifestIDs"] - depots = sorted((int(depot), int(gid)) for depot, gid in ids.items()) - except FileNotFoundError: + depots = config_depots((folder / ".DepotDownloader/depot.config").read_bytes()) + except OSError: + depots = None + if depots is None: depots = completed_depots(folder / ".DepotDownloader/completed") - except (OSError, ValueError, KeyError, TypeError, AttributeError): - return None if not depots or not all(0 < depot < 0x80000000 and 0 < gid < 0x7FFFFFFFFFFFFFFF for depot, gid in depots): return None return "".join(f' "{depot}"\n {{\n "manifest" "{gid}"\n }}\n' for depot, gid in depots) +def varint(data, i): + value = shift = 0 + while True: + byte = data[i] + i += 1 + value |= (byte & 0x7F) << shift + shift += 7 + if byte < 0x80: + return value, i + + +def proto_fields(data): + i = 0 + while i < len(data): + key, i = varint(data, i) + kind = key & 7 + if kind == 0: + value, i = varint(data, i) + elif kind == 2: + size, i = varint(data, i) + value, i = data[i:i + size], i + size + elif kind in (1, 5): + value, i = None, i + (8 if kind == 1 else 4) + else: + raise ValueError + yield key >> 3, kind, value + + +def config_depots(data): + try: + ids = json.loads(data.decode())["installedManifestIDs"] + return sorted((int(depot), int(gid)) for depot, gid in ids.items()) + except (ValueError, KeyError, TypeError, AttributeError): + pass + try: + depots = {} + for number, kind, entry in proto_fields(zlib.decompress(data, -15)): + if number == 1 and kind == 2: + fields = {n: v for n, k, v in proto_fields(entry) if k == 0} + if 1 in fields and 2 in fields: + depots[fields[1]] = fields[2] + return sorted(depots.items()) or None + except (zlib.error, ValueError, IndexError): + return None + + def completed_depots(completed): newest = {} try: diff --git a/tools/tests/test_steam_games.py b/tools/tests/test_steam_games.py index e5db51350..aa5b11566 100644 --- a/tools/tests/test_steam_games.py +++ b/tools/tests/test_steam_games.py @@ -6,6 +6,7 @@ from pathlib import Path import subprocess import tempfile import unittest +import zlib from unittest.mock import Mock, patch BIN = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin' @@ -247,6 +248,20 @@ class ImportsTest(unittest.TestCase): config.write_text(json.dumps({'installedManifestIDs': {'43': 0x7FFFFFFFFFFFFFFF}})) self.assertIsNone(imports.installed_build(self.folder, 42, 'DroidDeck-42')) + def test_compressed_protobuf_depot_config_records_installed_depots(self): + config = self.folder / '.DepotDownloader/depot.config' + config.parent.mkdir() + (self.folder / '.download_complete').touch() + entry = bytes([0x08, 43, 0x10, 99]) + packed = zlib.compressobj(wbits=-15) + config.write_bytes(packed.compress(bytes([0x0A, len(entry)]) + entry) + packed.flush()) + self.assertIn('"43"\n {\n "manifest" "99"', imports.installed_build(self.folder, 42, 'DroidDeck-42')) + config.write_bytes(b'not a config') + self.assertIsNone(imports.installed_build(self.folder, 42, 'DroidDeck-42')) + (config.parent / 'completed').mkdir() + (config.parent / 'completed/43_98.manifest').write_bytes(b'x') + self.assertIn('"manifest" "98"', imports.installed_build(self.folder, 42, 'DroidDeck-42')) + def test_completed_manifests_record_installed_depots(self): completed = self.folder / '.DepotDownloader/completed' completed.mkdir(parents=True)