#!/bin/sh
# Copyright (c) 2017, 2021 Jonas 'Sortie' Termansen.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# tix-fetch
# Download operating system files.

set -e

boot=false
collection=""
continue=""
execpatch=false
initrd=false
insecure_downgrade_to_http=false
insecure_no_check_certificate=false
input_release_file=
input_release_sig_file=
input_sha256sum=
normalize=false
output=""
outputdir=""
output_release_file= # TODO: A better term for this?
output_release_sig_file= # TODO: A better term for this?
output_sha256sum=
patch=false
port=false
porttix=false
release=false
sha256=false
sha256sum=false
source=false
source_full=false
srctix=false
sysroot=""
toolchain=false
url=false
url_master=false
url_master_release=false
url_mirror=false
url_mirror_release=false
url_release_sig=false
url_sha256sum=false
# TODO: Option to select this default:
# TODO: This hides errors. Fix wget so it has a quiet, but errors, mode.
wget_options="-q --show-progress"

# TODO: Ability to get source code easily for gcc/binutils/libstdc++.

dashdash=
previous_option=
for argument do
  if test -n "$previous_option"; then
    eval $previous_option=\$argument
    previous_option=
    shift
    continue
  fi

  case $argument in
  *=?*) parameter=$(expr "X$argument" : '[^=]*=\(.*\)' || true) ;;
  *=)   parameter= ;;
  *)    parameter=yes ;;
  esac

  case $dashdash$argument in
  --) dashdash=yes ;;
  -c) continue="-c" ;;
  # TODO: Support -ofoo
  -o) previous_option=output ;;
  -O) previous_option=outputdir ;;
  -q) wget_options="-q" ;;
  -v) wget_options="-v" ;;
  --boot) boot=true ;;
  --collection=*) collection=$parameter ;;
  --collection) previous_option=collection ;;
  --continue) continue="--continue" ;;
  --download-non-verbose) previous_option="-nv" ;;
  --download-non-verbose) previous_option="-v" ;;
  --download-quiet) previous_option="-q" ;;
  --download-verbose) previous_option="-v" ;;
  --execpatch) execpatch=true ;;
  --initrd) initrd=true ;;
  --input-release-file=*) input_release_file=$parameter ;;
  --insecure-downgrade-to-http) insecure_downgrade_to_http=true ;;
  --insecure-no-check-certificate) insecure_no_check_certificate=true ;;
  --input-release-file) previous_option=input_release_file ;;
  --input-release-sig-file=*) input_release_sig_file=$parameter ;;
  --input-release-sig-file) previous_option=input_release_sig_file ;;
  --input-sha256sum=*) input_sha256sum=$parameter ;;
  --input-sha256sum) previous_option=input_sha256sum ;;
  --normalize) normalize=true ;;
  --nv) wget_options="-nv" ;;
  --outputdir=*) outputdir=$parameter ;;
  --outputdir) previous_option=outputdir ;;
  --output=*) output=$parameter ;;
  --output) previous_option=output ;;
  --output-release-file=*) output_release_file=$parameter ;;
  --output-release-file) previous_option=output_release_file ;;
  --output-release-sig-file=*) output_release_sig_file=$parameter ;;
  --output-release-sig-file) previous_option=output_release_sig_file ;;
  --output-sha256sum=*) output_sha256sum=$parameter ;;
  --output-sha256sum) previous_option=output_sha256sum ;;
  --patch) patch=true ;;
  --port) port=true ;;
  --porttix) porttix=true ;;
  --release) release=true ;;
  --sha256) sha256=true ;;
  --sha256sum) sha256sum=true ;;
  --source-full) source_full=true ;;
  --source) source=true ;;
  --srctix) srctix=true ;;
  --sysroot) previous_option=sysroot ;;
  --sysroot=*) sysroot=$parameter ;;
  --toolchain) toolchain=true ;;
  --url) url=true ;;
  --url-master) url_master=true ;;
  --url-mirror) url_mirror=true ;;
  --url-master-release) url_master_release=true ;;
  --url-mirror-release) url_mirror_release=true ;;
  --url-release-sig) url_release_sig=true ;;
  --url-sha256sum) url_sha256sum=true ;;
  --wget-options) previous_option=wget_options ;;
  --wget-options=*) wget_options=$parameter ;;
  -*) echo "$0: unrecognized option $argument" >&2
      exit 1 ;;
  *) break ;;
  esac

  shift
done

if test -n "$previous_option"; then
  echo "$0: option '$argument' requires an argument" >&2
  exit 1
fi

# TODO: Mutually incompatible options.

conf() {
  grep -E "^$1[[:space:]]*=" -- "$collection/etc/upgrade.conf" |
  tail -n 1 |
  sed -E 's/^[^=]*=[[:space:]]*(|.*[^[:space:]])[[:space:]]*/\1/'
}

tmpdir=$(mktemp -dt tix-fetch-port.XXXXXX)
trap 'rm -rf -- "$tmpdir"' EXIT HUP INT QUIT TERM

