#!/bin/bash

# This script copies the desired information file to
# the failsafe data partition on the MTD device (flash memory) or
# block device designated for failsafe data, depending on platform.
#
# The arguments can be any of the following
# copy <dest> <source>
# set <dest> <content>
# remove <dest>
# where <dest> is the desination path in the failsafe data
# partition. The copy action copies the <source> file to
# <dest>, the set action outputs the <content> string to
# <dest> and the remove action removes <dest>.
#
# The exit status is
# 0 = success
# 1 = general error
# 2 = MTD / block device not found
# 3 = timeout waiting to obtain lock on MTD device
#
# When it is certain that no modifications are done the MTD is
# left unmodified, instead of erasing and rewriting the same data.

# The maximum wait for the MTD device to become free (seconds)
MAX_LOCK_WAIT=60

# The maximum wait for the MTD block device to appear (seconds)
# (at boot this can take long as udev might be busy timing out on other nodes)
MAX_BLOCKDEV_WAIT=30

# Load identifiers
[ -f /etc/spinetix/identifiers ] && . /etc/spinetix/identifiers

inithwinfo() {
    hwtype=

    # on ARM platforms cpuinfo has hardware name
    if [ "$HOSTTYPE" = "arm" ]; then
        while read tag sep val; do
            if [ "$tag" = "Hardware" ]; then
               hwtype="$val"
               break;
            fi
        done < /proc/cpuinfo
    fi

    case "$hwtype" in
        Sakura)
            FFSTYPE=jffs2
            PAGESIZE=2048
            BLOCKSIZE=131072
            MTDTYPE=nand
            ;;
        Bonsai)
            FFSTYPE=cramfs
            MTDTYPE=nor
            ;;
        ikebana)
            FFSTYPE=cramfs
            MTDTYPE=block
            BDEV1=/dev/mmcblk0boot0
            BDEV2=/dev/mmcblk0boot1
            BDEV1RO=/sys/block/mmcblk0boot0/force_ro
            BDEV2RO=/sys/block/mmcblk0boot1/force_ro
            ;;
        '')
            FFSTYPE=squashfs
            MTDTYPE=block
            BDEV1="$SPX_FAILSAFE_DATA_BDEV1"
            BDEV2="$SPX_FAILSAFE_DATA_BDEV2"
            BDEV1RO=/sys/block/${BDEV1##*/}/force_ro
            BDEV2RO=/sys/block/${BDEV2##*/}/force_ro
            [ -f "$BDEV1RO" ] || BDEV1RO=
            [ -f "$BDEV2RO" ] || BDEV2RO=
            ;;
        *)
            echo "uknown hardware type '$hwtype'" >&2
            return 1
            ;;
    esac

    return 0
}

# For MTD devices it sets mtddev to the character MTD device. For
# block devices it sets bdevdst to the block device where data is to
# be saved. In both cases bdevsrc is set to the block device with the
# current data to be copied over and it is set to empty if there is
# not current data. It also sets lockdev to the device used for
# locking.
function get_devs() {
    if [ "$MTDTYPE" != "block" ]; then
        mtdnr="$(sed -ne '/\"failsafe-data\"$/{s/mtd\([0-9]\+\).*/\1/;p}' /proc/mtd)" || return 1
        bdevsrc="/dev/mtdblock$mtdnr"
        mtddev="/dev/mtd$mtdnr"
        lockdev="$mtddev"
        [ -c "$mtddev" ] || return 1
        [ -b "$bdevsrc" ] && return 0
        modprobe mtdblock || return 1
        i=0
        while ! [ -e "$bdevsrc" ]; do
            if [ "$i" -ge $MAX_BLOCKDEV_WAIT ]; then
                return 1
            fi
            (( i++ ))
            sleep 1
        done
        blkid -c /dev/null -w /dev/null -s none -t TYPE=$FFSTYPE "$bdevsrc" # probe filesystem
        [ $? -ne 0 ] && bdevsrc=
    else
        if [ -z "$BDEV1" -o -z "$BDEV2" ]; then
            echo 'Failed to get disk partitions for failsafe-data' >&2
            return 1
        fi
        lockdev="$BDEV1" # always the same!
        bdevsrc="$BDEV1"
        bdevdst="$BDEV2"
        blkid -c /dev/null -w /dev/null -s none -t TYPE=$FFSTYPE "$bdevsrc" # probe filesystem
        [ $? -eq 0 ] && return 0
        bdevsrc="$BDEV2"
        bdevdst="$BDEV1"
        blkid -c /dev/null -w /dev/null -s none -t TYPE=$FFSTYPE "$bdevsrc" # probe filesystem
        [ $? -ne 0 ] && bdevsrc= # no current source
    fi
    return 0
}

