#!/usr/bin/perl
#
# Copyright (C) 2006 Novell Inc.
#
# 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.,
# 51 Franklin Street,
# Fifth Floor,
# Boston, MA  02110-1301,
# USA.
#
# $Id: createpatch,v 1.5 2007/12/11 13:42:01 lrupp Exp lrupp $
#

use strict;
use File::stat;
#use Data::Dumper;

my $basedir;
my @packagelist;
my $update_repo = 0;
my $do_signing = 0;

my $patch_id = `hostname -d`;
chomp $patch_id;

my $patch_name = "usefirst";
my $patch_version = 0;
my $patch_summary = "";
my $patch_description = "";
my $sign_id = "";
my $category = "recommended";
my $license_file = "";
my $validate = 1;
my @patchsupplements;
my @pkgrefresh;

while (my $param = shift(@ARGV)) {
    if ( $param eq "-h" ) {
       &usage;
    }
    if ( $param eq "-C" ) {
	
        $category = shift(@ARGV);
	if (!($category =~ /^recommended$|^security$|^optional$/)) {
		print("Invalid category supplied with -C\n");
		&usage;
	}
    }
    if ( $param eq "-i" ) {
       $patch_id = shift(@ARGV);
       next;
    }
    if ( $param eq "-n" ) {
	$patch_name = shift(@ARGV);
	next;
    }
    if ( $param eq "-v" ) {
	$patch_version = shift(@ARGV);
	next;
    }
    if ( $param eq "-s" ) {
	$patch_summary = shift(@ARGV);
	next;
    }
    if ( $param eq "-d" ) {
	$patch_description = shift(@ARGV);
	next;
    }
    if ( $param eq "-u" ) {
	$update_repo = 1;
	next;
    }
    if ( $param eq "-S" ) {
	$do_signing = 1;
	next;
    }
    if ( $param eq "-I" ) {
	$sign_id = shift(@ARGV);
	next;
    }
    if ( $param eq "-L" ) {
	$license_file = shift(@ARGV);
	next;
    }
    if ( $param eq "-p" ) {
	@packagelist = split (',',shift (@ARGV));
	next;
    }
    if ( $param eq "--validate" ){
	$validate = 0;
        next;
    }
    if ( $param eq "--novalidate" ){
	$validate = 0;
        next;
    }
    if ( $param eq "--patchsupplements" ){
	@patchsupplements = split (',',shift (@ARGV));
        next;
    }
    if ( $param eq "--pkgfreshens" ){
	@pkgrefresh = split (',',shift (@ARGV));
        next;    
    }
    $basedir = $param;
}

$patch_id =~ s/\./_/g;

die "basedir not specified or not existant\n"
	unless ( $basedir
		&& -d $basedir
		&& -d "$basedir/repodata" );

my $first_package = "";
$first_package = $packagelist[0] if ( @packagelist && $packagelist[0] );

if ( $patch_name eq "usefirst" ) {
    $patch_name = $first_package if ( $first_package );
}

if ( $update_repo ) {
    if ( opendir ( DIR, "$basedir/repodata" ) ) {
	mkdir "$basedir/repo_save";
	for (readdir(DIR)) {
	    unlink "$basedir/repodata/$_" if ( /\.key$/ || /\.asc$/ );
	    next unless ( /^patch/ || /^product/ );
	    if ( /^patches.xml/ ) {
		unlink "$basedir/repodata/patches.xml";
		next;
	    }
	    link "$basedir/repodata/$_","$basedir/repo_save/$_";
	    unlink "$basedir/repodata/$_";
	}
	closedir (DIR);
    }
    if (-x '/usr/bin/createrepo'){
        system("createrepo -x \"*.patch.rpm\" -x \"*.delta.rpm\" $basedir");
    } else {
        warn "/usr/bin/createrepo not found or not executable.\n";
        warn "Please check if package createrepo is installed on your system.\n";
		mkdir "$basedir/repodata" if (!-d "$basedir/repodata");
	}
    if ( opendir ( DIR, "$basedir/repo_save" ) ) {
	for (readdir(DIR)) {
	    link "$basedir/repo_save/$_","$basedir/repodata/$_";
	    unlink "$basedir/repo_save/$_";
	}
	closedir (DIR);
	rmdir "$basedir/repo_save";
    }
}

