#!/usr/bin/perl

################################################################
#
# Copyright (c) 2023 SUSE Linux LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################

BEGIN {
  if (!$::ENV{'BUILD_DIR'} && $0 ne '-' && $0 ne '-e' && -e $0 && ! -e '/etc/build.conf') {
    use Cwd ();
    my $p = Cwd::abs_path($0);
    $::ENV{'BUILD_DIR'} = $p if $p =~ s/\/[^\/]+$// && $p ne '/usr/lib/build' && -d "$p/Build";
  }
  unshift @INC, ($::ENV{'BUILD_DIR'} && ! -e '/etc/build.conf' ? $::ENV{'BUILD_DIR'} : '/usr/lib/build');
}

use strict;

use File::Find;
use File::Temp;

use Digest::SHA;
use Digest::MD5;

use Build;
use Build::Options;
use Build::Rpm;
use Build::Deb;
use Build::SimpleJSON;

use Build::SPDX;
use Build::IntrospectGolang;
use Build::IntrospectRust;

my $tool_name = 'obs_build_generate_sbom';
my $tool_version = '1.1';

my $agent_name;

my $buildtime;
my $with_dependencies;

my $config = {};

sub unify {
  my %h = map {$_ => 1} @_;
  return grep(delete($h{$_}), @_);
}

