"""Compare a final JAR with its preserved baseline and current native source. This is a packaging audit, not a substitute for the server's functional tests. Only private build artifacts are written; production sources are never edited. """ from __future__ import annotations import argparse import hashlib import json import subprocess import zipfile from pathlib import Path # INPUTS: the final JAR must be supplied explicitly after its build completes. ROOT = Path(__file__).resolve().parents[2] HERE = ROOT / 'build/jar-native-audit' HARNESS = Path(__file__).resolve().parent / 'JarRigExportAudit.java' SOURCE = ROOT / "src/main/java/ar/com/companeros/horror/client/ParasiteRig.java" BASELINE = ROOT / "respaldos/antes-contactos-20261007-140143/Raps.jar" JAVA_BIN = Path("C:/Program Files/Eclipse Adoptium/jdk-17.0.20.8-hotspot/bin") CLIENT_PREFIX = "ar/com/companeros/horror/client/" # Cambios visuales permitidos en esta revisión: el compositor de piel vive en # identity, y las dos texturas son los atlas regenerados del huésped. ALLOWED_CLIENT_ENTRIES = {"ar/com/companeros/identity/HostSkinAtlas.class"} ALLOWED_RESOURCE_ENTRIES = { "assets/companeros/textures/entity/parasite.png", "assets/companeros/textures/entity/parasite_initial.png", } def sha(path): result = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): result.update(chunk) return result.hexdigest() # ZIP: compare uncompressed entry bytes, independent of timestamps/compression. def entries(path): with zipfile.ZipFile(path) as archive: return {info.filename: hashlib.sha256(archive.read(info)).hexdigest() for info in archive.infolist() if not info.is_dir()} def compare_zip(old, new): before, after = entries(old), entries(new) changed = [{"name": name, "before_sha256": before.get(name), "after_sha256": after.get(name)} for name in sorted(set(before) | set(after)) if before.get(name) != after.get(name)] backend = [item["name"] for item in changed if item["name"].endswith(".class") and not item["name"].startswith(CLIENT_PREFIX) and item["name"] not in ALLOWED_CLIENT_ENTRIES] resources = [item["name"] for item in changed if not item["name"].endswith(".class") and not item["name"].startswith("META-INF/") and item["name"] not in ALLOWED_RESOURCE_ENTRIES] return {"baseline_entries": len(before), "final_entries": len(after), "changed_entries": changed, "backend_classes_changed": backend, "resources_changed": resources, "backend_and_resources_identical": not backend and not resources} # NATIVE: compile the actual source once; run identical public CLI calls in each. def export(classpath, destination): with destination.open("wb") as stream: subprocess.run([str(JAVA_BIN / "java.exe"), "-cp", classpath, "JarRigExportAudit"], stdout=stream, stderr=subprocess.PIPE, check=True, timeout=120) def compare_exports(left, right): rows = [] with left.open(encoding="utf-8") as original, right.open(encoding="utf-8") as packaged: for line_a, line_b in zip(original, packaged, strict=True): a, b = json.loads(line_a), json.loads(line_b) rows.append({"mode": a["mode"], "stage": a["stage"], "sample_count": len(a["samples"]), "identical_json": a == b, "identical_bytes": line_a == line_b}) if len(rows) != 70: raise AssertionError(f"Expected70 mode/stage comparisons, received{len(rows)}") return {"comparisons": len(rows), "native_frames_per_target": sum(row["sample_count"] for row in rows), "all_json_identical": all(row["identical_json"] for row in rows), "all_bytes_identical": all(row["identical_bytes"] for row in rows), "source_export_sha256": sha(left), "jar_export_sha256": sha(right), "results": rows} # REPORT: hash the final JAR and reject files that change during inspection. def main(): HERE.mkdir(parents=True, exist_ok=True) parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--jar", type=Path, required=True) parser.add_argument("--baseline", type=Path, default=BASELINE) parser.add_argument("--source-only", action="store_true", help="Verificar equivalencia fuente/JAR; registrar diferencias de backend sin evaluarlas") args = parser.parse_args() jar, baseline = args.jar.resolve(), args.baseline.resolve() stable = {path: sha(path) for path in (SOURCE, baseline, jar)} harness, source_classes = HERE / "harness-classes", HERE / "source-classes" harness.mkdir(exist_ok=True) source_classes.mkdir(exist_ok=True) subprocess.run([str(JAVA_BIN / "javac.exe"), "-encoding", "UTF-8", "-d", str(harness), str(HARNESS)], check=True, capture_output=True, timeout=45) subprocess.run([str(JAVA_BIN / "javac.exe"), "-encoding", "UTF-8", "-d", str(source_classes), str(SOURCE)], check=True, capture_output=True, timeout=45) source_output, jar_output = HERE / "source.jsonl", HERE / "packaged.jsonl" export(str(source_classes) + ";" + str(harness), source_output) export(str(jar) + ";" + str(harness), jar_output) native = compare_exports(source_output, jar_output) zip_audit = compare_zip(baseline, jar) unchanged = all(sha(path) == expected for path, expected in stable.items()) passed = native["all_json_identical"] and unchanged and (args.source_only or zip_audit["backend_and_resources_identical"]) report = {"passed": passed, "jar": str(jar), "jar_sha256": stable[jar], "baseline": str(baseline), "baseline_sha256": stable[baseline], "native_source_sha256": stable[SOURCE], "inputs_stable": unchanged, "native_cli": native, "zip_comparison": zip_audit, "scope": "Packaging/source equivalence only; no server/GameTests executed by this harness", "backend_comparison_enforced": not args.source_only} destination = HERE / "result.json" destination.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") print(json.dumps({"passed": passed, "jar_sha256": stable[jar], "report_sha256": sha(destination), "native_comparisons": native["comparisons"], "native_frames": native["native_frames_per_target"], "all_json_identical": native["all_json_identical"], "changed_entries": [x["name"] for x in zip_audit["changed_entries"]], "backend_classes_changed": zip_audit["backend_classes_changed"], "resources_changed": zip_audit["resources_changed"]}, indent=2), flush=True) if not passed: raise SystemExit(1) if __name__ == "__main__": main()