File Coverage

blib/lib/Paranoid/Glob.pm
Criterion Covered Total %
statement 152 206 73.7
branch 37 50 74.0
condition 6 6 100.0
subroutine 21 30 70.0
pod 17 17 100.0
total 233 309 75.4


line stmt bran cond sub pod time code
1             # Paranoid::Glob -- Paranoid Glob objects
2             #
3             # $Id: lib/Paranoid/Glob.pm, 2.08 2020/12/31 12:10:06 acorliss Exp $
4             #
5             # This software is free software. Similar to Perl, you can redistribute it
6             # and/or modify it under the terms of either:
7             #
8             # a) the GNU General Public License
9             # as published by the
10             # Free Software Foundation ; either version 1
11             # , or any later version
12             # , or
13             # b) the Artistic License 2.0
14             # ,
15             #
16             # subject to the following additional term: No trademark rights to
17             # "Paranoid" have been or are conveyed under any of the above licenses.
18             # However, "Paranoid" may be used fairly to describe this unmodified
19             # software, in good faith, but not as a trademark.
20             #
21             # (c) 2005 - 2020, Arthur Corliss (corliss@digitalmages.com)
22             # (tm) 2008 - 2020, Paranoid Inc. (www.paranoid.com)
23             #
24             #####################################################################
25              
26             #####################################################################
27             #
28             # Environment definitions
29             #
30             #####################################################################
31              
32             package Paranoid::Glob;
33              
34 15     15   789 use 5.008;
  15         51  
35              
36 15     15   82 use strict;
  15         31  
  15         297  
37 15     15   67 use warnings;
  15         29  
  15         475  
38 15     15   81 use vars qw($VERSION);
  15         26  
  15         614  
39              
40 15     15   131 use Carp;
  15         28  
  15         866  
41 15     15   624 use Errno qw(:POSIX);
  15         1377  
  15         4468  
42 15     15   112 use Fcntl qw(:mode);
  15         23  
  15         3279  
43 15     15   116 use File::Glob qw(bsd_glob);
  15         29  
  15         1314  
44 15     15   97 use Paranoid;
  15         24  
  15         727  
45 15     15   576 use Paranoid::Debug qw(:all);
  15         30  
  15         33695  
