| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | allow_missing=false |
| 5 | allow_legacy_manifest=false |
| 6 | allow_signature_differences=false |
| 7 | allow_authenticated_payload_differences=false |
| 8 | while [ "$#" -gt 0 ]; do |
| 9 | case "$1" in |
| 10 | --allow-missing) |
| 11 | allow_missing=true |
| 12 | shift |
| 13 | ;; |
| 14 | --allow-legacy-manifest) |
| 15 | allow_legacy_manifest=true |
| 16 | shift |
| 17 | ;; |
| 18 | --allow-signature-differences) |
| 19 | # Callers must cryptographically verify both signature sets before using |
| 20 | # this comparison mode. Minisign trusted comments make two valid |
| 21 | # signatures for identical content byte-distinct across recovery runs. |
| 22 | allow_signature_differences=true |
| 23 | shift |
| 24 | ;; |
| 25 | --allow-authenticated-payload-differences) |
| 26 | # Callers must cryptographically verify both complete payload/signature |
| 27 | # sets before using this comparison mode. Signed Desktop packages can be |
| 28 | # byte-distinct across rebuilds because platform signing and packaging |
| 29 | # embed timestamps and other non-deterministic data. |
| 30 | allow_authenticated_payload_differences=true |
| 31 | allow_signature_differences=true |
| 32 | shift |
| 33 | ;; |
| 34 | *) break ;; |
| 35 | esac |
| 36 | done |
| 37 | if [ "$#" -ne 2 ]; then |
| 38 | echo "usage: $0 [--allow-missing] [--allow-legacy-manifest] [--allow-signature-differences] [--allow-authenticated-payload-differences] CANDIDATE_DIRECTORY EXISTING_DIRECTORY" >&2 |
| 39 | exit 2 |
| 40 | fi |
| 41 | |
| 42 | candidate_dir="${1%/}" |
| 43 | existing_dir="${2%/}" |
| 44 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 45 | if [ ! -d "$candidate_dir" ] || [ ! -d "$existing_dir" ]; then |
| 46 | echo "Desktop release comparison requires two directories" >&2 |
| 47 | exit 2 |
| 48 | fi |
| 49 | |
| 50 | verify_subset() { |
| 51 | local source_dir="$1" |
| 52 | local target_dir="$2" |
| 53 | local source_file relative target_file |
| 54 | while IFS= read -r -d '' source_file; do |
| 55 | relative="${source_file#"$source_dir"/}" |
| 56 | target_file="$target_dir/$relative" |
| 57 | if [ ! -f "$target_file" ]; then |
| 58 | echo "Desktop release directory is missing $relative" >&2 |
| 59 | return 1 |
| 60 | fi |
| 61 | if [ "$allow_legacy_manifest" = "true" ] && [ "$relative" = "latest.json" ]; then |
| 62 | if ! bash "$script_dir/compare-desktop-release-manifests.sh" \ |
| 63 | "$candidate_dir/latest.json" "$existing_dir/latest.json"; then |
| 64 | echo "Desktop release directory has conflicting content for $relative" >&2 |
| 65 | return 1 |
| 66 | fi |
| 67 | continue |
| 68 | fi |
| 69 | if [ "$allow_signature_differences" = "true" ] && [[ "$relative" = *.minisig ]]; then |
| 70 | if [ ! -s "$source_file" ] || [ ! -s "$target_file" ]; then |
| 71 | echo "Desktop release directory has an empty signature for $relative" >&2 |
| 72 | return 1 |
| 73 | fi |
| 74 | continue |
| 75 | fi |
| 76 | if [ "$allow_authenticated_payload_differences" = "true" ] && |
| 77 | [ -s "$source_file.minisig" ] && [ -s "$target_file.minisig" ]; then |
| 78 | continue |
| 79 | fi |
| 80 | if ! cmp -s "$source_file" "$target_file"; then |
| 81 | echo "Desktop release directory has conflicting content for $relative" >&2 |
| 82 | return 1 |
| 83 | fi |
| 84 | done < <(find "$source_dir" -type f -print0) |
| 85 | } |
| 86 | |
| 87 | # Existing objects may never disagree with or fall outside the candidate set. |
| 88 | verify_subset "$existing_dir" "$candidate_dir" |
| 89 | if [ "$allow_missing" != "true" ]; then |
| 90 | verify_subset "$candidate_dir" "$existing_dir" |
| 91 | fi |
| 92 |