RELEASE_KEY=$(conf release_key)
RELEASE_SIG_URL=$(conf release_sig_url)
PREFERRED_MIRROR=$(conf mirror)
FORCE_MIRROR=$(conf force_mirror)
USER_AGENT="$(uname -s)/$(uname -r) ($(uname -m); $(uname -v))"

if $insecure_no_check_certificate; then
  echo "$0: warning: insecurely not checking https certificates" >&2
  wget_options="$wget_options --no-check-certificate"
fi

if $insecure_downgrade_to_http; then
  echo "$0: warning: insecurely downloading without https" >&2
  RELEASE_SIG_URL="$(echo "$RELEASE_SIG_URL" | sed -E 's,^https:,http:,')"
fi

if $url_release_sig; then
  printf "%s\n" "$RELEASE_SIG_URL"
  exit
fi

# HACK: Provide more useful errors when wget is silent:
do_wget() {
  (set +e
   wget "$@"
   status=$?
   set -e
   what=
   case $status in
   0) exit 0 ;;
   1) what="Generic error" ;;
   2) what="Parse error" ;;
   3) what="File I/O error" ;;
   4) what="Network I/O error" ;;
   5) what="Transport Layer Security verification failure" ;;
   6) what="Username/password failure" ;;
   7) what="Protocol error" ;;
   8) what="Error response" ;;
   *) what="Exit code $status" ;;
   esac
   echo "$0: $what when running: wget $@" >&2
   exit $status)
}

# Fetch signed release description.
if [ -z "$input_release_file" ]; then
  if [ -z "$input_release_sig_file" ]; then
    (cd "$tmpdir" &&
     do_wget -U "$USER_AGENT" $wget_options -O release.sh.sig \
             -- "$RELEASE_SIG_URL")
  else
    cp -T -- "$input_release_sig_file" "$tmpdir/release.sh.sig"
  fi
  signify -Vq -p "$RELEASE_KEY" -em "$tmpdir/release.sh"
else
  cp -T -- "$input_release_file" "$tmpdir/release.sh"
fi

# Store the signed release file if requested.
if [ -n "$output_release_sig_file" ]; then
  cp -T -- "$tmpdir/release.sh.sig" "$output_release_sig_file"
fi

# Store the release file (without signature) if requested.
if [ -n "$output_release_file" ]; then
  cp -T -- "$tmpdir/release.sh" "$output_release_file"
fi

# Load the release description.
# TODO: SECURITY: REMOTE CODE EXECUTION OF SIGNED REMOTE CODE.
# TODO: SECURITY: Protect against responding with older release.sh.
. "`realpath -- "$tmpdir/release.sh"`" # Avoid PATH search with absolute path.

if $url_master; then
  printf "%s\n" "$MASTER"
  exit
elif $url_master_release; then
  printf "%s\n" "$MASTER/$RELEASE"
  exit
fi

# Default to the master mirror but switch to the preferred mirror if the release
# description knows about the mirror and believes it to be trustworthy.
MIRROR="$MASTER"
for POTENTIAL_MIRROR in $MIRRORS; do
  if [ "$POTENTIAL_MIRROR" = "$PREFERRED_MIRROR" ]; then
    MIRROR="$PREFERRED_MIRROR"
  fi
done
if [ -n "$PREFERRED_MIRROR" ] && [ "$MIRROR" != "$PREFERRED_MIRROR" ]; then
  if [ "$FORCE_MIRROR" = yes ]; then
    MIRROR="$PREFERRED_MIRROR"
  else
    echo "$0: warning: ignoring unsupported mirror $PREFERRED_MIRROR" >&2
  fi
fi

if $insecure_downgrade_to_http; then
  MIRROR="$(echo "$MIRROR" | sed -E 's,^https:,http:,')"
fi

if $url_mirror; then
  printf "%s\n" "$MIRROR"
  exit
elif $url_mirror_release; then
  printf "%s\n" "$MIRROR/$RELEASE"
  exit
fi

RELEASE_URL="$MIRROR/$RELEASE"

# Fetch sha256sum file and check its SHA256 hash with the release description.
if $url_sha256sum; then
  printf "%s\n" "$RELEASE_URL/$SHA256SUM_FILE"
  exit
fi
if [ -z "$input_sha256sum" ]; then
  # TODO: If the mirror doesn't work, try the master.
  (cd "$tmpdir" &&
   do_wget -U "$USER_AGENT" $wget_options -O sha256sum \
           -- "$RELEASE_URL/$SHA256SUM_FILE")
else
  cp -T -- "$input_sha256sum" "$tmpdir/sha256sum"
fi
# TODO: Check if upstream release description changed, if so, start over.
echo "$SHA256SUM_SHA256SUM  $tmpdir/sha256sum" | sha256sum -cq

# Store the sha256sum file if requested.
if [ -n "$output_sha256sum" ]; then
  cp -T -- "$tmpdir/sha256sum" "$output_sha256sum"
fi

escape_extended_regex() {
  printf "%s\n" "$1" | sed -E -e 's/[[$()*?\+.^{|}]/\\\0/g'
}

