#! /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.78";
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";

# valid kernel module extensions
my $kext_regexp = '\.ko(?:\.xz)?';
my $kext_glob = '.ko{,.xz}';
my @kext_list = qw ( .ko .ko.xz );

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_or_checksums;
sub update_content;
sub update_checksums;
sub create_sign_key;
sub add_sign_key;
sub sign_content_or_checksums;
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;
sub analyze_products;
sub check_product;
sub crypto_cleanup;
sub run_crypto_disk;

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 = 'sha256';
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_sign_key_id;
my $opt_sign_pass_file;
my $opt_sign_image;
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;
my $opt_merge_repos = 1;
my $opt_list_repos;
my $opt_include_repos;
my $opt_enable_repos;
my $opt_crypto;
my $opt_crypto_fs = 'ext4';
my $opt_crypto_password;
my $opt_crypto_title;
my $opt_crypto_top_dir;
my $opt_instsys_in_repo = 1;
my $opt_create_repo;


GetOptions(
  'create|c=s'       => sub { $opt_create = 1; $opt_dst = $_[1] },
  'create_repo'      => sub { $opt_create_repo = 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-image'       => \$opt_sign_image,
  'no-sign-image'    => sub { $opt_sign_image = 0 },
  'sign-key=s'       => \$opt_sign_key,
  'sign-key-id=s'    => \$opt_sign_key_id,
  'sign-pass-file=s' => \$opt_sign_pass_file,
  '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 },
  'crypto'           => sub { $opt_crypto = 1; $opt_hybrid = 0; },
  'password=s'       => \$opt_crypto_password,
  'title=s'          => \$opt_crypto_title,
  'top-dir=s'        => \$opt_crypto_top_dir,
  'filesystem=s'     => \$opt_crypto_fs,
  '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,
  'instsys-in-repo!' => \$opt_instsys_in_repo,
  '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,
  'no-merge-repos'   => sub { $opt_merge_repos = 0 },
  'merge-repos'      => \$opt_merge_repos,
  'list-repos'       => \$opt_list_repos,
  'include-repos=s'  => \$opt_include_repos,
  'enable-repos=s'   => \$opt_enable_repos,
  '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 || $opt_list_repos;
usage 1 if $opt_hybrid_fs !~ '^(|iso|fat)$';
usage 1 if defined($opt_digest) && $opt_digest !~ '^(md5|sha1|sha224|sha256|sha384|sha512)$';
usage 1 if defined($opt_enable_repos) && $opt_enable_repos !~ /^(0|1|no|yes|auto|ask)$/i;

die "no password\n" if $opt_crypto && $opt_crypto_password eq "";

$ENV{PATH} = "/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'};
$opt_sign_key_id ||= $config{'sign-key-id'};

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_filelist = $tmp->file('filelist');
my $tmp_fat = $tmp->file('fat');

my @sources;
my $files;
my $files_to_keep;
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 $has_el_torito = 0;
my $sign_key_pub;
my $sign_key_dir;
my $sign_key_id;
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 $product_db;
my $repomd_instsys_location;
my $sign_passwd_option;

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_sign_pass_file) {
  if ($opt_sign_key || $opt_sign_key_id) {
    if (-e $opt_sign_pass_file) {
      $sign_passwd_option = "--pinentry-mode loopback --passphrase-file $opt_sign_pass_file"
    } else {
      die "Passphrasefile $opt_sign_pass_file does not exist\n";
    }
  } else {
    print "--sign-pass-file ignored because of missing --sign-key or --sign-key-id\n"
  }
}

if($opt_create || $opt_list_repos) {
#  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) || $opt_crypto;

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

  analyze_products \@sources;
  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 && !$opt_instsys_in_repo) {
      my $x = get_kernel_initrd;
      die "oops: no initrd?\n" unless $x;
      if($x->{initrd} =~ m#(boot/[^/]+)/#) {
        $repomd_instsys_location = "$1/root";
        # Note
        #   When encryption is in use we must not set the instsys location
        #   here. This would cause linuxrc to miss the instsys as the URL
        #   below does never point inside an encrypted volume.
        #   Instead, run_crypto_disk() handles this when writing 90_crypto.
        $opt_instsys = "disk:/$repomd_instsys_location" unless $opt_crypto;
      }
    }

    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' resp. '/CHECKSUMS' in one way or another
      @opt_initrds || @opt_kernel_rpms || $opt_boot_options ||
      $opt_new_boot_entry || $opt_include_repos || update_content_or_checksums
    )
  ) {
    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;

  if($opt_create_repo) {
    run_createrepo $sources[0]{dir};
  }

  prepare_addon;

  sign_content_or_checksums if update_content_or_checksums;
  $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) {
    if($opt_crypto) {
      $progress_start = 50;
      $progress_end = 100;
      run_crypto_disk;
      exit
    }

    rerun_mkisofs;
  }

  fix_catalog;
  relocate_catalog;

  if($opt_hybrid) {
    run_isohybrid;
    run_syslinux if $opt_hybrid_fs eq 'fat';
  }
  run_isozipl if $opt_zipl;

  wipe_iso if $opt_no_iso;

  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($opt_sign && $sign_key_dir && $opt_sign_image) {
      my $tmp_dir = $tmp->dir();
      system "tagmedia --export-tags $tmp_dir/tags $iso_file >/dev/null 2>&1";
      if(-s "$tmp_dir/tags") {
        print "signing $iso_file\n" if $opt_verbose >= 1;
        system "gpg --homedir=$sign_key_dir --local-user '$sign_key_id' --batch --yes --armor --detach-sign $sign_passwd_option $tmp_dir/tags";
        system "tagmedia --import-signature $tmp_dir/tags.asc $iso_file";
      }
    }
  }
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# 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.
      --create_repo             (Re)Create and sign the repository.
      --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-image              Embed signature for whole image. See Signing notes.
      --no-sign-image           Don't embed signature for whole image. (default)
      --sign-key KEY_FILE       Use this key file instead of generating a transient key.
                                See Signing notes below.
      --sign-key-id KEY_ID      Use this key id instead of generating a transient key.
                                Note: gpg might show an interactive dialog asking for a
                                password to unlock the key unless you use the 'sign-pass-file'
                                option.
                                See Signing notes below.
      --sign-pass-file          Use the password stored in this file to open the 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 or the --crypto option 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.
      --crypto                  If set, an encrypted disk image is created.
                                See Crypto notes below for details.
      --password PASSWORD       Use PASSWORD for encrypting the disk image.
      --title TITLE             The password query screen uses TITLE as title (default: openSUSE).
      --top-dir DIR             The installation files are placed into subdir DIR in the created
                                image. This helps keeping the directory structure nice and clean
                                in case you are using the image also for other things. The boot
                                config is adjusted accordingly.
      --filesystem FS           Use FILESYSTEM for the encrypted image (default: ext4). Don't be
                                too creative here - the filesystem must be supported by grub2.
      --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).
      --merge-repos             When mksusecd detects repositories in SOURCES it will try to make
                                them all available and create a common media.1/products file (default).
                                See Product module notes below.
      --no-merge-repos          Skip the special treatment of repositories and just merge all SOURCES.
      --include-repos LIST      Comma-separated list of repository names to include in the final image.
                                Use --list-repos to see valid values. (Default: include all repos.)
      --enable-repos WHEN       If WHEN is set to 'auto' or 'yes' the included repositories are
                                automatically added. If set to 'ask' the user may interactively deselect
                                repositories. The default is not to add any repository. Instead, the user
                                is expected to add the medium as 'add-on' during the installation.
      --list-repos              Just list all repositories and exit.
      --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.
      --instsys-in-repo         Load installation system from repository (default). The option --instsys
                                overrides this setting.
                                See Repository notes below.
      --no-instsys-in-repo      Do not load installation system from repository but search for it on
                                local disks. The option --instsys overrides this setting.
                                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 either the 'sign-key' or 'sign-key-id'
  option. 'sign-key' must point to a private key file, 'sign-key-id' is a
  key id recognized by gpg.

  If both '--sign-key' and '--sign-key-id' are specified, '--sign-key-id' wins.

  You can specify a file which contains the passphrase to the key specified with
  '--sign-key' or '--sign-key-id' to avoid an interactive dialog to enter
  the passphrase.

  If there's neither a 'sign-key' nor a 'sign-key-id' option, a transient
  key is created. The public part is added to the initrd and the root
  directory of the image and the key is deleted.

  The key file is named 'gpg-pubkey-xxxxxxxx-xxxxxxxx.asc'.

  mksusecd can also embed a signature of the checksum metadata into the image.
  This can be used by the checkmedia tool to verify the integrity of the
  image.

  As older versions (checkmedia < version 4.2) cannot handle this, it is not
  the default and you have to explicitly request it with '--sign-image'.

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 with product meta data and file checksums at the
  repo location and package meta data in a sub-directory 'suse/setup/descr'.

  (b) Uses '.treeinfo' for product meta data, 'CHECKSUMS' for file checksums,
  and has package meta data in a 'repodata' sub-directory.

  A repository usually also contains the installation system. If so, the
  image files are placed in a 'boot/<ARCH>' sub-directory and the installer
  can simply be loaded from the repository.

  But if it is just a plain repository without the installation system the
  installer has to be loaded from somewhere else.

  Use the --no-instsys-in-repo option to tell mksusecd that it can be loaded
  from a local disk or dvd. It will be searched for on any mountable local
  device at startup.

  You can override this using the --instsys option to load the
  installation system from any location. 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>.