sub urlencode {
  my ($str, $iscgi) = @_;
  if ($iscgi) {
    $str =~ s/([\000-\037<>;\"#\?&\+=%[\177-\377])/sprintf("%%%02X",ord($1))/sge;
    $str =~ tr/ /+/;
  } else {
    $str =~ s/([\000-\040<>;\"#\?&\+=%[\177-\377])/sprintf("%%%02X",ord($1))/sge;
  }
  return $str;
}

sub rfc3339time {
  my ($t) = @_;
  my @gt = gmtime($t || time());
  return sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ", $gt[5] + 1900, $gt[4] + 1, @gt[3,2,1,0];
}

sub sha256file {
  my ($fn) = @_;
  my $ctx = Digest::SHA->new(256);
  eval { $ctx->addfile($fn) };
  die("$fn: $@\n") if $@;
  return $ctx->hexdigest();
}

sub system_chroot {
  my ($root, @args) = @_;
  my $pid = 0;
  if ($args[0] eq 'exec') {
    shift @args;
  } else {
    $pid = fork();
    die("fork: $!\n") unless defined $pid;
  }
  if (!$pid) {
    if ($args[0] eq 'quiet') {
      shift @args;
      open(STDOUT, '>>', '/dev/null');
      open(STDERR, '>>', '/dev/null');
    }
    if ($args[0] eq 'stdout') {
      open(STDOUT, '>', $args[1]) || die("$args[1]: $!\n");
      splice(@args, 0, 2);
    }
    !$root || chroot($root) || die("chroot $root: $!\n");
    exec(@args);
    die("exec $args[0]: $!\n");
  }
  die unless waitpid($pid, 0) == $pid;
  return $?;
}

sub popen_chroot {
  my ($root, @args) = @_;

  my $fd;
  if (!$root) {
    open($fd, '-|', @args) || die("open: $!\n");
    return $fd;
  }
  my $pid = open($fd, '-|');
  die("open: $!\n") unless defined $pid;
  if ($pid == 0) {
    !$root || chroot($root) || die("chroot $root: $!\n");
    exec(@args);
    die("exec $args[0]: $!\n");
  }
  return $fd;
}

sub can_run {
  my ($root, $fname) = @_;
  return 0 if $root && $>;
  return -x "$root$fname";
}

sub systemq {
  my $pid = fork();
  die("fork: $!\n") unless defined $pid;
  if (!$pid) {
    open(STDOUT, '>', '/dev/null') || die("/dev/null: $!\n");
    exec @_;
    die("$_[0]: $!\n");
  }
  waitpid($pid, 0) == $pid || die("waitpid: $!\n");
  exit(1) if $?;
}


##################################################################################################
#
# Container unpacking
#

sub uncompress_container {
  my ($container, $outfile) = @_;
  my @decompressor;
  if ($container =~ /\.tar$/) {
    push @decompressor, 'cat';
  } elsif ($container =~ /\.tar\.gz$/) {
    push @decompressor, 'gunzip';
  } elsif ($container =~ /\.tar\.xz$/) {
    push @decompressor, 'xzdec';
  } else {
    die("$container: unknown container format\n");
  }
  my $pid = fork();
  die("fork: $!\n") unless defined $pid;
  if (!$pid) {
    open(STDIN, '<', $container) || die("$container: $!\n");
    open(STDOUT, '>', $outfile) || die("$outfile: $!\n");
    exec @decompressor;
    die("$decompressor[0]: $!\n");
  }
  waitpid($pid, 0) == $pid || die("waitpid: $!\n");
  exit(1) if $?;
}

sub unpack_container {
  my ($dir, $container, $format) = @_;
  $format ||= 'docker-archive';
  uncompress_container($container, "$dir/cont");
  systemq('skopeo', 'copy', "$format:$dir/cont", "oci:$dir/image:latest");
  unlink("$dir/cont");
  my @rootless;
  push @rootless, '--rootless' if $>;
  systemq('umoci', 'unpack', @rootless, '--image', "$dir/image:latest", "$dir/unpack");
  return "$dir/unpack/rootfs";
}

sub unpack_iso {
  my ($dir, $iso) = @_;
  systemq('7z', "-o$dir/unpack", 'x', $iso);
  return "$dir/unpack";
}


##################################################################################################
#
# Filelist generation and file introspection
#

sub detect_mime {
  my ($filename) = @_;
  my $fd;
  return undef unless open($fd, '<', $filename);
  my $prefix = '';
  if (read($fd, $prefix, 64) >= 8) {
    my $first = unpack('N', $prefix);
    if ($first == 0x7f454c46) {
      my $t = unpack('@16n', $prefix);
      return 'application/x-sharedlib' if $t == 0x0300 || $t == 0x0003;
      return 'application/x-elf';
    }
    if ($first == 0xcafebabe && unpack('@7C', $prefix) < 20) {
      return 'application/x-mach-binary';	# fat macho
    }
    if ($first == 0xfeedface || $first == 0xfeedfacf || $first == 0xcefaedfe || $first == 0xcffaedfe) {
      return 'application/x-mach-binary';
    }
    if (($first & 0xffff0000) == 0x4d5a0000 && length($prefix) >= 64) {
      my $o = unpack('@60V', $prefix);
      my $type = '';
      if (seek($fd, $o, 0) && read($fd, $type, 4) == 4 && unpack('N', $type) == 0x50450000) {
	return 'application/vnd.microsoft.portable-executable';
      }
    }
  }
  close($fd);
  return undef;
}

sub gen_filelist {
  my ($dir) = @_;
  my $fd;
  my $pid = open($fd, '-|');
  die("fork: $!\n") unless defined $pid;
  if (!$pid) {
    chdir($dir) || die("chdir $!\n");
    exec('find', '-print0');
    die("find: $!\n");
  }
  local $/ = "\0";
  my @files = <$fd>;
  chomp @files;
  close($fd) || die("find: $?\n");
  $_ =~ s/^\.\//\// for @files;
  $_ = {'name' => $_} for @files;
  for my $f (@files) {
    if (-l "$dir$f->{'name'}" || ! -f _) {
      $f->{'SKIP'} = 1;
      next;
    }
    my $mime = detect_mime("$dir/$f->{'name'}");
    $f->{'mime'} = $mime if $mime;
    $f->{'sha256sum'} = sha256file("$dir/$f->{'name'}");
  }
  @files = sort {$a->{'name'} cmp $b->{'name'}} @files;
  return \@files;
}

sub add_golang_mod {
  my ($m, $exeinfos) = @_;
  $m = $m->{'rep'} if $m->{'rep'};
  my $g = $exeinfos->{"golang:$m->{'path'}\@\@$m->{'version'}"};
  return $g if $g;
  $g = { 'NAME' => $m->{'path'}, 'VERSION' => $m->{'version'}, 'pkgtype' => 'golang' };
  $exeinfos->{"golang:$m->{'path'}\@\@$m->{'version'}"} = $g;
  return $g;
}

sub introspect_golang_exe {
  my ($fd, $fname, $exeinfos) = @_;
  my $buildinfo = Build::IntrospectGolang::buildinfo($fd);
  return unless $buildinfo && $buildinfo->{'main'};
  my $g = add_golang_mod($buildinfo->{'main'}, $exeinfos);
  push @{$g->{'filenames'}}, $fname;
  my @deps = @{$g->{'deps'} || []};
  if ($buildinfo->{'goversion'}) {
    my $gg = add_golang_mod({'path' => 'stdlib', 'version' => $buildinfo->{'goversion'}}, $exeinfos);
    push @{$gg->{'filenames'}}, $fname;
    push @deps, $gg unless grep {$_ eq $gg} @deps;
  }
  for my $dep (@{$buildinfo->{'deps'} || []}) {
    my $gg = add_golang_mod($dep, $exeinfos);
    push @{$gg->{'filenames'}}, $fname;
    push @deps, $gg unless grep {$_ eq $gg} @deps;
  }
  $g->{'deps'} = \@deps if @deps;
}

sub add_rust_pkg {
  my ($m, $exeinfos) = @_;
  my $g = $exeinfos->{"rust:$m->{'name'}\@\@$m->{'version'}"};
  return $g if $g;
  $g = { 'NAME' => $m->{'name'}, 'VERSION' => $m->{'version'}, 'pkgtype' => 'rust' };
  $exeinfos->{"rust:$m->{'name'}\@\@$m->{'version'}"} = $g;
  return $g;
}

sub introspect_rust_exe {
  my ($fd, $fname, $exeinfos) = @_;
  my $versioninfo = Build::IntrospectRust::versioninfo($fd);
  return unless $versioninfo && $versioninfo->{'packages'};
  for my $p (@{$versioninfo->{'packages'}}) {
    next unless ($p->{'kind'} || 'runtime') eq 'runtime';
    my $gg = add_rust_pkg($p, $exeinfos);
    push @{$gg->{'filenames'}}, $fname;
    my @deps = @{$gg->{'deps'} || []};
    for my $dep (@{$p->{'dependencies'} || []}) {
      my $d = int($dep);
      next if $d < 0 || $d >= @{$versioninfo->{'packages'}};
      my $p2 = $versioninfo->{'packages'}->[$d];
      next unless ($p2->{'kind'} || 'runtime') eq 'runtime';
      my $gg2 = add_rust_pkg($p2, $exeinfos);
      next if $gg2 == $gg;
      push @deps, $gg2 unless grep {$_ eq $gg2} @deps;
    }
    $gg->{'deps'} = \@deps if @deps;
  }
}

sub introspect_filelist {
  my ($dir, $files) = @_;
  return undef unless $files;
  my %exeinfos;
  for my $f (@$files) {
    my $mime = $f->{'mime'};
    next unless $mime && ($mime eq 'application/x-sharedlib' || $mime eq 'application/x-elf');
    my $fd;
    next unless open($fd, '<', "$dir/$f->{'name'}");
    eval { introspect_golang_exe($fd, $f->{'name'}, \%exeinfos) };
    warn($@) if $@;
    eval { introspect_rust_exe($fd, $f->{'name'}, \%exeinfos) };
    warn($@) if $@;
  }
  return [ map {$exeinfos{$_}} sort keys %exeinfos ];
}


##################################################################################################
#
# RPM package database support
#

sub dump_rpmdb {
  my ($root, $outfile) = @_;
  my $dbpath;
  for my $phase (0, 1) {
    if (can_run($root, '/usr/bin/rpmdb')) {
      # check if we have the exportdb option
      if (system_chroot($root, 'quiet', '/usr/bin/rpmdb', '--exportdb', '--version') == 0) {
        if ($dbpath) {
          system_chroot($root, 'stdout', $outfile, '/usr/bin/rpmdb', '--dbpath', $dbpath, '--exportdb');
        } else {
          system_chroot($root, 'stdout', $outfile, '/usr/bin/rpmdb', '--exportdb');
        }
        exit(1) if $?;
        return;
      }
    }
    # try to get the dbpath from the root if we can
    if (!$dbpath && can_run($root, '/usr/bin/rpm')) {
      my $fd = popen_chroot($root, '/usr/bin/rpm', '--eval', '%_dbpath');
      my $path = <$fd>;
      close($fd);
      chomp $path;
      $dbpath = $path if $path && $path =~ /^\//;
    }
    $dbpath ||= '/usr/lib/sysimage/rpm' if -e "$root/usr/lib/sysimage/rpm/Packages" || -e "$root/usr/lib/sysimage/rpm/Package.db";
    $dbpath ||= '/var/lib/rpm';           # guess
    # try to dump with rpmdb_dump
    if (-s "$root$dbpath/Packages" && can_run($root, '/usr/lib/rpm/rpmdb_dump')) {
      my $outfh;
      open($outfh, '>', $outfile) || die("$outfile: $!\n");
      my $fd = popen_chroot($root, '/usr/lib/rpm/rpmdb_dump', "$dbpath/Packages");
      while (<$fd>) {
	next unless /^\s*[0-9a-fA-F]{8}/;
	chomp;
	my $v = <$fd>;
	die("unexpected EOF\n") unless $v;
	chomp $v;
	substr($v, 0, 1, '') while substr($v, 0, 1) eq ' ';
	$v = pack('H*', $v);
	next if length($v) < 16;
	my ($il, $dl) = unpack('NN', $v);
	die("bad header length\n") unless length($v) == 8 + $il * 16 + $dl;
	die("print: $!\n") unless print $outfh pack('H*', '8eade80100000000');
	die("print: $!\n") unless print $outfh $v;
      }
      close($fd) || die("rpmdb_dump: $!\n");
      close($outfh) || die("close: $!\n");
      return;
    }

    last unless $root;
    # try with the system rpm and a dbpath
    $dbpath = "$root$dbpath";
    $root = '';
  }
  die("could not dump rpm database\n");
}

sub read_rpm {
  my ($rpm) = @_;
  my $sigmd5tag = ref($rpm) ? 'SIGMD5' : 'SIGTAG_MD5';
  my @t = (qw{NAME VERSION RELEASE EPOCH ARCH LICENSE SOURCERPM VCS DISTURL FILENAMES URL VENDOR FILEMODES FILEDIGESTS FILEDIGESTALGO}, $sigmd5tag);
  push @t, qw{PROVIDENAME PROVIDEFLAGS PROVIDEVERSION REQUIRENAME REQUIREFLAGS REQUIREVERSION} if $with_dependencies;
  my %r = Build::Rpm::rpmq($rpm, @t);
  delete $r{$_} for qw{BASENAMES DIRNAMES DIRINDEXES};	# save mem
  for (qw{NAME VERSION RELEASE EPOCH ARCH LICENSE SOURCERPM DISTURL URL VENDOR FILEDIGESTALGO}, $sigmd5tag) {
    next unless $r{$_};
    die("bad rpm entry for $_\n") unless ref($r{$_}) eq 'ARRAY' && @{$r{$_}} == 1;
    $r{$_} = $r{$_}->[0];
  }
  for (qw{VCS}) {
    next unless $r{$_};
    die("bad rpm entry for $_\n") unless ref($r{$_}) eq 'ARRAY';
  }
  $r{'SIGMD5'} = delete $r{$sigmd5tag} if $sigmd5tag eq 'SIGTAG_MD5' && exists($r{$sigmd5tag});
  delete $r{'LICENSE'} if $r{'NAME'} eq 'gpg-pubkey' && ($r{'LICENSE'} || '') eq 'pubkey';
  if ($with_dependencies) {
    Build::Rpm::add_flagsvers(\%r, 'PROVIDENAME', 'PROVIDEFLAGS', 'PROVIDEVERSION');
    Build::Rpm::add_flagsvers(\%r, 'REQUIRENAME', 'REQUIREFLAGS', 'REQUIREVERSION');
    $r{'RAW_PROVIDES'} = $r{'PROVIDENAME'} if $r{'PROVIDENAME'};
    $r{'RAW_REQUIRES'} = $r{'REQUIRENAME'} if $r{'REQUIRENAME'};
    delete $r{$_} for qw{PROVIDENAME PROVIDEFLAGS PROVIDEVERSION REQUIRENAME REQUIREFLAGS REQUIREVERSION};
  }
  return \%r;
}

sub read_pkgs_rpmdb {
  my ($rpmhdrs) = @_;
  my $fd;
  open($fd, '<', $rpmhdrs) || die("$rpmhdrs: $!\n");
  my @rpms;
  while (1) {
    my $hdr = '';
    last unless read($fd, $hdr, 16) == 16;
    my ($il, $dl) = unpack('@8NN', $hdr);
    die("bad rpm header\n") unless $il && $dl;
    die("bad rpm header\n") unless read($fd, $hdr, $il * 16 + $dl, 16) == $il * 16 + $dl;
    push @rpms, read_rpm([ $hdr ]);
  }
  close($fd);
  @rpms = sort {$a->{'NAME'} cmp $b->{'NAME'} || $a->{'VERSION'} cmp $b->{'VERSION'} || $a->{'RELEASE'} cmp $b->{'RELEASE'}} @rpms;
  return \@rpms;
}


##################################################################################################
#
# Debian package database support
#

sub parse_debian_copyright_file {
  my ($root, $pkg) = @_;
  my $file = "$root/usr/share/doc/$pkg/copyright";
  local *F;
  return {} unless open(F, '<', $file);
  my $firstline = <F>;
  return {} unless $firstline && $firstline =~ /^Format: https?:\/\/www.debian.org\/doc\/packaging-manuals\/copyright-format\/1.0\//;

  my $crfound = 0;
  my @copyright;
  my @license;
  while(<F>) {
    chomp;
    s/\s+$//;
    if (/^Copyright:\s*(.*)$/) {
      $crfound = 1;
      push @copyright, $1 if $1 ne '';
    } elsif (/^License:\s*(.*)$/) {
      $crfound = 0;
      push @license, $1 if $1 ne '';
    } elsif (/^(Files|Comment|Disclaimer|Source|Upstream-Name|Upstream-Contact):/) {
      $crfound = 0;
    } elsif (/^\s{1,}(.*)$/ and $crfound) {
      push @copyright, $1;
    }
  }
  close F;
  @copyright = unify(@copyright);
  @copyright = grep {!/^(\*No copyright\*|No copyright|none|\*unknown\*|unknown)$/} @copyright;
  @license = unify(@license);
  my %ret;
  $ret{'copyright'} = join('\n ', sort @copyright) if @copyright;
  $ret{'license'} = join(' AND ', sort @license) if @license;
  return \%ret;
}

sub read_deb {
  my ($root, $ctrl) = @_;
  my %res = Build::Deb::control2res($ctrl);
  return undef unless defined($res{'PACKAGE'}) && defined($res{'VERSION'});
  my %data;
  $data{'NAME'} = $res{'PACKAGE'};
  $data{'EVR'} = $res{'VERSION'};
  if ($res{'VERSION'} =~ /^(?:(\d+):)?(.*?)(?:-([^-]*))?$/s) {
    $data{'EPOCH'} = $1 if defined $1;
    $data{'VERSION'} = $2;
    $data{'RELEASE'} = $3 if defined $3;
  }
  $data{'ARCH'} = $res{'ARCHITECTURE'} if defined $res{'ARCHITECTURE'};
  $data{'URL'} = $res{'HOMEPAGE'} if defined $res{'HOMEPAGE'};
  $data{'MAINTAINER'} = $res{'MAINTAINER'} if defined $res{'MAINTAINER'};
  my $license = parse_debian_copyright_file($root, $data{'NAME'});
  $data{'LICENSE'} = $license->{'license'} if defined $license->{'license'};
  $data{'COPYRIGHTTEXT'} = $license->{'copyright'} if defined $license->{'copyright'};
  if ($res{'STATIC-BUILT-USING'}) {
    $data{'BUILT_USING'} = [ split /,\s*/, $res{'STATIC-BUILT-USING'} ];
  } elsif ($res{'BUILT-USING'}) {
    $data{'BUILT_USING'} = [ split /,\s*/, $res{'BUILT-USING'} ];
  }
  if ($with_dependencies) {
    my @provides = split(',\s*', $res{'PROVIDES'} || '');
    push @provides, "$res{'PACKAGE'} (= $res{'VERSION'})";
    my @depends = split(',\s*', $res{'DEPENDS'} || '');
    push @depends, split(',\s*', $res{'PRE-DEPENDS'} || '');
    for (@provides, @depends) {
      s/ \(([^\)]*)\)/ $1/g;
      s/<</</g;
      s/>>/>/g;
    }
    $data{'RAW_PROVIDES'} = \@provides;
    $data{'RAW_REQUIRES'} = \@depends if @depends;
  }
  return \%data;
}

sub read_deb_bu {
  my ($root, $name, $evr) = @_;
  my %data = ( 'NAME' => $name, 'EVR' => $evr );
  if ($evr =~ /^(?:(\d+):)?(.*?)(?:-([^-]*))?$/s) {
    $data{'EPOCH'} = $1 if defined $1;
    $data{'VERSION'} = $2;
    $data{'RELEASE'} = $3 if defined $3;
  }
  my $license = parse_debian_copyright_file($root, $name);
  $data{'LICENSE'} = $license->{'license'} if defined $license->{'license'};
  $data{'COPYRIGHTTEXT'} = $license->{'copyright'} if defined $license->{'copyright'};
  return \%data;
}

sub read_pkgs_deb {
  my ($root) = @_;

  my $vendorstring = Build::Rpm::expandmacros($config, '%?vendor');
  my @pkgs;
  local *F;
  my %seen_pkg;
  my @bupkgs;
  if (open(F, '<', "$root/var/lib/dpkg/status")) {
    my $ctrl = '';
    while(<F>) {
      if ($_ eq "\n") {
	my $data = read_deb($root, $ctrl);
	if ($data) {
	  $data->{'VENDOR'} = $vendorstring if $vendorstring;
	  push @pkgs, $data;
	  $seen_pkg{"$data->{'NAME'}-$data->{'EVR'}"} = 1;
	  push @bupkgs, @{$data->{'BUILT_USING'}} if $data->{'BUILT_USING'};
	}
        $ctrl = '';
        next;
      }
      $ctrl .= $_;
    }
    close F;
  }
  # create stubs for missing BUILT_USING packages
  for my $bd (@bupkgs) {
    next unless $bd =~ /(.+)\s+\(\s*=\s*(.+)\s*\)/;
    next if $seen_pkg{"$1-$2"};
    my $data = read_deb_bu($root, $1, $2);
    if ($data) {
      $data->{'VENDOR'} = $vendorstring if $vendorstring;
      push @pkgs, $data;
      $seen_pkg{"$data->{'NAME'}-$data->{'EVR'}"} = 1;
    }
  }
  return \@pkgs;
}


##################################################################################################
#
# Product support
#

sub read_product_repository {
  my ($fn, $dirprefix) = @_;

  require Build::Rpmmd;
  my @d = @{Build::Rpmmd::parse_repomd($fn)};
  my %d = map {$_->{'type'} => $_} @d;
  my $primary = $d{'primary'};
  return unless $primary && $primary->{'checksum'};
  my $checksum = $primary->{'checksum'};
  $checksum =~ s/.*://;
  my $pkg = { 'NAME' => 'repository', 'VERSION' => $checksum, 'primaryPackagePurpose' => 'install', 'pkgtype' => 'rpmmd', 'skip_purl' => 1 };
  if ($fn =~ s/\Q$dirprefix/\//) {
    $fn =~ s/repomd.xml//;
    my @files = map {$_->{'location'}} grep {$_->{'location'}} @d;
    push @files, "repodata/repomd.xml";
    @files = unify(sort(map {"$fn$_"} grep {s/^repodata\///} @files));
    $pkg->{'FILENAMES'} = \@files if @files;
  }
  return $pkg;
}

sub read_pkgs_from_product_directory {
  my ($dir) = @_;

  my @rpms;
  my @repos;
  my $dirprefix = $dir eq '/' ? $dir : "$dir/";
  my $addfile = sub {
    my $fn = $File::Find::name;
    push @rpms, read_rpm($fn) if $fn =~ /\.rpm$/;
    push @repos, read_product_repository($fn, $dirprefix) if $fn =~ /\/repomd\.xml$/;
  };
  die("product directory is missing: $dir\n") unless -d $dir;
  find({'wanted' => $addfile, 'no_chdir' => 1, 'preprocess' => sub {sort(@_)} }, $dir);
  # make sure that the packages are unique
  my %seen;
  for my $r (splice @rpms) {
    my $sigmd5 = $r->{'SIGMD5'};
    push @rpms, $r unless $sigmd5 && $seen{$sigmd5}++;;
  }
  push @rpms, grep {$_} @repos;
  return \@rpms;
}

sub read_pkgs_from_rpmmd {
  my ($primaryfile) = @_;

  require Build::Rpmmd;
  my $fh;
  if ($primaryfile =~ /\.zck$/) {
    open($fh, '-|', 'unzck', '-c', $primaryfile) || die("$primaryfile: $!\n");
  } elsif ($primaryfile =~ /\.zst$/) {
    open($fh, '-|', 'zstd', '-dc', $primaryfile) || die("$primaryfile: $!\n");
  } elsif ($primaryfile =~ /\.gz$/) {
    open($fh, '-|', 'gunzip', '-dc', $primaryfile) || die("$primaryfile: $!\n");
  } else {
    open($fh, '<', $primaryfile) || die("$primaryfile: $!\n");
  }
  my @rpms;
  for my $pkg (@{Build::Rpmmd::parse($fh, undef, 'withlicense' => 1, 'withchecksum' => 1, 'withvendor' => 1, 'withurl' => 1)}) {
    my $r = {};
    for (qw{name epoch version release arch url vendor sourcerpm license checksum}) {
      $r->{uc($_)} = $pkg->{$_} if defined $pkg->{$_};
    }
    push @rpms, $r;
  }
  close($fh) || die("close: $primaryfile: $!\n");
  return \@rpms;
}


##################################################################################################
#
# Dependency solving support
#

# this returns "potential" matches, i.e. AND is handled like OR, the IF part is ignored
sub _handle_rich_req {
  my ($config, $r) = @_;
  if ($r->[0] == 0) {
    my @rq = Build::whatprovides($config, $r->[1]);
    push @rq, @{$config->{'f2p'}->{$r->[1]} || []} if $config->{'f2p'} && $r->[1] =~ /^\//;
    return @rq;
  }
  if ($r->[0] == 1 || $r->[0] == 2) {
    return (_handle_rich_req($config, $r->[1]), _handle_rich_req($config, $r->[2]));
  }
  if ($r->[0] == 3 || $r->[0] == 4) {
    my @rq = _handle_rich_req($config, $r->[1]);
    push @rq, _handle_rich_req($config, $r->[3]) if @$r == 4;
    return @rq;
  }
  if ($r->[0] == 6) {
    my %s = map {$_ => 1} _handle_rich_req($config, $r->[2]);
    return grep {$s{$_}} _handle_rich_req($config, $r->[1]);
  }
  if ($r->[0] == 7) {
    my %s = map {$_ => 1} _handle_rich_req($config, $r->[2]);
    return grep {!$s{$_}} _handle_rich_req($config, $r->[1]);
  }
  return ()
}

sub setup_provides_whatprovides {
  my ($config, @pprov) = @_;
  my %provides;
  my %whatprovides;
  while (@pprov) {
    my ($p, $pp) = splice(@pprov, 0, 2);
    $provides{$p} = $pp if $pp;
    my @pp = @{$pp || []};
    s/[ <=>].*// for @pp;
    push @{$whatprovides{$_}}, $p for Build::unify(@pp);
  }
  $config->{'providesh'} = \%provides;
  $config->{'whatprovidesh'} = \%whatprovides;
}

sub generate_pkg_dependencies {
  my ($pkgs, $pkgtype) = @_;
  return if $pkgtype ne 'rpm' && $pkgtype ne 'deb';
  my $config = { 'binarytype' => $pkgtype };
  setup_provides_whatprovides($config, map {$_ => ($_->{'RAW_PROVIDES'} || [])} @$pkgs);
  my %f2p;
  if ($pkgtype eq 'rpm') {
    for my $p (@$pkgs) {
      push @{$f2p{$_}}, $p for @{$p->{'FILENAMES'} || []};
    }
  }
  $config->{'f2p'} = \%f2p if %f2p;
  for my $p (@$pkgs) {
    my @rp;
    for my $req (@{$p->{'RAW_REQUIRES'} || []}) {
      if ($pkgtype eq 'rpm') {
        if ($req =~ /^\(/) {
	  my $r = Build::Rpm::parse_rich_dep($req);
	  push @rp, _handle_rich_req($config, $r) if $r;
	  next;
        }
        push @rp, @{$f2p{$req} || []} if $req =~ /^\//;
      }
      push @rp, Build::whatprovides($config, $req);
    }
    @rp = grep {$_ != $p} Build::unify(@rp);
    push @{$p->{'deps'}}, @rp if @rp;
  }
}

##################################################################################################
#
# Small helpers
#

sub read_dist {
  my ($dir) = @_;
  my %dist;
  my $fd;
  if (open($fd, '<', "$dir/etc/os-release") || open($fd, '<', "$dir/usr/lib/os-release")) {
    while(<$fd>) {
      chomp;
      next unless /\s*(\S+)=(.*)/;
      my $k = lc($1);
      my $v = $2;
      $v =~ s/\s+$//;
      $v =~ s/^\"(.*)\"$/$1/;
      if ($k eq 'id_like') {
        push @{$dist{$k}}, $v;
      } else {
        $dist{$k} = $v;
      }
    }
    close($fd);
  }
  return %dist ? \%dist : undef;
}

sub pkgtype_from_dist {
  my ($dist) = @_;
  return 'rpm' unless $dist && $dist->{'id'};
  return 'deb' if $dist->{'id'} eq 'debian';
  return 'deb' if grep { $_ eq "debian" } @{$dist->{'id_like'} || []};
  return 'rpm';
}

sub gen_purl {
  my ($p, $distro, $pkgtype) = @_;
  my $name = $p->{'NAME'};
  my $vr = $p->{'VERSION'};
  $vr =~ s/^go// if $pkgtype eq 'golang' && $name eq 'stdlib';
  my $purltype = $pkgtype eq 'rust' ? 'cargo' : $pkgtype;
  my $subpath;
  if ($pkgtype eq 'golang' && $name =~ /\A([^\/]+\/[^\/]+\/[^\/]+)\/(.+)/s) {
    $name = $1;
    $subpath = $2;
  }
  $vr = "$vr-$p->{'RELEASE'}" if defined $p->{'RELEASE'};
  my $vendor = '';
  if ($p->{'VENDOR'}) {
    $vendor = lc($p->{'VENDOR'});
    $vendor =~ s/obs:\/\///; # third party OBS builds
    $vendor =~ s/\ .*//;     # eg. SUSE LLC...
    $vendor =~ s/\/?$/\//;
  }
  my $purlurl = "pkg:".urlencode("$purltype/$vendor$name\@$vr").'?';
  $purlurl .= '&epoch='.urlencode($p->{'EPOCH'}) if $p->{'EPOCH'};
  $purlurl .= '&arch='.urlencode($p->{'ARCH'}) if $p->{'ARCH'};
  $purlurl .= '&upstream='.urlencode($p->{'SOURCERPM'}) if $p->{'SOURCERPM'};
  $purlurl .= '&distro='.urlencode($distro) if $distro;
  $purlurl =~ s/\?\&/\?/;
  $purlurl =~ s/\?$//;
  $purlurl .= '#'.urlencode($subpath) if defined $subpath;
  return $purlurl;
}

sub gen_uuid {
  my $uuid = pack('H*', '1e9d579964de4594a4e835719a1c259f');	# uuid ns
  $uuid = substr(Digest::SHA::sha1($uuid . Build::SimpleJSON::unparse($_[0], 'keepspecial' => 1)), 0, 16);
  substr($uuid, 6, 1, pack('C', unpack('@6C', $uuid) & 0x0f | 0x50));
  substr($uuid, 8, 1, pack('C', unpack('@8C', $uuid) & 0x3f | 0x80));
  return join('-', unpack("H8H4H4H4H12", $uuid));
}

sub gen_pkg_id {
  my ($p) = @_;
  if ($p->{'SIGMD5'}) {
    return unpack('H*', $p->{'SIGMD5'});
  } elsif ($p->{'CHECKSUM'}) {
    my $id = $p->{'CHECKSUM'};
    $id =~ s/.*://;
    return substr($id, 0, 32);
  }
  my %p = %$p;
  delete $p{'RELATION'};
  delete $p{'deps'};
  return Digest::MD5::md5_hex(Build::SimpleJSON::unparse(\%p));
}

sub gen_source_info {
  my ($p, $pkgtype) = @_;
  return $p->{'sourceInfo'} if $p->{'sourceInfo'};
  my $si;
  $pkgtype = $p->{'pkgtype'} if $p->{'pkgtype'};
  if ($pkgtype eq 'deb') {
    $si = 'acquired package info from DPKG DB';
  } elsif ($pkgtype eq 'rpm') {
    $si = 'acquired package info from RPM DB';
  } elsif ($pkgtype eq 'golang') {
    $si = 'acquired package info from go module information';
  } elsif ($pkgtype eq 'rust') {
    $si = 'acquired package info from rust cargo manifest';
  } elsif ($pkgtype eq 'rpmmd') {
    $si = 'acquired package info from repomd.xml';
  }
  if (($pkgtype eq 'golang' || $pkgtype eq 'rust') && @{$p->{'filenames'} || []}) {
    $si .= ': '.join(', ', @{$p->{'filenames'}});
  }
  return $si;
}


##################################################################################################
#
# CycloneDX support
#

my $cyclonedx_json_template_supplier = {
  '_order' => [ qw{bom-ref name address url contact} ],
  'contact' => { '_order' => [ qw{name email} ] },
};

my $cyclonedx_json_template_component = {
  '_order' => [ qw{bom-ref type supplier manufacturer authors name version description cpe purl externalReferences properties } ],
  'externalReferences' => { '_order' => [ qw{url comment type} ] },
  'supplier' => $cyclonedx_json_template_supplier,
  'manufacturer' => $cyclonedx_json_template_supplier,
};

my $cyclonedx_json_template = {
  '_order' => [ qw{bomFormat specVersion serialNumber version metadata components services externalReferences dependencies compositions vulnerabilities signature} ],
  'version' => 'number',
  'metadata' => {
    '_order' => [ qw{timestamp tools manufacturer authors component supplier} ],
    'tools' => { '_order' => [ qw{vendor name version } ] },
    'component' => $cyclonedx_json_template_component,
    'supplier' => $cyclonedx_json_template_supplier,
    'manufacturer' => $cyclonedx_json_template_supplier,
  },
  'components' => $cyclonedx_json_template_component,
  'dependencies' => { '_order' => [ qw{ref dependsOn} ] }
};

sub cyclonedx_encode_license {
  my ($license) = @_;
  my $nlicense = Build::SPDX::normalize_license($license);
  if (!defined($nlicense)) {
    # non-standard (sub-)license found, normalize and encode as name
    my $dummy_cb = sub { $_[0] };
    $nlicense = Build::SPDX::normalize_license($license, 'unknown_license_cb' => $dummy_cb, 'unknown_exception_cb' => $dummy_cb);
    return { 'license' => {'name' => $nlicense } };
  }
  return { 'expression' => $nlicense } if $nlicense =~ /\s+/;
  return { 'license' => {'id' => $nlicense } };
}

sub cyclonedx_encode_pkg {
  my ($p, $distro, $pkgtype) = @_;
  $pkgtype = $p->{'pkgtype'} if $p->{'pkgtype'};
  my $vr = $p->{'VERSION'};
  $vr = "$vr-$p->{'RELEASE'}" if defined $p->{'RELEASE'};
  my $cyc = { 'type' => 'library', 'name' => $p->{'NAME'}, 'version' => $vr };
  $cyc->{'publisher'} = $p->{'VENDOR'} if $p->{'VENDOR'};
  push @{$cyc->{'licenses'}}, cyclonedx_encode_license($p->{'LICENSE'}) if $p->{'LICENSE'};
  my $purlurl = $p->{'skip_purl'} ? undef : gen_purl($p, $distro, $pkgtype);
  $cyc->{'purl'} = $purlurl if $purlurl;
  push @{$cyc->{'externalReferences'}}, { 'url' => $p->{'DISTURL'}, 'type' => 'build-meta' } if $p->{'DISTURL'};
  push @{$cyc->{'externalReferences'}}, { 'url' => $_, 'type' => 'vcs' } for @{$p->{'VCS'} || []};
  if (!$p->{'cyc_id'}) {
    $p->{'cyc_id'} = "$p->{'NAME'}-" . gen_pkg_id($p);
    $p->{'cyc_id'} =~ s/[^a-zA-Z0-9\.\-]/-/g;
    $p->{'cyc_id'} = "pkg:$pkgtype/$p->{'cyc_id'}";
  }
  $cyc->{'bom-ref'} = $p->{'cyc_id'};
  return $cyc;
}

sub cyclonedx_encode_dist {
  my ($dist) = @_;
  my $cyc = {
    'type' => 'operating-system',
    'name' => $dist->{'id'},
  };
  $cyc->{'version'} = $dist->{'version_id'} if defined($dist->{'version_id'}) && $dist->{'version_id'} ne '';
  $cyc->{'description'} = $dist->{'pretty_name'} if $dist->{'pretty_name'};
  push @{$cyc->{'externalReferences'}}, { 'url' => $dist->{'bug_report_url'}, 'type' => 'issue-tracker' } if $dist->{'bug_report_url'};
  push @{$cyc->{'externalReferences'}}, { 'url' => $dist->{'home_url'}, 'type' => 'website' } if $dist->{'home_url'};
  return $cyc;
}

sub cyclonedx_encode_relations {
  my ($p) = @_;
  return unless $p->{'cyc_id'};
  my @r = grep {$_->[0]->{'cyc_id'} && $_->[1] eq 'DEPENDS_ON'} @{$p->{'RELATION'} || []};
  return unless @r;
  my $cyc = {
    'ref' => $p->{'cyc_id'},
    'dependsOn' => [ map {$_->[0]->{'cyc_id'}} @r ],
  };
  return $cyc;
}

sub cyclonedx_encode_header {
  my ($subjectname, $type, $rootpkg) = @_;
  $type = 'library' if $type eq 'install';
  my $cyc = {
    'bomFormat' => 'CycloneDX',
    'specVersion' => '1.5',
    'version' => 1,
    'metadata' => {
      'timestamp' => rfc3339time($buildtime),
      'tools' => [ {'name' => $tool_name, 'version' => $tool_version } ],
      'component' => { 'bom-ref' => 'root', 'type' => ($type || 'application'), 'name' => $subjectname },
    },
  };
  push @{$cyc->{'metadata'}->{'component'}->{'externalReferences'}}, { 'url' => $rootpkg->{'DISTURL'}, 'type' => 'build-meta' } if $rootpkg->{'DISTURL'};
  push @{$cyc->{'metadata'}->{'component'}->{'externalReferences'}}, { 'url' => $_, 'type' => 'vcs' } for @{$rootpkg->{'VCS'} || []};
  return $cyc;
}

sub cyclonedx_finish {
  my ($doc, $subjectname) = @_;
  $doc->{'serialNumber'} = 'urn:uuid:'.gen_uuid($doc);
  return $doc;
}

##################################################################################################
#
# SPDX support
#

my $spdx_json_template = {
  '_order' => [ qw{spdxVersion dataLicense SPDXID name documentNamespace creationInfo packages files hasExtractedLicensingInfos relationships} ],
  'creationInfo' => {
    '_order' => [ qw{created creators licenseListVersion} ],
  },
  'packages' => {
    '_order' => [ qw{name SPDXID versionInfo supplier originator downloadLocation sourceInfo homepage licenseConcluded licenseDeclared copyrightText externalRefs} ],
    'externalRefs' => {
      '_order' => [ qw{referenceCategory referenceType referenceLocator} ],
    },
  },
  'files' => {
    '_order' => [ qw{fileName SPDXID fileTypes checksums licenseConcluded licenseInfoInFiles copyrightText comment} ],
  },
  'hasExtractedLicensingInfos' => {
    '_order' => [ qw{licenseId extractedText} ],
  },
  'relationships' => {
    '_order' => [ qw{spdxElementId relatedSpdxElement relationshipType} ],
  },
};

sub spdx_encode_unknown_license {
  my ($name, $unknown_spdx_licenses) = @_;
  my $l = $unknown_spdx_licenses->{lc($name)};
  return $l->{'name'} if $l;
  $l = {'name' => $name};
  $l->{'name'} = "LicenseRef-".lc($name);
  $l->{'name'} =~ s/\+$/-plus/;
  $l->{'name'} =~ s/[^a-zA-Z0-9\.\-]/-/g;
  $l->{'text'} = $name;
  $unknown_spdx_licenses->{lc($name)} = $l;
  return $l->{'name'};
}

sub spdx_encode_extracted_license {
  my ($l) = @_;
  my $spdx = { 'licenseId' => $l->{'name'}, 'extractedText' => $l->{'text'} };
  return $spdx;
}

sub spdx_encode_pkg {
  my ($p, $distro, $pkgtype, $unknown_spdx_licenses) = @_;
  my $vr = $p->{'VERSION'};
  $vr = "$vr-$p->{'RELEASE'}" if defined $p->{'RELEASE'};
  my $evr = $vr;
  $evr = "$p->{'EPOCH'}:$evr" if $p->{'EPOCH'};
  my $spdx = { 'name' => $p->{'NAME'} };
  $spdx->{'versionInfo'} = $evr if defined $evr;
  $spdx->{'supplier'} = 'NOASSERTION';
  if ($p->{'VENDOR'}) {
    $spdx->{'originator'} = "Organization: $p->{'VENDOR'}";
    $spdx->{'supplier'} = $spdx->{'originator'}; # same as originator OBS-247
  }
  $spdx->{'downloadLocation'} = 'NOASSERTION';

  $pkgtype = $p->{'pkgtype'} if $p->{'pkgtype'};
  my $si = gen_source_info($p, $pkgtype);
  $spdx->{'sourceInfo'} = $si if $si;

  $spdx->{'licenseConcluded'} = 'NOASSERTION';
  $spdx->{'licenseDeclared'} = 'NOASSERTION';
  my $license = $p->{'LICENSE'};
  if ($license) {
    my $unknown_license_cb = sub { spdx_encode_unknown_license($_[0], $unknown_spdx_licenses) };
    $license = Build::SPDX::normalize_license($license , 'unknown_license_cb' => $unknown_license_cb);
    $spdx->{'licenseConcluded'} = $license;
    $spdx->{'licenseDeclared'} = $license unless ($config->{'buildflags:spdx-declared-license'} || '') eq 'NOASSERTION';
  }
  $spdx->{'copyrightText'} = $p->{'COPYRIGHTTEXT'} ? $p->{'COPYRIGHTTEXT'} : 'NOASSERTION';
  $spdx->{'homepage'} = $p->{'URL'} if $p->{'URL'};

  my $purlurl = $p->{'skip_purl'} ? undef : gen_purl($p, $distro, $pkgtype);
  push @{$spdx->{'externalRefs'}}, { 'referenceCategory' => 'PACKAGE-MANAGER', 'referenceType' => 'purl', 'referenceLocator' => $purlurl } if $purlurl;
  push @{$spdx->{'externalRefs'}}, { 'referenceCategory' => 'OTHER', 'referenceType' => 'vcs', 'referenceLocator' => $_ } for @{$p->{'VCS'} || []};
  push @{$spdx->{'externalRefs'}}, { 'referenceCategory' => 'OTHER', 'referenceType' => 'obs-disturl', 'referenceLocator' => $p->{'DISTURL'} } if $p->{'DISTURL'};

  $spdx->{'primaryPackagePurpose'} = uc($p->{'primaryPackagePurpose'}) if $p->{'primaryPackagePurpose'};

  if (!$p->{'spdx_id'}) {
    my $spdxtype = "Package-$pkgtype";
    $spdxtype = "Package-go-module" if $pkgtype eq 'golang';
    $spdxtype = "Package-rust-crate" if $pkgtype eq 'rust';
    $p->{'spdx_id'} = "SPDXRef-$spdxtype-$p->{'NAME'}-" . gen_pkg_id($p);
    $p->{'spdx_id'} =~ s/[^a-zA-Z0-9\.\-]/-/g;
  }
  $spdx->{'SPDXID'} = $p->{'spdx_id'};
  return $spdx;
}

sub spdx_encode_file {
  my ($f) = @_;
  my $spdx = {
    'fileName' => $f->{'name'},
    'licenseConcluded' => 'NOASSERTION',
    'licenseInfoInFiles' => [ 'NOASSERTION' ],
    'copyrightText' => '',
  };
  my $mime = $f->{'mime'};
  if ($mime && ($mime eq 'application/x-sharedlib' || $mime eq 'application/x-elf' || $mime eq 'application/x-mach-binary' || $mime eq 'application/vnd.microsoft.portable-executable')) {
    push @{$spdx->{'fileTypes'}}, 'BINARY';
  }
  my @chks;
  push @chks, { 'algorithm' => 'SHA256', 'checksumValue' => $f->{'sha256sum'} } if $f->{'sha256sum'};
  $spdx->{'checksums'} = \@chks if @chks;
  if (!$f->{'spdx_id'}) {
    my $fn = $f->{'name'};
    $fn =~ s/\A\/+//s;
    $fn =~ s/\/+\z//s;
    if (length($fn) > 42) {
      1 while length($fn) > 42 && $fn =~ s/.*?\///;
      $fn = "...".substr($fn, -42);
    }
    $f->{'spdx_id'} = "SPDXRef-File-$fn-".Digest::MD5::md5_hex($f->{'name'}.($f->{'sha256sum'} || ''));
    $f->{'spdx_id'} =~ s/[^a-zA-Z0-9\.\-]/-/g;
  }
  $spdx->{'SPDXID'} = $f->{'spdx_id'};
  return $spdx;
}

sub spdx_encode_one_relation {
  my ($p, $op, $type) = @_;
  return unless $p->{'spdx_id'} && $op->{'spdx_id'};
  return if $type eq 'DEPENDS_ON';
  my $spdx = { 'spdxElementId' => $p->{'spdx_id'}, 'relatedSpdxElement' => $op->{'spdx_id'}, 'relationshipType' => $type };
  if ($type ne 'DEPENDENCY_OF' && $type ne 'DESCRIBES' && $type ne 'CONTAINS') {
    $spdx->{'relationshipType'} = 'OTHER';
    $spdx->{'comment'} = $type;
  }
  return $spdx;
}

sub spdx_encode_relations {
  my ($p) = @_;
  return unless $p->{'spdx_id'};
  return map {spdx_encode_one_relation($p, $_->[0], $_->[1])} @{$p->{'RELATION'} || []};
}

sub spdx_encode_header {
  my ($subjectname, $type, $rootpkg) = @_;
  my $spdx = {
    'spdxVersion' => 'SPDX-2.3',
    'dataLicense' => 'CC0-1.0',
    'SPDXID' => 'SPDXRef-DOCUMENT',
    'name' => $subjectname,
  };
  my $creationinfo = {
    'created' => rfc3339time($buildtime),
    'creators' => [ "Tool: $tool_name-$tool_version" ],
    'licenseListVersion' => $Build::SPDX::licenseListVersion,
  };
  $spdx->{'creationInfo'} = $creationinfo;
  my $rootspdx = spdx_encode_pkg($rootpkg, undef, '', {});
  $spdx->{'packages'} = [ $rootspdx ];
  return $spdx;
}

sub spdx_encode_dist {
  my ($dist) = @_;

  my $distp = {
    NAME => $dist->{id},
    VERSION => $dist->{version_id},
    spdx_id => sprintf('SPDXRef-OperatingSystem-%s', gen_pkg_id($dist)),
    primaryPackagePurpose => 'OPERATING-SYSTEM',
    skip_purl => 1
  };
  return spdx_encode_pkg($distp, undef, '', {});
}

sub spdx_finish {
  my ($doc, $subjectname) = @_;
  $doc->{'documentNamespace'} = 'http://open-build-service.org/spdx/'.urlencode($subjectname).'-'.gen_uuid($doc);
  return $doc;
}

##################################################################################################
#
# SPDX3 support
#

my $spdx3_no_assertion_license = 'https://spdx.org/rdf/3.0.1/terms/ExpandedLicensing/NoAssertionLicense';

my $spdx3_json_template = {
  '_order' => [ qw{@context @graph} ],
  '@graph' => {
    '_order' => [ qw{@id spdxId type name
relationshipType completeness to from
specVersion createdBy createdUsing created
dataLicense rootElement description
expandedlicensing_member expandedlicensing_subjectLicense
simplelicensing_licenseText
software_copyrightText suppliedBy software_downloadLocation software_packageVersion software_homePage software_sourceInfo externalRef software_packageUrl software_primaryPurpose
originatedBy
comment creationInfo} ],
    'externalRef' => {
      '_order' => [ qw{type locator externalRefType} ],
    }
  },
};

sub spdx3_gen_spdxid {
  my ($doc, $element) = @_;
  $element->{'spdxId'} = 'SPDXRef-gnrtd'.$doc->[0]->[0]++;
  push @{$doc->[0]->[1]}, \$element->{'spdxId'};
}

sub spdx3_encode_element {
  my ($doc, $type, @elements) = @_;
  my $spdx = { 'type' => $type, 'creationInfo' => '_:creationInfo_0', @elements };
  if ($spdx->{'spdxId'}) {
    push @{$doc->[0]->[1]}, \$spdx->{'spdxId'} unless $spdx->{'spdxId'} =~ /^https?:\/\//;
  } elsif (defined $spdx->{'spdxId'}) {
    delete $spdx->{'spdxId'};
  } else {
    spdx3_gen_spdxid($doc, $spdx);
  }
  return $spdx;
}

sub spdx3_encode_organization {
  my ($doc, $organization) = @_;
  return $doc->[0]->[2]->{$organization} if $doc->[0]->[2]->{$organization};
  my $spdx = spdx3_encode_element($doc, 'Organization', 'name' => $organization);
  push @$doc, $spdx;
  $doc->[0]->[2]->{$organization} = $spdx->{'spdxId'};
  return $spdx->{'spdxId'};
}

sub spdx3_encode_custom_license {
  my ($doc, $license) = @_;
  my $idstr = $license;
  return $doc->[0]->[3]->{$idstr} if $doc->[0]->[3]->{$idstr};
  my $spdx = spdx3_encode_element($doc, 'expandedlicensing_CustomLicense', 'simplelicensing_licenseText' => $license, 'name' => '', 'comment' => '');
  push @$doc, $spdx;
  $doc->[0]->[3]->{$idstr} = $spdx->{'spdxId'};
  return $spdx->{'spdxId'};
}

sub spdx3_encode_custom_license_addition {
  my ($doc, $addition) = @_;
  my $idstr = "WITH $addition";
  return $doc->[0]->[3]->{$idstr} if $doc->[0]->[3]->{$idstr};
  my $spdx = spdx3_encode_element($doc, 'expandedlicensing_CustomLicenseAddition', 'expandedlicensing_additionText' => $addition, 'name' => '', 'comment' => '');
  push @$doc, $spdx;
  $doc->[0]->[3]->{$idstr} = $spdx->{'spdxId'};
  return $spdx->{'spdxId'};
}

sub spdx3_encode_orlater_license {
  my ($doc, $spdxid) = @_;
  my $idstr = "$spdxid PLUS";
  return $doc->[0]->[3]->{$idstr} if $doc->[0]->[3]->{$idstr};
  my $spdx = spdx3_encode_element($doc, 'expandedlicensing_OrLaterOperator', 'expandedlicensing_subjectLicense' => $spdxid);
  push @{$doc->[0]->[1]}, \$spdx->{'expandedlicensing_subjectLicense'} unless $spdxid =~ /^https?:\/\//;
  push @$doc, $spdx;
  $doc->[0]->[3]->{$idstr} = $spdx->{'spdxId'};
  return $spdx->{'spdxId'};
}

sub spdx3_encode_withaddition_license {
  my ($doc, $spdxid, $exception) = @_;
  my $idstr = "$spdxid WITH $exception";
  return $doc->[0]->[3]->{$idstr} if $doc->[0]->[3]->{$idstr};
  my $spdx = spdx3_encode_element($doc, 'expandedlicensing_WithAdditionOperator', 'expandedlicensing_subjectExtendableLicense' => $spdxid, 'expandedlicensing_subjectAddition' => $exception);
  push @{$doc->[0]->[1]}, \$spdx->{'expandedlicensing_subjectExtendableLicense'} unless $spdxid =~ /^https?:\/\//;
  push @{$doc->[0]->[1]}, \$spdx->{'expandedlicensing_subjectAddition'} unless $exception =~ /^https?:\/\//;
  push @$doc, $spdx;
  $doc->[0]->[3]->{$idstr} = $spdx->{'spdxId'};
  return $spdx->{'spdxId'};
}

sub spdx3_encode_junctive_license {
  my ($doc, $op, @members) = @_;
  die("spdx3_encode_junctive_license: bad op $op\n") unless $op eq 'AND' || $op eq 'OR';
  my $idstr = join(" $op ", @members);
  return $doc->[0]->[3]->{$idstr} if $doc->[0]->[3]->{$idstr};
  my $type = $op eq 'AND' ? 'expandedlicensing_ConjunctiveLicenseSet' : 'expandedlicensing_DisjunctiveLicenseSet';
  my $spdx = spdx3_encode_element($doc, $type, 'expandedlicensing_member' => [ @members ]);
  for (@{$spdx->{'expandedlicensing_member'}}) {
    push @{$doc->[0]->[1]}, \$_ unless /^https?:\/\//;
  }
  push @$doc, $spdx;
  $doc->[0]->[3]->{$idstr} = $spdx->{'spdxId'};
  return $spdx->{'spdxId'};
}

sub spdx3_encode_tokenized_license {
  my ($doc, $n) = @_;
  my @n = ('START', @$n);
  my $lastop;
  my @members;
  while (@n) {
    my ($op, $t) = splice(@n, 0, 2);
    return undef if $op ne 'START' && !@members;
    $lastop = $op if $op eq 'AND' || $op eq 'OR';
    my $id;
    if ($op eq 'PLUS') {
      $id = spdx3_encode_orlater_license($doc, pop(@members));
    } elsif ($op eq 'WITH') {
      my $nt = Build::SPDX::canonicalize_known_license_exception($t);
      my $id = $nt ? "http://spdx.org/licenses/$nt" : spdx3_encode_custom_license_addition($doc, $t);
      $id = spdx3_encode_withaddition_license($doc, pop(@members), $id) if defined $id;
    } elsif (ref $t) {
      $id = spdx3_encode_tokenized_license($doc, $t);
    } else {
      my $nt = Build::SPDX::canonicalize_known_license($t);
      $id = $nt ? "http://spdx.org/licenses/$nt" : spdx3_encode_custom_license($doc, $t);
    }
    return undef unless defined $id;
    push @members, $id;
  }
  return $members[0] if @members == 1;
  return undef unless @members > 1 && $lastop;
  return spdx3_encode_junctive_license($doc, $lastop, @members);
}

sub spdx3_encode_license {
  my ($doc, $license) = @_;
  $license = Build::SPDX::preprocess_license($license);
  my $n = Build::SPDX::tokenize_license($license, 'no_mixed_junction' => 1);
  my $id = $n ? spdx3_encode_tokenized_license($doc, $n) : undef;
  return $id || spdx3_encode_custom_license($doc, $license);
}

sub spdx3_encode_one_license_relation {
  my ($doc, $from, $to, $type) = @_;
  my $rel = spdx3_encode_element($doc, 'Relationship', 'relationshipType' => $type, 'to' => [ $to ], 'from' => $from);
  push @{$doc->[0]->[1]}, \$rel->{'from'};
  push @{$doc->[0]->[1]}, \$rel->{'to'}->[0] unless $rel->{'to'}->[0] =~ /^https?:\/\//;
  push @$doc, $rel;
  return $rel->{'spdxId'};
}

sub spdx3_encode_pkg {
  my ($doc, $p, $distro, $pkgtype) = @_;
  my $vr = $p->{'VERSION'};
  $vr = "$vr-$p->{'RELEASE'}" if defined $p->{'RELEASE'};
  my $evr = $vr;
  $evr = "$p->{'EPOCH'}:$evr" if $p->{'EPOCH'};

  my $spdx = spdx3_encode_element($doc, 'software_Package', 'spdxId' => $p->{'spdx_id'}, 'name' => $p->{'NAME'});
  $spdx->{'software_packageVersion'} = $evr if defined $evr;
  if ($p->{'VENDOR'}) {
    my $vendor = $p->{'VENDOR'};
    spdx3_encode_organization($doc, $vendor) unless $doc->[0]->[2]->{$vendor};
    $spdx->{'suppliedBy'} = $doc->[0]->[2]->{$vendor};
    push @{$doc->[0]->[1]}, \$spdx->{'suppliedBy'};
    $spdx->{'originatedBy'} = [ $doc->[0]->[2]->{$vendor} ];
    push @{$doc->[0]->[1]}, \$spdx->{'originatedBy'}->[0];
  }
  $spdx->{'software_downloadLocation'} = 'NOASSERTION';

  $pkgtype = $p->{'pkgtype'} if $p->{'pkgtype'};
  my $si = gen_source_info($p, $pkgtype);
  $spdx->{'software_sourceInfo'} = $si if $si;
  $spdx->{'software_copyrightText'} = $p->{'COPYRIGHTTEXT'} ? $p->{'COPYRIGHTTEXT'} : 'NOASSERTION';
  $spdx->{'software_homePage'} = $p->{'URL'} if $p->{'URL'};

  my $purlurl = $p->{'skip_purl'} ? undef : gen_purl($p, $distro, $pkgtype);
  $spdx->{'software_packageUrl'} = $purlurl if $purlurl;
  push @{$spdx->{'externalRef'}}, { 'type' => 'ExternalRef', 'externalRefType' => 'vcs', 'locator' => [ $_ ] } for @{$p->{'VCS'} || []};
  push @{$spdx->{'externalRef'}}, { 'type' => 'ExternalRef', 'externalRefType' => 'other', 'comment' => 'obs-disturl', 'locator' => [ $p->{'DISTURL'} ] } if $p->{'DISTURL'};

  $spdx->{'software_primaryPurpose'} = $p->{'primaryPackagePurpose'} if $p->{'primaryPackagePurpose'};
  $p->{'spdx_id'} = $spdx->{'spdxId'};
  push @$doc, $spdx;
  my $license = $p->{'LICENSE'};
  my $license_spdxid = $license ? spdx3_encode_license($doc, $license) : $spdx3_no_assertion_license;
  spdx3_encode_one_license_relation($doc, $spdx->{'spdxId'}, $license_spdxid, 'hasConcludedLicense');
  $license_spdxid = $spdx3_no_assertion_license if ($config->{'buildflags:spdx-declared-license'} || '') eq 'NOASSERTION';
  spdx3_encode_one_license_relation($doc, $spdx->{'spdxId'}, $license_spdxid, 'hasDeclaredLicense');
  return $spdx->{'spdxId'};
}

sub spdx3_encode_file {
  my ($doc, $f) = @_;
  my $spdx = spdx3_encode_element($doc, 'software_File', 'spdxId' => $f->{'spdx_id'}, 'name' => $f->{'name'}, 'software_copyrightText' => '');
  my $mime = $f->{'mime'};
  $spdx->{'contentType'} = $mime || 'application/octet-stream';
  if ($f->{'sha256sum'}) {
    $spdx->{'verifiedUsing'} = [ { 'type' => 'Hash', 'algorithm' => 'sha256', 'hashValue' => $f->{'sha256sum'} } ];
  }
  $f->{'spdx_id'} = $spdx->{'spdxId'};
  push @$doc, $spdx;
  return $spdx->{'spdxId'};
}

sub spdx3_encode_one_relation {
  my ($doc, $p, $op, $type) = @_;
  $op = [ $op ] unless ref($op) eq 'ARRAY';
  my @op = grep {$_->{'spdx_id'}} @$op;
  return unless $p->{'spdx_id'} && @op;
  return if $type eq 'DEPENDENCY_OF';
  my $spdx = spdx3_encode_element($doc, 'Relationship');
  $spdx->{'relationshipType'} = 'describes' if $type eq 'DESCRIBES';
  $spdx->{'relationshipType'} = 'contains' if $type eq 'CONTAINS';
  $spdx->{'relationshipType'} = 'dependsOn' if $type eq 'DEPENDS_ON';
  if (!$spdx->{'relationshipType'}) {
    $spdx->{'relationshipType'} = 'other';
    $spdx->{'comment'} = $type;
  }
  $spdx->{'from'} = $p->{'spdx_id'};
  $spdx->{'to'} = [ map {$_->{'spdx_id'}} @op ];
  push @{$doc->[0]->[1]}, \$spdx->{'from'};
  push @{$doc->[0]->[1]}, \$spdx->{'to'}->[$_] for 0 .. $#op;
  push @$doc, $spdx;
  return $spdx->{'spdxId'};
}

sub spdx3_encode_relations {
  my ($doc, $p) = @_;
  return unless $p->{'spdx_id'};
  # pull out file relations and create just one relation for them
  my (@relations, @file_relations);
  for my $r (@{$p->{'RELATION'} || []}) {
    if ($r->[1] eq 'CONTAINS' && $r->[0]->{'sha256sum'}) {
      push @file_relations, $r->[0];
    } else {
      push @relations, $r;
    }
  }
  my @ret;
  push @ret, map {spdx3_encode_one_relation($doc, $p, $_->[0], $_->[1])} @relations;
  push @ret, spdx3_encode_one_relation($doc, $p, \@file_relations, 'CONTAINS');
  return @ret;
}

sub spdx3_encode_header {
  my ($subjectname, $type, $rootp) = @_;
  my $doc = [];
  push @$doc, [ '1', [], {}, {}];
  my $tool = spdx3_encode_element($doc, 'Tool', 'name' => "$tool_name-$tool_version");
  my $agent;
  if ($agent_name) {
    $agent = spdx3_encode_element($doc, 'SoftwareAgent', 'name' => $agent_name);
  } else {
    $agent = spdx3_encode_element($doc, 'Person');
  }
  my $ci = { '@id' => '_:creationInfo_0', 'type' => 'CreationInfo', 'specVersion' => '3.0.1', 'createdBy' => [ $agent->{'spdxId'} ], 'createdUsing' => [ $tool->{'spdxId'} ], 'created' => rfc3339time($buildtime) };
  push @{$doc->[0]->[1]}, \$ci->{'createdUsing'}->[0];
  push @{$doc->[0]->[1]}, \$ci->{'createdBy'}->[0];
  push @$doc, $ci, $tool, $agent;
  my $document = spdx3_encode_element($doc, 'SpdxDocument', 'spdxId' => 'document0', 'dataLicense' => 'http://spdx.org/licenses/CC0-1.0');
  push @$doc, $document;
  spdx3_encode_pkg($doc, $rootp, undef, '');
  spdx3_encode_one_relation($doc, {'spdx_id' => $document->{'spdxId'}}, $rootp, 'DESCRIBES');
  return $doc;
}

sub spdx3_encode_dist {
  my ($doc, $dist) = @_;
  my $distp = {
    NAME => $dist->{id},
    VERSION => $dist->{version_id},
    primaryPackagePurpose => 'operatingSystem',
    skip_purl => 1
  };
  return spdx3_encode_pkg($doc, $distp, undef, '');
}

sub spdx3_finish {
  my ($doc, $subjectname) = @_;
  my $ids = shift @$doc;
  my $idprefix = 'http://open-build-service.org/spdx/'.urlencode($subjectname).'-'.gen_uuid($doc).'-specv3/';
  $$_ = "$idprefix$$_" for @{$ids->[1]};
  $doc = {
    '@context' => 'https://spdx.org/rdf/3.0.1/spdx-context.jsonld',
    '@graph' => $doc,
  };
  return $doc;
}

##################################################################################################
#
# Main
#

sub print_help {
    print "
The Software Bill of Materials (SBOM) generation tool
=====================================================

This tool generates SBOM data based on data from rpm and deb packages.

Output formats
==============

  --format spdx
     Generates SPDX 2.3 formatted data. This is the default.

  --format cyclonedx
     Generates CycloneDX 1.5 formatted data

  --intoto
     Can be used to optionally wrap the generated data into an
     in-toto attestation.

Supported content
=================

  --dir DIRECTORY
     The RPM/Dpkg database of the system below DIRECTORY will be evaluated, also all
     files will be referenced in the SBOM if RPM is used.

  --product DIRECTORY
     An installation medium. All .rpm files in any sub directory will be scanned.

  --rpmmd DIRECTORY
     A directory providing rpm-md meta data. A 'repodata/repomd.xml' file is expected.

   --container-docker-archive CONTAINER_ARCHIVE
   --container-oci-archive CONTAINER_ARCHIVE
      An container archive providing a system

Additional metadata
===================

  --vcs URL
     Add a version control system URL to the root component of the SBOM.
     This option can be given multiple times to reference multiple
     source repositories.

Supported options for generation
================================

  --no-files-generation
     Skip generation of filelists

  --no-pretty
     Reduce size by stripping white-spaces in JSON output. Default when file lists are generated.
";
}

my $known_options = {
  'distro' => ':',
  'subject' => ':',
  'intoto' => sub { $_[0]->{'intoto'} = Build::Options::getarg($_[2], $_[3], 2) || 'v0.1' },
  'product' => 'type=product',
  'dir' => 'type=dir',
  'rpmmd' => 'type=rpmmd',
  'container-archive' => 'type=docker-archive',
  'container-docker-archive' => 'type=docker-archive',
  'container-oci-archive' => 'type=oci-archive',
  'format' => ':',
  'help' => '',
  'h' => 'help',
  'dist' => ':',
  'vcs' => '::',
  'disturl' => ':',
  'arch' => ':',
  'archpath' => 'arch:',
  'configdir' => ':',
  'no-files-generation' => '',
  'no-pretty' => '',
  'with-dependencies' => '',
  'buildtime' => ':',
  'agent' => ':',
  'obsname' => sub { $_[0]->{'agent'} = "Open Build Service ".Build::Options::getarg($_[2], $_[3]) },
};

my ($opts, @args) = Build::Options::parse_options($known_options, @ARGV);

if ($opts->{'help'}) {
  print_help();
  exit(0);
}

# set defaults
$opts->{'configdir'} ||= ($::ENV{'BUILD_DIR'} || '/usr/lib/build') . '/configs';
$opts->{'type'} ||= 'docker-archive';	# compat
$opts->{'format'} ||= 'spdx';

die("unknown format $opts->{'format'}\n") unless $opts->{'format'} eq 'spdx' || $opts->{'format'} eq 'spdx3' || $opts->{'format'} eq 'cyclonedx';
die("unsupported intoto version $opts->{'intoto'}\n") if $opts->{'intoto'} && $opts->{'intoto'} ne 'v0.1' && $opts->{'intoto'} ne 'v1';


die("usage: generate_sbom [--distro NAME] [--format spdx|cyclonedx] [--intoto] [--dir DIRECTORY]|[--product DIRECTORY]|[--rpmmd DIRECTORY]|[--container-archive CONTAINER_ARCHIVE]\n") unless @args == 1;
my $toprocess = $args[0];

my $tmpdir = File::Temp::tempdir( CLEANUP => 1 );

my $filepkgs;
my $files;
my $pkgs;
my $dist;
my $pkgtype = 'rpm';

$config = Build::read_config_dist($opts->{'dist'}, $opts->{'arch'} || 'noarch', $opts->{'configdir'}) if $opts->{'dist'};

my $no_files_generation = $opts->{'no-files-generation'};
$with_dependencies = $opts->{'with-dependencies'};

# set defaults
$no_files_generation = 1 if !defined($no_files_generation) && ($config->{'buildflags:spdx-files-generation'} || '') eq 'no';

$buildtime = $opts->{'buildtime'} || time();
$agent_name = $opts->{'agent'};

my $targettype;
my $unpackdir;
my $rootpkg_version;

# unpack into a directory if the argument is a file
if (($opts->{'type'} eq 'product' || $opts->{'type'} eq 'dir') && $toprocess =~ /\.iso$/ && -f $toprocess) {
  $unpackdir = unpack_iso($tmpdir, $toprocess);
} elsif ($opts->{'type'} eq 'docker-archive' || $opts->{'type'} eq 'oci-archive') {
  $unpackdir = unpack_container($tmpdir, $toprocess, $opts->{'type'});
} else {
  $unpackdir = $toprocess;
}

if ($opts->{'type'} eq 'product') {
  # product case
  $targettype = 'library';
  $no_files_generation = 1 unless defined $no_files_generation;
  $files = gen_filelist($unpackdir) unless $no_files_generation;
  $pkgs = read_pkgs_from_product_directory($unpackdir);
} elsif ($opts->{'type'} eq 'rpmmd') {
  $targettype = 'library';
  my $primaryfile;
  require Build::Rpmmd;
  if (-d $unpackdir) {
    $targettype = 'install';
    my $repodatadir = -f "$unpackdir/repomd.xml" ? $unpackdir : "$unpackdir/repodata";
    my %d = map {$_->{'type'} => $_} @{Build::Rpmmd::parse_repomd("$repodatadir/repomd.xml")};
    my $primary = $d{'primary'};
    die("no primary type in repomd.xml\n") unless $primary;
    my $loc = $primary->{'location'};
    $loc =~ s/.*\///;
    $primaryfile = "$repodatadir/$loc";
    my $checksum = $primary->{'checksum'};
    if ($checksum) {
      $checksum =~ s/.*://;
      $rootpkg_version = $checksum;
    }
    $no_files_generation = 1 if $repodatadir eq $unpackdir;
  } else {
    $primaryfile = $unpackdir;
    $no_files_generation = 1;
  }
  die("$primaryfile: $!\n") unless -e $primaryfile;
  $no_files_generation = 1 unless defined $no_files_generation;
  $files = gen_filelist($unpackdir) unless $no_files_generation;
  $pkgs = read_pkgs_from_rpmmd($primaryfile);
} elsif ($opts->{'type'} eq 'dir') {
  $targettype = 'application';
  $dist = read_dist($unpackdir);
  $pkgtype = pkgtype_from_dist($dist);
  #if it is a ubuntu id_like contains debian
  if ($pkgtype eq 'deb') {
    $pkgs = read_pkgs_deb($unpackdir);
  } elsif ($pkgtype eq 'rpm') {
    dump_rpmdb($unpackdir, "$tmpdir/rpmdb");
    $pkgs = read_pkgs_rpmdb("$tmpdir/rpmdb");
  }
  $files = gen_filelist($unpackdir);
  $filepkgs = introspect_filelist($unpackdir, $files);
  $with_dependencies = 1 unless defined $with_dependencies;
} elsif ($opts->{'type'} eq 'docker-archive' || $opts->{'type'} eq 'oci-archive') {
  # container archive case
  $targettype = 'container';
  $dist = read_dist($unpackdir);
  $pkgtype = pkgtype_from_dist($dist);
  $files = gen_filelist($unpackdir);
  if ($pkgtype eq 'deb') {
    $pkgs = read_pkgs_deb($unpackdir);
  } elsif ($pkgtype eq 'rpm') {
    dump_rpmdb($unpackdir, "$tmpdir/rpmdb");
    $pkgs = read_pkgs_rpmdb("$tmpdir/rpmdb");
  }
  $filepkgs = introspect_filelist($unpackdir, $files);
  $with_dependencies = 1 unless defined $with_dependencies;
} else {
  die("Unsupported content type $opts->{'type'}\n");
}

# generate inter-package dependencies
if ($with_dependencies) {
  generate_pkg_dependencies($pkgs, $pkgtype);
  for my $p (@$pkgs) {
    for my $p2 (@{$p->{'deps'} || []}) {
      push @{$p2->{'RELATION'}}, [ $p, 'DEPENDENCY_OF' ];
      push @{$p->{'RELATION'}}, [ $p2, 'DEPENDS_ON' ];
    }
  }
}

# generate file relations
if (!$no_files_generation && @{$files || []}) {
  my %f2p;
  for my $p (@$pkgs) {
    push @{$f2p{$_}}, $p for @{$p->{'FILENAMES'} || []};
  }
  for my $f (@$files) {
    next if $f->{'SKIP'};
    #warn("unpackaged file: $f->{'name'}\n") unless @{$f2p{$f->{'name'}} || []};
    for my $p (@{$f2p{$f->{'name'}} || []}) {
      push @{$p->{'RELATION'}}, [ $f, 'CONTAINS' ];
    }
  }
}

# handle file packages, those are generated by introspecting files
if (@{$filepkgs || []}) {
  my %f2f;
  if (!$no_files_generation) {
    for my $f (@{$files || []}) {
      next if $f->{'SKIP'};
      $f2f{$f->{'name'}} = $f;
    }
  }
  my %f2p;
  for my $p (@$pkgs) {
    push @{$f2p{$_}}, $p for @{$p->{'FILENAMES'} || []};
  }
  for my $p (@{$filepkgs || []}) {
    for my $fn (@{$p->{'filenames'}}) {
      my $f = $f2f{$fn};
      push @{$p->{'RELATION'}}, [ $f, "evident-by: indicates the package's existence is evident by the given file" ] if $f;
    }
    for my $p2 (@{$p->{'deps'} || []}) {
      push @{$p2->{'RELATION'}}, [ $p, 'DEPENDENCY_OF' ];
      push @{$p->{'RELATION'}}, [ $p2, 'DEPENDS_ON' ];
    }
    my @overp;
    for my $fn (@{$p->{'filenames'}}) {
      for my $pp (@{$f2p{$fn}}) {
	push @overp, $pp unless grep {$_ == $pp} @overp;
      }
    }
    for my $pp (@overp) {
      push @{$pp->{'RELATION'}}, [ $p, "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by" ];
    }
  }
}

my $subjectname = $opts->{'subject'};
if (!$subjectname) {
  $subjectname = $toprocess;
  $subjectname =~ s/\/+$//;
  $subjectname =~ s/.*\///;
}
my $distro = $opts->{'distro'};
if (!$distro && $dist) {
  $distro = $dist->{'id'};
  $distro .= "-$dist->{'version_id'}" if defined($dist->{'version_id'}) && $dist->{'version_id'} ne '';
  $distro .= "-$dist->{'build_id'}" if defined($dist->{'build_id'}) && $dist->{'build_id'} ne '';
}

my $json_template;
my $intoto_type;
my $doc;

my $rootpkg = {
  NAME => $subjectname,
  primaryPackagePurpose => $targettype,
  skip_purl => 1
};
$rootpkg->{'VERSION'} = $rootpkg_version if $rootpkg_version;
$rootpkg->{'VCS'} = $opts->{'vcs'} if $opts->{'vcs'};
$rootpkg->{'DISTURL'} = $opts->{'disturl'} if $opts->{'disturl'};
$rootpkg->{'pkgtype'} = 'rpmmd' if $opts->{'type'} eq 'rpmmd';

if ($opts->{'format'} eq 'spdx') {
  my %unknown_spdx_licenses;
  $json_template = $spdx_json_template;
  $intoto_type = 'https://spdx.dev/Document';
  $rootpkg->{'spdx_id'} = 'SPDXRef-DOCUMENT-ROOT';
  $doc = spdx_encode_header($subjectname, $targettype, $rootpkg);
  for my $p (@$pkgs) {
    push @{$doc->{'packages'}}, spdx_encode_pkg($p, $distro, $pkgtype, \%unknown_spdx_licenses);
  }
  for my $p (@{$filepkgs || []}) {
    push @{$doc->{'packages'}}, spdx_encode_pkg($p, undef, $p->{'pkgtype'}, \%unknown_spdx_licenses);
  }
  if (!$no_files_generation) {
    for my $f (@{$files || []}) {
      next if $f->{'SKIP'};
      push @{$doc->{'files'}}, spdx_encode_file($f);
    }
  }

  if ($dist && %$dist) {
    push @{$doc->{'packages'}}, spdx_encode_dist($dist);
    push @$pkgs, { 'spdx_id' => $doc->{'packages'}->[-1]->{'SPDXID'} };
  }

  for (sort keys %unknown_spdx_licenses) {
    push @{$doc->{'hasExtractedLicensingInfos'}}, spdx_encode_extracted_license($unknown_spdx_licenses{$_});
  }
  for my $p (@$pkgs) {
    push @{$doc->{'relationships'}}, spdx_encode_relations($p);
  }
  for my $p (@{$filepkgs || []}) {
    push @{$doc->{'relationships'}}, spdx_encode_relations($p);
  }
  if (!$no_files_generation) {
    for my $f (@{$files || []}) {
      push @{$doc->{'relationships'}}, spdx_encode_relations($f) unless $f->{'SKIP'};
    }
  }
  for my $p (@$pkgs) {
    push @{$doc->{'relationships'}}, spdx_encode_one_relation($rootpkg, $p, 'CONTAINS');
  }
  push @{$doc->{'relationships'}}, {
    'spdxElementId' => 'SPDXRef-DOCUMENT',
    'relatedSpdxElement' => $rootpkg->{'spdx_id'},
    'relationshipType', 'DESCRIBES',
  };
  $doc = spdx_finish($doc, $subjectname);
} elsif ($opts->{'format'} eq 'spdx3') {
  $json_template = $spdx3_json_template;
  $intoto_type = 'https://spdx.dev/Document/v3';
  $doc = spdx3_encode_header($subjectname, $targettype, $rootpkg);
  for my $p (@$pkgs) {
    spdx3_encode_pkg($doc, $p, $distro, $pkgtype);
  }
  for my $p (@{$filepkgs || []}) {
    spdx3_encode_pkg($doc, $p, undef, $p->{'pkgtype'});
  }
  if (!$no_files_generation) {
    for my $f (@{$files || []}) {
      next if $f->{'SKIP'};
      spdx3_encode_file($doc, $f);
    }
  }
  for my $p (@$pkgs) {
    spdx3_encode_relations($doc, $p);
  }
  for my $p (@{$filepkgs || []}) {
    spdx3_encode_relations($doc, $p);
  }
  if (!$no_files_generation) {
    for my $f (@{$files || []}) {
      spdx3_encode_relations($doc, $f) unless $f->{'SKIP'};
    }
  }
  if ($dist && %$dist) {
    my $dist_spdxid = spdx3_encode_dist($doc, $dist);
    push @$pkgs, { 'spdx_id' => $dist_spdxid };
  }
  if (@$pkgs) {
    spdx3_encode_one_relation($doc, $rootpkg, $pkgs, 'CONTAINS');
  }
  $doc = spdx3_finish($doc, $subjectname);
} elsif ($opts->{'format'} eq 'cyclonedx') {
  $json_template = $cyclonedx_json_template;
  $intoto_type = 'https://cyclonedx.org/bom';
  $doc = cyclonedx_encode_header($subjectname, $targettype, $rootpkg);
  for my $p (@$pkgs) {
    next if $p->{'primaryPackagePurpose'} && $p->{'primaryPackagePurpose'} eq 'install';
    push @{$doc->{'components'}}, cyclonedx_encode_pkg($p, $distro, $pkgtype);
  }
  for my $p (@{$filepkgs || []}) {
    push @{$doc->{'components'}}, cyclonedx_encode_pkg($p, undef, $p->{'pkgtype'});
  }
  if ($dist && %$dist) {
    push @{$doc->{'components'}}, cyclonedx_encode_dist($dist);
  }
  for my $p (@$pkgs) {
    push @{$doc->{'dependencies'}}, cyclonedx_encode_relations($p);
  }
  for my $p (@{$filepkgs || []}) {
    push @{$doc->{'dependencies'}}, cyclonedx_encode_relations($p);
  }
  $doc = cyclonedx_finish($doc, $subjectname);
} else {
  die("internal error, format not supported\n");
}

if ($opts->{'intoto'}) {
  my $subject = { 'name' => $subjectname };
  # no digest for products as it might be a directory. And an iso file would change the checksum later while signing.
  $subject->{'digest'} = { 'sha256' => sha256file($toprocess) } if -f $toprocess;
  $doc = {
    '_type' => "https://in-toto.io/Statement/$opts->{'intoto'}",
    'subject' => [ $subject ],
    'predicateType' => $intoto_type,
    'predicate' => $doc,
  };
  $json_template = {
    '_order' => [ qw{_type predicateType subject predicate} ],
    'subject' => { '_order' => [ qw{name digest} ] },
    'predicate' => $json_template,
  };
}

my $no_pretty_output = defined($opts->{'no-pretty'}) ? $opts->{'no-pretty'} : !$no_files_generation;

print Build::SimpleJSON::unparse($doc, 'template' => $json_template, 'keepspecial' => 1, 'ugly' => $no_pretty_output)."\n";

