b6e847e981
struct_diff сравнивает классы по составу и показывает, каких не хватает. Но он отвечает на вопрос «того же ли набора методов», а не «тот ли код». Оба вопроса приходится задавать: прыжки и звуки шагов были сломаны именно кодом, который компилировался без ошибок и состоял из правильных методов. javap_diff сравнивает инструкции построчно, вызывая javap на обоих деревьях классификации. Наивное сравнение текста здесь бесполезно: перекомпиляция перенумеровывает constant pool, поэтому почти каждая строка отличается номером #123, хотя код совпадает. Инструмент убирает номера пула, сдвиги смещений от ldc/ldc_w и форму инструкций, после чего остаётся реальная разница. На звуке инструмент показал, что все аудио-классы совпадают с оригиналом. Остатки объясняются версией компилятора, а не правками: javac 8 собирает конкатенацию строк через StringBuilder, javac 17 через invokedynamic, а $values() у enum появился в более новых версиях. ogg_duration читает частоту дискретизации из заголовка Vorbis и число отсчётов из granule position последней страницы OggS. Отсюда видно, что файл сам по себе короче или что его просто обрывают при воспроизведении, — вопрос, который иначе приходится решать на слух. Проверено, что все 1085 ogg совпадают с индексом по SHA-1.
86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
#!/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 <javap-exe> <reference-classes-dir> <rebuilt-classes-dir> <class> [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) + " <target>"
|
|
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:])) |