#! /bin/sh

# sdsetup - manage encrypted SD card
# usage:  sdsetup format N   - format and mount SD card #N
#         sdsetup mount N    - mount a previously formatted card
#         sdsetup unmount N  - unmount a previously mounted card
#         sdsetup status N   - print status of card #N

# Sends the following messages to /app-sdinfo:
#   F#            = started formatting card #
#   f#:/sdcard#   = finished formatting card #, mounted on /sdcard#
#   I#            = started mounting card #
#   i#:/sdcard#   = finished mounting card # on /sdcard#
#   r#:/sdcard#   = unmounted card # from /sdcard#
#   e#:STAT       = encountered error "STAT" on card #
#   p#:pct        = progress: "pct" percent complete on card #

# Parse parameters.
OUTPUT_MSGQUEUE=1
if [ "x$1" = "x-p" ]; then shift; OUTPUT_MSGQUEUE=0; fi
CMD=$1
CARD_NUM=$2
if [ -z "$CMD" ]; then CMD='?'; fi
if [ -z "$CARD_NUM" ]; then CARD_NUM=0; fi

# Real SD card device.
LOWER_NAME=mmcblk$CARD_NUM
LOWER_DEV=/dev/$LOWER_NAME

# Device mapper device for data area of the card (all except first sector).
DATA_NAME=sdcard$CARD_NUM
DATA_DEV=/dev/mapper/$DATA_NAME

# Device mapper device for header area of the card (just the first sector).
HDR_NAME=sdcard_hdr$CARD_NUM
HDR_DEV=/dev/mapper/$HDR_NAME

# Error codes.
STAT_USAGE=1
STAT_NOT_FMT=10
STAT_WRONG_ESN=11
STAT_FSCK=12
STAT_MOUNT=13
STAT_NO_DEVICE=14
STAT_BAD_VERSION=15
STAT_MKFS=24
STAT_CP_HDR=25
STAT_CP_FILE=26
STAT_GETSIZE=31

# Other constants.
MOUNTPOINT=/sdcard$CARD_NUM
WWW_MOUNTPOINT=/www$MOUNTPOINT
WWW_INCOMING=$WWW_MOUNTPOINT/incoming
WWW_PKGS=/www/cgi-bin/pkgs$MOUNTPOINT
FSTYPE=ext3
FSOPTIONS=data=journal
MAGIC=FE113713B2F1
FORMAT=roku/enc3/fs1
VERSION=1 # Version of this code.
MIN_VERSION=1 # Minimum version required to read cards formatted by this code.
ESN=$(esn)

PLATFORM=$(/bin/roku_platform)
if [ $PLATFORM = giga ]; then
    KEY=$(/bin/gencmd 'encrypt d8c7f654683ef18de98d9248429275a6')
else
    KEY=$(echo ab5ad2dd5d0284179b4a7be8b78c8b1 | enc 1 | hexdump -v -n16 -e '1/1 "%02x"')
fi

# -------------------------------------------------------
# Set up HDR_DEV to map to first sector of the card (unencrypted).
#
mk_dev_card_hdr() {
    if [ -b $HDR_DEV ]; then return; fi
    echo "0 1 linear $LOWER_DEV 0" | dmsetup create $HDR_NAME
}

rm_dev_card_hdr() {
    if [ -b $HDR_DEV ]; then
        dmsetup remove $HDR_NAME 2> /dev/null
    fi
}

# -------------------------------------------------------
# Set up DATA_DEV to map to the second and following sectors.
#
mk_dev_card() {
    if [ -b $DATA_DEV ]; then return; fi
    SIZE=`cat /sys/block/$LOWER_NAME/size`
    if expr "x$SIZE" : "x[0-9][0-9]*$" > /dev/null && [ $SIZE -gt 0 ]; then :; else
        echo "Cannot get size from /sys/block/$LOWER_NAME/size ($SIZE)"
        exit $STAT_GETSIZE
    fi
    SIZE=`expr $SIZE - 1` # first block of device is hdr
    echo "0 $SIZE crypt aes-plain $KEY 0 $LOWER_DEV 1" | dmsetup create $DATA_NAME
}

