#!/bin/sh
# fetch-sources.sh — verify or recover the complete corresponding source for
# Neocodec's FFmpeg/FFprobe 9.0 commercial helper build.
#
# SHA256SUMS is committed provenance and is NEVER generated or overwritten by this script.
# SOURCE-URLS.tsv maps each expected relative path to one HTTPS upstream URL.
#
# Usage:
#   ./fetch-sources.sh                         # verify the populated tree (no network)
#   ./fetch-sources.sh --verify                # same
#   ./fetch-sources.sh --fetch                 # fresh HTTPS fetch, verify all, then promote
#   ./fetch-sources.sh --candidate FILE        # write computed hashes to a separate candidate
#
# --fetch downloads the complete inventory into a fresh temporary tree. Nothing is promoted
# until every expected file is present, no unexpected artifact exists, and every committed
# checksum matches. Initial URLs and redirects are HTTPS-only and TLS 1.2 is the minimum.

set -eu

here=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
checksums="$here/SHA256SUMS"
urls="$here/SOURCE-URLS.tsv"
tab=$(printf '\t')

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

usage() {
  sed -n '4,11p' "$0" >&2
  exit 64
}

case "${TMPDIR:-}" in
  /*) temp_base=${TMPDIR%/} ;;
  *)  temp_base=/tmp ;;
esac
work=$(mktemp -d "$temp_base/neocodec-source-fetch.XXXXXX")
trap 'rm -rf -- "$work"' EXIT HUP INT TERM

url_records="$work/url-records"
url_paths="$work/url-paths"
sum_paths="$work/sum-paths"
: > "$url_records"
: > "$url_paths"
: > "$sum_paths"

safe_path() {
  candidate=$1
  case "$candidate" in
    libs/*)
      base=${candidate#libs/}
      case "$base" in
        ""|*/*|.*|*[!A-Za-z0-9._+-]*) return 1 ;;
      esac
      ;;
    ""|*/*|.*|*[!A-Za-z0-9._+-]*) return 1 ;;
  esac
  return 0
}

validate_manifests() {
  candidate_mode=${1:-0}
  [ -f "$urls" ] || die "missing source URL manifest: $urls"

  while IFS="$tab" read -r url path extra || [ -n "${url}${path}${extra}" ]; do
    case "$url" in ""|\#*) continue ;; esac
    [ -n "$path" ] && [ -z "$extra" ] ||
      die "SOURCE-URLS.tsv must contain exactly URL<TAB>relative-path"
    case "$url" in
      https://*) ;;
      *) die "insecure or unsupported source URL: $url" ;;
    esac
    safe_path "$path" || die "unsafe source destination: $path"
    if grep -Fqx -- "$path" "$url_paths"; then
      die "duplicate source destination: $path"
    fi
    printf '%s\n' "$path" >> "$url_paths"
    printf '%s\t%s\n' "$url" "$path" >> "$url_records"
  done < "$urls"
  [ -s "$url_records" ] || die "source URL manifest is empty"
  LC_ALL=C sort "$url_paths" > "$work/url-paths.sorted"

  if [ "$candidate_mode" = "1" ]; then
    expected_paths="$work/url-paths.sorted"
    return
  fi

  [ -f "$checksums" ] || die "missing committed checksum manifest: $checksums"
  while IFS= read -r line || [ -n "$line" ]; do
    [ -n "$line" ] || die "blank line in SHA256SUMS"
    hash=${line%%  *}
    path=${line#*  }
    [ "$line" = "$hash  $path" ] ||
      die "invalid SHA256SUMS record (expected HASH<two spaces>PATH)"
    [ "${#hash}" -eq 64 ] || die "invalid SHA-256 length for $path"
    case "$hash" in *[!0-9a-f]*) die "invalid lowercase SHA-256 for $path" ;; esac
    safe_path "$path" || die "unsafe checksum destination: $path"
    if grep -Fqx -- "$path" "$sum_paths"; then
      die "duplicate checksum destination: $path"
    fi
    printf '%s\n' "$path" >> "$sum_paths"
  done < "$checksums"
  [ -s "$sum_paths" ] || die "checksum manifest is empty"

  LC_ALL=C sort "$sum_paths" > "$work/sum-paths.sorted"
  if ! cmp -s "$work/url-paths.sorted" "$work/sum-paths.sorted"; then
    echo "ERROR: SOURCE-URLS.tsv and SHA256SUMS describe different path sets" >&2
    diff -u "$work/sum-paths.sorted" "$work/url-paths.sorted" >&2 || true
    exit 1
  fi
  expected_paths="$work/sum-paths.sorted"
}

hash_file() {
  if command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  elif command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  else
    die "neither shasum nor sha256sum is available"
  fi
}

expected_hash() {
  awk -v wanted="$1" '$2 == wanted { print $1; found = 1; exit }
    END { if (!found) exit 1 }' "$checksums"
}

actual_artifact_paths() {
  root=$1
  if [ -d "$root/libs" ]; then
    find "$root/libs" ! -type d -print | while IFS= read -r path; do
      printf 'libs/%s\n' "${path##*/}"
    done
  fi
  for path in "$root"/*; do
    [ -e "$path" ] || [ -L "$path" ] || continue
    [ -d "$path" ] && continue
    name=${path##*/}
    case "$name" in
      *.tar|*.tar.*|*.tgz|*.zip|*.part) printf '%s\n' "$name" ;;
    esac
  done
}

