#! /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.52";
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 new_file;
sub copy_or_new_file;
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 run_syslinux;
sub run_createrepo;
sub isols;
sub find_magic;
sub meta_iso;
sub meta_fat;
sub fat_data_start;
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 exclude_files;
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;
sub new_products_xml;
sub prepare_addon;
sub check_mksquashfs_comp;
sub eval_size;
sub add_linuxrc_option;
sub wipe_iso;

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;
my @opt_addon_packages;
my $opt_addon_name;
my $opt_addon_alias;
my $opt_addon_prio = 60;
my $opt_rebuild_initrd;
my $opt_size;
my $opt_net;
my $opt_instsys;
my $opt_defaultrepo;
my $opt_no_iso;


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] },
  'fat'              => sub { $opt_hybrid = 1; $opt_hybrid_fs = 'fat'; $opt_efi = 0; $opt_no_iso = 1 },
  'no-iso'           => \$opt_no_iso,
  'size=s'           => \$opt_size,
  '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,
  'rebuild-initrd'   => \$opt_rebuild_initrd,
  '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' },
  'net=s'            => \$opt_net,
  'instsys=s'        => \$opt_instsys,
  'defaultrepo=s'    => \$opt_defaultrepo,
  '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,
  'addon=s{1,}'      => \@opt_addon_packages,
  'addon-name=s'     => \$opt_addon_name,
  'addon-alias=s'    => \$opt_addon_alias,
  'addon-prio=i'     => \$opt_addon_prio,
  '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)$';

$ENV{PATH} = "$LIBEXECDIR/mksusecd:/usr/bin:/bin:/usr/sbin:/sbin";

if($opt_rebuild_initrd && $>) {
  die "mksusecd must be run with root permissions when --rebuild-initrd is used\n"
}

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 $mksquashfs_has_comp;
my $image_size;
my $syslinux_config;
my $linuxrc_options;
my $has_content;

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};

$mksquashfs_has_comp = check_mksquashfs_comp;

if(defined $opt_size) {
  $image_size = eval_size $opt_size;
  die "$opt_size: invalid size\n" unless $image_size;
}

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;

  die "$iso_file: block device not allowed\n" if -b $iso_file;

  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";
    if(@opt_kernel_rpms || @opt_kernel_modules || @opt_addon_packages) {
      $msg .= "Maybe you forgot '--' after --kernel, --modules, or --addon?\n";
    }
    die $msg;
  }

  build_filelist \@sources;
  $boot = analyze_boot;
  get_initrd_format;

  # assume repomd layout if 'content' file is missing
  $has_content = 1 if fname "content";
  if(!$has_content) {
    print "assuming repo-md sources\n";
    if(!$opt_instsys) {
      my $x = get_kernel_initrd;
      die "oops: no initrd?\n" unless $x;
      if($x->{initrd} =~ m#(boot/[^/]+)/#) {
        $opt_instsys = "disk:/$1/root";
      }
    }

    exclude_files [ "README", "net" ];
  }

  if($opt_instsys) {
    add_linuxrc_option "InstSys", $opt_instsys;
  }

  if($opt_net && !$opt_defaultrepo) {
    $opt_defaultrepo = "cd:/,hd:/,$opt_net";
  }

  if($opt_defaultrepo) {
    add_linuxrc_option "DefaultRepo", $opt_defaultrepo;
  }

  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;

  prepare_addon;

  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;

  if($opt_hybrid) {
    run_isohybrid;
    run_syslinux if $opt_hybrid_fs eq 'fat';
  }
  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";
  }

  wipe_iso if $opt_no_iso;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# usage(exit_code)
