#!/bin/sh
# shellcheck shell=dash

# If you need an offline install, or you'd prefer to run the binary directly, head to
# https://git.lix.systems/lix-project/lix-installer/releases then pick the version and platform
# most appropriate for your deployment target.
#
# This is just a little script that selects and downloads the right `lix-installer`. It does
# platform detection, downloads the installer, and runs it; that's it.
#
# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local`
# extension. Note: Most shells limit `local` to 1 var per line, contra bash.

# This script is based off https://github.com/rust-lang/rustup/blob/2d429e8e172d5854b6dd7244ecbb0dc3da88a678/rustup-init.sh

# Some versions of ksh have no `local` keyword. Alias it to `typeset`, but
# beware this makes variables global with f()-style function syntax in ksh93.
# mksh has this alias by default.
has_local() {
  # shellcheck disable=SC2034  # deliberately unused
  local _has_local
}

has_local 2>/dev/null || alias local=typeset

is_zsh() {
  [ -n "${ZSH_VERSION-}" ]
}

set -u

# If NIX_INSTALLER_FORCE_ALLOW_HTTP is unset or empty, default it.
NIX_INSTALLER_BINARY_ROOT="${NIX_INSTALLER_BINARY_ROOT:-https://install.lix.systems/lix}"

main() {
  downloader --check
  need_cmd uname
  need_cmd mktemp
  need_cmd chmod
  need_cmd mkdir
  need_cmd rm
  need_cmd rmdir

  get_architecture || return 1
  local _arch="$RETVAL"
  assert_nz "$_arch" "arch"

  local _ext=""
  case "$_arch" in
  *windows*)
    _ext=".exe"
    ;;
  esac

  local _dir
  if ! _dir="$(ensure mktemp -d)"; then
    # Because the previous command ran in a subshell, we must manually
    # propagate exit status.
    exit 1
  fi
  local _file="${_dir}/lix-installer${_ext}"

  local _ansi_escapes_are_valid=false
  if [ -t 2 ]; then
    if [ "${TERM+set}" = 'set' ]; then
      case "$TERM" in
      xterm* | rxvt* | urxvt* | linux* | vt*)
        _ansi_escapes_are_valid=true
        ;;
      esac
    fi
  fi

  # check if we have to use /dev/tty to prompt the user
  local need_tty=yes
  for arg in "$@"; do
    case "$arg" in
    --no-confirm)
      need_tty=no
      ;;
    *)
      continue
      ;;
    esac
  done
  if [ "${NIX_INSTALLER_NO_CONFIRM-}" ]; then
    need_tty=no
  fi

  # Handle legacy installs.
  local _url="${NIX_INSTALLER_OVERRIDE_URL-${NIX_INSTALLER_BINARY_ROOT}/lix-installer-${_arch}${_ext}}"
  if [ "$_arch" = "x86_64-darwin" ]; then
    if [ "${NIX_INSTALLER_USE_LEGACY_INTEL_MAC_VERSION:-0}" -eq 0 ]; then
      err "Lix no longer supports Intel macs!" 1>&2
      echo 1>&2
      echo "You can use an older/unsupported installer by setting the environment variable" 1>&2
      echo "'NIX_INSTALLER_USE_LEGACY_INTEL_MAC_VERSION' to 1." 1>&2
      exit 1
    else
      local _last_version="2.94.0"
      warn "Lix no longer supports Intel macs! Using the older ${_last_version} installer and creating an unsupported install. " 1>&2
      _url="${NIX_INSTALLER_OVERRIDE_URL-${NIX_INSTALLER_BINARY_ROOT}/${_last_version}/lix-installer-${_arch}${_ext}}"
    fi
  fi

  say 'downloading installer'

  ensure mkdir -p "$_dir"
  ensure downloader "$_url" "$_file" "$_arch"
  ensure chmod u+x "$_file"
  if [ ! -x "$_file" ]; then
    err "Cannot execute $_file (likely because of mounting /tmp as noexec)." 1>&2
    err "Please copy the file to a location where you can execute binaries and run ./lix-installer${_ext}." 1>&2
    exit 1
  fi

  if [ "$need_tty" = "yes" ] && [ ! -t 0 ]; then
    # The installer is going to want to ask for confirmation by
    # reading stdin.  This script was piped into `sh` though and
    # doesn't have stdin to pass to its children. Instead we're going
    # to explicitly connect /dev/tty to the installer's stdin.
    if [ ! -t 1 ]; then
      err "Unable to run interactively. Run with --no-confirm to accept defaults, --help for additional options"
      exit 1
    fi

    ignore "$_file" "$@" </dev/tty
  else
    ignore "$_file" "$@"
  fi

  local _retval=$?

  ignore rm "$_file"
  ignore rmdir "$_dir"

  return "$_retval"
}

