#! /bin/bash

function usage() {
  cat <<EoF
** Remix a typical live ISO 9660 image (e.g. CD image)
** giving it some additional last-minute custom files,
** including its very own random.seed file.

Usage:  $0 [options] src.iso dest.iso
Or:     $0 [options] target.iso -u

Options include:
 -help              print this message, then exit
 -verbose           increase verbosity
 -v -h              print longer message, then exit
 -update-in-place   update ISO image in place.
 -assume-scripts    assume unpacking scripts already in place; don't check
EoF

  if test "$verbose" -gt 0 ; then
    cat <<EoF
 -wait     for experts only:
           ... start a subshell so you can look at intermediate results;
           ... exit the subshell to finish the job
           ... exit 86 to abort and clean up

Each option can be abbreviated to a single letter.

Here's the rationale for the whole exercise: For security, it is
important for each bootable disk to have a random-seed file whose
contents are unique and unknown to possible attackers.  This is true
even for read-only disks.  A leading application for this script is to
add a unique random-seed file onto a Live CD.iso filesystem image.

More generally: Suppose you want to make a large number of disks all
different.
 a) For the first copy, you should
     $0 ubunto-\$version-\$arch.iso foo.iso
    which installs the fs_fixup.d/*.tgz archive files _and_
    installs the script that will unpack those archives.
    Then burn foo.iso to disk (CD, memory stick, or whatever).
 b) For each of the remaining N-1 copies, you should
     $0 foo.iso -update
    or perhaps
     $0 foo.iso -update -assume-scripts
    Then burn foo.iso to disk.
    This step using -update-in-place is about 20 times faster than
    step (a), because the scripts are already installed, and all we
    need to do is replace the *.tgz files.  Using -assume-scripts
    speeds this up by another order of magnitude.

Note that running the script without the -u option requires root
privileges, or more precisely enough privileges to use do a "mount -o
loop".  In contrast, any humble user can do an update-in-place.

Hint for testing the images produced by this script:
   qemu -boot order=d -m 512 -net none -cdrom whatever.iso
Early in the boot sequence, hit ESC ESC Enter to 
   "Try Ubuntu without installing."
EoF
fi
}

###########
# Rough Draft -- testing stage
# It seems to work chez moi, but has not been carefully
# tested.
#
# The rationale for the design goes like this:
#  -- Rebuilding the main filesystem.squashfs file would
#   by verrry slow and we do not need to do that.
#  -- Rebuilding the initrd.lz file is fairly slow,
#   and we avoid that if possible, i.e. if our unpacking
#   script is already there.  However, if necessary, we
#   will install our script and rebuild this fs.
#  -- Rebuilding the outermost .iso filesystem is also
#   fairly slow.  Sometimes that is necessary, but it
#   should only be necessary once.
#  -- When making N disks, N-1 of the fixups should be
#   very fast, since we can do an update-in-place.
#
# Part of the conceptual burden is the fact that there are
# four different filesystems in play:
# -- the initial CD.iso filesystem;
# -- the initrd.lz filesystem;
# -- the fixup.tgz archive; and
# -- the final .squashfs filesystem.
#
# We use a script located in the initrd filesystem.
# The script looks into the CD.iso filesystem,
# finds the fixup.tgz archive,
# and transfers the contents to the final .squashfs filesystem.
#
# General reference on how to build remixed CDs:
#   https://help.ubuntu.com/community/LiveCDCustomization
#   "How to Customise the Ubuntu Desktop CD"
#
# Reference on the structure of Debian boot disks:
#  http://bromavilleherald.com/index.php/Casper_boot_process
#
# Another useful reference, including why /var is called /var:
#    http://www.pathname.com/fhs/pub/fhs-2.3.html#THEFILESYSTEM
#
# Hint for testing the images produced by this script:
#   qemu -boot order=d -m 512 -net none -cdrom whatever.iso
#
# Another hint:  You can pass "kernel command line" parameters
# to the Live CD if you hit "enter, enter, F6" soon after the
# boot process starts, or by editing isolinux/text.cfg ....
# Passing "break=init" is good for debugging.
#
# Also note:
#  On my machine I had to change BIOS settings to enable KVM.
#  If KVM is not enabled, the "kvm" kernel module loads
#  seemingly OK, with no error message, but is non-functional.
#  I consider this a bug.
#  The thing should either work or print an informative message.
#  The primary symptom of non-functional VM features is that
#  the /dev/kvm device does not exist, even after the kvm module
#  is loaded.
#  A secondary symptom is that qemu runs very slowly
#    (and complains about the lack of /dev/kvm).
#
#  TODO:  Recompute checksum of initrd.lz (if needed) so that
#   it passes the ubuntu CD integrity check.
#
###########

bindir=$( cd $( dirname $0) ; pwd )
userdir=$( pwd )
verbose=0

##
## This function prints the text of the script
## that will be installed as "scripts/casper-bottom/06fixup_fs"
function new_script() {
<<\EoF cat
#!/bin/sh

PREREQ=""
DESCRIPTION="Install various last-minute additions to the filesystem."

prereqs()
{
       echo "$PREREQ"
}

case $1 in
# get pre-requisites
prereqs)
       prereqs
       exit 0
       ;;
esac

. /scripts/casper-functions

log_begin_msg "$DESCRIPTION"

# Install various last-minute additions to the filesystem

umask 177

# fix PATH so we can find tar:
PATH=$PATH:${rootmnt}/bin:${rootmnt}/usr/bin:

for zfile in $rootmnt/cdrom/casper/fs_fixup.d/*.tgz       \
             $rootmnt/isodevice/casper/fs_fixup.d/*.tgz ; do
  if test -f "$zfile" ; then
    tar -C $rootmnt -xpvf "$zfile"
  fi
done

if : TEMPORARY KLUDGE ; then
  ## This code does not belong here.
  ## It belongs in init.d/urandom ...
  ## but since it is not there yet, doing it here
  ## is better than not doing it at all.
  ## Doing it twice is harmless.
  ## Feel free to delete this after init.d/urandom
  ## has been brought up to speed.
  date +%s.%N > ${rootmnt}/dev/random
fi

log_end_msg
EoF
}

function the_patch() {
<<\EoF cat
--- scripts/casper-bottom/ORDER	2010/08/02 22:58:47	1.1
+++ scripts/casper-bottom/ORDER	2010/08/02 22:59:55
@@ -4,6 +4,8 @@
 [ -e /conf/param.conf ] && . /conf/param.conf
 /scripts/casper-bottom/05mountpoints_lupin
 [ -e /conf/param.conf ] && . /conf/param.conf
+/scripts/casper-bottom/06fixup_fs
+[ -e /conf/param.conf ] && . /conf/param.conf
 /scripts/casper-bottom/10adduser
 [ -e /conf/param.conf ] && . /conf/param.conf
 /scripts/casper-bottom/10custom_installation
EoF
#######
}


# main program:
src=''
dest=''
update=''
unset redo_md5

for arg in $* ; do
  xarg="$( echo "$arg" | sed 's/^--/-/' )"
  case $xarg in
    [?]|-[?]|-h*)
      usage
      exit 0
      ;;
    -u|-up|-update|-update-in-place)
      update=yes
      ;;
    -v|-verbose)
      ((verbose++))
      ;;
    -w|-wait)
      wait=yes
      ;;
    -a|-assume-scrpits)
      assume_scripts=yes
      ;;
    -*)
      1>&2 echo "Unrecognized option '$arg'; try $0 -help"
      exit 1
      ;;
    *) if test -z "$src" ; then
         src=$arg
       else
         dest=$arg
       fi
  esac
done

if test -z "$src"; then
  1>&2 echo "A source ISO image must be specified."
  1>&2 echo "Try $0 -help"
  exit 1
fi

if ! test -r "$src"; then
  1>&2 echo "Cannot read source image '$src'"
  exit 1
fi

if test -z "$dest" -a -z "$update" ; then
  1>&2 echo "A destination ISO image (or -update) must be specified."
  1>&2 echo "Try $0 -help"
  exit 1
fi

if test -n "$dest" -a -n "$update" ; then
  1>&2 echo "You can't specify a destination with -update."
  1>&2 echo "Try $0 -help"
  exit 1
fi

unset cleanup

function clean1() {
  local top
  top="${cleanup[${#cleanup[@]}-1]}"
  unset cleanup[${#cleanup[@]}-1]
  $top
}

function quit() {
  while test -n "${cleanup[*]}" ; do
    clean1
  done
  exit $1
}

## end of function definitions
################################################
## start of main code


vol_label=$(
  2>/dev/null dd if=$src skip=32808 count=32  bs=1  \
  | sed 's/ *$//'
)

#xx echo "ISO volume label: '$vol_label'" ; quit 0


mydir=/tmp/fixup-live-cd-$$.d
mkdir $mydir
cleanup[${#cleanup[@]}]="rmdir $mydir"

if test -z "$assume_scripts" ; then
  ird_changed=''
  mkdir $mydir/ird
  cleanup[${#cleanup[@]}]="rm -rf $mydir/ird"

# Unpack the initial ramdisk,
# which contains several things we need to check.
# Unpacking the whole thing at once is quickest.
# Takes about 1 second on my machine.
  #xx date +%s.%N
  $bindir/isofs cat $src/casper/initrd.lz | unlzma | (
    cd $mydir/ird
    cpio -imd --no-absolute-filenames --quiet
  )
  if test "$?" -ne 0 ; then
    1>&2 echo "Failed to unpack initrd.lz"
    quit 1
  fi
  #xx date +%s.%N

# Check on the initial ramdisk.
# Upgrade the script stuff, if necessary.
    cd $mydir/ird

## Install the fixup_fs script, if necessary.
## Compare checksums to see whether old script is
## identical to new script.
  < <(new_script | md5sum) read newsum junk
  script="scripts/casper-bottom/06fixup_fs"
  if test -x $script ; then
    < <(<$script md5sum) read oldsum junk
  fi
  if test "_$newsum" = "_$oldsum" ; then
    if test $verbose -gt 0 ; then
      echo ": Existing script '$script' is up to date."
    fi
  else
    new_script > $script
    chmod a+rx $script
    if test $verbose -gt 0 ; then
      echo ": Installed script '$script' from scratch."
    fi
    ird_changed=yes
  fi

## patch the ORDER file, so the fixup_fs script actually gets called.
  if the_patch | patch -p0 --force --reverse --dry-run >/dev/null ; then
    if test $verbose -gt 0 ; then
      echo : The ORDER patch is already in place.
    fi
  else
    the_patch | patch -p0 --quiet --forward || quit 2
    ird_changed=yes
    if test $verbose -gt 0 ; then
      echo ": Patched casper-bottom/ORDER"
    fi
  fi

## Rebuild initrd.lz if necessary, and if non-futile.
  cd $mydir
  if test -n "$ird_changed" ; then
    if test -z "$dest" ; then
      1>&2 echo "This image cannot be updated in place."
      1>&2 echo "It needs to be rebuilt."
      quit 1
    fi
    if test -n "$TESTING"; then
      if test $verbose -gt 0 ; then
        1>&2 echo ": Installing FAKE initrd.lz !!!!"
      fi
      echo test-test-test > $mydir/initrd.lz    ## quick, for testing
    else
## Rebuilding takes about 20 seconds.
      cd $mydir/ird
      #xx date +%s.%N
      if test $verbose -gt 0 ; then
        1>&2 echo -n ": Rebuilding initrd.lz ..."
      fi
      # The "-H newc" is needed here, because
      # whatever the default format is, the kernel doesn't like it.
      find . | cpio -o --quiet --dereference -H newc \
         | lzma -7 > $mydir/initrd.lz
      if test $verbose -gt 0 ; then 1>&2 echo "." ; fi
      #xx date +%s.%N

      redo_md5[${#redo_md5[@]}]="./casper/initrd.lz"
      # destination will be: $mydir/srccopy/casper/initrd.lz
    fi
  fi
fi

# Build the random-seed-fixup file. 
# We're going to need it sooner or later.

# First, create the desired content.
  mkdir $mydir/tartmp
  cd $mydir/tartmp
  rseed=./var/lib/urandom/random-seed
  install --mode=600 -D /dev/null $rseed
  2>/dev/null dd if=/dev/urandom of=$rseed      \
         bs=1 count=512 || (1>&2 echo "urandom 'dd' failed" ; quit 1)
  if : TESTING ; then
    date > $rseed-static-date       # indicate that Kilroy was here
  fi

# Bundle it up.
# Coerce owner and group to "root",
# even if this CD image is being built by a non-privilged user.
# This is important for security of the random-seed file.
#
# Also, pad it to an integral number of ISO sectors.
# We expect all urandom.tgz files to fit comfortably into
# a single 2048-byte sector, since the payload is only 512 bytes.
# The gzip format is self-delimiting, so padding is harmless.

  cd $mydir
  tar --owner=0 --group=0 -C tartmp -c . | gzip        \
     | 2>/dev/null dd bs=2048 conv=sync of=urandom.tgz
   
  if test "$?" -ne 0 ; then
    1>&2 echo "Failed to construct urandom.tgz" ; quit 1
  fi
  redo_md5[${#redo_md5[@]}]="./casper/fs_fixup.d/urandom.tgz"
  rm -rf tartmp

  cd $userdir
  newsize=$( stat -c %s $mydir/urandom.tgz )
  < <( $bindir/isofs ls $src/casper/fs_fixup.d/urandom.tgz ) \
    read oldsize where fn junk

# See if we need to rebuild the ISO image
  if test -f "$mydir/initrd.lz" \
    -o "_$newsize" != "_$oldsize" ; then

    if test -z "$dest" ; then
      1>&2 echo "This image cannot be updated in place."
      1>&2 echo "It needs to be rebuilt."
      quit 1
    fi

# Mount the source image.
# This will be read-only, alas.
    mkdir $mydir/src
    cleanup[${#cleanup[@]}]="rmdir $mydir/src"
    mount -o loop $src $mydir/src
    cleanup[${#cleanup[@]}]="umount $mydir/src"

# Make a writable copy.
# This takes about 17 seconds.
    #xx date +%s.%N
    if test $verbose -gt 0 ; then
      1>&2 echo -n ": Copying ISO filesystem ..."
    fi
    mkdir $mydir/srccopy
    cleanup[${#cleanup[@]}]="rm -rf $mydir/srccopy"
    tar -C $mydir/src -c . | tar -C $mydir/srccopy -xp
    if test $verbose -gt 0 ; then 1>&2 echo "." ; fi
    #xx date +%s.%N

# Install the goodies:
    mv $mydir/initrd.lz   $mydir/srccopy/casper/
    mkdir                 $mydir/srccopy/casper/fs_fixup.d/
    mv $mydir/urandom.tgz $mydir/srccopy/casper/fs_fixup.d/

    if test -n "$wait" ; then
        echo "Waiting for you to look and/or hack around."
        echo "Exit 86 to bail out and clean up."
        ( cd $mydir ; PSx=hack bash )
        if test $? = 86 ; then quit 86 ; fi
    fi
    if test "_${vol_label}" = "_${vol_label#r+}" ; then
      vol_label="r+$vol_label"
      # else the label already started with "r+"
    fi

# Fix up the checksums:
  cd $mydir/srccopy
  cleanup[${#cleanup[@]}]="rm -f $mydir/md5sum.new"
  $bindir/update-md5 -i md5sum.txt "${redo_md5[@]}" > ../md5sum.new || quit 1
  cp ../md5sum.new md5sum.txt

# Generate new ISO image.
# Takes about 23 seconds.
    cd $userdir
    #xx date +%s.%N
    if test $verbose -gt 0 ; then
      1>&2 echo -n ": Generating new ISO image ..."
    fi
    genisoimage -r -J -D -V "$vol_label" -cache-inodes  \
       -l -b isolinux/isolinux.bin -c isolinux/boot.cat   \
       -no-emul-boot -boot-load-size 4 -boot-info-table   \
       -input-charset utf-8 -quiet                    \
       -o $dest $mydir/srccopy
    if test $verbose -gt 0 ; then 1>&2 echo "." ; fi
    #xx date +%s.%N

  else
    if test -n "$dest" ; then
      #xx date +%s.%N
      if test $verbose -gt 0 ; then
        1>&2 echo -n ": Copying ISO image, src to dest ..."
      fi
      # Copying takes about 23 seconds on my machine.
      cp $src $dest
      if test $verbose -gt 0 ; then 1>&2 echo -n "." ; fi
      #xx date +%s.%N
      target=$dest
    else
      if test $verbose -gt 0 ; then
        1>&2 echo -n ": Updating ISO image in place ..."
      fi
      target=$src
    fi

    $bindir/isofs cp $mydir/urandom.tgz $target/casper/fs_fixup.d/urandom.tgz

# Fix up the checksums:
    cd $mydir
    cleanup[${#cleanup[@]}]="rm -rf $mydir/srccopy"
    mkdir ./srccopy
    mkdir ./srccopy/casper
    mkdir ./srccopy/casper/fs_fixup.d
    mv ./urandom.tgz ./srccopy/casper/fs_fixup.d/

    cd $userdir
    $bindir/isofs cp $target/md5sum.txt  $mydir/srccopy/md5sum.txt
    cd $mydir/srccopy
    cleanup[${#cleanup[@]}]="rm -f $mydir/md5sum.new"
    $bindir/update-md5 -i md5sum.txt "${redo_md5[@]}" > ../md5sum.new || quit 1

    cd $userdir
    $bindir/isofs cp $mydir/md5sum.new $target/md5sum.txt

  fi  # end (not) rebuilding ISO image

quit 0
