#!/bin/sh
#
# SPDX-License-Identifier:     GPL-3.0-or-later
#
#
# Copyright:
#	2012 Axel Heider
#	2015 Pascal Rosin
#	Sebastian P.
#	Nicholas A. Schembri State College PA USA
#
# Description:
#
#  Read-only Root-FS for Raspian
#
#  https://help.ubuntu.com/community/aufsRootFileSystemOnUsbFlash#Overlayfs
#
# Changelog:
#
# v1.0.0
#   - written by Axel Heider for Ubuntu 11.10
#
# v2.0.0
#   - Modified to work with overlayfs integrated in Linux Kernel (>= 3.18)
#   - introduce workdir needed for new overlayfs
#   - change `mount --move` to `mount -o move` to drop busybox requirement
#   - Tested with raspian-ua-netinst v1.0.7
#     (Linux 3.18.0-trunk-rpi, Debian 3.18.5) on a Raspberry Pi.
#     The aufs part is not tested!
#
# v0.3
#   - Add SPDX-License-Identifier: GPL3+
#   - Drop /etc/fstab manipulation
#   - Fix versioning scheme to a more simple one (new v0.3)
#
# v0.4
#   - Modified to use a the directory ".root-rw" on the partition number 5 of the used root rfs device
#     instead of an tmpfs in the memory.
#
# v0.5
#   - Check via magic file if reset of overlay is requested to implement a general factory reset
#
# v0.6
#   - Don't retain serialnumber file on factory reset, because file will be removed
#
# v0.7
#   - During factory reset, backup and restore paths listed in /etc/cel/resetexcludes.d/
#     to prevent them from being wiped or overridden by the factory defaults
#   - Add log_info_msg helper function
#
# v0.8
#   - Do check if path is under /data/ earlier during path list construction
#   - Trim leading and trailing whitespace from paths read from resetexcludes.d files

# Install:
#  put this file in /etc/initramfs-tools/scripts/init-bottom/root-ro
#  chmod 0755 /etc/initramfs-tools/scripts/init-bottom/root-ro
#  update-initramfs -u
#  add `root-ro-driver=overlay` to the line in /boot/cmdline.txt
#
# Disable read-only root fs
#   * option 1: kernel boot parameter "disable-root-ro=true"
#   * option 2: create file "/disable-root-ro"
#
# ROOT_RO_DRIVER variable controls which driver isused for the ro/rw layering
#   Supported drivers are: overlayfs, aufs
#  the kernel parameter "root-ro-driver=[driver]" can be used to initialize
#  the variable ROOT_RO_DRIVER. If nothing is given, overlayfs is used.
#

# no pre requirement
PREREQ=""

prereqs()
{
    echo "${PREREQ}"
}

case "$1" in
    prereqs)
    prereqs
    exit 0
    ;;
esac

# import /usr/share/initramfs-tools/scripts/functions
. /scripts/functions

MYTAG="root-ro"
DISABLE_MAGIC_FILE="/disable-root-ro"
FACTORY_RESET_MAGIC_FILE="/reset-factory"
RESET_DONE_MAGIC_FILE="/reset-done"
RESET_SUCCESS_MAGIC_FILE="/reset-success"
RESET_FAILURE_MAGIC_FILE="/reset-error"

