"""Read-only, offline ZIP study. Python 3 standard library; never extracts or runs files. Usage: python inspect-reference.py CLIENT.zip > reference.json Hashing reads the whole ZIP; selected member reads are bounded to 32 MiB each. Output contains aggregate metadata and a fixed list of public software filenames, not archive paths, file contents, user configuration, or personal identifiers. """ import collections import datetime import hashlib import json from pathlib import Path import re import sys import xml.etree.ElementTree as ET import zipfile from html.parser import HTMLParser SELECTED = ('eqgame.exe', 'EQUI_KeyRingWnd.xml', 'EQUI_RespawnWnd.xml', 'RespawnWnd.html', 'InventoryWindow.html', 'CastingWindow.html', 'ClaimWindow.html', 'SendMoneyWnd.html', 'BankWindow.html') LIMIT = 32 * 1024 * 1024 class TagCounter(HTMLParser): def __init__(self): super().__init__() self.tags = collections.Counter() def handle_starttag(self, tag, attrs): self.tags[tag] += 1 def inspect(path): digest = hashlib.sha256() with open(path, 'rb') as stream: for block in iter(lambda: stream.read(1024 * 1024), b''): digest.update(block) report = {'schemaVersion': 1, 'inspectedOn': datetime.date.today().isoformat(), 'zipSHA256': digest.hexdigest(), 'zipBytes': Path(path).stat().st_size, 'binariesExecuted': False, 'networkConnections': False} with zipfile.ZipFile(path) as archive: entries = archive.infolist() files = [entry for entry in entries if not entry.is_dir()] report.update(files=len(files), directories=len(entries) - len(files), uncompressedBytes=sum(e.file_size for e in files)) extensions = collections.defaultdict(lambda: {'files': 0, 'bytes': 0}) for entry in files: extension = Path(entry.filename.replace('\\', '/')).suffix.lower() # Aggregate unusual/non-ASCII suffixes without publishing user text. if not re.fullmatch(r'\.[a-z0-9]{1,10}', extension): extension = '(other/no extension)' extensions[extension]['files'] += 1 extensions[extension]['bytes'] += entry.file_size report['extensions'] = dict(sorted(extensions.items(), key=lambda row: (-row[1]['files'], row[0]))) selected = {} for name in SELECTED: matches = [e for e in files if e.filename.replace('\\', '/').rsplit('/', 1)[-1].lower() == name.lower()] record = {'matches': len(matches)} if len(matches) == 1 and matches[0].file_size <= LIMIT: with archive.open(matches[0]) as stream: content = stream.read(LIMIT + 1) if len(content) > LIMIT: raise ValueError('Selected member exceeds inspection limit.') record.update(bytes=len(content), sha256=hashlib.sha256(content).hexdigest()) if name.endswith('.xml'): if b' reference.json') print(json.dumps(inspect(sys.argv[1]), indent=2, sort_keys=False))