get_current_exe() {
  # Returns the executable used for system architecture detection
  # This is only run on Linux
  local _current_exe
  if test -L /proc/self/exe; then
    _current_exe=/proc/self/exe
  else
    warn "Unable to find /proc/self/exe. System architecture detection might be inaccurate."
    if test -n "$SHELL"; then
      _current_exe=$SHELL
    else
      need_cmd /bin/sh
      _current_exe=/bin/sh
    fi
    warn "Falling back to $_current_exe."
  fi
  echo "$_current_exe"
}

get_architecture() {
  local _ostype _cputype _arch
  _ostype="$(uname -s)"
  _cputype="$(uname -m)"

  if [ "$_ostype" = Linux ]; then
    if [ "$(uname -o)" = Android ]; then
      _ostype=Android
    fi
  fi

  if [ "$_ostype" = Darwin ]; then
    # Darwin `uname -m` can lie due to Rosetta shenanigans. If you manage to
    # invoke a native shell binary and then a native uname binary, you can
    # get the real answer, but that's hard to ensure, so instead we use
    # `sysctl` (which doesn't lie) to check for the actual architecture.
    if [ "$_cputype" = i386 ]; then
      # Handling i386 compatibility mode in older macOS versions (<10.15)
      # running on x86_64-based Macs.
      # Starting from 10.15, macOS explicitly bans all i386 binaries from running.
      # See: <https://support.apple.com/en-us/HT208436>

      # Avoid `sysctl: unknown oid` stderr output and/or non-zero exit code.
      if (sysctl hw.optional.x86_64 2>/dev/null || true) | grep -q ': 1'; then
        _cputype=x86_64
      fi
    elif [ "$_cputype" = x86_64 ]; then
      # Handling x86-64 compatibility mode (a.k.a. Rosetta 2)
      # in newer macOS versions (>=11) running on arm64-based Macs.
      # Rosetta 2 is built exclusively for x86-64 and cannot run i386 binaries.

      # Avoid `sysctl: unknown oid` stderr output and/or non-zero exit code.
      if (sysctl hw.optional.arm64 2>/dev/null || true) | grep -q ': 1'; then
        _cputype=arm64
      fi
    fi
  fi

  if [ "$_ostype" = SunOS ]; then
    # Both Solaris and illumos presently announce as "SunOS" in "uname -s"
    # so use "uname -o" to disambiguate.  We use the full path to the
    # system uname in case the user has coreutils uname first in PATH,
    # which has historically sometimes printed the wrong value here.
    if [ "$(/usr/bin/uname -o)" = illumos ]; then
      _ostype=illumos
    fi

    # illumos systems have multi-arch userlands, and "uname -m" reports the
    # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86
    # systems.  Check for the native (widest) instruction set on the
    # running kernel:
    if [ "$_cputype" = i86pc ]; then
      _cputype="$(isainfo -n)"
    fi
  fi

  local _current_exe
  case "$_ostype" in
  Linux)
    _current_exe=$(get_current_exe)
    _ostype=linux
    ;;

  Darwin)
    _ostype=darwin
    ;;

  *)
    err "unrecognized OS type: $_ostype"
    exit 1
    ;;

  esac

  case "$_cputype" in
  aarch64 | arm64)
    _cputype=aarch64
    ;;

  x86_64 | x86-64 | x64 | amd64)
    _cputype=x86_64
    ;;

  *)
    err "unknown CPU type: $_cputype"
    ;;

  esac

  _arch="${_cputype}-${_ostype}"

  RETVAL="$_arch"
}

__print() {
  if $_ansi_escapes_are_valid; then
    printf '\33[1m%s:\33[0m %s\n' "$1" "$2" >&2
  else
    printf '%s: %s\n' "$1" "$2" >&2
  fi
}

warn() {
  __print 'warn' "$1" >&2
}

say() {
  __print 'info' "$1" >&2
}

# NOTE: you are required to exit yourself
# we don't do it here because of multiline errors
err() {
  __print 'error' "$1" >&2
}

need_cmd() {
  if ! check_cmd "$1"; then
    err "need '$1' (command not found)"
    exit 1
  fi
}

check_cmd() {
  command -v "$1" >/dev/null 2>&1
}

assert_nz() {
  if [ -z "$1" ]; then
    err "assert_nz $2"
    exit 1
  fi
}

# Run a command that should never fail. If the command fails execution
# will immediately terminate with an error showing the failing
# command.
ensure() {
  if ! "$@"; then
    err "command failed: $*"
    exit 1
  fi
}

# This is just for indicating that commands' results are being
# intentionally ignored. Usually, because it's being executed
# as part of error handling.
ignore() {
  "$@"
}