rm_dev_card() {
    if [ -b $DATA_DEV ]; then
        dmsetup remove $DATA_NAME 2> /dev/null
    fi
}

# -------------------------------------------------------
# Mount/unmount a card.
#
is_sdcard_mounted() {
    grep -q " $MOUNTPOINT " /proc/mounts
}

unmount_sdcard() {
    if is_sdcard_mounted; then
        umount $WWW_PKGS
        umount $WWW_INCOMING
        umount $MOUNTPOINT
    fi
}

mount_plugin_install_dir() {
    SRC="$1"
    DST="$2"
    if [ -d $SRC ] || mkdir -p $SRC; then
        rm -rf $MOUNTPOINT/tmp?* $SRC/tmp?*
        chown default:default $SRC
        if [ -d $DST ] || mkdir -p $DST; then
            mount $SRC $DST
        fi
    fi
}

mount_sdcard() {
    unmount_sdcard
    if mount -t $FSTYPE -o $FSOPTIONS $DATA_DEV $MOUNTPOINT; then
        mount_plugin_install_dir $MOUNTPOINT/incoming $WWW_INCOMING
        mount_plugin_install_dir $MOUNTPOINT/pkgs $WWW_PKGS
    fi
}

# -------------------------------------------------------
# Unmount card and unmap device-mapper devices.
#
unmount_and_remove() {
    unmount_sdcard
    rm_dev_card
    rm_dev_card_hdr
}

# -------------------------------------------------------
# Construct an info file.
#
print_info() {
    echo "magic=$MAGIC"
    echo "format=$FORMAT"
    echo "version=$VERSION"
    echo "min_version=$MIN_VERSION"
    echo "esn=$ESN"
    FAKEKEY=`echo "red herring for $ESN" | md5sum | sed 's/ .*//'`
    echo "key=$FAKEKEY"  # just misdirection
    echo "end="
}

# -------------------------------------------------------
# Make sure the info file contains the expected data.
#
check_info() {
    FILE="$1"
    if grep -q "^magic=$MAGIC\$" $FILE && 
       grep -q "^format=$FORMAT\$" $FILE; then :; else
        echo "$LOWER_DEV is not formatted as $FORMAT"
        return $STAT_NOT_FMT
    fi
    if grep -q "^esn=$ESN\$" $FILE; then :; else
        echo "$LOWER_DEV is not formatted for ESN $ESN"
        return $STAT_WRONG_ESN
    fi
    FILE_MIN_VERSION=`grep "^min_version=" $FILE | sed 's/min_version=//'`
    if [ -n "$FILE_MIN_VERSION" ]; then
        if [ $VERSION -lt $FILE_MIN_VERSION ]; then
            echo "$LOWER_DEV requires version $FILE_MIN_VERSION; running $VERSION"
            return $STAT_BAD_VERSION
        fi
    fi
    return 0
}

# -------------------------------------------------------
# Send a message to app message queue.
#
msgsend() {
    if [ $OUTPUT_MSGQUEUE -eq 1 ]; then
        plethora msg-send /app-sdinfo "$1" 8 32
    else
        echo " $1"
    fi
}

# -------------------------------------------------------
# Forward progress events from stdin to /app-sdinfo
#
progress() {
    while read PROGRESS; do
        if [ -n "$PROGRESS" -a ${PROGRESS:0:1} = '%' ]; then
            msgsend "p$CARD_NUM:${PROGRESS:1}"
        fi
    done
}

# -------------------------------------------------------
# Process commands.
#

STAT=0

