#!/bin/sh
# Copyright 2010 Palm, Inc.  All Rights Reserved.

# This script constructs the RAM disk uimage that is used when installing
# updates.  It takes PACKAGE_LIST as input, which is a manifest of the
# packages that compose the RAM disk's update file system.
#
# If a package needed for the update file system is in the downloaded update,
# the files are extracted from it.  Otherwise, the files are copied from
# the device file system.  Each package has an optional list of files.  If
# there is a list of files, only those files are copied.  If the package is
# not followed by a list of files, then all of the package's files are copied.
#
# The updatefsinfo is included in every update.  It contains fields used to
# construct and validate the uimage.

# set DEBUG to include gdbserver and novacom.  Set DEBUG in base-files'
# miniboot.sh script as well.
DEBUG=0

LOG=/var/log/uimage.log
TMP=/tmp
UPDATEINFO=$TMP/boot/updatefs-info
UPDATE_IMAGE=$TMP/update-image
UPDATE_IMAGE_GZ=$UPDATE_IMAGE.gz
UPDATE_FS=$TMP/update-fs
INFODIR=/usr/lib/ipkg/info
UPDATE_LIST=/var/lib/software/SessionFiles/update_list.txt
UPDATE_FLAG=/var/lib/software/updating
STATUS_FILE=/var/lib/software/SessionFiles/update_status.txt
FINAL_UIMAGE=/boot/update-uimage
LOOP_KB=15000
LOOP_INODES=640
MD5SUM=

if [ $DEBUG -eq 1 ]
then
    PACKAGE_LIST=/usr/share/ota-scripts/uimage-packages-dbg
else
    PACKAGE_LIST=/usr/share/ota-scripts/uimage-packages
fi


mount_partitions()
{
    if ! mount -o remount,rw /
    then
        cleanup "mount error"
    fi

    if ! mount -o remount,rw /boot
    then
        cleanup "mount error"
    fi
    
    # update partition should already be mounted
    mount /dev/mapper/store-update /var/lib/update > /dev/null 2>&1
}

# read fields from updatefsinfo package
read_updatefsinfo()
{
    if [ ! -f $UPDATE_LIST ]
    then
        cleanup "$UPDATE_LIST not found"
    fi
    
    PATH_TO_PKG=`grep updatefsinfo $UPDATE_LIST`
    if [ -n "$PATH_TO_PKG" ]
    then
        if ar t $PATH_TO_PKG | grep data.tar.xz > /dev/null 2>&1
        then
            if ! ar p $PATH_TO_PKG data.tar.xz | xz | \
                tar -C $TMP -xvf - ./boot/updatefs-info >> $LOG 2>&1
            then
                cleanup "error unpacking xz $PATH_TO_PKG"
            fi
        else
            if ! ar p $PATH_TO_PKG data.tar.gz | \
                tar -C $TMP -xvzf - ./boot/updatefs-info >> $LOG 2>&1
            then
                cleanup "error unpacking $PATH_TO_PKG"
            fi
        fi
    elif [ -f /boot/updatefs-info ]
    then
        # updatefsinfo not in update, so read file from file system
        UPDATEINFO=/boot/updatefs-info
    else
        cleanup "updatefsinfo not found"
    fi
        
    tmp=`grep UPDATEFS_KB $UPDATEINFO | cut -d'=' -f2`
    if [ -z "$tmp" ]
    then
        echo "Using default LOOP_KB value"
    else
        LOOP_KB=$tmp
    fi

    if [ $DEBUG -eq 1 ]
    then
        # add 2000K for gdbserver and libraries it needs
        LOOP_KB=`expr $LOOP_KB + 2000`
    fi

    echo "loop device size = $LOOP_KB" >> $LOG

    tmp=`grep UPDATEFS_INODES $UPDATEINFO | cut -d'=' -f2`
    if [ -z "$tmp" ]
    then
        echo "Using default LOOP_INODES value"
    else
        LOOP_INODES=$tmp
    fi

    MD5SUM=`grep UPDATEFS_MD5 $UPDATEINFO | cut -d'=' -f2`
    if [ -z "$MD5SUM" ]
    then
        echo "UPDATEFS_MD5 not found"
    fi
}

