#!/bin/sh
set -e

# /usr/libexec/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 and reboot, or
#    * reboot and append 'lockfs' parameter in the boot commandline
# 2. Once '/' is an union filesystem (aufs or overlay 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. /etc/fstab is parsed by 'mount -a', and then mount calls mount.lockfs
#    with the proper arguments when a 'lockfs' fstype is encountered.
# }}}

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}"
                ;;
            remount)
                ;;
            rw)
                [ "${BILIBOP_LOCKFS_FALLBACK_POLICY}" = "asis" ] ||
                options="${options:+${options},}ro"
                ;;
            *)
                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 the mount command (/bin/mount for backward
# compatibility, or /usr/bin/mount). This will ensure arguments are passed in a
# specific order, and shorten the code to parse them.
case "$(readlink -f /proc/${PPID}/exe)" in
    "/bin/mount"|"/usr/bin/mount")
        ;;
    *)
        usage >&2
        exit 3
        ;;
esac

. /usr/lib/bilibop/common.sh
get_bilibop_variables

# Works only if the root filesystem is already managed by bilibop-lockfs:
if is_aufs_mountpoint -q / && [ -f "${BILIBOP_RUNDIR}/lock" ]; then
    LOCKFS="true"
    METHOD="aufs"
    ROBR="$(aufs_readonly_branch /)"
    RWBR="$(aufs_writable_branch /)"
elif is_overlay_mountpoint -q / && [ -f "${BILIBOP_RUNDIR}/lock" ]; then
    LOCKFS="true"
    METHOD="overlay"
    ROBR="$(overlay_lowerdir /)"
    RWBR="$(overlay_upperdir /)"
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=""
                        BILIBOP_LOCKFS_FALLBACK_POLICY=""
                        BILIBOP_LOCKFS_PATH_SCHEME=""
                        ;;
                    hard|soft)
                        BILIBOP_LOCKFS_POLICY="${policy}"
                        ;;
                    ro|asis)
                        BILIBOP_LOCKFS_FALLBACK_POLICY="${policy}"
                        ;;
                    isolated|hybrid|nested)
                        BILIBOP_LOCKFS_PATH_SCHEME="${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 and allocate them to the proper branches or their
# union:
# 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:
#    nodev, noexec, nosuid, if they exist.
# 3. when set in the persistent fstab, the ro flag could be deferred to
#    the tmpfs mount (and the union will inherit of it), but this would
#    forbid creation of subdirectories for submountpoints.

for opt in $(IFS=','; echo ${options}); do
    case "${opt}" in
        fstype=*)
            eval "${opt}"
            ;;
        rw|remount)
            ;;
        ro)
            union_opts="${union_opts:+${union_opts},}${opt}"
            ;;
        nodev|noexec|nosuid)
            rwbr_opts="${rwbr_opts:+${rwbr_opts},}${opt}"
            robr_opts="${robr_opts:+${robr_opts},}${opt}"
            ;;
        *)
            robr_opts="${robr_opts:+${robr_opts},}${opt}"
            ;;
    esac
done

# For aufs:
# Several schemes are available (there was only one until bilibop 0.4.23).
# 1. nested
# 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). For examples:
# <readonly branch>  + <writable branch>  -> <aufs mount>
# /aufs/ro           + /aufs/rw           -> /
# /aufs/ro/boot      + /aufs/rw/boot      -> /boot
# /aufs/ro/usr/local + /aufs/rw/usr/local -> /usr/local
# 2. isolated
# Each readonly branch is mounted beside its writable branch in a dedicated
# subdirectory referring to the union fs mount point. For examples:
# <readonly branch>  + <writable branch>  -> <aufs mount>
# /aufs/ro           + /aufs/rw           -> /
# /aufs/boot/ro      + /aufs/boot/rw      -> /boot
# /aufs/usr/local/ro + /aufs/usr/local/rw -> /usr/local
# 3. hybrid
# Each writable branch is mounted in a dedicated subdirectory referring to
# the union fs mount point, and readonly branches share a same tree.
# <readonly branch>  + <writable branch>  -> <aufs mount>
# /aufs/ro           + /aufs/rw           -> /
# /aufs/ro/boot      + /aufs/boot/rw      -> /boot
# /aufs/ro/usr/local + /aufs/usr/local/rw -> /usr/local
#
# For overlay:
# A dedicated temporary filesystem (tmpfs) is set for the first overlay mount
# point, and readonly and writable branches (and also workdir) are
# subdirectories of the tmpfs mountpoint. Next mounts (done by this script)
# may use the same *isolated* scheme or a hybrid one. Nested scheme is not
# possible. For examples (where /overlay and /overlay/boot are the tmpfs
# mountpoints):
# 1. isolated
# <lowerdir>       + <upperdir>       + <workdir>         -> <overlay>
# /overlay/ro      + /overlay/rw      + /overlay/.rw      -> /
# /overlay/boot/ro + /overlay/boot/rw + /overlay/boot/.rw -> /boot
# 2. hybrid
# <lowerdir>       + <upperdir>       + <workdir>         -> <overlay>
# /overlay/ro      + /overlay/rw      + /overlay/.rw      -> /
# /overlay/ro/boot + /overlay/boot/rw + /overlay/boot/.rw -> /boot
#
# In any non-isolated scheme, we will create symlinks to make that
# ${UNION}${MNTPNT}/r{o,w} always exist.
#
# /aufs/usr/local/ro -> ../../ro/usr/local
# /aufs/usr/local/rw -> ../../rw/usr/local
# or
# /overlay/boot/ro -> ../ro/boot
# /overlay/rw/boot -> ../boot/rw

