ZapretKVN-android/scripts/verify-project.sh
loop-uh 41b6c3283f
Some checks failed
Android CI / build (push) Has been cancelled
Серверы: страна по IP из офлайн-таблицы DB-IP, флаг и при скрытых адресах
У пользователя 28 из 30 серверов попадали в «Другие»: имена из подписки
не несут флагов, а при включённом скрытии адресов тег, похожий на адрес,
маскировался целиком и вообще не разбирался.

- Страна и протокол берутся из настоящего тега и адреса; маскируется
  только показываемый текст.
- Офлайн-таблица DB-IP IP to Country Lite (CC BY 4.0, атрибуция в
  notices), assets/geoip/country.bin ~2 МБ, IPv4 и IPv6, служебные
  диапазоны всегда без страны; scripts/build-geoip-country.py и проверка
  хэша в verify-project.sh.
- Имена хостов разрешаются тем же доверенным DoH, что и при подключении
  VPN (по IP-литералам, мимо туннеля): один запрос на хост, не чаще раза
  в 7 суток, без таймеров; кэш в noBackupFilesDir.
- Порядок: флаг в имени → страна по IP → сосед на том же адресе → слова.
- JSON-редактор: кнопки в нижней панели, сообщения над ней — снекбар после
  импорта больше не перекрывает «Validate» и «Сохранить».
- Тест обновлятора дожидается закрытия временной VPN-сессии (закрывается в
  finally проверки), а не проверяет это в тот же миг.

Проверено: юнит-тесты, lint, verify-project.sh, 34/34 инструментальных
теста на эмуляторе API 37.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:07:12 +03:00

