#! /usr/bin/perl

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# FirmwareUpdateKit version 2.0
#
# Create bootable DOS system to assist with DOS-based firmware updates.
#
# Copyright (c) 2008 Steffen Winterfeldt
#
# License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
# This is free software: you are free to change and redistribute it.


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# package HDImage version 1.7
#
# Create disk image with partition table and a single partition.
#
# Copyright (c) 2008 Steffen Winterfeldt
#
# License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
# This is free software: you are free to change and redistribute it.
#
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{
  package HDImage;

  use strict;
  use integer;
  use bigint;

  sub new
  {
    my $self = {};

    bless $self;

    # initialize value
    $self->{fit_size_acc} = 0;

    return $self;
  }

  sub verbose
  {
    my $self = shift;

    $self->{verbose} = shift;
  }

  sub mbr
  {
    my $self = shift;

    if(@_) {
      my $file = shift;
      open F1, $file;
      sysread F1, $self->{mbr}, 440;
      close F1;

      if(length($self->{mbr}) != 440) {
        print STDERR "warning: $file: no valid MBR\n";
      }
    }
    else {
      undef $self->{mbr};
    }
  }

  sub boot_fat12
  {
    my $self = shift;

    if(@_) {
      my $file = shift;
      open F1, $file;
      sysread F1, $self->{boot_fat12}, 512;
      close F1;

      if(length($self->{boot_fat12}) != 512 || substr($self->{boot_fat12}, 0x1fe, 2) ne "\x55\xaa") {
        print STDERR "warning: $file: no valid boot block\n";
      }
    }
    else {
      undef $self->{boot_fat12};
    }
  }

  sub boot_fat16
  {
    my $self = shift;

    if(@_) {
      my $file = shift;
      open F1, $file;
      sysread F1, $self->{boot_fat16}, 512;
      close F1;

      if(length($self->{boot_fat16}) != 512 || substr($self->{boot_fat16}, 0x1fe, 2) ne "\x55\xaa") {
        print STDERR "warning: $file: no valid boot block\n";
      }
    }
    else {
      undef $self->{boot_fat16};
    }
  }

  sub chs
  {
    my $self = shift;
    my $c = shift;
    my $h = shift;
    my $s = shift;

    $h = 255 if $h < 1 || $h > 255;
    $s = 63 if $s < 1 || $s > 63;

    $self->{h} = $h;
    $self->{s} = $s;

    if($c == 0 && $self->{size}) {
      $c = ($self->{size} + $h * $s - 1) / $h / $s;
    }

    if($c > 0) {
      $self->{c} = $c;
      $self->{size} = $c * $h * $s;
    }

    return $self->{size};
  }

  sub size
  {
    my $self = shift;
    my $size = $self->parse_size(shift);

    $self->{size} = $size;
    if($self->{h} && $self->{s}) {
      $self->{c} = ($self->{size} + $self->{h} * $self->{s} - 1) / $self->{h} / $self->{s};
      $self->{size} = $self->{c} * $self->{h} * $self->{s};
    }

    return $self->{size};
  }

  sub extra_size
  {
    my $self = shift;

    $self->{extra_size} = $self->parse_size(shift);
  }

  sub type
  {
    my $self = shift;

    $self->{type} = shift;
  }

  sub fit_size
  {
    my $self = shift;

    $self->{fit_size} = shift;
  }

  sub label
  {
    my $self = shift;

    $self->{label} = shift;
  }

  sub fs
  {
    my $self = shift;

    $self->{fs} = shift;
  }

  sub add_files
  {
    my $self = shift;
    my $s;
    local $_;

    for (@_) {
      if(-f || -d) {
        push @{$self->{files}}, $_;
        if($self->{fit_size}) {
          $s = `du --apparent-size -k -s $_ 2>/dev/null`;
          $s =~ s/\s.*$//;
          $s <<= 1;
          $self->{fit_size_acc} += $s;
        }
      }
      else {
        print STDERR "$_: no such file or directory\n";
      }
    }
  }

  sub tmp_file
  {
    my $self = shift;

    chomp (my $t = `mktemp /tmp/HDImage.XXXXXXXXXX`);
    die "error: mktemp failed\n" if $?;

    eval 'END { unlink $t }';

    my $s_t = $SIG{TERM};
    $SIG{TERM} = sub { unlink $t; &$s_t if $s_t };

    my $s_i = $SIG{INT};
    $SIG{INT} = sub { unlink $t; &$s_i if $s_i };

    return $t;
  }

  sub partition_ofs
  {
    my $self = shift;

    $self->{part_ofs} = $self->parse_size(shift) if @_;

    return defined($self->{part_ofs}) ? $self->{part_ofs} : $self->{s};
  }

  sub write
  {
    my $self = shift;
    local $_;

    return undef unless @_;

    my $file = shift;
    $self->{image_name} = $file;

    $self->chs(0, 255, 63) unless $self->{s};

    $self->size($self->{size} + $self->{fit_size_acc}) if $self->{fit_size_acc};

    $self->{extra_size} = 0 if !defined $self->{extra_size};

    my $p_size = $self->{size} - $self->partition_ofs - $self->{extra_size};
    return undef if $p_size < 0;

    my $c = $self->{c};
    my $h = $self->{h};
    my $s = $self->{s};
    my $type = $self->{type};

    my $pt_size = $self->partition_ofs;
    my $p_end = $p_size + $pt_size - 1;

    $type = 0x83 unless defined $type;

    print "$file: chs = $c/$h/$s, size = $self->{size} blocks\n" if $self->{verbose};

    print "- writing mbr\n" if $self->{verbose} && $self->{mbr};

    $c = 1023 if $c > 1023;

    my $s_0 = $pt_size % $s + 1;
    my $h_0 = ($pt_size / $s) % $h;
    my $c_0 = $pt_size / ($s * $h);
    $c_0 = 1023 if $c_0 > 1023;

    my $s_1 = $p_end % $s + 1;
    my $h_1 = ($p_end / $s) % $h;
    my $c_1 = $p_end / ($s * $h);
    $c_1 = 1023 if $c_1 > 1023;

    my $p_0 = $pt_size;
    $p_0 = 0xffffffff if $p_0 > 0xffffffff;
    my $p_1 = $p_size;
    $p_1 = 0xffffffff if $p_1 > 0xffffffff;

    open W1, ">$file";
    if($pt_size) {
      my $mbr = pack (
        "Z446CCCCCCCCVVZ48v",
        $self->{mbr},                 # boot code, if any
        0x80,                         # bootflag
        $h_0,                         # head start
        (($c_0 >> 8) << 6) + $s_0,    # cyl/sector start, low
        $c_0 & 0xff,                  # cyl/sector start, hi
        $type,                        # partition type
        $h_1,                         # head last
        (($c_1 >> 8) << 6) + $s_1,    # cyl/sector last, low
        $c_1 & 0xff,                  # cyl/sector last, hi
        $p_0,                         # partition offset
        $p_1,                         # partition size
        "", 0xaa55
      );

      syswrite W1, $mbr;
      if($pt_size > 1) {
        sysseek W1, $pt_size * 512 - 1, 0;
        syswrite W1, "\x00", 1;
      }
    }
    close W1;

    if($p_size) {
      if($self->{fs}) {
        my $f = $pt_size ? tmp_file() : $file;
        open W1, ">$f";
        sysseek W1, $p_size * 512 - 1, 0;
        syswrite W1, "\x00", 1;
        close W1;
        if($self->{fs} eq 'fat') {
          my $x = " -n '$self->{label}'" if $self->{label} ne "";
          system "mkfs.vfat -h $pt_size$x $f >/dev/null";

          my ($fat, $boot);

          # mkfs.vfat is a bit stupid; fix FAT superblock
          open W1, "+<$f";
          sysseek W1, 0x18, 0;
          syswrite W1, pack("vv", $s, $h);
          sysseek W1, 0x24, 0;
          syswrite W1, "\xff";
          sysseek W1, 0x36, 0;
          sysread W1, $fat, 5;
          # FAT32: at ofs 0x52
          close W1;

          $boot = $self->{boot_fat12} if $fat eq "FAT12";
          $boot = $self->{boot_fat16} if $fat eq "FAT16";

          # write boot block ex bpb
          if($boot) {
            print "- writing \L$fat\E boot block\n" if $self->{verbose};
            open W1, "+<$f";
            syswrite W1, $boot, 11;
            sysseek W1, 0x3e, 0;
            syswrite W1, substr($boot, 0x3e);
            close W1;
          }

          if($self->{files}) {
            print "- copying:\n    " . join("\n    ", @{$self->{files}}) . "\n" if $self->{verbose};
            system "mcopy -D o -s -i $f " . join(" ", @{$self->{files}}) . " ::";
          }
        }
        elsif($self->{fs} eq 'ext2' || $self->{fs} eq 'ext3') {
          my $x = " -L '$self->{label}'" if $self->{label} ne "";
          system "mkfs.$self->{fs} -q -m 0 -F$x $f";
          system "tune2fs -c 0 -i 0 $f >/dev/null 2>&1";
        }
        elsif($self->{fs} eq 'reiserfs') {
          my $x = " -l '$self->{label}'" if $self->{label} ne "";
          system "mkfs.reiserfs -q -ff$x $f";
        }
        elsif($self->{fs} eq 'xfs') {
          my $x = " -L '$self->{label}'" if $self->{label} ne "";
          system "mkfs.xfs -q$x $f";
        }
        elsif($self->{fs} eq 'ntfs') {
          my $x = " -L '$self->{label}'" if $self->{label} ne "";
          system "mkfs.ntfs -f -F$x -s 512 -H $self->{h} -S $self->{s} -p ${\($self->partition_ofs)} $f";
        }
        else {
          print STDERR "warning: $self->{fs}: unsupported file system\n";
        }

        if($pt_size) {
          system "cat $f >>$file";
          unlink $f;
        }
      }
      else {
        open W1, "+<$file";
        sysseek W1, ($self->{size} - $self->{extra_size}) * 512 - 1, 0;
        syswrite W1, "\x00", 1;
        close W1;
      }
    }

    if($self->{extra_size}) {
      open W1, "+<$file";
      sysseek W1, $self->{extra_size} * 512 - 1, 2;
      syswrite W1, "\x00", 1;
      close W1;
    }
  }

  sub parse_size
  {
    my $self = shift;
    my $s = shift;
    my $bs = 0;

    if($s =~ s/(b|k|M|G|T|P|E)$//) {
      $bs =  0 if $1 eq 'b';
      $bs =  1 if $1 eq 'k';
      $bs = 11 if $1 eq 'M';
      $bs = 21 if $1 eq 'G';
      $bs = 31 if $1 eq 'T';
      $bs = 41 if $1 eq 'P';
      $bs = 51 if $1 eq 'E';
    }

    # note: 'bigint' works a bit differently when converting strings to numbers

    $s = $s << $bs;

    return $s + 1 == $s ? undef : $s;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
use integer;
use strict;

use Getopt::Long;

sub usage;
sub tmp_dir;
sub do_iso;
sub do_lilo;
sub do_grub1;
sub do_grub2;

usage 0 if !@ARGV;

my $opt_verbose;
my $opt_iso;
my $opt_floppy;
my $opt_image;
my $opt_lilo;
my $opt_grub1;
my $opt_grub2;
my $opt_title = "Firmware Update";
my $opt_run;
# leave some free space
my $opt_free = "100k";

GetOptions(
  'help'     => sub { usage 0 },
  'verbose+' => \$opt_verbose,
  'version'  => sub { print "2.0\n"; exit 0 },
  'iso=s'    => \$opt_iso,
  'floppy=s' => \$opt_floppy,
  'image=s'  => \$opt_image,
  'lilo'     => \$opt_lilo,
  'grub1'    => \$opt_grub1,
  'grub2'    => \$opt_grub2,
  'title=s'  => \$opt_title,
  'run=s'    => \$opt_run,
  'free=s'   => \$opt_free,
) || usage 1;

usage 1 if !@ARGV;

# force PATH to sensible system default
$ENV{PATH} = "/usr/bin:/bin:/usr/sbin:/sbin";

my @files;
@files = @ARGV;

my $fuk_dir = tmp_dir;

open F, ">$fuk_dir/config.sys";
print F "switches=/f\n";
print F "device=c:\\himemx.exe\n";
close F;
open F, ">$fuk_dir/autoexec.bat";
print F "$opt_run\n" if $opt_run;
close F;

my $hdimage = HDImage::new;
$hdimage->verbose($opt_verbose);

$hdimage->size($opt_free);

if($opt_floppy) {
  $hdimage->chs(80, 2, 18);
  $hdimage->partition_ofs(0);
}
else {
  $hdimage->chs(0, 4, 16);
  $hdimage->fit_size(1);
}

$hdimage->type(1);
$hdimage->label('FWUPDATE');
$hdimage->fs('fat');
$hdimage->mbr('/usr/share/syslinux/mbr.bin');
$hdimage->boot_fat12('/usr/share/FirmwareUpdateKit/freedos_boot.fat12');
$hdimage->boot_fat16('/usr/share/FirmwareUpdateKit/freedos_boot.fat16');
$hdimage->add_files(('/usr/share/FirmwareUpdateKit/kernel.sys'));
$hdimage->add_files(('/usr/share/FirmwareUpdateKit/himemx.exe'));
$hdimage->add_files(('/usr/share/FirmwareUpdateKit/command.com'));
$hdimage->add_files(("$fuk_dir/config.sys", "$fuk_dir/autoexec.bat"));
$hdimage->add_files(@files);

if($opt_floppy) {
  $hdimage->write("$opt_floppy");

  exit 0;
}
elsif($opt_image) {
  $hdimage->write("$opt_image");

  exit 0;
}
else {
  $hdimage->write("$fuk_dir/fwupdate.img");
}

my $memdisk_option = "harddisk c=$hdimage->{c} h=$hdimage->{h} s=$hdimage->{s}";

if(!-f "/usr/share/syslinux/memdisk") {
  die "/usr/share/syslinux/memdisk: no such file\nPlease install package 'syslinux'.\n";
}

my $done = 0;
$done = do_iso $fuk_dir if $opt_iso;
$done = do_lilo $fuk_dir if $opt_lilo;
$done = do_grub1 $fuk_dir if $opt_grub1;
$done = do_grub2 $fuk_dir if $opt_grub2;

if(!$done) {
  print
    "Warning: nothing done.\n" .
    "Please use one of these options: --grub1, --grub2, --lilo, --iso, --floppy or --image.\n";
}


sub usage
{
  print <<"  usage";
Usage: fuk [OPTIONS] FILES
FirmwareUpdateKit version 2.0.

Create bootable DOS system and add FILES to it.
The main purpose is to assist with DOS-based firmware updates.

Options:
  --grub1                       Add boot entry to /boot/grub/menu.lst.
  --grub2                       Add boot entry to XXX.
  --lilo                        Add boot entry to /etc/lilo.conf.
  --title TITLE                 Use TITLE as label for boot menu entry.
  --iso FILE                    Create bootable CD.
  --floppy FILE                 Create bootable (1440 kB) floppy disk.
  --image FILE                  Create bootable harddisk.
  --run COMMAND                 Run COMMAND after booting DOS.
  --free SIZE                   Add SIZE free space to disk image.
  --version                     Show program version.
  --verbose                     Be more verbose.

SIZE may include a unit (b, k, M, G, T, P, E). Default is b (512 bytes).

  usage

  exit shift;
}


sub tmp_dir
{
  my $self = shift;

  chomp (my $t = `mktemp -d /tmp/fuk.XXXXXXXXXX`);
  die "error: mktemp failed\n" if $?;

  eval 'END { system "rm -rf $t" }';

  my $s_t = $SIG{TERM};
  $SIG{TERM} = sub { system "rm -rf $t"; &$s_t if $s_t };

  my $s_i = $SIG{INT};
  $SIG{INT} = sub { system "rm -rf $t"; &$s_i if $s_i };

  return $t;
}


sub do_iso
{
  my $fuk_dir = $_[0];

  if(!-f "/usr/share/syslinux/isolinux.bin") {
    die "/usr/share/syslinux/isolinux.bin: no such file\nPlease install package 'syslinux'.\n";
  }

  my $mkisofs = "/usr/bin/mkisofs";
  $mkisofs = "/usr/bin/genisoimage" if ! -x $mkisofs;

  if(!-x $mkisofs) {
    die "mkisofs: command not found\nPlease install package 'mkisofs'.\n";
  }

  mkdir "$fuk_dir/cd", 0755;
  link "$fuk_dir/fwupdate.img", "$fuk_dir/cd/fwupdate.img";
  $_ = <<"  isolinux";
default fwupdate

label fwupdate
  kernel memdisk
  append initrd=fwupdate.img $memdisk_option

implicit	1
prompt		0
timeout		0
  isolinux

  open F, ">$fuk_dir/cd/isolinux.cfg";
  print F;
  close F;

  system "cp /usr/share/syslinux/memdisk $fuk_dir/cd";
  system "cp /usr/share/syslinux/isolinux.bin $fuk_dir/cd";
  system $mkisofs . ($opt_verbose ? "" : " --quiet") .
    " -o $opt_iso -f -no-emul-boot -boot-load-size 4 -boot-info-table -b isolinux.bin -hide boot.catalog $fuk_dir/cd";

  return 1;
}


sub do_lilo
{
  my $fuk_dir = $_[0];
  my $lilo_cfg = "/etc/lilo.conf";

  my $config;
  if(open my $f, $lilo_cfg) {
    local $/;
    $config = <$f>;
    close $f;
  }
  else {
    die "$lilo_cfg: $!\n";
  }

  exit 1 if system "cp $fuk_dir/fwupdate.img /boot";
  exit 1 if system "cp /usr/share/syslinux/memdisk /boot";

  my $title = substr($opt_title, 0, 15);
  $title =~ s/(\s|")+/_/g;

  if($config =~ /^\s*label\s*=\s*"?$title"?\s*$/m) {
    print "$opt_title: entry already exists\n";
  }
  else {
    $config .= "\n" if substr($config, -2) ne "\n\n";
    $config .= "image = /boot/memdisk\nlabel = \"$title\"\nappend = \"$memdisk_option\"\ninitrd = /boot/fwupdate.img\n\n";
    die "$lilo_cfg: $!\n" unless rename $lilo_cfg, "$lilo_cfg.fuk_backup";

    if(open my $f, ">$lilo_cfg") {
      print $f $config;
      close $f;
    }
    else {
      die "$lilo_cfg: $!\n";
    }
  }

  print "You may need to run 'lilo' now.\n";

  return 1;
}


sub do_grub1
{
  my $fuk_dir = $_[0];
  my $grub1_cfg = "/boot/grub/menu.lst";

  my $config;
  if(open my $f, $grub1_cfg) {
    local $/;
    $config = <$f>;
    close $f;
  }
  else {
    die "$grub1_cfg: $!\n";
  }

  exit 1 if system "cp $fuk_dir/fwupdate.img /boot";
  exit 1 if system "cp /usr/share/syslinux/memdisk /boot";

  if($config =~ /^\s*title\s+$opt_title\s*$/m) {
    print "$opt_title: entry already exists\n";
  }
  else {
    $config .= "\n" if substr($config, -2) ne "\n\n";
    $config .= "title $opt_title\n    kernel /boot/memdisk $memdisk_option\n    initrd /boot/fwupdate.img\n\n";
    die "$grub1_cfg: $!\n" unless rename $grub1_cfg, "$grub1_cfg.fuk_backup";

    if(open my $f, ">$grub1_cfg") {
      print $f $config;
      close $f;
    }
    else {
      die "$grub1_cfg: $!\n";
    }
  }

  return 1;
}


sub do_grub2
{
  my $fuk_dir = $_[0];
  my $grub2_cfg = "/etc/grub.d/42_firmware_update";

  exit 1 if system "cp $fuk_dir/fwupdate.img /boot";
  exit 1 if system "cp /usr/share/syslinux/memdisk /boot";

  if(-r $grub2_cfg) {
    print "$grub2_cfg: config file already exists\n";

    return 1;
  }
  else {
    (my $config = <<'    = = = = = = = =') =~ s/^ {6}//mg;
      #! /bin/sh

      prefix="/usr"
      exec_prefix="/usr"
      datarootdir="/usr/share"

      . "$pkgdatadir/grub-mkconfig_lib"

      boot_dev=$(prepare_grub_to_access_device ${GRUB_DEVICE_BOOT} | grub_add_tab)

      cat <<EOF
      menuentry 'Firmware Update' {
      $boot_dev
      	linux16 /boot/memdisk harddisk c=8 h=4 s=16
      	initrd16 /boot/fwupdate.img
      }
      EOF
    = = = = = = = =
    if(open my $f, ">$grub2_cfg") {
      print $f $config;
      close $f;
      chmod 0755, $grub2_cfg;
    }
    else {
      die "$grub2_cfg: $!\n";
    }
  }

  print
    "$grub2_cfg created.\n" .
    "Run 'pbl --config' to update your grub2 config.\n";

  return 1;
}
