#!/usr/bin/env python3
"""DF-5730/DF-5731 — drive DDB over the socket serial console.

DF-5730 (sys/ddb/db_variables.c, beyond DF-5724's $tabstops):
  db_write_variable() (db_variables.c:120-129) stores any 64-bit value
  through the FCN_NULL fast path with zero validation for $radix,
  $maxoff and $maxwidth (table at db_variables.c:48-53).  Consequences,
  all SEMANTIC (no crash — every consumer traced):
    * $radix=N (N not 8/10/16): db_lex() parses every subsequent number
      literal in base N and silently accepts digits invalid for base N
      (db_lex.c:170-216 — the per-base digit bound at :192 is only
      special-cased for base 8).  Even the value of a later
      `set $maxwidth=-55` is misparsed.  Restoring requires the forced
      prefixes 0x/0t because decimal literals are themselves corrupted.
    * $maxoff=-1 -> (unsigned long)db_maxoff = ULONG_MAX suppresses all
      symbol+offset resolution (db_sym.c:289, db_print.c:56).
    * $maxwidth<0 -> db_end_line() always breaks (db_output.c:221),
      cosmetic.
DF-5731 (sys/ddb/db_access.c):
  db_get_value/db_put_value clamp only size>sizeof(long) (db_access.c:79,
  :107); a size<=0 would become a huge size_t in db_read_bytes/
  db_write_bytes' `while (size-- > 0)` loops (db_interface.c:238-249,
  :255-326) -> unbounded OOB copy.  Demonstrated here to be UNREACHABLE
  from the console: the write command's size modifier is a fixed
  whitelist (db_write_cmd.c:60-70) — `w/q` is rejected.

usage: drive.py
"""
import os, socket, sys, threading, time

SOCK  = os.environ.get("SOCK", "/tmp/opencode/df-5730-ser.sock")
DRAIN = os.environ.get("DRAIN", "/tmp/opencode/df-5730-drain.log")

def connect():
    while True:
        try:
            s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            s.connect(SOCK)
            return s
        except (FileNotFoundError, ConnectionRefusedError) as e:
            print(f"connect failed: {e}; retrying...", flush=True)
            time.sleep(2)

def send_raw(s, data, chunk=8, pace=0.15):
    for i in range(0, len(data), chunk):
        s.sendall(data[i:i+chunk])
        time.sleep(pace)

def send_line(s, line, wait=4):
    print(f"\n>>> {line!r}", flush=True)
    send_raw(s, line.encode() + b"\n")
    time.sleep(wait)

def main():
    s = connect()
    def drainer(sock):
        with open(DRAIN, "ab", buffering=0) as d:
            while True:
                try:
                    b = sock.recv(65536)
                except OSError:
                    return
                if not b:
                    return
                d.write(b)
                sys.stdout.write(b.decode("latin1")); sys.stdout.flush()
    threading.Thread(target=drainer, args=(s,), daemon=True).start()
    time.sleep(2)

    # ---- controls (defaults: radix=16, maxoff=0x100000, maxwidth=79) ----
    send_line(s, "x/x db_active")            # output + symbol table alive
    send_line(s, "print/d 10")               # radix 16: "10" parses 0x10 = 16
    send_line(s, "print/d 0t10")             # forced decimal: 10

    # ---- DF-5730: $radix=3 silently corrupts the expression language ----
    send_line(s, "set $radix=3")             # accepted, no validation
    send_line(s, "print/d 10")               # "10" base 3 -> 3
    send_line(s, "print/d 19")               # '9' ACCEPTED in base 3 -> 1*3+9 = 12
    send_line(s, "set $maxwidth=-55")        # parses "55" base 3 = 20 -> stores -20!
    send_line(s, "print/d $maxwidth")        # shows -20 (typed -55, radix corrupted it)

    # ---- DF-5730: $maxoff=-1 suppresses symbol resolution ----
    send_line(s, "print/a db_active")        # control: symbol form
    send_line(s, "set $maxoff=-1")           # accepted
    send_line(s, "print/a db_active")        # raw hex now (name suppressed)

    # ---- restore (0x/0t prefixes bypass the corrupted radix) ----
    send_line(s, "set $maxoff=0x100000")
    send_line(s, "set $radix=0x10")          # restore radix 16 via forced hex
    send_line(s, "set $maxwidth=0t79")
    send_line(s, "print/d 10")               # back to 16 — proves restoration
    send_line(s, "print/a db_active")        # symbol form back

    # ---- DF-5731: size modifier is a fixed whitelist — no way to a
    #      zero/negative size from the console ----
    send_line(s, "w/q 0xc0100000 0x90")      # -> "Unknown size" (db_write_cmd.c:70)
    send_line(s, "x/x db_active")            # session still healthy, zero traps
    send_line(s, "c")                        # continue -> kernel resumes

    print("\n>>> session script complete", flush=True)
    time.sleep(3)

if __name__ == "__main__":
    main()
