#!/bin/bash
#
# Start:
# 1) Bind mount the bind-root
#
# General
# 2) do a bunch of bind mounts under bind-root
#    (create directories and touch files as needed)
# 3) Munge files
#
# Add:
# 4) optionally do more bind mounts under ro-root
#    (create directories and touch files as needed)

# Clean:
# 5) do a recursive umount
# 6) Delete touched files and created directories from the *original* FS


ME=${0##*/}

USER_PW=demo:demo
ROOT_PW=root:root

BIND_ROOT=/.bind-root
REAL_ROOT=/
WORK_DIR=/tmp/$ME

EXCLUDES_DIR=/usr/local/share/excludes
VERSION_FILE=/etc/live/version/linuxfs.ver

CONF_FILE=$WORK_DIR/cleanup.conf

TEMPLATE_DIR=/usr/local/share/live-files

DEF_TZ="America/New_York"
DEF_REPO="us"

DEF_SHELL=$(grep ^DSHELL= /etc/adduser.conf 2>/dev/null | tail -1 | cut -d= -f2)
DEF_BIN=$(sed -n '/^valid_user()/,/^}/{/^\s*local ent=/{s=.*:/bin/.*=/bin=p;q}}' /live/etc/init.d/live-init 2>/dev/null)
: ${DEF_SHELL:=/usr/bin/bash}
: ${DEF_BIN:=/usr/bin}
DEF_SHELL=${DEF_BIN}/${DEF_SHELL##*/bin/}

PRETEND=

SUBID_FILES="/etc/subuid /etc/subgid"
PW_FILES="/etc/passwd /etc/shadow /etc/gshadow /etc/group $SUBID_FILES"
MUNGE_FILES="/etc/slimski.conf /etc/lightdm/lightdm.conf"
BIND_FILES="$PW_FILES"

LIVE_FILE_LIST="
/etc/desktop-session/startup
/etc/fstab
/etc/systemd/*
/etc/systemd/system-generators/systemd-fstab-generator
/etc/systemd/system-generators/systemd-gpt-auto-generator
/etc/systemd/system/wpa_supplicant@.service
/etc/systemd/system/wpa_supplicant-nl80211@.service
/etc/systemd/system/wpa_supplicant-wired@.service
/etc/grub.d/*
/etc/init.d/checkfs.sh
/etc/init.d/console-setup.sh
/etc/init.d/cron
/etc/init.d/debian/*
/etc/init.d/gpm
/etc/init.d/hwclock.sh
/etc/init.d/keyboard-setup.sh
/etc/init.d/sendsigs
/etc/init.d/slimski
/etc/init.d/udev
/etc/init.d/umountfs
/etc/issue
/etc/rc.local
/etc/rc?.d/*virtualbox*
/etc/skel/.fluxbox/menu
/etc/skel/.icewm/menu
/etc/skel/.jwm/menu
/etc/skel/Desktop/Installer.desktop
/etc/skel/.idesktop/gazelle.lnk
/etc/udev/rules.d/90-fstab-automount.rules
/lib/udev/rules.d/92-console-font.rules
/usr/share/applications/mx/minstall.desktop
/usr/share/applications/minstall.desktop
/usr/share/applications/antixsources.desktop
/usr/share/applications/persist-config.desktop
/usr/share/applications/persist_setup.desktop
/usr/share/applications/persist_save.desktop
/usr/share/applications/remaster.desktop
"

GENERAL_LIVE_FILE_LIST="
/etc/adjtime
/etc/default/*
/etc/dhcp/dhclient.conf
/etc/dnsmasq.conf
/etc/hostname
/etc/init.d/ifplugd
/etc/init.d/networking
/etc/kbd/config
/etc/lightdm/lightdm.conf
/etc/modprobe.d/*
/etc/network/interfaces
/etc/network/interfaces.d/*
/etc/slimski.conf
/lib/udev/ifupdown-hotplug
"

usage() {
    local ret=${1:-0}

    cat<<Usage
Usage: $ME [options] [commands ...]
Use bind mounts to create a filesystem that looks like the live system.

Create a config file $CONF_FILE that can be used to undo what we did.
This is needed because sometimes we create directories and touch files.

Commands:
    start               Do the main bind mount and create cleanup.conf

    status              Show status including contents of cleanup.conf

    live-files          bind mount files under $TEMPLATE_DIR/files

    general-files       bind mount files under $TEMPLATE_DIR/general-files

    read-only           Remount the bind root directory as read-only

    read-write          Remount the bind root directory as read-write

    passwd              Make general purpose password/group files

    add=<dir1,dir2,..>  Add files from the dirs template

    bind=<dir1,dir2,..>  Bind mount each dir IF it is a mountpoint

    general             Same as "empty=/etc/modprobe.d/ empty=/etc/grub.d
                        empty=/etc/network/interfaces.d/
                        all-files passwd repo timezone"

    adjtime             Reset the adjtime file but leave the utc / local selection

    cleanup             Clean up all that we did

    repo[=<country>]    Reset the apt repos to the country given (default us)

    timezone=[<zone>]   Reset the timezone back to EST or as specfied

    empty=<dir1,dir2,...>
                        Bind mount empty directories at these locations

    exclude[=<prefix>] file1 file2 ...
                        Generate an exclude list from the files listed. Prepend
                        each entry with <prefix> if it is given.  (This is a
                        convenience routine for use with mksquashfs).

    version-file[=<title>]
                        Create a standard linuxfs.ver file and bind mount it at
                        the appropriate location: $VERSION_FILE

    populate-live-files <from-dir> [<to-dir>]
                        Populate the $TEMPLATE_DIR with files extracted from
                        <from-dir>.  This is usually a mounted linuxfs file.
                        The optional <to-dir> is the top level directory under
                        which we will put the template directory.
Options:

    -b --bind-root=<dir>       The root of the new fs.   Default: $BIND_ROOT
    -e --excludes-dir=<dir>    Directory for exclude files  Default: $EXCLUDES_DIR
    -f --from=<dir>            Directory we copy from.  Default: $REAL_ROOT
    -F --Force                 Delete work directory when starting
    -h --help                  Show this help
    -o --output=<file>         Send "exclude" command output to <file> instead of stdout
    -P --pretend               Don't do anything just show what would be done
    -r --root=<user:password>  The root user and optional password
    -t --template=<dir>        Template directory
    -u --user=<user:password>  Default username and optional password
    -w --work=<dir>            Temporary directory to hold our work: $WORK_DIR

    -v --verbose               Print more

Note: if passwords aren't given then the username is used as the password.

Current defaults:
--bind-root=$BIND_ROOT
     --from=$REAL_ROOT
 --template=$TEMPLATE_DIR

     --user=$USER_PW
     --root=$ROOT_PW
Usage

    exit $ret
}

main() {

    [ $# -eq 0 ] && usage

    local param val
    local short_stack="bdefFhoprtuv"
    while [ $# -gt 0 -a -n "$1" -a -z "${1##-*}" ]; do
        param=${1#-}
        shift

       # Unstack single-letter options
        case $param in
            [$short_stack][$short_stack]*)
                if [ -z "${param//[$short_stack]/}" ]; then
                    set -- $(echo $param | sed -r 's/([a-zA-Z])/ -\1 /g') "$@"
                    continue
                fi;;
        esac

        case $param in
            -bind-root|-excludes-dir|-from|-output|-root|-template|-user|-work|[befortuw])
                    [ $# -lt 1 ] && fatal "Expected a parameter after: -$param"
                    val=$1
                    [ -n "$val" -a -z "${val##-*}" ] \
                        && fatal "Suspicious parameter after -$param: $val"
                    shift           ;;

              *=*)  val=${param#*=} ;;
                *)  val="???"       ;;
        esac

        case $param in
          -bind-root=*|b=*) BIND_ROOT=$val                    ;;
              -bind-root|b) BIND_ROOT=$val                    ;;
       -excludes-dir=*|e=*) EXCLUDES_DIR=$val                 ;;
           -excludes-dir|e) EXCLUDES_DIR=$val                 ;;
               -from=*|f=*) REAL_ROOT=$val                    ;;
                   -from|f) REAL_ROOT=$val                    ;;
                  -Force|F) FORCE=true                        ;;
                   -help|h) usage                             ;;
             -output=*|o=*) OUTPUT_FILE=$val                  ;;
                 -output|o) OUTPUT_FILE=$val                  ;;
                -pretend|p) PRETEND=true                      ;;
               -root=*|r=*) ROOT_PW=$val                      ;;
                   -root|r) ROOT_PW=$val                      ;;
           -template=*|t=*) TEMPLATE_DIR=$val                 ;;
               -template|t) TEMPLATE_DIR=$val                 ;;
               -user=*|u=*) USER_PW=$val                      ;;
                   -user|u) USER_PW=$val                      ;;
                -verbose|v) VERBOSE=true                      ;;
               -work=*|w=*) WORK_DIR=$val                     ;;
                  -work|w=) WORK_DIR=$val                     ;;
                         *) fatal "Unknown argument: -$param" ;;
        esac
    done

    CONF_FILE=$WORK_DIR/cleanup.conf

    [ $# -eq 0 ] && fatal "Expected as least one commmand"
    while [ $# -gt 0 ]; do
        local val cmd=$1
        shift
        val=${cmd#*=}
        case $cmd in
              start) do_start         $CONF_FILE $WORK_DIR             ;;
             status) do_status        $CONF_FILE                       ;;
              add=*) do_add           $CONF_FILE $WORK_DIR "$val"      ;;
             bind=*) do_bind          $CONF_FILE $WORK_DIR "$val"      ;;
         live-files) do_live_files    $CONF_FILE $WORK_DIR             ;;
      general-files) do_general_files $CONF_FILE $WORK_DIR             ;;
            general) do_general       $CONF_FILE $WORK_DIR             ;;
             passwd) do_passwd        $CONF_FILE                       ;;
          read-only) do_read_only     $CONF_FILE                       ;;
         read-write) do_read_write    $CONF_FILE                       ;;
            cleanup) do_cleanup       $CONF_FILE $WORK_DIR             ;;
            empty=*) do_empty         $CONF_FILE $WORK_DIR "$val"      ;;
       version-file) do_version_file  $CONF_FILE $WORK_DIR             ;;
     version-file=*) do_version_file  $CONF_FILE $WORK_DIR "$val"      ;;
            adjtime) do_adjtime       $CONF_FILE $WORK_DIR             ;;
          exclude=*) do_exclude       "$val" "$@" ; exit 0             ;;
            exclude) do_exclude       ""     "$@" ; exit 0             ;;
               repo) do_repo          $CONF_FILE $WORK_DIR "$DEF_REPO" ;;
             repo=*) do_repo          $CONF_FILE $WORK_DIR "$val"      ;;
           timezone) do_timezone      $CONF_FILE $WORK_DIR "$DEF_TZ"   ;;
         timezone=*) do_timezone      $CONF_FILE $WORK_DIR "$val"      ;;

 populate-live-files) do_populate_live "$@"                            ;;

                  *) fatal "Unexpected command: $cmd."
        esac
    done
}

do_populate_live() {
    [ $# -lt 1 ] && fatal "Missing <from> directory"
    local from_dir=$1  to_dir=$2
    test -d "$from_dir" || fatal "$from is not a directory"
    copy_files "$from_dir" "$LIVE_FILE_LIST"         "$to_dir$TEMPLATE_DIR/files"
    copy_files "$from_dir" "$GENERAL_LIVE_FILE_LIST" "$to_dir$TEMPLATE_DIR/general-files"

    exit 0
}

copy_files() {
    local from_dir=$1 files=$2 to_dir=$3
    [ -z "$to_dir" -o -n "${to_dir##*[a-zA-Z]*}" ] && fatal "suspicious to-dir: $to_dir"

    pretend rm -rf $to_dir
    pretend mkdir -p $to_dir
    local list=$(echo "$files" | grep -v "^\s*#\s*" | sed 's/\s*#.*//' \
        | grep "^/" | sed "s=^=$from_dir=")

    local file
    for file in $list; do
        test -e $file || continue
        local dest_file=${file#$from_dir}
        local dest=$to_dir$dest_file
        local dest_dir=$(dirname $dest)
        test -d $dest_dir || pretend mkdir -p $dest_dir
        pretend cp -a $file $dest
    done
}

do_version_file() {
    local conf_file=$1  work_dir=$2  title=$3
    read_conf_file $conf_file
    local vdir=$work_dir/version-file
    local vfile=$vdir$VERSION_FILE

    mkdir -p $(dirname $vfile) || fatal "Could not make directory for version-file"
    test -e $VERSION_FILE && cp $VERSION_FILE $vfile
    cat >> $vfile << Version_Info
$(version_id)

title: $title
creation date: $(date +"%e %B %Y %T %Z")
kernel: $(uname -sr)
machine: $(uname -m)
Version_Info

    bind_mount_template "$vdir" "$BIND_ROOT" "$REAL_ROOT"
}

do_exclude() {
    local file ecnt=0 prefix=$1
    shift

    [ "$OUTPUT_FILE" ] && exec > $OUTPUT_FILE

    for file; do
        [ -z "${file##/*}" -o -z "${file##.*}" ] || file="$EXCLUDES_DIR/$file"
        #echo "file: $file"
        if ! test -f $file; then
            error "Exclude file $file does not exist"
            ecnt=$((ecnt + 1))
            continue
        fi

        for line in $(grep -v "^\s*#" $file | sed -r -e "s=^\s*/?==" -e 's/\s+#.*//'); do
            echo "$prefix$line"
        done

    done  | sort -u

    case $ecnt in
        0) exit 0;;
        1) fatal "One missing exclude file"    ; exit 2 ;;
        *) fatal "$ecnt missing exclude files" ; exit 2 ;;
    esac

    exit 3
}