if [ "${METHOD}" = "aufs" ]; then
    case "${BILIBOP_LOCKFS_PATH_SCHEME}" in
        isolated)
            # Use clearly separated mountpoints (this breaks symlinks over fs
            # boundaries).
            robr="${ROBR%/*}${mntpnt}/ro"   # /aufs/usr/local/ro        |ro not symlinked
            rwbr="${RWBR%/*}${mntpnt}/rw"   # /aufs/usr/local/rw        |rw not symlinked
            ;;
        hybrid)
            # ro branches are nested, rw are isolated.
            roln="${ROBR%/*}${mntpnt}/ro"   # /aufs/usr/local/ro        |ro symlink
            rwln="${RWBR}${mntpnt}"         # /aufs/rw/usr/local        |rw symlink
            robr="${ROBR}${mntpnt}"         # /aufs/ro/usr/local        |ro symlinked
            rwbr="${RWBR%/*}${mntpnt}/rw"   # /aufs/usr/local/rw        |rw symlinked
            ;;
        *)
            # This scheme is only available with aufs; this is the only one
            # available in bilibop =<0.4.23. Symlinks are now created for
            # compatibility with other schemes
            roln="${ROBR%/*}${mntpnt}/ro"   # /aufs/usr/local/ro        |ro symlink
            rwln="${RWBR%/*}${mntpnt}/rw"   # /aufs/usr/local/rw        |rw symlink
            robr="${ROBR}${mntpnt}"         # /aufs/ro/usr/local        |ro symlinked
            rwbr="${RWBR}${mntpnt}"         # /aufs/rw/usr/local        |rw symlinked
            ;;
    esac
    temp="${rwbr}"

elif [ "${METHOD}" = "overlay" ]; then
    # Upperdirs (writable branches) can't be nested, as they are a subdirectory
    # of a tmpfs mountpoint, not the mountpoint itself.
    TEMP="$(find_mountpoint ${RWBR})"   # /overlay
    temp="${TEMP}${mntpnt}"             # /overlay/usr/local
    work="${temp}/.rw"                  # /overlay/usr/local/.rw
    rwbr="${temp}/rw"                   # /overlay/usr/local/rw         |rw not nested
    case "${BILIBOP_LOCKFS_PATH_SCHEME}" in
        hybrid)
            # ro branches are nested, rw are isolated.
            robr="${ROBR}${mntpnt}"         # /overlay/ro/usr/local     |ro symlinked
            rwln="${RWBR}${mntpnt}"         # /overlay/rw/usr/local     |rw symlink
            roln="${temp}/ro"               # /overlay/usr/local/ro     |ro symlink
            ;;
        *)
            # isolated
            robr="${temp}/ro"           # /overlay/usr/local/ro         |ro not symlinked
            ;;
    esac
fi

# 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

# 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

# Prepare the tmpfs mountpoint (it should not exist before this step):
if [ ! -d "${temp}" ]; then
    mkdir -p ${temp}
    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.