function clear_force_ro() {
    if [ "$1" = "$BDEV1" -a -n "$BDEV1RO" ]; then
        [ -z "$BDEV1_SAVED_RO" ] && BDEV1_SAVED_RO="$(< "$BDEV1RO" )"
        echo 0 > "$BDEV1RO"
    elif [ "$1" = "$BDEV2" -a -n "$BDEV2RO" ]; then
        [ -z "$BDEV2_SAVED_RO" ] && BDEV2_SAVED_RO="$(< "$BDEV2RO" )"
        echo 0 > "$BDEV2RO"
    fi
}

function restore_force_ro() {
    if [ "$1" = "$BDEV1" -a -n "$BDEV1RO" ]; then
        echo "$BDEV1_SAVED_RO" > "$BDEV1RO"
    elif [ "$1" = "$BDEV2" -a -n "$BDEV2RO" ]; then
        echo "$BDEV2_SAVED_RO" > "$BDEV2RO"
    fi
}

function lock() {
    i=0
    while ! lockdev -l "$lockdev"; do
        if [ "$i" -ge $MAX_LOCK_WAIT ]; then
            return 1
        fi
        (( i++ ))
        sleep 1
    done
    return 0
}

function unlock() {
    lockdev -u "$lockdev"
}

function files_differ() {
    [ ! -f "$1" -o ! -f "$2" ] && return
    # as cmp is not available we use sha1sum to compare
    [ "$(cat "$1" | sha1sum)" != "$(cat "$2" | sha1sum)" ]
}

function create_parent_dirs() {
    local dir="$1"
    local p="$2"
    while [ "$p" != "${p#/}" ]; do
        p="${p#/}"
    done
    if [ "${p%/}" != "$p" ]; then
        echo "destination cannot be a directory" >&2
        return 1
    fi
    if [ "$p" != "${p#*/}" ]; then
        mkdir -p "$dir"/"${p%/*}" || return 1
    fi
    return 0
}

