#!/bin/sh
set -e

# /lib/bilibop/lockfs_mount_helper {{{
# Mount helper script for 'lockfs' filesystem type entries in /etc/fstab.
# This script cannot be run manually. The expected way to run it is the
# following:
# 1. Enable bilibop-lockfs:
#    * set BILIBOP_LOCKFS to "true" in /etc/bilibop/bilibop.conf or
#    * append 'lockfs' parameter in the boot commandline
# 2. One time '/' is an aufs mountpoint, the temporary /etc/fstab is
#    modified to replace filesystem types (third field) of some entries
#    by 'lockfs' (options are modified too to remember the original
#    fstype).
# 3. /sbin/mount.lockfs is created if it don't already exist. This can be a
#    symlink to /lib/bilibop/lockfs_mount_helper if this helper is executable,
#    or a copy of the helper (followed by chmod +x) if the helper is not
#    executable. If the helper is missing, /sbin/mount.lockfs will be a
#    poor fallback to call mount normally.
# 4. /etc/fstab is parsed by 'mount -a', and then mount calls mount.lockfs
#    with the proper arguments when a 'lockfs' fstype is encountered.
# }}}

PATH="/sbin:/bin"

usage() {
    cat <<EOF
${0##*/}: mount helper script for bilibop-lockfs.
This script can not be run manually, but only by a mount process,
and only if bilibop-lockfs is enabled.
EOF
}

# mount_fallback() =========================================================={{{
# What we want is: mount a device on its original mountpoint and rewrite the
# fstab entry to keep it consistent. This function should be called in case of
# error or if the device is whitelisted. This function parses the arguments of
# the script itself (i.e. mount_fallback "$@").
mount_fallback() {
    ${DEBUG} && echo "> mount_fallback $@" >&2
    local opt options= fstype=
    for opt in $(IFS=','; echo ${4}); do
        case "${opt}" in
            fstype=*)
                eval "${opt}"
                ;;
            *)
                options="${options:+${options},}${opt}"
                ;;
        esac
    done
    sed -i "s;^\s*\([^#][^ ]\+\s\+${2}\s\+\)lockfs\s.*;\1${fstype:-auto} ${options:-defaults} 0 0;" /etc/fstab
    mount ${flags} ${1} ${2} ${fstype:+-t ${fstype}} ${options:+-o ${options}}
}
# ===========================================================================}}}

# Works only if the parent process is /bin/mount:
if [ "$(readlink -f /proc/${PPID}/exe)" != "/bin/mount" ]; then
    usage >&2
    exit 3
fi

. /lib/bilibop/common.sh
get_bilibop_variables
get_udev_root

# Works only if the root filesystem is already managed by bilibop-lockfs:
if is_aufs_mountpoint -q / && [ -f "${BILIBOP_RUNDIR}/lock" ]; then
    LOCKFS="true"
    robr="$(aufs_readonly_branch /)"
    rwbr="$(aufs_writable_branch /)"
else
    echo "${0##*/}: bilibop-lockfs is disabled." >&2
    exit 1
fi

