#!/usr/bin/env python3
"""GDR-8050L READ(10) parser/descriptor boundary audit.

This probe corrects the final parser/descriptor proof boundary.  READ(10) is
selected by the front opcode table as class 0x48 and enters wrapper 0x9002EA7C
/ common READ handler 0x9001EED0.  The handler writes the host LBA to 0x05EC,
sets 0x05B6 bit 0x04, writes 0x05F0=0x11, and writes 0x06F1=0x04.

The descriptor scheduler branch at 0x9001C710 is gated on D2 == 0x30.  The
natural READ(10) front path reaches 0x9001C710 with D2 == 0x48 and returns at
0x9001C714; it does not naturally enter 0x9001C717/0x9001C78A/0x9001B4B3 in
this modeled invocation.  Class 0x30 is the MODE SENSE(10) opcode class in the
front-dispatch tables, not READ(10).

So the parser/LBA handoff proof for READ(10) is complete through the firmware
READ handler state handoff, while the previous seeded descriptor-builder probes
remain useful descriptor-consumer microprobes rather than natural READ(10)
control-flow proof.  This still does not promote any media-window delta.
"""
from __future__ import annotations
import argparse, csv, json, subprocess, sys
from pathlib import Path
from typing import Any

try:
    from hlds_gdr_firmware_lba_cpu_smoke import resolve_firmware, load_json, write_json, parse_int
except Exception:
    def load_json(path: Path) -> dict[str, Any]:
        with path.open('r', encoding='utf-8') as f: return json.load(f)
    def write_json(path: Path, obj: Any) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open('w', encoding='utf-8') as f: json.dump(obj, f, indent=2); f.write('\n')
    def parse_int(v: Any, default: int=0) -> int:
        if v is None or v == '': return default
        if isinstance(v, int): return v
        return int(str(v), 0)
    def resolve_firmware(config: dict[str, Any], config_path: Path, firmware_arg: str|None) -> str:
        return str(firmware_arg or config.get('custom_bin') or config.get('proof_firmware_image') or '')

VERSION='v257'
ROM_BASE=0x90000000
FRONT_ENTRY=0x9001C5F4
TABLE_SELECT_CALL=0x9001BD1D
NORMAL_CLASS_TABLE=0x9003BDB4
ALT_CLASS_TABLE=0x9003BCB4
HANDLER_PTR_TABLE=0x9003BEB4
FRONT_CLASS_LOAD_PC=0x9001C612
FRONT_PTR_LOAD_PC=0x9001C61D
FRONT_PTR_STORE_PC=0x9001C622
FRONT_INITIAL_HANDLER_CALL_PC=0x9001C66A
FRONT_AFTER_HANDLER_PC=0x9001C670
FRONT_SCHEDULER_ENTRY_PC=0x9001C710
FRONT_SCHEDULER_ACCEPT_PC=0x9001C717
FRONT_SCHEDULER_RETURN_PC=0x9001C714
FRONT_DESCRIPTOR_CALL_PC=0x9001C78A
DESCRIPTOR_FUNCTION_ENTRY_PC=0x9001B4B3
DESCRIPTOR_COPY_PC=0x9001B5EF
DESCRIPTOR_WRITE_PC=0x9001B5F2
READ10_WRAPPER=0x9002EA7C
READ10_HANDLER=0x9001EED0
READ_GATE_HELPER=0x9001BAB9
READ_STORE_05EC=0x9001EEE9
READ_STORE_05F0=0x9001EEEE
READ_STORE_06F1=0x9001EEF3
READ_SET_GATE=0x9001EEDA
CLASS_PREP_HELPER=0x9001AB92
MMIO_LOCK_PC=0x90006464
MMIO_UNLOCK_PC=0x90006470
SRAM_05B6=0x05B6
SRAM_05D0=0x05D0
SRAM_05EC=0x05EC
SRAM_05F0=0x05F0
SRAM_06E0=0x06E0
SRAM_06E4=0x06E4
SRAM_06F1=0x06F1
SRAM_06F4=0x06F4
SRAM_DE=0x00DE

