#!/bin/sh
#===============================================================================
#
#  recovery-initramfs-init
#
#  Copyright (C) 2016, 2017 by Digi International Inc.
#  All rights reserved.
#
#  This program is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License version 2 as published by
#  the Free Software Foundation.
#
#
#  !Description: Init script for recovery initramfs
#
#===============================================================================

# Variables.
#------------------------------------------------------------------------------
ENV_BOOT_RECOVERY="boot_recovery"
ENV_RECOVERY_COMMAND="recovery_command"

SW_CONFIG="/etc/swupdate.cfg"
PUBLIC_KEY="/etc/ssl/certs/key.pub"

USB_MOUNT_DIR="/run/media"
UPDATE_MOUNT_DIR="/mnt/update"

REBOOT_TIME=10

# Functions.
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Function - log
#
# Prints the given text in the console.
#
# @param ${1}  - Text to print.
#------------------------------------------------------------------------------
log() {
	echo "[RECOVERY] ${1}" 1>&2
}

#------------------------------------------------------------------------------
# Function - log_warning
#
# Prints the given text in the console as a warning.
#
# @param ${1}  - Warning text to print.
#------------------------------------------------------------------------------
log_warning() {
	log "[WARNING] ${1}"
}

#------------------------------------------------------------------------------
# Function - log_error
#
# Prints the given text in the console as an error.
#
# @param ${1}  - Error text to print.
#------------------------------------------------------------------------------
log_error() {
	log "[ERROR] ${1}"
	psplash_message "ERROR: ${1}"
	psplash_progress "0"
}

#------------------------------------------------------------------------------
# Function - clear_uboot_vars
#
# Clears recovery U-Boot variables.
#------------------------------------------------------------------------------
clear_uboot_vars() {
	fw_setenv "${ENV_BOOT_RECOVERY}"
	fw_setenv "${ENV_RECOVERY_COMMAND}"
}

#------------------------------------------------------------------------------
# Function - read_uboot_var
#
# Reads the given U-Boot variable.
#
# @param ${1}  - U-Boot variable to read.
# @param ${2}  - Where to store the value of the read variable.
#------------------------------------------------------------------------------
read_uboot_var() {
	eval "${2}=\"$(fw_printenv -n ${1} 2>/dev/null)\""
}

#------------------------------------------------------------------------------
# Function - set_uboot_var
#
# Sets the given U-Boot variable.
#
# @param ${1}  - U-Boot variable to set.
# @param ${2}  - Value to set.
#------------------------------------------------------------------------------
set_uboot_var() {
	fw_setenv ${1} ${2} 2>/dev/null
}

#------------------------------------------------------------------------------
# Function - psplash_message
#
# Shows the given message in the psplash screen.
#
# @param ${1}  - Message to show.
#------------------------------------------------------------------------------
psplash_message() {
	echo "MSG ${1}" > /tmp/psplash_fifo
	sleep 0.2
}

#------------------------------------------------------------------------------
# Function - psplash_progress
#
# Sets the psplash progress bar percentage to the given one.
#
# @param ${1}  - Progress percentage.
#------------------------------------------------------------------------------
psplash_progress() {
	echo "PROGRESS ${1}" > /tmp/psplash_fifo
	sleep 0.2
}

#------------------------------------------------------------------------------
# Function - reboot_system
#
# Reboots the system.
#------------------------------------------------------------------------------
reboot_system() {
	sync && reboot -f
}

#------------------------------------------------------------------------------
# Function - quit_with_error
#
# Ends the recovery process with the given error message.
#
# @param ${1}  - Error message.
#------------------------------------------------------------------------------
quit_with_error() {
	clear_uboot_vars
	log_error "${1}"
	log "The system will now reboot in ${REBOOT_TIME} seconds"
	sleep "${REBOOT_TIME}"
	reboot_system
}

