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

85 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Dump the guest's DECOMPRESSED executable image out of live Xenia memory.
Why this exists: the static PPC route the corpus is built on ran against a
disassembly database at `/work/xenia-rs/sylpheed.db`, and that file was **not in
the container** — the same migration that took the Xenia storage root. Without
it, every finding that cites a `sub_82xxxxxx` was unre-checkable. (The database
now lives in this repository's root as `sylpheed.db`, rebuilt from the ISO by
`sylph-xexdb`, which decrypts and decompresses the XEX itself; query it with
`tools/zq.py`. This dump remains the way to get the image a *running* guest holds.)
`/disc/default.xex` cannot substitute on its own: it is encrypted and
LZX-compressed. Its header is intact (`XEX2`, original PE name `default.pe`) and
everything after is noise — `strings` finds **zero** occurrences of `GamePart` in it.
Xenia decompresses, decrypts and relocates the image at load, so a running guest
holds exactly the flat VA image the corpus calls the `.pe`. Dump it once and the
static route works offline, with no emulator and no disc.
Validated on write, and both checks are the corpus's own, not this tool's:
* `0x820A1630` must hold the **GamePart id table** — 29 pointers into `.rdata`
resolving to `GP_TITLE` … `GP_TEST`, with `GP_CHALLENGE` at id 26
(docs/re/challenge-mission-gate.md);
* the image must contain the Xbox 360 D3D runtime's own error strings, which a
mis-based or partial dump does not.
dump_image.py [OUT.pe] # with Canary running
"""
import os
import struct
import sys
SD = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SD)
import gmem # noqa: E402
LO, HI = 0x82000000, 0x82400000
BASE = LO
EXPECT = {0: "GP_TITLE", 3: "GP_LOAD", 11: "GP_READY_ROOM", 26: "GP_CHALLENGE", 28: "GP_TEST"}
def validate(buf):
def s_at(va):
o = va - BASE
e = buf.find(b"\0", o, o + 64)
return buf[o:e].decode("ascii", "replace")
bad = []
for i, want in EXPECT.items():
p = struct.unpack_from(">I", buf, 0x820A1630 - BASE + 4 * i)[0]
got = s_at(p) if LO <= p < HI else f"<ptr {p:#x} out of range>"
if got != want:
bad.append(f"GamePart id {i}: expected {want!r}, got {got!r}")
if buf.count(b"ERR[D3D]") < 1:
bad.append("no Xbox 360 D3D runtime strings — this is not the game image")
return bad
def main(out):
path = gmem.mem_path()
off = gmem.va_to_off(LO)
with open(path, "rb") as f:
f.seek(off)
buf = f.read(HI - LO)
if len(buf) < HI - LO:
print(f"short read: {len(buf)} of {HI - LO}", file=sys.stderr)
return 1
bad = validate(buf)
for b in bad:
print("FAIL:", b, file=sys.stderr)
if bad:
return 2
open(out, "wb").write(buf)
pages = sum(1 for i in range(0, len(buf), 4096) if any(buf[i:i + 4096]))
print(f"wrote {out} {len(buf)} bytes VA {LO:#x}..{HI:#x}")
print(f"validated: GamePart id table + D3D runtime strings; "
f"{pages}/{len(buf)//4096} non-empty 4K pages")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "/sylph-home/re/sylpheed-image.pe"))