#!/bin/sh

set -u

VERSION=1 # API version only (not incremented after *every* change)

source /lib/functions/debugservices.sh

json_encode() {
  awk 'BEGIN { FS="\t"; sep=""; printf "{"; }
  {
    # Split the key into key-name and optional key-type
    # Key-types are suffixes containing one or more "{}[]@" characters
    type = $1;
    #sub(/^[^\{\}\[\]@]*/, "", type); FIXME cant escape square brackets in busybox 1.20.2
    sub(/^[^\{\}@]*/, "", type);
    $1 = substr($1, 1, length($1) - length(type));

    #if (type ~ /[\]\}]/) { FIXME cant escape square brackets in busybox 1.20.2
    if (type ~ /[\}]/) {
      sep=",";			# Closing brace
    } else if (type ~ /[\{]/) {
      printf sep; sep="";	# Opening brace
    } else {
      printf sep; sep=",";	# Everything else
    }

    # Remove any spaces from keyname
    gsub(/ /, "", $1);

    if ($1 ne "") printf "\"" $1 "\":";

    for (n=2; n<=NF; n++) {
      # Escape quotes
      gsub(/\"/, "\\\"", $n);

      # Quote string arguments (numerals left unquoted)
      if ($n !~ /^-?(0|([1-9][0-9]*))(\.[0-9]+)?$/) {
        $n = "\"" $n "\"";
      }
    }

    if (type=="@") {
      # Array format
      printf "[";
      for (n=2; n<=NF; n++) printf ((n>2)?",":"") $n;
      printf "]";
    } else {
      # Single value
      printf type $2;
    }
  }
  END { printf "}"; }'
}

pack_mac() {
  awk '{
    split($1, ar, ":");
    for (n=1; n<=6; n++) {
      printf("%02X", "0x"ar[n]);
    }
  }'
}

mfd_status_base() {
  # Entries in this structure should be constant for this boot
  {
    strings -n32 </dev/mtd0ro | awk '
    {
      if (FILENAME ~ /\.emb$/) {
	if (length(app_ver) || (FNR>1 && length(pkg_regex)==0)) {
	  nextfile;

	} else if (FNR==1) {
	  pkg_regex = "";

	  if ($0 ~ /^PACKAGE NAME:/) {
	    sub(/^.*:\s*/, "", $0);
	    match($0, /^[A-Za-z]+App-/);    len_a = RLENGTH;
	    match($0, /-[^-]+-[^-]+.emb$/); len_b = RLENGTH;

	    if (len_a>0 && len_b>0) {
	      pkg_name = substr($0, 0, len_a-1);
	      pkg_ver  = substr($0, len_a+1, length($0)-len_a-len_b);

	      if (pkg_name == "RadarApp") {
		pkg_regex = "/RadarApp-.+\.riw$";
	      } else if (pkg_name == "ThorApp") {
		pkg_regex = "^/?usr/bin/ecdis-thor$";
	      } else if (pkg_name == "WebSocketApp") {
		# Specifically ignore WebSocketApp - only installed on Thor.
	      } else {
		pkg_regex = "^/?usr/bin/" pkg_name "$";
	      }
	    }
	  }

	} else if ($0 ~ pkg_regex) {
	  app_ver = pkg_ver;
	}

      } else if (FILENAME == "/proc/mtd") {
	if ($1 ~ /^mtd[0-9]:$/) { nand_b += ("0x"$2) }

      } else if (FILENAME == "/etc/version") {
	if ($1 ~ /^(platform|product)=/) {
	  sub(/=/, "\t", $0);
	  etc_ver = etc_ver $0 "\n";
	}

      } else if (FILENAME == "-") {
	if (match($0, /U-Boot [0-9]{4}\.[0-9]{2} [^)]{17,67})/)) {
	  boot_ver = substr($0, RSTART+7, RLENGTH-7);
	  nextfile;
	}

      } else if (FNR==1) {
	if (FILENAME ~ /^\/proc\/navico_platform\//) {
	  key = FILENAME; sub(/.*\//, "", key);

	  if (key != "cpu_card_features" &&
	    key != "gpio" &&
	    key != "sku_ver" &&
	    key != "sub_family_id")
	  {
	    platdata = platdata key "@\t" $0 "\n";
	  }

	} else if (FILENAME == "/proc/cmdline") {
	  if (match($0, /navico_sku\.codename=[^ ]+/)) {
	    codename = substr($0, RSTART+20, RLENGTH-20);
	  }

	} else if (FILENAME == "/etc/NOS/can_unique_id")     { can_id = $1;
	} else if (FILENAME ~ /\/block\/mmcblk0\/size$/)     { emmc_b = $1 * 512;
	} else if (FILENAME == "/proc/sys/kernel/osrelease") { os_rel = $0;
	} else if (FILENAME == "/proc/sys/kernel/version")   { os_ver = $0;
	} else if (FILENAME == "/etc/NOS/can_unique_id")     { can_id = $1;
	}
      }
    }
    END {
      printf "platform{\n%s}\n", platdata;
      if (length(codename)) { printf("codename\t%s\n", codename); }
      printf "flash@\t%d\t%d\n", nand_b/1024, emmc_b/1024;
      if (length(can_id)) { printf("can_id\t%s\n", can_id); }

      printf "version{\nkernel\t%s %s\n%s", os_rel, os_ver, etc_ver;
      if (length(app_ver))  { printf "app\t%s\n", app_ver;   }
      if (length(boot_ver)) { printf "boot\t%s\n", boot_ver; }
      printf "}\n";
    }
  ' $(/bin/ls \
      /proc/navico_platform/* \
      /proc/cmdline \
      /proc/mtd \
      /proc/sys/kernel/osrelease \
      /proc/sys/kernel/version \
      /sys/devices/platform/*.2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/size \
      /sys/devices/soc0/soc/2100000.aips-bus/2198000.usdhc/mmc_host/mmc0/mmc0:0001/block/mmcblk0/size \
      /etc/version \
      /etc/NOS/can_unique_id \
      2>/dev/null) \
    $(find /var/log/packages -maxdepth 1 -name '*App-*.emb' ! \( -iname '*resources*' -o -iname '*translations*' \)) \
    -
  } | json_encode
}

