"""Small shared helpers: timestamps, hashing, semver, input validation.""" from __future__ import annotations import hashlib import re from datetime import datetime, timezone from pathlib import Path PLUGIN_ID_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{0,63}$") VERSION_RE = re.compile(r"^[0-9A-Za-z][0-9A-Za-z.+-]{0,31}$") SETTING_KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$") USERNAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") def now_iso() -> str: return datetime.now(timezone.utc).isoformat(timespec="seconds") def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sha256_text(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() def sha256_file(path: Path) -> str: h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def semver_tuple(version: str) -> tuple[int, int, int]: """Best-effort numeric compare key: '1.10.0-beta' -> (1, 10, 0).""" parts: list[int] = [] for p in version.split("."): digits = "" for ch in p: if ch.isdigit(): digits += ch else: break parts.append(int(digits) if digits else 0) while len(parts) < 3: parts.append(0) return tuple(parts[:3])