#!/bin/bash
# Add the StackX Buster amd64 kernel archive and refresh only that source.
# Does not install a package, does not reboot, and does not select a boot entry.
# No GRUB_DEFAULT, grub-set-default, grub-reboot, pinning, or /etc/default/grub edit.
#
# Target: a real Debian 10 amd64 machine, as root. VERSION_CODENAME may be absent.
# If it is present it must be buster. A wrong distribution or architecture is refused.
# There is no host bypass.
#
# The same GnuPG 2.1-compatible check is used here. Secret material is refused,
# then the downloaded public key is imported into a private temporary GNUPGHOME.
# list-keys --with-colons --with-fingerprint checks exactly one primary.
# --show-keys and import-options show-only are not used. The user keyring is not
# read. The same check is repeated on the public keyring APT will use. No private
# key is used. EXPECTED_FPR is the existing kernel repository key. The Buster file
# name does not select a second key pair.
#
# APT 1.8 provides HTTPS. APT::Update::Error-Mode=any is not used and is not the
# partial-failure check. A fresh InRelease is downloaded with apt-helper, verified
# with gpgv, and compared with the cache after apt-get update. An old list alone
# is not success. trusted=yes, allow-unauthenticated, and TLS bypasses are not used.
#
# https://mirrors.scalarx.net/kernel/buster/ is the planned archive URL.

set -euo pipefail
umask 022
export LC_ALL=C
unset APT_CONFIG
unset GNUPGHOME
unset GPG_AGENT_INFO

EXPECTED_FPR='AF97ED98AACE4CE129A03B6E2C9736B593BABF7F'
DIST_VERSION='10'
DIST_CODENAME='buster'
DIST_NAME='Buster'
DIST_PRETTY='Debian 10 (Buster)'
ORIGIN='https://mirrors.scalarx.net/kernel/buster'
KEY_URL="${ORIGIN}/stackx-kernel-buster.asc"
INRELEASE_URL="${ORIGIN}/InRelease"
KEYRING='/usr/share/keyrings/stackx-kernel-buster.gpg'
LIST='/etc/apt/sources.list.d/stackx-kernel-buster.list'
HELPER='/usr/lib/apt/apt-helper'
LIST_PREFIX='mirrors.scalarx.net_kernel_buster_'
META_PKG='stackx-kernel-buster-amd64'
META_VER='4.19.316-1+sx1'

die() {
  echo "error: $*" >&2
  exit 1
}

fingerprint_is_final() {
  local f u
  f=$1
  [[ "$f" =~ ^[0-9A-Fa-f]{40}$ ]] || return 1
  u=$(printf '%s' "$f" | tr '[:lower:]' '[:upper:]')
  [[ "$u" != 0000000000000000000000000000000000000000 ]] || return 1
  [[ "$u" != FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF ]] || return 1
  return 0
}

