"""challenges.yml: which repos are part of this CTF, and their check exceptions.""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path import yaml from .challenge import SELECTIONS from .checks import REPO_CHECKS SELECTION_FILE = Path(__file__).parent.parent / "challenges.yml" # next to ctf_status.py @dataclass class Selection: states: dict # repo path -> one of SELECTIONS skip_checks: dict # repo path -> names of REPO_CHECKS it need not pass def load_selection(path: Path = SELECTION_FILE) -> Selection: """Raises ValueError on anything off. A broken file must stop the run: without it nothing counts as included, and challs.txt would be committed empty.""" try: doc = yaml.safe_load(path.read_text()) except (OSError, yaml.YAMLError) as e: raise ValueError(f"cannot read {path.name}: {e}") from None states = doc.get("challenges") if isinstance(doc, dict) else None if not states or not isinstance(states, dict): raise ValueError(f"{path.name} has no `challenges:` mapping") bad = [f"{k}: {v}" for k, v in states.items() if v not in SELECTIONS] if bad: raise ValueError(f"{path.name}: expected one of {', '.join(SELECTIONS)}, got " + ", ".join(bad)) skip = doc.get("skip_checks") or {} if not isinstance(skip, dict) or not all(isinstance(v, list) for v in skip.values()): raise ValueError(f"{path.name}: `skip_checks:` must map repos to lists of checks") known = {check.__name__ for check in REPO_CHECKS} bad = sorted({str(n) for names in skip.values() for n in names} - known) if bad: raise ValueError(f"{path.name}: no such repo check: " + ", ".join(bad)) return Selection({str(k): v for k, v in states.items()}, {str(k): [str(n) for n in v] for k, v in skip.items()})