#!/bin/bash
# Ajoute le dépôt noyau StackX 6.12 pour Debian 9 Stretch amd64.
# Rafraîchit seulement cet index. N'installe aucun paquet, ne redémarre pas,
# ne modifie pas GRUB_DEFAULT et ne touche pas aux dépôts sx1.
#
# Canal envisagé : https://mirrors.scalarx.net/stackx-kernel/6.12/stretch/
# Empreinte primaire APT : placeholder bloquant, canal non signé et non vérifié.
# Aucune clé privée n'est générée, importée, exportée ou utilisée pour signer.
# Aucun contournement d'hôte, aucun trusted=yes, aucune validation TLS désactivée.
# Famille APT 1.4 : APT_HAS_ERROR_MODE=no. Ne pas supposer APT::Update::Error-Mode=any.
# La comparaison de l'InRelease frais signé avec la copie en cache reste obligatoire.

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

EXPECTED_FPR='AF97ED98AACE4CE129A03B6E2C9736B593BABF7F'
KERNEL_ARCHIVE_FPR='647F28654894E3BD457199BE38DBBDC86092693E'
DIST_VERSION='9'
DIST_CODENAME='stretch'
DIST_NAME='Stretch'
DIST_PRETTY='Debian 9 (Stretch)'
DEBIAN_VERSION_PREFIX='9'
APT_HAS_ERROR_MODE=no
ORIGIN='https://mirrors.scalarx.net/stackx-kernel/6.12/stretch'
KEY_URL="${ORIGIN}/stackx-kernel-6.12-stretch.asc"
INRELEASE_URL="${ORIGIN}/InRelease"
KEYRING='/usr/share/keyrings/stackx-kernel-6.12-stretch.gpg'
LIST='/etc/apt/sources.list.d/stackx-kernel-6.12-stretch.list'
HELPER='/usr/lib/apt/apt-helper'
LIST_PREFIX='mirrors.scalarx.net_stackx-kernel_6.12_stretch_'
META_PKG='stackx-kernel-6.12-stretch-amd64'
META_VER='6.12.115-1'
IMAGE_PKG='stackx-linux-image-6.12.115-stretch-1-amd64'
HEADERS_PKG='stackx-linux-headers-6.12.115-stretch-1-amd64'
TOOLCHAIN_PKG='stackx-toolchain-6.12-stretch-amd64'
TOOLCHAIN_VER='12.4.0-1'
META_SHA256='5a6ef2d54e35c3a57ea3dcafa1b5c63c65255e458985e60c4787fb1507fb21e4'
IMAGE_SHA256='5e2668e5da86d8080e544efbabf4f52d963e3e54ed59e533fa793c046bead59d'
HEADERS_SHA256='20cbc39866e55287907ee67265545bf0da76dc94d1d1e0778e62870a56e494d4'
TOOLCHAIN_SHA256='66a3de139f0ad971867d8735312a5327c9e5c3f18bcb4def8196a8269a45de54'
META_DEB='pool/stackx-kernel-6.12-stretch-amd64_6.12.115-1_amd64.deb'
IMAGE_DEB='pool/stackx-linux-image-6.12.115-stretch-1-amd64_6.12.115-1_amd64.deb'
HEADERS_DEB='pool/stackx-linux-headers-6.12.115-stretch-1-amd64_6.12.115-1_amd64.deb'
TOOLCHAIN_DEB='pool/stackx-toolchain-6.12-stretch-amd64_12.4.0-1_amd64.deb'
created_key=0
created_list=0

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

fail() {
  echo "error: $*" >&2
  if [[ -n "${work:-}" && -d "$work" ]]; then
    if [[ "$created_list" -eq 1 && -f "$LIST" && -f "$work/source.list" ]] && cmp -s -- "$work/source.list" "$LIST"; then
      rm -f -- "$LIST"
    fi
    if [[ "$created_key" -eq 1 && -f "$KEYRING" && -f "$work/keyring.gpg" ]] && cmp -s -- "$work/keyring.gpg" "$KEYRING"; then
      rm -f -- "$KEYRING"
    fi
  fi
  echo "Aucun paquet n'a été installé. Le système n'a pas été redémarré." >&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
  [[ "$u" != "$KERNEL_ARCHIVE_FPR" ]] || return 1
  printf '%s\n' "$u"
}

