from __future__ import annotations import re from urllib.parse import urlsplit from packaging.utils import canonicalize_name SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$") DISTRIBUTION_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$") IMPORT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,127}$") SHA256_RE = re.compile(r"^[a-fA-F0-9]{64}$") SELF_IMPORT_NAME = "netbox_plugin_store" SELF_DISTRIBUTION_NAME = canonicalize_name("netbox-plugin-store") class ValidationError(ValueError): pass def validate_slug(value: str) -> str: if not isinstance(value, str) or not SLUG_RE.fullmatch(value): raise ValidationError("Invalid plugin slug.") return value def validate_distribution_name(value: str) -> str: if not isinstance(value, str) or not DISTRIBUTION_RE.fullmatch(value): raise ValidationError("Invalid Python distribution name.") return value def validate_import_name(value: str) -> str: if not isinstance(value, str) or not IMPORT_RE.fullmatch(value): raise ValidationError("Invalid Python import name.") return value def validate_sha256(value: str) -> str: if not isinstance(value, str) or not SHA256_RE.fullmatch(value): raise ValidationError("Invalid SHA-256 digest.") return value.lower() def ensure_not_self(package_name: str, import_name: str) -> None: if canonicalize_name(package_name) == SELF_DISTRIBUTION_NAME or import_name == SELF_IMPORT_NAME: raise ValidationError("The Plugin Store cannot manage its own lifecycle.") def validate_public_url(value: str) -> str: parsed = urlsplit(value) if parsed.scheme not in {"https", "http"} or not parsed.hostname: raise ValidationError("URL must be an absolute HTTP(S) URL.") if parsed.username is not None or parsed.password is not None: raise ValidationError("URLs containing credentials are not accepted.") return value