magisk-zapret2/zapret2/scripts/package-contract.sh
loop-uh 05b9e0af8c Let one preset change be one transaction, not six conversations
Applying a preset was a conversation the app held with the module: snapshot
runtime.ini, ask whether the service was running, read runtime.ini again, stage
a candidate, commit it, verify it, then restart. Seven privileged round trips,
each paying a libsu round trip and a full common.sh sourcing, and — worse — the
rollback decisions lived in Kotlin. The app was deciding what the module's state
was, which is exactly what the typed machine payload exists to prevent.

zapret2/scripts/zapret-apply-preset.sh is now the single app-facing entry point
for that mutation. It sources the lifecycle helpers once, inherits the Android
lease through ZAPRET2_LIFECYCLE_TOKEN, and under that one lock it validates the
request with the packaged name policy, qualifies the preset through the same
compile-and-dry-run the preview machinery already owned, publishes the selection
through runtime-config.sh --commit-candidate, and hands the replacement to
zapret-start.sh --replace exactly as zapret-restart.sh does. Nothing here
reimplements a transaction that already had an owner.

Because the qualification happens before the commit, an incompatible preset is
now refused with the live selection untouched instead of being written, failed
and rolled back. A failed replacement restores the previous generation and says
so, and a rollback that cannot restore it says that instead — the payload
distinguishes the two, carrying the exact Z2_ERROR identity of whichever nested
stage refused. A service the user had stopped is still only saved to, measured
from the committed lifecycle receipt the way zapret-status.sh derives Z2_PROCESS.

The app projects that payload and nothing more. It keeps the stepwise flow for
one reason: a module generation installed before this entry point existed cannot
grow it, so the same round trip answers with an unsupported sentinel and the old
path runs. A payload that is truncated, inconsistent or written by a newer module
is never guessed at — the repository reads the published selection, and accepts
an application only when the live owner generation is the one this lease stamped,
which is the same proof the restart path already required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:58:07 +03:00

1216 lines
52 KiB
Shell