Product module notes:

  In SLE 15 the product is split into several repositories called 'modules'
  (don't confuse this with kernel modules). These modules are distributed
  over several media or in separate directories on a network installation
  server.

  mksusecd lets you combine the installation medium together with the
  modules you need into a single medium.

  Check the available modules with --list-repos and then pick the modules
  you need with --include-repos.

Crypto notes:

  The --crypto option allows you to create an encrypted installation disk.
  Note that this image is explicitly *not* bootable as cd/dvd (no hybrid
  image). It is both legacy BIOS and UEFI bootable, though.

  Everything except the plain grub2 binaries is encrypted on a LUKS
  partition. Including the installer specific boot config. So if you for
  example put some password into the default boot options via --boot this
  is also stored in the encrypted part.

  At the moment only x86_64 is supported. And you have to run mksusecd on a
  machine that has grub2-i386-pc installed (to get the legacy BIOS setup).

  Unlike the usual setup, grub2 is used for both legacy BIOS and UEFI
  booting. So the boot screen really looks identical in both cases.

  The default image size is chosen to leave only minimal free space. To
  adjust the image size to your needs, use the --size option.

  ***  Important  ***

    For this to work, the 'cryptsetup' tools must be available in the
    installer's initrd. This is not the case for older media (prior to
    recent Tumbleweed and SLE/Leap 15).

    If you work with these old media you must also add the following two
    packages to the initrd explicitly:

      - cryptsetup
      - libpwquality1

    You can find the required versions on the install medium in either the
    /suse/x86_64 or /x86_64 directory. Copy them to some temporary location
    and add

      --initrd cryptsetup.rpm --initrd libpwquality1.rpm

    to your mksusecd command line.

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.

    sign-key-id: Key id of the signing key. The same as the --sign-key-id
      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];

  # Internally generated files are put into the $tmp_new base directory.
  # $files holds a database of all files and their locations (their base
  # directories).
  #
  # The aim is that files that come later in the $src list replace earlier
  # versions. With $tmp_new taking even more precedence.
  #
  # At the start of this function $files has already been set up with the
  # files in $tmp_new.
  #
  # So, go through $src in reverse, put new files into $files and exclude
  # duplicates.
  #
  # This does only apply to files, not directories.
  #
  for my $s (reverse @$src) {
    File::Find::find({
      wanted => sub {
        if(m#^$s->{dir}/(.+)#) {
          my $file_name = $1;
          if($files->{$file_name}) {
            if(-f "$s->{dir}/$file_name") {
              push @{$mkisofs->{exclude}}, "$s->{dir}/$file_name";
            }
          }
          else {
            $files->{$file_name} = $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 => $_ };
        if(-f fname("boot/$_/suse.ins")) {
          $boot->{$_}{bl}{ikr}{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_boot_options) {
        my $f = copy_or_new_file "boot/s390x/parmfile";
        if(open my $f, ">$f") {
          print $f "$opt_boot_options\n";
          close $f;
        }
      }
      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 -joliet-long" if $opt_joliet;

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

    if($t eq 'eltorito') {
      $has_el_torito = 1;
      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'";
  }

  # pass source locations via separate file to mksusecd, not as command line options
  $mkisofs->{filelist} = [ (map { $_->{dir} } grep { !$_->{skip} } @sources), $tmp_new ];

  # add relocated directory trees (graft points in mkisofs speak)
  push @{$mkisofs->{filelist}}, @{$mkisofs->{grafts}} if $mkisofs->{grafts};

  $mkisofs->{options} .= " -graft-points -path-list '$tmp_filelist'";
}


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

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

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

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

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

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

  if(!$has_el_torito && $has_efi) {
    # change platform id (1 byte at offset 1) from 0x0 to 0xef (EFI)...
    substr($boot_catalog, 1, 1) = "\xef";
    # ... and adjust header checksum (16 bits at offset 0x1c)
    substr($boot_catalog, 0x1c, 2) = pack("v", unpack("v", substr($boot_catalog, 0x1c, 2)) - 0xef00);
  }

  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 .= " --no-legacy" if !$has_el_torito;
  $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 = "$LIBEXECDIR/mksusecd/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"
  }

  # syslinux must be run as root now
  susystem "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;

  $opt = " --options '$opt_boot_options'" if $opt_boot_options;

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

  # sign repomd.xml

  my $name = "$dir/repodata/repomd.xml";

  return if !$sign_key_dir || !-f $name;

  system "cp $sign_key_pub $name.key";

  print "signing '$name'\n" if $opt_verbose >= 1;

  system "gpg --homedir=$sign_key_dir --local-user '$sign_key_id' --batch --yes --armor --detach-sign $sign_passwd_option $name";
}


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

    # isoinfo format change
    # cf. https://sourceforge.net/p/cdrtools/mailman/message/35173024
    s/^\s*\d+\s+//;

    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 = $tmp->dir();

  if($opt_rebuild_initrd) {
    unpack_orig_initrd if !$orig_initrd;
    die "initrd unpacking failed\n" if !$orig_initrd;
    $tmp_dir = $orig_initrd;
  }

  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 = $ins;
  $src =~ s#/[^/]*$##;

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

  my $new_initrd_siz = copy_or_new_file "$src/initrd.siz";
  if(open my $f, ">", $new_initrd_siz) {
    syswrite $f, pack("N", -s fname("$src/initrd"));
    close $f;
  }

  my @layout;

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

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

  my $new_ikr = copy_or_new_file $ikr;

  if(open my $d, ">", $new_ikr) {
    for (@layout) {
      my $fname = $_->{file};
      my $is_parmfile;
      $is_parmfile = 1 if $fname =~ m#/parmfile$#;
      if(open my $f, $fname) {
        sysread $f, my $buf, -s($f);
        close $f;
        sysseek $d, $_->{ofs}, 0;
        if($is_parmfile) {
          # write at least 1 block
          my $pad = 0x200 - length($buf);
          $buf .= "\x00" x $pad if $pad > 0;
          # remove newlines from parmfile
          $buf =~ s/\n+/ /g;
        }
        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 2>/dev/null") {
          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 && !$files_to_keep->{$f};
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Register files that must _not_ be removed from iso.
#
# keep_files(ref_to_file_list)
#
# ref_to_file_list is an array ref with file name patterns (regexp) to exclude
#
sub keep_files
{
  my $list = $_[0];

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

  for (sort keys %$files) {
    if(m#^($ex)$#) {
      my $f = fname($_);
      $files_to_keep->{$f} = 1;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# 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
{
  # be careful not to remove yast's control files
  keep_files [
    "x86_64/skelcd-control-.*",
  ];

  exclude_files [
    (map { "suse/$_" } @boot_archs, "i586", "noarch"),
    "(aarch64|ppc64le|s390x|x86_64)/.*", "src", "nosrc", "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",
    # be more specific, else we'll kill the grub themes
    "boot/[^/]*/openSUSE",
    "boot/[^/]*/SLES",
    "boot/[^/]*/SLED",
    "boot/.*/.*-xen",
    "control\\.xml",
    "gpg-.*",
    "NEWS",
    "license\\.tar\\.gz",
    "(|.*/)directory\\.yast",
    "suse",
    "(aarch64|ppc64le|s390x|x86_64)",
    "repodata",
  ];
}


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


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_content_or_checksums()
#
# Create a new /content or /CHECKSUMS file and return 1 if it is different
# from the existing one (meaning it needs to be re-signed).
#
sub update_content_or_checksums
{
  return $has_content ? update_content : update_checksums;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_content()
#
# 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;
}


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

  # don't modify CHECKSUMS 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 "CHECKSUMS";

  my $cont;

  # $cont is modelled after $cont in update_content() so that
  # it can be passed to add_to_content_file()
  $cont->{bits} = 256;

  # first, read existing file
  # ($content_file may be undefined - which is ok)
  if(open(my $f, $content_file)) {
    while(<$f>) {
      next if /^\s*$/;
      if(/^(\S+)\s+(\S+)/) {
        my $sum = "\L$1";
        my $name = $2;

        $cont->{HASH}{$name}{old} = "$cont->{bits} $sum";
        add_to_content_file $cont, "HASH", $name, '^';
      }
    }
    close $f;
  }

  # then, pick the files we want to be checksummed
  for (sort keys %$files) {
    next if m#directory\.yast$#;

    add_to_content_file $cont, "HASH", $_, '^boot/';
    add_to_content_file $cont, "HASH", $_, '^media\.1/';
    add_to_content_file $cont, "HASH", $_, '^docu/';
    add_to_content_file $cont, "HASH", $_, '^EFI/';
  }

  # compare new and old file checksums
  for (keys %{$cont->{HASH}}) {
    if($cont->{HASH}{$_}{new} ne $cont->{HASH}{$_}{old}) {
      $changed = 1;
      last;
    }
  }

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

    if($n) {
      if(open my $f, ">", $n) {
        for (sort keys %{$cont->{HASH}}) {
          next if !$cont->{HASH}{$_}{new};
          my $hash = $cont->{HASH}{$_}{new};
          $hash = (split ' ', $hash)[1];
          printf $f "%s  %s\n", $hash, $_;
        }

        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
= = = = = = = =

  if($opt_sign_key_id) {
    # step 1: export the public key, using the supplied id - this also ensures
    #         the key exists
    # step 2: get the canonical key id and creation date from the exported blob

    $sign_key_dir = $gpg_dir = "$ENV{HOME}/.gnupg";
    die "$sign_key_dir: no such gpg directory\n" unless -d $sign_key_dir;

    my $tmp_dir = $tmp->dir();
    system "gpg --homedir=$gpg_dir --export --armor --output $tmp_dir/key.pub $sign_passwd_option '$opt_sign_key_id'  >/dev/null 2>&1";

    my $keyid;
    my $date;

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

    if(!$keyid || !$date) {
      die "$opt_sign_key_id: failed to extract public key\n";
    }

    my $cname = sprintf "gpg-pubkey-%08x-%08x.asc", hex($keyid) & 0xffffffff, $date;
    $sign_key_pub = "$tmp_dir/$cname";
    rename "$tmp_dir/key.pub", $sign_key_pub;

    $sign_key_id = $keyid;

    print "using signing key, keyid = $sign_key_id\n";

    return;
  }

  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 $sign_passwd_option --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 $sign_passwd_option --export --armor --output $sign_key_pub >/dev/null 2>&1";

    $sign_key_id = $keyid;

    if($opt_sign_key) {
      print "using signing key, keyid = $sign_key_id\n";
    }
    else {
      print "transient signing key created, keyid = $sign_key_id\n";
    }
  }
  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";
  }

  push @opt_initrds, $tmp_dir;

  my $name = $sign_key_pub;
  $name =~ s#.*/##;

  my $k = copy_or_new_file "$name";
  system "cp $sign_key_pub $k";

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


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

  my $name = $has_content ? "content" : "CHECKSUMS";

  my $c = copy_file $name;
  return if !defined $c;

  my $k = copy_or_new_file "$name.key";

  copy_file "$name.asc";

  system "cp $sign_key_pub $k";

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

  system "gpg --homedir=$sign_key_dir --local-user '$sign_key_id' --batch --yes --armor --detach-sign $sign_passwd_option $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 multiple concatenated cpio archives.
#
# The archives are expected to be in cpio ASCII format ('cpio -H newc').
# Between the idividual archives an arbitrary sequence of (binary) zeros is
# allowed. (This is what the kernel allows for the initramfs image.)
#
# unpack_cpiox(dst, file, part)
#
# -  dst: the directory to unpack to
# - file: the archive file name
# - part: the part number (1 based) of a multipart archive (0 = unpack all)
#
sub unpack_cpiox
{
  my $dst = shift;
  my $file = shift;
  my $part = shift() + 0;

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

  # the archive number we are looking for (1 based)
  my $cnt = 1;

  # input and output file handles
  my ($f, $p);

  # data transfer buffer
  my $buf;

  # search for cpio header in input stream on next read operation
  my $sync = 0;

  # track # of written bytes (reset at start of each cpio archive)
  my $write_ofs;

  # Read # of bytes from input and write to output.
  #
  # bytes = $read_write->(len)
  # -   len: number of bytes to transfer
  # - bytes: size of data actually transferred
  #
  # This function implicitly opens a new output pipe if none is open and data
  # need to be written.
  #
  # If the $sync variable is set search the input stream for a valid cpio
  # header (and reset $sync to 0).
  #
  my $read_write = sub
  {
    my $len = $_[0];

    # nothing to do
    return $len if !$len;

    # clear buffer
    undef $buf;

    # Search for next cpio header.
    #
    # This assumes there's a number of binary zeros in the input stream
    # until the next cpio header.
    # Actually this only looks for the next non-zero data blob.
    if($sync) {
      $sync = 0;
      while(sysread($f, $buf, 1) == 1 && $buf eq "\x00") {};
      $len -= length $buf;
    }

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

    # In case we did read something, write it to output pipe.
    if(length $buf) {
      # Open a new pipe if needed.
      # But only if part number matches or is 0 (== all parts).
      if(!$p && ($part == 0 || $part == $cnt)) {
        open $p, "| ( cd $dst ; $cpio_cmd )" or die "failed to open cpio: $!\n";
        $write_ofs = 0;
      }

      # Write data and track output size for padding calculation at the end.
      if($p) {
        syswrite $p, $buf;
        $write_ofs += length $buf;
      }
    }

    return length $buf;
  };

  # Write padding bytes (pad with 0 to full 512 byte blocks) and close
  # output pipe.
  #
  # $write_pad_and_close->()
  #
  # This also sets a sync flag indicating that we should search for the next
  # valid cpio header in the input stream.
  #
  my $write_pad_and_close = sub
  {
    if($p) {
      my $pad = (($write_ofs + 0x1ff) & ~0x1ff) - $write_ofs;
      syswrite $p, "\x00" x $pad, $pad if $pad;
      close $p;
      undef $p;
    }

    # search for next cpio header in input stream
    $sync = 1;
  };

  # open archive and get going...
  if(open $f, $file) {
    my $len;

    # We have to trace the cpio archive structure.
    # Keep going as long as there's a header.
    while(($len = $read_write->(110)) == 110) {
      my $magic = substr($buf, 0, 6);
      my $head = substr($buf, 6);

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

      die "broken cpio header\n" if $magic !~ /^07070[12]$/;

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

      $read_write->($fname_len);

      my $fname = $buf;
      $fname =~ s/\x00*$//;

      $read_write->($data_len);

      # Look for cpio archive end marker.
      # If found, close cpio process. A new process will be started at the
      # next valid cpio archive header.
      if(
        $fname eq 'TRAILER!!!' &&
        $head =~ /^0{39}10{55}b0{8}$/i
      ) {
        $write_pad_and_close->();
        # exit if we're done
        if($cnt++ == $part) {
          close $f;
          return;
        }
      }
    }

    # we're done, close input file...
    close $f;

    # ...and output file.
    $write_pad_and_close->();

    # If $len is != 0 this means we've seen something that's not a header of
    # a cpio archive entry.
    die "invalid cpio data\n" if $len;
  }
  else {
    die "error reading cpio archive: $!\n";
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Unpack archive file.
#
# unpack_archive(type, file, dir, part)
#
# - type: a type string 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#([^/]+)${kext_regexp}$#) {
        $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 all provided kernel packages 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 names, per architecture
  #
  #  aarch64:	Image, Image-*; vmlinux-*
  #  i586:	vmlinuz, vmlinuz-*; vmlinux-*
  #  ppc64le:	vmlinux, vmlinux-*
  #  s390x:	image, image-*; vmlinux-*
  #  x86_64:	vmlinuz, vmlinuz-*; vmlinux-*
  #
  $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/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/${kext_regexp}$//;
      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#([^/]+)${kext_regexp}$#) {
      $kernel->{modules}{$1} = $i[0];
      # resolve module deps
      if($kernel->{initrd_modules}{$1} && @i > 1) {
        shift @i;
        for my $m (@i) {
          if($m =~ m#([^/]+)${kext_regexp}$#) {
            $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/*${kext_glob}")) {
    chomp $m;

    next unless -f $m;

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

    $m =~ s#.*/##;
    $m =~ s#${kext_regexp}$##;

    $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 (qw (loop squashfs lz4_decompress xxhash zstd_decompress)) {
      for my $ext (@kext_list) {
        if(-f "$kernel->{new_dir}/lib/modules/$kernel->{version}/initrd/$_$ext") {
          rename "$kernel->{new_dir}/lib/modules/$kernel->{version}/initrd/$_$ext", "$tmp_dir/lib/modules/$kernel->{version}/initrd/$_$ext";
        }
      }
    }

    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, ask_user)
#
# 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, $ask_user) = @_;
  my $new_xml;
  my @x;

  $ask_user = $ask_user ? 'true' : 'false';

  @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">$ask_user</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, ">", new_file("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;

  # keep some data:
  #   - application id: 0x80 bytes at 0x823e
  #   - tags set by tagmedia: 0x200 bytes starting at file offset 0x8373
  my $buf = read_sector 0x10;
  my $appid = substr $buf, 0x23e, 0x80;
  my $tags = substr $buf, 0x373, 0x200;
  $buf = "\x00" x 0x800;
  substr $buf, 0x23e, 0x80, $appid;
  substr $buf, 0x373, 0x200, $tags;

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

  close $iso_fh;
  undef $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# analyze_products(sources)
#
# sources is an array_ref containing a list of directories to be scanned and
# checked for product repositories (aka "modules").
#
# Repositories can use the same directory on different source isos. So we
# have to place them in new locations on the final medium. To avoid
# temporary copies, we do this via mkisofs's graft points (cf. man mkisofs).
#
# Repositories on the first medium are not relocated.
#
# Only repomd-style repositories are considered.
#
# Note: this assumes the lines in media.1/products to be structured like
# 'dir product version'.
#
sub analyze_products
{
  my $src = $_[0];

  my $src_idx = 0;

  for my $s (@$src) {
    # read top-level products file
    $_ = "$s->{dir}/media.1/products";
    if(open my $f, $_) {
      my @fields;
      while(my $l = <$f>) {
        @fields = split /\s/, $l;
        # ... and for each product definition, analyse it
        my $ok = check_product($src_idx, $_, @fields) if @fields == 3;
        # If we find a valid product, skip the source dir (except if it's the first).
        # Instead, only the product (the repository) is added to the final medium.
        $s->{skip} = 1 if $ok && $src_idx > 0;
      }
      close $f;
    }
    $src_idx++;
  }

  # inform the user
  print "Repositories:\n" if $product_db->{list};

  for (@{$product_db->{list}}) {
    next if !$_->{include};
    print "  $_->{name} [$_->{ver}]";
    print " ($_->{label})" if $_->{label};
    print "\n";
  }

  exit 0 if $opt_list_repos;

  # don't merge repos if the user doesn't want to
  return if !$opt_merge_repos;

  # rebuild products file
  my $prod_file = copy_or_new_file "media.1/products";

  # create/update add_on_products.xml
  my $products_xml;
  my $products_xml_updated;

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

  # rewrite entire product file
  open my $prod_fd, ">$prod_file" or die "media.1/products: $!\n";

  # ... and append any products we found above

  # Exclude all products that are in subdirectories and re-add them as
  # needed via mkisofs graft points. That's needed as they might be on
  # different media originally.
  #
  for (@{$product_db->{list}}) {
    push @{$mkisofs->{exclude}}, $_->{base_dir} if $_->{product_dir};

    next if !$_->{include};

    # FIXME: add $label to name?
    print $prod_fd "/$_->{repo_dir} $_->{name} $_->{ver}\n";

    # include full product dir, not just repo-specific subdirs
    push @{$mkisofs->{grafts}}, "$_->{product_dir}=$_->{base_dir}" if $_->{product_dir};

    if($opt_enable_repos =~ /^(1|yes|auto|ask)$/i) {
      my $ask_user = $opt_enable_repos =~ /^ask$/i;
      $products_xml = new_products_xml(
        $products_xml, "$_->{repo_dir}", $_->{repo_dir}, $_->{repo_dir},
        $opt_addon_prio, $ask_user
      );
      $products_xml_updated = 1;
    }
  }

  if($products_xml_updated && open my $fh, ">", new_file("add_on_products.xml")) {
    print $fh $products_xml;
    close $fh;
  }

  close $prod_fd;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# check_product(source_idx, product_file, base_dir, name, version)
#
# -   source_idx: # of source medium (0-based)
# - product_file: full path to 'media.1/products'
# -     base_dir: directory the repos are in
# -         name: product name
# -      version: some version string
#
# Analyze a single repomd repository and add result to global $product_db
# structure.
#
# This includes determining the directories belonging to the repository and
# repo type (binaries, debug, source).
#
sub check_product
{
  my ($src_idx, $prod, $dir, $name, $ver) = @_;

  my $base_dir = $prod;
  $base_dir =~ s#/media.1/products$#$dir#;
  $base_dir =~ s#/+$##;

  $dir =~ s#^/##;

  # skip if we did this already
  return 0 if $product_db->{checked}{$base_dir};

  $product_db->{checked}{$base_dir} = 1;

  # print "$base_dir: $name, $ver\n";

  # not repo-md
  return 0 unless -d "$base_dir/repodata";

  my %repodirs;
  my $debug = 0;

  # scan primary.xml for directories belonging to the repo and check if
  # there are debuginfo packages
  if(open my $f, "gunzip -c $base_dir/repodata/*-primary.xml.gz |") {
    while(my $l = <$f>) {
      $repodirs{$1} = 1 if $l =~ m#<location href="([^/]+)/#;
      $debug = 1 if $l =~ m#<location href=".*-debug(info|source)-#;
    }
    close $f;
  }

  # tag repo if it's a debuginfo or a source repo
  my @labels;
  push @labels, "sources" if $repodirs{src} || $repodirs{nosrc};
  push @labels, "debuginfo" if $debug;
  my $label = join ",", @labels;

  # Include label in new product base dir.
  # The reason is that on our module media the same directory is used for
  # binary, source, and debuginfo modules. So we'd have a file conflict when
  # putting them on the same medium..
  my $repo_dir = $dir;

  if($label) {
    $repo_dir = $name if !$repo_dir;
    $repo_dir .= "_$label";
  }

  # Check repo list if the repo should be included on the final medium.
  # See --include-repos option.
  # If unset, include everything.
  #
  # Products in media root dir are always included.
  my $inc = 1;
  if($opt_include_repos && $dir) {
    my @repos = split /,/, $opt_include_repos;
    $inc = grep { $_ eq $name } @repos;
  }

  # create internal product database entry
  push @{$product_db->{list}}, {
    base_dir => $base_dir,
    product_dir => $dir,
    name => $name,
    ver => $ver,
    dirs => [ "repodata", sort keys %repodirs ],
    debug => $debug,
    label => $label,
    src_idx => $src_idx,
    repo_dir => $repo_dir,
    include => $inc
  };

  return 1;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# crypto_cleanup(image_file, crypt_vol, crypt_loop, crypt_mount, esp_loop, esp_mount, iso_mount)
#
# Cleanup things in case we have to leave run_crypto_disk() early.
#
sub crypto_cleanup
{
  my ($image_file, $crypt_vol, $crypt_loop, $crypt_mount, $esp_loop, $esp_mount, $iso_mount) = @_;

  # unmount things that might have been mounted
  susystem "umount $crypt_mount" if -d $crypt_mount;
  susystem "umount $esp_mount" if -d $esp_mount;
  susystem "umount $iso_mount" if -d $iso_mount;

  # close the luks volume
  susystem "cryptsetup close $crypt_vol" if -b "/dev/mapper/$crypt_vol";

  # detach loop devices
  susystem "losetup -d $esp_loop" if -b $esp_loop;
  susystem "losetup -d $crypt_loop" if -b $crypt_loop;

  # remove the temporary image
  unlink $image_file;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_crypto_disk()
#
# Create an LUKS-encrypted install disk.
#
# Notes:
#   - this is not a hybrid image - you cannot use it as dvd image
#   - so far only for x86_64
#   - the image is UEFI and legacy BIOS bootable
#   - there are two partitions: an (unencrypted) EFI system partition and
#     the encrypted install partition
#   - everything except the grub binary is encrypted - including the boot
#     config files used for installation
#   - a lot of things have to be done with root permissions, even though
#     we are working only on image files :-(
#
# This function uses the iso produced in the first pass as basis.
#
sub run_crypto_disk
{
  die "\nsorry, package grub2-i386-pc must be installed\n" unless -f "/usr/lib/grub2/i386-pc/boot.img";

  die "\nfilesystem '$opt_crypto_fs' not supported\n" unless -x "/usr/sbin/mkfs.$opt_crypto_fs";

  # if set, it must have a "/" at the end (e.g. "foo/")
  my $top_dir = $opt_crypto_top_dir;
  die "\n$top_dir: top-dir value not allowed\n" if $top_dir =~ m#/# || $top_dir eq '.' || $top_dir eq '..';
  $top_dir .= "/" if $top_dir ne "";

  # EFI system partition start in MiB
  my $esp_start_mb = 1;

  # EFI system partition size in MiB
  my $esp_size_mb = 7;

  # install partition start in MiB
  my $crypt_start_mb = $esp_start_mb + $esp_size_mb;

  # we need a temporary device mapper target - just use our tmp dir name
  my $crypt_vol;
  $crypt_vol = $1 if $tmp_new =~ m#/tmp/([^/]+)#;
  die "\noops: can't generate device mapper name\n" if $crypt_vol eq "";

  # get the image size
  if(!$image_size) {
    my $size = (-s $iso_file) >> 20;
    # increase size slightly to ensure we have enough space
    $size = $size * 1.2;
    $size += $esp_size_mb + $esp_start_mb + 8;
    $image_size = int($size) << 11;
  }

  my $image_file = "${iso_file}.tmp.$crypt_vol";

  my ($crypt_loop, $crypt_mount, $esp_loop, $esp_mount, $iso_mount, $fh);

  # store the password to be used by cryptsetup later
  my $crypt_pw = $tmp->file('pw');
  open $fh, ">$crypt_pw" or die "\npassword file: $!\n";
  print $fh $opt_crypto_password;
  close $fh;

  # register cleanup function in case we error out
  END { crypto_cleanup $image_file, $crypt_vol, $crypt_loop, $crypt_mount, $esp_loop, $esp_mount, $iso_mount }

  # create the empty image
  open $fh, ">", $image_file;
  close $fh;
  truncate $image_file, $image_size << 9;

  # partition it
  system "parted -s '$image_file' mklabel msdos";
  system "parted -s '$image_file' mkpart p ${esp_start_mb}MiB ${crypt_start_mb}MiB";
  system "parted -s '$image_file' mkpart p ${crypt_start_mb}MiB 100%";
  system "parted -s '$image_file' set 1 boot on";
  system "sfdisk --change-id '$image_file' 1 0xef 2>/dev/null";

  # make an efi system partition (fat)
  fat_mkfs $tmp_fat, $esp_size_mb << 11, $esp_start_mb << 11, 1, "EFI_PART";

  # ... and copy it into the efi system partition
  system "dd if='$tmp_fat' of='$image_file' bs=1b seek=" . ($esp_start_mb << 11) . " conv=notrunc status=none ; sync";

  # get loop device for install partition
  $crypt_loop = `${sudo}losetup --show -f -o ${\($crypt_start_mb << 20)} '$image_file'`;
  chomp $crypt_loop;

  die "\noops: no loop device\n" unless -b $crypt_loop;

  show_progress 10;

  # create luks container
  susystem "cryptsetup --batch-mode --force-password --key-file=$crypt_pw luksFormat $crypt_loop";
  susystem "cryptsetup --key-file=$crypt_pw open $crypt_loop $crypt_vol";

  # add filesystem
  susystem "mkfs.$opt_crypto_fs -q /dev/mapper/$crypt_vol";

  # get filesystem uuid
  my $fs_uuid=`${sudo}blkid -o value -s UUID /dev/mapper/$crypt_vol`;
  chomp $fs_uuid;

  # ... and luks partition uuid
  my $luks_uuid=`${sudo}blkid -o value -s UUID $crypt_loop`;
  chomp $luks_uuid;

  # grub prefers the uuid without dashes ('-')
  my $grub_uuid = $luks_uuid;
  $grub_uuid =~ tr/-//d;

  die "\noops: cryptsetup failed\n" if $fs_uuid eq "" || $luks_uuid eq "";

  show_progress 30;

  # get loop device for efi system partition
  $esp_loop = `${sudo}losetup --show -f -o ${\($esp_start_mb << 20)} --sizelimit ${\($esp_size_mb << 20)} '$image_file'`;
  chomp $esp_loop;

  die "\noops: no loop device\n" unless -b $esp_loop;

  # create temporary mount points
  $esp_mount = $tmp->dir('esp');
  $crypt_mount = $tmp->dir('crypt');
  $iso_mount = $tmp->dir('iso');

  # mount install partition, efi partition, and the prepared iso image
  die "\ncrypto mount failed\n" if susystem "mount /dev/mapper/$crypt_vol $crypt_mount";
  die "\nesp mount failed\n" if susystem "mount -oumask=0 $esp_loop $esp_mount";
  die "\niso mount failed\n" if susystem "mount -oloop,ro '$iso_file' $iso_mount";

  # some checks
  die "\nsorry, only x86_64 media supported atm\n" unless -d "$iso_mount/boot/x86_64/grub2-efi";
  die "\nsorry, efi boot is required\n" unless -d "$iso_mount/EFI/BOOT";

  # now our partitions are prepared and mounted

  show_progress 50;

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

  my $title = $opt_crypto_title || "openSUSE";

  # ---  1. grub2 legacy setup  ---

  (my $grub_mods = <<"  = = = = = = = =") =~ s/\s+/ /g;
    gfxmenu gfxterm
    video videoinfo vga vbe
    biosdisk linux
    ext2 btrfs xfs jfs reiserfs iso9660 tar memdisk probe
    cryptodisk luks gcry_rijndael gcry_sha1 gcry_sha256
    all_video boot cat chain configfile echo
    font gzio halt
    jpeg minicmd normal part_apple part_msdos part_gpt
    password_pbkdf2 png reboot search search_fs_uuid
    search_fs_file search_label sleep test video fat loadenv
  = = = = = = = =

  # copy grub boot block to the required place for grub2-mkimage
  File::Path::make_path "$esp_mount/BOOT/grub2/i386-pc";
  system "cp /usr/lib/grub2/i386-pc/boot.img $esp_mount/boot/grub2/i386-pc";

  my $title_centered = $title;
  if(length $title <= 78) {
    $title_centered = (" " x ((80 - length $title) / 2)) . $title;
  }

  # This is the initial grub config - just ask for password and mount
  # the luks volume.
  # The real grub config with the install menu is inside the luks volume.
  (my $load_cfg = <<"  = = = = = = = =") =~ s/^ +//mg;
    locale_dir=\$prefix/locale
    lang=en_US
    clear
    echo
    echo "$title_centered"
    echo
    echo "                                   -- BIOS --"
    echo
    cryptomount -u $grub_uuid
    root=crypto0
    set prefix=(\$root)/${top_dir}boot/x86_64/grub2-efi
  = = = = = = = =

  (my $grub_po = <<"  = = = = = = = =") =~ s/^ +//mg;
    msgid "GNU GRUB  version %s"
    msgstr "$title"

    msgid "Attempting to decrypt master key..."
    msgstr " "

    msgid "Enter passphrase for %s%s%s (%s): "
    msgstr "Enter passphrase: "
  = = = = = = = =

  open $fh, ">$tmp_dir/load.cfg";
  print $fh $load_cfg;
  close $fh;

  # The default grub password dialog looks dead ugly; (ab)use
  # localization to make it look nice.
  # The en.mo file is placed into a memdisk which is then embedded into the
  # grub image.
  File::Path::make_path "$tmp_dir/memdisk/locale";
  open $fh, "| msgfmt -o $tmp_dir/memdisk/locale/en.mo -";
  print $fh $grub_po;
  close $fh;
  system "cd $tmp_dir/memdisk ; tar -cf ../memdisk.tar *";

  system "grub2-mkimage -O i386-pc -m $tmp_dir/memdisk.tar -p '(memdisk)'" .
    " -c $tmp_dir/load.cfg -o $esp_mount/boot/grub2/i386-pc/core.img $grub_mods";

  # grub2-bios-setup behaves a bit weird; I've seen no way to stop it from
  # trying to figure out things on its own. So it needs root permissions
  # to trace things through the loop device.
  susystem "grub2-bios-setup -s -d $esp_mount/boot/grub2/i386-pc '$image_file'";

  # the grub stuff is no longer needed, clear the efi partition
  system "rm -r $esp_mount/boot";

  # ---  grub2 legacy setup done  ---

  # ---  2. grub2 efi setup  ---

  # copy the efi config from the prepared iso and add our en.mo file
  system "cp -r $iso_mount/EFI $esp_mount/";
  system "cp $tmp_dir/memdisk/locale/en.mo $esp_mount/EFI/BOOT/locale";

  # adjust the startup message to indicate we've booted via efi
  $load_cfg =~ s/BIOS/UEFI/g;

  # we have to explicitly load the final grub config (in the legacy case it
  # is done automatically)
  $load_cfg .= "configfile \$prefix/efi.cfg\n";

  # write initial grub config
  open $fh, ">$esp_mount/EFI/BOOT/grub.cfg";
  print $fh $load_cfg;
  close $fh;

  # ---  grub2 efi setup done  ---

  show_progress 60;

  # ---  3. install partition setup  ---

  # 3.1. copy everything

  # maybe put everything into a separate directory
  susystem "mkdir $crypt_mount/$top_dir" if $top_dir;

  # copy everything except the efi config - it's already on the efi system
  # partition
  system "${sudo}tar -C $iso_mount --exclude EFI -cf - . | ${sudo}tar -C $crypt_mount/$top_dir -xpf -";

  show_progress 90;

  # move locale settings to the correct place
  susystem "cp -r $iso_mount/EFI/BOOT/locale $crypt_mount/${top_dir}boot/x86_64/grub2-efi";

  # sanitize permissions (everything is ro on an iso9660 fs)
  susystem "chmod -R u+w $crypt_mount";

  # the el-torito efi image is not needed
  susystem "rm -f $crypt_mount/${top_dir}boot/x86_64/efi";

  # 3.2. adjust grub install config

  # There's only a grub config for efi on our media. We derive the legacy
  # config from it - it needs just a few modifications.

  # get it
  my $grub_cfg = `cat $iso_mount/EFI/BOOT/grub.cfg`;
  die "\nno grub config found\n" if $grub_cfg eq "";

  # strip things we don't want (it's been setup in the initial grub config)
  $grub_cfg =~ s/^search .*\n//m;
  $grub_cfg =~ s/^prefix=.*\n//m;
  $grub_cfg =~ s/^insmod efi_.*\n//mg;

   # adjust paths
  if($top_dir ne "") {
    $grub_cfg =~ s#/boot/#/${top_dir}boot/#g
  }

  # write grub efi config

  # Due to permission issues (the install partition is only root-writable)
  # write it to a tmp file and copy later.
  open $fh, ">$tmp_dir/grub.cfg";
  print $fh $grub_cfg;
  close $fh;

  # it's the efi config
  susystem "cp $tmp_dir/grub.cfg $crypt_mount/${top_dir}boot/x86_64/grub2-efi/efi.cfg";

  # convert efi config to legacy config

  # Basically replace linuxefi/initrdefi with linux/initrd and replace the
  # 'local boot' entry.
  $grub_cfg =~ s/\b(linux|initrd)efi\b/$1/g;

  (my $local_boot = <<"  = = = = = = = =") =~ s/^ {4}//mg;
    menuentry "Boot from Hard Disk" --class opensuse --class gnu-linux --class gnu --class os {
      set root=hd1
      chainloader (hd1)+1
    }
  = = = = = = = =

  $grub_cfg =~ s/^menuentry "Boot from Hard Disk".*?^\}\n/$local_boot/sm;

  # write grub legacy config

  # Due to permission issues (the install partition is only root-writable)
  # write it to a tmp file and copy later.
  open $fh, ">$tmp_dir/grub.cfg";
  print $fh $grub_cfg;
  close $fh;

  susystem "cp $tmp_dir/grub.cfg $crypt_mount/${top_dir}boot/x86_64/grub2-efi/grub.cfg";

  # adjust initrd

  # Now inject code to decrypt and mount the install partition. Also, point
  # the install source to the luks volume.

  # find the initrd
  my $initrd_file = "$crypt_mount/${top_dir}boot/x86_64/loader/initrd";
  $initrd_file = "$crypt_mount/${top_dir}boot/x86_64/initrd" unless -f $initrd_file;
  die "\nsorry, no initrd found\n" unless -f $initrd_file;

  File::Path::make_path "$tmp_dir/initrd/etc/linuxrc.d";

  # Store the password in the initrd so the user doesn't have to enter it twice.
  # (Note the initrd is on the encrypted volume.)
  # To avoid accidentally leaking the password when the user hands out the
  # initrd to someone else, encrypt the password file with the luks and
  # filesystem uuid as key. The uuid info is separate from the initrd. So even
  # if you copy the whole install souces from the unlocked luks volume, it
  # doesn't leak your password.
  open $fh, "| gpg --passphrase '$luks_uuid $fs_uuid' -c --batch --cipher-algo aes256 -o $tmp_dir/initrd/.password 2>/dev/null";
  print $fh "$opt_crypto_password";
  close $fh;

  die "\noops: password setup failed\n" unless -s "$tmp_dir/initrd/.password";

  # strip the final slash
  my $t = ${top_dir};
  $t =~ s#/$##;

  # Inject a linuxrc config entry to unlock the luks volume and point the
  # install source to the volume.
  # Note: we cannot unlock the volume directly here but have to append the
  # call to the 'early_setup' script as the config files are parsed _before_
  # udev sets up the devices.
  # The install option can still be overriden by anything passed via kernel
  # command line.
  (my $linuxrc_setup = <<"  = = = = = = = =") =~ s/^ +//mg;
    exec=echo /scripts/crypt_setup >>/scripts/early_setup
    install=hd:/$t?device=/dev/mapper/install.luks
  = = = = = = = =

  # for repo-md, instsys location should be set explicitly
  if($repomd_instsys_location) {
    my $dir = "/$t/$repomd_instsys_location";
    # be careful to have only single slashes
    $dir =~ s#//#/#g;
    $linuxrc_setup .= "instsys=hd:$dir?device=/dev/mapper/install.luks\n";
  }

  open $fh, ">$tmp_dir/initrd/etc/linuxrc.d/90_crypto";
  print $fh $linuxrc_setup;
  close $fh;

  # Store a small part of the id in the script to help identify the right
  # volume.
  # It's strictly not necessary but this way we don't waste too much efford
  # on systems with lots of volumes.
  my $short_uuid = substr $luks_uuid, 0, 4;

  # Here's the script. It iterates over all volumes, tries to decrypt the
  # password with the uuids it sees, and on success sets up the luks volume
  # and deletes the password files and itself.
  (my $crypt_setup = <<"  = = = = = = = =") =~ s/^ {4}//mg;
    #! /bin/bash
    cd /dev/disk/by-uuid
    for uuid in $short_uuid* ; do
      gpg --passphrase \"\$uuid $fs_uuid\" -d --batch --cipher-algo aes256 -o /.password. /.password 2>/dev/null
      if [ -f /.password. ] ; then
        cryptsetup --key-file=/.password. open /dev/disk/by-uuid/\$uuid install.luks
        break
      fi
    done
    rm -f /.password* /scripts/crypt_setup
  = = = = = = = =

  # add the script to our initrd tree
  mkdir "$tmp_dir/initrd/scripts";
  open $fh, ">$tmp_dir/initrd/scripts/crypt_setup";
  print $fh $crypt_setup;
  close $fh;
  chmod 0755, "$tmp_dir/initrd/scripts/crypt_setup";

  # pack, compress, and append our initrd stuff to the initrd
  system "( cd $tmp_dir/initrd ; find . | cpio --quiet -o -H newc --owner 0:0 | xz --check=crc32 -c ) > $tmp_dir/initrd.xz";
  susystem "sh -c 'cat $tmp_dir/initrd.xz >> $initrd_file'";

  # ---  install partition setup done  ---

  # That's it, we're done.

  show_progress 100;

  # rename our temporary image to the final name
  rename $image_file, $iso_file;

  print "\n";
}
