summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2015-04-11 05:46:40 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2015-04-11 05:46:40 +0000
commit26d8d4d1d2a7a8ebcffc6ff5c6b13495f74dd129 (patch)
tree15bdb6a40ae142e85d0c30789e2d387d3f9b2a84 /lib
downloadCPAN-Checksums-tarball-master.tar.gz
Diffstat (limited to 'lib')
-rw-r--r--lib/CPAN/Checksums.pm432
1 files changed, 432 insertions, 0 deletions
diff --git a/lib/CPAN/Checksums.pm b/lib/CPAN/Checksums.pm
new file mode 100644
index 0000000..342e392
--- /dev/null
+++ b/lib/CPAN/Checksums.pm
@@ -0,0 +1,432 @@
+package CPAN::Checksums;
+
+use strict;
+use vars qw(
+ $CAUTION
+ $DIRNAME
+ $IGNORE_MATCH
+ $MIN_MTIME_CHECKSUMS
+ $SIGNING_KEY
+ $SIGNING_PROGRAM
+ $TRY_SHORTNAME
+ $VERSION
+ @EXPORT_OK
+ @ISA
+ );
+
+require Exporter;
+
+@ISA = qw(Exporter);
+@EXPORT_OK = qw(updatedir);
+$VERSION = "2.10";
+$VERSION =~ s/_//;
+$CAUTION ||= 0;
+$TRY_SHORTNAME ||= 0;
+$SIGNING_PROGRAM ||= 'gpg --clearsign --default-key ';
+$SIGNING_KEY ||= '';
+$MIN_MTIME_CHECKSUMS ||= 0;
+$IGNORE_MATCH = qr{(?i-xsm:readme$)};
+
+use DirHandle ();
+use IO::File ();
+use Digest::MD5 ();
+use Compress::Bzip2();
+use Compress::Zlib ();
+use File::Spec ();
+use File::Temp;
+use Data::Dumper ();
+use Data::Compare ();
+use Digest::SHA ();
+
+sub _dir_to_dref {
+ my($dirname,$old_dref) = @_;
+ my($dref) = {};
+ my($dh)= DirHandle->new;
+ my($fh) = new IO::File;
+ $dh->open($dirname) or die "Couldn't opendir $dirname\: $!";
+ my(%shortnameseen);
+ DIRENT: for my $de ($dh->read) {
+ next if $de =~ /^\./;
+ next if substr($de,0,9) eq "CHECKSUMS";
+ next if $IGNORE_MATCH && $de =~ $IGNORE_MATCH;
+
+ my $abs = File::Spec->catfile($dirname,$de);
+
+ #
+ # SHORTNAME offers an 8.3 name, probably not needed but it was
+ # always there,,,
+ #
+ if ($TRY_SHORTNAME) {
+ my $shortname = lc $de;
+ $shortname =~ s/\.tar[._-]gz$/\.tgz/;
+ my $suffix;
+ ($suffix = $shortname) =~ s/.*\.//;
+ substr($suffix,3) = "" if length($suffix) > 3;
+ my @p;
+ if ($shortname =~ /\-/) {
+ @p = $shortname =~ /(.{1,16})-.*?([\d\.]{2,8})/;
+ } else {
+ @p = $shortname =~ /(.{1,8}).*?([\d\.]{2,8})/;
+ }
+ $p[0] ||= lc $de;
+ $p[0] =~ s/[^a-z0-9]//g;
+ $p[1] ||= 0;
+ $p[1] =~ s/\D//g;
+ my $counter = 7;
+ while (length($p[0]) + length($p[1]) > 8) {
+ substr($p[0], $counter) = "" if length($p[0]) > $counter;
+ substr($p[1], $counter) = "" if length($p[1]) > $counter--;
+ }
+ my $dot = $suffix ? "." : "";
+ $shortname = "$p[0]$p[1]$dot$suffix";
+ while (exists $shortnameseen{$shortname}) {
+ my($modi) = $shortname =~ /([a-z\d]+)/;
+ $modi++;
+ $shortname = "$modi$dot$suffix";
+ if (++$counter > 1000){ # avoid endless loops and accept the buggy choice
+ warn "Warning: long loop on shortname[$shortname]de[$de]";
+ last;
+ }
+ }
+ $dref->{$de}->{shortname} = $shortname;
+ $shortnameseen{$shortname} = undef; # for exists check good enough
+ }
+
+ #
+ # STAT facts
+ #
+ if (-l File::Spec->catdir($dirname,$de)){
+ # Symlinks are a mess on a replicated, database driven system,
+ # but as they are not forbidden, we cannot ignore them. We do
+ # have a directory with nothing but a symlink in it. When we
+ # ignored the symlink, we did not write a CHECKSUMS file and
+ # CPAN.pm issued lots of warnings:-(
+ $dref->{$de}{issymlink} = 1;
+ }
+ if (-d File::Spec->catdir($dirname,$de)){
+ $dref->{$de}{isdir} = 1;
+ } else {
+ my @stat = stat $abs or next DIRENT;
+ $dref->{$de}{size} = $stat[7];
+ my(@gmtime) = gmtime $stat[9];
+ $gmtime[4]++;
+ $gmtime[5]+=1900;
+ $dref->{$de}{mtime} = sprintf "%04d-%02d-%02d", @gmtime[5,4,3];
+ _add_digests($de,$dref,"Digest::SHA",[256],"sha256",$abs,$old_dref);
+ my $can_reuse_old_md5 = 1;
+ COMPARE: for my $param (qw(size mtime sha256)) {
+ if (!exists $old_dref->{$de}{$param} ||
+ $dref->{$de}{$param} ne $old_dref->{$de}{$param}) {
+ $can_reuse_old_md5 = 0;
+ last COMPARE;
+ }
+ }
+ if ( $can_reuse_old_md5 ) {
+ for my $param (qw(md5 md5-ungz md5-unbz2)) {
+ next unless exists $old_dref->{$de}{$param};
+ $dref->{$de}{$param} = $old_dref->{$de}{$param};
+ }
+ } else {
+ _add_digests($de,$dref,"Digest::MD5",[],"md5",$abs,$old_dref);
+ }
+
+ } # ! -d
+ }
+ $dh->close;
+ $dref;
+}
+
+sub _read_old_ddump {
+ my($ckfn) = @_;
+ my $is_signed = 0;
+ my($fh) = new IO::File;
+ my $old_ddump = "";
+ if ($fh->open($ckfn)) {
+ local $/ = "\n";
+ while (<$fh>) {
+ next if /^\#/;
+ $is_signed = 1 if /SIGNED MESSAGE/;
+ $old_ddump .= $_;
+ }
+ close $fh;
+ }
+ return($old_ddump,$is_signed);
+}
+
+sub updatedir ($) {
+ my($dirname) = @_;
+ my $ckfn = File::Spec->catfile($dirname, "CHECKSUMS"); # checksum-file-name
+ my($old_ddump,$is_signed) = _read_old_ddump($ckfn);
+ my($old_dref) = makehashref($old_ddump);
+ my $dref = _dir_to_dref($dirname,$old_dref);
+ local $Data::Dumper::Indent = 1;
+ local $Data::Dumper::Quotekeys = 1;
+ local $Data::Dumper::Sortkeys = 1;
+ my $ddump = Data::Dumper->new([$dref],["cksum"])->Dump;
+ my @ckfnstat = stat $ckfn;
+ if ($old_ddump) {
+ local $DIRNAME = $dirname;
+ if ( !!$SIGNING_KEY == !!$is_signed ) { # either both or neither
+ if (!$MIN_MTIME_CHECKSUMS || $ckfnstat[9] > $MIN_MTIME_CHECKSUMS ) {
+ # recent enough
+ return 1 if $old_ddump eq $ddump;
+ return 1 if ckcmp($old_dref,$dref);
+ }
+ }
+ if ($CAUTION) {
+ my $report = investigate($old_dref,$dref);
+ warn $report if $report;
+ }
+ }
+ my $ft = File::Temp->new(
+ DIR => $dirname,
+ TEMPLATE => "CHECKSUMS.XXXX",
+ CLEANUP => 0,
+ ) or die;
+ my $tckfn = $ft->filename;
+ close $ft;
+ my($fh) = new IO::File;
+ open $fh, ">$tckfn\0" or die "Couldn't open >$tckfn\: $!";
+
+ local $\;
+ if ($SIGNING_KEY) {
+ print $fh "0&&<<''; # this PGP-signed message is also valid perl\n";
+ close $fh;
+ open $fh, "| $SIGNING_PROGRAM $SIGNING_KEY >> $tckfn"
+ or die "Could not call gpg: $!";
+ $ddump .= "__END__\n";
+ }
+
+ my $message = sprintf "# CHECKSUMS file written on %s GMT by CPAN::Checksums (v%s)\n%s",
+ scalar gmtime, $VERSION, $ddump;
+ print $fh $message;
+ my $success = close $fh;
+ if ($SIGNING_KEY && !$success) {
+ warn "Couldn't run '$SIGNING_PROGRAM $SIGNING_KEY'!
+Writing to $tckfn directly";
+ open $fh, ">$tckfn\0" or die "Couldn't open >$tckfn\: $!";
+ print $fh $message;
+ close $fh or warn "Couldn't close $tckfn: $!";
+ }
+ chmod 0644, $ckfn or die "Couldn't chmod to 0644 for $ckfn\: $!" if -f $ckfn;
+ rename $tckfn, $ckfn or die "Could not rename: $!";
+ chmod 0444, $ckfn or die "Couldn't chmod to 0444 for $ckfn\: $!";
+ return 2;
+}
+
+sub _add_digests ($$$$$$$) {
+ my($de,$dref,$module,$constructor_args,$keyname,$abs,$old_dref) = @_;
+ my($fh) = new IO::File;
+ my $dig = $module->new(@$constructor_args);
+ $fh->open("$abs\0") or die "Couldn't open $abs: $!";
+ binmode($fh); # make sure it's called as a function, solaris with
+ # perl 5.8.4 complained about missing method in
+ # IO::File
+ $dig->addfile($fh);
+ $fh->close;
+ my $digest = $dig->hexdigest;
+ $dref->{$de}{$keyname} = $digest;
+ $dig = $module->new(@$constructor_args);
+ if ($de =~ /\.gz$/) {
+ my($buffer, $zip);
+ if (exists $old_dref->{$de}{$keyname} &&
+ $dref->{$de}{$keyname} eq $old_dref->{$de}{$keyname} &&
+ exists $old_dref->{$de}{"$keyname-ungz"}
+ ) {
+ $dref->{$de}{"$keyname-ungz"} = $old_dref->{$de}{"$keyname-ungz"};
+ return;
+ }
+ if ($zip = Compress::Zlib::gzopen($abs, "rb")) {
+ $dig->add($buffer)
+ while $zip->gzread($buffer) > 0;
+ $dref->{$de}{"$keyname-ungz"} = $dig->hexdigest;
+ $zip->gzclose;
+ }
+ } elsif ($de =~ /\.bz2$/) {
+ my($buffer, $zip);
+ if (exists $old_dref->{$de}{$keyname} &&
+ $dref->{$de}{$keyname} eq $old_dref->{$de}{$keyname} &&
+ exists $old_dref->{$de}{"$keyname-unbz2"}
+ ) {
+ $dref->{$de}{"$keyname-unbz2"} = $old_dref->{$de}{"$keyname-unbz2"};
+ return;
+ }
+ if ($zip = Compress::Bzip2::bzopen($abs, "rb")) {
+ $dig->add($buffer)
+ while $zip->bzread($buffer) > 0;
+ $dref->{$de}{"$keyname-unbz2"} = $dig->hexdigest;
+ $zip->bzclose;
+ }
+ }
+}
+
+sub ckcmp ($$) {
+ my($old,$new) = @_;
+ for ($old,$new) {
+ $_ = makehashref($_);
+ }
+ Data::Compare::Compare($old,$new);
+}
+
+# see if a file changed but the name not
+sub investigate ($$) {
+ my($old,$new) = @_;
+ for ($old,$new) {
+ $_ = makehashref($_);
+ }
+ my $complain = "";
+ for my $dist (sort keys %$new) {
+ if (exists $old->{$dist}) {
+ my $headersaid;
+ for my $diff (qw/md5 sha256 size md5-ungz sha256-ungz mtime/) {
+ next unless exists $old->{$dist}{$diff} &&
+ exists $new->{$dist}{$diff};
+ next if $old->{$dist}{$diff} eq $new->{$dist}{$diff};
+ $complain .=
+ scalar gmtime().
+ " GMT:\ndiffering old/new version of same file $dist:\n"
+ unless $headersaid++;
+ $complain .=
+ qq{\t$diff "$old->{$dist}{$diff}" -> "$new->{$dist}{$diff}"\n}; #};
+ }
+ }
+ }
+ $complain;
+}
+
+sub makehashref ($) {
+ local($_) = shift;
+ unless (ref $_ eq "HASH") {
+ require Safe;
+ my($comp) = Safe->new("CPAN::Checksums::reval");
+ my $cksum; # used by Data::Dumper
+ $_ = $comp->reval($_) || {};
+ die "CPAN::Checksums: Caught error[$@] while checking $DIRNAME" if $@;
+ }
+ $_;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+CPAN::Checksums - Write a C<CHECKSUMS> file for a directory as on CPAN
+
+=head1 SYNOPSIS
+
+ use CPAN::Checksums qw(updatedir);
+ my $success = updatedir($directory);
+
+=head1 INCOMPATIBILITY ALERT
+
+Since version 1.0 the generation of the attribute C<shortname> is
+turned off by default. It was too slow and was not used as far as I
+know, and above all, it could fail on large directories. The shortname
+feature can still be turned on by setting the global variable
+$TRY_SHORTNAME to a true value.
+
+=head1 DESCRIPTION
+
+=over 2
+
+=item $success = updatedir($dir)
+
+C<updatedir()> takes a directory name as argument and writes a typical
+C<CHECKSUMS> file in that directory as used on CPAN unless a previously
+written C<CHECKSUMS> file is there that is still valid. Returns 2 if a
+new C<CHECKSUMS> file has been written, 1 if a valid C<CHECKSUMS> file is
+already there, otherwise dies.
+
+Note: since version 2.0 updatedir on empty directories behaves just
+the same. In older versions it silently did nothing.
+
+=back
+
+=head2 Global Variables in package CPAN::Checksums
+
+=over
+
+=item $IGNORE_MATCH
+
+If the global variable $IGNORE_MATCH is set, then all files matching
+this expression will be completely ignored and will not be included in
+the CPAN C<CHECKSUMS> files. Per default this variable is set to
+
+ qr{(?i-xsm:readme$)}
+
+=item $CAUTION
+
+Setting the global variable $CAUTION causes updatedir() to report
+changes of files in the attributes C<size>, C<mtime>, C<md5>, or
+C<md5-ungz> to STDERR.
+
+=item $TRY_SHORTNAME
+
+By setting the global variable $TRY_SHORTNAME to a true value, you can
+tell updatedir() to include an attribute C<shortname> in the resulting
+hash that is 8.3-compatible. Please note, that updatedir() in this
+case may be slow and may even fail on large directories, because it
+will always only try 1000 iterations to find a name that is not yet
+taken and then give up.
+
+=item $SIGNING_KEY
+
+Setting the global variable $SIGNING_KEY makes the generated C<CHECKSUMS>
+file to be clear-signed by the command specified in $SIGNING_PROGRAM
+(defaults to C<gpg --clearsign --default-key >), passing the signing
+key as an extra argument. The resulting C<CHECKSUMS> file should look like:
+
+ 0&&<<''; # this PGP-signed message is also valid perl
+ -----BEGIN PGP SIGNED MESSAGE-----
+ Hash: SHA1
+
+ # CHECKSUMS file written on ... by CPAN::Checksums (v...)
+ $cksum = {
+ ...
+ };
+
+ __END__
+ -----BEGIN PGP SIGNATURE-----
+ ...
+ -----END PGP SIGNATURE-----
+
+note that the actual data remains intact, but two extra lines are
+added to make it legal for both OpenPGP and perl syntax.
+
+=item $MIN_MTIME_CHECKSUMS
+
+If the global variable $MIN_MTIME_CHECKSUMS is set, then updatedir
+will renew signatures on checksum files that have an older mtime than
+the given value.
+
+=back
+
+=head1 PREREQUISITES
+
+DirHandle, IO::File, Digest::MD5, Digest::SHA, Compress::Bzip2,
+Compress::Zlib, File::Spec, Data::Dumper, Data::Compare, File::Temp
+
+=head1 BUGS
+
+If updatedir is interrupted, it may leave a temporary file lying
+around. These files have the File::Temp template C<CHECKSUMS.XXXX> and
+should be harvested by a cronjob.
+
+=head1 AUTHOR
+
+Andreas Koenig, andreas.koenig@anima.de; GnuPG support by Autrijus Tang
+
+=head1 COPYRIGHT & LICENSE
+
+Copyright (c) 2002-2008 Andreas Koenig, Audrey Tang, Steve Peters.
+
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+perl(1).
+
+=cut