reccmp: HTML refactor and diff address display (#581)

* reccmp: HTML refactor and diff address display

* Restore the @@ range indicator
This commit is contained in:
MS 2024-02-20 02:56:33 -05:00 committed by GitHub
parent ba8f2b1c0f
commit 9c71209fb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 878 additions and 240 deletions

View file

@ -192,11 +192,13 @@ def replace_immediate(chunk: str) -> str:
def parse_asm(self, data: bytes, start_addr: Optional[int] = 0) -> List[str]: def parse_asm(self, data: bytes, start_addr: Optional[int] = 0) -> List[str]:
asm = [] 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 # Use heuristics to disregard some differences that aren't representative
# of the accuracy of a function (e.g. global offsets) # 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 # mnemonic + " " + op_str
asm.append(" ".join(result)) asm.append((hex(inst.address), " ".join(result)))
return asm return asm

View file

@ -12,6 +12,7 @@
from isledecomp.types import SymbolType from isledecomp.types import SymbolType
from isledecomp.compare.asm import ParseAsm, can_resolve_register_differences from isledecomp.compare.asm import ParseAsm, can_resolve_register_differences
from .db import CompareDb, MatchInfo from .db import CompareDb, MatchInfo
from .diff import combined_diff
from .lines import LinesDb from .lines import LinesDb
@ -307,8 +308,12 @@ def recomp_lookup(addr: int) -> Optional[str]:
float_lookup=recomp_float, float_lookup=recomp_float,
) )
orig_asm = orig_parse.parse_asm(orig_raw, match.orig_addr) orig_combined = orig_parse.parse_asm(orig_raw, match.orig_addr)
recomp_asm = recomp_parse.parse_asm(recomp_raw, match.recomp_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) diff = difflib.SequenceMatcher(None, orig_asm, recomp_asm)
ratio = diff.ratio() ratio = diff.ratio()
@ -317,7 +322,9 @@ def recomp_lookup(addr: int) -> Optional[str]:
# Check whether we can resolve register swaps which are actually # Check whether we can resolve register swaps which are actually
# perfect matches modulo compiler entropy. # perfect matches modulo compiler entropy.
is_effective_match = can_resolve_register_differences(orig_asm, recomp_asm) 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: else:
is_effective_match = False is_effective_match = False
unified_diff = [] unified_diff = []
@ -352,9 +359,7 @@ def _compare_vtable(self, match: MatchInfo) -> DiffReport:
[t for (t,) in struct.iter_unpack("<L", recomp_table)], [t for (t,) in struct.iter_unpack("<L", recomp_table)],
) )
def match_text( def match_text(m: Optional[MatchInfo], raw_addr: Optional[int] = None) -> str:
i: int, m: Optional[MatchInfo], raw_addr: Optional[int] = None
) -> str:
"""Format the function reference at this vtable index as text. """Format the function reference at this vtable index as text.
If we have not identified this function, we have the option to 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 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 should override the given function from the superclass, but we have not
implemented this yet. implemented this yet.
""" """
index = f"vtable0x{i*4:02x}"
if m is not None: if m is not None:
orig = hex(m.orig_addr) if m.orig_addr is not None else "no orig" orig = hex(m.orig_addr) if m.orig_addr is not None else "no orig"
recomp = ( recomp = (
hex(m.recomp_addr) if m.recomp_addr is not None else "no 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: 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 = [] orig_text = []
recomp_text = [] recomp_text = []
@ -395,14 +399,22 @@ def match_text(
ratio += 1 ratio += 1
n_entries += 1 n_entries += 1
orig_text.append(match_text(i, orig, raw_orig)) index = f"vtable0x{i*4:02x}"
recomp_text.append(match_text(i, recomp)) 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 ratio = ratio / float(n_entries) if n_entries > 0 else 0
# n=100: Show the entire table if there is a diff to display. # n=100: Show the entire table if there is a diff to display.
# Otherwise it would be confusing if the table got cut off. # 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( return DiffReport(
match_type=SymbolType.VTABLE, match_type=SymbolType.VTABLE,

View 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

View file

@ -5,7 +5,70 @@
import colorama 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): def print_diff(udiff, plain):
"""Print diff in difflib.unified_diff format."""
if udiff is None: if udiff is None:
return False return False

605
tools/reccmp/reccmp.js Normal file
View 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' ? '&#9650;' : '&#9660;';
}
}
}
// 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' });
};

View file

