File Coverage

lib/Archive/Probe.pm
Criterion Covered Total %
statement 224 323 69.3
branch 85 174 48.8
condition 5 24 20.8
subroutine 33 39 84.6
pod 7 7 100.0
total 354 567 62.4


line stmt bran cond sub pod time code
1             package Archive::Probe;
2             #
3             # This class searches and extracts files matching given pattern within
4             # deeply nested archive files. Mixed archive types are supported.
5             # Pre-requisite: unrar, 7za should be in PATH
6             # Get free unrar from: http://www.rarlab.com/rar_add.htm
7             # Get free 7za from: http://www.7-zip.org
8             # Author: JustinZhang
9             # Creation Date: 2013-05-06
10             #
11 8     8   778283 use strict;
  8         21  
  8         261  
12 8     8   44 use warnings;
  8         14  
  8         247  
13 8     8   42 use Carp;
  8         17  
  8         551  
14 8     8   43 use File::Basename;
  8         14  
  8         771  
15 8     8   8236 use File::Copy;
  8         25600  
  8         695  
16 8     8   63 use File::Path;
  8         16  
  8         480  
17 8     8   48 use File::Spec::Functions qw(catdir catfile devnull path);
  8         17  
  8         657  
18 8     8   47 use File::Temp qw(tempfile);
  8         14  
  8         74644  
