from __future__ import annotations import re from urllib.parse import urlparse APP_ID_RE = re.compile(r"^[a-zA-Z0-9._+-]+$") PACKAGE_NAME_RE = re.compile(r"^[a-zA-Z0-9._+-]+$") SERVICE_NAME_RE = re.compile(r"^[a-zA-Z0-9._@+-]+\.service$") VERSION_RE = re.compile(r"^[a-zA-Z0-9._:+~=-]+$") SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$") def validate_app_id(value: str) -> str: if not value or not APP_ID_RE.fullmatch(value): raise ValueError("appId contains invalid characters") return value def validate_package_name(value: str) -> str: if not value or not PACKAGE_NAME_RE.fullmatch(value): raise ValueError("packageName contains invalid characters") return value def validate_service_name(value: str | None) -> str | None: if value is None or value == "": return None if not SERVICE_NAME_RE.fullmatch(value): raise ValueError("serviceName must be a systemd .service name") return value def validate_version(value: str) -> str: if not value or not VERSION_RE.fullmatch(value): raise ValueError("version contains invalid characters") return value def validate_sha256(value: str) -> str: if not value or not SHA256_RE.fullmatch(value): raise ValueError("sha256/checksum must be a 64 character hex digest") return value.lower() def validate_url_host(url: str, allowed_hosts: list[str]) -> str: parsed = urlparse(url) if parsed.scheme not in {"http", "https"}: raise ValueError("download URL must use http or https") if not parsed.hostname: raise ValueError("download URL is missing a host") if parsed.hostname not in set(allowed_hosts): raise ValueError(f"download host is not allowed: {parsed.hostname}") return url