mfd_status_extra() {
  # Entries in this structure can change between calls
  {
    printf "date\t%d\n" "$(date '+%s')"
    printf "host\t%s\n" "$(uname -n)"

    ip addr show dev $(syslog_iface) | awk '
      BEGIN { ips = "inet@" }
      /^\s+inet /{ ips = ips "\t" $2; }
      END { printf ips "\n" }
    '

    [ -r /run/peer-mac ] && printf "peer\t%s\n" "$(head -qn1 /run/peer-mac | pack_mac)"

    awk ' 
      FILENAME ~ /\/mode$/           && FNR==1 { mode=$1; }
      FILENAME ~ /\/bits_per_pixel$/ && FNR==1 { bpp=$1; }
      END { printf "fb0@\t%s\t%d\n", mode, bpp; }
    ' /sys/class/graphics/fb0/mode /sys/class/graphics/fb0/bits_per_pixel 2>/dev/null
  } | json_encode
}

mfd_status_load() {
  {
    local avail_files="" files="\
      /proc/uptime \
      /proc/loadavg \
      /proc/meminfo \
      /proc/stat \
      /proc/sys/kernel/osrelease \
      /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state \
      /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq \
      /sys/devices/virtual/thermal/thermal_zone0/temp \
      /sys/block/zram0/compr_data_size \
      "

    for f in $files; do
      [ -r $f ] && avail_files="$avail_files $f"
    done

    awk '{
	if      (FILENAME == "/proc/uptime"  && FNR==1) { up_secs=$1; }
	else if (FILENAME == "/proc/loadavg" && FNR==1) { load_1=$1; load_5=$2; load_15=$3; }
	else if (FILENAME == "/proc/meminfo") {
	  if ($1 == "MemTotal:")            mem_total=$2;
	  else if ($1 == "MemFree:")        mem_free=$2;
	  else if ($1 == "MemAvailable:")   mem_avail=$2;
	  else if ($1 == "Buffers:")        mem_buf=$2;
	  else if ($1 == "Cached:")         mem_cache=$2;
	  else if ($1 == "SwapTotal:")      swp_total=$2;
	  else if ($1 == "SwapFree:")       swp_free=$2;
	  else if ($1 == "SwapCached:")     swp_cache=$2;
	}
	else if (FILENAME == "/proc/stat" && $1 == "cpu") {
	  # jiffies in: user($2), nice, system, idle, iowait, SUM(irq($7), softirq, steal, guest, guest_nice)
	  for (f=2; f<=NF; f++) {
	    if (f<=7)  cpu_s[f-2] = $f;
	    else       cpu_s[5] += $f;
	  }
	}
	else if (FILENAME == "/proc/sys/kernel/osrelease") { os_rel=$0; }
	else if (FILENAME ~ /\/time_in_state$/ && NF==2) {
	  cpu_dvfs = sprintf("%s\t%d\t%d", cpu_dvfs, $1/1000, $2);
	}
	else if (FILENAME ~ /\/scaling_cur_freq$/ && FNR==1) { cpu_mhz=$1/1000; }
	else if (FILENAME ~ /\/temp$/             && FNR==1) { cpu_degc=$1; }
	else if (FILENAME == "/sys/block/zram0/compr_data_size") { swp_compr=$0/1024; }
      }
      END {
	printf "up@\t%d\t%.2f\t%.2f\t%.2f\n", up_secs, load_1, load_5, load_15;
	printf "mem@\t%d\t%d\t%d\t%d%s\n",
	  mem_total, mem_free, mem_buf, mem_cache,
	  length(mem_avail) ? "\t"mem_avail : "";
	if (swp_total > 0)
          printf "swp@\t%d\t%d\t%d\t%d\n", swp_total, swp_free, swp_cache, swp_compr;
	printf "cpu@\t%d\t%d\t%d\t%d\t%d\t%d\n", cpu_s[0], cpu_s[1], cpu_s[2], cpu_s[3], cpu_s[4], cpu_s[5];
	printf "cpu_dvfs@%s\n", cpu_dvfs;
	printf "cpu_op@\t%d\t%.1f\n", cpu_mhz, cpu_degc/1000;
      }
    ' $avail_files
  } | json_encode
}