19              
20             our $VERSION = "0.86";
21              
22             my %_CMD_LOC_FOR = ();
23              
24             =pod
25              
26             =head1 NAME
27              
28             Archive::Probe - A generic library to search file within archive
29              
30             =head1 SYNOPSIS
31              
32             use Archive::Probe;
33              
34             my $tmpdir = '';
35             my $base = '';
36             my $probe = Archive::Probe->new();
37             $probe->working_dir($tmpdir);
38             $probe->add_pattern(
39             '',
40             sub {
41             my ($pattern, $file_ref) = @_;
42              
43             # do something with result files
44             });
45             $probe->search($base, 1);
46              
47             # or use it as generic archive extractor
48             use Archive::Probe;
49              
50             my $archive = '';
51             my $dest_dir = '';
52             $probe->extract($archive, $dest_dir, 1);
53              
54             =head1 DESCRIPTION
55              
56             Archive::Probe is a generic utility to search or extract archives.
57              
58             It facilitates searching of particular file by name or content inside
59             deeply nested archive with mixed types. It can also extract embedded
60             archive inside the master archive recursively. It is built on top of
61             common archive tools such as 7zip, unrar, unzip and tar. It supports
62             common archive types such as .tar, .tgz, .bz2, .rar, .zip .7z and Java
63             archive such as .jar, .war, .ear. If the target archive file contains
64             another archive file of same or other type, this module extracts the
65             embedded archive to fulfill the inquiry. The level of embedding is
66             unlimited. This module depends on unzip, unrar, 7za and tar which are
67             assumed to be present in PATH. The 7za is part of 7zip utility. It is
68             preferred tool to deal with .zip archive it runs faster and handles meta
69             character better than unzip. The 7zip is open source software and you
70             download and install it from www.7-zip.org or install the binary package
71             p7zip with your favorite package management software. The unrar is
72             freeware which can be downloaded from http://www.rarlab.com/rar_add.htm.
73              
74             =cut
75              
76             =head1 METHODS
77              
78             =head2 constructor new()
79              
80             Creates a new C object.
81              
82             =cut
83              
84             sub new {
85 8     8 1 5233 my $self = shift;
86              
87 8   33     75 my $class = ref $self || $self;
88 8         515 return bless {}, $class;
89             }
90              
91             =head2 add_pattern($pattern, $callback)
92              
93             Register a file pattern to search with in the archive file(s) and the
94             callback code to handle the matched files. The callback will be passed
95             two arguments:
96              
97             =over 4
98              
99             =item $pattern
100              
101             This is the pattern of files to be searched.
102              
103             =item $callback
104              
105             This is the callback to examine the search result. The array reference
106             to the files matched the pattern is passed to the callback. If you want
107             to examine the content of the matched files, then you set the second
108             argument of the C method to true.
109              
110             =back
111              
112             =cut
113              
114             sub add_pattern {
115 4     4 1 1230 my ($self, $pattern, $callback) = @_;
116              
117             # validate pattern and callback
118 4 50       171 confess("Pattern is mandatory\n") unless $pattern;
119 4 50       177 confess("Code reference is expected\n") unless ref($callback) eq 'CODE';
120              
121 4         15 my $pattern_map = $self->_search_pattern();
122 4 100       344 if (!$pattern_map) {
123 2         4 $pattern_map = {};
124 2         179 $self->_search_pattern($pattern_map);
125             }
126              
127 4         371 $pattern_map->{$pattern} = [$callback];
128             }
129              
130             =head2 search($base, $extract_matched)
131              
132             Search files of interest under 'base' and invoke the callback.
133             It requires two arguments:
134              
135             =over 4
136              
137             =item $base
138              
139             This is the directory containing the archive file(s) or the archive file
140             itself.
141              
142             =item $extract_matched
143              
144             Extract or copy the matched files to the working directory
145             if this parameter evaluates to true. This is useful when you need search
146             files based on their content not just by name.
147              
148             =back
149              
150             =cut
151              
152             sub search {
153 2     2 1 24 my ($self, $base, $do_extract) = @_;
154            
155 2         5 my @queue = ();
156 2         7 push @queue, $base;
157              
158 2         9 while (my $path = shift @queue) {
159 10 100       519 if (-d $path) {
    50          
160 4 50       229 opendir(my $dh, $path) or do {
161 0         0 carp("Can't read directory due to: $!\n");
162 0         0 next;
163             };
164              
165 4         151 while (my $entry = readdir($dh)) {
166 16 100 100     107 next if $entry eq '.' || $entry eq '..';
167 8         75 push @queue, catfile($path, $entry);
168             }
169 4         80 closedir($dh);
170             }
171             elsif (-f $path) {
172 6         12 my $new_base = $base;
173 6 50       15 $new_base = dirname($base) if $base eq $path;
174             # Test if the file matches regestered pattern
175 6         34 $self->_match($do_extract, $new_base, '', $path);
176 6 100       26 if ($self->_is_archive_file($path)) {
177 5         13 my $ctx = $self->_strip_dir($new_base, $path) ;
178 5 50       18 $ctx .= '__' if $ctx ne '';
179 5         17 $self->_search_in_archive(
180             $do_extract,
181             $new_base,
182             $ctx,
183             $path
184             );
185             }
186             }
187             }
188              
189             # check search result & invoke callback
190 2         19 $self->_callback();
191             }
192              
193             =head2 extract($base, $to_dir, $recursive, $flat)
194              
195             Extract archive to given destination directory.
196             It requires three arguments:
197              
198             =over 4
199              
200             =item $base
201              
202             This is the path to the archive file or the base archive directory.
203              
204             =item $to_dir
205              
206             The destination directory.
207              
208             =item $recursive
209              
210             Recursively extract all embedded archive files in the master archive if
211             this parameter evaluates to true. It defaults to true.
212              
213             =item $flat
214              
215             If this parameter evaluates to true, C extracts embedded
216             archives under the same folder as their containing folder in recursive
217             mode. Otherwise, it extracts the content of embedded archives into their
218             own directories to avoid files with same name from different embedded
219             archive being overwritten. Default is false.
220              
221             =item return value
222              
223             The return value of this method evaluates to true if the archive is
224             extacted successfully. Otherwise, it evaluates to false.
225              
226             =back
227              
228             =cut
229              
230             sub extract {
231 0     0 1 0 my ($self, $base, $to_dir, $recursive, $flat) = @_;
232            
233 0 0       0 $recursive = 1 unless defined($recursive);
234 0         0 my @queue = ();
235 0         0 my %searched_for = ();
236 0         0 push @queue, $base;
237              
238 0         0 while (my $path = shift @queue) {
239 0 0       0 if (-d $path) {
    0          
240             # search archives in this directory
241 0         0 my $ret = opendir(my $dh, $path);
242 0 0       0 if (!$ret) {
243 0         0 carp("Can't read directory due to: $!\n");
244 0         0 next;
245             }
246              
247 0         0 while (my $entry = readdir($dh)) {
248 0 0 0     0 next if $entry eq '.' || $entry eq '..';
249 0         0 my $f = catfile($path, $entry);
250 0 0 0     0 if (-d $f ) {
    0          
251 0         0 push @queue, $f;
252             }
253             elsif (-f $f && $self->_is_archive_file($f)) {
254 0 0       0 push @queue, $f unless $searched_for{$f};
255             }
256             }
257 0         0 closedir($dh);
258             }
259             elsif ($self->_is_archive_file($path)) {
260 0         0 $searched_for{$path} = 1;
261             # extract archive and find any embedded archives
262             # if recursive extraction is required
263 0         0 my $dest_dir = $to_dir;
264 0 0       0 if (index($path, $to_dir) >= 0) {
265 0 0       0 if ($flat) {
266 0         0 $dest_dir = dirname($path);
267             }
268             else {
269 0         0 $dest_dir = catdir(
270             dirname($path),
271             basename($path) . "__"
272             );
273             }
274             }
275 0         0 my $ret = $self->_extract_archive_file($path, "", $dest_dir);
276 0 0 0     0 if ($ret && $recursive) {
    0          
277 0         0 push @queue, $dest_dir;
278             }
279             elsif (!$ret) {
280 0         0 return 0;
281             }
282             }
283             }
284 0         0 return 1;
285             }
286              
287             =head2 reset_matches()
288              
289             Reset the matched files list.
290              
291             =cut
292              
293             sub reset_matches {
294 2     2 1 210 my ($self) = @_;
295              
296 2         20 my $patterns = $self->_search_pattern();
297 2         10 foreach my $pat (keys(%$patterns)) {
298 4         14 undef($patterns->{$pat}[1]);
299             }
300             }
301              
302             =head1 ACCESSORS
303              
304             =head2 working_dir([$directory])
305              
306             Set or get the working directory where the temporary files will be created.
307              
308             =cut
309              
310             sub working_dir {
311 10     10 1 36 my ($self, $value) = @_;
312              
313 10 100       37 if(defined $value) {
314 2         17 my $oldval = $self->{working_dir};
315 2         1610 $self->{working_dir} = $value;
316 2         189 return $oldval;
317             }
318              
319 8         39 return $self->{working_dir};
320             }
321              
322             =head2 show_extracting_output([BOOL])
323              
324             Enable or disable the output of command line archive tool.
325              
326             =cut
327              
328             sub show_extracting_output {
329 5     5 1 11 my ($self, $value) = @_;
330              
331 5 50       17 if(defined $value) {
332 0         0 my $oldval = $self->{show_extracting_output};
333 0         0 $self->{show_extracting_output} = $value;
334 0         0 return $oldval;
335             }
336              
337 5         24 return $self->{show_extracting_output};
338             }
339              
340             sub _extract_matched {
341 4     4   16 my ($self, $base_dir, $ctx, $file, $do_extract) = @_;
342              
343 4         9 my $dest;
344 4         32 my $work_dir = $self->working_dir();
345             # extract the matched file here
346 4 100       29 if ($ctx ne '') {
347             # parent file location = $base_dir + substr($ctx, 0, -2)
348 3         51 my $parent = catfile($base_dir, substr($ctx, 0, -2));
349 3         24 my $extract_dir = catdir($work_dir, $ctx);
350 3 50       16 if ($do_extract) {
351 3         128 my $ret = $self->_extract_archive_file(
352             $parent,
353             $file,
354             $extract_dir
355             );
356 3 50       85 if (!$ret) {
357 0         0 carp("$file can not be extracted from $parent, ignored\n");
358 0         0 return;
359             }
360             }
361 3         69 $dest = catfile($extract_dir, $file);
362             }
363             else {
364             # matched files are unarchived
365             # copy to working directory as-is
366             # create absent local dir first
367 1         10 my $local_path = $self->_strip_dir($base_dir, $file);
368 1         9 $dest = catfile($work_dir, $local_path);
369              
370 1 50       12 if ($do_extract) {
371 1         33 my $dir2 = catdir($work_dir, $self->_dir_name($local_path));
372 1 50       342 mkpath($dir2) unless -d $dir2;
373 1         20 my $ret = copy($file, $dest);
374 1 50       422 if (!$ret) {
375 0         0 carp("Can't copy file $file to $dest due to: $!\n");
376 0         0 return;
377             }
378             }
379             }
380 4         68 return $dest;
381             }
382              
383             sub _match {
384 11     11   94 my ($self, $do_extract, $base_dir, $ctx, $file) = @_;
385              
386 11         21 my $matches = 0;
387 11         132 my $part = $self->_strip_dir(catdir($base_dir, $ctx), $file);
388 11         69 my $patterns = $self->_search_pattern();
389 11         73 foreach my $pat (keys(%$patterns)) {
390 25 100       738 if ($part =~ /$pat/) {
391 4         27 $matches ++;
392 4         25 my $dest = $self->_extract_matched(
393             $base_dir,
394             $ctx,
395             $file,
396             $do_extract
397             );
398             # do not add file to matched list if extract fails
399 4 50       34 next unless $dest;
400              
401 4         23 my $pat_ref = $patterns->{$pat};
402 4 50       55 if (!defined($pat_ref->[1])) {
403 4         183 $pat_ref->[1] = [$dest];
404             }
405             else {
406 0         0 push @{$pat_ref->[1]}, $dest;
  0         0  
407             }
408             }
409             }
410 11         62 return $matches;
411             }
412              
413             sub _callback {
414 2     2   6 my ($self) = @_;
415              
416 2         13 my $patterns = $self->_search_pattern();
417 2         17 foreach my $pat (keys(%$patterns)) {
418 4         14 my $pat_ref = $patterns->{$pat};
419 4 50 33     49 if (ref($pat_ref->[0]) eq 'CODE' && defined($pat_ref->[1])) {
420 4         40 $pat_ref->[0]->($pat, $pat_ref->[1]);
421             }
422             }
423 2         17 $self->_cleanup();
424             }
425              
426             sub _search_in_archive {
427 7     7   43 my ($self, $do_extract, $base_dir, $ctx, $file) = @_;
428              
429 7 100       185 if ($file =~ /\.zip$|\.jar$|\.war$|\.ear$/) {
    50          
    100          
    100          
    100          
    50          
430 1 50       6 if ($self->_is_cmd_avail('7za')) {
431             $self->_peek_archive(
432             $do_extract,
433             $base_dir,
434             $ctx,
435             $file,
436             '7za l',
437             '(-+)\s+(-+)\s+(-+)\s+(-+)\s+(-+)',
438             '---+',
439             '',
440             sub {
441 0     0   0 my ($entry, undef, undef, undef, undef, $file_pos) = @_;
442 0         0 my (undef, undef, $a, undef) = split(' ', $entry, 4);
443 0 0       0 return if $a =~ /^D/;
444 0 0 0     0 if ($file_pos && $file_pos < length($entry)) {
445 0         0 my $f = substr($entry, $file_pos);
446 0         0 return $f;
447             }
448 0         0 return;
449             }
450 0         0 );
451             }
452             else {
453             $self->_peek_archive(
454             $do_extract,
455             $base_dir,
456             $ctx,
457             $file,
458             "unzip -l",
459             "--------",
460             "--------",
461             '',
462             sub {
463 0     0   0 my ($entry) = @_;
464 0         0 my (undef, undef, undef, $f) = split(' ', $entry, 4);
465 0         0 return $f;
466             }
467 1         22 );
468              
469             }
470             }
471             elsif ($file =~ /\.7z$/) {
472             $self->_peek_archive(
473             $do_extract,
474             $base_dir,
475             $ctx,
476             $file,
477             '7za l',
478             '(-+)\s+(-+)\s+(-+)\s+(-+)\s+(-+)',
479             '---+',
480             '',
481             sub {
482 0     0   0 my ($entry, undef, undef, undef, undef, $file_pos_7z) = @_;
483 0         0 my (undef, undef, $a, undef) = split(' ', $entry, 4);
484 0 0       0 return if $a =~ /^D/;
485 0 0 0     0 if ($file_pos_7z && $file_pos_7z < length($entry)) {
486 0         0 my $f = substr($entry, $file_pos_7z);
487 0         0 return $f;
488             }
489 0         0 return;
490             }
491 0         0 );
492             }
493             elsif ($file =~ /\.rar$/) {
494             $self->_peek_archive(
495             $do_extract,
496             $base_dir,
497             $ctx,
498             $file,
499             "unrar vb",
500             '',
501             '',
502             '',
503             sub {
504 0     0   0 my ($entry) = @_;
505 0         0 return $entry;
506             }
507 2         12 );
508             }
509             elsif ($file =~ /\.tgz$|\.tar\.gz$|\.tar\.Z$/) {
510             $self->_peek_archive(
511             $do_extract,
512             $base_dir,
513             $ctx,
514             $file,
515             "tar -tzf",
516             '',
517             '',
518             '\/$',
519             sub {
520 3     3   23 my ($entry) = @_;
521 3         21 return $entry;
522             }
523 2         59 );
524             }
525             elsif ($file =~ /\.bz2$/) {
526             $self->_peek_archive(
527             $do_extract,
528             $base_dir,
529             $ctx,
530             $file,
531             "tar -tjf",
532             '',
533             '',
534             '\/$',
535             sub {
536 1     1   15 my ($entry) = @_;
537 1         14 return $entry;
538             }
539 1         588 );
540             }
541             elsif ($file =~ /\.tar$/) {
542             $self->_peek_archive(
543             $do_extract,
544             $base_dir,
545             $ctx,
546             $file,
547             "tar -tf",
548             '',
549             '',
550             '\/$',
551             sub {
552 1     1   4 my ($entry) = @_;
553 1         6 return $entry;
554             }
555 1         9 );
556             }
557             else {
558 0         0 carp("Archive file $file is not supported\n");
559             }
560             }
561              
562             sub _peek_archive {
563 7     7   33 my ($self,
564             $do_extract,
565             $base_dir,
566             $ctx,
567             $file,
568             $list_cmd,
569             $begin_pat,
570             $end_pat,
571             $ignore_pat,
572             $sub
573             ) = @_;
574              
575             # stop peeking if archive tool is not available
576 7         28 my ($ar_cmd) = split(/\s+/, $list_cmd);
577 7 100       34 if (!$self->_is_cmd_avail($ar_cmd)) {
578 3         489 carp("$ar_cmd not in PATH, archive $file ignored\n");
579 3         465 return;
580             }
581            
582 4         28 my $tmpdir = $self->working_dir();
583 4         29 my $lst_file = $self->_get_list_file();
584 4         23 my $cmd = join(" ", $list_cmd, $self->_escape($file));
585 4         17 my $cmd_shell = "$cmd > $lst_file 2>&1";
586 4         91485 my $ret = system($cmd_shell);
587 4 50       185 if ($ret != 0) {
588 0         0 carp("Can't run $cmd\n");
589 0         0 return;
590             }
591 4         544 $ret = open(my $fh, q{<}, "$lst_file");
592 4 50       32 if (!$ret) {
593 0         0 carp("Can't open file $lst_file due to: $!\n");
594 0         0 return;
595             }
596              
597 4         20 my @col_indexes;
598 4         10 my $file_list_begin = 0;
599 4         264 while(<$fh>) {
600 5         23 chomp;
601 5         21 my $line = $_;
602 5 50       28 if ($begin_pat) {
603 0 0       0 if (! $file_list_begin) {
604             # determine if the start of file list and
605             # calculate start position of each column
606 0         0 my @captures = $line =~ /$begin_pat/g;
607 0 0       0 if (@captures) {
608 0         0 my $pos = 0;
609 0         0 $file_list_begin = 1;
610 0         0 foreach my $cap (@captures) {
611 0         0 push @col_indexes, index($line, $cap, $pos);
612 0         0 $pos += length($cap);
613             }
614             }
615 0         0 next;
616             }
617             }
618              
619 5 50       26 if ($ignore_pat) {
620 5 50       119 next if /$ignore_pat/;
621             }
622              
623 5 50       40 if ($end_pat) {
624 0 0       0 last if /$end_pat/;
625             }
626              
627 5         69 my $f = $sub->($line, @col_indexes);
628             # ignore empty line, usually directory
629 5 50       23 next unless $f;
630 5         87 $self->_match($do_extract, $base_dir, $ctx, $f);
631 5 100       230 if ($self->_is_archive_file($f)) {
632 2         10 my $extract_dir = catdir($tmpdir, $ctx);
633 2         14 my $ret = $self->_extract_archive_file($file, $f, $extract_dir);
634 2 50       36 if ($ret) {
635 2         59 my $new_ctx = catfile($ctx, $f . '__');
636 2         82 $self->_search_in_archive(
637             $do_extract,
638             $tmpdir,
639             $new_ctx,
640             catfile($extract_dir, $f)
641             );
642             }
643             else {
644 0         0 carp("$f can not be extracted from $file, ignored\n");
645             }
646             }
647             }
648 4         278 close($fh);
649             }
650              
651             sub _extract_archive_file {
652 5     5   13 my ($self, $parent, $file, $extract_dir) = @_;
653              
654 5 100       1826 mkpath($extract_dir) unless -d $extract_dir;
655 5         32 my $cmd = "";
656 5 50       173 if ($parent =~ /\.zip$|\.jar$|\.war$|\.ear$/) {
    50          
    50          
    100          
    100          
    50          
657 0 0       0 if ($self->_is_cmd_avail('7za')) {
658             # specify dummy password to make 7za fail fast
659             # instead of waiting for user input password when
660             # the zip file is password-protected
661 0         0 $cmd = $self->_build_cmd(
662             '7za x -y -pxxx',
663             $extract_dir,
664             $parent,
665             $file
666             );
667             }
668             else {
669 0 0       0 if ($^O !~ /bsd$/i) {
670             # specify dummy password to make unzip fail fast
671             # instead of waiting for user input password when
672             # the zip file is password-protected
673 0         0 $cmd = $self->_build_cmd(
674             'unzip -P xxx -o',
675             $extract_dir,
676             $parent,
677             $file
678             );
679             }
680             else {
681             # FreeBSD and its derivatives do NOT support -P
682 0 0       0 if ($file !~ /[;<>\\\*\|`&\$!#\(\)\[\]\{\}:'"]/) {
683 0         0 $cmd = $self->_build_cmd(
684             'unzip -o',
685             $extract_dir,
686             $parent,
687             $file
688             );
689             }
690             else {
691             # extract all files if the matched
692             # file has shell meta-char in the name
693 0         0 $cmd = $self->_build_cmd(
694             'unzip -o',
695             $extract_dir,
696             $parent,
697             ''
698             );
699             }
700             }
701             }
702             }
703             elsif ($parent =~ /\.7z$/) {
704             # specify dummy password to make 7za fail fast
705             # instead of waiting for user input password when
706             # the zip file is password-protected
707 0         0 $cmd = $self->_build_cmd(
708             '7za x -y -pxxx',
709             $extract_dir,
710             $parent,
711             $file
712             );
713             }
714             elsif ($parent =~ /\.rar$/) {
715 0         0 $cmd = $self->_build_cmd(
716             'unrar x -o+',
717             $extract_dir,
718             $parent,
719             $file
720             );
721             }
722             elsif ($parent =~ /\.tgz$|\.tar\.gz$|\.tar\.Z$/) {
723             # The "-o" avoid to restore the owner as it could be root
724 3         29 $cmd = $self->_build_cmd(
725             'tar -xzof',
726             $extract_dir,
727             $parent,
728             $file
729             );
730             }
731             elsif ($parent =~ /\.bz2$/) {
732             # The "-o" avoid to restore the owner as it could be root
733 1         8 $cmd = $self->_build_cmd(
734             'tar -xjof',
735             $extract_dir,
736             $parent,
737             $file
738             );
739             }
740             elsif ($parent =~ /\.tar$/) {
741             # The "-o" avoid to restore the owner as it could be root
742 1         9 $cmd = $self->_build_cmd(
743             'tar -xof',
744             $extract_dir,
745             $parent,
746             $file
747             );
748             }
749 5         24 my $cmd_shell = sprintf("%s 2>%s 1>&2", $cmd, devnull());
750 5 50       27 $cmd_shell = "$cmd 1>&2" if $self->show_extracting_output();
751 5         408003 my $ret = system($cmd_shell);
752 5         497 return $ret == 0;
753             }
754              
755             sub _build_cmd {
756 5     5   16 my ($self, $extract_cmd, $dir, $parent, $file) = @_;
757              
758 5         8 my $chdir_cmd = q[cd];
759 5 50       32 if ($^O eq 'MSWin32') {
760 0         0 $chdir_cmd = q[cd /d];
761             }
762 5         23 return sprintf(
763             "%s %s && %s %s %s",
764             $chdir_cmd,
765             $self->_escape($dir),
766             $extract_cmd,
767             $self->_escape($parent),
768             $self->_escape($file)
769             );
770             }
771              
772             sub _is_cmd_avail {
773 19     19   3737 my ($self, $cmd) = @_;
774              
775 19 100       92 if (!exists $_CMD_LOC_FOR{$cmd}) {
776 11         62 my @path = path();
777 11         248 foreach my $p (@path) {
778 75         326 my $fp = catfile($p, $cmd);
779 75 100       1830 if (-f $fp) {
780 2         8 $_CMD_LOC_FOR{$cmd} = $fp;
781 2         11 return 1;
782             }
783             else {
784 73 50       277 if($^O eq 'MSWin32') {
785             # try to append .exe to the name
786 0         0 my $fp_win = $fp . ".exe";
787 0 0       0 if (-f $fp_win) {
788 0         0 $_CMD_LOC_FOR{$cmd} = $fp_win;
789 0         0 return 1;
790             }
791             # try to append .bat to the name
792 0         0 $fp_win = $fp . ".bat";
793 0 0       0 if (-f $fp_win) {
794 0         0 $_CMD_LOC_FOR{$cmd} = $fp_win;
795 0         0 return 1;
796             }
797             }
798             }
799             }
800             # executable not found, won't try again
801 9         47 $_CMD_LOC_FOR{$cmd} = "";
802             }
803 17 100       91 return $_CMD_LOC_FOR{$cmd} ? 1 : 0;
804             }
805              
806             sub _strip_dir {
807 21     21   74 my ($self, $base_dir, $path) = @_;
808              
809 21         38 my $dir1 = $base_dir;
810 21         35 my $path1 = $path;
811              
812 21         54 my $path_sep = '/';
813 21 50       132 $path_sep = '\\' if $^O eq 'MSWin32';
814              
815 21 50       100 $dir1 .= $path_sep unless substr($dir1, -1, 1) eq $path_sep;
816 21 100       71 if (index($path1, $dir1) == 0) {
817 16         37 $path1 = substr($path1, length($dir1));
818             }
819 21         94 return $path1;
820             }
821              
822             sub _escape {
823 19     19   44 my ($self, $str) = @_;
824              
825 19         39 my $ret = $str;
826 19 50       61 if ($^O ne 'MSWin32') {
827 19         54 $ret =~ s/([ ;<>\\\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g;
828             }
829             else {
830 0 0       0 $ret = qq["$ret"] if $ret =~ /[ &#*\|\[\]\(\)\{\}\=;!+,`~']/;
831             }
832 19         109 return $ret;
833             }
834              
835             sub _is_archive_file {
836 11     11   31 my ($self, $file) = @_;
837              
838 11         268 return $file =~ /\.(zip|jar|war|ear|7z|rar|tgz|bz2|tar|tar\.gz|tar\.Z)$/
839             }
840              
841             sub _property {
842 8     8   35 my ($self, $attr, $value) = @_;
843              
844 8 100       29 if(defined $value) {
845 2         6 my $oldval = $self->{$attr};
846 2         6 $self->{$attr} = $value;
847 2 50       13 $self->{_properties_with_value} = {}
848             if(!exists $self->{_properties_with_value});
849 2         5 $self->{_properties_with_value}{$attr} = 1;
850 2         5 return $oldval;
851             }
852              
853 6         22 return $self->{$attr};
854             }
855              
856             sub _remove_property {
857 0     0   0 my ($self, $attr) = @_;
858              
859 0         0 $self->{$attr} = undef;
860             }
861              
862             sub _search_pattern {
863 21     21   208 my ($self, $value) = @_;
864              
865 21 100       259 if(defined $value) {
866 2         171 my $oldval = $self->{search_pattern};
867 2         5 $self->{search_pattern} = $value;
868 2         698 return $oldval;
869             }
870              
871 19         60 return $self->{search_pattern};
872             }
873              
874             sub _dir_name {
875 1     1   4 my ($self, $path) = @_;
876              
877 1         8 my $path_sep = '/';
878 1 50       12 $path_sep = '\\' if $^O eq 'MSWin32';
879 1         6 my $idx = rindex($path, $path_sep);
880 1 50       6 if ($idx > 0) {
881 1         12 return substr($path, 0, $idx);
882             }
883             else {
884 0         0 return '';
885             }
886             }
887              
888             sub _get_list_file {
889 4     4   10 my ($self) = @_;
890              
891 4         65 my (undef, $lst) = tempfile();
892 4         3091 my $files = $self->_property('archive_lst_files');
893 4 100       33 if (!defined($files)) {
894 2         4 $files = [];
895 2         58 $self->_property('archive_lst_files', $files);
896             }
897 4         17 push @$files, $lst;
898 4         9 return $lst;
899             }
900              
901             sub _cleanup {
902 2     2   11 my ($self) = @_;
903              
904 2         10 my $files = $self->_property('archive_lst_files');
905 2         19 foreach my $f (@$files) {
906 4         511 unlink($f);
907             }
908             }
909              
910             1;
911              
912             =pod
913              
914             =head1 HOW IT WORKS
915              
916             C provides plumbing boiler code to search files in nested
917             archive files. It does the heavy lifting to extract mininal files necessary
918             to fulfill the inquiry.
919              
920             =head1 SOURCE AVAILABILITY
921              
922             This code is hosted on Github
923              
924             https://github.com/schnell18/archive-probe
925              
926             =head1 BUG REPORTS
927              
928             Please report bugs or other issues to Efgz@rt.cpan.orgE.
929              
930             =head1 AUTHOR
931              
932             This module is developed by Justin Zhang Efgz@cpan.orgE.
933              
934             =head1 COPYRIGHT
935              
936             Copyright (C) 2013 by Justin Zhang
937              
938             This library is free software; you may redistribute and/or modify it
939             under the same terms as Perl itself.
940              
941             =cut
942              
943             # vim: set ai nu nobk expandtab sw=4 ts=4 tw=72 :