#------------------------------------------------------------------------------
# Function - is_nand
#
# Verifies if the system is running in a NAND flash.
#
# @return  - "yes" if the system is running in NAND, "no" otherwise
#------------------------------------------------------------------------------
is_nand() {
	if [ -f /proc/mtd ] && grep -qs mtd /proc/mtd; then
		echo "yes"
	else
		echo "no"
	fi
}

#------------------------------------------------------------------------------
# Function - format_partition
#
# Formats the given partition.
#
# @param ${1}  - Partition name to format.
#------------------------------------------------------------------------------
format_partition() {
	if [ "$(is_nand)" = "yes" ]; then
		format_ubi_volume "${1}"
	else
		format_emmc_block "${1}"
	fi
}

#------------------------------------------------------------------------------
# Function - format_ubi_volume
#
# Formats and re-creates the given UBI volume.
#
# @param ${1}  - UBI Volume name to format.
#------------------------------------------------------------------------------
format_ubi_volume() {
	# Find the MTD partition.
	local mtd_num="$(sed -ne "s/mtd\([0-9]\+\):.*\<${1}\>.*/\1/g;T;p" /proc/mtd 2>/dev/null)"
	if [ -z "${mtd_num}" ]; then
		quit_with_error "Could not find MTD partition for volume '${1}'"
	else
		# Umount in case partition is mounted, ignore errors.
		if grep -qs "${1}" /proc/mounts; then
			local path="$(sed -ne "s/.*:${1} \(.*\) ubifs.*/\1/g;T;p" /proc/mounts 2>/dev/null)"
			umount "${path}" >/dev/null 2>&1
		fi
		ubidetach -p "/dev/mtd${mtd_num}" >/dev/null 2>&1
		# Format MTD partition.
		if ! ubiformat "/dev/mtd${mtd_num}" -q -y; then
			quit_with_error "Error erasing '/dev/mtd${mtd_num}' block"
		fi
		psplash_progress "50"
		# Attach and get UBI device number
		local dev_number="$(ubiattach -p /dev/mtd${mtd_num} 2>/dev/null | sed -ne 's,.*device number \([0-9]\).*,\1,g;T;p' 2>/dev/null)"
		# Create UBI Vol.
		ubimkvol "/dev/ubi${dev_number}" -m -N "${1}" >/dev/null 2>&1
		if [ "$?" = "0" ]; then
			# Configure the empty UBIFS partition to use ZLIB
			[ "${1}" = "update" ] && UBIFS_COMPRESSION="-x zlib"

			volid="$(ubinfo "/dev/ubi${dev_number}" -N "${1}" | sed -ne 's,Volume ID:[[:blank:]]\+\([0-9]\+\)[[:blank:]]\+.*,\1,g;T;p')"
			mkfs.ubifs ${UBIFS_COMPRESSION} -F /dev/ubi${dev_number}_${volid}
			psplash_progress "100"
			log "Partition '${1}' successfully erased!"
			# Detach MTD partition.
			ubidetach -p "/dev/mtd${mtd_num}" >/dev/null 2>&1
		else
			quit_with_error "Error creating '${1}' UBI volume"
		fi
	fi
}

#------------------------------------------------------------------------------
# Function - format_emmc_block
#
# Formats the given emmc partition block name.
#
# @param ${1}  - Partition name to format.
#------------------------------------------------------------------------------
format_emmc_block() {
	# Find partition block number.
	local partition_block="/dev/mmcblk0p$(fdisk -l /dev/mmcblk0 | sed -ne "s,^[^0-9]*\([0-9]\+\).*\<${1}\>.*,\1,g;T;p")"
	if [ -b "${partition_block}" ]; then
		# Umount in case partition is mounted, ignore errors.
		if grep -qs "${partition_block}" /proc/mounts; then
			umount "${partition_block}" >/dev/null 2>&1
		fi
		# Format emmc block.
		mkfs.ext4 "${partition_block}" >/dev/null 2>&1
		if [ "$?" = "0" ]; then
			psplash_progress "100"
			log "Partition '${1}' successfully erased!"
		else
			quit_with_error "Error erasing '${1}' partition"
		fi
	else
		quit_with_error "Could not find partition block for '${1}'"
	fi
}