report_mfd_status() {
  # Periodically report status for MFD monitoring
  # This should only run when the remote syslog server is contactable
  local mac_str=$(syslog_mac | pack_mac)
  local base_str=$(mfd_status_base)

  # Reporting frequency:
  #   base   120 secs (ofs 0)
  #   extra   60 secs (ofs 2)
  #   load    30 secs (ofs 4)
  local sec=0

  while sleep 2; do
    if [ $sec -eq 0 ]; then
      printf "$mac_str\tbase\t%s\n" "$base_str"
    elif [ $(( sec % 60 - 2 )) -eq 0 ]; then
      printf "$mac_str\textra\t%s\n" "$(mfd_status_extra)"
    elif [ $(( sec % 30 - 4 )) -eq 0 ]; then
      printf "$mac_str\tload\t%s\n" "$(mfd_status_load)"
    fi

    sec=$(( (2+sec) % 120 ))
  done | logger -t "mfd_status" -p debug
}

ip_init() {
  local iface=${1:-eth0}

  if sysconfig_enabled static_ip && [ "$iface" = "$(syslog_iface)" ]; then
    local static_ip=$(calc_static_ip "$iface")

    if [ "${iface:0:3}" != "ppp" ]; then
      ip address add dev $iface local $static_ip/32 label $iface:static
    fi

    ip route replace to $MFD_RTR_IP/32 src $static_ip dev $iface

    if sysconfig_enabled navico_lan; then
      ip route add 172.27.0.0/16 via $MFD_RTR_IP src $static_ip
      ip route add 172.28.0.0/15 via $MFD_RTR_IP src $static_ip
    fi
  fi
}

ip_remove() {
  local iface=${1:-eth0}

  if sysconfig_enabled static_ip && [ "$iface" = "$(syslog_iface)" ]; then
    local static_ip=$(calc_static_ip "$iface")

    ip route del to $MFD_RTR_IP/32 src $static_ip dev $iface

    if [ "${iface:0:3}" != "ppp" ]; then
      ip address del dev $iface local $static_ip/32 label $iface:static
    fi
  fi
}

