#!/usr/bin/env bash # Copyright (C) 2018 Luke Shumaker # Copyright (C) 2023-2024 Umorpha Systems # SPDX-License-Identifier: AGPL-3.0-or-later declare -r NAME=osi-mount declare -r VERSION=20231023 set -euE install_prefix="$(realpath --logical --canonicalize-missing -- "${BASH_SOURCE[0]}/../..")" readonly install_prefix source "${install_prefix}/lib/osi.sh" source "${install_prefix}/lib/argparse.sh" main() { opt_specs+=(': --inside : : internal-use only') opt_visit:inside() { argparse:setmode inside; } opt_specs+=(': --user : : mount the image RO, run COMMAND as SUDO_USER') opt_specs+=(': --root : : mount the image RW, run COMMAND as root, protect the rest of the system') local arg_user= opt_visit:user() { set_arg_user 'user'; } opt_visit:root() { set_arg_user 'root'; } set_arg_user() { if [[ -n $arg_user ]]; then argparse:error "Multiple --user/--root flags given" fi arg_user=$1 } opt_final:root() { if [[ -z $arg_user ]]; then argparse:error "Must specify either --root or --user" fi } opt_specs+=(': --rwdir : DIR : keep DIR read+write, even when --root') local arg_rwdirs=() opt_visit:rwdir() { arg_rwdirs+=("$1"); } opt_specs+=('-o : --options : LIST : comma-separated list of mount options') opt_visit:options() { arg_mountopt="$1"; } local arg_device= local arg_mountopt= local arg_cmd=() opt_positional() { case "$#" in 0) osi.sh:error 0 "Must specify an image, mountpoint, and a command"; return;; 1) osi.sh:error 0 "Must specify a mountpoint and a command"; return;; 2) osi.sh:error 0 "Must specify a command"; return;; esac arg_device=$(realpath -- "$1") arg_mountpoint=$2 arg_cmd=("${@:3}") } local arg_orig=("$@") argparse outside "$@" case "$opt_mode" in error) osi.sh:print "Try '%q --help' for more information" "${0##*/}" >&2 return $EXIT_INVALIDARGUMENT ;; version) osi.sh:print "%s (osi-tools) %s" "$NAME" "$VERSION" return $EXIT_SUCCESS ;; help) osi.sh:print 'Usage: sudo %s [OPTIONS] -- FILENAME.img MOUNTPOINT COMMAND...' "${0##*/}" osi.sh:print 'Operating System Image: Mount' echo # 000000000011111111112222222222333333333344444444445555555555666666666677777777778 # 012345678901234567890123456789012345678901234567890123456789012345678901234567890 osi.sh:print 'Run COMMAND with FILENAME.img mounted at MOUNTPOINT.' echo osi.sh:print 'OPTIONS:' echo "${opt_flaghelp}" | grep -v -e --inside return $EXIT_SUCCESS ;; outside) osi.sh:needs_root osi.sh:needs_sudo if out="$(losetup --associated "$arg_device")" && [[ -n $out ]]; then osi.sh:error 0 "Seems to already be mounted somewhere: %s" "$arg_device" printf '%s\n' "$out" exit $EXIT_FAILURE fi # I feel guilty pulling in the hugeness of systemd-run # just for `--property=ProtectSystem=strict`, but I # don't want to implement my own protection system. flags=( --pty --same-dir --wait --collect --quiet --setenv=SUDO_UID="$SUDO_UID" ${SOURCE_DATE_EPOCH:+"--setenv=SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH"} ${TMPDIR:+"--setenv=TMPDIR=$TMPDIR"} --property=PrivateMounts=true ) case "$arg_user" in user) :;; root) flags+=( --property=ProtectSystem=strict --property=ProtectHome=read-only --property=ReadWritePaths="$arg_device" "${arg_rwdirs[@]/#/--property=ReadWritePaths=}" ) ;; esac systemd-run "${flags[@]}" -- "${BASH_SOURCE[0]}" --inside "${arg_orig[@]}" if out="$(losetup --associated "$arg_device")" && [[ -n $out ]]; then osi.sh:error 0 "umount'ed, but file is still associated with a loop device: %s" "$arg_device" printf '%s\n' "$out" exit $EXIT_FAILURE fi ;; inside) osi.sh:needs_root osi.sh:needs_sudo case "$arg_user" in user) arg_mountopt+="${arg_mountopt:+,}ro";; root) arg_mountopt+="${arg_mountopt:+,}rw,discard";; esac cmd=(mount --no-mtab -o "$arg_mountopt" -- "$arg_device" "$arg_mountpoint") r=0 out=$("${cmd[@]}" 2>&1) || r=$? if (( r != 0 )); then osi.sh:error $r '%s: %s' "${cmd[*]@Q}" "$out" fi case "$arg_user" in user) osi.sh:sudo -u "#${SUDO_UID}" -- "${arg_cmd[@]}";; root) command -- "${arg_cmd[@]}";; esac ;; *) osi.sh:bug 'unknown opt_mode: %s' "$opt_mode";; esac } main "$@"