Current File : //bin/X11/debsums
#!/usr/bin/perl

#
#  Check installed files against package md5sums or debs.
#

use strict;
use warnings;

use File::Find 'find';
use File::Temp 'tempdir';
use File::Path 'rmtree';
use File::Copy 'copy';
use Fcntl qw/O_RDONLY O_NONBLOCK O_NOATIME/;
use Getopt::Long qw/:config bundling/;
use Digest::MD5;
use constant ELF_MAGIC => "\177ELF";
use Errno;
use POSIX;
use File::Basename;
use File::Spec;
use Dpkg::Conf;
use File::FnMatch qw(:fnmatch);

sub version {
    my $changelog = File::Spec->catfile(dirname($0), "debian", "changelog");
    my $cmd;
    if (-f $changelog) {
        $cmd = qq(dpkg-parsechangelog -SVersion '-l$changelog');
    } else {
        $cmd = q(dpkg-query -W -f '${Version}' debsums);
    }
    my $res = `$cmd`;
    chomp($res);
    if ($res !~ /^[0-9.~a-z+]+$/) {
        $res = "";
    }
    return $res;
}

(my $self = $0) =~ s!.*/!!;

sub version_info {
    my $version_number = version();
    my $version = <<"EOT";
$self $version_number

Copyright (c) 2002, 2004, 2005, 2006, 2007  Brendan O'Dea <bod\@debian.org>
This is free software, licensed under the terms of the GNU General Public
License.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

Written by Brendan O'Dea <bod\@debian.org>, based on a program by
Christoph Lameter <clameter\@debian.org> and Petr Cech <cech\@debian.org>.
EOT
    return $version;
}

my $help = <<"EOT";
$self checks the MD5 sums of installed debian packages.

Usage: $self [OPTIONS] [PACKAGE|DEB] ...

Options:
 -a, --all                    check configuration files (normally excluded)
 -e, --config                 check only configuration files
 -c, --changed                report changed files (implies -s)
 -l, --list-missing           list packages which don't have an md5sums file
 -s, --silent                 only report errors
 -m, --md5sums=FILE           read list of deb checksums from FILE
 -x, --report-mismatches      report errors and print the md5sums mismatch
 -r, --root=DIR               root directory to check (default /)
 -d, --admindir=DIR           dpkg admin directory (default /var/lib/dpkg)
 -p, --deb-path=DIR[:DIR...]  search path for debs
 -g, --generate=[all][,keep[,nocheck]]
                              generate md5sums from deb contents
     --no-locale-purge        report missing locale files even if localepurge
                              is configured
     --no-prelink             report changed ELF files even if prelink is
                              configured
     --ignore-obsolete        ignore obsolete conffiles.
     --help                   print this help, then exit
     --version                print version number, then exit
EOT

my $gen_opt;
GetOptions (
    'a|all'		=> \my $all,
    'e|config'		=> \my $config,
    'c|changed'		=> \my $changed,
    'l|list-missing'    => \my $missing,
    's|silent'		=> \my $silent,
    'x|report-mismatches'   => \my $report,
    'm|md5sums=s'	=> \my $md5sums,
    'r|root=s'		=> \my $root,
    'd|admindir=s'	=> \my $admindir,
    'p|deb-path=s'	=> \my $debpath,
    'generate=s'	=> \$gen_opt,
    'locale-purge!'	=> \my $localepurge,
    'prelink!'		=> \my $prelink,
    'ignore-permissions' => \my $ignore_permissions,
    'ignore-obsolete!'  => \my $ignore_obsolete,
    g			=> sub { $gen_opt = 'missing' },
    help		=> sub { print $help; exit },
    version		=> sub { print version_info(); exit },
) or die "Try '$self --help' for more information.\n";

sub can_ignore {
  return $!{EACCES} && $ignore_permissions && getuid();
}

my $my_noatime = 0;
eval { $my_noatime = O_NOATIME };

sub warn_or_die {
    if (can_ignore()) {
        unless ($silent) {
            warn $_[0];
        }
    } else {
        die $_[0];
    }
}

sub parse_dpkg {
    my ($command_cb, $field_names) = @_;

    local $/ = "\n\n";  # Separator that cannot appear in dpkg status format
    my @command = &$command_cb('--showformat=' .
                               (join '', map {"\${$_}$/"} @$field_names));
    open DPKG, '-|', @command
        or die "$self: can't run " . $command[0] . " ($!)\n";

    my @ret;
    while (!eof DPKG)
    {
        my %field = map {$_, scalar <DPKG>} @$field_names;
        chomp @field{@$field_names};
        push @ret, \%field;
    }

    close DPKG or die "$self: @command failed (",
      $! ? $! : $? >> 8 ? "exit status " . ($? >> 8) : "signal " . ($? & 127),
      ")\n";
    return @ret;
}

$root ||= '';
$admindir ||= '/var/lib/dpkg';
my $DPKG = $root . $admindir;

my $is_path_pattern_opt = sub {
    return shift =~ /^--path-(?:exclude|include)=/;
};