do_start() {
    local conf_file=$1  work_dir=$2

    [ -z "$FORCE" -a -e "$work_dir" ] && fatal "Work dir $work_dir exists.  Use --Force to delete it"
    test -e $work_dir && rm -rf $work_dir

    mkdir -p $BIND_ROOT || fatal "Could not make bind root directory $BIND_ROOT"
    if is_mounted $BIND_ROOT &>/dev/null; then

        [ -z "$FORCE" ] && fatal "bind-root: $BIND_ROOT is already a mount point"

    else
        mount --bind --make-slave $REAL_ROOT $BIND_ROOT
    fi

    write_conf_file $conf_file
}

do_read_only() {
    local conf_file=$1  work_dir=$2  dirs=$3

    read_conf_file $conf_file

    is_mounted $BIND_ROOT &>/dev/null || fatal "read-only: $BIND_ROOT is not mounted"

    mount -o remount,bind,ro $BIND_ROOT
}

do_read_write() {
    local conf_file=$1  work_dir=$2  dirs=$3

    read_conf_file $conf_file

    is_mounted $BIND_ROOT &>/dev/null || fatal "read-only: $BIND_ROOT is not mounted"

    mount -o remount,bind,rw $BIND_ROOT
}

do_add() {
    local conf_file=$1  work_dir=$2  dirs=$3

    read_conf_file $conf_file

    local dir real_dir real_root=${REAL_ROOT%/}
    echo "$dirs" | tr ',' '\n' | while read dir; do
	    [ "$dir" ] || continue
        real_dir=$real_root/${dir#/}
        test -d $real_dir || fatal "Add dir: $real_dir is not a directory"
        bind_mount_template "$dir" "$BIND_ROOT" "$REAL_ROOT"
    done

    write_conf_file $CONF_FILE

}

do_bind() {
    local conf_file=$1  work_dir=$2  dirs=$3

    read_conf_file $conf_file

    local dir real_dir real_root=${REAL_ROOT%/}
    echo "$dirs" | tr ',' '\n' | while read dir; do
        dir=${dir#/}
	    [ "$dir" ] || continue

        real_dir=$real_root/$dir

        mountpoint -q $real_dir || continue
        pretend mount --bind $real_dir $BIND_ROOT/$dir
    done

    write_conf_file $CONF_FILE
}

do_empty() {
    local conf_file=$1  work_dir=$2  dirs=$3

    read_conf_file $conf_file

    bind_empty_dirs "$WORK_DIR/empty" "$BIND_ROOT" "$REAL_ROOT" "$dirs"
}

do_repo() {
    local conf_file=$1  work_dir=$2  country=$3
	local verbose="--verbose"
	[ "$VERBOSE" ] || verbose="--quiet"
    read_conf_file $conf_file

    real_dir="/etc/apt/sources.list.d"
    repo_dir=$work_dir/repo
    copy_dir=$repo_dir$real_dir
    pretend mkdir -p $copy_dir
    pretend cp $real_dir/*.list $copy_dir
    pretend localize-repo $verbose --dir=$copy_dir $country
    bind_mount_template "$repo_dir"  "$BIND_ROOT" "$REAL_ROOT"
}


#------------------------------------------------------------------------------
#
#------------------------------------------------------------------------------
do_timezone() {
    local conf_file=$1  work_dir=$2  tz=$3

    read_conf_file $conf_file

    local zone_file=$REAL_ROOT/usr/share/zoneinfo/$tz
    local local_file=$REAL_ROOT/etc/localtime
    local tz_file=$REAL_ROOT/etc/timezone

    if ! test -e "$zone_file"; then
        error "Invalid timezone: $tz"
        return
    fi

    real_zone=$(readlink -f "$zone_file")
    if ! test -f "$real_zone"; then
        error "Invalid zoneinfo file: $real_zone"
        return
    fi

    local orig_tz=$(cat $tz_file 2>/dev/null)

    # If the timezone is already set to what we want then do nothing
    if [ "$tz" = "$orig_tz" ]; then
        return
    fi

    local tz_dir=$work_dir/tz
    local etc_dir=$tz_dir/etc
    mkdir -p $etc_dir

    # Always bindmount the timezone file (when needed)
    echo "$tz" > $etc_dir/timezone

    # If localtime is a real file then do the bindmount trick
    if ! test -L $local_file; then
        cp $local_file $etc_dir/localtime
    fi

    bind_mount_template "$tz_dir"  "$BIND_ROOT" "$REAL_ROOT"
}


do_status() {
    local conf_file=$1

    echo "$ME status:"
    if ! read_conf_file $conf_file force; then
        echo "  unstarted"
        exit 0
    fi

    echo "  started"

    if ! test -d $BIND_ROOT; then
        echo "  BIND_ROOT: $BIND_ROOT is not a directory"
    elif is_mounted $BIND_ROOT; then
        echo "  BIND_ROOT: $BIND_ROOT is mounted"
    else
        echo "  BIND_ROOT: $BIND_ROOT is not mounted"
    fi

    echo
    echo "  Config file:"
    echo ">>>>>>>>>>"
    cat $conf_file
    echo "<<<<<<<<<<"

    [ "$VERBOSE" ] || exit 0
    echo
    echo ">> df -a | grep $BIND_ROOT | awk '{print \$6}'"
    df -a | grep $BIND_ROOT | awk '{print $6}'

    exit 0
}

do_adjtime() {
    local conf_file=$1  work_dir=$2
    read_conf_file $conf_file
    local orig_file=/etc/adjtime
    test -r $orig_file || return 0

    local template_dir=$work_dir/adjtime
    local targ_file=$template_dir$orig_file
    mkdir -p $(dirname $targ_file)
    cp $orig_file $targ_file
    sed -i -e "1 s/.*/0.0 0 0.0/" -e "2 s/.*/0/" $targ_file
    bind_mount_template "$template_dir"  "$BIND_ROOT" "$REAL_ROOT"
}

do_passwd() {
    local conf_file=$1
    read_conf_file $conf_file

    munge_files "$TEMPLATE_DIR" "$BIND_ROOT" "$WORK_DIR" "$USER_PW" "$ROOT_PW"

    write_conf_file $CONF_FILE
}

do_live_files() {
    local conf_file=$1  work_dir=$2
    _do_files $conf_file $work_dir $TEMPLATE_DIR/files $work_dir/live-files
}

do_general_files() {
    local conf_file=$1  work_dir=$2
    _do_files $conf_file $work_dir $TEMPLATE_DIR/general-files $work_dir/general-files
}

_do_files() {
    local conf_file=$1 work_dir=$2 from=$3 temp=$4
    read_conf_file $conf_file
    test -d $from || fatal "Directory $from does not exist"
    mkdir -p $temp || fatal "Could not mkdir -p $temp"
    cp -r $from/* $temp/ 2>/dev/null
    bind_mount_template "$temp"  "$BIND_ROOT" "$REAL_ROOT"

    write_conf_file $CONF_FILE
}

do_all_files() {
    do_live_files "$@"
    do_general_files "$@"
}

#------------------------------------------------------------------------------
# This is a catch-all for doing the normal things we usually want to do.  As
# more nooks and crannies are discovered they are usually added here and the
# callers don't have to change anything.
#------------------------------------------------------------------------------
do_general() {
    local conf_file=$1  work_dir=$2

    do_empty     $conf_file $work_dir "/etc/modprobe.d"
    do_empty     $conf_file $work_dir "/etc/grub.d"
    do_empty     $conf_file $work_dir "/etc/network/interfaces.d"
    do_all_files $conf_file $work_dir
    do_passwd    $conf_file
    do_repo      $conf_file $work_dir $DEF_REPO
    do_timezone  $conf_file $work_dir $DEF_TZ

    write_conf_file $conf_file
}

#------------------------------------------------------------------------------
# Try to put the system back into the same state it was in before we started.
#------------------------------------------------------------------------------
do_cleanup() {
    local conf_file=$1  work_dir=$2
    read_conf_file $conf_file $FORCE

    is_mounted $BIND_ROOT &>/dev/null && pretend umount --recursive $BIND_ROOT

    # We MUST umount before we remove files and directories because otherwise
    # we would be trying to remove mountpoints which the system does not like

    # the recursive umount may be imperfect but we only need a few iterations
    # to get it all umounted.
    local i
    for i in $(seq 1 10); do
        is_mounted $BIND_ROOT &>/dev/null || break
        pretend umount --recursive $BIND_ROOT
        [ "$PRETEND" ] && break
        sleep 0.1
    done

    [ -z "$PRETEND" ] && is_mounted $BIND_ROOT &>/dev/null && fatal "Could not umount $BIND_DIR"

    test -d $BIND_ROOT && pretend rmdir $BIND_ROOT

    # Remove files that we touched on the root file system
    local file dir
    echo "$RM_FILES" | tr ',' '\n' | tac | while read file; do
	[ "$file" ] || continue
        pretend rm -f "$file"
    done

    # removed directories we created on the root file system
    echo "$RM_DIRS" | tr ',' '\n' | tac | while read dir; do
	[ "$dir" ] || continue
        pretend rmdir --ignore-fail-on-non-empty --parents "$dir"
    done

    # Finally, clean up our working directory
    pretend rm -f $conf_file
    [ "$work_dir" ] && pretend rm -rf $work_dir
    exit 0
}

#------------------------------------------------------------------------------
# This is a central routine.  We bind mount every file that is in the template
# directory to the equivalent location under the bind root.  We may have to
# make directories and touch files to do this so in those cases we record the
# location of the created file or directory on the real file system so we can
# clean them up after we are done and after the bind root was unmounted.
#------------------------------------------------------------------------------
bind_mount_template() {
    local template_dir=${1%/}  bind_root=${2%/}  real_root=${3%/}

    test -d $template_dir || return
    local file
    while read file; do
        local base=${file#$template_dir}
        local targ=$bind_root$base
        local orig=$real_root$base
        if test -L "$file"; then
            # Symlinks -- can't bind mount them

            # don't do anything if the orig already exists
            test -e "$orig" && continue

            # Otherwise, copy our symlimk to the real file system ...
            pretend cp -a "$file" "$orig"

            [ "$PRETEND" ] && continue

            # Don't add broken links
            if test -e $(readlink -f "$orig"); then
                # .. and either delete it when we are done
                RM_FILES="$RM_FILES${RM_FILES:+,}$orig"
            else
                # ... or delete it now if it is broken
                pretend rm -f "$orig"
            fi
        else
            # Can't bind mount to a symlink
            if test -L "$orig"; then
                error "Won't bind mount symlink: $orig"
                continue
            fi
            # Normal files -- bind mount template file, mkdir and touch if needed
            touch_file "$targ" "$orig"
            is_mounted "$targ" && error "File $targ is already mounted"
            pretend mount --bind "$file" "$targ"
        fi
    done <<Bind_Template
$(find $template_dir -type f -o -type l)
Bind_Template
}

#------------------------------------------------------------------------------
# "Clear out" existing directories by bind mounting an empty directory  over
# them.  We have to create a different empty directory for each directory we
# want to clear in case we want to populate some of the empty directories
# with files.
#------------------------------------------------------------------------------
bind_empty_dirs() {
    local template_dir=${1%/}  bind_root=${2%/}  real_root=${3%/}  dirs=$4

    local base
    echo "$dirs" | tr ',' '\n' | tac | while read base; do
        [ "$base" ] || continue
        local targ=$bind_root/$base
        # If directory doesn't exist then skip it
        test -d "$targ" || continue
        make_dir "$targ" "$real_root/$base"
        pretend mkdir -p "$template_dir/$base"
        pretend mount --bind "$template_dir/$base" "$targ"
    done
}

#------------------------------------------------------------------------------
# If a directory we need  doesn't exist then create it and mark the *original*
# (under the real root) to be removed when we are done.  We need to remove the
# original because the bind root will already be unmounted.  Only mark the
# original for deletion if it doesn't already exist.
#------------------------------------------------------------------------------
make_dir() {
    local dir=$1  orig=$2
    test -d "$dir" && return
    test -d "$orig" || RM_DIRS="$RM_DIRS${RM_DIRS:+,}$orig"
    pretend mkdir -p "$dir"
}

#------------------------------------------------------------------------------
# If a file doesn't exist then touch it so we can bind mount a template file
# over it.  Mark the original (under the real root) for deletion if it doesn't
# already exist.
#------------------------------------------------------------------------------
touch_file() {
    local file=$1  orig=$2
    local dir=$(dirname $file)
    make_dir "$dir" "$(dirname $orig)"
    test -f $file && return
    test -e $file && fatal "Expected a plain file at $orig"

    test -e "$orig" || RM_FILES="$RM_FILES${RM_FILES:+,}$orig"

    pretend touch $file
}

#------------------------------------------------------------------------------
#
#------------------------------------------------------------------------------
munge_files() {
    local template_dir=$1  bind_root=$2  work_dir=$3  user_pw=${4:-demo}  root_pw=${5:-root}
    local bind_from=$work_dir/bind

    local def_user=${user_pw%%:*}
    local def_group=1000

    local empty_dir=$work_dir/empty
    mkdir -p $empty_dir

    # create lists of pw_files and grp_files that actually exist
    local file pw_files grp_files passwd_file group_file subuid_file subgid_file
    for file in $PW_FILES; do
        test -e $bind_root$file || continue
        pw_files="$pw_files $bind_from$file"
        case $file in
            */gshadow) grp_files="$grp_files $bind_from$file";;
             */passwd) passwd_file="$bind_from$file";;
              */group) group_file="$bind_from$file"; grp_files="$grp_files $group_file";;
             */subuid) subuid_file="$bind_from$file";;
             */subgid) subgid_file="$bind_from$file";;
        esac
    done

    # we add a new user temporarily
    local quiet="--quiet"
    [ "$VERBOSE" ] && quiet=""

    local comment_opt=$(LC_ALL=C adduser --help 2>/dev/null | grep -m1 -o -- --comment | head -1)
    [ x"$comment_opt" != x"--comment" ] && comment_opt="--gecos"

    local added_user="live_temp_user"$(dd if=/dev/urandom bs=1 count=40 2>/dev/null | md5sum | cut -c 1-12)
    pretend adduser $quiet --disabled-login --shell "${DEF_SHELL:-/usr/bin/bash}" --no-create-home  $comment_opt $def_user $added_user
    local added_group=$(id -u $added_user 2>/dev/null)


    # Copy in files to the bind_from *after* we've added added_user (if needed)
    # Use files from template_dir first if they are available.
    local live_files
    for file in $BIND_FILES; do
        test -e $bind_root$file || continue
        dir=$(dirname $file)
        bdir=$bind_from$dir

        # Don't munge files that have a template file
        [ "$template_dir" -a -e $template_dir$file ] && continue
        pretend mkdir -p $bdir

        # Probably makes no difference that we copy from bind_root, not real_root
        pretend cp -a $bind_root$file $bdir
    done

    # Remove the added user from the originals (but it is still in the copies
    # under bind_from/)
    pretend deluser $added_user

    # Modify files (under bind_from/)

    # Remove all other normal users from the passwd and group files
    # Normal means having a home dir under /home and a login shell that ends in "sh"
    local other_users=$(cut /etc/passwd -d: -f1,6,7 | grep ":/home/.*:.*sh$" | cut -d: -f1 \
        | grep -v "^$added_user$")

    local other_user_regex=$(echo $other_users | tr " " '|')
    pretend sed -i -r "/^($other_user_regex):/d" $pw_files
    pretend perl -i -p -e "s/,($other_user_regex)(?=(,|$))//g; s/:($other_user_regex)(,|$)/:/;" $grp_files

    # Replace added_user with def_user (demo)
    pretend sed -i -r "s/^$added_user:/$def_user:/" $pw_files
    pretend perl -i -p -e "s/,$added_user(?=(,|$))/,$def_user/g; s/:$added_user(,|$)/:$def_user\1/;" $grp_files

    # Replace added_group with def_group (1000)
    pretend sed -i -r -e "s/^($def_user:[^:]*:)$added_group:/\1$def_group:/; s/(^$def_user:[^:]*:$def_group:)$added_group:/\1$def_group:/;" $passwd_file
    pretend sed -i -r -e "s/^($def_user:[^:]*:)$added_group:/\1$def_group:/;" $group_file

    # adjust subuid and subgid if only one entry for def_user (demo) exist
    # get defaults
    local sub_uid_min=$(sed -n -r 's/^\s*SUB_UID_MIN=([0-9]+).*/\1/p' /etc/login.defs 2>/dev/null)
    local sub_gid_min=$(sed -n -r 's/^\s*SUB_GID_MIN=([0-9]+).*/\1/p' /etc/login.defs 2>/dev/null)
    # set defaults
    : ${sub_uid_min:=100000}
    : ${sub_gid_min:=100000}
    if [ -f $subuid_file ] && [ $(wc -l < $subuid_file) = 1 ]; then
       pretend sed -i -r "s/^$def_user:[^:]+:/$def_user:$sub_uid_min:/" $subuid_file
    fi
    if [ -f $subgid_file ] && [ $(wc -l < $subgid_file) = 1 ]; then
       pretend sed -i -r "s/^$def_user:[^:]+:/$def_user:$sub_gid_min:/" $subgid_file
    fi

    update_home $def_user /home/$def_user $bind_from

    set_password $user_pw $bind_from
    set_password $root_pw $bind_from

    # Bind mount files under $bind_from/
    for file in $BIND_FILES; do
        local from=$bind_from$file
        local to=$bind_root$file
        local bak=${to}-
        test -e $from || continue
        test -e $to || continue
        is_mounted $to && error "File $to is already mounted"
        pretend mount --bind $from $to
        test -e $bak || continue
        is_mounted $bak && error "File $bak is already mounted"
        pretend mount --bind $from $bak
    done
}

