#!/usr/bin/perl
# Given a set of rpm packages and directory with their new content,
# generate a specfile that generates new packages
#
# Copyright (c) 2013 SUSE Linux Products GmbH, Nuernberg, Germany.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA

use strict;
use warnings;

my $USAGE = "Usage: $0 --directory <payload directory> rpm...\n";

use Getopt::Long;
use Fcntl qw(:mode :seek);

my $directory;
my $output = ".";
my $cert_subpackage;
my $kmp_basename;
my @rpms;

$ENV{LC_ALL} = "en_US.UTF-8";

GetOptions(
	"help|h" => sub { print $USAGE; exit; },
	"directory|d=s" => \$directory,
	"output|o=s" => \$output,
	"cert-subpackage|c=s" => \$cert_subpackage,
) or die $USAGE;
@rpms = @ARGV;
if (!@rpms) {
	print STDERR "$0: No packages given\n";
	die $USAGE;
}
if (!$directory || substr($directory, 0, 1) ne '/' || ! -d $directory) {
	print STDERR "$0: --directory must be an absolute path\n";
	die $USAGE;
}

sub query_array {
	my ($rpm, @tags) = @_;
	my @res;

	my $format = "[" . join("|", map { "\%{$_}" } @tags) . "\\n]";
	open(my $fh, '-|', "rpm", "-qp", "--qf", $format, $rpm)
		or die "rpm: $!\n";
	while (<$fh>) {
		chomp;
		my @t = split(/\|/, $_, -1);
		push(@res, \@t);
	}
	close($fh);
	return @res;
}

sub query_multiline_array {
	my ($rpm, $tag) = @_;
	my @res;
	my $delim = "|||"; # XXX - dangerous

	my $format = "[$delim\\n\%{$tag}\\n]";
	open(my $fh, '-|', "rpm", "-qp", "--qf", $format, $rpm)
		or die "rpm: $!\n";
	my $line = <$fh>;
	return unless $line;
	chomp($line);
	return if $line eq "(none)";
	die "Expected \"$delim\" at beginning of rpm output, got \"$line\""
		if $line ne $delim;
	my $cur = "";
	while ($line = <$fh>) {
		chomp($line);
		if ($line eq $delim) {
			$cur = "" if $cur eq "\n";
			push(@res, $cur);
			$cur = "";
		} else {
			$cur .= $line . "\n";
		}
	}
	$cur = "" if $cur eq "\n";
	push(@res, $cur);
	close($fh);
	return @res;
}

sub query_single {
	my ($rpm, $tag) = @_;
	my $res;

	open(my $fh, '-|', "rpm", "-qp", "--qf", "\%{$tag}\\n", $rpm)
		or die "rpm: $!\n";
	{
		local $/ = undef;
		$res = <$fh>;
	}
	chomp $res;
	if ($res eq "(none)") {
		$res = "";
	}
	close($fh);
	return $res;

}

# specfile dependency => rpm tag name
my %dep2tag = (
	conflicts => "conflict",
	obsoletes => "obsolete",
	provides => "provide",
	requires => "require",
	suggests => "suggest",
	enhances => "enhance",
	recommends => "recommend",
	supplements => "supplement",
);

# specfile scriptlet => rpm tag name
my %script2tag = (
	pre          => "prein",
	post         => "postin",
	preun        => "preun",
	postun       => "postun",
	pretrans     => "pretrans",
	posttrans    => "posttrans",
	verifyscript => "verifyscript",
	# FIXME: triggers
);

# tags which are printed verbatim in the specfile
my @simple_tags = qw(version release license group summary packager vendor
                     url distribution);