#------------------------------------------------------------------------------
# Function - swu_package_path
#
# Get absolute path of update package searching in local media
#
# @param ${1}  - SWU package
#------------------------------------------------------------------------------
swu_package_path() {
	# Check whether the package is local and get the absolute path.
	if echo "${1}" | grep -qs '^file://'; then
		local pkg_name="$(basename ${1})"

		for i in ${UPDATE_MOUNT_DIR} $(echo ${USB_MOUNT_DIR}/*); do
			echo $i | grep -qs "${USB_MOUNT_DIR}/\*" && continue
			if [ -f "${i}/${pkg_name}" ]; then
				swu_abspath="${i}/${pkg_name}"
				break
			fi
		done

		if [ -n "${swu_abspath}" ]; then
			echo "${swu_abspath}"
		else
			quit_with_error "Unable to find update package '${pkg_name}'"
		fi
	else
		echo "${1}"
	fi
}

#------------------------------------------------------------------------------
# Function - check_swu_package
#
# Check if the update package is a valid one and get its absolute path
#
# @param ${1}  - SWU package
#------------------------------------------------------------------------------
check_swu_package() {
	log "Checking update package '$(basename ${1})'"

	if [ -z "${1}" ]; then
		quit_with_error "Firmware update package not specified"
	fi

	update_package="$(swu_package_path ${1})"

	# Check software update package.
	if [ -f "${PUBLIC_KEY}" ]; then
		swupdate -c -i "${update_package}" -k "${PUBLIC_KEY}"
	else
		swupdate -c -i "${update_package}"
	fi

	if [ "$?" != "0" ]; then
		quit_with_error "Invalid update package '$(basename ${1})'"
	fi
}

#------------------------------------------------------------------------------
# Function - set_encryption_flag
#
# Set the rootfs encryption flag to the mtdparts variable.
#------------------------------------------------------------------------------
set_encryption_flag() {
	if [ "$(is_nand)" = "no" ]; then
		return
	fi

	# Read the mtdparts variable.
	read_uboot_var mtdparts mtdparts

	# Check if there is any command.
	if [ -z "${mtdparts}" ]; then
		quit_with_error "No mtdparts found"
	fi

	# Parse the mtdparts value.
	case "${mtdparts}" in
		*\(rootfs\)enc*)
			# Partition already flagged.
			;;
		*\(rootfs\)*)
			# Add the flag to the rootfs.
			local new_mtdparts=$(echo "${mtdparts}" | sed "s/(rootfs)/(rootfs)enc/g")
			set_uboot_var mtdparts "${new_mtdparts}"
			;;
		*)
			quit_with_error "Error flagging rootfs as encrypted"
			;;
	esac

	if [ -n "${update_package_bool}" ]; then
		# Modify the recovery command not to set again the key.
		new_command=$(echo "${COMMAND}" | sed "s/encryption_key=[^ ]*//g")
		set_uboot_var "${ENV_RECOVERY_COMMAND}" "${new_command}"
		psplash_progress "100"
		reboot_system
	fi
}

#------------------------------------------------------------------------------
# Function - get_kernel_version
#
# Obtain the kernel version and return it in number format
#
# @return  - kernel version
#------------------------------------------------------------------------------
get_kernel_version() {
	major="$(uname -r | cut -d'.' -f1)"
	minor="$(uname -r | cut -d'.' -f2)"
	echo $(((major << 8) + minor))
}

# Main
#------------------------------------------------------------------------------
# Setup the environment.
export PATH=/bin:/sbin:/usr/bin:/usr/sbin

# Mount virtual file system.
mkdir -p /proc /sys /dev /tmp
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp

# Register mdev as device manager
if [ -f /proc/sys/kernel/hotplug ]; then
	echo > /dev/mdev.seq
	echo > /dev/mdev.log
	echo "/sbin/mdev" > /proc/sys/kernel/hotplug
	mdev -s
fi

# Give some time for the devices to settle down
sleep 2

# Run all shell scripts in postinstall folder
run-parts /etc/*-postinsts

# Setup fw_printenv.
mkdir -p /var/lock

# Set kernel console loglevel.
sysctl -q -w kernel.printk=4

# Start psplash.
psplash &

# Parse the kernel command line.
for arg in $(cat /proc/cmdline); do
	case "${arg}" in
		rescue=1) eval "${arg}";;
	esac
done

# Jump to a rescue shell if requested.
if [ -n "${rescue}" ]; then
	# Expand console and respawn if exited
	while true; do
		setsid cttyhack sh -l
		sleep 1
	done
fi

log "Starting recovery..."
psplash_message "Starting recovery..."

# Read the recovery command.
read_uboot_var "${ENV_RECOVERY_COMMAND}" COMMAND

# Check if there is any command.
if [ -z "${COMMAND}" ]; then
	quit_with_error "No command found"
fi

# Parse the recovery command.
for arg in ${COMMAND}; do
	case "${arg}" in
		wipe_update)
			wipe_update_bool=true;;
		encryption_key=*)
			encryption_key_bool=true;
			eval "${arg}";;
		update_package=*)
			update_package_bool=true;
			eval "${arg}";;
		*)
			# Not supported command
			quit_with_error "Unknown recovery command '${arg}'";;
	esac
done

# Sanity checks.
if [ -n "${update_package_bool}" ]; then
	check_swu_package "${update_package}"
fi

# Check if encryption key command is configured.
if [ -n "${encryption_key_bool}" ]; then
	log "Trustfence encryption key setup requested (new key: ${encryption_key:-random})"
	psplash_message "Configuring new encryption key..."
	psplash_progress "0"

	trustfence-tool "--newkey${encryption_key:+=${encryption_key}}"
	if [ "$?" = "0" ]; then
		psplash_progress "10"
		log "Trustfence encryption key setup succeed!"
	else
		quit_with_error "Error configuring trustfence encryption key"
	fi

	# Set the encryption flag to the rootfs.
	set_encryption_flag

	# Format partition.
	if [ "$(is_nand)" = "no" ]; then
		psplash_message "Formatting rootfs partition..."
		rootfs_block="/dev/mmcblk0p$(fdisk -l /dev/mmcblk0 | sed -ne "s,^[^0-9]*\([0-9]\+\).*\<rootfs\>.*,\1,g;T;p")"
		trustfence-tool --format ${rootfs_block} cryptroot
	fi
	psplash_progress "100"
fi

# Check if update package command is configured.
if [ -n "${update_package_bool}" ]; then
	log "Firmware update requested"
	psplash_message "Updating firmware..."
	psplash_progress "0"

	# Format the UBI volume before updating.
	if [ "$(is_nand)" = "yes" ]; then
		psplash_message "Formatting rootfs partition..."
		format_ubi_volume rootfs
		psplash_progress "0"
	fi

	log "Update package location: ${update_package}"
	# Execute the progress binary.
	progress -wp &
	# Execute the software update.
	if [ -f "${PUBLIC_KEY}" ]; then
		swupdate -f "${SW_CONFIG}" -i "${update_package}" -k "${PUBLIC_KEY}"
	else
		swupdate -f "${SW_CONFIG}" -i "${update_package}"
	fi
	if [ "$?" = "0" ]; then
		log "Firmware update process succeed!"
	else
		quit_with_error "Error executing the firmware update"
	fi
fi

# Check if wipe update patition command is configured.
if [ -n "${wipe_update_bool}" ]; then
	log "Wipe 'update' partition requested"
	psplash_message "Erasing update partition..."
	psplash_progress "0"
	format_partition update
fi

# End the recovery process.
clear_uboot_vars
reboot_system
