File Coverage

blib/lib/CPAN/Access/AdHoc.pm
Criterion Covered Total %
statement 265 280 94.6
branch 76 110 69.0
condition 12 21 57.1
subroutine 50 51 98.0
pod 11 12 91.6
total 414 474 87.3


line stmt bran cond sub pod time code
1             package CPAN::Access::AdHoc;
2              
3 4     4   96993 use 5.008;
  4         10  
  4         130  
4              
5 4     4   17 use strict;
  4         6  
  4         169  
6 4     4   15 use warnings;
  4         5  
  4         92  
7              
8 4     4   1022 use Config::Tiny ();
  4         7  
  4         64  
9 4     4   1201 use CPAN::Access::AdHoc::Archive;
  4         12  
  4         152  
10 4         520 use CPAN::Access::AdHoc::Util qw{
11             :carp __attr __cache __expand_distribution_path __guess_media_type
12 4     4   20 };
  4         5  
13 4     4   2258 use Digest::SHA ();
  4         9818  
  4         101  
14 4     4   1176 use File::HomeDir ();
  4         56  
  4         74  
15 4     4   19 use File::Spec ();
  4         5  
  4         40  
16 4     4   15 use IO::File ();
  4         6  
  4         35  
17 4     4   2219 use LWP::UserAgent ();
  4         36187  
  4         111  
18 4     4   27 use LWP::Protocol ();
  4         6  
  4         52  
19 4     4   15 use Module::Pluggable::Object;
  4         4  
  4         66  
20 4     4   2147 use Safe;
  4         103202  
  4         246  
21 4     4   37 use Scalar::Util qw{ blessed };
  4         7  
  4         248  
22 4     4   1731 use Text::ParseWords ();
  4         4170  
  4         103  
23 4     4   25 use URI ();
  4         4  
  4         4773  