46              
47             ($VERSION) = ( q$Revision: 2.08 $ =~ /(\d+(?:\.\d+)+)/s );
48              
49             #####################################################################
50             #
51             # Module code follows
52             #
53             #####################################################################
54              
55             sub _sanitize (\@) {
56              
57             # Purpose: Detaints passed strings
58             # Returns: True if successful, false on any detaint errors
59             # Usage: $rv = _sanitize(@globs);
60              
61 35     35   73 my $aref = shift;
62 35         53 my $rv = 1;
63              
64             # Make sure all glob entries are sane
65 35         76 foreach (@$aref) {
66 42 50       177 if (/^([[:print:]]+)$/s) {
67 42         137 $_ = $1;
68 42         106 $_ =~ s#/{2,}#/#sg;
69             } else {
70 0         0 $Paranoid::ERROR =
71             pdebug( 'invalid glob entry: %s', PDLEVEL1, $_ );
72 0         0 $rv = 0;
73 0         0 last;
74             }
75             }
76              
77 35         59 return $rv;
78             }
79              
80             sub new {
81              
82             # Purpose: Instantiates a new object of this class
83             # Returns: Object reference if successful, undef otherwise
84             # Usage: $obj = Paranoid::Glob->new(
85             # globs => [ qw(/lib/* /sbin/*) ],
86             # literals => [ qw(/lib/{sadfe-asda}) ],
87             # );
88              
89 36     36 1 7143 my ( $class, %args ) = splice @_;
90 36         85 my $self = [];
91 36         68 my $rv = 1;
92              
93             # Validate arguments
94 36 100       110 if ( exists $args{globs} ) {
95             croak 'Optional key/value pair "globs" not properly defined'
96 32 50       104 unless ref $args{globs} eq 'ARRAY';
97             }
98 36 100       90 if ( exists $args{literals} ) {
99             croak 'Optional key/value pair "literals" not properly defined'
100 3 50       13 unless ref $args{literals} eq 'ARRAY';
101             }
102              
103 36         174 pdebug( 'entering w/keys %s', PDLEVEL1, keys %args );
104 36         110 pIn();
105              
106 36         74 bless $self, $class;
107              
108             # Add any globs or literals if they were passed during inititation
109 3         10 $rv = $self->addLiterals( @{ $args{literals} } )
110 36 100       90 if exists $args{literals};
111 36 50       88 if ($rv) {
112 36 100       84 $rv = $self->addGlobs( @{ $args{globs} } ) if exists $args{globs};
  32         112  
113             }
114              
115 36 50       83 if ($rv) {
116 36         50 $rv = $self;
117             } else {
118 0         0 $rv = 'undef';
119 0         0 $self = undef;
120             }
121              
122 36         101 pOut();
123 36         80 pdebug( 'leaving w/rv: %s', PDLEVEL1, $self );
124              
125 36         159 return $self;
126             }
127              
128             sub addGlobs {
129              
130             # Purpose: Adds more globs to the object that need to be filtered through
131             # the bsd_glob
132             # Returns: True if all globs passed muster, false if not
133             # Usage: $rv = $obj->addGlobs(qw(/etc/* /root/*));
134              
135 32     32 1 104 my ( $self, @globs ) = splice @_;
136 32         59 my $rv = 1;
137 32         66 my @tmp;
138              
139             # Silently remove undefs and zero strings
140 32 50       83 @globs = grep { defined $_ and length $_ } @globs;
  39         220  
141              
142 32         274 pdebug( 'entering w/%d globs', PDLEVEL1, scalar @globs );
143 32         192 pIn();
144              
145             # Make sure all glob entries are sane
146 32         94 $rv = _sanitize(@globs);
147              
148 32 50       76 if ($rv) {
149              
150             # Filter them through bsd_glob unless the file exists as named in the
151             # literal string
152 32         64 foreach (@globs) {
153 39 100       3156 push @tmp, -e $_ ? $_ : bsd_glob($_);
154             }
155              
156             # Final detaint
157 32 50       106 foreach (@tmp) { /^([[:print:]]+)$/s and $_ = $1 }
  148         613  
158              
159 32         133 pdebug( 'added %d entries', PDLEVEL2, scalar @tmp );
160              
161             # Add to ourself
162 32         145 push @$self, splice @tmp;
163             }
164              
165 32         116 pOut();
166 32         78 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
167              
168 32         69 return $rv;
169             }
170              
171             sub addLiterals {
172              
173             # Purpose: Adds more globs to the object as literal strings
174             # Returns: True if all globs passed muster, false if not
175             # Usage: $rv = $obj->addLiterals(qw(/etc/* /root/*));
176              
177 3     3 1 13 my ( $self, @globs ) = splice @_;
178 3         5 my $rv = 1;
179              
180             # Silently remove undefs and zero strings
181 3 50       7 @globs = grep { defined $_ and length $_ } @globs;
  3         23  
182              
183 3         10 pdebug( 'entering w/%d literals', PDLEVEL1, scalar @globs );
184 3         8 pIn();
185              
186             # Make sure all glob entries are sane
187 3         10 $rv = _sanitize(@globs);
188              
189 3 50       14 push @$self, splice @globs if $rv;
190              
191 3         9 pOut();
192 3         8 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
193              
194 3         6 return $rv;
195             }
196              
197             sub consolidate {
198              
199             # Purpose: Removes redundant entries and sorts alphabetically
200             # Returns: True
201             # Usage: $obj->consolidate;
202              
203 50     50 1 91 my ($self) = @_;
204 50         73 my (%tmp);
205              
206 50         153 pdebug( 'entering w/%d entries', PDLEVEL1, scalar @$self );
207              
208 50         117 %tmp = map { $_ => 1 } @$self;
  154         427  
209 50         250 @$self = sort keys %tmp;
210              
211 50         161 pdebug( 'leaving w/%d entries', PDLEVEL1, scalar @$self );
212              
213 50         119 return 1;
214             }
215              
216             sub exists {
217              
218             # Purpose: Returns a list of the entries that exist on the file system
219             # Returns: List of existing filesystem entries
220             # Usage: @entries = $obj->existing;
221              
222 8     8 1 2308 my ($self) = @_;
223 8         17 my @entries = grep { scalar lstat $_ } @$self;
  195         2194  
224              
225 8         35 pdebug( 'leaving w/rv: %s', PDLEVEL1, @entries );
226              
227 8         60 return @entries;
228             }
229              
230             sub readable {
231              
232             # Purpose: Returns a list of the entries that are readable by the
233             # effective user
234             # Returns: List of readable entries
235             # Usage: @entries = $obj->readable;
236              
237 0     0 1 0 my ($self) = @_;
238 0         0 my @entries = grep { -r $_ } $self->exists;
  0         0  
239              
240 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @entries );
241              
242 0         0 return @entries;
243             }
244              
245             sub writable {
246              
247             # Purpose: Returns a list of the entries that are writable by the
248             # effective user
249             # Returns: List of writable entries
250             # Usage: @entries = $obj->writable;
251              
252 0     0 1 0 my ($self) = @_;
253 0         0 my @entries = grep { -w $_ } $self->exists;
  0         0  
254              
255 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @entries );
256              
257 0         0 return @entries;
258             }
259              
260             sub executable {
261              
262             # Purpose: Returns a list of the entries that are executable/traversable
263             # by the effective user
264             # Returns: List of executable/traversable entries
265             # Usage: @entries = $obj->executable;
266              
267 0     0 1 0 my ($self) = @_;
268 0         0 my @entries = grep { -x $_ } $self->exists;
  0         0  
269              
270 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @entries );
271              
272 0         0 return @entries;
273             }
274              
275             sub owned {
276              
277             # Purpose: Returns a list of the entries that are owned by the
278             # effective user
279             # Returns: List of owned entries
280             # Usage: @entries = $obj->owned;
281              
282 0     0 1 0 my ($self) = @_;
283 0         0 my @entries = grep { -o $_ } $self->exists;
  0         0  
284              
285 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @entries );
286              
287 0         0 return @entries;
288             }
289              
290             sub directories {
291              
292             # Purpose: Returns a list of existing directories
293             # Returns: List of directories
294             # Usage: @dirs = $obj->directories;
295              
296 3     3 1 12 my ($self) = @_;
297 3         8 my @dirs = grep { -d $_ } $self->exists;
  96         1125  
298              
299 3         18 pdebug( 'leaving w/rv: %s', PDLEVEL1, @dirs );
300              
301 3         24 return @dirs;
302             }
303              
304             sub files {
305              
306             # Purpose: Returns a list of existing files
307             # Returns: List of files
308             # Usage: @files = $obj->files;
309              
310 0     0 1 0 my ($self) = @_;
311 0         0 my @files = grep { -f $_ } $self->exists;
  0         0  
312              
313 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @files );
314              
315 0         0 return @files;
316             }
317              
318             sub symlinks {
319              
320             # Purpose: Returns a list of existing symlinks
321             # Returns: List of symlinks
322             # Usage: @files = $obj->symlinks;
323              
324 2     2 1 6 my ($self) = @_;
325 2         6 my @symlinks = grep { -l $_ } $self->exists;
  64         716  
326              
327 2         12 pdebug( 'leaving w/rv: %s', PDLEVEL1, @symlinks );
328              
329 2         12 return @symlinks;
330             }
331              
332             sub pipes {
333              
334             # Purpose: Returns a list of existing pipes
335             # Returns: List of pipes
336             # Usage: @files = $obj->pipes;
337              
338 0     0 1 0 my ($self) = @_;
339 0         0 my @pipes = grep { -p $_ } $self->exists;
  0         0  
340              
341 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @pipes );
342              
343 0         0 return @pipes;
344             }
345              
346             sub sockets {
347              
348             # Purpose: Returns a list of existing sockets
349             # Returns: List of sockets
350             # Usage: @files = $obj->sockets;
351              
352 0     0 1 0 my ($self) = @_;
353 0         0 my @sockets = grep { -S $_ } $self->exists;
  0         0  
354              
355 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @sockets );
356              
357 0         0 return @sockets;
358             }
359              
360             sub blockDevs {
361              
362             # Purpose: Returns a list of existing block nodes
363             # Returns: List of block devs
364             # Usage: @files = $obj->blockDevs;
365              
366 0     0 1 0 my ($self) = @_;
367 0         0 my @bdevs = grep { -b $_ } $self->exists;
  0         0  
368              
369 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @bdevs );
370              
371 0         0 return @bdevs;
372             }
373              
374             sub charDevs {
375              
376             # Purpose: Returns a list of existing character nodes
377             # Returns: List of character devs
378             # Usage: @files = $obj->charDevs;
379              
380 0     0 1 0 my ($self) = @_;
381 0         0 my @cdevs = grep { -c $_ } $self->exists;
  0         0  
382              
383 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, @cdevs );
384              
385 0         0 return @cdevs;
386             }
387              
388             sub recurse {
389              
390             # Purpose: Recursively adds all subdirectories and their contents to the
391             # glob. Passing an optional boolean argument will tell it
392             # whether or not to follow symlinks. Defaults to not following
393             # symlinks (false). Another optional boolean argument instructs
394             # this method whether or not to include hidden directories. In
395             # accordance with the traditional behavior of shell globbing it
396             # defaults to false.
397             # Returns: True if successful, false on any errors (like permission
398             # denied, etc.)
399             # Usage: $rv = $obj->recurse;
400             # Usage: $rv = $obj->recurse(1);
401             # Usage: $rv = $obj->recurse(1, 1);
402              
403 14     14 1 595 my ( $self, $follow, $hidden ) = @_;
404 14         20 my $rv = 1;
405 14         25 my ( %seen, @crawl, $lindex, $slindex );
406              
407 14         39 pdebug( 'entering', PDLEVEL1 );
408 14         41 pIn();
409              
410             # Define our dirFilter sub, who's sole purpose is to extract a list of
411             # directories from the passed list of entries
412             my $dirFilter = sub {
413 54     54   118 my @entries = @_;
414 54         81 my ( $entry, @fstat, @dirs );
415              
416             # Extract a list of directories from our current contents
417 54         96 foreach $entry (@entries) {
418 66         1243 @fstat = lstat $entry;
419 66 100       243 if (@fstat) {
420              
421             # Entry exists
422 62 100 100     298 if ( S_ISDIR( $fstat[2] ) ) {
    100          
423              
424             # Filter out sockets, etc.
425 44 100       204 next if $fstat[2] &
426              
427             # Add the directory
428             push @dirs, $entry;
429              
430             } elsif ( $follow and S_ISLNK( $fstat[2] ) ) {
431              
432             # Add symlinks pointing to directories if we're set
433             # to follow
434 4 50       86 push @dirs, $entry if -d $entry;
435             }
436              
437             } else {
438              
439             # Report any errors for anything other than ENOENT
440 4 50       39 unless ( $! == ENOENT ) {
441 0         0 Paranoid::ERROR = pdebug( 'couldn\'t access %s: %s',
442             PDLEVEL1, $entry, $! );
443 0         0 $rv = 0;
444             }
445             }
446             }
447              
448 54         188 return @dirs;
449 14         89 };
450              
451             # Define our addDir sub, whose purpose is to return the contents of the
452             # passed directory
453             my $addDir = sub {
454 40     40   80 my $dir = shift;
455 40         64 my ( $fh, @contents );
456              
457 40 50       1214 if ( opendir $fh, $dir ) {
458              
459             # Get the list, filtering out '.' & '..'
460 40         773 foreach ( readdir $fh ) {
461 117 100       459 next if m/^\.\.?$/s;
462 37 100 100     108 next if m/^\./s and not $hidden;
463 36         182 push @contents, "$dir/$_";
464             }
465 40         464 closedir $fh;
466              
467             } else {
468 0         0 Paranoid::ERROR =
469             pdebug( 'error opening directory %s: %s', PDLEVEL1, $dir,
470             $! );
471 0         0 $rv = 0;
472             }
473              
474 40         272 return @contents;
475 14         65 };
476              
477             # Consolidate to reduce potential redundancies
478 14         47 $self->consolidate;
479              
480             # Get our initial list of directories to crawl
481 14         37 @crawl = &$dirFilter(@$self);
482              
483             # Start crawling
484 14         31 $lindex = 0;
485 14         26 $slindex = $#$self;
486 14         48 while ( $lindex <= $#crawl ) {
487              
488             # Skip the directory if we've already crawled it
489 48 100       133 if ( exists $seen{ $crawl[$lindex] } ) {
490 8         13 $lindex++;
491 8         15 next;
492             }
493              
494             # Add the directory's contents
495 40         92 push @$self, ( &$addDir( $crawl[$lindex] ) );
496 40         132 $seen{ $crawl[$lindex] } = 0;
497 40         55 $lindex++;
498 40         68 $slindex++;
499              
500             # Add any new directories to the crawl list
501 40         120 push @crawl, ( &$dirFilter( @$self[ $slindex .. $#$self ] ) );
502 40         128 $slindex = $#$self;
503             }
504              
505             # Final consolidation
506 14         47 $self->consolidate;
507              
508 14         37 pOut();
509 14         37 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
510              
511 14         222 return $rv;
512             }
513              
514             1;
515              
516             __END__