516 lines
24 KiB
Shell
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST="$PROJECT_ROOT/app/src/main/AndroidManifest.xml"
[[ -f "$PROJECT_ROOT/settings.gradle.kts" ]]
[[ -f "$PROJECT_ROOT/app/build.gradle.kts" ]]
[[ -f "$MANIFEST" ]]
[[ -f "$PROJECT_ROOT/docs/NOTICE" ]]
[[ -f "$PROJECT_ROOT/docs/THIRD_PARTY_NOTICES.md" ]]
[[ -f "$PROJECT_ROOT/gradle/verification-metadata.xml" ]]
[[ -f "$PROJECT_ROOT/app/src/main/res/raw/sing_box_extended_license.txt" ]]
[[ -f "$PROJECT_ROOT/app/src/main/res/raw/sing_geoip_license.txt" ]]
[[ -f "$PROJECT_ROOT/audit/rule_set_performance_test.go" ]]
[[ -f "$PROJECT_ROOT/scripts/core-patchset.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/publish-local-stable.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/publish-forgejo-stable.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/test-publish-forgejo-stable.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/verify-release-bundle.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/derive-test-version.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/build-test-bundle.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/create-test-bundle.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/verify-test-bundle.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/publish-forgejo-test.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/check-libbox-fingerprint.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/ensure-libbox.sh" ]]
[[ -x "$PROJECT_ROOT/scripts/test-libbox-fingerprint.sh" ]]
grep -Fq 'scripts/ensure-libbox.sh' "$PROJECT_ROOT/app/build.gradle.kts"
[[ -f "$PROJECT_ROOT/release.properties" ]]
source "$PROJECT_ROOT/core.properties"
source "$PROJECT_ROOT/scripts/core-patchset.sh"
verify_core_patchset "$PROJECT_ROOT"
"$PROJECT_ROOT/scripts/test-libbox-fingerprint.sh"
[[ -f "$PROJECT_ROOT/scripts/gate8-performance-summary.jq" ]]
RELEASE_WORKFLOW="$PROJECT_ROOT/.forgejo/workflows/release-verify.yml"
grep -Fq 'workflow_dispatch:' "$RELEASE_WORKFLOW"
grep -Fq 'scripts/ci-build.sh' "$RELEASE_WORKFLOW"
grep -Fq 'scripts/verify-release-bundle.sh' "$RELEASE_WORKFLOW"
grep -Fq 'git.zapret.moe/api/v1/repos/' "$RELEASE_WORKFLOW"
if grep -Fq 'release create' "$RELEASE_WORKFLOW" ||
grep -Fq 'ANDROID_SIGNING_KEYSTORE_BASE64' "$RELEASE_WORKFLOW" ||
grep -Fq 'secrets.ANDROID_SIGNING' "$RELEASE_WORKFLOW"; then
echo "Background release verification must not publish or access the production key" >&2
exit 1
fi
if grep -Eq '^[[:space:]]+push:' "$RELEASE_WORKFLOW"; then
echo "Stable verification must be dispatched by the local publisher" >&2
exit 1
fi
LOCAL_PUBLISHER="$PROJECT_ROOT/scripts/publish-local-stable.sh"
grep -Fq -- '--final-gate-approved' "$LOCAL_PUBLISHER"
grep -Fq 'scripts/publish-forgejo-stable.sh' "$LOCAL_PUBLISHER"
grep -Fq 'actions/workflows/release-verify.yml/dispatches' "$LOCAL_PUBLISHER"
grep -Fq 'scripts/ci-build.sh' "$LOCAL_PUBLISHER"
grep -Fq 'scripts/verify-release-bundle.sh' "$LOCAL_PUBLISHER"
FORGEJO_PUBLISHER="$PROJECT_ROOT/scripts/publish-forgejo-stable.sh"
grep -Fq '/api/v1' "$FORGEJO_PUBLISHER"
grep -Fq 'draft:true,prerelease:false' "$FORGEJO_PUBLISHER"
grep -Fq 'draft:false,prerelease:false' "$FORGEJO_PUBLISHER"
grep -Fq 'sha256sum "$downloaded"' "$FORGEJO_PUBLISHER"
TEST_RELEASE_WORKFLOW="$PROJECT_ROOT/.forgejo/workflows/test-release-verify.yml"
grep -Fq 'workflow_dispatch:' "$TEST_RELEASE_WORKFLOW"
grep -Fq 'scripts/verify-test-bundle.sh' "$TEST_RELEASE_WORKFLOW"
grep -Fq ':app:testDebugUnitTest' "$TEST_RELEASE_WORKFLOW"
TEST_PUBLISHER="$PROJECT_ROOT/scripts/publish-forgejo-test.sh"
grep -Fq 'draft:true,prerelease:true' "$TEST_PUBLISHER"
grep -Fq 'draft:false,prerelease:true' "$TEST_PUBLISHER"
grep -Fq 'sha256sum "$downloaded"' "$TEST_PUBLISHER"
grep -Fq 'actions/workflows/test-release-verify.yml/dispatches' "$TEST_PUBLISHER"
"$PROJECT_ROOT/scripts/test-publish-forgejo-stable.sh"
source "$PROJECT_ROOT/release.properties"
if [[ ! "$RELEASE_SIGNER_SHA256" =~ ^[0-9a-f]{64}$ ]]; then
echo "Invalid public production signing fingerprint" >&2
exit 1
fi
cmp -s "$PROJECT_ROOT/docs/LICENSE" "$PROJECT_ROOT/app/src/main/res/raw/license_gpl_3.txt"
cmp -s "$PROJECT_ROOT/docs/NOTICE" "$PROJECT_ROOT/app/src/main/res/raw/notice.txt"
cmp -s "$PROJECT_ROOT/docs/THIRD_PARTY_NOTICES.md" "$PROJECT_ROOT/app/src/main/res/raw/third_party_notices.txt"
WRAPPER_PROPERTIES="$PROJECT_ROOT/gradle/wrapper/gradle-wrapper.properties"
grep -Fqx 'distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip' "$WRAPPER_PROPERTIES"
grep -Fqx 'distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb' "$WRAPPER_PROPERTIES"
printf '%s %s\n' \
'55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c' \
"$PROJECT_ROOT/gradle/wrapper/gradle-wrapper.jar" \
| sha256sum -c - >/dev/null
INVALID_ACTIONS="$(
sed -n 's/^[[:space:]]*uses:[[:space:]]*//p' "$PROJECT_ROOT"/.forgejo/workflows/*.yml \
| grep -Ev '^https://data\.forgejo\.org/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[^[:space:]]+$' || true
)"
if [[ -n "$INVALID_ACTIONS" ]]; then
echo "Forgejo Actions must use explicit trusted action URLs:" >&2
printf '%s\n' "$INVALID_ACTIONS" >&2
exit 1
fi
mapfile -t ANDROID_MODULES < <(
find "$PROJECT_ROOT" -mindepth 2 -maxdepth 2 -name build.gradle.kts -printf '%h\n' \
| sed "s|$PROJECT_ROOT/||" \
| sort
)
EXPECTED_ANDROID_MODULES=(app app-updater network-bootstrap wireguard-import)
if [[ "${ANDROID_MODULES[*]}" != "${EXPECTED_ANDROID_MODULES[*]}" ]]; then
echo "Unexpected Android module set: ${ANDROID_MODULES[*]:-none}" >&2
exit 1
fi
if [[ "$(grep -l 'com.android.application' "$PROJECT_ROOT"/*/build.gradle.kts | wc -l)" -ne 1 ]] ||
! grep -Fq 'com.android.application' "$PROJECT_ROOT/app/build.gradle.kts"; then
echo "Exactly app must be the Android application module" >&2
exit 1
fi
if grep -Eq 'WAKE_LOCK|REQUEST_IGNORE_BATTERY_OPTIMIZATIONS|android:process=' "$MANIFEST"; then
echo "Manifest contains a forbidden permission/process" >&2
exit 1
fi
if grep -R -E 'import android\.util\.Log|\bLog\.[vdiewtf]\(|printStackTrace\(|System\.(out|err)' \
"$PROJECT_ROOT/app/src/main/java" "$PROJECT_ROOT/app-updater/src/main/java"; then
echo "Production source must not write credentials or runtime details to Logcat/stdout" >&2
exit 1
fi
if grep -R -E 'addPrimaryClipChangedListener|setPrimaryClip\(' \
"$PROJECT_ROOT/app/src/main/java" "$PROJECT_ROOT/app-updater/src/main/java"; then
echo "Application clipboard history/listeners are forbidden; clipboard import is read-on-action only" >&2
exit 1
fi
if grep -R -E 'android\.os\.PowerManager|android\.app\.AlarmManager|androidx\.work|ScheduledExecutorService|scheduleAtFixedRate|scheduleWithFixedDelay' \
"$PROJECT_ROOT/app/src/main/java" "$PROJECT_ROOT/app-updater/src/main/java"; then
echo "Production source contains an app-owned wake/alarm/work/periodic scheduler" >&2
exit 1
fi
# The only app-owned scheduler is the platform JobScheduler daily update check (no wakelock, no alarm,
# no WorkManager, not persisted across reboot because RECEIVE_BOOT_COMPLETED is not requested).
JOB_SCHEDULER_USERS="$(
grep -R -l 'android\.app\.job\.JobScheduler' "$PROJECT_ROOT/app/src/main/java" "$PROJECT_ROOT/app-updater/src/main/java" || true
)"
if [[ "$JOB_SCHEDULER_USERS" != "$PROJECT_ROOT/app/src/main/java/io/github/zapretkvn/android/updates/UpdateCheckJob.kt" ]]; then
echo "JobScheduler may only be used by the daily update check (updates/UpdateCheckJob.kt): $JOB_SCHEDULER_USERS" >&2
exit 1
fi
if grep -R -F 'CommandConnections' "$PROJECT_ROOT/app/src/main/java"; then
echo "Production runtime must not subscribe to per-connection polling" >&2
exit 1
fi
python3 - "$PROJECT_ROOT" <<'PY'
from pathlib import Path
import sys
root = Path(sys.argv[1])
source_root = root / "app/src/main/java"
for source in source_root.rglob("*.kt"):
declaration = next((line for line in source.read_text().splitlines() if line.startswith("package ")), "")
expected_package = ".".join(source.relative_to(source_root).parts[:-1])
if declaration != "package " + expected_package:
raise SystemExit(f"Kotlin package/path mismatch: {source.relative_to(root)}")
vpn_root = source_root / "io/github/zapretkvn/android/vpn"
if {path.name for path in vpn_root.glob("*.kt")} != {
"ZapretVpnService.kt", "ZapretQuickSettingsTileService.kt",
"VpnController.kt", "VpnState.kt", "VpnRecoveryPolicy.kt",
}:
raise SystemExit("Keep VPN lifecycle in vpn/; place adapters, probes and app scope in their packages")
delays = []
for source in (
root / "app/src/main/java",
root / "app-updater/src/main/java",
root / "network-bootstrap/src/main/java",
):
for path in source.rglob("*.kt"):
count = path.read_text().count("delay(")
if count:
delays.append((str(path.relative_to(root)), count))
delays.sort()
expected = [
# Случайная пауза 0,15–1,2 с между разовыми DoH-запросами страны сервера внутри
# одной пачки (не больше 3 параллельно): запросы не идут залпом. Пачка стартует
# только после импорта/обновления профиля или при запуске; повторов и циклов нет.
("app/src/main/java/io/github/zapretkvn/android/network/geo/ServerCountryStore.kt", 1),
("app/src/main/java/io/github/zapretkvn/android/network/probes/HealthProbeRace.kt", 1),
# Разовый случайный сдвиг старта ICMP-замера (ICMP_START_JITTER_MILLIS):
# воркеры не шлют эхо-запросы одним залпом. Не повтор и не цикл ожидания.
("app/src/main/java/io/github/zapretkvn/android/network/probes/LatencyProbeCoordinator.kt", 1),
# Видимое время сессии на Главной: тикает, только пока таймер на экране.
("app/src/main/java/io/github/zapretkvn/android/ui/home/HomeScreen.kt", 1),
# Прореживание перерисовки списка серверов (не чаще 4 раз в секунду) во
# время пачек замеров. Только UI: сетевых запросов и повторов не создаёт.
("app/src/main/java/io/github/zapretkvn/android/ui/servers/ServersRoute.kt", 1),
# Единый дебаунс смены сети (перезапуск, пауза и возобновление по правилам
# сети) плюс ограниченный backoff восстановления. Одновременно активен
# только один debounce Job; ожидание сети событийное и таймера не добавляет.
("app/src/main/java/io/github/zapretkvn/android/vpn/runtime/VpnRuntime.kt", 2),
("network-bootstrap/src/main/java/io/github/zapretkvn/networkbootstrap/UnderlyingNetworkMonitor.kt", 1),
]
if delays != expected:
raise SystemExit(f"Unexpected production timer/retry surface: {delays!r}")
print(
"Production delay calls are limited to visible session time, server-list redraw throttle, probe stagger, "
"network debounce, bounded recovery backoff and bootstrap settle."
)
PY
python3 - "$MANIFEST" "$PROJECT_ROOT/app/src/main/res/xml/diagnostic_file_paths.xml" <<'PY'
from pathlib import Path
import sys
import xml.etree.ElementTree as ET
android = "{http://schemas.android.com/apk/res/android}"
manifest_path, paths_path = map(Path, sys.argv[1:])
root = ET.parse(manifest_path).getroot()
application = root.find("application")
if application is None:
raise SystemExit("Manifest has no application")
if application.get(android + "usesCleartextTraffic") != "false":
raise SystemExit("Application must explicitly reject platform cleartext traffic")
source_permissions = {node.get(android + "name") for node in root.findall("uses-permission")}
expected_source_permissions = {
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_WIFI_STATE",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED",
"android.permission.POST_NOTIFICATIONS",
"android.permission.REQUEST_INSTALL_PACKAGES",
"android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION",
"android.permission.CAMERA",
"android.permission.QUERY_ALL_PACKAGES",
}
if source_permissions != expected_source_permissions:
raise SystemExit(
f"Source manifest permission allowlist mismatch: "
f"{sorted(source_permissions ^ expected_source_permissions)}"
)
queries = root.find("queries")
if queries is None:
raise SystemExit("Manifest must declare launcher package visibility fallback")
query_intents = set()
for intent in queries.findall("intent"):
actions = tuple(sorted(
node.get(android + "name") for node in intent.findall("action")
))
categories = tuple(sorted(
node.get(android + "name") for node in intent.findall("category")
))
query_intents.add((actions, categories))
expected_query_intents = {
(("android.intent.action.MAIN",), ("android.intent.category.LAUNCHER",)),
(("android.intent.action.MAIN",), ("android.intent.category.LEANBACK_LAUNCHER",)),
}
if query_intents != expected_query_intents:
raise SystemExit(
f"Launcher package visibility queries mismatch: "
f"{sorted(query_intents ^ expected_query_intents)}"
)
profileable = application.findall("profileable")
if len(profileable) != 1 or profileable[0].get(android + "shell") != "true":
raise SystemExit("Release-gate profiling requires exactly one shell-profileable declaration")
vpn_services = [
node for node in application.findall("service")
if node.get(android + "permission") == "android.permission.BIND_VPN_SERVICE"
]
if len(vpn_services) != 1:
raise SystemExit(f"Expected exactly one VPN service, found {len(vpn_services)}")
vpn_service = vpn_services[0]
if vpn_service.get(android + "exported") != "false":
raise SystemExit("VPN service must not be exported")
always_on_metadata = [
node for node in vpn_service.findall("meta-data")
if node.get(android + "name") == "android.net.VpnService.SUPPORTS_ALWAYS_ON"
]
if len(always_on_metadata) != 1 or always_on_metadata[0].get(android + "value") != "false":
raise SystemExit("VPN service must explicitly opt out of Always-on")
job_services = [
node for node in application.findall("service")
if node.get(android + "permission") == "android.permission.BIND_JOB_SERVICE"
]
if (
len(job_services) != 1
or job_services[0].get(android + "name") != ".updates.UpdateCheckJobService"
or job_services[0].get(android + "exported") != "false"
or job_services[0].findall("intent-filter")
):
raise SystemExit("Expected exactly one non-exported update-check JobService without intent filters")
receivers = application.findall("receiver")
if (
len(receivers) != 1
or receivers[0].get(android + "name") != ".updates.UpdateInstallReceiver"
or receivers[0].get(android + "exported") != "false"
or receivers[0].findall("intent-filter")
):
raise SystemExit("Expected exactly one non-exported install-result receiver without intent filters")
quick_settings_tiles = [
node for node in application.findall("service")
if node.get(android + "permission") == "android.permission.BIND_QUICK_SETTINGS_TILE"
]
if len(quick_settings_tiles) != 1:
raise SystemExit(
f"Expected exactly one Quick Settings tile service, found {len(quick_settings_tiles)}"
)
quick_settings_tile = quick_settings_tiles[0]
if (
quick_settings_tile.get(android + "name") != ".vpn.ZapretQuickSettingsTileService"
or quick_settings_tile.get(android + "exported") != "true"
):
raise SystemExit("Quick Settings tile must be the single exported system-bound tile service")
tile_actions = {
action.get(android + "name")
for intent_filter in quick_settings_tile.findall("intent-filter")
for action in intent_filter.findall("action")
}
if tile_actions != {"android.service.quicksettings.action.QS_TILE"}:
raise SystemExit(f"Quick Settings tile intent actions differ: {sorted(tile_actions)}")
providers = [
node for node in application.findall("provider")
if node.get(android + "name") == "androidx.core.content.FileProvider"
]
if len(providers) != 1:
raise SystemExit(f"Expected one FileProvider, found {len(providers)}")
provider = providers[0]
expected = {
android + "authorities": "${applicationId}.fileprovider",
android + "exported": "false",
android + "grantUriPermissions": "true",
}
for attribute, value in expected.items():
if provider.get(attribute) != value:
raise SystemExit(f"Unsafe FileProvider attribute {attribute}: {provider.get(attribute)!r}")
metadata = provider.findall("meta-data")
if len(metadata) != 1 or metadata[0].get(android + "resource") != "@xml/diagnostic_file_paths":
raise SystemExit("FileProvider must reference only diagnostic_file_paths")
paths = ET.parse(paths_path).getroot()
children = list(paths)
entries = {(child.tag, child.get("name"), child.get("path")) for child in children}
expected_paths = {
("cache-path", "diagnostics", "diagnostics/"),
}
if entries != expected_paths:
raise SystemExit(f"FileProvider paths differ from bounded cache paths: {entries!r}")
PY
python3 - "$PROJECT_ROOT" <<'PY'
from pathlib import Path
import hashlib
import re
import sys
root = Path(sys.argv[1])
docs_root = root / "docs"
docs = [docs_root / name for name in (
"ARCHITECTURE.md",
"DNS_ARCHITECTURE.md",
"ROUTING_ARCHITECTURE.md",
"IMPORT_FORMATS.md",
"IMPLEMENTATION_PLAN.md",
"README.md",
"LICENSING.md",
"SIGNING.md",
"GATE8_RESULTS.md",
"THIRD_PARTY_NOTICES.md",
)]
all_text = "\n".join(path.read_text() for path in docs)
fixture_root = root / "testdata"
manifest = {}
for line in (fixture_root / "SHA256SUMS").read_text().splitlines():
digest, relative = line.split(maxsplit=1)
manifest[relative] = digest
fixtures = sorted(fixture_root.rglob("*.json"))
fixture_names = {str(path.relative_to(fixture_root)) for path in fixtures}
if fixture_names != set(manifest):
raise SystemExit("Fixture set differs from testdata/SHA256SUMS")
for fixture in fixtures:
digest = hashlib.sha256(fixture.read_bytes()).hexdigest()
relative = str(fixture.relative_to(fixture_root))
if manifest[relative] != digest:
raise SystemExit(f"Fixture hash mismatch: {fixture}: {digest}")
if digest not in all_text:
raise SystemExit(f"Fixture hash is not documented: {fixture}: {digest}")
for doc in docs:
text = doc.read_text()
if len(re.findall(r"^```", text, re.MULTILINE)) % 2:
raise SystemExit(f"Unbalanced Markdown fences: {doc}")
for target in re.findall(r"\[[^\]]+\]\(([^)]+)\)", text):
if "://" in target or target.startswith("#"):
continue
local = target.split("#", 1)[0]
if local and not (doc.parent / local).exists():
raise SystemExit(f"Broken local link in {doc}: {target}")
print("Markdown, local links and fixture hashes are valid.")
PY
python3 "$PROJECT_ROOT/scripts/check-release-changes.py" "$PROJECT_ROOT/scripts/release-changes.txt"
python3 - "$PROJECT_ROOT" <<'PY'
from pathlib import Path
import sys
root = Path(sys.argv[1])
surfaces = {
"README": (root / "docs/README.md").read_text(),
"UI": (root / "app/src/main/java/io/github/zapretkvn/android/ui/settings/AboutPage.kt").read_text(),
"release notes generator": (root / "scripts/create-release-bundle.sh").read_text(),
}
required = (
"Известные ограничения MVP",
"arm64-v8a",
"Always-on/Lockdown",
"shared UID",
"DoH, DoT",
"FakeIP",
"Domain-only block",
"Clash YAML",
"Hysteria v1",
"silent install",
"plaintext DNS fallback",
)
for surface, text in surfaces.items():
missing = [value for value in required if value not in text]
if missing:
raise SystemExit(f"{surface} omits known limitations: {missing!r}")
print("Known limitations are synchronized across UI, README and release notes.")
PY
python3 - "$PROJECT_ROOT" <<'PY'
from pathlib import Path
import hashlib
import json
import sys
root = Path(sys.argv[1])
asset_root = root / "app/src/main/assets/rule-sets"
manifest = json.loads((asset_root / "manifest.json").read_text())
if manifest.get("generated_with_core") != "ff11f007ec798136a5de258f947a4f34011a37ea":
raise SystemExit("Rule-set manifest core revision mismatch")
expected = {"zapret-ru-domains", "zapret-ru-ip"}
entries = manifest.get("sets", [])
if {entry["tag"] for entry in entries} != expected:
raise SystemExit("Unexpected packaged rule-set tag set")
for entry in entries:
path = asset_root / entry["file"]
digest = hashlib.sha256(path.read_bytes()).hexdigest()
if digest != entry["sha256"]:
raise SystemExit(f"Rule-set asset hash mismatch: {path}: {digest}")
print("Packaged rule-set manifest and hashes are valid.")
PY
python3 - "$PROJECT_ROOT" <<'PY'
from pathlib import Path
import hashlib
import json
import re
import struct
import sys
root = Path(sys.argv[1])
asset_root = root / "app/src/main/assets/geoip"
manifest = json.loads((asset_root / "manifest.json").read_text())
blob = (asset_root / manifest["file"]).read_bytes()
if hashlib.sha256(blob).hexdigest() != manifest["sha256"] or len(blob) != manifest["bytes"]:
raise SystemExit("GeoIP country asset hash/size mismatch")
if blob[:4] != b"ZGC1" or manifest.get("format") != "ZGC1":
raise SystemExit("GeoIP country asset has an unexpected format")
if len(blob) > 3 * 1024 * 1024:
raise SystemExit("GeoIP country asset exceeds the 3 MiB budget")
if manifest.get("license") != "CC-BY-4.0" or manifest.get("attribution") != "IP Geolocation by DB-IP":
raise SystemExit("GeoIP manifest lost the DB-IP license/attribution")
if not re.fullmatch(r"\d{4}-\d{2}", manifest["month"]) or not manifest["source"].endswith(
f"dbip-country-lite-{manifest['month']}.csv.gz"
):
raise SystemExit("GeoIP manifest source/month mismatch")
month, count = struct.unpack_from(">IB", blob, 4)
if month != int(manifest["month"].replace("-", "")) or count != manifest["countries"]:
raise SystemExit("GeoIP asset header disagrees with its manifest")
notices = (root / "docs/THIRD_PARTY_NOTICES.md").read_text()
for required in ("IP Geolocation by DB-IP", "https://db-ip.com", "CC BY 4.0"):
if required not in notices:
raise SystemExit(f"Third-party notices omit the DB-IP attribution: {required!r}")
print("Packaged GeoIP country asset, manifest and attribution are valid.")
PY
PERFORMANCE_POLICY="$PROJECT_ROOT/scripts/gate8-performance-summary.jq"
POLICY_RESULT="$(
printf '%s\n' \
'{"scenario":"stack_current_mixed","cpu_ticks":100,"elapsed_nanos":1000000000,"pss_kb":100,"rss_kb":100,"tun_bytes":100,"throughput_mbps":100}' \
'{"scenario":"stack_system","cpu_ticks":104,"elapsed_nanos":1040000000,"pss_kb":104,"rss_kb":104,"tun_bytes":104,"throughput_mbps":104}' \
'{"scenario":"mtu_default_9000","cpu_ticks":100,"elapsed_nanos":1000000000,"pss_kb":100,"rss_kb":100,"tun_bytes":100,"throughput_mbps":100}' \
'{"scenario":"mtu_1500","cpu_ticks":106,"elapsed_nanos":1060000000,"pss_kb":106,"rss_kb":106,"tun_bytes":106,"throughput_mbps":106}' \
| jq -s --argjson threshold 5 -f "$PERFORMANCE_POLICY"
)"
BELOW_THRESHOLD_DECISION="$(
jq -r '.comparisons[] | select(.name == "mixed_vs_system") | .decision' <<<"$POLICY_RESULT"
)"
ABOVE_THRESHOLD_DECISION="$(
jq -r '.comparisons[] | select(.name == "mtu_default_vs_1500") | .decision' <<<"$POLICY_RESULT"
)"
if [[ "$BELOW_THRESHOLD_DECISION" != "NO_CHANGE_BELOW_5_PERCENT_OR_NO_SIGNAL" ]]; then
echo "Performance policy accepted a sub-5% change" >&2
exit 1
fi
if [[ "$ABOVE_THRESHOLD_DECISION" != "PHYSICAL_CONFIRMATION_REQUIRED" ]]; then
echo "Performance policy ignored a >=5% change" >&2
exit 1
fi
echo "Performance significance policy verified."
echo "Project structure verified."