# Note: this code is no longer called/used.  We use fresh copies which
# a lot of problems.  Although we will need to call it if the name of
# the default user changes.
do_xorg_login() {
    exit 10
    do_sed $bind_from/etc/slimski.conf \
        -e "s/^(\s#)*(default_user +).*/\2$def_user/" \
        -e "/xserver_arguments/ s/ -dpi\s+[0-9]+//"   \
        -e "s/^(\s#)*(auto_login +).*/\2yes/"

    do_sed $bind_from/etc/lightdm/lightdm.conf \
        -e "/autologin-user=demo/ s/^#+//" \
        -e "/xserver-command/ s/ -dpi [0-9]+//"
}

do_sed() {
    local file=$1
    shift
    echo sed $* $file
    if [ "$PRETEND" ]; then
        local expr
        file=${file/$bind_from/$real_root}
        test -e $file || return
        for expr; do
            [ "$expr" = -e ] && continue
            echo sed -n -r "${expr}" $file
            sed -n -r "${expr}p" $file
        done
    else
        test -e $file || return
        pretend sed -i -r "$@" $file
    fi
}

# first param is "username=password" and the "=password" part is optional
# If the password is empty then we use the username for the password.
# Second param is a "chroot" directory containing the /etc/shadow file.
set_password() {
    local user_pw=$1  dir=$2
    local user=${user_pw%%:*}
    local pw=${user_pw#*:}
    : ${pw:=$user}
    local hash=$(mkpasswd -m sha-512 "$pw")
    pretend sed -i -r "s=^($user):[^:]*:=\1:$hash:=" $dir/etc/shadow
}

update_home() {
    local user=$1  home=$2  dir=$3
    pretend sed -i -r "s=^($user:[^:]*:[^:]*:[^:]*:[^:]*:)[^:]*=\1$home=" $dir/etc/passwd
}

write_conf_file() {
    local file=$1
    mkdir -p $(dirname $file)
    cat <<Conf_File > $file
REAL_ROOT="$REAL_ROOT"
BIND_ROOT="$BIND_ROOT"
RM_DIRS="$RM_DIRS"

RM_FILES="$RM_FILES"
Conf_File
}

read_conf_file() {
    local conf_file=$1  force=$2

    if ! test -r $conf_file; then
        [ "$force" ] || fatal "Could not find config file $conf_file"
        return 1
    fi

    . $conf_file

    return 0
}

make_real() {
    local file=$1
    test -L $file || return
    local dest=$(readlink -f $file)
    test -f "$dest" || return
    local tmp=$(mktemp $file.XXXXXXXX)
    cp $dest $tmp
    mv $tmp $file
}

random_hex_32() {
    dd if=/dev/urandom bs=1 count=40 2>/dev/null | md5sum | cut -d" " -f1
}

version_id() {
    echo "==== $(random_hex_32)"
}

fatal() {
    local msg=$1  ret=${2:-2}
    echo -e "$ME Fatal Error: $msg" >&2
    exit $ret
}

error() {
    local msg=$1
    echo -e "$ME Error: $msg" >&2
}

is_mounted() {
    local file=$1
    cut -d" " -f2 /proc/mounts | grep -q "^$(readlink -f $file)$"
    return $?
}

pretend() {
    [ "$PRETEND" -o "$VERBOSE" ] && echo "$@"
    [ "$PRETEND" ] && return
    "$@"
}

main "$@"

