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>
85 lines
3.2 KiB
Python
Executable File
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"))
|