parse_primary() {
  local line rectype validity keylen algo keyid created expires cert ownertrust uid_or_fpr sigclass caps extra
  local pubs=0 state=none fpr= primary_validity= primary_expires= 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 "le fichier contient plus d'une clé primaire" >&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 "empreinte primaire en double" >&2
          return 1
        fi
        ;;
      uid|uat|sub)
        if [[ "$state" == need_fpr ]]; then
          echo "empreinte primaire absente" >&2
          return 1
        fi
        [[ "$rectype" == sub ]] && state=sub
        ;;
      sec|ssb)
        echo "matériel secret refusé" >&2
        return 1
        ;;
      rev|rvk)
        echo "matériel révoqué refusé" >&2
        return 1
        ;;
    esac
  done < "$1"
  if (( pubs != 1 )); then
    echo "le fichier ne contient pas exactement une clé primaire" >&2
    return 1
  fi
  if [[ "$state" == need_fpr || -z "$fpr" ]]; then
    echo "empreinte primaire absente" >&2
    return 1
  fi
  vchar=${primary_validity:0:1}
  case "$vchar" in
    '-'|q|o|m|f|u|w|s) ;;
    *)
      echo "validité GnuPG refusée : ${primary_validity}" >&2
      return 1
      ;;
  esac
  if [[ -n "$primary_expires" && "$primary_expires" != 0 ]]; then
    [[ "$primary_expires" =~ ^[0-9]+$ ]] || { echo "expiration illisible" >&2; return 1; }
    if [[ "$primary_expires" -le "$now_epoch" ]]; then
      echo "clé primaire expirée" >&2
      return 1
    fi
  fi
  [[ "$fpr" =~ ^[0-9A-Fa-f]{40}$ ]] || { echo "empreinte primaire invalide" >&2; return 1; }
  printf '%s\n' "$fpr"
}

refuse_secret_packets() {
  local src=$1 tag=$2 out pubs
  out="$work/packets-${tag}.txt"
  [[ -d "$work/gpg-packets" ]] || mkdir -m 700 "$work/gpg-packets"
  chmod 700 "$work/gpg-packets"
  if ! gpg --no-options --batch --no-tty --no-autostart --homedir "$work/gpg-packets" \
    --list-packets "$src" >"$out" 2>"$work/packets-${tag}.err"; then
    echo "GnuPG ne peut pas lire le matériau de clé" >&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 "matériel secret refusé" >&2
    return 1
  fi
  pubs=$(grep -c -F ':public key packet:' "$out" || true)
  [[ "$pubs" -eq 1 ]] || { echo "une seule clé publique primaire est acceptée" >&2; return 1; }
}

assert_no_secret_keys() {
  local home=$1 ring=$2 sec_rc=0 hit
  if [[ -s "$home/list.txt" ]] && grep -Eq '^(sec|ssb|rvk):' "$home/list.txt"; then
    echo "matériel secret ou révoqué dans le listage public" >&2
    return 1
  fi
  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 "matériel secret après import" >&2
    return 1
  fi
  if [[ "$sec_rc" -ne 0 && "$sec_rc" -ne 2 ]]; then
    echo "gpg --list-secret-keys a échoué" >&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 "matériel secret après import" >&2
      return 1
    fi
  fi
  if [[ -s "$home/secring.gpg" ]]; then
    echo "matériel secret après import" >&2
    return 1
  fi
}

check_imported_key() {
  local home=$1 ring=$2 src=$3 got got_u
  [[ ! -L "$home" ]] || { echo "répertoire GnuPG temporaire refusé" >&2; return 1; }
  [[ -d "$home" ]] || mkdir -m 700 "$home"
  chmod 700 "$home"
  if [[ -e "$ring" || -L "$ring" ]]; then
    echo "trousseau temporaire déjà présent" >&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 "import de la clé publique impossible" >&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 "gpg --list-keys a échoué" >&2
    cat "$home/list.err" >&2
    return 1
  fi
  got=$(parse_primary "$home/list.txt") || return 1
  got_u=$(printf '%s' "$got" | tr '[:lower:]' '[:upper:]')
  if [[ "$got_u" != "$EXPECTED_FPR" ]]; then
    echo "empreinte primaire différente de la valeur attendue" >&2
    echo "observée ${got_u}" >&2
    echo "attendue ${EXPECTED_FPR}" >&2
    return 1
  fi
  assert_no_secret_keys "$home" "$ring"
}

locate_cached_inrelease() {
  local candidate
  local -a 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 "le cache apt ne contient pas exactement un InRelease pour cette source" >&2
    return 1
  fi
  printf '%s\n' "${found[0]}"
}