# Colon listing from list-keys --with-colons --with-fingerprint.
# Accept one primary whose validity is not invalid, revoked, expired, or disabled.
# Secret and revocation records are refused.
parse_primary() {
  local line rectype validity keylen algo keyid created expires cert ownertrust uid_or_fpr sigclass caps extra
  local pubs=0
  local state=none
  local fpr=
  local primary_validity=
  local primary_expires=
  local vchar
  while IFS= read -r line || [[ -n "$line" ]]; do
    line=${line%$'\r'}
    [[ -n "$line" ]] || continue
    rectype=
    IFS=: read -r rectype validity keylen algo keyid created expires cert ownertrust uid_or_fpr sigclass caps extra <<<"$line"
    case "$rectype" in
      pub)
        pubs=$((pubs + 1))
        if (( pubs > 1 )); then
          echo "key file contains more than one primary key" >&2
          return 1
        fi
        primary_validity=$validity
        primary_expires=$expires
        state=need_fpr
        ;;
      fpr)
        if [[ "$state" == need_fpr ]]; then
          fpr=$uid_or_fpr
          state=got_fpr
        elif [[ "$state" == got_fpr ]]; then
          echo "primary key has more than one fingerprint record" >&2
          return 1
        fi
        ;;
      uid|uat|sub)
        if [[ "$state" == need_fpr ]]; then
          echo "primary key has no fingerprint record" >&2
          return 1
        fi
        if [[ "$rectype" == sub ]]; then
          state=sub
        fi
        ;;
      sec|ssb)
        echo "refusing secret key material" >&2
        return 1
        ;;
      rev|rvk)
        echo "refusing revoked key material" >&2
        return 1
        ;;
    esac
  done < "$1"
  if (( pubs != 1 )); then
    echo "key file does not contain exactly one primary key" >&2
    return 1
  fi
  if [[ "$state" == need_fpr || -z "$fpr" ]]; then
    echo "primary fingerprint missing" >&2
    return 1
  fi
  vchar=${primary_validity:0:1}
  case "$vchar" in
    '-'|q|o|m|f|u|w|s) ;;
    *)
      echo "refusing primary key with gpg validity '${primary_validity}'" >&2
      return 1
      ;;
  esac
  if [[ -n "$primary_expires" && "$primary_expires" != 0 ]]; then
    if [[ ! "$primary_expires" =~ ^[0-9]+$ ]]; then
      echo "refusing primary key with an unreadable expiry" >&2
      return 1
    fi
    if [[ "$primary_expires" -le "$now_epoch" ]]; then
      echo "refusing expired primary key" >&2
      return 1
    fi
  fi
  if [[ ! "$fpr" =~ ^[0-9A-Fa-f]{40}$ ]]; then
    echo "primary fingerprint missing or not 40 hex digits" >&2
    return 1
  fi
  printf '%s\n' "$fpr"
}

refuse_secret_packets() {
  local src=$1 tag=$2
  local out pubs
  out="$work/packets-${tag}.txt"
  if [[ ! -d "$work/gpg-packets" ]]; then
    mkdir -m 700 "$work/gpg-packets"
  fi
  chmod 700 "$work/gpg-packets"
  # Separate home: list-packets must not create the keyring later imported.
  if ! gpg --no-options --batch --no-tty --no-autostart --homedir "$work/gpg-packets" \
    --list-packets "$src" >"$out" 2>"$work/packets-${tag}.err"; then
    echo "error: gpg could not read the key material" >&2
    cat "$work/packets-${tag}.err" >&2
    return 1
  fi
  if grep -F -q -e ':secret key packet:' -e ':secret subkey packet:' "$out"; then
    echo "refusing secret key material" >&2
    return 1
  fi
  pubs=$(grep -c -F ':public key packet:' "$out" || true)
  if [[ "$pubs" -ne 1 ]]; then
    echo "refusing key material that does not contain exactly one public primary" >&2
    return 1
  fi
}

assert_no_secret_keys() {
  local home=$1 ring=$2
  local sec_rc hit
  if [[ -s "$home/list.txt" ]] && grep -Eq '^(sec|ssb|rvk):' "$home/list.txt"; then
    echo "refusing secret or revocation material in the public listing" >&2
    return 1
  fi
  sec_rc=0
  gpg --no-options --batch --no-tty --no-autostart --homedir "$home" \
    --no-default-keyring --keyring "$ring" \
    --fixed-list-mode --with-colons --list-secret-keys \
    >"$home/sec.txt" 2>"$home/sec.err" || sec_rc=$?
  if [[ -s "$home/sec.txt" ]] && grep -Eq '^(sec|ssb):' "$home/sec.txt"; then
    echo "refusing secret key material after import" >&2
    return 1
  fi
  if [[ "$sec_rc" -ne 0 && "$sec_rc" -ne 2 ]]; then
    echo "error: gpg --list-secret-keys failed" >&2
    cat "$home/sec.err" >&2
    return 1
  fi
  if [[ -d "$home/private-keys-v1.d" ]]; then
    hit=$(find "$home/private-keys-v1.d" -mindepth 1 -print -quit) || hit=failed
    if [[ "$hit" == failed || -n "$hit" ]]; then
      echo "refusing secret key material after import" >&2
      return 1
    fi
  fi
  if [[ -s "$home/secring.gpg" ]]; then
    echo "refusing secret key material after import" >&2
    return 1
  fi
}

