##
##  rpmx -- RPM eXtension (Perl program)
##  Copyright (c) 2001-2002 Ralf S. Engelschall <rse@engelschall.com>
##
##  list all files in the hierarchy which are not listed in RPM database
##  $ rpm --stowaway
##
##  generate repository information (on server only)
##  $ rpm --makeindex <pkgdir>
##
##  update local database with latest repository information
##  $ rpm --update
##
##  update the package sources by downloading missing files
##  $ rpm --fetch <spec-file>
##
##  make a proxy package
##  $ rpm --makeproxy <binary-rpm>
##
##  query repository information
##  $ rpm -rqa
##  $ rpm -rqai
##  $ rpm -rqal
##  $ rpm -rqalv
##  $ rpm -rqi package
##  $ rpm -rql package
##  $ rpm -rqlv package
##
##  update already installed packages
##  $ rpm -Uvhs '*'
##
##  install/update a particular package and its dependencies
##  $ rpm -Uvhs file-pattern
##
##  erase a particular package and its dependencies
##  $ rpm -es file-pattern
##

require 5.000;

use IO::File;
use IO::Handle;
use Data::Dumper;

$|++;

##  ______________________________________________________________________
##
##  Determine Configuration and Run-Time Information
##  ______________________________________________________________________
##

my $CFG = {
    'PRG' => {},
    'RC' => {},
    'OPT' => [],
    'OPT_OPT' => {},
    'OPT_ARG' => {},
    'ARG' => [],
};

#   determine path to executables
my $prefix = shift(@ARGV);
$CFG->{PRG}->{"rpm"}   = $prefix."/bin/rpm";
$CFG->{PRG}->{"curl"}  = $prefix."/lib/openpkg/curl";
$CFG->{PRG}->{"bzip2"} = $prefix."/lib/openpkg/bzip2";
$CFG->{PRG}->{"gzip"}  = $prefix."/lib/openpkg/gzip";

#   determine a few RPM rc-file configuration variables
my $var;
my $vars = '';
foreach $var (qw(
    _dbpath _rpmdir _srcrpmdir _tmppath
    _target
    l_prefix
    l_fetch_mirror_0 l_fetch_mirror_1 l_fetch_mirror_2 l_fetch_mirror_3 l_fetch_mirror_4 
    l_fetch_mirror_5 l_fetch_mirror_6 l_fetch_mirror_7 l_fetch_mirror_8 l_fetch_mirror_9 
    l_fetch_backup_0 l_fetch_backup_1 l_fetch_backup_2 l_fetch_backup_3 l_fetch_backup_4 
    l_fetch_backup_5 l_fetch_backup_6 l_fetch_backup_7 l_fetch_backup_8 l_fetch_backup_9 
)) {
    $vars .= "${var}=\%{${var}};";
}
my @assign = split(/;/, `$CFG->{PRG}->{"rpm"} --eval '$vars'`);
foreach $assign (@assign) {
    if ($assign =~ m|^(\S+)=(.*)$|s) {
        $CFG->{RC}->{$1} = $2;
    }
}