mountpoint -q ${temp} ||
if ! mount ${flags} -t tmpfs -o ${rwbr_opts:+${rwbr_opts},}${SIZE:+size=${SIZE},}mode=0755 tmpfs ${temp}; then
    [ "${RO}" = "rr" ] && [ -b "${device}" ] && blockdev --setrw "${device}"
    mount_fallback "${@}"
    exit 3
fi

# Create the needed directories; some may not exist, depending on the union fs
# type and the mount scheme.
[ -d "${robr}" ] || mkdir -p ${robr}
[ -d "${rwbr}" ] || mkdir -p ${rwbr}
if [ "${METHOD}" = "overlay" ]; then
    mkdir -p ${work}
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:
mountpoint -q ${robr} ||
if ! mount ${flags} ${fstype:+-t ${fstype}} -o ${robr_opts:+${robr_opts},}ro ${device:-${LOFILE}} ${robr}; then
    umount ${temp}
    [ "${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

# Now set the union filesystem mount options
if [ "${METHOD}" = "aufs" ]; then
    UNIONFS_OPTS="${union_opts:+${union_opts},}br:${rwbr}=rw:${robr}=${RO}"
elif [ "${METHOD}" = "overlay" ]; then
    UNIONFS_OPTS="${union_opts:+${union_opts},}lowerdir=${robr},upperdir=${rwbr},workdir=${work}"
fi

# Try to mount the union fs now. In case of failure, undo what has been done
# before, etc.
#
# If the virtual filesystem's arbitrary name exists in the current directory,
# then mount will record it (in /proc/mounts) by translating it as an absolute
# path (here, /aufs or /overlay), which may confuse users parsing df or mount
# output. So we move somewhere else before calling mount.
cd /tmp
mountpoint -q ${mntpnt} ||
if ! mount ${flags} -t ${METHOD} -o ${UNIONFS_OPTS} ${METHOD} ${mntpnt}; then
    umount ${robr}
    umount ${temp}
    [ "${RO}" = "rr" ] && [ -b "${device}" ] && blockdev --setrw "${device}"
    mount_fallback "${@}"
    exit 3
fi
cd ${OLDPWD}

# Create symlinks, for example:
#
# (/overlay/)ro/usr/local: the target
# (/overlay/)usr/local/ro: the symlink
# ln -s ../../ro/usr/local /overlay/usr/local/ro
#
# (/overlay/)usr/local/rw: the target
# (/overlay/)rw/usr/local: the symlink
# ln -s ../../usr/local/rw /overlay/rw/usr/local
prefix="$(echo ${mntpnt} | sed -e 's,[^/],,g; s,/,../,g')"

if [ "${METHOD}" = "aufs" ]; then
    case "${BILIBOP_LOCKFS_PATH_SCHEME}" in
        isolated)
            ;;
        hybrid)
            [ -d "${rwln%/*}" ] || mkdir -p ${rwln%/*}
            [ -d "${roln%/*}" ] || mkdir -p ${roln%/*}
            [ -h "${rwln}" ] || ln -s ${prefix%/}${mntpnt}/rw ${rwln}
            [ -h "${roln}" ] || ln -s ${prefix%/}/ro${mntpnt} ${roln}
            ;;
        *)
            [ -d "${rwln%/*}" ] || mkdir -p ${rwln%/*}
            [ -d "${roln%/*}" ] || mkdir -p ${roln%/*}
            [ -h "${rwln}" ] || ln -s ${prefix%/}/rw${mntpnt} ${rwln}
            [ -h "${roln}" ] || ln -s ${prefix%/}/ro${mntpnt} ${roln}
            ;;
    esac

elif [ "${METHOD}" = "overlay" ]; then
    case "${BILIBOP_LOCKFS_PATH_SCHEME}" in
        hybrid)
            [ -d "${rwln%/*}" ] || mkdir -p ${rwln%/*}
            [ -d "${roln%/*}" ] || mkdir -p ${roln%/*}
            [ -h "${rwln}" ] || ln -s ${prefix%/}${mntpnt}/rw ${rwln}
            [ -h "${roln}" ] || ln -s ${prefix%/}/ro${mntpnt} ${roln}
            ;;
        *)
            ;;
    esac
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 ${temp} tmpfs ${rwbr_opts:+${rwbr_opts},}${SIZE:+size=${SIZE},}${uid:+uid=${uid},}${gid:+gid=${gid},}mode=${mod} 0 0"
union_line="${METHOD} ${mntpnt} ${METHOD} ${UNIONFS_OPTS} 0 0"

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

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