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   10799948 use strict;
  47         344  
  47         1402  
11 47     47   268 use warnings;
  47         96  
  47         1217  
12              
13             # ABSTRACT: override/mock perl file check -X: -e, -f, -d, ...
14              
15 47     47   20875 use Errno ();
  47         61713  
  47         1305  
16              
17 47     47   332 use base 'Exporter';
  47         100  
  47         6189  
18              
19             BEGIN {
20              
21 47     47   222 our $VERSION = '0.012'; # VERSION: generated by DZP::OurPkgVersion
22              
23 47         258 require XSLoader;
24 47         35685 XSLoader::load(__PACKAGE__);
25             }
26              
27             use Fcntl (
28 47         60152 '_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   418 );
  47         106  
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   468 my ( $class, @args ) = @_;
165              
166             # mock on import...
167 46         116 my $_next_check;
168             my @for_exporter;
169 46         131 foreach my $check (@args) {
170 57 100 100     801 if ( !$_next_check && $check !~ qr{^-} ) {
171              
172             # this is a valid arg for exporter
173 53         145 push @for_exporter, $check;
174 53         206 next;
175             }
176 4 100       14 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     11 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         3 mock_file_check( $_next_check, $code );
191             }
192              
193 2         5 undef $_next_check;
194             }
195             }
196              
197             # callback the exporter logic
198 46         92908 return __PACKAGE__->export_to_level( 1, $class, @for_exporter );
199             }
200              
201             sub mock_all_file_checks {
202 8     8 0 503 my ($sub) = @_;
203              
204 8         153 foreach my $check ( sort keys %MAP_FC_OP ) {
205 232 100       923 next if $check =~ qr{^l?stat$}; # we should not mock stat
206             mock_file_check(
207             $check,
208             sub {
209 518     518   1071 my (@args) = @_;
210 518         1236 return $sub->( $check, @args );
211             }
212 216         894 );
213             }
214              
215 8         39 return 1;
216             }
217              
218             sub mock_file_check {
219 246     246 1 175675 my ( $check, $sub ) = @_;
220              
221 246 50       606 die q[Check is not defined] unless defined $check;
222 246 50       582 die q[Second arg must be a CODE ref] unless ref $sub eq 'CODE';
223              
224 246         470 $check =~ s{^-+}{}; # strip any extra dashes
225             #return -1 unless defined $MAP_FC_OP{$check}; # we should not do that
226 246 50       553 die qq[Unknown check '$check'] unless defined $MAP_FC_OP{$check};
227              
228 246         384 my $optype = $MAP_FC_OP{$check};
229 246 100       596 die qq[-$check is already mocked by Overload::FileCheck] if exists $_current_mocks->{$optype};
230              
231 245         529 $_current_mocks->{$optype} = $sub;
232              
233 245         644 _xs_mock_op($optype);
234              
235 245         552 return 1;
236             }
237              
238             sub unmock_file_check {
239 30     30 1 5772 my (@checks) = @_;
240              
241 30         101 foreach my $check (@checks) {
242 115 50       275 die q[Check is not defined] unless defined $check;
243 115         221 $check =~ s{^-+}{}; # strip any extra dashes
244 115 50       291 die qq[Unknown check '$check'] unless defined $MAP_FC_OP{$check};
245              
246 115         216 my $optype = $MAP_FC_OP{$check};
247              
248 115         312 delete $_current_mocks->{$optype};
249              
250 115         296 _xs_unmock_op($optype);
251             }
252              
253 30         157 return 1;
254             }
255              
256             sub mock_all_from_stat {
257 6     6 1 21693 my ($sub_for_stat) = @_;
258              
259             # then mock all -X checks to our custom
260             mock_all_file_checks(
261             sub {
262 352     352   731 my ( $check, $f_or_fh ) = @_;
263              
264             # the main call
265 352         728 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 352         1164 _xs_mock_op( $MAP_FC_OP{$check} );
269              
270 352         988 return $return;
271             }
272 6         69 );
273              
274             # start by mocking 'stat' and 'lstat' call
275 6         54 mock_stat($sub_for_stat);
276              
277 6         38 return 1;
278             }
279              
280             sub _check_from_stat {
281 352     352   667 my ( $check, $f_or_fh, $sub_for_stat ) = @_;
282              
283 352         680 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 352         484 my $can_use_stat;
291 352 100       2165 $can_use_stat = 1 if $check =~ qr{^[sfdMXxzACORWeorw]$};
292              
293 352 100       1082 my $stat_or_lstat = $can_use_stat ? 'stat' : 'lstat';
294              
295 352         879 my (@mocked_lstat_result) = $sub_for_stat->( $stat_or_lstat, $f_or_fh );
296 352 100 66     5426 if ( scalar @mocked_lstat_result == 1
      66        
297             && !ref $mocked_lstat_result[0]
298             && $mocked_lstat_result[0] == FALLBACK_TO_REAL_OP ) {
299 1         6 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 351     351   688 return @mocked_lstat_result;
305 351         1976 };
306              
307             # now performing a real stat call [ using the mocked stat function ]
308 351         649 my ( @stat, @lstat );
309              
310 351 100       638 if ($can_use_stat) {
311 47     47   439 no warnings; # throw warnings with Perl <= 5.14
  47         103  
  47         3820  
312 312 50       1227 @stat = stat($f_or_fh) if defined $f_or_fh;
313             }
314             else {
315 47     47   343 no warnings;
  47         107  
  47         97680  
316 39 50       148 @lstat = lstat($f_or_fh) if defined $f_or_fh;
317             }
318              
319 351 100       2315 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         55 _xs_unmock_op($optype);
327 19         166 return _to_bool( scalar -r _ );
328             }
329             elsif ( $check eq 'w' ) {
330              
331             # -w File is writable by effective uid/gid.
332 19         57 _xs_unmock_op($optype);
333 19         157 return _to_bool( scalar -w _ );
334             }
335             elsif ( $check eq 'x' ) {
336              
337             # -x File is executable by effective uid/gid.
338 13         42 _xs_unmock_op($optype);
339 13         115 return _to_bool( scalar -x _ );
340             }
341             elsif ( $check eq 'o' ) {
342              
343             # -o File is owned by effective uid.
344 19         56 _xs_unmock_op($optype);
345 19         175 return _to_bool( scalar -o _ );
346             }
347             elsif ( $check eq 'R' ) {
348              
349             # -R File is readable by real uid/gid.
350 19         55 _xs_unmock_op($optype);
351 19         163 return _to_bool( scalar -R _ );
352             }
353             elsif ( $check eq 'W' ) {
354              
355             # -W File is writable by real uid/gid.
356 19         59 _xs_unmock_op($optype);
357 19         206 return _to_bool( scalar -W _ );
358             }
359             elsif ( $check eq 'X' ) {
360              
361             # -X File is executable by real uid/gid.
362              
363 12         38 _xs_unmock_op($optype);
364 12         111 return _to_bool( scalar -X _ );
365             }
366             elsif ( $check eq 'O' ) {
367              
368             # -O File is owned by real uid.
369 19         58 _xs_unmock_op($optype);
370 19         176 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     137 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         21 _xs_unmock_op($optype);
386 7         21 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         91 return $stat[ST_SIZE];
396             }
397             elsif ( $check eq 'f' ) {
398              
399             # -f File is a plain file.
400 20         62 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         98 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         57 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         27 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         10 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         5 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         6 _xs_unmock_op($optype);
458 1         5 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         16 _xs_unmock_op($optype);
467 5         137 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       40 return CHECK_IS_TRUE if -d $f_or_fh;
474              
475             # ... we cannot really know...
476             # ... this is an heuristic guess...
477              
478 4         13 _xs_unmock_op($optype);
479 4         169 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 19 50 33     84 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_MTIME];
486 19         142 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 19 50 33     91 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_ATIME];
496              
497 19         188 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 19 50 33     78 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_CTIME];
505              
506 19         138 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   723 my ($s) = @_;
519              
520 265 100       1863 return ( $s ? CHECK_IS_TRUE : CHECK_IS_FALSE );
521             }
522              
523             sub _check_mode_type {
524 75     75   147 my ( $mode, $type ) = @_;
525              
526 75 100       179 return CHECK_IS_FALSE unless defined $mode;
527 74         180 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 8575 my ($sub) = @_;
533              
534 8 50       47 die q[First arg must be a CODE ref] unless ref $sub eq 'CODE';
535              
536 8         30 foreach my $opname (qw{stat lstat}) {
537 16         40 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   67 my $file_or_handle = shift;
543 34         233 return $sub->( $opname, $file_or_handle );
544 16         99 };
545              
546             # setup the mock for the OP
547 16         53 _xs_mock_op($optype);
548             }
549              
550 8         28 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 4089 if ( !scalar %REVERSE_MAP ) {
561 7         84 foreach my $k ( keys %MAP_FC_OP ) {
562 203         485 $REVERSE_MAP{ $MAP_FC_OP{$k} } = $k;
563             }
564             }
565              
566 9         66 my @mocks = sort map { $REVERSE_MAP{$_} } keys %$_current_mocks;
  93         221  