my $repodata = "$basedir/repodata";

if ( -f "$repodata/primary.xml.gz" ) {
    open ( PRIMARY, "zcat $repodata/primary.xml.gz |");
} else {
    open ( PRIMARY, "$repodata/primary.xml");
}

my @PRIMARY;
{
    local $/ = "<package ";
    @PRIMARY = <PRIMARY>;
    chomp (@PRIMARY);
}
close ( PRIMARY );

my @package_data;
my %packdata;
my $field = "package";
my $lastfield = "package";

# start extremely primitive xml parser ;)
for (@PRIMARY) {
    next if ( /^<\?xml/ );
    for (split ('>',$_)) {
	$_ =~ s/^\s*(.*?)\s*$/$1/s;
	if ( /^<([^\ ]*)\ (.*)/ ) {
	    $lastfield = $1;
	    $field .= ".$lastfield";
	    my $trail = $2;
	    my $field_ends = 0;
	    $field_ends = 1 if ( $trail =~ /\/$/ );
	    $trail =~ s/\/$//;
	    for (split('\ ',$trail)) {
		my ($key,$val) = split ('=',$_);
		$val =~ s/^\"(.*)\"$/$1/;
		$packdata{"$field.$key"} = $val;
		#print "'$field.$key' val = \"".$packdata{"$field.$key"}."\" (3)\n";
	    }
	    if ( $field_ends ) {
		if ( $field =~ /\./ ) {
		    $field =~ s/\.[^\.]*$//;
		    $lastfield = $field;
	            $lastfield =~ s/^.*([^\.]*)$/$1/;
		} else {
		    $field = "package";
		    $lastfield = "package";
		    if ($packdata{'package.name'}) {
			my %pack_data_tmp = %packdata;
			push @package_data, \%pack_data_tmp;
		    }
		    %packdata = ();
		}
	    }
	} elsif ( /^<\/(.*)/ ) {
	    if ( $field =~ /\./ ) {
		$field =~ s/\.[^\.]*$//;
		$lastfield = $field;
		$lastfield =~ s/^.*([^\.]*)$/$1/;
	    } else {
		$field = "package";
		$lastfield = "package";
		if ($packdata{'package.name'}) {
			my %pack_data_tmp = %packdata;
			push @package_data, \%pack_data_tmp;
		}
		%packdata = ();
	    }
	} elsif ( /^<([^\ >]*)/ ) {
	   my $tfield = $1;
	   if ( $tfield !~ /\/$/ ) {
		$field .= ".$tfield";
		$lastfield = $tfield;
	   }
	} elsif ( /^([^<]*)<\/$lastfield/ ) {
	    $packdata{"$field"} = $1;
	    #print "'$field' val = \"".$packdata{"$field"}."\" (2)\n";
	    if ( $field =~ /\./ ) {
		$field =~ s/\.[^\.]*$//;
		$lastfield = $field;
		$lastfield =~ s/^.*([^\.]*)$/$1/;
	    } else {
		$field = "package";
		$lastfield = "package";
		if ($packdata{'package.name'}) {
		my %pack_data_tmp = %packdata;
		push @package_data, \%pack_data_tmp;
		}
		%packdata = ();
	    }
	} elsif ( /^(.*)=(.*)$/ ) {
	   my $key = $1;
	   my $val = $2;
	   $val =~ s/^\"(.*)\"$/$1/;
	   $packdata{"$field.$key"} = $val;
	   #print "'$field.$key' val = \"".$packdata{"$field.$key"}."\" lastfield = \"$lastfield\" (1)\n";
	} 
    }
}
# end extremely primitive xml parser ;)

# for debugging only
#print Dumper($package_data[0]);


for (@package_data) {
    my $pack_name = $_->{'package.name'};
    my $long_name = $_->{'package.location.href'};
    $long_name =~ s/^.*\/([^\/]*)/$1/;
    next unless ( $pack_name eq $first_package || $long_name eq $first_package );
    # primitive approach: use summary and description of the first package found
    $patch_summary = $_->{'package.summary'} unless ($patch_summary);
    $patch_description = $_->{'package.description'} unless ($patch_description);
}


