import hashlib import json import os import shutil import stat import struct import tempfile import urllib.error import urllib.request import uuid import zipfile USER_AGENT = "Modernization-Tool/1.0 (+https://github.com/Dusk-92/Modernization-Tool)" GITHUB_API = "https://api.github.com" NETWORK_TIMEOUT = 30 WOWPRESENCE_REPO = "Dusk-92/WowPresence" WOWPRESENCE_MANAGED_ID = "wowpresence" class RemotePackageError(RuntimeError): pass def _emit_progress(callback, message, current=None, total=None): if callback is None: return try: callback(message, current, total) except Exception: # UI/progress reporting must never break an installation. pass def _request(url, accept=None): headers = {"User-Agent": USER_AGENT} if accept: headers["Accept"] = accept return urllib.request.Request(url, headers=headers) def _get_json(url): try: with urllib.request.urlopen(_request(url, "application/vnd.github+json"), timeout=NETWORK_TIMEOUT) as response: return json.loads(response.read().decode("utf-8")) except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, json.JSONDecodeError) as exc: raise RemotePackageError(f"GitHub request failed: {exc}") from exc def _latest_release(repo): data = _get_json(f"{GITHUB_API}/repos/{repo}/releases/latest") if data.get("draft") or data.get("prerelease"): raise RemotePackageError(f"{repo}: latest release is not a stable release.") return data def _find_asset(release, exact_name=None, predicate=None): for asset in release.get("assets", []): name = asset.get("name", "") if exact_name is not None and name.lower() == exact_name.lower(): return asset if predicate is not None and predicate(name): return asset wanted = exact_name or "matching release asset" raise RemotePackageError(f"Could not find {wanted} in release {release.get('tag_name', '?')}.") def _download(url, suffix="", expected_digest=None, progress=None, label="Downloading", timeout=NETWORK_TIMEOUT): fd, temp_path = tempfile.mkstemp(prefix="modernization_", suffix=suffix) os.close(fd) try: digest = hashlib.sha256() with urllib.request.urlopen(_request(url), timeout=timeout) as response, open(temp_path, "wb") as out: total = None length = response.headers.get("Content-Length") if length: try: total = int(length) except (TypeError, ValueError): total = None downloaded = 0 _emit_progress(progress, label, downloaded, total) while True: chunk = response.read(256 * 1024) if not chunk: break out.write(chunk) digest.update(chunk) downloaded += len(chunk) _emit_progress(progress, label, downloaded, total) _emit_progress(progress, label, downloaded, total or downloaded) if expected_digest and expected_digest.startswith("sha256:"): expected = expected_digest.split(":", 1)[1].lower() actual = digest.hexdigest().lower() if actual != expected: raise RemotePackageError( f"SHA-256 mismatch for downloaded file (expected {expected}, got {actual})." ) return temp_path except Exception: try: os.remove(temp_path) except OSError: pass raise def _download_asset(asset, progress=None, label=None): url = asset.get("browser_download_url") if not url: raise RemotePackageError(f"Release asset {asset.get('name', '?')} has no download URL.") suffix = os.path.splitext(asset.get("name", ""))[1] display = label or f"Downloading {asset.get('name', 'release asset')}" try: return _download( url, suffix=suffix, expected_digest=asset.get("digest"), progress=progress, label=display, ) except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as exc: raise RemotePackageError(f"Download failed for {asset.get('name', '?')}: {exc}") from exc def _verify_x86_pe(path, label="downloaded DLL", min_size=1024): """Validate a 32-bit x86 PE binary before it can reach the WoW folder.""" try: size = os.path.getsize(path) if size < min_size: raise RemotePackageError( f"{label} is unexpectedly small ({size} bytes)." ) with open(path, "rb") as handle: dos = handle.read(64) if len(dos) < 64 or dos[:2] != b"MZ": raise RemotePackageError( f"{label} is not a valid Windows PE binary (missing MZ header)." ) pe_offset = struct.unpack_from(" size - 26: raise RemotePackageError( f"{label} has an invalid PE header offset." ) handle.seek(pe_offset) if handle.read(4) != b"PE\0\0": raise RemotePackageError( f"{label} is not a valid Windows PE binary (missing PE signature)." ) coff = handle.read(20) if len(coff) != 20: raise RemotePackageError( f"{label} has a truncated COFF header." ) machine = struct.unpack_from("