mirror of
https://github.com/bikini/patchwork
synced 2026-06-27 08:08:41 +00:00
132 lines
4.1 KiB
Python
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
|