CASES=[
 {'name':'read10_front_boundary_normal_table_video_lba','table_return':0,'lba':0x00000010},
 {'name':'read10_front_boundary_alt_table_unlocked_max_lba','table_return':1,'lba':0x00345B5F},
 {'name':'read10_front_boundary_normal_table_redump_boundary_lba','table_return':0,'lba':0x0004ABBF},
]

def hex32(v:int)->str: return f'0x{v & 0xffffffff:08X}'
def hex8(v:int)->str: return f'0x{v & 0xff:02X}'
def le32(v:int)->list[int]: return [(v>>(8*i))&255 for i in range(4)]
def be32(v:int)->list[int]: return [(v>>24)&255,(v>>16)&255,(v>>8)&255,v&255]
def norm_pc(v:Any)->str:
    try: return hex32(parse_int(v,0))
    except Exception: return str(v or '')
def read_csv(path:Path)->list[dict[str,str]]:
    if not path.exists(): return []
    with path.open('r',encoding='utf-8',newline='') as f: return list(csv.DictReader(f))
def u8(fw:bytes, addr:int)->int: return fw[addr-ROM_BASE]
def u32le(fw:bytes, addr:int)->int: return int.from_bytes(fw[addr-ROM_BASE:addr-ROM_BASE+4], 'little')

def static_audit(firmware:str)->dict[str,Any]:
    fw=Path(firmware).read_bytes() if firmware and Path(firmware).exists() else b''
    rows=[]; class30=[]; ok=bool(fw)
    if fw:
        for name,tbl in [('normal',NORMAL_CLASS_TABLE),('alternate',ALT_CLASS_TABLE)]:
            for opc,label in [(0x28,'READ(10)'),(0xA8,'READ(12)'),(0x5A,'MODE SENSE(10)')]:
                cls=u8(fw,tbl+opc); ptr=u32le(fw,HANDLER_PTR_TABLE+cls*4)
                rows.append({'table':name,'opcode_hex':hex8(opc),'name':label,'class_hex':hex8(cls),'handler_ptr':hex32(ptr)})
            for opc in range(256):
                if u8(fw,tbl+opc)==0x30:
                    class30.append({'table':name,'opcode_hex':hex8(opc)})
        ok = ok and all(r['class_hex']=='0x48' and r['handler_ptr']==hex32(READ10_WRAPPER) for r in rows if r['opcode_hex']=='0x28')
        ok = ok and any(r['opcode_hex']=='0x5A' and r['class_hex']=='0x30' for r in rows)
    return {
        'ok':ok,
        'front_dispatch_rows':rows,
        'class_0x30_opcode_members':class30,
        'read10_class_hex':'0x48',
        'descriptor_scheduler_gate_class_hex':'0x30',
        'interpretation':'READ(10) class 0x48 is not the descriptor-scheduler class 0x30; class 0x30 contains MODE SENSE(10) opcode 0x5A.',
    }

