#!/usr/bin/env python3
"""Analyze an instrumented xbox_auth_disc capture folder or zip.

This is for captures generated by xbox_auth_disc_instrumented.exe. It verifies
locked->unlocked capacity and prints/exports the specific candidate byte timeline
that matters for the GDR-8050L auth transition.
"""
from __future__ import annotations
import argparse, csv, json, shutil, tempfile, zipfile
from pathlib import Path

SNAPS = [
    ("s00", "s00_locked_idle"),
    ("s01", "s01_after_read_capacity_locked"),
    ("s02", "s02_after_mode_sense_pre"),
    ("s03", "s03_after_read_dvd_structure_c0"),
    ("s04", "s04_after_mode_select_challenge1"),
    ("s05", "s05_after_mode_sense_challenge1"),
    ("s06", "s06_after_mode_select_challenge2"),
    ("s07", "s07_after_mode_sense_challenge2"),
    ("s08", "s08_after_mode_select_partition1"),
    ("s09", "s09_after_final_mode_sense"),
    ("s11", "s11_after_final_read_capacity"),
]
ADDRS = [
    (0x0144,"transient scheduler/event flag"),
    (0x05B1,"demoted packet/status flag"),
    (0x05CD,"A: auth transition candidate, clears at partition unlock"),
    (0x05CE,"neighbor cleared with 0x05CD"),
    (0x05E2,"transfer length/status byte"),
    (0x071E,"A: state advances 01->02 at partition unlock"),
    (0x0722,"A-: state slot 00->01 at partition unlock"),
    (0x0724,"challenge1 bookkeeping/control"),
    (0x076D,"B: persistent session state byte"),
    (0x076E,"B: persistent session state byte"),
    (0x077C,"B: aggregate/session word byte 0"),
    (0x077D,"B: aggregate/session word byte 1"),
    (0x07E8,"packet mirror byte 0"),
    (0x07EB,"packet mirror byte 3"),
    (0x07EC,"packet mirror byte 4"),
    (0x07ED,"packet mirror byte 5"),
    (0x07EE,"packet mirror byte 6"),
    (0x07F8,"A-: status word low byte, 0x8444 marker"),
    (0x07F9,"A-: status word high byte, 0x8444 marker"),
    (0xD95C,"C: hardware/MMIO status"),
    (0xDF30,"host CDB/mirror byte 0"),
    (0xDF33,"host CDB/mirror byte 3"),
    (0xDF34,"host CDB/mirror byte 4"),
    (0xDF35,"host CDB/mirror byte 5"),
    (0xDF36,"host CDB/mirror byte 6"),
]
PC_HINTS = {
    "0x05CD": "0x9001BC3B movbu d0,(0x5cd), preceded by clr d0",
    "0x071E": "0x9001D086 movbu d0,(0x71e), d0=2 state path",
    "0x0722": "0x9001CFCA movbu d1,(0x722), selector d0==2 path",
    "0x077C": "0x9001EF66 mov d1,(0x77c), 32-bit aggregate update",
    "0x07F8": "0x9001EE4A movhu d0,(0x7f8), immediate d0=0x8444",
    "0x07F9": "0x9001EE4A movhu d0,(0x7f8), immediate d0=0x8444",
}

def be32(b: bytes) -> int:
    return int.from_bytes(b[:4], "big")

def hexs(b: bytes) -> str:
    return " ".join(f"{x:02X}" for x in b)

def unpack_capture(path: Path) -> tuple[Path, tempfile.TemporaryDirectory | None]:
    if path.is_dir():
        # accept either capture folder or its parent
        if (path / "instrumented_auth.log").exists():
            return path, None
        kids = [p for p in path.iterdir() if p.is_dir() and (p / "instrumented_auth.log").exists()]
        if kids:
            return kids[0], None
        return path, None
    td = tempfile.TemporaryDirectory(prefix="gdr_auth_capture_")
    root = Path(td.name)
    with zipfile.ZipFile(path) as z:
        z.extractall(root)
    kids = [p for p in root.rglob("instrumented_auth.log")]
    if not kids:
        raise SystemExit(f"[-] no instrumented_auth.log found in {path}")
    return kids[0].parent, td

def find_dump(root: Path, stem: str) -> Path | None:
    for suffix in ("_low4m.bin", "_low64k.bin"):
        p = root / f"{stem}{suffix}"
        if p.exists(): return p
    return None