collect_package_hashes() {
  local src=$1 dest=$2 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
    [[ "$in_sha" -eq 1 ]] || continue
    case "$line" in
      " "*) ;;
      *) in_sha=0; continue ;;
    esac
    trim=${line#"${line%%[![:space:]]*}"}
    read -r sum sz path <<<"$trim"
    sum=$(printf '%s' "$sum" | tr '[:upper:]' '[:lower:]')
    [[ "$sum" =~ ^[0-9a-f]{64}$ ]] || die "ligne SHA256 illisible dans InRelease"
    [[ "$sz" =~ ^[0-9]+$ ]] || die "taille SHA256 illisible dans InRelease"
    [[ -n "$path" && "$path" != *" "* && "$path" != *".."* ]] || die "chemin SHA256 illisible dans InRelease"
    case "$path" in
      Packages) printf '%s %s\n' "$sum" "$sz" >>"$dest" ;;
    esac
  done < "$src"
}

package_stanza() {
  local file=$1 pkg=$2 dest=$3
  local line in=0
  : >"$dest"
  while IFS= read -r line || [[ -n "$line" ]]; do
    line=${line%$'\r'}
    if [[ "$in" -eq 0 && "$line" == "Package: ${pkg}" ]]; then
      in=1
    fi
    if [[ "$in" -eq 1 ]]; then
      if [[ -z "$line" ]]; then
        return 0
      fi
      printf '%s\n' "$line" >>"$dest"
    fi
  done < "$file"
  [[ "$in" -eq 1 ]]
}

