################################################################ # # Copyright (c) 1995-2014 SUSE Linux Products GmbH # # 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 # ################################################################ package Build::Rpm; our $unfilteredprereqs = 0; our $conflictdeps = 0; use strict; use Digest::MD5; sub expr { my $expr = shift; my $lev = shift; $lev ||= 0; my ($v, $v2); $expr =~ s/^\s+//; my $t = substr($expr, 0, 1); if ($t eq '(') { ($v, $expr) = expr(substr($expr, 1), 0); return undef unless defined $v; return undef unless $expr =~ s/^\)//; } elsif ($t eq '!') { ($v, $expr) = expr(substr($expr, 1), 5); return undef unless defined $v; $v = 0 if $v && $v eq '\"\"'; $v =~ s/^0+/0/ if $v; $v = !$v; } elsif ($t eq '-') { ($v, $expr) = expr(substr($expr, 1), 5); return undef unless defined $v; $v = -$v; } elsif ($expr =~ /^([0-9]+)(.*?)$/) { $v = $1; $expr = $2; } elsif ($expr =~ /^([a-zA-Z_0-9]+)(.*)$/) { $v = "\"$1\""; $expr = $2; } elsif ($expr =~ /^(\".*?\")(.*)$/) { $v = $1; $expr = $2; } else { return; } return ($v, $expr) if $lev >= 5; while (1) { $expr =~ s/^\s+//; if ($expr =~ /^&&/) { return ($v, $expr) if $lev > 1; ($v2, $expr) = expr(substr($expr, 2), 1); return undef unless defined $v2; $v = 0 if $v && $v eq '\"\"'; $v =~ s/^0+/0/; $v2 = 0 if $v2 && $v2 eq '\"\"'; $v2 =~ s/^0+/0/; $v &&= $v2; } elsif ($expr =~ /^\|\|/) { return ($v, $expr) if $lev > 1; ($v2, $expr) = expr(substr($expr, 2), 1); return undef unless defined $v2; $v = 0 if $v && $v eq '\"\"'; $v =~ s/^0+/0/; $v2 = 0 if $v2 && $v2 eq '\"\"'; $v2 =~ s/^0+/0/; $v ||= $v2; } elsif ($expr =~ /^>=/) { return ($v, $expr) if $lev > 2; ($v2, $expr) = expr(substr($expr, 2), 2); return undef unless defined $v2; $v = (($v =~ /^\"/) ? $v ge $v2 : $v >= $v2) ? 1 : 0; } elsif ($expr =~ /^>/) { return ($v, $expr) if $lev > 2; ($v2, $expr) = expr(substr($expr, 1), 2); return undef unless defined $v2; $v = (($v =~ /^\"/) ? $v gt $v2 : $v > $v2) ? 1 : 0; } elsif ($expr =~ /^<=/) { return ($v, $expr) if $lev > 2; ($v2, $expr) = expr(substr($expr, 2), 2); return undef unless defined $v2; $v = (($v =~ /^\"/) ? $v le $v2 : $v <= $v2) ? 1 : 0; } elsif ($expr =~ /^ 2; ($v2, $expr) = expr(substr($expr, 1), 2); return undef unless defined $v2; $v = (($v =~ /^\"/) ? $v lt $v2 : $v < $v2) ? 1 : 0; } elsif ($expr =~ /^==/) { return ($v, $expr) if $lev > 2; ($v2, $expr) = expr(substr($expr, 2), 2); return undef unless defined $v2; $v = (($v =~ /^\"/) ? $v eq $v2 : $v == $v2) ? 1 : 0; } elsif ($expr =~ /^!=/) { return ($v, $expr) if $lev > 2; ($v2, $expr) = expr(substr($expr, 2), 2); return undef unless defined $v2; $v = (($v =~ /^\"/) ? $v ne $v2 : $v != $v2) ? 1 : 0; } elsif ($expr =~ /^\+/) { return ($v, $expr) if $lev > 3; ($v2, $expr) = expr(substr($expr, 1), 3); return undef unless defined $v2; $v += $v2; } elsif ($expr =~ /^-/) { return ($v, $expr) if $lev > 3; ($v2, $expr) = expr(substr($expr, 1), 3); return undef unless defined $v2; $v -= $v2; } elsif ($expr =~ /^\*/) { ($v2, $expr) = expr(substr($expr, 1), 4); return undef unless defined $v2; $v *= $v2; } elsif ($expr =~ /^\//) { ($v2, $expr) = expr(substr($expr, 1), 4); return undef unless defined $v2 && 0 + $v2; $v /= $v2; } elsif ($expr =~ /^([=&|])/) { warn("syntax error while parsing $1$1\n"); return ($v, $expr); } else { return ($v, $expr); } } } sub adaptmacros { my ($macros, $optold, $optnew) = @_; for (keys %$optold) { delete $macros->{$_}; } for (keys %$optnew) { $macros->{$_} = $optnew->{$_}; } return $optnew; } sub grabargs { my ($macname, $getopt, @args) = @_; my %m; $m{'0'} = $macname; $m{'**'} = join(' ', @args); my %go; %go = ($getopt =~ /(.)(:*)/sg) if defined $getopt; while (@args && $args[0] =~ s/^-//) { my $o = shift @args; last if $o eq '-'; while ($o =~ /^(.)(.*)$/) { if ($go{$1}) { my $arg = $2; $arg = shift(@args) if @args && $arg eq ''; $m{"-$1"} = "-$1 $arg"; $m{"-$1*"} = $arg; last; } $m{"-$1"} = "-$1"; $o = $2; } } $m{'#'} = scalar(@args); my $i = 1; for (@args) { $m{$i} = $_; $i++; } $m{'*'} = join(' ', @args); return \%m; } # xspec may be passed as array ref to return the parsed spec files # an entry in the returned array can be # - a string: verbatim line from the original file # - a two element array ref: # - [0] original line # - [1] undef: line unused due to %if # - [1] scalar: line after macro expansion. Only set if it's a build deps # line and build deps got modified or 'save_expanded' is set in # config sub parse { my ($config, $specfile, $xspec) = @_; my $packname; my $exclarch; my $badarch; my @subpacks; my @packdeps; my @prereqs; my $hasnfb; my $nfbline; my %macros; my %macros_args; my $ret = {}; my $ifdeps; my $specdata; local *SPEC; if (ref($specfile) eq 'GLOB') { *SPEC = *$specfile; } elsif (ref($specfile) eq 'ARRAY') { $specdata = [ @$specfile ]; } elsif (!open(SPEC, '<', $specfile)) { warn("$specfile: $!\n"); $ret->{'error'} = "open $specfile: $!"; return $ret; } my @macros = @{$config->{'macros'}}; my $skip = 0; my $main_preamble = 1; my $preamble = 1; my $inspec = 0; my $hasif = 0; my $lineno = 0; while (1) { my $line; if (@macros) { $line = shift @macros; $hasif = 0 unless @macros; } elsif ($specdata) { $inspec = 1; last unless @$specdata; $line = shift @$specdata; ++$lineno; if (ref $line) { $line = $line->[0]; # verbatim line push @$xspec, $line if $xspec; $xspec->[-1] = [ $line, undef ] if $xspec && $skip; next; } } else { $inspec = 1; $line = ; last unless defined $line; chomp $line; ++$lineno; } push @$xspec, $line if $inspec && $xspec; if ($line =~ /^#\s*neededforbuild\s*(\S.*)$/) { if (defined $hasnfb) { $xspec->[-1] = [ $xspec->[-1], undef ] if $inspec && $xspec; next; } $hasnfb = $1; $nfbline = \$xspec->[-1] if $inspec && $xspec; next; } if ($line =~ /^\s*#/) { next unless $line =~ /^#!Build(?:Ignore|Conflicts)\s*:/i; } my $expandedline = ''; if (!$skip && ($line =~ /%/)) { my $tries = 0; my @expandstack; my $optmacros = {}; # newer perls: \{((?:(?>[^{}]+)|(?2))*)\} reexpand: while ($line =~ /^(.*?)%(\{([^\}]+)\}|[\?\!]*[0-9a-zA-Z_]+|%|\*\*?|#|\()(.*?)$/) { if ($tries++ > 1000) { print STDERR "Warning: spec file parser ",($lineno?" line $lineno":''),": macro too deeply nested\n" if $config->{'warnings'}; $line = 'MACRO'; last; } $expandedline .= $1; $line = $4; my $macname = defined($3) ? $3 : $2; my $macorig = $2; my $macdata; my $macalt; if (defined($3)) { if ($macname =~ /{/) { # { while (($macname =~ y/{/{/) > ($macname =~ y/}/}/)) { last unless $line =~ /^([^}]*)}(.*)$/; $macname .= "}$1"; $macorig .= "$1}"; $line = $2; } } $macdata = ''; if ($macname =~ /^([^\s:]+)([\s:])(.*)$/) { $macname = $1; if ($2 eq ':') { $macalt = $3; } else { $macdata = $3; } } } my $mactest = 0; if ($macname =~ /^\!\?/ || $macname =~ /^\?\!/) { $mactest = -1; } elsif ($macname =~ /^\?/) { $mactest = 1; } $macname =~ s/^[\!\?]+//; if ($macname eq '%') { $expandedline .= '%'; next; } elsif ($macname eq '(') { print STDERR "Warning: spec file parser",($lineno?" line $lineno":''),": can't expand %(...)\n" if $config->{'warnings'}; $line = 'MACRO'; last; } elsif ($macname eq 'define' || $macname eq 'global') { if ($line =~ /^\s*([0-9a-zA-Z_]+)(?:\(([^\)]*)\))?\s*(.*?)$/) { my $macname = $1; my $macargs = $2; my $macbody = $3; if (defined $macargs) { $macros_args{$macname} = $macargs; } else { delete $macros_args{$macname}; } $macros{$macname} = $macbody; } $line = ''; last; } elsif ($macname eq 'defined' || $macname eq 'with' || $macname eq 'undefined' || $macname eq 'without' || $macname eq 'bcond_with' || $macname eq 'bcond_without') { my @args; if ($macorig =~ /^\{(.*)\}$/) { @args = split(' ', $1); shift @args; } else { @args = split(' ', $line); $line = ''; } next unless @args; if ($macname eq 'bcond_with') { $macros{"with_$args[0]"} = 1 if exists $macros{"_with_$args[0]"}; next; } if ($macname eq 'bcond_without') { $macros{"with_$args[0]"} = 1 unless exists $macros{"_without_$args[0]"}; next; } $args[0] = "with_$args[0]" if $macname eq 'with' || $macname eq 'without'; $line = ((exists($macros{$args[0]}) ? 1 : 0) ^ ($macname eq 'undefined' || $macname eq 'without' ? 1 : 0)).$line; } elsif ($macname eq 'expand') { $macalt = $macros{$macname} unless defined $macalt; $macalt = '' if $mactest == -1; push @expandstack, ($expandedline, $line, undef); $line = $macalt; $expandedline = ''; } elsif (exists($macros{$macname})) { if (!defined($macros{$macname})) { print STDERR "Warning: spec file parser",($lineno?" line $lineno":''),": can't expand '$macname'\n" if $config->{'warnings'}; $line = 'MACRO'; last; } if (defined($macros_args{$macname})) { # macro with args! if (!defined($macdata)) { $line =~ /^\s*([^\n]*).*$/; $macdata = $1; $line = ''; } push @expandstack, ($expandedline, $line, $optmacros); $optmacros = adaptmacros(\%macros, $optmacros, grabargs($macname, $macros_args{$macname}, split(' ', $macdata))); $line = $macros{$macname}; $expandedline = ''; next; } $macalt = $macros{$macname} unless defined $macalt; $macalt = '' if $mactest == -1; if ($macalt =~ /%/) { push @expandstack, ('', $line, 1) if $line ne ''; $line = $macalt; } else { $expandedline .= $macalt; } } elsif ($mactest) { $macalt = '' if !defined($macalt) || $mactest == 1; if ($macalt =~ /%/) { push @expandstack, ('', $line, 1) if $line ne ''; $line = $macalt; } else { $expandedline .= $macalt; } } else { $expandedline .= "%$macorig" unless $macname =~ /^-/; } } $line = $expandedline . $line; if (@expandstack) { my $m = pop(@expandstack); if ($m) { $optmacros = adaptmacros(\%macros, $optmacros, $m) if ref $m; $expandstack[-2] .= $line; $line = pop(@expandstack); $expandedline = pop(@expandstack); } else { my $todo = pop(@expandstack); $expandedline = pop(@expandstack); push @expandstack, ('', $todo, 1) if $todo ne ''; } goto reexpand; } } if ($line =~ /^\s*%else\b/) { $skip = 1 - $skip if $skip < 2; next; } if ($line =~ /^\s*%endif\b/) { $skip-- if $skip; next; } $skip++ if $skip && $line =~ /^\s*%if/; if ($skip) { $xspec->[-1] = [ $xspec->[-1], undef ] if $xspec; $ifdeps = 1 if $line =~ /^(BuildRequires|BuildPrereq|BuildConflicts|\#\!BuildIgnore|\#\!BuildConflicts)\s*:\s*(\S.*)$/i; next; } if ($line =~ /^\s*%ifarch(.*)$/) { my $arch = $macros{'_target_cpu'} || 'unknown'; my @archs = grep {$_ eq $arch} split(/\s+/, $1); $skip = 1 if !@archs; $hasif = 1; next; } if ($line =~ /^\s*%ifnarch(.*)$/) { my $arch = $macros{'_target_cpu'} || 'unknown'; my @archs = grep {$_ eq $arch} split(/\s+/, $1); $skip = 1 if @archs; $hasif = 1; next; } if ($line =~ /^\s*%ifhostarch(.*)$/) { my $hostarch = $macros{'hostarch'} || 'unknown'; my @hostarchs = grep {$_ eq $hostarch} split(/\s+/, $1); $skip = 1 if !@hostarchs; $hasif = 1; next; } if ($line =~ /^\s*%ifnhostarch(.*)$/) { my $hostarch = $macros{'hostarch'} || 'unknown'; my @hostarchs = grep {$_ eq $hostarch} split(/\s+/, $1); $skip = 1 if @hostarchs; $hasif = 1; next; } if ($line =~ /^\s*%ifos(.*)$/) { my $os = $macros{'_target_os'} || 'unknown'; my @oss = grep {$_ eq $os} split(/\s+/, $1); $skip = 1 if !@oss; $hasif = 1; next; } if ($line =~ /^\s*%ifnos(.*)$/) { my $os = $macros{'_target_os'} || 'unknown'; my @oss = grep {$_ eq $os} split(/\s+/, $1); $skip = 1 if @oss; $hasif = 1; next; } if ($line =~ /^\s*%if(.*)$/) { my ($v, $r) = expr($1); $v = 0 if $v && $v eq '\"\"'; $v =~ s/^0+/0/ if $v; $skip = 1 unless $v; $hasif = 1; next; } if ($main_preamble) { if ($line =~ /^(Name|Version|Disttag|Release)\s*:\s*(\S+)/i) { $ret->{lc $1} = $2; $macros{lc $1} = $2; } elsif ($line =~ /^(Source\d*|Patch\d*|Url)\s*:\s*(\S+)/i) { $ret->{lc $1} = $2; } elsif ($line =~ /^ExclusiveArch\s*:\s*(.*)/i) { $exclarch ||= []; push @$exclarch, split(' ', $1); } elsif ($line =~ /^ExcludeArch\s*:\s*(.*)/i) { $badarch ||= []; push @$badarch, split(' ', $1); } } if (@subpacks && $preamble && exists($ret->{'version'}) && $line =~ /^Version\s*:\s*(\S+)/i) { $ret->{'multiversion'} = 1 if $ret->{'version'} ne $1; } if ($line =~ /^(?:Requires\(pre\)|Requires\(post\)|PreReq)\s*:\s*(\S.*)$/i) { my $deps = $1; my @deps = $deps =~ /([^\s\[,]+)(\s+[<=>]+\s+[^\s\[,]+)?(\s+\[[^\]]+\])?[\s,]*/g; while (@deps) { my ($pack, $vers, $qual) = splice(@deps, 0, 3); if (!$unfilteredprereqs && $pack =~ /^\//) { $ifdeps = 1; next unless $config->{'fileprovides'}->{$pack}; } push @prereqs, $pack unless grep {$_ eq $pack} @prereqs; } next; } if ($preamble && ($line =~ /^(BuildRequires|BuildPrereq|BuildConflicts|\#\!BuildIgnore|\#\!BuildConflicts)\s*:\s*(\S.*)$/i)) { my $what = $1; my $deps = $2; $ifdeps = 1 if $hasif; # XXX: weird syntax addition. can append arch or project to dependency # BuildRequire: foo > 17 [i586,x86_64] # BuildRequire: foo [home:bar] # BuildRequire: foo [!home:bar] my @deps = $deps =~ /([^\s\[,]+)(\s+[<=>]+\s+[^\s\[,]+)?(\s+\[[^\]]+\])?[\s,]*/g; my $replace = 0; my @ndeps = (); while (@deps) { my ($pack, $vers, $qual) = splice(@deps, 0, 3); if (defined($qual)) { $replace = 1; my $arch = $macros{'_target_cpu'} || ''; my $proj = $macros{'_target_project'} || ''; $qual =~ s/^\s*\[//; $qual =~ s/\]$//; my $isneg = 0; my $bad; for my $q (split('[\s,]', $qual)) { $isneg = 1 if $q =~ s/^\!//; $bad = 1 if !defined($bad) && !$isneg; if ($isneg) { if ($q eq $arch || $q eq $proj) { $bad = 1; last; } } elsif ($q eq $arch || $q eq $proj) { $bad = 0; } } next if $bad; } $vers = '' unless defined $vers; $vers =~ s/=(>|<)/$1=/; push @ndeps, "$pack$vers"; } $replace = 1 if grep {/^-/} @ndeps; if (lc($what) ne 'buildrequires' && lc($what) ne 'buildprereq') { if ($conflictdeps && $what =~ /conflict/i) { push @packdeps, map {"!$_"} @ndeps; next; } push @packdeps, map {"-$_"} @ndeps; next; } if (defined($hasnfb)) { if ((grep {$_ eq 'glibc' || $_ eq 'rpm' || $_ eq 'gcc' || $_ eq 'bash'} @ndeps) > 2) { # ignore old generated BuildRequire lines. $xspec->[-1] = [ $xspec->[-1], undef ] if $xspec; next; } } push @packdeps, @ndeps; next unless $xspec && $inspec; if ($replace) { my @cndeps = grep {!/^-/} @ndeps; if (@cndeps) { $xspec->[-1] = [ $xspec->[-1], "$what: ".join(' ', @cndeps) ]; } else { $xspec->[-1] = [ $xspec->[-1], '']; } } next; } if ($line =~ /^\s*%package\s+(-n\s+)?(\S+)/) { if ($1) { push @subpacks, $2; } else { push @subpacks, $ret->{'name'}.'-'.$2 if defined $ret->{'name'}; } $preamble = 1; $main_preamble = 0; } if ($line =~ /^\s*%(prep|build|install|check|clean|preun|postun|pretrans|posttrans|pre|post|files|changelog|description|triggerpostun|triggerun|triggerin|trigger|verifyscript)/) { $main_preamble = 0; $preamble = 0; } # do this always? if ($xspec && @$xspec && $config->{'save_expanded'}) { $xspec->[-1] = [ $xspec->[-1], $line ]; } } close SPEC unless ref $specfile; if (defined($hasnfb)) { if (!@packdeps) { @packdeps = split(' ', $hasnfb); } elsif ($nfbline) { $$nfbline = [$$nfbline, undef ]; } } unshift @subpacks, $ret->{'name'} if defined $ret->{'name'}; $ret->{'subpacks'} = \@subpacks; $ret->{'exclarch'} = $exclarch if defined $exclarch; $ret->{'badarch'} = $badarch if defined $badarch; $ret->{'deps'} = \@packdeps; $ret->{'prereqs'} = \@prereqs if @prereqs; $ret->{'configdependent'} = 1 if $ifdeps; return $ret; } ########################################################################### my %rpmstag = ( "SIGTAG_SIZE" => 1000, # Header+Payload size in bytes. */ "SIGTAG_PGP" => 1002, # RSA signature over Header+Payload "SIGTAG_MD5" => 1004, # MD5 hash over Header+Payload "SIGTAG_GPG" => 1005, # DSA signature over Header+Payload "NAME" => 1000, "VERSION" => 1001, "RELEASE" => 1002, "EPOCH" => 1003, "SUMMARY" => 1004, "DESCRIPTION" => 1005, "BUILDTIME" => 1006, "ARCH" => 1022, "OLDFILENAMES" => 1027, "SOURCERPM" => 1044, "PROVIDENAME" => 1047, "REQUIREFLAGS" => 1048, "REQUIRENAME" => 1049, "REQUIREVERSION" => 1050, "NOSOURCE" => 1051, "NOPATCH" => 1052, "SOURCEPACKAGE" => 1106, "PROVIDEFLAGS" => 1112, "PROVIDEVERSION" => 1113, "DIRINDEXES" => 1116, "BASENAMES" => 1117, "DIRNAMES" => 1118, "DISTURL" => 1123, "CONFLICTFLAGS" => 1053, "CONFLICTNAME" => 1054, "CONFLICTVERSION" => 1055, "OBSOLETENAME" => 1090, "OBSOLETEFLAGS" => 1114, "OBSOLETEVERSION" => 1115, "OLDSUGGESTSNAME" => 1156, "OLDSUGGESTSVERSION" => 1157, "OLDSUGGESTSFLAGS" => 1158, "OLDENHANCESNAME" => 1159, "OLDENHANCESVERSION" => 1160, "OLDENHANCESFLAGS" => 1161, "RECOMMENDNAME" => 5046, "RECOMMENDVERSION" => 5047, "RECOMMENDFLAGS" => 5048, "SUGGESTNAME" => 5049, "SUGGESTVERSION" => 5050, "SUGGESTFLAGS" => 5051, "SUPPLEMENTNAME" => 5052, "SUPPLEMENTVERSION" => 5053, "SUPPLEMENTFLAGS" => 5054, "ENHANCENAME" => 5055, "ENHANCEVERSION" => 5056, "ENHANCEFLAGS" => 5057, ); sub rpmq { my ($rpm, @stags) = @_; my @sigtags = grep {/^SIGTAG_/} @stags; @stags = grep {!/^SIGTAG_/} @stags; my $dosigs = @sigtags && !@stags; @stags = @sigtags if $dosigs; my $need_filenames = grep { $_ eq 'FILENAMES' } @stags; push @stags, 'BASENAMES', 'DIRNAMES', 'DIRINDEXES', 'OLDFILENAMES' if $need_filenames; @stags = grep { $_ ne 'FILENAMES' } @stags if $need_filenames; my %stags = map {0 + ($rpmstag{$_} || $_) => $_} @stags; my ($magic, $sigtype, $headmagic, $cnt, $cntdata, $lead, $head, $index, $data, $tag, $type, $offset, $count); local *RPM; my $forcebinary; if (ref($rpm) eq 'ARRAY') { ($headmagic, $cnt, $cntdata) = unpack('N@8NN', $rpm->[0]); if ($headmagic != 0x8eade801) { warn("Bad rpm\n"); return (); } if (length($rpm->[0]) < 16 + $cnt * 16 + $cntdata) { warn("Bad rpm\n"); return (); } $index = substr($rpm->[0], 16, $cnt * 16); $data = substr($rpm->[0], 16 + $cnt * 16, $cntdata); } else { if (ref($rpm) eq 'GLOB') { *RPM = *$rpm; } elsif (!open(RPM, '<', $rpm)) { warn("$rpm: $!\n"); return (); } if (read(RPM, $lead, 96) != 96) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } ($magic, $sigtype) = unpack('N@78n', $lead); if ($magic != 0xedabeedb || $sigtype != 5) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } $forcebinary = 1 if unpack('@6n', $lead) != 1; if (read(RPM, $head, 16) != 16) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } ($headmagic, $cnt, $cntdata) = unpack('N@8NN', $head); if ($headmagic != 0x8eade801) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } if (read(RPM, $index, $cnt * 16) != $cnt * 16) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } $cntdata = ($cntdata + 7) & ~7; if (read(RPM, $data, $cntdata) != $cntdata) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } } my %res = (); if (@sigtags && !$dosigs) { %res = &rpmq(["$head$index$data"], @sigtags); } if (ref($rpm) eq 'ARRAY' && !$dosigs && @$rpm > 1) { my %res2 = &rpmq([ $rpm->[1] ], @stags); %res = (%res, %res2); return %res; } if (ref($rpm) ne 'ARRAY' && !$dosigs) { if (read(RPM, $head, 16) != 16) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } ($headmagic, $cnt, $cntdata) = unpack('N@8NN', $head); if ($headmagic != 0x8eade801) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } if (read(RPM, $index, $cnt * 16) != $cnt * 16) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } if (read(RPM, $data, $cntdata) != $cntdata) { warn("Bad rpm $rpm\n"); close RPM unless ref($rpm); return (); } } close RPM unless ref($rpm); # return %res unless @stags; while($cnt-- > 0) { ($tag, $type, $offset, $count, $index) = unpack('N4a*', $index); $tag = 0+$tag; if ($stags{$tag} || !@stags) { eval { my $otag = $stags{$tag} || $tag; if ($type == 0) { $res{$otag} = [ '' ]; } elsif ($type == 1) { $res{$otag} = [ unpack("\@${offset}c$count", $data) ]; } elsif ($type == 2) { $res{$otag} = [ unpack("\@${offset}c$count", $data) ]; } elsif ($type == 3) { $res{$otag} = [ unpack("\@${offset}n$count", $data) ]; } elsif ($type == 4) { $res{$otag} = [ unpack("\@${offset}N$count", $data) ]; } elsif ($type == 5) { $res{$otag} = [ undef ]; } elsif ($type == 6) { $res{$otag} = [ unpack("\@${offset}Z*", $data) ]; } elsif ($type == 7) { $res{$otag} = [ unpack("\@${offset}a$count", $data) ]; } elsif ($type == 8 || $type == 9) { my $d = unpack("\@${offset}a*", $data); my @res = split("\0", $d, $count + 1); $res{$otag} = [ splice @res, 0, $count ]; } else { $res{$otag} = [ undef ]; } }; if ($@) { warn("Bad rpm $rpm: $@\n"); return (); } } } if ($forcebinary && $stags{1044} && !$res{$stags{1044}} && !($stags{1106} && $res{$stags{1106}})) { $res{$stags{1044}} = [ '(none)' ]; # like rpm does... } if ($need_filenames) { if ($res{'OLDFILENAMES'}) { $res{'FILENAMES'} = [ @{$res{'OLDFILENAMES'}} ]; } else { my $i = 0; $res{'FILENAMES'} = [ map {"$res{'DIRNAMES'}->[$res{'DIRINDEXES'}->[$i++]]$_"} @{$res{'BASENAMES'}} ]; } } return %res; } sub add_flagsvers { my ($res, $name, $flags, $vers) = @_; return unless $res && $res->{$name}; my @flags = @{$res->{$flags} || []}; my @vers = @{$res->{$vers} || []}; for (@{$res->{$name}}) { if (@flags && ($flags[0] & 0xe) && @vers) { $_ .= ' '; $_ .= '<' if $flags[0] & 2; $_ .= '>' if $flags[0] & 4; $_ .= '=' if $flags[0] & 8; $_ .= " $vers[0]"; } shift @flags; shift @vers; } } sub filteroldweak { my ($res, $name, $flags, $data, $strong, $weak) = @_; return unless $res && $res->{$name}; my @flags = @{$res->{$flags} || []}; my @strong; my @weak; for (@{$res->{$name}}) { if (@flags && ($flags[0] & 0x8000000)) { push @strong, $_; } else { push @weak, $_; } shift @flags; } $data->{$strong} = \@strong if @strong; $data->{$weak} = \@weak if @weak; } sub verscmp_part { my ($s1, $s2) = @_; if (!defined($s1)) { return defined($s2) ? -1 : 0; } return 1 if !defined $s2; return 0 if $s1 eq $s2; while (1) { $s1 =~ s/^[^a-zA-Z0-9~]+//; $s2 =~ s/^[^a-zA-Z0-9~]+//; if ($s1 =~ s/^~//) { next if $s2 =~ s/^~//; return -1; } return 1 if $s2 =~ /^~/; if ($s1 eq '') { return $s2 eq '' ? 0 : -1; } return 1 if $s2 eq ''; my ($x1, $x2, $r); if ($s1 =~ /^([0-9]+)(.*?)$/) { $x1 = $1; $s1 = $2; $s2 =~ /^([0-9]*)(.*?)$/; $x2 = $1; $s2 = $2; return 1 if $x2 eq ''; $x1 =~ s/^0+//; $x2 =~ s/^0+//; $r = length($x1) - length($x2) || $x1 cmp $x2; } elsif ($s1 ne '' && $s2 ne '') { $s1 =~ /^([a-zA-Z]*)(.*?)$/; $x1 = $1; $s1 = $2; $s2 =~ /^([a-zA-Z]*)(.*?)$/; $x2 = $1; $s2 = $2; return -1 if $x1 eq '' || $x2 eq ''; $r = $x1 cmp $x2; } return $r > 0 ? 1 : -1 if $r; } } sub verscmp { my ($s1, $s2, $dtest) = @_; return 0 if $s1 eq $s2; my ($e1, $v1, $r1) = $s1 =~ /^(?:(\d+):)?(.*?)(?:-([^-]*))?$/s; $e1 = 0 unless defined $e1; my ($e2, $v2, $r2) = $s2 =~ /^(?:(\d+):)?(.*?)(?:-([^-]*))?$/s; $e2 = 0 unless defined $e2; if ($e1 ne $e2) { my $r = verscmp_part($e1, $e2); return $r if $r; } return 0 if $dtest && ($v1 eq '' || $v2 eq ''); if ($v1 ne $v2) { my $r = verscmp_part($v1, $v2); return $r if $r; } $r1 = '' unless defined $r1; $r2 = '' unless defined $r2; return 0 if $dtest && ($r1 eq '' || $r2 eq ''); if ($r1 ne $r2) { return verscmp_part($r1, $r2); } return 0; } sub query { my ($handle, %opts) = @_; my @tags = qw{NAME SOURCERPM NOSOURCE NOPATCH SIGTAG_MD5 PROVIDENAME PROVIDEFLAGS PROVIDEVERSION REQUIRENAME REQUIREFLAGS REQUIREVERSION SOURCEPACKAGE}; push @tags, qw{EPOCH VERSION RELEASE ARCH}; push @tags, qw{FILENAMES} if $opts{'filelist'}; push @tags, qw{SUMMARY DESCRIPTION} if $opts{'description'}; push @tags, qw{DISTURL} if $opts{'disturl'}; push @tags, qw{BUILDTIME} if $opts{'buildtime'}; push @tags, qw{CONFLICTNAME CONFLICTVERSION CONFLICTFLAGS OBSOLETENAME OBSOLETEVERSION OBSOLETEFLAGS} if $opts{'conflicts'}; push @tags, qw{RECOMMENDNAME RECOMMENDVERSION RECOMMENDFLAGS SUGGESTNAME SUGGESTVERSION SUGGESTFLAGS SUPPLEMENTNAME SUPPLEMENTVERSION SUPPLEMENTFLAGS ENHANCENAME ENHANCEVERSION ENHANCEFLAGS OLDSUGGESTSNAME OLDSUGGESTSVERSION OLDSUGGESTSFLAGS OLDENHANCESNAME OLDENHANCESVERSION OLDENHANCESFLAGS} if $opts{'weakdeps'}; my %res = rpmq($handle, @tags); return undef unless %res; my $src = $res{'SOURCERPM'}->[0]; $src = '' unless defined $src; $src =~ s/-[^-]*-[^-]*\.[^\.]*\.rpm//; add_flagsvers(\%res, 'PROVIDENAME', 'PROVIDEFLAGS', 'PROVIDEVERSION'); add_flagsvers(\%res, 'REQUIRENAME', 'REQUIREFLAGS', 'REQUIREVERSION'); my $data = { name => $res{'NAME'}->[0], hdrmd5 => unpack('H32', $res{'SIGTAG_MD5'}->[0]), }; if ($opts{'alldeps'}) { $data->{'provides'} = [ @{$res{'PROVIDENAME'} || []} ]; $data->{'requires'} = [ @{$res{'REQUIRENAME'} || []} ]; } else { $data->{'provides'} = [ grep {!/^rpmlib\(/ && !/^\//} @{$res{'PROVIDENAME'} || []} ]; $data->{'requires'} = [ grep {!/^rpmlib\(/ && !/^\//} @{$res{'REQUIRENAME'} || []} ]; } if ($opts{'conflicts'}) { add_flagsvers(\%res, 'CONFLICTNAME', 'CONFLICTFLAGS', 'CONFLICTVERSION'); add_flagsvers(\%res, 'OBSOLETENAME', 'OBSOLETEFLAGS', 'OBSOLETEVERSION'); $data->{'conflicts'} = [ @{$res{'CONFLICTNAME'}} ] if $res{'CONFLICTNAME'}; $data->{'obsoletes'} = [ @{$res{'OBSOLETENAME'}} ] if $res{'OBSOLETENAME'}; } if ($opts{'weakdeps'}) { for (qw{RECOMMEND SUGGEST SUPPLEMENT ENHANCE}) { next unless $res{"${_}NAME"}; add_flagsvers(\%res, "${_}NAME", "${_}FLAGS", "${_}VERSION"); $data->{lc($_)."s"} = [ @{$res{"${_}NAME"}} ]; } if ($res{'OLDSUGGESTSNAME'}) { add_flagsvers(\%res, 'OLDSUGGESTSNAME', 'OLDSUGGESTSFLAGS', 'OLDSUGGESTSVERSION'); filteroldweak(\%res, 'OLDSUGGESTSNAME', 'OLDSUGGESTSFLAGS', $data, 'recommends', 'suggests'); } if ($res{'OLDENHANCESNAME'}) { add_flagsvers(\%res, 'OLDENHANCESNAME', 'OLDENHANCESFLAGS', 'OLDENHANCESVERSION'); filteroldweak(\%res, 'OLDENHANCESNAME', 'OLDENHANCESFLAGS', $data, 'supplements', 'enhances'); } } # rpm3 compatibility: retrofit missing self provides if ($src ne '') { my $haveselfprovides; if (@{$data->{'provides'}}) { if ($data->{'provides'}->[-1] =~ /^\Q$res{'NAME'}->[0]\E =/) { $haveselfprovides = 1; } elsif (@{$data->{'provides'}} > 1 && $data->{'provides'}->[-2] =~ /^\Q$res{'NAME'}->[0]\E =/) { $haveselfprovides = 1; } } if (!$haveselfprovides) { my $evr = "$res{'VERSION'}->[0]-$res{'RELEASE'}->[0]"; $evr = "$res{'EPOCH'}->[0]:$evr" if $res{'EPOCH'} && $res{'EPOCH'}->[0]; push @{$data->{'provides'}}, "$res{'NAME'}->[0] = $evr"; } } $data->{'source'} = $src eq '(none)' ? $data->{'name'} : $src if $src ne ''; if ($opts{'evra'}) { my $arch = $res{'ARCH'}->[0]; $arch = $res{'NOSOURCE'} || $res{'NOPATCH'} ? 'nosrc' : 'src' unless $src ne ''; $data->{'version'} = $res{'VERSION'}->[0]; $data->{'release'} = $res{'RELEASE'}->[0]; $data->{'arch'} = $arch; $data->{'epoch'} = $res{'EPOCH'}->[0] if exists $res{'EPOCH'}; } if ($opts{'filelist'}) { $data->{'filelist'} = $res{'FILENAMES'}; } if ($opts{'description'}) { $data->{'summary'} = $res{'SUMMARY'}->[0]; $data->{'description'} = $res{'DESCRIPTION'}->[0]; } $data->{'buildtime'} = $res{'BUILDTIME'}->[0] if $opts{'buildtime'}; $data->{'disturl'} = $res{'DISTURL'}->[0] if $opts{'disturl'} && $res{'DISTURL'}; return $data; } sub queryhdrmd5 { my ($bin, $leadsigp) = @_; local *F; open(F, '<', $bin) || die("$bin: $!\n"); my $buf = ''; my $l; while (length($buf) < 96 + 16) { $l = sysread(F, $buf, 4096, length($buf)); if (!$l) { warn("$bin: read error\n"); close(F); return undef; } } my ($magic, $sigtype) = unpack('N@78n', $buf); if ($magic != 0xedabeedb || $sigtype != 5) { warn("$bin: not a rpm (bad magic of header type)\n"); close(F); return undef; } my ($headmagic, $cnt, $cntdata) = unpack('@96N@104NN', $buf); if ($headmagic != 0x8eade801) { warn("$bin: not a rpm (bad sig header magic)\n"); close(F); return undef; } my $hlen = 96 + 16 + $cnt * 16 + $cntdata; $hlen = ($hlen + 7) & ~7; while (length($buf) < $hlen) { $l = sysread(F, $buf, 4096, length($buf)); if (!$l) { warn("$bin: read error\n"); close(F); return undef; } } close F; $$leadsigp = Digest::MD5::md5_hex(substr($buf, 0, $hlen)) if $leadsigp; my $idxarea = substr($buf, 96 + 16, $cnt * 16); if ($idxarea !~ /\A(?:.{16})*\000\000\003\354\000\000\000\007(....)\000\000\000\020/s) { warn("$bin: no md5 signature header\n"); return undef; } my $md5off = unpack('N', $1); if ($md5off >= $cntdata) { warn("$bin: bad md5 offset\n"); return undef; } $md5off += 96 + 16 + $cnt * 16; return unpack("\@${md5off}H32", $buf); } sub queryinstalled { my ($root, %opts) = @_; $root = '' if !defined($root) || $root eq '/'; local *F; my $dochroot = $root ne '' && !$opts{'nochroot'} && !$< ? 1 : 0; my $pid = open(F, '-|'); die("fork: $!\n") unless defined $pid; if (!$pid) { if ($dochroot && chroot($root)) { chdir('/') || die("chdir: $!\n"); $root = ''; } my @args; unshift @args, '--nodigest', '--nosignature' if -e "$root/usr/bin/rpmquery "; unshift @args, '--dbpath', "$root/var/lib/rpm" if $root ne ''; push @args, '--qf', '%{NAME}/%{ARCH}/%|EPOCH?{%{EPOCH}}:{0}|/%{VERSION}/%{RELEASE}/%{BUILDTIME}\n'; if (-x "$root/usr/bin/rpm") { exec("$root/usr/bin/rpm", '-qa', @args); die("$root/usr/bin/rpm: $!\n"); } if (-x "$root/bin/rpm") { exec("$root/bin/rpm", '-qa', @args); die("$root/bin/rpm: $!\n"); } die("rpm: command not found\n"); } my @pkgs; while () { chomp; my @s = split('/', $_); next unless @s >= 5; my $q = {'name' => $s[0], 'arch' => $s[1], 'version' => $s[3], 'release' => $s[4]}; $q->{'epoch'} = $s[2] if $s[2]; $q->{'buildtime'} = $s[5] if $s[5]; push @pkgs, $q; } if (!close(F)) { return queryinstalled($root, %opts, 'nochroot' => 1) if !@pkgs && $dochroot; die("rpm: exit status $?\n"); } return \@pkgs; } # return (lead, sighdr, hdr [, hdrmd5]) of a rpm sub getrpmheaders { my ($path, $withhdrmd5) = @_; my $hdrmd5; local *F; open(F, '<', $path) || die("$path: $!\n"); my $buf = ''; my $l; while (length($buf) < 96 + 16) { $l = sysread(F, $buf, 4096, length($buf)); die("$path: read error\n") unless $l; } die("$path: not a rpm\n") unless unpack('N', $buf) == 0xedabeedb && unpack('@78n', $buf) == 5; my ($headmagic, $cnt, $cntdata) = unpack('@96N@104NN', $buf); die("$path: not a rpm (bad sig header)\n") unless $headmagic == 0x8eade801 && $cnt < 16384 && $cntdata < 1048576; my $hlen = 96 + 16 + $cnt * 16 + $cntdata; $hlen = ($hlen + 7) & ~7; while (length($buf) < $hlen + 16) { $l = sysread(F, $buf, 4096, length($buf)); die("$path: read error\n") unless $l; } if ($withhdrmd5) { my $idxarea = substr($buf, 96 + 16, $cnt * 16); die("$path: no md5 signature header\n") unless $idxarea =~ /\A(?:.{16})*\000\000\003\354\000\000\000\007(....)\000\000\000\020/s; my $md5off = unpack('N', $1); die("$path: bad md5 offset\n") unless $md5off; $md5off += 96 + 16 + $cnt * 16; $hdrmd5 = unpack("\@${md5off}H32", $buf); } ($headmagic, $cnt, $cntdata) = unpack('N@8NN', substr($buf, $hlen)); die("$path: not a rpm (bad header)\n") unless $headmagic == 0x8eade801 && $cnt < 1048576 && $cntdata < 33554432; my $hlen2 = $hlen + 16 + $cnt * 16 + $cntdata; while (length($buf) < $hlen2) { $l = sysread(F, $buf, 4096, length($buf)); die("$path: read error\n") unless $l; } close F; return (substr($buf, 0, 96), substr($buf, 96, $hlen - 96), substr($buf, $hlen, $hlen2 - $hlen), $hdrmd5); } 1;