def case_config(base_config:dict[str,Any], firmware:str, case:dict[str,Any], case_dir:Path)->dict[str,Any]:
    lba=int(case['lba'])
    cdb=[0x28,0]+be32(lba)+[0,0,0,1,0,0]
    return {
      'test_name':f'GDR-8050L {VERSION} READ(10) descriptor boundary audit: {case["name"]}',
      'workspace':base_config.get('workspace', str(Path.cwd())),
      'target_profile':'GDR-8050L','custom_bin':firmware,'custom_base_addr':'0x90000000',
      'entry_point':hex32(FRONT_ENTRY),'max_cycles':1600,'header_skip_bytes':'0x0','config_version':VERSION,
      'requires_emulator_version':base_config.get('requires_emulator_version','V7-direct-config-trace-v120-gcc-version-sync-gdr163-v227-media-preflight'),
      'initial_registers':{'SP':'0x003FF000'},
      'memory_aliases':base_config.get('memory_aliases',[{'name':'SRAM alias 0x80000000->0x00000000','from':'0x80000000','to':'0x00000000','size':'0x400000'}]),
      'memory_injections':[
        {'address':hex32(SRAM_05D0),'data':cdb,'description':'READ(10) CDB shadow'},
        {'address':hex32(SRAM_DE),'data':[0],'description':'force natural non-DE==2 front-dispatch route'},
        {'address':hex32(SRAM_05B6),'data':[0],'description':'clear gate flags'},
        {'address':hex32(SRAM_05EC),'data':[0,0,0,0],'description':'clear assembled LBA'},
        {'address':hex32(SRAM_05F0),'data':[0],'description':'clear packet state'},
        {'address':hex32(SRAM_06F1),'data':[0],'description':'clear packet phase'},
        {'address':hex32(SRAM_06F4),'data':[0,0,0,0],'description':'clear front-dispatch pointer'},
        {'address':hex32(SRAM_06E0),'data':[0],'description':'clear descriptor transfer flag'},
        {'address':hex32(SRAM_06E4),'data':[0,0,0,0],'description':'clear descriptor LBA'},
      ],
      'trace':{
        'enabled':True,'live_instruction_log':False,'out_csv':str((case_dir/f'{case["name"]}.trace.csv').resolve()),
        'call_policy':{
          'skip_targets':[hex32(TABLE_SELECT_CALL),hex32(CLASS_PREP_HELPER),hex32(READ_GATE_HELPER),hex32(MMIO_LOCK_PC),hex32(MMIO_UNLOCK_PC)],
          'return_values':{hex32(TABLE_SELECT_CALL):{'D0':hex32(int(case['table_return']))},hex32(READ_GATE_HELPER):{'D0':'0x1'}},
        },
        'watchpoints':[
          {'name':'watch_05d0_cdb_shadow','start':hex32(SRAM_05D0),'size':'0x10','kind':'access'},
          {'name':'watch_05b6_gate_flags','start':hex32(SRAM_05B6),'size':'0x01','kind':'access'},
          {'name':'watch_05ec_assembled_lba','start':hex32(SRAM_05EC),'size':'0x04','kind':'access'},
          {'name':'watch_05f0_packet_state','start':hex32(SRAM_05F0),'size':'0x01','kind':'access'},
          {'name':'watch_06f1_packet_phase','start':hex32(SRAM_06F1),'size':'0x01','kind':'access'},
          {'name':'watch_06f4_front_handler_ptr','start':hex32(SRAM_06F4),'size':'0x04','kind':'access'},
          {'name':'watch_06e0_descriptor_flag','start':hex32(SRAM_06E0),'size':'0x01','kind':'access'},
          {'name':'watch_06e4_descriptor_lba','start':hex32(SRAM_06E4),'size':'0x04','kind':'access'},
        ],
        'breakpoints':[{'address':hex32(a),'name':n} for a,n in [
          (FRONT_ENTRY,'front_dispatch_entry_9001C5F4'),(FRONT_CLASS_LOAD_PC,'front_class_load_9001C612'),(FRONT_PTR_LOAD_PC,'front_ptr_load_9001C61D'),(FRONT_PTR_STORE_PC,'front_ptr_store_9001C622'),(FRONT_INITIAL_HANDLER_CALL_PC,'front_initial_handler_call_9001C66A'),(READ10_WRAPPER,'read10_wrapper_9002EA7C'),(READ10_HANDLER,'read10_handler_9001EED0'),(READ_SET_GATE,'read10_set_gate_05b6_9001EEDA'),(READ_STORE_05EC,'read10_store_05ec_9001EEE9'),(READ_STORE_05F0,'read10_store_05f0_state11_9001EEEE'),(READ_STORE_06F1,'read10_store_06f1_phase04_9001EEF3'),(FRONT_AFTER_HANDLER_PC,'front_after_handler_9001C670'),(FRONT_SCHEDULER_ENTRY_PC,'front_scheduler_entry_9001C710'),(FRONT_SCHEDULER_RETURN_PC,'front_scheduler_return_9001C714'),(FRONT_SCHEDULER_ACCEPT_PC,'front_scheduler_accept_9001C717'),(FRONT_DESCRIPTOR_CALL_PC,'front_descriptor_call_9001C78A'),(DESCRIPTOR_FUNCTION_ENTRY_PC,'descriptor_function_entry_9001B4B3'),(DESCRIPTOR_COPY_PC,'descriptor_copy_9001B5EF'),(DESCRIPTOR_WRITE_PC,'descriptor_write_9001B5F2')]]
      }
    }

