File Coverage

lib/CPANPLUS/Module/Checksums.pm
Criterion Covered Total %
statement 91 112 81.2
branch 35 62 56.4
condition 2 3 66.6
subroutine 13 13 100.0
pod 1 1 100.0
total 142 191 74.3


line stmt bran cond sub pod time code
1             package CPANPLUS::Module::Checksums;
2              
3 20     20   155 use strict;
  20         41  
  20         1049  
4 20     20   153 use vars qw[@ISA $VERSION];
  20         50  
  20         1435  
5              
6 20     20   192 use CPANPLUS::Error;
  20         37  
  20         1457  
7 20     20   122 use CPANPLUS::Internals::Constants;
  20         57  
  20         9160  
8              
9 20     20   205 use FileHandle;
  20         39  
  20         206  
10              
11 20     20   7961 use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
  20         109  
  20         194  
12 20     20   5543 use Params::Check qw[check];
  20         39  
  20         1314  
13 20     20   120 use Module::Load::Conditional qw[can_load];
  20         36  
  20         46736  
14              
15             $Params::Check::VERBOSE = 1;
16              
17             @ISA = qw[ CPANPLUS::Module::Signature ];
18             $VERSION = "0.9916";
19              
20             =head1 NAME
21              
22             CPANPLUS::Module::Checksums - checking the checksum of a distribution
23              
24             =head1 SYNOPSIS
25              
26             $file = $modobj->checksums;
27             $bool = $mobobj->_validate_checksum;
28              
29             =head1 DESCRIPTION
30              
31             This is a class that provides functions for checking the checksum
32             of a distribution. Should not be loaded directly, but used via the
33             interface provided via C.
34              
35             =head1 METHODS
36              
37             =head2 $mod->checksums
38              
39             Fetches the checksums file for this module object.
40             For the options it can take, see C.
41              
42             Returns the location of the checksums file on success and false
43             on error.
44              
45             The location of the checksums file is also stored as
46              
47             $mod->status->checksums
48              
49             =cut
50              
51             sub checksums {
52 4 50   4 1 589 my $mod = shift or return;
53              
54 4         42 my $file = $mod->_get_checksums_file( @_ );
55              
56 4 50       156 return $mod->status->checksums( $file ) if $file;
57              
58 0         0 return;
59             }
60              
61             ### checks if the package checksum matches the one
62             ### from the checksums file
63             sub _validate_checksum {
64 16     16   66 my $self = shift; #must be isa CPANPLUS::Module
65 16         135 my $conf = $self->parent->configure_object;
66 16         100 my %hash = @_;
67              
68 16         64 my $verbose;
69 16         164 my $tmpl = {
70             verbose => { default => $conf->get_conf('verbose'),
71             store => \$verbose },
72             };
73              
74 16 50       179 check( $tmpl, \%hash ) or return;
75              
76             ### if we can't check it, we must assume it's ok ###
77 16 50       1275 return $self->status->checksum_ok(1)
78             unless can_load( modules => { 'Digest::SHA' => '0.0' } );
79             #class CPANPLUS::Module::Status is runtime-generated
80              
81 16 50       85308 my $file = $self->_get_checksums_file( verbose => $verbose ) or (
82             error(loc(q[Could not fetch '%1' file], CHECKSUMS)), return );
83              
84 16 50       1070 $self->_check_signature_for_checksum_file( file => $file ) or (
85             error(loc(q[Could not verify '%1' file], CHECKSUMS)), return );
86             #for whole CHECKSUMS file
87              
88 16 50       242 my $href = $self->_parse_checksums_file( file => $file ) or (
89             error(loc(q[Could not parse '%1' file], CHECKSUMS)), return );
90              
91 16         112 my $cpan_path = $href->{ $self->package }->{'cpan_path'};
92              
93 16 50       73 if ( defined $cpan_path ) {
94 0         0 my $chk_pth = join '/', 'authors/id', $cpan_path;
95 0 0       0 if ( $chk_pth ne $self->path ) {
96 0         0 error(loc( "Archive checksum path for '%1': " .
97             "should be '%2', but it says it is '%3'. Abandoning.",
98             $self->package, $self->path, $chk_pth));
99 0         0 return $self->status->checksum_ok(0);
100             }
101             }
102              
103 16         72 my $size = $href->{ $self->package }->{'size'};
104              
105             ### the checksums file tells us the size of the archive
106             ### but the downloaded file is of different size
107 16 100       90 if( defined $size ) {
108 15 50       71 if( not (-s $self->status->fetch == $size) ) {
109 0         0 error(loc( "Archive size does not match for '%1': " .
110             "size is '%2' but should be '%3'",
111             $self->package, -s $self->status->fetch, $size));
112 0         0 return $self->status->checksum_ok(0);
113             }
114             } else {
115 1         10 msg(loc("Archive size is not known for '%1'",$self->package),$verbose);
116             }
117              
118 16         5227 my $sha = $href->{ $self->package }->{'sha256'};
119              
120 16 50       85 unless( defined $sha ) {
121 16         68 msg(loc("No 'sha256' checksum known for '%1'",$self->package),$verbose);
122              
123 16         264 return $self->status->checksum_ok(1);
124             }
125              
126 0         0 $self->status->checksum_value($sha);
127              
128              
129 0 0       0 my $fh = FileHandle->new( $self->status->fetch ) or return;
130 0         0 binmode $fh;
131              
132 0         0 my $ctx = Digest::SHA->new(256);
133 0         0 $ctx->addfile( $fh );
134              
135 0         0 my $hexdigest = $ctx->hexdigest;
136 0         0 my $flag = $hexdigest eq $sha;
137 0 0       0 $flag
138             ? msg(loc("Checksum matches for '%1'", $self->package),$verbose)
139             : error(loc("Checksum does not match for '%1': " .
140             "SHA256 is '%2' but should be '%3'",
141             $self->package, $hexdigest, $sha),$verbose);
142              
143              
144 0 0       0 return $self->status->checksum_ok(1) if $flag;
145 0         0 return $self->status->checksum_ok(0);
146             }
147              
148              
149             ### fetches the module objects checksum file ###
150             sub _get_checksums_file {
151 24     24   128 my $self = shift;
152 24         218 my %hash = @_;
153              
154 24         511 my $clone = $self->clone;
155 24         243 $clone->package( CHECKSUMS );
156              
157             # If the user specified a fetchdir, then every CHECKSUMS file will always
158             # be stored there, not in an author-specific subdir. Thus, in this case,
159             # we need to always re-fetch the CHECKSUMS file and hence need to set the
160             # TTL to something small.
161 24         415 my $have_fetchdir =
162             $self->parent->configure_object->get_conf('fetchdir') ne '';
163 24 50       243 my $ttl = $have_fetchdir ? 0.001 : 3600;
164 24 50       333 my $file = $clone->fetch( ttl => $ttl, %hash ) or return;
165              
166 24         851 return $file;
167             }
168              
169             sub _parse_checksums_file {
170 18     18   78 my $self = shift;
171 18         110 my %hash = @_;
172              
173 18         73 my $file;
174 18         152 my $tmpl = {
175             file => { required => 1, allow => FILE_READABLE, store => \$file },
176             };
177 18         130 my $args = check( $tmpl, \%hash );
178              
179 18 50       742 my $fh = OPEN_FILE->( $file ) or return;
180              
181             ### loop over the header, there might be a pgp signature ###
182 18         61 my $signed;
183 18         693 while (local $_ = <$fh>) {
184 108 100       490 last if /^\$cksum = \{\s*$/; # skip till this line
185 90         240 my $header = PGP_HEADER; # but be tolerant of whitespace
186 90 100       1029 $signed = 1 if /^${header}\s*$/;# due to crossplatform linebreaks
187             }
188              
189             ### read the filehandle, parse it rather than eval it, even though it
190             ### *should* be valid perl code
191 18         100 my $dist;
192 18         89 my $cksum = {};
193 18         110 while (local $_ = <$fh>) {
194              
195 381 100 66     3535 if (/^\s*'([^']+)' => \{\s*$/) {
    100          
    100          
    100          
196 69         293 $dist = $1;
197              
198             } elsif (/^\s*'([^']+)' => '?([^'\n]+)'?,?\s*$/ and defined $dist) {
199 173         1289 $cksum->{$dist}{$1} = $2;
200              
201             } elsif (/^\s*}[,;]?\s*$/) {
202 87         390 undef $dist;
203              
204             } elsif (/^__END__\s*$/) {
205 18         76 last;
206              
207             } else {
208 34         227 error( loc("Malformed %1 line: %2", CHECKSUMS, $_) );
209             }
210             }
211              
212 18         733 return $cksum;
213             }
214              
215             sub _check_signature_for_checksum_file {
216 16     16   70 my $self = shift;
217              
218 16         127 my $conf = $self->parent->configure_object;
219 16         112 my %hash = @_;
220              
221             ### you don't want to check signatures,
222             ### so let's just return true;
223 16 100       171 return 1 unless $conf->get_conf('signature');
224              
225 1         12 my($force,$file,$verbose);
226 1         22 my $tmpl = {
227             file => { required => 1, allow => FILE_READABLE, store => \$file },
228             force => { default => $conf->get_conf('force'), store => \$force },
229             verbose => { default => $conf->get_conf('verbose'), store => \$verbose },
230             };
231              
232 1 50       13 my $args = check( $tmpl, \%hash ) or return;
233              
234 1 50       61 my $fh = OPEN_FILE->($file) or return;
235              
236 1         12 my $signed;
237 1         51 while (local $_ = <$fh>) {
238 35         74 my $header = PGP_HEADER;
239 35 100       344 $signed = 1 if /^$header$/;
240             }
241              
242 1 50       12 if ( !$signed ) {
243 0         0 msg(loc("No signature found in %1 file '%2'",
244             CHECKSUMS, $file), $verbose);
245              
246 0 0       0 return 1 unless $force;
247              
248 0         0 error( loc( "%1 file '%2' is not signed -- aborting",
249             CHECKSUMS, $file ) );
250 0         0 return;
251              
252             }
253              
254 1 50       25 if( can_load( modules => { 'Module::Signature' => '0.06' } ) ) {
255             # local $Module::Signature::SIGNATURE = $file;
256             # ... check signatures ...
257             }
258              
259 1         918 return 1;
260             }
261              
262              
263              
264             # Local variables:
265             # c-indentation-style: bsd
266             # c-basic-offset: 4
267             # indent-tabs-mode: nil
268             # End:
269             # vim: expandtab shiftwidth=4:
270              
271             1;