@ -10,7 +10,7 @@
from isledecomp import ( from isledecomp import (
Bin, Bin,
get_file_in_script_dir, get_file_in_script_dir,
print_diff, print_combined_diff,
diff_json, diff_json,
percent_string, percent_string,
) )
@ -48,8 +48,12 @@ def gen_json(json_file: str, orig_file: str, data):
def gen_html(html_file, 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( 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: 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" f"{addrs}: {match.name} Effective 100%% match. (Differs in register allocation only)\n\n{ok_text} (still differs in register allocation)\n\n"
) )
else: else:
print_diff(match.udiff, is_plain) print_combined_diff(match.udiff, is_plain, show_both_addrs)
print( print(
f"\n{match.name} is only {percenttext} similar to the original, diff above" f"\n{match.name} is only {percenttext} similar to the original, diff above"
@ -282,7 +286,7 @@ def main():
html_obj["effective"] = True html_obj["effective"] = True
if match.udiff is not None: if match.udiff is not None:
html_obj["diff"] = "\n".join(match.udiff) html_obj["diff"] = match.udiff
if match.is_stub: if match.is_stub:
html_obj["stub"] = True html_obj["stub"] = True

View file

@ -43,15 +43,15 @@
background: #404040 !important; background: #404040 !important;
} }
.funcrow:nth-child(odd), #listing th { .funcrow:nth-child(odd of :not([hidden])), #listing > thead th {
background: #282828; background: #282828;
} }
.funcrow:nth-child(even) { .funcrow:nth-child(even of :not([hidden])) {
background: #383838; background: #383838;
} }
#listing td, #listing th { .funcrow > td, .diffRow > td, #listing > thead th {
border: 1px #f0f0f0 solid; border: 1px #f0f0f0 solid;
padding: 0.5em; padding: 0.5em;
word-break: break-all !important; word-break: break-all !important;
@ -65,233 +65,109 @@
color: #80FF80; color: #80FF80;
} }
.diffslug {
color: #8080FF;
}
.identical { .identical {
font-style: italic; font-style: italic;
text-align: center; text-align: center;
} }
#sortind { sort-indicator {
margin: 0 0.5em; margin: 0 0.5em;
} }
.filters { .filters {
align-items: top;
display: flex;
font-size: 10pt; font-size: 10pt;
text-align: center; justify-content: space-between;
margin: 0.5em 0 1em 0; 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> </style>
<script> <script>var data = {{{data}}};</script>
var data = {{{data}}}; <script>{{{reccmp_js}}}</script>
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 ? '&#9650;' : '&#9660;';
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> </script>
</head> </head>
<body> <body>
<div class="main"> <div class="main">
<h1>Decompilation Status</h1> <h1>Decompilation Status</h1>
<input id="search" type="search" placeholder="Search for offset or function name..."> <listing-table>
<div class="filters"> <input id="search" type="search" placeholder="Search for offset or function name...">
<label for="cbHidePerfect">Hide 100% match</label> <div class="filters">
<input type="checkbox" id="cbHidePerfect" /> <fieldset>
<label for="cbHideStub">Hide stubs</label> <legend>Options:</legend>
<input type="checkbox" id="cbHideStub" /> <input type="checkbox" id="cbHidePerfect" />
</div> <label for="cbHidePerfect">Hide 100% match</label>
<table id="listing"> <input type="checkbox" id="cbHideStub" />
<tr id='listingheader'><th style='width: 20%'>Address</th><th style="width:60%">Name</th><th style='width: 20%'>Matching</th></tr> <label for="cbHideStub">Hide stubs</label>
</table> </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> </div>
</body> </body>
</html> </html>

View file

@ -7,7 +7,7 @@
import colorama import colorama
from isledecomp.bin import Bin as IsleBin from isledecomp.bin import Bin as IsleBin
from isledecomp.compare import Compare as IsleCompare 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. # Ignore all compare-db messages.
logging.getLogger("isledecomp.compare").addHandler(logging.NullHandler()) logging.getLogger("isledecomp.compare").addHandler(logging.NullHandler())
@ -53,13 +53,8 @@ def parse_args() -> argparse.Namespace:
return args return args
def show_vtable_diff(udiff: List[str], verbose: bool = False, plain: bool = False): def show_vtable_diff(udiff: List, _: bool = False, plain: bool = False):
lines = [ print_combined_diff(udiff, plain)
line
for line in udiff
if verbose or line.startswith("+") or line.startswith("-")
]
print_diff(lines, plain)
def print_summary(vtable_count: int, problem_count: int): def print_summary(vtable_count: int, problem_count: int):