# Some configurations can have been overridden from the boot commandline:
for param in $(cat /proc/cmdline); do
    case "${param}" in
        lockfs=*)
            for policy in $(IFS=','; echo ${param#lockfs=}); do
                case "${policy}" in
                    default)
                        BILIBOP_LOCKFS_POLICY=""
                        BILIBOP_LOCKFS_WHITELIST=""
                        BILIBOP_LOCKFS_SIZE=""
                        ;;
                    hard|soft)
                        BILIBOP_LOCKFS_POLICY="${policy}"
                        ;;
                    all)
                        BILIBOP_LOCKFS_WHITELIST=""
                        ;;
                    -/*)
                        BILIBOP_LOCKFS_WHITELIST="${BILIBOP_LOCKFS_WHITELIST:+${BILIBOP_LOCKFS_WHITELIST} }${policy#-}"
                        ;;
                esac
            done
            ;;
    esac
done

# But if there is a physical lock, it takes precedence:
if [ -f "${BILIBOP_RUNDIR}/plocked" ]; then
    . ${BILIBOP_RUNDIR}/plocked
fi

# the mount(8) command, after parsing commandline arguments and/or /etc/fstab,
# always provides arguments to the helper scripts in this order:
# FILESYSTEM MOUNTPOINT [FLAGS] -o MOUNTOPTIONS
# where FLAGS are generic, not filesystem specific: -n, -s, -v for example; -r
# (or --read-only) and -w (or --rw or --read-write) are translated to -o ro and
# -o rw respectively by the mount command itself.

while [ "${1}" ]; do
    case "${1}" in
        -o)
            MNTARGS="${MNTARGS:+${MNTARGS} }${1} ${2}"
            shift 2
            ;;
        -*)
            # Do not skip other options (-n, -s, -v), but take them
            # apart: we will reuse them for each mount invocation.
            flags="${flags:+${flags} }${1}"
            shift
            ;;
        *)
            MNTARGS="${MNTARGS:+${MNTARGS} }${1}"
            shift
            ;;
    esac
done

# Reinitialize script arguments
eval set -- "${MNTARGS}"

if [ -b "${1}" ]; then
    device="${1}"
    # Check if this device is whitelisted:
    if [ -n "${BILIBOP_LOCKFS_WHITELIST}" ]; then
        # Query ID_FS_* udev environment variables of the device:
        eval $(query_udev_envvar $(readlink -f ${device}))
        if [ -z "${ID_FS_USAGE}" ]; then
            eval $(blkid -o udev -p ${device})
        fi
        [ "${ID_FS_USAGE}" = "filesystem" -o "${ID_FS_USAGE}" = "crypto" ] &&
        for skip in ${BILIBOP_LOCKFS_WHITELIST}; do
            case "${skip}" in
                UUID=${ID_FS_UUID}|LABEL=${ID_FS_LABEL}|TYPE=${ID_FS_TYPE})
                    LOCKFS="false"
                    break
                    ;;
            esac
        done
    fi

elif [ -f "${1}" ]; then
    lofile="${1}"
    LOFILE="${robr}${lofile}"

else
    # There is no block device to mount (here 'block device' includes
    # files associated to a loop device). Bind mounts and remote fs
    # should have been discarded by the bilibop-lockfs script in the
    # initramfs...
    LOCKFS="false"
fi

# If bilibop-lockfs is not enabled (the device is whitelisted, or we don't
# know how to manage it), rewrite the fstab entry and do a normal mount:
if [ "${LOCKFS}" != "true" ]; then
    mount_fallback "${@}"
    exit $?
fi

mntpnt="${2}"
options="${4}"

# Parse mount options. Two cases:
# 1. the block device will be mounted with the same options than in the
#    original fstab entry, plus 'ro'.
# 2. the tmpfs will be mounted with only some options of the previous:
#    ro, nodev, noexec, nosuid, if they exist.

for opt in $(IFS=','; echo ${options}); do
    # 1. Options for the readonly branch:
    case "${opt}" in
        fstype=*)
            eval "${opt}"
            ;;
        rw)
            ;;
        *)
            robr_opts="${robr_opts:+${robr_opts},}${opt}"
            ;;
    esac

    # 2. Options for the writable branch:
    case "${opt}" in
        ro|nodev|noexec|nosuid)
            rwbr_opts="${rwbr_opts:+${rwbr_opts},}${opt}"
            ;;
        *)
            ;;
    esac
done

# Each readonly branch is mounted under the subtree of the main readonly
# branch (/aufs/ro) and each writable branch is mounted under the subtree
# of the main writable branch (/aufs/rw):
robr="${robr}${mntpnt}"
rwbr="${rwbr}${mntpnt}"

# If the policy is not explicitly set to 'soft', set the block device as
# readonly, and use 'rr' aufs option to improve performances:
if [ "${BILIBOP_LOCKFS_POLICY}" = "soft" ]; then
    RO="ro"
else
    RO="rr"
    [ -b "${device}" ] && blockdev --setro ${device}
fi

# Try to mount the readonly branch. In case of failure, undo what has been
# done before, do a normal mount, rewrite the fstab entry to be consistent
# with that, and exit:
if ! mount ${flags} ${fstype:+-t ${fstype}} -o ${robr_opts:+${robr_opts},}ro ${device:-${LOFILE}} ${robr}; then
    [ "${RO}" = "rr" ] && [ -b "${device}" ] && blockdev --setrw "${device}"
    mount_fallback "${@}"
    exit 3
fi

# The amount of RAM to allow to this mountpoint:
SIZE=
for size in ${BILIBOP_LOCKFS_SIZE}; do
    case "${size}" in
        ${mntpnt}=[1-9]*)
            SIZE="$(printf "${size#${mntpnt}=}" | grep '^[1-9][0-9]*[KkMmGg%]\?$')"
            break
            ;;
    esac
done

# Create the mountpoint (it should not exist before this step):
if [ ! -d "${rwbr}" ]; then
    mkdir -p ${rwbr}
    grep -q "^${mntpnt}$" ${BILIBOP_RUNDIR}/lock ||
    echo "${mntpnt}" >>${BILIBOP_RUNDIR}/lock
fi

# Try to mount the writable branch, and in case of failure, undo what
# has been done before, etc.
if ! mount ${flags} -t tmpfs -o ${rwbr_opts:+${rwbr_opts},}${SIZE:+size=${SIZE},}mode=0755 tmpfs ${rwbr}; then
    umount ${robr}
    [ "${RO}" = "rr" ] && [ -b "${device}" ] && blockdev --setrw "${device}"
    mount_fallback "${@}"
    exit 3
fi

# Fix permissions and ownership of the writable branch (and catch the values;
# they will be reused later):

mod="$(LC_ALL=C chmod -v --reference="${robr}" "${rwbr}" | sed 's;.* \([0-7]\{4\}\) (.\+)$;\1;')"
own="$(LC_ALL=C chown -v --reference="${robr}" "${rwbr}" | sed 's;.* \([^:]\+:[^:]\+\)$;\1;')"

owner="${own%:*}"
if [ "${owner}" != "root" ]; then
    uid="$(grep "^${owner}:" /etc/passwd | sed 's;^\([^:]*:\)\{2\}\([^:]\+\):.*;\2;')"
fi

group="${own#*:}"
if [ "${group}" != "root" ]; then
    gid="$(grep "^${group}:" /etc/group | sed 's;^\([^:]*:\)\{2\}\([^:]\+\):.*;\2;')"
fi

# Try to mount the aufs now. In case of failure, undo what has been done
# before, etc.
if ! mount ${flags} -t aufs -o br:${rwbr}=rw:${robr}=${RO} none ${mntpnt}; then
    umount ${robr}
    umount ${rwbr}
    [ "${RO}" = "rr" ] && [ -b "${device}" ] && blockdev --setrw "${device}"
    mount_fallback "${@}"
    exit 3
fi

# All is OK. So we can rewrite fstab entry to reflect the real mounts. This
# can be important for clean unmounts at shutdown (for the case a filesystem
# is remounted rw during a session).
robr_line="${device:-${LOFILE}} ${robr} ${fstype:-auto} ${robr_opts:+${robr_opts},}ro 0 0"
rwbr_line="tmpfs ${rwbr} tmpfs ${rwbr_opts:+${rwbr_opts},}${SIZE:+size=${SIZE},}${uid:+uid=${uid},}${gid:+gid=${gid},}mode=${mod} 0 0"
aufs_line="none ${mntpnt} aufs br:${rwbr}=rw:${robr}=${RO} 0 0"

sed -i "s;^\s*[^#][^ ]\+\s\+${mntpnt}\s\+lockfs\s.*;${robr_line}\n${rwbr_line}\n${aufs_line};" /etc/fstab

# vim: et sts=4 sw=4 ts=4
