#!/usr/bin/perl

BEGIN {
  unshift @INC, ($::ENV{'BUILD_DIR'} || '/usr/lib/build');
}

use MIME::Base64 ();
use Data::Dumper;

use Build;
use Build::Download;
use Build::SimpleJSON;

use strict;

sub readstr {
  my ($fn) = @_;
  my $f;
  open($f, '<', $fn) || die("$fn: $!\n");
  my $d = '';
  1 while sysread($f, $d, 8192, length($d));
  close $f;
  return $d;
}

sub writestr {
  my ($fn, $fnf, $d) = @_;
  my $f;
  open($f, '>', $fn) || die("$fn: $!\n");
  if (length($d)) {
    (syswrite($f, $d) || 0) == length($d) || die("$fn write: $!\n");
  }
  close($f) || die("$fn close: $!\n");
  return unless defined $fnf;
  rename($fn, $fnf) || die("rename $fn $fnf: $!\n");
}


sub material2digest {
  my ($material) = @_;
  my $digests = $material->{'digest'};
  return undef unless ref($digests) eq 'HASH';
  my $digest;
  my $digest_t;
  for my $t (sort keys %$digests) {
    my $v = $digests->{$t};
    next if $digest && length($v) < length($digest);
    $digest = $v;
    $digest_t = $t;
  }
  return undef unless $digest;
  return lc($digest_t).":$digest";
}

sub check_existing {
  my ($fn, $digest) = @_;
  if (-l $fn || -e _) {
    if ($digest) {
      eval { Build::Download::checkfiledigest($fn, $digest) };
      return 1 unless $@;
    }
    unlink($fn) || die("unlink $fn: $!\n");
  }
  return 0;
}

sub download_materials {
  my ($materials, $dir, $subdir) = @_;
  if ($subdir) {
    mkdir("$dir/$subdir") || die("mkdir $dir/$subdir: $!\n") unless -d "$dir/$subdir";
    $subdir .= "/";
  }
  $subdir ||= '';
  for my $material (@{$materials || []}) {
    my $fn = $material->{'filename'};
    $fn = '.build.config' if $material->{'intent'} eq 'buildconfig';
    my $digest = material2digest($material);
    next if check_existing("$dir/$subdir$fn", $digest);
    Build::Download::download($material->{'uri'}, "$dir/$subdir$fn", undef, 'digest' => $digest);
  }
}

die("usage: unpack_slsa_provenance <provenance.json> <dir>\n") unless @ARGV == 2;
my ($provenance_file, $dir) = @ARGV;

die("$provenance_file: $!\n") unless -e $provenance_file;
die("$dir: $!\n") unless -e $dir;
die("$dir: Not a directory\n") unless -d $dir;

my $provenance = readstr($provenance_file);
$provenance = Build::SimpleJSON::parse($provenance);
if ($provenance->{'payload'}) {
  $provenance = MIME::Base64::decode_base64($provenance->{'payload'});
  $provenance = Build::SimpleJSON::parse($provenance);
}
my $predicate_type = $provenance->{'predicateType'};
die("no predicateType in provenance?\n") unless $predicate_type;
die("unsupported predicate type '$predicate_type'\n") unless $predicate_type eq 'https://slsa.dev/provenance/v0.1' || $predicate_type eq 'https://slsa.dev/provenance/v0.2' || $predicate_type eq 'https://slsa.dev/provenance/v1';

my $predicate = $provenance->{'predicate'};
die("no predicate in provenance?\n") unless ref($predicate) eq 'HASH';

my ($materials, $recipefile, $parameters);

if ($predicate_type eq 'https://slsa.dev/provenance/v1') {
  my $build_definition = $predicate->{'buildDefinition'};
  die("no buildDefinition in predicate?\n") unless ref($build_definition) eq 'HASH';
  my $build_type = $build_definition->{'buildType'} || '';
  die("Unsupported buildType '$build_type'\n") unless $build_type eq 'https://open-build-service.org/worker';
  my $external_parameters = $build_definition->{'externalParameters'};
  die("no externalParameters in buildDefinition?\n") unless ref($build_definition) eq 'HASH';
  $recipefile = $external_parameters->{'recipeFile'};
  die("no recipeFile in externalParameters?\n") unless defined($recipefile) && ref($recipefile) eq '';
  $materials = $build_definition->{'resolvedDependencies'};
  $parameters = $external_parameters;
} else {
  my $build_type = $predicate->{'buildType'} || '';
  die("Unsupported buildType '$build_type'\n") unless $build_type eq 'https://open-build-service.org/worker';
  $materials = $predicate->{'materials'};
  my $invocation = $predicate->{'invocation'};
  die("no invocation in predicate?\n") unless ref($invocation) eq 'HASH';
  my $configsource = $invocation->{'configSource'};
  die("no configSource in invocation?\n") unless ref($configsource) eq 'HASH';
  $recipefile = $configsource->{'entryPoint'};
  die("no entryPoint in configSource?\n") unless defined($recipefile) && ref($recipefile) eq '';
  $parameters = $invocation->{'parameters'};
  die("bad parameters in invocation?\n") unless !$parameters || ref($parameters) eq 'HASH';
}


die("no materials in predicate?\n") unless ref($materials) eq 'ARRAY';

