import os import re from pathlib import Path _INSTANCE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$") DEFAULT_LEFT4ME_ROOT = Path("/var/lib/left4me") def get_left4me_root() -> Path: raw = os.environ.get("LEFT4ME_ROOT") if raw is None: return DEFAULT_LEFT4ME_ROOT root = raw.strip() if not root: raise ValueError("LEFT4ME_ROOT must not be empty") if root != raw: raise ValueError("LEFT4ME_ROOT must not contain leading or trailing whitespace") path = Path(root) if not path.is_absolute(): raise ValueError("LEFT4ME_ROOT must be absolute") return path def validate_instance_name(name: str) -> str: if not _INSTANCE_NAME_RE.fullmatch(name): raise ValueError( "instance name must match [a-z0-9][a-z0-9_-]{0,63} " "(lowercase, no path separators, no whitespace)" ) return name def validate_overlay_ref(ref: str) -> str: stripped = ref.strip() if stripped != ref: raise ValueError("overlay ref must not contain leading or trailing whitespace") if stripped in {"", ".", ".."}: raise ValueError("overlay ref must not be empty or current/parent directory") if Path(stripped).is_absolute(): raise ValueError("overlay ref must be relative") components = stripped.split("/") if any(component in {"", ".", ".."} for component in components): raise ValueError("overlay ref must not contain empty, current, or parent components") return stripped def overlay_path(ref: str, *, root: Path | None = None) -> Path: safe_ref = validate_overlay_ref(ref) left4me_root = get_left4me_root() if root is None else Path(root) overlays_root = left4me_root / "overlays" candidate = overlays_root / safe_ref resolved_overlays_root = overlays_root.resolve() resolved_candidate = candidate.resolve() if resolved_candidate != resolved_overlays_root and resolved_overlays_root not in resolved_candidate.parents: raise ValueError("overlay path escapes overlay root") return candidate