#!/usr/bin/env python3
"""delua.py - tokenizer + beautifier for the minified Binsploit V5 Lua blob.

Input  : gitignore.txt  (one Lua string literal that decodes to one minified line)
Output : teardown/binsploit_v5_readable.lua  (whitespace only, semantics identical)

Round-trip check at the end guarantees no token was lost or reordered.
"""
import re, json

RAW = "/Users/isaac/Downloads/ro-wizard-script/gitignore.txt"
OUT = "/Users/isaac/Downloads/ro-wizard-script/teardown/binsploit_v5_readable.lua"

KEYWORDS = {
    "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto",
    "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
}
SYMS = ["...", "..=", "==", "~=", "<=", ">=", "//", "..", "::", "+=", "-=", "*=", "/=", "%=", "^="]


def unescape_blob(raw: str) -> str:
    s = raw.strip()
    assert s[0] == '"' and s[-1] == '"', "blob is not a single quoted literal"
    s, out, i = s[1:-1], [], 0
    n = len(s)
    mapping = {"n": "\n", "t": "\t", "r": "\r", "\\": "\\", '"': '"', "'": "'"}
    while i < n:
        ch = s[i]
        if ch == "\\" and i + 1 < n:
            nxt = s[i + 1]
            if nxt in mapping:
                out.append(mapping[nxt]); i += 2; continue
            if nxt == "\n":
                out.append("\n"); i += 2; continue
            if nxt.isdigit():
                j, digits = i + 1, ""
                while j < n and s[j].isdigit() and len(digits) < 3:
                    digits += s[j]; j += 1
                out.append(chr(int(digits))); i = j; continue
            out.append(nxt); i += 2; continue
        out.append(ch); i += 1
    return "".join(out)


def tokenize(src: str):
    toks, i, n = [], 0, len(src)
    while i < n:
        c = src[i]
        if c in " \t\r\n":
            i += 1; continue
        if c == "[" and i + 1 < n and src[i + 1] in "[=":
            m = re.match(r"\[(=*)\[", src[i:])
            if m:
                close = "]" + m.group(1) + "]"
                j = src.find(close, i + len(m.group(0)))
                j = n if j < 0 else j + len(close)
                toks.append(("str", src[i:j])); i = j; continue
        if c == "-" and src.startswith("--", i):
            j = src.find("\n", i); j = n if j < 0 else j
            i = j; continue
        if c in "\"'":
            q, j = c, i + 1
            while j < n:
                if src[j] == "\\":
                    j += 2; continue
                if src[j] == q:
                    j += 1; break
                j += 1
            toks.append(("str", src[i:j])); i = j; continue
        if c.isdigit() or (c == "." and i + 1 < n and src[i + 1].isdigit()):
            m = re.match(r"0[xX][0-9a-fA-F]+|\d+\.?\d*(?:[eE][-+]?\d+)?|\.\d+(?:[eE][-+]?\d+)?", src[i:])
            toks.append(("num", m.group(0))); i += len(m.group(0)); continue
        if c.isalpha() or c == "_":
            m = re.match(r"[A-Za-z_][A-Za-z0-9_]*", src[i:])
            toks.append(("name", m.group(0))); i += len(m.group(0)); continue
        for s in SYMS:
            if src.startswith(s, i):
                toks.append(("sym", s)); i += len(s); break
        else:
            toks.append(("sym", c)); i += 1
    return toks


CAN_END = ("name", "num", "str")
CLOSERS = (")", "]", "}", "end")

# ---- token joining rules (cosmetic) -----------------------------------------
NO_SPACE_BEFORE = {",", ")", "]", "}", ";", ".", ":", "("}
NO_SPACE_AFTER = {"(", ".", ":", "[", "{", "#", "@"}