# add name/intent to all materials
for (@$materials) {
  my $intent = $_->{'intent'};
  $intent = $_->{'annotations'}->{'intent'} if $_->{'annotations'};
  if (!$intent && $_->{'uri'}) {
    # autodetect sources/buildconfig
    $intent = 'source' unless $_->{'uri'} =~ /\/_slsa\//;
    $intent = 'buildconfig' if $_->{'uri'} =~ /\/_slsa\// && $_->{'uri'} =~ /\/_config\/[^\/]+$/
  }
  $intent ||= 'buildenv';
  die("unknown intent in material: '$intent'\n") unless $intent eq 'buildenv' || $intent eq 'buildconfig' || $intent eq 'source' || $intent eq 'repos' || $intent eq 'containers';

  my $filename = $_->{'name'};
  if (!$filename && $_->{'uri'}) {
    $filename = $_->{'uri'};
    $filename =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/sge;
    $filename =~ s/\/[^\/]+$// if $filename =~ /\/_slsa\//;
    $filename =~ s/.*\///;
  }
  die("cannot determine file name for material\n") unless defined $filename;
  die("bad file name $filename\n") if $filename eq '.' || $filename eq '..' || $filename eq '' || $filename =~ /[\000-\037\/]/ || $filename =~ /^\.build\./s;

  $_->{'intent'} = $intent;
  $_->{'filename'} = $filename;
}

# classify materials by intent
my %materials;
push @{$materials{$_->{'intent'}}}, $_ for @$materials;

# check for missing materials
for my $needed_intent ('source', 'buildconfig', 'buildenv') {
  die("missing materials for '$needed_intent'\n") unless $materials{$needed_intent};
}
die("more than one buildconfig material\n") if @{$materials{'buildconfig'}} != 1;
die("recipefile $recipefile is missing from source\n") unless grep {$_->{'filename'} eq $recipefile} @{$materials{'source'}};

$| = 1;

print "fetching sources\n";
download_materials($materials{'source'}, $dir);

print "fetching build environment\n";
download_materials($materials{'buildenv'}, $dir, '.build.binaries');

if ($materials{'sysroot'}) {
  print "fetching sysroot binaries\n";
  download_materials($materials{'buildenv'}, $dir, '.sysroot.binaries');
}

if ($materials{'repos'}) {
  print "fetching repository binaries\n";
  download_materials($materials{'repos'}, $dir, 'repos');
}

if ($materials{'containers'}) {
  print "fetching containers\n";
  download_materials($materials{'containers'}, $dir, 'containers');
}

print "fetching build config\n";
download_materials($materials{'buildconfig'}, $dir);

my %flags;
if ($predicate_type eq 'https://slsa.dev/provenance/v1') {
  # get preinstall/vminstall/runscripts information from the annotations
  for my $material (@{$materials{'buildenv'}}) {
    my $flags = ($material->{'annotations'} || {})->{'flags'};
    next unless $flags;
    my $fn = $material->{'filename'};
    my $n;
    $n = $1 if $fn =~ /^(.*)\.(?:rpm|deb|pkg\.tar\.gz|pkg\.tar\.xz|pkg\.tar.zst)$/;
    next unless $n;
    push @{$flags{$_}}, $n for split(',', $flags);
  }
} else {
  # parse the config to get preinstall/vminstall/runscripts information
  my $bconf = Build::read_config('noarch', "$dir/.build.config");
  die("cannot expand preinstalls\n") if $bconf->{'expandflags:preinstallexpand'};
  $flags{'preinstall'} = [ Build::get_preinstalls($bconf) ];
  $flags{'vminstall'} = [ Build::get_vminstalls($bconf) ];
  $flags{'runscripts'} = [ Build::get_runscripts($bconf) ];
}

# add all buildenv materials to the rpmlist
my @rpmlist;
for my $material (@{$materials{'buildenv'} || []}) {
  my $fn = $material->{'filename'};
  my $n;
  $n = $1 if $fn =~ /^(.*)\.(?:rpm|deb|pkg\.tar\.gz|pkg\.tar\.xz|pkg\.tar.zst)$/;
  push @rpmlist, "$n $dir/.build.binaries/$fn" if $n;
}
for my $material (@{$materials{'sysroot'} || []}) {
  my $fn = $material->{'filename'};
  my $n;
  $n = $1 if $fn =~ /^(.*)\.(?:rpm|deb|pkg\.tar\.gz|pkg\.tar\.xz|pkg\.tar.zst)$/;
  next unless $n;
  push @rpmlist, "sysroot: $n $dir/.sysroot.binaries/$fn" if $n;
}

for (qw{preinstall vminstall runscripts installonly noinstall}) {
  push @rpmlist, "$_: @{$flags{$_}}" if $flags{$_};
}
writestr("$dir/.build.rpmlist", undef, join("\n", @rpmlist)."\n");

my @params;
if ($parameters) {
  for my $k (sort keys %$parameters) {
    next unless defined $parameters->{$k} && !ref($parameters->{$k});
    push @params, 'release', $parameters->{$k} if $k eq 'release';
    push @params, 'debuginfo', 1 if $k eq 'debuginfo' && $parameters->{$k};
  }
}
push @params, 'recipe', $recipefile;
my $params = '';
while (@params) {
  my ($k, $v) = splice(@params, 0, 2);
  $v =~ s/[\r\n].*\z//s;
  $params .= "$k=$v\n";
}
writestr("$dir/.build.params", undef, $params);