#   parse argument line
#foreach my $arg (@ARGV) {
#    print "<$arg>";
#}
#print "\n";
my $op = 'pass';
my $isopt = 1;
my $optname = '';
my $arg;
foreach $arg (@ARGV) {
    if ($arg =~ m/^--(stowaway|makeindex|update|fetch|makeproxy|repo|smart)$/) {
        $op = $1;
        next;
    }
    if ($arg eq '--') {
        $isopt = 0;
        next;
    }
    if ($isopt) {
        push(@{$CFG->{OPT}}, $arg);
        if ($arg =~ m|^-.|) {
            $optname = $arg;
            $CFG->{OPT_OPT}->{$optname} = 1;
            $CFG->{OPT_ARG}->{$optname} = '--';
        }
        else {
            $CFG->{OPT_ARG}->{$optname} = $arg;
        } 
    }
    else {
        push(@{$CFG->{ARG}}, $arg);
    }
}
#print Data::Dumper->Dump([$CFG]);
if ($op eq 'pass') {
    # exec $CFG->{PRG}->{"rpm"} (@{$CFG->{OPT}}, @{$CFG->{ARG}});
}
else {
    my $rc;
    #eval "\$rc = \&op_${op}(\$CFG);";
    if ($op eq 'stowaway') {
        $rc = &op_stowaway($CFG);
    }
    elsif ($op eq 'makeindex') {
        $rc = &op_makeindex($CFG);
    }
    elsif ($op eq 'update') {
        $rc = &op_update($CFG);
    }
    elsif ($op eq 'fetch') {
        $rc = &op_fetch($CFG);
    }
    elsif ($op eq 'makeproxy') {
        $rc = &op_makeproxy($CFG);
    }
    elsif ($op eq 'repo') {
        $rc = &op_repo($CFG);
    }
    elsif ($op eq 'smart') {
        $rc = &op_smart($CFG);
    }
    exit($rc);
}

##  ______________________________________________________________________
##
##  Database Handling
##  ______________________________________________________________________
##

#   read database file from disk
sub db_load {
    my ($dbfile) = @_;

    if (not -f $dbfile) {
        my $DB = {};
        return $DB;
    }
    my $fp = new IO::File ("<$dbfile") || die "cannot read from Database file `$dbfile'";
    my $db = '';
    my $buf; 
    $db .= $buf while ($fp->read($buf, 128*1024));
    $fp->close;

    my $DB = {};
    $db =~ s|<rpm\s+(\S+)>\n(.+?)</rpm>|&load_rpm($DB, $2, $1), ''|isge;
    sub load_rpm {
        my ($DB, $db, $rpm) = @_;
        $DB->{$rpm} = {};
        $db =~ s/<(repos|info|files|requires)>\n(.+?)<\/\1>/$DB->{$rpm}->{uc($1)} = $2, ''/isge;
    }
    return $DB;
}

#   write database file to disk
sub db_store {
    my ($dbfile, $DB) = @_;

    my $fp = new IO::File (">$dbfile") || die "cannot store to Database file `$dbfile'";
    my $rpm;
    foreach $rpm (sort(keys(%{$DB}))) {
        print $fp "<rpm $rpm>\n" .
                  "<repos>\n" .
                  $DB->{$rpm}->{REPOS} .
                  "</repos>\n" .
                  "<requires>\n" .
                  $DB->{$rpm}->{REQUIRES} .
                  "</requires>\n" .
                  "<info>\n" .
                  $DB->{$rpm}->{INFO} .
                  "</info>\n" .
                  "<files>\n" .
                  $DB->{$rpm}->{FILES} .
                  "</files>\n" .
                  "</rpm>\n";
    }
    $fp->close;
    return;
}

##  ______________________________________________________________________
##
##  Fetch an URL
##  ______________________________________________________________________
##

sub fetch_url {
    my ($CFG, $src, $dst) = @_;

    #   make sure file URLs have a fully-qualified scheme.
    if ($src =~ m|^/.+|) {
        $src = "file://$src" 
    }

    #   make sure only schemes curl(1) supports are used.
    if ($src !~ m;^(file|http|ftp)://.+;) {
        return "invalid URL - only file, http and ftp schemes supported";
    }

    #   try to fetch the URL
    unlink("$dst.hdr");
    $rc = system($CFG->{PRG}->{"curl"} .
                 " --location" .
                 " --max-time 1800" .
                 " --connect-timeout 20" .
                 " --dump-header $dst.hdr" .
                 " --output '$dst' '$src'");

    #   check whether command failed
    if ($rc != 0) {
        return "cURL error";
    }

    #   check whether remote sites failed
    if (-s "$dst.hdr") {
        open(FP, "<$dst.hdr");
        my $response = <FP>;
        close(FP);
        if ($response =~ m|^HTTP/[\d.]+\s+(\d+)|) {
            if ($1 ne 200) {
                $response =~ s|\n$||;
                unlink($dst);
                return $response;
            }
        }
    }
     
    #   cleanup
    unlink("$dst.hdr");

    return '';
}