create_loop_device()
{
    if ! dd if=/dev/zero of=$UPDATE_IMAGE bs=1k count=$LOOP_KB
    then
        cleanup "dd error"
    fi

    # use ext2 to avoid overhead of journaling
    if ! mkfs.ext2 -F -m0 -O ^resize_inode -N $LOOP_INODES $UPDATE_IMAGE
    then
        cleanup "mkfs error"
    fi
    
    mkdir -p $UPDATE_FS
    if ! mount $UPDATE_IMAGE $UPDATE_FS -o loop
    then
        cleanup "error mounting loop device"
    fi
}

make_dev_nodes()
{
    # make the dev nodes
    mkdir $UPDATE_FS/dev
    mknod $UPDATE_FS/dev/null c 1 3                          
    mknod $UPDATE_FS/dev/zero c 1 5                          
    mknod $UPDATE_FS/dev/systty c 4 0                        
    mknod $UPDATE_FS/dev/tty c 5 0                           
    mknod $UPDATE_FS/dev/console c 5 1                       
    mknod $UPDATE_FS/dev/ptmx c 5 2                          
    mknod $UPDATE_FS/dev/fb c 29 0                           
    mknod $UPDATE_FS/dev/tty0 c 4 0                          
    mknod $UPDATE_FS/dev/tty1 c 4 1                          
    mknod $UPDATE_FS/dev/tty2 c 4 2                          
    mknod $UPDATE_FS/dev/tty3 c 4 3                          
    mknod $UPDATE_FS/dev/tty4 c 4 4   
    mknod $UPDATE_FS/dev/tty5 c 4 5   
    mknod $UPDATE_FS/dev/tty6 c 4 6   
    mknod $UPDATE_FS/dev/tty7 c 4 7   
    mknod $UPDATE_FS/dev/tty8 c 4 8   
    mknod $UPDATE_FS/dev/tty9 c 4 9   
    mknod $UPDATE_FS/dev/tty10 c 4 10
    mknod $UPDATE_FS/dev/tty11 c 4 11
    mknod $UPDATE_FS/dev/tty12 c 4 12
    mknod $UPDATE_FS/dev/ttyS0 c 4 64
    mknod $UPDATE_FS/dev/ttyS1 c 4 65
    mknod $UPDATE_FS/dev/ttyS2 c 4 66
    mknod $UPDATE_FS/dev/ttyS3 c 4 67

    # Make additional devices
    mknod $UPDATE_FS/dev/urandom c 1 9 
    mknod $UPDATE_FS/dev/mmcblk0 b 179 0
    mknod $UPDATE_FS/dev/mmcblk0p1 b 179 1
    mknod $UPDATE_FS/dev/mmcblk0p2 b 179 2
    mknod $UPDATE_FS/dev/mmcblk0p3 b 179 3

    mkdir $UPDATE_FS/proc
    mkdir $UPDATE_FS/sys
    mkdir $UPDATE_FS/tmp
}