24              
25             our $VERSION = '0.000_194';
26              
27             # In the following list of attribute names, 'config' must be first
28             # because it supplies default values for everything else. 'cpan' must be
29             # after 'default_cpan_source' because 'default_cpan_source' determines
30             # how the default value of 'cpan' is computed.
31             my @attributes = qw{
32             config __debug http_error_handler default_cpan_source cpan
33             };
34              
35             sub new {
36 12     12 1 3956 my ( $class, %arg ) = @_;
37              
38 12   33     77 my $self = bless {}, ref $class || $class;
39              
40 12         37 $self->__init( \%arg );
41              
42 12 100       30 %arg
43             and __wail( 'Unknown attribute(s): ', join ', ', sort keys %arg );
44              
45 11         34 return $self;
46             }
47              
48             sub __init {
49 12     12   16 my ( $self, $arg ) = @_;
50              
51 12         23 foreach my $name ( @attributes ) {
52 60         160 $self->$name( delete $arg->{$name} );
53             }
54              
55 12         16 return $self;
56             }
57              
58             sub corpus {
59 1     1 1 219 my ( $self, $cpan_id ) = @_;
60 1         2 $cpan_id = uc $cpan_id;
61              
62 1         4 my $prefix = join '/',
63             substr( $cpan_id, 0, 1 ),
64             substr( $cpan_id, 0, 2 ),
65             $cpan_id;
66              
67             return (
68 0         0 map { "$prefix/$_" }
  0         0  
69 1         2 grep { $_ !~ m/ [.] meta \z /smx }
70 1         2 sort keys %{ $self->fetch_distribution_checksums( $cpan_id ) }
71             );
72             }
73              
74             sub exists : method { ## no critic (ProhibitBuiltinHomonyms)
75 0     0 1 0 my ( $self, $path ) = @_;
76              
77 0         0 return $self->_request_path( head => $path )->is_success();
78             }
79              
80             sub fetch {
81 24     24 1 2780 my ( $self, $path ) = @_;
82              
83 24         63 my $rslt = $self->_request_path( get => $path );
84              
85 24 100       29988 $rslt->is_success
86             or return $self->http_error_handler()->( $self, $path, $rslt );
87              
88 17         172 __guess_media_type( $rslt, $path );
89              
90 17         64 $self->_checksum( $rslt );
91              
92 17 100       120 my $archive =
93             CPAN::Access::AdHoc::Archive->__handle_http_response( $rslt )
94             or __wail( sprintf q{Unsupported Content-Type '%s'},
95             $rslt->header( 'Content-Type' ) );
96              
97 16         251 return $archive;
98             }
99              
100             sub fetch_author_index {
101 2     2 1 1771 my ( $self ) = @_;
102              
103 2         9 my $cache = $self->__cache();
104 2 50       13 exists $cache->{author_index}
105             and return $cache->{author_index};
106              
107 2         7 my $author_details = $self->fetch( 'authors/01mailrc.txt.gz' );
108 2 50       10 _got_archive( $author_details )
109             or return $author_details;
110 2         8 $author_details = $author_details->get_item_content();
111              
112 2 100       21 my $fh = IO::File->new( \$author_details, '<' )
113             or __wail( "Unable to open string reference: $!" );
114              
115 1         49 my %author_index;
116 1         6 while ( <$fh> ) {
117 2         9 s/ \s+ \z //smx;
118 2         9 my ( undef, $cpan_id, $address ) = Text::ParseWords::parse_line(
119             qr{ \s+ }smx, 0, $_ );
120 2         205 ( my $name = $address ) =~ s{ \s+ < (.*) > }{}smx;
121 2         4 my $mail_addr = $1;
122 2         14 $author_index{ uc $cpan_id } = {
123             name => $name,
124             address => $mail_addr,
125             };
126             }
127              
128 1         5 return ( $cache->{author_index} = \%author_index );
129             }
130              
131             sub fetch_distribution_archive {
132 4     4 1 1997 my ( $self, $distribution ) = @_;
133 4         18 my $path = __expand_distribution_path( $distribution );
134 4         22 return $self->fetch( "authors/id/$path" );
135             }
136              
137             sub fetch_distribution_checksums {
138 9     9 1 655 my ( $self, $distribution ) = @_;
139              
140 9         46 $distribution =~ s{ \A ( . ) / \1 ( . ) / \1 \2 ( [^/]* ) }
141             {$1$2$3}smx;
142 9 100       33 $distribution =~ m{ / }smx
143             or $distribution .= '/';
144 9 50       49 $distribution =~ m{ \A ( .* / ) ( [^/]* ) \z }smx
145             or __wail( "Invalid distribution '$distribution'" );
146 9         22 my ( $dir, $file ) = ( $1, $2 );
147              
148 9 50       28 $file eq 'CHECKSUMS'
149             and $file = '';
150 9         39 my $path = __expand_distribution_path( $dir . 'CHECKSUMS' );
151 9         43 ( $dir = $path ) =~ s{ [^/]* \z }{}smx;
152              
153 9         31 my $cache = $self->__cache();
154              
155 9 100       28 if ( ! $cache->{checksums}{$dir} ) {
156 5         23 my $archive = $self->fetch( "authors/id/$path" );
157 4 50       14 _got_archive( $archive )
158             or return $archive;
159 4         16 $cache->{checksums}{$dir} = _eval_string(
160             $archive->get_item_content() );
161             }
162              
163 8 100       349 $file eq ''
164             and return $cache->{checksums}{$dir};
165 6         36 return $cache->{checksums}{$dir}{$file};
166             }
167              
168             # TODO finish implementing error handling. See above, _got_archive().
169              
170             sub fetch_module_index {
171 5     5 1 1104 my ( $self ) = @_;
172              
173 5         23 my $cache = $self->__cache();
174              
175 0         0 exists $cache->{module_index}
176             and return wantarray ?
177 5 50       22 @{ $cache->{module_index} } :
    100          
178             $cache->{module_index}[0];
179              
180 4         8 my ( $meta, %module );
181              
182             # The only way this can return undef is if the http_error_handler
183             # returns it. We take that as a request to cache an empty index.
184 4 100       12 if ( my $packages_details = $self->fetch(
185             'modules/02packages.details.txt.gz' ) ) {
186 2         9 $packages_details = $packages_details->get_item_content();
187              
188 2 100       14 my $fh = IO::File->new( \$packages_details, '<' )
189             or __wail( "Unable to open string reference: $!" );
190              
191 1         1096 $meta = $self->_read_meta( $fh );
192              
193 1         3 while ( <$fh> ) {
194 3         4 chomp;
195 3         11 my ( $mod, @info ) = split qr{ \s+ }smx;
196             ## 'undef' eq $ver
197             ## and $ver = undef;
198 3         4 my ( $pkg, $ver ) = reverse @info;
199 3 50       6 defined $ver or $ver = 'undef';
200 3         20 $module{$mod} = {
201             distribution => $pkg,
202             version => $ver,
203             };
204             }
205              
206             } else {
207 1         18 $meta = {};
208             }
209              
210 2         15 $cache->{module_index} = [ \%module, $meta ];
211              
212 2 100       12 return wantarray ? ( \%module, $meta ) : \%module;
213             }
214              
215             sub fetch_registered_module_index {
216 2     2 1 1411 my ( $self ) = @_;
217              
218 2         26 my $cache = $self->__cache();
219 0         0 exists $cache->{registered_module_index}
220             and return wantarray ?
221 2 0       8 @{ $cache->{registered_module_index} } :
    50          
222             $cache->{registered_module_index}[0];
223              
224 2         9 my $packages_details = $self->fetch(
225             'modules/03modlist.data.gz'
226             )->get_item_content();
227              
228 2         7 my ( $meta, $reg );
229              
230             {
231              
232 2 100       3 my $fh = IO::File->new( \$packages_details, '<' )
  2         17  
233             or __wail( "Unable to open string reference: $!" );
234              
235 1         55 $meta = $self->_read_meta( $fh );
236              
237 1         4 local $/ = undef;
238 1         7 $reg = <$fh>;
239             }
240              
241 1         6 my $hash = _eval_string( "$reg\nCPAN::Modulelist->data();" );
242              
243 1         76 $cache->{registered_module_index} = [ $hash, $meta ];
244              
245 1 50       5 return wantarray ? ( $hash, $meta ) : $hash;
246             }
247              
248             sub flush {
249 13     13 1 16 my ( $self ) = @_;
250 13         20 delete $self->{'.cache'};
251 13         16 return $self;
252             }
253              
254             sub indexed_distributions {
255 1     1 1 3 my ( $self ) = @_;
256              
257 1         4 my $cache = $self->__cache();
258              
259 0         0 $cache->{indexed_distributions}
260 1 50       6 and return @{ $cache->{indexed_distributions} };
261              
262 1         3 my $inx = $self->fetch_module_index();
263              
264 1         2 my %pkg;
265 1         2 foreach my $info ( values %{ $inx } ) {
  1         3  
266 3         9 $pkg{$info->{distribution}}++;
267             }
268              
269 1         5 return @{ $cache->{indexed_distributions} = [ sort keys %pkg ] };
  1         13  
270             }
271              
272             # Set up the accessor/mutators. All mutators interpret undef as being a
273             # request to restore the default, from the configuration if that exists,
274             # or from the configured default code.
275              
276             __PACKAGE__->__create_accessor_mutators( @attributes );
277              
278             sub _create_accessor_mutator_helper {
279 40     40   38 my ( $class, $name, $code ) = @_;
280 40 100       170 $class->can( $name )
281             and return;
282 20         27 my $full_name = "${class}::$name";
283 4     4   26 no strict qw{ refs };
  4         6  
  4         468  
284 20         38 *$full_name = $code;
285 20         21 return;
286             }
287              
288             sub __create_accessor_mutators {
289 4     4   10 my ( $class, @attrs ) = @_;
290 4         10 foreach my $name ( @attrs ) {
291 20 50       117 $class->can( $name ) and next;
292 20         33 my $full_name = "${class}::$name";
293             $class->_create_accessor_mutator_helper(
294 20     12   64 "__attr__${name}__validate" => sub { return $_[1] } );
  12         13  
295             $class->_create_accessor_mutator_helper(
296 20     51   68 "__attr__${name}__post_assignment" => sub { return $_[1] } );
  51         47  
297 4     4   20 no strict qw{ refs };
  4         7  
  4         5346  
298             *$full_name = sub {
299 173     173   4404 my ( $self, @arg ) = @_;
300 173         533 my $attr = $self->__attr();
301 173 100       257 if ( @arg ) {
302 66         59 my $value = $arg[0];
303 66 100 100     252 not defined $value
304             and 'config' ne $name
305             and $value = $self->config()->{_}{$name};
306 66         57 my $code;
307 66 100 100     387 not defined $value
308             and $code = $self->can( "__attr__${name}__default" )
309             and $value = $code->( $self );
310 66 50       341 $code = $self->can( "__attr__${name}__validate" )
311             and $value = $code->( $self, $value );
312 63         112 $attr->{$name} = $value;
313 63 50       299 $code = $self->can( "__attr__${name}__post_assignment" )
314             and $code->( $self );
315 63         133 return $self;
316             } else {
317 107         1030 return $attr->{$name};
318             }
319 20         80 };
320             }
321 4         8 return;
322             }
323              
324             {
325              
326             # Compute the config file's name and location.
327              
328             ( my $dist = __PACKAGE__ ) =~ s{ :: }{-}smxg;
329             my $config_file = $dist . '.ini';
330             my $config_dir = File::HomeDir->my_dist_config( $dist );
331             my $config_path;
332             defined $config_dir
333             and $config_path = File::Spec->catfile( $config_dir, $config_file );
334              
335             sub __attr__config__default {
336 12     12   16 my ( $self ) = @_;
337 12 50 33     163 defined $config_path
338             and -f $config_path
339             and return Config::Tiny->read( $config_path );
340 12         63 return Config::Tiny->new();
341             }
342             }
343              
344             sub __attr__config__validate {
345 13     13   17 my ( $self, $value ) = @_;
346              
347 13         19 my $err = "Attribute 'config' must be a file name or a " .
348             "Config::Tiny reference";
349 13 50       32 if ( ref $value ) {
350 13 100       16 eval {
351 13         77 $value->isa( 'Config::Tiny' );
352             } or __wail( $err );
353             } else {
354 0 0       0 -f $value
355             or __wail( $err );
356 0         0 $value = Config::Tiny->read( $value );
357             }
358              
359 12         35 delete $value->{_}{config};
360 12         22 return $value;
361             }
362              
363             # The rationale of the default order is:
364             # 1) Mini cpan: guaranteed to be local, and since it is non-core,
365             # the user had to install it, and can be presumed to be using it.
366             # 2) CPAN minus: since it is non-core, the user had to install it,
367             # and can be presumed to be using it.
368             # 3) CPAN: It is core, but it needs to be set up to be used, and the
369             # wrapper will detect if it has not been set up.
370             # 4) CPANPLUS: It is core as of 5.10, and works out of the box, so
371             # we can not presume that the user actually uses it.
372             sub __attr__default_cpan_source__default {
373 6     6   10 return 'CPAN::Mini,cpanm,CPAN,CPANPLUS';
374             }
375              
376             sub DEFAULT_HTTP_ERROR_HANDLER {
377 4     4 0 8 my ( $self, $path, $resp ) = @_;
378 4         9 my $url = $self->cpan() . $path;
379 4         29 __wail( "Failed to get $url: ", $resp->status_line() );
380             }
381              
382             sub __attr__http_error_handler__default {
383 13     13   30 return \&DEFAULT_HTTP_ERROR_HANDLER;
384             }
385              
386             sub __attr__http_error_handler__validate {
387 15     15   20 my ( $self, $value ) = @_;
388 15 50       41 'CODE' eq ref $value
389             or __wail(
390             q{Attribute 'http_error_handler' must be a code reference}
391             );
392 15         19 return $value;
393             }
394              
395             sub __attr__cpan__post_assignment {
396 12     12   17 my ( $self ) = @_;
397              
398 12         49 $self->flush();
399              
400 12         8 return;
401             }
402              
403             sub __attr__cpan__validate {
404 13     13   63 my ( $self, $value ) = @_;
405              
406 13         101 $value = "$value"; # Stringify
407 13         64 $value =~ s{ (?
408              
409 13 50       43 my $url = URI->new( $value )
410             or _wail( "Bad URL '$value'" );
411 13         4284 $value = $url;
412              
413 13         61 my $scheme = $value->scheme();
414 13 100 66     338 $value->can( 'authority' )
415             and LWP::Protocol::implementor( $scheme )
416             or __wail ( "URL scheme $scheme: is unsupported" );
417              
418 12         34586 return $value;
419             }
420              
421             # Check the file's checksum if appropriate.
422             #
423             # The argument is the HTTP::Response object that contains the data to
424             # check. This object is expected to have its Content-Location set to the
425             # path relative to the root of the site.
426             #
427             # Files are not checked unless they are in authors/id/, and are not
428             # named CHECKSUM.
429              
430             sub _checksum {
431 17     17   25 my ( $self, $rslt ) = @_;
432 17 50       39 defined( my $path = $rslt->header( 'Content-Location' ) )
433             or return;
434 17 100       466 $path =~ m{ \A authors/id/ ( [^/] ) / ( \1 [^/] ) / \2 }smx
435             or return;
436 8 100       33 $path =~ m{ /CHECKSUMS \z }smx
437             and return;
438 4         7 my $cks_path = $path;
439 4 50       27 $cks_path =~ s{ \A authors/id/ }{}smx
440             or return;
441 4 50       16 my $cksum = $self->fetch_distribution_checksums( $cks_path )
442             or return;
443 0 0       0 $cksum->{sha256}
444             or return;
445 0         0 my $got = Digest::SHA::sha256_hex( $rslt->content() );
446 0 0       0 $got eq $cksum->{sha256}
447             or __wail( "Checksum failure on $path" );
448 0         0 return;
449             }
450              
451             # Expand the default_cpan_source attribute into a list of class names,
452             # each implementing one of the listed defaults.
453              
454             {
455              
456             my $search_path = 'CPAN::Access::AdHoc::Default::CPAN';
457             my %defaulter = map { (
458             $_ => $_,
459             substr( $_, length( $search_path ) + 2 ) => $_,
460             ) } Module::Pluggable::Object->new(
461             search_path => $search_path,
462             inner => 0,
463             require => 1,
464             )->plugins();
465              
466             sub __attr__default_cpan_source__validate {
467 13     13   19 my ( $self, $value ) = @_;
468              
469 13 50       166 ref $value
470             or $value = [ split qr{ \s* , \s* }smx, $value ];
471              
472 13 50       57 'ARRAY' eq ref $value
473             or __wail( q{Attribute 'default_cpan_source' takes an array } .
474             q{reference or a comma-delimited string} );
475 13         15 my @rslt;
476 13         16 foreach my $source ( @{ $value } ) {
  13         28  
477 31 100       70 defined( my $class = $defaulter{$source} )
478             or __wail( "Unknown default_cpan_source '$source'" );
479 30         48 push @rslt, $class;
480             }
481 12         29 return \@rslt;
482             }
483              
484             }
485              
486             # Eval a string in a sandbox, and return the result. This was cribbed
487             # _very_ heavily from CPAN::Distribution CHECKSUM_check_file().
488             sub _eval_string {
489 5     5   7 my ( $string ) = @_;
490 5         106 $string =~ s/ \015? \012 /\n/smxg;
491 5         35 my $sandbox = Safe->new();
492 5         3757 $sandbox->permit_only( ':default' );
493 5         39 my $rslt = $sandbox->reval( $string );
494 5 50       2207 $@ and __wail( $@ );
495 5         25 return $rslt;
496             }
497              
498             # Return the argument if it is a CPAN::Access::AdHoc::Archive; otherwise
499             # just return.
500              
501             sub _got_archive {
502 6     6   11 my ( $rtn ) = @_;
503 6 50 33     87 blessed( $rtn )
504             and $rtn->isa( 'CPAN::Access::AdHoc::Archive' )
505             and return $rtn;
506 0         0 return;
507             }
508              
509             # Get the repository URL from the first source that actually supplies
510             # it. The CPAN::Access::AdHoc::Default::CPAN plug-ins are called in the
511             # order specified in the default_cpan_source attribute, and the first
512             # source that actually supplies a URL is used. If that source provides a
513             # file: URL, the first such is returned. Otherwise the first URL is
514             # returned, whatever its scheme. If no URL can be determined, we die.
515              
516             sub __attr__cpan__default {
517 9     9   11 my ( $self ) = @_;
518              
519 9         10 my $url;
520              
521 9         16 my $debug = $self->__debug();
522              
523 9         13 foreach my $class ( @{ $self->default_cpan_source() } ) {
  9         13  
524              
525 9 50       71 my @url_list = $class->get_default()
526             or next;
527              
528 9         21 foreach ( @url_list ) {
529 12 100       35 m/ \A file: /smx
530             or next;
531 7         30 $url = $_;
532 7         11 last;
533             }
534              
535 9 100       17 defined $url
536             or $url = $url_list[0];
537              
538 9 50       15 $debug
539             and warn "Debug - Default cpan '$url' from $class\n";
540              
541 9         20 return $url;
542             }
543              
544 0         0 __wail( 'No CPAN URL obtained from ' . $self->default_cpan_source() );
545             }
546              
547             # modules/02packages.details.txt.gz and modules/03modlist.data.gz have
548             # metadata at the top. This metadata is organized as lines of
549             # key: value
550             # with the key left-justified. Lines can be wrapped, with leading
551             # spaces.
552              
553             sub _read_meta {
554 2     2   5 my ( $self, $fh ) = @_;
555 2         3 my %meta;
556             {
557 2         3 my ( $name, $value );
  2         3  
558 2         13 while ( <$fh> ) {
559 14         16 chomp;
560 14 100       35 m/ \S /smx or last;
561 12 100       25 if ( s/ \A \s+ //smx ) {
562 4         12 $meta{$name} .= " $_";
563             } else {
564 8         40 ( $name, $value ) = split qr{ : \s* }smx, $_, 2;
565 8         30 $meta{$name} = $value;
566             }
567             }
568             }
569 2         5 return \%meta;
570             }
571              
572             # Request a path relative to the root of the CPAN repository. The
573             # arguments are the request name (which must be a valid method for
574             # LWP::UserAgent, something like 'get' or 'head'. The HTTP::Response
575             # object is returned.
576              
577             sub _request_path {
578 24     24   37 my ( $self, $rqst, $path ) = @_;
579              
580 24         37 $path =~ s{ \A / }{}smx;
581              
582 24   33     183 my $ua = $self->{__user_agent} || LWP::UserAgent->new();
583              
584 24         8073 my $url = $self->cpan() . $path;
585              
586 24         164 return $ua->$rqst( $url );
587             }
588              
589              
590             1;
591              
592             __END__