sub load_package {
	my $rpm = shift;
	my %res;

	for my $tag (qw(name arch sourcerpm description), @simple_tags) {
		$res{$tag} = query_single($rpm, $tag);
	}
	my @files;
	my @list = query_array($rpm, qw(filenames fileflags filemodes fileusername filegroupname filesizes filemtimes filelinktos fileverifyflags));
	for my $file (@list) {
		my $new = {
				name   => $file->[0],
				flags  => $file->[1],
				mode   => $file->[2],
				owner  => $file->[3],
				group  => $file->[4],
				size   => $file->[5],
				mtime  => $file->[6],
				target => $file->[7],
				verify => $file->[8],
		};
		push(@files, $new);
		if ($new->{name} =~ /\.ko$/ && S_ISREG($new->{mode})) {
			$res{is_kmp} = 1;
		}
	}
	$res{files} = \@files;
	while (my ($dep, $tag) = each(%dep2tag)) {
		my @deps;
		my @list = query_array($rpm, "${tag}name", "${tag}flags", "${tag}version");
		for my $d (@list) {
			next if $d->[0] eq "(none)";
			push(@deps, {
					name => $d->[0],
					flags => $d->[1],
					version => $d->[2],
			});
		}
		$res{$dep} = \@deps;
	}

	while (my ($script, $tag) = each(%script2tag)) {
		my $interp = query_single($rpm, "${tag}prog");
		next unless $interp;
		my $s = query_single($rpm, $tag);
		$res{$script} = {
			interp => $interp,
			script => $s,
		};
	}
	my @triggers = query_array($rpm, qw(triggertype triggerscriptprog triggerconds));
	my @triggerscripts = query_multiline_array($rpm, "triggerscripts");
	if (scalar(@triggers) != scalar(@triggerscripts)) {
		die "# of %%{triggertype} tags (" . scalar(@triggers) .
		") != # of %%{triggerscripts} tags (" . scalar(@triggerscripts)
		. ")";
	}
	for (my $i = 0; $i < scalar(@triggers); $i++) {
		$res{triggers} ||= [];
		push(@{$res{triggers}}, {
				type =>   $triggers[$i]->[0],
				interp => $triggers[$i]->[1],
				conds =>  $triggers[$i]->[2],
				script => $triggerscripts[$i],
		});
	}
	open(my $fh, '-|', "rpm", "-qp", "--changelog", $rpm) or die "rpm: $!\n";
	{
		local $/ = undef;
		my $changelog = <$fh>;
		close($fh);
		$res{changelog} = $changelog;
	}

	return \%res;
}

# quote percent signs in text
sub quote {
	my $text = shift;

	$text =~ s/%/%%/g;
	return $text;
}

sub print_script {
	my ($file, $script) = @_;

	return unless $script->{script};
	open(my $fh, '>', "$output/$file")
		or die "$output/$file: $!\n";
	print $fh $script->{script};
	close($fh);
	print SPEC " -f $file";
}

sub print_package {
	my ($p, $is_main) = @_;

	if ($is_main) {
		print SPEC "Name: $p->{name}\n";
		print SPEC "Buildroot: $directory\n";
		print SPEC "\%define _use_internal_dependency_generator 0\n";
		print SPEC "\%define __find_provides %{nil}\n";
		print SPEC "\%define __find_requires %{nil}\n";
		print SPEC "\%define __find_supplements %{nil}\n";
		if ($p->{nosource}) {
			# We do not generate any no(src).rpm, but we want the
			# %{sourcerpm} tag in the binary packages to match.
			# So we add a dummy source and mark it as nosource.
			print SPEC "Source0: repackage.spec\n";
			print SPEC "NoSource: 0\n";
		}
	} else {
		print SPEC "\%package -n $p->{name}\n";
	}
	for my $tag (@simple_tags) {
		next if $p->{$tag} eq "";
		print SPEC "$tag: " . quote($p->{$tag}) . "\n";
	}
	print SPEC "BuildArch: noarch\n" if $p->{arch} eq "noarch";
	for my $dep (sort(keys(%dep2tag))) {
		print_deps($dep, $p->{$dep});
	}
	if ($cert_subpackage && $p->{is_kmp}) {
		print SPEC "Requires: $kmp_basename-ueficert\n";
	}
	print SPEC "\%description -n $p->{name}\n";
	print SPEC quote($p->{description}) . "\n\n";

	for my $script (sort(keys(%script2tag))) {
		next unless $p->{$script};
		print SPEC "\%$script -p $p->{$script}{interp} -n $p->{name}";
		print_script("$script-$p->{name}", $p->{$script});
		print SPEC "\n";
	}
	my $i = 0;
	for my $trigger (@{$p->{triggers}}) {
		print SPEC "\%trigger$trigger->{type} -p $trigger->{interp} -n $p->{name}";
		print_script("trigger$i-$p->{name}", $trigger);
		print SPEC " -- $trigger->{conds}\n";
		$i++;
	}
	if ($p->{files}) {
		print SPEC "\%files -n $p->{name}\n";
		print_files($p->{files});
	}
	print SPEC "\n";
}