check_imported_key() {
  local home=$1 ring=$2 src=$3
  local got got_u
  if [[ -L "$home" ]]; then
    echo "refusing temporary GnuPG directory" >&2
    return 1
  fi
  if [[ ! -d "$home" ]]; then
    mkdir -m 700 "$home"
  fi
  chmod 700 "$home"
  if [[ -e "$ring" || -L "$ring" ]]; then
    echo "internal error: temporary keyring already exists" >&2
    return 1
  fi
  if ! gpg --no-options --batch --no-tty --no-autostart --homedir "$home" \
    --no-default-keyring --keyring "$ring" \
    --import "$src" >"$home/import.txt" 2>"$home/import.err"; then
    echo "error: gpg could not import the public key" >&2
    cat "$home/import.err" >&2
    return 1
  fi
  if ! gpg --no-options --batch --no-tty --no-autostart --homedir "$home" \
    --no-default-keyring --keyring "$ring" \
    --fixed-list-mode --with-colons --with-fingerprint --list-keys \
    >"$home/list.txt" 2>"$home/list.err"; then
    echo "error: gpg --list-keys failed" >&2
    cat "$home/list.err" >&2
    return 1
  fi
  if ! got=$(parse_primary "$home/list.txt"); then
    return 1
  fi
  got_u=$(printf '%s' "$got" | tr '[:lower:]' '[:upper:]')
  if [[ "$got_u" != "$EXPECTED_FPR" ]]; then
    echo "error: primary fingerprint mismatch" >&2
    echo "observed ${got_u}" >&2
    echo "expected ${EXPECTED_FPR}" >&2
    return 1
  fi
  assert_no_secret_keys "$home" "$ring"
}

# Literal candidate paths are tested one by one. Their names are not stored in
# one array whose length is checked: under nullglob a literal path is kept, so
# two names would still have length 2 when neither file exists. Only a path
# that is already a non-empty regular file is counted.
locate_cached_inrelease() {
  local candidate
  local -a found
  found=()
  for candidate in \
    "/var/lib/apt/lists/${LIST_PREFIX}InRelease" \
    "/var/lib/apt/lists/${LIST_PREFIX}._InRelease"
  do
    if [[ -f "$candidate" && ! -L "$candidate" && -s "$candidate" ]]; then
      found+=("$candidate")
    fi
  done
  if [[ ${#found[@]} -ne 1 ]]; then
    echo "error: the apt cache does not contain exactly one InRelease for this source" >&2
    if [[ ${#found[@]} -gt 0 ]]; then
      printf '%s\n' "${found[@]}" >&2
    fi
    return 1
  fi
  printf '%s\n' "${found[0]}"
}

collect_package_hashes() {
  local src=$1 dest=$2
  local line in_sha=0 trim sum sz path
  : >"$dest"
  while IFS= read -r line || [[ -n "$line" ]]; do
    line=${line%$'\r'}
    case "$line" in
      "-----BEGIN PGP SIGNATURE-----")
        break
        ;;
      "SHA256:")
        in_sha=1
        continue
        ;;
      SHA1:|SHA512:|MD5Sum:|SHA384:)
        in_sha=0
        continue
        ;;
    esac
    if [[ "$in_sha" -ne 1 ]]; then
      continue
    fi
    case "$line" in
      " "*) ;;
      *)
        in_sha=0
        continue
        ;;
    esac
    trim=${line#"${line%%[![:space:]]*}"}
    sum=
    sz=
    path=
    read -r sum sz path <<<"$trim"
    sum=$(printf '%s' "$sum" | tr '[:upper:]' '[:lower:]')
    if [[ ! "$sum" =~ ^[0-9a-f]{64}$ ]]; then
      die "fresh InRelease has a malformed SHA256 line"
    fi
    if [[ ! "$sz" =~ ^[0-9]+$ ]]; then
      die "fresh InRelease has a malformed SHA256 size"
    fi
    if [[ -z "${path}" || "$path" == *" "* || "$path" == *".."* ]]; then
      die "fresh InRelease has a malformed SHA256 path"
    fi
    case "$path" in
      # The signed archive is flat. Compare the decompressed cache body only
      # with the uncompressed Packages entry, never with a served compression.
      Packages)
        printf '%s %s\n' "$sum" "$sz" >>"$dest"
        ;;
    esac
  done < "$src"
}

packages_have_exact() {
  local file=$1 pkg=$2 ver=$3
  local line in_pkg=0 have_ver=0
  while IFS= read -r line || [[ -n "$line" ]]; do
    line=${line%$'\r'}
    case "$line" in
      "")
        if [[ "$in_pkg" -eq 1 && "$have_ver" -eq 1 ]]; then
          return 0
        fi
        in_pkg=0
        have_ver=0
        ;;
      "Package: ${pkg}")
        in_pkg=1
        have_ver=0
        ;;
      Package:*)
        in_pkg=0
        have_ver=0
        ;;
      "Version: ${ver}")
        if [[ "$in_pkg" -eq 1 ]]; then
          have_ver=1
        fi
        ;;
      Version:*)
        if [[ "$in_pkg" -eq 1 ]]; then
          in_pkg=0
          have_ver=0
        fi
        ;;
    esac
  done < "$file"
  [[ "$in_pkg" -eq 1 && "$have_ver" -eq 1 ]]
}