if ($#packagelist ge 0) {

    my $timestamp = time;
    my $iteration = $patch_version;
    while ( -f "$repodata/patch-$patch_id-$patch_name-$iteration.xml" ) {
	warn ("patch-$patch_id-$patch_name-$iteration.xml does already exist, increasing version\n");
	$iteration++;
    }

    # patch header, mostly static
    my $patch_text = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    $patch_text .= "<patch\n    xmlns=\"http://novell.com/package/metadata/suse/patch\"\n";
    $patch_text .= "    xmlns:yum=\"http://linux.duke.edu/metadata/common\"\n";
    $patch_text .= "    xmlns:rpm=\"http://linux.duke.edu/metadata/rpm\"\n";
    $patch_text .= "    xmlns:suse=\"http://novell.com/package/metadata/suse/common\"\n";
    $patch_text .= "    patchid=\"$patch_id-$patch_name-$iteration\"\n    timestamp=\"$timestamp\"\n    engine=\"1.0\">\n";
    $patch_text .= "  <yum:name>$patch_id-$patch_name</yum:name>\n";
    $patch_text .= "  <summary lang=\"en\">".xml_escape($patch_summary)."</summary>\n";
    $patch_text .= "  <description lang=\"en\">".xml_escape($patch_description)."</description>\n";
    $patch_text .= "  <yum:version ver=\"$iteration\" rel=\"0\"/>\n";
    $patch_text .= "  <rpm:requires>\n";

    my %seen_already;
    for (@package_data) {
	# this package wanted in patch ?
	my $pack_name = $_->{'package.name'};
	my $long_name = $_->{'package.location.href'};
	$long_name =~ s/^.*\/([^\/]*)/$1/;
	next unless grep { $_ eq $pack_name || $_ eq $long_name } @packagelist;

	# duplicate filter
	next if ( $seen_already{"$_->{'package.name'}-$_->{'package.version.epoch'}-$_->{'package.version.ver'}-$_->{'package.version.rel'}"} );
	$seen_already{"$_->{'package.name'}-$_->{'package.version.epoch'}-$_->{'package.version.ver'}-$_->{'package.version.rel'}"} = 1;

	# make the patch require the atom associated with this package
	$patch_text .= "    <rpm:entry kind=\"atom\" name=\"$_->{'package.name'}\"";
        $patch_text .= " epoch=\"$_->{'package.version.epoch'}\"";
        $patch_text .= " ver=\"$_->{'package.version.ver'}\"";
        $patch_text .= " rel=\"$_->{'package.version.rel'}\" flags=\"EQ\"/>\n";
    }
    $patch_text .= "  </rpm:requires>\n";

    # do we have any supplements tags at all
    if ($#patchsupplements ge 0){
	$patch_text .= "  <rpm:supplements>\n";
	for (@patchsupplements){
		$patch_text .= "    <rpm:entry kind=\"patch\" name=\"$_\"/>\n";
	}
	$patch_text .= "  </rpm:supplements>\n";
    }

    $patch_text .="  <category>$category</category>\n";

    if ( $license_file && -f $license_file ) {
		$patch_text .= "  <license-to-confirm>\n";
		open ( LIC, "< $license_file");
		while ( <LIC> ) {
		    $patch_text .= xml_escape($_);
		}
		close ( LIC );
		$patch_text .= "  </license-to-confirm>\n";
    }
    $patch_text .= "  <atoms>\n";

    for (@package_data) {
	# this package wanted in patch ?
	my $pack_name = $_->{'package.name'};
	my $long_name = $_->{'package.location.href'};
	my $freshens;
	$long_name =~ s/^.*\/([^\/]*)/$1/;
	next unless grep { $_ eq $pack_name || $_ eq $long_name } @packagelist;
        # create atom data, mostly just copies from primary package data
	$patch_text .= "    <package xmlns=\"http://linux.duke.edu/metadata/common\" type=\"rpm\">\n";
	$patch_text .= "      <name>$_->{'package.name'}</name>\n";
	$patch_text .= "      <arch>$_->{'package.arch'}</arch>\n";
	$patch_text .= "      <version epoch=\"$_->{'package.version.epoch'}\" ver=\"$_->{'package.version.ver'}\" rel=\"$_->{'package.version.rel'}\"/>\n";
	$patch_text .= "      <checksum type=\"sha\" pkgid=\"YES\">$_->{'package.checksum'}</checksum>\n";
	$patch_text .= "      <time file=\"$_->{'package.time.file'}\" build=\"$_->{'package.time.build'}\"/>\n";
	$patch_text .= "      <size package=\"$_->{'package.size.package'}\" installed=\"$_->{'package.size.installed'}\" archive=\"$_->{'package.size.archive'}\"/>\n";
	$patch_text .= "      <location href=\"$_->{'package.location.href'}\"/>\n";
	# here starts the association of the atom to the real package
	$patch_text .= "      <format>\n        <rpm:requires>\n";
	$patch_text .= "          <rpm:entry kind=\"package\"";
	$patch_text .= " name=\"$_->{'package.name'}\"";
	$patch_text .= " epoch=\"$_->{'package.version.epoch'}\"";
	$patch_text .= " ver=\"$_->{'package.version.ver'}\"";
	$patch_text .= " rel=\"$_->{'package.version.rel'}\"";
	$patch_text .= " flags=\"GE\"/>\n";
	$patch_text .= "        </rpm:requires>\n";
	# now have the atom pulled in, if this package is installed on the system

	$patch_text .= "        <suse:freshens>\n";
	# do we need to override the freshens tag
	if ($#pkgrefresh ge 0){
		for(@pkgrefresh){
			$patch_text .= "          <suse:entry kind=\"package\" name=\"$_\"/>\n";
		}
	}
	else{
		$patch_text .= "          <suse:entry kind=\"package\" name=\"$_->{'package.name'}\"/>\n";
	}
	
	$patch_text .= "        </suse:freshens>\n      </format>\n    </package>\n";
    }

    $patch_text .= "  </atoms>\n</patch>\n";
    open ( PATCH, "> $repodata/patch-$patch_id-$patch_name-$iteration.xml");
    print PATCH $patch_text;
    close ( PATCH );
}