# This wraps curl or wget. Try curl first, if not installed,
# use wget instead.
downloader() {
  # zsh does not split words by default, Required for curl retry arguments below.
  is_zsh && setopt local_options shwordsplit

  local _dld
  local _ciphersuites
  local _err
  local _status
  local _retry
  if check_cmd curl; then
    # Check if we have a broken snap curl
    # https://github.com/boukendesho/curl-snap/issues/1
    _curl_path=$(command -v curl)
    if echo "$_curl_path" | grep "/snap/" >/dev/null 2>&1; then
      if check_cmd wget; then
        _dld=wget
      else
        err "curl installed with snap cannot be used to install Rust"
        err "due to missing permissions. Please uninstall it and"
        err "reinstall curl with a different package manager (e.g., apt)."
        err "See https://github.com/boukendesho/curl-snap/issues/1"
        exit 1
      fi
    else
      _dld=curl
    fi
  elif check_cmd wget; then
    _dld=wget
  else
    _dld='curl or wget' # to be used in error message of need_cmd
  fi

  if [ "$1" = --check ]; then
    need_cmd "$_dld"
  elif [ "$_dld" = curl ]; then
    check_curl_for_retry_support
    _retry="$RETVAL"
    get_ciphersuites_for_curl
    _ciphersuites="$RETVAL"
    if [ -n "$_ciphersuites" ]; then
      # shellcheck disable=SC2086
      _err=$(curl $_retry --proto '=https' --tlsv1.2 --ciphers "$_ciphersuites" --silent --show-error --fail --location "$1" --output "$2" 2>&1)
      _status=$?
    else
      warn "Not enforcing strong cipher suites for TLS, this is potentially less secure"
      if ! check_help_for "$3" curl --proto --tlsv1.2; then
        warn "Not enforcing TLS v1.2, this is potentially less secure"
        # shellcheck disable=SC2086
        _err=$(curl $_retry --silent --show-error --fail --location "$1" --output "$2" 2>&1)
        _status=$?
      else
        # shellcheck disable=SC2086
        _err=$(curl $_retry --proto '=https' --tlsv1.2 --silent --show-error --fail --location "$1" --output "$2" 2>&1)
        _status=$?
      fi
    fi
    if [ -n "$_err" ]; then
      warn "$_err"
      if echo "$_err" | grep -q 404$; then
        err "installer for platform '$3' not found, this may be unsupported"
        exit 1
      fi
    fi
    return $_status
  elif [ "$_dld" = wget ]; then
    if [ "$(wget -V 2>&1 | head -2 | tail -1 | cut -f1 -d" ")" = "BusyBox" ]; then
      warn "using the BusyBox version of wget.  Not enforcing strong cipher suites for TLS or TLS v1.2, this is potentially less secure"
      _err=$(wget "$1" -O "$2" 2>&1)
      _status=$?
    else
      get_ciphersuites_for_wget
      _ciphersuites="$RETVAL"
      if [ -n "$_ciphersuites" ]; then
        _err=$(wget --https-only --secure-protocol=TLSv1_2 --ciphers "$_ciphersuites" "$1" -O "$2" 2>&1)
        _status=$?
      else
        warn "Not enforcing strong cipher suites for TLS, this is potentially less secure"
        if ! check_help_for "$3" wget --https-only --secure-protocol; then
          warn "Not enforcing TLS v1.2, this is potentially less secure"
          _err=$(wget "$1" -O "$2" 2>&1)
          _status=$?
        else
          _err=$(wget --https-only --secure-protocol=TLSv1_2 "$1" -O "$2" 2>&1)
          _status=$?
        fi
      fi
    fi
    if [ -n "$_err" ]; then
      warn "$_err"
      if echo "$_err" | grep -q ' 404 Not Found$'; then
        err "installer for platform '$3' not found, this may be unsupported"
        exit 1
      fi
    fi
    return $_status
  else
    err "Unknown downloader" # should not reach here
    exit 1
  fi
}

check_help_for() {
  local _arch
  local _cmd
  local _arg
  _arch="$1"
  shift
  _cmd="$1"
  shift

  local _category
  if "$_cmd" --help | grep -q '"--help all"'; then
    _category="all"
  else
    _category=""
  fi

  case "$_arch" in

  *darwin*)
    if check_cmd sw_vers; then
      local _os_version
      local _os_major
      _os_version=$(sw_vers -productVersion)
      _os_major=$(echo "$_os_version" | cut -d. -f1)
      case $_os_major in
      10)
        # If we're running on macOS, older than 10.13, then we always
        # fail to find these options to force fallback
        if [ "$(echo "$_os_version" | cut -d. -f2)" -lt 13 ]; then
          # Older than 10.13
          warn "Detected macOS platform older than 10.13"
          return 1
        fi
        ;;
      *)
        if ! { [ "$_os_major" -eq "$_os_major" ] 2>/dev/null && [ "$_os_major" -ge 11 ]; }; then
          # Unknown product version, warn and continue
          warn "Detected unknown macOS major version: $_os_version"
          warn "TLS capabilities detection may fail"
        fi
        ;; # We assume that macOS v11+ will always be okay.
      esac
    fi
    ;;

  esac

  for _arg in "$@"; do
    if ! "$_cmd" --help "$_category" | grep -q -- "$_arg"; then
      return 1
    fi
  done

  true # not strictly needed
}