get_sysconfig() {
  local config=$(
    find /etc/NOS/sysconfig/ -type f 2>/dev/null | while read file; do
      if [ ! -e "/run/sysconfig/${file##*/}" ]; then
	file="$file (inactive)"
      else
        local label=$(sysconfig_metadata ${file##*/} 'label')
        if [ -n "$label" ]; then
	  file="$file: $label"
	fi
      fi
      printf "%s\n" "$file"
    done | sort
    grep -sHi 'CreateCrashLog=true\|DefaultConfigIni=' /etc/NOS/Config.ini
    [ -e /run/sysconfig/debug_mode ] && printf "//U-Boot: debug mode\n"
    [ -e /run/sysconfig/serialcon ]  && printf "//U-Boot: serial console\n"
    grep -q factorydata /etc/fstab && ! mountpoint -q /media/factorydata && printf "//Factory partition unmounted\n"
  )

  case ${1:-} in
    --to-fbdev)
      if [ -n "$config" ]; then
        local txt=$(printf " DEBUG OPTIONS ARE ENABLED:\n\n%s" "$config" | sed 's_^/.*/_ _')
        local width=$(printf '%s\n' "$txt" | wc -L)

        printf '\33[97;41m\n%s\n\n' "$txt" |    # Set initial colour, output message content
        sed $'s/$/\33[K/' |                     # Clear to end of line
        fbdraw --width $((8*(width < 32 ? 32 : 1+width))) --text >/dev/null
      fi
      ;;

    *)
      # Output to console
      {
        printf "Configuration:\n"
        [ -n "$config" ] && printf "%s\n" "$config"
        printf "\nActive/effective flags:\n"
        find /run/sysconfig/ -type f 2>/dev/null | sort
        printf "\n"
      } | sed 's_^/.*/_\t_'
      ;;
  esac
}

