mirror of
https://github.com/isledecomp/isle.git
synced 2024-11-29 11:06:05 -05:00
reccmp: HTML refactor and diff address display (#581)
* reccmp: HTML refactor and diff address display * Restore the @@ range indicator
This commit is contained in:
parent
ba8f2b1c0f
commit
9c71209fb9
8 changed files with 878 additions and 240 deletions
|
@ -192,11 +192,13 @@ def replace_immediate(chunk: str) -> str:
|
|||
def parse_asm(self, data: bytes, start_addr: Optional[int] = 0) -> List[str]:
|
||||
asm = []
|
||||
|
||||
for inst in disassembler.disasm_lite(data, start_addr):
|
||||
for raw_inst in disassembler.disasm_lite(data, start_addr):
|
||||
# Use heuristics to disregard some differences that aren't representative
|
||||
# of the accuracy of a function (e.g. global offsets)
|
||||
result = self.sanitize(DisasmLiteInst(*inst))
|
||||
inst = DisasmLiteInst(*raw_inst)
|
||||
result = self.sanitize(inst)
|
||||
|
||||
# mnemonic + " " + op_str
|
||||
asm.append(" ".join(result))
|
||||
asm.append((hex(inst.address), " ".join(result)))
|
||||
|
||||
return asm
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
from isledecomp.types import SymbolType
|
||||
from isledecomp.compare.asm import ParseAsm, can_resolve_register_differences
|
||||
from .db import CompareDb, MatchInfo
|
||||
from .diff import combined_diff
|
||||
from .lines import LinesDb
|
||||
|
||||
|
||||
|
@ -307,8 +308,12 @@ def recomp_lookup(addr: int) -> Optional[str]:
|
|||
float_lookup=recomp_float,
|
||||
)
|
||||
|
||||
orig_asm = orig_parse.parse_asm(orig_raw, match.orig_addr)
|
||||
recomp_asm = recomp_parse.parse_asm(recomp_raw, match.recomp_addr)
|
||||
orig_combined = orig_parse.parse_asm(orig_raw, match.orig_addr)
|
||||
recomp_combined = recomp_parse.parse_asm(recomp_raw, match.recomp_addr)
|
||||
|
||||
# Detach addresses from asm lines for the text diff.
|
||||
orig_asm = [x[1] for x in orig_combined]
|
||||
recomp_asm = [x[1] for x in recomp_combined]
|
||||
|
||||
diff = difflib.SequenceMatcher(None, orig_asm, recomp_asm)
|
||||
ratio = diff.ratio()
|
||||
|
@ -317,7 +322,9 @@ def recomp_lookup(addr: int) -> Optional[str]:
|
|||
# Check whether we can resolve register swaps which are actually
|
||||
# perfect matches modulo compiler entropy.
|
||||
is_effective_match = can_resolve_register_differences(orig_asm, recomp_asm)
|
||||
unified_diff = difflib.unified_diff(orig_asm, recomp_asm, n=10)
|
||||
unified_diff = combined_diff(
|
||||
diff, orig_combined, recomp_combined, context_size=10
|
||||
)
|
||||
else:
|
||||
is_effective_match = False
|
||||
unified_diff = []
|
||||
|
@ -352,9 +359,7 @@ def _compare_vtable(self, match: MatchInfo) -> DiffReport:
|
|||
[t for (t,) in struct.iter_unpack("<L", recomp_table)],
|
||||
)
|
||||
|
||||
def match_text(
|
||||
i: int, m: Optional[MatchInfo], raw_addr: Optional[int] = None
|
||||
) -> str:
|
||||
def match_text(m: Optional[MatchInfo], raw_addr: Optional[int] = None) -> str:
|
||||
"""Format the function reference at this vtable index as text.
|
||||
If we have not identified this function, we have the option to
|
||||
display the raw address. This is only worth doing for the original addr
|
||||
|
@ -363,19 +368,18 @@ def match_text(
|
|||
should override the given function from the superclass, but we have not
|
||||
implemented this yet.
|
||||
"""
|
||||
index = f"vtable0x{i*4:02x}"
|
||||
|
||||
if m is not None:
|
||||
orig = hex(m.orig_addr) if m.orig_addr is not None else "no orig"
|
||||
recomp = (
|
||||
hex(m.recomp_addr) if m.recomp_addr is not None else "no recomp"
|
||||
)
|
||||
return f"{index:>12} : ({orig:10} / {recomp:10}) : {m.name}"
|
||||
return f"({orig} / {recomp}) : {m.name}"
|
||||
|
||||
if raw_addr is not None:
|
||||
return f"{index:>12} : 0x{raw_addr:x} from orig not annotated."
|
||||
return f"0x{raw_addr:x} from orig not annotated."
|
||||
|
||||
return f"{index:>12} : (no match)"
|
||||
return "(no match)"
|
||||
|
||||
orig_text = []
|
||||
recomp_text = []
|
||||
|
@ -395,14 +399,22 @@ def match_text(
|
|||
ratio += 1
|
||||
|
||||
n_entries += 1
|
||||
orig_text.append(match_text(i, orig, raw_orig))
|
||||
recomp_text.append(match_text(i, recomp))
|
||||
index = f"vtable0x{i*4:02x}"
|
||||
orig_text.append((index, match_text(orig, raw_orig)))
|
||||
recomp_text.append((index, match_text(recomp)))
|
||||
|
||||
ratio = ratio / float(n_entries) if n_entries > 0 else 0
|
||||
|
||||
# n=100: Show the entire table if there is a diff to display.
|
||||
# Otherwise it would be confusing if the table got cut off.
|
||||
unified_diff = difflib.unified_diff(orig_text, recomp_text, n=100)
|
||||
|
||||
sm = difflib.SequenceMatcher(
|
||||
None,
|
||||
[x[1] for x in orig_text],
|
||||
[x[1] for x in recomp_text],
|
||||
)
|
||||
|
||||
unified_diff = combined_diff(sm, orig_text, recomp_text, context_size=100)
|
||||
|
||||
return DiffReport(
|
||||
match_type=SymbolType.VTABLE,
|
||||
|
|
81
tools/isledecomp/isledecomp/compare/diff.py
Normal file
81
tools/isledecomp/isledecomp/compare/diff.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
from difflib import SequenceMatcher
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
CombinedDiffInput = List[Tuple[str, str]]
|
||||
CombinedDiffOutput = List[Tuple[str, List[Dict[str, Tuple[str, str]]]]]
|
||||
|
||||
|
||||
def combined_diff(
|
||||
diff: SequenceMatcher,
|
||||
orig_combined: CombinedDiffInput,
|
||||
recomp_combined: CombinedDiffInput,
|
||||
context_size: int = 3,
|
||||
) -> CombinedDiffOutput:
|
||||
"""We want to diff the original and recomp assembly. The "combined" assembly
|
||||
input has two components: the address of the instruction and the assembly text.
|
||||
We have already diffed the text only. This is the SequenceMatcher object.
|
||||
The SequenceMatcher can generate "opcodes" that describe how to turn "Text A"
|
||||
into "Text B". These refer to list indices of the original arrays, so we can
|
||||
use those to create the final diff and include the address for each line of assembly.
|
||||
This is almost the same procedure as the difflib.unified_diff function, but we
|
||||
are reusing the already generated SequenceMatcher object.
|
||||
"""
|
||||
|
||||
unified_diff = []
|
||||
|
||||
for group in diff.get_grouped_opcodes(context_size):
|
||||
subgroups = []
|
||||
|
||||
# Keep track of the addresses we've seen in this diff group.
|
||||
# This helps create the "@@" line. (Does this have a name?)
|
||||
# Do it this way because not every line in each list will have an
|
||||
# address. If our context begins or ends on a line that does not
|
||||
# have one, we will have an incomplete range string.
|
||||
orig_addrs = set()
|
||||
recomp_addrs = set()
|
||||
|
||||
for code, i1, i2, j1, j2 in group:
|
||||
if code == "equal":
|
||||
# The sections are equal, so the list slices are guaranteed
|
||||
# to have the same length. We only need the diffed value (asm text)
|
||||
# from one of the lists, but we need the addresses from both.
|
||||
# Use zip to put the two lists together and then take out what we want.
|
||||
both = [
|
||||
(a, b, c)
|
||||
for ((a, b), (c, _)) in zip(
|
||||
orig_combined[i1:i2], recomp_combined[j1:j2]
|
||||
)
|
||||
]
|
||||
|
||||
for orig_addr, _, recomp_addr in both:
|
||||
if orig_addr is not None:
|
||||
orig_addrs.add(orig_addr)
|
||||
|
||||
if recomp_addr is not None:
|
||||
recomp_addrs.add(recomp_addr)
|
||||
|
||||
subgroups.append({"both": both})
|
||||
else:
|
||||
for orig_addr, _ in orig_combined[i1:i2]:
|
||||
if orig_addr is not None:
|
||||
orig_addrs.add(orig_addr)
|
||||
|
||||
for recomp_addr, _ in recomp_combined[j1:j2]:
|
||||
if recomp_addr is not None:
|
||||
recomp_addrs.add(recomp_addr)
|
||||
|
||||
subgroups.append(
|
||||
{
|
||||
"orig": orig_combined[i1:i2],
|
||||
"recomp": recomp_combined[j1:j2],
|
||||
}
|
||||
)
|
||||
|
||||
orig_sorted = sorted(orig_addrs)
|
||||
recomp_sorted = sorted(recomp_addrs)
|
||||
|
||||
diff_slug = f"@@ -{orig_sorted[0]},{orig_sorted[-1]} +{recomp_sorted[0]},{recomp_sorted[-1]} @@"
|
||||
|
||||
unified_diff.append((diff_slug, subgroups))
|
||||
|
||||
return unified_diff
|
|
@ -5,7 +5,70 @@
|
|||
import colorama
|
||||
|
||||
|
||||
def print_combined_diff(udiff, plain: bool = False, show_both: bool = False):
|
||||
if udiff is None:
|
||||
return
|
||||
|
||||
# We don't know how long the address string will be ahead of time.
|
||||
# Set this value for each address to try to line things up.
|
||||
padding_size = 0
|
||||
|
||||
for slug, subgroups in udiff:
|
||||
if plain:
|
||||
print("---")
|
||||
print("+++")
|
||||
print(slug)
|
||||
else:
|
||||
print(f"{colorama.Fore.RED}---")
|
||||
print(f"{colorama.Fore.GREEN}+++")
|
||||
print(f"{colorama.Fore.BLUE}{slug}")
|
||||
print(colorama.Style.RESET_ALL, end="")
|
||||
|
||||
for subgroup in subgroups:
|
||||
equal = subgroup.get("both") is not None
|
||||
|
||||
if equal:
|
||||
for orig_addr, line, recomp_addr in subgroup["both"]:
|
||||
padding_size = max(padding_size, len(orig_addr))
|
||||
if show_both:
|
||||
print(f"{orig_addr} / {recomp_addr} : {line}")
|
||||
else:
|
||||
print(f"{orig_addr} : {line}")
|
||||
else:
|
||||
for orig_addr, line in subgroup["orig"]:
|
||||
padding_size = max(padding_size, len(orig_addr))
|
||||
addr_prefix = (
|
||||
f"{orig_addr} / {'':{padding_size}}" if show_both else orig_addr
|
||||
)
|
||||
|
||||
if plain:
|
||||
print(f"{addr_prefix} : -{line}")
|
||||
else:
|
||||
print(
|
||||
f"{addr_prefix} : {colorama.Fore.RED}-{line}{colorama.Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
for recomp_addr, line in subgroup["recomp"]:
|
||||
padding_size = max(padding_size, len(recomp_addr))
|
||||
addr_prefix = (
|
||||
f"{'':{padding_size}} / {recomp_addr}"
|
||||
if show_both
|
||||
else recomp_addr
|
||||
)
|
||||
|
||||
if plain:
|
||||
print(f"{addr_prefix} : +{line}")
|
||||
else:
|
||||
print(
|
||||
f"{addr_prefix} : {colorama.Fore.GREEN}+{line}{colorama.Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
# Newline between each diff subgroup.
|
||||
print()
|
||||
|
||||
|
||||
def print_diff(udiff, plain):
|
||||
"""Print diff in difflib.unified_diff format."""
|
||||
if udiff is None:
|
||||
return False
|
||||
|
||||
|
|
605
tools/reccmp/reccmp.js
Normal file
605
tools/reccmp/reccmp.js
Normal file
|
@ -0,0 +1,605 @@
|
|||
// reccmp.js
|
||||
/* global data */
|
||||
|
||||
// Unwrap array of functions into a dictionary with address as the key.
|
||||
const dataDict = Object.fromEntries(data.map(row => [row.address, row]));
|
||||
|
||||
function getDataByAddr(addr) {
|
||||
return dataDict[addr];
|
||||
}
|
||||
|
||||
//
|
||||
// Pure functions
|
||||
//
|
||||
|
||||
function formatAsm(entries, addrOption) {
|
||||
const output = [];
|
||||
|
||||
const createTh = (text) => {
|
||||
const th = document.createElement('th');
|
||||
th.innerText = text;
|
||||
return th;
|
||||
};
|
||||
|
||||
const createTd = (text, className = '') => {
|
||||
const td = document.createElement('td');
|
||||
td.innerText = text;
|
||||
td.className = className;
|
||||
return td;
|
||||
};
|
||||
|
||||
entries.forEach(obj => {
|
||||
// These won't all be present. You get "both" for an equal node
|
||||
// and orig/recomp for a diff.
|
||||
const { both = [], orig = [], recomp = [] } = obj;
|
||||
|
||||
output.push(...both.map(([addr, line, recompAddr]) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.appendChild(createTh(addr));
|
||||
tr.appendChild(createTh(recompAddr));
|
||||
tr.appendChild(createTd(line));
|
||||
return tr;
|
||||
}));
|
||||
|
||||
output.push(...orig.map(([addr, line]) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.appendChild(createTh(addr));
|
||||
tr.appendChild(createTh(''));
|
||||
tr.appendChild(createTd(`-${line}`, 'diffneg'));
|
||||
return tr;
|
||||
}));
|
||||
|
||||
output.push(...recomp.map(([addr, line]) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.appendChild(createTh(''));
|
||||
tr.appendChild(createTh(addr));
|
||||
tr.appendChild(createTd(`+${line}`, 'diffpos'));
|
||||
return tr;
|
||||
}));
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function getMatchPercentText(row) {
|
||||
if ('stub' in row) {
|
||||
return 'stub';
|
||||
}
|
||||
|
||||
if ('effective' in row) {
|
||||
return '100.00%*';
|
||||
}
|
||||
|
||||
return (row.matching * 100).toFixed(2) + '%';
|
||||
}
|
||||
|
||||
// Helper for this set/remove attribute block
|
||||
function setBooleanAttribute(element, attribute, value) {
|
||||
if (value) {
|
||||
element.setAttribute(attribute, '');
|
||||
} else {
|
||||
element.removeAttribute(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Global state
|
||||
//
|
||||
|
||||
class ListingState {
|
||||
constructor() {
|
||||
this._query = '';
|
||||
this._sortCol = 'address';
|
||||
this._filterType = 1;
|
||||
this.sortDesc = false;
|
||||
this.hidePerfect = false;
|
||||
this.hideStub = false;
|
||||
}
|
||||
|
||||
get filterType() {
|
||||
return parseInt(this._filterType);
|
||||
}
|
||||
|
||||
set filterType(value) {
|
||||
value = parseInt(value);
|
||||
if (value >= 1 && value <= 3) {
|
||||
this._filterType = value;
|
||||
}
|
||||
}
|
||||
|
||||
get query() {
|
||||
return this._query;
|
||||
}
|
||||
|
||||
set query(value) {
|
||||
// Normalize search string
|
||||
this._query = value.toLowerCase().trim();
|
||||
}
|
||||
|
||||
get sortCol() {
|
||||
return this._sortCol;
|
||||
}
|
||||
|
||||
set sortCol(column) {
|
||||
if (column === this._sortCol) {
|
||||
this.sortDesc = !this.sortDesc;
|
||||
} else {
|
||||
this._sortCol = column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const StateProxy = {
|
||||
set(obj, prop, value) {
|
||||
if (prop === 'onsort') {
|
||||
this._onsort = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (prop === 'onfilter') {
|
||||
this._onfilter = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
obj[prop] = value;
|
||||
|
||||
if (prop === 'sortCol' || prop === 'sortDesc') {
|
||||
this._onsort();
|
||||
} else {
|
||||
this._onfilter();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const appState = new Proxy(new ListingState(), StateProxy);
|
||||
|
||||
//
|
||||
// Stateful functions
|
||||
//
|
||||
|
||||
function addrShouldAppear(addr) {
|
||||
// Destructuring sets defaults for optional values from this object.
|
||||
const {
|
||||
effective = false,
|
||||
stub = false,
|
||||
diff = '',
|
||||
name,
|
||||
address,
|
||||
matching
|
||||
} = getDataByAddr(addr);
|
||||
|
||||
if (appState.hidePerfect && (effective || matching >= 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (appState.hideStub && stub) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (appState.query === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Name/addr search
|
||||
if (appState.filterType === 1) {
|
||||
return (
|
||||
address.includes(appState.query) ||
|
||||
name.toLowerCase().includes(appState.query)
|
||||
);
|
||||
}
|
||||
|
||||
// no diff for review.
|
||||
if (diff === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// special matcher for combined diff
|
||||
const anyLineMatch = ([addr, line]) => line.toLowerCase().trim().includes(appState.query);
|
||||
|
||||
// Flatten all diff groups for the search
|
||||
const diffs = diff.map(([slug, subgroups]) => subgroups).flat();
|
||||
for (const subgroup of diffs) {
|
||||
const { both = [], orig = [], recomp = [] } = subgroup;
|
||||
|
||||
// If search includes context
|
||||
if (appState.filterType === 2 && both.some(anyLineMatch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (orig.some(anyLineMatch) || recomp.some(anyLineMatch)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Row comparator function, using our chosen sort column and direction.
|
||||
// -1 (A before B)
|
||||
// 1 (B before A)
|
||||
// 0 (equal)
|
||||
function rowSortOrder(addrA, addrB) {
|
||||
const objA = getDataByAddr(addrA);
|
||||
const objB = getDataByAddr(addrB);
|
||||
const valA = objA[appState.sortCol];
|
||||
const valB = objB[appState.sortCol];
|
||||
|
||||
if (valA > valB) {
|
||||
return appState.sortDesc ? -1 : 1;
|
||||
} else if (valA < valB) {
|
||||
return appState.sortDesc ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// Custom elements
|
||||
//
|
||||
|
||||
// Sets sort indicator arrow based on element attributes.
|
||||
class SortIndicator extends window.HTMLElement {
|
||||
static observedAttributes = ['data-sort'];
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (newValue === null) {
|
||||
this.textContent = '';
|
||||
} else {
|
||||
this.innerHTML = newValue === 'asc' ? '▲' : '▼';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for <tr>
|
||||
class FuncRow extends window.HTMLTableRowElement {
|
||||
static observedAttributes = ['expanded'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onclick = evt => (this.expanded = !this.expanded);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.address === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.querySelector('td')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = getDataByAddr(this.address);
|
||||
|
||||
const td0 = document.createElement('td');
|
||||
const td1 = document.createElement('td');
|
||||
const td2 = document.createElement('td');
|
||||
|
||||
td0.innerText = `${obj.address}`;
|
||||
td1.innerText = `${obj.name}`;
|
||||
td2.innerText = getMatchPercentText(obj);
|
||||
|
||||
this.appendChild(td0);
|
||||
this.appendChild(td1);
|
||||
this.appendChild(td2);
|
||||
}
|
||||
|
||||
get address() {
|
||||
return this.getAttribute('data-address');
|
||||
}
|
||||
|
||||
get expanded() {
|
||||
return this.getAttribute('expanded') !== null;
|
||||
}
|
||||
|
||||
set expanded(value) {
|
||||
setBooleanAttribute(this, 'expanded', value);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (name !== 'expanded') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.onchangeExpand) {
|
||||
this.onchangeExpand(this.expanded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for <tr>
|
||||
// Displays asm diff for the given @data-address value.
|
||||
class FuncRowChild extends window.HTMLTableRowElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const td = document.createElement('td');
|
||||
td.setAttribute('colspan', 3);
|
||||
this.appendChild(td);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const td = this.querySelector('td');
|
||||
|
||||
const addr = this.getAttribute('data-address');
|
||||
|
||||
const obj = getDataByAddr(addr);
|
||||
|
||||
if ('stub' in obj) {
|
||||
const diff = document.createElement('div');
|
||||
diff.className = 'identical';
|
||||
diff.innerText = 'Stub. No diff.';
|
||||
td.appendChild(diff);
|
||||
} else if (obj.diff.length === 0) {
|
||||
const diff = document.createElement('div');
|
||||
diff.className = 'identical';
|
||||
diff.innerText = 'Identical function - no diff';
|
||||
td.appendChild(diff);
|
||||
} else {
|
||||
const dd = new DiffDisplay();
|
||||
dd.option = '1';
|
||||
dd.address = addr;
|
||||
td.appendChild(dd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DiffDisplayOptions extends window.HTMLElement {
|
||||
static observedAttributes = ['data-option'];
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shadow = this.attachShadow({ mode: 'open' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
fieldset {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
<fieldset>
|
||||
<legend>Address display:</legend>
|
||||
<input type="radio" id="showNone" name="addrDisplay" value=0>
|
||||
<label for="showNone">None</label>
|
||||
<input type="radio" id="showOrig" name="addrDisplay" value=1>
|
||||
<label for="showOrig">Original</label>
|
||||
<input type="radio" id="showBoth" name="addrDisplay" value=2>
|
||||
<label for="showBoth">Both</label>
|
||||
</fieldset>`;
|
||||
|
||||
shadow.querySelectorAll('input[type=radio]').forEach(radio => {
|
||||
const checked = this.option === radio.getAttribute('value');
|
||||
setBooleanAttribute(radio, 'checked', checked);
|
||||
|
||||
radio.addEventListener('change', evt => (this.option = evt.target.value));
|
||||
});
|
||||
}
|
||||
|
||||
set option(value) {
|
||||
this.setAttribute('data-option', parseInt(value));
|
||||
}
|
||||
|
||||
get option() {
|
||||
return this.getAttribute('data-option') ?? 1;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (name !== 'data-option') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
class DiffDisplay extends window.HTMLElement {
|
||||
static observedAttributes = ['data-option'];
|
||||
|
||||
connectedCallback() {
|
||||
if (this.querySelector('diff-display-options') !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optControl = new DiffDisplayOptions();
|
||||
optControl.option = this.option;
|
||||
optControl.addEventListener('change', evt => (this.option = evt.target.option));
|
||||
this.appendChild(optControl);
|
||||
|
||||
const div = document.createElement('div');
|
||||
const obj = getDataByAddr(this.address);
|
||||
|
||||
const createSingleCellRow = (text, className) => {
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.setAttribute('colspan', 3);
|
||||
td.textContent = text;
|
||||
td.className = className;
|
||||
tr.appendChild(td);
|
||||
return tr;
|
||||
};
|
||||
|
||||
const groups = obj.diff;
|
||||
groups.forEach(([slug, subgroups]) => {
|
||||
const secondTable = document.createElement('table');
|
||||
secondTable.classList.add('diffTable', 'showOrig');
|
||||
|
||||
const tbody = document.createElement('tbody');
|
||||
secondTable.appendChild(tbody);
|
||||
tbody.appendChild(createSingleCellRow('---', 'diffneg'));
|
||||
tbody.appendChild(createSingleCellRow('+++', 'diffpos'));
|
||||
tbody.appendChild(createSingleCellRow(slug, 'diffslug'));
|
||||
|
||||
const diffs = formatAsm(subgroups, this.option);
|
||||
for (const el of diffs) {
|
||||
tbody.appendChild(el);
|
||||
}
|
||||
|
||||
div.appendChild(secondTable);
|
||||
});
|
||||
|
||||
this.appendChild(div);
|
||||
}
|
||||
|
||||
get address() {
|
||||
return this.getAttribute('data-address');
|
||||
}
|
||||
|
||||
set address(value) {
|
||||
this.setAttribute('data-address', value);
|
||||
}
|
||||
|
||||
get option() {
|
||||
return this.getAttribute('data-option') ?? 1;
|
||||
}
|
||||
|
||||
set option(value) {
|
||||
this.setAttribute('data-option', value);
|
||||
}
|
||||
}
|
||||
|
||||
// Main application.
|
||||
class ListingTable extends window.HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Redraw the table on any changes.
|
||||
appState.onsort = () => this.sortRows();
|
||||
appState.onfilter = () => this.filterRows();
|
||||
|
||||
const input = this.querySelector('input[type=search]');
|
||||
input.oninput = evt => (appState.query = evt.target.value);
|
||||
|
||||
const hidePerf = this.querySelector('input#cbHidePerfect');
|
||||
hidePerf.onchange = evt => (appState.hidePerfect = evt.target.checked);
|
||||
hidePerf.checked = appState.hidePerfect;
|
||||
|
||||
const hideStub = this.querySelector('input#cbHideStub');
|
||||
hideStub.onchange = evt => (appState.hideStub = evt.target.checked);
|
||||
hideStub.checked = appState.hideStub;
|
||||
|
||||
this.querySelectorAll('input[name=filterType]').forEach(radio => {
|
||||
const checked = appState.filterType === parseInt(radio.getAttribute('value'));
|
||||
setBooleanAttribute(radio, 'checked', checked);
|
||||
|
||||
radio.onchange = evt => (appState.filterType = radio.getAttribute('value'));
|
||||
});
|
||||
}
|
||||
|
||||
setRowExpand(address, shouldExpand) {
|
||||
const tbody = this.querySelector('tbody');
|
||||
const funcrow = tbody.querySelector(`tr.funcrow[data-address="${address}"]`);
|
||||
if (funcrow === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = tbody.querySelector(`tr.diffRow[data-address="${address}"]`);
|
||||
if (shouldExpand) {
|
||||
if (existing === null) {
|
||||
const diffrow = document.createElement('tr', { is: 'func-row-child' });
|
||||
diffrow.className = 'diffRow';
|
||||
diffrow.setAttribute('data-address', address);
|
||||
// Insert the diff row after the parent func row.
|
||||
tbody.insertBefore(diffrow, funcrow.nextSibling);
|
||||
}
|
||||
} else {
|
||||
if (existing !== null) {
|
||||
tbody.removeChild(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const thead = this.querySelector('thead');
|
||||
const headers = thead.querySelectorAll('th');
|
||||
headers.forEach(th => {
|
||||
const col = th.getAttribute('data-col');
|
||||
if (col) {
|
||||
th.onclick = evt => (appState.sortCol = col);
|
||||
}
|
||||
});
|
||||
|
||||
const tbody = this.querySelector('tbody');
|
||||
|
||||
for (const row of data) {
|
||||
const tr = document.createElement('tr', { is: 'func-row' });
|
||||
tr.className = 'funcrow';
|
||||
tr.setAttribute('data-address', row.address);
|
||||
tr.onchangeExpand = shouldExpand => this.setRowExpand(row.address, shouldExpand);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
this.sortRows();
|
||||
this.filterRows();
|
||||
}
|
||||
|
||||
sortRows() {
|
||||
const thead = this.querySelector('thead');
|
||||
const headers = thead.querySelectorAll('th');
|
||||
|
||||
// Update sort indicator
|
||||
headers.forEach(th => {
|
||||
const col = th.getAttribute('data-col');
|
||||
const indicator = th.querySelector('sort-indicator');
|
||||
if (appState.sortCol === col) {
|
||||
indicator.setAttribute('data-sort', appState.sortDesc ? 'desc' : 'asc');
|
||||
} else {
|
||||
indicator.removeAttribute('data-sort');
|
||||
}
|
||||
});
|
||||
|
||||
// Select only the function rows and the diff child row.
|
||||
// Exclude any nested tables used to *display* the diffs.
|
||||
const tbody = this.querySelector('tbody');
|
||||
const rows = tbody.querySelectorAll('tr.funcrow[data-address], tr.diffRow[data-address]');
|
||||
|
||||
// Sort all rows according to chosen order
|
||||
const newRows = Array.from(rows);
|
||||
newRows.sort((rowA, rowB) => {
|
||||
const addrA = rowA.getAttribute('data-address');
|
||||
const addrB = rowB.getAttribute('data-address');
|
||||
|
||||
// Diff row always sorts after its parent row
|
||||
if (addrA === addrB && rowB.className === 'diffRow') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return rowSortOrder(addrA, addrB);
|
||||
});
|
||||
|
||||
// Replace existing rows with updated order
|
||||
newRows.forEach(row => tbody.appendChild(row));
|
||||
}
|
||||
|
||||
filterRows() {
|
||||
const tbody = this.querySelector('tbody');
|
||||
const rows = tbody.querySelectorAll('tr.funcrow[data-address], tr.diffRow[data-address]');
|
||||
|
||||
rows.forEach(row => {
|
||||
const addr = row.getAttribute('data-address');
|
||||
const hidden = !addrShouldAppear(addr);
|
||||
setBooleanAttribute(row, 'hidden', hidden);
|
||||
});
|
||||
|
||||
// Update row count
|
||||
this.querySelector('#rowcount').textContent = `${tbody.querySelectorAll('tr.funcrow:not([hidden])').length}`;
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
window.customElements.define('listing-table', ListingTable);
|
||||
window.customElements.define('diff-display', DiffDisplay);
|
||||
window.customElements.define('diff-display-options', DiffDisplayOptions);
|
||||
window.customElements.define('sort-indicator', SortIndicator);
|
||||
window.customElements.define('func-row', FuncRow, { extends: 'tr' });
|
||||
window.customElements.define('func-row-child', FuncRowChild, { extends: 'tr' });
|
||||
};
|
|
@ -10,7 +10,7 @@
|
|||
from isledecomp import (
|
||||
Bin,
|
||||
get_file_in_script_dir,
|
||||
print_diff,
|
||||
print_combined_diff,
|
||||
diff_json,
|
||||
percent_string,
|
||||
)
|
||||
|
@ -48,8 +48,12 @@ def gen_json(json_file: str, orig_file: str, data):
|
|||
|
||||
|
||||
def gen_html(html_file, data):
|
||||
js_path = get_file_in_script_dir("reccmp.js")
|
||||
with open(js_path, "r", encoding="utf-8") as f:
|
||||
reccmp_js = f.read()
|
||||
|
||||
output_data = Renderer().render_path(
|
||||
get_file_in_script_dir("template.html"), {"data": data}
|
||||
get_file_in_script_dir("template.html"), {"data": data, "reccmp_js": reccmp_js}
|
||||
)
|
||||
|
||||
with open(html_file, "w", encoding="utf-8") as htmlfile:
|
||||
|
@ -106,7 +110,7 @@ def print_match_verbose(match, show_both_addrs: bool = False, is_plain: bool = F
|
|||
f"{addrs}: {match.name} Effective 100%% match. (Differs in register allocation only)\n\n{ok_text} (still differs in register allocation)\n\n"
|
||||
)
|
||||
else:
|
||||
print_diff(match.udiff, is_plain)
|
||||
print_combined_diff(match.udiff, is_plain, show_both_addrs)
|
||||
|
||||
print(
|
||||
f"\n{match.name} is only {percenttext} similar to the original, diff above"
|
||||
|
@ -282,7 +286,7 @@ def main():
|
|||
html_obj["effective"] = True
|
||||
|
||||
if match.udiff is not None:
|
||||
html_obj["diff"] = "\n".join(match.udiff)
|
||||
html_obj["diff"] = match.udiff
|
||||
|
||||
if match.is_stub:
|
||||
html_obj["stub"] = True
|
||||
|
|
|
@ -43,15 +43,15 @@
|
|||
background: #404040 !important;
|
||||
}
|
||||
|
||||
.funcrow:nth-child(odd), #listing th {
|
||||
.funcrow:nth-child(odd of :not([hidden])), #listing > thead th {
|
||||
background: #282828;
|
||||
}
|
||||
|
||||
.funcrow:nth-child(even) {
|
||||
.funcrow:nth-child(even of :not([hidden])) {
|
||||
background: #383838;
|
||||
}
|
||||
|
||||
#listing td, #listing th {
|
||||
.funcrow > td, .diffRow > td, #listing > thead th {
|
||||
border: 1px #f0f0f0 solid;
|
||||
padding: 0.5em;
|
||||
word-break: break-all !important;
|
||||
|
@ -65,233 +65,109 @@
|
|||
color: #80FF80;
|
||||
}
|
||||
|
||||
.diffslug {
|
||||
color: #8080FF;
|
||||
}
|
||||
|
||||
.identical {
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#sortind {
|
||||
sort-indicator {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.filters {
|
||||
align-items: top;
|
||||
display: flex;
|
||||
font-size: 10pt;
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
margin: 0.5em 0 1em 0;
|
||||
}
|
||||
|
||||
.filters > fieldset {
|
||||
/* checkbox and radio buttons v-aligned with text */
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filters > fieldset > label {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
table.diffTable {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.diffTable:not(:last-child) {
|
||||
/* visual gap *between* diff context groups */
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
table.diffTable td, table.diffTable th {
|
||||
border: 0 none;
|
||||
padding: 0 10px 0 0;
|
||||
}
|
||||
|
||||
table.diffTable th {
|
||||
/* don't break address if asm line is long */
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
diff-display[data-option="0"] th:nth-child(1) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
diff-display[data-option="0"] th:nth-child(2),
|
||||
diff-display[data-option="1"] th:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var data = {{{data}}};
|
||||
|
||||
function diffCssClass(firstChar) {
|
||||
return firstChar === '-' ? 'diffneg' : (firstChar === '+' ? 'diffpos' : '');
|
||||
}
|
||||
|
||||
function asmLineToDiv(line) {
|
||||
const diff_line = document.createElement('div');
|
||||
diff_line.className = diffCssClass(line[0]);
|
||||
diff_line.innerText = line;
|
||||
return diff_line;
|
||||
}
|
||||
|
||||
function formatAsm(asm) {
|
||||
var lines = asm.split('\n');
|
||||
return lines.filter(line => line.length > 0)
|
||||
.map(asmLineToDiv);
|
||||
}
|
||||
|
||||
function rowClick() {
|
||||
if (this.dataset.expanded === 'true') {
|
||||
this.nextSibling.remove();
|
||||
this.dataset.expanded = false;
|
||||
} else {
|
||||
var row = this.parentNode.insertBefore(document.createElement('tr'), this.nextSibling);
|
||||
row.classList.add('diff');
|
||||
var decCel = row.appendChild(document.createElement('td'));
|
||||
decCel.colSpan = 3;
|
||||
var diff = data[this.dataset.index].diff;
|
||||
const stub = "stub" in data[this.dataset.index];
|
||||
|
||||
if (stub) {
|
||||
diff = document.createElement('div');
|
||||
diff.className = 'identical';
|
||||
diff.innerText = 'Stub. No diff.';
|
||||
decCel.appendChild(diff);
|
||||
} else if (diff == '') {
|
||||
diff = document.createElement('div');
|
||||
diff.className = 'identical';
|
||||
diff.innerText = 'Identical function - no diff';
|
||||
decCel.appendChild(diff);
|
||||
} else {
|
||||
diff = formatAsm(diff);
|
||||
for (const el of diff) {
|
||||
decCel.appendChild(el);
|
||||
}
|
||||
}
|
||||
this.dataset.expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllDiffs() {
|
||||
const collection = document.getElementsByClassName("diff");
|
||||
for (var ele of collection) {
|
||||
ele.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const filterOptions = { text: '', hidePerfect: false, hideStub: false };
|
||||
|
||||
function filter() {
|
||||
closeAllDiffs();
|
||||
|
||||
var ltext = filterOptions.text.toLowerCase();
|
||||
|
||||
const collection = document.getElementsByClassName("funcrow");
|
||||
var searchCount = 0;
|
||||
for (var ele of collection) {
|
||||
var eledata = data[ele.dataset.index];
|
||||
|
||||
const stubOk = (!filterOptions.hideStub || !("stub" in eledata));
|
||||
|
||||
const textOk = (ltext == ''
|
||||
|| eledata.address.toLowerCase().includes(ltext)
|
||||
|| eledata.name.toLowerCase().includes(ltext));
|
||||
|
||||
const perfOk = (!filterOptions.hidePerfect || (eledata.matching < 1));
|
||||
|
||||
if (stubOk && textOk && perfOk) {
|
||||
ele.style.display = '';
|
||||
searchCount++;
|
||||
} else {
|
||||
ele.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lastSortedCol = -1;
|
||||
var ascending = true;
|
||||
|
||||
function sortByColumn(column) {
|
||||
closeAllDiffs();
|
||||
|
||||
if (column == lastSortedCol) {
|
||||
ascending = !ascending;
|
||||
}
|
||||
lastSortedCol = column;
|
||||
|
||||
const collection = document.getElementsByClassName("funcrow");
|
||||
|
||||
var newOrder = [];
|
||||
|
||||
for (var ele of collection) {
|
||||
var inserted = false;
|
||||
|
||||
for (var i = 0; i < newOrder.length; i++) {
|
||||
var cmpEle = newOrder[i];
|
||||
|
||||
var ourCol = ele.childNodes[column];
|
||||
var cmpCol = cmpEle.childNodes[column];
|
||||
|
||||
if ((cmpCol.dataset.value > ourCol.dataset.value) == ascending) {
|
||||
newOrder.splice(i, 0, ele);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inserted) {
|
||||
newOrder.push(ele);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 1; i < newOrder.length; i++) {
|
||||
newOrder[i - 1].after(newOrder[i]);
|
||||
}
|
||||
|
||||
var sortIndicator = document.getElementById('sortind');
|
||||
if (!sortIndicator) {
|
||||
sortIndicator = document.createElement('span');
|
||||
sortIndicator.id = 'sortind';
|
||||
}
|
||||
sortIndicator.innerHTML = ascending ? '▲' : '▼';
|
||||
|
||||
var th = document.getElementById('listingheader').childNodes[column];
|
||||
th.appendChild(sortIndicator);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
var listing = document.getElementById('listing');
|
||||
|
||||
const headers = listing.getElementsByTagName('th');
|
||||
var headerCount = 0;
|
||||
for (const header of headers) {
|
||||
header.addEventListener('click', function(){
|
||||
sortByColumn(this.dataset.column, true);
|
||||
});
|
||||
|
||||
header.dataset.column = headerCount;
|
||||
headerCount++;
|
||||
}
|
||||
|
||||
data.forEach((element, index) => {
|
||||
var row = listing.appendChild(document.createElement('tr'));
|
||||
var addrCel = row.appendChild(document.createElement('td'));
|
||||
var nameCel = row.appendChild(document.createElement('td'));
|
||||
var matchCel = row.appendChild(document.createElement('td'));
|
||||
|
||||
addrCel.innerText = addrCel.dataset.value = element.address;
|
||||
nameCel.innerText = nameCel.dataset.value = element.name;
|
||||
|
||||
if ("stub" in element) {
|
||||
matchCel.innerHTML = 'stub'
|
||||
matchCel.dataset.value = -1;
|
||||
} else {
|
||||
var effectiveNote = (element.matching == 1 && element.diff != '') ? '*' : '';
|
||||
matchCel.innerHTML = (element.matching * 100).toFixed(2) + '%' + effectiveNote;
|
||||
matchCel.dataset.value = element.matching;
|
||||
}
|
||||
|
||||
row.classList.add('funcrow');
|
||||
row.addEventListener('click', rowClick);
|
||||
row.dataset.index = index;
|
||||
row.dataset.expanded = false;
|
||||
});
|
||||
|
||||
var search = document.getElementById('search');
|
||||
search.addEventListener('input', function (evt) {
|
||||
filterOptions.text = search.value;
|
||||
filter();
|
||||
});
|
||||
|
||||
const cbHidePerfect = document.getElementById('cbHidePerfect');
|
||||
cbHidePerfect.addEventListener('change', evt => {
|
||||
filterOptions.hidePerfect = evt.target.checked;
|
||||
filter();
|
||||
})
|
||||
|
||||
const cbHideStub = document.querySelector('#cbHideStub');
|
||||
cbHideStub.addEventListener('change', evt => {
|
||||
filterOptions.hideStub = evt.target.checked;
|
||||
filter();
|
||||
})
|
||||
|
||||
sortByColumn(0);
|
||||
});
|
||||
<script>var data = {{{data}}};</script>
|
||||
<script>{{{reccmp_js}}}</script>
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<h1>Decompilation Status</h1>
|
||||
<input id="search" type="search" placeholder="Search for offset or function name...">
|
||||
<div class="filters">
|
||||
<label for="cbHidePerfect">Hide 100% match</label>
|
||||
<input type="checkbox" id="cbHidePerfect" />
|
||||
<label for="cbHideStub">Hide stubs</label>
|
||||
<input type="checkbox" id="cbHideStub" />
|
||||
</div>
|
||||
<table id="listing">
|
||||
<tr id='listingheader'><th style='width: 20%'>Address</th><th style="width:60%">Name</th><th style='width: 20%'>Matching</th></tr>
|
||||
</table>
|
||||
<listing-table>
|
||||
<input id="search" type="search" placeholder="Search for offset or function name...">
|
||||
<div class="filters">
|
||||
<fieldset>
|
||||
<legend>Options:</legend>
|
||||
<input type="checkbox" id="cbHidePerfect" />
|
||||
<label for="cbHidePerfect">Hide 100% match</label>
|
||||
<input type="checkbox" id="cbHideStub" />
|
||||
<label for="cbHideStub">Hide stubs</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Search filters on:</legend>
|
||||
<input type="radio" name="filterType" id="filterName" value=1 checked />
|
||||
<label for="filterName">Name/address</label>
|
||||
<input type="radio" name="filterType" id="filterAsm" value=2 />
|
||||
<label for="filterAsm">Asm output</label>
|
||||
<input type="radio" name="filterType" id="filterDiff" value=3 />
|
||||
<label for="filterDiff">Asm diffs only</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p>Results: <span id="rowcount"></span></p>
|
||||
<table id="listing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="address" style="width: 20%">Address<sort-indicator/></th>
|
||||
<th data-col="name" style="width: 60%">Name<sort-indicator/></th>
|
||||
<th data-col="matching" style="width: 20%">Matching<sort-indicator/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</listing-table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import colorama
|
||||
from isledecomp.bin import Bin as IsleBin
|
||||
from isledecomp.compare import Compare as IsleCompare
|
||||
from isledecomp.utils import print_diff
|
||||
from isledecomp.utils import print_combined_diff
|
||||
|
||||
# Ignore all compare-db messages.
|
||||
logging.getLogger("isledecomp.compare").addHandler(logging.NullHandler())
|
||||
|
@ -53,13 +53,8 @@ def parse_args() -> argparse.Namespace:
|
|||
return args
|
||||
|
||||
|
||||
def show_vtable_diff(udiff: List[str], verbose: bool = False, plain: bool = False):
|
||||
lines = [
|
||||
line
|
||||
for line in udiff
|
||||
if verbose or line.startswith("+") or line.startswith("-")
|
||||
]
|
||||
print_diff(lines, plain)
|
||||
def show_vtable_diff(udiff: List, _: bool = False, plain: bool = False):
|
||||
print_combined_diff(udiff, plain)
|
||||
|
||||
|
||||
def print_summary(vtable_count: int, problem_count: int):
|
||||
|
|
Loading…
Reference in a new issue