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

# NOTE: busybox sort leaves a 0-length file when using "sort -o x x", so
#       there are several lines where I use "sort x > tmp; mv tmp x" instead

INFODIR=/usr/lib/ipkg/info
STATUSFILE=/usr/lib/ipkg/status
ROOT=/
PREFIX=
TMP=/tmp/mmipkg.$$
OLD_IPKS=$TMP/old_ipks
MOD_IPKS=$TMP/mod_ipks
OLD_FILE_MAP=$TMP/file_map.old
NEW_FILE_MAP=$TMP/file_map.new
ALL_PKGS=$TMP/all_pkgs

noaction=0
verbose=0
userootfs=0

usage()
{
    echo "mmipkg [-v] [-o root] [-n] [-r] install <list of ipks>" >&2
    echo "mmipkg [-v] [-o root] [-n] [-r] remove <list of pkgs>" >&2
    echo "mmipkg [-v] [-o root] [-n] info <list of pkgs>" >&2
    echo "mmipkg [-v] [-o root] [-n] status <list of pkgs>" >&2
    echo "mmipkg [-v] [-o root] [-n] files <list of pkgs>" >&2
}

errmsg()
{
    echo Error: $1
    exit 1
}

usageerr()
{
    echo Error: $1
    usage
    exit 1
}

echo_script_contents()
{
    echo "    Script contents:"
    echo "    ----------------"
    sed 's/^/    /' $INFODIR/$1.$2
    echo "    ----------------"
}

echo_run_script()
{
    if [ $verbose -eq 0 ]
    then
	   return
    fi
    
    if [ -f $INFODIR/$1.$2 ]
    then
        if [ $ROOT = "/" ] || [ $ROOT = "/rootfs" ]
        then
            echo "  Running $2 script for $1"
            echo_script_contents $1 $2
        else
            echo "  Not running $2 script for $1"
        fi
    fi
}

echo_list_files()
{
    if [ $verbose -eq 1 ]
    then
        echo "  $1 data files for $2"
        sed 's/^/    /' $INFODIR/$2.list
    fi
}

echo_control_files()
{
    if [ $verbose -eq 1 ]
    then
        # do not use wild card (NOV-94787)
        ls -1 $INFODIR/$1.control | sort | sed 's/^/    /'
        ls -1 $INFODIR/$1.list | sort | sed 's/^/    /'
        ls -1 $INFODIR/$1.md5sums | sort | sed 's/^/    /'
        if [ -e $INFODIR/$1.preinst ]
        then
            ls -1 $INFODIR/$1.preinst | sort | sed 's/^/    /'
        fi
        if [ -e $INFODIR/$1.postinst ]
        then
            ls -1 $INFODIR/$1.postinst | sort | sed 's/^/    /'
        fi
        if [ -e $INFODIR/$1.prerm ]
        then
            ls -1 $INFODIR/$1.prerm | sort | sed 's/^/    /'
        fi
        if [ -e $INFODIR/$1.postrm ]
        then
            ls -1 $INFODIR/$1.postrm | sort | sed 's/^/    /'
        fi
    fi
}

