#! /usr/bin/perl

use strict;

use File::Temp;
use Getopt::Long;

sub verify_test;
sub run_test;
sub create_image;
sub gpg_init;
sub sign_image;

my $testdir = "tests";
my $gpg_dir1;
my $gpg_dir2;

# store reference output, don't do checks
my $opt_create_reference;

GetOptions(
  'create-reference' => \$opt_create_reference,
);

# test cases
my $tests = [
  {
    name => "iso_and_partition_no_padding",
    digest => "md5",
    full_blocks => 1000,
    iso_blocks => 1000,
    pad_blocks => 0,
    part_start => 100,
    part_blocks => 900,
  },

  {
    name => "iso_and_partition_with_padding",
    digest => "sha1",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
  },

  {
    name => "iso_and_partition_no_isomagic",
    digest => "sha224",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    no_iso_magic => 1,
  },

  {
    name => "iso_and_partition_no_isodigest",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    tag_options => "--remove sha256sum",
  },

  {
    name => "iso_and_partition_no_partitiondigest",
    digest => "sha384",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    tag_options => "--remove partition",
  },

  {
    name => "iso_and_partition_no_digest",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    tag_options => "--remove partition --remove sha256sum",
  },

  {
    name => "iso_and_partition_wrong_padding",
    digest => "sha512",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    tag_options => "--add-tag pad=50",
  },

  {
    name => "iso_and_no_partition",
    digest => "sha224",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 0,
    part_blocks => 0,
  },

  {
    name => "no_iso_and_partition",
    digest => "sha384",
    full_blocks => 1000,
    iso_blocks => 0,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
  },

  {
    name => "iso_and_partition_odd_sizes",
    digest => "sha1",
    full_blocks => 1001,
    iso_blocks => 1000,
    pad_blocks => 100,
    part_start => 101,
    part_blocks => 900,
  },

  {
    name => "iso_and_partition_odd_partition_size",
    digest => "md5",
    full_blocks => 1002,
    iso_blocks => 1000,
    pad_blocks => 100,
    part_start => 101,
    part_blocks => 901,
  },

  {
    name => "iso_and_partition_low_partition_start",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 1000,
    pad_blocks => 100,
    part_start => 2,
    part_blocks => 998,
  },

  {
    name => "iso_too_small_ends_before_partition_start",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 600,
    pad_blocks => 100,
    part_start => 700,
    part_blocks => 300,
  },

  {
    name => "iso_too_small_ends_at_partition_start",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 700,
    pad_blocks => 100,
    part_start => 700,
    part_blocks => 300,
  },

  {
    name => "iso_too_small_ends_after_partition_start",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 800,
    pad_blocks => 100,
    part_start => 700,
    part_blocks => 300,
  },

  {
    name => "iso_and_partition_not_signed",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    sign => 1,
  },

  {
    name => "iso_and_partition_signed_ok",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    sign => 2,
  },

  {
    name => "iso_and_partition_signed_bad",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    sign => 3,
  },

  {
    name => "iso_and_partition_signed_wrong_key",
    digest => "sha256",
    full_blocks => 1000,
    iso_blocks => 900,
    pad_blocks => 100,
    part_start => 100,
    part_blocks => 900,
    sign => 4,
  },
];


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$ENV{LD_LIBRARY_PATH} = ".";

my $count = 0;
my $failed = 0;

$gpg_dir1 = gpg_init;
$gpg_dir2 = gpg_init;

for my $test (@$tests) {
  $count++;
  create_image $test;
  run_test $test;
  $failed += verify_test $test if !$opt_create_reference;
}

if($opt_create_reference) {
  print "$count test results created\n";
}
else {
  print "--\n$failed of $count tests failed\n";
}

