Files
Sylpheed/tools/re-capture/ratc_opt_name_census.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

77 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Every `opt ` name in every RATC bundle on the disc, by extension -- and the
`.tan` frame sequences among them.
Written to close the reach caveat in docs/re/structures/ratc-child-names.md:
60 of 18 002 RATC children carry no `opt ` block of their own. They are not
children. They are the ten frames of the disc's only `.tan` resource, and one
`opt ` block names the whole run.
python3 tools/re-capture/ratc_opt_name_census.py
Reads $SYLPHEED_DISC/dat/*.pak directly (IPFB TOC + Z1/zlib entries), so it does
not depend on the Rust parser it is checking. Takes a few minutes.
"""
import struct, zlib, glob, os, collections, bisect
from disc import disc_root
MAG = (b"T8aD", b"RATC", b"ttcf", b"\x89PNG")
ext = collections.Counter()
tan_sites = []
opt_total = 0
DISC = disc_root()
for pakpath in sorted(glob.glob(f"{DISC}/dat/*.pak")):
base = pakpath[:-4]
pak = open(pakpath, "rb").read()
if pak[:4] != b"IPFB": continue
n = struct.unpack_from(">I", pak, 4)[0]
toc = [struct.unpack_from(">III", pak, 0x10 + 12*i) for i in range(n)]
segs = sorted(glob.glob(base + ".p[0-9][0-9]"))
if not segs: continue
data = b"".join(open(s, "rb").read() for s in segs)
for ei, (h, off, cs) in enumerate(toc):
raw = data[off:off+cs]
try:
b = zlib.decompress(raw[10:]) if raw[:2] == b"Z1" else raw
except Exception:
continue
if b[:4] != b"RATC": continue
names = []
p = b.find(b"opt ")
while p >= 0:
ln = struct.unpack_from(">I", b, p+4)[0] if p+8 <= len(b) else 0
if 0 < ln <= 64 and p+8+ln <= len(b):
nm = b[p+8:p+8+ln].decode('latin1', 'replace')
if nm and all(32 < ord(c) < 127 for c in nm):
names.append((p, nm)); opt_total += 1
ext[os.path.splitext(nm)[1].lower()] += 1
p = b.find(b"opt ", p+4)
offs, i = [], 4
while i + 4 <= len(b):
if b[i:i+4] in MAG:
offs.append(i); i += 4
else: i += 1
if not names or not offs: continue
npos = [p for p, _ in names]
# each child -> index of the nearest preceding opt
owner = collections.defaultdict(list)
for k, o in enumerate(offs):
j = bisect.bisect_left(npos, o) - 1
if j >= 0: owner[j].append(k)
for j, (p, nm) in enumerate(names):
if not nm.lower().endswith(".tan"): continue
ks = owner.get(j, [])
if not ks: continue
sizes = sorted({(offs[k+1] if k+1 < len(offs) else len(b)) - offs[k] for k in ks})
tan_sites.append((os.path.basename(pakpath), ei, nm, len(ks), sizes))
print(f"`opt ` blocks disc-wide: {opt_total}")
print("\nby extension:")
for e, c in ext.most_common(25):
print(f" {e or '(none)':10} x{c}")
print(f"\n.tan resources with >=1 child: {len(tan_sites)}")
seen = collections.Counter()
for t in tan_sites:
seen[(t[0], t[2], t[3], tuple(t[4]))] += 1
for (pk, nm, fr, sz), c in sorted(seen.items()):
print(f" {pk:24} {nm:30} frames={fr:3} sizes={list(sz)} x{c} bundles")