def hit(rows:list[dict[str,str]], pc:int, event:str|None=None)->bool:
    for r in rows:
        if event and r.get('event')!=event: continue
        if norm_pc(r.get('pc'))==hex32(pc): return True
    return False

def writes_value(rows:list[dict[str,str]], addr:int, val:int|None=None, pc:int|None=None)->bool:
    for r in rows:
        if r.get('event')!='watch_write': continue
        if parse_int(r.get('addr'),-1)!=addr: continue
        if pc is not None and norm_pc(r.get('pc'))!=hex32(pc): continue
        if val is not None and (parse_int(r.get('value'),-1)&255)!=(val&255): continue
        return True
    return False

def collect_bytes(rows:list[dict[str,str]], base:int, pc:int|None=None, n:int=4)->list[int|None]:
    got=[None]*n
    for r in rows:
        if r.get('event')!='watch_write': continue
        a=parse_int(r.get('addr'),-1)
        if base <= a < base+n:
            if pc is not None and norm_pc(r.get('pc'))!=hex32(pc): continue
            got[a-base]=parse_int(r.get('value'),-1)&255
    return got

def d2_at(rows:list[dict[str,str]], pc:int)->str|None:
    for r in rows:
        if r.get('event')=='break' and norm_pc(r.get('pc'))==hex32(pc): return r.get('d2')
    return None

def ptr_written(rows:list[dict[str,str]])->int|None:
    got=collect_bytes(rows,SRAM_06F4,None,4)
    if any(v is None for v in got): return None
    return sum((int(got[i])<<(8*i)) for i in range(4))

def analyze_case(trace:Path, case:dict[str,Any], rc:int, log_path:Path)->dict[str,Any]:
    rows=read_csv(trace); lba=int(case['lba']); expect=le32(lba)
    lba_05ec=collect_bytes(rows,SRAM_05EC,READ_STORE_05EC,4)==expect
    desc_06e4=collect_bytes(rows,SRAM_06E4,DESCRIPTOR_WRITE_PC,4)==expect
    d2_sched=d2_at(rows,FRONT_SCHEDULER_ENTRY_PC)
    natural_boundary=(d2_sched==hex32(0x48) and hit(rows,FRONT_SCHEDULER_RETURN_PC,'break') and not hit(rows,FRONT_SCHEDULER_ACCEPT_PC,'break') and not hit(rows,FRONT_DESCRIPTOR_CALL_PC,'break') and not hit(rows,DESCRIPTOR_FUNCTION_ENTRY_PC,'break') and not hit(rows,DESCRIPTOR_COPY_PC,'break') and not desc_06e4)
    ok=(rc==0 and hit(rows,FRONT_ENTRY,'break') and hit(rows,READ10_WRAPPER,'break') and hit(rows,READ10_HANDLER,'break') and writes_value(rows,SRAM_05B6,0x04,READ_SET_GATE) and lba_05ec and writes_value(rows,SRAM_05F0,0x11,READ_STORE_05F0) and writes_value(rows,SRAM_06F1,0x04,READ_STORE_06F1) and natural_boundary)
    return {
      'case':case['name'],'input_lba_hex':hex32(lba),'table_return':int(case['table_return']),'run_returncode':rc,'row_count':len(rows),'trace_csv':str(trace),'stdout_stderr_log':str(log_path),
      'handler_pointer_06f4':hex32(ptr_written(rows) or 0),'handler_pointer_ok':ptr_written(rows)==READ10_WRAPPER,
      'front_class_d2_at_scheduler':d2_sched,'read10_wrapper_hit':hit(rows,READ10_WRAPPER,'break'),'read10_handler_hit':hit(rows,READ10_HANDLER,'break'),
      'gate_05b6_0x04_set':writes_value(rows,SRAM_05B6,0x04,READ_SET_GATE),'read_lba_05ec_match':lba_05ec,'state_05f0_0x11':writes_value(rows,SRAM_05F0,0x11,READ_STORE_05F0),'phase_06f1_0x04':writes_value(rows,SRAM_06F1,0x04,READ_STORE_06F1),
      'scheduler_entry_hit':hit(rows,FRONT_SCHEDULER_ENTRY_PC,'break'),'scheduler_return_9001c714_hit':hit(rows,FRONT_SCHEDULER_RETURN_PC,'break'),'scheduler_accept_9001c717_hit':hit(rows,FRONT_SCHEDULER_ACCEPT_PC,'break'),'descriptor_call_9001c78a_hit':hit(rows,FRONT_DESCRIPTOR_CALL_PC,'break'),'descriptor_function_9001b4b3_hit':hit(rows,DESCRIPTOR_FUNCTION_ENTRY_PC,'break'),'descriptor_copy_9001b5ef_hit':hit(rows,DESCRIPTOR_COPY_PC,'break'),'descriptor_lba_06e4_match':desc_06e4,
      'natural_read10_descriptor_boundary_ok':natural_boundary,'ok':ok,
    }

