Files
Modernization-Tool/scripts/prepare_remote_fallbacks.py
T
2026-09-03 21:28:55 +02:00

154 lines
5.2 KiB
Python

import hashlib
import json
import os
import shutil
import struct
import time
import urllib.error
import urllib.request
import zipfile
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
OUTPUT_ROOT = os.path.join(ROOT, "Payload", "Fallback", "Remote")
MANIFEST_PATH = os.path.join(ROOT, "Payload", "Fallback", "remote_fallbacks.json")
USER_AGENT = "Modernization-Tool build fallback fetcher"
FALLBACKS = [
{
"id": "wowpresence",
"filename": "WowPresence.zip",
"revision": "v1.3",
"url": "https://github.com/Dusk-92/WowPresence/releases/download/v1.3/WowPresence.zip",
"kind": "wowpresence_zip",
"size": 141423,
"sha256": "9b2dca9c69b5ab3b6dae3ef3b3efc9671779a227802ebb6ac98cb22bc6ea448a",
},
]
def _sha256(path):
digest = hashlib.sha256()
with open(path, "rb") as handle:
while True:
chunk = handle.read(1024 * 1024)
if not chunk:
break
digest.update(chunk)
return digest.hexdigest().lower()
def _download(url, destination, retries=3):
os.makedirs(os.path.dirname(destination), exist_ok=True)
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
last_error = None
for attempt in range(1, retries + 1):
temp_path = destination + ".new"
try:
with urllib.request.urlopen(request, timeout=300) as response, open(temp_path, "wb") as out:
while True:
chunk = response.read(1024 * 1024)
if not chunk:
break
out.write(chunk)
os.replace(temp_path, destination)
return
except (OSError, urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as exc:
last_error = exc
try:
os.remove(temp_path)
except OSError:
pass
if attempt < retries:
time.sleep(attempt * 2)
raise RuntimeError(f"Could not download {url}: {last_error}")
def _verify_x86_pe_bytes(data, label):
if len(data) < 1024 or data[:2] != b"MZ":
raise RuntimeError(f"{label} is not a valid PE file")
pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
if pe_offset < 64 or pe_offset > len(data) - 26:
raise RuntimeError(f"{label} has an invalid PE offset")
if data[pe_offset:pe_offset + 4] != b"PE\0\0":
raise RuntimeError(f"{label} is missing the PE signature")
machine = struct.unpack_from("<H", data, pe_offset + 4)[0]
optional_size = struct.unpack_from("<H", data, pe_offset + 20)[0]
optional_offset = pe_offset + 24
if optional_size < 2 or optional_offset + 2 > len(data):
raise RuntimeError(f"{label} has an invalid optional header")
optional_magic = struct.unpack_from("<H", data, optional_offset)[0]
if machine != 0x014C or optional_magic != 0x010B:
raise RuntimeError(f"{label} is not a 32-bit x86 PE file")
def _verify_wowpresence_zip(path):
with zipfile.ZipFile(path) as archive:
entries = {name.rsplit("/", 1)[-1].lower(): name for name in archive.namelist()}
for filename in ("WowPresence.dll", "WowPresence.exe"):
member = entries.get(filename.lower())
if member is None:
raise RuntimeError(f"{filename} is missing from {path}")
_verify_x86_pe_bytes(archive.read(member), filename)
def main():
shutil.rmtree(OUTPUT_ROOT, ignore_errors=True)
os.makedirs(OUTPUT_ROOT, exist_ok=True)
manifest = {
"schema": 1,
"generated_for": "Modernization Tool release build",
"fallbacks": {},
}
for item in FALLBACKS:
destination = os.path.join(OUTPUT_ROOT, item["id"], item["filename"])
print(f"Preparing bundled fallback: {item['id']}", flush=True)
_download(item["url"], destination)
if item["kind"] == "wowpresence_zip":
_verify_wowpresence_zip(destination)
else:
raise RuntimeError(f"Unknown fallback kind: {item['kind']}")
digest = _sha256(destination)
expected = item.get("sha256")
if expected and digest != expected.lower():
raise RuntimeError(
f"SHA-256 mismatch for {item['id']}: expected {expected}, got {digest}"
)
size = os.path.getsize(destination)
expected_size = item.get("size")
if expected_size is not None and size != expected_size:
raise RuntimeError(
f"Size mismatch for {item['id']}: expected {expected_size}, got {size}"
)
manifest["fallbacks"][item["id"]] = {
"filename": item["filename"],
"kind": item["kind"],
"revision": item["revision"],
"source": item["url"],
"size": size,
"sha256": digest,
}
print(
f"FALLBACK {item['id']} size={size} sha256={digest}",
flush=True,
)
staged = MANIFEST_PATH + ".new"
with open(staged, "w", encoding="utf-8", newline="\n") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
os.replace(staged, MANIFEST_PATH)
print(f"Wrote {MANIFEST_PATH}", flush=True)
if __name__ == "__main__":
main()