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   2724 use strict;
  1         3  
  1         42  
5 1         212 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   5 );
  1         2  
17              
18             require Exporter;
19              
20             @ISA = qw(Exporter);
21             @EXPORT_OK = qw(updatedir);
22             $VERSION = "2.13";
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   470 use DirHandle ();
  1         1768  
  1         24  
32 1     1   451 use IO::File ();
  1         7599  
  1         25  
33 1     1   7 use Digest::MD5 ();
  1         2  
  1         14  
34 1     1   593 use Compress::Bzip2();
  1         10207  
  1         29  
35 1     1   615 use Compress::Zlib ();
  1         57728  
  1         29  
36 1     1   8 use File::Spec ();
  1         3  
  1         17  
37 1     1   805 use File::Temp;
  1         10059  
  1         70  
38 1     1   634 use Data::Dumper ();
  1         6669  
  1         30  
39 1     1   515 use Data::Compare ();
  1         13149  
  1         27  
40 1     1   517 use Digest::SHA ();
  1         3245  
  1         2672  
41              
42             sub _dir_to_dref {
43 10     10   30 my($dirname,$old_dref,$root) = @_;
44 10         940 my $cpan_path = File::Spec->abs2rel( $dirname, $root ) ;
45 10         43 my($dref) = {};
46 10         75 my($dh)= DirHandle->new;
47 10         289 my($fh) = new IO::File;
48 10 50       312 $dh->open($dirname) or die "Couldn't opendir $dirname\: $!";
49 10         509 my(%shortnameseen);
50 10         53 DIRENT: for my $de ($dh->read) {
51 70 100       572 next DIRENT if $de =~ /^\./;
52 50 100       150 next DIRENT if substr($de,0,9) eq "CHECKSUMS";
53 41 50 33     281 next DIRENT if $IGNORE_MATCH && $de =~ $IGNORE_MATCH;
54              
55 41         461 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       129 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       765 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       673 if (-d File::Spec->catdir($dirname,$de)){
109 0         0 $dref->{$de}{isdir} = 1;
110             } else {
111 41 50       551 my @stat = stat $abs or next DIRENT;
112 41         205 $dref->{$de}{size} = $stat[7];
113 41         202 my(@gmtime) = gmtime $stat[9];
114 41         61 $gmtime[4]++;
115 41         82 $gmtime[5]+=1900;
116 41         221 $dref->{$de}{mtime} = sprintf "%04d-%02d-%02d", @gmtime[5,4,3];
117 41         152 _add_digests($de,$dref,"Digest::SHA",[256],"sha256",$abs,$old_dref);
118 41         92 my $can_reuse_old_md5 = 1;
119 41         88 COMPARE: for my $param (qw(size mtime sha256)) {
120 113 100 100     523 if (!exists $old_dref->{$de}{$param} ||
121             $dref->{$de}{$param} ne $old_dref->{$de}{$param}) {
122 5         11 $can_reuse_old_md5 = 0;
123 5         13 last COMPARE;
124             }
125             }
126 41 50 100     232 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       80 if ( $can_reuse_old_md5 ) {
135 36         60 MD5KEY: for my $param (qw(md5 md5-ungz md5-unbz2)) {
136 108 100       283 next MD5KEY unless exists $old_dref->{$de}{$param};
137 46         116 $dref->{$de}{$param} = $old_dref->{$de}{$param};
138             }
139             } else {
140 5         18 _add_digests($de,$dref,"Digest::MD5",[],"md5",$abs,$old_dref);
141             }
142              
143             } # ! -d
144 41         127 $dref->{$de}{cpan_path} = $cpan_path;
145              
146             }
147 10         63 $dh->close;
148 10         242 $dref;
149             }
150              
151             sub _read_old_ddump {
152 10     10   30 my($ckfn) = @_;
153 10         19 my $is_signed = 0;
154 10         78 my($fh) = new IO::File;
155 10         411 my $old_ddump = "";
156 10 100       38 if ($fh->open($ckfn)) {
157 9         611 local $/ = "\n";
158 9         286 while (<$fh>) {
159 327 100       603 next if /^\#/;
160 318 50       534 $is_signed = 1 if /SIGNED MESSAGE/;
161 318         679 $old_ddump .= $_;
162             }
163 9         128 close $fh;
164             }
165 10         130 return($old_ddump,$is_signed);
166             }
167              
168             sub updatedir ($;$) {
169 10     10 1 2030771 my($dirname, $root) = @_;
170 10         127 my $ckfn = File::Spec->catfile($dirname, "CHECKSUMS"); # checksum-file-name
171 10         40 my($old_ddump,$is_signed) = _read_old_ddump($ckfn);
172 10         38 my($old_dref) = makehashref($old_ddump);
173 10         36 my $dref = _dir_to_dref($dirname,$old_dref,$root);
174 10         247 local $Data::Dumper::Indent = 1;
175 10         17 local $Data::Dumper::Quotekeys = 1;
176 10         24 local $Data::Dumper::Sortkeys = 1;
177 10         87 my $ddump = Data::Dumper->new([$dref],["cksum"])->Dump;
178 10         1738 my @ckfnstat = stat $ckfn;
179 10 100       46 if ($old_ddump) {
180 9         25 local $DIRNAME = $dirname;
181 9 50       36 if ( !!$SIGNING_KEY == !!$is_signed ) { # either both or neither
182 9 100 66     35 if (!$MIN_MTIME_CHECKSUMS || $ckfnstat[9] > $MIN_MTIME_CHECKSUMS ) {
183             # recent enough
184 8 100       90 return 1 if $old_ddump eq $ddump;
185 4 50       17 return 1 if ckcmp($old_dref,$dref);
186             }
187             }
188 5 100       6128 if ($CAUTION) {
189 1         4 my $report = investigate($old_dref,$dref);
190 1 50       18 warn $report if $report;
191             }
192             }
193 6 50       68 my $ft = File::Temp->new(
194             DIR => $dirname,
195             TEMPLATE => "CHECKSUMS.XXXX",
196             CLEANUP => 0,
197             ) or die;
198 6         3295 my $tckfn = $ft->filename;
199 6         124 close $ft;
200 6         45 my($fh) = new IO::File;
201 6 50       537 open $fh, ">$tckfn\0" or die "Couldn't open >$tckfn\: $!";
202              
203 6         49 local $\;
204 6 50       22 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         85 my $message = sprintf "# CHECKSUMS file written on %s GMT by CPAN::Checksums (v%s)\n%s",
213             scalar gmtime, $VERSION, $ddump;
214 6         42 print $fh $message;
215 6         684 my $success = close $fh;
216 6 50 33     39 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     165 chmod 0644, $ckfn or die "Couldn't chmod to 0644 for $ckfn\: $!" if -f $ckfn;
224 6 50       15482 rename $tckfn, $ckfn or die "Could not rename: $!";
225 6 50       126 chmod 0444, $ckfn or die "Couldn't chmod to 0444 for $ckfn\: $!";
226 6         85 return 2;
227             }
228              
229             sub _add_digests ($$$$$$$) {
230 46     46   149 my($de,$dref,$module,$constructor_args,$keyname,$abs,$old_dref) = @_;
231 46         254 my($fh) = new IO::File;
232 46         1565 my $dig = $module->new(@$constructor_args);
233 46 50       616 $fh->open("$abs\0") or die "Couldn't open $abs: $!";
234 46         2081 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 46         18082 $dig->addfile($fh);
238 46         118798 $fh->close;
239 46         1056 my $digest = $dig->hexdigest;
240 46         164 $dref->{$de}{$keyname} = $digest;
241 46         241 $dig = $module->new(@$constructor_args);
242 46 100       939 if ($de =~ /\.(gz|tgz)$/) {
    100          
243 5         12 my($buffer, $zip);
244 5 50 33     60 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         24 $dref->{$de}{"$keyname-ungz"} = $old_dref->{$de}{"$keyname-ungz"};
249 5         28 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         12 my($buffer, $zip);
259 5 50 33     52 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         27 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 11 my($old,$new) = @_;
277 4         12 for ($old,$new) {
278 8         17 $_ = makehashref($_);
279             }
280 4         27 Data::Compare::Compare($old,$new);
281             }
282              
283             # see if a file changed but the name not
284             sub investigate ($$) {
285 1     1 0 5 my($old,$new) = @_;
286 1         3 for ($old,$new) {
287 2         33 $_ = makehashref($_);
288             }
289 1         3 my $complain = "";
290 1         7 for my $dist (sort keys %$new) {
291 7 50       27 if (exists $old->{$dist}) {
292 7         9 my $headersaid;
293 7         13 for my $diff (qw/md5 sha256 size md5-ungz sha256-ungz mtime/) {
294             next unless exists $old->{$dist}{$diff} &&
295 42 100 66     112 exists $new->{$dist}{$diff};
296 30 100       68 next if $old->{$dist}{$diff} eq $new->{$dist}{$diff};
297 4 100       20 $complain .=
298             scalar gmtime().
299             " GMT:\ndiffering old/new version of same file $dist:\n"
300             unless $headersaid++;
301 4         16 $complain .=
302             qq{\t$diff "$old->{$dist}{$diff}" -> "$new->{$dist}{$diff}"\n}; #};
303             }
304             }
305             }
306 1         4 $complain;
307             }
308              
309             sub makehashref ($) {
310 20     20 0 41 local($_) = shift;
311 20 100       58 unless (ref $_ eq "HASH") {
312 10         601 require Safe;
313 10         37924 my($comp) = Safe->new("CPAN::Checksums::reval");
314 10         9889 my $cksum; # used by Data::Dumper
315 10   100     37 $_ = $comp->reval($_) || {};
316 10 50       8225 die "CPAN::Checksums: Caught error[$@] while checking $DIRNAME" if $@;
317             }
318 20         170 $_;
319             }
320              
321             1;
322              
323             __END__