def write_md(path:Path, report:dict[str,Any])->None:
    lines=[]
    lines.append('# GDR-8050L v257 READ(10) descriptor boundary audit\n')
    lines.append(f"- ok: `{str(report['ok']).lower()}`")
    lines.append(f"- firmware_derived_host_lba: `{str(report['firmware_derived_host_lba']).lower()}`")
    lines.append(f"- read10_parser_handoff_complete: `{str(report['read10_parser_handoff_complete']).lower()}`")
    lines.append(f"- read10_natural_descriptor_boundary_identified: `{str(report['read10_natural_descriptor_boundary_identified']).lower()}`")
    lines.append(f"- descriptor_builder_is_seeded_microprobe_for_read10: `{str(report['descriptor_builder_is_seeded_microprobe_for_read10']).lower()}`")
    lines.append(f"- firmware_derived_delta: `{str(report['firmware_derived_delta']).lower()}`\n")
    lines.append('## Static front-dispatch table audit\n')
    lines.append('| table | opcode | command | class | handler pointer |')
    lines.append('|---|---:|---|---:|---:|')
    for r in report['static_audit']['front_dispatch_rows']:
        lines.append(f"| `{r['table']}` | `{r['opcode_hex']}` | `{r['name']}` | `{r['class_hex']}` | `{r['handler_ptr']}` |")
    lines.append('\nClass `0x30` opcode members observed in the front-dispatch tables:\n')
    lines.append('| table | opcode |')
    lines.append('|---|---:|')
    for r in report['static_audit']['class_0x30_opcode_members']:
        lines.append(f"| `{r['table']}` | `{r['opcode_hex']}` |")
    lines.append('\n## CPU READ(10) front-boundary cases\n')
    lines.append('| case | LBA | table | 0x06F4 ptr | 0x05B6=04 | 0x05EC LBA | 0x05F0=11 | 0x06F1=04 | D2 at 0x9001C710 | ret 0x9001C714 | desc 0x9001B5EF | ok |')
    lines.append('|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|')
    for c in report['cpu_cases']:
        lines.append(f"| `{c['case']}` | `{c['input_lba_hex']}` | `{c['table_return']}` | `{c['handler_pointer_06f4']}` | `{c['gate_05b6_0x04_set']}` | `{c['read_lba_05ec_match']}` | `{c['state_05f0_0x11']}` | `{c['phase_06f1_0x04']}` | `{c['front_class_d2_at_scheduler']}` | `{c['scheduler_return_9001c714_hit']}` | `{c['descriptor_copy_9001b5ef_hit']}` | `{c['ok']}` |")
    lines.append('\n## Interpretation\n')
    lines.append('READ(10) parser/handler proof is complete through firmware state handoff: the natural front-dispatch path selects class `0x48`, invokes wrapper `0x9002EA7C` / handler `0x9001EED0`, writes the host LBA to `0x05EC`, sets gate/state/phase (`0x05B6=0x04`, `0x05F0=0x11`, `0x06F1=0x04`), then reaches `0x9001C710` with `D2=0x48` and returns at `0x9001C714`.')
    lines.append('\nThe descriptor branch at `0x9001C717 -> 0x9001C78A -> 0x9001B4B3 -> 0x9001B5EF` is gated on `D2=0x30`; static table audit shows class `0x30` belongs to opcode `0x5A` / MODE SENSE(10), not READ(10). The seeded descriptor-builder probes remain valid microprobes for the descriptor consumer, but they should not be promoted as natural READ(10) descriptor control flow.')
    lines.append('\nThis does **not** promote the Redump/unlocked media-window delta. Keep `firmware_derived_delta=false`. The next proof target after parser/handler handoff is the real hardware/event path that consumes `0x05EC/0x05F0/0x06F1` for data transfer/window selection.')
    path.write_text('\n'.join(lines)+'\n',encoding='utf-8')

