#!/usr/bin/env python3 """Compare recompiled class files against the reference, instruction by instruction. A naive text diff of two javap dumps is useless here: recompiling renumbers the constant pool, so nearly every line differs by its ``#123`` reference even when the code is identical. Normalising those references away turns the diff into something that only reports real behavioural differences. Usage: javap_diff.py [class ...] """ import re import subprocess import sys # "12: invokevirtual #123 // Method foo" -> "invokevirtual // Method foo" POOL = re.compile(r"#\d+(?:,\s*\d+)*\b") # jump targets are printed as "42" / "123" after a branch opcode OFFSET = re.compile(r"^(\s*\d+:\s*\w+)\s+\d+\s*$") # ldc_w is only a wider index form of ldc; javap picks whichever fits LDC_W = re.compile(r"\bldc_w\b") # instruction offsets shift by one byte per ldc_w/ldc difference, so they carry # no meaning here; the line order already encodes control flow OFF = re.compile(r"^\s*\d+:\s*") def dump(javap, classpath, name): out = subprocess.run( [javap, "-p", "-c", "-cp", classpath, name], capture_output=True, text=True, errors="replace") return out.stdout def normalise(text): lines = [] for raw in text.splitlines(): line = raw.rstrip() if not line.strip(): continue if line.startswith("Compiled from"): continue if "major version" in line: continue line = POOL.sub("#", line) line = re.sub(r"#\s+", "# ", line) line = LDC_W.sub("ldc", line) m = OFFSET.match(line) if m: line = m.group(1) + " " line = OFF.sub("", line) lines.append(line.strip()) return lines def main(argv): if len(argv) < 4: sys.exit(__doc__) javap, ref, new, classes = argv[0], argv[1], argv[2], argv[3:] bad = 0 for cls in classes: a = normalise(dump(javap, ref, cls)) b = normalise(dump(javap, new, cls)) if not a or not b: print("=== {} : javap produced nothing (ref={} new={})".format( cls, len(a), len(b))) bad += 1 continue if a == b: print("=== {} : IDENTICAL ({})".format(cls, len(a))) continue sa, sb = set(a), set(b) only_ref = [x for x in a if x not in sb] only_new = [x for x in b if x not in sa] print("=== {} : {} ref / {} new -> {} ref-only, {} new-only".format( cls, len(a), len(b), len(only_ref), len(only_new))) bad += 1 import difflib for line in list(difflib.unified_diff(a, b, "reference", "rebuilt", lineterm="", n=2))[:80]: print(" " + line) return 1 if bad else 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))