from collections.abc import Mapping from urllib.parse import parse_qs, urlparse MOBILE_UPDATE_STORES = ("google_play", "rustore", "app_store") MOBILE_UPDATE_FIELDS = ( "enabled", "latest_build", "minimum_build", "latest_version", "store_url", "release_notes", ) MOBILE_UPDATE_SETTING_KEYS = { f"mobile_update.{store}.{field}" for store in MOBILE_UPDATE_STORES for field in MOBILE_UPDATE_FIELDS } MOBILE_UPDATE_BOOLEAN_KEYS = { f"mobile_update.{store}.enabled" for store in MOBILE_UPDATE_STORES } MOBILE_UPDATE_INTEGER_KEYS = { f"mobile_update.{store}.{field}" for store in MOBILE_UPDATE_STORES for field in ("latest_build", "minimum_build") } ANDROID_PACKAGE = "ru.han.chat" def validate_mobile_update_settings(values: Mapping[str, str]) -> None: for store in MOBILE_UPDATE_STORES: prefix = f"mobile_update.{store}" required = [f"{prefix}.{field}" for field in MOBILE_UPDATE_FIELDS] present = [key for key in required if key in values] if not present: continue if len(present) != len(required): missing = sorted(set(required) - values.keys()) raise ValueError(f"{prefix}: incomplete policy; missing {missing}") enabled = _boolean(values[f"{prefix}.enabled"], f"{prefix}.enabled") latest_build = _optional_build(values[f"{prefix}.latest_build"], f"{prefix}.latest_build") minimum_build = _optional_build( values[f"{prefix}.minimum_build"], f"{prefix}.minimum_build" ) latest_version = values[f"{prefix}.latest_version"].strip() store_url = values[f"{prefix}.store_url"].strip() release_notes = values[f"{prefix}.release_notes"].strip() if len(release_notes) > 4000: raise ValueError(f"{prefix}.release_notes: value must not exceed 4000 characters") if not enabled: if any( value not in (None, "") for value in ( latest_build, minimum_build, latest_version, store_url, ) ): raise ValueError(f"{prefix}: disabled policy fields must be empty") continue if latest_build is None or minimum_build is None: raise ValueError(f"{prefix}: enabled policy requires build thresholds") if minimum_build > latest_build: raise ValueError(f"{prefix}: minimum_build must not exceed latest_build") if not latest_version: raise ValueError(f"{prefix}: enabled policy requires latest_version") _validate_store_url(store, store_url, f"{prefix}.store_url") def _boolean(raw: str, key: str) -> bool: if raw not in {"true", "false"}: raise ValueError(f"{key}: canonical boolean value expected") return raw == "true" def _optional_build(raw: str, key: str) -> int | None: if raw == "": return None try: value = int(raw) except ValueError as error: raise ValueError(f"{key}: integer value expected") from error if str(value) != raw or value < 1: raise ValueError(f"{key}: positive canonical integer value expected") return value def _validate_store_url(store: str, raw: str, key: str) -> None: parsed = urlparse(raw) if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password: raise ValueError(f"{key}: absolute HTTPS URL expected") if parsed.fragment: raise ValueError(f"{key}: URL fragments are not allowed") host = parsed.hostname.lower() if store == "google_play": package = parse_qs(parsed.query).get("id", []) valid = host == "play.google.com" and parsed.path == "/store/apps/details" valid = valid and package == [ANDROID_PACKAGE] elif store == "rustore": valid = host == "www.rustore.ru" and parsed.path == ( f"/catalog/app/{ANDROID_PACKAGE}" ) valid = valid and not parsed.query else: valid = host == "apps.apple.com" and "/id" in parsed.path if not valid: raise ValueError(f"{key}: URL does not match the {store} application")