File Coverage

blib/lib/Overload/FileCheck.pm
Criterion Covered Total %
statement 214 225 95.1
branch 124 146 84.9
condition 29 45 64.4
subroutine 31 32 96.8
pod 12 13 92.3
total 410 461 88.9


line stmt bran cond sub pod time code
1             # Copyright (c) 2018, cPanel, LLC.
2             # All rights reserved.
3             # http://cpanel.net
4             #
5             # This is free software; you can redistribute it and/or modify it under the
6             # same terms as Perl itself. See L.
7              
8             package Overload::FileCheck;
9              
10 47     47   10212448 use strict;
  47         323  
  47         1326  
11 47     47   308 use warnings;
  47         88  
  47         1139  
12              
13             # ABSTRACT: override/mock perl file check -X: -e, -f, -d, ...
14              
15 47     47   19528 use Errno ();
  47         58788  
  47         1278  
16              
17 47     47   310 use base 'Exporter';
  47         95  
  47         6038  
18              
19             BEGIN {
20              
21 47     47   178 our $VERSION = '0.011'; # VERSION: generated by DZP::OurPkgVersion
22              
23 47         234 require XSLoader;
24 47         32923 XSLoader::load(__PACKAGE__);
25             }
26              
27             use Fcntl (
28 47         56771 '_S_IFMT', # bit mask for the file type bit field
29             #'S_IFPERMS', # bit mask for file perms.
30             'S_IFSOCK', # socket
31             'S_IFLNK', # symbolic link
32             'S_IFREG', # regular file
33             'S_IFBLK', # block device
34             'S_IFDIR', # directory
35             'S_IFCHR', # character device
36             'S_IFIFO', # FIFO
37              
38             # qw{S_IRUSR S_IWUSR S_IXUSR S_IRWXU}
39 47     47   409 );
  47         144  