#!/system/bin/sh
# Shared parser and validator for zapret2/runtime-manifest.tsv.
#
# The manifest separates immutable package files, mutable seed data, ABI
# binaries, generated installed files, catalog entries, and the exact declared
# dependency closure for compatible presets. Mutable seeds are required in a
# fresh package but may be restored from the previous installation afterwards.
PACKAGE_CONTRACT_MANIFEST_REL="zapret2/runtime-manifest.tsv"
PACKAGE_CONTRACT_OWNER_PROTOCOL=7
PACKAGE_CONTRACT_MAX_MANIFEST_BYTES=262144
PACKAGE_CONTRACT_MAX_MODULE_PROP_BYTES=4096
PACKAGE_CONTRACT_MAX_SHELL_EXEC_BYTES=262144
PACKAGE_CONTRACT_UPDATE_JSON="https://github.com/youtubediscord/magisk-zapret2/releases/latest/download/update.json"
# Mounted /system/bin wrappers no longer have a module-relative $0. All three
# supported managers define this same active root, so the package contract owns
# the one intentional absolute entrypoint path.
PACKAGE_CONTRACT_ACTIVE_MODULE_DIR="/data/adb/modules/zapret2"
PACKAGE_CONTRACT_CODE="OK"
PACKAGE_CONTRACT_DETAIL=""
package_contract_fail() {
PACKAGE_CONTRACT_CODE="$1"
PACKAGE_CONTRACT_DETAIL="${2:-}"
return 1
}
package_contract_safe_relative_path_syntax() {
local path="$1"
[ -n "$path" ] || return 1
[ "${path# }" = "$path" ] && [ "${path% }" = "$path" ] || return 1
case "$path" in
*'|'*) return 1 ;;
/*|*\\*|*//*|*/|../*|*/../*|*/..|.|./*|*/./*|*/.) return 1 ;;
esac
package_contract_safe_path_component_lengths "$path" || return 1
return 0
}
package_contract_safe_relative_path() {
local path="$1"
package_contract_safe_relative_path_syntax "$path" || return 1
if LC_ALL=C printf '%s' "$path" | grep -q '[[:cntrl:]]'; then return 1; fi
return 0
}
package_contract_safe_file_name_byte_length() {
local value="$1"
local LC_ALL=C
[ "${#value}" -le 255 ] 2>/dev/null
}
package_contract_safe_path_component_lengths() {
local remainder="$1" component
while :; do
case "$remainder" in
*/*) component="${remainder%%/*}"; remainder="${remainder#*/}" ;;
*) component="$remainder"; remainder="" ;;
esac
package_contract_safe_file_name_byte_length "$component" || return 1
[ -n "$remainder" ] || break
done
return 0
}
package_contract_safe_preset_name_syntax() {
local value="$1"
[ -n "$value" ] && package_contract_safe_file_name_byte_length "$value" || return 1
[ "${value# }" = "$value" ] && [ "${value% }" = "$value" ] || return 1
case "$value" in
_*|.|..|*/*|*\\*|*"'"*|*'"'*|*.TXT|*.Txt|*.tXt|*.txT|*.TXt|*.TxT|*.tXT) return 1 ;;
*.txt) ;;
*) return 1 ;;
esac
return 0
}
package_contract_safe_preset_name() {
local value="$1"
package_contract_safe_preset_name_syntax "$value" || return 1
if LC_ALL=C printf '%s' "$value" | grep -q '[[:cntrl:]]'; then return 1; fi
return 0
}
package_contract_runtime_core_value() {
local root="${1%/}" key="$2" runtime
runtime="$root/zapret2/runtime.ini"
[ -f "$runtime" ] && [ ! -L "$runtime" ] || return 1
awk -v wanted="$key" '
function trim(v) { sub(/^[[:space:]]+/, "", v); sub(/[[:space:]]+$/, "", v); return v }
{
line=$0
sub(/\r$/, "", line)
normalized=trim(line)
}
normalized ~ /^\[[^]]+\]$/ { section=normalized; next }
section == "[core]" && normalized !~ /^[#;]/ {
separator=index(line, "=")
if (separator == 0) next
key=trim(substr(line, 1, separator - 1))
if (key == wanted) {
count++
value=trim(substr(line, separator + 1))
first=substr(value, 1, 1)
last=substr(value, length(value), 1)
if (first == "\"" || first == "\047") {
if (last != first) invalid=1
else value=substr(value, 2, length(value) - 2)
} else if (last == "\"" || last == "\047") invalid=1
}
}
END { if (invalid || count != 1 || value == "") exit 1; printf "%s", value }
' "$runtime"
}
package_contract_manifest_has_entry() {
local manifest="$1"
local expected="$2"
local class=""
local mode=""
local path=""
local extra=""
local cr
cr=$(printf '\r')
while IFS='|' read -r class mode path extra || [ -n "$class$mode$path$extra" ]; do
class="${class%"$cr"}"
mode="${mode%"$cr"}"
path="${path%"$cr"}"
extra="${extra%"$cr"}"
[ -z "$extra" ] || continue
[ "$class|$mode|$path" = "$expected" ] && return 0
done < "$manifest"
return 1
}
package_contract_validate_manifest_paths_file() {
local paths="$1" failure="" code="" detail=""
failure="$(LC_ALL=C awk '
{
path=$0
if (seen[path]++) {
printf "MANIFEST_DUPLICATE_PATH|%s\n", path
failed=1
exit
}
paths[++count]=path
}
END {
if (failed) exit
for (i=1; i<=count; i++) {
path=paths[i]
remainder=path
prefix=""
while ((separator=index(remainder, "/")) > 0) {
component=substr(remainder, 1, separator - 1)
remainder=substr(remainder, separator + 1)
prefix=(prefix == "" ? component : prefix "/" component)
if (prefix in seen) {
printf "MANIFEST_PATH_COLLISION|%s\n", path
exit
}
}
}
}
' "$paths")" || {
package_contract_fail "MANIFEST_TEMP_FAILED"
return 1
}
[ -z "$failure" ] && return 0
code="${failure%%|*}"
detail="${failure#*|}"
package_contract_fail "$code" "$detail"
}
package_contract_validate_manifest() {
local root="$1"
local manifest="$root/$PACKAGE_CONTRACT_MANIFEST_REL"
local seen="" entries="" required="" missing=""
local class=""
local mode=""
local path=""
local extra=""
local schema_count=0
local owner_protocol_count=0
local metadata_stage=0
local line_number=0
local entry_count=0
local required_entry=""
local manifest_bytes=""
local expanded_abi=""
local expanded_path=""
local cr
PACKAGE_CONTRACT_CODE="OK"
PACKAGE_CONTRACT_DETAIL=""
[ -f "$manifest" ] && [ ! -L "$manifest" ] && [ -s "$manifest" ] && [ -r "$manifest" ] || {
package_contract_fail "MANIFEST_MISSING" "$PACKAGE_CONTRACT_MANIFEST_REL"
return 1
}
manifest_bytes="$(wc -c < "$manifest" 2>/dev/null)" || {
package_contract_fail "MANIFEST_SIZE_UNREADABLE" "$PACKAGE_CONTRACT_MANIFEST_REL"
return 1
}
case "$manifest_bytes" in ''|*[!0-9]*) package_contract_fail "MANIFEST_SIZE_UNREADABLE" "$manifest_bytes"; return 1 ;; esac
[ "$manifest_bytes" -le "$PACKAGE_CONTRACT_MAX_MANIFEST_BYTES" ] || {
package_contract_fail "MANIFEST_TOO_LARGE" "$manifest_bytes"
return 1
}
LC_ALL=C awk '
{
line=$0
sub(/\r$/, "", line)
if (line ~ /[[:cntrl:]]/) exit 1
}
' "$manifest" || {
package_contract_fail "MANIFEST_CONTROL_CHARACTER" "$PACKAGE_CONTRACT_MANIFEST_REL"
return 1
}
seen="$(mktemp)" || { package_contract_fail "MANIFEST_TEMP_FAILED"; return 1; }
entries="$(mktemp)" || { rm -f "$seen"; package_contract_fail "MANIFEST_TEMP_FAILED"; return 1; }
cr=$(printf '\r')
while IFS='|' read -r class mode path extra || [ -n "$class$mode$path$extra" ]; do
line_number=$((line_number + 1))
class="${class%"$cr"}"
mode="${mode%"$cr"}"
path="${path%"$cr"}"
extra="${extra%"$cr"}"
case "$class" in ""|'#'*) continue ;; esac
[ -z "$extra" ] || { rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_FIELD_COUNT" "$class"; return 1; }
if [ "$class" = "schema" ]; then
[ "$metadata_stage" -eq 0 ] && [ "$line_number" -eq 1 ] || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_METADATA_ORDER" "schema"; return 1;
}
[ "$mode" = "1" ] && [ "$path" = "zapret2-runtime" ] || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_SCHEMA" "$mode|$path"; return 1;
}
schema_count=$((schema_count + 1))
metadata_stage=1
continue
fi
if [ "$class" = "owner_protocol" ]; then
[ "$metadata_stage" -eq 1 ] && [ "$line_number" -eq 2 ] || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_METADATA_ORDER" "owner_protocol"; return 1;
}
[ "$mode" = "$PACKAGE_CONTRACT_OWNER_PROTOCOL" ] && [ "$path" = "zapret2-firewall" ] || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_OWNER_PROTOCOL" "$mode|$path"; return 1;
}
owner_protocol_count=$((owner_protocol_count + 1))
metadata_stage=2
continue
fi
[ "$metadata_stage" -eq 2 ] || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_METADATA_ORDER" "$class"; return 1;
}
case "$class" in
immutable-file|mutable-seed|runtime-dependency-immutable|runtime-dependency-mutable-seed|preset-compatible|preset-quarantined)
[ "$mode" = "0644" ] || { rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_MODE" "$class|$mode"; return 1; }
;;
immutable-exec|abi-exec|installed-exec)
[ "$mode" = "0755" ] || { rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_MODE" "$class|$mode"; return 1; }
;;
*) rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_CLASS" "$class"; return 1 ;;
esac
package_contract_safe_relative_path_syntax "$path" || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_PATH" "$path"; return 1;
}
case "$class:$path" in
abi-exec:zapret2/bin/'{abi}'/nfqws2) ;;
abi-exec:*) rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_ABI_TEMPLATE" "$path"; return 1 ;;
installed-exec:zapret2/nfqws2) ;;
installed-exec:*) rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_INSTALLED_EXEC" "$path"; return 1 ;;
preset-compatible:zapret2/presets/*.txt|preset-quarantined:zapret2/presets/*.txt)
package_contract_safe_preset_name_syntax "${path##*/}" || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_PRESET_PATH" "$path"; return 1;
}
;;
preset-compatible:*|preset-quarantined:*) rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_PRESET_PATH" "$path"; return 1 ;;
esac
if [ "$class" = "abi-exec" ]; then
for expanded_abi in arm64-v8a armeabi-v7a; do
expanded_path="zapret2/bin/$expanded_abi/nfqws2"
printf '%s\n' "$expanded_path" >> "$seen" || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_TEMP_FAILED"; return 1;
}
done
else
printf '%s\n' "$path" >> "$seen" || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_TEMP_FAILED"; return 1;
}
fi
printf '%s|%s|%s\n' "$class" "$mode" "$path" >> "$entries" || {
rm -f "$seen" "$entries"; package_contract_fail "MANIFEST_TEMP_FAILED"; return 1;
}
entry_count=$((entry_count + 1))
done < "$manifest"
if ! package_contract_validate_manifest_paths_file "$seen"; then
rm -f "$seen" "$entries"
return 1
fi
rm -f "$seen"
[ "$schema_count" -eq 1 ] || { rm -f "$entries"; package_contract_fail "MANIFEST_SCHEMA_COUNT" "$schema_count"; return 1; }
[ "$owner_protocol_count" -eq 1 ] || { rm -f "$entries"; package_contract_fail "MANIFEST_OWNER_PROTOCOL_COUNT" "$owner_protocol_count"; return 1; }
[ "$entry_count" -gt 0 ] || { rm -f "$entries"; package_contract_fail "MANIFEST_EMPTY"; return 1; }
required="$(mktemp)" || { rm -f "$entries"; package_contract_fail "MANIFEST_TEMP_FAILED"; return 1; }
for required_entry in \
"immutable-file|0644|module.prop" \
"immutable-file|0644|zapret2/runtime-manifest.tsv" \
"immutable-file|0644|zapret2/lifecycle-contract.version" \
"immutable-file|0644|zapret2/upstream-zapret2.commit" \
"immutable-file|0644|zapret2/upstream-zapret2.release" \
"immutable-file|0644|zapret2/upstream-zapret2.archive.sha256" \
"immutable-file|0644|zapret2/strategy-catalogs/tcp.txt" \
"immutable-file|0644|zapret2/strategy-catalogs/udp.txt" \
"immutable-file|0644|zapret2/strategy-catalogs/voice.txt" \
"immutable-file|0644|zapret2/strategy-catalogs/http80.txt" \
"mutable-seed|0644|zapret2/runtime.ini" \
"mutable-seed|0644|zapret2/hosts.ini" \
"runtime-dependency-immutable|0644|zapret2/lua/custom_funcs.lua" \
"runtime-dependency-immutable|0644|zapret2/lua/zapret-antidpi.lua" \
"runtime-dependency-immutable|0644|zapret2/lua/zapret-auto.lua" \
"runtime-dependency-immutable|0644|zapret2/lua/zapret-lib.lua" \
"runtime-dependency-immutable|0644|zapret2/lua/zapret-obfs.lua" \
"runtime-dependency-immutable|0644|zapret2/lua/zapret-pcap.lua" \
"runtime-dependency-immutable|0644|zapret2/lua/zapret-tests.lua" \
"runtime-dependency-immutable|0644|zapret2/lua/zapret-multishake.lua" \
"immutable-exec|0755|customize.sh" \
"immutable-exec|0755|service.sh" \
"immutable-exec|0755|uninstall.sh" \
"immutable-exec|0755|action.sh" \
"immutable-exec|0755|zapret2/scripts/common.sh" \
"immutable-exec|0755|zapret2/scripts/firewall-reconciler.sh" \
"immutable-exec|0755|zapret2/scripts/command-builder.sh" \
"immutable-exec|0755|zapret2/scripts/package-contract.sh" \
"immutable-exec|0755|zapret2/scripts/runtime-config.sh" \
"immutable-exec|0755|zapret2/scripts/runtime-init.sh" \
"immutable-exec|0755|zapret2/scripts/zapret-start.sh" \
"immutable-exec|0755|zapret2/scripts/zapret-stop.sh" \
"immutable-exec|0755|zapret2/scripts/zapret-restart.sh" \
"immutable-exec|0755|zapret2/scripts/zapret-apply-preset.sh" \
"immutable-exec|0755|zapret2/scripts/zapret-status.sh" \
"immutable-exec|0755|zapret2/scripts/zapret-full-rollback.sh" \
"immutable-exec|0755|zapret2/scripts/lifecycle/purge-contract.sh" \
"immutable-exec|0755|zapret2/scripts/lifecycle/zapret-purge.sh" \
"immutable-exec|0755|system/bin/zapret2-start" \
"immutable-exec|0755|system/bin/zapret2-stop" \
"immutable-exec|0755|system/bin/zapret2-status" \
"immutable-exec|0755|system/bin/zapret2-restart" \
"immutable-exec|0755|system/bin/zapret2-full-rollback" \
"abi-exec|0755|zapret2/bin/{abi}/nfqws2" \
"installed-exec|0755|zapret2/nfqws2" \
"preset-compatible|0644|zapret2/presets/Default v1 (game filter).txt"
do
printf '%s\n' "$required_entry" >> "$required" || {
rm -f "$entries" "$required"; package_contract_fail "MANIFEST_TEMP_FAILED"; return 1;
}
done
missing="$(awk '
NR == FNR { present[$0]=1; next }
!($0 in present) { print; exit }
' "$entries" "$required")" || {
rm -f "$entries" "$required"; package_contract_fail "MANIFEST_TEMP_FAILED"; return 1;
}
rm -f "$entries" "$required"
[ -z "$missing" ] || { package_contract_fail "MANIFEST_REQUIRED_ENTRY" "$missing"; return 1; }
return 0
}
package_contract_check_regular() {
local file="$1"
local relative="$2"
local links=""
if [ -L "$file" ]; then package_contract_fail "PACKAGE_SYMLINK" "$relative"; return 1; fi
if [ ! -f "$file" ]; then package_contract_fail "PACKAGE_MISSING" "$relative"; return 1; fi
if [ ! -s "$file" ]; then package_contract_fail "PACKAGE_EMPTY" "$relative"; return 1; fi
if [ ! -r "$file" ]; then package_contract_fail "PACKAGE_UNREADABLE" "$relative"; return 1; fi
links="$(stat -c %h "$file" 2>/dev/null)" || { package_contract_fail "PACKAGE_LINK_COUNT" "$relative"; return 1; }
[ "$links" = 1 ] || { package_contract_fail "PACKAGE_LINK_COUNT" "$relative:$links"; return 1; }
return 0
}
package_contract_validate_module_prop() {
local root="$1"
local file="$root/module.prop"
local file_bytes=""
local without_nul_bytes=""
package_contract_check_regular "$file" "module.prop" || return 1
file_bytes="$(wc -c < "$file" 2>/dev/null)"
case "$file_bytes" in ''|*[!0-9]*) package_contract_fail "MODULE_PROP_SIZE_UNREADABLE"; return 1 ;; esac
[ "$file_bytes" -le "$PACKAGE_CONTRACT_MAX_MODULE_PROP_BYTES" ] || {
package_contract_fail "MODULE_PROP_TOO_LARGE" "$file_bytes"
return 1
}
without_nul_bytes="$(tr -d '\000' < "$file" | wc -c 2>/dev/null)"
case "$without_nul_bytes" in ''|*[!0-9]*) package_contract_fail "MODULE_PROP_INVALID_DATA"; return 1 ;; esac
[ "$without_nul_bytes" = "$file_bytes" ] || {
package_contract_fail "MODULE_PROP_INVALID_DATA" "NUL"
return 1
}
if LC_ALL=C grep -q "$(printf '\r')" "$file"; then
package_contract_fail "MODULE_PROP_INVALID_DATA" "CR"
return 1
fi
LC_ALL=C awk \
-v expected_update_json="$PACKAGE_CONTRACT_UPDATE_JSON" '
function trim(value) {
sub(/^[[:space:]]+/, "", value)
sub(/[[:space:]]+$/, "", value)
return value
}
function canonical_long(value) {
if (value == "0") return 1
if (value !~ /^[1-9][0-9]*$/ || length(value) > 19) return 0
return length(value) < 19 || ("x" value <= "x9223372036854775807")
}
function canonical_version_code(value) {
if (value !~ /^[1-9][0-9]*$/ || length(value) > 10) return 0
return length(value) < 10 || ("x" value <= "x2100000000")
}
{
line = trim($0)
if (line == "" || substr(line, 1, 1) == "#") next
separator = index(line, "=")
if (separator <= 1) { invalid = 1; next }
key = trim(substr(line, 1, separator - 1))
value = trim(substr(line, separator + 1))
if (key !~ /^[A-Za-z][A-Za-z0-9_]*$/ || value ~ /[[:cntrl:]]/ || seen[key]++) {
invalid = 1
next
}
properties[key] = value
}
END {
split("id name version versionCode author description updateJson", required, " ")
for (i in required) if (!(required[i] in properties)) invalid = 1
if (invalid) exit 1
if ("webRoot" in properties) exit 1
if (properties["id"] != "zapret2" || properties["name"] == "" ||
properties["author"] == "" || properties["description"] == "" ||
properties["updateJson"] != expected_update_json) exit 1
version = properties["version"]
if (substr(version, 1, 1) != "v") exit 1
version = substr(version, 2)
prerelease_separator = index(version, "-")
if (prerelease_separator > 0) {
prerelease = substr(version, prerelease_separator + 1)
version = substr(version, 1, prerelease_separator - 1)
if (split(prerelease, pre, ".") != 3 ||
pre[1] != "dev" ||
pre[2] !~ /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/ ||
pre[3] !~ /^[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]$/) exit 1
}
if (split(version, parts, ".") != 3 || !canonical_long(parts[1]) ||
!canonical_long(parts[2]) || !canonical_long(parts[3]) ||
parts[1] + 0 < 1 || parts[1] + 0 > 2100 ||
parts[2] + 0 > 99 || parts[3] + 0 > 9999 ||
!canonical_version_code(properties["versionCode"])) exit 1
expected_code = sprintf("%.0f",
(parts[1] + 0) * 1000000 + (parts[2] + 0) * 10000 + (parts[3] + 0))
if (properties["versionCode"] != expected_code) exit 1
}
' "$file" || {
package_contract_fail "MODULE_PROP_INVALID" "module.prop"
return 1
}
return 0
}
package_contract_validate_lifecycle_contract() {
local root="$1"
local relative="zapret2/lifecycle-contract.version"
local file="$root/$relative"
package_contract_check_regular "$file" "$relative" || return 1
[ "$(wc -l < "$file" 2>/dev/null)" = 1 ] &&
[ "$(cat "$file" 2>/dev/null)" = 8 ] || {
package_contract_fail "LIFECYCLE_CONTRACT_INVALID" "$relative"
return 1
}
return 0
}
package_contract_for_each_path() {
# Usage: package_contract_for_each_path <root> <profile> <callback>
local root="$1"
local profile="$2"
local callback="$3"
local manifest="$root/$PACKAGE_CONTRACT_MANIFEST_REL"
local class="" mode="" path="" extra="" abi=""
while IFS='|' read -r class mode path extra || [ -n "$class$mode$path$extra" ]; do
case "$class" in ""|'#'*|schema|owner_protocol) continue ;; esac
if [ "$class" = "installed-exec" ] && [ "$profile" != "installed" ]; then continue; fi
# Root managers source customize.sh from the package and then remove it from
# the installed module, so installer-only code is excluded from the
# canonical installed profile.
if [ "$profile" = "installed" ] && [ "$path" = "customize.sh" ]; then continue; fi
if [ "$class" = "abi-exec" ]; then
for abi in arm64-v8a armeabi-v7a; do
"$callback" "$root" "$class" "$mode" "zapret2/bin/$abi/nfqws2" || return 1
done
else
"$callback" "$root" "$class" "$mode" "$path" || return 1
fi
done < "$manifest"
return 0
}
package_contract_allowlist_callback() {
local root="$1" class="$2" mode="$3" path="$4" parent=""
printf '%s\n' "$path" >> "$PACKAGE_CONTRACT_ALLOWED_FILES" || return 1
case "$path" in
*/*) parent="${path%/*}" ;;
*) parent="" ;;
esac
while [ -n "$parent" ]; do
printf '%s\n' "$parent" >> "$PACKAGE_CONTRACT_ALLOWED_DIRECTORIES" || return 1
case "$parent" in
*/*) parent="${parent%/*}" ;;
*) parent="" ;;
esac
done
return 0
}
package_contract_build_allowlist() {
local root="$1" profile="$2" files="$3" directories="$4" result=0
: > "$files" && : > "$directories" || {
package_contract_fail "ALLOWLIST_TEMP_FAILED"
return 1
}
PACKAGE_CONTRACT_ALLOWED_FILES="$files"
PACKAGE_CONTRACT_ALLOWED_DIRECTORIES="$directories"
package_contract_for_each_path "$root" "$profile" package_contract_allowlist_callback
result=$?
unset PACKAGE_CONTRACT_ALLOWED_FILES PACKAGE_CONTRACT_ALLOWED_DIRECTORIES
[ "$result" -eq 0 ] || { package_contract_fail "ALLOWLIST_TEMP_FAILED"; return "$result"; }
LC_ALL=C sort -u "$files" -o "$files" && LC_ALL=C sort -u "$directories" -o "$directories" || {
package_contract_fail "ALLOWLIST_TEMP_FAILED"
return 1
}
return 0
}
package_contract_validate_exact_tree() {
local root="${1%/}" profile="${2:-package}" meta_policy="${3:-no-meta}"
local files="" directories="" listing="" actual="" allowed_records=""
local entry="" relative="" unsafe="" hardlinked="" unexpected=""
[ -n "$root" ] && [ -d "$root" ] && [ ! -L "$root" ] || {
package_contract_fail "PACKAGE_ROOT_INVALID" "$root"
return 1
}
package_contract_validate_manifest "$root" || return 1
files="$(mktemp)" || { package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1; }
directories="$(mktemp)" || { rm -f "$files"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1; }
listing="$(mktemp)" || {
rm -f "$files" "$directories"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1;
}
actual="$(mktemp)" || {
rm -f "$files" "$directories" "$listing"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1;
}
allowed_records="$(mktemp)" || {
rm -f "$files" "$directories" "$listing" "$actual"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1;
}
package_contract_build_allowlist "$root" "$profile" "$files" "$directories" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"; return 1;
}
find "$root" -mindepth 1 -print > "$listing" 2>/dev/null || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_ENUMERATION_FAILED"; return 1;
}
unsafe="$(LC_ALL=C awk -v prefix="$root/" '
{ relative=substr($0, length(prefix) + 1) }
relative ~ /[[:cntrl:]|]/ { print relative; exit }
' "$listing")" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_ENUMERATION_FAILED"; return 1;
}
[ -z "$unsafe" ] || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_UNSAFE_ENTRY" "$unsafe"; return 1;
}
hardlinked="$(find "$root" -type f ! -links 1 -print -quit 2>/dev/null)" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_ENUMERATION_FAILED"; return 1;
}
[ -z "$hardlinked" ] || {
relative="${hardlinked#"$root"/}"
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_LINK_COUNT" "$relative"; return 1;
}
while IFS= read -r entry || [ -n "$entry" ]; do
relative="${entry#"$root"/}"
package_contract_safe_relative_path_syntax "$relative" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_UNSAFE_ENTRY" "$relative"; return 1;
}
[ ! -L "$entry" ] || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_SYMLINK" "$relative"; return 1;
}
if [ -f "$entry" ]; then
printf 'file|%s\n' "$relative" >> "$actual" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_ENUMERATION_FAILED"; return 1;
}
elif [ -d "$entry" ]; then
printf 'directory|%s\n' "$relative" >> "$actual" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_ENUMERATION_FAILED"; return 1;
}
else
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_UNDECLARED_ENTRY" "$relative"; return 1
fi
done < "$listing"
awk '{ print "file|" $0 }' "$files" > "$allowed_records" &&
awk '{ print "directory|" $0 }' "$directories" >> "$allowed_records" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1;
}
unexpected="$(awk -F '|' -v profile="$profile" -v meta_policy="$meta_policy" '
NR == FNR { allowed[$0]=1; next }
{
record=$0
separator=index(record, "|")
kind=substr(record, 1, separator - 1)
path=substr(record, separator + 1)
if (record in allowed) next
if (profile == "installed") {
if (kind == "file" && (path == "disable" || path == "zapret2/install-generation.meta" ||
index(path, "zapret2/lists/") == 1 || index(path, "zapret2/presets/") == 1)) next
if (kind == "directory" && index(path, "zapret2/lists/") == 1) next
}
if (meta_policy == "allow-meta" && (path == "META-INF" || index(path, "META-INF/") == 1)) next
print path
exit
}
' "$allowed_records" "$actual")" || {
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
package_contract_fail "PACKAGE_ENUMERATION_FAILED"; return 1;
}
rm -f "$files" "$directories" "$listing" "$actual" "$allowed_records"
[ -z "$unexpected" ] || { package_contract_fail "PACKAGE_UNDECLARED_ENTRY" "$unexpected"; return 1; }
return 0
}
package_contract_copy_callback() {
local root="$1" class="$2" mode="$3" path="$4"
local source="$root/$path" target="$PACKAGE_CONTRACT_ASSEMBLY_ROOT/$path" parent=""
package_contract_check_regular "$source" "$path" || return 1
parent="${target%/*}"
mkdir -p "$parent" || { package_contract_fail "ASSEMBLY_MKDIR" "$path"; return 1; }
cp "$source" "$target" || { package_contract_fail "ASSEMBLY_COPY" "$path"; return 1; }
chmod "$mode" "$target" || { package_contract_fail "ASSEMBLY_CHMOD" "$path"; return 1; }
}
package_contract_assemble_package() {
local source_root="${1%/}" destination_root="${2%/}" result=0
[ -n "$source_root" ] && [ -d "$source_root" ] && [ ! -L "$source_root" ] || {
package_contract_fail "PACKAGE_ROOT_INVALID" "$source_root"; return 1;
}
[ -n "$destination_root" ] && [ -d "$destination_root" ] && [ ! -L "$destination_root" ] || {
package_contract_fail "ASSEMBLY_ROOT_INVALID" "$destination_root"; return 1;
}
if find "$destination_root" -mindepth 1 -print -quit | grep -q .; then
package_contract_fail "ASSEMBLY_ROOT_NOT_EMPTY" "$destination_root"
return 1
fi
package_contract_validate_manifest "$source_root" || return 1
package_contract_for_each_path "$source_root" package package_contract_content_callback || return 1
PACKAGE_CONTRACT_ASSEMBLY_ROOT="$destination_root"
package_contract_for_each_path "$source_root" package package_contract_copy_callback
result=$?
unset PACKAGE_CONTRACT_ASSEMBLY_ROOT
[ "$result" -eq 0 ] || return "$result"
package_contract_validate_exact_tree "$destination_root" package || return 1
package_contract_validate_modes "$destination_root" package || return 1
return 0
}
package_contract_content_callback() {
local root="$1" class="$2" mode="$3" path="$4"
package_contract_check_regular "$root/$path" "$path"
}
package_contract_compare_release_callback() {
local source_root="$1" class="$2" mode="$3" path="$4"
local source="$source_root/$path"
local target="$PACKAGE_CONTRACT_COMPARE_TARGET/$path"
case "$class" in
immutable-file|immutable-exec|runtime-dependency-immutable|abi-exec|preset-compatible|preset-quarantined) ;;
*) return 0 ;;
esac
[ "$path" != customize.sh ] || return 0
package_contract_check_regular "$source" "$path" || return 1
package_contract_check_regular "$target" "$path" || return 1
cmp -s "$source" "$target" || {
package_contract_fail "PACKAGE_GENERATION_MISMATCH" "$path"
return 1
}
}
# Constant-size generation comparison for release qualification and offline
# diagnostics. APK releases delegate publication exclusively to root-manager staging.
package_contract_compare_release() {
local source_root="${1%/}" target_root="${2%/}" path
[ -n "$source_root" ] && [ -d "$source_root" ] && [ ! -L "$source_root" ] || {
package_contract_fail "PACKAGE_ROOT_INVALID" "$source_root"; return 1;
}
[ -n "$target_root" ] && [ -d "$target_root" ] && [ ! -L "$target_root" ] || {
package_contract_fail "PACKAGE_ROOT_INVALID" "$target_root"; return 1;
}
package_contract_validate_manifest "$source_root" || return 1
package_contract_validate_manifest "$target_root" || return 1
cmp -s "$source_root/$PACKAGE_CONTRACT_MANIFEST_REL" \
"$target_root/$PACKAGE_CONTRACT_MANIFEST_REL" || {
package_contract_fail "PACKAGE_GENERATION_MISMATCH" "$PACKAGE_CONTRACT_MANIFEST_REL"
return 1
}
for path in \
module.prop service.sh uninstall.sh action.sh \
system/bin/zapret2-start system/bin/zapret2-stop \
system/bin/zapret2-status system/bin/zapret2-restart \
system/bin/zapret2-full-rollback \
zapret2/lifecycle-contract.version \
zapret2/scripts/common.sh zapret2/scripts/command-builder.sh \
zapret2/scripts/package-contract.sh zapret2/scripts/runtime-config.sh \
zapret2/scripts/runtime-init.sh zapret2/scripts/zapret-start.sh \
zapret2/scripts/zapret-stop.sh zapret2/scripts/zapret-status.sh
do
package_contract_check_regular "$source_root/$path" "$path" || return 1
package_contract_check_regular "$target_root/$path" "$path" || return 1
cmp -s "$source_root/$path" "$target_root/$path" || {
package_contract_fail "PACKAGE_GENERATION_MISMATCH" "$path"
return 1
}
done
return 0
}
# Exhaustive immutable-byte comparison belongs to release qualification and
# offline diagnostics, never to the device publication/status hot path.
package_contract_compare_release_all() {
local source_root="${1%/}" target_root="${2%/}" result=0
package_contract_compare_release "$source_root" "$target_root" || return 1
PACKAGE_CONTRACT_COMPARE_TARGET="$target_root"
package_contract_for_each_path "$source_root" installed package_contract_compare_release_callback
result=$?
unset PACKAGE_CONTRACT_COMPARE_TARGET
return "$result"
}
package_contract_shell_exec_callback() {
local root="$1" class="$2" mode="$3" path="$4" file="$1/$4" bytes=""
[ "$class" = immutable-exec ] || return 0
bytes="$(wc -c < "$file" 2>/dev/null)"
case "$bytes" in ''|*[!0-9]*) package_contract_fail "SHELL_EXEC_SIZE_UNREADABLE" "$path"; return 1 ;; esac
[ "$bytes" -le "$PACKAGE_CONTRACT_MAX_SHELL_EXEC_BYTES" ] || {
package_contract_fail "SHELL_EXEC_TOO_LARGE" "$path:$bytes"; return 1;
}
[ "$(sed -n '1p' "$file")" = '#!/system/bin/sh' ] || {
package_contract_fail "SHELL_EXEC_SHEBANG" "$path"; return 1;
}
if LC_ALL=C grep -q "$(printf '\r')" "$file"; then
package_contract_fail "SHELL_EXEC_CR" "$path"
return 1
fi
return 0
}
package_contract_validate_entrypoints() {
local root="${1%/}" command_name="" wrapper="" target="" expected=""
expected="$(mktemp)" || {
package_contract_fail "ENTRYPOINT_TEMP_FAILED"
return 1
}
for command_name in start stop status restart full-rollback; do
wrapper="system/bin/zapret2-$command_name"
target="$PACKAGE_CONTRACT_ACTIVE_MODULE_DIR/zapret2/scripts/zapret-$command_name.sh"
package_contract_check_regular "$root/$wrapper" "$wrapper" || {
rm -f "$expected"
return 1
}
{
printf '%s\n' '#!/system/bin/sh'
printf 'exec %s "$@"\n' "$target"
} > "$expected" || {
rm -f "$expected"
package_contract_fail "ENTRYPOINT_TEMP_FAILED"
return 1
}
cmp -s "$expected" "$root/$wrapper" || {
rm -f "$expected"
package_contract_fail "PACKAGE_ENTRYPOINT_BYTES" "$wrapper"
return 1
}
done
rm -f "$expected"
return 0
}
package_contract_mode_record_callback() {
local root="$1" class="$2" expected="$3" path="$4"
printf '%s|%s\n' "$expected" "$path" >> "$PACKAGE_CONTRACT_EXPECTED_MODES" || {
package_contract_fail "PACKAGE_MODE_TEMP_FAILED"; return 1;
}
}
package_contract_chmod_callback() {
local root="$1" class="$2" mode="$3" path="$4"
chmod "$mode" "$root/$path" || {
package_contract_fail "PACKAGE_CHMOD" "$path"; return 1;
}
}
# Root-manager installers first assign 0644 to every staged regular file in one
# batched find invocation. Only manifest executables need another chmod pass.
package_contract_chmod_executable_callback() {
local root="$1" class="$2" mode="$3" path="$4"
[ "$mode" = 0755 ] || return 0
chmod 0755 "$root/$path" || {
package_contract_fail "PACKAGE_CHMOD" "$path"; return 1;
}
}
package_contract_apply_executable_modes() {
local root="$1" profile="${2:-package}"
package_contract_for_each_path "$root" "$profile" package_contract_chmod_executable_callback
}
package_contract_validate_runtime_selection() {
local root="${1%/}" preset relative path manifest
preset="$(package_contract_runtime_core_value "$root" active_preset)" || {
package_contract_fail "RUNTIME_PRESET_BINDING_INVALID"
return 1
}
package_contract_safe_preset_name "$preset" || {
package_contract_fail "RUNTIME_PRESET_BINDING_INVALID" "$preset"
return 1
}
relative="zapret2/presets/$preset"
path="$root/$relative"
manifest="$root/$PACKAGE_CONTRACT_MANIFEST_REL"
# Installed custom presets are intentionally outside the immutable package
# manifest; the same strict scanner below still has to classify them valid.
[ -f "$path" ] && [ ! -L "$path" ] && [ -s "$path" ] &&
[ "$(stat -c %u "$path" 2>/dev/null)" = 0 ] &&
[ "$(stat -c %h "$path" 2>/dev/null)" = 1 ] &&
[ "$(stat -c %a "$path" 2>/dev/null)" = 644 ] || {
package_contract_fail "RUNTIME_PRESET_UNSAFE" "$relative"
return 1
}
return 0
}
package_contract_validate_tree() {
local root="$1" profile="${2:-package}" files="" directories="" relative="" file="" hardlinked=""
package_contract_validate_manifest "$root" || return 1
files="$(mktemp)" || { package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1; }
directories="$(mktemp)" || { rm -f "$files"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1; }
package_contract_build_allowlist "$root" "$profile" "$files" "$directories" || {
rm -f "$files" "$directories"
return 1
}
while IFS= read -r relative || [ -n "$relative" ]; do
file="$root/$relative"
if [ -L "$file" ]; then
rm -f "$files" "$directories"; package_contract_fail "PACKAGE_SYMLINK" "$relative"; return 1
fi
if [ ! -f "$file" ]; then
rm -f "$files" "$directories"; package_contract_fail "PACKAGE_MISSING" "$relative"; return 1
fi
if [ ! -s "$file" ]; then
rm -f "$files" "$directories"; package_contract_fail "PACKAGE_EMPTY" "$relative"; return 1
fi
if [ ! -r "$file" ]; then
rm -f "$files" "$directories"; package_contract_fail "PACKAGE_UNREADABLE" "$relative"; return 1
fi
done < "$files"
hardlinked="$(find "$root" -type f ! -links 1 -print -quit 2>/dev/null)" || {
rm -f "$files" "$directories"; package_contract_fail "PACKAGE_ENUMERATION_FAILED"; return 1
}
rm -f "$files" "$directories"
[ -z "$hardlinked" ] || {
relative="${hardlinked#"$root"/}"
package_contract_fail "PACKAGE_LINK_COUNT" "$relative"; return 1
}
if [ "$profile" = installed ]; then
package_contract_validate_runtime_selection "$root" || return 1
fi
return 0
}
package_contract_extract_zip_names() {
local listing="$1" output="$2"
awk '
/^[[:space:]]*--------/ {
separators++
if (separators == 2) exit
next
}
separators == 1 {
line=$0
sub(/^[[:space:]]*[0-9]+[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+[[:space:]]+/, "", line)
if (line != "") print line
}
' "$listing" > "$output" || {
package_contract_fail "ZIP_LIST_PARSE"; return 1;
}
[ -s "$output" ] || { package_contract_fail "ZIP_LIST_EMPTY"; return 1; }
}
package_contract_validate_zip_topology_file() {
local records="$1" failure="" code="" detail=""
failure="$(LC_ALL=C awk -F '|' '
{
record=$0
separator=index(record, "|")
kind=substr(record, 1, separator - 1)
path=substr(record, separator + 1)
if (path in kinds) {
printf "ZIP_DUPLICATE_ENTRY|%s\n", path
failed=1
exit
}
kinds[path]=kind
paths[++count]=path
}
END {
if (failed) exit
for (i=1; i<=count; i++) {
path=paths[i]
remainder=path
prefix=""
while ((separator=index(remainder, "/")) > 0) {
component=substr(remainder, 1, separator - 1)
remainder=substr(remainder, separator + 1)
prefix=(prefix == "" ? component : prefix "/" component)
if ((prefix in kinds) && kinds[prefix] == "file") {
printf "ZIP_PATH_COLLISION|%s\n", path
exit
}
}
}
}
' "$records")" || {
package_contract_fail "ZIP_TOPOLOGY_SCAN_FAILED"
return 1
}
[ -z "$failure" ] && return 0
code="${failure%%|*}"
detail="${failure#*|}"
package_contract_fail "$code" "$detail"
}
package_contract_validate_zip_names() {
local root="$1" names="$2" files="" directories="" actual="" allowed_records=""
local raw="" path="" kind="" unsafe="" missing="" unexpected=""
package_contract_validate_manifest "$root" || return 1
[ -f "$names" ] && [ ! -L "$names" ] && [ -s "$names" ] || { package_contract_fail "ZIP_LIST_MISSING"; return 1; }
unsafe="$(LC_ALL=C awk '$0 ~ /[[:cntrl:]|]/ { print; exit }' "$names")" || {
package_contract_fail "ZIP_LIST_PARSE"
return 1
}
[ -z "$unsafe" ] || { package_contract_fail "ZIP_UNSAFE_ENTRY" "$unsafe"; return 1; }
files="$(mktemp)" || { package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1; }
directories="$(mktemp)" || { rm -f "$files"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1; }
actual="$(mktemp)" || { rm -f "$files" "$directories"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1; }
allowed_records="$(mktemp)" || {
rm -f "$files" "$directories" "$actual"; package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1;
}
package_contract_build_allowlist "$root" package "$files" "$directories" || {
rm -f "$files" "$directories" "$actual" "$allowed_records"; return 1;
}
while IFS= read -r raw || [ -n "$raw" ]; do
case "$raw" in
*/) path="${raw%/}"; kind="directory" ;;
*) path="$raw"; kind="file" ;;
esac
package_contract_safe_relative_path_syntax "$path" || {
rm -f "$files" "$directories" "$actual" "$allowed_records"
package_contract_fail "ZIP_UNSAFE_ENTRY" "$raw"; return 1;
}
printf '%s|%s\n' "$kind" "$path" >> "$actual" || {
rm -f "$files" "$directories" "$actual" "$allowed_records"
package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1;
}
done < "$names"
missing="$(awk -F '|' '
NR == FNR {
if ($1 == "file") {
separator=index($0, "|")
counts[substr($0, separator + 1)]++
}
next
}
counts[$0] != 1 { printf "%s:%d\n", $0, counts[$0] + 0; exit }
' "$actual" "$files")" || {
rm -f "$files" "$directories" "$actual" "$allowed_records"
package_contract_fail "ZIP_LIST_PARSE"; return 1;
}
[ -z "$missing" ] || {
rm -f "$files" "$directories" "$actual" "$allowed_records"
package_contract_fail "ZIP_ENTRY_COUNT" "$missing"; return 1;
}
if ! package_contract_validate_zip_topology_file "$actual"; then
rm -f "$files" "$directories" "$actual" "$allowed_records"
return 1
fi
awk '{ print "file|" $0 }' "$files" > "$allowed_records" &&
awk '{ print "directory|" $0 }' "$directories" >> "$allowed_records" || {
rm -f "$files" "$directories" "$actual" "$allowed_records"
package_contract_fail "ALLOWLIST_TEMP_FAILED"; return 1;
}
unexpected="$(awk -F '|' '
NR == FNR { allowed[$0]=1; next }
{
record=$0
separator=index(record, "|")
path=substr(record, separator + 1)
if (record in allowed || path == "META-INF" || index(path, "META-INF/") == 1) next
print path
exit
}
' "$allowed_records" "$actual")" || {
rm -f "$files" "$directories" "$actual" "$allowed_records"
package_contract_fail "ZIP_LIST_PARSE"; return 1;
}
rm -f "$files" "$directories" "$actual" "$allowed_records"
[ -z "$unexpected" ] || { package_contract_fail "ZIP_UNDECLARED_ENTRY" "$unexpected"; return 1; }
return 0
}
package_contract_apply_modes() {
local root="$1" profile="${2:-package}"
package_contract_validate_tree "$root" "$profile" || return 1
# One batched filesystem walk replaces hundreds of per-manifest chmod
# subprocesses. Installer-owned state is outside the release manifest and
# retains its stricter private mode.
find "$root" -type f \
! -path "$root/zapret2/install-generation.meta" \
! -path "$root/disable" \
-exec chmod 0644 {} + || {
package_contract_fail "PACKAGE_CHMOD" "$root"
return 1
}
package_contract_apply_executable_modes "$root" "$profile" || return 1
}
package_contract_validate_modes() {
local root="${1%/}" profile="${2:-package}" expected="" actual="" failure="" code="" detail=""
expected="$(mktemp)" || { package_contract_fail "PACKAGE_MODE_TEMP_FAILED"; return 1; }
actual="$(mktemp)" || { rm -f "$expected"; package_contract_fail "PACKAGE_MODE_TEMP_FAILED"; return 1; }
PACKAGE_CONTRACT_EXPECTED_MODES="$expected"
package_contract_for_each_path "$root" "$profile" package_contract_mode_record_callback || {
unset PACKAGE_CONTRACT_EXPECTED_MODES
rm -f "$expected" "$actual"
return 1
}
unset PACKAGE_CONTRACT_EXPECTED_MODES
find "$root" -type f -exec stat -c '%a|%n' {} + > "$actual" 2>/dev/null || {
rm -f "$expected" "$actual"; package_contract_fail "PACKAGE_MODE_UNREADABLE"; return 1
}
failure="$(awk -F '|' -v prefix="$root/" '
NR == FNR {
separator=index($0, "|")
mode=substr($0, 1, separator - 1)
absolute=substr($0, separator + 1)
if (index(absolute, prefix) == 1) actual[substr(absolute, length(prefix) + 1)]=mode
next
}
{
separator=index($0, "|")
expected=substr($0, 1, separator - 1)
path=substr($0, separator + 1)
if (!(path in actual)) {
printf "PACKAGE_MODE_UNREADABLE|%s\n", path
exit
}
if ("0" actual[path] != expected) {
printf "PACKAGE_MODE|%s:%s:%s\n", path, actual[path], expected
exit
}
}
' "$actual" "$expected")" || {
rm -f "$expected" "$actual"; package_contract_fail "PACKAGE_MODE_TEMP_FAILED"; return 1
}
rm -f "$expected" "$actual"
[ -z "$failure" ] && return 0
code="${failure%%|*}"
detail="${failure#*|}"
package_contract_fail "$code" "$detail"
}
package_contract_validate_catalog() {
local root="$1"
local manifest="$root/$PACKAGE_CONTRACT_MANIFEST_REL"
local builder="$root/zapret2/scripts/command-builder.sh"
local temp_root="" expected="" actual="" declared="" scan="" catalog_expected="" catalog_actual=""
local strategy_scan="" strategy_expected=""
local class="" mode="" path="" extra="" file="" name=""
temp_root="$(mktemp -d)" || { package_contract_fail "CATALOG_TEMP_FAILED"; return 1; }
expected="$temp_root/expected"
actual="$temp_root/actual"
declared="$temp_root/declared"
scan="$temp_root/scan"
catalog_expected="$temp_root/catalog.expected"
catalog_actual="$temp_root/catalog.actual"
strategy_scan="$temp_root/strategies.scan"
strategy_expected="$temp_root/strategies.expected"
: > "$expected"; : > "$declared"; : > "$catalog_expected" || {
rm -rf "$temp_root"; package_contract_fail "CATALOG_TEMP_FAILED"; return 1;
}
printf 'Z2_STRATEGIES\tOK\n' > "$strategy_expected" || {
rm -rf "$temp_root"; package_contract_fail "CATALOG_TEMP_FAILED"; return 1;
}
while IFS='|' read -r class mode path extra || [ -n "$class$mode$path$extra" ]; do
case "$class" in
preset-compatible)
name="${path##*/}"
printf '%s\tVALID\n' "$name" >> "$expected"
printf '%s\n' "$name" >> "$catalog_expected"
;;
preset-quarantined)
name="${path##*/}"
printf '%s\tQUARANTINED\n' "$name" >> "$expected"
printf '%s\n' "$name" >> "$catalog_expected"
;;
runtime-dependency-immutable|runtime-dependency-mutable-seed)
printf '%s\n' "$path" >> "$declared"
;;
immutable-file)
case "$path" in zapret2/presets/*.txt) printf '%s\n' "${path##*/}" >> "$catalog_expected" ;; esac
;;
esac
done < "$manifest"
if ! /system/bin/sh "$builder" --validate-strategies-machine "$root/zapret2" > "$strategy_scan" 2>/dev/null; then
if ! sh "$builder" --validate-strategies-machine "$root/zapret2" > "$strategy_scan" 2>/dev/null; then
rm -rf "$temp_root"
package_contract_fail "STRATEGY_CATALOG_INVALID"
return 1
fi
fi
cmp -s "$strategy_expected" "$strategy_scan" || {
rm -rf "$temp_root"; package_contract_fail "STRATEGY_CATALOG_PROTOCOL"; return 1;
}
PRESET_ALLOWED_DEPENDENCIES_FILE="$declared"
export PRESET_ALLOWED_DEPENDENCIES_FILE
if ! /system/bin/sh "$builder" --scan-presets-machine "$root/zapret2" > "$scan" 2>/dev/null; then
if ! sh "$builder" --scan-presets-machine "$root/zapret2" > "$scan" 2>/dev/null; then
unset PRESET_ALLOWED_DEPENDENCIES_FILE
rm -rf "$temp_root"
package_contract_fail "CATALOG_SCAN_FAILED"
return 1
fi
fi
unset PRESET_ALLOWED_DEPENDENCIES_FILE
awk -F '\t' 'NR==FNR { wanted[$1]=1; next } $1 == "Z2_PRESET" && ($4 in wanted) { print $4 "\t" $2 }' \
"$catalog_expected" "$scan" | LC_ALL=C sort > "$actual"
LC_ALL=C sort "$expected" -o "$expected"
cmp -s "$expected" "$actual" || {
rm -rf "$temp_root"; package_contract_fail "CATALOG_CLASSIFICATION_MISMATCH"; return 1;
}
[ "$(grep -c '^Z2_PRESET_SUMMARY[[:space:]]' "$scan")" -eq 1 ] || {
rm -rf "$temp_root"; package_contract_fail "CATALOG_SUMMARY"; return 1;
}
if grep -q '^Z2_PRESET[[:space:]]QUARANTINED[[:space:]]' "$scan"; then
file="$(awk -F '\t' '$1=="Z2_PRESET" && $2=="QUARANTINED" { print $4; exit }' "$scan")"
rm -rf "$temp_root"; package_contract_fail "PRESET_INVALID" "$file"; return 1
fi
for file in "$root/zapret2/presets"/*.txt; do
[ -e "$file" ] || [ -L "$file" ] || continue
[ -f "$file" ] && [ ! -L "$file" ] || {
rm -rf "$temp_root"; package_contract_fail "CATALOG_UNSAFE_ENTRY" "${file##*/}"; return 1;
}
name="${file##*/}"
grep -Fqx "$name" "$catalog_expected" || continue
printf '%s\n' "$name" >> "$catalog_actual"
done
LC_ALL=C sort "$catalog_expected" -o "$catalog_expected"
LC_ALL=C sort "$catalog_actual" -o "$catalog_actual"
cmp -s "$catalog_expected" "$catalog_actual" || {
rm -rf "$temp_root"; package_contract_fail "CATALOG_CONTENT_MISMATCH"; return 1;
}
rm -rf "$temp_root"
return 0
}
package_contract_validate_all() {
local root="$1" profile="${2:-package}"
package_contract_validate_tree "$root" "$profile" || return 1
package_contract_validate_module_prop "$root" || return 1
package_contract_validate_lifecycle_contract "$root" || return 1
package_contract_for_each_path "$root" "$profile" package_contract_shell_exec_callback || return 1
package_contract_validate_entrypoints "$root" || return 1
return 0
}
# Catalog parsing executes nfqws2 dry-runs and scans every shipped preset. It is
# a release qualification boundary, not a device publication or status check.
package_contract_validate_release_all() {
local root="$1" profile="${2:-package}"
package_contract_validate_all "$root" "$profile" || return 1
package_contract_validate_catalog "$root" || return 1
return 0
}