#
# Print help text and exit with exit_code.
#
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.
      --no-iso                  Don't make image accessible as ISO9660 file system.
      --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').
      --fat                     Create an image that's suitable to be put on a usb disk.
                                The image holds a single FAT32 partition and it can NOT be
                                used to write a DVD. You can adjust the file system size
                                with the --size option.
                                Technically an alias for '--hybrid-fs=fat --no-efi --no-iso'.
      --size SIZE_SPEC          When using a FAT file system, you can set the intended size of
                                the disk image.
                                SIZE_SPEC can be a number, optionally followed by a unit ('b',
                                'k', 'm', 'g', 't') indicating blocks, kiB, MiB, GiB, or TiB.
                                But SIZE_SPEC can also be a device name like '/dev/sda', in
                                which casee the size of the device is used.
      --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.
      --rebuild-initrd          Rebuild the entire initrd instead of appending changes.
                                This makes the initrd smaller but requires to run mksusecd
                                with root permissions.
      --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 '--'.
      --addon RPM_LIST          A list of RPMs that should be made available as an add-on to
                                the main product.
                                Note: this option takes a variable number of arguments. So
                                it may be necessary to terminate the arg list with '--'.
                                See Add-on notes below.
      --addon-name NAME         Use NAME as the add-on name.
                                If unset, the auto-generated name 'Add-On NUM' is used, with NUM
                                set to the smallest number that avoids name conflicts.
      --addon-alias ALIAS       Set repo alias to ALIAS.
                                If unset, an alias based on the repo name is generated.
      --addon-prio NUM          Set add-on repository priority to NUM; lower NUM means higher
                                priority (default: 60).
      --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.
      --net URL                 Use URL as default network repository url.
                                See Repository notes below.
      --instsys URL             Load the installation system from the specified URL.
                                See Repository notes below.
      --defaultrepo URL_LIST    List of comma (',') separated URLs. The installer will try each URL
                                in turn for an installation repository.
      --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.

Add-on notes:

    The add-on created here is just a repository, not a full add-on product.
    If you need the latter, you will have to create that on your own and add
    it to the iso.

    Although it auto-generates a name for the repository, it's not a very
    creative one and it's probably a good idea to choose one explicitly
    using the --addon-name option.

    The default installation repositories have priority 99. Any smaller
    number for the add-on repository will prefer the add-on packages even
    though the package version number is smaller than in the standard
    repository.

    The default priority of 60 is chosen to be between the priority of the
    default installation repositories (99) and the repositories created by
    driver updates (50).