GeneratePatchesXml($repodata);
GenerateRepomdXml($repodata);
unlink "$repodata/repomd.xml.asc";
unlink "$repodata/repomd.xml.key";
if ( $do_signing ) {
    if ( $sign_id ) {
	system("gpg -a -b --default-key \"$sign_id\" $repodata/repomd.xml");
    } else {
	system("gpg -a -b $repodata/repomd.xml");
	$sign_id = `gpg --verify $repodata/repomd.xml.asc 2>&1 | sed -ne "s/.* ID //p"`;
	chomp ($sign_id);
    }
    system("gpg -a --export \"$sign_id\" > $repodata/repomd.xml.key") if ( $sign_id );
}

if ( $validate && @packagelist ){
   validate($patch_name);
}

#system("//bin/sign -d $patches_directory/repomd.xml");
#system("cp $patchinfo_lib_dir/public-key $patches_directory/repomd.xml.key");

sub GeneratePatchesXml {
    my ($patches_directory) = @_;
    opendir(PDIR,"$patches_directory");
    my @all_patches = grep {/^patch-.*\.xml$/} readdir(PDIR);
    closedir(PDIR);
    open (NEWDIR,">$patches_directory/patches.xml");
    print NEWDIR "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    print NEWDIR "<patches xmlns=\"http://novell.com/package/metadata/suse/patches\">\n";
    my $pdirname = $patches_directory;
    $pdirname =~ s/^.*\///;
    for (@all_patches) {
        my ($checksum,$dummy) = split('\s+',`sha1sum "$patches_directory/$_"`);
        $_ =~ s/.xml$//;
        my $name = $_;
        $name =~ s/^patch-//;
        print NEWDIR "  <patch id=\"$name\">\n";
        print NEWDIR "    <checksum type=\"sha\">$checksum</checksum>\n";
        print NEWDIR "    <location href=\"$pdirname/$_.xml\"/>\n";
        print NEWDIR "  </patch>\n";
    }
    print NEWDIR "</patches>\n";
}