run_script()
{
    if [ -f $INFODIR/$1.$2 ]
    then
	if [ $ROOT = "/" ] || [ $ROOT = "/rootfs" ]
	then
	    echo "  Running $2 script for $1"
	    if [ $verbose -eq 1 ]
	    then
            echo_script_contents $1 $2
	    fi
        if [ $userootfs -eq 1 ]
        then
            # running from ram disk
            chroot $ROOT /${INFODIR#/*/}/$1.$2
        else
            $INFODIR/$1.$2
        fi
	    if [ $? -ne 0 ] 
	    then
            errmsg "Script execution failed"
	    fi
	else
	    echo "  Not running $2 script for $1"
	fi
    fi
}

remove()
{
    if [ $noaction -eq 1 ]
    then
        if [ ! -f $INFODIR/$1.control ]
        then
            errmsg "No such package $1"
        fi
        if [ $verbose -eq 1 ]
        then
            echo "Removing $1"
        fi
        echo_run_script $1 prerm
        echo_list_files Removing $1
        echo_run_script $1 postrm
        return
    fi
    
    echo "Removing $1"
    run_script $1 prerm
    if [ -f $INFODIR/$1.list ]
    then
        echo "  Removing data files for $1"
        if [ $verbose -eq 1 ]
        then
            sed 's/^/    /' $INFODIR/$1.list
        fi
        while read file
        do
            rm "$ROOT$file"
            
            # remove empty directories from current directory up
            DIR=`dirname "$ROOT$file"`
            while rmdir $DIR > /dev/null 2>&1
            do
                DIR=`dirname $DIR`
            done
        done < $INFODIR/$1.list
    fi
    
    run_script $1 postrm
    if [ -f $INFODIR/$1.control ]
    then
        echo "  Removing control files for $1"
        echo_control_files $1
        
        # do not use wild card (NOV-94787)
        rm $INFODIR/$1.control
        rm $INFODIR/$1.list
        rm $INFODIR/$1.md5sums
        rm -f $INFODIR/$1.preinst
        rm -f $INFODIR/$1.postinst
        rm -f $INFODIR/$1.prerm
        rm -f $INFODIR/$1.postrm
    fi
}

validate_ipk()
{
    case "$1" in
	*.ipk) ;;
	*) errmsg "$1 does not end with .ipk";;
    esac
    if [ ! -r $1 ]
    then
	   errmsg "Cannot read $1"
    fi
    if ! ar t $1 > /dev/null 2>&1
    then
	   errmsg "Cannot read ipk contents for $1"
    fi
    
    if ar t $1 | grep control.tar.xz > /dev/null 2>&1
    then
        if ! ar p $1 control.tar.xz | xz | tar tf - > /dev/null 2>&1
        then
            errmsg "Cannot unpack control.tar.xz for $1"
        fi
    else
        if ! ar p $1 control.tar.gz | tar tzf - > /dev/null 2>&1
        then
            errmsg "Cannot unpack control file for $1"
        fi
    fi
    
    if ar t $1 | grep data.tar.xz > /dev/null 2>&1
    then
        if ! ar p $1 data.tar.xz | xz | tar tf - > /dev/null 2>&1
        then
            errmsg "Cannot unpack data.tar.xz for $1"
        fi
    else
        if ! ar p $1 data.tar.gz | tar tzf - > /dev/null 2>&1
        then
            errmsg "Cannot unpack data file for $1"
        fi
    fi
}

unpack_control()
{
    echo "  Unpacking control file for $2"
    if ! mkdir -p $INFODIR
    then
	errmsg "Error creating info directory"
    fi
    
    if ar t $1 | grep control.tar.xz > /dev/null 2>&1
    then
        if ! ar p $1 control.tar.xz | xz |
        tar -C $INFODIR --transform "s,/\(.*\),/$2.\1," -xf -
        then
            errmsg "Error unpacking xz control files for $2"
        fi
    else
        if ! ar p $1 control.tar.gz |
        tar -C $INFODIR --transform "s,/\(.*\),/$2.\1," -xzf -
        then
            errmsg "Error unpacking control files for $2"
        fi
    fi
    
    echo "  Installing control files for $2"
    echo_control_files $2
}

unpack_data()
{
    echo "  Unpacking data file for $2"
    
    if ar t $1 | grep data.tar.xz > /dev/null 2>&1
    then
        ar p $1 data.tar.xz | xz |
            tar -C $ROOT -xvf - |
            grep -v '/$' | cut -c2- |
            sort | sed "s,^,$PREFIX," > $INFODIR/$2.list
        if [ $? -ne 0 ]
        then
            errmsg "Error xz unpacking data for $2"
        fi
    else
        ar p $1 data.tar.gz |
            tar -C $ROOT -xvzf - |
            grep -v '/$' | cut -c2- |
            sort | sed "s,^,$PREFIX," > $INFODIR/$2.list
        if [ $? -ne 0 ]
        then
            errmsg "Error unpacking data for $2"
        fi
    fi
    
    echo_list_files Installing $2
}

