#! /usr/bin/perl

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# package Tmp version 1.0
#
# Create temporary files/directories and ensures they are removed at
# program end.
#
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{
  package Tmp;

  use File::Temp;
  use strict 'vars';

  sub new
  {
    my $self = {};
    my $save_tmp = shift;

    bless $self;

    my $x = $0;
    $x =~ s#.*/##;
    $x =~ s/(\s+|"|\\|')/_/;
    $x = 'tmp' if$x eq "";

    my $t = File::Temp::tempdir("/tmp/$x.XXXXXXXX", CLEANUP => $save_tmp ? 0 : 1);

    $self->{base} = $t;

    if(!$save_tmp) {
      my $s_t = $SIG{TERM};
      $SIG{TERM} = sub { File::Temp::cleanup; &$s_t if $s_t };

      my $s_i = $SIG{INT};
      $SIG{INT} = sub { File::Temp::cleanup; &$s_i if $s_i };
    }

    return $self
  }

  sub dir
  {
    my $self = shift;
    my $dir = shift;
    my $t;

    if($dir ne "" && !-e("$self->{base}/$dir")) {
      $t = "$self->{base}/$dir";
      die "error: mktemp failed\n" unless mkdir $t, 0755;
    }
    else {
      chomp ($t = `mktemp -d $self->{base}/XXXX`);
      die "error: mktemp failed\n" if $?;
    }

    return $t;
  }

  sub file
  {
    my $self = shift;
    my $file = shift;
    my $t;

    if($file ne "" && !-e("$self->{base}/$file")) {
      $t = "$self->{base}/$file";
      open my $f, ">$t";
      close $f;
    }
    else {
      chomp ($t = `mktemp $self->{base}/XXXX`);
      die "error: mktemp failed\n" if $?;
    }

    return $t;
  }

  # helper function
  sub umount
  {
    my $mp = shift;

    if(open(my $f, "/proc/mounts")) {
      while(<$f>) {
        if((split)[1] eq $mp) {
          # print STDERR "umount $mp\n";
          ::susystem("umount $mp");
          return;
        }
      }
      close $f;
    }
  }

  sub mnt
  {
    my $self = shift;
    my $dir = shift;

    my $t = $self->dir($dir);

    if($t ne '') {
      eval 'END { umount $t }';

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

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

    return $t;
  }
}


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

use Getopt::Long;
use Digest::MD5;
use Digest::SHA;
use File::Find;
use File::Path;
use Cwd 'abs_path';

use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Terse = 1;
$Data::Dumper::Indent = 1;

our $VERSION = "1.34";
our $LIBEXECDIR = "/usr/lib";

my @boot_archs = qw ( x86_64 i386 s390x s390 ia64 aarch64 ppc ppc64 ppc64le );
my $magic_id = "7984fc91-a43f-4e45-bf27-6d3aa08b24cf";

sub usage;
sub check_root;
sub show_progress;
sub susystem;
sub fname;
sub analyze_boot;
sub build_todo;
sub copy_file;
sub prepare_mkisofs;
sub build_filelist;
sub run_mkisofs;
sub read_sector;
sub write_sector;
sub fix_catalog;
sub relocate_catalog;
sub rerun_mkisofs;
sub run_isohybrid;
sub run_isozipl;
sub isols;
sub find_magic;
sub meta_iso;
sub meta_fat;
sub create_initrd;
sub get_kernel_initrd;
sub update_kernel_initrd;
sub get_initrd_format;
sub unpack_orig_initrd;
sub extract_installkeys;
sub create_cd_ikr;
sub isolinux_add_option;
sub grub2_add_option;
sub yaboot_add_option;
sub update_boot_options;
sub prepare_normal;
sub prepare_micro;
sub prepare_nano;
sub prepare_pico;
sub set_mkisofs_metadata;
sub add_to_content_file;
sub update_content;
sub create_sign_key;
sub add_sign_key;
sub sign_content;
sub file_magic;
sub get_archive_type;
sub unpack_cpiox;
sub unpack_archive;
sub format_array;
sub get_initrd_modules;
sub build_module_list;
sub add_modules_to_initrd;
sub replace_kernel_mods;

my %config;
my $sudo;
my $sudo_checked;
my $opt_create;
my $opt_save_temp;
my $opt_dst;
my $opt_joliet = 1;
my $opt_verbose = 0;
my $opt_efi = 1;
my $opt_hybrid = 1;
my $opt_hybrid_fs = 'iso';
my $opt_hybrid_gpt;
my $opt_hybrid_mbr;
my $opt_no_prot_mbr;
my $opt_no_mbr_code;
my $opt_no_mbr_chs;
my $opt_zipl;
my $opt_check;
my $opt_digest = 'sha1';
my @opt_initrds;
my $opt_boot_options;
my $opt_type;
my $opt_vendor;
my $opt_preparer;
my $opt_application;
my $opt_volume;
my $opt_no_docs = 1;
my $opt_loader;
my $opt_sign = 1;
my $opt_sign_key;
my @opt_kernel_rpms;
my @opt_kernel_modules;
my $opt_arch;
my $opt_new_boot_entry;

GetOptions(
  'create|c=s'       => sub { $opt_create = 1; $opt_dst = $_[1] },
  'joliet'           => \$opt_joliet,
  'no-joliet'        => sub { $opt_joliet = 0 },
  'efi'              => \$opt_efi,
  'no-efi'           => sub { $opt_efi = 0 },
  'uefi'             => \$opt_efi,
  'no-uefi'          => sub { $opt_efi = 0 },
  'check'            => \$opt_check,
  'no-check'         => sub { $opt_check = 0 },
  'digest=s'         => \$opt_digest,
  'no-digest'        => sub { $opt_digest = undef },
  'sign'             => \$opt_sign,
  'no-sign'          => sub { $opt_sign = 0 },
  'sign-key=s'       => \$opt_sign_key,
  'gpt'              => sub { $opt_hybrid = 1; $opt_hybrid_gpt = 1 },
  'mbr'              => sub { $opt_hybrid = 1; $opt_hybrid_mbr = 1 },
  'hybrid'           => \$opt_hybrid,
  'no-hybrid'        => sub { $opt_hybrid = 0 },
  'hybrid-fs=s'      => sub { $opt_hybrid = 1; $opt_hybrid_fs = $_[1] },
  'protective-mbr'   => sub { $opt_no_prot_mbr = 0 },
  'no-protective-mbr' => \$opt_no_prot_mbr,
  'mbr-code'         => sub { $opt_no_mbr_code = 0 },
  'no-mbr-code'      => \$opt_no_mbr_code,
  'mbr-chs'          => sub { $opt_no_mbr_chs = 0 },
  'no-mbr-chs'       => \$opt_no_mbr_chs,
  'initrd=s'         => \@opt_initrds,
  'boot=s'           => \$opt_boot_options,
  'grub2'            => sub { $opt_loader = "grub" },
  'isolinux'         => sub { $opt_loader = "isolinux" },
  'zipl'             => \$opt_zipl,
  'no-zipl'          => sub { $opt_zipl = 0 },
  'micro'            => sub { $opt_type = 'micro' },
  'nano'             => sub { $opt_type = 'nano' },
  'pico'             => sub { $opt_type = 'pico' },
  'volume=s'         => \$opt_volume,
  'vendor=s'         => \$opt_vendor,
  'preparer=s'       => \$opt_preparer,
  'application=s'    => \$opt_application,
  'no-docs'          => \$opt_no_docs,
  'keep-docs'        => sub { $opt_no_docs = 0 },
  'kernel=s{1,}'     => \@opt_kernel_rpms,
  'modules=s{1,}'    => \@opt_kernel_modules,
  'arch=s'           => \$opt_arch,
  'add-entry=s'      => \$opt_new_boot_entry,
  'save-temp'        => \$opt_save_temp,
  'verbose|v'        => sub { $opt_verbose++ },
  'version'          => sub { print "$VERSION\n"; exit 0 },
  'help'             => sub { usage 0 },
) || usage 1;

usage 1 unless $opt_create;
usage 1 if $opt_hybrid_fs !~ '^(|iso|fat)$';
usage 1 if defined($opt_digest) && $opt_digest !~ '^(md5|sha1|sha224|sha256|sha384|sha512)$';

if(open my $f, "$ENV{HOME}/.mksusecdrc") {
  while(<$f>) {
    next if /^\s*#/;
    if(/^\s*(\S+?)\s*=\s*(.*?)\s*$/) {
      my $key = $1;
      my $val = $2;
      $val =~ s/^\"|\"$//g;
      $config{$key} = $val;
    }
  }
  close $f;
}

if($config{sudo}) {
  $sudo = $config{sudo};
  $sudo =~ s/\s*$/ /;
}

$opt_sign_key ||= $config{'sign-key'};

my $tmp = Tmp::new($opt_save_temp);

# my $tmp_mnt = $tmp->mnt('mnt');
my $tmp_new = $tmp->dir('new');
my $tmp_err = $tmp->file('err');
my $tmp_sort = $tmp->file('sort');
my $tmp_exclude = $tmp->file('exclude');
my $tmp_fat = $tmp->file('fat');

my @sources;
my $files;
my $boot;
my $todo;
my $iso_cnt = 0;
my $mkisofs = { command => '/usr/bin/mkisofs' };
my $iso_file;
my $iso_fh;
my $two_runs;
my $add_kernel;
my $add_initrd;
my $orig_initrd;
my $initrd_has_parts;
my $has_efi = 0;
my $sign_key_pub;
my $sign_key_dir;
my $initrd_installkeys;
my $initrd_format;
my $rebuild_initrd;
my $hybrid_part_type;
my $kernel;
my $warned;
my $read_write;

my $progress_start = 0;
my $progress_end = 100;
my $progress_txt = 'building:';

$mkisofs->{command} = "/usr/bin/genisoimage" if ! -x $mkisofs->{command};

die "mkisofs: command not found\n" if ! -x $mkisofs->{command};

if($opt_create) {
#  if(@opt_kernel_rpms) {
#    die "Sorry, you must run mksusecd as root to replace kernel modules." if $>;
#  }

  # we might need two mkisofs runs...
  $two_runs = $opt_hybrid && $opt_hybrid_fs;

  $iso_file = $opt_dst;

  for (@ARGV) {
    s#/*$##;
    next if $_ eq "";
    if(-d) {
      push @sources, { dir => $_, real_name => $_, type => 'dir' };
    }
    elsif(-f _) {
      my $t = `file -b -k -L $_ 2>/dev/null`;
      if($t =~ /ISO 9660 CD-ROM/) {
        check_root "Sorry, can't access ISO images; you need root privileges.";
        $iso_cnt++;
        my $d = $tmp->mnt(sprintf("mnt_%04d", $iso_cnt));
        susystem "mount -oro,loop $_ $d";
        push @sources, { dir => $d, real_name => $_, type => 'iso' };
      }
      else {
        die "$_: unsupported source type\n";
      }
    }
    elsif(-e _) {
      die "$_: unsupported source type\n";
    }
    else {
      die "$_: no such file or directory\n";
    }
  }

  if(!@sources) {
    my $msg = "no sources - nothing to do\n";
    $msg .= "Maybe you forgot '--' after --kernel or --modules?\n" if @opt_kernel_rpms || @opt_kernel_modules;
    die $msg;
  }

  $files = build_filelist;
  $boot = analyze_boot;
  get_initrd_format;
  if($opt_sign && (
    # we are going to change '/content' in one way or another
    @opt_initrds || @opt_kernel_rpms || $opt_boot_options || $opt_new_boot_entry || update_content)
  ) {
    extract_installkeys;
    create_sign_key;
    add_sign_key;
  }
  if(@opt_kernel_rpms) {
    replace_kernel_mods;
  }
  $add_initrd = create_initrd;
  update_kernel_initrd;
  update_boot_options;
  sign_content if update_content;
  $todo = build_todo;
  set_mkisofs_metadata;

  prepare_normal;
  prepare_micro if $opt_type eq 'micro';
  prepare_nano if $opt_type eq 'nano';
  prepare_pico if $opt_type eq 'pico';

  prepare_mkisofs;

  # print "sources = ", Dumper(\@sources);
  # print "boot = ", Dumper($boot);
  # print "todo = ", Dumper($todo);
  # print "mkisofs = ", Dumper($mkisofs);

  # print Dumper($mkisofs->{exclude});

  if($two_runs) {
    if($opt_hybrid_fs eq 'iso') {
      $progress_end = 50;
    }
    if($opt_hybrid_fs eq 'fat') {
      $progress_end = 33;
    }
  }

  run_mkisofs;

  if($two_runs) {
    rerun_mkisofs;
  }

  fix_catalog;
  relocate_catalog;

  run_isohybrid if $opt_hybrid;
  run_isozipl if $opt_zipl;

  if(defined $opt_digest) {
    my $chk = $opt_check ? " --check" : "";
    print "calculating $opt_digest...";
    system "tagmedia $chk --digest '$opt_digest' --pad 150 '$iso_file' >/dev/null";
    print "\n";
  }

  if($mkisofs->{partition_start}) {
    system "tagmedia --add-tag  'partition=$mkisofs->{partition_start}' '$iso_file' >/dev/null";
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# usage($exit_code)
#
# Print help text and exit.
#
sub usage
{
  print <<"= = = = = = = =";
Usage: mksusecd [OPTIONS] [SOURCES]
Create SUSE installation CD/DVD.

SOURCES can be directories or ISO image files. All SOURCES are combined
into a single ISO.

General options:

      --version                 Show mksusecd version.
      --verbose                 Show more messages. Can be repeated to log even more.
      --save-temp               Keep temporary files.
      --help                    Write this help text.

Create ISO image:

  -c, --create FILE             Create ISO image from SOURCES.
                                SOURCES are either directories or existing ISO images.
      --joliet                  Use Joliet extensions (default).
      --no-joliet               Don't use Joliet extensions.
      --uefi                    Make ISO UEFI bootable (default).
      --no-uefi                 Don't make ISO UEFI bootable.
      --check                   Tag ISO to be verified before starting the installation.
      --no-check                Don't tag ISO (default).
      --digest DIGEST           Use DIGEST to verify ISO integrity (default: SHA1).
      --no-digest               Don't calculate any digest.
      --sign                    Re-sign '/content' if it has changed. The public part of
                                the sign key is added to the initrd. (default)
      --no-sign                 Don't re-sign '/content'.
      --sign-key KEY_FILE       Use this key instead of generating a transient key.
                                See Signing notes below.
      --gpt                     Add GPT when in isohybrid mode.
      --mbr                     Add MBR when in isohybrid mode (default).
                                Note that when both --mbr and --gpt are specified both
                                MBR and GPT are written - which looks nice but is against
                                the UEFi spec.
      --prot-mbr                When writing a GPT, write a protective MBR (default).
      --no-prot-mbr             When writing a GPT, don't write a protective MBR.
      --mbr-code                Include x86 MBR boot code (default).
      --no-mbr-code             Don't include x86 MBR boot code.
      --mbr-chs                 Fill in sensible CHS values in MBR partition table (default).
      --no-mbr-chs              Use 0xffffff instead of CHS values in MBR partition table.
      --hybrid                  Create an isohybrid image which is both an ISO and a
                                regular disk image (default).
      --no-hybrid               Create a regular ISO image without extra gimmicks.
      --hybrid-fs FS            Use FS for the disk partition created in hybrid mode. FS
                                can be either "" (empty string) producing a partition
                                starting at offset 0 and extending across the entire ISO
                                image (partitioning tools don't really like this) or
                                'iso' or 'fat' in which case you get a regular partition
                                with an ISO960 or FAT file system (default: 'iso').
      --zipl                    Make zIPL bootable (default on s390x).
      --no-zipl                 Don't make zIPL bootable (default except on s390x).
      --initrd DIR|RPM|DUD      Add directory DIR or package RPM or driver update DUD
                                to initrd.
      --no-docs                 Don't include package documentation when updating the
                                initrd (default).
      --keep-docs               Include package documentation when updating initrd.
      --boot OPTIONS            Add OPTIONS to default boot options.
      --add-entry BOOT_ENTRY    Instead of modifying the default boot files, create a new
                                boot entry. This also means that in case initrd or kernel
                                have to be changed, the originals are not overwritten but
                                new files added.
                                BOOT_ENTRY is the name used for this new entry.
      --kernel KERNEL_RPMS      Replace kernel and modules used for booting. KERNEL_RPMS is
                                a list of rpms that contain the new kernel, modules, and
                                firmware files.
                                Note: this option takes a variable number of arguments. So
                                it may be necessary to terminate the arg list with '--'.
      --modules MODULE_LIST     A list of modules to be included additionally to the initrd.
                                Use this in combination with --kernel.
                                You can prefix module names with '-' to have them removed
                                instead. MODULE_LIST may be space or comma separated.
                                Note: this option takes a variable number of arguments. So
                                it may be necessary to terminate the arg list with '--'.
      --grub2                   Use grub2 for El-Torito legacy setup (for debugging).
      --isolinux                Use isolinux for El-Torito legacy setup (for debugging).
      --micro                   Create an ISO with just enough files to test the
                                installation setup. But you can't actually install as
                                all packages have been removed. (Similar to the
                                Network-ISO.)
      --nano                    Create an ISO with just enough files to test the boot
                                process.
      --pico                    Even less than --nano. Keep just the bootloader.
      --volume                  Set ISO volume id.
      --vendor                  Set ISO publisher id.
      --preparer                Set ISO data preparer id.
      --application             Set ISO application id.

Hybrid mode notes:

  Hybrid mode means the image can be used both as an ISO for a DVD or
  directly as a disk image. In other words, there is a partition table
  written on the ISO image, either GPT or MBR.

  If you need UEFI support you will get two paritions: one for the UEFI
  image, one for the entire DVD. If not, you get just one partition covering
  all files.

  There are 2 variants this script supports:

    (1) Partition 1 is the data partition starting at offset 0 and covering
    the entire ISO, partition 2 is the UEFI system partition pointing
    somwhere inside the first partition. This produces an obviously
    inconsistent partition table and partitioning tools really don't like it.

    (2) Partition 1 is a data partition _not_ starting at offset 0 but still
    holding all data files. When you mount it, you see either an ISO9660 or
    a FAT filesystem. If you need UEFI support this partition becomes
    partition 2 and partition 1 points to the UEFI image. Partition 1 and 2
    don't overlap. In this variant a consistent partition table is written.

Signing notes:

    On all media there is a file '/content' holding SHA256 sums of all files
    relevant during installation. The file is signed and is used to ensure
    the integrity of the installation environment.

    If you modify any file mentioned there (e.g. replacing it or implicitly
    as a result of the --initrd or --boot options) '/content' is updated and
    must be re-signed. Otherwise the installer will complain when it starts
    up. For this, mksusecd will re-sign the file and add the public part of
    the signing key to the initrd.

    You can specify the key to use with the 'sign-key' option. The option
    must point to a private key file.

    If there's no 'sign-key' option, a transient key is created. The public
    part is added to the initrd and the key is deleted.

Configuration file:

  \$HOME/.mksusecdrc

    sudo: To access existing ISO image files you will need root privileges.
      (It will be mounted.) This entry lets you specify a command granting
      you root privileges. E.g. sudo="foo".

    sign-key: File name of the private key file with the signing key. The
      same as the 'sign-key' option. See Signing notes above.

Examples:

  # create foo.iso from /foo_dir
  mksusecd --create foo.iso /foo_dir

  # create foo.iso from /foo_dir, no hybrid mode
  mksusecd --create foo.iso --no-hybrid /foo_dir

  # create foo.iso from old.iso and add files to the initrd
  mksusecd --create foo.iso --initrd /dir_with_new_initrd_stuff --initrd foo.rpm old.iso

  # create foo.iso from old.iso and add some boot option
  mksusecd --create foo.iso --boot 'debug=1' old.iso

= = = = = = = =

  exit shift;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub check_root
{
  my $p;
  my $msg = shift;

  return if $sudo_checked;

  $sudo_checked = 1;

  if(!$>) {
    undef $sudo;
    return;
  }

  chomp($p = `bash -c 'type -p $sudo'`) if $sudo;

  $msg = "sorry, you must be root" if $msg eq "";

  die "$msg\n" if $p eq "";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub susystem
{
  system $sudo . $_[0];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub show_progress
{
  my $p = shift;

  return if $progress_end - $progress_start < 1;

  $p = 0 if $p < 0;
  $p = 100 if $p > 100;

  $p = ($progress_end - $progress_start) / 100.0 * $p + $progress_start;

  printf "\r$progress_txt %3d%%", $p;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub fname
{
  if(exists $files->{$_[0]}) {
    return "$files->{$_[0]}/$_[0]";
  }
  else {
    return undef;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub build_filelist
{
  my $files;

  for my $s (@sources) {
    File::Find::find({
      wanted => sub {
        if(m#^$s->{dir}/(.+)#) {
          push @{$mkisofs->{exclude}}, "$files->{$1}/$1" if $files->{$1} && -f "$files->{$1}/$1";
          $files->{$1} = $s->{dir};
        }
      },
      no_chdir => 1
    }, $s->{dir});
  }

  return $files;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub analyze_boot
{
  my $boot;

  for (@boot_archs) {
    if(-d fname("boot/$_")) {
      $boot->{$_} = { base => "boot/$_" };

      $boot->{$_}{initrd} = "boot/$_/loader/initrd" if -f fname("boot/$_/loader/initrd");
      $boot->{$_}{initrd} = "boot/$_/isolinux/initrd" if -f fname("boot/$_/isolinux/initrd");
      $boot->{$_}{initrd} = "boot/$_/initrd" if -f fname("boot/$_/initrd");

      $boot->{$_}{kernel} = "boot/$_/loader/linux" if -f fname("boot/$_/loader/linux");
      $boot->{$_}{kernel} = "boot/$_/isolinux/linux" if -f fname("boot/$_/isolinux/linux");
      $boot->{$_}{kernel} = "boot/$_/vmrdr.ikr" if -f fname("boot/$_/vmrdr.ikr");
      $boot->{$_}{kernel} = "boot/$_/linux" if -f fname("boot/$_/linux");

      if(-f fname("boot/$_/loader/isolinux.bin")) {
        $boot->{$_}{bl}{isolinux} = { base => "boot/$_/loader", file => "isolinux.bin", arch => $_ };
      }
      if(-f fname("boot/$_/isolinux/isolinux.bin")) {
        $boot->{$_}{bl}{isolinux} = { base => "boot/$_/isolinux", file => "isolinux.bin", arch => $_ };
      }
      if(-f fname("boot/$_/cd.ikr")) {
        $boot->{$_}{bl}{ikr} = { base => "boot/$_/cd.ikr", arch => $_ };
      }
      elsif(-f fname("boot/$_/suse.ins")) {
        $boot->{$_}{bl}{ikr} = { base => "boot/$_/cd.ikr", arch => $_, ins => "boot/$_/suse.ins" };
      }
      if(-f fname("boot/$_/grub2-efi/cd.img")) {
        $boot->{$_}{bl}{grub2} = { base => "boot/$_/grub2-efi", file => "cd.img", arch => $_ };
      }
      if(-f fname("boot/$_/grub2/cd.img")) {
        $boot->{$_}{bl}{grub2} = { base => "boot/$_/grub2", file => "cd.img", arch => $_ };
      }
      if(-f fname("boot/$_/grub2-ieee1275/core.elf")) {
        $boot->{$_}{bl}{grub2} = { base => "boot/$_/grub2-ieee1275", file => "core.elf", arch => $_ };
      }
      if(-f fname("boot/$_/efi")) {
        $boot->{$_}{bl}{efi} = { base => "boot/$_/efi", arch => $_ };
      }
      if(-f fname("ppc/bootinfo.txt")) {
        $boot->{$_}{bl}{chrp} = { base => "ppc", arch => $_ };
      }
    }

    if(-f fname("suseboot/linux64")) {
      $boot->{ppc64} = { base => "suseboot", arch => "ppc64", kernel => "suseboot/linux64"};
      $boot->{ppc64}{initrd} = "suseboot/initrd64" if -f fname("suseboot/initrd64");
      if(-f fname("ppc/bootinfo.txt")) {
        $boot->{ppc64}{bl}{yaboot} = { base => "suseboot", file => "suseboot/yaboot.ibm", arch => "ppc64" };
        $boot->{ppc64}{bl}{chrp} = { base => "ppc", arch => "ppc64" };
      }
    }
  }

  # sanitize; kiwi creates stray directories
  for (keys %$boot) {
    delete $boot->{$_} unless $boot->{$_}{kernel} && $boot->{$_}{initrd};
  }

  if(-d fname("EFI/BOOT")) {
    $boot->{efi} = { base => "EFI/BOOT" };
  }

  return $boot;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub build_todo
{
  my $todo;
  my @legacy_eltorito;

  # legacy El-Torito x86 boot
  # In theory more than one entry could be created, but BIOSes don't really
  # expect that...
  for (sort keys %$boot) {
    if($boot->{$_}{bl}{isolinux} && (!$opt_loader || $opt_loader eq "isolinux")) {
      push @legacy_eltorito, { eltorito => $boot->{$_}{bl}{isolinux} };
    }
    if(
      $boot->{$_}{bl}{grub2} &&
      !$boot->{$_}{bl}{chrp} &&
      (!$opt_loader || $opt_loader eq "grub")
    ) {
      push @legacy_eltorito, { eltorito => $boot->{$_}{bl}{grub2} };
    }
  }

  # ... so we just pick one.
  if(@legacy_eltorito) {
    my $x = $legacy_eltorito[0]{eltorito}{base};
    push @$todo, $legacy_eltorito[0];
    if(@legacy_eltorito > 1) {
      print "More than one El Torito legacy boot entry detected, choosing /$x\n";
    }
  }

  # standard UEFI boot
  for (sort keys %$boot) {
    if($boot->{$_}{bl}{efi}) {
      push @$todo, { efi => $boot->{$_}{bl}{efi} };
    }
  }

  # s390 also uses el-torito 
  for (sort keys %$boot) {
    if($_ eq 's390x') {
      $opt_no_mbr_code = 1 if !defined $opt_no_mbr_code;
      $opt_zipl = 1 if !defined $opt_zipl;
      if($opt_zipl) {
        if(!fname("boot/s390x/zipl.map")) {
          # add zipl map file, if necessary
          mkdir "$tmp_new/boot", 0755;
          mkdir "$tmp_new/boot/s390x", 0755;
          if(open my $f, ">$tmp_new/boot/s390x/zipl.map") {
            syswrite $f, ("\x00" x 0x4000);	# 16k should be enough
            close $f;
          }
        }
        print "zIPL bootable (s390x)\n";
      }
    }
    if($boot->{$_}{bl}{ikr}) {
      push @$todo, { ikr => $boot->{$_}{bl}{ikr} };
    }
  }

  # chrp: just ensure we get a proper partition table
  for (sort keys %$boot) {
    if($boot->{$_}{bl}{chrp}) {
      print "CHRP bootable ($_)\n";
      $hybrid_part_type = 0x96;
      $opt_hybrid = 1;
      $opt_hybrid_fs = "";
      $opt_no_mbr_chs = 1 if !defined $opt_no_mbr_chs;
      $opt_no_mbr_code = 1 if !defined $opt_no_mbr_code;
      $two_runs = 0;
      $mkisofs->{options} .= " -U";	# untranslated filenames for ppc firmware
    }
  }

  return $todo;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub copy_file
{
  my $f = fname($_[0]);
  my $n;

  return undef unless defined $f;

  # we may already have a copy...
  if($f eq "$tmp_new/$_[0]") {
    return $f;
  }

  if(-d $f) {
    $n = "$tmp_new/$_[0]";
    system "mdir -p '$n'";
  }
  elsif(-f $f) {
    if($_[0] =~ m#(.+)/([^/]+)#) {
      $n = "$tmp_new/$1/$2";
      system "mkdir -p '$tmp_new/$1'; cp '$f' '$tmp_new/$1'";
    }
    elsif($_[0] !~ m#/#) {
      $n = "$tmp_new/$_[0]";
      system "cp '$f' '$tmp_new'";
    }

    push @{$mkisofs->{exclude}}, $f;
    system "chmod u+w '$tmp_new/$_[0]'";
  }

  # update file location database
  if(defined $n) {
    my $x = $n;
    if($x =~ s#/$_[0]$##) {
      $files->{$_[0]} = $x;
      # print "$_[0] -> $x\n";
    }
  }

  return $n;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub prepare_mkisofs
{
  my $iso_catalog;

  # general options
  $mkisofs->{options} .= " -l -r -pad -input-charset utf8 -o '$iso_file'";
  $mkisofs->{options} .= " -V '" . substr($opt_volume, 0, 32) . "'";
  $mkisofs->{options} .= " -A '" . substr($opt_application, 0, 128) . "'";
  $mkisofs->{options} .= " -p '" . substr($opt_preparer, 0, 128) . "'";
  $mkisofs->{options} .= " -publisher '" . substr($opt_vendor, 0, 128) . "'";
  $mkisofs->{options} .= " -J -f" if $opt_joliet;

  # special loader options
  for (@$todo) {
    my $t = (keys %$_)[0];

    if($t eq 'eltorito') {
      copy_file "$_->{$t}{base}/$_->{$t}{file}";
      push @{$mkisofs->{sort}}, "$tmp_new/$_->{$t}{base}/boot.catalog 4";
      # push @{$mkisofs->{sort}}, fname("$_->{$t}{base}/$_->{$t}{file}") . " 3";
      push @{$mkisofs->{sort}}, "$tmp_new/$_->{$t}{base}/$_->{$t}{file} 3";
      # push @{$mkisofs->{sort}}, "$_->{$t}{base}/$_->{$t}{file} 3";
      push @{$mkisofs->{sort}}, "$tmp_new/$_->{$t}{base} 1";
      $mkisofs->{options} .=
        " -no-emul-boot -boot-load-size 4 -boot-info-table" .
        " -b $_->{$t}{base}/$_->{$t}{file} -c $_->{$t}{base}/boot.catalog" .
        " -hide $_->{$t}{base}/boot.catalog -hide-joliet $_->{$t}{base}/boot.catalog";
      print "El-Torito legacy bootable ($_->{$t}{arch})\n";
      push @$iso_catalog, "Legacy ($_->{$t}{arch})";
    }
    elsif($opt_efi && $t eq 'efi') {
      $has_efi = 1;
      my $f = fname($_->{$t}{base});
      my $s = -s $f;
      $s = (($s + 2047) >> 11) << 2;
      $s = 1 if $s == 0 || $s >> 0xffff;
      push @{$mkisofs->{sort}}, "$f 1000001";
      $mkisofs->{options} .=
        " -eltorito-alt-boot -no-emul-boot -boot-load-size $s -b $_->{$t}{base}";
      print "El-Torito UEFI bootable ($_->{$t}{arch})\n";
      push @$iso_catalog, "UEFI ($_->{$t}{arch})";
      $mkisofs->{fix_catalog} = $iso_catalog;
    }
    elsif($t eq 'ikr') {
      if($_->{$t}{ins}) {
        # need to create base
        create_cd_ikr($_->{$t}{base}, $_->{$t}{ins});
      }
      $mkisofs->{options} .=
        " -eltorito-alt-boot -no-emul-boot -boot-load-size 1 -b $_->{$t}{base}";
      print "El-Torito legacy bootable ($_->{$t}{arch})\n";
      push @$iso_catalog, "Legacy ($_->{$t}{arch})";
      $mkisofs->{fix_catalog} = $iso_catalog;
    }
  }

  if($two_runs) {
    if(open my $fh, ">$tmp_new/glump") {
      print $fh "$magic_id\n";
      close $fh;
    }

    push @{$mkisofs->{sort}}, "$tmp_new/glump 1000000";

    $mkisofs->{options} .= " -hide glump -hide-joliet glump";
  }

  if($mkisofs->{sort}) {
    $mkisofs->{options} .= " -sort '$tmp_sort'";
  }

  if($mkisofs->{exclude}) {
    $mkisofs->{options} .= " -exclude-list '$tmp_exclude'";
  }

  # add our source dirs
  for (@sources) {
    $mkisofs->{options} .= " '$_->{dir}'";
  }
  $mkisofs->{options} .= " '$tmp_new'";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub run_mkisofs
{
  my $log;
  my $ok;
  my $cmd;

  # create sort file
  if($mkisofs->{sort}) {
    if(open my $fh, ">$tmp_sort") {
      print $fh "$_\n" for @{$mkisofs->{sort}};
      close $fh;
    }
  }

  # create exclude file
  if($mkisofs->{exclude}) {
    if(open my $fh, ">$tmp_exclude") {
      print $fh "$_\n" for @{$mkisofs->{exclude}};
      close $fh;
    }
  }

  $cmd = "$mkisofs->{command}$mkisofs->{options}";

  print "running:\n$cmd\n" if $opt_verbose >= 2;

  print "$mkisofs->{command} sort file:\n", join("\n", @{$mkisofs->{sort}}), "\n" if $opt_verbose >= 3;

  print "$mkisofs->{command} exclude file:\n", join("\n", @{$mkisofs->{exclude}}), "\n" if $opt_verbose >= 3;

  # seems to be necessary, else some changes are lost...
  system "sync";

  if(open my $fh, "$cmd 2>&1 |") {
    $| = 1;
    $ok = 1;	# sometimes mkisofs doesn't show any progress, so set ok here...
    while(<$fh>) {
      if(/^\s*(\d*\.\d)\d%/) {
        $ok = 1;
        show_progress $1;
      }
      else {
        $log .= $_;
      }
    }
    show_progress 100 if $ok;
    print "\n" if $progress_end == 100;
    close $fh;
    # printf STDERR "ret = $?\n";
    $ok = 0 if $?;
  }

  print $log if $opt_verbose >= 3 || !$ok;

  die "Error: $mkisofs->{command} failed\n" if !$ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub read_sector
{
  my $buf;

  die "$iso_file: seek error\n" unless seek($iso_fh, $_[0] * 0x800, 0);
  die "$iso_file: read error\n" if sysread($iso_fh, $buf, 0x800) != 0x800;

  return $buf;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub write_sector
{
  die "$iso_file: seek error\n" unless seek($iso_fh, $_[0] * 0x800, 0);
  die "$iso_file: write error\n" if syswrite($iso_fh, $_[1], 0x800) != 0x800;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub fix_catalog
{
  return unless $mkisofs->{fix_catalog};

  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;

  my $vol_descr = read_sector 0x10;
  my $vol_id = substr($vol_descr, 0, 7);
  die "$iso_file: not an iso9660 fs\n" if $vol_id ne "\x01CD001\x01";

  my $eltorito_descr = read_sector 0x11;
  my $eltorito_id = substr($eltorito_descr, 0, 0x1e);
  die "$iso_file: not bootable\n" if $eltorito_id ne "\x00CD001\x01EL TORITO SPECIFICATION";

  my $boot_catalog_idx = unpack "V", substr($eltorito_descr, 0x47, 4);
  die "$iso_file: strange boot catalog location: $boot_catalog_idx\n" if $boot_catalog_idx < 0x12;

  my $boot_catalog = read_sector $boot_catalog_idx;

  my $entries = @{$mkisofs->{fix_catalog}};

  my @entry;

  # collect boot catalog entries
  # depending on the mkisofs variant, the catalog may or may not be correct
  # that is, have section headers (type 0x90, 0x91) or not

  for (my $i = my $j = 0; $i < $entries; $j++) {
    my $ent = substr $boot_catalog, 32 * ($j + 1), 32;
    my $t = (unpack "C", $ent)[0];

    next if $t == 0x90 || $t == 0x91;

    if($t != 0x88) {
      die "$iso_file: boot entry $i: strange content\n";
    }

    push @entry, $ent;
    substr($entry[-1], 12, 20) = pack "Ca19", 1, $mkisofs->{fix_catalog}[$i];

    $i++;
  }

  # rewrite the boot catalog completely

  substr($boot_catalog, 32) = "\x00" x (length($boot_catalog) - 32);

  substr($boot_catalog, 32 * 1, 32) = $entry[0];

  for (my $i = 1; $i < $entries; $i++) {
    my $section_head = pack "CCva28", $i == $entries - 1 ? 0x91 : 0x90, 0xef, 1, "";
    substr($boot_catalog, 32 * (2 * $i), 32) = $section_head;
    substr($boot_catalog, 32 * (2 * $i + 1), 32) = $entry[$i];
  }

  write_sector $boot_catalog_idx, $boot_catalog;

  close $iso_fh;
  undef $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub relocate_catalog
{
  return unless $mkisofs->{fix_catalog};

  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;

  my $vol_descr = read_sector 0x10;
  my $vol_id = substr($vol_descr, 0, 7);
  die "$iso_file: not an iso9660 fs\n" if $vol_id ne "\x01CD001\x01";

  my $path_table = unpack "V", substr($vol_descr, 0x08c, 4);
  die "$iso_file: strange path table location: $path_table\n" if $path_table < 0x11;

  my $new_location = $path_table - 1;

  my $eltorito_descr = read_sector 0x11;
  my $eltorito_id = substr($eltorito_descr, 0, 0x1e);
  die "$iso_file: not bootable\n" if $eltorito_id ne "\x00CD001\x01EL TORITO SPECIFICATION";

  my $boot_catalog_idx = unpack "V", substr($eltorito_descr, 0x47, 4);
  die "$iso_file: strange boot catalog location: $boot_catalog_idx\n" if $boot_catalog_idx < 0x12;

  my $boot_catalog = read_sector $boot_catalog_idx;

  my $vol_descr2 = read_sector $new_location - 1;
  my $vol_id2 = substr($vol_descr2, 0, 7);
  if($vol_id2 ne "\xffCD001\x01") {
    undef $new_location;
    for(my $i = 0x12; $i < 0x40; $i++) {
      $vol_descr2 = read_sector $i;
      $vol_id2 = substr($vol_descr2, 0, 7);
      if($vol_id2 eq "\x00TEA01\x01" || $boot_catalog_idx == $i + 1) {
        $new_location = $i + 1;
        last;
      }
    }
  }

  die "$iso_file: unexpected layout\n" unless defined $new_location;

  # oops, already relocated?
  return if $boot_catalog_idx == $new_location;

  my $version_descr = read_sector $new_location;
  die "$iso_file: unexpected layout\n" if $version_descr ne ("\x00" x 0x800) && substr($version_descr, 0, 4) ne "MKI ";

  # now reloacte to $new_location
  substr($eltorito_descr, 0x47, 4) = pack "V", $new_location;
  write_sector $new_location, $boot_catalog;
  write_sector 0x11, $eltorito_descr;

  printf "boot catalog moved: %d -> %d\n", $boot_catalog_idx, $new_location if $opt_verbose >= 1;

  close $iso_fh;
  undef $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub rerun_mkisofs
{
  my $iso_file_list = isols;
  my $iso_magic = find_magic($iso_file_list);

  die "$iso_file: oops, magic not found\n" unless $iso_magic;

  if($opt_hybrid_fs eq 'iso') {
    meta_iso($iso_magic);
    $progress_start = 50;
  }
  elsif($opt_hybrid_fs eq 'fat') {
    $progress_start = 33;
    $progress_end = 67;
    meta_fat($iso_magic, $iso_file_list);
    $progress_start = 67;
  }

  $progress_end = 100;

  run_mkisofs;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub run_isohybrid
{
  my $opt;
  my $ok;

  $opt .= " --uefi" if $has_efi;
  $opt .= " --gpt" if $opt_hybrid_gpt;
  $opt .= " --mbr" if $opt_hybrid_mbr;
  $opt .= " --no-mbr" if $opt_no_prot_mbr;
  $opt .= " --no-code" if $opt_no_mbr_code;
  $opt .= " --no-chs" if $opt_no_mbr_chs;
  $opt .= sprintf(" --type 0x%x", $hybrid_part_type) if $hybrid_part_type;
  $opt .= " --offset $mkisofs->{partition_start}" if $mkisofs->{partition_start};

  # prefer our own isohybrid variant
  my $cmd = "$LIBEXECDIR/mksusecd/isohybrid";
  $cmd = "isohybrid" unless -x $cmd;

  $cmd .= "$opt $iso_file";

  print "running:\n$cmd\n" if $opt_verbose >= 1;

  $ok = !system("$cmd 2>$tmp_err >&2");

  if(open my $fh, "<", $tmp_err) {
    local $/;
    $_ = <$fh>;
    close $fh;
  }

  print $_ if $opt_verbose >= 2 || !$ok;

  die "Error: isohybrid failed\n" if !$ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub run_isozipl
{
  my $opt;
  my $ok;

  # prefer our own isozipl variant
  my $cmd = "$LIBEXECDIR/mksusecd/isozipl";
  $cmd = "isozipl" unless -x $cmd;

  $cmd .= " $iso_file";

  print "running:\n$cmd\n" if $opt_verbose >= 1;

  $ok = !system("$cmd 2>$tmp_err >&2");

  if(open my $fh, "<", $tmp_err) {
    local $/;
    $_ = <$fh>;
    close $fh;
  }

  print $_ if $opt_verbose >= 2 || !$ok;

  die "Error: isozipl failed\n" if !$ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ISO file list sorted by start address.
#
# Return ref to array with files.
#
sub isols
{
  my $files;

  open my $fd, "isoinfo -R -l -i $iso_file 2>/dev/null |";

  my $dir = "/";

  while(<$fd>) {
    if(/^Directory listing of\s*(\/.*\/)/) {
      $dir = $1;
      next;
    }

    if(/^(.)(.*)\s\[\s*(\d+)(\s+\d+)?\]\s+(.*?)\s*$/) {
      my $type = $1;
      my @x = split ' ', $2;
      $type = ' ' if $type eq '-';
      if($5 ne '.' && $5 ne '..') {
        push @$files, { name => "$dir$5", type => $type, start => $3 + 0, size => $x[4] };
      }
    }
  } 

  close $fd;

  $files = [ sort { $a->{start} <=> $b->{start} } @$files ] if $files;

  # we need some more date for fat fs
  if($opt_hybrid_fs eq 'fat') {
    for (my $i = 0; $i < @$files - 1; $i++) {
      next unless $files->[$i]{type} eq ' ';
      my $p = $files->[$i + 1]{start} - $files->[$i]{start} - (($files->[$i]{size} + 0x7ff) >> 11);
      $files->[$i]{pad} = $p if $p > 0;
      my $is_link = $files->[$i + 1]{start} == $files->[$i]{start};
      $files->[$i + 1]{link} = 1 if $is_link;
      if($p < 0) {
        if($is_link) {
          print STDERR "link found: $files->[$i]{name} = $files->[$i+1]{name}\n";
        }
        else {
          die "$files->[$i]{name}: oops, negative padding: $p\n";
        }
      }
    }
  }

  # printf "%6d\t%s %8d %s\n", $_->{start}, $_->{type}, $_->{size}, $_->{name} for @$files;

  return $files;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# find magic block
sub find_magic
{
  my $cnt;
  my $start;
  my $first;

  my $files = shift;

  die "$iso_file: $!\n" unless open $iso_fh, "<", $iso_file;

  found: for (@$files) {
    next unless $_->{type} eq ' ';
    last if $cnt++ >= 8;			# check just first 8 files
    my $buf;
    for (my $i = 0; $i >= -16; $i--) {		# go back up to 16 blocks
      seek $iso_fh, ($_->{start} + $i) << 11, 0;
      sysread $iso_fh, $buf, length $magic_id;
      $start = $_->{start} + $i, last found if $buf eq $magic_id;
    }
  }

  close $iso_fh;

  for (@$files) {
    next unless $_->{type} eq ' ';
    $first = $_->{start};
    last;
  }

  $first = 0 if $first >= $start;

  print "meta data found: first = $first, start = $start\n" if $opt_verbose >= 1;

  return { extra => $first, block => $start };
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub meta_iso
{
  my $magic = shift;

  # copy meta data

  $mkisofs->{partition_start} = $magic->{block} * 4;

  my $blocks = $magic->{block} + 1;
  my $buf;

  die "$iso_file: $!\n" unless open $iso_fh, "<", $iso_file;
  open my $fh, ">", "$tmp_new/glump" or die "$tmp_new/glump: $?\n";

  for (my $i = 0; $i < $blocks; $i++) {
    die "$iso_file: read error\n" unless sysread($iso_fh, $buf, 2048) == 2048;
    die "$tmp_new/glump: write error\n" unless syswrite($fh, $buf, 2048) == 2048;
  }

  close $fh;
  close $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub meta_fat
{
  my $magic = shift;
  my $iso_files = shift;

  my $fat_size;

  my $tmp = $tmp->file('somefile');

  for (reverse @$iso_files) {
    next unless $_->{type} eq ' ';
    $fat_size =  $_->{start} + (($_->{size} + 0x7ff) >> 11);
    last;
  }

  for (@$iso_files) {
    next unless $_->{type} eq 'd';
    $fat_size++;
  }
   
  $fat_size += ($fat_size >> 8) + 4;

  # add a bit free space (4MB)
  $fat_size += 4 << 9;

  # and round up to full MB
  my $fat_mb = ($fat_size + 511) >> 9;

  $fat_size = $fat_mb << 9;

  printf "fat_size = $fat_size\n" if $opt_verbose >= 2;

  open my $fh, ">", $tmp_fat;
  close $fh;
  truncate $tmp_fat, $fat_mb << 20;
  # try fat32 first
  system "mformat -i '$tmp_fat|cylinders=" . $fat_mb * 4 . " heads=16 sectors=32' -c 4 -F -v 'SUSEDVD' :: 2>/dev/null" and
  system "mformat -i '$tmp_fat|cylinders=" . $fat_mb * 4 . " heads=16 sectors=32' -c 4 -v 'SUSEDVD' ::";

  # 1.: directories
  for (@$iso_files) {
    next unless $_->{type} eq 'd';
    system "mmd -i '$tmp_fat' -D o ::$_->{name}";
  }
   
  # 2.: directory entries
  for (@$iso_files) {
    next unless $_->{type} eq ' ';
    system "mcopy -i '$tmp_fat' -D o $tmp ::$_->{name}";
  }

  # 3.: add files
  my $pad = 0;
  my $pad_cnt = 0;
  my $pr_size = (@$iso_files);
  $pr_size = 1 if !$pr_size;
  my $pr_cnt = 0;
  for (@$iso_files) {
    $pr_cnt++;
    next unless $_->{type} eq ' ';
    truncate $tmp, $_->{size};
    system "mcopy -i '$tmp_fat' -D o $tmp ::$_->{name}";
    if($_->{pad}) {
      $pad += $_->{pad};
      truncate $tmp, $pad << 11;
      truncate $tmp, $_->{pad} << 11;
      $pad_cnt++;
      system "mcopy -i '$tmp_fat' -D o $tmp ::padding$pad_cnt";
    }
    show_progress 100 * $pr_cnt / $pr_size;
  }  
     
  system "mdel -i '$tmp_fat' '::padding*'" if $pad;

  # 4.: read file offsets
  for (@$iso_files) {
    $_->{fat} = 0;   
    $_->{fat} = $1 if `mshowfat -i '$tmp_fat' ::$_->{name}` =~ /<(\d+)/;
  }
   
  # 5.: verify file offsets
  my $dif;
  my $first;
  for (@$iso_files) {
    next unless $_->{type} eq ' ' && $_->{fat};
    $first = $_->{fat};
    $dif = $_->{start} - $_->{fat};
    last;
  }
   
  # for (@$iso_files) {
  #   printf "%6d %6d  [%4d]  (%d)\t%s %8d %s\n", $_->{start}, $_->{fat}, $_->{start} - $_->{fat}, $_->{pad} ? $_->{pad} : 0, $_->{type}, $_->{size}, $_->{name};
  # }

  for (@$iso_files) {
    next unless $_->{type} eq ' ' && $_->{fat};
    if($_->{start} - $_->{fat} != $dif) {
      printf STDERR "%6d %6d\t%s %8d %s\n", $_->{start}, $_->{fat}, $_->{type}, $_->{size}, $_->{name};
      die "$_->{name}: wrong fat offset: $dif\n";
    }
  }

  my $last_block;

  for (reverse @$iso_files) {
    next unless $_->{type} eq ' ' && $_->{fat};
    print "last file: $_->{name} $_->{fat}\n" if $opt_verbose >= 2;
    $last_block =  $_->{fat} + (($_->{size} + 0x7ff) >> 11);
    last;
  }

  print "last block: $last_block\n" if $opt_verbose >= 2;


  my $data_start;
  for (`dosfsck -v '$tmp_fat'`) {
    if(/Data area starts at byte (\d+)/) {
      $data_start = $1 + (($first - 2) << 11);
      $last_block = ($1 >> 9) + (($last_block - 2) << 2);
    }
  }

  printf "last_block = $last_block\n" if $opt_verbose >= 2;

  my $iso_pad = (($fat_size << 2) - $last_block) >> 2;

  die "$tmp_fat: oops, data start not found\n" unless $data_start;

  print "data start = $data_start\n" if $opt_verbose >= 2;

  truncate $tmp_fat, $data_start;

  my $align = ($data_start & 0x7ff) >> 9;
  $align = (4 - $align) & 3;

  if($align) {
    print "alignment needed: $align\n" if $opt_verbose >= 2;
    $iso_pad++;
  }
   
  printf "iso_pad = $iso_pad\n" if $opt_verbose >= 2;

  $mkisofs->{partition_start} = ($magic->{block} << 2) + $align;

  # now copy the fat

  open my $fh, ">", "$tmp_new/glump";

  seek $fh, $align << 9, 0;

  open my $fat_fh, $tmp_fat;

  for (my $i = 0; $i < $data_start >> 20; $i++) {
    my $buf;
    sysread $fat_fh, $buf, 1 << 20;
    syswrite $fh, $buf, 1 << 20;
  }

  if(my $i = $data_start & ((1 << 20) - 1)) {
    my $buf;
    sysread $fat_fh, $buf, $i;
    syswrite $fh, $buf, $i;
  }

  close $fat_fh;

  if($magic->{extra}) {
    my $buf;
    open $iso_fh, $iso_file;
    seek $iso_fh, $magic->{extra} << 11, 0;
    for (my $i = $magic->{extra}; $i < $magic->{block} + 1; $i++) {
      sysread $iso_fh, $buf, 0x800;
      syswrite $fh, $buf, 0x800;
    }
    close $iso_fh;
  }

  close $fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub create_initrd
{
  return undef if !@opt_initrds;

  my $tmp_initrd = $tmp->file();
  my $tmp_dir = $tmp->dir();

  for my $i (@opt_initrds) {
    my $type = get_archive_type $i;

    if($type) {
      unpack_archive $type, $i, $tmp_dir;
    }
    else {
      print STDERR "Warning: ignoring $i\n";
    }
  }

  if($opt_no_docs) {
    system "rm -rf $tmp_dir/usr/share/{doc,info,man}";
    rmdir "$tmp_dir/usr/share";
    rmdir "$tmp_dir/usr";
  }

  # make it possible to directly add linuxrc.rpm - it's a bit special
  if(-f "$tmp_dir/usr/sbin/linuxrc") {
    rename "$tmp_dir/usr/sbin/linuxrc", "$tmp_dir/init";
    print "initrd: linuxrc detected, renamed to /init\n";
  }

  my $compr = $initrd_format eq "xz" ? "xz --check=crc32 -c" : "gzip -9c";

  system "( cd $tmp_dir; find . | cpio --quiet -o -H newc --owner 0:0 | $compr ) >> $tmp_initrd";

  # system "ls -lR $tmp_dir";

  return $tmp_initrd;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#
# Return hash with kernel/initrd pair used for booting.
#
sub get_kernel_initrd
{
  my $x;
  my $cnt;

  for my $b (sort keys %$boot) {
    next if $opt_arch && $opt_arch ne $_;
    if($boot->{$b}{initrd} && $boot->{$b}{kernel}) {
      $x = { initrd => $boot->{$b}{initrd}, kernel => $boot->{$b}{kernel}} if !$x;
      $cnt++;
    }
  }

  if($cnt > 1 && !$warned->{multi_arch}) {
    $warned->{multi_arch} = 1;
    print "Warning: more than one kernel/initrd pair to choose from\n";
    print "(Use '--arch' option to select a different one.)\n";
    print "Using $x->{kernel} & $x->{initrd}.\n";
  }

  if($opt_new_boot_entry && $x->{kernel} =~ m#/s390x/#) {
    die "sorry, --add-entry option does not work for s390x\n";
  }

  # look for potential initrd & kernel names
  if($x && $opt_new_boot_entry) {
    for (my $i = 1; $i < 100; $i++) {
      my $ext = sprintf "_%02d", $i;
      if(!fname("$x->{kernel}$ext") && !fname("$x->{initrd}$ext")) {
        $x->{kernel_ext} = $ext;
        $x->{initrd_ext} = $ext;
        last;
      }
    }
  }

  return $x;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub update_kernel_initrd
{
  my $x = get_kernel_initrd;

  return if !$x;

  if($add_initrd) {
    my $n = copy_file $x->{initrd};

    if(my $ext = $x->{initrd_ext}) {
      my $n = fname $x->{initrd};
      if($n) {
        system "cp '$n' '$n$ext'";
        $files->{"$x->{initrd}$ext"} = $files->{$x->{initrd}};
        $x->{initrd} .= $ext;
        delete $x->{initrd_ext};
      }
    }

    if(my $n = fname $x->{initrd}) {
      system "cat '$add_initrd' >> '$n'";
    }
  }
  else {
    delete $x->{initrd_ext};
  }

  if($add_kernel) {
    copy_file $x->{kernel};

    if(my $ext = $x->{kernel_ext}) {
      my $n = fname $x->{kernel};
      if($n) {
        system "cp '$n' '$n$ext'";
        $files->{"$x->{kernel}$ext"} = $files->{$x->{kernel}};
        $x->{kernel} .= $ext;
        delete $x->{kernel_ext};
      }
    }

    if(my $n = fname $x->{kernel}) {
      system "cp '$add_kernel' '$n'";
    }
  }
  else {
    delete $x->{kernel_ext};
  }

  $kernel->{current} = $x;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub get_initrd_format
{
  my $f;

  return if $initrd_format;

  if(my $x = get_kernel_initrd) {
    my $c = get_archive_type fname($x->{initrd});
    if($c =~ /\.(gz|xz)$/) {
      if($f) {
        die "differing initrd formats: $f & $1\n" if $1 ne $f;
      }
      else {
        $f = $1;
      }
    }
    else {
      die "$x->{initrd}: invalid initrd format\n"
    }
  }

  # print "initrd format: $f\n";

  $initrd_format = $f;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub unpack_orig_initrd
{
  if(my $x = get_kernel_initrd) {
    my $f = fname($x->{initrd});
    if(-f $f) {
      $orig_initrd = $tmp->dir();
      my $type = get_archive_type $f;
      if($type) {
        unpack_archive $type, $f, $orig_initrd;
        if(-d "$orig_initrd/parts") {
          my $last_part;
          $last_part = (glob "$orig_initrd/parts/??_*")[-1];
          if($last_part =~ m#/(\d\d)_[^/]*$#) {
            $initrd_has_parts = $1 + 1;
          }
        }
      }
      else {
        undef $orig_initrd;
      }
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub extract_installkeys
{
  return if !$opt_sign;

  unpack_orig_initrd if !$orig_initrd;

  die "initrd unpacking failed\n" if !$orig_initrd;

  if(-f "$orig_initrd/installkey.gpg") {
    $initrd_installkeys = "$orig_initrd/installkey.gpg";
    print "old style initrd found\n" if $opt_verbose >= 1;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub create_cd_ikr
{
  local $_;

  my $ikr = $_[0];
  my $ins = $_[1];

  my $src = fname($ins);
  $src =~ s#/[^/]*$##;

  my $dst = $ikr;
  $dst =~ s#/[^/]*$##;

  my @layout;

  if(open my $s, fname($ins)) {
    while(<$s>) {
      next if /^\s*\*/;
      push @layout, { file => "$src/$1", ofs => oct($2) } if /^\s*(\S+)\s+(\S+)/;
    }
    close $s;
  }

  die "$ins: nothing to do?\n" if !@layout;

  system("mkdir -p $tmp_new/$dst");

  if(open my $d, ">", "$tmp_new/$ikr") {
    for (@layout) {
      my $fname = $_->{file};
      my $is_parmfile;
      $is_parmfile = 1 if $fname =~ s#(/parmfile)$#$1.cd#;
      if(open my $f, $fname) {
        sysread $f, my $buf, -s($f);
        close $f;
        sysseek $d, $_->{ofs}, 0;
        # remove newlines from parmfile
        $buf =~ s/\n+/ /g if $is_parmfile;
        syswrite $d, $buf;
        # print "$fname: $_->{ofs} ", length($buf), "\n";
      }
      else {
        die "$_->{file}: $!\n";
      }
    }

    sysseek $d, 4, 0;
    syswrite $d, pack("N",0x80010000);

    # align to 4k
    sysseek $d, -s($d) | 0xfff, 0;
    syswrite $d, "\x00";

    close $d;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub isolinux_add_option
{
  my $n = shift;
  my $b = shift;
  my $m = shift;

  if(open my $f, $n) {
    my @f = <$f>;
    close $f;

    # if we should add a new entry, base it loosely on the 'linux' entry
    if($opt_new_boot_entry) {
      my %label;
      my $ext;
      my $comment;
      for (@f) {
        $label{$1} = 1 if /^\s*label\s+(\S+)/i;
      }
      # find first unused label
      for (; $ext < 99; $ext++) {
        last if !$label{"linux$ext"};
      }
      my $ent;
      # and insert a new entry after the 'linux' entry
      for (@f) {
        if($ent && /^\s*$/) {
          $_ .= $ent;
          last;
        }
        elsif(/^\s*label\s+linux/i) {
          $ent = "# install - $opt_new_boot_entry\nlabel linux$ext\n  menu label Installation - $opt_new_boot_entry\n";
          my $k = $kernel->{current}{kernel};
          $k =~ s#.*/##;
          $ent .= "  kernel $k\n";
          my $i = $kernel->{current}{initrd};
          $i =~ s#.*/##;
          $b = " $b" if $b;
          $ent .= "  append initrd=$i splash=silent showopts$b\n\n";
          $comment = "  linux" . sprintf("%-4s", $ext) . " - Installation - $opt_new_boot_entry";
        }
      }

      if($m && $comment) {
        if(open my $f, $m) {
          local $/ = undef;
          my $x = <$f>;
          close $f;
          $x =~ s/(^\s+linux\s+-\s.*)$/$1\n$comment/m;
          if(open my $f, ">", $m) {
            print $f $x;
            close $f;
          }
        }
      }
    }
    else {
      if($b) {
        @f = map { chomp; $_ .= " $b" if /^\s*append\s.*initrd=/; "$_\n" } @f;
      }
    }

    if(open my $f, ">", $n) {
      print $f @f;
      close $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub grub2_add_option
{
  my $n = shift;
  my $b = shift;

  if(open my $f, $n) {
    my @f = <$f>;
    close $f;

    # if we should add a new entry, base it loosely on the 'Installation' entry
    if($opt_new_boot_entry) {
      my $ent;
      # insert a new entry after the 'Installation' entry
      for (@f) {
        if($ent) {
          if(!/^\s*$/) {
            $ent .= $_;
            next;
          }
          $b = " $b" if $b;
          $ent =~ s/'Installation'/'Installation - $opt_new_boot_entry'/;
          my $k = $kernel->{current}{kernel};
          $ent =~ s#(\slinux(efi)?\s+)(\S+)(.*?)\n#$1/$k$4$b\n#;
          my $i = $kernel->{current}{initrd};
          $ent =~ s#(\sinitrd(efi)?\s+)(\S+)#$1/$i#;
          $_ .= $ent;
          last;
        }
        elsif(/^\s*menuentry\s+'Installation'/) {
          $ent .= $_;
        }
      }
    }
    else {
      if($b) {
        @f = map { chomp; $_ .= " $b" if /^\s*linux(efi)?\s/; "$_\n" } @f;
      }
    }

    if(open my $f, ">", $n) {
      print $f @f;
      close $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub yaboot_add_option
{
  my $n = shift;
  my $b = shift;
  my $m = shift;

  if(open my $f, $n) {
    my @f = <$f>;
    close $f;

    # if we should add a new entry, base it loosely on the 'install' entry
    if($opt_new_boot_entry) {
      my %label;
      my $ext;
      my $comment;
      for (@f) {
        $label{$1} = 1 if /^\s*label=(\S+)/i;
      }
      # find first unused label
      for (; $ext < 99; $ext++) {
        last if !$label{"install$ext"};
      }
      my $ent;
      # and append a new entry at the end

      my $k = $kernel->{current}{kernel};
      $k =~ s#.*/##;
      my $i = $kernel->{current}{initrd};
      $i =~ s#.*/##;
      $b = " $b" if $b;
      $ent = "image[64bit]=$k\n  label=install$ext\n  append=\"quiet sysrq=1$b\"\n  initrd=$i\n\n";
      $comment = "  Type  \"install$ext\" to start Installation - $opt_new_boot_entry";

      pop @f if $f[-1] =~ /^\s*$/;
      push @f, $ent;

      if($m && $comment) {
        if(open my $f, $m) {
          local $/ = undef;
          my $x = <$f>;
          close $f;
          $x =~ s/(^\s*Type\s+"install"\s.*)$/$1\n$comment/m;
          if(open my $f, ">", $m) {
            print $f $x;
            close $f;
          }
        }
      }
    }
    else {
      if($b) {
        $_ = "$1$2 $b\"\n" if/^(\s*append\s*=\s*")\s*(.*?)\s*"\s*$/;
      }
    }

    if(open my $f, ">", $n) {
      print $f @f;
      close $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub update_boot_options
{
  return unless defined $opt_boot_options || $opt_new_boot_entry;

  # print Dumper($boot);

  for my $b (sort keys %$boot) {
    if($boot->{$b}{bl}{isolinux}) {
      my $n = copy_file "$boot->{$b}{bl}{isolinux}{base}/isolinux.cfg";
      my $m;
      $m = copy_file "$boot->{$b}{bl}{isolinux}{base}/message" if $opt_new_boot_entry;
      isolinux_add_option $n, $opt_boot_options, $m;
    }
    if($boot->{$b}{bl}{grub2}) {
      my $n = copy_file "$boot->{$b}{bl}{grub2}{base}/grub.cfg";
      grub2_add_option $n, $opt_boot_options;
    }
    if($boot->{$b}{bl}{yaboot}) {
      my $n = copy_file "$boot->{$b}{bl}{yaboot}{base}/yaboot.cnf";
      my $m;
      $m = copy_file "$boot->{$b}{bl}{yaboot}{base}/yaboot.txt" if $opt_new_boot_entry;
      yaboot_add_option $n, $opt_boot_options, $m;
    }
    if($boot->{$b}{bl}{efi}) {
      my $n = copy_file $boot->{$b}{bl}{efi}{base};
      if(defined $n) {
        my $tmp = $tmp->file();
        if(!system "mcopy -n -i $n ::/efi/boot/grub.cfg $tmp") {
          grub2_add_option $tmp, $opt_boot_options;
          if(system "mcopy -D o -i $n $tmp ::/efi/boot/grub.cfg") {
            print STDERR "Warning: failed to update grub.cfg\n";
          }
        }
      }
    }
    if($b eq 'efi') {
      my $n = copy_file "$boot->{$b}{base}/grub.cfg";
      grub2_add_option $n, $opt_boot_options;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# wipe files we really don't want
sub prepare_normal
{
  # cleaning up KIWI isos a bit
  for ( "glump" ) {
    my $f = fname($_);
    push @{$mkisofs->{exclude}}, $f if $f;
  }

  push @{$mkisofs->{exclude}}, "TRANS.TBL";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub prepare_micro
{
  for (
    (map { "suse/$_" } @boot_archs, "i586", "noarch"),
    "docu", "ls-lR.gz", "INDEX.gz", "ARCHIVES.gz", "ChangeLog", "updates", "linux"
  ) {
    my $f = fname($_);
    push @{$mkisofs->{exclude}}, $f if $f;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub prepare_nano
{
  prepare_micro;

  my $ex = join "|", (
    "boot/.*/.*\\.rpm",
    "boot/.*/bind",
    "boot/.*/common",
    "boot/.*/gdb",
    "boot/.*/rescue",
    "boot/.*/root",
    "boot/.*/sax2",
    "boot/.*/branding",
    "boot/.*/openSUSE",
    "boot/.*/SLES",
    "boot/.*/SLED",
    "boot/.*/.*-xen",
    "control.xml",
    "gpg-.*",
    "NEWS",
  );

  for (sort keys %$files) {
    if(m#^suse(/|$)|^$ex$#) {
      my $f = fname($_);
      push @{$mkisofs->{exclude}}, $f if $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub prepare_pico
{
  prepare_nano;

  my $ex = join "|", (
    "boot/.*/linux",
    "boot/.*/initrd",
    "boot/.*/biostest",
    "boot/.*/en.tlk",
  );

  if(!$opt_efi) {
    $ex .= "|" . join "|", (
      "boot/.*/efi",
      "boot/.*/grub2.*",
    )
  }

  $ex = "^$ex\$";

  if(!$opt_efi) {
    $ex .= "|^EFI(/|\$)";
  }

  for (sort keys %$files) {
    if(m#$ex#) {
      my $f = fname($_);
      push @{$mkisofs->{exclude}}, $f if $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub set_mkisofs_metadata
{
  my $media;

  # first, try using old values, if we remaster an image
  if($sources[0]{type} eq 'iso') {
    if(open my $f, "isoinfo -d -i $sources[0]{real_name} 2>/dev/null |") {
      while(<$f>) {
        $opt_volume = $1 if !defined $opt_volume && /^Volume id:\s*(.*?)\s*$/ && $1 ne "" && $1 ne "CDROM";
        $opt_vendor = $1 if !defined $opt_vendor && /^Publisher id:\s*(.*?)\s*$/ && $1 ne "";
        $opt_application = $1 if !defined $opt_application && /^Application id:\s*(.*?)\s*$/ && $1 ne "";
        $opt_preparer = $1 if !defined $opt_preparer && /^Data preparer id:\s*(.*?)\s*$/ && $1 ne "";
      }
      close $f;
      undef $opt_application if $opt_application =~ /^GENISOIMAGE/;
    }
  }

  # else, build new ones based on media.1 dir
  for (sort sort keys %$files) {
    $media = $_, last if /^media\.\d+$/;
  }

  if($media) {
    if(open my $f, "<", fname("$media/build")) {
      my $x = <$f>;
      close $f;
      chomp $x;
      my $m .= $1 if $media =~ /\.(\d+)$/;
      if(!defined $opt_application) {
        $opt_application = $x;
        $opt_application .= "-Media$m" if defined $m;
      }
      if(!defined $opt_volume) {
        $opt_volume = $x;
        $opt_volume =~ s/\-?Build.*$//;
        # try to cut volume id to fit into 32 bytes
        while(length $opt_volume > 25 && $opt_volume =~ s/\-([^\-])*$//) {}
        $opt_volume .= "-Media$m" if defined $m;
      }
    }

    if(open my $f, "<", fname("$media/media")) {
      my $x = <$f>;
      close $f;
      chomp $x;
      $x = "SUSE LINUX GmbH" if $x eq "SUSE" || $x eq "openSUSE";
      $opt_vendor = $x if $x ne "" && !defined $opt_vendor;
    }

    if(open my $f, "<", fname("$media/info.txt")) {
      local $/;
      my $x = <$f>;
      close $f;
      if($x =~ /\n([^\n]+)\n\s*$/) {
        $x = $1;
        $x =~ s/^\s*|\s*$//g;
        $x =~ s/\.//;
        $opt_preparer = $x if $x ne "" && !defined $opt_preparer;
      }
    }
  }

  # if nothing worked, put in some defaults
  $opt_vendor = "mksusecd $VERSION" if !defined $opt_vendor;
  $opt_preparer = "mksusecd $VERSION" if !defined $opt_preparer;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Add a file's check sum to /content.
#
# add_to_content_file($content, $type, $file_name, $pattern)
#
sub add_to_content_file
{
  my $cont = shift;
  my $type = shift;
  my $name = shift;
  my $pattern = shift;

  my $match = $name;
  $name =~ s#.*/## if $type eq "META";

  if($match =~ m#$pattern# && !$cont->{$type}{$name}{new}) {
    my $digest = Digest::SHA->new($cont->{bits});
    my $f = fname($type eq "META" ? "suse/setup/descr/$name" : $name);
    if(-f $f) {
      # print "$name\n";
      $digest->addfile($f);
      my $sum = $digest->hexdigest;
      $cont->{$type}{$name}{new} = "$cont->{bits} $sum";
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Create a new /content file and return 1 if it is different from the
# existing one.
#
sub update_content
{
  my $changed = 0;

  # don't modify content if we're not going to re-sign it
  return $changed if !$opt_sign;

  my $content_file = fname "content";

  return unless defined $content_file;

  my $cont;

  # first, read file
  if(open(my $f, $content_file)) {
    while(<$f>) {
      next if /^\s*$/;
      if(/^((META|HASH|KEY)\s+SHA(\d+)\s+)(\S+)(\s+(\S+))/) {
        my $type = $2;
        my $bits = $3;
        my $sum = "\L$4";
        my $name = $6;
        $cont->{bits} = $bits if !$cont->{bits};
        $cont->{$type}{$name}{old} = "$bits $sum";

        add_to_content_file $cont, $type, $name, '^';
      }
      else {
        $cont->{head} .= $_;
      }
    }
    close $f;
  }

  $cont->{bits} = 256 if !$cont->{bits};

  # then, adjust file list
  for (sort keys %$files) {
    next if m#directory\.yast$#;

    add_to_content_file $cont, "KEY", $_, '^gpg-pubkey-';
    add_to_content_file $cont, "HASH", $_, '^boot/[^/]+/[^/]+$';
    add_to_content_file $cont, "HASH", $_, '^boot/.+/initrd[^/.]*$';
    add_to_content_file $cont, "HASH", $_, '^boot/.+/linux[^/.]*$';
    add_to_content_file $cont, "HASH", $_, '^docu/RELEASE-NOTES[^/]*$';
    add_to_content_file $cont, "META", $_, '^suse/setup/descr/[^/]+$';
  }

  # print Dumper($cont);

  # compare new and old file checksums
  for my $type (qw (META HASH KEY)) {
    for (keys %{$cont->{$type}}) {
      if($cont->{$type}{$_}{new} ne $cont->{$type}{$_}{old}) {
        # print "changed: $_\n";
        $changed = 1;
        last;
      }
    }
    last if $changed;
  }

  # if something changed, write new file
  if($changed) {
    my $n = copy_file "content";

    if($n) {
      if(open my $f, ">", $n) {
        print $f $cont->{head};

        for my $type (qw (META HASH KEY)) {
          for (sort keys %{$cont->{$type}}) {
            next if !$cont->{$type}{$_}{new};
            printf $f "%-4s SHA%s  %s\n", $type, $cont->{$type}{$_}{new}, $_;
          }
        }

        close $f;
      }
    }
  }

  return $changed;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub create_sign_key
{
  my $gpg_dir = $tmp->dir();

my $c = <<"= = = = = = = =";
%no-ask-passphrase
%no-protection
%transient-key
Key-Type: RSA
Key-Length: 2048
Name-Real: mksusecd Signing Key
Name-Comment: transient key
%pubring mksusecd.pub
%secring mksusecd.sec
%commit
= = = = = = = =

  my $key;
  my $is_gpg21;

  if($opt_sign_key) {
    $key = $opt_sign_key;
    $key =~ s/^~/$ENV{HOME}/;
    die "$key: no such key file\n" unless -f $key;
  }
  else {
    if(open my $p, "| cd $gpg_dir ; gpg --homedir=$gpg_dir --batch --armor --debug-quick-random --gen-key - 2>/dev/null") {
      print $p $c;
      close $p;
    }
    $key = "$gpg_dir/mksusecd.sec";
    if(!-f $key) {
      $key = "$gpg_dir/mksusecd.pub";
      $is_gpg21 = 1;
    }
  }

  my $keyid;
  my $date;
  my $priv;
  my $pub;

  if(open my $p, "gpg -v -v $key 2>&1 |") {
    while(<$p>) {
      $priv = 1 if /BEGIN PGP PRIVATE KEY BLOCK/;
      $pub = 1 if /BEGIN PGP PUBLIC KEY BLOCK/;
      $keyid = $1 if !$keyid && /^:signature packet:.*keyid\s+([0-9a-zA-Z]+)/;
      $date = $1, last if !$date && $keyid && /created\s+(\d+)/;
    }
    close $p;
  }

  if(($priv || ($is_gpg21 && $pub)) && $date) {
    $sign_key_dir = $gpg_dir;

    system "gpg --homedir=$gpg_dir --import $key >/dev/null 2>&1";

    my $cname = sprintf "gpg-pubkey-%08x-%08x.asc", hex($keyid) & 0xffffffff, $date;
    $sign_key_pub = "$gpg_dir/$cname";
    system "gpg --homedir=$gpg_dir --export --armor --output $sign_key_pub >/dev/null 2>&1";

    if($opt_sign_key) {
      print "using signing key, keyid = $keyid\n" if $opt_verbose >= 1;
    }
    else {
      print "transient signing key created, keyid = $keyid\n" if $opt_verbose >= 1;
    }
  }
  else {
    if($pub) {
      die "$key: signing key is not a private key\n";
    }
    else {
      die "$key: signing key not usable\n";
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub add_sign_key
{
  return if !$sign_key_pub;

  my $tmp_dir = $tmp->dir();

  if($initrd_installkeys) {
    # old style, gpg key ring
    system "cp $initrd_installkeys $tmp_dir/installkey.gpg";
    system "gpg --homedir=$sign_key_dir --batch --no-default-keyring --ignore-time-conflict --ignore-valid-from --keyring $tmp_dir/installkey.gpg --import $sign_key_pub 2>/dev/null";
    unlink "$tmp_dir/installkey.gpg~";
  }
  else {
    # new style, directory of gpg keys
    system "mkdir -p $tmp_dir/usr/lib/rpm/gnupg/keys";
    system "cp $sign_key_pub $tmp_dir/usr/lib/rpm/gnupg/keys";
  }

  print "signing key added to initrd\n" if $opt_verbose >= 1;

  push @opt_initrds, $tmp_dir;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub sign_content
{
  return if !$sign_key_dir;

  my $c = copy_file "content";
  return if !defined $c;

  my $k = copy_file "content.key";
  return if !defined $k;

  copy_file "content.asc";

  system "cp $sign_key_pub $k";

  print "re-signing '/content'\n" if $opt_verbose >= 1;

  system "gpg --homedir=$sign_key_dir --batch --yes --armor --detach-sign $c";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Run 'file' system command.
#
# result = file_magic(file, pipe)
#
# -   file: the input file, or '-' if pipe is set
# -   pipe: (if set) the command to read from
# - result: everything 'file' returns
#
sub file_magic
{
  my $type = "file -b -k -L $_[0] 2>/dev/null";
  $type = "$_[1] | $type" if $_[1];

  return `$type`;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Get archive type;
#
# type = get_archive_type(file)
#
# - file: the archive name
# - type: something like 'tar.xz' or undef if the archive is unsupported.
#
sub get_archive_type
{
  my $file = $_[0];
  my $type;
  my $cmd;

  my $orig = $file;

  if(-d $file) {
    return 'dir';
  }

  if(! -f $file) {
    return undef;
  }

  do {
    my $t = file_magic $file, $cmd;

    if($t =~ /^RPM/) {
      $type = "cpio.rpm$type";
    }
    elsif($t =~ /^ASCII cpio archive \(SVR4/) {
      $type = "cpiox$type";
    }
    elsif($t =~ /\b(cpio|tar) archive/) {
      $type = "$1$type";
    }
    elsif($t =~ /^(gzip|XZ) compressed data/) {
      my $c = "\L$1";
      if($cmd) {
        $cmd .= " | $c -dc";
      }
      else {
        $cmd = "$c -dc '$file'";
      }
      $file = "-";
      $type = "." . ($c eq 'gzip' ? 'gz' : 'xz') . "$type";
    }
    else {
      die "$orig: unsupported archive format\n";
    }
  } while($type =~ /^\./);

  # print "$file = $type\n";

  return $type;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Unpack cpio archive.
#
# unpack_cpiox(dir, file, part)
#
# -  dir: the directory to unpack to
# - file: the archive file name
# - part: the part number of a multipart archive (kernel initramfs-like)
#         (0 = unpack all)
#
sub unpack_cpiox
{
  my $dst = shift;
  my $file = shift;
  my $part = shift() + 0;

  my ($f, $p, $buf, $cnt, $len, $magic, $head, $fname_len, $data_len, $fname, $ofs);

  my $cpio_cmd = 'cpio --quiet -dmiu --sparse --no-absolute-filenames 2>/dev/null';

  $cnt = 1;

  $read_write = sub
  {
    my $len = $_[0];
    my $read;

    undef $buf;

    open $p, "| ( cd $dst ; $cpio_cmd )" if !$p && ($part == 0 || $part == $cnt);

    return $len if !$len;

    while($len) {
      my $x = sysread $f, substr($buf, length $buf), $len;
      last if !$x;
      $read += $x;
      $len -= $x;
    };

    syswrite $p, $buf, $read if $read && $p;

    return $read;
  };

  if(open $f, $file) {
    while(($len = &$read_write(110)) == 110) {
      $ofs += 110;

      # printf "header = \"%s\" %d\n", $buf, length($buf);

      $magic = substr($buf, 0, 6);
      $head = substr($buf, 6);

      $fname_len = hex substr $buf, 94, 8;
      $data_len = hex substr $buf, 54, 8;

      # print "$magic - $fname_len - $data_len\n";

      die "broken cpio header\n" if $magic ne "070701" && $magic ne "070702";

      $fname_len += (2, 1, 0, 3)[$fname_len & 3];
      $data_len = (($data_len + 3) & ~3);

      &$read_write($fname_len);
      $fname = $buf;
      $fname =~ s/\x00*$//;

      &$read_write($data_len);

      $ofs += $fname_len + $data_len;

      # print "fname = \"$fname\"\n";

      # printf "ofs = 0x%x\n", $ofs;

      if(
        $fname eq 'TRAILER!!!' &&
        $head eq '00000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000'
      ) {
        my $pad = ($ofs + 0x1ff) & ~0x1ff;
        &$read_write($pad - $ofs);
        $ofs = $pad;
        $cnt++;
        close $p if $p;
        undef $p;
      }
    }

    close $p if $p;

    close $f;

    if($len) {
      die "error reading cpio archive $len\n";
    }
  }
  else {
    die "failed to read file\n";
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Unpack archive file.
#
# unpack_archive(type, file, dir, part)
#
# - type: a type sring as returned by get_archive_type
# - file: the archive
# -  dir: the directory to unpack to
# - part: is the part number of a multipart archive (0 = unpack all)
#
sub unpack_archive
{
  my $type = $_[0];
  my $file = $_[1];
  my $dir = $_[2];
  my $part = $_[3];

  return undef if $type eq '';

  my $cmd;
  my $cpiox;

  if($type eq 'dir') {
    $cmd = "tar -C '$file' -cf - .";
    $type = 'tar';
  }

  for (reverse split /\./, $type) {
    if(/^(gz|xz|rpm)$/) {
      my $c;
      if($1 eq 'gz') {
        $c = 'gzip -dc';
      }
      elsif($1 eq 'xz') {
        $c = 'xz -dc';
      }
      else {
        $c = 'rpm2cpio';
      }
      if($cmd) {
        $cmd .= " | $c";
      }
      else {
        $cmd = "$c '$file'";
      }
    }
    elsif($_ eq 'tar') {
      $cmd = "cat '$file'" if !$cmd;
      $cmd .= " | tar -C '$dir' -xpf - 2>/dev/null";
      last;
    }
    elsif($_ eq 'cpio') {
      $cmd = "cat '$file'" if !$cmd;
      $cmd .= " | ( cd '$dir' ; cpio --quiet -dmiu --sparse --no-absolute-filenames 2>/dev/null )";
      last;
    }
    elsif($_ eq 'cpiox') {
      if(!$cmd) {
        $cmd = $file;
      }
      else {
        $cmd .= " |";
      }
      $cpiox = 1;
      last;
    }
  }

  # cpiox = concatenated compressed cpio archives as the kernel uses for initrd
  # must be SVR4 ASCII format, with or without CRC ('cpio -H newc')
  # in this case we have to parse the cpio stream and handle the 'TRAILER!!!' entries
  if($cpiox) {
    # print STDERR "unpack_cpiox($cmd)\n";
    unpack_cpiox $dir, $cmd, $part;
  }
  else {
    # print STDERR "$cmd\n";
    system $cmd;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $string = format_array(\@list, $indentation)
#
# Return joined list values with line breaks added if it gets too long.
#
sub format_array
{
  my $ar = shift;
  my $ind = shift;
  my $x;

  for (@$ar) {
    if(!defined $x) {
      $x = (" " x $ind) . $_;
    }
    else {
      my $xx = $x;
      $xx =~ s/^.*\n//s;
      my $l1 = length($xx) + 3;
      my $l2 = length($_);
      if($l1 + $l2 > 79) {
        $x .= ",\n" . (" " x $ind);
      }
      else {
        $x .= ", ";
      }
      $x .= $_;
    }
  }

  return $x;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub get_initrd_modules
{
  my $unpack_dir = $tmp->dir();

  if(-l "$orig_initrd/modules") {
    $_ = readlink "$orig_initrd/modules";
    if(m#/modules/([^/]+)#) {
      $kernel->{orig_version} = $1;
    }
  }

  die "oops, incompatible initrd layout\n" unless $kernel->{orig_version};

  if(-f "$orig_initrd/parts/00_lib") {
    rmdir $unpack_dir;
    system "unsquashfs -n -d $unpack_dir $orig_initrd/parts/00_lib >/dev/null 2>&1";
  }

  File::Find::find({
    wanted => sub {
      return if -l;	# we don't want links
      if(m#([^/]+)\.ko$#) {
        $kernel->{initrd_modules}{$1} = 1;
      }
      if(m#/module\.config$#) {
        $kernel->{initrd_module_config} = $_;
      }
    },
    no_chdir => 1
  }, "$orig_initrd/modules/", $unpack_dir);

  die "no initrd modules?\n" if !$kernel->{initrd_modules};
  die "no module config?\n" if !$kernel->{initrd_module_config};
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub unpack_kernel_rpms
{
  $kernel->{dir} = $tmp->dir();

  for (@opt_kernel_rpms) {
    my $type = get_archive_type $_;
    die "$_: don't know how to unpack this\n" if !$type;
    unpack_archive $type, $_, $kernel->{dir};
  }

  $kernel->{version} = (glob "$kernel->{dir}/boot/System.map-*")[0];

  if($kernel->{version} =~ m#/boot/System.map-([^/]+)#) {
    $kernel->{version} = $1;
  }
  else {
    die "Couldn't determine kernel version. No kernel package?\n";
  }

  $kernel->{image} = (glob "$kernel->{dir}/boot/vmlinuz-*")[0];
  $kernel->{image} = (glob "$kernel->{dir}/boot/image-*")[0] if !$kernel->{image};
  $kernel->{image} = (glob "$kernel->{dir}/boot/vmlinux-*")[0] if !$kernel->{image};

  die "no module dir?\n" if $kernel->{version} eq "";
  die "no kernel?\n" if !$kernel->{image};

  for (glob "$kernel->{dir}/lib/modules/*") {
    s#.*/##;
    next if $_ eq $kernel->{version};
    print "warning: kmp version mismatch, adjusting: $_ --> $kernel->{version}\n";
    system "tar -C '$kernel->{dir}/lib/modules/$_' -cf - . | tar -C '$kernel->{dir}/lib/modules/$kernel->{version}' -xf -";
  }

  system "depmod -a -b $kernel->{dir} $kernel->{version}";

  # print Dumper($kernel);
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub build_module_list
{
  my %mods_remove;

  for my $m (@opt_kernel_modules) {
    for (split /,/, $m) {
      s/\.ko$//;
      if(s/^-//) {
        $mods_remove{$_} = 1;
      }
      else {
        $kernel->{initrd_modules}{$_} = 2 if !$kernel->{initrd_modules}{$_};
      }
    }
  }

  die "no modules.dep\n" if !open my $f, "$kernel->{dir}/lib/modules/$kernel->{version}/modules.dep";

  # get module paths
  for (<$f>) {
    my @i = split;
    $i[0] =~ s/:$//;
    if($i[0] =~ m#([^/]+)\.ko$#) {
      $kernel->{modules}{$1} = $i[0];
      # resolve module deps
      if($kernel->{initrd_modules}{$1} && @i > 1) {
        shift @i;
        for my $m (@i) {
          if($m =~ m#([^/]+)\.ko$#) {
            $kernel->{initrd_modules}{$1} = 3 if !$kernel->{initrd_modules}{$1};
          }
        }
      }
    }
  }

  close $f;

  $kernel->{new_dir} = $tmp->dir();

  mkdir "$kernel->{new_dir}/lib", 0755;
  mkdir "$kernel->{new_dir}/lib/modules", 0755;
  mkdir "$kernel->{new_dir}/lib/modules/$kernel->{version}", 0755;
  mkdir "$kernel->{new_dir}/lib/modules/$kernel->{version}/initrd", 0755;

  for (sort keys %{$kernel->{initrd_modules}}) {
    if($kernel->{modules}{$_} && !$mods_remove{$_}) {
      system "cp $kernel->{dir}/lib/modules/$kernel->{version}/$kernel->{modules}{$_} $kernel->{new_dir}/lib/modules/$kernel->{version}/initrd";
      push @{$kernel->{added}}, $_ if $kernel->{initrd_modules}{$_} > 1;
    }
    else {
      push @{$kernel->{missing}}, $_;
    }
  }

  system "depmod -a -b $kernel->{new_dir} $kernel->{version}";

  # now get firmware files

  my %fw;

  for my $m (glob("$kernel->{new_dir}/lib/modules/$kernel->{version}/initrd/*.ko")) {
    chomp $m;

    my @l;
    chomp(@l = `modinfo -F firmware $m`);

    $m =~ s#.*/##;
    $m =~ s#.ko$##;

    $fw{$m} = [ @l ] if @l;
  }

  for my $m (sort keys %fw) {
    for (@{$fw{$m}}) {
      my $f;
      $f = "$_" if -f "$kernel->{dir}/lib/firmware/$_";
      $f = "$kernel->{version}/$_" if -f "$kernel->{dir}/lib/firmware/$kernel->{version}/$_";

      if($f) {
        system "install -m 644 -D $kernel->{dir}/lib/firmware/$f $kernel->{new_dir}/lib/firmware/$f";
      }
    }
  }

  # print Dumper(\%fw);

  # adjust module.config file

  if(open my $f, $kernel->{initrd_module_config}) {
    $kernel->{module_config} = [ <$f> ];
    close $f;

    # print "got it\n";
    # FIXME: adjust config

    open my $f, ">$kernel->{new_dir}/lib/modules/$kernel->{version}/initrd/module.config";
    print $f @{$kernel->{module_config}};
    close $f;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub add_modules_to_initrd
{
  my $tmp_dir;

  if($initrd_has_parts) {
    $tmp_dir = $tmp->dir();

    mkdir "$tmp_dir/parts", 0755;

    my $p = sprintf "%02u_lib", $initrd_has_parts++;

    mkdir "$tmp_dir/lib", 0755;
    mkdir "$tmp_dir/lib/modules", 0755;
    mkdir "$tmp_dir/lib/modules/$kernel->{version}", 0755;
    mkdir "$tmp_dir/lib/modules/$kernel->{version}/initrd", 0755;

    for ("loop.ko", "squashfs.ko") {
      rename "$kernel->{new_dir}/lib/modules/$kernel->{version}/initrd/$_", "$tmp_dir/lib/modules/$kernel->{version}/initrd/$_";
    }

    system "mksquashfs $kernel->{new_dir} $tmp_dir/parts/$p -comp xz -noappend -no-progress >/dev/null 2>&1";
  }
  else {
    $tmp_dir = $kernel->{new_dir};
  }

  # add module symlink

  symlink "lib/modules/$kernel->{version}/initrd", "$tmp_dir/modules";

  my $cmd = "Exec:\t\tln -snf lib/modules/`uname -r`/initrd /modules\n";

  if(open my $f, "$orig_initrd/linuxrc.config") {
    my $cmd_found;
    my @lines;
    while(<$f>) {
      push @lines, $_;
      $cmd_found = 1 if $_ eq $cmd;
    }
    close $f;

    if(!$cmd_found) {
      open my $f, ">$tmp_dir/linuxrc.config";
      print $f $cmd;
      print $f @lines;
      close $f;
    }
  }

  push @opt_initrds, $tmp_dir;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub replace_kernel_mods
{
  my @modules;
  my $unpack_dir;

  unpack_orig_initrd if !$orig_initrd;

  die "initrd unpacking failed\n" if !$orig_initrd;

  get_initrd_modules;

  unpack_kernel_rpms;

  build_module_list;

  print "kernel version: $kernel->{orig_version} --> $kernel->{version}\n";

  if($kernel->{added}) {
    print "kernel modules added:\n", format_array $kernel->{added}, 2;
    print "\n";
  }

  if($kernel->{missing}) {
    print "kernel modules missing:\n", format_array $kernel->{missing}, 2;
    print "\n";
  }

  add_modules_to_initrd;

  # now replace kernel

  if(my $x = get_kernel_initrd) {
    $add_kernel = $kernel->{image};
  }
}