567 9 50       46 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 1259     1259   632441 my ( $optype, $file, @others ) = @_;
579              
580 1259 50       3355 die if scalar @others; # need to move this in a unit test
581              
582             # we have no custom mock at this point
583 1259 50       3336 return FALLBACK_TO_REAL_OP unless defined $_current_mocks->{$optype};
584              
585 1259 50 66     3021 $file = $_last_call_for if !defined $file && defined $_last_call_for && !defined $_current_mocks->{ $MAP_FC_OP{'stat'} };
      66        
586 1259         2990 my ( $out, @extra ) = $_current_mocks->{$optype}->($file);
587 1259         263734 $_last_call_for = $file;
588              
589             # FIXME return undef when not defined out
590              
591 1259 100 66     5224 if ( defined $out && $OP_CAN_RETURN_INT{$optype} ) {
592 141         1941 return $out; # limitation to int for now in fact some returns NVs
593             }
594              
595 1118 100       2310 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       1142 if ( !int($!) ) {
601 23   66     243 $! = $DEFAULT_ERRNO{ $REVERSE_MAP{$optype} || 'default' } || $DEFAULT_ERRNO{'default'};
602             }
603              
604             #return undef unless defined $out;
605 195         2370 return CHECK_IS_FALSE;
606             }
607              
608 923 100 100     6616 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 786 100       1660 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 382   33     850 my $stat = $out // $others[0]; # can be array or hash at this point
630 382         977 my $stat_is_a = ref $stat;
631 382 100       699 die q[Your mocked function for stat should return a stat array or hash] unless $stat_is_a;
632              
633 380         501 my $stat_as_arrayref;
634              
635             # can handle one ARRAY or a HASH
636 380         565 my $stat_t_max = STAT_T_MAX;
637 380 100       754 if ( $stat_is_a eq 'ARRAY' ) {
    50          
638 374         492 $stat_as_arrayref = $stat;
639 374         550 my $av_size = scalar @$stat;
640 374 100 100     1345 if (
641             $av_size # 0 is valid when the file is missing
642             && $av_size != $stat_t_max
643             ) {
644 4         64 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         26 foreach my $k ( keys %$stat ) {
650 8         26 my $ix = $MAP_STAT_T_IX{ lc($k) };
651 8 100       54 die qq[Unknown index for stat_t struct key $k] unless defined $ix;
652 6         15 $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 374         2438 return ( CHECK_IS_TRUE, $stat_as_arrayref );
660             }
661              
662 404         4113 return CHECK_IS_TRUE;
663             }
664              
665             # accessors for testing purpose mainly
666             sub _get_filecheck_ops_map {
667 42     42   101439 return {%MAP_FC_OP}; # return a copy
668             }
669              
670             ######################################################
671             ### stat helpers
672             ######################################################
673              
674             sub stat_as_directory {
675 2     2 1 99 my (%opts) = @_;
676              
677 2         7 return _stat_for( S_IFDIR, \%opts );
678             }
679              
680             sub stat_as_file {
681 18     18 1 11440 my (%opts) = @_;
682              
683 18         55 return _stat_for( S_IFREG, \%opts );
684             }
685              
686             sub stat_as_symlink {
687 2     2 1 6 my (%opts) = @_;
688              
689 2         8 return _stat_for( S_IFLNK, \%opts );
690             }
691              
692             sub stat_as_socket {
693 2     2 1 6 my (%opts) = @_;
694              
695 2         7 return _stat_for( S_IFSOCK, \%opts );
696             }
697              
698             sub stat_as_chr {
699 1     1 1 5 my (%opts) = @_;
700              
701 1         4 return _stat_for( S_IFCHR, \%opts );
702             }
703              
704             sub stat_as_block {
705 1     1 1 3 my (%opts) = @_;
706              
707 1         5 return _stat_for( S_IFBLK, \%opts );
708             }
709              
710             sub _stat_for {
711 26     26   52 my ( $type, $opts ) = @_;
712              
713 26         84 my @stat = ( (0) x 13 ); # STAT_T_MAX
714              
715             # set file type
716 26 50       66 if ( defined $type ) {
717              
718             # _S_IFMT is used as a protection to do not flip outside the mask
719 26         105 $stat[ST_MODE] |= ( $type & _S_IFMT );
720             }
721              
722             # set permission using octal
723 26 100       65 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       66 if ( defined $opts->{uid} ) {
731 2 100       23 if ( $opts->{uid} =~ qr{^[0-9]+$} ) {
732 1         2 $stat[ST_UID] = $opts->{uid};
733             }
734             else {
735              
736 1         74 $stat[ST_UID] = getpwnam( $opts->{uid} );
737             }
738             }
739              
740 26 100       68 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         54 $stat[ST_GID] = getgrnam( $opts->{gid} );
746             }
747             }
748              
749             # options that we can simply copy to a slot
750 26         114 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         81 foreach my $k ( keys %$opts ) {
760 20         47 $k = lc($k);
761 20         43 $k =~ s{^st_}{};
762 20 100       55 next unless defined $name2ix{$k};
763              
764 14         32 $stat[ $name2ix{$k} ] = $opts->{$k};
765             }
766              
767 26         169 return \@stat;
768             }
769              
770             1;
771              
772             __END__