40              
41             my @STAT_T_IX = qw{
42             ST_DEV
43             ST_INO
44             ST_MODE
45             ST_NLINK
46             ST_UID
47             ST_GID
48             ST_RDEV
49             ST_SIZE
50             ST_ATIME
51             ST_MTIME
52             ST_CTIME
53             ST_BLKSIZE
54             ST_BLOCKS
55             };
56              
57             my @CHECK_STATUS = qw{CHECK_IS_FALSE CHECK_IS_TRUE FALLBACK_TO_REAL_OP};
58              
59             my @STAT_HELPERS = qw{ stat_as_directory stat_as_file stat_as_symlink
60             stat_as_socket stat_as_chr stat_as_block};
61              
62             our @EXPORT_OK = (
63             qw{
64             mock_all_from_stat
65             mock_all_file_checks mock_file_check mock_stat
66             unmock_file_check unmock_all_file_checks unmock_stat
67             },
68             @CHECK_STATUS,
69             @STAT_T_IX,
70             @STAT_HELPERS,
71             );
72              
73             our %EXPORT_TAGS = (
74             all => [@EXPORT_OK],
75              
76             # status code
77             check => [@CHECK_STATUS],
78              
79             # STAT array indexes
80             stat => [ @STAT_T_IX, @STAT_HELPERS ],
81             );
82              
83             # hash for every filecheck we can mock
84             # and their corresonding OP_TYPE
85             my %MAP_FC_OP = (
86             'R' => OP_FTRREAD,
87             'W' => OP_FTRWRITE,
88             'X' => OP_FTREXEC,
89             'r' => OP_FTEREAD,
90             'w' => OP_FTEWRITE,
91             'x' => OP_FTEEXEC,
92              
93             'e' => OP_FTIS,
94             's' => OP_FTSIZE, # OP_CAN_RETURN_INT
95             'M' => OP_FTMTIME, # OP_CAN_RETURN_INT
96             'C' => OP_FTCTIME, # OP_CAN_RETURN_INT
97             'A' => OP_FTATIME, # OP_CAN_RETURN_INT
98              
99             'O' => OP_FTROWNED,
100             'o' => OP_FTEOWNED,
101             'z' => OP_FTZERO,
102             'S' => OP_FTSOCK,
103             'c' => OP_FTCHR,
104             'b' => OP_FTBLK,
105             'f' => OP_FTFILE,
106             'd' => OP_FTDIR,
107             'p' => OP_FTPIPE,
108             'u' => OP_FTSUID,
109             'g' => OP_FTSGID,
110             'k' => OP_FTSVTX,
111              
112             'l' => OP_FTLINK,
113              
114             't' => OP_FTTTY,
115              
116             'T' => OP_FTTEXT,
117             'B' => OP_FTBINARY,
118              
119             # special cases for stat & lstat
120             'stat' => OP_STAT,
121             'lstat' => OP_LSTAT,
122              
123             );
124              
125             my %MAP_STAT_T_IX = (
126             st_dev => ST_DEV,
127             st_ino => ST_INO,
128             st_mode => ST_MODE,
129             st_nlink => ST_NLINK,
130             st_uid => ST_UID,
131             st_gid => ST_GID,
132             st_rdev => ST_RDEV,
133             st_size => ST_SIZE,
134             st_atime => ST_ATIME,
135             st_mtime => ST_MTIME,
136             st_ctime => ST_CTIME,
137             st_blksize => ST_BLKSIZE,
138             st_blocks => ST_BLOCKS,
139             );
140              
141             # op_type_id => check
142             my %REVERSE_MAP;
143              
144             my %OP_CAN_RETURN_INT = map { $MAP_FC_OP{$_} => 1 } qw{ s M C A };
145             my %OP_IS_STAT_OR_LSTAT = map { $MAP_FC_OP{$_} => 1 } qw{ stat lstat };
146             #
147             # This is listing the default ERRNO codes
148             # used by each test when the test fails and
149             # the user did not provide one ERRNO error
150             #
151             my %DEFAULT_ERRNO = (
152             'default' => Errno::ENOENT, # default value for any other not listed
153             'x' => Errno::ENOEXEC,
154             'X' => Errno::ENOEXEC,
155              
156             # ...
157             );
158              
159             # this is saving our custom ops
160             # optype_id => sub
161             my $_current_mocks = {};
162              
163             sub import {
164 46     46   484 my ( $class, @args ) = @_;
165              
166             # mock on import...
167 46         108 my $_next_check;
168             my @for_exporter;
169 46         130 foreach my $check (@args) {
170 57 100 100     789 if ( !$_next_check && $check !~ qr{^-} ) {
171              
172             # this is a valid arg for exporter
173 53         148 push @for_exporter, $check;
174 53         215 next;
175             }
176 4 100       15 if ( !$_next_check ) {
177              
178             # we found a key like '-e' in '-e => sub {} '
179 2         4 $_next_check = $check;
180             }
181             else {
182             # now this is the value
183 2         4 my $code = $check;
184              
185             # use Overload::FileCheck -from_stat => \&my_stat;
186 2 100 66     15 if ( $_next_check eq q{-from_stat} || $_next_check eq q{-from-stat} ) {
187 1         3 mock_all_from_stat($code);
188             }
189             else {
190 1         4 mock_file_check( $_next_check, $code );
191             }
192              
193 2         20 undef $_next_check;
194             }
195             }
196              
197             # callback the exporter logic
198 46         87236 return __PACKAGE__->export_to_level( 1, $class, @for_exporter );
199             }
200              
201             sub mock_all_file_checks {
202 8     8 0 369 my ($sub) = @_;
203              
204 8         135 foreach my $check ( sort keys %MAP_FC_OP ) {
205 232 100       939 next if $check =~ qr{^l?stat$}; # we should not mock stat
206             mock_file_check(
207             $check,
208             sub {
209 483     483   1047 my (@args) = @_;
210 483         1220 return $sub->( $check, @args );
211             }
212 216         892 );
213             }
214              
215 8         30 return 1;
216             }
217              
218             sub mock_file_check {
219 246     246 1 171637 my ( $check, $sub ) = @_;
220              
221 246 50       527 die q[Check is not defined] unless defined $check;
222 246 50       705 die q[Second arg must be a CODE ref] unless ref $sub eq 'CODE';
223              
224 246         451 $check =~ s{^-+}{}; # strip any extra dashes
225             #return -1 unless defined $MAP_FC_OP{$check}; # we should not do that
226 246 50       563 die qq[Unknown check '$check'] unless defined $MAP_FC_OP{$check};
227              
228 246         385 my $optype = $MAP_FC_OP{$check};
229 246 100       574 die qq[-$check is already mocked by Overload::FileCheck] if exists $_current_mocks->{$optype};
230              
231 245         503 $_current_mocks->{$optype} = $sub;
232              
233 245         637 _xs_mock_op($optype);
234              
235 245         534 return 1;
236             }
237              
238             sub unmock_file_check {
239 30     30 1 5380 my (@checks) = @_;
240              
241 30         98 foreach my $check (@checks) {
242 115 50       260 die q[Check is not defined] unless defined $check;
243 115         221 $check =~ s{^-+}{}; # strip any extra dashes
244 115 50       323 die qq[Unknown check '$check'] unless defined $MAP_FC_OP{$check};
245              
246 115         197 my $optype = $MAP_FC_OP{$check};
247              
248 115         308 delete $_current_mocks->{$optype};
249              
250 115         298 _xs_unmock_op($optype);
251             }
252              
253 30         152 return 1;
254             }
255              
256             sub mock_all_from_stat {
257 6     6 1 20111 my ($sub_for_stat) = @_;
258              
259             # then mock all -X checks to our custom
260             mock_all_file_checks(
261             sub {
262 317     317   659 my ( $check, $f_or_fh ) = @_;
263              
264             # the main call
265 317         745 my $return = _check_from_stat( $check, $f_or_fh, $sub_for_stat );
266              
267             # auto remock the OP (it could have been temporary unmocked to use -X _)
268 317         1029 _xs_mock_op( $MAP_FC_OP{$check} );
269              
270 317         882 return $return;
271             }
272 6         67 );
273              
274             # start by mocking 'stat' and 'lstat' call
275 6         26 mock_stat($sub_for_stat);
276              
277 6         33 return 1;
278             }
279              
280             sub _check_from_stat {
281 317     317   589 my ( $check, $f_or_fh, $sub_for_stat ) = @_;
282              
283 317         612 my $optype = $MAP_FC_OP{$check};
284              
285             # stat would need to be called twice
286             # 1/ we first need to check if we are mocking the file
287             # or if we let it fallback to the Perl OP
288             # 2/ doing a second stat call in order to cache _
289              
290 317         463 my $can_use_stat;
291 317 100       1932 $can_use_stat = 1 if $check =~ qr{^[sfdMXxzACORWeorw]$};
292              
293 317 100       1015 my $stat_or_lstat = $can_use_stat ? 'stat' : 'lstat';
294              
295 317         808 my (@mocked_lstat_result) = $sub_for_stat->( $stat_or_lstat, $f_or_fh );
296 317 100 66     5106 if ( scalar @mocked_lstat_result == 1
      66        
297             && !ref $mocked_lstat_result[0]
298             && $mocked_lstat_result[0] == FALLBACK_TO_REAL_OP ) {
299 1         5 return FALLBACK_TO_REAL_OP;
300             }
301              
302             # avoid a second callback to the user hook (do not really happen for now)
303             local $_current_mocks->{ $MAP_FC_OP{$stat_or_lstat} } = sub {
304 316     316   624 return @mocked_lstat_result;
305 316         1697 };
306              
307             # now performing a real stat call [ using the mocked stat function ]
308 316         640 my ( @stat, @lstat );
309              
310 316 100       597 if ($can_use_stat) {
311 47     47   430 no warnings; # throw warnings with Perl <= 5.14
  47         100  
  47         3719  
312 277 50       1229 @stat = stat($f_or_fh) if defined $f_or_fh;
313             }
314             else {
315 47     47   314 no warnings;
  47         97  
  47         93324  
316 39 50       178 @lstat = lstat($f_or_fh) if defined $f_or_fh;
317             }
318              
319 316 100       2287 if ( $check eq 'r' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    100          
    100          
    100          
    100          
    100          
    50          
320              
321             # -r File is readable by effective uid/gid.
322             # return _cando(stat_mode, effective, &PL_statcache)
323             # return _cando( S_IRUSR, 1 )
324              
325             # ugly need a better way to do this...
326 19         61 _xs_unmock_op($optype);
327 19         165 return _to_bool( scalar -r _ );
328             }
329             elsif ( $check eq 'w' ) {
330              
331             # -w File is writable by effective uid/gid.
332 19         69 _xs_unmock_op($optype);
333 19         173 return _to_bool( scalar -w _ );
334             }
335             elsif ( $check eq 'x' ) {
336              
337             # -x File is executable by effective uid/gid.
338 13         45 _xs_unmock_op($optype);
339 13         112 return _to_bool( scalar -x _ );
340             }
341             elsif ( $check eq 'o' ) {
342              
343             # -o File is owned by effective uid.
344 19         61 _xs_unmock_op($optype);
345 19         189 return _to_bool( scalar -o _ );
346             }
347             elsif ( $check eq 'R' ) {
348              
349             # -R File is readable by real uid/gid.
350 19         76 _xs_unmock_op($optype);
351 19         167 return _to_bool( scalar -R _ );
352             }
353             elsif ( $check eq 'W' ) {
354              
355             # -W File is writable by real uid/gid.
356 19         80 _xs_unmock_op($optype);
357 19         183 return _to_bool( scalar -W _ );
358             }
359             elsif ( $check eq 'X' ) {
360              
361             # -X File is executable by real uid/gid.
362              
363 12         36 _xs_unmock_op($optype);
364 12         105 return _to_bool( scalar -X _ );
365             }
366             elsif ( $check eq 'O' ) {
367              
368             # -O File is owned by real uid.
369 19         72 _xs_unmock_op($optype);
370 19         185 return _to_bool( scalar -O _ );
371             }
372             elsif ( $check eq 'e' ) {
373              
374             # -e File exists.
375             # a file can only exists if MODE is set ?
376 35   66     151 return _to_bool( scalar @stat && $stat[ST_MODE] );
377             }
378             elsif ( $check eq 'z' ) {
379              
380             # -z File has zero size (is empty).
381              
382             # TODO: can probably avoid the extra called...
383             # by checking it ourself
384              
385 7         49 _xs_unmock_op($optype);
386 7         22 return _to_bool( scalar -z _ );
387             }
388             elsif ( $check eq 's' ) {
389              
390             # -s File has nonzero size (returns size in bytes).
391              
392             # fallback does not work with symlinks
393             # do the check ourself, which also save a few calls
394              
395 18         99 return $stat[ST_SIZE];
396             }
397             elsif ( $check eq 'f' ) {
398              
399             # -f File is a plain file.
400 20         65 return _check_mode_type( $stat[ST_MODE], S_IFREG );
401             }
402             elsif ( $check eq 'd' ) {
403              
404             # -d File is a directory.
405              
406 36         141 return _check_mode_type( $stat[ST_MODE], S_IFDIR );
407             }
408             elsif ( $check eq 'l' ) {
409              
410             # -l File is a symbolic link (false if symlinks aren't
411             # supported by the file system).
412              
413 9         29 return _check_mode_type( $lstat[ST_MODE], S_IFLNK );
414             }
415             elsif ( $check eq 'p' ) {
416              
417             # -p File is a named pipe (FIFO), or Filehandle is a pipe.
418 1         7 return _check_mode_type( $lstat[ST_MODE], S_IFIFO );
419             }
420             elsif ( $check eq 'S' ) {
421              
422             # -S File is a socket.
423 5         28 return _check_mode_type( $lstat[ST_MODE], S_IFSOCK );
424             }
425             elsif ( $check eq 'b' ) {
426              
427             # -b File is a block special file.
428 2         6 return _check_mode_type( $lstat[ST_MODE], S_IFBLK );
429             }
430             elsif ( $check eq 'c' ) {
431              
432             # -c File is a character special file.
433 2         6 return _check_mode_type( $lstat[ST_MODE], S_IFCHR );
434             }
435             elsif ( $check eq 't' ) {
436              
437             # -t Filehandle is opened to a tty.
438 0         0 _xs_unmock_op($optype);
439 0         0 return _to_bool( scalar -t _ );
440             }
441             elsif ( $check eq 'u' ) {
442              
443             # -u File has setuid bit set.
444 0         0 _xs_unmock_op($optype);
445 0         0 return _to_bool( scalar -u _ );
446             }
447             elsif ( $check eq 'g' ) {
448              
449             # -g File has setgid bit set.
450 0         0 _xs_unmock_op($optype);
451 0         0 return _to_bool( scalar -g _ );
452             }
453             elsif ( $check eq 'k' ) {
454              
455             # -k File has sticky bit set.
456              
457 1         5 _xs_unmock_op($optype);
458 1         4 return _to_bool( scalar -k _ );
459             }
460             elsif ( $check eq 'T' ) { # heuristic guess.. throw a die?
461              
462             # -T File is an ASCII or UTF-8 text file (heuristic guess).
463              
464             #return CHECK_IS_FALSE if -d $f_or_fh;
465              
466 5         59 _xs_unmock_op($optype);
467 5         203 return _to_bool( scalar -T *_ );
468             }
469             elsif ( $check eq 'B' ) { # heuristic guess.. throw a die?
470              
471             # -B File is a "binary" file (opposite of -T).
472              
473 14 100       41 return CHECK_IS_TRUE if -d $f_or_fh;
474              
475             # ... we cannot really know...
476             # ... this is an heuristic guess...
477              
478 4         16 _xs_unmock_op($optype);
479 4         171 return _to_bool( scalar -B *_ );
480             }
481             elsif ( $check eq 'M' ) {
482              
483             # -M Script start time minus file modification time, in days.
484              
485 7 50 33     47 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_MTIME];
486 7         56 return ( ( get_basetime() - $stat[ST_MTIME] ) / 86400.0 );
487              
488             #return int( scalar -M _ );
489             }
490             elsif ( $check eq 'A' ) {
491              
492             # -A Same for access time.
493             #
494             # ((NV)PL_basetime - PL_statcache.st_atime) / 86400.0
495 8 50 33     63 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_ATIME];
496              
497 8         78 return ( ( get_basetime() - $stat[ST_ATIME] ) / 86400.0 );
498             }
499             elsif ( $check eq 'C' ) {
500              
501             # -C Same for inode change time (Unix, may differ for other
502             #_xs_unmock_op($optype);
503             #return scalar -C *_;
504 7 50 33     42 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_CTIME];
505              
506 7         62 return ( ( get_basetime() - $stat[ST_CTIME] ) / 86400.0 );
507             }
508             else {
509 0         0 die "Unknown check $check.\n";
510             }
511              
512 0         0 die "FileCheck -$check is not implemented by Overload::FileCheck...";
513              
514 0         0 return FALLBACK_TO_REAL_OP;
515             }
516              
517             sub _to_bool {
518 265     265   735 my ($s) = @_;
519              
520 265 100       1806 return ( $s ? CHECK_IS_TRUE : CHECK_IS_FALSE );
521             }
522              
523             sub _check_mode_type {
524 75     75   173 my ( $mode, $type ) = @_;
525              
526 75 100       199 return CHECK_IS_FALSE unless defined $mode;
527 74         214 return _to_bool( ( $mode & _S_IFMT ) == $type );
528             }
529              
530             # this is a special case used to mock OP_STAT & OP_LSTAT
531             sub mock_stat {
532 8     8 1 7782 my ($sub) = @_;
533              
534 8 50       40 die q[First arg must be a CODE ref] unless ref $sub eq 'CODE';
535              
536 8         27 foreach my $opname (qw{stat lstat}) {
537 16         39 my $optype = $MAP_FC_OP{$opname};
538 16 50       43 die qq[No optype found for $opname] unless $optype;
539              
540             # plug the sub
541             $_current_mocks->{$optype} = sub {
542 34     34   72 my $file_or_handle = shift;
543 34         224 return $sub->( $opname, $file_or_handle );
544 16         86 };
545              
546             # setup the mock for the OP
547 16         53 _xs_mock_op($optype);
548             }
549              
550 8         24 return 1;
551             }
552              
553             # just an alias to unmock stat & lstat at the same time
554             sub unmock_stat {
555 0     0 1 0 return unmock_file_check(qw{stat lstat});
556             }
557              
558             sub unmock_all_file_checks {
559              
560 9 100   9 1 2878 if ( !scalar %REVERSE_MAP ) {
561 7         82 foreach my $k ( keys %MAP_FC_OP ) {
562 203         481 $REVERSE_MAP{ $MAP_FC_OP{$k} } = $k;
563             }
564             }
565              
566 9         53 my @mocks = sort map { $REVERSE_MAP{$_} } keys %$_current_mocks;
  93         222  
567 9 50       42 return unless scalar @mocks;
568              
569 9         47 return unmock_file_check(@mocks);
570             }
571              
572             # should not be called directly
573             # this is called from XS to check if one OP is mocked
574             # and trigger the callback function when mocked
575             my $_last_call_for;
576              
577             sub _check {
578 1189     1189   527738 my ( $optype, $file, @others ) = @_;
579              
580 1189 50       3010 die if scalar @others; # need to move this in a unit test
581              
582             # we have no custom mock at this point
583 1189 50       3166 return FALLBACK_TO_REAL_OP unless defined $_current_mocks->{$optype};
584              
585 1189 50 66     2983 $file = $_last_call_for if !defined $file && defined $_last_call_for && !defined $_current_mocks->{ $MAP_FC_OP{'stat'} };
      66        
586 1189         3209 my ( $out, @extra ) = $_current_mocks->{$optype}->($file);
587 1189         233817 $_last_call_for = $file;
588              
589             # FIXME return undef when not defined out
590              
591 1189 100 66     4745 if ( defined $out && $OP_CAN_RETURN_INT{$optype} ) {
592 106         1650 return $out; # limitation to int for now in fact some returns NVs
593             }
594              
595 1083 100       2258 if ( !$out ) {
596              
597             # check if the user provided a custom ERRNO error otherwise
598             # set one for him, so a test could never fail without having
599             # ERRNO set
600 195 100       1079 if ( !int($!) ) {
601 23   66     241 $! = $DEFAULT_ERRNO{ $REVERSE_MAP{$optype} || 'default' } || $DEFAULT_ERRNO{'default'};
602             }
603              
604             #return undef unless defined $out;
605 195         2146 return CHECK_IS_FALSE;
606             }
607              
608 888 100 100     6127 return FALLBACK_TO_REAL_OP if !ref $out && $out == FALLBACK_TO_REAL_OP;
609              
610             # stat and lstat OP are returning a stat ARRAY in addition to the status code
611 751 100       1610 if ( $OP_IS_STAT_OR_LSTAT{$optype} ) {
612              
613             # .......... Stat_t
614             # dev_t st_dev Device ID of device containing file.
615             # ino_t st_ino File serial number.
616             # mode_t st_mode Mode of file (see below).
617             # nlink_t st_nlink Number of hard links to the file.
618             # uid_t st_uid User ID of file.
619             # gid_t st_gid Group ID of file.
620             # dev_t st_rdev Device ID (if file is character or block special).
621             # off_t st_size For regular files, the file size in bytes.
622             # time_t st_atime Time of last access.
623             # time_t st_mtime Time of last data modification.
624             # time_t st_ctime Time of last status change.
625             # blksize_t st_blksize A file system-specific preferred I/O block size for
626             # blkcnt_t st_blocks Number of blocks allocated for this object.
627             # ......
628              
629 347   33     788 my $stat = $out // $others[0]; # can be array or hash at this point
630 347         554 my $stat_is_a = ref $stat;
631 347 100       702 die q[Your mocked function for stat should return a stat array or hash] unless $stat_is_a;
632              
633 345         492 my $stat_as_arrayref;
634              
635             # can handle one ARRAY or a HASH
636 345         512 my $stat_t_max = STAT_T_MAX;
637 345 100       668 if ( $stat_is_a eq 'ARRAY' ) {
    50          
638 339         455 $stat_as_arrayref = $stat;
639 339         528 my $av_size = scalar @$stat;
640 339 100 100     1150 if (
641             $av_size # 0 is valid when the file is missing
642             && $av_size != $stat_t_max
643             ) {
644 4         59 die qq[Stat array should contain exactly 0 or $stat_t_max values];
645             }
646             }
647             elsif ( $stat_is_a eq 'HASH' ) {
648 6         21 $stat_as_arrayref = [ (0) x $stat_t_max ]; # start with an empty array
649 6         24 foreach my $k ( keys %$stat ) {
650 9         29 my $ix = $MAP_STAT_T_IX{ lc($k) };
651 9 100       109 die qq[Unknown index for stat_t struct key $k] unless defined $ix;
652 7         20 $stat_as_arrayref->[$ix] = $stat->{$k};
653             }
654             }
655             else {
656 0         0 die q[Your mocked function for stat should return a stat array or hash];
657             }
658              
659 339         2281 return ( CHECK_IS_TRUE, $stat_as_arrayref );
660             }
661              
662 404         3968 return CHECK_IS_TRUE;
663             }
664              
665             # accessors for testing purpose mainly
666             sub _get_filecheck_ops_map {
667 42     42   113341 return {%MAP_FC_OP}; # return a copy
668             }
669              
670             ######################################################
671             ### stat helpers
672             ######################################################
673              
674             sub stat_as_directory {
675 2     2 1 95 my (%opts) = @_;
676              
677 2         7 return _stat_for( S_IFDIR, \%opts );
678             }
679              
680             sub stat_as_file {
681 18     18 1 11955 my (%opts) = @_;
682              
683 18         57 return _stat_for( S_IFREG, \%opts );
684             }
685              
686             sub stat_as_symlink {
687 2     2 1 9 my (%opts) = @_;
688              
689 2         7 return _stat_for( S_IFLNK, \%opts );
690             }
691              
692             sub stat_as_socket {
693 2     2 1 6 my (%opts) = @_;
694              
695 2         6 return _stat_for( S_IFSOCK, \%opts );
696             }
697              
698             sub stat_as_chr {
699 1     1 1 4 my (%opts) = @_;
700              
701 1         5 return _stat_for( S_IFCHR, \%opts );
702             }
703              
704             sub stat_as_block {
705 1     1 1 4 my (%opts) = @_;
706              
707 1         5 return _stat_for( S_IFBLK, \%opts );
708             }
709              
710             sub _stat_for {
711 26     26   51 my ( $type, $opts ) = @_;
712              
713 26         81 my @stat = ( (0) x 13 ); # STAT_T_MAX
714              
715             # set file type
716 26 50       70 if ( defined $type ) {
717              
718             # _S_IFMT is used as a protection to do not flip outside the mask
719 26         57 $stat[ST_MODE] |= ( $type & _S_IFMT );
720             }
721              
722             # set permission using octal
723 26 100       62 if ( defined $opts->{perms} ) {
724              
725             # _S_IFMT is used as a protection to do not flip outside the mask
726 2         6 $stat[ST_MODE] |= ( $opts->{perms} & ~_S_IFMT );
727             }
728              
729             # deal with UID / GID
730 26 100       59 if ( defined $opts->{uid} ) {
731 2 100       27 if ( $opts->{uid} =~ qr{^[0-9]+$} ) {
732 1         4 $stat[ST_UID] = $opts->{uid};
733             }
734             else {
735              
736 1         75 $stat[ST_UID] = getpwnam( $opts->{uid} );
737             }
738             }
739              
740 26 100       76 if ( defined $opts->{gid} ) {
741 2 100       15 if ( $opts->{gid} =~ qr{^[0-9]+$} ) {
742 1         3 $stat[ST_GID] = $opts->{gid};
743             }
744             else {
745 1         55 $stat[ST_GID] = getgrnam( $opts->{gid} );
746             }
747             }
748              
749             # options that we can simply copy to a slot
750 26         117 my %name2ix = (
751             size => ST_SIZE,
752             atime => ST_ATIME,
753             mtime => ST_MTIME,
754             ctime => ST_CTIME,
755             blksize => ST_BLKSIZE,
756             blocks => ST_BLOCKS,
757             );
758              
759 26         86 foreach my $k ( keys %$opts ) {
760 20         49 $k = lc($k);
761 20         43 $k =~ s{^st_}{};
762 20 100       51 next unless defined $name2ix{$k};
763              
764 14         39 $stat[ $name2ix{$k} ] = $opts->{$k};
765             }
766              
767 26         178 return \@stat;
768             }
769              
770             1;
771              
772             __END__