Repository notes:

  The installer supports two types of repositories:
  (a) the 'classical' variant and
  (b) a repo-md repository.

  (a) has a 'content' file at the repo location and package meta data
  in a sub-directory 'suse/setup/descr', while
  (b) does not require a 'content' file and has package meta data
  in a 'suse/repodata' sub-directory.

  (a) contains the to-be-installed packages together with the installation
  system (the installer itself, in a 'boot/<ARCH>' sub-directory), while
  (b) contains just the to-be-installed packages.

  This means that for (a) the installer can just be loaded from the repository,
  while for (b) the installer must be loaded from somewhere else.
  mksusecd normally assumes that it can be loaded from a local disk or dvd.
  But you can override this using the --instsys option.
  Please look at the linuxrc documentation at https://en.opensuse.org/SDB:Linuxrc
  for details before using this option.

  The installer normally uses an internal list of repository locations that are
  tried in turn. You can change it using the --defaultrepo option. For example,
  --defaultrepo=cd:/,http://foo/bar means to check the local dvd drive first and
  then try via network at http://foo/bar.

  The --net option is just a short hand for --defaultrepo=cd:/,hd:/,<NET_URL>.

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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# check_root(msg)
#
# Checks if we can get root privileges if required.
#
# - msg: message to show to user if things fail
#
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 "";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# susystem(cmd)
#
# Run command with root privileges.
#
# - cmd: command to run
#
sub susystem
{
  system $sudo . $_[0];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# show_progress(percent)
#
# Helper function to update progress indicator.
#
# - percent: percentage to show
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fname(name)
#
# Get full file name.
#
# - name: file name
#
# Returns full file name including path.
#
# We keep track of files and their locations as they can come from different
# sources (directories). For mkisofs it's necessary to ensure file names are
# unique.
#
# The function returns the current instance of the file.
#
sub fname
{
  if(exists $files->{$_[0]}) {
    return "$files->{$_[0]}/$_[0]";
  }
  else {
    return undef;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# build_filelist(sources)
#
# sources is an array_ref containing a list of directories to be scanned and
# added to our internal file list.
#
# The global vars $files and $mkisofs->{exclude} are updated.
#
# The point here is that mkisofs refuses to resolve name conflicts (when
# merging several sources). So we have to do this ourselves and track
# obsolete (that is, when duplicates show up) files in $mkisofs->{exclude}.
#
# That's only needed for regular files; directories don't matter.
#
sub build_filelist
{
  my $src = $_[0];

  for my $s (@$src) {
    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});
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# analyze_boot()
#
# Scan sources and determne boot configuration. The result is put into the
# global $boot var.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# todo = build_todo()
#
# Build list of boot configurations the new image should have and return it.
#
# This list is later used contructing the mkisofs/isohybrid calls.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# new_file(fname)
#
# Create a new empty file with name fname.
#
# Return full path to fname.
#
sub new_file
{
  my $fname = $_[0];
  my $new_path = "$tmp_new/$fname";

  if($fname =~ m#(.+)/([^/]+)#) {
    system "mkdir -p '$tmp_new/$1'";
  }

  if(open my $x, ">$new_path") { close $x }

  # update file location database
  $files->{$fname} = $tmp_new;

  return $new_path;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# copy_or_new_file(fname)
#
# Create a writable copy of fname or a new empty file if fname does not exist.
#
# Return full path to fname.
#
sub copy_or_new_file
{
  return copy_file($_[0]) || new_file($_[0]);
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# copy_file(fname)
#
# Create a writable copy of fname.
#
# Return full path to fname or undef if it does not exist.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_mkisofs()
#
# Gather information needed to build the mkisofs command line.
#
# The result is stored in the global $mkisofs var.
#
# This uses the todo list from build_todo() to setup the boot config.
#
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'";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_mkisofs()
#
# Build actual mkisofs command line and run it.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# buf = read_sector(nr)
#
# Read 2k sector from iso image.
#
# - nr: sector number
#
# Uses global file handle $iso_fh.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# write_sector(nr, buf)
#
# Write 2k sector to iso image.
#
# - nr: sector number
# - buf: data to write
#
# Uses global file handle $iso_fh.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fix_catalog()
#
# Fixes el torito boot catalog.
#
# mkisofs writes a booot catalog that's not exactly standard conform. This
# function fixes it.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# relocate_catalog()
#
# Relocate boot catalog.
#
# Some hardware has problems booting from dvd when the boot catalog is not
# near the beginning of the iso image.
#
# The catalog can actually be nearly in any place in the iso image but
# mkisofs doesn't let you influence it (much).
#
# But mkisofs puts a 'comment' block near the start of the iso image. So, we
# take the somewhat drastic step to relocate the catalog into this 'comment'
# block and have the catalog as much at the top of the image as possible.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# rerun_mkisofs()
#
# Prepare hybrid image and run mkisofs again.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_isohybrid()
#
# Add a partition table to the iso image and, on x86, add boot code to the
# partition table.
#
sub run_isohybrid
{
  my $opt;
  my $ok;
  my $part_type = $hybrid_part_type;

  if($opt_hybrid_fs eq 'fat') {
    $part_type = 0x0c if !$part_type;
  }

  $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", $part_type) if $part_type;
  $opt .= " --offset $mkisofs->{partition_start}" if $mkisofs->{partition_start};
  $opt .= " --size $image_size" if $image_size;

  my $cmd = "isohybrid $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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_syslinux()
#
# Make fat partition bootable using syslinux. This requires the 'real'
# syslinux package and does not work on non-x86 achitectures.
#
sub run_syslinux
{
  return unless $syslinux_config && $mkisofs->{partition_start};

  my $mbr;
  if(open my $f, "/usr/share/syslinux/mbr.bin") {
    local $/;
    $mbr = <$f>;
    close $f;
  }

  if(!-x "/usr/bin/syslinux" || length($mbr) != 440) {
    die "syslinux is needed to build a bootable FAT image, please install package 'syslinux'\n"
  }

  system "syslinux -t " . ($mkisofs->{partition_start} << 9) . " -d '$syslinux_config' -i '$iso_file'";

  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;
  syswrite $iso_fh, $mbr;
  close $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_isozipl()
#
# Make iso image zipl bootable.
#
sub run_isozipl
{
  my $opt;
  my $ok;

  my $cmd = "isozipl '$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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_createrepo(repo_dir)
#
# Run 'createrepo' on repo_dir to create a repo-md repo.
#
sub run_createrepo
{
  my $dir = $_[0];
  my $ok;

  my $cmd = "createrepo --simple-md-filenames '$dir'";

  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: createrepo 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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# magic = find_magic(file_list)
#
# Find magic block.
# - file_list: array ref with file names as produced by isols()
# - magic: hash ref with offset of magic block ('block') and
#   offset of first (with lowest start offset) file ('extra')
#
# Offsets are in 2k units (due to iso fs heritage).
#
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 };
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# metca_iso(magic)
#
# Prepare hybrid image using iso fs for partition.
# - magic: hash ref as returned by find_magic()
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fat_mkfs(name, size, hidden)
#
# Create a fat file system image.
# - name: image name
# - size: size in blocks
# - hidden: hidden blocks (aka planned partition offset)
#
sub fat_mkfs
{
  my ($name, $size, $hidden) = @_;

  open my $fh, ">", $name;
  close $fh;
  truncate $name, $size << 9;
  # try fat32 first
  system "mformat -i '$name' -T $size -H $hidden -s 32 -h 64 -c 4 -F -d 1 -v 'SUSEDISK' :: 2>/dev/null" and
  system "mformat -i '$name' -T $size -H $hidden -s 32 -h 64 -c 4 -d 1 -v 'SUSEDISK' ::";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# meta_fat(magic, file_list)
#
# Prepare hybrid image using fat fs for partition.
# - magic: hash ref as returned by find_magic()
# - file_list: array ref with file names as produced by isols()
#
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;

  # we want $fat_size to count 512 byte blocks, not 2k blocks as in iso fs
  $fat_size *= 4;

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

  # and round up to full MB
  my $fat_size = (($fat_size + 2047) >> 11) << 11;

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

  # disk size - partition offset - max alignment
  my $user_fat_size = $image_size - ($magic->{block} << 2) - 3;

  # use user-specified value, if possible
  $fat_size = $user_fat_size if $user_fat_size > $fat_size;

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

  fat_mkfs $tmp_fat, $fat_size, 0;

  my $fat_data_start = fat_data_start $tmp_fat;

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

  print "fat fs alignment: $align blocks\n" if $opt_verbose >= 2;

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

  # remake, but with correct start offset stored in bpb
  fat_mkfs $tmp_fat, $fat_size, $mkisofs->{partition_start};

  # 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;

  # we're going to use syslinux instead of isolinux, so rename the config file
  if($opt_hybrid_fs eq 'fat') {
    for (@$iso_files) {
      if($_->{name} =~ m#/isolinux.cfg$#) {
        system "mren -i '$tmp_fat' '::$_->{name}' syslinux.cfg";
        $syslinux_config = $_->{name};
        $syslinux_config =~ s#^/##;
        $syslinux_config =~ s#/[^/]+$##;
        last;
      }
    }
  }

  my $data_start = $fat_data_start + (($first - 2) << 11);
  $last_block = ($fat_data_start >> 9) + (($last_block - 2) << 2);

  printf "last_block = $last_block\n" if $opt_verbose >= 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;

  # 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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fat_data_start(fs_image_file)
#
# Returns the offset (in bytes) of the data area of the fat fs in
# fs_image_file or not at all if there are problems detecting it.
#
sub fat_data_start
{
  my $data_start;

  for (`dosfsck -v '$_[0]' 2>/dev/null`) {
    if(/Data area starts at byte (\d+)/) {
      $data_start = $1;
      last;
    }
  }

  die "error: dosfsck failed\n" unless $data_start;

  return $data_start;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create_initrd()
#
# Combine the various initrd parts into the new one.
#
# This will only _append_ the new parts to the original unless
# $opt_rebuild_initrd is set.
#
sub create_initrd
{
  return undef if !@opt_initrds;

  my $tmp_initrd = $tmp->file();
  my $tmp_dir = $opt_rebuild_initrd ? $orig_initrd : $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 = 'cat';
  $compr = "xz --check=crc32 -c" if $initrd_format eq "xz";
  $compr = "gzip -9c" if $initrd_format eq "gz";

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

  # system "ls -lR $tmp_dir";

  return $tmp_initrd;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_kernel_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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_kernel_initrd()
#
# Put new kernel/initrd into the image (at the correct location).
#
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}) {
      if($opt_rebuild_initrd) {
        system "cp '$add_initrd' '$n'";
      }
      else {
        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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_initrd_format()
#
# Analyze original initrd parts and remember compression type.
#
# Raise an error if you are about the combine initrd parts with different
# compression types. While it _would_ technically be ok for the kernel to do
# this, this is really a nightmare on the user level side.
#
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 {
      print STDERR "Warning: $x->{initrd}: uncompressed initrd\n";
      $f = 'cat';
    }
  }

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

  $initrd_format = $f;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# unpack_orig_initrd()
#
# Locate original initrd and unpack it into a temporary directory.
#
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;
      }
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# extract_installkeys()
#
# Get 'installkey.gpg' from the ooriginal initrd.
#
# Older SUSE install initrds has the gpg keys in a file installkey.gpg. To
# be able to add keys we have to extract the file first, add the keys, and
# then write the new file.
#
# Current SUSE initrds don't have this file and use keys in
# /usr/lib/rpm/gnupg/keys directly.
#
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;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create_cd_ikr()
#
# Needed to handle s390x systems.
#
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;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# isolinux_add_option()
#
# Add new boot option to isolinux.cfg.
#
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;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# grub2_add_option()
#
# Add new boot option to grub.cfg.
#
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;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# yaboot_add_option()
#
# Add new boot option to yaboot.txt.
#
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;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_boot_options()
#
# Add new booot option. Modifies files according to used boot loader.
#
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;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_normal()
#
# Wipe files we really don't want to see in our image.
#
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";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Exclude files from iso.
#
# exclude_files(ref_to_file_list)
#
# ref_to_file_list is an array ref with file name patterns (regexp) to exclude
#
sub exclude_files
{
  my $list = $_[0];

  my $ex = join "|", @$list;

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


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_micro()
#
# Remove all files not needed to run the installer itself. Basicallly whis removes
# the rpms from the repository in the image.
#
sub prepare_micro
{
  exclude_files [
    (map { "suse/$_" } @boot_archs, "i586", "noarch"),
    "docu",
    "ls-lR\\.gz",
    "INDEX\\.gz",
    "ARCHIVES\\.gz",
    "ChangeLog",
    "updates",
    "linux",
    "images",
    "autorun.inf",
    ".*\\.ico",
    ".*\\.exe",
  ];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_nano()
#
# Remove all files not needed to run linuxrc. This will also remove the
# installation system (as for the network iso).
#
sub prepare_nano
{
  prepare_micro;

  exclude_files [
    "boot/.*/.*\\.rpm",
    "boot/.*/bind",
    "boot/.*/common",
    "boot/.*/gdb",
    "boot/.*/rescue",
    "boot/.*/root",
    "boot/.*/sax2",
    "boot/.*/libstoragemgmt",
    "boot/.*/branding",
    "boot/.*/openSUSE",
    "boot/.*/SLES",
    "boot/.*/SLED",
    "boot/.*/.*-xen",
    "control\\.xml",
    "gpg-.*",
    "NEWS",
    "license\\.tar\\.gz",
    "(|.*/)directory\\.yast",
    "suse",
  ];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_pico()
#
# Remove all files not needed to run the boot loader. This makes only sense
# for testing.
#
sub prepare_pico
{
  prepare_nano;

  exclude_files [
    "boot/.*/linux",
    "boot/.*/initrd",
    "boot/.*/biostest",
    "boot/.*/en\\.tlk",
  ];

  if(!$opt_efi) {
    exclude_files [
      "boot/.*/efi",
      "boot/.*/grub2.*",
      "EFI",
    ]
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# set_mkisofs_metadata()
#
# Construct iso metadata from existing iso and product files found in the
# source image.
#
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 it doesn't exist or we're not going to re-sign it
  return $changed if !$has_content || !$opt_sign;

  my $content_file = fname "content";

  my $cont;

  # first, read file
  # ($content_file may be undefined - which is ok)
  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", $_, '^license.tar.gz$';
    add_to_content_file $cont, "HASH", $_, '^control.xml$';
    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/[^/]+$';
    add_to_content_file $cont, "HASH", $_, '^images/[^/]+\.(xz|xml)$';
  }

  # 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_or_new_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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create_sign_key()
#
# Create a temporary gpg keyring and either add the provided gpg key or
# create a temporary key.
#
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";
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_sign_key()
#
# Add public part of mksusecd sign key to image so it's used by the installer.
#
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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# sign_content()
#
# Re-sign 'content' file with our own key if we modified it.
#
sub sign_content
{
  return if !$sign_key_dir;

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

  my $k = copy_or_new_file "content.key";

  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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_initrd_modules()
#
# Get list of modules that are in the initrd.
#
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;
    if(system "unsquashfs -n -d $unpack_dir $orig_initrd/parts/00_lib >/dev/null 2>&1") {
      die "parts/00_lib: failed to unpack squashfs image - squashfs tools too old?\n";
    }
  }

  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};
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# unpack_kernel_rpms()
#
# Unpack al provided kernel packaged in a temporary location.
#
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);
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# build_module_list()
#
# Build list of modules to include in the new initrd.
#
# This is based on the list of modules in the original initrd minus modules
# no longer exist plus modules needed to fulfill all module dependencies.
#
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/:$//;
    # older modutils put the full path into modules.dep
    # so remove the "/lib/modules/VERSION/" part if it exists
    @i = map { s#^/lib/modules/([^/]+)/##; $_ } @i;
    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}}, $_;
    }
  }

  # copy modules.order & modules.builtin

  if(-f "$kernel->{dir}/lib/modules/$kernel->{version}/modules.builtin") {
    system "cp $kernel->{dir}/lib/modules/$kernel->{version}/modules.builtin $kernel->{new_dir}/lib/modules/$kernel->{version}/";
  }

  if(open my $f, "$kernel->{dir}/lib/modules/$kernel->{version}/modules.order") {
    if(open my $w, ">$kernel->{new_dir}/lib/modules/$kernel->{version}/modules.order") {
      while(<$f>) {
        chomp;
        s#.*/#initrd/#;
        print $w "$_\n" if -f "$kernel->{new_dir}/lib/modules/$kernel->{version}/$_";
      }
      close $w;
    }
    close $f;
  }

  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;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_modules_to_initrd()
#
# Add new modules to initrd.
#
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++;
    # XX_lib contains kernel modules - replace the original one if we are
    # going to rebuild the initrd anyway
    $p = "00_lib" if $opt_rebuild_initrd;

    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", "lz4_decompress.ko") {
      rename "$kernel->{new_dir}/lib/modules/$kernel->{version}/initrd/$_", "$tmp_dir/lib/modules/$kernel->{version}/initrd/$_";
    }

    my $err = system "mksquashfs $kernel->{new_dir} $tmp_dir/parts/$p" .
      ($mksquashfs_has_comp ? " -comp xz" : "") .
      " -noappend -no-progress >/dev/null 2>&1";
    die "mksquashfs failed\n" if $err;
  }
  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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# replace_kernel_mods()
#
# Replace kernel modules.
#
# Includes getting the list of modules in the original initrd, unpacking the
# kernel packages, and including the new modules to the new initrd.
#
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};
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# new_products_xml(old_xml, dir, name, alias, prio)
#
# Add a product to an existing add_on_products.xml or create a new one.
#
# This doesn't use a full xml parser but assumes a reasonably formatted
# add_on_products.xml.
#
sub new_products_xml
{
  my ($old_xml, $dir, $name, $alias, $prio) = @_;
  my $new_xml;
  my @x;

  @x = split /^/m, $old_xml || <<'# template';
<?xml version="1.0"?>
<add_on_products xmlns="http://www.suse.com/1.0/yast2ns"
    xmlns:config="http://www.suse.com/1.0/configns">
    <product_items config:type="list">
    </product_items>
</add_on_products>
# template

  my $product = <<"# product";
        <product_item>
            <name>$name</name>
            <url>relurl://$dir?alias=$alias</url>
            <priority config:type="integer">$prio</priority>
            <ask_user config:type="boolean">false</ask_user>
            <selected config:type="boolean">true</selected>
            <check_name config:type="boolean">false</check_name>
        </product_item>
# product

  # inject the new product at the end of the list
  for (@x) {
    if(m#\s*</product_items>#) {
      $_ = $product . $_;
    }
  }

  $new_xml = join '', @x;

  return $new_xml;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_addon()
#
# If there are RPMs for an add-on specified in @opt_addon_packages, create
# an add-on on the media.
#
# The add-on is placed into /addons/<repo_alias>/ and a file /add_on_products.xml
# is created/updated on the iso.
#
# Details can be influenced via $opt_addon_name, $opt_addon_alias, $opt_addon_prio.
#
sub prepare_addon
{
  return if !@opt_addon_packages;

  my $addon_name = $opt_addon_name;

  if($addon_name eq "") {
    # ok, be creative...

    my $idx = 1;
    $idx++ while fname "addons/Add-On_$idx";

    $addon_name = "Add-On $idx";
  }

  my $addon_alias = $opt_addon_alias;

  # strip chars we don't like to create an alias from addon name
  if($addon_alias eq "") {
    $addon_alias = $addon_name;
    $addon_alias =~ s/\s+/_/g;
    $addon_alias =~ tr/a-zA-Z0-9._\-//cd;
  }

  die "error: '$addon_name' is not a suitable add-on name, please choose a different one\n" if $addon_alias eq "";
  die "error: 'addons/$addon_alias' already exists\n" if fname "addons/$addon_alias";

  print "creating add-on \"$addon_name\" (alias $addon_alias):\n";

  my $tmp_dir = $tmp->dir();
  my $repo_dir = "$tmp_dir/addons/$addon_alias";
  mkdir "$tmp_dir/addons", 0755;
  mkdir $repo_dir, 0755;

  for (@opt_addon_packages) {
    die "$_: not a RPM\n" unless -f && file_magic($_) =~ /^RPM/;
    system "cp", $_, $repo_dir;
    print "  - $_\n";
  }

  # create repo-md files
  run_createrepo $repo_dir;

  # create/update add_on_products.xml
  my $products_xml;

  my $f = fname "add_on_products.xml";
  if($f && open my $fh, "<", $f) {
    local $/;
    $products_xml = <$fh>;
    close $fh;
  }

  $products_xml = new_products_xml($products_xml, "addons/$addon_alias", $addon_name, $addon_alias, $opt_addon_prio);

  if(open my $fh, ">", "$tmp_dir/add_on_products.xml") {
    print $fh $products_xml;
    close $fh;
  }

  # add our add-on to the iso
  my $new_source = { dir => $tmp_dir, real_name => $tmp_dir, type => 'dir' };
  push @sources, $new_source;
  build_filelist [ $new_source ];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# check_mksquashfs_comp()
#
# Return 1 if mksquahsfs supports '-comp' option, else 0.
#
sub check_mksquashfs_comp
{
  my $comp_ok = 0;

  if(open my $f, "mksquashfs -help 2>&1 |") {
    while(<$f>) {
      $comp_ok = 1, last if /^\s*-comp\s/;
    }
    close $f;
  }

  print "mksquashfs has '-comp': $comp_ok\n" if $opt_verbose >= 2;

  return $comp_ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# eval_size(size_string)
#
# Interpret size_string and return size in (512 byte)-blocks.
#
# size_string is either a numerical size like '64G' or a file or block
# device name. In this case the size of the file or block device is used.
#
sub eval_size
{
  my $size = $_[0];
  my $unit = { b => 9 - 9, k => 10 - 9, m => 20 - 9, g => 30 - 9, t => 40 - 9 };

  return undef unless $size;

  if($size =~ /^(\d+)\s*([bkmgt]?)/i) {
    $size <<= $unit->{"\L$2"} if $2;
  }
  elsif($size =~ m#/dev/#) {
    my $s;
    my $x = `readlink -f $size 2>/dev/null`;
    if($x =~ m#/dev/([^/]+?)\s*$#) {
      my $dev = $1;
      for (</sys/block/$dev/size /sys/block/*/$dev/size>) {
        if(open(my $f, $_)) {
          $s = <$f> + 0;
          close $f;
          last;
        }
      }
    }
    $size = $s;
  }
  elsif(-s $size) {
    $size = (-s _) >> 9;
  }
  else {
    $size = undef;
  }

  printf "target image size: %.2f GiB ($size blocks)\n", $size / (1 << 21);

  return $size;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_linuxrc_option(key, value)
#
# Add linxurc config option.
# - key: option name
# - value: option value
#
# Options are stored in /etc/linuxrc.d/60_mksusecd in the initrd.
#
sub add_linuxrc_option
{
  my ($key, $value) = @_;

  my $linuxrc_cfg = "etc/linuxrc.d/60_mksusecd";

  unpack_orig_initrd if !$orig_initrd;

  if(!$linuxrc_options) {
    $linuxrc_options = $tmp->dir();
    push @opt_initrds, $linuxrc_options;

    mkdir "$linuxrc_options/etc", 0755;
    mkdir "$linuxrc_options/etc/linuxrc.d", 0755;

    if($orig_initrd && -f "$orig_initrd/$linuxrc_cfg") {
      system "cp $orig_initrd/$linuxrc_cfg $linuxrc_options/$linuxrc_cfg";
    }
  }

  if(open my $f, ">>$linuxrc_options/$linuxrc_cfg") {
    print $f "$key:\t\"$value\"\n";
    close $f;
  }

  print "added linuxrc option $key=\"$value\"\n" if $opt_verbose >= 1;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# wipe_iso()
#
# Wipe iso9660 file system header.
#
sub wipe_iso
{
  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;

  write_sector 0x10, ("\x00" x 0x800);
  write_sector 0x11, ("\x00" x 0x800);

  close $iso_fh;
  undef $iso_fh;
}

