File Coverage

inc/File/Path.pm
Criterion Covered Total %
statement 0 230 0.0
branch 0 188 0.0
condition 0 96 0.0
subroutine 0 11 0.0
pod 4 4 100.0
total 4 529 0.7


line stmt bran cond sub pod time code
1             #line 1
2             package File::Path;
3              
4             use 5.005_04;
5             use strict;
6              
7             use Cwd 'getcwd';
8             use File::Basename ();
9             use File::Spec ();
10              
11             BEGIN {
12             if ($] < 5.006) {
13             # can't say 'opendir my $dh, $dirname'
14             # need to initialise $dh
15             eval "use Symbol";
16             }
17             }
18              
19             use Exporter ();
20             use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
21             $VERSION = '2.08_01';
22             @ISA = qw(Exporter);
23             @EXPORT = qw(mkpath rmtree);
24             @EXPORT_OK = qw(make_path remove_tree);
25              
26             my $Is_VMS = $^O eq 'VMS';
27             my $Is_MacOS = $^O eq 'MacOS';
28              
29             # These OSes complain if you want to remove a file that you have no
30             # write permission to:
31             my $Force_Writeable = grep {$^O eq $_} qw(amigaos dos epoc MSWin32 MacOS os2);
32              
33             # Unix-like systems need to stat each directory in order to detect
34             # race condition. MS-Windows is immune to this particular attack.
35             my $Need_Stat_Check = !($^O eq 'MSWin32');
36              
37 0     0     sub _carp {
38 0           require Carp;
39             goto &Carp::carp;
40             }
41              
42 0     0     sub _croak {
43 0           require Carp;
44             goto &Carp::croak;
45             }
46              
47 0     0     sub _error {
48 0           my $arg = shift;
49 0           my $message = shift;
50             my $object = shift;
51 0 0          
52 0 0         if ($arg->{error}) {
53 0 0         $object = '' unless defined $object;
54 0           $message .= ": $!" if $!;
  0            
  0            
55             push @{${$arg->{error}}}, {$object => $message};
56             }
57 0 0         else {
58             _carp(defined($object) ? "$message for $object: $!" : "$message: $!");
59             }
60             }
61              
62 0 0 0 0 1   sub make_path {
63 0           push @_, {} unless @_ and UNIVERSAL::isa($_[-1],'HASH');
64             goto &mkpath;
65             }
66              
67 0   0 0 1   sub mkpath {
68             my $old_style = !(@_ and UNIVERSAL::isa($_[-1],'HASH'));
69 0            
70             my $arg;
71             my $paths;
72 0 0          
73 0           if ($old_style) {
74 0           my ($verbose, $mode);
75 0 0         ($paths, $verbose, $mode) = @_;
76 0           $paths = [$paths] unless UNIVERSAL::isa($paths,'ARRAY');
77 0 0         $arg->{verbose} = $verbose;
78             $arg->{mode} = defined $mode ? $mode : 0777;
79             }
80 0           else {
81 0 0         $arg = pop @_;
82 0 0         $arg->{mode} = delete $arg->{mask} if exists $arg->{mask};
83 0 0         $arg->{mode} = 0777 unless exists $arg->{mode};
  0            
84 0 0         ${$arg->{error}} = [] if exists $arg->{error};
85 0 0         $arg->{owner} = delete $arg->{user} if exists $arg->{user};
86 0 0 0       $arg->{owner} = delete $arg->{uid} if exists $arg->{uid};
87 0           if (exists $arg->{owner} and $arg->{owner} =~ /\D/) {
88 0 0         my $uid = (getpwnam $arg->{owner})[2];
89 0           if (defined $uid) {
90             $arg->{owner} = $uid;
91             }
92 0           else {
93 0           _error($arg, "unable to map $arg->{owner} to a uid, ownership not changed");
94             delete $arg->{owner};
95             }
96 0 0 0       }
97 0           if (exists $arg->{group} and $arg->{group} =~ /\D/) {
98 0 0         my $gid = (getgrnam $arg->{group})[2];
99 0           if (defined $gid) {
100             $arg->{group} = $gid;
101             }
102 0           else {
103 0           _error($arg, "unable to map $arg->{group} to a gid, group ownership not changed");
104             delete $arg->{group};
105             }
106 0 0 0       }
107 0           if (exists $arg->{owner} and not exists $arg->{group}) {
108             $arg->{group} = -1; # chown will leave group unchanged
109 0 0 0       }
110 0           if (exists $arg->{group} and not exists $arg->{owner}) {
111             $arg->{owner} = -1; # chown will leave owner unchanged
112 0           }
113             $paths = [@_];
114 0           }
115             return _mkpath($arg, $paths);
116             }
117              
118 0     0     sub _mkpath {
119 0           my $arg = shift;
120             my $paths = shift;
121 0            
122 0           my(@created,$path);
123 0 0 0       foreach $path (@$paths) {
124 0 0 0       next unless defined($path) and length($path);
125             $path .= '/' if $^O eq 'os2' and $path =~ /^\w:\z/s; # feature of CRT
126 0 0         # Logic wants Unix paths, so go with the flow.
127 0 0         if ($Is_VMS) {
128 0           next if $path eq '/';
129             $path = VMS::Filespec::unixify($path);
130 0 0         }
131 0           next if -d $path;
132 0 0 0       my $parent = File::Basename::dirname($path);
133 0           unless (-d $parent or $path eq $parent) {
134             push(@created,_mkpath($arg, [$parent]));
135 0 0         }
136 0 0         print "mkdir $path\n" if $arg->{verbose};
137 0           if (mkdir($path,$arg->{mode})) {
138 0 0         push(@created, $path);
139             if (exists $arg->{owner}) {
140 0 0         # NB: $arg->{group} guaranteed to be set during initialisation
141 0           if (!chown $arg->{owner}, $arg->{group}, $path) {
142             _error($arg, "Cannot change ownership of $path to $arg->{owner}:$arg->{group}");
143             }
144             }
145             }
146 0           else {
147 0           my $save_bang = $!;
148 0 0         my ($e, $e1) = ($save_bang, $^E);
149             $e .= "; $e1" if $e ne $e1;
150 0 0         # allow for another process to have created it meanwhile
151 0           if (!-d $path) {
152 0 0         $! = $save_bang;
153 0           if ($arg->{error}) {
  0            
  0            
154             push @{${$arg->{error}}}, {$path => $e};
155             }
156 0           else {
157             _croak("mkdir $path: $e");
158             }
159             }
160             }
161 0           }
162             return @created;
163             }
164              
165 0 0 0 0 1   sub remove_tree {
166 0           push @_, {} unless @_ and UNIVERSAL::isa($_[-1],'HASH');
167             goto &rmtree;
168             }
169              
170 0     0     sub _is_subdir {
171             my($dir, $test) = @_;
172 0            
173 0           my($dv, $dd) = File::Spec->splitpath($dir, 1);
174             my($tv, $td) = File::Spec->splitpath($test, 1);
175              
176 0 0         # not on same volume
177             return 0 if $dv ne $tv;
178 0            
179 0           my @d = File::Spec->splitdir($dd);
180             my @t = File::Spec->splitdir($td);
181              
182 0 0         # @t can't be a subdir if it's shorter than @d
183             return 0 if @t < @d;
184 0            
185             return join('/', @d) eq join('/', splice @t, 0, +@d);
186             }
187              
188 0   0 0 1   sub rmtree {
189             my $old_style = !(@_ and UNIVERSAL::isa($_[-1],'HASH'));
190 0            
191             my $arg;
192             my $paths;
193 0 0          
194 0           if ($old_style) {
195 0           my ($verbose, $safe);
196 0           ($paths, $verbose, $safe) = @_;
197 0 0         $arg->{verbose} = $verbose;
198             $arg->{safe} = defined $safe ? $safe : 0;
199 0 0 0        
200 0 0         if (defined($paths) and length($paths)) {
201             $paths = [$paths] unless UNIVERSAL::isa($paths,'ARRAY');
202             }
203 0           else {
204 0           _carp ("No root path(s) specified\n");
205             return 0;
206             }
207             }
208 0           else {
209 0 0         $arg = pop @_;
  0            
210 0 0         ${$arg->{error}} = [] if exists $arg->{error};
  0            
211 0           ${$arg->{result}} = [] if exists $arg->{result};
212             $paths = [@_];
213             }
214 0            
215 0           $arg->{prefix} = '';
216             $arg->{depth} = 0;
217 0            
218 0 0         my @clean_path;
219 0           $arg->{cwd} = getcwd() or do {
220 0           _error($arg, "cannot fetch initial working directory");
221             return 0;
222 0           };
  0            
  0            
223             for ($arg->{cwd}) { /\A(.*)\Z/; $_ = $1 } # untaint
224 0            
225             for my $p (@$paths) {
226 0 0         # need to fixup case and map \ to / on Windows
227 0 0         my $ortho_root = $^O eq 'MSWin32' ? _slash_lc($p) : $p;
228 0           my $ortho_cwd = $^O eq 'MSWin32' ? _slash_lc($arg->{cwd}) : $arg->{cwd};
229 0 0         my $ortho_root_length = length($ortho_root);
230 0 0 0       $ortho_root_length-- if $^O eq 'VMS'; # don't compare '.' with ']'
231 0           if ($ortho_root_length && _is_subdir($ortho_root, $ortho_cwd)) {
232 0           local $! = 0;
233 0           _error($arg, "cannot remove path when cwd is $arg->{cwd}", $p);
234             next;
235             }
236 0 0          
    0          
237 0 0         if ($Is_MacOS) {
238 0 0         $p = ":$p" unless $p =~ /:/;
239             $p .= ":" unless $p =~ /:\z/;
240             }
241 0           elsif ($^O eq 'MSWin32') {
242             $p =~ s{[/\\]\z}{};
243             }
244 0           else {
245             $p =~ s{/\z}{};
246 0           }
247             push @clean_path, $p;
248             }
249 0 0          
  0            
250 0           @{$arg}{qw(device inode perm)} = (lstat $arg->{cwd})[0,1] or do {
251 0           _error($arg, "cannot stat initial working directory", $arg->{cwd});
252             return 0;
253             };
254 0            
255             return _rmtree($arg, \@clean_path);
256             }
257              
258 0     0     sub _rmtree {
259 0           my $arg = shift;
260             my $paths = shift;
261 0            
262 0           my $count = 0;
263 0           my $curdir = File::Spec->curdir();
264             my $updir = File::Spec->updir();
265 0            
266             my (@files, $root);
267 0           ROOT_DIR:
268             foreach $root (@$paths) {
269             # since we chdir into each directory, it may not be obvious
270             # to figure out where we are if we generate a message about
271             # a file name. We therefore construct a semi-canonical
272             # filename, anchored from the directory being unlinked (as
273             # opposed to being truly canonical, anchored from the root (/).
274 0 0          
275             my $canon = $arg->{prefix}
276             ? File::Spec->catfile($arg->{prefix}, $root)
277             : $root
278             ;
279 0 0          
280             my ($ldev, $lino, $perm) = (lstat $root)[0,1,2] or next ROOT_DIR;
281 0 0          
282 0 0         if ( -d _ ) {
283             $root = VMS::Filespec::vmspath(VMS::Filespec::pathify($root)) if $Is_VMS;
284 0 0          
285             if (!chdir($root)) {
286             # see if we can escalate privileges to get in
287 0           # (e.g. funny protection mask such as -w- instead of rwx)
288 0           $perm &= 07777;
289 0 0 0       my $nperm = $perm | 0700;
    0          
290 0           if (!($arg->{safe} or $nperm == $perm or chmod($nperm, $root))) {
291 0           _error($arg, "cannot make child directory read-write-exec", $canon);
292             next ROOT_DIR;
293             }
294 0           elsif (!chdir($root)) {
295 0           _error($arg, "cannot chdir to child", $canon);
296             next ROOT_DIR;
297             }
298             }
299 0 0          
300 0           my ($cur_dev, $cur_inode, $perm) = (stat $curdir)[0,1,2] or do {
301 0           _error($arg, "cannot stat current working directory", $canon);
302             next ROOT_DIR;
303             };
304 0 0          
305 0 0 0       if ($Need_Stat_Check) {
306             ($ldev eq $cur_dev and $lino eq $cur_inode)
307             or _croak("directory $canon changed before chdir, expected dev=$ldev ino=$lino, actual dev=$cur_dev ino=$cur_inode, aborting.");
308             }
309 0            
310 0           $perm &= 07777; # don't forget setuid, setgid, sticky bits
311             my $nperm = $perm | 0700;
312              
313             # notabene: 0700 is for making readable in the first place,
314             # it's also intended to change it to writable in case we have
315             # to recurse in which case we are better than rm -rf for
316             # subtrees with strange permissions
317 0 0 0        
      0        
318 0           if (!($arg->{safe} or $nperm == $perm or chmod($nperm, $curdir))) {
319 0           _error($arg, "cannot make directory read+writeable", $canon);
320             $nperm = $perm;
321             }
322 0            
323 0 0         my $d;
324 0 0         $d = gensym() if $] < 5.006;
325 0           if (!opendir $d, $curdir) {
326 0           _error($arg, "cannot opendir", $canon);
327             @files = ();
328             }
329             else {
330 0 0 0       no strict 'refs';
  0            
  0            
331             if (!defined ${"\cTAINT"} or ${"\cTAINT"}) {
332             # Blindly untaint dir names if taint mode is
333 0           # active, or any perl < 5.006
  0            
  0            
334             @files = map { /\A(.*)\z/s; $1 } readdir $d;
335             }
336 0           else {
337             @files = readdir $d;
338 0           }
339             closedir $d;
340             }
341 0 0          
342             if ($Is_VMS) {
343             # Deleting large numbers of files from VMS Files-11
344             # filesystems is faster if done in reverse ASCIIbetical order.
345 0 0         # include '.' to '.;' from blead patch #31775
  0            
346             @files = map {$_ eq '.' ? '.;' : $_} reverse @files;
347             }
348 0 0          
  0            
349             @files = grep {$_ ne $updir and $_ ne $curdir} @files;
350 0 0          
351             if (@files) {
352 0           # remove the contained files before the directory itself
353 0           my $narg = {%$arg};
  0            
354             @{$narg}{qw(device inode cwd prefix depth)}
355 0           = ($cur_dev, $cur_inode, $updir, $canon, $arg->{depth}+1);
356             $count += _rmtree($narg, \@files);
357             }
358              
359             # restore directory permissions of required now (in case the rmdir
360             # below fails), while we are still in the directory and may do so
361 0 0 0       # without a race via '.'
362 0           if ($nperm != $perm and not chmod($perm, $curdir)) {
363             _error($arg, "cannot reset chmod", $canon);
364             }
365              
366 0 0         # don't leave the client code in an unexpected directory
367             chdir($arg->{cwd})
368             or _croak("cannot chdir to $arg->{cwd} from $canon: $!, aborting.");
369              
370             # ensure that a chdir upwards didn't take us somewhere other
371 0 0         # than we expected (see CVE-2002-0435)
372             ($cur_dev, $cur_inode) = (stat $curdir)[0,1]
373             or _croak("cannot stat prior working directory $arg->{cwd}: $!, aborting.");
374 0 0          
375 0 0 0       if ($Need_Stat_Check) {
376             ($arg->{device} eq $cur_dev and $arg->{inode} eq $cur_inode)
377             or _croak("previous directory $arg->{cwd} changed before entering $canon, expected dev=$ldev ino=$lino, actual dev=$cur_dev ino=$cur_inode, aborting.");
378             }
379 0 0 0        
380 0 0 0       if ($arg->{depth} or !$arg->{keep_root}) {
    0          
381             if ($arg->{safe} &&
382 0 0         ($Is_VMS ? !&VMS::Filespec::candelete($root) : !-w $root)) {
383 0           print "skipped $root\n" if $arg->{verbose};
384             next ROOT_DIR;
385 0 0 0       }
386 0           if ($Force_Writeable and !chmod $perm | 0700, $root) {
387             _error($arg, "cannot make directory writeable", $canon);
388 0 0         }
389 0 0         print "rmdir $root\n" if $arg->{verbose};
390 0 0         if (rmdir $root) {
  0            
  0            
391 0           push @{${$arg->{result}}}, $root if $arg->{result};
392             ++$count;
393             }
394 0           else {
395 0 0 0       _error($arg, "cannot remove directory", $canon);
    0          
396             if ($Force_Writeable && !chmod($perm, ($Is_VMS ? VMS::Filespec::fileify($root) : $root))
397 0           ) {
398             _error($arg, sprintf("cannot restore permissions to 0%o",$perm), $canon);
399             }
400             }
401             }
402             }
403             else {
404 0 0 0       # not a directory
      0        
405             $root = VMS::Filespec::vmsify("./$root")
406             if $Is_VMS
407             && !File::Spec->file_name_is_absolute($root)
408             && ($root !~ m/(?]+/); # not already in VMS syntax
409 0 0 0        
    0 0        
410             if ($arg->{safe} &&
411             ($Is_VMS ? !&VMS::Filespec::candelete($root)
412             : !(-l $root || -w $root)))
413 0 0         {
414 0           print "skipped $root\n" if $arg->{verbose};
415             next ROOT_DIR;
416             }
417 0            
418 0 0 0       my $nperm = $perm & 07777 | 0600;
      0        
419 0           if ($Force_Writeable and $nperm != $perm and not chmod $nperm, $root) {
420             _error($arg, "cannot make file writeable", $canon);
421 0 0         }
422             print "unlink $canon\n" if $arg->{verbose};
423 0           # delete all versions under VMS
424 0 0         for (;;) {
425 0 0         if (unlink $root) {
  0            
  0            
426             push @{${$arg->{result}}}, $root if $arg->{result};
427             }
428 0           else {
429 0 0 0       _error($arg, "cannot unlink file", $canon);
430             $Force_Writeable and chmod($perm, $root) or
431 0           _error($arg, sprintf("cannot restore permissions to 0%o",$perm), $canon);
432             last;
433 0           }
434 0 0 0       ++$count;
435             last unless $Is_VMS && lstat $root;
436             }
437             }
438 0           }
439             return $count;
440             }
441              
442             sub _slash_lc {
443             # fix up slashes and case on MSWin32 so that we can determine that
444 0     0     # c:\path\to\dir is underneath C:/Path/To
445 0           my $path = shift;
446 0           $path =~ tr{\\}{/};
447             return lc($path);
448             }
449              
450             1;
451             __END__