my $dpkg_conf = Dpkg::Conf->new();

foreach (glob($root . "/etc/dpkg/dpkg.cfg.d/[0-9a-zA-Z_-]*"),
         ($root . "/etc/dpkg/dpkg.cfg", $root . glob("~/.dpkg.cfg"))) {
    if (-f $_) {
        my $name = "$_";
        $dpkg_conf->load($name);
    }
}
$dpkg_conf->filter(keep => $is_path_pattern_opt);
my @dpkg_opts = $dpkg_conf->get_options;
my @dpkg_patterns = ();
foreach my $opt(@dpkg_opts) {
    my @res = ($opt =~ /^--path-(exclude|include)=(.+)/);
    push @dpkg_patterns, \@res;
}

sub excluded_by_dpkg {
    my $file = "/" . shift;
    my $excluded = 0;
    foreach my $rule(@dpkg_patterns) {
        my ($type, $pattern) = @{$rule};
        if (fnmatch($pattern, $file)) {
            $excluded = $type eq 'exclude' ? 1 : 0;
        }
    }
    return $excluded;
}


my %locales;
my $nopurge = '/etc/locale.nopurge';

# default is to ignore purged locale files if /etc/locale.nopurge exists
$localepurge = -e $nopurge unless defined $localepurge;

if ($localepurge and -e $nopurge)
{
    open L, $nopurge or die "$self: can't open $nopurge ($!)\n";
    while (<L>)
    {
        $locales{$1}++ if /^(\w.+)/;
    }

    close L;
}

# default is to use prelink to fetch the original checksums if installed
if (!defined $prelink or $prelink)
{
    # use the binary in preference to the wrapper which asks questions
    # interactively
    ($prelink) = grep -x, map +("$_.bin", $_), '/usr/sbin/prelink';
}

$silent++ if $changed;

my @debpath = '.';
@debpath = map +(length) ? $_ : '.', split /:/, $debpath, -1 if $debpath;

my $arch;
chomp ($arch = `/usr/bin/dpkg --print-architecture`);

my %generate;
if ($gen_opt)
{
    for (split /,/, $gen_opt)
    {
        if (/^(missing|all|keep|nocheck)$/)
        {
            $generate{$1}++;
        }
        else
        {
            die "$self: invalid --generate value '$_'\n";
        }
    }

    die "$self: --generate values 'all' and 'missing' are mutually exclusive\n"
        if $generate{all} and $generate{missing};

    $generate{missing}++ unless $generate{all} or $generate{missing};

    # ensure generated files are world readable
    umask 022;
}

my %installed;
my %package_name;
my %replaced;
{
    for my $fields (parse_dpkg(
                        sub {'dpkg-query', "--admindir=$DPKG", @_, '--show'},
                        [qw(Package PackageSpec binary:Package Version
                            Status Conffiles Replaces)])) {
        my %field = %$fields;
        $field{"binary:Package"} = $field{PackageSpec}
            if $field{"binary:Package"} eq '';
        $field{"binary:Package"} = $field{Package}
            if $field{"binary:Package"} eq '';
        next unless $field{"binary:Package"} ne ''
                and $field{Version} ne ''
                and $field{Status} =~ /\s(installed|half-configured)$/;

        $installed{$field{"binary:Package"}}{Version} = $field{Version};
        if ($field{"binary:Package"} ne $field{"Package"} &&
            $field{"binary:Package"} eq ($field{"Package"} . ":" . $arch))
        {
            $package_name{$field{"Package"}} = $field{"binary:Package"};
        }
        $installed{$field{"binary:Package"}}{Conffiles} = {
            map m!^\s*/(\S+)\s+([\da-f]+)!,
                grep { not ($ignore_obsolete and / obsolete$/) }
                split /\n/, $field{Conffiles}
        } if $field{Conffiles};

        for (split /,\s*/, $field{Replaces})
        {
            my ($pack, $ver) = /^(\S+)(?:\s+\(([^)]+)\))?$/;
            unless ($pack)
            {
                warn "$self: invalid Replaces for " .
                     $field{"binary:Package"} . " '$_'\n";
                next;
            }

            push @{$replaced{$pack}{$ver || 'all'}}, $field{"binary:Package"};
        }
    }
}

my %diversion;
for (`LC_ALL=C dpkg-divert --list --admindir $DPKG`)
{
    my ($by) = /^(local) diversion/ ? $1 : / by (\S+)$/;
    $diversion{$1} = [$2, $by]
        if m!diversion of /(.*) to /(.*?)\s!;
}

my %debsum;
if ($md5sums)
{
    open F, $md5sums
        or warn_or_die "$self: can't open sums file '$md5sums' ($!)\n";
    if (fileno(F)) {
        while (<F>)
        {
            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 (<S>)
        {
            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/, <P>;
                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 <pack>_<ver>_<arch>.deb or <pack>_<ver>.deb
                # where <ver> 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 (<F>)
            {
                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 (<SUMS>)
            {
                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;