|
|
@@ -1,3 +1,6 @@
|
|
|
+# Copyright © 2008 Frank Lichtenheld <djpig@debian.org>
|
|
|
+# Copyright © 2010 Raphaël Hertzog <hertzog@debian.org>
|
|
|
+#
|
|
|
# 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
|
|
|
@@ -16,104 +19,357 @@ package Dpkg::Checksums;
|
|
|
use strict;
|
|
|
use warnings;
|
|
|
|
|
|
-our $VERSION = "0.01";
|
|
|
+our $VERSION = "1.00";
|
|
|
|
|
|
use Dpkg;
|
|
|
use Dpkg::Gettext;
|
|
|
use Dpkg::ErrorHandling;
|
|
|
+use Dpkg::IPC;
|
|
|
|
|
|
use base qw(Exporter);
|
|
|
-our @EXPORT = qw(@check_supported %check_supported %check_prog %check_regex
|
|
|
- readchecksums readallchecksums getchecksums);
|
|
|
-
|
|
|
-our @check_supported = qw(md5 sha1 sha256);
|
|
|
-our %check_supported = map { $_ => 1 } @check_supported;
|
|
|
-our %check_prog = ( md5 => 'md5sum', sha1 => 'sha1sum',
|
|
|
- sha256 => 'sha256sum' );
|
|
|
-our %check_regex = ( md5 => qr/[0-9a-f]{32}/,
|
|
|
- sha1 => qr/[0-9a-f]{40}/,
|
|
|
- sha256 => qr/[0-9a-f]{64}/ );
|
|
|
-
|
|
|
-sub extractchecksum {
|
|
|
- my ($alg, $checksum) = @_;
|
|
|
- ($checksum =~ /^($check_regex{$alg})(\s|$)/m)
|
|
|
- || error(_g("checksum program gave bogus output `%s'"), $checksum);
|
|
|
- return $1;
|
|
|
+our @EXPORT = qw(checksums_get_list checksums_is_supported
|
|
|
+ checksums_get_property);
|
|
|
+
|
|
|
+=head1 NAME
|
|
|
+
|
|
|
+Dpkg::Checksums - generate and manipulate file checksums
|
|
|
+
|
|
|
+=head1 DESCRIPTION
|
|
|
+
|
|
|
+This module provides an object that can generate and manipulate
|
|
|
+various file checksums as well as some methods to query information
|
|
|
+about supported checksums.
|
|
|
+
|
|
|
+=head1 EXPORTED FUNCTIONS
|
|
|
+
|
|
|
+=over 4
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+my $CHECKSUMS = {
|
|
|
+ "md5" => {
|
|
|
+ "program" => [ "md5sum" ],
|
|
|
+ "regex" => qr/[0-9a-f]{32}/,
|
|
|
+ },
|
|
|
+ "sha1" => {
|
|
|
+ "program" => [ "sha1sum" ],
|
|
|
+ "regex" => qr/[0-9a-f]{40}/,
|
|
|
+ },
|
|
|
+ "sha256" => {
|
|
|
+ "program" => [ "sha256sum" ],
|
|
|
+ "regex" => qr/[0-9a-f]{64}/,
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+=item @list = checksums_get_list()
|
|
|
+
|
|
|
+Returns the list of supported checksums algorithms.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub checksums_get_list() {
|
|
|
+ return sort keys %{$CHECKSUMS};
|
|
|
}
|
|
|
|
|
|
+=item $bool = checksums_is_supported($alg)
|
|
|
|
|
|
-sub readchecksums {
|
|
|
- my ($alg, $fieldtext, $checksums, $sizes) = @_;
|
|
|
- my %checksums;
|
|
|
+Returns a boolean indicating whether the given checksum algorithm is
|
|
|
+supported. The checksum algorithm is case-insensitive.
|
|
|
|
|
|
- $alg = lc($alg);
|
|
|
- unless ($check_supported{$alg}) {
|
|
|
- warning(_g("Unknown checksum algorithm \`%s', ignoring"), $alg);
|
|
|
- return;
|
|
|
+=cut
|
|
|
+
|
|
|
+sub checksums_is_supported($) {
|
|
|
+ my ($alg) = @_;
|
|
|
+ return exists $CHECKSUMS->{lc($alg)};
|
|
|
+}
|
|
|
+
|
|
|
+=item $value = checksums_get_property($alg, $property)
|
|
|
+
|
|
|
+Returns the requested property of the checksum algorithm. Returns undef if
|
|
|
+either the property or the checksum algorithm doesn't exist. Valid
|
|
|
+properties currently include "program" (returns an array reference with
|
|
|
+a program name and parameters required to compute the checksum of the
|
|
|
+filename given as last parameter) and "regex" for the regular expression
|
|
|
+describing the common string representation of the checksum (as output
|
|
|
+by the program that generates it).
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub checksums_get_property($$) {
|
|
|
+ my ($alg, $property) = @_;
|
|
|
+ return undef unless checksums_is_supported($alg);
|
|
|
+ return $CHECKSUMS->{lc($alg)}{$property};
|
|
|
+}
|
|
|
+
|
|
|
+=back
|
|
|
+
|
|
|
+=head1 OBJECT METHODS
|
|
|
+
|
|
|
+=over 4
|
|
|
+
|
|
|
+=item my $ck = Dpkg::Checksums->new()
|
|
|
+
|
|
|
+Create a new Dpkg::Checksums object. This object is able to store
|
|
|
+the checksums of several files to later export them or verify them.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub new {
|
|
|
+ my ($this, %opts) = @_;
|
|
|
+ my $class = ref($this) || $this;
|
|
|
+
|
|
|
+ my $self = {};
|
|
|
+ bless $self, $class;
|
|
|
+ $self->reset();
|
|
|
+
|
|
|
+ return $self;
|
|
|
+}
|
|
|
+
|
|
|
+=item $ck->reset()
|
|
|
+
|
|
|
+Forget about all checksums stored. The object is again in the same state
|
|
|
+as if it was newly created.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub reset {
|
|
|
+ my ($self) = @_;
|
|
|
+ $self->{files} = [];
|
|
|
+ $self->{checksums} = {};
|
|
|
+ $self->{size} = {};
|
|
|
+}
|
|
|
+
|
|
|
+=item $ck->add_from_file($filename, %opts)
|
|
|
+
|
|
|
+Add checksums information for the file $filename. The file must exists
|
|
|
+for the call to succeed. If you don't want the given filename to appear
|
|
|
+when you later export the checksums you might want to set the "key"
|
|
|
+option with the public name that you want to use. Also if you don't want
|
|
|
+to generate all the checksums, you can pass an array reference of the
|
|
|
+wanted checksums in the "checksums" option.
|
|
|
+
|
|
|
+It the object already contains checksums information associated the
|
|
|
+filename (or key), it will error out if the newly computed information
|
|
|
+does not match what's stored.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub add_from_file {
|
|
|
+ my ($self, $file, %opts) = @_;
|
|
|
+ my $key = exists $opts{key} ? $opts{key} : $file;
|
|
|
+ my @alg;
|
|
|
+ if (exists $opts{checksums}) {
|
|
|
+ push @alg, map { lc($_) } @{$opts{checksums}};
|
|
|
+ } else {
|
|
|
+ push @alg, checksums_get_list();
|
|
|
+ }
|
|
|
+
|
|
|
+ push @{$self->{files}}, $key unless exists $self->{size}{$key};
|
|
|
+ (my @s = stat($file)) || syserr(_g("cannot fstat file %s"), $file);
|
|
|
+ if (exists $self->{size}{$key} and $self->{size}{$key} != $s[7]) {
|
|
|
+ error(_g("File %s has size %u instead of expected %u"),
|
|
|
+ $file, $s[7], $self->{size}{$key});
|
|
|
}
|
|
|
+ $self->{size}{$key} = $s[7];
|
|
|
+
|
|
|
+ foreach my $alg (@alg) {
|
|
|
+ my @exec = (@{$CHECKSUMS->{$alg}{"program"}}, $file);
|
|
|
+ my $regex = $CHECKSUMS->{$alg}{"regex"};
|
|
|
+ my $output;
|
|
|
+ spawn('exec' => \@exec, to_string => \$output);
|
|
|
+ if ($output =~ /^($regex)(\s|$)/m) {
|
|
|
+ my $newsum = $1;
|
|
|
+ if (exists $self->{checksums}{$key}{$alg} and
|
|
|
+ $self->{checksums}{$key}{$alg} ne $newsum) {
|
|
|
+ error(_g("File %s has checksum %s instead of expected %s (algorithm %s)"),
|
|
|
+ $file, $newsum, $self->{checksums}{$key}{$alg}, $alg);
|
|
|
+ }
|
|
|
+ $self->{checksums}{$key}{$alg} = $newsum;
|
|
|
+ } else {
|
|
|
+ error(_g("checksum program gave bogus output `%s'"), $output);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+=item $ck->add_from_string($alg, $value)
|
|
|
+
|
|
|
+Add checksums of type $alg that are stored in the $value variable.
|
|
|
+$value can be multi-lines, each line should be a space separated list
|
|
|
+of checksum, file size and filename. Leading or trailing spaces are
|
|
|
+not allowed.
|
|
|
+
|
|
|
+It the object already contains checksums information associated to the
|
|
|
+filenames, it will error out if the newly read information does not match
|
|
|
+what's stored.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub add_from_string {
|
|
|
+ my ($self, $alg, $fieldtext) = @_;
|
|
|
+ $alg = lc($alg);
|
|
|
my $rx_fname = qr/[0-9a-zA-Z][-+:.,=0-9a-zA-Z_~]+/;
|
|
|
+ my $regex = checksums_get_property($alg, "regex");
|
|
|
+ my $checksums = $self->{checksums};
|
|
|
+
|
|
|
for my $checksum (split /\n */, $fieldtext) {
|
|
|
next if $checksum eq '';
|
|
|
- $checksum =~ m/^($check_regex{$alg})\s+(\d+)\s+($rx_fname)$/
|
|
|
- || do {
|
|
|
- warning(_g("Checksums-%s field contains bad line \`%s'"),
|
|
|
- ucfirst($alg), $checksum);
|
|
|
- next;
|
|
|
- };
|
|
|
+ unless ($checksum =~ m/^($regex)\s+(\d+)\s+($rx_fname)$/) {
|
|
|
+ error(_g("invalid line in %s checksums string: %s"),
|
|
|
+ $alg, $checksum);
|
|
|
+ }
|
|
|
my ($sum, $size, $file) = ($1, $2, $3);
|
|
|
if (exists($checksums->{$file}{$alg})
|
|
|
and $checksums->{$file}{$alg} ne $sum) {
|
|
|
error(_g("Conflicting checksums \`%s\' and \`%s' for file \`%s'"),
|
|
|
$checksums->{$file}{$alg}, $sum, $file);
|
|
|
}
|
|
|
- if (exists($sizes->{$file})
|
|
|
- and $sizes->{$file} != $size) {
|
|
|
+ if (exists $self->{size}{$file} and $self->{size}{$file} != $size) {
|
|
|
error(_g("Conflicting file sizes \`%u\' and \`%u' for file \`%s'"),
|
|
|
- $sizes->{$file}, $size, $file);
|
|
|
+ $self->{size}{$file}, $size, $file);
|
|
|
}
|
|
|
+ push @{$self->{files}}, $file unless exists $self->{size}{$file};
|
|
|
$checksums->{$file}{$alg} = $sum;
|
|
|
- $sizes->{$file} = $size;
|
|
|
+ $self->{size}{$file} = $size;
|
|
|
}
|
|
|
-
|
|
|
- return 1;
|
|
|
}
|
|
|
|
|
|
-sub readallchecksums {
|
|
|
- my ($fields, $checksums, $sizes) = @_;
|
|
|
+=item $ck->add_from_control($control, %opts)
|
|
|
|
|
|
- foreach my $field (keys %$fields) {
|
|
|
- if ($field =~ /^Checksums-(\w+)$/
|
|
|
- && defined($fields->{$field})) {
|
|
|
- readchecksums($1, $fields->{$field}, $checksums, $sizes);
|
|
|
+Read checksums from Checksums-* fields stored in the Dpkg::Control object
|
|
|
+$control. It uses $self->add_from_string() on the field values to do the
|
|
|
+actual work.
|
|
|
+
|
|
|
+If the option "use_files_for_md5" evaluates to true, then the "Files"
|
|
|
+field is used in place of the "Checksums-Md5" field. By default the option
|
|
|
+is false.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub add_from_control {
|
|
|
+ my ($self, $control, %opts) = @_;
|
|
|
+ $opts{use_files_for_md5} = 0 unless exists $opts{use_files_for_md5};
|
|
|
+ foreach my $alg (checksums_get_list()) {
|
|
|
+ my $key = "Checksums-$alg";
|
|
|
+ $key = "Files" if ($opts{use_files_for_md5} and $alg eq "md5");
|
|
|
+ if (exists $control->{$key}) {
|
|
|
+ $self->add_from_string($alg, $control->{$key});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-sub getchecksums {
|
|
|
- my ($file, $checksums, $size) = @_;
|
|
|
+=item @files = $ck->get_files()
|
|
|
|
|
|
- (my @s = stat($file)) || syserr(_g("cannot fstat file %s"), $file);
|
|
|
- my $newsize = $s[7];
|
|
|
- if (defined($$size)
|
|
|
- and $newsize != $$size) {
|
|
|
- error(_g("File %s has size %u instead of expected %u"),
|
|
|
- $file, $newsize, $$size);
|
|
|
+Return the list of files whose checksums are stored in the object.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub get_files {
|
|
|
+ my ($self) = @_;
|
|
|
+ return @{$self->{files}};
|
|
|
+}
|
|
|
+
|
|
|
+=item $bool = $ck->has_file($file)
|
|
|
+
|
|
|
+Return true if we have checksums for the given file. Returns false
|
|
|
+otherwise.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub has_file {
|
|
|
+ my ($self, $file) = @_;
|
|
|
+ return exists $self->{size}{$file};
|
|
|
+}
|
|
|
+
|
|
|
+=item $ck->remove_file($file)
|
|
|
+
|
|
|
+Remove all checksums of the given file.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub remove_file {
|
|
|
+ my ($self, $file) = @_;
|
|
|
+ return unless $self->has_file($file);
|
|
|
+ delete $self->{'checksums'}{$file};
|
|
|
+ delete $self->{'size'}{$file};
|
|
|
+ @{$self->{'files'}} = grep { $_ ne $file } $self->get_files();
|
|
|
+}
|
|
|
+
|
|
|
+=item $checksum = $ck->get_checksum($file, $alg)
|
|
|
+
|
|
|
+Return the checksum of type $alg for the requested $file. This will not
|
|
|
+compute the checksum but only return the checksum stored in the object, if
|
|
|
+any.
|
|
|
+
|
|
|
+If $alg is not defined, it returns a reference to a hash: keys are
|
|
|
+the checksum algorithms and values are the checksums themselves. The
|
|
|
+hash returned must not be modified, it's internal to the object.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub get_checksum {
|
|
|
+ my ($self, $file, $alg) = @_;
|
|
|
+ $alg = lc($alg) if defined $alg;
|
|
|
+ if (exists $self->{checksums}{$file}) {
|
|
|
+ return $self->{checksums}{$file} unless defined $alg;
|
|
|
+ return $self->{checksums}{$file}{$alg};
|
|
|
}
|
|
|
- $$size = $newsize;
|
|
|
-
|
|
|
- foreach my $alg (@check_supported) {
|
|
|
- my $prog = $check_prog{$alg};
|
|
|
- my $newsum = `$prog $file`;
|
|
|
- $? && subprocerr("%s %s", $prog, $file);
|
|
|
- $newsum = extractchecksum($alg, $newsum);
|
|
|
-
|
|
|
- if (defined($checksums->{$alg})
|
|
|
- and $newsum ne $checksums->{$alg}) {
|
|
|
- error(_g("File %s has checksum %s instead of expected %s (algorithm %s)"),
|
|
|
- $file, $newsum, $checksums->{$alg}, $alg);
|
|
|
- }
|
|
|
- $checksums->{$alg} = $newsum;
|
|
|
+ return undef;
|
|
|
+}
|
|
|
+
|
|
|
+=item $size = $ck->get_size($file)
|
|
|
+
|
|
|
+Return the size of the requested file if it's available in the object.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub get_size {
|
|
|
+ my ($self, $file) = @_;
|
|
|
+ return $self->{size}{$file};
|
|
|
+}
|
|
|
+
|
|
|
+=item $ck->export_to_string($alg, %opts)
|
|
|
+
|
|
|
+Return a multi-line string containing the checksums of type $alg. The
|
|
|
+string can be stored as-is in a Checksum-* field of a Dpkg::Control
|
|
|
+object.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub export_to_string {
|
|
|
+ my ($self, $alg, %opts) = @_;
|
|
|
+ my $res = "";
|
|
|
+ foreach my $file ($self->get_files()) {
|
|
|
+ my $sum = $self->get_checksum($file, $alg);
|
|
|
+ my $size = $self->get_size($file);
|
|
|
+ next unless defined $sum and defined $size;
|
|
|
+ $res .= "\n$sum $size $file";
|
|
|
+ }
|
|
|
+ return $res;
|
|
|
+}
|
|
|
+
|
|
|
+=item $ck->export_to_control($control, %opts)
|
|
|
+
|
|
|
+Export the checksums in the Checksums-* fields of the Dpkg::Control
|
|
|
+$control object.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub export_to_control {
|
|
|
+ my ($self, $control, %opts) = @_;
|
|
|
+ $opts{use_files_for_md5} = 0 unless exists $opts{use_files_for_md5};
|
|
|
+ foreach my $alg (checksums_get_list()) {
|
|
|
+ my $key = "Checksums-$alg";
|
|
|
+ $key = "Files" if ($opts{use_files_for_md5} and $alg eq "md5");
|
|
|
+ $control->{$key} = $self->export_to_string($alg, %opts);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+=back
|
|
|
+
|
|
|
+=head1 AUTHOR
|
|
|
+
|
|
|
+Raphaël Hertzog <hertzog@debian.org>.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
1;
|