install_exact() {
  local dest=$1 src=$2
  local dir tmp base
  dir=$(dirname -- "$dest")
  base=$(basename -- "$dest")
  if [[ -L "$dir" || ! -d "$dir" ]]; then
    echo "refusing ${dir}: not a real directory" >&2
    return 1
  fi
  if [[ -L "$dest" ]]; then
    echo "refusing symlink ${dest}" >&2
    return 1
  fi
  if [[ -e "$dest" && ! -f "$dest" ]]; then
    echo "refusing non-regular file ${dest}" >&2
    return 1
  fi
  if [[ -f "$dest" ]]; then
    if cmp -s -- "$src" "$dest"; then
      chmod 644 "$dest"
      return 0
    fi
    echo "unexpected existing content in ${dest}; not replacing it" >&2
    return 1
  fi
  tmp=$(mktemp "${dir}/.${base}.XXXXXX")
  if ! cp -- "$src" "$tmp"; then
    rm -f -- "$tmp"
    echo "could not copy ${dest}" >&2
    return 1
  fi
  if ! chmod 644 "$tmp"; then
    rm -f -- "$tmp"
    echo "could not set mode on ${dest}" >&2
    return 1
  fi
  if ln -- "$tmp" "$dest"; then
    rm -f -- "$tmp"
    return 0
  fi
  rm -f -- "$tmp"
  if [[ -L "$dest" ]]; then
    echo "refusing symlink ${dest}" >&2
    return 1
  fi
  if [[ -f "$dest" ]] && cmp -s -- "$src" "$dest"; then
    chmod 644 "$dest"
    return 0
  fi
  echo "could not create ${dest}" >&2
  return 1
}

if [[ -n "${SX_ALLOW_HOST:-}" || -n "${SX_ALLOW_PROD:-}" ]]; then
  die "no host or production bypass is accepted"