# parse kernel boot command line 
ROOT_RO_DRIVER=
DISABLE_ROOT_RO=
ROOT_DEVICE=
for CMD_PARAM in $(cat /proc/cmdline); do
    case ${CMD_PARAM} in
        disable-root-ro=*)
            DISABLE_ROOT_RO=${CMD_PARAM#disable-root-ro=}
            ;;
        root-ro-driver=*)
            ROOT_RO_DRIVER=${CMD_PARAM#root-ro-driver=}
            ;;
        root=*)
            ROOT_DEVICE=${CMD_PARAM#root=}
            ;;
    esac
done

log_info_msg()
{
	_log_msg "Info: %s\\n" "$*"
}

# check if read-only root fs is disabled
if [ ! -z "${DISABLE_ROOT_RO}" ]; then
    log_warning_msg "${MYTAG}: disabled, found boot parameter disable-root-ro=${DISABLE_ROOT_RO}"
    exit 0
fi
if [ -e "${rootmnt}${DISABLE_MAGIC_FILE}" ]; then
    log_warning_msg "${MYTAG}: disabled, found file ${rootmnt}${DISABLE_MAGIC_FILE}"
    exit 0
fi

# generic settings
# ${ROOT} and ${rootmnt} are predefined by caller of this script. Note that
# the root fs ${rootmnt} it mounted readonly on the initrams, which fits nicely
# for our purposes.
ROOT_RO=/mnt/root-ro
ROOT_RW=/data/.root-rw
ROOT_RW_UPPER=${ROOT_RW}/upper
ROOT_RW_WORK=${ROOT_RW}/work
RESET_BACKUP_DIR=/data/.reset-backup

# check if ${ROOT_RO_DRIVER} is defined, otherwise set default
if [ -z "${ROOT_RO_DRIVER}" ]; then
    ROOT_RO_DRIVER=overlay
fi
# settings based in ${ROOT_RO_DRIVER}, stop here if unsupported.
case ${ROOT_RO_DRIVER} in
    overlay)
        MOUNT_PARMS="-t overlay -o lowerdir=${ROOT_RO},upperdir=${ROOT_RW_UPPER},workdir=${ROOT_RW_WORK} overlay ${rootmnt}"
        ;;
    aufs)
        MOUNT_PARMS="-t aufs -o dirs=${ROOT_RW}:${ROOT_RO}=ro aufs-root ${rootmnt}"
        ;;
    *)
        panic "${MYTAG} ERROR 1: invalid ROOT_RO_DRIVER ${ROOT_RO_DRIVER}"
        ;;
esac


# check if kernel module exists
modprobe -qb ${ROOT_RO_DRIVER}
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 2: missing kernel module ${ROOT_RO_DRIVER}"
    exit 0
fi

# create mount point for data partition
mkdir /data

# make the mount point on the init root fs for the data partition
[ -d /data ] || mkdir -p /data
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 3.1: failed to create /data"
    exit 0
fi

# Because mount of busybox doesn't support mount by label a workaround will be used. 
# With that workaround the script will work for Systems where the data partition is p5 (SD-Card or eMMC)

# Alternatively we would need to make the findfs tool accessable to find the corresponding /dev/* 
# to the label [ DEVICE=$(findfs LABEL=data) ]
# For that we would need to create a hook script which copies the binary and the necessary libraries 
# to the initramfs

# get the device name of the data partition (%? removes the last character)
DATA_DEVICE=${ROOT_DEVICE%?}5

# mount data partiton to the created mountpoint
mount ${DATA_DEVICE} /data

