File Coverage

blib/lib/CPAN/Checksums.pm
Criterion Covered Total %
statement 162 209 77.5
branch 58 102 56.8
condition 25 48 52.0
subroutine 19 19 100.0
pod 1 4 25.0
total 265 382 69.3


line stmt bran cond sub pod time code
1             # -*- cperl-indent-level: 2 -*-
2             package CPAN::Checksums;
3              
4 1     1   3519 use strict;
  1         3  
  1         40  
5 1         222 use vars qw(
6             $CAUTION
7             $DIRNAME
8             $IGNORE_MATCH
9             $MIN_MTIME_CHECKSUMS
10             $SIGNING_KEY
11             $SIGNING_PROGRAM
12             $TRY_SHORTNAME
13             $VERSION
14             @EXPORT_OK
15             @ISA
16 1     1   6 );
  1         1  
17              
18             require Exporter;
19              
20             @ISA = qw(Exporter);
21             @EXPORT_OK = qw(updatedir);
22             $VERSION = "2.14";
23             $VERSION =~ s/_//;
24             $CAUTION ||= 0;
25             $TRY_SHORTNAME ||= 0;
26             $SIGNING_PROGRAM ||= 'gpg --clearsign --default-key ';
27             $SIGNING_KEY ||= '';
28             $MIN_MTIME_CHECKSUMS ||= 0;
29             $IGNORE_MATCH = qr{(?i-xsm:readme$)};
30              
31 1     1   550 use DirHandle ();
  1         1736  
  1         22  
32 1     1   504 use IO::File ();
  1         8501  
  1         25  
33 1     1   8 use Digest::MD5 ();
  1         1  
  1         15  
34 1     1   2042 use Compress::Bzip2();
  1         10502  
  1         31  
35 1     1   599 use Compress::Zlib ();
  1         56103  
  1         28  
36 1     1   8 use File::Spec ();
  1         2  
  1         18  
37 1     1   1536 use File::Temp;
  1         9957  
  1         72  
38 1     1   592 use Data::Dumper ();
  1         6320  
  1         27  
39 1     1   462 use Data::Compare ();
  1         12249  
  1         25  
40 1     1   532 use Digest::SHA ();
  1         3060  
  1         2538  