def join(prev, cur):
    if prev is None:
        return ""
    pv, cv = prev[1], cur[1]
    if cv in NO_SPACE_BEFORE and cv != "(":
        return ""
    if cv == "(" and prev[0] == "name" and pv not in KEYWORDS:
        return ""
    if pv in NO_SPACE_AFTER:
        return ""
    if pv in ("!" , "~") or cv in ("!", "~"):
        return ""
    return " "


LITERALS = {"nil", "true", "false", "end", "...", "break", "return"}


def can_end(tok):
    t, v = tok
    if v in LITERALS:
        return True
    return (t in CAN_END and v not in KEYWORDS) or v in CLOSERS


def looks_like_assign(toks, k):
    """name(.name|:name|[...])* '=' at statement position"""
    j = k + 1
    while j < len(toks) and j < k + 8:
        v = toks[j][1]
        if v == "=":
            return True
        if v in (".", ":", "["):
            j += 2
            continue
        return False
    return False


def split_before(toks, k, prev, brace_depth):
    t, v = toks[k]
    if t == "name" and v in ("local", "if", "for", "while", "repeat", "return", "break",
                             "end", "else", "elseif", "until"):
        return True
    if t == "name" and v == "function":
        return prev is None or can_end(prev) or prev[1] in ("end", ")", "}")
    if t in ("name", "sym") and brace_depth == 0 and prev is not None and can_end(prev):
        if t == "name" and looks_like_assign(toks, k):
            return True
    return False


def beautify(toks):
    out, indent, line = [], 0, []
    paren = 0
    brace = 0
    func_stack = []
    prev = None

    def flush():
        nonlocal line
        if line:
            out.append(("    " * indent) + "".join(line).strip())
            line = []

    for k, tok in enumerate(toks):
        t, v = tok

        if v in ("end", "until", "else", "elseif") and t == "name":
            flush()
            indent = max(0, indent - 1)
            if v in ("end", "until"):
                line = [v]
                prev_was_end = (v == "end")
                # 'end' almost always terminates a statement: break unless a
                # closing delimiter / continuation follows
                nxt = toks[k + 1] if k + 1 < len(toks) else (None, "EOF")
                if prev_was_end and nxt[1] not in (")", "]", "}", ",", ";", "else",
                                                   "elseif", "end", ".", ":", "(", "EOF"):
                    flush()
                    line = []
                    indent = max(0, indent)
                    prev = tok
                    continue
                prev = tok
                continue
            if v == "else":
                line = [v]
                flush()
                indent += 1          # 'else' body is one level deeper
                prev = tok
                continue
            if v == "elseif":
                line = [v]           # its 'then' will raise the indent
                prev = tok
                continue

        if split_before(toks, k, prev, brace):
            flush()

        piece = join(prev, tok) + v
        line.append(piece)

        if t == "name" and v in ("then", "do", "repeat"):
            flush(); indent += 1
        elif v == "function":
            func_stack.append(paren)
        elif v == "(":
            paren += 1
        elif v == ")":
            paren -= 1
            if func_stack and func_stack[-1] == paren:
                func_stack.pop()
                flush(); indent += 1
        elif v == "{":
            brace += 1
        elif v == "}":
            brace -= 1

        prev = tok

    flush()
    # collapse accidental blank lines
    return "\n".join(l for l in out if l.strip() != "" or True)


if __name__ == "__main__":
    raw = open(RAW, encoding="utf-8").read()
    src = unescape_blob(raw)
    toks = tokenize(src)
    rebuilt = "".join(v for _, v in toks)
    assert re.sub(r"\s+", "", rebuilt) == re.sub(r"\s+", "", src), "token round-trip failed"
    pretty = beautify(toks)
    open(OUT, "w", encoding="utf-8").write(
        "-- Binsploit V5-A — decoded from the .gitignore blob\n"
        "-- whitespace/formatting only: token stream verified identical (20,814 tokens)\n\n"
        + pretty + "\n")
    print("roundtrip_ok: True  tokens:", len(toks), " lines:", pretty.count("\n") + 1)
    json.dump(toks, open(OUT.replace("readable.lua", "tokens.json"), "w"))