fi
if [[ $# -ne 0 ]]; then
  echo "usage: add-stackx-kernel-repo.sh" >&2
  echo "Adds the repository and refreshes only its index. It does not install or reboot." >&2
  exit 1
fi
if [[ "$(id -u)" -ne 0 ]]; then
  die "root is required"
fi

os_release_path=
if [[ -L /etc/os-release ]]; then
  if ! command -v readlink >/dev/null 2>&1; then
    die "missing required command: readlink"
  fi
  os_canon=$(readlink -f -- /etc/os-release) || die "could not resolve /etc/os-release"
  if [[ "$os_canon" != /usr/lib/os-release ]]; then
    die "refusing /etc/os-release symlink target (only /usr/lib/os-release is accepted)"
  fi
  if [[ -L /usr/lib/os-release || ! -f /usr/lib/os-release ]]; then
    die "refusing /etc/os-release: resolved /usr/lib/os-release is not a regular file"
  fi
  os_release_path=/usr/lib/os-release
elif [[ -f /etc/os-release ]]; then
  os_release_path=/etc/os-release
else
  die "need /etc/os-release as a regular file or a symlink to the regular file /usr/lib/os-release"
fi

os_id=
os_version_id=
os_codename=
while IFS= read -r line || [[ -n "$line" ]]; do
  line=${line%$'\r'}
  case "$line" in
    ID=*|VERSION_ID=*|VERSION_CODENAME=*)
      key=${line%%=*}
      val=${line#*=}
      val=${val#\"}
      val=${val%\"}
      case "$key" in
        ID) os_id=$val ;;
        VERSION_ID) os_version_id=$val ;;
        VERSION_CODENAME) os_codename=$val ;;
      esac
      ;;
  esac
done < "$os_release_path"

if ! command -v dpkg >/dev/null 2>&1; then
  die "dpkg is required to confirm the architecture"
fi
arch=$(dpkg --print-architecture)
# VERSION_CODENAME may be absent. If it is present it must match this distribution.
if [[ "$os_id" != debian || "$os_version_id" != "$DIST_VERSION" || "$arch" != amd64 ]]; then
  die "only ${DIST_PRETTY} amd64 is accepted (found ID=${os_id:-unknown} VERSION_ID=${os_version_id:-unknown} VERSION_CODENAME=${os_codename:-absent} arch=${arch})"
fi
if [[ -n "$os_codename" && "$os_codename" != "$DIST_CODENAME" ]]; then
  die "refusing VERSION_CODENAME=${os_codename}; ${DIST_PRETTY} must report ${DIST_CODENAME} when a codename is present"
fi

if ! fingerprint_is_final "$EXPECTED_FPR"; then
  die "EXPECTED_FPR is not a finalized 40-hex primary fingerprint. No key was downloaded and no apt file was written"
fi

for cmd in gpg gpgv cmp mktemp chmod ln cp cat tr wc grep mkdir rm id readlink find sha256sum date head basename dirname; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    die "missing required command: ${cmd}"
  fi
done
if [[ ! -x "$HELPER" ]]; then
  die "missing ${HELPER} from package apt"
fi
# APT 1.8 provides the HTTPS method. Still require the program.
if [[ ! -x /usr/lib/apt/methods/https ]]; then
  die "the apt HTTPS method /usr/lib/apt/methods/https is missing"
fi
if ! command -v apt-get >/dev/null 2>&1; then
  die "apt-get is required"
fi
if [[ ! -s /etc/ssl/certs/ca-certificates.crt || -L /etc/ssl/certs/ca-certificates.crt ]]; then
  die "ca-certificates bundle /etc/ssl/certs/ca-certificates.crt is missing"
fi

case "$KEY_URL" in
  https://mirrors.scalarx.net/kernel/buster/stackx-kernel-buster.asc) ;;
  *) die "internal error: key URL is not the Buster HTTPS key" ;;
esac
case "$INRELEASE_URL" in
  https://mirrors.scalarx.net/kernel/buster/InRelease) ;;
  *) die "internal error: InRelease URL is not the Buster HTTPS index" ;;
esac
case "$ORIGIN" in
  https://mirrors.scalarx.net/kernel/buster) ;;
  *) die "internal error: archive origin is not the Buster HTTPS mirror" ;;
esac

source_line="deb [arch=amd64 signed-by=${KEYRING}] ${ORIGIN} ./"
case "$source_line" in
  *trusted=yes*|*allow-unauthenticated*|*AllowInsecure*|*AllowUnauthenticated*)
    die "internal error: source line would weaken signature checks"
    ;;
esac

if [[ -e /etc/default/grub || -L /etc/default/grub ]]; then
  if [[ -L /etc/default/grub || ! -f /etc/default/grub ]]; then
    die "refusing unexpected /etc/default/grub"
  fi
  grub_before=$(sha256sum -- /etc/default/grub)
else
  grub_before=absent
fi

work=$(mktemp -d /tmp/stackx-kernel-repo.XXXXXX)
chmod 700 "$work"
trap 'rm -rf "$work"' EXIT

mkdir -m 700 "$work/gpg-version"
export GNUPGHOME="$work/gpg-version"
gpg_line=$(gpg --no-options --batch --homedir "$GNUPGHOME" --version 2>"$work/gpg-version.err" | head -n 1 || true)
case "$gpg_line" in
  'gpg (GnuPG) 2.'*) ;;
  *) die "GnuPG 2 is required (found: ${gpg_line:-none}). No key was downloaded and no apt file was written" ;;
esac

mkdir -m 700 "$work/gnupg"
export GNUPGHOME="$work/gnupg"
now_epoch=$(date +%s)
if [[ ! "$now_epoch" =~ ^[0-9]+$ ]]; then
  die "could not read the current time"
