mirror of
https://github.com/open-goal/jak-project
synced 2026-09-05 10:57:28 -04:00
9c86c86c74
Breaks the 300 file milestone, also includes the majority of mips2c functions carried forward. Some functions need some fixes in the c++ code to mips2c properly, and a small handful of the mips2c functions require manual patching -- all spots were marked with `TODO - fix` that had to be commented out to get things compiling.
529 lines
9.6 KiB
Python
529 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import difflib
|
|
import os
|
|
import re
|
|
import sys
|
|
import termios
|
|
import tty
|
|
from dataclasses import dataclass
|
|
from io import StringIO
|
|
|
|
|
|
OLD_FILE = "ir2-jak3-test_ir2.asm"
|
|
NEW_FILE = "ir2-jakx-test_ir2.asm"
|
|
|
|
|
|
# ============================================================================
|
|
# Colors
|
|
# ============================================================================
|
|
|
|
class Color:
|
|
RESET = "\033[0m"
|
|
RED = "\033[31m"
|
|
GREEN = "\033[32m"
|
|
YELLOW = "\033[33m"
|
|
CYAN = "\033[36m"
|
|
DIM = "\033[2m"
|
|
BOLD = "\033[1m"
|
|
|
|
|
|
def color(text, c):
|
|
return f"{c}{text}{Color.RESET}"
|
|
|
|
|
|
# ============================================================================
|
|
# Data Structures
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class Operation:
|
|
number: int
|
|
asm: str
|
|
mnemonic: str
|
|
normalized: str
|
|
|
|
|
|
@dataclass
|
|
class Function:
|
|
name: str
|
|
stack_total: int
|
|
operations: list[Operation]
|
|
has_errors: bool = False
|
|
|
|
|
|
# ============================================================================
|
|
# Parsing
|
|
# ============================================================================
|
|
|
|
FUNC_RE = re.compile(r";\s*\.function\s+(.+)")
|
|
STACK_RE = re.compile(r";stack:\s*total\s+(0x[0-9a-fA-F]+|\d+)")
|
|
OP_RE = re.compile(r"^\s*(.*?)\s*;;\s*\[\s*(\d+)\]")
|
|
ERROR_RE = re.compile(r";;\s*ERROR:\s*")
|
|
|
|
|
|
REGISTER_RE = re.compile(
|
|
r"\b("
|
|
r"r0|at|v[01]|a[0-3]|t[0-9]|s[0-7]|gp|sp|fp|ra|hi|lo"
|
|
r")\b"
|
|
)
|
|
|
|
|
|
def normalize_registers(asm):
|
|
|
|
mapping = {}
|
|
next_id = 0
|
|
|
|
def repl(match):
|
|
|
|
nonlocal next_id
|
|
|
|
reg = match.group(1)
|
|
|
|
if reg not in mapping:
|
|
mapping[reg] = f"R{next_id}"
|
|
next_id += 1
|
|
|
|
return mapping[reg]
|
|
|
|
return REGISTER_RE.sub(repl, asm)
|
|
|
|
|
|
def parse_file(path):
|
|
|
|
functions = {}
|
|
|
|
current = None
|
|
|
|
with open(path) as f:
|
|
|
|
for line in f:
|
|
|
|
m = FUNC_RE.search(line)
|
|
|
|
if m:
|
|
|
|
if current:
|
|
functions[current.name] = current
|
|
|
|
current = Function(
|
|
name=m.group(1),
|
|
stack_total=0,
|
|
operations=[],
|
|
)
|
|
|
|
continue
|
|
|
|
|
|
if current is None:
|
|
continue
|
|
|
|
|
|
if ERROR_RE.search(line):
|
|
current.has_errors = True
|
|
|
|
|
|
m = STACK_RE.search(line)
|
|
|
|
if m:
|
|
current.stack_total = int(m.group(1), 0)
|
|
continue
|
|
|
|
|
|
m = OP_RE.match(line)
|
|
|
|
if m:
|
|
|
|
asm = m.group(1).rstrip()
|
|
|
|
current.operations.append(
|
|
Operation(
|
|
number=int(m.group(2)),
|
|
asm=asm,
|
|
mnemonic=asm.split()[0] if asm else "",
|
|
normalized=normalize_registers(asm),
|
|
)
|
|
)
|
|
|
|
|
|
if current:
|
|
functions[current.name] = current
|
|
|
|
|
|
return functions
|
|
|
|
|
|
# ============================================================================
|
|
# Analysis
|
|
# ============================================================================
|
|
|
|
def similarity(a, b):
|
|
return difflib.SequenceMatcher(None, a, b).ratio()
|
|
|
|
|
|
def is_decompiled(func):
|
|
return not func.has_errors
|
|
|
|
|
|
def compare_exact(old, new):
|
|
|
|
return (
|
|
len(old.operations) == len(new.operations)
|
|
and all(
|
|
a.asm == b.asm
|
|
for a, b in zip(
|
|
old.operations,
|
|
new.operations
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
def classify(old, new):
|
|
|
|
if compare_exact(old, new):
|
|
return "IDENTICAL"
|
|
|
|
|
|
mnemonic_score = similarity(
|
|
[x.mnemonic for x in old.operations],
|
|
[x.mnemonic for x in new.operations],
|
|
)
|
|
|
|
normalized_score = similarity(
|
|
[x.normalized for x in old.operations],
|
|
[x.normalized for x in new.operations],
|
|
)
|
|
|
|
|
|
if normalized_score > .995:
|
|
return "REGISTER CHANGES"
|
|
|
|
if mnemonic_score > .98:
|
|
return "NEAR MATCH"
|
|
|
|
if mnemonic_score > .75:
|
|
return "MODIFIED"
|
|
|
|
return "DIFFERENT"
|
|
|
|
|
|
CLASS_COLOR = {
|
|
"IDENTICAL": Color.GREEN,
|
|
"REGISTER CHANGES": Color.GREEN,
|
|
"NEAR MATCH": Color.YELLOW,
|
|
"MODIFIED": Color.YELLOW,
|
|
"DIFFERENT": Color.RED,
|
|
}
|
|
|
|
|
|
CLASS_ORDER = {
|
|
"DIFFERENT": 0,
|
|
"MODIFIED": 1,
|
|
"NEAR MATCH": 2,
|
|
"REGISTER CHANGES": 3,
|
|
"IDENTICAL": 4,
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Diff
|
|
# ============================================================================
|
|
|
|
def print_diff(old, new, out):
|
|
|
|
matcher = difflib.SequenceMatcher(
|
|
None,
|
|
[x.normalized for x in old.operations],
|
|
[x.normalized for x in new.operations],
|
|
)
|
|
|
|
|
|
print("Diff:\n", file=out)
|
|
|
|
|
|
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
|
|
if tag == "equal":
|
|
continue
|
|
|
|
|
|
print(
|
|
color(
|
|
f"@@ {tag} @@",
|
|
Color.CYAN
|
|
),
|
|
file=out
|
|
)
|
|
|
|
|
|
if tag in ("replace", "delete"):
|
|
|
|
for op in old.operations[i1:i2]:
|
|
|
|
print(
|
|
color(
|
|
f"- [{op.number:4}] {op.asm}",
|
|
Color.RED
|
|
),
|
|
file=out
|
|
)
|
|
|
|
|
|
if tag in ("replace", "insert"):
|
|
|
|
for op in new.operations[j1:j2]:
|
|
|
|
print(
|
|
color(
|
|
f"+ [{op.number:4}] {op.asm}",
|
|
Color.GREEN
|
|
),
|
|
file=out
|
|
)
|
|
|
|
|
|
print(file=out)
|
|
|
|
|
|
|
|
def analyze(old, new):
|
|
|
|
out = StringIO()
|
|
|
|
def p(*args):
|
|
print(*args, file=out)
|
|
|
|
|
|
classification = classify(old,new)
|
|
|
|
|
|
p("=" * 100)
|
|
p(old.name)
|
|
p("=" * 100)
|
|
p()
|
|
|
|
|
|
p(
|
|
"Stack:"
|
|
)
|
|
|
|
stack_match = old.stack_total == new.stack_total
|
|
|
|
p(f" old : 0x{old.stack_total:x}")
|
|
p(f" new : 0x{new.stack_total:x}")
|
|
p(
|
|
" ",
|
|
color(
|
|
"MATCH" if stack_match else "DIFFERENT",
|
|
Color.GREEN if stack_match else Color.RED
|
|
)
|
|
)
|
|
|
|
p()
|
|
|
|
|
|
p("Operations:")
|
|
p(f" old : {len(old.operations)}")
|
|
p(f" new : {len(new.operations)}")
|
|
p(f" diff: {len(new.operations)-len(old.operations):+}")
|
|
|
|
|
|
mnemonic = similarity(
|
|
[x.mnemonic for x in old.operations],
|
|
[x.mnemonic for x in new.operations],
|
|
)
|
|
|
|
normalized = similarity(
|
|
[x.normalized for x in old.operations],
|
|
[x.normalized for x in new.operations],
|
|
)
|
|
|
|
|
|
p()
|
|
|
|
p("Similarity:")
|
|
p(f" Exact Assembly : {compare_exact(old,new)}")
|
|
p(f" Mnemonic : {mnemonic:.3%}")
|
|
p(f" Register Normalized : {normalized:.3%}")
|
|
|
|
|
|
p()
|
|
|
|
p(
|
|
"Classification:",
|
|
color(
|
|
classification,
|
|
CLASS_COLOR[classification]
|
|
)
|
|
)
|
|
|
|
|
|
if classification != "IDENTICAL":
|
|
|
|
p()
|
|
|
|
print_diff(
|
|
old,
|
|
new,
|
|
out
|
|
)
|
|
|
|
|
|
return out.getvalue()
|
|
|
|
|
|
|
|
# ============================================================================
|
|
# Pager
|
|
# ============================================================================
|
|
|
|
def clear_screen():
|
|
|
|
os.system(
|
|
"clear" if os.name != "nt"
|
|
else "cls"
|
|
)
|
|
|
|
|
|
def read_key():
|
|
|
|
fd = sys.stdin.fileno()
|
|
|
|
old = termios.tcgetattr(fd)
|
|
|
|
try:
|
|
|
|
tty.setraw(fd)
|
|
|
|
key = sys.stdin.read(1)
|
|
|
|
if key == "\x1b":
|
|
key += sys.stdin.read(2)
|
|
|
|
return key
|
|
|
|
finally:
|
|
|
|
termios.tcsetattr(
|
|
fd,
|
|
termios.TCSADRAIN,
|
|
old
|
|
)
|
|
|
|
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
old_functions = parse_file(OLD_FILE)
|
|
new_functions = parse_file(NEW_FILE)
|
|
|
|
|
|
functions = []
|
|
|
|
for name in set(old_functions) & set(new_functions):
|
|
|
|
old = old_functions[name]
|
|
new = new_functions[name]
|
|
|
|
functions.append(
|
|
(
|
|
old,
|
|
new,
|
|
analyze(old,new)
|
|
)
|
|
)
|
|
|
|
|
|
functions.sort(
|
|
key=lambda x:
|
|
(
|
|
1 if is_decompiled(x[0]) else 0,
|
|
CLASS_ORDER.get(
|
|
classify(x[0],x[1]),
|
|
99
|
|
),
|
|
x[0].name
|
|
)
|
|
)
|
|
|
|
|
|
show_decompiled = False
|
|
index = 0
|
|
|
|
|
|
while True:
|
|
|
|
visible = [
|
|
f for f in functions
|
|
if show_decompiled or not is_decompiled(f[0])
|
|
]
|
|
|
|
|
|
if not visible:
|
|
break
|
|
|
|
|
|
index = min(index, len(visible)-1)
|
|
|
|
|
|
clear_screen()
|
|
|
|
|
|
print(
|
|
color(
|
|
f"Functions: {len(functions)} | "
|
|
f"Review: {len(functions)-sum(is_decompiled(x[0]) for x in functions)} | "
|
|
f"Decompiled: {sum(is_decompiled(x[0]) for x in functions)}",
|
|
Color.CYAN
|
|
)
|
|
)
|
|
|
|
print()
|
|
|
|
|
|
for i,(old,new,_) in enumerate(visible):
|
|
|
|
cls = classify(old,new)
|
|
|
|
marker = "> " if i == index else " "
|
|
|
|
print(
|
|
marker
|
|
+ f"{old.name:<40}"
|
|
+ color(
|
|
cls,
|
|
CLASS_COLOR[cls]
|
|
)
|
|
)
|
|
|
|
|
|
print()
|
|
print(visible[index][2])
|
|
|
|
|
|
print(
|
|
color(
|
|
"[↑↓/jk] move [d] show decompiled [q] quit",
|
|
Color.DIM
|
|
)
|
|
)
|
|
|
|
|
|
key = read_key()
|
|
|
|
|
|
if key.lower() == "q":
|
|
break
|
|
|
|
elif key in ("\x1b[A","k","K"):
|
|
index = max(index-1,0)
|
|
|
|
elif key in ("\x1b[B","j","J"):
|
|
index = min(index+1,len(visible)-1)
|
|
|
|
elif key.lower() == "d":
|
|
show_decompiled = not show_decompiled
|
|
index = 0 |