from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path CONFIG_NAME = "config.json" @dataclass(frozen=True) class ClientConfig: hashcat_exe: Path potfile_path: Path client_root: Path wordlists: dict[str, Path] poll_seconds: int max_parallel_jobs: int def load_config(script_dir: Path) -> ClientConfig: path = script_dir / CONFIG_NAME if not path.is_file(): raise FileNotFoundError( f"Missing {path}. " "Copy config.example.json to config.json and edit it." ) try: data = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise ValueError(f"Invalid JSON in {path}: {exc}") from exc if not isinstance(data, dict): raise ValueError("Client config must contain a JSON object.") required = ( "hashcat_exe", "potfile_path", "client_root", "wordlists", "poll_seconds", "max_parallel_jobs", ) missing = [name for name in required if name not in data] if missing: raise ValueError( "Missing required config fields: " + ", ".join(missing) ) for name in ( "hashcat_exe", "potfile_path", "client_root", ): if not isinstance(data[name], str) or not data[name].strip(): raise ValueError( f"Config field {name!r} must be a non-empty string." ) if not isinstance(data["wordlists"], dict) or not data["wordlists"]: raise ValueError( "Config field 'wordlists' must be a non-empty object." ) for name, path_value in data["wordlists"].items(): if not isinstance(name, str) or not name.strip(): raise ValueError( "Each wordlists key must be a non-empty string." ) if not isinstance(path_value, str) or not path_value.strip(): raise ValueError( f"Wordlist {name!r} path must be a non-empty string." ) for name in ( "poll_seconds", "max_parallel_jobs", ): value = data[name] if isinstance(value, bool) or not isinstance(value, int): raise ValueError( f"Config field {name!r} must be an integer." ) if value <= 0: raise ValueError( f"Config field {name!r} must be greater than zero." ) return ClientConfig( hashcat_exe=Path(data["hashcat_exe"]).expanduser().resolve(), potfile_path=Path(data["potfile_path"]).expanduser().resolve(), client_root=Path(data["client_root"]).expanduser().resolve(), wordlists={ name: Path(path_value).expanduser().resolve() for name, path_value in data["wordlists"].items() }, poll_seconds=data["poll_seconds"], max_parallel_jobs=data["max_parallel_jobs"], )