Files
bikini-patchwork/patchwork/config.py
T
2026-06-17 20:10:17 -05:00

132 lines
4.1 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from typing import Any
DEFAULT_CONFIG: dict[str, Any] = {
"seed": None,
"layers": 3,
"stage2_layers": 3,
"rename": True,
"encrypt_strings": True,
"obfuscate_numbers": True,
"opaque_predicates": True,
"mba": True,
"junk_branches": True,
"lazy_funcs": True,
"anti_debug": True,
"abyss": False,
"abyss_functions": [],
"keep": [],
"max_output_bytes": None,
"max_ratio": None,
"max_review_indicators": None,
}
BOOLEAN_KEYS = {
"rename",
"encrypt_strings",
"obfuscate_numbers",
"opaque_predicates",
"mba",
"junk_branches",
"lazy_funcs",
"anti_debug",
"abyss",
}
INTEGER_KEYS = {"seed", "layers", "stage2_layers", "max_output_bytes", "max_review_indicators"}
FLOAT_KEYS = {"max_ratio"}
LIST_KEYS = {"keep", "abyss_functions"}
class ConfigError(ValueError):
pass
def load_config(path: str | Path | None) -> dict[str, Any]:
if path is None:
return {}
config_path = Path(path)
try:
raw = json.loads(config_path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise ConfigError(f"config file not found: {config_path}") from exc
except json.JSONDecodeError as exc:
raise ConfigError(f"invalid config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ConfigError("config file must contain a JSON object")
unknown = sorted(set(raw) - set(DEFAULT_CONFIG))
if unknown:
raise ConfigError(f"unknown config option(s): {', '.join(unknown)}")
return dict(raw)
def read_keep_file(path: str | Path | None) -> list[str]:
if path is None:
return []
keep_path = Path(path)
try:
lines = keep_path.read_text(encoding="utf-8").splitlines()
except FileNotFoundError as exc:
raise ConfigError(f"keep file not found: {keep_path}") from exc
names: list[str] = []
for line in lines:
value = line.split("#", 1)[0].strip()
if not value:
continue
names.extend(part.strip() for part in value.split(",") if part.strip())
return names
def normalize_config(config: dict[str, Any]) -> dict[str, Any]:
normalized = dict(DEFAULT_CONFIG)
normalized.update(config)
for key in BOOLEAN_KEYS:
if not isinstance(normalized[key], bool):
raise ConfigError(f"{key} must be a boolean")
for key in INTEGER_KEYS:
value = normalized[key]
if value is None:
continue
if not isinstance(value, int) or isinstance(value, bool):
raise ConfigError(f"{key} must be an integer")
for key in FLOAT_KEYS:
value = normalized[key]
if value is None:
continue
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise ConfigError(f"{key} must be a number")
normalized[key] = float(value)
for key in LIST_KEYS:
value = normalized[key]
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
raise ConfigError(f"{key} must be a list of strings")
normalized[key] = sorted(set(item for item in value if item))
if normalized["layers"] < 1:
raise ConfigError("layers must be at least 1")
if normalized["stage2_layers"] < 1:
raise ConfigError("stage2_layers must be at least 1")
if normalized["max_output_bytes"] is not None and normalized["max_output_bytes"] < 1:
raise ConfigError("max_output_bytes must be at least 1")
if normalized["max_ratio"] is not None and normalized["max_ratio"] <= 0:
raise ConfigError("max_ratio must be positive")
if normalized["max_review_indicators"] is not None and normalized["max_review_indicators"] < 0:
raise ConfigError("max_review_indicators cannot be negative")
return normalized
def write_config(path: str | Path, config: dict[str, Any]) -> Path:
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(normalize_config(config), indent=2, sort_keys=True) + "\n", encoding="utf-8")
return output_path