refuse_extra_artifacts() {
  root=$1
  actual="$work/actual.$(basename "$root")"
  actual_artifact_paths "$root" | LC_ALL=C sort -u > "$actual"
  extras=$(comm -23 "$actual" "$expected_paths")
  [ -z "$extras" ] || die "unexpected source artifact(s) under $root:
$extras"
}

verify_tree() {
  root=$1
  refuse_extra_artifacts "$root"
  while IFS= read -r line || [ -n "$line" ]; do
    expected=${line%%  *}
    path=${line#*  }
    file="$root/$path"
    [ -f "$file" ] && [ ! -L "$file" ] || die "missing/non-regular source artifact: $path"
    actual=$(hash_file "$file")
    [ "$actual" = "$expected" ] ||
      die "checksum mismatch for $path (expected $expected, got $actual)"
  done < "$checksums"
}

fetch_all() {
  refuse_extra_artifacts "$here"
  stage="$work/stage"
  mkdir -p "$stage/libs"

  while IFS="$tab" read -r url path; do
    destination="$stage/$path"
    mkdir -p "$(dirname "$destination")"
    echo "get: $path"
    curl --disable --fail --location \
      --proto '=https' --proto-redir '=https' --tlsv1.2 \
      --retry 3 --connect-timeout 30 --max-redirs 10 \
      --output "$destination.part" "$url"
    [ -f "$destination.part" ] && [ ! -L "$destination.part" ] ||
      die "download did not produce a regular file: $path"
    expected=$(expected_hash "$path")
    actual=$(hash_file "$destination.part")
    [ "$actual" = "$expected" ] ||
      die "download checksum mismatch for $path (expected $expected, got $actual)"
    mv "$destination.part" "$destination"
  done < "$url_records"

  verify_tree "$stage"
  while IFS="$tab" read -r _url path; do
    mkdir -p "$(dirname "$here/$path")"
    mv "$stage/$path" "$here/$path"
  done < "$url_records"
  verify_tree "$here"
  echo "verified and promoted $(wc -l < "$url_paths" | tr -d ' ') source artifacts"
}

write_candidate() {
  output=$1
  output_dir=$(CDPATH= cd -- "$(dirname "$output")" && pwd) ||
    die "candidate output directory does not exist"
  output="$output_dir/$(basename "$output")"
  [ "$output" != "$checksums" ] ||
    die "refusing to overwrite committed SHA256SUMS; choose a separate candidate path"

  refuse_extra_artifacts "$here"
  candidate="$work/SHA256SUMS.candidate"
  : > "$candidate"
  while IFS= read -r path; do
    file="$here/$path"
    [ -f "$file" ] && [ ! -L "$file" ] || die "missing/non-regular source artifact: $path"
    printf '%s  %s\n' "$(hash_file "$file")" "$path" >> "$candidate"
  done < "$work/url-paths.sorted"
  mv "$candidate" "$output"
  echo "wrote candidate checksum manifest: $output"
}

case "$#" in
  0)
    validate_manifests
    verify_tree "$here"
    echo "verified $(wc -l < "$sum_paths" | tr -d ' ') source artifacts"
    ;;
  1)
    case "$1" in
      --verify)
        validate_manifests
        verify_tree "$here"
        echo "verified $(wc -l < "$sum_paths" | tr -d ' ') source artifacts"
        ;;
      --fetch)
        validate_manifests
        fetch_all
        ;;
      --help|-h) usage ;;
      *) usage ;;
    esac
    ;;
  2)
    [ "$1" = "--candidate" ] || usage
    validate_manifests 1
    write_candidate "$2"
    ;;
  *) usage ;;
esac