echo_install_script()
{
    if [ $verbose -eq 0 ]
    then
	   return
    fi

    if ar t $1 | grep control.tar.xz > /dev/null 2>&1
    then
        if ar p $1 control.tar.xz | xz | tar tf - | grep -q $2
        then
            if [ $ROOT = "/" ] || [ $ROOT = "/rootfs" ]
            then
                echo "  Running $2 script for $1"
                echo "    Script contents:"
                echo "    ----------------"
                ar p $1 control.tar.xz | xz | tar xOf - ./$2 | sed 's/^/    /'
                echo "    ----------------"
            else
                echo "  Not running $2 script for $1"
            fi
        fi
    else
        if ar p $1 control.tar.gz | tar tzf - | grep -q $2
        then
            if [ $ROOT = "/" ] || [ $ROOT = "/rootfs" ]
            then
                echo "  Running $2 script for $1"
                echo "    Script contents:"
                echo "    ----------------"
                ar p $1 control.tar.gz | tar xOzf - ./$2 | sed 's/^/    /'
                echo "    ----------------"
            else
                echo "  Not running $2 script for $1"
            fi
        fi
    fi   
}

echo_unpack_data()
{
    if [ $verbose -eq 0 ]
    then
	return
    fi
    echo "  Installing data files for $1"

    if ar t $1 | grep data.tar.xz > /dev/null 2>&1
    then
        ar p $1 data.tar.xz | xz |
        tar tf - | grep -v '/$' | cut -c2- | sort | sed "s,^,    $PREFIX,"
    else
        ar p $1 data.tar.gz |
        tar tzf - | grep -v '/$' | cut -c2- | sort | sed "s,^,    $PREFIX,"
    fi
}

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

install_prep()
{
    pkg=$(convert_ipk_to_pkg $1)
    if [ -f $INFODIR/$pkg.control ]
    then # Existing package
        # Create a list of all existing packages being updated
        echo $pkg >> $ALL_PKGS
        # Create a mapping from the package to the ipk file
        echo ${pkg}:$1 >> $OLD_IPKS
        # Create a list of mappings from the existing files to its package
        while read file
        do
            echo ${file}:$pkg
        done < $INFODIR/$pkg.list >> $OLD_FILE_MAP
        
        # Create a list of mappings from the existing files to its new package
        if ar t $1 | grep data.tar.xz > /dev/null 2>&1
        then
            ar p $1 data.tar.xz | xz | tar tf - |
            grep -v '/$' | cut -c2- | sed "s,^,$PREFIX," |
                while read file
                do
                    echo ${file}:$pkg
                done >> $NEW_FILE_MAP
        else
            ar p $1 data.tar.gz | tar tzf - |
            grep -v '/$' | cut -c2- | sed "s,^,$PREFIX," |
                while read file
                do
                    echo ${file}:$pkg
                done >> $NEW_FILE_MAP
        fi            
    else # New package
	   NEW_IPKS="$NEW_IPKS $1"
    fi
}

