forked from Keelrleek/Harbor
next_functions: skip functions already in an open review item (by scope, or the addresses its title names), so agents are not offered work they or others have already done
This commit is contained in:
1 parent
9b6ea3bc57
commit
3d9ac4ca9a
1 file changed
+269
-252
+269
-252
@@ -1,252 +1,269 @@
|
||||
"""Which functions to decompile next, which to set aside, and how far the project is, per function.
|
||||
|
||||
- next_functions(agent): the functions in the agent's bands that are not decompiled, not deferred and not claimed by
|
||||
someone else, easiest first. Ease is scored from the bytes and the team's records: small functions, callees that
|
||||
already have names, stack-only conventions, game code, few earlier rejections and a readable near-duplicate rank
|
||||
higher. A function rejected `attempt_cap` times or more is left out (defer it or ask the coordinator).
|
||||
- defer(agent, address, reason, evidence): set a function aside with a reason slug; it becomes a note on fn:0x...
|
||||
(`DEFERRED <reason>: <evidence>`) so everyone sees it, and the queue skips it until the note is replaced.
|
||||
- progress(): every function in a tier, best first: exact (byte match), verified (behaves the same in the emulator),
|
||||
readable, deferred, raw, plus library and engine functions counted apart.
|
||||
|
||||
Configured from the project's [reverse] table (facts, annotations, src, conventions) plus optional result files:
|
||||
match_results (match71.py --json), diffrun_results (diffrun.py --json), library_results (libsweep csv)."""
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from .reverse import Reverse
|
||||
from .util import HarborError
|
||||
|
||||
DEFER_REASONS = ("register-args", "needs-type", "callee-unnamed", "inline", "x87", "layout", "too-large",
|
||||
"engine-template", "data-not-code", "other")
|
||||
READABLE = " * status: readable"
|
||||
TIER_ORDER = ("exact", "verified", "readable", "deferred", "raw")
|
||||
|
||||
|
||||
class FunctionQueue:
|
||||
def __init__(self, harbor):
|
||||
self.h = harbor
|
||||
self.config = harbor.project.reverse or {}
|
||||
self.r = Reverse(self.config)
|
||||
self.cap = int(self.config.get("attempt_cap", 2))
|
||||
self.min_size = int(self.config.get("queue_min_size", 8))
|
||||
# [[lo, hi, "label"], ...] ranked low, e.g. the compiler-generated static initializers at the end of .text
|
||||
self.low_priority = [(int(str(lo), 0), int(str(hi), 0), label)
|
||||
for lo, hi, label in self.config.get("low_priority_ranges", [])]
|
||||
|
||||
# ------------------------------------------------------------------ records
|
||||
def statuses(self):
|
||||
"""address -> (status, file name) from the headers of src/**/<ADDR>_<Name>.c."""
|
||||
out = {}
|
||||
src = self.r.src
|
||||
if not src or not src.exists():
|
||||
return out
|
||||
for path in src.rglob("*.c"):
|
||||
m = re.match(r"([0-9A-F]{6,8})_", path.name)
|
||||
if not m:
|
||||
continue
|
||||
try:
|
||||
with path.open(encoding="utf-8", errors="replace") as f:
|
||||
head = [next(f, "") for _ in range(8)]
|
||||
except (FileNotFoundError, PermissionError):
|
||||
continue # a landing on main moved or rewrote it while we scanned; its new path is found
|
||||
|
||||
status = "readable" if any(line.startswith(READABLE) for line in head) else \
|
||||
next((line.split("status:", 1)[1].strip() for line in head if "status:" in line), "raw")
|
||||
out[int(m.group(1), 16)] = (status, path.name)
|
||||
return out
|
||||
|
||||
def rejections(self):
|
||||
"""address -> how many review items on fn:0x... were rejected or sent back."""
|
||||
out = {}
|
||||
rows = self.h.conn.execute(
|
||||
"SELECT i.scope, COUNT(*) AS n FROM item_events e JOIN items i ON i.id = e.item_id "
|
||||
"WHERE e.to_state IN ('rejected', 'changes_requested') AND i.scope LIKE 'fn:0x%' GROUP BY i.scope")
|
||||
for row in rows:
|
||||
try:
|
||||
out[int(row["scope"][3:], 16)] = row["n"]
|
||||
except ValueError:
|
||||
pass
|
||||
return out
|
||||
|
||||
def deferrals(self):
|
||||
"""address -> (reason, evidence, author) for live DEFERRED notes."""
|
||||
out = {}
|
||||
for row in self.h.conn.execute("SELECT scope, body, author FROM notes WHERE replaced_by IS NULL "
|
||||
"AND body LIKE 'DEFERRED %' AND scope LIKE 'fn:0x%'"):
|
||||
m = re.match(r"DEFERRED ([\w-]+): (.*)", row["body"], re.S)
|
||||
if m:
|
||||
out[int(row["scope"][3:], 16)] = (m.group(1), m.group(2).strip(), row["author"])
|
||||
return out
|
||||
|
||||
def bands(self, agent):
|
||||
return [(row["lo"], row["hi"]) for row in self.h.conn.execute(
|
||||
"SELECT lo, hi FROM claims WHERE kind='band' AND agent=? AND released_at IS NULL", (agent,))
|
||||
if row["lo"] is not None]
|
||||
|
||||
def claimed_by_others(self, agent):
|
||||
return [(row["lo"], row["hi"]) for row in self.h.conn.execute(
|
||||
"SELECT lo, hi FROM claims WHERE kind IN ('draft','edit','naming') AND agent<>? AND released_at IS NULL",
|
||||
(agent,)) if row["lo"] is not None]
|
||||
|
||||
def _named(self):
|
||||
names = self.r._csv("names.csv")
|
||||
return {a for a, row in names.items() if len(row) > 1 and row[1] and not re.match(r"(FUN|sub|thunk)_", row[1])}
|
||||
|
||||
def _category(self, addr):
|
||||
row = self.r._csv("names.csv").get(addr)
|
||||
if row and len(row) > 2 and row[2]:
|
||||
return row[2]
|
||||
return (self.r._facts().get(addr) or {}).get("category", "unsorted")
|
||||
|
||||
def _results(self, key):
|
||||
path = self.config.get(key)
|
||||
if not path or not Path(path).exists():
|
||||
return {}
|
||||
if str(path).endswith(".csv"):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return {int(row["address"], 16): row for row in csv.DictReader(f)}
|
||||
return {int(row["address"], 16): row for row in json.loads(Path(path).read_text(encoding="utf-8"))}
|
||||
|
||||
def similar_readable(self):
|
||||
"""Raw functions with a readable near-duplicate (from the [reverse] similar_clusters file)."""
|
||||
path = self.config.get("similar_clusters")
|
||||
if not path or not Path(path).exists():
|
||||
return frozenset()
|
||||
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
return frozenset(int(m["address"], 16) for c in data.get("clusters", []) for m in c.get("members", [])
|
||||
if m.get("best_readable") and m.get("status") != "readable")
|
||||
|
||||
def library_ranges(self):
|
||||
"""[(start, end)] of statically linked library blocks (the [reverse] library_ranges csv)."""
|
||||
path = self.config.get("library_ranges")
|
||||
if not path or not Path(path).exists():
|
||||
return []
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return [(int(r["start"], 16), int(r["end"], 16)) for r in csv.DictReader(f)
|
||||
if r.get("verdict", "").startswith("D3DX") or "stubs" in r.get("verdict", "")]
|
||||
# the D3DX9 block, and the compiler's import thunks and exception-unwind funclets: nothing to decompile
|
||||
|
||||
def is_library(self, addr, library, ranges):
|
||||
return addr in library or any(lo <= addr < hi for lo, hi in ranges)
|
||||
|
||||
# ------------------------------------------------------------------ queue
|
||||
def score(self, addr, facts, named, rejected, similar_readable=frozenset(), with_convention=True):
|
||||
d = facts[addr]
|
||||
size = int(d.get("size") or 0)
|
||||
calls = [int(c, 16) if isinstance(c, str) else c for c in d.get("calls", [])]
|
||||
named_share = (sum(1 for c in calls if c in named) / len(calls)) if calls else 1.0
|
||||
score = 1.0 / (1.0 + size / 250.0) # small first
|
||||
score *= 0.5 + 0.5 * named_share # callees with names make readable code possible
|
||||
conv = self.r.convention(addr) if with_convention and size <= 4000 else None
|
||||
if conv and conv["summary"].startswith("custom"):
|
||||
score *= 0.75 # register arguments: prototypes.h work too
|
||||
if self._category(addr) not in ("game", "unsorted"):
|
||||
score *= 0.6
|
||||
score *= 0.5 ** rejected.get(addr, 0)
|
||||
low = next((label for lo, hi, label in self.low_priority if lo <= addr < hi), None)
|
||||
if low:
|
||||
score *= 0.2 # e.g. the compiler's static initializers
|
||||
reasons_low = [low]
|
||||
else:
|
||||
reasons_low = []
|
||||
if addr in similar_readable:
|
||||
score *= 1.5 # a readable near-duplicate to start from
|
||||
reasons = [f"{size} bytes", f"{round(named_share * 100)}% of {len(calls)} callees named"]
|
||||
if conv:
|
||||
reasons.append(conv["summary"])
|
||||
if rejected.get(addr):
|
||||
reasons.append(f"rejected {rejected[addr]}x")
|
||||
if addr in similar_readable:
|
||||
reasons.append("has a readable near-duplicate")
|
||||
reasons += reasons_low
|
||||
return score, reasons
|
||||
|
||||
def next_functions(self, agent, n=10, similar_readable=frozenset()):
|
||||
bands = self.bands(agent)
|
||||
if not bands:
|
||||
raise HarborError(f"{agent} has no band; ask the coordinator for one")
|
||||
taken = self.claimed_by_others(agent)
|
||||
statuses, deferred, rejected = self.statuses(), self.deferrals(), self.rejections()
|
||||
facts, named = self.r._facts(), self._named()
|
||||
library = self._results("library_results")
|
||||
ranges = self.library_ranges()
|
||||
ranked = []
|
||||
for addr in facts:
|
||||
if not any(lo <= addr <= hi for lo, hi in bands) or any(lo <= addr <= hi for lo, hi in taken):
|
||||
continue
|
||||
if statuses.get(addr, ("raw",))[0] == "readable" or addr in deferred or self.is_library(addr, library, ranges):
|
||||
continue
|
||||
if rejected.get(addr, 0) >= self.cap:
|
||||
continue
|
||||
if self._category(addr) in ("library", "platform"):
|
||||
continue
|
||||
if int(facts[addr].get("size") or 0) < self.min_size or \
|
||||
re.match(r"(nullsub|thunk)_", self.r.name_of(addr) or "") or \
|
||||
re.match(r"thunk_", (statuses.get(addr) or (None, ""))[1].split("_", 1)[-1]):
|
||||
continue # stubs and thunks are named in bulk, not decompiled
|
||||
head = self.r.read(addr, 10) if int(facts[addr].get("size") or 0) == 10 else b""
|
||||
if head[:1] and 0xB8 <= head[0] <= 0xBF and head[5:6] == b"\xe9":
|
||||
continue # mov reg, imm32; jmp: an at-exit destructor thunk or an SEH handler stub (compiler's)
|
||||
score, reasons = self.score(addr, facts, named, rejected, similar_readable, with_convention=False)
|
||||
ranked.append((score, addr, reasons))
|
||||
ranked.sort(reverse=True)
|
||||
# the convention read from the bytes only lowers a score, and it is the slow part: read it for the head only
|
||||
head = [(*self.score(a, facts, named, rejected, similar_readable), a) for _, a, _ in ranked[:max(4 * n, 40)]]
|
||||
ranked = sorted(((s, a, why) for s, why, a in head), reverse=True)
|
||||
return [{"address": f"0x{a:X}", "name": self.r.name_of(a), "score": round(s, 4), "why": ", ".join(why),
|
||||
"file": statuses.get(a, (None, None))[1]} for s, a, why in ranked[:n]]
|
||||
|
||||
def defer(self, agent, address, reason, evidence):
|
||||
if reason not in DEFER_REASONS:
|
||||
raise HarborError("reason must be one of " + ", ".join(DEFER_REASONS))
|
||||
if not (evidence or "").strip():
|
||||
raise HarborError("say what you found (evidence), so the next person does not start from zero")
|
||||
addr = self.r.resolve(address)
|
||||
func = self.r.function_at(addr)
|
||||
start = func[0] if func else addr
|
||||
return self.h.note(agent, f"fn:0x{start:X}", f"DEFERRED {reason}: {evidence.strip()}")
|
||||
|
||||
# ------------------------------------------------------------------ progress
|
||||
def progress(self):
|
||||
facts, statuses, deferred = self.r._facts(), self.statuses(), self.deferrals()
|
||||
exact = {a for a, row in self._results("match_results").items()
|
||||
if str(row.get("status")) in ("match", "exact", "exact-relaxed")}
|
||||
# behaves the same in the emulator, with at least one trial that ran to the end in both (not 'weak')
|
||||
verified = {a for a, row in self._results("diffrun_results").items()
|
||||
if str(row.get("verdict")) == "same" and not row.get("weak")}
|
||||
library = set(self._results("library_results"))
|
||||
ranges = self.library_ranges()
|
||||
tiers = {t: {"functions": 0, "bytes": 0} for t in TIER_ORDER + ("library", "engine")}
|
||||
for addr, d in facts.items():
|
||||
size = int(d.get("size") or 0)
|
||||
category = self._category(addr)
|
||||
readable = statuses.get(addr, ("raw",))[0] == "readable"
|
||||
if self.is_library(addr, library, ranges) or category in ("library", "platform", "crt"):
|
||||
tier = "library"
|
||||
elif category == "engine" and not readable:
|
||||
tier = "engine"
|
||||
elif addr in exact:
|
||||
tier = "exact"
|
||||
elif addr in verified:
|
||||
tier = "verified"
|
||||
elif readable:
|
||||
tier = "readable"
|
||||
elif addr in deferred:
|
||||
tier = "deferred"
|
||||
else:
|
||||
tier = "raw"
|
||||
tiers[tier]["functions"] += 1
|
||||
tiers[tier]["bytes"] += size
|
||||
sources = {k: bool(self.config.get(k) and Path(self.config[k]).exists())
|
||||
for k in ("match_results", "diffrun_results", "library_results")}
|
||||
return {"tiers": tiers, "deferred_reasons": _count(r for r, _, _ in deferred.values()), "sources": sources}
|
||||
|
||||
|
||||
def _count(values):
|
||||
out = {}
|
||||
for v in values:
|
||||
out[v] = out.get(v, 0) + 1
|
||||
return out
|
||||
"""Which functions to decompile next, which to set aside, and how far the project is, per function.
|
||||
|
||||
- next_functions(agent): the functions in the agent's bands that are not decompiled, not deferred and not claimed by
|
||||
someone else, easiest first. Ease is scored from the bytes and the team's records: small functions, callees that
|
||||
already have names, stack-only conventions, game code, few earlier rejections and a readable near-duplicate rank
|
||||
higher. A function rejected `attempt_cap` times or more is left out (defer it or ask the coordinator).
|
||||
- defer(agent, address, reason, evidence): set a function aside with a reason slug; it becomes a note on fn:0x...
|
||||
(`DEFERRED <reason>: <evidence>`) so everyone sees it, and the queue skips it until the note is replaced.
|
||||
- progress(): every function in a tier, best first: exact (byte match), verified (behaves the same in the emulator),
|
||||
readable, deferred, raw, plus library and engine functions counted apart.
|
||||
|
||||
Configured from the project's [reverse] table (facts, annotations, src, conventions) plus optional result files:
|
||||
match_results (match71.py --json), diffrun_results (diffrun.py --json), library_results (libsweep csv)."""
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from .reverse import Reverse
|
||||
from .util import HarborError, parse_scope
|
||||
|
||||
DEFER_REASONS = ("register-args", "needs-type", "callee-unnamed", "inline", "x87", "layout", "too-large",
|
||||
"engine-template", "data-not-code", "other")
|
||||
READABLE = " * status: readable"
|
||||
TIER_ORDER = ("exact", "verified", "readable", "deferred", "raw")
|
||||
|
||||
|
||||
class FunctionQueue:
|
||||
def __init__(self, harbor):
|
||||
self.h = harbor
|
||||
self.config = harbor.project.reverse or {}
|
||||
self.r = Reverse(self.config)
|
||||
self.cap = int(self.config.get("attempt_cap", 2))
|
||||
self.min_size = int(self.config.get("queue_min_size", 8))
|
||||
# [[lo, hi, "label"], ...] ranked low, e.g. the compiler-generated static initializers at the end of .text
|
||||
self.low_priority = [(int(str(lo), 0), int(str(hi), 0), label)
|
||||
for lo, hi, label in self.config.get("low_priority_ranges", [])]
|
||||
|
||||
# ------------------------------------------------------------------ records
|
||||
def statuses(self):
|
||||
"""address -> (status, file name) from the headers of src/**/<ADDR>_<Name>.c."""
|
||||
out = {}
|
||||
src = self.r.src
|
||||
if not src or not src.exists():
|
||||
return out
|
||||
for path in src.rglob("*.c"):
|
||||
m = re.match(r"([0-9A-F]{6,8})_", path.name)
|
||||
if not m:
|
||||
continue
|
||||
try:
|
||||
with path.open(encoding="utf-8", errors="replace") as f:
|
||||
head = [next(f, "") for _ in range(8)]
|
||||
except (FileNotFoundError, PermissionError):
|
||||
continue # a landing on main moved or rewrote it while we scanned; its new path is found
|
||||
|
||||
status = "readable" if any(line.startswith(READABLE) for line in head) else \
|
||||
next((line.split("status:", 1)[1].strip() for line in head if "status:" in line), "raw")
|
||||
out[int(m.group(1), 16)] = (status, path.name)
|
||||
return out
|
||||
|
||||
def rejections(self):
|
||||
"""address -> how many review items on fn:0x... were rejected or sent back."""
|
||||
out = {}
|
||||
rows = self.h.conn.execute(
|
||||
"SELECT i.scope, COUNT(*) AS n FROM item_events e JOIN items i ON i.id = e.item_id "
|
||||
"WHERE e.to_state IN ('rejected', 'changes_requested') AND i.scope LIKE 'fn:0x%' GROUP BY i.scope")
|
||||
for row in rows:
|
||||
try:
|
||||
out[int(row["scope"][3:], 16)] = row["n"]
|
||||
except ValueError:
|
||||
pass
|
||||
return out
|
||||
|
||||
def deferrals(self):
|
||||
"""address -> (reason, evidence, author) for live DEFERRED notes."""
|
||||
out = {}
|
||||
for row in self.h.conn.execute("SELECT scope, body, author FROM notes WHERE replaced_by IS NULL "
|
||||
"AND body LIKE 'DEFERRED %' AND scope LIKE 'fn:0x%'"):
|
||||
m = re.match(r"DEFERRED ([\w-]+): (.*)", row["body"], re.S)
|
||||
if m:
|
||||
out[int(row["scope"][3:], 16)] = (m.group(1), m.group(2).strip(), row["author"])
|
||||
return out
|
||||
|
||||
def bands(self, agent):
|
||||
return [(row["lo"], row["hi"]) for row in self.h.conn.execute(
|
||||
"SELECT lo, hi FROM claims WHERE kind='band' AND agent=? AND released_at IS NULL", (agent,))
|
||||
if row["lo"] is not None]
|
||||
|
||||
def claimed_by_others(self, agent):
|
||||
return [(row["lo"], row["hi"]) for row in self.h.conn.execute(
|
||||
"SELECT lo, hi FROM claims WHERE kind IN ('draft','edit','naming') AND agent<>? AND released_at IS NULL",
|
||||
(agent,)) if row["lo"] is not None]
|
||||
|
||||
def in_review(self):
|
||||
"""(lo, hi) of every item not yet landed or closed, from any author: work already done on a branch"""
|
||||
out = []
|
||||
for row in self.h.conn.execute("SELECT scope, title FROM items WHERE state IN "
|
||||
"('submitted','in_review','approved','changes_requested','held')"):
|
||||
lo = None
|
||||
if row["scope"]:
|
||||
try:
|
||||
_, lo, hi = parse_scope(row["scope"])
|
||||
except HarborError:
|
||||
lo = None
|
||||
if lo is not None:
|
||||
out.append((lo, hi))
|
||||
else: # no scope given: the addresses its title names
|
||||
out += [(int(a, 16), int(a, 16)) for a in re.findall(r"\b0x([0-9A-Fa-f]{6})\b", row["title"] or "")]
|
||||
return out
|
||||
|
||||
def _named(self):
|
||||
names = self.r._csv("names.csv")
|
||||
return {a for a, row in names.items() if len(row) > 1 and row[1] and not re.match(r"(FUN|sub|thunk)_", row[1])}
|
||||
|
||||
def _category(self, addr):
|
||||
row = self.r._csv("names.csv").get(addr)
|
||||
if row and len(row) > 2 and row[2]:
|
||||
return row[2]
|
||||
return (self.r._facts().get(addr) or {}).get("category", "unsorted")
|
||||
|
||||
def _results(self, key):
|
||||
path = self.config.get(key)
|
||||
if not path or not Path(path).exists():
|
||||
return {}
|
||||
if str(path).endswith(".csv"):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return {int(row["address"], 16): row for row in csv.DictReader(f)}
|
||||
return {int(row["address"], 16): row for row in json.loads(Path(path).read_text(encoding="utf-8"))}
|
||||
|
||||
def similar_readable(self):
|
||||
"""Raw functions with a readable near-duplicate (from the [reverse] similar_clusters file)."""
|
||||
path = self.config.get("similar_clusters")
|
||||
if not path or not Path(path).exists():
|
||||
return frozenset()
|
||||
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
return frozenset(int(m["address"], 16) for c in data.get("clusters", []) for m in c.get("members", [])
|
||||
if m.get("best_readable") and m.get("status") != "readable")
|
||||
|
||||
def library_ranges(self):
|
||||
"""[(start, end)] of statically linked library blocks (the [reverse] library_ranges csv)."""
|
||||
path = self.config.get("library_ranges")
|
||||
if not path or not Path(path).exists():
|
||||
return []
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return [(int(r["start"], 16), int(r["end"], 16)) for r in csv.DictReader(f)
|
||||
if r.get("verdict", "").startswith("D3DX") or "stubs" in r.get("verdict", "")]
|
||||
# the D3DX9 block, and the compiler's import thunks and exception-unwind funclets: nothing to decompile
|
||||
|
||||
def is_library(self, addr, library, ranges):
|
||||
return addr in library or any(lo <= addr < hi for lo, hi in ranges)
|
||||
|
||||
# ------------------------------------------------------------------ queue
|
||||
def score(self, addr, facts, named, rejected, similar_readable=frozenset(), with_convention=True):
|
||||
d = facts[addr]
|
||||
size = int(d.get("size") or 0)
|
||||
calls = [int(c, 16) if isinstance(c, str) else c for c in d.get("calls", [])]
|
||||
named_share = (sum(1 for c in calls if c in named) / len(calls)) if calls else 1.0
|
||||
score = 1.0 / (1.0 + size / 250.0) # small first
|
||||
score *= 0.5 + 0.5 * named_share # callees with names make readable code possible
|
||||
conv = self.r.convention(addr) if with_convention and size <= 4000 else None
|
||||
if conv and conv["summary"].startswith("custom"):
|
||||
score *= 0.75 # register arguments: prototypes.h work too
|
||||
if self._category(addr) not in ("game", "unsorted"):
|
||||
score *= 0.6
|
||||
score *= 0.5 ** rejected.get(addr, 0)
|
||||
low = next((label for lo, hi, label in self.low_priority if lo <= addr < hi), None)
|
||||
if low:
|
||||
score *= 0.2 # e.g. the compiler's static initializers
|
||||
reasons_low = [low]
|
||||
else:
|
||||
reasons_low = []
|
||||
if addr in similar_readable:
|
||||
score *= 1.5 # a readable near-duplicate to start from
|
||||
reasons = [f"{size} bytes", f"{round(named_share * 100)}% of {len(calls)} callees named"]
|
||||
if conv:
|
||||
reasons.append(conv["summary"])
|
||||
if rejected.get(addr):
|
||||
reasons.append(f"rejected {rejected[addr]}x")
|
||||
if addr in similar_readable:
|
||||
reasons.append("has a readable near-duplicate")
|
||||
reasons += reasons_low
|
||||
return score, reasons
|
||||
|
||||
def next_functions(self, agent, n=10, similar_readable=frozenset()):
|
||||
bands = self.bands(agent)
|
||||
if not bands:
|
||||
raise HarborError(f"{agent} has no band; ask the coordinator for one")
|
||||
taken = self.claimed_by_others(agent) + self.in_review() # claimed elsewhere, or already in an item
|
||||
statuses, deferred, rejected = self.statuses(), self.deferrals(), self.rejections()
|
||||
facts, named = self.r._facts(), self._named()
|
||||
library = self._results("library_results")
|
||||
ranges = self.library_ranges()
|
||||
ranked = []
|
||||
for addr in facts:
|
||||
if not any(lo <= addr <= hi for lo, hi in bands) or any(lo <= addr <= hi for lo, hi in taken):
|
||||
continue
|
||||
if statuses.get(addr, ("raw",))[0] == "readable" or addr in deferred or self.is_library(addr, library, ranges):
|
||||
continue
|
||||
if rejected.get(addr, 0) >= self.cap:
|
||||
continue
|
||||
if self._category(addr) in ("library", "platform"):
|
||||
continue
|
||||
if int(facts[addr].get("size") or 0) < self.min_size or \
|
||||
re.match(r"(nullsub|thunk)_", self.r.name_of(addr) or "") or \
|
||||
re.match(r"thunk_", (statuses.get(addr) or (None, ""))[1].split("_", 1)[-1]):
|
||||
continue # stubs and thunks are named in bulk, not decompiled
|
||||
head = self.r.read(addr, 10) if int(facts[addr].get("size") or 0) == 10 else b""
|
||||
if head[:1] and 0xB8 <= head[0] <= 0xBF and head[5:6] == b"\xe9":
|
||||
continue # mov reg, imm32; jmp: an at-exit destructor thunk or an SEH handler stub (compiler's)
|
||||
score, reasons = self.score(addr, facts, named, rejected, similar_readable, with_convention=False)
|
||||
ranked.append((score, addr, reasons))
|
||||
ranked.sort(reverse=True)
|
||||
# the convention read from the bytes only lowers a score, and it is the slow part: read it for the head only
|
||||
head = [(*self.score(a, facts, named, rejected, similar_readable), a) for _, a, _ in ranked[:max(4 * n, 40)]]
|
||||
ranked = sorted(((s, a, why) for s, why, a in head), reverse=True)
|
||||
return [{"address": f"0x{a:X}", "name": self.r.name_of(a), "score": round(s, 4), "why": ", ".join(why),
|
||||
"file": statuses.get(a, (None, None))[1]} for s, a, why in ranked[:n]]
|
||||
|
||||
def defer(self, agent, address, reason, evidence):
|
||||
if reason not in DEFER_REASONS:
|
||||
raise HarborError("reason must be one of " + ", ".join(DEFER_REASONS))
|
||||
if not (evidence or "").strip():
|
||||
raise HarborError("say what you found (evidence), so the next person does not start from zero")
|
||||
addr = self.r.resolve(address)
|
||||
func = self.r.function_at(addr)
|
||||
start = func[0] if func else addr
|
||||
return self.h.note(agent, f"fn:0x{start:X}", f"DEFERRED {reason}: {evidence.strip()}")
|
||||
|
||||
# ------------------------------------------------------------------ progress
|
||||
def progress(self):
|
||||
facts, statuses, deferred = self.r._facts(), self.statuses(), self.deferrals()
|
||||
exact = {a for a, row in self._results("match_results").items()
|
||||
if str(row.get("status")) in ("match", "exact", "exact-relaxed")}
|
||||
# behaves the same in the emulator, with at least one trial that ran to the end in both (not 'weak')
|
||||
verified = {a for a, row in self._results("diffrun_results").items()
|
||||
if str(row.get("verdict")) == "same" and not row.get("weak")}
|
||||
library = set(self._results("library_results"))
|
||||
ranges = self.library_ranges()
|
||||
tiers = {t: {"functions": 0, "bytes": 0} for t in TIER_ORDER + ("library", "engine")}
|
||||
for addr, d in facts.items():
|
||||
size = int(d.get("size") or 0)
|
||||
category = self._category(addr)
|
||||
readable = statuses.get(addr, ("raw",))[0] == "readable"
|
||||
if self.is_library(addr, library, ranges) or category in ("library", "platform", "crt"):
|
||||
tier = "library"
|
||||
elif category == "engine" and not readable:
|
||||
tier = "engine"
|
||||
elif addr in exact:
|
||||
tier = "exact"
|
||||
elif addr in verified:
|
||||
tier = "verified"
|
||||
elif readable:
|
||||
tier = "readable"
|
||||
elif addr in deferred:
|
||||
tier = "deferred"
|
||||
else:
|
||||
tier = "raw"
|
||||
tiers[tier]["functions"] += 1
|
||||
tiers[tier]["bytes"] += size
|
||||
sources = {k: bool(self.config.get(k) and Path(self.config[k]).exists())
|
||||
for k in ("match_results", "diffrun_results", "library_results")}
|
||||
return {"tiers": tiers, "deferred_reasons": _count(r for r, _, _ in deferred.values()), "sources": sources}
|
||||
|
||||
|
||||
def _count(values):
|
||||
out = {}
|
||||
for v in values:
|
||||
out[v] = out.get(v, 0) + 1
|
||||
return out
|
||||
Reference in new issue
Block a user