if [ "$CMD" = format ]; then

    msgsend "F$CARD_NUM"
    if [ -b $LOWER_DEV ]; then
        unmount_and_remove
        mk_dev_card_hdr
        TEMPINFO=$(mktemp)
        print_info > $TEMPINFO
        if dd if=$TEMPINFO of=$HDR_DEV bs=512 conv=sync 2>/dev/null; then 
            mk_dev_card
            STATFILE=$(mktemp -t) # little kludge to get status of first program in pipeline.
            ( mke2fs -q -p3 -j -O^dir_index $DATA_DEV; echo $? >$STATFILE ) 3>&1 1>/dev/null | progress
            sync; sleep 1; sync # voodoo to try to push buffered data to card
            if [ $(cat $STATFILE) -eq 0 ]; then # mke2fs succeeded
                if mount_sdcard; then 
                    STAT=0
                else
                    STAT=$STAT_MOUNT
                fi
            else
                STAT=$STAT_MKFS
            fi
            rm -f $STATFILE
        else 
            STAT=$STAT_CP_HDR
        fi
        rm -f $TEMPINFO
    else
        STAT=$STAT_NO_DEVICE
    fi
    if [ $STAT -eq 0 ]; then
        chown app.app $MOUNTPOINT
        msgsend "f$CARD_NUM:$MOUNTPOINT"
    else
        unmount_and_remove
        msgsend "e$CARD_NUM:$STAT"
    fi

elif [ "$CMD" = mount ]; then

    msgsend "I$CARD_NUM"
    if [ -b $LOWER_DEV ]; then
        if is_sdcard_mounted; then
            STAT=0
        else
            mk_dev_card_hdr
            check_info $HDR_DEV
            STAT=$?
            if [ $STAT -eq 0 ]; then
                mk_dev_card
                e2fsck -p $DATA_DEV >/dev/null
                if [ $? -lt 4 ]; then # no errors or corrected errors
                    if mount_sdcard; then 
                        STAT=0
                    else
                        STAT=$STAT_MOUNT
                    fi
                else
                    STAT=$STAT_FSCK
                fi
            fi
        fi
    else
        STAT=$STAT_NO_DEVICE
    fi
    if [ $STAT -eq 0 ]; then
        msgsend "i$CARD_NUM:$MOUNTPOINT"
    else
        unmount_and_remove
        msgsend "e$CARD_NUM:$STAT"
    fi

elif [ "$CMD" = unmount ]; then

    # Wait for any format threads to exit before sending the remove message
    while pgrep -f "${0} format"; do sleep .1; done                                                                                                              

    msgsend "R$CARD_NUM"
    # This unmount will fail until the app has closed all files on the card.
    while is_sdcard_mounted; do
        sleep 1
        unmount_sdcard
    done
    unmount_and_remove
    msgsend "r$CARD_NUM:$MOUNTPOINT" 

elif [ "$CMD" = map ]; then

    mk_dev_card_hdr
    mk_dev_card

elif [ "$CMD" = status ]; then

    if is_sdcard_mounted; then
        if [ $OUTPUT_MSGQUEUE -eq 1 ]; then
            msgsend "s$CARD_NUM:0:$MOUNTPOINT"
        else
            echo "card $CARD_NUM mounted on $MOUNTPOINT"
        fi
    elif [ -b $DATA_DEV ]; then
        if [ $OUTPUT_MSGQUEUE -eq 1 ]; then
            msgsend "s$CARD_NUM:M"
        else
            echo "card $CARD_NUM not mounted but mapped"
        fi
    elif [ -b $LOWER_DEV ]; then
        if [ $OUTPUT_MSGQUEUE -eq 1 ]; then
            msgsend "s$CARD_NUM:m"
        else
            echo "card $CARD_NUM not mounted"
        fi
    else
        if [ $OUTPUT_MSGQUEUE -eq 1 ]; then
            msgsend "s$CARD_NUM:p"
        else
            echo "card $CARD_NUM not present"
        fi
    fi

else

    echo "usage: sdsetup [-p] [mount|unmount|format|map|status] [card_num]"
    echo "                 -p     Print status to stdout; otherwise to msgqueue /app-sdinfo"
    echo "                format  Format and mount a card"
    echo "                mount   Mount a previously formatted card"
    echo "                unmount Unmount a previously mounted card"
    echo "                status  Print status of card"
    STAT=$STAT_USAGE
fi

exit $STAT