# Based on a list of packages which are being updated (in $ALL_PKGS), a
# mapping of packages to ipk filenames (in $OLD_IPKS), and a mapping of
# files to package for both the existing system (in $OLD_FILE_MAP) and the
# system # after the update (in $NEW_FILE_MAP), create a file ($MOD_IPKS)
# which lists the updated ipks in an order so that the install will work
analyze()
{
    test -f $ALL_PKGS || return

    ONLY_OLD=$TMP/only_old
    ONLY_NEW=$TMP/only_new
    OLD_FILES=$TMP/old_files
    NEW_FILES=$TMP/new_files
    MOVED_FILES=$TMP/moved_files
    DEPS=$TMP/deps
    NO_DEPS=$TMP/no_deps

    # Find the file:pkg mappings that are only in the existing set of packages
    # or only in the new set of packages
    sort $OLD_FILE_MAP > $TMP/x; mv $TMP/x $OLD_FILE_MAP
    sort $NEW_FILE_MAP > $TMP/x; mv $TMP/x $NEW_FILE_MAP
    comm -23 $OLD_FILE_MAP $NEW_FILE_MAP > $ONLY_OLD
    comm -13 $OLD_FILE_MAP $NEW_FILE_MAP > $ONLY_NEW

    # Find which files moved between packages
    cut -d: -f1 $ONLY_OLD | sort > $OLD_FILES
    cut -d: -f1 $ONLY_NEW | sort > $NEW_FILES
    comm -12 $OLD_FILES $NEW_FILES > $MOVED_FILES

    # Create a dependency map
    while read file
    do
        echo $(grep "^$file:" $ONLY_OLD | \
            cut -d: -f2):$(grep "^$file:" $ONLY_NEW | cut -d: -f2)
    done < $MOVED_FILES | sort -u > $DEPS

    sort $ALL_PKGS > $TMP/x; mv $TMP/x $ALL_PKGS
    while :
    do
        test -s $DEPS || break
        # Find the packages without a dependency (they can be installed first)
        cut -d: -f2 $DEPS | sort | comm -13 - $ALL_PKGS > $NO_DEPS
        test -s $NO_DEPS || errmsg "Circular dependency found"
        while read pkg
        do
            # Remove the package from the dependency file and the package list
            sed -i "/$pkg:/d" $DEPS
            sed -i "/$pkg$/d" $ALL_PKGS
            # Add the ipk for this package to the output list
            grep "^$pkg:" $OLD_IPKS | cut -d: -f2 >> $MOD_IPKS
	   done < $NO_DEPS
	   rm $NO_DEPS
    done
    
    # Add the ipks for the remaining packages to the output list
    while read pkg
    do
	   grep "^$pkg:" $OLD_IPKS | cut -d: -f2
    done < $ALL_PKGS >> $MOD_IPKS
}

install()
{
    echo "Installing $1"
    if [ $noaction -eq 1 ]
    then
        validate_ipk $1
        echo_install_script $1 preinst
        echo_unpack_data $1
        echo_install_script $1 postinst
	   return
    fi
    
    pkg=$(convert_ipk_to_pkg $1)
    if [ -n "$2" ]
    then
	   remove $pkg | sed 's/^/  /'
    fi
    
    unpack_control $1 $pkg
    run_script $pkg preinst
    unpack_data $1 $pkg
    run_script $pkg postinst
}

