File Coverage

blib/lib/Paranoid/Filesystem.pm
Criterion Covered Total %
statement 326 408 79.9
branch 142 228 62.2
condition 31 60 51.6
subroutine 29 31 93.5
pod 16 16 100.0
total 544 743 73.2


line stmt bran cond sub pod time code
1             # Paranoid::Filesystem -- Filesystem support for paranoid programs
2             #
3             # $Id: lib/Paranoid/Filesystem.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::Filesystem;
33              
34 20     20   5197 use 5.008;
  20         71  
35              
36 20     20   91 use strict;
  20         32  
  20         325  
37 20     20   85 use warnings;
  20         32  
  20         729  
38 20     20   158 use vars qw($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS);
  20         31  
  20         1276  
39 20     20   114 use base qw(Exporter);
  20         28  
  20         1485  
40 20     20   123 use Cwd qw(realpath);
  20         40  
  20         1140  
41 20     20   2954 use Errno qw(:POSIX);
  20         8883  
  20         6396  
42 20     20   170 use Fcntl qw(:DEFAULT :seek :flock :mode);
  20         44  
  20         9328  
43 20     20   137 use Paranoid;
  20         31  
  20         862  
44 20     20   101 use Paranoid::Debug qw(:all);
  20         39  
  20         2901  
45 20     20   4212 use Paranoid::Process qw(ptranslateUser ptranslateGroup);
  20         40  
  20         1412  
46 20     20   4373 use Paranoid::Input;
  20         45  
  20         1074  
47 20     20   4887 use Paranoid::IO;
  20         48  
  20         2104  
48 20     20   9325 use Paranoid::Glob;
  20         55  
  20         1677  
49              
50             ($VERSION) = ( q$Revision: 2.10 $ =~ /(\d+(?:\.\d+)+)/sm );
51              
52             @EXPORT = qw(
53             preadDir psubdirs pfiles
54             pmkdir prm prmR ptouch
55             ptouchR pchmod pchmodR pchown
56             pchownR pwhich
57             );
58             @EXPORT_OK = (
59             @EXPORT, qw(
60             ptranslateLink
61             pcleanPath
62             ptranslatePerms
63             ) );
64             %EXPORT_TAGS = ( all => [@EXPORT_OK], );
65              
66 20     20   148 use constant PERMMASK => 0777;
  20         39  
  20         75555  