# check if factory reset is requested
if [ -e "${ROOT_RW}${FACTORY_RESET_MAGIC_FILE}" ]; then
    log_info_msg "${MYTAG} Factory reset requested using magic file ${ROOT_RW}${FACTORY_RESET_MAGIC_FILE}"

    # remove magic file which requested the reset
    rm ${ROOT_RW}${FACTORY_RESET_MAGIC_FILE}

    # collect excluded paths from files in /etc/cel/resetexcludes.d/ (base RO layer from buildtime)
    # and /data/.root-rw/etc/cel/resetexcludes.d/ (overlayfs upper layer from runtime)
    RESET_EXCLUDE_PATHS=""
    for exclude_conf_dir in "${rootmnt}/etc/cel/resetexcludes.d" "${ROOT_RW_UPPER}/etc/cel/resetexcludes.d"; do
        [ -d "${exclude_conf_dir}" ] || continue
        for exclude_file in "${exclude_conf_dir}"/*; do
            [ -f "${exclude_file}" ] || continue
            while IFS= read -r exclude_path || [ -n "${exclude_path}" ]; do
                # trim leading/trailing whitespace, so indented blank lines, comments
                # and paths are recognized correctly regardless of surrounding whitespace
                exclude_path="${exclude_path#"${exclude_path%%[![:space:]]*}"}"
                exclude_path="${exclude_path%"${exclude_path##*[![:space:]]}"}"
                case "${exclude_path}" in
                    # Empty lines and comments are ignored
                    ''|'#'*)
                        continue
                        ;;
                    # Paths outside of /data are ignored
                    /data/*) ;;
                    *)
                        log_warning_msg "${MYTAG}: excluded path ${exclude_path} is not under /data/, skipping"
                        continue
                        ;;
                esac
                RESET_EXCLUDE_PATHS="${RESET_EXCLUDE_PATHS} ${exclude_path}"
            done < "${exclude_file}"
        done
    done

    # backup excluded paths to a temporary directory on the data partition before wiping
    if [ -n "${RESET_EXCLUDE_PATHS}" ]; then
        log_info_msg "${MYTAG}: Backing up excluded paths before factory reset"
        mkdir -p "${RESET_BACKUP_DIR}"
        for exclude_path in ${RESET_EXCLUDE_PATHS}; do
            if [ ! -e "${exclude_path}" ]; then
                log_warning_msg "${MYTAG}: excluded path ${exclude_path} does not exist, skipping"
                continue
            fi
            exclude_path_rel="${exclude_path#/data/}"
            mkdir -p "${RESET_BACKUP_DIR}/$(dirname "${exclude_path_rel}")"
            if ! mv -f "${exclude_path}" "${RESET_BACKUP_DIR}/${exclude_path_rel}"; then
                log_warning_msg "${MYTAG}: failed to backup excluded path ${exclude_path}, skipping"
            fi
        done
        log_success_msg "${MYTAG}: Backup of excluded paths completed"
    fi

	# delete upper directory if existing
	if [ -d ${ROOT_RW_UPPER} ]; then
    	if ! rm -rf ${ROOT_RW_UPPER}; then
            # in case of error don't stop here (no panic) but pass information to user
            log_failure_msg "${MYTAG} Failed to remove ${ROOT_RW_UPPER}"
            # create a magic file to indicate an error for the service after bootup
            touch ${ROOT_RW}${RESET_FAILURE_MAGIC_FILE}
        else
            log_success_msg "${MYTAG}: Upper layer successfully removed"
        fi
	fi

    # delete content on data partition except of hidden files directly under /data (e.g. .root-rw is retained)
    #   unfortunally with sh we can't use the easy syntax "rm -rf /data/!(.root-rw)" we would use in bash
    #   but with find we reach the similar result, although it's a bit more complex
    if ! find /data -mindepth 1 -maxdepth 1 ! \( -name ".root-rw" -o -name ".reset-backup" \) -exec rm -rf {} +; then
        log_failure_msg "${MYTAG} Failed to remove files and directories from /data"
    else
        log_success_msg "${MYTAG}: Data directory successfully removed"
    fi

    # restore factory defaults of data partition
    if ! tar -xzf "${rootmnt}/etc/cel/factory-defaults/data-factory-defaults.tar.gz" -C /data/; then
        log_failure_msg "${MYTAG} Failed to restore factory defaults of data partition"
        # create a magic file to indicate an error for the service after bootup
        touch ${ROOT_RW}${RESET_FAILURE_MAGIC_FILE}
    else
        log_success_msg "${MYTAG}: Factory defaults of data partition successfully restored"
    fi

    # restore excluded paths on top of the factory defaults, then clean up the backup
    if [ -d "${RESET_BACKUP_DIR}" ]; then
        log_info_msg "${MYTAG}: Restoring excluded paths after factory reset"
        for exclude_path in ${RESET_EXCLUDE_PATHS}; do
            exclude_path_rel="${exclude_path#/data/}"
            exclude_backup="${RESET_BACKUP_DIR}/${exclude_path_rel}"
            [ -e "${exclude_backup}" ] || continue
            # Create the whole path to the excluded path, because it might not exist yet
            mkdir -p "$(dirname "${exclude_path}")"
            if [ -d "${exclude_backup}" ]; then
                # Directory
                rm -rf "${exclude_path}"
                if ! mv -f "${exclude_backup}" "${exclude_path}"; then
                    log_warning_msg "${MYTAG}: failed to restore excluded directory ${exclude_path}"
                fi
            else
                # File
                if ! mv -f "${exclude_backup}" "${exclude_path}"; then
                    log_warning_msg "${MYTAG}: failed to restore excluded file ${exclude_path}"
                fi
            fi
        done
        rm -rf "${RESET_BACKUP_DIR}"
        log_success_msg "${MYTAG}: Recovery of excluded paths completed"
    fi

    # if there was no error set a magic file for success
    if [ ! -e ${ROOT_RW}${RESET_FAILURE_MAGIC_FILE} ]; then
        # create a magic file to indicate the success of the reset for the service after bootup
        touch ${ROOT_RW}${RESET_SUCCESS_MAGIC_FILE}
    fi

    # create magic file to give service after boot up the ability to check if reset was done
    #   for being prepared for other reset targets it's a general reset done magic file
    touch ${ROOT_RW}${RESET_DONE_MAGIC_FILE}

    # write cache to device
    sync
fi

# make the mount point on the init root fs ${ROOT_RW}
[ -d ${ROOT_RW} ] || mkdir -p ${ROOT_RW}
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 3.2: failed to create ${ROOT_RW}"
    exit 0
fi

# make the mount point on the init root fs ${ROOT_RO}
[ -d ${ROOT_RO} ] || mkdir -p ${ROOT_RO}
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 4: failed to create ${ROOT_RO}"
    exit 0
fi

# make the mount point on the init root fs ${ROOT_WORKDIR}
[ -d ${ROOT_WORKDIR} ] || mkdir -p ${ROOT_WORKDIR}
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 5: failed to create ${ROOT_WORKDIR}"
    exit 0
fi

# create the upper and work directory
if [ "${ROOT_RO_DRIVER}" = "overlay" ]; then
    [ -d ${ROOT_RW_UPPER} ] || mkdir -p ${ROOT_RW_UPPER}
    if [ $? -ne 0 ]; then
        log_failure_msg "${MYTAG} ERROR 6.1: failed to create ${ROOT_RW_UPPER}"
        exit 0
    fi

    [ -d ${ROOT_RW_WORK} ] || mkdir -p ${ROOT_RW_WORK}
    if [ $? -ne 0 ]; then
        log_failure_msg "${MYTAG} ERROR 6.2: failed to create ${ROOT_RW_WORK}"
        exit 0
    fi

fi

# root is mounted on ${rootmnt}, move it to ${ROOT_RO}.
mount -o move ${rootmnt} ${ROOT_RO}
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 7: failed to move root away from ${rootmnt} to ${ROOT_RO}"
    exit 0
fi

# there is nothing left at ${rootmnt} now. So for any error we get we should
# either do recovery to restore ${rootmnt} for drop to a initramfs shell using
# "panic". Otherwise the boot process is very likely to fail with even more
# errors and leave the system in a wired state.

# mount virtual fs ${rootmnt} with rw-fs ${ROOT_RW} on top or ro-fs ${ROOT_RO}.
mount ${MOUNT_PARMS}
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 8: failed to create new ro/rw layerd ${rootmnt}"
    # do recovery and try resoring the mount for ${rootmnt}
    mount -o move ${ROOT_RO} ${rootmnt}
    if [ $? -ne 0 ]; then
       # thats bad, drop to shell to let the user try fixing this
       panic "${MYTAG} RECOVERY ERROR: failed to move ${ROOT_RO} back to ${rootmnt}"
    fi
    exit 0
fi

# now the real root fs is on ${ROOT_RO} of the init file system, our layered
# root fs is set up at ${rootmnt}. So we can write anywhere in {rootmnt} and the
# changes will end up in ${ROOT_RW} while ${ROOT_RO} it not touched. However
# ${ROOT_RO} and ${ROOT_RW} are on the initramfs root fs, which will be removed
# an replaced by ${rootmnt}. Thus we must move ${ROOT_RO} and ${ROOT_RW} to the
# rootfs visible later, ie. ${rootmnt}${ROOT_RO} and ${rootmnt}${ROOT_RO}.
# Since the layered ro/rw is already up, these changes also end up on
# ${ROOT_RW} while ${ROOT_RO} is not touched.

# move mount from ${ROOT_RO} to ${rootmnt}${ROOT_RO}
[ -d ${rootmnt}${ROOT_RO} ] || mkdir -p ${rootmnt}${ROOT_RO}
mount -o move ${ROOT_RO} ${rootmnt}${ROOT_RO}
if [ $? -ne 0 ]; then
    log_failure_msg "${MYTAG} ERROR 9: failed to move ${ROOT_RO} to ${rootmnt}${ROOT_RO}"
    exit 0
fi