import json import struct import typing # noqa from dataclasses import dataclass from enum import Enum from pathlib import Path from shlex import shlex from typing import BinaryIO, Union # https://learn.microsoft.com/en-us/windows/win32/debug/pe-format endian = "<" IMAGE_DIRECTORY_ENTRY = 2 VERSION_RESOURCE = 16 RESOURCE_NODE = ".rsrc" PE32_x86 = "0x10b" PE32_x64 = "0x20b" VS_VERSION_INFO_MAGIC = "0xfeef04bd0000" VS_VERSION_INFO_ID = "VS_VERSION_INFO" class VersionMatch(Enum): LOCAL_OLDER = 1 LOCAL_NEWER = 2 SAME_VERSION = 3 class u8: fmt = "B" class u16: fmt = "H" class u32: fmt = "L" class u64: fmt = "Q" class i8: fmt = "b" class i16: fmt = "h" class i32: fmt = "l" class i64: fmt = "q" class PackedData: @classmethod def unpack(cls, data: BinaryIO): r = [] for key, value in cls.__annotations__.items(): if value == str: f = data.read(8).rstrip(b"\x00\x00").decode() else: fmt = endian + (value.fmt) size = struct.calcsize(fmt) f = struct.unpack(fmt, data.read(size))[0] r.append(f) return cls(*r) @dataclass(slots=True, frozen=True) class COFF_FILE_HDR(PackedData): machine_type: u16 number_of_sections: u16 timestamp: u32 pointer_to_symbol_table: u32 number_of_symbols: u32 size_of_optional_header: u16 characteristics: u16 @dataclass(slots=True, frozen=True) class OPTIONAL_HDR_X86(PackedData): magic: u16 major_linker_ver: u8 minor_linker_ver: u8 size_of_code: u32 size_of_initialized_data: u32 size_of_uninitialized_data: u32 address_of_entry_point: u32 base_of_code: u32 base_of_data: u32 @dataclass(slots=True, frozen=True) class OPTIONAL_HDR_X64(PackedData): magic: u16 major_linker_ver: u8 minor_linker_ver: u8 size_of_code: u32 size_of_initialized_data: u32 size_of_uninitialized_data: u32 address_of_entry_point: u32 base_of_code: u32 @dataclass(slots=True, frozen=True) class OPTIONAL_HDR_WIN_X86(PackedData): image_base: u32 section_alignment: u32 file_alignment: u32 major_operating_system_version: u16 minor_operating_system_version: u16 major_image_version: u16 minor_image_version: u16 major_subsystem_version: u16 minor_subsystem_version: u16 win32_version_value: u32 size_of_image: u32 size_of_headers: u32 checksum: u32 subsystem: u16 dll_characteristics: u16 size_of_stack_reserve: u32 size_of_stack_commit: u32 size_of_heap_reserve: u32 size_of_heap_commit: u32 loader_flags: u32 number_of_rva_and_sizes: u32 @dataclass(slots=True, frozen=True) class OPTIONAL_HDR_WIN_X64(PackedData): image_base: u64 section_alignment: u32 file_alignment: u32 major_operating_system_version: u16 minor_operating_system_version: u16 major_image_version: u16 minor_image_version: u16 major_subsystem_version: u16 minor_subsystem_version: u16 win32_version_value: u32 size_of_image: u32 size_of_headers: u32 checksum: u32 subsystem: u16 dll_characteristics: u16 size_of_stack_reserve: u64 size_of_stack_commit: u64 size_of_heap_reserve: u64 size_of_heap_commit: u64 loader_flags: u32 number_of_rva_and_sizes: u32 @dataclass(slots=True, frozen=True) class DATA_DIR(PackedData): virtual_address: u32 size: u32 @dataclass(slots=True, frozen=True) class SECTION_HDR(PackedData): name: str virtual_size: u32 virtual_address: u32 size_of_raw_data: u32 pointer_to_raw_data: u32 pointer_to_relocations: u32 pointer_to_line_numbers: u32 number_of_relocations: u16 number_of_line_numbers: u16 characteristics: u32 @dataclass(slots=True, frozen=True) class RESOURCE_DIRECTORY_TABLE(PackedData): characteristics: u32 timestamp: u32 major_version: u16 minor_version: u16 number_of_name_entries: u16 number_of_id_entries: u16 @dataclass(slots=True, frozen=True) class RESOURCE_DIRECTORY_ENTRY(PackedData): """ This field is either a string identifying a data leaf (if the high bit is set) or an ID to another nested directory (if the high bit is clear). The outermost level is always a directory. If it is a name, the lower 31 bits are the offset from the beginning of the resource section's raw data to the name (the name consists of 16 bits length and trailing wide characters, in Unicode, not 0-terminated). """ name_or_id: u32 data_or_subdir: u32 @dataclass(slots=True, frozen=True) class RESOURCE_DATA_ENTRY(PackedData): data_rva: u32 size: u32 codepage: u32 reserved: u32 @dataclass(slots=True, frozen=True) class VS_VERSION_INFO_HDR(PackedData): size: u16 value_length: u16 value_type: u16 @dataclass(slots=True, frozen=True) class DayZVersion: major: int minor: int patch: int @dataclass(slots=True, frozen=True) class FileVersion: major: int minor: int build: int revision: int @dataclass(slots=True, frozen=True) class Result: local: Union[str, None] remote: str build: str path: Union[Path, None] match: VersionMatch error: Union[Exception, None] class PeFileError(Exception): """Expected contents missing from headers or resource nodes""" pass class AppNotInstalledError(Exception): """App not present in user's libraryfolders""" pass class AppMovedError(Exception): """VDF points to a nonexistent location on disk""" pass class VDFLoadError(Exception): """Malformed VDF or JSON conversion""" pass def parse_version_number(data: BinaryIO): # https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo minor = struct.unpack("> 16 & 0xFFFF major = struct.unpack("> 0 & 0xFFFF build = struct.unpack("> 0 & 0xFFFF revision = struct.unpack("> 16 & 0xFFFF return FileVersion(major, minor, build, revision) def seek_to_hex(address: str, data: BinaryIO) -> None: data.seek(int(address, base=16)) def seek_to_pe_stub(data: BinaryIO) -> None: MAGIC = "0x3c" seek_to_hex(MAGIC, data) e_lfanew = hex(struct.unpack(" DayZVersion | Exception: try: version = get_version(file) except Exception as e: raise e patch = str(version.build) + str(version.revision) dz_vers = DayZVersion(version.major, version.minor, int(patch)) return dz_vers def dayz_version_to_str(v: DayZVersion) -> str: return ".".join(str(el) for el in [v.major, v.minor, v.patch]) def dayz_version_from_str(v: str) -> DayZVersion: vers = v.split(".") assert len(vers) == 3 return DayZVersion(*[int(el) for el in vers]) def get_version(file): with open(file, "rb") as f: seek_to_pe_stub(f) COFF = COFF_FILE_HDR.unpack(f) pos = f.tell() blob = f.read() magic = hex(struct.unpack(" VERSION_RESOURCE: raise PeFileError("no version info node found") seek_to_hex(hex(offset + entry.data_or_subdir), f) data = RESOURCE_DATA_ENTRY.unpack(f) # https://stackoverflow.com/questions/2170843/va-virtual-address-rva-relative-virtual-address offset = data.data_rva - hdr.virtual_address + hdr.pointer_to_raw_data seek_to_hex(hex(offset), f) hdr = VS_VERSION_INFO_HDR.unpack(f) # https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo byte_len = len(VS_VERSION_INFO_ID.encode("utf-16le")) label = f.read(byte_len).decode("utf-16le") if label != VS_VERSION_INFO_ID: raise PeFileError(f"header identifier != '{VS_VERSION_INFO_ID}'") f.read(32 - byte_len) identifier = hex(struct.unpack(" Path: binary = "DayZ_x64.exe" identifier = {221100: "DayZ", 1024020: "DayZ Exp"} name = identifier[appid] pe_path = None path = path + "/steamapps/libraryfolders.vdf" with open(path, "r") as f: try: j = json.loads(vdf_to_json(f)) except Exception: raise VDFLoadError("Failed to parse libraryfolders") for obj in j["libraryfolders"]: if str(appid) in j["libraryfolders"][obj]["apps"]: pe_path = j["libraryfolders"][obj]["path"] pe_path += f"/steamapps/common/{name}/{binary}" break if pe_path is None: raise AppNotInstalledError( f"Failed to find a libraryfolder for the appid '{appid}'" ) pe_path = Path(pe_path) if pe_path.exists() is False: raise AppMovedError( f"Path '{pe_path}' specified in libraryfolders does not exist" ) return pe_path def compare_versions(local: DayZVersion, remote: DayZVersion): """ packaging.version module is not available OOTB on some distributions """ if dayz_version_to_str(local) == dayz_version_to_str(remote): return VersionMatch.SAME_VERSION if local.major < remote.major: return VersionMatch.LOCAL_OLDER if local.major > remote.major: return VersionMatch.LOCAL_NEWER if local.major == remote.major: if local.minor < remote.minor: return VersionMatch.LOCAL_OLDER if local.minor > remote.minor: return VersionMatch.LOCAL_NEWER if local.minor == remote.minor: if local.patch < remote.patch: return VersionMatch.LOCAL_OLDER if local.patch > remote.patch: return VersionMatch.LOCAL_NEWER if local.patch == remote.patch: return VersionMatch.SAME_VERSION def vdf_to_json(stream): def _istr(indent, string): return (indent * " ") + string jbuf = "{\n" lex = shlex(stream) indent = 1 while True: tok = lex.get_token() if not tok: return jbuf + "}\n" if tok == "}": indent -= 1 jbuf += _istr(indent, "}") ntok = lex.get_token() lex.push_token(ntok) if ntok and ntok != "}": jbuf += "," jbuf += "\n" else: ntok = lex.get_token() if ntok == "{": jbuf += _istr(indent, tok + ": {\n") indent += 1 else: jbuf += _istr(indent, tok + ": " + ntok) ntok = lex.get_token() lex.push_token(ntok) if ntok != "}": jbuf += "," jbuf += "\n"