sub GenerateRepomdXml {
    my ($patches_directory) = @_;
    opendir(PDIR,"$patches_directory");
    my @all_patches = grep {/\.xml(\.gz)?$/} readdir(PDIR);
    closedir(PDIR);
    open (NEWDIR,">$patches_directory/repomd.xml");
    print NEWDIR "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    print NEWDIR "<repomd xmlns=\"http://linux.duke.edu/metadata/repo\">\n";
    my $pdirname = $patches_directory;
    $pdirname =~ s/^.*\///;
    for (@all_patches) {
        next if (/^patch-/);
        next if (/^repomd/);
        my ($checksum,$dummy) = split('\s+',`sha1sum "$patches_directory/$_"`);
        my $o_checksum = $checksum;
        if ( /\.gz/ ) {
            ($o_checksum,my $dummy) = split('\s+',`gzip -dc "$patches_directory/$_" | sha1sum`);
        }
        my $timestamp = stat("$patches_directory/$_")->mtime;
        my $filename = $_;
        $_ =~ s/.xml(\.gz)?$//;
        print NEWDIR "  <data type=\"$_\">\n";
        print NEWDIR "    <location href=\"$pdirname/$filename\"/>\n";
        print NEWDIR "    <checksum type=\"sha\">$checksum</checksum>\n";
        print NEWDIR "    <timestamp>$timestamp</timestamp>\n";
        print NEWDIR "    <open-checksum type=\"sha\">$o_checksum</open-checksum>\n";
        print NEWDIR "  </data>\n";
    }
    print NEWDIR "</repomd>\n";
    close ( NEWDIR );
}

sub usage {
       print <<EOF

$0 Usage Information:

 $0 [OPTION] ... <base_dir>

 <base_dir> is the base directory to the repository.

 -i <PATCH_ID>          : Patch id, needs to be unique in world, will be prefixed
                          by "hostname -d" as default followed by the name of the
                          first package. Dots in "hostname -d" will be converted
                          to "_"s.
 -n <PATCH_NAME>        : required parameter, terse patch name, like aaa_base
 -v <PATCH_VERSION>     : default to "0"/first non-existant if not given
 -s <PATCH_SUMMARY>     : default to the package summary of the first RPM
                          specified on the command line
 -d <PATCH_DESCRTIPON>  : Long description, defaults to the package description
                          of the first RPM specified on commandline
 -C <CATEGORY>	        : Category for the patch.  Defaults to recommended.
                          Possible values: security, recommended, optional
 
 -u                     : run createrepo to update repository and take care of keeping
                          the patch*xml files - use when augmenting existing repository
                          with new patches.
 -S                     : detached sign the repomd.xml file
 -I <KEY_ID>            : key-id to use for signing the repomd.xml file, if not given
                          gpg will use the default signing key
 -L <CONFIRMATION_FILE> : add a confirmation request (EULA or Reboot request) to the patch, 
                          read from the file specified
 --validate             : use xmllint to validate the resulting xml file
 
 -p <rpm_basename>[,rpm_basename...]          : List of RPMs for this patch.
                                                You need at least one.
 --patchsupplements <PATCH_NAME>[,PATCH_NAME] : List of patches which are supplemented 
                                                by this patch
 --pkgfreshens rpm_basename                   : optional parameter which will override 
                                                the default freshens value
EOF
;

   exit 1;
}

sub xml_escape {
    my ($text) = @_;
    $text =~ s/&/&amp;/sg;
    $text =~ s/</&lt;/sg;
    $text =~ s/>/&gt;/sg;
    $text =~ s/"/&quot;/sg;
    #$text =~ s/([\x80-\xff])/$1 lt "\xC0" ? "\xC2$1" : "\xC3".chr(ord($1)-64)/ge;
    return $text;
}

sub validate {
	my $file = shift;
	my $result = `xmllint --valid $file`;
	print $result;
}

# Logfile:
# $Log: createpatch,v $
# Revision 1.5  2007/12/11 13:42:01  lrupp
# - added "license-to-confirm"
# - check for createrepo and warn user if not exist
#
# Revision 1.4  2007/07/13 08:44:35  lrupp
# addad @packagelist to condition
#
# Revision 1.3  2007/05/15 15:59:21  lrupp
# beautify usage message
#
#