def main(argv=None)->int:
    ap=argparse.ArgumentParser()
    ap.add_argument('--config', default='gdr8050l_auth_v257_stateful_relock_keep_loaded_iscsi_config.json')
    ap.add_argument('--firmware')
    ap.add_argument('--mn103s-emu', default='mn103s_emu.py')
    ap.add_argument('--json', default='gdr8050l_auth_v257_firmware_read10_descriptor_boundary_audit_report.json')
    ap.add_argument('--md', default='gdr8050l_auth_v257_firmware_read10_descriptor_boundary_audit_report.md')
    ap.add_argument('--case-dir', default='gdr8050l_auth_v257_firmware_read10_descriptor_boundary_audit_report_cases')
    ap.add_argument('--timeout', type=float, default=30.0)
    args=ap.parse_args(argv)
    cfg_path=Path(args.config); cfg=load_json(cfg_path) if cfg_path.exists() else {}
    firmware=resolve_firmware(cfg,cfg_path,args.firmware)
    case_dir=Path(args.case_dir); case_dir.mkdir(parents=True,exist_ok=True)
    static=static_audit(firmware)
    cases=[]
    for case in CASES:
        cdir=case_dir
        conf=case_config(cfg,firmware,case,cdir)
        conf_path=cdir/f'{case["name"]}.config.json'; write_json(conf_path,conf)
        log_path=cdir/f'{case["name"]}.output.txt'
        proc=subprocess.run([sys.executable,args.mn103s_emu,str(conf_path)],cwd=str(Path.cwd()),text=True,capture_output=True,timeout=args.timeout)
        log_path.write_text((proc.stdout or '')+('\n[stderr]\n'+proc.stderr if proc.stderr else ''),encoding='utf-8')
        cases.append(analyze_case(Path(conf['trace']['out_csv']),case,proc.returncode,log_path))
    parser_handoff=all(c['read_lba_05ec_match'] and c['state_05f0_0x11'] and c['phase_06f1_0x04'] and c['gate_05b6_0x04_set'] for c in cases)
    boundary=all(c['natural_read10_descriptor_boundary_ok'] for c in cases)
    report={
        'probe':'firmware_read10_descriptor_boundary_audit','version':VERSION,'config':str(cfg_path),'firmware':firmware,
        'static_audit':static,'cpu_cases':cases,'case_count':len(cases),'passed_count':sum(1 for c in cases if c['ok']),
        'firmware_derived_host_lba':parser_handoff,
        'read10_parser_handoff_complete':parser_handoff,
        'read10_natural_descriptor_boundary_identified':boundary,
        'descriptor_builder_is_seeded_microprobe_for_read10':True,
        'firmware_derived_delta':False,
        'ok':static.get('ok') and all(c['ok'] for c in cases),
    }
    write_json(Path(args.json),report); write_md(Path(args.md),report)
    print(f"[FIRMWARE-READ10-DESCRIPTOR-BOUNDARY] cases={report['passed_count']}/{report['case_count']} read10_parser_handoff={str(parser_handoff).lower()} natural_descriptor_boundary={str(boundary).lower()} firmware_derived_delta=false ok={str(report['ok']).lower()}")
    return 0 if report['ok'] else 2
if __name__=='__main__': raise SystemExit(main())