control_field() {
  local file=$1 name=$2
  local line have=0 acc=
  while IFS= read -r line || [[ -n "$line" ]]; do
    line=${line%$'\r'}
    if [[ "$have" -eq 0 && "$line" == "${name}:"* ]]; then
      have=1
      acc=${line#*:}
      acc=${acc#"${acc%%[![:space:]]*}"}
      continue
    fi
    if [[ "$have" -eq 1 && ( "$line" == " "* || "$line" == $'\t'* ) ]]; then
      acc+=" ${line#"${line%%[![:space:]]*}"}"
      continue
    fi
    if [[ "$have" -eq 1 ]]; then
      break
    fi
  done < "$file"
  printf '%s\n' "$acc"
  return 0
}

relation_exact() {
  local atom=$1 pkg=$2 ver=$3
  local name base qual rest inside
  atom=${atom#"${atom%%[![:space:]]*}"}
  atom=${atom%"${atom##*[![:space:]]}"}
  [[ -n "$atom" ]] || return 1
  name=${atom%%[[:space:](]*}
  [[ -n "$name" ]] || return 1
  base=${name%%:*}
  [[ "$base" == "$pkg" ]] || return 1
  if [[ "$name" == *:* ]]; then
    qual=${name#*:}
    [[ "$qual" == amd64 || "$qual" == any ]] || return 1
  fi
  rest=${atom#"$name"}
  rest=${rest#"${rest%%[![:space:]]*}"}
  rest=${rest%"${rest##*[![:space:]]}"}
  [[ "$rest" == "("*")" ]] || return 1
  inside=${rest#(}
  inside=${inside%)}
  inside=${inside#"${inside%%[![:space:]]*}"}
  inside=${inside%"${inside##*[![:space:]]}"}
  [[ "$inside" == "="* ]] || return 1
  inside=${inside#=}
  inside=${inside#"${inside%%[![:space:]]*}"}
  inside=${inside%"${inside##*[![:space:]]}"}
  [[ "$inside" == "$ver" && "$inside" != *" "* ]]
}

depends_exact() {
  local depends=$1 pkg=$2 ver=$3
  local group
  local -a groups=()
  IFS=',' read -r -a groups <<<"$depends"
  for group in "${groups[@]}"; do
    case "$group" in
      *'|'*) continue ;;
    esac
    if relation_exact "$group" "$pkg" "$ver"; then
      return 0
    fi
  done
  return 1
}

field_rejects_libc() {
  local stanza=$1
  local label value
  for label in Depends Pre-Depends Recommends Suggests Replaces Conflicts Provides Breaks; do
    value=$(control_field "$stanza" "$label")
    if [[ "$value" == *linux-libc-dev* ]]; then
      return 1
    fi
  done
  return 0
}

install_exact() {
  local dest=$1 src=$2 dir tmp base
  dir=$(dirname -- "$dest")
  base=$(basename -- "$dest")
  [[ -d "$dir" && ! -L "$dir" ]] || { echo "répertoire refusé : ${dir}" >&2; return 1; }
  [[ ! -L "$dest" ]] || { echo "lien symbolique refusé : ${dest}" >&2; return 1; }
  if [[ -e "$dest" && ! -f "$dest" ]]; then
    echo "fichier non régulier refusé : ${dest}" >&2
    return 1
  fi
  if [[ -f "$dest" ]]; then
    if cmp -s -- "$src" "$dest"; then
      chmod 644 "$dest"
      return 0
    fi
    echo "contenu inattendu déjà présent, non remplacé : ${dest}" >&2
    return 1
  fi
  tmp=$(mktemp "${dir}/.${base}.XXXXXX")
  if ! cp -- "$src" "$tmp"; then
    rm -f -- "$tmp"
    return 1
  fi
  chmod 644 "$tmp"
  if ln -- "$tmp" "$dest"; then
    rm -f -- "$tmp"
    return 0
  fi
  rm -f -- "$tmp"
  if [[ -f "$dest" && ! -L "$dest" ]] && cmp -s -- "$src" "$dest"; then
    chmod 644 "$dest"
    return 0
  fi
  echo "création impossible : ${dest}" >&2
  return 1
}

snapshot_one() {
  local path=$1 key
  key=$(printf '%s' "$path" | tr '/' '_')
  if [[ -L "$path" ]]; then
    echo symlink >"$work/snap/${key}.kind"
  elif [[ -f "$path" ]]; then
    sha256sum -- "$path" >"$work/snap/${key}.sum"
    echo file >"$work/snap/${key}.kind"
  elif [[ -e "$path" ]]; then
    echo other >"$work/snap/${key}.kind"
  else
    echo absent >"$work/snap/${key}.kind"
  fi
}

verify_snapshots() {
  local path kind key now
  for path in "$@"; do
    key=$(printf '%s' "$path" | tr '/' '_')
    kind=$(cat "$work/snap/${key}.kind")
    if [[ -L "$path" ]]; then
      now=symlink
    elif [[ -f "$path" ]]; then
      now=file
    elif [[ -e "$path" ]]; then
      now=other
    else
      now=absent
    fi
    [[ "$now" == "$kind" ]] || fail "fichier protégé modifié : ${path}"
    if [[ "$kind" == file ]]; then
      sha256sum -- "$path" >"$work/snap/${key}.now"
      cmp -s "$work/snap/${key}.sum" "$work/snap/${key}.now" || fail "contenu protégé modifié : ${path}"
    fi
  done
}

if [[ -n "${SX_ALLOW_HOST:-}" || -n "${SX_ALLOW_PROD:-}" ]]; then
  die "aucun contournement d'hôte ou de production n'est accepté"
fi
if [[ $# -ne 0 ]]; then
  echo "usage: add-stackx-kernel-repo.sh" >&2
  echo "Ajoute le dépôt et rafraîchit son index. N'installe rien et ne redémarre pas." >&2
  exit 1
fi
[[ "$(id -u)" -eq 0 ]] || die "root est requis"

os_release_path=
if [[ -L /etc/os-release ]]; then
  command -v readlink >/dev/null 2>&1 || die "readlink est requis"
  os_canon=$(readlink -f -- /etc/os-release) || die "résolution de /etc/os-release impossible"
  [[ "$os_canon" == /usr/lib/os-release ]] || die "cible du lien /etc/os-release refusée"
  [[ -f /usr/lib/os-release && ! -L /usr/lib/os-release ]] || die "/usr/lib/os-release n'est pas un fichier régulier"
  os_release_path=/usr/lib/os-release
elif [[ -f /etc/os-release ]]; then
  os_release_path=/etc/os-release
else
  die "/etc/os-release doit être un fichier régulier ou un lien vers /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"
command -v dpkg >/dev/null 2>&1 || die "dpkg est requis"
arch=$(dpkg --print-architecture)
if [[ "$os_id" != debian || "$os_version_id" != "$DIST_VERSION" || "$arch" != amd64 ]]; then
  die "seule la cible ${DIST_PRETTY} amd64 est acceptée (ID=${os_id:-inconnu} VERSION_ID=${os_version_id:-inconnu} VERSION_CODENAME=${os_codename:-absent} arch=${arch})"
fi
if [[ -n "$os_codename" && "$os_codename" != "$DIST_CODENAME" ]]; then
  die "VERSION_CODENAME=${os_codename} refusé"
fi
if [[ -L /etc/debian_version ]]; then
  die "/etc/debian_version est un lien"
fi
if [[ -f /etc/debian_version ]]; then
  dv=$(head -n 1 /etc/debian_version)
  [[ "$dv" == "${DEBIAN_VERSION_PREFIX}"* ]] || die "/etc/debian_version=${dv} incompatible avec ${DIST_PRETTY}"
fi

EXPECTED_FPR=$(fingerprint_is_final "$EXPECTED_FPR") || die "EXPECTED_FPR n'est pas une empreinte primaire finale de 40 chiffres hexadécimaux. Aucun fichier apt n'est écrit"

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
  command -v "$cmd" >/dev/null 2>&1 || die "commande absente : ${cmd}"
done
[[ -x "$HELPER" ]] || die "apt-helper absent (${HELPER})"
if [[ ! -x /usr/lib/apt/methods/https ]]; then
  if [[ "$DIST_CODENAME" == stretch ]]; then
    die "méthode HTTPS absente. Sur Debian 9, le paquet apt-transport-https est requis"
  fi
  die "méthode HTTPS absente : /usr/lib/apt/methods/https"
fi
command -v apt-get >/dev/null 2>&1 || die "apt-get est requis"
[[ -s /etc/ssl/certs/ca-certificates.crt && ! -L /etc/ssl/certs/ca-certificates.crt ]] || die "paquet ca-certificates incomplet"

case "$KEY_URL" in
  "https://mirrors.scalarx.net/stackx-kernel/6.12/${DIST_CODENAME}/stackx-kernel-6.12-${DIST_CODENAME}.asc") ;;
  *) die "URL de clé hors du canal ${DIST_CODENAME} 6.12" ;;
esac
case "$INRELEASE_URL" in
  "https://mirrors.scalarx.net/stackx-kernel/6.12/${DIST_CODENAME}/InRelease") ;;
  *) die "URL InRelease hors du canal ${DIST_CODENAME} 6.12" ;;
esac
case "$ORIGIN" in
  "https://mirrors.scalarx.net/stackx-kernel/6.12/${DIST_CODENAME}") ;;
  *) die "origine hors du canal ${DIST_CODENAME} 6.12" ;;
esac

source_line="deb [arch=amd64 signed-by=${KEYRING}] ${ORIGIN} ./"
case "$source_line" in
  *trusted=yes*|*trusted=no*|*allow-unauthenticated*|*AllowInsecure*|*AllowUnauthenticated*|*Verify-Peer=false*|*Verify-Host=false*)
    die "la ligne de source affaiblirait les contrôles"
    ;;
esac

protected=(
  /etc/default/grub
  /boot/grub/grub.cfg
  /boot/grub/grubenv
  /etc/apt/sources.list.d/stackx-kernel-stretch.list
  /etc/apt/sources.list.d/stackx-kernel-buster.list
  /etc/apt/sources.list.d/stackx-kernel-bullseye.list
  /usr/share/keyrings/stackx-kernel-stretch.gpg
  /usr/share/keyrings/stackx-kernel-buster.gpg
  /usr/share/keyrings/stackx-kernel-bullseye.gpg
)
work=$(mktemp -d /tmp/stackx-kernel-6.12-repo.XXXXXX)
chmod 700 "$work"
trap 'rm -rf "$work"' EXIT
mkdir -m 700 "$work/snap"
for path in "${protected[@]}"; do
  snapshot_one "$path"
done

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 est requis (lu : ${gpg_line:-aucun}). Aucun fichier apt n'est écrit" ;;
esac
mkdir -m 700 "$work/gnupg"
export GNUPGHOME="$work/gnupg"
now_epoch=$(date +%s)
[[ "$now_epoch" =~ ^[0-9]+$ ]] || die "heure illisible"

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
  cat "$work/download.log" >&2
  die "téléchargement de la clé publique impossible. Aucun fichier apt n'est écrit"
fi
[[ -f "$work/key.asc" && ! -L "$work/key.asc" ]] || die "la clé téléchargée n'est pas un fichier régulier"
key_bytes=$(wc -c < "$work/key.asc")
key_bytes=${key_bytes//[[:space:]]/}
[[ "$key_bytes" =~ ^[0-9]+$ ]] && (( key_bytes >= 32 && key_bytes <= 65536 )) || die "taille de clé refusée"
grep -q -- '-----BEGIN PGP PUBLIC KEY BLOCK-----' "$work/key.asc" || die "clé non blindée"
grep -q -- 'PRIVATE KEY' "$work/key.asc" && die "matériel secret refusé"
grep -q -- 'SECRET KEY' "$work/key.asc" && die "matériel secret refusé"
refuse_secret_packets "$work/key.asc" asc || die "clé refusée. Aucun fichier apt n'est écrit"
check_imported_key "$work/gnupg" "$work/gnupg/pubring.gpg" "$work/key.asc" || die "empreinte refusée. Aucun fichier apt n'est écrit"

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" || die "export du trousseau public impossible"
[[ -f "$work/keyring.gpg" && ! -L "$work/keyring.gpg" ]] || die "trousseau exporté illisible"
ring_bytes=$(wc -c < "$work/keyring.gpg")
ring_bytes=${ring_bytes//[[:space:]]/}
[[ "$ring_bytes" =~ ^[0-9]+$ ]] && (( ring_bytes >= 32 && ring_bytes <= 65536 )) || die "taille de trousseau refusée"
refuse_secret_packets "$work/keyring.gpg" ring || die "trousseau refusé"
check_imported_key "$work/gnupg-export" "$work/gnupg-export/pubring.gpg" "$work/keyring.gpg" || die "le trousseau exporté ne repasse pas le contrôle"

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
  cat "$work/inrelease-download.log" >&2
  die "téléchargement d'InRelease impossible. Aucun fichier apt n'est écrit"
fi
[[ -f "$work/InRelease.fresh" && ! -L "$work/InRelease.fresh" ]] || die "InRelease absent"
in_bytes=$(wc -c < "$work/InRelease.fresh")
in_bytes=${in_bytes//[[:space:]]/}
[[ "$in_bytes" =~ ^[0-9]+$ ]] && (( in_bytes >= 1 && in_bytes <= 1048576 )) || die "taille d'InRelease refusée"
grep -q -- '-----BEGIN PGP SIGNED MESSAGE-----' "$work/InRelease.fresh" || die "InRelease non signé en clair"
grep -q -- '-----BEGIN PGP SIGNATURE-----' "$work/InRelease.fresh" || die "signature InRelease absente"
grep -q -- 'PRIVATE KEY' "$work/InRelease.fresh" && die "matériel secret dans InRelease"
mkdir -m 700 "$work/gpgv"
unset GNUPGHOME
gpgv --homedir "$work/gpgv" --keyring "$work/keyring.gpg" "$work/InRelease.fresh" >"$work/gpgv.txt" 2>&1 || fail "gpgv a rejeté l'InRelease frais"
collect_package_hashes "$work/InRelease.fresh" "$work/pkg.sha256"
[[ -s "$work/pkg.sha256" ]] || die "InRelease sans enregistrement Packages"

{
  echo "# Géré par add-stackx-kernel-repo.sh. Dépôt noyau StackX ${DIST_NAME} amd64 6.12."
  echo "# Cette ligne ne choisit pas une entrée de démarrage."
  printf '%s\n' "$source_line"
} >"$work/source.list"
deb_lines=$(grep -c '^deb ' "$work/source.list" || true)
[[ "$deb_lines" -eq 1 ]] || die "une seule ligne deb est attendue"
grep -Eq 'trusted=yes|allow-unauthenticated|AllowInsecure|AllowUnauthenticated|Verify-Peer=false' "$work/source.list" && die "ligne de source affaiblie"

if grep -R -n -F -- "$ORIGIN" /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null | grep -v -F -- "$LIST:" | grep -q .; then
  die "le canal est déjà déclaré dans un autre fichier apt"
fi

[[ -d /usr/share && ! -L /usr/share ]] || die "/usr/share refusé"
if [[ -L /usr/share/keyrings ]]; then
  die "lien /usr/share/keyrings refusé"
fi
if [[ ! -d /usr/share/keyrings ]]; then
  [[ ! -e /usr/share/keyrings ]] || die "/usr/share/keyrings inattendu"
  mkdir -m 755 /usr/share/keyrings
fi
[[ -d /etc/apt && ! -L /etc/apt ]] || die "/etc/apt refusé"
[[ -d /etc/apt/sources.list.d && ! -L /etc/apt/sources.list.d ]] || die "/etc/apt/sources.list.d refusé"

[[ -e "$KEYRING" ]] || created_key=1
[[ -e "$LIST" ]] || created_list=1
install_exact "$KEYRING" "$work/keyring.gpg" || fail "trousseau non installé"
install_exact "$LIST" "$work/source.list" || fail "liste de sources non installée"

update_opts=(
  -o Dir::Etc::sourcelist="sources.list.d/stackx-kernel-6.12-${DIST_CODENAME}.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
)
if [[ "$APT_HAS_ERROR_MODE" == yes ]]; then
  update_opts+=(-o APT::Update::Error-Mode=any)
fi
apt-get update "${update_opts[@]}" || fail "rafraîchissement de la seule source StackX impossible"
cached=$(locate_cached_inrelease) || fail "InRelease de cache absent ou ambigu"
case "$cached" in
  /var/lib/apt/lists/${LIST_PREFIX}InRelease|/var/lib/apt/lists/${LIST_PREFIX}._InRelease) ;;
  *) fail "chemin de cache InRelease inattendu" ;;
esac
cmp -s -- "$work/InRelease.fresh" "$cached" || fail "l'InRelease en cache diffère de la copie fraîche signée"

[[ -d /var/lib/apt/lists && ! -L /var/lib/apt/lists ]] || fail "/var/lib/apt/lists refusé"
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
  [[ -f "$list" && ! -L "$list" && -s "$list" ]] || fail "index Packages illisible : ${list}"
  base=$(basename -- "$list")
  case "$base" in
    *_Packages|*_Packages.gz|*_Packages.xz|*_Packages.bz2|*_Packages.lzma|*_Packages.lz4) ;;
    *) continue ;;
  esac
  "$HELPER" cat-file "$list" >"$work/packages.body" 2>"$work/packages.err" || fail "lecture de ${base} impossible"
  body_bytes=$(wc -c < "$work/packages.body")
  body_bytes=${body_bytes//[[:space:]]/}
  [[ "$body_bytes" =~ ^[0-9]+$ ]] && (( body_bytes >= 1 && body_bytes <= 8388608 )) || fail "taille de ${base} refusée"
  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"
  [[ "$hash_ok" -eq 1 ]] || fail "${base} ne correspond pas à l'InRelease frais"
  grep -E -q '^Package: linux-libc-dev$' "$work/packages.body" && fail "l'index contient linux-libc-dev"
  package_stanza "$work/packages.body" "$META_PKG" "$work/meta.stanza" || fail "${META_PKG} absent de ${base}"
  meta_ver=$(control_field "$work/meta.stanza" Version)
  [[ "$meta_ver" == "$META_VER" ]] || fail "${META_PKG} version ${meta_ver:-absente}, attendu ${META_VER}"
  meta_depends=$(control_field "$work/meta.stanza" Depends)
  depends_exact "$meta_depends" "$IMAGE_PKG" "$META_VER" || fail "${META_PKG} n'a pas la dépendance exacte ${IMAGE_PKG} (= ${META_VER})"
  depends_exact "$meta_depends" "$HEADERS_PKG" "$META_VER" || fail "${META_PKG} n'a pas la dépendance exacte ${HEADERS_PKG} (= ${META_VER})"
  depends_exact "$meta_depends" "$TOOLCHAIN_PKG" "$TOOLCHAIN_VER" || fail "${META_PKG} n'a pas la dépendance exacte ${TOOLCHAIN_PKG} (= ${TOOLCHAIN_VER})"
  field_rejects_libc "$work/meta.stanza" || fail "${META_PKG} cite linux-libc-dev dans une relation"
  package_stanza "$work/packages.body" "$IMAGE_PKG" "$work/image.stanza" || fail "${IMAGE_PKG} absent"
  package_stanza "$work/packages.body" "$HEADERS_PKG" "$work/headers.stanza" || fail "${HEADERS_PKG} absent"
  image_ver=$(control_field "$work/image.stanza" Version)
  headers_ver=$(control_field "$work/headers.stanza" Version)
  [[ "$image_ver" == "$META_VER" ]] || fail "version d'image ${image_ver:-absente}, attendu ${META_VER}"
  [[ "$headers_ver" == "$META_VER" ]] || fail "version d'en-têtes ${headers_ver:-absente}, attendu ${META_VER}"
  image_arch=$(control_field "$work/image.stanza" Architecture)
  headers_arch=$(control_field "$work/headers.stanza" Architecture)
  [[ "$image_arch" == amd64 ]] || fail "image architecture ${image_arch:-absente}"
  [[ "$headers_arch" == amd64 ]] || fail "en-têtes architecture ${headers_arch:-absente}"
  field_rejects_libc "$work/image.stanza" || fail "${IMAGE_PKG} cite linux-libc-dev dans une relation"
  field_rejects_libc "$work/headers.stanza" || fail "${HEADERS_PKG} cite linux-libc-dev dans une relation"
  headers_depends=$(control_field "$work/headers.stanza" Depends)
  depends_exact "$headers_depends" "$TOOLCHAIN_PKG" "$TOOLCHAIN_VER" || fail "${HEADERS_PKG} n'a pas la dépendance exacte ${TOOLCHAIN_PKG} (= ${TOOLCHAIN_VER})"
  meta_arch=$(control_field "$work/meta.stanza" Architecture)
  [[ "$meta_arch" == amd64 ]] || fail "méta architecture ${meta_arch:-absente}"
  meta_sha=$(control_field "$work/meta.stanza" SHA256)
  meta_sha=$(printf '%s' "$meta_sha" | tr '[:upper:]' '[:lower:]')
  [[ "$meta_sha" == "$META_SHA256" ]] || fail "SHA256 de ${META_PKG} différent du lot qualifié"
  meta_file=$(control_field "$work/meta.stanza" Filename)
  [[ "$meta_file" == "$META_DEB" ]] || fail "nom de ${META_PKG} différent du lot qualifié"
  image_sha=$(control_field "$work/image.stanza" SHA256)
  image_sha=$(printf '%s' "$image_sha" | tr '[:upper:]' '[:lower:]')
  [[ "$image_sha" == "$IMAGE_SHA256" ]] || fail "SHA256 de ${IMAGE_PKG} différent du lot qualifié"
  image_file=$(control_field "$work/image.stanza" Filename)
  [[ "$image_file" == "$IMAGE_DEB" ]] || fail "nom de ${IMAGE_PKG} différent du lot qualifié"
  headers_sha=$(control_field "$work/headers.stanza" SHA256)
  headers_sha=$(printf '%s' "$headers_sha" | tr '[:upper:]' '[:lower:]')
  [[ "$headers_sha" == "$HEADERS_SHA256" ]] || fail "SHA256 de ${HEADERS_PKG} différent du lot qualifié"
  headers_file=$(control_field "$work/headers.stanza" Filename)
  [[ "$headers_file" == "$HEADERS_DEB" ]] || fail "nom de ${HEADERS_PKG} différent du lot qualifié"
  package_stanza "$work/packages.body" "$TOOLCHAIN_PKG" "$work/toolchain.stanza" || fail "${TOOLCHAIN_PKG} absent"
  tool_ver=$(control_field "$work/toolchain.stanza" Version)
  [[ "$tool_ver" == "$TOOLCHAIN_VER" ]] || fail "${TOOLCHAIN_PKG} version ${tool_ver:-absente}, attendu ${TOOLCHAIN_VER}"
  tool_arch=$(control_field "$work/toolchain.stanza" Architecture)
  [[ "$tool_arch" == amd64 ]] || fail "toolchain architecture ${tool_arch:-absente}"
  field_rejects_libc "$work/toolchain.stanza" || fail "${TOOLCHAIN_PKG} cite linux-libc-dev dans une relation"
  tool_sha=$(control_field "$work/toolchain.stanza" SHA256)
  tool_sha=$(printf '%s' "$tool_sha" | tr '[:upper:]' '[:lower:]')
  [[ "$tool_sha" == "$TOOLCHAIN_SHA256" ]] || fail "SHA256 de ${TOOLCHAIN_PKG} différent du lot qualifié"
  tool_file=$(control_field "$work/toolchain.stanza" Filename)
  [[ "$tool_file" == "$TOOLCHAIN_DEB" ]] || fail "nom de ${TOOLCHAIN_PKG} différent du lot qualifié"
  pkg_names=$(grep -c '^Package: ' "$work/packages.body" || true)
  [[ "$pkg_names" -eq 4 ]] || fail "l'index ne contient pas exactement quatre paquets"
  meta_n=$(grep -c "^Package: ${META_PKG}$" "$work/packages.body" || true)
  image_n=$(grep -c "^Package: ${IMAGE_PKG}$" "$work/packages.body" || true)
  headers_n=$(grep -c "^Package: ${HEADERS_PKG}$" "$work/packages.body" || true)
  tool_n=$(grep -c "^Package: ${TOOLCHAIN_PKG}$" "$work/packages.body" || true)
  [[ "$meta_n" -eq 1 && "$image_n" -eq 1 && "$headers_n" -eq 1 && "$tool_n" -eq 1 ]] || fail "les quatre paquets qualifiés ne sont pas exactement présents"
  pkg_count=$((pkg_count + 1))
done < "$work/pkg.files"
[[ "$pkg_count" -gt 0 ]] || fail "aucun index Packages pour cette source"

verify_snapshots "${protected[@]}"
echo "Dépôt noyau StackX ${DIST_NAME} configuré."
echo "Clé du dépôt et index APT vérifiés."
echo "Aucun paquet installé, aucun redémarrage, aucun choix de démarrage modifié."
echo
echo "1. Simuler l'installation et examiner les opérations proposées :"
echo "   apt-get --simulate --no-remove install ${META_PKG}"
echo "2. Si la simulation convient, installer explicitement :"
echo "   apt-get --no-remove install ${META_PKG}"
echo
echo "Ces commandes sont affichées uniquement ; le script ne les exécute pas."
echo "Ne pas retirer --no-remove. Ne pas lancer apt-get upgrade, dist-upgrade ni autoremove."
echo "Conserver l'ancien noyau et linux-image-amd64."
echo "Ce script ne change aucune préférence GRUB. Une installation séparée déclenche les hooks Debian et peut placer 6.12 en tête lorsque GRUB_DEFAULT=0, sans modifier GRUB_DEFAULT."
echo "Avant de redémarrer, vérifier le noyau sélectionné dans GRUB (voir README.txt)."
echo "grub-reboot ou next_entry peut persister sur diskfilter ; aucun retour automatique n'est promis."
echo "Après redémarrage : uname -r doit afficher 6.12.115-stackx-stretch-1."
echo "APT signé ne prouve pas Secure Boot."