set_sysconfig() {
  local dir=/etc/NOS/sysconfig

  mount -o remount,rw /etc/NOS
  rm -fr "$dir"

  if [ $# -gt 0 ]; then
    mkdir -p "$dir"

    for f; do
      touch "$dir/$f"
    done
  fi

  mount -o remount,ro /etc/NOS
}

tftp_get() {
  local ip=${TFTP_SRV_IP:-}
  local f_rmt="$1" f_loc=${2:-${1##*/}} mode=${3:-}

  [ -n "${TFTP_SRV_ROOT:-}" ] && f_rmt="$TFTP_SRV_ROOT/$f_rmt"

  if [ -z "$ip" ]; then
    if sysconfig_enabled static_ip; then
      ip=$MFD_RTR_IP
    else
      ip=169.254.254.254
    fi
  fi

  local result timer_pid

  # TFTP_SRV_RETRY: empty "", or sleep argument (eg "10", "1d")
  if [ -n "${TFTP_SRV_RETRY:-}" ]; then
    sleep $TFTP_SRV_RETRY &
    timer_pid=$!
    trap "kill $timer_pid 2>/dev/null || true" TERM EXIT
  fi

  until result=$(tftp -gr "$f_rmt" -l "${f_loc}.tmp" $ip 2>&1); do
    # Abort if we get a tftp-server error, eg:
    #   "tftp: server error: (1) File not found"
    #   "tftp: server error: (2) Access violation"
    # Refer "Error Codes" in https://tools.ietf.org/html/rfc1350
    # Continue to retry for all other error types, eg:
    #   "tftp: timeout"
    #   "tftp: bad address 'google.com'"
    #   "tftp: sendto: Network is unreachable"

    case $result in
    *[Ss]"erver "[Ee]"rror"*) unset timer_pid ;;
    esac

    if [ -z "${timer_pid:-}" ] || ! kill -0 $timer_pid 2>/dev/null; then
      rm -f "${f_loc}.tmp"
      printf "Unable to fetch %s: %s\n" "$f_rmt" "$result" >&2
      return 1
    fi

    printf "%s, retrying...\n" "$result" >&2
    sleep 1
  done

  # File fetched, clean up
  [ -n "$mode" ] && chmod $mode "${f_loc}.tmp"
  mv "${f_loc}.tmp" "$f_loc"
}

fetch_exec() {
  local script=${1:-help}; shift
  local dest="/tmp/debugservices/fax"
  TFTP_SRV_ROOT=${TFTP_SRV_ROOT:-"debugservices/fax"}

  # Note: Script *must* be exec'd to maintain PPID (etc)
  mkdir -p "$dest" && tftp_get "$script" "$dest/$script" "+x" && exec "$dest/$script" "$@"

  return 127
}

fetch_exec_signed() {
  local script=${1:-help}; shift
  local dest="/tmp/debugservices/fax"
  local f_tmp="$dest/.${script}.signed.$$"
  TFTP_SRV_ROOT=${TFTP_SRV_ROOT:-"debugservices/fax-signed"}
  TFTP_SRV_RETRY=${TFTP_SRV_RETRY:-30}

  local path_full=$PATH
  PATH=/usr/bin:/bin

  if mkdir -p "$dest" && tftp_get "$script" "$f_tmp"; then
    if verify_signature "$f_tmp"; then
      rm "$f_tmp"
      # Shebang marks the start of the executable script
      printf "%s" "$GOLDCARD_TEXT" | sed -ne '/^#!/,$p' > "$dest/$script"
      chmod +x "$dest/$script"
      PATH=$path_full
      exec "$dest/$script" "$@"
    else
      print_gc_err_txt $? >&2
    fi
  fi

  return 127
}

get_mmc_id() {
  # Determine the CID and CSD of the MMC device
  # containing the specified file or directory.
  #
  # Return values:
  #  0: Success - actual cid/csd values to stdout
  #  1: Unable to determine device name (mmcblk?p?)
  #  2: Device name not found in sysfs
  #  3: Unable to locate cid/csd in sysfs
  local file=$1

  local dev_name=$(stat -c'%d' -- "$file" | awk '
    FILENAME=="-" && FNR==1 {
      dev=$1
    }
    FILENAME=="/proc/partitions" && dev==($1*256+$2) {
      print $4; exit
    }
  ' - /proc/partitions)

  [ -n "$dev_name" ] || return 1

  local mmc_sysfs=$(find "/sys/devices/" -type d -name "$dev_name" | head -n1)

  [ -n "$mmc_sysfs" ] || return 2

  local cid csd cd_max=5
  while [ $((cd_max--)) -gt 0 ]; do
    if [ -e "$mmc_sysfs/cid" -a -e "$mmc_sysfs/csd" ]; then
       cid="$(head -n1 <$mmc_sysfs/cid)"
       csd="$(head -n1 <$mmc_sysfs/csd)"
       break
    fi
    mmc_sysfs="$mmc_sysfs/.."
  done

  if [ -z "${cid:-}${csd:-}" ]; then
    local cid_csd
    if cid_csd=$(/usr/sbin/cr-util --device="/dev/${dev_name}" --getcid --getcsd); then
      if [ ${#cid_csd} -eq 65 ] && [ "${cid_csd:32:1}" = $'\n' ]; then
        cid=${cid_csd:0:32}     # First line
        csd=${cid_csd:33:32}    # Second line
      fi
    fi
  fi

  if [ -z "${cid:-}" -o -z "${csd:-}" ]; then
    return 3
  else
    printf "mmc_cid=%s\nmmc_csd=%s\n" "$cid" "$csd"
  fi
}

verify_mmc_id() {
  # Compare mmc_cid/mmc_csd entries from specified
  # file and its containing MMC device:
  #   mmc_cid=1d414453442020201021113c1200ab00
  #   mmc_csd=002f00325f5a83c6edb7ffbf96800000

  local file=$1 id_act id_def

  if [ "$(cut -f1 /proc/navico_platform/model_id)" != "226" ]; then
    # General case, for directly attached media
    id_act=$(get_mmc_id "$file") || return $?

  else
    # Special case for Hatchetfish CPU_B, which doesn't have direct access to media
    # - Try to extract CID from MediaDaemon output in syslog
    # - Append the CSD from original file (CSD unavailable in syslog)
    id_act=$(
      {
        dirname "$file"
        /sbin/logread || tail -c 100k /var/log/messages
      } | awk '
        NR==1 { mntpt = $0 }
        NR>=2 && $0 ~ "MediaDaemon.*<dataarrival path=\"" mntpt "\" " { rpt = $0 }
        END {
          if (rpt ~ / remote="false" / && match(rpt, / key="[0-9a-z]{32}" /)) {
            printf "mmc_cid=%s\n", substr(rpt, RSTART+6, RLENGTH-8);
          }
        }'

      printf "%s" "$GOLDCARD_TEXT" | grep '^mmc_csd='
    ) 2>/dev/null || return 4
  fi

  id_def=$(printf "%s" "$GOLDCARD_TEXT" | grep '^mmc_c[is]d=') || return 5

  # Valid entries are 40 characters each, so combined
  # (with \n in between) are 81 bytes in total.
  [ ${#id_act} -eq 81 -a ${#id_def} -eq 81 ] || return 6

  # Last two characters in each string (one hex byte)
  # are a checksum (CRC7<<1)|1, which is not consistently
  # reported across platforms (mx28/51/61) so don't
  # include them in the comparison.
  id_act="${id_act:0:38},${id_act:41:38}"
  id_def="${id_def:0:38},${id_def:41:38}"

  [ "$id_act" == "$id_def" ] || return 7
}

pgp_sections() {
  # Print only the specified number of ascii-armored PGP sections from stdin
  awk 'BEGIN { n='${1:-0}' } {
    if ($0 ~ /^--/ && (--n < 0)) {
      printf "%s", s; exit;
    }
    s = s $0 "\n";
  }'
}

verify_signature() {
  local file=$1

  if [ "$(head -n 1 < "$file")" != "-----BEGIN PGP SIGNED MESSAGE-----" ]; then
    return 9  # File is unsigned

  else
    # Store all text up to and including the signature block for GPG verification
    # (tr command strips everything except: tab, newline, space and {! to ~})
    local gc_text_sig=$(head -c 10k < "$file" | tr -cd '\011\012\040!-~' | pgp_sections 2)
    # Store only the signed/ascii-armored section for mmc_cid/csd and command tags
    GOLDCARD_TEXT=$(printf "%s" "$gc_text_sig" | pgp_sections 1 | sed 's/\s\+$//; 1,/^$/d')

    printf "%s\n" "$gc_text_sig" |
    gpgv --homedir=/usr/share/debugservices --ignore-time-conflict --status-fd=3 - 3>&1 >&2 |
    (
      while read pfx status arg1 rem; do
        if [ "$pfx" == "[GNUPG:]" -a "$status" == "VALIDSIG" ]; then
          case "$arg1" in
            "635BF9008FF433EC2B22A43AFB5AB44745F4E293") # "Navico goldcard key #1"
              exit 0
              ;;
          esac
        fi
      done

      exit 8 # Bad signature, or unknown signing key
    )

    return $?
  fi
}

print_gc_err_txt() {
  local err=$1

  if [ $err -ne 0 ]; then
    printf "Error %d: " $err
    case $err in
      [1-3])    printf "Unable to determine card CSD/CID.\n" ;;
      [4-7])    printf "Card CSD/CID mismatch.\n" ;;
      8)        printf "Bad signature, or unknown signing key.\n" ;;
      9)        printf "File is unsigned.\n" ;;
      *)        printf "(undefined)\n" ;;
    esac
  fi
}

