#!/usr/bin/env -S uv run # # /// script # requires-python = ">=3.12" # /// """Render the hook-id registry + status error codes from data/hooks.toml. The registry is the global id space shared by the control/stats protocol (docs/protocol.md §5): bit N of a config mask == hook id N == the id used in a stats line. This script emits the matching id enums, per-backend "own" masks, and status error codes for every language that touches the protocol: - kmod/KPM C -> kmod/generated/hook_ids.h - protocol Rust-> crates/protocol/src/generated/hook_ids.rs - zygisk Rust-> zygisk/src/generated/hook_ids.rs - lsposed Rust-> lsposed/native/src/generated/hook_ids.rs - app Kotlin-> .../generated/HookIds.kt Re-run after editing data/hooks.toml and commit the regenerated files. CI's lint job re-runs the codegen and fails on drift, so the numbering can never diverge between backends. Run via `uv run scripts/codegen-hooks.py` (or `./scripts/codegen-hooks.py` — the uv shebang provisions a pinned interpreter). No external deps: tomllib is stdlib and the emitters are plain string building. """ from __future__ import annotations import sys import tomllib from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parent.parent TOML_PATH = REPO_ROOT / "data" / "hooks.toml" # Targets are only the protocol participants (§1.4): the kernel backends (C), # the Zygisk backend (Rust), and the app + system_server hook (Kotlin). NOT # lsposed/native — that Rust crate is the uniffi diagnostic-probe library; it # consumes iface_lists (VPN-name matching) but never the protocol/registry. OUT_KMOD = REPO_ROOT / "kmod" / "generated" / "hook_ids.h" OUT_PROTOCOL_RS = REPO_ROOT / "crates" / "protocol" / "src" / "generated" / "hook_ids.rs" OUT_ZYGISK = REPO_ROOT / "zygisk" / "src" / "generated" / "hook_ids.rs" OUT_LSP_KT = ( REPO_ROOT / "lsposed" / "app" / "src" / "main" / "kotlin" / "dev" / "okhsunrog" / "vpnhide" / "generated" / "HookIds.kt" ) GENERATED_HEADER_LINE = ( "AUTO-GENERATED from data/hooks.toml — do not edit by hand. " "Regenerate with: uv run scripts/codegen-hooks.py" ) KNOWN_BACKENDS = ("kernel", "zygisk", "lsposed") # --------------------------------------------------------------------------- # load + validate # --------------------------------------------------------------------------- class Hook: def __init__(self, raw: dict[str, Any]) -> None: self.id: int = raw["id"] self.name: str = raw["name"] self.backend: str = raw["backend"] self.note: str = raw.get("note", "") class Err: def __init__(self, raw: dict[str, Any]) -> None: self.id: int = raw["id"] self.name: str = raw["name"] self.note: str = raw.get("note", "") class Backend: def __init__(self, raw: dict[str, Any]) -> None: self.id: int = raw["id"] self.name: str = raw["name"] self.note: str = raw.get("note", "") def load() -> tuple[list[Hook], list[Err], list[Backend]]: with TOML_PATH.open("rb") as fh: data = tomllib.load(fh) hooks = [Hook(h) for h in data.get("hook", [])] errs = [Err(e) for e in data.get("error", [])] backends = [Backend(b) for b in data.get("backend", [])] # ids must be dense, append-only, and 0-based — anything else is a bug # that would silently shift the global id space. for label, items in (("hook", hooks), ("error", errs), ("backend", backends)): ids = [it.id for it in items] if ids != list(range(len(ids))): sys.exit(f"error: {label} ids must be 0..N-1 with no gaps, got {ids}") names = [it.name for it in items] if len(set(names)) != len(names): sys.exit(f"error: duplicate {label} name in {names}") for h in hooks: if h.backend not in KNOWN_BACKENDS: sys.exit(f"error: hook {h.name!r} has unknown backend {h.backend!r}") return hooks, errs, backends def backend_mask(hooks: list[Hook], backend: str) -> int: m = 0 for h in hooks: if h.backend == backend: m |= 1 << h.id return m # name-casing helpers ------------------------------------------------------ def upper(name: str) -> str: return name.upper() def pascal(name: str) -> str: return "".join(part.capitalize() for part in name.split("_")) def kt_string(value: str) -> str: return value.replace("\\", "\\\\").replace('"', '\\"') # --------------------------------------------------------------------------- # emitters # --------------------------------------------------------------------------- def emit_kmod(hooks: list[Hook], errs: list[Err], backends: list[Backend]) -> str: L: list[str] = [f"/* {GENERATED_HEADER_LINE} */"] L.append("#ifndef VPNHIDE_GENERATED_HOOK_IDS_H") L.append("#define VPNHIDE_GENERATED_HOOK_IDS_H") L.append("") L.append("/* Global hook id space (data/hooks.toml). bit N == hook id N. */") width = max(len(f"VPNHIDE_HOOK_{upper(h.name)}") for h in hooks) for h in hooks: macro = f"VPNHIDE_HOOK_{upper(h.name)}" L.append(f"#define {macro:<{width}} {h.id}") L.append(f"#define {'VPNHIDE_HOOK_COUNT':<{width}} {len(hooks)}") L.append("") L.append("/* Hooks owned by each backend: apply `mask & own`, ignore foreign bits. */") for b in KNOWN_BACKENDS: L.append(f"#define VPNHIDE_{upper(b)}_HOOK_MASK 0x{backend_mask(hooks, b):x}u") L.append("") L.append("/* status error codes (protocol §5.1). */") ewidth = max(len(f"VPNHIDE_ERR_{upper(e.name)}") for e in errs) for e in errs: L.append(f"#define {f'VPNHIDE_ERR_{upper(e.name)}':<{ewidth}} {e.id}") L.append("") L.append("/* backend ids (protocol §4.3 `status backend `). */") bwidth = max(len(f"VPNHIDE_BACKEND_{upper(b.name)}") for b in backends) for b in backends: L.append(f"#define {f'VPNHIDE_BACKEND_{upper(b.name)}':<{bwidth}} {b.id}") L.append("") L.append("/* Hook name for an id (labeling / debug). Inline so the header stays") L.append(" self-contained and an unused table never warns. */") L.append("static inline const char *vpnhide_hook_name(int id)") L.append("{") L.append("\tswitch (id) {") for h in hooks: L.append(f'\tcase {h.id}: return "{h.name}";') L.append('\tdefault: return "?";') L.append("\t}") L.append("}") L.append("") L.append("#endif /* VPNHIDE_GENERATED_HOOK_IDS_H */") L.append("") return "\n".join(L) def emit_rust(hooks: list[Hook], errs: list[Err], backends: list[Backend]) -> str: L: list[str] = [f"// {GENERATED_HEADER_LINE}", "", "#![allow(dead_code)]", ""] L.append("/// Global hook id space (data/hooks.toml). bit N == hook id N.") L.append("#[repr(u32)]") L.append("#[derive(Copy, Clone, Eq, PartialEq, Debug)]") L.append("pub enum Hook {") for h in hooks: if h.note: L.append(f" /// {h.note}") L.append(f" {pascal(h.name)} = {h.id},") L.append("}") L.append("") L.append(f"pub const HOOK_COUNT: u32 = {len(hooks)};") L.append("") L.append("/// Hooks owned by each backend: apply `mask & own`.") for b in KNOWN_BACKENDS: L.append(f"pub const {upper(b)}_HOOK_MASK: u32 = 0x{backend_mask(hooks, b):x};") L.append("") L.append("/// status error codes (protocol §5.1).") L.append("#[repr(u32)]") L.append("#[derive(Copy, Clone, Eq, PartialEq, Debug)]") L.append("pub enum StatusError {") for e in errs: if e.note: L.append(f" /// {e.note}") L.append(f" {pascal(e.name)} = {e.id},") L.append("}") L.append("") L.append("/// backend ids (protocol §4.3 `status backend `).") L.append("#[repr(u32)]") L.append("#[derive(Copy, Clone, Eq, PartialEq, Debug)]") L.append("pub enum Backend {") for b in backends: if b.note: L.append(f" /// {b.note}") L.append(f" {pascal(b.name)} = {b.id},") L.append("}") L.append("") # One name per line so the output is rustfmt-clean (CI runs `cargo fmt # --check`); rustfmt wraps an over-width array into exactly this shape. L.append(f"pub const HOOK_NAMES: [&str; {len(hooks)}] = [") for h in hooks: L.append(f' "{h.name}",') L.append("];") L.append("") return "\n".join(L) def emit_kotlin(hooks: list[Hook], errs: list[Err], backends: list[Backend]) -> str: # Shaped to match `ktlint --format` (lsposed's quality gate): multiline # class signatures with trailing comma, a blank line between commented enum # entries, no trailing `;`. Keep this in sync if the ktlint style changes. L: list[str] = [f"// {GENERATED_HEADER_LINE}", ""] L.append("package dev.okhsunrog.vpnhide.generated") L.append("") L.append("/** Global hook id space (data/hooks.toml). bit N == hook id N. */") L.append("internal object HookIds {") L.append(" enum class Hook(") L.append(" val id: Int,") L.append(" val hookName: String,") L.append(" val note: String,") L.append(" ) {") for i, h in enumerate(hooks): if i: L.append("") if h.note: L.append(f" // {h.note}") L.append(f' {upper(h.name)}({h.id}, "{h.name}", "{kt_string(h.note)}"),') L.append(" }") L.append("") L.append(" // Hooks owned by each backend: apply `mask and own`.") for b in KNOWN_BACKENDS: L.append(f" const val {upper(b)}_HOOK_MASK = 0x{backend_mask(hooks, b):x}") L.append("") L.append(" /** status error codes (protocol §5.1). */") L.append(" enum class StatusError(") L.append(" val code: Int,") L.append(" ) {") for i, e in enumerate(errs): if i: L.append("") if e.note: L.append(f" // {e.note}") L.append(f" {upper(e.name)}({e.id}),") L.append(" }") L.append("") L.append(" /** backend ids (protocol §4.3 `status backend `). */") L.append(" enum class Backend(") L.append(" val id: Int,") L.append(" ) {") for i, b in enumerate(backends): if i: L.append("") if b.note: L.append(f" // {b.note}") L.append(f" {upper(b.name)}({b.id}),") L.append(" }") L.append("}") L.append("") return "\n".join(L) # --------------------------------------------------------------------------- # main # --------------------------------------------------------------------------- def main() -> None: hooks, errs, backends = load() outputs = { OUT_KMOD: emit_kmod(hooks, errs, backends), OUT_PROTOCOL_RS: emit_rust(hooks, errs, backends), OUT_ZYGISK: emit_rust(hooks, errs, backends), OUT_LSP_KT: emit_kotlin(hooks, errs, backends), } for path, text in outputs.items(): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") print(f"wrote {path.relative_to(REPO_ROOT)}") if __name__ == "__main__": main()