#!/usr/bin/env python3
import mmap
import os
import sys
import hashlib
from typing import List, Iterable, Tuple

PREFIX_HEX = "4D 53 54 41 52 5F 53 45 43 55 52 45 5F 53 54 4F 52 45 5F 46 49 4C 45 5F 4D 41 47 49 43 5F 49 44"
POSTFIX_HEX_LIST = ["00 00"]

EXTRACT_LEN = 228
PRINT_HEXVIEW = True
HEXVIEW_WIDTH = 16
MAX_HITS = 0

REQUIRE_POSTFIX_AT_EXTRACT_END = True

MAX_ZERO_FRACTION = 0.25
MAX_ZERO_RUN = 16
TAIL_START = 0x90
MIN_TAIL_NONZERO_RATIO = 0.70

SCRIPT_PATH = os.path.realpath(__file__)
SCRIPT_DIR = os.path.dirname(SCRIPT_PATH)
SCRIPT_NAME = os.path.basename(SCRIPT_PATH)


def clean_hex(s: str) -> bytes:
    s = s.replace("0x", "").replace("0X", "")
    s = "".join(s.split())
    if not s:
        return b""
    if len(s) % 2 != 0:
        raise ValueError
    return bytes.fromhex(s)


def iter_all(mm: mmap.mmap, needle: bytes, start: int = 0) -> Iterable[int]:
    i = start
    while True:
        pos = mm.find(needle, i)
        if pos == -1:
            return
        yield pos
        i = pos + 1


def is_printable_ascii(b: int) -> bool:
    return 32 <= b <= 126


def hexview(data: bytes, base_offset: int = 0, width: int = 16) -> str:
    lines = []
    for i in range(0, len(data), width):
        chunk = data[i:i + width]
        hex_part = " ".join(f"{x:02X}" for x in chunk)
        hex_part_padded = hex_part.ljust(width * 3 - 1)
        ascii_part = "".join(chr(x) if is_printable_ascii(x) else "." for x in chunk)
        lines.append(f"{base_offset + i:08X}  {hex_part_padded}  |{ascii_part}|")
    return "\n".join(lines)


def best_postfix_window(postfixes: List[bytes]) -> int:
    return max((len(p) for p in postfixes if p), default=0)

def find_postfix_after_extract(mm: mmap.mmap, postfixes: List[bytes], prefix_pos: int, file_size: int) -> Tuple[int, bytes]:
    start = prefix_pos + EXTRACT_LEN
    if start >= file_size:
        return -1, b""
    window = best_postfix_window(postfixes)
    end = min(file_size, start + window)
    for pf in postfixes:
        if not pf:
            continue
        if start + len(pf) > file_size:
            continue
        pos = mm.find(pf, start, end)
        if pos != -1:
            return pos, pf
    return -1, b""


def max_zero_run(b: bytes) -> int:
    best = 0
    cur = 0
    for x in b:
        if x == 0:
            cur += 1
            if cur > best:
                best = cur
        else:
            cur = 0
    return best


def zero_fraction(b: bytes) -> float:
    if not b:
        return 1.0
    z = sum(1 for x in b if x == 0)
    return z / len(b)


def nonzero_ratio(b: bytes) -> float:
    if not b:
        return 0.0
    nz = sum(1 for x in b if x != 0)
    return nz / len(b)


def passes_filters(block: bytes) -> bool:
    if zero_fraction(block) > MAX_ZERO_FRACTION:
        return False
    if max_zero_run(block) > MAX_ZERO_RUN:
        return False
    tail = block[TAIL_START:] if TAIL_START < len(block) else b""
    if tail and nonzero_ratio(tail) < MIN_TAIL_NONZERO_RATIO:
        return False
    return True


def safe_prefix_from_filename(path: str) -> str:
    base = os.path.basename(path)
    root, _ = os.path.splitext(base)
    safe = []
    for ch in root:
        if ch.isalnum() or ch in "._-":
            safe.append(ch)
        else:
            safe.append("_")
    s = "".join(safe).strip("._-")
    return s or "file"


def is_skipped_file(path: str) -> bool:
    base = os.path.basename(path)
    if base == SCRIPT_NAME:
        return True
    if base.endswith("_keybox.bin") or "_keybox_" in base:
        return True
    return False


def out_name_for_file(prefix: str, hit_idx: int) -> str:
    if hit_idx == 1:
        return f"{prefix}_keybox.bin"
    return f"{prefix}_keybox_{hit_idx}.bin"


def extract_from_file(path: str, prefix: bytes, postfixes: List[bytes]) -> int:
    if not os.path.isfile(path):
        return 0

    file_size = os.path.getsize(path)
    if file_size <= 0:
        return 0

    per_file_prefix = safe_prefix_from_filename(path)
    saved = 0
    seen = set()

    with open(path, "rb") as f:
        mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
        try:
            for ppos in iter_all(mm, prefix, 0):
                if ppos + EXTRACT_LEN > file_size:
                    continue

                qpos, _ = find_postfix_after_extract(mm, postfixes, ppos, file_size)
                if qpos == -1:
                    continue

                if REQUIRE_POSTFIX_AT_EXTRACT_END and qpos != (ppos + EXTRACT_LEN):
                    continue

                block = mm[ppos:ppos + EXTRACT_LEN]

                if not passes_filters(block):
                    continue

                h = hashlib.sha256(block).digest()
                if h in seen:
                    continue
                seen.add(h)

                saved += 1
                out_name = out_name_for_file(per_file_prefix, saved)
                with open(out_name, "wb") as out:
                    out.write(block)

                print(f"[{os.path.basename(path)} hit {saved}] -> {out_name} prefix@0x{ppos:X} postfix@0x{qpos:X}")

                if PRINT_HEXVIEW:
                    print(hexview(block, base_offset=ppos, width=HEXVIEW_WIDTH))
                    print()

                if MAX_HITS and saved >= MAX_HITS:
                    break
        finally:
            mm.close()

    return saved


def main() -> int:
    try:
        prefix = clean_hex(PREFIX_HEX)
        postfixes = [clean_hex(x) for x in POSTFIX_HEX_LIST]
    except:
        return 2

    postfixes = [p for p in postfixes if p]
    if not prefix or not postfixes or EXTRACT_LEN <= 0:
        return 2

    if len(sys.argv) == 2 and sys.argv[1] == "--all":
        total = 0
        for name in sorted(os.listdir(SCRIPT_DIR)):
            path = os.path.join(SCRIPT_DIR, name)
            if not os.path.isfile(path):
                continue
            if is_skipped_file(path):
                continue
            total += extract_from_file(path, prefix, postfixes)
        return 0 if total else 1

    if len(sys.argv) == 2:
        path = sys.argv[1]
        if not os.path.isfile(path):
            return 2
        return 0 if extract_from_file(path, prefix, postfixes) else 1

    print("Usage:")
    print("  python find_keybox.py <file>")
    print("  python find_keybox.py --all")
    return 2


if __name__ == "__main__":
    raise SystemExit(main())