function set_data() {
    local ret

    # Compute paths and create temp sub-dirs
    tmpmount="$tmpdir/mnt"
    tmpdatadir="$tmpdir/data"
    tmpfsimg="$tmpdir/fsimg"

    mkdir -m 700 "$tmpmount" || return 1
    mkdir -m 700 "$tmpdatadir" || return 1

    # Mount filesystem and copy data over to temp dir if present
    if [ -n "$bdevsrc" ]; then
        mount -t "$FFSTYPE" -r "$bdevsrc" "$tmpmount" 2>/dev/null
        if [ $? -eq 0 ]; then
            cp -a "$tmpmount/." "$tmpdatadir"
            r=$?
            umount "$tmpmount"
            [ $r -eq 0 ] || return 1
        fi
    fi

    # perform the specified operations
    changed=false
    while [ "$#" -ne 0 ]; do
        case "$1" in
            "copy")
                if files_differ "$3" "$tmpdatadir"/"$2" ; then
                    create_parent_dirs "$tmpdatadir" "$2" || return 1
                    cp -f -- "$3" "$tmpdatadir"/"$2" || return 1
                    chmod 0644 "$tmpdatadir"/"$2" || return 1
                    changed=true
                fi
                shift 3
                ;;
            "set")
                create_parent_dirs "$tmpdatadir" "$2" || return 1
                echo -n "$3" > "$tmpdatadir"/"$2" || return 1
                changed=true
                shift 3
                ;;
            "remove")
                if [ -e "$tmpdatadir"/"$2" ]; then
                    rm -rf "$tmpdatadir"/"$2" || return 1
                    changed=true
                fi
                shift 2
                ;;
            "ls")
                ls -lgGAR1F "$tmpdatadir"
                shift
                ;;
            "to-tar")
                tar -C "$tmpdatadir" -c -f - .
                shift
                ;;
            *)
                echo "unknown action '$1'" >&2
                return 1
        esac
    done

    # do not re-make the filesystem if nothing changed
    [ "$changed" = "false" ] && return 0

    # make filesystem again
    case "$FFSTYPE" in
        cramfs)
            mkfs.cramfs -n failsafe-data "$tmpdatadir" "$tmpfsimg" || return 1
            ;;
        squashfs)
            # mksquashfs can write directly to the block device and writes superblock at end of process
            tmpfsimg=
            clear_force_ro "$bdevdst"
            mksquashfs "$tmpdatadir" "$bdevdst" -noappend -all-root -no-recovery \
                -no-duplicates -no-progress -processors 1 -mem 32M > /dev/null && \
                blockdev --flushbufs "$bdevdst"
            ret=$?
            restore_force_ro "$bdevdst"
            [ $ret -eq 0 ] || return $ret
            ;;
        jffs2)
            mkfs.jffs2 -n -s "$PAGESIZE" -e "$BLOCKSIZE" \
                -d "$tmpdatadir" -o "$tmpfsimg" || return 1
            ;;
        *)
            echo "unknown filesystem type" >&2
            return 1
    esac

    # save filesystem to MTD or block device
    case "$MTDTYPE" in
        nand)
            flash_erase -q "$mtddev" 0 0 || return 1
            nandwrite -p -q "$mtddev" "$tmpfsimg"
            ;;
        nor)
            flashcp "$tmpfsimg" "$mtddev" || return 1
            ;;
        block)
            if [ -n "$tmpfsimg" ]; then
                clear_force_ro "$bdevdst"
                dd if="$tmpfsimg" of="$bdevdst" bs=4096 conv=sync,fsync 2> /dev/null
                ret=$?
                restore_force_ro "$bdevdst"
                [ $ret -eq 0 ] || return 1
            fi
            # invalidate old copy
            if [ -n "$bdevsrc" -a "$bdevsrc" != "$bdevdst" ]; then
                clear_force_ro "$bdevsrc"
                dd if=/dev/zero of="$bdevsrc" bs=4096 count=1 \
                    conv=fsync 2> /dev/null
                ret=$?
                restore_force_ro "$bdevsrc"
                [ $ret -eq 0 ] || return 1
            fi
            ;;
        *)
            echo "unknown device type" >&2
            return 1
    esac

    return 0
}

function safe_data() {
    # Identify the MTD or block device and lock it
    get_devs || return 2
    lock || return 3

    # Do all operations under tmpdir
    tmpdir="$(mktemp -d -t failsafe-data.XXXXXX)"
    if [ $? -ne 0 ]; then
        unlock
        return 1
    fi

    # Set the data
    set_data "$@"
    RET=$?

    # Cleanup all temporary files and unlock the device
    rm -rf "$tmpdir"
    unlock

    return $RET
}

if [ "$#" -eq 0 ]; then
    echo "usage: $0 {copy <dest> <source>|set <dest> <content>|remove <dest>|ls|to-tar} ..." >&2
    exit 1
fi

inithwinfo || exit 1
umask 0022 || exit 1
safe_data "$@"
