# -*- coding: utf-8 -*-
"""
q_pack_inv_5 -- DIMENSION 1 pass 5: DECOMPILE-BASED deserializer atlas.

WHY THIS PASS EXISTS
  Pass 4's instruction-level CMP/SUB scan FAILED its own control: FUN_18014cc60
  provably dispatches on atom 0x2cd (squad) yet the scan did not see it. Reason:
  the dispatch is a RUNNING-SUM ladder ("sub eax,0x5d / jz / sub eax,0x148 / jz"
  where 0x5d+0x148 = 0x1a5), so the raw immediates are DIFFERENCES, not atoms.
  Ghidra's decompiler already folds the ladder back into `== 0x2cd`, so this pass
  reads the atoms out of the decompiled C instead of the instruction stream.

HYPOTHESIS
  Decompiling every function that calls the FNV hasher 0x180180d00 or the wrapper
  FUN_180141ee0 and regexing `== 0xNNN` / `!= 0xNNN` / `case 0xNNN` gives the
  complete key-set of every JSON parser in CardsDLL.

CONTROLS (the pass is void if any fails)
  * FUN_18014cc60 must report 0x2cd(squad) AND 0x2e5(starterPack).
  * FUN_18013ec10 must report 0x35e(unopenedPacks) AND 0x24b(preOrderPacks).
  * FUN_18013af30 must report 0x20c(packContentInfo) AND 0x35d(unopened).
  * atom_name(0x2e5) must be 'starterPack'.
"""
import re
import traceback

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres"
ATOM_TABLE = 0x1802D2760
_nc = {}


def atom_name(i):
    if i in _nc:
        return _nc[i]
    v = "?"
    try:
        p = qword(ATOM_TABLE + i * 8)
        if 0x180001000 <= p <= 0x1802EFC08:
            v = rd_str(p, 60)
    except Exception:
        pass
    _nc[i] = v
    return v


EQ = re.compile(r"(?:==|!=)\s*(0x[0-9a-fA-F]+|\d+)")
CASE = re.compile(r"case\s+(0x[0-9a-fA-F]+|\d+)\s*:")
NOISE = {6, 10, 0xB, 0xD, 0x38C, 0, 1, 2, 3, 4, 5, 7, 8, 9}

try:
    print("CONTROL atom_name(0x2e5) = %r" % atom_name(0x2E5))
    hashers = set()
    for tgt in (0x180180D00, 0x180141EE0):
        for (fr, ty, fn, en) in xrefs_to(tgt):
            if en:
                hashers.add(en)
    print("parser candidates: %d" % len(hashers))

    atlas = {}
    bodies = {}
    for e in sorted(hashers):
        try:
            src = dec(e, 240)
        except Exception:
            src = ""
        bodies[e] = src
        ats = set()
        for m in EQ.finditer(src):
            v = int(m.group(1), 0)
            if v not in NOISE and 1 <= v <= 0x38C:
                ats.add(v)
        for m in CASE.finditer(src):
            v = int(m.group(1), 0)
            if v not in NOISE and 1 <= v <= 0x38C:
                ats.add(v)
        atlas[e] = ats

    print("\n########## CONTROLS ##########")
    ck = [(0x18014CC60, 0x2CD), (0x18014CC60, 0x2E5), (0x18013EC10, 0x35E),
          (0x18013EC10, 0x24B), (0x18013AF30, 0x20C), (0x18013AF30, 0x35D)]
    ok = True
    for fn, at in ck:
        hit = at in atlas.get(fn, set())
        ok = ok and hit
        print("  %#x has %#x(%-16s): %s" % (fn, at, atom_name(at),
                                            "PASS" if hit else "FAIL"))
    print("ATLAS CONTROL OVERALL: %s" % ("PASS" if ok else "FAIL"))

    print("\n########## ATLAS ##########")
    lines = []
    for e in sorted(atlas):
        ats = sorted(atlas[e])
        lines.append("\n%#x  len(src)=%d  keys=%d" % (e, len(bodies[e]), len(ats)))
        lines.append("   " + ", ".join("%#x=%s" % (a, atom_name(a)) for a in ats))
    print("\n".join(lines))
    open(OUT + "/d1_atlas.txt", "w").write("\n".join(lines))

    print("\n########## PACK-INVENTORY ATOM OWNERSHIP ##########")
    for a in (0x20D, 0x35D, 0x35E, 0x2E5, 0x5D, 0x24B, 0x27B, 0x260, 0x262,
              0x264, 0x20C, 0x16E, 0x16B, 0xEC, 0x1DD, 0xBC, 0x2E3, 0x2EB, 0x37D):
        owners = [e for e in atlas if a in atlas[e]]
        print("  atom %#x %-22s -> %s"
              % (a, atom_name(a), [hex(x) for x in sorted(owners)] or "NONE"))

    print("\n########## EXTRA DECOMPILES ##########")
    for va, tag in ((0x180122C50, "f180122c50"), (0x18017FC20, "f18017fc20"),
                    (0x180161B00, "f180161b00"), (0x18013C3A0, "f18013c3a0"),
                    (0x180144E80, "f180144e80"), (0x180138E10, "dupidlist"),
                    (0x1801234E0, "storepacktypes_root")):
        s = bodies.get(va) or dec(va)
        print("\n--- %s %#x len=%d" % (tag, va, len(s)))
        print(s)
        open(OUT + "/d1_%s.txt" % tag, "w").write(s)

    print("\n########## hub-tile table around 0x1802097f8 ##########")
    for va in range(0x1802096C0, 0x180209900, 8):
        try:
            q = qword(va)
        except Exception:
            continue
        tag = ""
        f = fm.getFunctionAt(addr(q)) if 0x180001000 <= q <= 0x1801E4F62 else None
        if f:
            tag = "FUNC " + f.getName()
        elif 0x180001000 <= q <= 0x1802EFC08:
            try:
                s = rd_str(q, 60)
                if s.isprintable() and len(s) > 1:
                    tag = repr(s)
            except Exception:
                pass
        if tag:
            print("  %#x -> %#x %s" % (va, q, tag))

    print("\nDONE q_pack_inv_5")
except Exception:
    traceback.print_exc()
