File Coverage

blib/lib/CPAN/Access/AdHoc.pm
Criterion Covered Total %
statement 263 273 96.3
branch 76 108 70.3
condition 11 18 61.1
subroutine 49 49 100.0
pod 10 11 90.9
total 409 459 89.1


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