Files
sasheg dev 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.
2026-10-19 00:25:35 +03:00

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:]))