# /usr/include/rpm/rpmds.h
my %deptypes = (
	pre    => (1 <<  9),
	post   => (1 << 10),
	preun  => (1 << 11),
	postun => (1 << 12),
	verify => (1 << 13),
);
my %depflags = (
	"<"    => (1 << 1),
	">"    => (1 << 2),
	"="    => (1 << 3),
	rpmlib => (1 << 24),
);

sub print_deps {
	my ($depname, $list) = @_;

	foreach my $d (@$list) {
		next if ($d->{flags} & $depflags{rpmlib});

		print SPEC $depname;
		my @deptypes;
		while (my ($type, $bit) = each(%deptypes)) {
			push(@deptypes, $type) if $d->{flags} & $bit;
		}
		print SPEC "(", join(",", @deptypes), ")" if @deptypes;
		print SPEC ": ";

		print SPEC quote($d->{name});
		if ($d->{version}) {
			print SPEC " ";
			for my $op (qw(< > =)) {
				print SPEC $op if $d->{flags} & $depflags{$op};
			}
			print SPEC " " . quote($d->{version});
		}
		print SPEC "\n";
	}
}

# /usr/include/rpm/rpmfi.h
my %filetypes = (
	config    => (1 << 0),
	doc       => (1 << 1),
	missingok => (1 << 3),
	noreplace => (1 << 4),
	ghost     => (1 << 6),
);

my %verifyflags = (
	filedigest=> (1 << 0),
	size      => (1 << 1),
	link      => (1 << 2),
	user      => (1 << 3),
	group     => (1 << 4),
	mtime     => (1 << 5),
	mode      => (1 << 6),
	rdev      => (1 << 7),
	caps      => (1 << 8),
);

sub print_files {
	my $files = shift;

	for my $f (@$files) {
		my $path = "$directory/$f->{name}";
		my $attrs = "";
		# Fix mtime of directories, which cpio -idm fails to preserve
		if (S_ISDIR($f->{mode})) {
			$attrs .= "\%dir ";
			utime($f->{mtime}, $f->{mtime}, $path);
		}
		$attrs .= sprintf('%%attr(%04o, %s, %s) ', ($f->{mode} & 0777),
			$f->{owner}, $f->{group});
		if ($f->{flags} & $filetypes{config}) {
			$attrs .= "%config ";
			my @cfg_attrs;
			for my $attr (qw(missingok noreplace)) {
				next unless $f->{flags} & $filetypes{$attr};
				push(@cfg_attrs, $attr);
			}
			$attrs .= "(" . join(",", @cfg_attrs) . ")" if @cfg_attrs;
		}
		$attrs .= "%doc " if $f->{flags} & $filetypes{doc};
		if ($f->{flags} & $filetypes{ghost}) {
			$attrs .= "%ghost ";
			if (S_ISREG($f->{mode})) {
				open(my $fh, '>', $path) or die "$path: $!\n";
				if ($f->{size} > 0) {
					sysseek($fh, $f->{size} - 1, SEEK_SET);
					syswrite($fh, ' ', 1);
				}
				close($fh);
				utime($f->{mtime}, $f->{mtime}, $path);
			} elsif (S_ISLNK($f->{mode})) {
				symlink($f->{target}, $path);
			}
		}
		# mtime of symlinks is also not preserved by cpio
		if (S_ISLNK($f->{mode})) {
			# perl core does not provide lutimes()/utimensat()
			system("touch", "-h", "-d\@$f->{mtime}", $path);
		}
		my $verify_attrs = "";
		for my $flag (sort(keys(%verifyflags))) {
			if (!($f->{verify} & $verifyflags{$flag})) {
				$verify_attrs .= "$flag ";
			}
		}
		if ($verify_attrs) {
			$attrs .= "%verify(not $verify_attrs) ";
		}

		print SPEC "$attrs " . quote($f->{name}) . "\n";
		if (-e "$path.sig") {
			print SPEC "$attrs " . quote($f->{name}) . ".sig\n";
		}
	}
}

