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   142 use strict;
  20         42  
  20         741  
4 20     20   130 use vars qw[@ISA $VERSION];
  20         65  
  20         1113  
5              
6 20     20   146 use CPANPLUS::Error;
  20         42  
  20         1225  
7 20     20   139 use CPANPLUS::Internals::Constants;
  20         42  
  20         7619  
8              
9 20     20   154 use FileHandle;
  20         61  
  20         187  
10              
11 20     20   7130 use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
  20         41  
  20         173  
12 20     20   5396 use Params::Check qw[check];
  20         49  
  20         1214  
13 20     20   146 use Module::Load::Conditional qw[can_load];
  20         42  
  20         28706  
14              
15             $Params::Check::VERBOSE = 1;
16              
17             @ISA = qw[ CPANPLUS::Module::Signature ];
18             $VERSION = "0.9914";
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 644 my $mod = shift or return;
53              
54 4         34 my $file = $mod->_get_checksums_file( @_ );
55              
56 4 50       66 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   108 my $self = shift; #must be isa CPANPLUS::Module
65 16         215 my $conf = $self->parent->configure_object;
66 16         101 my %hash = @_;
67              
68 16         86 my $verbose;
69 16         222 my $tmpl = {
70             verbose => { default => $conf->get_conf('verbose'),
71             store => \$verbose },
72             };
73              
74 16 50       147 check( $tmpl, \%hash ) or return;
75              
76             ### if we can't check it, we must assume it's ok ###
77 16 50       1520 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       82234 my $file = $self->_get_checksums_file( verbose => $verbose ) or (
82             error(loc(q[Could not fetch '%1' file], CHECKSUMS)), return );
83              
84 16 50       1197 $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       487 my $href = $self->_parse_checksums_file( file => $file ) or (
89             error(loc(q[Could not parse '%1' file], CHECKSUMS)), return );
90              
91 16         153 my $cpan_path = $href->{ $self->package }->{'cpan_path'};
92              
93 16 50       87 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         87 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       83 if( defined $size ) {
108 15 50       89 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         6 msg(loc("Archive size is not known for '%1'",$self->package),$verbose);
116             }
117              
118 16         1903 my $sha = $href->{ $self->package }->{'sha256'};
119              
120 16 50       75 unless( defined $sha ) {
121 16         73 msg(loc("No 'sha256' checksum known for '%1'",$self->package),$verbose);
122              
123 16         262 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   133 my $self = shift;
152 24         178 my %hash = @_;
153              
154 24         321 my $clone = $self->clone;
155 24         265 $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         137 my $have_fetchdir =
162             $self->parent->configure_object->get_conf('fetchdir') ne '';
163 24 50       215 my $ttl = $have_fetchdir ? 0.001 : 3600;
164 24 50       412 my $file = $clone->fetch( ttl => $ttl, %hash ) or return;
165              
166 24         682 return $file;
167             }
168              
169             sub _parse_checksums_file {
170 18     18   90 my $self = shift;
171 18         169 my %hash = @_;
172              
173 18         64 my $file;
174 18         164 my $tmpl = {
175             file => { required => 1, allow => FILE_READABLE, store => \$file },
176             };
177 18         157 my $args = check( $tmpl, \%hash );
178              
179 18 50       654 my $fh = OPEN_FILE->( $file ) or return;
180              
181             ### loop over the header, there might be a pgp signature ###
182 18         74 my $signed;
183 18         717 while (local $_ = <$fh>) {
184 108 100       551 last if /^\$cksum = \{\s*$/; # skip till this line
185 90         246 my $header = PGP_HEADER; # but be tolerant of whitespace
186 90 100       962 $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         78 my $dist;
192 18         62 my $cksum = {};
193 18         141 while (local $_ = <$fh>) {
194              
195 381 100 66     3481 if (/^\s*'([^']+)' => \{\s*$/) {
    100          
    100          
    100          
196 69         400 $dist = $1;
197              
198             } elsif (/^\s*'([^']+)' => '?([^'\n]+)'?,?\s*$/ and defined $dist) {
199 173         1409 $cksum->{$dist}{$1} = $2;
200              
201             } elsif (/^\s*}[,;]?\s*$/) {
202 87         354 undef $dist;
203              
204             } elsif (/^__END__\s*$/) {
205 18         91 last;
206              
207             } else {
208 34         211 error( loc("Malformed %1 line: %2", CHECKSUMS, $_) );
209             }
210             }
211              
212 18         553 return $cksum;
213             }
214              
215             sub _check_signature_for_checksum_file {
216 16     16   114 my $self = shift;
217              
218 16         1024 my $conf = $self->parent->configure_object;
219 16         130 my %hash = @_;
220              
221             ### you don't want to check signatures,
222             ### so let's just return true;
223 16 100       183 return 1 unless $conf->get_conf('signature');
224              
225 1         6 my($force,$file,$verbose);
226 1         28 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       21 my $args = check( $tmpl, \%hash ) or return;
233              
234 1 50       65 my $fh = OPEN_FILE->($file) or return;
235              
236 1         4 my $signed;
237 1         40 while (local $_ = <$fh>) {
238 35         66 my $header = PGP_HEADER;
239 35 100       200 $signed = 1 if /^$header$/;
240             }
241              
242 1 50       15 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       37 if( can_load( modules => { 'Module::Signature' => '0.06' } ) ) {
255             # local $Module::Signature::SIGNATURE = $file;
256             # ... check signatures ...
257             }
258              
259 1         866 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;