67              
68             #####################################################################
69             #
70             # Module code follows
71             #
72             #####################################################################
73              
74             sub pmkdir ($;$\%) {
75              
76             # Purpose: Simulates a 'mkdir -p' command in pure Perl
77             # Returns: True (1) if all targets were successfully created,
78             # False (0) if there are any errors
79             # Usage: $rv = pmkdir("/foo/{a1,b2}");
80             # Usage: $rv = pmkdir("/foo", 0750);
81             # Usage: $rv = pmkdir("/foo", 0750, %errors);
82              
83 7     7 1 49 my $path = shift;
84 7         12 my $mode = shift;
85 7   50     34 my $eref = shift || {};
86 7         18 my ( $dirs, $directory, $subdir, @parts, $i );
87 7         11 my $rv = 1;
88              
89 7         31 subPreamble( PDLEVEL1, '$;$\%', $path, $mode, $eref );
90              
91             # Create a glob object if we weren't handed one.
92 7 100       30 if ( defined $path ) {
93 6 100       51 $dirs =
94             ref $path eq 'Paranoid::Glob'
95             ? $path
96             : Paranoid::Glob->new( globs => [$path] );
97             }
98              
99             # Leave Paranoid::Glob's errors in place if there was a problem
100 7 100       19 $rv = 0 unless defined $dirs;
101              
102             # Set and detaint mode
103 7 100       14 if ($rv) {
104 6 100       49 $mode = ptranslatePerms( defined $mode ? $mode : umask ^ PERMMASK );
105 6 100       23 unless ( detaint( $mode, 'int' ) ) {
106 1         6 Paranoid::ERROR =
107             pdebug( 'invalid mode argument passed', PDLEVEL1 );
108 1         2 $rv = 0;
109             }
110             }
111              
112             # Start creating directories
113 7 100       18 if ($rv) {
114              
115             # Iterate over each directory in the glob
116 5         20 foreach $directory (@$dirs) {
117 11         41 pdebug( 'processing %s', PDLEVEL2, $directory );
118              
119             # Skip directories already present
120 11 50       191 next if -d $directory;
121              
122             # Otherwise, split so we can backtrack to the first available
123             # subdirectory and start creating subdirectories from there
124 11         109 @parts = split m#/+#s, $directory;
125 11 50       34 $i = $parts[0] eq '' ? 1 : 0;
126 11   100     312 $i++ while $i < $#parts and -d join '/', @parts[ 0 .. $i ];
127 11         38 while ( $i <= $#parts ) {
128 17         53 $subdir = join '/', @parts[ 0 .. $i ];
129 17 50       189 unless ( -d $subdir ) {
130 17 50       1008 if ( mkdir $subdir, $mode ) {
131              
132             # Make sure perms are applied
133 17         267 chmod $mode, $subdir;
134              
135             } else {
136              
137             # Error out and halt all work
138 0         0 Paranoid::ERROR = pdebug( 'failed to create %s: %s',
139             PDLEVEL1, $subdir, $! );
140 0         0 $rv = 0;
141 0         0 last;
142             }
143             }
144 17         68 $i++;
145             }
146             }
147             }
148              
149 7         29 subPostamble( PDLEVEL1, '$', $rv );
150              
151 7         73 return $rv;
152             }
153              
154             sub prm ($;\%) {
155              
156             # Purpose: Simulates a "rm -f" command in pure Perl
157             # Returns: True (1) if all targets were successfully removed,
158             # False (0) if there are any errors
159             # Usage: $rv = prm("/foo");
160             # Usage: $rv = prm("/foo", %errors);
161              
162 7     7 1 467 my $target = shift;
163 7         13 my $errRef = shift;
164 7         13 my $rv = 1;
165 7         14 my ( $glob, $tglob, @fstat );
166              
167 7         24 subPreamble( PDLEVEL1, '$;\%', $target, $errRef );
168              
169             # Prep error hash
170 7 50       18 $errRef = {} unless defined $errRef;
171 7         13 %$errRef = ();
172              
173             # Create a glob object if we weren't handed one.
174 7 50       20 if ( defined $target ) {
175 7 100       55 $glob =
176             ref $target eq 'Paranoid::Glob'
177             ? $target
178             : Paranoid::Glob->new( globs => [$target] );
179             }
180 7 50       21 $rv = 0 unless defined $glob;
181              
182             # Start removing files
183 7 50       16 if ($rv) {
184              
185             # Consolidate the entries
186 7         34 $glob->consolidate;
187              
188             # Iterate over entries
189 7         21 foreach ( reverse @$glob ) {
190 21         131 pdebug( 'processing %s', PDLEVEL2, $_ );
191              
192             # Stat the file
193 21         349 @fstat = lstat $_;
194              
195 21 100       69 unless (@fstat) {
196              
197             # If the file is missing, consider the removal successful and
198             # move on.
199 2 50       20 next if $! == ENOENT;
200              
201             # Report remaining errors (permission denied, etc.)
202 0         0 $rv = 0;
203 0         0 $$errRef{$_} = $!;
204 0         0 Paranoid::ERROR =
205             pdebug( 'failed to remove %s: %s', PDLEVEL1, $_, $! );
206 0         0 next;
207             }
208              
209 19 100       70 if ( S_ISDIR( $fstat[2] ) ) {
210              
211             # Remove directories
212 12 100       573 unless ( rmdir $_ ) {
213              
214             # Record errors
215 1         4 $rv = 0;
216 1         24 $$errRef{$_} = $!;
217 1         7 Paranoid::ERROR =
218             pdebug( 'failed to remove %s: %s', PDLEVEL1, $_, $! );
219             }
220              
221             } else {
222              
223             # Remove all non-directories
224 7 50       391 unless ( unlink $_ ) {
225              
226             # Record errors
227 0         0 $rv = 0;
228 0         0 $$errRef{$_} = $!;
229 0         0 Paranoid::ERROR =
230             pdebug( 'failed to remove %s: %s', PDLEVEL1, $_, $! );
231             }
232             }
233             }
234             }
235              
236 7         35 subPostamble( PDLEVEL1, '$', $rv );
237              
238 7         46 return $rv;
239             }
240              
241             sub prmR ($;$\%) {
242              
243             # Purpose: Recursively calls prm to simulate "rm -rf"
244             # Returns: True (1) if all targets were successfully removed,
245             # False (0) if there are any errors
246             # Usage: $rv = prmR("/foo");
247             # Usage: $rv = prmR("/foo", 1);
248             # Usage: $rv = prmR("/foo", 1, %errors);
249              
250 4     4 1 689 my $target = shift;
251 4         9 my $follow = shift;
252 4         8 my $errRef = shift;
253 4         7 my $rv = 1;
254 4         8 my ( $glob, $tglob );
255              
256 4         18 subPreamble( PDLEVEL1, '$;$\%', $target, $follow, $errRef );
257              
258             # Prep error hash
259 4 50       40 $errRef = {} unless defined $errRef;
260 4         12 %$errRef = ();
261              
262             # Create a glob object if we weren't handed one.
263 4 50       14 if ( defined $target ) {
264 4 50       35 $glob =
265             ref $target eq 'Paranoid::Glob'
266             ? $target
267             : Paranoid::Glob->new( globs => [$target] );
268             }
269 4 50       16 $rv = 0 unless defined $glob;
270              
271 4 50       9 if ($rv) {
272              
273             # Load the directory tree and execute prm
274 4   33     21 $rv = $glob->recurse( $follow, 1 ) && prm( $glob, %$errRef );
275             }
276              
277 4         17 subPostamble( PDLEVEL1, '$', $rv );
278              
279 4         31 return $rv;
280             }
281              
282             sub preadDir ($\@;$) {
283              
284             # Purpose: Populates the passed array ref with a list of all the
285             # directory entries (minus the '.' & '..') in the passed
286             # directory
287             # Returns: True (1) if the read was successful,
288             # False (0) if there are any errors
289             # Usage: $rv = preadDir("/tmp", @entries);
290             # Usage: $rv = preadDir("/tmp", @entries, 1);
291              
292 9     9 1 486 my ( $dir, $aref, $noLinks ) = @_;
293 9         17 my $rv = 1;
294 9         10 my $fh;
295              
296 9         30 subPreamble( PDLEVEL1, '$\@;$', $dir, $aref, $noLinks );
297 9         27 @$aref = ();
298              
299             # Validate directory and exit early, if need be
300 9 100 66     342 unless ( defined $dir and -e $dir and -d _ and -r _ ) {
      100        
      66        
301 4         14 $rv = 0;
302 4 50       27 Paranoid::ERROR = pdebug( (
    100          
    50          
303             !defined $dir ? 'undefined value passed as directory name'
304             : !-e _ ? 'directory (%s) does not exist'
305             : !-d _ ? '%s is not a directory'
306             : 'directory (%s) is not readable by the effective user'
307             ),
308             PDLEVEL1, $dir
309             );
310             }
311              
312 9 100       36 if ($rv) {
313              
314             # Read the directory's contents
315 5         248 $rv = opendir $fh, $dir;
316              
317 5 50       19 if ($rv) {
318              
319             # Get the list, filtering out '.' & '..'
320 5         180 foreach ( readdir $fh ) {
321 23 100       106 push @$aref, "$dir/$_" unless m/^\.\.?$/s;
322             }
323 5         109 closedir $fh;
324              
325             # Filter out symlinks, if necessary
326 5 50       18 @$aref = grep { !-l $_ } @$aref if $noLinks;
  0         0  
327              
328             } else {
329 0         0 Paranoid::ERROR = pdebug( 'error opening directory (%s): %s',
330             PDLEVEL1, $dir, $! );
331             }
332             }
333              
334 9         53 pdebug( 'returning %d entries', PDLEVEL2, scalar @$aref );
335              
336 9         31 subPostamble( PDLEVEL1, '$', $rv );
337              
338 9         40 return $rv;
339             }
340              
341             sub psubdirs ($\@;$) {
342              
343             # Purpose: Performs a preadDir but filters out all non-directory entries
344             # so that only subdirectory entries are returned. Can
345             # optionally filter out symlinks to directories as well.
346             # Returns: True (1) if the directory read was successful,
347             # False (0) if there are any errors
348             # Usage: $rv = psubdirs($dir, @entries);
349             # Usage: $rv = psubdirs($dir, @entries, 1);
350              
351 3     3 1 10 my ( $dir, $aref, $noLinks ) = @_;
352 3         7 my $rv = 0;
353              
354 3         12 subPreamble( PDLEVEL1, '$\@;$', $dir, $aref, $noLinks );
355              
356             # Validate arguments
357 3 50       10 $noLinks = 0 unless defined $noLinks;
358              
359             # Empty target array and retrieve list
360 3         11 $rv = preadDir( $dir, @$aref, $noLinks );
361              
362             # Filter out all non-directories
363 3 100       14 @$aref = grep { -d $_ } @$aref if $rv;
  5         76  
364              
365 3         18 pdebug( 'returning %d entries', PDLEVEL2, scalar @$aref );
366              
367 3         10 subPostamble( PDLEVEL1, '$', $rv );
368              
369 3         17 return $rv;
370             }
371              
372             sub pfiles ($\@;$) {
373              
374             # Purpose: Performs a preadDir but filters out all directory entries
375             # so that only file entries are returned. Can
376             # optionally filter out symlinks to files as well.
377             # Returns: True (1) if the directory read was successful,
378             # False (0) if there are any errors
379             # Usage: $rv = pfiles($dir, @entries);
380             # Usage: $rv = pfiles($dir, @entries, 1);
381              
382 3     3 1 12 my ( $dir, $aref, $noLinks ) = @_;
383 3         4 my $rv = 0;
384              
385 3         11 subPreamble( PDLEVEL1, '$\@;$', $dir, $aref, $noLinks );
386              
387             # Validate arguments
388 3 50       9 $noLinks = 0 unless defined $noLinks;
389              
390             # Empty target array and retrieve list
391 3         7 @$aref = ();
392 3         8 $rv = preadDir( $dir, @$aref, $noLinks );
393              
394             # Filter out all non-files
395 3 100       13 @$aref = grep { -f $_ } @$aref if $rv;
  4         55  
396              
397 3         15 pdebug( 'returning %d entries', PDLEVEL2, scalar @$aref );
398              
399 3         11 subPostamble( PDLEVEL1, '$', $rv );
400              
401 3         16 return $rv;
402             }
403              
404             sub pcleanPath ($) {
405              
406             # Purpose: Removes/resolves directory artifacts like '/../', etc.
407             # Returns: Filtered string
408             # Usage: $filename = pcleanPath($filename);
409              
410 9     9 1 4252 my $filename = shift;
411              
412 9         37 subPreamble( PDLEVEL1, '$', $filename );
413              
414 9 100       19 if ( defined $filename ) {
415              
416             # Strip all //+, /./, and /{parent}/../
417 8         39 while ( $filename =~ m#/\.?/+#s ) { $filename =~ s#/\.?/+#/#sg }
  1         9  
418 8         28 while ( $filename =~ m#/(?:(?!\.\.)[^/]{2,}|[^/])/\.\./#s ) {
419 6         30 $filename =~ s#/(?:(?!\.\.)[^/]{2,}|[^/])/\.\./#/#sg;
420             }
421              
422             # Strip trailing /. and leading /../
423 8         19 $filename =~ s#/\.$##s;
424 8         17 while ( $filename =~ m#^/\.\./#s ) { $filename =~ s#^/\.\./#/#s }
  3         11  
425              
426             # Strip any ^[^/]+/../
427 8         19 while ( $filename =~ m#^[^/]+/\.\./#s ) {
428 1         5 $filename =~ s#^[^/]+/\.\./##s;
429             }
430              
431             # Strip any trailing /^[^/]+/..$
432 8         20 while ( $filename =~ m#/[^/]+/\.\.$#s ) {
433 1         5 $filename =~ s#/[^/]+/\.\.$##s;
434             }
435             }
436              
437 9         54 subPostamble( PDLEVEL1, '$', $filename );
438              
439 9         26 return $filename;
440             }
441              
442             sub ptranslateLink ($;$) {
443              
444             # Purpose: Performs either a full (realpath) or a partial one (last
445             # filename element only) on the passed filename
446             # Returns: Altered filename if successful, undef if there are any
447             # failures
448             # Usage: $filename = ptranslateLink($filename);
449             # Usage: $filename = ptranslateLink($filename, 1);
450              
451 2     2 1 876 my $link = shift;
452 2   50     13 my $fullyTranslate = shift || 0;
453 2         5 my $nLinks = 0;
454 2         5 my ( $i, $target );
455              
456 2         8 subPreamble( PDLEVEL1, '$;$', $link, $fullyTranslate );
457              
458             # Validate link and exit early, if need be
459 2 50 50     55 unless ( defined $link and scalar lstat $link ) {
460 0         0 Paranoid::ERROR = pdebug( 'link (%s) does not exist on filesystem',
461             PDLEVEL1, $link );
462 0         0 pOut();
463 0         0 pdebug( 'leaving w/rv: undef', PDLEVEL1 );
464 0         0 return undef;
465             }
466              
467             # Check every element in the path for symlinks and translate it if
468             # if a full translation was requested
469 2 50       8 if ($fullyTranslate) {
470              
471             # Resolve the link
472 0         0 $target = realpath($link);
473              
474             # Make sure we got an answer
475 0 0       0 if ( defined $target ) {
476              
477             # Save the answer
478 0         0 $link = $target;
479              
480             } else {
481              
482             # Report our inability to resolve the link
483 0         0 Paranoid::ERROR =
484             pdebug( 'link (%s) couldn\'t be resolved fully: %s',
485             PDLEVEL1, $link, $! );
486 0         0 $link = undef;
487             }
488              
489             } else {
490              
491             # Is the file passed a symlink?
492 2 50       22 if ( -l $link ) {
493              
494             # Yes it is, let's get the target
495 2         26 $target = readlink $link;
496 2         12 pdebug( 'last element is a link to %s', PDLEVEL1, $target );
497              
498             # Is the target a relative filename?
499 2 50       9 if ( $target =~ m#^(?:\.\.?/|[^/])#s ) {
500              
501             # Yupper, replace the filename with the target
502 2         14 $link =~ s#[^/]+$#$target#s;
503              
504             } else {
505              
506             # The target is fully qualified, so replace link entirely
507 0         0 $link = $target;
508             }
509             }
510             }
511              
512 2 50       10 $link = pcleanPath($link) if defined $link;
513              
514 2         8 subPostamble( PDLEVEL1, '$', $link );
515              
516 2         6 return $link;
517             }
518              
519             sub ptouch ($;$\%) {
520              
521             # Purpose: Simulates a "touch" command in pure Perl
522             # Returns: True (1) if all targets were successfully touched,
523             # False (0) if there are any errors
524             # Usage: $rv = ptouch("/foo/*");
525             # Usage: $rv = ptouch("/foo/*", $tstamp);
526             # Usage: $rv = ptouch("/foo/*", $tstamp, %errors);
527              
528 7     7 1 585 my $target = shift;
529 7         15 my $stamp = shift;
530 7         15 my $errRef = shift;
531 7         15 my $rv = 1;
532 7         12 my $irv = 1;
533 7         15 my ( $glob, $tglob, $fh );
534              
535 7         38 subPreamble( PDLEVEL1, '$;$\%', $target, $stamp, $errRef );
536              
537             # Prep error hash
538 7 100       23 $errRef = {} unless defined $errRef;
539 7         14 %$errRef = ();
540              
541             # Create a glob object if we weren't handed one.
542 7 50       23 if ( defined $target ) {
543 7 100       64 $glob =
544             ref $target eq 'Paranoid::Glob'
545             ? $target
546             : Paranoid::Glob->new( globs => [$target] );
547             }
548 7 50       27 $rv = 0 unless defined $glob;
549              
550 7 50       22 if ($rv) {
551              
552             # Apply the default timestamp if omitted
553 7 100       20 $stamp = time unless defined $stamp;
554              
555 7 50       41 unless ( detaint( $stamp, 'int' ) ) {
556 0         0 Paranoid::ERROR = pdebug( 'Invalid characters in timestamp: %s',
557             PDLEVEL2, $stamp );
558 0         0 $rv = 0;
559             }
560             }
561              
562             # Start touching stuff
563 7 50       18 if ($rv) {
564              
565             # Consolidate the entries
566 7         30 $glob->consolidate;
567              
568             # Iterate over entries
569 7         23 foreach $target (@$glob) {
570 16         54 pdebug( 'processing %s', PDLEVEL2, $target );
571 16         19 $irv = 1;
572              
573             # Create the target if it does not exist
574 16 100       261 unless ( -e $target ) {
575 4         21 pdebug( 'creating empty file (%s)', PDLEVEL2, $target );
576 4   66     23 $fh = popen( $target, O_CREAT | O_EXCL | O_RDWR )
577             || popen( $target, O_RDWR );
578 4 100       17 if ( defined $fh ) {
579 2         10 pclose($target);
580             } else {
581 2         12 $$errRef{$target} = $!;
582 2         5 $irv = $rv = 0;
583             }
584             }
585              
586             # Touch the file
587 16 100       52 if ($irv) {
588 14 50       278 unless ( utime $stamp, $stamp, $target ) {
589 0         0 $$errRef{$target} = $!;
590 0         0 $rv = 0;
591             }
592             }
593             }
594             }
595              
596 7         35 subPostamble( PDLEVEL1, '$', $rv );
597              
598 7         70 return $rv;
599             }
600              
601             sub ptouchR ($;$$\%) {
602              
603             # Purpose: Calls ptouch recursively
604             # Returns: True (1) if all targets were successfully touched,
605             # False (0) if there are any errors
606             # Usage: $rv = ptouchR("/foo");
607             # Usage: $rv = ptouchR("/foo", $tstamp);
608             # Usage: $rv = ptouchR("/foo", $tstamp, $follow);
609             # Usage: $rv = ptouchR("/foo", $tstamp, $follow, %errors);
610              
611 3     3 1 1289 my $target = shift;
612 3         8 my $stamp = shift;
613 3         5 my $follow = shift;
614 3         8 my $errRef = shift;
615 3         8 my $rv = 1;
616 3         6 my ( $glob, $tglob );
617              
618 3         15 subPreamble( PDLEVEL1, '$;$$\%', $target, $stamp, $follow, $errRef );
619              
620             # Prep error hash
621 3 100       10 $errRef = {} unless defined $errRef;
622 3         7 %$errRef = ();
623              
624             # Create a glob object if we weren't handed one.
625 3 50       10 if ( defined $target ) {
626 3 100       28 $glob =
627             ref $target eq 'Paranoid::Glob'
628             ? $target
629             : Paranoid::Glob->new( globs => [$target] );
630             }
631 3 50       12 $rv = 0 unless defined $glob;
632              
633 3 50       9 if ($rv) {
634              
635             # Load the directory tree and execute prm
636 3   66     17 $rv = $glob->recurse( $follow, 1 )
637             && ptouch( $glob, $stamp, %$errRef );
638             }
639              
640 3         11 subPostamble( PDLEVEL1, '$', $rv );
641              
642 3         28 return $rv;
643             }
644              
645             sub ptranslatePerms ($) {
646              
647             # Purpose: Translates symbolic permissions (as supported by userland
648             # chmod, etc.) into the octal permissions.
649             # Returns: Numeric permissions if valid symbolic permissions were passed,
650             # undef otherwise
651             # Usage: $perm = ptranslatePerms('ug+srw');
652              
653 21     21 1 3225 my $perm = shift;
654 21         33 my $rv = undef;
655 21         27 my ( @tmp, $o, $p );
656              
657 21         60 subPreamble( PDLEVEL1, '$', $perm );
658              
659             # Validate permissions string
660 21 100 66     162 if ( defined $perm and $perm =~ /^\d+$/s ) {
    100 66        
661              
662 12 100       33 if ( $perm =~ /^0/s ) {
663 2 100       6 if ( $perm =~ /^0[0-8]{3,4}$/s ) {
664              
665             # String representation of octal number
666 1         56 eval "\$perm = $perm;";
667 1         7 detaint( $perm, 'int', $p );
668              
669             } else {
670 1         2 pdebug( 'invalid octal presentation: %s', PDLEVEL1, $perm );
671             }
672              
673             } else {
674              
675             # Probably a converted integer already, treat it as verbatim
676 10         50 detaint( $perm, 'int', $p );
677             }
678              
679             } elsif ( defined $perm and $perm =~ /^([ugo]+)([+\-])([rwxst]+)$/s ) {
680              
681             # Translate symbolic representation
682 6         10 $o = $p = 00;
683 6         26 @tmp = ( $1, $2, $3 );
684 6 100       18 $o = S_IRWXU if $tmp[0] =~ /u/s;
685 6 100       14 $o |= S_IRWXG if $tmp[0] =~ /g/s;
686 6 100       20 $o |= S_IRWXO if $tmp[0] =~ /o/s;
687 6 50       24 $p = ( S_IRUSR | S_IRGRP | S_IROTH ) if $tmp[2] =~ /r/s;
688 6 100       13 $p |= ( S_IWUSR | S_IWGRP | S_IWOTH ) if $tmp[2] =~ /w/s;
689 6 50       11 $p |= ( S_IXUSR | S_IXGRP | S_IXOTH ) if $tmp[2] =~ /x/s;
690 6         9 $p &= $o;
691 6 100       12 $p |= S_ISVTX if $tmp[2] =~ /t/s;
692 6 50 66     16 $p |= S_ISGID if $tmp[2] =~ /s/s && $tmp[0] =~ /g/s;
693 6 100 66     14 $p |= S_ISUID if $tmp[2] =~ /s/s && $tmp[0] =~ /u/s;
694              
695             } else {
696              
697             # Report invalid characters in permission string
698 3         10 Paranoid::ERROR =
699             pdebug( 'invalid permissions (%s)', PDLEVEL1, $perm );
700              
701             }
702 21         35 $rv = $p;
703              
704 21 100       121 subPostamble( PDLEVEL1, '$', defined $rv ? sprintf( '%04o', $rv ) : $rv );
705              
706 21         51 return $rv;
707             }
708              
709             sub pchmod ($$;\%) {
710              
711             # Purpose: Simulates a "chmod" command in pure Perl
712             # Returns: True (1) if all targets were successfully chmod'd,
713             # False (0) if there are any errors
714             # Usage: $rv = pchmod("/foo", $perms);
715             # Usage: $rv = pchmod("/foo", $perms, %errors);
716              
717 8     8 1 25 my $target = shift;
718 8         18 my $perms = shift;
719 8         11 my $errRef = shift;
720 8         8 my $rv = 1;
721 8         18 my ( $glob, $tglob, @fstat );
722 8         0 my ( $ptrans, $cperms, $addPerms, @tmp );
723              
724 8         28 subPreamble( PDLEVEL1, '$$;\%', $target, $perms, $errRef );
725              
726             # Prep error hash
727 8 100       13 $errRef = {} unless defined $errRef;
728 8         35 %$errRef = ();
729              
730             # Create a glob object if we weren't handed one.
731 8 50       19 if ( defined $target ) {
732 8 100       22 $glob =
733             ref $target eq 'Paranoid::Glob'
734             ? $target
735             : Paranoid::Glob->new( globs => [$target] );
736             }
737 8 50       15 $rv = 0 unless defined $glob;
738              
739             # Convert perms if they're symbolic
740 8 50 33     34 if ( defined $perms and defined( $ptrans = ptranslatePerms($perms) ) ) {
741 8 100       25 if ( $perms =~ /[ugo]+[+-]/si ) {
742 3 50       11 $addPerms = $perms =~ /-/s ? 0 : 1;
743             } else {
744 5         9 $ptrans = undef;
745             }
746             } else {
747 0         0 pdebug( 'invalid permissions passed: %s', PDLEVEL1, $perms );
748 0         0 $rv = 0;
749             }
750              
751 8 50       13 if ($rv) {
752              
753             # Consolidate the entries
754 8         25 $glob->consolidate;
755              
756             # Iterate over entries
757 8         14 foreach (@$glob) {
758 20         63 pdebug( 'processing %s', PDLEVEL2, $_ );
759              
760 20 100       31 if ( defined $ptrans ) {
761              
762             # Get the current file mode
763 8         107 @fstat = stat $_;
764 8 100       21 unless (@fstat) {
765 1         3 $rv = 0;
766 1         13 $$errRef{$_} = $!;
767 1         3 Paranoid::ERROR =
768             pdebug( 'failed to adjust permissions of %s: %s',
769             PDLEVEL1, $_, $! );
770 1         26 next;
771             }
772              
773             # If ptrans is defined we're going to do relative
774             # application of permissions
775             pdebug(
776 7 50       44 $addPerms
777             ? sprintf( 'adding perms %04o', $ptrans )
778             : sprintf( 'removing perms %04o', $ptrans ),
779             PDLEVEL2
780             );
781              
782             # Get the current permissions
783 7         13 $cperms = $fstat[2] & PERMMASK;
784 7         28 pdebug(
785             sprintf( 'current permissions of %s: %04o', $_, $cperms ),
786             PDLEVEL2
787             );
788 7 50       16 $cperms =
789             $addPerms
790             ? ( $cperms | $ptrans )
791             : ( $cperms & ( PERMMASK ^ $ptrans ) );
792 7         29 pdebug( sprintf( 'new permissions of %s: %04o', $_, $cperms ),
793             PDLEVEL2 );
794 7 50       120 unless ( chmod $cperms, $_ ) {
795 0         0 $rv = 0;
796 0         0 $$errRef{$_} = $!;
797 0         0 Paranoid::ERROR =
798             pdebug( 'failed to adjust permissions of %s: %s',
799             PDLEVEL1, $_, $! );
800             }
801              
802             } else {
803              
804             # Otherwise, the permissions are explicit
805             #
806             # Detaint number mode
807 12 50       36 if ( detaint( $perms, 'int' ) ) {
808              
809             # Detainted, now apply
810 12         54 pdebug(
811             sprintf(
812             'assigning permissions of %04o to %s',
813             $perms, $_
814             ),
815             PDLEVEL2
816             );
817 12 100       224 unless ( chmod $perms, $_ ) {
818 2         8 $rv = 0;
819 2         17 $$errRef{$_} = $!;
820             }
821             } else {
822              
823             # Detainting failed -- report
824 0         0 $$errRef{$_} = $!;
825 0         0 Paranoid::ERROR =
826             pdebug( 'failed to detaint permissions mode',
827             PDLEVEL1 );
828 0         0 $rv = 0;
829             }
830             }
831             }
832             }
833              
834 8         31 subPostamble( PDLEVEL1, '$', $rv );
835              
836 8         37 return $rv;
837             }
838              
839             sub pchmodR ($$;$\%) {
840              
841             # Purpose: Recursively calls pchmod
842             # Returns: True (1) if all targets were successfully chmod'd,
843             # False (0) if there are any errors
844             # Usage: $rv = pchmodR("/foo", $perms);
845             # Usage: $rv = pchmodR("/foo", $perms, $follow);
846             # Usage: $rv = pchmodR("/foo", $perms, $follow, %errors);
847              
848 4     4 1 13 my $target = shift;
849 4         7 my $perms = shift;
850 4         4 my $follow = shift;
851 4         6 my $errRef = shift;
852 4         5 my $rv = 1;
853 4         7 my ( $glob, $tglob );
854              
855 4         15 subPreamble( PDLEVEL1, '$$;$\%', $target, $perms, $follow, $errRef );
856              
857             # Prep error hash
858 4 100       10 $errRef = {} unless defined $errRef;
859 4         7 %$errRef = ();
860              
861             # Create a glob object if we weren't handed one.
862 4 50       9 if ( defined $target ) {
863 4 50       20 $glob =
864             ref $target eq 'Paranoid::Glob'
865             ? $target
866             : Paranoid::Glob->new( globs => [$target] );
867             }
868 4 50       20 $rv = 0 unless defined $glob;
869              
870 4 50       6 if ($rv) {
871              
872             # Load the directory tree and execute pchmod
873 4   66     18 $rv = $glob->recurse( $follow, 1 )
874             && pchmod( $glob, $perms, %$errRef );
875             }
876              
877 4         12 subPostamble( PDLEVEL1, '$', $rv );
878              
879 4         19 return $rv;
880             }
881              
882             sub pchown ($$;$\%) {
883              
884             # Purpose: Simulates a "chown" command in pure Perl
885             # Returns: True (1) if all targets were successfully owned,
886             # False (0) if there are any errors
887             # Usage: $rv = pchown("/foo", $user);
888             # Usage: $rv = pchown("/foo", $user, $group);
889             # Usage: $rv = pchown("/foo", $user, $group, %errors);
890              
891 0     0 1 0 my $target = shift;
892 0         0 my $user = shift;
893 0         0 my $group = shift;
894 0         0 my $errRef = shift;
895 0         0 my $rv = 1;
896 0         0 my ( $glob, $tglob, @fstat );
897              
898 0         0 subPreamble( PDLEVEL1, '$$;$\%', $target, $user, $group, $errRef );
899              
900             # Translate to UID/GID
901 0 0       0 $user = -1 unless defined $user;
902 0 0       0 $group = -1 unless defined $group;
903 0 0       0 $user = ptranslateUser($user) unless $user =~ /^-?\d+$/s;
904 0 0       0 $group = ptranslateGroup($group) unless $group =~ /^-?\d+$/s;
905 0 0 0     0 unless ( defined $user and defined $group ) {
906 0         0 $rv = 0;
907 0         0 Paranoid::ERROR =
908             pdebug( 'unsuccessful at translating uid/gid', PDLEVEL1 );
909             }
910              
911             # Prep error hash
912 0 0       0 $errRef = {} unless defined $errRef;
913 0         0 %$errRef = ();
914              
915             # Create a glob object if we weren't handed one.
916 0 0       0 if ( defined $target ) {
917 0 0       0 $glob =
918             ref $target eq 'Paranoid::Glob'
919             ? $target
920             : Paranoid::Glob->new( globs => [$target] );
921             }
922 0 0       0 $rv = 0 unless defined $glob;
923              
924 0 0 0     0 if ( $rv and ( $user != -1 or $group != -1 ) ) {
      0        
925              
926             # Proceed
927 0         0 pdebug( 'UID: %s GID: %s', PDLEVEL2, $user, $group );
928              
929             # Consolidate the entries
930 0         0 $glob->consolidate;
931              
932             # Process the list
933 0         0 foreach (@$glob) {
934              
935 0         0 pdebug( 'processing %s', PDLEVEL2, $_ );
936              
937 0 0       0 unless ( chown $user, $group, $_ ) {
938 0         0 $rv = 0;
939 0         0 $$errRef{$_} = $!;
940 0         0 Paranoid::ERROR =
941             pdebug( 'failed to adjust ownership of %s: %s',
942             PDLEVEL1, $_, $! );
943             }
944             }
945             }
946              
947 0         0 subPostamble( PDLEVEL1, '$', $rv );
948              
949 0         0 return $rv;
950             }
951              
952             sub pchownR ($$;$$\%) {
953              
954             # Purpose: Calls pchown recursively
955             # Returns: True (1) if all targets were successfully owned,
956             # False (0) if there are any errors
957             # Usage: $rv = pchownR("/foo", $user);
958             # Usage: $rv = pchownR("/foo", $user, $group);
959             # Usage: $rv = pchownR("/foo", $user, $group, $follow);
960             # Usage: $rv = pchownR("/foo", $user, $group, $follow, %errors);
961              
962 0     0 1 0 my $target = shift;
963 0         0 my $user = shift;
964 0         0 my $group = shift;
965 0         0 my $follow = shift;
966 0         0 my $errRef = shift;
967 0         0 my $rv = 1;
968 0         0 my ( $glob, $tglob );
969              
970 0         0 subPreamble( PDLEVEL1, '$$;$$\%', $target, $user, $group, $follow,
971             $errRef );
972              
973             # Prep error hash
974 0 0       0 $errRef = {} unless defined $errRef;
975 0         0 %$errRef = ();
976              
977             # Create a glob object if we weren't handed one.
978 0 0       0 if ( defined $target ) {
979 0 0       0 $glob =
980             ref $target eq 'Paranoid::Glob'
981             ? $target
982             : Paranoid::Glob->new( globs => [$target] );
983             }
984 0 0       0 $rv = 0 unless defined $glob;
985              
986 0 0       0 if ($rv) {
987              
988             # Load the directory tree and execute pchown
989 0   0     0 $rv = $glob->recurse( $follow, 1 )
990             && pchown( $glob, $user, $group, %$errRef );
991             }
992              
993 0         0 subPostamble( PDLEVEL1, '$', $rv );
994              
995 0         0 return $rv;
996             }
997              
998             sub pwhich ($) {
999              
1000             # Purpose: Simulates a "which" command in pure Perl
1001             # Returns: The full path to the requested program if successful
1002             # undef if not found
1003             # Usage: $filename = pwhich('ls');
1004              
1005 2     2 1 1535 my $binary = shift;
1006 2         25 my @directories = grep /^.+$/s, split /:/s, $ENV{PATH};
1007 2         5 my $match = undef;
1008              
1009 2         8 subPreamble( PDLEVEL1, '$', $binary );
1010              
1011             # Try to detaint filename
1012 2 50       11 if ( detaint( $binary, 'filename', $b ) ) {
1013              
1014             # Success -- start searching directories in PATH
1015 2         5 foreach (@directories) {
1016 5         16 pdebug( 'searching %s', PDLEVEL2, $_ );
1017 5 100 66     128 if ( -r "$_/$b" && -x _ ) {
1018 1         6 $match = "$_/$b";
1019 1         7 $match =~ s#/+#/#sg;
1020 1         3 last;
1021             }
1022             }
1023              
1024             } else {
1025              
1026             # Report detaint failure
1027 0         0 Paranoid::ERROR = pdebug( 'failed to detaint %s', PDLEVEL1, $binary );
1028             }
1029              
1030 2         9 subPostamble( PDLEVEL1, '$', $match );
1031              
1032 2         7 return $match;
1033             }
1034              
1035             1;
1036              
1037             __END__