def read_byte(root: Path, stem: str, addr: int) -> str:
    p = find_dump(root, stem)
    if not p: return "--"
    data = p.read_bytes()
    if addr >= len(data): return "--"
    return f"{data[addr]:02X}"

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("capture", type=Path, help="capture folder or .zip")
    ap.add_argument("--out-dir", type=Path, default=Path("auth_transition_analysis"))
    ns = ap.parse_args()
    root, td = unpack_capture(ns.capture)
    ns.out_dir.mkdir(parents=True, exist_ok=True)

    s01 = root / "s01_data_in.bin"
    s09 = root / "s09_data_in.bin"
    s11 = root / "s11_data_in.bin"
    rc1 = s01.read_bytes() if s01.exists() else b""
    rc11 = s11.read_bytes() if s11.exists() else b""
    ms9 = s09.read_bytes() if s09.exists() else b""

    locked_ok = rc1 == bytes.fromhex("00001B4F00000800")
    unlocked_ok = rc11 == bytes.fromhex("00345B5F00000800")
    final_ms_ok = len(ms9) >= 13 and ms9[10:13] == b"\x01\x01\x01"

    rows=[]
    for addr,label in ADDRS:
        row={"address":f"0x{addr:04X}","label":label,"pc_hint":PC_HINTS.get(f"0x{addr:04X}","")}
        vals=[]
        for tag,stem in SNAPS:
            v=read_byte(root,stem,addr)
            row[tag]=v; vals.append(v)
        row["changes_at_s08"] = vals[7] != vals[8] if vals[7] != "--" and vals[8] != "--" else False
        row["persistent_s08_to_s11"] = vals[8] == vals[9] == vals[10] if all(v != "--" for v in vals[8:11]) else False
        rows.append(row)

    csv_path = ns.out_dir / "auth_candidate_timeline.csv"
    with csv_path.open("w", newline="", encoding="utf-8") as f:
        fields=["address","label",*[t for t,_ in SNAPS],"changes_at_s08","persistent_s08_to_s11","pc_hint"]
        w=csv.DictWriter(f, fieldnames=fields); w.writeheader(); w.writerows(rows)

    report = []
    report.append("# Instrumented GDR auth capture analysis\n")
    report.append(f"Capture root: `{root}`\n")
    report.append("## Proof checks\n")
    report.append(f"- Step 1 READ CAPACITY: `{hexs(rc1) if rc1 else 'missing'}` -> {'LOCKED OK' if locked_ok else 'NOT LOCKED'}")
    if rc1: report.append(f"  - last_lba={be32(rc1)}")
    report.append(f"- Step 9 MODE SENSE: `{hexs(ms9) if ms9 else 'missing'}` -> {'FINAL STATE OK' if final_ms_ok else 'not final'}")
    report.append(f"- Step 11 READ CAPACITY: `{hexs(rc11) if rc11 else 'missing'}` -> {'UNLOCKED OK' if unlocked_ok else 'NOT UNLOCKED'}")
    if rc11: report.append(f"  - last_lba={be32(rc11)}")
    report.append("\n## Step-8 persistent candidates\n")
    for r in rows:
        if r["changes_at_s08"] and r["persistent_s08_to_s11"]:
            report.append(f"- `{r['address']}`: {r['s07']} -> {r['s08']} -> {r['s09']} -> {r['s11']} — {r['label']}")
            if r.get("pc_hint"):
                report.append(f"  - PC hint: {r['pc_hint']}")
    report.append("\n## Demotions\n")
    report.append("- `0x05B1` is not auth state here: it is unchanged through locked and unlocked states.")
    report.append("- `0x0144` is transient scheduler/event noise unless a later capture proves otherwise.")
    md_path = ns.out_dir / "auth_transition_report.md"
    md_path.write_text("\n".join(report)+"\n", encoding="utf-8")

    print(f"[OK] locked_ok={locked_ok} final_mode_sense_ok={final_ms_ok} unlocked_ok={unlocked_ok}")
    print(f"[OK] wrote {md_path}")
    print(f"[OK] wrote {csv_path}")
    if td: td.cleanup()
    return 0 if locked_ok and final_ms_ok and unlocked_ok else 1
if __name__ == "__main__":
    raise SystemExit(main())
