File Coverage

blib/lib/Paranoid/Glob.pm
Criterion Covered Total %
statement 149 212 70.2
branch 37 50 74.0
condition 6 6 100.0
subroutine 21 30 70.0
pod 17 17 100.0
total 230 315 73.0


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