Files
Sylpheed/tools/re-capture/unit_discover.py
sim e909c7c133 chore: retire the last dead paths and names from the consolidation
Nothing here changes what a tool computes; it changes where tools look.

- tools/re-capture: 33 censuses globbed /work/sylph_extract, a path that has
  existed nowhere since /work became a clone, so they matched nothing and
  printed empty results. They now resolve the disc through a new disc.py
  from $SYLPHEED_DISC and exit loudly without it (the #44 fix, generalised).
  Nine scripts that imported siblings from the retired Reborn checkout or an
  old session scratchpad now import from their own directory. unitgroup.py
  only needs the variable when --pak is not given.
- sylpheed-xex: the loader only ever uses the XEX2 retail key. The dead
  devkit key and a doc comment claiming a devkit fallback that does not
  exist are gone; Project Sylpheed is a retail XEX2, so no XEX1 key either.
- sylpheed-viewer: real_font_rasterizes looked for /tmp/sylph_extract and so
  always skipped. It reads $SYLPHEED_DISC now, and passes against the disc.
- Comments and docs that named xenia-rs, the Reborn repository or /work/*.pe
  as places to look now name sylpheed.db, Canary's ppc_context.h and the
  flat .pe; docs/re/README.md no longer says the native Canary build does not
  run.

Historical records keep their original paths: findings that were measured
against /work/xenia-rs/sylpheed.db still say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:30:28 +02:00

124 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Discover the runtime class that carries the parsed `UN_*` unit definitions.
No vtable VA is assumed. Procedure:
1. locate every disc unit-ID string in the snapshot;
2. find every 4-byte-aligned word in RAM that points at (string_va - d) for a
few plausible name-record deltas;
3. for each such pointer site P and each small back-offset k, read the word at
P-k and tally it across *distinct* unit IDs.
A word that appears at the same (d, k) for many different units is that class's
vtable pointer, and k is the ID-pointer offset inside the object.
"""
import os
import re
import struct
import sys
from collections import defaultdict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gmem # noqa: E402
DELTAS = [0x00, 0x10, 0x08, 0x04, 0x0C, 0x14, 0x18, 0x20]
BACK = list(range(0, 0x60, 4))
def load_ids(path):
ids = []
for line in open(path):
if line.startswith("REC "):
p = line.split()
if p[3] == "Generic" and p[4].startswith("UN_"):
ids.append(p[4])
return sorted(set(ids))
def scan_strings(f, size, ids):
"""{id: [va,...]} — every NUL-terminated occurrence of each id string."""
pats = {i: (i.encode() + b"\0") for i in ids}
rx = re.compile(b"|".join(re.escape(p) for p in pats.values()))
out = defaultdict(list)
for a, b in gmem.extents(f.fileno(), size):
f.seek(a)
blob = f.read(b - a)
for m in rx.finditer(blob):
off = a + m.start()
va = gmem.primary_va(off)
if va is not None:
out[m.group(0)[:-1].decode()].append(va)
return out
def scan_pointers(f, size, targets):
"""{target_va: [site_va,...]} for every aligned word equal to a target."""
want = {struct.pack(">I", t): t for t in targets}
rx = re.compile(b"|".join(re.escape(k) for k in want))
out = defaultdict(list)
for a, b in gmem.extents(f.fileno(), size):
f.seek(a)
blob = f.read(b - a)
for m in rx.finditer(blob):
off = a + m.start()
if off % 4:
continue
va = gmem.primary_va(off)
if va is not None:
out[want[m.group(0)]].append(va)
return out
def main():
ids = load_ids(sys.argv[1])
path = gmem.mem_path()
size = os.path.getsize(path)
with open(path, "rb") as f:
strs = scan_strings(f, size, ids)
print(f"# {len(ids)} disc unit IDs, {len(strs)} found in RAM "
f"({sum(len(v) for v in strs.values())} occurrences)", file=sys.stderr)
# every (id, string_va - delta) we want pointers to
targets, owner = set(), defaultdict(set)
for uid, vas in strs.items():
for va in vas:
for d in DELTAS:
t = va - d
targets.add(t)
owner[t].add((uid, d))
ptrs = scan_pointers(f, size, targets)
print(f"# {len(ptrs)} of {len(targets)} candidate targets are pointed at",
file=sys.stderr)
# tally (delta, back-offset, word) over distinct unit IDs
tally = defaultdict(set)
sites = defaultdict(list)
for tgt, plist in ptrs.items():
for uid, d in owner[tgt]:
for p in plist:
for k in BACK:
base = p - k
try:
f.seek(gmem.va_to_off(base))
except ValueError:
continue
w = f.read(4)
if len(w) < 4:
continue
(vt,) = struct.unpack(">I", w)
if 0x82000000 <= vt < 0x83000000:
tally[(d, k, vt)].add(uid)
sites[(d, k, vt)].append((base, uid))
rank = sorted(tally.items(), key=lambda kv: -len(kv[1]))
print(f"{'delta':>6} {'idoff':>6} {'word':>10} {'#ids':>5}")
for (d, k, vt), s in rank[:25]:
print(f"{d:#6x} {k:#6x} {vt:#010x} {len(s):5d} e.g. {sorted(s)[:2]}")
if rank:
best = rank[0][0]
print("\n# object bases for the top candidate (first 20):", file=sys.stderr)
for base, uid in sorted(set(sites[best]))[:20]:
print(f"# {base:#010x} {uid}", file=sys.stderr)
if __name__ == "__main__":
main()