# TODO: Remove:
#escape_extended_regex_test_self() {
#  printf "%s\n" "$1" | grep -E "^$(escape_extended_regex "$1")\$"
#}

request() {
  REQUEST="$1"
  REQUESTDIR="$2"
  REQUESTFINAL="${3-$1}"
  FULLREQUEST="$REQUESTDIR$REQUEST"

  if $url; then
    printf '%s\n' "$RELEASE_URL/$FULLREQUEST"
    return
  fi

  if $sha256 || $sha256sum; then
    set +e # Don't fail if grep exits 1 (no match).
    # TODO: Should this be a checksum(1) feature to look up a hash?
    grep -E "^[0-9a-fA-F]{64}  $(escape_extended_regex "$FULLREQUEST")$" \
            "$tmpdir/sha256sum" > "$tmpdir/match"
    EXITCODE=$?
    set -e
    if [ 2 -le "$EXITCODE" ]; then (exit $EXITCODE); fi
    if $sha256 && [ -s "$tmpdir/match" ]; then
      grep -Eo '^[0-9a-fA-F]{64}' "$tmpdir/match"
    fi
    if $sha256sum && [ -s "$tmpdir/match" ]; then
      cat "$tmpdir/match"
    fi
    return
  fi

  # Decide the final location the file will end up.
  if [ -n "$output" ]; then
    FINAL="$output"
    OUTPUTDIR=$(dirname -- "$output")
  elif [ -n "$outputdir" ]; then
    FINAL="$outputdir/$REQUESTFINAL"
    OUTPUTDIR="$outputdir"
  else
    FINAL="$REQUESTFINAL"
    OUTPUTDIR=.
  fi

  # If a resumable download, store the file directly to the destination path.
  # Otherwise download to a temporary directory and move only to the final
  # location if the cryptographic check is passed.
  if [ -n "$continue" ]; then
    DOWNLOADDIR="$OUTPUTDIR"
    OUTPUT="$FINAL"
  else
    DOWNLOADDIR="$tmpdir/download"
    OUTPUT="$DOWNLOADDIR/$REQUEST"
    mkdir -p -- "$DOWNLOADDIR"
  fi

  # Fetch the file.
  # TODO: If the mirror doesn't work, try the master.
  (cd "$DOWNLOADDIR" &&
   mkdir -p -- "$(dirname -- "$REQUEST")" &&
   do_wget -U "$USER_AGENT" $wget_options $continue -O "$REQUEST" \
           -- "$RELEASE_URL/$FULLREQUEST")

  # Verify the cryptographic integrity of the fetched file.
  ABSOLUTE_OUTPUT=$(realpath -- "$OUTPUT")
  mkdir -p -- "$tmpdir/check"
  (cd "$tmpdir/check" &&
   mkdir -p -- "$(dirname -- "$FULLREQUEST")"
   ln -s -- "$ABSOLUTE_OUTPUT" "$FULLREQUEST"
   if ! sha256sum -q -C "$tmpdir/sha256sum" -- "$FULLREQUEST"; then
    # Don't leave behind a file that didn't pass a cryptographic check.
    if [ -n "$continue" ]; then
      # TODO: Check if upstream release description changed, if so, start over.
      echo "error: Deleting corrupted output file: $OUTPUT" 2>&1
      rm -f -- "$OUTPUT"
    fi
    exit 1
   fi)
  rm -rf -- "$tmpdir/check"

  # Move the file to the final destination if not already.
  if [ -z "$continue" ]; then
    if [ -z "$output" ]; then
      (cd "$OUTPUTDIR" && mkdir -p -- "$(dirname -- "$REQUESTFINAL")")
    fi
    cp -T -- "$OUTPUT" "$FINAL"
    rm -rf -- "$tmpdir/download"
  fi
}

if $release; then
  request "$BUILD_FILE" "" "$(basename -- "$BUILD_FILE")"
fi

# TODO: --source, --source-full
# TODO: --binutils, --gcc, --libstdc++

# Fetch each of the specified signed files from the mirror.
for REQUEST; do
  # If --port then fetch the port by the requested name.
  if $port; then
    REQUEST="$REQUEST.tix.tar.xz"
    REQUESTDIR="repository/$MACHINE-sortix/"
  elif $boot; then
    REQUEST="$REQUEST"
    REQUESTDIR="$MACHINE/boot/"
  elif $initrd; then
    REQUEST="$REQUEST.initrd.xz"
    REQUESTDIR="$MACHINE/boot/"
  elif $patch; then
    REQUEST="$REQUEST.patch"
    REQUESTDIR="patches/"
  elif $normalize; then
    REQUEST="$REQUEST.normalize"
    REQUESTDIR="patches/"
  elif $srctix; then
    REQUEST="$REQUEST.srctix.tar.xz"
    REQUESTDIR="srctix/"
  elif $porttix; then
    REQUEST="$REQUEST.porttix.tar.xz"
    REQUESTDIR="porttix/"
  elif $toolchain; then
    REQUESTDIR="toolchain/"
  else
    REQUESTDIR=""
  fi
  request "$REQUEST" "$REQUESTDIR"
done