generate_status()
{
    if [ $noaction -eq 0 ]
    then
        rm -f $STATUSFILE
        for control in $INFODIR/*.control
        do
            grep -E '^Package|^Version|^Depends' $control
            echo "Status: install user installed"
            grep '^Architecture' $control
            echo ""
        done > $STATUSFILE
    fi
}

info()
{
    if [ -f $INFODIR/$1.control ]
    then
        #cat $INFODIR/$1.control
        grep -E '^Package|^Version|^Depends' $INFODIR/$1.control
    else
        errmsg "No such package $1"
    fi
}

files()
{
    if [ -f $INFODIR/$1.control ]
    then
	   cat $INFODIR/$1.list
    else
	   errmsg "No such package $1"
    fi
}

flash_firmware()
{
    TMP_MOD_IPKS=$TMP/tmp_mod_ipks
    touch $TMP_MOD_IPKS
    FOUND=0
    
    while read one_ipk
    do
        if [ $FOUND -eq 0 ]
        then
            if echo $one_ipk | grep cdmafw- - || echo $one_ipk | grep umtsfw- -
            then
                # flash firmware and remove it from MOD_IPKS
                install $one_ipk
                FOUND=1
            else
                echo $one_ipk >> $TMP_MOD_IPKS
            fi
        else
            echo $one_ipk >> $TMP_MOD_IPKS
        fi
    done < $MOD_IPKS

    mv $TMP_MOD_IPKS $MOD_IPKS

    # if it was not in modified packages, look in new packages
    if [ $FOUND -eq 0 ]
    then
        TMP_NEW_IPKS=
        for one_ipk in $NEW_IPKS
        do
            if echo $one_ipk | grep cdmafw- - || echo $one_ipk | grep umtsfw- -
            then
                # flash firmware and remove it from NEW_IPKS
                install $one_ipk
            else
                TMP_NEW_IPKS="$TMP_NEW_IPKS $one_ipk"
            fi
        done

        NEW_IPKS="$TMP_NEW_IPKS"
    fi
}

install_one_new_pkg()
{
    TMP_NEW_IPKS=
    for one_ipk in $NEW_IPKS
    do
        match=${one_ipk%$1}
        if [ $match != $one_ipk ]
        then
            # install package and remove it from NEW_IPKS
            install $one_ipk
        else
            TMP_NEW_IPKS="$TMP_NEW_IPKS $one_ipk"            
        fi
    done

    NEW_IPKS=$TMP_NEW_IPKS
}

install_one_mod_pkg()
{
    TMP_MOD_IPKS=$TMP/tmp_mod_ipks
    touch $TMP_MOD_IPKS
    
    while read one_ipk
    do
        match=${one_ipk%$1}
        if [ $match != $one_ipk ]
        then
            # install package and remove it from MOD_IPKS
            install $one_ipk

            if [ $1 = "kernel.ipk" ] && [ $noaction -eq 0 ]
            then
                # If kernel.ipk is installed, uImage is overwritten.  Restore 
                # symlink in case there is an installation retry.
                cd $ROOT/boot
                rm uImage
                ln -s update-uimage uImage
                sync
                ls -l uImage
                # do not stay in $ROOT/boot since post-update umounts it
                cd /
            fi
        else
            echo $one_ipk >> $TMP_MOD_IPKS
        fi
    done < $MOD_IPKS

    mv $TMP_MOD_IPKS $MOD_IPKS
}


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

# Parse arguments
while getopts hno:rv options
do
    case $options in
	 h) usage; exit 0;;
	 n) noaction=1;;
	 o) ROOT=$OPTARG;;
     r) userootfs=1;;   
	 v) verbose=1;;
	\?) usageerr "Unknown option";;
    esac
done
shift $(($OPTIND - 1))

test $# -ne 0 || usageerr "Missing command"
case $ROOT in
    /*) ;;
     *) usageerr "-o value must be an absolute path" ;;
esac

if [ $userootfs -eq 1 ]
then
    ROOT=/rootfs
    INFODIR=$ROOT/usr/lib/ipkg/info
    STATUSFILE=$ROOT/usr/lib/ipkg/status
elif [ $ROOT != "/" ]
then
    INFODIR=$ROOT/usr/lib/ipkg/info
    STATUSFILE=$ROOT/usr/lib/ipkg/status
    PREFIX=$ROOT
fi

case $1 in
    install)
        shift
        mkdir $TMP
        touch $MOD_IPKS
        # Analyze ipks and put them in a valid install order
        for ipk
        do
            install_prep $ipk
        done        
        analyze
        
        # Install new toolchain first.  postinst scripts need toolchain on
        # rootfs, but old codesourcery is deleted before any packages are
        # installed.
        install_one_new_pkg armtoolchain.ipk
        
        # Install PmModemUpdater if it is in the update.  Flashing the firmware
        # depends on the latest version of PmModemUpdater.
        install_one_mod_pkg pmmodemupdater.ipk

        # uImage symlink must be restored after kernel.ipk is installed
        install_one_mod_pkg kernel.ipk

        # Flashing firmware fails if done after 10+ minutes into the install
        flash_firmware
        
        # Install modified ipks
        while read ipk
        do
            install $ipk mod
        done < $MOD_IPKS
        # Install new ipks
        for ipk in $NEW_IPKS
        do
            install $ipk
        done
        rm -rf $TMP
        generate_status;;
        
    remove)
        shift
        for pkg
        do
            remove $pkg
        done;;
        
    info|status)
        shift
        for pkg
        do
            info $pkg
        done;;
        
    files)
        shift
        for pkg
        do
            files $pkg
        done;;
        
    *) usageerr "Unrecognized command $1";;
esac