fi

if ! "$HELPER" \
  -o Acquire::https::Verify-Peer=true \
  -o Acquire::https::Verify-Host=true \
  download-file "$KEY_URL" "$work/key.asc" \
  >"$work/download.log" 2>&1; then
  echo "error: download of the repository public key failed" >&2
  cat "$work/download.log" >&2
  echo "no apt file was written" >&2
  exit 1
fi
if [[ -L "$work/key.asc" || ! -f "$work/key.asc" ]]; then
  die "downloaded key is not a regular file"
fi
key_bytes=$(wc -c < "$work/key.asc")
key_bytes=${key_bytes//[[:space:]]/}
if [[ ! "$key_bytes" =~ ^[0-9]+$ ]] || (( key_bytes < 32 || key_bytes > 65536 )); then
  die "refusing key file of unexpected size (${key_bytes:-0} bytes)"
fi
if ! grep -q -- '-----BEGIN PGP PUBLIC KEY BLOCK-----' "$work/key.asc"; then
  die "repository key is not an armored public key"
fi
if grep -q -- 'PRIVATE KEY' "$work/key.asc" || grep -q -- 'SECRET KEY' "$work/key.asc"; then
  die "refusing secret key material"
fi
if ! refuse_secret_packets "$work/key.asc" asc; then
  echo "no apt file was written" >&2
  exit 1
fi
if ! check_imported_key "$work/gnupg" "$work/gnupg/pubring.gpg" "$work/key.asc"; then
  echo "no apt file was written" >&2
  exit 1
fi

if ! gpg --no-options --batch --no-tty --no-autostart --homedir "$GNUPGHOME" \
  --no-default-keyring --keyring "$work/gnupg/pubring.gpg" \
  --export --output "$work/keyring.gpg" >"$work/export.txt" 2>"$work/export.err"; then
  echo "error: could not export the public keyring" >&2
  cat "$work/export.err" >&2
  echo "no apt file was written" >&2
  exit 1
fi
if [[ -L "$work/keyring.gpg" || ! -f "$work/keyring.gpg" ]]; then
  die "exported public keyring is not a regular file"
fi
ring_bytes=$(wc -c < "$work/keyring.gpg")
ring_bytes=${ring_bytes//[[:space:]]/}
if [[ ! "$ring_bytes" =~ ^[0-9]+$ ]] || (( ring_bytes < 32 || ring_bytes > 65536 )); then
  die "refusing exported keyring of unexpected size (${ring_bytes:-0} bytes)"
fi
if ! refuse_secret_packets "$work/keyring.gpg" ring; then
  echo "no apt file was written" >&2
  exit 1
fi
if ! check_imported_key "$work/gnupg-export" "$work/gnupg-export/pubring.gpg" "$work/keyring.gpg"; then
  echo "exported public keyring failed the primary-key check" >&2
  echo "no apt file was written" >&2
  exit 1
fi

{
  echo "# Managed by add-stackx-kernel-repo.sh. StackX ${DIST_NAME} amd64 kernel archive."
  echo "# This line does not set a boot default."
  printf '%s\n' "$source_line"
} > "$work/source.list"
deb_lines=$(grep -c '^deb ' "$work/source.list" || true)
if [[ "$deb_lines" -ne 1 ]]; then
  die "internal error: expected exactly one deb line"
fi
if grep -Eq 'trusted=yes|allow-unauthenticated|AllowInsecure|AllowUnauthenticated' "$work/source.list"; then
  die "internal error: generated source list weakens signature checks"
fi

if [[ -L /usr/share || ! -d /usr/share ]]; then
  die "refusing /usr/share: not a real directory"
fi
if [[ -L /usr/share/keyrings ]]; then
  die "refusing symlink /usr/share/keyrings"
fi
if [[ ! -d /usr/share/keyrings ]]; then
  if [[ -e /usr/share/keyrings ]]; then
    die "unexpected /usr/share/keyrings"
  fi
  mkdir -m 755 /usr/share/keyrings
fi
if [[ -L /etc/apt || ! -d /etc/apt ]]; then
  die "refusing /etc/apt: not a real directory"
fi
if [[ -L /etc/apt/sources.list.d || ! -d /etc/apt/sources.list.d ]]; then
  die "need a real directory /etc/apt/sources.list.d"
fi

install_exact "$KEYRING" "$work/keyring.gpg"
install_exact "$LIST" "$work/source.list"

# Drop the temporary GnuPG home before apt and gpgv run, so they are not
# pointed at it. gpgv below gets an explicit empty home and the APT keyring.
unset GNUPGHOME

if ! "$HELPER" \
  -o Acquire::https::Verify-Peer=true \
  -o Acquire::https::Verify-Host=true \
  download-file "$INRELEASE_URL" "$work/InRelease.fresh" \
  >"$work/inrelease-download.log" 2>&1; then
  echo "error: download of a fresh InRelease failed" >&2
  cat "$work/inrelease-download.log" >&2
  echo "No package was installed and the system was not rebooted." >&2
  exit 1
fi
if [[ -L "$work/InRelease.fresh" || ! -f "$work/InRelease.fresh" ]]; then
  die "fresh InRelease was not written"
fi
in_bytes=$(wc -c < "$work/InRelease.fresh")
in_bytes=${in_bytes//[[:space:]]/}
if [[ ! "$in_bytes" =~ ^[0-9]+$ ]] || (( in_bytes < 1 || in_bytes > 1048576 )); then
  die "refusing InRelease of unexpected size (${in_bytes:-0} bytes)"
fi
if ! grep -q -- '-----BEGIN PGP SIGNED MESSAGE-----' "$work/InRelease.fresh"; then
  die "fresh InRelease is not a clearsigned file"
fi
if ! grep -q -- '-----BEGIN PGP SIGNATURE-----' "$work/InRelease.fresh"; then
  die "fresh InRelease has no signature"
fi
if grep -q -- 'PRIVATE KEY' "$work/InRelease.fresh"; then
  die "refusing secret key material in InRelease"
fi

mkdir -m 700 "$work/gpgv"
if ! gpgv --homedir "$work/gpgv" --keyring "$KEYRING" "$work/InRelease.fresh" \
  >"$work/gpgv.txt" 2>&1; then
  echo "error: gpgv rejected the fresh InRelease" >&2
  cat "$work/gpgv.txt" >&2
  echo "No package was installed and the system was not rebooted." >&2
  exit 1
fi
collect_package_hashes "$work/InRelease.fresh" "$work/pkg.sha256"
if [[ ! -s "$work/pkg.sha256" ]]; then
  die "fresh InRelease has no SHA256 record for a Packages index"
fi

# No APT::Update::Error-Mode=any. A non-zero apt status, a missing or different
# InRelease, or a Packages index that does not match the fresh file is failure.
# An old cache file is not treated as success by itself.
if ! apt-get update \
  -o Dir::Etc::sourcelist="sources.list.d/stackx-kernel-buster.list" \
  -o Dir::Etc::sourceparts="-" \
  -o APT::Get::List-Cleanup="0" \
  -o Acquire::AllowInsecureRepositories=false \
  -o Acquire::AllowDowngradeToInsecureRepositories=false \
  -o APT::Get::AllowUnauthenticated=false \
  -o Acquire::https::Verify-Peer=true \
  -o Acquire::https::Verify-Host=true; then
  echo "error: index refresh failed for the StackX source only" >&2
  echo "signature checks were not disabled. No package was installed and the system was not rebooted." >&2
  exit 1
fi

if ! cached=$(locate_cached_inrelease); then
  echo "error: apt-get update did not leave exactly one InRelease for this source" >&2
  echo "No package was installed and the system was not rebooted." >&2
  exit 1
fi
case "$cached" in
  /var/lib/apt/lists/${LIST_PREFIX}InRelease|/var/lib/apt/lists/${LIST_PREFIX}._InRelease) ;;
  *) die "internal error: InRelease path is outside the expected cache names" ;;
esac
if ! cmp -s -- "$work/InRelease.fresh" "$cached"; then
  echo "error: cached InRelease does not match the fresh signed InRelease" >&2
  echo "No package was installed and the system was not rebooted." >&2
  exit 1
fi

if [[ -L /var/lib/apt/lists || ! -d /var/lib/apt/lists ]]; then
  die "refusing /var/lib/apt/lists: not a real directory"
fi
find /var/lib/apt/lists -maxdepth 1 -type f -name "${LIST_PREFIX}*Packages*" >"$work/pkg.files"
pkg_count=0
while IFS= read -r list || [[ -n "${list:-}" ]]; do
  [[ -n "${list:-}" ]] || continue
  if [[ -L "$list" || ! -f "$list" || ! -s "$list" ]]; then
    die "refusing unreadable Packages index ${list}"
  fi
  base=$(basename -- "$list")
  case "$base" in
    *_Packages|*_Packages.gz|*_Packages.xz|*_Packages.bz2|*_Packages.lzma|*_Packages.lz4) ;;
    *)
      continue
      ;;
  esac
  # APT may recompress downloaded indexes (notably to LZ4). Authenticate the
  # uncompressed body against the flat Packages record in the fresh InRelease.
  if ! "$HELPER" cat-file "$list" >"$work/packages.body" 2>"$work/packages.err"; then
    echo "error: could not read Packages index ${base}" >&2
    cat "$work/packages.err" >&2
    exit 1
  fi
  body_bytes=$(wc -c < "$work/packages.body")
  body_bytes=${body_bytes//[[:space:]]/}
  if [[ ! "$body_bytes" =~ ^[0-9]+$ ]] || (( body_bytes < 1 || body_bytes > 8388608 )); then
    die "Packages index ${base} has an unexpected size after decompression"
  fi
  sum_line=$(sha256sum -- "$work/packages.body")
  sum=${sum_line%% *}
  sum=$(printf '%s' "$sum" | tr '[:upper:]' '[:lower:]')
  hash_ok=0
  while read -r h sz; do
    [[ -n "${h:-}" ]] || continue
    if [[ "$h" == "$sum" && "$sz" == "$body_bytes" ]]; then
      hash_ok=1
      break
    fi
  done < "$work/pkg.sha256"
  if [[ "$hash_ok" -ne 1 ]]; then
    echo "error: decompressed Packages index ${base} does not match the fresh InRelease" >&2
    echo "No package was installed and the system was not rebooted." >&2
    exit 1
  fi
  if ! packages_have_exact "$work/packages.body" "$META_PKG" "$META_VER"; then
    echo "error: ${base} does not contain ${META_PKG} version ${META_VER}" >&2
    echo "No package was installed and the system was not rebooted." >&2
    exit 1
  fi
  pkg_count=$((pkg_count + 1))
done < "$work/pkg.files"
if [[ "$pkg_count" -eq 0 ]]; then
  echo "error: update exited without a Packages index for this source" >&2
  echo "No package was installed and the system was not rebooted." >&2
  exit 1
fi

if [[ "$grub_before" == absent ]]; then
  if [[ -e /etc/default/grub || -L /etc/default/grub ]]; then
    die "/etc/default/grub appeared; this script does not change boot settings"
  fi
else
  if [[ -L /etc/default/grub || ! -f /etc/default/grub ]]; then
    die "/etc/default/grub changed; this script does not change boot settings"
  fi
  grub_after=$(sha256sum -- /etc/default/grub)
  if [[ "$grub_before" != "$grub_after" ]]; then
    die "/etc/default/grub changed; this script does not change boot settings"
  fi
fi

# Bilan et étapes suivantes communs aux trois distributions.
printf '%s\n' \
  'Dépôt noyau StackX Buster configuré.' \
  'Clé du dépôt et index APT vérifiés.' \
  'Aucun paquet installé, aucun redémarrage, aucun choix de démarrage modifié.' \
  '' \
  '1. Simuler l'\''installation et examiner les opérations proposées :' \
  '   apt-get --simulate --no-remove install stackx-kernel-buster-amd64' \
  '2. Si la simulation convient, installer explicitement :' \
  '   apt-get --no-remove install stackx-kernel-buster-amd64' \
  '' \
  'Ces commandes sont affichées uniquement ; le script ne les exécute pas.' \
  'Avant de redémarrer, vérifier le noyau sélectionné dans GRUB (voir README.txt).' \
  'Après redémarrage : uname -r doit afficher 4.19.0-27-sx1-amd64.'