##  ______________________________________________________________________
##
##  List all files which are not known to RPM
##  ______________________________________________________________________
##

sub op_stowaway {
    my ($CFG) = @_;
    my $file;

    my $prefix = $CFG->{RC}->{"l_prefix"};
    my $rpm = $CFG->{PRG}->{"rpm"};
    print "OpenPKG Hierarchy $prefix\n";
    my @known = `$rpm -qla`;
    my %known = ();
    foreach $file (@known) {
        $file =~ s|\n$||s;
        $known{$file} = 1;
    }
    my @exist = `cd $prefix && find . -print 2>/dev/null`;
    foreach $file (sort(@exist)) {
        $file =~ s|^\./|$prefix/|s;
        $file =~ s|\n$||s;
        next if ($file eq '.');
        if (not $known{$file}) {
            next if ($file =~ m|^$prefix/RPM/?|);
            print "$file\n";
        }
    }
    return 0; 
}

##  ______________________________________________________________________
##
##  Generate Repository Index
##  ______________________________________________________________________
##

sub op_makeindex {
    my ($CFG) = @_;

    if ($#{$CFG->{ARG}} ne 0) {
        print STDERR "rpm: option --makeindex requires an argument\n";
        return 1;
    }
    my $dir = $CFG->{ARG}->[0];
    if (not -d $dir) {
        print STDERR "rpm: --makeindex argument `$dir' is not a directory\n";
        return 1;
    }
    my $index = '';
    my $file;
    my $rpm = $CFG->{PRG}->{"rpm"};
    foreach $file (sort(glob("$dir/*.rpm"))) {
        $file =~ s|^$dir/*||;
        $index .= "<rpm $file>\n";
        $index .= "<requires>\n";
        $index .= `$rpm -qp --qf '[%{REQUIRENAME} %{REQUIREFLAGS:depflags} %{REQUIREVERSION}\n]' $dir/$file`;
        $index .= "</requires>\n";
        $index .= "<info>\n";
        $index .= `$rpm -qpi $dir/$file`;
        $index .= "</info>\n";
        $index .= "<files>\n";
        $index .= `$rpm -qplv $dir/$file`;
        $index .= "</files>\n";
        $index .= "</rpm>\n";
    }
    open(FP, "|".$CFG->{PRG}->{"bzip2"}.">$dir/INDEX.bz2");
    print FP $index;
    close(FP);
    return 0;
}

##  ______________________________________________________________________
##
##  Fetch Repository Indices
##  ______________________________________________________________________
##

sub op_update {
    my ($CFG) = @_;

    my $DB = {};
    my $tmpfile = $CFG->{RC}->{_tmppath}."/rpm.update.tmp.".$$;

    for ($i = 9; $i >= 0; $i--) {
        my $repo = $CFG->{RC}->{"l_repo_$i"};
        next if (not defined($repo) or $repo eq '' or $repo eq '-');
        $repo =~ s|([^/])$|$1/|;

        my $src = $repo."INDEX.bz2";

        my $prefix = $src;
        $prefix = substr($prefix, 0, 37)."..." if (length($prefix) > 40);
        printf(STDOUT "%-40s ", $prefix);

        if (($err = &fetch_url($CFG, $src, $tmpfile))) {
            $err = substr($err, 0, 37)."..." if (length($err) > 40);
            print STDOUT "FAILED: $err\n";
            next;
        }

        my $size = (stat($tmpfile))[7];
        print STDOUT "OK: $size bytes\n";

        my $fh = new IO::Handle; 
        open($fh, $CFG->{PRG}->{"bzip2"}." -d -c $tmpfile|");
        my $db = '';
        my $buf; 
        $db .= $buf while ($fh->read($buf, 128*1024));
        $fh->close;

        unlink($tmpfile);

        $db =~ s|<rpm\s+(\S+)>\n(.+?)</rpm>|&load_rpm2($DB, $repo, $2, $1), ''|isge;
        sub load_rpm2 {
            my ($DB, $repo, $db, $rpm) = @_;
            my $target = $CFG->{RC}->{_target};
            return if ($rpm !~ m|\.src\.rpm$| and $rpm !~ m|\.$target\.rpm$|);
            my $repos = ''; 
            if (defined($DB->{$rpm})) {
                $repos = $DB->{$rpm}->{REPOS};
            }
            $repos = $repo."\n".$repos;
            $DB->{$rpm} = {};
            $DB->{$rpm}->{REPOS} = $repos;
            $db =~ s/<(info|files|requires)>\n(.+?)<\/\1>/$DB->{$rpm}->{uc($1)} = $2, ''/isge;
        }
    }

    my $dbfile = $CFG->{RC}->{_dbpath}."/Repository";
    &db_store($dbfile, $DB);

    return 0;
}

##  ______________________________________________________________________
##
##  Repository Queries
##  ______________________________________________________________________
##

sub op_repo {
    my ($CFG) = @_;

    #   argument line consistency check
    if (not defined($CFG->{OPT_OPT}->{"-q"})) {
        print STDERR "rpm: option --repo always requires option -q\n";
        return 1;
    }
    my @optok = (qw(--rcfile -q --all -i --list --verbose));
    my $opt;
    my $check;
    foreach $opt (keys(%{$CFG->{OPT_OPT}})) {
        my $ok = 0;
        foreach $check (@optok) {
            if ($check eq $opt) {
                $ok = 1;
                last;
            }
        }
        if (not $ok) {
            print STDERR "rpm: option $opt not supported in conjunction with option --repo\n";
            return 1;
        }
    }

    #   load database
    my $dbfile = $CFG->{RC}->{_dbpath}."/Repository";
    if (not -f $dbfile) {
        print STDERR "rpm: option --repo requires local repository index.\n";
        print STDERR "rpm: run \"rpm --update\", first.\n";
        return 1;
    }
    my $DB = &db_load($dbfile);

    #   perform query operation
    my $target = $CFG->{RC}->{_target};
    my $rpm;
    my $rpms = {};
    foreach $rpm (sort(keys(%{$DB}))) {
        my $name = $rpm;
        $name =~ s|\.src\.rpm$||;
        $name =~ s|\.${target}\.rpm$||;
        next if (defined($rpms->{$name}));
        $rpms->{$name} = 1;
        if ((defined($CFG->{ARG}->[0]) and $rpm =~ m|^$CFG->{ARG}->[0]|)
            or defined($CFG->{OPT_OPT}->{"--all"})) {
            if (not defined($CFG->{OPT_OPT}->{"--list"}) and not defined($CFG->{OPT_OPT}->{"-i"})) {
                print "$name\n";
            }
            if (defined($CFG->{OPT_OPT}->{"-i"})) {
                print $DB->{$rpm}->{INFO};
            }
            if (defined($CFG->{OPT_OPT}->{"--list"})) {
                my $files = $DB->{$rpm}->{FILES};
                if (not defined($CFG->{OPT_OPT}->{"--verbose"})) {
                    if (not ($files =~ s|^.*\s+(\S+\s+->\s+\S+)\s*$|$1|mg)) {
                        $files =~ s|^.*\s+(\S+)\s*$|$1|mg;
                    }
                }
                print "$files";
            }
        }
    }
}

##  ______________________________________________________________________
##
##  Fetch Operation
##  ______________________________________________________________________
##

sub op_fetch {
    my ($CFG) = @_;
    
    if ($#{$CFG->{ARG}} ne 0) {
        print STDERR "rpm: option --fetch requires an argument\n";
        return 1;
    }
    my $spec = $CFG->{ARG}->[0];
    if (not -f $spec) {
        print STDERR "rpm: spec file `$spec' not found\n";
        return 1;
    }

    #   determine package name and source directory
    my $name = $spec;
    $name =~ s|\.[^.]+$||;
    $name =~ s|^.+/([^/]+)$|$1|;
    my $local_srcdir = `$CFG->{PRG}->{"rpm"} --define 'name $name' --eval '%_sourcedir'`;
    $local_srcdir =~ s|\n+$||s;
    my $local_specdir = `$CFG->{PRG}->{"rpm"} --define 'name $name' --eval '%_specdir'`;
    $local_specdir =~ s|\n+$||s;

    #   make sure source and spec directory actually exists
    if (not -d $local_srcdir) {
        print(STDERR "rpm: Creating directory $local_srcdir\n");
        system("mkdir $local_srcdir");
    }
    if (not -d $local_specdir) {
        print(STDERR "rpm: Creating directory $local_specdir\n");
        system("mkdir $local_specdir");
    }

    #   parse spec file
    my $DEF = {};
    my $SRC = {};
    open(SPEC, "<$spec");
    while (<SPEC>) {
        s|\n+$||s;
        if (m/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.+?)\s*$/) {
            $DEF->{lc($1)} = $2;
        }
        if (m/^%define\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(.+?)\s*$/) {
            $DEF->{lc($1)} = $2;
        }
        if (m/^((Source|Patch)[0-9]+?):\s*(.+?)\s*$/i) {
            my ($remote_srcid, $remote_srcurl) = ($1, $3);

            #   determine expanded remote source location
            1 while ($remote_srcurl =~ s|%{?([a-zA-Z_][a-zA-Z0-9_]*)}?|$DEF->{lc($1)}|sge);
            my $remote_srcdir  = '';
            my $remote_srcfile = $remote_srcurl;
            if ($remote_srcfile =~ m|^(.+)/([^/]+)$|) {
                $remote_srcdir  = $1;
                $remote_srcfile = $2;
            }

            #   display remote source location
            my $file = $remote_srcfile;
            $file = substr($file, 0, 40) if (length($file) > 40);
            printf(STDERR "%-9s %-40s ", "$remote_srcid:", $file);

            if (-f "$local_srcdir/$remote_srcfile" or -f "$local_specdir/$remote_srcfile") {
                #   source already on local filesystem
                my $size;
                if (-f "$local_srcdir/$remote_srcfile") {
                    $size = (stat("$local_srcdir/$remote_srcfile"))[7];
                }
                else {
                    $size = (stat("$local_specdir/$remote_srcfile"))[7];
                }
                if ($size > 1024*1024) {
                    $size = sprintf("%.1fMB", $size / (1024*1024));
                }
                elsif ($size > 1024) {
                    $size = sprintf("%.0fKB", $size / 1024);
                }
                else {
                    $size = sprintf("%d", $size);
                }
                print STDERR "...OK [$size]\n";
            }
            else {
                #   fetch source onto local filesystem
                print STDERR "...MISSING\n";

                #   determine prioritized list of possible source locations
                my @fetch_srcdir = ();
                for ($i = 0; $i <= 9; $i++) {
                    my $url = $CFG->{RC}->{"l_fetch_mirror_$i"};
                    next if (not defined($url) or $url eq '' or $url eq '-');
                    1 while ($url =~ s|%{?([a-zA-Z_][a-zA-Z0-9_]*)}?|$DEF->{lc($1)}|sge);
                    push(@fetch_srcdir, $url);
                }
                push(@fetch_srcdir, $remote_srcdir);
                for ($i = 0; $i <= 9; $i++) {
                    my $url = $CFG->{RC}->{"l_fetch_backup_$i"};
                    next if (not defined($url) or $url eq '' or $url eq '-');
                    1 while ($url =~ s|%{?([a-zA-Z_][a-zA-Z0-9_]*)}?|$DEF->{lc($1)}|sge);
                    push(@fetch_srcdir, $url);
                }

                #   try to fetch from all possible locations
                foreach $fetch_srcdir (@fetch_srcdir) {
                    $fetch_srcdir =~ s|/+$||s;
                    print STDERR "rpm: Fetching from $fetch_srcdir/\n";
                    if (($err = &fetch_url($CFG, "$fetch_srcdir/$remote_srcfile", "$local_srcdir/$remote_srcfile"))) {
                        $err = substr($err, 0, 37)."..." if (length($err) > 40);
                        print STDOUT "rpm: Error: $err\n";
                        next;
                    }
                    last;
                }
                if (not -f "$local_srcdir/$remote_srcfile") {
                    print STDERR "rpm: Failed to fetch source file `$remote_srcfile'\n";
                    return 1;
                }
            }
        }
    }
    close(SPEC);

    return 0;
}

##  ______________________________________________________________________
##
##  Make Proxy Operation
##  ______________________________________________________________________
##

sub op_makeproxy {
    my ($CFG) = @_;

    if ($#{$CFG->{ARG}} != 0) {
        print STDERR "rpm: option --makeproxy requires exactly one argument\n";
        return 1;
    }
    my $perl    = $^X;
    my $prefix  = $CFG->{RC}->{"l_prefix"};
    my $rpm     = $CFG->{PRG}->{"rpm"};
    my $tmpdir  = $CFG->{RC}->{"_tmppath"};
    exec $perl, "$prefix/lib/openpkg/mkproxyrpm.pl", "--rpm=$rpm", "--tmpdir=$tmpdir", @{$CFG->{ARG}};
}

##  ______________________________________________________________________
##
##  Smart Operations
##  ______________________________________________________________________
##

sub op_smart {
    my ($CFG) = @_;

    if ($#{$CFG->{ARG}} ne 0) {
        print STDERR "rpm: option --smart requires one argument\n";
        return 1;
    }
    my $package = $CFG->{ARG}->[0];

    #   load database
    my $dbfile = $CFG->{RC}->{_dbpath}."/Repository";
    if (not -f $dbfile) {
        print STDERR "rpm: option --smart requires local repository index.\n";
        print STDERR "rpm: run \"rpm --update\", first.\n";
        return 1;
    }
    my $DB = &db_load($dbfile);

    my $deps = [];
    &find_deps($CFG, $DB, $deps, $package, '=', 'ANY');
    sub find_deps {
        my ($CFG, $DB, $deps, $pkg, $op, $ver) = @_;
        push(@{$deps}, "$pkg $op $ver");
        my $rpm;
        foreach $rpm (keys(%{$DB})) {
            if (&match_rpm($CFG, $rpm, $pkg, $op, $ver)) {
                my $req;
                foreach $req (split(/\n/, $DB->{$rpm}->{REQUIRES})) {
                    $req =~ s|^\s+||sg;
                    $req =~ s|\s+$||sg;
                    if ($req =~ m|^OpenPKG$| or
                        $req =~ m|^rpmlib\(.+\)|) {
                        next;
                    }
                    if ($req =~ m|^(\S+)$|) {
                        $req .= " = ANY";
                    }
                    if ($req =~ m|^(\S+)\s+([=><]+)\s+(\S+)$|) {
                        &find_deps($CFG, $DB, $deps, $1, $2, $3);
                    }
                }
            }
        }
    }
    sub match_rpm {
        my ($CFG, $rpm, $name, $op, $ver) = @_;
        my $rc = 0;
        my $target = $CFG->{RC}->{_target};
        $rpm =~ s|\.src\.rpm$||;
        $rpm =~ s|\.${target}\.rpm$||;
        if ($rpm =~ m|^(\S+)-([^-]+-[^-]+)$|) {
            my ($tname, $tver) = ($1, $2);
            if ($name eq $tname and &match_version($ver, $op, $tver)) {
                $rc = 1;
            }
        }
        return $rc;
    }
    sub match_version {
        my ($ver, $op, $tver) = @_;
        return 1;
    }
    my $dep;
    foreach $dep (@{$deps}) {
        print "$dep\n";
    }
}