my %packages;
for my $rpm (@rpms) {
	my $p = load_package($rpm);
	$packages{$p->{name}} = $p;
}

my $sourcerpm;
for my $p (values(%packages)) {
	$sourcerpm = $p->{sourcerpm} unless $sourcerpm;
	if ($p->{sourcerpm} ne $sourcerpm) {
		die "Error: packages built from different source rpm: $sourcerpm vs $p->{sourcerpm}\n";
	}
}
if ($sourcerpm !~ /^(.+)-([^-]+)-([^-]+)\.(no)?src\.rpm$/) {
	die "Error: malformed %{sourcerpm} tag: $sourcerpm\n";
}
my ($main_name, $main_ver, $main_rel, $nosrc) = ($1, $2, $3, $4);
if (!exists($packages{$main_name})) {
	# create an empty main package
	my $first = (sort(keys(%packages)))[0];
	$packages{$main_name} = {
		name => $main_name,
		version => $main_ver,
		release => $main_rel,
	};
	for my $tag (qw(description changelog arch), @simple_tags) {
		next if $packages{$main_name}->{$tag};
		$packages{$main_name}->{$tag} = $packages{$first}->{$tag};
	}
}
$packages{$main_name}->{nosource} = $nosrc ? 1 : 0;

# Find out the basename of <name>-kmp-<flavor>, falling back to the
# main package name
for my $p (values(%packages)) {
	next unless $p->{is_kmp};
	(my $n = $p->{name}) =~ s/-kmp-.*//;
	$kmp_basename = $n unless $kmp_basename;
	if ($n ne $kmp_basename) {
		$kmp_basename = undef;
		last;
	}
}
$kmp_basename = $main_name unless $kmp_basename;

open(SPEC, '>', "$output/repackage.spec") or die "$output/repackage.spec: $!\n";
print_package($packages{$main_name}, 1);
for my $name (sort(keys(%packages))) {
	next if $name eq $main_name;
	print_package($packages{$name}, 0);
}
if ($cert_subpackage) {
	my $certdir = "/etc/uefi/certs";
	my $certs = "";
	if (-d "$directory/$certdir") {
		opendir(my $dh, "$directory/$certdir") or die "$directory/$certdir";
		while (my $cert = readdir($dh)) {
			next if $cert =~ /^\.\.?$/;
			if ($cert !~ /\.crt$/) {
				print STDERR "warning: Ignoring $directory/$certdir/$cert (no .crt suffix)\n";
				next;
			}
			$certs .= " $certdir/$cert";
		}
	}
	if (!$certs) {
		print STDERR "warning: --cert-subpackage specified, but no certs found in $directory/$certdir\n";
	}
	local $/ = undef;
	open(my $fh, '<', $cert_subpackage) or die "$cert_subpackage: $!\n";
	my $template = <$fh>;
	close($fh);
	$template =~ s/\%\{-n\*}/$kmp_basename/g;
	$template =~ s/\@CERTS\@/$certs/g;
	print SPEC $template;
}
print SPEC "\%changelog\n";
print SPEC quote($packages{$main_name}->{changelog});
close(SPEC);
