mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-07-17 21:18:42 +00:00
* fix(scripts): close audit findings in clean-device, release, changelog, update-json Five tooling fixes from the codebase audit. - clean-device.sh wiped only three /data/adb dirs and the legacy uids file, so a "clean" test left the canonical config, the root-owned APatch superkey under /data/adb/vpnhide, and the KPM/ports state behind. Mirror the full FULL_RESET_FILES / FULL_RESET_DIRS list from FullReset.kt. - release.py ran the irreversible changelog rotation (save_json + delete the fragment files) BEFORE patching the version-bearing source files, so a drifted module.prop / Cargo.toml / gradle format failed mid-release with the changelog already mutated and the duplicate-version guard then refusing a retry. Dry-run- validate every version-patch regex before the changelog step. - changelog.py printed the new fragment path relative to cwd, raising ValueError when cwd is not an ancestor of changelog.d (e.g. run from scripts/), a spurious traceback after the fragment was written fine. Make it relative to REPO_ROOT. - update-json.sh computed versionCode with bare $(( )), so a zero-padded version component (1.08.0) was read as octal and aborted on digit 8/9. Force base-10. - codegen-hooks.py now uses write_if_changed and reports only what changed, like codegen-interfaces.py, instead of rewriting every file and bumping mtimes. * refactor(scripts): extract scripts/codegen_lib.py shared by both generators codegen-interfaces.py and codegen-hooks.py each re-derived the same scaffolding: REPO_ROOT, the "AUTO-GENERATED … Regenerate with …" banner, the verbatim 11-segment path to the LSPosed app's generated Kotlin package, and the write-if-changed + change-reporting tail of main(). That duplication had already drifted. Move it into scripts/codegen_lib.py (REPO_ROOT, generated_header, lsposed_generated_kt, write_if_changed, emit_outputs), imported by both — the shared-lib pattern scripts/ already uses for changelog_lib.py. Generated output is byte-identical (verified: re-running both codegen scripts leaves no diff in any generated file). The escaping helpers stay per-script — they legitimately differ (C/Rust/Kotlin) and consolidating them risks output drift for no gain. * style: ruff format codegen_lib + generators
298 lines
11 KiB
Python
Executable file
298 lines
11 KiB
Python
Executable file
#!/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
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from codegen_lib import ( # type: ignore[import-not-found]
|
|
REPO_ROOT,
|
|
emit_outputs,
|
|
generated_header,
|
|
lsposed_generated_kt,
|
|
)
|
|
|
|
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 = lsposed_generated_kt("HookIds.kt")
|
|
|
|
GENERATED_HEADER_LINE = generated_header("data/hooks.toml", "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 <id>`). */")
|
|
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 <id>`).")
|
|
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 <id>`). */")
|
|
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() -> int:
|
|
hooks, errs, backends = load()
|
|
return emit_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),
|
|
}
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|