#!/usr/bin/env python3
"""Reject PATH wrappers that can re-enter themselves.

usage: wrapper-reinvoke-lint.py <dir-or-file>...

A wrapper here is a shim BODY: a file in a scanned directory that one or more sibling
symlinks point at. The symlink names are the commands it wraps.

Two failures, both of which have fork-bombed this workstation:

  W1 bare re-invocation — the body runs a command it wraps by bare name (`exec git`,
     `command -v git`). PATH still has the shim dir in front, so the name resolves back
     to the wrapper. The target must be an absolute path.

  W2 unguarded PATH resolution — the body resolves its target by consulting PATH
     (iterating $PATH, `command -v`, `which`, `type -P`) without a re-entry bound:
     either skipping its own directory or short-circuiting on a sentinel it exports.

Nothing else is checked. A lint that flags more than the shape that caused the incident
gets ignored, and an ignored lint is not a gate.
"""
import os
import re
import sys

RESOLVERS = re.compile(
    r"""(?:\bIFS\s*=\s*['"]?:['"]?\s+read\b.*\bPATH\b)"""
    r"""|(?:\bcommand\s+-[vV]\b)|(?:\bwhich\s)|(?:\btype\s+-[pP]\b)""",
)
SELF_DIR_BOUND = re.compile(
    r"""BASH_SOURCE|\$0.*\bdirname\b""",
)
SENTINEL = re.compile(r"\b[A-Z][A-Z0-9_]*(?:ACTIVE|GUARD|REENTRY|DEPTH|MARKER|PIN)[A-Z0-9_]*\b")
CMD_LEAD = r"(?:^|[;&|(`]|\$\(|&&|\|\||\bexec\s+|\bthen\s+|\belse\s+|\bdo\s+)"


def strip_comments(text):
    out = []
    for line in text.split("\n"):
        out.append("" if line.lstrip().startswith("#") else line)
    return out


def wrapped_names(directory):
    """body-path -> set of command names installed as symlinks to it."""
    names = {}
    try:
        entries = os.listdir(directory)
    except OSError as exc:
        raise SystemExit(f"wrapper-reinvoke-lint: cannot read {directory}: {exc}")
    for entry in sorted(entries):
        path = os.path.join(directory, entry)
        if not os.path.islink(path):
            continue
        target = os.path.realpath(path)
        if os.path.dirname(target) != os.path.realpath(directory):
            continue
        names.setdefault(target, set()).add(entry)
    return names


def check_body(path, names):
    lines = strip_comments(open(path, encoding="utf-8", errors="replace").read())
    findings = []

    for name in sorted(names):
        pattern = re.compile(CMD_LEAD + r"\s*" + re.escape(name) + r"(?=\s|$)")
        resolver = re.compile(r"(?:\bcommand\s+-[vV]|\bwhich|\btype\s+-[pP])\s+" + re.escape(name) + r"\b")
        for n, line in enumerate(lines, 1):
            if pattern.search(line) or resolver.search(line):
                findings.append(
                    (n, "W1", f"invokes wrapped command '{name}' by bare name: {line.strip()}")
                )

    body = "\n".join(lines)
    if RESOLVERS.search(body):
        bounded = SELF_DIR_BOUND.search(body) and re.search(r"\bcontinue\b|\breturn\b|!=|==", body)
        if not (bounded or SENTINEL.search(body)):
            findings.append(
                (0, "W2", "resolves its target through PATH with no re-entry bound "
                          "(skip its own directory, or short-circuit on an exported sentinel)")
            )
    return findings


def main(argv):
    if not argv:
        raise SystemExit(__doc__)
    targets = []
    for arg in argv:
        real = os.path.realpath(arg)
        if os.path.isdir(real):
            targets.append(real)
        else:
            raise SystemExit(f"wrapper-reinvoke-lint: not a directory: {arg}")

    violations = 0
    scanned = 0
    for directory in targets:
        for body, names in sorted(wrapped_names(directory).items()):
            scanned += 1
            for line_no, code, detail in check_body(body, names):
                violations += 1
                where = f"{body}:{line_no}" if line_no else body
                print(f"{where}: {code} {detail}", file=sys.stderr)

    if violations:
        print(
            f"\nwrapper-reinvoke-lint: {violations} violation(s) in {scanned} wrapper bod"
            f"{'y' if scanned == 1 else 'ies'}.\n"
            "A wrapper must exec an ABSOLUTE path it resolved while excluding its own "
            "directory, or hold a pin written at install time. Re-entry here is how the\n"
            "workstation was fork-bombed three times in one week.",
            file=sys.stderr,
        )
        return 1
    print(f"wrapper-reinvoke-lint: {scanned} wrapper bodies clean")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
