File Coverage

blib/lib/Overload/FileCheck.pm
Criterion Covered Total %
statement 214 225 95.1
branch 124 146 84.9
condition 30 45 66.6
subroutine 31 32 96.8
pod 12 13 92.3
total 411 461 89.1


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 48     48   9301098 use strict;
  48         315  
  48         1232  
11 48     48   236 use warnings;
  48         82  
  48         1057  
12              
13             # ABSTRACT: override/mock perl file check -X: -e, -f, -d, ...
14              
15 48     48   17836 use Errno ();
  48         53973  
  48         1153  
16              
17 48     48   280 use base 'Exporter';
  48         101  
  48         5378  
18              
19             BEGIN {
20              
21 48     48   166 our $VERSION = '0.013'; # VERSION: generated by DZP::OurPkgVersion
22              
23 48         214 require XSLoader;
24 48         31315 XSLoader::load(__PACKAGE__);
25             }
26              
27             use Fcntl (
28 48         51215 '_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 48     48   383 );
  48         91  
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 47     47   413 my ( $class, @args ) = @_;
165              
166             # mock on import...
167 47         109 my $_next_check;
168             my @for_exporter;
169 47         134 foreach my $check (@args) {
170 60 100 100     762 if ( !$_next_check && $check !~ qr{^-} ) {
171              
172             # this is a valid arg for exporter
173 56         129 push @for_exporter, $check;
174 56         197 next;
175             }
176 4 100       13 if ( !$_next_check ) {
177              
178             # we found a key like '-e' in '-e => sub {} '
179 2         5 $_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     8 if ( $_next_check eq q{-from_stat} || $_next_check eq q{-from-stat} ) {
187 1         2 mock_all_from_stat($code);
188             }
189             else {
190 1         3 mock_file_check( $_next_check, $code );
191             }
192              
193 2         4 undef $_next_check;
194             }
195             }
196              
197             # callback the exporter logic
198 47         77495 return __PACKAGE__->export_to_level( 1, $class, @for_exporter );
199             }
200              
201             sub mock_all_file_checks {
202 9     9 0 365 my ($sub) = @_;
203              
204 9         141 foreach my $check ( sort keys %MAP_FC_OP ) {
205 261 100       858 next if $check =~ qr{^l?stat$}; # we should not mock stat
206             mock_file_check(
207             $check,
208             sub {
209 492     492   882 my (@args) = @_;
210 492         998 return $sub->( $check, @args );
211             }
212 243         915 );
213             }
214              
215 9         30 return 1;
216             }
217              
218             sub mock_file_check {
219 273     273 1 149301 my ( $check, $sub ) = @_;
220              
221 273 50       510 die q[Check is not defined] unless defined $check;
222 273 50       522 die q[Second arg must be a CODE ref] unless ref $sub eq 'CODE';
223              
224 273         425 $check =~ s{^-+}{}; # strip any extra dashes
225             #return -1 unless defined $MAP_FC_OP{$check}; # we should not do that
226 273 50       501 die qq[Unknown check '$check'] unless defined $MAP_FC_OP{$check};
227              
228 273         361 my $optype = $MAP_FC_OP{$check};
229 273 100       537 die qq[-$check is already mocked by Overload::FileCheck] if exists $_current_mocks->{$optype};
230              
231 272         470 $_current_mocks->{$optype} = $sub;
232              
233 272         603 _xs_mock_op($optype);
234              
235 272         502 return 1;
236             }
237              
238             sub unmock_file_check {
239 31     31 1 4882 my (@checks) = @_;
240              
241 31         98 foreach my $check (@checks) {
242 144 50       266 die q[Check is not defined] unless defined $check;
243 144         217 $check =~ s{^-+}{}; # strip any extra dashes
244 144 50       292 die qq[Unknown check '$check'] unless defined $MAP_FC_OP{$check};
245              
246 144         197 my $optype = $MAP_FC_OP{$check};
247              
248 144         335 delete $_current_mocks->{$optype};
249              
250 144         315 _xs_unmock_op($optype);
251             }
252              
253 31         137 return 1;
254             }
255              
256             sub mock_all_from_stat {
257 7     7 1 25795 my ($sub_for_stat) = @_;
258              
259             # then mock all -X checks to our custom
260             mock_all_file_checks(
261             sub {
262 326     326   546 my ( $check, $f_or_fh ) = @_;
263              
264             # the main call
265 326         575 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 326         960 _xs_mock_op( $MAP_FC_OP{$check} );
269              
270 326         736 return $return;
271             }
272 7         74 );
273              
274             # start by mocking 'stat' and 'lstat' call
275 7         33 mock_stat($sub_for_stat);
276              
277 7         33 return 1;
278             }
279              
280             sub _check_from_stat {
281 326     326   493 my ( $check, $f_or_fh, $sub_for_stat ) = @_;
282              
283 326         471 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 326         367 my $can_use_stat;
291 326 100       1574 $can_use_stat = 1 if $check =~ qr{^[sfdMXxzACORWeorw]$};
292              
293 326 100       840 my $stat_or_lstat = $can_use_stat ? 'stat' : 'lstat';
294              
295 326         650 my (@mocked_lstat_result) = $sub_for_stat->( $stat_or_lstat, $f_or_fh );
296 326 100 66     7260 if ( scalar @mocked_lstat_result == 1
      66        
297             && !ref $mocked_lstat_result[0]
298             && $mocked_lstat_result[0] == FALLBACK_TO_REAL_OP ) {
299 1         7 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 325     325   518 return @mocked_lstat_result;
305 325         1402 };
306              
307             # now performing a real stat call [ using the mocked stat function ]
308 325         573 my ( @stat, @lstat );
309              
310 325 100       518 if ($can_use_stat) {
311 48     48   374 no warnings; # throw warnings with Perl <= 5.14
  48         92  
  48         3332  
312 282 50       973 @stat = stat($f_or_fh) if defined $f_or_fh;
313             }
314             else {
315 48     48   291 no warnings;
  48         107  
  48         84019  
316 43 50       142 @lstat = lstat($f_or_fh) if defined $f_or_fh;
317             }
318              
319 325 100       1685 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         46 _xs_unmock_op($optype);
327 19         138 return _to_bool( scalar -r _ );
328             }
329             elsif ( $check eq 'w' ) {
330              
331             # -w File is writable by effective uid/gid.
332 19         46 _xs_unmock_op($optype);
333 19         137 return _to_bool( scalar -w _ );
334             }
335             elsif ( $check eq 'x' ) {
336              
337             # -x File is executable by effective uid/gid.
338 13         31 _xs_unmock_op($optype);
339 13         95 return _to_bool( scalar -x _ );
340             }
341             elsif ( $check eq 'o' ) {
342              
343             # -o File is owned by effective uid.
344 19         40 _xs_unmock_op($optype);
345 19         141 return _to_bool( scalar -o _ );
346             }
347             elsif ( $check eq 'R' ) {
348              
349             # -R File is readable by real uid/gid.
350 19         42 _xs_unmock_op($optype);
351 19         138 return _to_bool( scalar -R _ );
352             }
353             elsif ( $check eq 'W' ) {
354              
355             # -W File is writable by real uid/gid.
356 19         44 _xs_unmock_op($optype);
357 19         141 return _to_bool( scalar -W _ );
358             }
359             elsif ( $check eq 'X' ) {
360              
361             # -X File is executable by real uid/gid.
362              
363 12         28 _xs_unmock_op($optype);
364 12         89 return _to_bool( scalar -X _ );
365             }
366             elsif ( $check eq 'O' ) {
367              
368             # -O File is owned by real uid.
369 19         47 _xs_unmock_op($optype);
370 19         139 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 39   100     140 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         35 _xs_unmock_op($optype);
386 7         18 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         72 return $stat[ST_SIZE];
396             }
397             elsif ( $check eq 'f' ) {
398              
399             # -f File is a plain file.
400 21         49 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         86 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 13         33 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         4 return _check_mode_type( $lstat[ST_MODE], S_IFIFO );
419             }
420             elsif ( $check eq 'S' ) {
421              
422             # -S File is a socket.
423 5         24 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         4 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         4 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         4 _xs_unmock_op($optype);
458 1         3 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         55 _xs_unmock_op($optype);
467 5         118 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       34 return CHECK_IS_TRUE if -d $f_or_fh;
474              
475             # ... we cannot really know...
476             # ... this is an heuristic guess...
477              
478 4         9 _xs_unmock_op($optype);
479 4         136 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     28 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_MTIME];
486 7         41 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     37 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_ATIME];
496              
497 8         53 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     33 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_CTIME];
505              
506 7         45 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 272     272   952 my ($s) = @_;
519              
520 272 100       1545 return ( $s ? CHECK_IS_TRUE : CHECK_IS_FALSE );
521             }
522              
523             sub _check_mode_type {
524 80     80   156 my ( $mode, $type ) = @_;
525              
526 80 100       167 return CHECK_IS_FALSE unless defined $mode;
527 77         151 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 9     9 1 26666 my ($sub) = @_;
533              
534 9 50       61 die q[First arg must be a CODE ref] unless ref $sub eq 'CODE';
535              
536 9         33 foreach my $opname (qw{stat lstat}) {
537 18         38 my $optype = $MAP_FC_OP{$opname};
538 18 50       45 die qq[No optype found for $opname] unless $optype;
539              
540             # plug the sub
541             $_current_mocks->{$optype} = sub {
542 34     34   58 my $file_or_handle = shift;
543 34         213 return $sub->( $opname, $file_or_handle );
544 18         81 };
545              
546             # setup the mock for the OP
547 18         53 _xs_mock_op($optype);
548             }
549              
550 9         46 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 10 100   10 1 2739 if ( !scalar %REVERSE_MAP ) {
561 8         90 foreach my $k ( keys %MAP_FC_OP ) {
562 232         494 $REVERSE_MAP{ $MAP_FC_OP{$k} } = $k;
563             }
564             }
565              
566 10         50 my @mocks = sort map { $REVERSE_MAP{$_} } keys %$_current_mocks;
  122         260  
567 10 50       52 return unless scalar @mocks;
568              
569 10         51 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 1207     1207   415585 my ( $optype, $file, @others ) = @_;
579              
580 1207 50       2707 die if scalar @others; # need to move this in a unit test
581              
582             # we have no custom mock at this point
583 1207 50       2770 return FALLBACK_TO_REAL_OP unless defined $_current_mocks->{$optype};
584              
585 1207 50 66     2429 $file = $_last_call_for if !defined $file && defined $_last_call_for && !defined $_current_mocks->{ $MAP_FC_OP{'stat'} };
      66        
586 1207         2440 my ( $out, @extra ) = $_current_mocks->{$optype}->($file);
587 1207         207097 $_last_call_for = $file;
588              
589             # FIXME return undef when not defined out
590              
591 1207 100 66     4233 if ( defined $out && $OP_CAN_RETURN_INT{$optype} ) {
592 106         1298 return $out; # limitation to int for now in fact some returns NVs
593             }
594              
595 1101 100       1892 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 200 100       1075 if ( !int($!) ) {
601 23   66     260 $! = $DEFAULT_ERRNO{ $REVERSE_MAP{$optype} || 'default' } || $DEFAULT_ERRNO{'default'};
602             }
603              
604             #return undef unless defined $out;
605 200         1902 return CHECK_IS_FALSE;
606             }
607              
608 901 100 100     5615 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 764 100       1363 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 356   33     626 my $stat = $out // $others[0]; # can be array or hash at this point
630 356         463 my $stat_is_a = ref $stat;
631 356 100       547 die q[Your mocked function for stat should return a stat array or hash] unless $stat_is_a;
632              
633 354         403 my $stat_as_arrayref;
634              
635             # can handle one ARRAY or a HASH
636 354         416 my $stat_t_max = STAT_T_MAX;
637 354 100       620 if ( $stat_is_a eq 'ARRAY' ) {
    50          
638 348         388 $stat_as_arrayref = $stat;
639 348         432 my $av_size = scalar @$stat;
640 348 100 100     947 if (
641             $av_size # 0 is valid when the file is missing
642             && $av_size != $stat_t_max
643             ) {
644 4         53 die qq[Stat array should contain exactly 0 or $stat_t_max values];
645             }
646             }
647             elsif ( $stat_is_a eq 'HASH' ) {
648 6         22 $stat_as_arrayref = [ (0) x $stat_t_max ]; # start with an empty array
649 6         22 foreach my $k ( keys %$stat ) {
650 10         26 my $ix = $MAP_STAT_T_IX{ lc($k) };
651 10 100       48 die qq[Unknown index for stat_t struct key $k] unless defined $ix;
652 8         26 $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 348         1911 return ( CHECK_IS_TRUE, $stat_as_arrayref );
660             }
661              
662 408         3345 return CHECK_IS_TRUE;
663             }
664              
665             # accessors for testing purpose mainly
666             sub _get_filecheck_ops_map {
667 42     42   86185 return {%MAP_FC_OP}; # return a copy
668             }
669              
670             ######################################################
671             ### stat helpers
672             ######################################################
673              
674             sub stat_as_directory {
675 2     2 1 111 my (%opts) = @_;
676              
677 2         7 return _stat_for( S_IFDIR, \%opts );
678             }
679              
680             sub stat_as_file {
681 18     18 1 12644 my (%opts) = @_;
682              
683 18         45 return _stat_for( S_IFREG, \%opts );
684             }
685              
686             sub stat_as_symlink {
687 2     2 1 5 my (%opts) = @_;
688              
689 2         5 return _stat_for( S_IFLNK, \%opts );
690             }
691              
692             sub stat_as_socket {
693 2     2 1 6 my (%opts) = @_;
694              
695 2         5 return _stat_for( S_IFSOCK, \%opts );
696             }
697              
698             sub stat_as_chr {
699 1     1 1 3 my (%opts) = @_;
700              
701 1         3 return _stat_for( S_IFCHR, \%opts );
702             }
703              
704             sub stat_as_block {
705 1     1 1 2 my (%opts) = @_;
706              
707 1         3 return _stat_for( S_IFBLK, \%opts );
708             }
709              
710             sub _stat_for {
711 26     26   45 my ( $type, $opts ) = @_;
712              
713 26         74 my @stat = ( (0) x 13 ); # STAT_T_MAX
714              
715             # set file type
716 26 50       58 if ( defined $type ) {
717              
718             # _S_IFMT is used as a protection to do not flip outside the mask
719 26         42 $stat[ST_MODE] |= ( $type & _S_IFMT );
720             }
721              
722             # set permission using octal
723 26 100       57 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       52 if ( defined $opts->{uid} ) {
731 2 100       22 if ( $opts->{uid} =~ qr{^[0-9]+$} ) {
732 1         3 $stat[ST_UID] = $opts->{uid};
733             }
734             else {
735              
736 1         102 $stat[ST_UID] = getpwnam( $opts->{uid} );
737             }
738             }
739              
740 26 100       63 if ( defined $opts->{gid} ) {
741 2 100       17 if ( $opts->{gid} =~ qr{^[0-9]+$} ) {
742 1         2 $stat[ST_GID] = $opts->{gid};
743             }
744             else {
745 1         65 $stat[ST_GID] = getgrnam( $opts->{gid} );
746             }
747             }
748              
749             # options that we can simply copy to a slot
750 26         100 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         69 foreach my $k ( keys %$opts ) {
760 20         44 $k = lc($k);
761 20         31 $k =~ s{^st_}{};
762 20 100       44 next unless defined $name2ix{$k};
763              
764 14         30 $stat[ $name2ix{$k} ] = $opts->{$k};
765             }
766              
767 26         151 return \@stat;
768             }
769              
770             1;
771              
772             __END__