File Coverage

blib/lib/CPAN/Checksums.pm
Criterion Covered Total %
statement 158 207 76.3
branch 47 102 46.0
condition 13 48 27.0
subroutine 19 19 100.0
pod 1 4 25.0
total 238 380 62.6


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