)
{
my ($sum, $deb) = split;
$debsum{$deb} = $sum;
}
close F;
}
}
my $digest = Digest::MD5->new;
my $tmp;
my $status = 0;
@ARGV = sort keys %installed unless @ARGV;
sub dpkg_cmp
{
my $ver = shift;
my ($op, $testver) = split ' ', shift;
$op .= '=' if $op =~ /^[<>]$/; # convert old <, >
return 0 unless grep $op eq $_, qw/<< <= = => >>/;
return $op =~ /=/ if $ver eq $testver; # short cut equivalence
!system '/usr/bin/dpkg', '--compare-versions', $ver, $op, $testver;
}
sub md5sums_path
{
# Calling dpkg-query --control-path for every package is too slow,
# so we cheat a little bit.
my ($pack) = @_;
my $path = '';
if (-e "$DPKG/info/$pack.list") {
$path = "$DPKG/info/$pack.md5sums";
} elsif ($pack !~ /:/ and -e "$DPKG/info/$pack:$arch.list") {
$path = "$DPKG/info/$pack:$arch.md5sums";
} elsif ($pack =~ /^(.*):/ and -e "$DPKG/info/$1.list") {
$path = "$DPKG/info/$1.md5sums";
} else {
die "Cannot find md5sums path for $pack\n";
}
if (-e $path and -z _) {
# Empty .md5sums file: check if that's ok, either print a warning
my $list_file = $path;
$list_file =~ s/md5sums$/list/;
unless (-e $list_file) {
warn "$path is empty and $list_file does not exist!\n";
$status |= 2;
} else {
my $rc = open(my $lffd, '<', $list_file);
unless ($rc) {
warn "Couldn't open $list_file for reading: $!";
$status |= 2;
} else {
my $found_a_file = 0;
while (my $line = <$lffd>) {
chomp($line);
next if -l "$root$line";
next if -d _;
if (-f _) {
warn "$path is empty but shouldn't!\n";
$status |= 2;
last;
}
}
close($lffd);
}
}
}
return $path;
}
sub is_replaced
{
my ($pack, $path, $sum) = @_;
unless ($installed{$pack}{ReplacedBy})
{
(my $name = $pack) =~ s/:[^:]*$//;
return 0 unless $replaced{$name};
while (my ($ver, $p) = each %{$replaced{$name}})
{
next unless $ver eq 'all'
or dpkg_cmp $installed{$pack}{Version}, $ver;
push @{$installed{$pack}{ReplacedBy}}, @$p;
}
}
for my $p (@{$installed{$pack}{ReplacedBy} || []})
{
open S, md5sums_path($p) or next;
while ()
{
if ($_ eq "$sum $path\n")
{
close S;
return 1;
}
}
close S;
}
0;
}
sub is_localepurge_file {
my $path = shift;
my $locale = "";
if ($path =~ m!usr/share/(locale|man|gnome/help|omf|doc/kde/HTML|tcltk|aptitude|calendar|cups/templates|cups/locale|cups/doc-root|help|vim/vim[^/]+/lang|X11/locale)/!) {
my $type = $1;
if ($type eq "man" || $type eq "locale" || $type eq "doc/kde/HTML") {
$path =~ m!^usr/share/(?:man|locale|doc/kde/HTML)/([^/]+)/!;
$locale = $1;
} elsif ($type eq "gnome/help") {
$path =~ m!^usr/share/gnome/help/[^/]+/([^/]+)/!;
$locale = $1;
} elsif ($type eq "omf") {
$path =~ m!^usr/share/omf/([^/]+)/\1-([^/]+).omf$!;
$locale = $2;
} elsif ($type eq "tcltk") {
$path =~ m!^usr/share/tcltk/t[^/]+/msgs/([^/]+).msg$!;
$locale = $1;
} elsif ($type eq "aptitude") {
$path =~ m!^usr/share/aptitude/aptitude-defaults\.(.+)$!;
$locale = $1;
} elsif ($type eq "calendar") {
$path =~ m!^usr/share/calendar/([\w]{2}_.+)$!;
$locale = $1;
} elsif ($type eq "cups/locale") {
$path =~ m!^usr/share/cups/locale/([^/]+)!;
$locale = $1;
} elsif ($type eq "cups/templates") {
$path =~ m!^usr/share/cups/templates/([^/]+)/!;
$locale = $1;
} elsif ($type eq "cups/doc-root") {
$path =~ m!^usr/share/cups/doc-root/([^/]+)/!;
$locale = $1;
} elsif ($type eq "help") {
$path =~ m!^usr/share/help/([^/]+)$!;
$locale = $1;
} elsif ($type =~ /^vim/) {
$path =~ m!^usr/share/vim/vim[^/]+/lang/([^/]+)/LC_MESSAGES/vim\.mo$!;
$locale = $1;
} elsif ($type eq "X11/locale") {
$path =~ m!^usr/share/X11/locale/([^/]+)/!;
$locale = $1;
}
}
return length($locale) && !$locales{$locale};
}
# resolve symlinks without escaping $root
sub resolve_path {
my $path = shift;
my $package = shift;
my @tokens = split(/\//, $path);
my @parts = ();
my %seen;
while (@tokens) {
my $token = shift @tokens;
next if $token eq '.' || $token eq '';
if ($token eq '..') {
pop @parts;
next;
}
my $fp = $root . '/' . join('/', @parts) . '/' . $token;
if ($seen{$fp}) {
# better die now with a clear error message then later
# with a sysopen fails
die "$self: Error: symlink loop detected in path '$path'. ",
"Please file a bug against $package.\n";
}
$seen{$fp} = 1;
if (-l $fp) {
my $link = readlink($fp);
@parts = () if $link =~ /^\//;
unshift @tokens, split(/\//, $link);
} else {
push @parts, $token;
}
}
return join('/', @parts);
}
{
my $width = ($ENV{COLUMNS} || 80) - 3;
$width = 6 if $width < 6;
sub check
{
my ($pack, $path, $sum) = @_;
$path = $diversion{$path}[0] if exists $diversion{$path}
and $diversion{$path}[1] ne $pack
and $diversion{$path}[1] ne $pack =~ s/:.*//r;
my $resolved = resolve_path($path,$pack);
if ((!sysopen F, "$root/$resolved", O_RDONLY|O_NONBLOCK|$my_noatime) &&
(!sysopen F, "$root/$resolved", O_RDONLY|O_NONBLOCK))
{
return 0 if $localepurge
and is_localepurge_file($path);
return 0 if excluded_by_dpkg($path);
my $err = "$self: can't open $pack file $root/$path ($!)\n";
if (can_ignore()) {
warn $err unless ($silent);
return 0;
} else {
if ($!{ENOENT}) {
warn "$self: missing file $root/$path (from $pack package)\n";
} else {
warn $err;
}
return 2;
}
}
unless (-f F) {
warn "$self: can't check $pack file $root/$path ",
"(not a regular file)\n";
close F;
return 2;
}
my $magic = '';
eval {
defined read F, $magic, length ELF_MAGIC or die $!;
$digest->add($magic);
$digest->addfile(\*F);
};
close F;
if ($@) {
$@ =~ s/ at \S+ line.*\n//;
warn "$self: can't check $pack file $root/$path ($@)\n";
return 2;
}
my $s = $digest->hexdigest;
if ($s ne $sum and $prelink and $magic eq ELF_MAGIC) {
if (open P, '-|', $prelink, '--verify', '--md5', "$root/$path")
{
my ($prelink_s) = map /^([\da-f]{32})\s/, ;
close P;
$s = $prelink_s if $prelink_s;
}
}
# Good cases
if ($s eq $sum) {
printf "%-*s OK\n", $width, "$root/$path" unless ($silent || $report);
return 0;
}
if (is_replaced $pack, $path, $s) {
printf "%-*s REPLACED\n", $width - 6, "$root/$path" unless ($silent || $report);
return 0;
}
my $correct_package =
`dpkg-query "--admindir=$DPKG" -S "/$path" | awk -F': ' '{print \$1}'`;
chomp($correct_package);
if ($pack ne $correct_package) {
#print "$pack != $correct_package\n";
return 0;
}
# Bad cases
if ($changed) {
print "$root/$path\n";
return 2;
}
if ($report) {
warn "$self: changed file $root/$path (observed:$s expected:$sum) (from $pack package)\n";
return 2;
}
if ($silent) {
warn "$self: changed file $root/$path (from $pack package)\n";
return 2;
}
printf "%-*s FAILED\n", $width - 4, "$root/$path";
return 2;
}
}
for (@ARGV)
{
my $sums;
my $pack;
my $conffiles;
# looks like a package name
unless (/[^a-z\d+.:-]/ or /\.deb$/)
{
$pack = $_;
unless (exists $installed{$pack})
{
if (exists $package_name{$pack}) {
$pack = $package_name{$pack};
}
unless (exists $installed{$pack})
{
warn "$self: package $pack is not installed\n";
$status |= 1;
next;
}
}
my $deb;
if (%generate)
{
my @v = $installed{$pack}{Version};
if ($v[0] =~ s/(\d+):/$1%3a/)
{
push @v, $installed{$pack}{Version};
$v[1] =~ s/\d+://;
}
for my $dir (@debpath)
{
# look for __.deb or _.deb
# where may or may not contain an epoch
my ($debname, $debarch);
($debname, $debarch) = ($pack =~ /^(.*):([^:]*)$/)
or ($debname, $debarch) = ($pack, $arch);
if (($deb) = grep -f, map +(glob "$dir/${debname}_$_.deb"),
map +("${_}_$debarch", "${_}_all", $_), @v)
{
$deb =~ s!^\./+!!;
last;
}
}
}
if ($generate{all})
{
unless ($deb)
{
warn "$self: no deb available for $pack\n";
$status |= 1;
next;
}
$_ = $deb;
}
else
{
$sums = md5sums_path($pack);
unless (-f $sums or $config)
{
if ($missing)
{
print "$pack\n";
next;
}
unless ($generate{missing})
{
warn "$self: no md5sums for $pack\n";
next;
}
unless ($deb)
{
warn "$self: no md5sums for $pack and no deb available\n"
unless $generate{nocheck} and $silent;
next;
}
undef $sums;
$_ = $deb;
}
}
next if $missing;
}
unless ($sums)
{
unless (-f and /\.deb$/)
{
warn "$self: invalid package name '$_'\n";
$status |= 1;
next;
}
my $deb = $_;
my ($fields) = parse_dpkg(sub {'dpkg-deb', @_, '--show', $deb},
[qw(Package PackageSpec binary:Package
Version Conffiles)])
or do {
warn "$self: $deb does not seem to be a valid debian archive\n";
$status |= 1;
next;
};
my %field = %$fields;
$field{"binary:Package"} = $field{PackageSpec}
if $field{"binary:Package"} eq '';
$field{"binary:Package"} = $field{Package}
if $field{"binary:Package"} eq '';
unless ($field{"binary:Package"} ne '' and $field{Version} ne '')
{
warn "$self: $deb does not seem to be a valid debian archive\n";
$status |= 1;
next;
}
$pack = $field{"binary:Package"};
unless (exists $installed{$pack})
{
if (exists $package_name{$pack}) {
$pack = $package_name{$pack};
}
unless (exists $installed{$pack})
{
warn "$self: package $pack is not installed\n";
$status |= 1;
next;
}
}
unless ($installed{$pack}{Version} eq $field{Version})
{
warn "$self: package $pack version $field{Version} !=",
" installed version $installed{$pack}{Version}\n";
$status |= 1;
next;
}
if ($md5sums)
{
if (exists $debsum{$deb})
{
open F, $deb or warn_or_die "$self: can't open $deb ($!)\n";
if (fileno(F)) {
$digest->addfile(\*F);
close F;
}
unless ($digest->hexdigest eq $debsum{$deb})
{
warn "$self: checksum mismatch for $deb; not checked\n";
$status |= 2;
next;
}
}
else
{
warn "$self: no checksum available for $deb\n";
}
}
unless ($tmp)
{
my $catch = sub { exit 1 };
$SIG{$_} = $catch for qw/HUP INT QUIT TERM/;
$tmp = tempdir CLEANUP => 1
or die "$self: can't create temporary directory ($!)\n";
}
my $control = "$tmp/DEBIAN";
$sums = "$control/md5sums";
rmtree ($control, {safe => 1}) if -d $control;
system 'dpkg', '--control', $deb, $control
and die "$self: can't extract control info from $deb\n";
if ($missing)
{
print "$deb\n" unless -s $sums;
next;
}
my %conf;
if (open F, "$control/conffiles")
{
while ()
{
chomp;
$conf{$1}++ if m!^/?(.+)!;
}
close F;
}
if (!-s $sums)
{
my $unpacked = "$tmp/$pack";
print "Generating missing md5sums for $deb..." unless $silent;
system 'dpkg', '--extract', $deb, $unpacked
and die "$self: can't unpack $deb\n";
$conffiles = {};
open SUMS, ">$sums" or die "$self: can't create $sums ($!)\n";
my $skip = (length $unpacked) + 1;
find sub {
return if -l or ! -f;
open F, $_ or warn_or_die "$self: can't open $_ ($!)\n";
if (fileno(F)) {
$digest->addfile(\*F);
close F;
}
my $md5 = $digest->hexdigest;
my $path = substr $File::Find::name, $skip;
if (delete $conf{$path})
{
$conffiles->{$path} = $md5;
}
else
{
print SUMS "$md5 $path\n";
}
}, $unpacked;
close SUMS;
rmtree ($unpacked, {safe => 1});
print "done.\n" unless $silent;
warn "$self: extra conffiles listed in $deb: (",
(join ', ', keys %conf), ")\n" if %conf;
}
if ($generate{keep})
{
warn "$self: the --generate=keep option has been removed and does nothing."
}
}
next if $generate{nocheck};
$conffiles = $installed{$pack}{Conffiles} || {}
unless $conffiles;
unless ($config)
{
open SUMS, $sums or warn_or_die "$self: can't open $sums ($!)\n";
if (fileno(SUMS)) {
while ()
{
chomp;
my ($sum, $path) = split ' ', $_, 2;
unless ($path and $sum =~ /^[0-9a-f]{32}$/)
{
warn "$self: invalid line ($.) in md5sums for $pack: $_\n";
next;
}
$path =~ s!^\./!!;
next if exists $conffiles->{$path};
$status |= check $pack, $path, $sum;
}
close SUMS;
}
}
next unless ($all or $config) and %$conffiles;
while (my ($path, $sum) = each %$conffiles)
{
$status |= check $pack, $path, $sum;
}
}
exit $status;