# Check if curl supports the --retry flag, then pass it to the curl invocation.
# Note that --speed-limit and --speed-time were in the very first commit of curl.
# So this should be pretty much ubiquitously safe.
check_curl_for_retry_support() {
  local _retry_part=""
  local _continue_part=""
  local _speed_limit_part=""

  # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
  if check_help_for "notspecified" "curl" "--retry"; then
    _retry_part="--retry 3"

    if check_help_for "notspecified" "curl" "--continue-at"; then
      # "-C -" tells curl to automatically find where to resume the download when retrying.
      _continue_part="--continue-at -"
    fi

    if check_help_for "notspecified" "curl" "--speed-limit" &&
      check_help_for "notspecified" "curl" "--speed-time"; then
      # 250000 is approximately 20% of the bandwidth of typical DSL
      # these limits mean users below these limits will see failures.
      _speed_limit_part="--speed-limit 250000 --speed-time 15"
    fi
  fi

  RETVAL="$_retry_part $_continue_part $_speed_limit_part"
}

# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites
# if support by local tools is detected. Detection currently supports these curl backends:
# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty.
get_ciphersuites_for_curl() {
  if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then
    # user specified custom cipher suites, assume they know what they're doing
    RETVAL="$RUSTUP_TLS_CIPHERSUITES"
    return
  fi

  local _openssl_syntax="no"
  local _gnutls_syntax="no"
  local _backend_supported="yes"
  if curl -V | grep -q ' OpenSSL/'; then
    _openssl_syntax="yes"
  elif curl -V | grep -iq ' LibreSSL/'; then
    _openssl_syntax="yes"
  elif curl -V | grep -iq ' BoringSSL/'; then
    _openssl_syntax="yes"
  elif curl -V | grep -iq ' GnuTLS/'; then
    _gnutls_syntax="yes"
  else
    _backend_supported="no"
  fi

  local _args_supported="no"
  if [ "$_backend_supported" = "yes" ]; then
    # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
    if check_help_for "notspecified" "curl" "--tlsv1.2" "--ciphers" "--proto"; then
      _args_supported="yes"
    fi
  fi

  local _cs=""
  if [ "$_args_supported" = "yes" ]; then
    if [ "$_openssl_syntax" = "yes" ]; then
      _cs=$(get_strong_ciphersuites_for "openssl")
    elif [ "$_gnutls_syntax" = "yes" ]; then
      _cs=$(get_strong_ciphersuites_for "gnutls")
    fi
  fi

  RETVAL="$_cs"
}

# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites
# if support by local tools is detected. Detection currently supports these wget backends:
# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty.
get_ciphersuites_for_wget() {
  if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then
    # user specified custom cipher suites, assume they know what they're doing
    RETVAL="$RUSTUP_TLS_CIPHERSUITES"
    return
  fi

  local _cs=""
  if wget -V | grep -q '\-DHAVE_LIBSSL'; then
    # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
    if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then
      _cs=$(get_strong_ciphersuites_for "openssl")
    fi
  elif wget -V | grep -q '\-DHAVE_LIBGNUTLS'; then
    # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
    if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then
      _cs=$(get_strong_ciphersuites_for "gnutls")
    fi
  fi

  RETVAL="$_cs"
}

# Return strong TLS 1.2-1.3 cipher suites in OpenSSL or GnuTLS syntax. TLS 1.2
# excludes non-ECDHE and non-AEAD cipher suites. DHE is excluded due to bad
# DH params often found on servers (see RFC 7919). Sequence matches or is
# similar to Firefox 68 ESR with weak cipher suites disabled via about:config.
# $1 must be openssl or gnutls.
get_strong_ciphersuites_for() {
  if [ "$1" = "openssl" ]; then
    # OpenSSL is forgiving of unknown values, no problems with TLS 1.3 values on versions that don't support it yet.
    echo "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
  elif [ "$1" = "gnutls" ]; then
    # GnuTLS isn't forgiving of unknown values, so this may require a GnuTLS version that supports TLS 1.3 even if wget doesn't.
    # Begin with SECURE128 (and higher) then remove/add to build cipher suites. Produces same 9 cipher suites as OpenSSL but in slightly different order.
    echo "SECURE128:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS-ALL:-CIPHER-ALL:-MAC-ALL:-KX-ALL:+AEAD:+ECDHE-ECDSA:+ECDHE-RSA:+AES-128-GCM:+CHACHA20-POLY1305:+AES-256-GCM"
  fi
}

main "$@" || exit 1