exit $failed ? 1 : 0;


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Compare expected output against reference output.
#
# For both tagmedia and checkmedia calls.
#
sub verify_test
{
  my ($config) = @_;
  my $err = 1;

  my $base = "$testdir/$config->{name}";
  my $digest = $config->{digest} || "sha256";

  my ($tag, $check, $ref_tag, $ref_check);

  if(open my $f, "$base.$digest.tag") { local $/; $tag = <$f>; close $f; }
  if(open my $f, "$base.$digest.check") { local $/; $check = <$f>; close $f; }
  if(open my $f, "$base.$digest.tag.ref") { local $/; $ref_tag = <$f>; close $f; }
  if(open my $f, "$base.$digest.check.ref") { local $/; $ref_check = <$f>; close $f; }

  $err = 0 if $tag eq $ref_tag && $check eq $ref_check;

  printf "%s.%s: %s\n", $config->{name}, $digest, $err ? "failed" : "ok";

  return $err;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Run tagmedia and checkmedia on test image.
#
sub run_test
{
  my ($config) = @_;

  my $base = "$testdir/$config->{name}";
  my $digest = $config->{digest} || "sha256";
  my $ref = $opt_create_reference ? ".ref" : "";

  my $pad;
  if($config->{pad_blocks}) {
    $pad = " --pad ${\($config->{pad_blocks} / 4)}"
  }

  system "./tagmedia --digest $digest $pad $config->{tag_options} $base.img >$base.$digest.tag$ref";

  sign_image "$base.img", $config->{sign};

  my $verbose;
  $verbose = "-v -v" if $config->{sign} <= 1;	# avoid gpg log

  system "./checkmedia $verbose --key-file $gpg_dir1/test.pub $base.img >$base.$digest.check$ref";

  # patch out actual checksum as it varies for each run
  if(!$verbose) {
    if(open my $f, "$base.$digest.check$ref") {
      local $/;
      my $log = <$f>;
      close $f;
      if(open my $f, ">$base.$digest.check$ref") {
        $log =~ s/(sha256: )(\S+)/${1}*/g;
        print $f $log;
        close $f;
      }
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Create test image according to config.
#
# This doesn't put real values (like files in file systems) into the images
# but produces meta data as used by tagmedia/checkmedia:
#
# - a simplified iso9660 header
# - a partition table (mbr) with a single partition
# - some (4 bytes) data at iso image start/end, partition start/end,
#   full image start/end to make sure we catch boundaries correctly
#
sub create_image
{
  my ($config) = @_;

  my $part_end = $config->{part_start} + $config->{part_blocks};
  if(
    $part_end > $config->{iso_blocks} - $config->{pad_blocks} &&
    $part_end < $config->{iso_blocks}
  ) {
    die "$config->{name}: partition end can't be inside padding area\n";
  }

  if(
    $part_end > $config->{full_blocks} ||
    $config->{iso_blocks} > $config->{full_blocks}
  ) {
    die "$config->{name}: image sizes inconsistent\n";
  }

  my $file = "$testdir/$config->{name}.img";

  open my $f, ">", $file;
  die "$file: $!\n" if !$f;

  truncate $f, $config->{full_blocks} << 9;

  if($config->{iso_blocks}) {
    if(!$config->{no_iso_magic}) {
      seek $f, 0x8000, 0;
      syswrite $f, "\x01CD001\x01\x00";
    }

    seek $f, 0x8050, 0;
    syswrite $f, pack("VN", $config->{iso_blocks} / 4, $config->{iso_blocks} / 4);

    seek $f, (($config->{iso_blocks} - $config->{pad_blocks}) << 9) - 4, 0;
    syswrite $f, "efgh";
  }

  if($config->{name}) {
    seek $f, 0x823e, 0;
    syswrite $f, pack("A128", substr($config->{name}, 0, 128));
  }

  if($config->{full_blocks} > $config->{iso_blocks}) {
    seek $f, ($config->{iso_blocks} << 9), 0;
    syswrite $f, "ijkl";
    seek $f, ($config->{full_blocks} << 9) - 4, 0;
    syswrite $f, "uvwx";
  }

  if($config->{part_start}) {
    seek $f, 0x0, 0;
    syswrite $f, "abcd";

    seek $f, 0x1fe, 0;
    syswrite $f, "\x55\xaa";

    seek $f, 0x1be, 0;
    syswrite $f, pack("V4", 0xffffff00, 0xffffff83, $config->{part_start}, $config->{part_blocks});

    seek $f, ($config->{part_start} << 9), 0;
    syswrite $f, "mnop";

    seek $f, (($config->{part_start} + $config->{part_blocks})<< 9) - 4, 0;
    syswrite $f, "qrst";
  }

  # reserve signature block
  if($config->{sign} && $config->{part_blocks} > 164) {
    seek $f, (($config->{part_start} + 160)<< 9), 0;
    syswrite $f, "7984fc91-a43f-4e45-bf27-6d3aa08b24cf";
  }

  close $f;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Setup gpg dir and create key pair.
#
sub gpg_init
{
  my $gpg_dir = File::Temp::tempdir("/tmp/testmediacheck.XXXXXXXX", CLEANUP => 1);

  (my $c = <<"  = = = = = = = =") =~ s/^ {4}//mg;
    %no-ask-passphrase
    %no-protection
    %transient-key
    Key-Type: RSA
    Key-Length: 2048
    Name-Real: test Signing Key
    Name-Comment: transient key
    %pubring test.pub
    %secring test.sec
    %commit
  = = = = = = = =

  if(open my $p, "| cd $gpg_dir ; /usr/bin/gpg --homedir=$gpg_dir --batch --armor --debug-quick-random --gen-key - 2>/dev/null") {
    print $p $c;
    close $p;
  }

  # older gpg versions use the secret key file here
  my $key = "$gpg_dir/test.sec";
  $key = "$gpg_dir/test.pub" unless -f $key;

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

  return $gpg_dir;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Sign image.
#
# type:
#   0: no signature (no signature block)
#   1: no signature (empty signature block)
#   2: signature ok
#   3: signature bad
#   4: signature with wrong key
#
sub sign_image
{
  my ($file, $type) = @_;

  return if $type <= 1;

  my $gpg_dir = $gpg_dir1;

  $gpg_dir = $gpg_dir2 if $type == 4;	# wrong key

  system "./tagmedia --export-tags $gpg_dir/foo $file";
  system "echo foo >>$gpg_dir/foo" if $type == 3;	# bad signature
  system "/usr/bin/gpg --homedir=$gpg_dir --batch --yes --armor --detach-sign $gpg_dir/foo";
  system "./tagmedia --import-signature $gpg_dir/foo.asc $file";
}