41              
42             sub _dir_to_dref {
43 10     10   36 my($dirname,$old_dref,$root) = @_;
44 10         1436 my $cpan_path = File::Spec->abs2rel( $dirname, $root ) ;
45 10         55 my($dref) = {};
46 10         104 my($dh)= DirHandle->new;
47 10         360 my($fh) = new IO::File;
48 10 50       411 $dh->open($dirname) or die "Couldn't opendir $dirname\: $!";
49 10         730 my(%shortnameseen);
50 10         63 DIRENT: for my $de ($dh->read) {
51 70 100       761 next DIRENT if $de =~ /^\./;
52 50 100       149 next DIRENT if substr($de,0,9) eq "CHECKSUMS";
53 41 50 33     314 next DIRENT if $IGNORE_MATCH && $de =~ $IGNORE_MATCH;
54              
55 41         473 my $abs = File::Spec->catfile($dirname,$de);
56              
57             #
58             # SHORTNAME offers an 8.3 name, probably not needed but it was
59             # always there,,,
60             #
61 41 50       134 if ($TRY_SHORTNAME) {
62 0         0 my $shortname = lc $de;
63 0         0 $shortname =~ s/\.tar[._-]gz$/\.tgz/;
64 0         0 my $suffix;
65 0         0 ($suffix = $shortname) =~ s/.*\.//;
66 0 0       0 substr($suffix,3) = "" if length($suffix) > 3;
67 0         0 my @p;
68 0 0       0 if ($shortname =~ /\-/) {
69 0         0 @p = $shortname =~ /(.{1,16})-.*?([\d\.]{2,8})/;
70             } else {
71 0         0 @p = $shortname =~ /(.{1,8}).*?([\d\.]{2,8})/;
72             }
73 0   0     0 $p[0] ||= lc $de;
74 0         0 $p[0] =~ s/[^a-z0-9]//g;
75 0   0     0 $p[1] ||= 0;
76 0         0 $p[1] =~ s/\D//g;
77 0         0 my $counter = 7;
78 0         0 while (length($p[0]) + length($p[1]) > 8) {
79 0 0       0 substr($p[0], $counter) = "" if length($p[0]) > $counter;
80 0 0       0 substr($p[1], $counter) = "" if length($p[1]) > $counter--;
81             }
82 0 0       0 my $dot = $suffix ? "." : "";
83 0         0 $shortname = "$p[0]$p[1]$dot$suffix";
84 0         0 while (exists $shortnameseen{$shortname}) {
85 0         0 my($modi) = $shortname =~ /([a-z\d]+)/;
86 0         0 $modi++;
87 0         0 $shortname = "$modi$dot$suffix";
88 0 0       0 if (++$counter > 1000){ # avoid endless loops and accept the buggy choice
89 0         0 warn "Warning: long loop on shortname[$shortname]de[$de]";
90 0         0 last;
91             }
92             }
93 0         0 $dref->{$de}->{shortname} = $shortname;
94 0         0 $shortnameseen{$shortname} = undef; # for exists check good enough
95             }
96              
97             #
98             # STAT facts
99             #
100 41 50       785 if (-l File::Spec->catdir($dirname,$de)){
101             # Symlinks are a mess on a replicated, database driven system,
102             # but as they are not forbidden, we cannot ignore them. We do
103             # have a directory with nothing but a symlink in it. When we
104             # ignored the symlink, we did not write a CHECKSUMS file and
105             # CPAN.pm issued lots of warnings:-(
106 0         0 $dref->{$de}{issymlink} = 1;
107             }
108 41 50       703 if (-d File::Spec->catdir($dirname,$de)){
109 0         0 $dref->{$de}{isdir} = 1;
110             } else {
111 41 50       518 my @stat = stat $abs or next DIRENT;
112 41         214 $dref->{$de}{size} = $stat[7];
113 41         233 my(@gmtime) = gmtime $stat[9];
114 41         71 $gmtime[4]++;
115 41         91 $gmtime[5]+=1900;
116 41         236 $dref->{$de}{mtime} = sprintf "%04d-%02d-%02d", @gmtime[5,4,3];
117 41         174 _add_digests($de,$dref,"Digest::SHA",[256],"sha256",$abs,$old_dref);
118 41         89 my $can_reuse_old_md5 = 1;
119 41         80 COMPARE: for my $param (qw(size mtime sha256)) {
120 115 100 100     531 if (!exists $old_dref->{$de}{$param} ||
121             $dref->{$de}{$param} ne $old_dref->{$de}{$param}) {
122 4         10 $can_reuse_old_md5 = 0;
123 4         11 last COMPARE;
124             }
125             }
126 41 50 100     245 if ($can_reuse_old_md5
      66        
      66        
      66        
127             and $de =~ /\.(gz|tgz|bz2|tbz)$/
128             and exists $old_dref->{$de}{md5}
129             and !exists $old_dref->{$de}{"md5-ungz"}
130             and !exists $old_dref->{$de}{"md5-unbz2"}
131             ) {
132 0         0 $can_reuse_old_md5 = 0;
133             }
134 41 100       79 if ( $can_reuse_old_md5 ) {
135 37         66 MD5KEY: for my $param (qw(md5 md5-ungz md5-unbz2)) {
136 111 100       287 next MD5KEY unless exists $old_dref->{$de}{$param};
137 47         122 $dref->{$de}{$param} = $old_dref->{$de}{$param};
138             }
139             } else {
140 4         14 _add_digests($de,$dref,"Digest::MD5",[],"md5",$abs,$old_dref);
141             }
142              
143             } # ! -d
144 41         123 $dref->{$de}{cpan_path} = $cpan_path;
145              
146             }
147 10         71 $dh->close;
148 10         286 $dref;
149             }
150              
151             sub _read_old_ddump {
152 10     10   33 my($ckfn) = @_;
153 10         22 my $is_signed = 0;
154 10         94 my($fh) = new IO::File;
155 10         513 my $old_ddump = "";
156 10 100       47 if ($fh->open($ckfn)) {
157 9         758 local $/ = "\n";
158 9         367 while (<$fh>) {
159 327 100       732 next if /^\#/;
160 318 50       595 $is_signed = 1 if /SIGNED MESSAGE/;
161 318         764 $old_ddump .= $_;
162             }
163 9         192 close $fh;
164             }
165 10         155 return($old_ddump,$is_signed);
166             }
167              
168             sub updatedir ($;$) {
169 10     10 1 2104706 my($dirname, $root) = @_;
170 10         177 my $ckfn = File::Spec->catfile($dirname, "CHECKSUMS"); # checksum-file-name
171 10         63 my($old_ddump,$is_signed) = _read_old_ddump($ckfn);
172 10         58 my($old_dref) = makehashref($old_ddump);
173 10         43 my $dref = _dir_to_dref($dirname,$old_dref,$root);
174 10         254 local $Data::Dumper::Indent = 1;
175 10         23 local $Data::Dumper::Quotekeys = 1;
176 10         23 local $Data::Dumper::Sortkeys = 1;
177 10         117 my $ddump = Data::Dumper->new([$dref],["cksum"])->Dump;
178 10         1874 my @ckfnstat = stat $ckfn;
179 10 100       52 if ($old_ddump) {
180 9         22 local $DIRNAME = $dirname;
181 9 50       31 if ( !!$SIGNING_KEY == !!$is_signed ) { # either both or neither
182 9 100 66     36 if (!$MIN_MTIME_CHECKSUMS || $ckfnstat[9] > $MIN_MTIME_CHECKSUMS ) {
183             # recent enough
184 8 100       66 return 1 if $old_ddump eq $ddump;
185 4 50       13 return 1 if ckcmp($old_dref,$dref);
186             }
187             }
188 5 100       6073 if ($CAUTION) {
189 1         4 my $report = investigate($old_dref,$dref);
190 1 50       21 warn $report if $report;
191             }
192             }
193 6 50       84 my $ft = File::Temp->new(
194             DIR => $dirname,
195             TEMPLATE => "CHECKSUMS.XXXX",
196             CLEANUP => 0,
197             ) or die;
198 6         3729 my $tckfn = $ft->filename;
199 6         132 close $ft;
200 6         42 my($fh) = new IO::File;
201 6 50       551 open $fh, ">$tckfn\0" or die "Couldn't open >$tckfn\: $!";
202              
203 6         43 local $\;
204 6 50       20 if ($SIGNING_KEY) {
205 0         0 print $fh "0&&<<''; # this PGP-signed message is also valid perl\n";
206 0         0 close $fh;
207 0 0       0 open $fh, "| $SIGNING_PROGRAM $SIGNING_KEY >> $tckfn"
208             or die "Could not call gpg: $!";
209 0         0 $ddump .= "__END__\n";
210             }
211              
212 6         97 my $message = sprintf "# CHECKSUMS file written on %s GMT by CPAN::Checksums (v%s)\n%s",
213             scalar gmtime, $VERSION, $ddump;
214 6         48 print $fh $message;
215 6         1371 my $success = close $fh;
216 6 50 33     38 if ($SIGNING_KEY && !$success) {
217 0         0 warn "Couldn't run '$SIGNING_PROGRAM $SIGNING_KEY'!
218             Writing to $tckfn directly";
219 0 0       0 open $fh, ">$tckfn\0" or die "Couldn't open >$tckfn\: $!";
220 0         0 print $fh $message;
221 0 0       0 close $fh or warn "Couldn't close $tckfn: $!";
222             }
223 6 100 50     167 chmod 0644, $ckfn or die "Couldn't chmod to 0644 for $ckfn\: $!" if -f $ckfn;
224 6 50       447 rename $tckfn, $ckfn or die "Could not rename: $!";
225 6 50       103 chmod 0444, $ckfn or die "Couldn't chmod to 0444 for $ckfn\: $!";
226 6         86 return 2;
227             }
228              
229             sub _add_digests ($$$$$$$) {
230 45     45   133 my($de,$dref,$module,$constructor_args,$keyname,$abs,$old_dref) = @_;
231 45         262 my($fh) = new IO::File;
232 45         1593 my $dig = $module->new(@$constructor_args);
233 45 50       623 $fh->open("$abs\0") or die "Couldn't open $abs: $!";
234 45         2131 binmode($fh); # make sure it's called as a function, solaris with
235             # perl 5.8.4 complained about missing method in
236             # IO::File
237 45         17445 $dig->addfile($fh);
238 45         118147 $fh->close;
239 45         1035 my $digest = $dig->hexdigest;
240 45         167 $dref->{$de}{$keyname} = $digest;
241 45         240 $dig = $module->new(@$constructor_args);
242 45 100       946 if ($de =~ /\.(gz|tgz)$/) {
    100          
243 5         14 my($buffer, $zip);
244 5 50 33     52 if (exists $old_dref->{$de}{$keyname} &&
      33        
245             $dref->{$de}{$keyname} eq $old_dref->{$de}{$keyname} &&
246             exists $old_dref->{$de}{"$keyname-ungz"}
247             ) {
248 5         22 $dref->{$de}{"$keyname-ungz"} = $old_dref->{$de}{"$keyname-ungz"};
249 5         30 return;
250             }
251 0 0       0 if ($zip = Compress::Zlib::gzopen($abs, "rb")) {
252 0         0 $dig->add($buffer)
253             while $zip->gzread($buffer) > 0;
254 0         0 $dref->{$de}{"$keyname-ungz"} = $dig->hexdigest;
255 0         0 $zip->gzclose;
256             }
257             } elsif ($de =~ /\.(bz2|tbz)$/) {
258 5         13 my($buffer, $zip);
259 5 50 33     49 if (exists $old_dref->{$de}{$keyname} &&
      33        
260             $dref->{$de}{$keyname} eq $old_dref->{$de}{$keyname} &&
261             exists $old_dref->{$de}{"$keyname-unbz2"}
262             ) {
263 5         21 $dref->{$de}{"$keyname-unbz2"} = $old_dref->{$de}{"$keyname-unbz2"};
264 5         29 return;
265             }
266 0 0       0 if ($zip = Compress::Bzip2::bzopen($abs, "rb")) {
267 0         0 $dig->add($buffer)
268             while $zip->bzread($buffer) > 0;
269 0         0 $dref->{$de}{"$keyname-unbz2"} = $dig->hexdigest;
270 0         0 $zip->bzclose;
271             }
272             }
273             }
274              
275             sub ckcmp ($$) {
276 4     4 0 12 my($old,$new) = @_;
277 4         11 for ($old,$new) {
278 8         17 $_ = makehashref($_);
279             }
280 4         23 Data::Compare::Compare($old,$new);
281             }
282              
283             # see if a file changed but the name not
284             sub investigate ($$) {
285 1     1 0 4 my($old,$new) = @_;
286 1         3 for ($old,$new) {
287 2         31 $_ = makehashref($_);
288             }
289 1         3 my $complain = "";
290 1         9 for my $dist (sort keys %$new) {
291 7 50       25 if (exists $old->{$dist}) {
292 7         10 my $headersaid;
293 7         12 for my $diff (qw/md5 sha256 size md5-ungz sha256-ungz mtime/) {
294             next unless exists $old->{$dist}{$diff} &&
295 42 100 66     109 exists $new->{$dist}{$diff};
296 30 100       66 next if $old->{$dist}{$diff} eq $new->{$dist}{$diff};
297 3 100       23 $complain .=
298             scalar gmtime().
299             " GMT:\ndiffering old/new version of same file $dist:\n"
300             unless $headersaid++;
301 3         12 $complain .=
302             qq{\t$diff "$old->{$dist}{$diff}" -> "$new->{$dist}{$diff}"\n}; #};
303             }
304             }
305             }
306 1         3 $complain;
307             }
308              
309             sub makehashref ($) {
310 20     20 0 49 local($_) = shift;
311 20 100       66 unless (ref $_ eq "HASH") {
312 10         1816 require Safe;
313 10         40287 my($comp) = Safe->new("CPAN::Checksums::reval");
314 10         11147 my $cksum; # used by Data::Dumper
315 10   100     49 $_ = $comp->reval($_) || {};
316 10 50       9425 die "CPAN::Checksums: Caught error[$@] while checking $DIRNAME" if $@;
317             }
318 20         207 $_;
319             }
320              
321             1;
322              
323             __END__