convert_ipk_to_pkg()
{
    local bname=${1##*/}
    local rname=${bname%.ipk}
    echo ${rname%%_*}
}

copy_file()
{
    if [ ! -e $1 ]
    then
        echo "$1 not found"
        return
    fi
    
    DIR=`dirname $1`
    if [ ! -d $UPDATE_FS$DIR ]
    then
        mkdir -p $UPDATE_FS$DIR
    fi

    if [ -L $1 ]
    then
        file=$(echo $1)
        echo "creating $UPDATE_FS$file link" >> $LOG
        ln -s $(readlink $file) $UPDATE_FS$file
    else
        echo "copying $1" >> $LOG
        cp $1 $UPDATE_FS$DIR
    fi
}

copy_files()
{
    while read file
    do
        copy_file $file
    done < $1
}

copy_files_with_filter()
{
    filter=$2
    substring=${filter:0:11}
    if [ "$substring" = " --exclude=" ]
    then
        # copy all files from package list except for excluded files
        while read file
        do
            echo "file = $file" >> $LOG
            list=${filter%$file*}
            echo "list = $list, filter = $filter" >> $LOG
            if [ "$list" != "$filter" ]
            then
                # skip excluded file
                continue
            fi            
            copy_file $file
        done < $1
    else
        # copy only files following the package
        echo "copying $2" >> $LOG
        for file in $2
        do
            # remove initial '.'
            echo "file = $file" >> $LOG
            substring=${file:1}
            copy_file $substring
        done
    fi
}

copy_package_files()
 {
    if [ ! -s $PACKAGE_LIST ]
    then
        cleanup "$PACKAGE_LIST not found"
    fi
    
    while read line
    do
        # each line of PACKAGE_LIST is a package optionally followed by a list
        # of space-separated directories and/or files from that package
        package=${line%% *}
        files=${line#$package}
        
        # use package from downloaded update if it exists
        # add '/' to distinguish initscipts.ipk from upstart-initscripts.ipk
        PATH_TO_PKG=`grep "/$package" $UPDATE_LIST`
        if [ -n "$PATH_TO_PKG" ]
        then
            echo "unpacking $package" >> $LOG

            # tar must expand wildcards, not sh
            set -o noglob
            if ar t $PATH_TO_PKG | grep data.tar.xz > /dev/null 2>&1
            then
                if ! ar p $PATH_TO_PKG data.tar.xz | xz | \
                    tar -C $UPDATE_FS -xvf - --wildcards $files >> $LOG 2>&1
                then
                    cleanup "error unpacking xz $package"
                fi
            else
                if ! ar p $PATH_TO_PKG data.tar.gz | \
                    tar -C $UPDATE_FS -xvzf - --wildcards $files >> $LOG 2>&1
                then
                    cleanup "error unpacking $package"
                fi
            fi
            set +o noglob
        # package is not in update, so copy files from rootfs
        elif [ -z "$files" ]
        then
            # copy all files from package
            echo "copying files for $package" >> $LOG
            
            pkg=$(convert_ipk_to_pkg $package)
            copy_files $INFODIR/$pkg.list
        else
            # copy files using $files
            echo "filter files for $package" >> $LOG
            pkg=$(convert_ipk_to_pkg $package)
            copy_files_with_filter $INFODIR/$pkg.list "$files"
        fi
    done < $PACKAGE_LIST

    # /var/tmp is a sym link in the build, but mountall.sh changes it to a
    # separate tmpfs during boot.  Remove this directory from updatefs to 
    # avoid a checksum error.
    rm -fR $UPDATE_FS/var/tmp

    # /etc/ld.so.cache is a symlink to /var/cache/ld.so.cache on a flashed
    # device, but is a file after an update's post-install scripts run
    rm -f $UPDATE_FS/etc/ld.so.cache
}

validate_fs()
{
    if [ -n "$MD5SUM" ]
    then
        # The MD5 is generated from a file containing
        #  1.  A list of all files and their MD5 sums in the update file 
        #       system, not including links and directories
        #  2.  Concatenated to #1 is a list of links in the update file system
        
        local FS_MD5_FILE=/var/log/md5-files
        find $UPDATE_FS -type f | sort | xargs md5sum | \
            sed "s|$UPDATE_FS||" > $FS_MD5_FILE
        find $UPDATE_FS -type l | sort | xargs ls -l | \
            sed "s|^[^\/]*$UPDATE_FS||" >> $FS_MD5_FILE
        FS_MD5=`md5sum $FS_MD5_FILE | cut -d' ' -f1`

        # do not use checksum if rootfs is writable
        if [ -f /etc/.rootfs_RW ]
        then
            tmp=`cat /etc/.rootfs_RW`
            if [ $tmp = "1" ]
            then
                return 0
            fi
        fi
        
        if [ $FS_MD5 != $MD5SUM ]
        then
            cleanup "update-fs: $FS_MD5, updatefs-info: $MD5SUM"
        fi
    fi
}

create_fs_uimage()
{
    # unmount before gzipping
    umount $UPDATE_FS

    # gzip file system
    echo "gzip file system" >> $LOG
    gzip -c -1 $UPDATE_IMAGE > $UPDATE_IMAGE_GZ

    # clean up
    rmdir $UPDATE_FS
    rm -f $UPDATE_IMAGE

    if [ ! -x /usr/bin/mkimage ]
    then
        cleanup "mkimage missing"
    fi

    # create uimage with the update file system
    echo "call mkimage to build fs uimage" >> $LOG
    if ! /usr/bin/mkimage -A arm -O linux -C none -T ramdisk -n update-fs -d \
        $UPDATE_IMAGE_GZ $UPDATE_IMAGE_GZ.uImage
    then
        cleanup "error making update fs image"
    fi

    rm -f $UPDATE_IMAGE_GZ
}

create_multi_uimage()
{
    # Extract the kernel from the kernel-image-* package in the update.
    # If it is not in the update, use the kernel in the boot partition.
    PATH_TO_PKG=`grep kernel-image- $UPDATE_LIST`
    if [ -n "$PATH_TO_PKG" ]
    then
        echo "unpacking kernel" >> $LOG
        if ar t $PATH_TO_PKG | grep data.tar.xz > /dev/null 2>&1
        then
            if ! ar p $PATH_TO_PKG data.tar.xz | xz | tar -C $TMP -xvf - \
                >> $LOG 2>&1
            then
                cleanup "error unpacking xz kernel"
            fi
        else
            if ! ar p $PATH_TO_PKG data.tar.gz | tar -C $TMP -xvzf - \
                >> $LOG 2>&1
            then
                cleanup "error unpacking kernel"
            fi
        fi
        KERNEL=`ls $TMP/boot/uImage-*`
    else
        # kernel is not in update, so use kernel already in /boot
        echo "using kernel from /boot" >> $LOG
        KERNEL=`ls /boot/uImage-*`
    fi

    echo "call mkimage to build update-uimage" >> $LOG
    if ! /usr/bin/mkimage -A arm -O linux -C none -T multi -n update-uimage \
        -d $KERNEL:$UPDATE_IMAGE_GZ.uImage $FINAL_UIMAGE >> $LOG 2>&1
    then
        cleanup "error making update image"
    fi
}

cleanup()
{
    echo $1 >> $LOG
    
    mount -o remount,ro /boot
    mount -o remount,ro /

    echo "remove tmp files" >> $LOG
    rm -f $UPDATE_IMAGE_GZ
    rm -f $UPDATE_IMAGE_GZ.uImage
    rm -fR $TMP/boot
    
    # remove log from last update
    rm -f /var/log/ota.log.gz
    
    echo "syncing" >> $LOG
    sync

    echo "exiting" >> $LOG
    exit
}


###### Entry point ######

if [ -f $UPDATE_FLAG ]
then
    # remove flag file intended for miniboot, so that if the script hangs, 
    # the device boots without attempting to install the update using the 
    # old upstart-based PmUpdater
    rm -f $UPDATE_FLAG
    
    # The 1.4 version of UpdateDaemon looks for a non-zero value in
    # update_status.txt to conclude that the revalidation failed.  Create
    # it now in case the script fails, and delete it on success.
    echo "E0060011" > $STATUS_FILE
else
    echo "$UPDATE_FLAG not found" > $LOG
    exit
fi
    
# Clear out tmp to make room for the file system and its uimage.  Do this
# before calling fbprogress (NOV-117253).
rm -fR $TMP/*
    
# redisplay shutdown animation since sendsigs killed fbprogress
/usr/bin/fbprogress -f battery-only &

echo "begin construction of $FINAL_UIMAGE" > $LOG

# mount root, boot, and clear tmp
mount_partitions

# unpack info package to get size for loop device and other parameters
read_updatefsinfo

# create a loop device for the update file system
create_loop_device

# add /dev nodes and necessary directories
make_dev_nodes

# untar files from packages in uimage-packages
copy_package_files

# do not validate when debugging since MD5SUM does not include debug files
if [ $DEBUG -eq 0 ]
then
    # validate file system
    validate_fs
fi

# gzip fs and create fs uimage
create_fs_uimage

# add kernel uimage to fs uimage, making multi uimage
create_multi_uimage
echo "return from create_multi_uimage" >> $LOG

# uimage successfully built, so write flag file for /etc/init.d/reboot
echo "update" > /tmp/reboot_reason

# 1.4 does not support bootie flag file, so point symlink to update-uimage
cd /boot
echo "change uImage link" >> $LOG
rm -f uImage
ln -s update-uimage uImage

# see entry point
echo "0" > $UPDATE_FLAG
rm -f $STATUS_FILE

echo "call cleanup to exit" >> $LOG
cleanup "$FINAL_UIMAGE constructed"