goldcard_exec() {
  local file=$1

  if [ ! -e "$file" -o ! -s "$file" ]; then
    local card_id

    # get_mmc_id will fail on HF CPU_B, so CPU_A will do the work
    if card_id=$(get_mmc_id "$file"); then
      printf "%s\n" "$card_id" > "$file"
      fsync -- "$file"
      printf "Initialised %s\n" "$file"
    else
      printf "Unable to initialise %s\n" "$file"
    fi

  else
    printf "Loading %s\n" "$file"

    local path_full=$PATH
    PATH=/usr/bin:/bin

    if verify_signature "$file" && verify_mmc_id "$file"; then
      # Signature and CID/CSD OK
      # Output the title/author tags (debug only)
      printf "%s" "$GOLDCARD_TEXT" | grep -i '^\(title\|author\)='

      cd /tmp
      PATH=$path_full
      export GOLDCARD="$file"
      exec sh -cx "$(printf "%s" "$GOLDCARD_TEXT" | sed -ne 's/^command=//p')"
    else
      local result=$?
      print_gc_err_txt $result
      return $result
    fi
  fi
}

case ${1:-} in
  ip-init)
    # Usually called from zcip.sh
    shift && ip_init "$@"
    ;;
  ip-remove)
    # Usually called from zcip.sh
    shift && ip_remove "$@"
    ;;
  get-sysconfig)
    shift && get_sysconfig "$@"
    ;;
  set-sysconfig)
    shift && set_sysconfig "$@"
    ;;
  report-status)
    report_mfd_status
    ;;
  tftp)
    shift && tftp_get "$@"
    ;;
  fax)
    shift && fetch_exec "$@"
    ;;
  fax-signed)
    shift && fetch_exec_signed "$@"
    ;;
  get_mmc_id)
    shift && get_mmc_id "$@"
    ;;
  goldcard)
    shift && goldcard_exec "$@"
    ;;
  sourced)
    : # Do nothing (for script inclusion/testing)
    ;;
  *)
    printf "Usage: %s { get-sysconfig | set-sysconfig | fax }\n" "${0##*/}" >&2
    exit 1
    ;;
esac
