File Coverage

blib/lib/Overload/FileCheck.pm
Criterion Covered Total %
statement 270 273 98.9
branch 102 116 87.9
condition 36 51 70.5
subroutine 66 67 98.5
pod 14 15 93.3
total 488 522 93.4


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 64     64   11242794 use strict;
  64         117  
  64         2042  
11 64     64   304 use warnings;
  64         119  
  64         2696  
12              
13             # ABSTRACT: override/mock perl file check -X: -e, -f, -d, ...
14              
15 64     64   23973 use Errno ();
  64         80512  
  64         1413  
16 64     64   361 use Carp ();
  64         119  
  64         1391  
17              
18 64     64   227 use base 'Exporter';
  64         82  
  64         7881  
19              
20             BEGIN {
21              
22 64     64   219 our $VERSION = '0.015'; # VERSION: generated by DZP::OurPkgVersion
23              
24 64         236 require XSLoader;
25 64         58370 XSLoader::load(__PACKAGE__);
26             }
27              
28             use Fcntl (
29 64         76029 '_S_IFMT', # bit mask for the file type bit field
30             #'S_IFPERMS', # bit mask for file perms.
31             'S_IFSOCK', # socket
32             'S_IFLNK', # symbolic link
33             'S_IFREG', # regular file
34             'S_IFBLK', # block device
35             'S_IFDIR', # directory
36             'S_IFCHR', # character device
37             'S_IFIFO', # FIFO
38              
39             # qw{S_IRUSR S_IWUSR S_IXUSR S_IRWXU}
40 64     64   441 );
  64         186  
41              
42             my @STAT_T_IX = qw{
43             ST_DEV
44             ST_INO
45             ST_MODE
46             ST_NLINK
47             ST_UID
48             ST_GID
49             ST_RDEV
50             ST_SIZE
51             ST_ATIME
52             ST_MTIME
53             ST_CTIME
54             ST_BLKSIZE
55             ST_BLOCKS
56             };
57              
58             my @CHECK_STATUS = qw{CHECK_IS_FALSE CHECK_IS_TRUE CHECK_IS_NULL FALLBACK_TO_REAL_OP};
59              
60             my @STAT_HELPERS = qw{ stat_as_directory stat_as_file stat_as_symlink
61             stat_as_socket stat_as_chr stat_as_block stat_as_fifo};
62              
63             our @EXPORT_OK = (
64             qw{
65             mock_all_from_stat
66             mock_all_file_checks mock_file_check mock_file_check_guard mock_stat
67             unmock_file_check unmock_all_file_checks unmock_stat
68             },
69             @CHECK_STATUS,
70             @STAT_T_IX,
71             @STAT_HELPERS,
72             );
73              
74             our %EXPORT_TAGS = (
75             all => [@EXPORT_OK],
76              
77             # status code
78             check => [@CHECK_STATUS],
79              
80             # STAT array indexes
81             stat => [ @STAT_T_IX, @STAT_HELPERS ],
82             );
83              
84             # hash for every filecheck we can mock
85             # and their corresonding OP_TYPE
86             my %MAP_FC_OP = (
87             'R' => OP_FTRREAD,
88             'W' => OP_FTRWRITE,
89             'X' => OP_FTREXEC,
90             'r' => OP_FTEREAD,
91             'w' => OP_FTEWRITE,
92             'x' => OP_FTEEXEC,
93              
94             'e' => OP_FTIS,
95             's' => OP_FTSIZE, # OP_CAN_RETURN_INT
96             'M' => OP_FTMTIME, # OP_CAN_RETURN_INT
97             'C' => OP_FTCTIME, # OP_CAN_RETURN_INT
98             'A' => OP_FTATIME, # OP_CAN_RETURN_INT
99              
100             'O' => OP_FTROWNED,
101             'o' => OP_FTEOWNED,
102             'z' => OP_FTZERO,
103             'S' => OP_FTSOCK,
104             'c' => OP_FTCHR,
105             'b' => OP_FTBLK,
106             'f' => OP_FTFILE,
107             'd' => OP_FTDIR,
108             'p' => OP_FTPIPE,
109             'u' => OP_FTSUID,
110             'g' => OP_FTSGID,
111             'k' => OP_FTSVTX,
112              
113             'l' => OP_FTLINK,
114              
115             't' => OP_FTTTY,
116              
117             'T' => OP_FTTEXT,
118             'B' => OP_FTBINARY,
119              
120             # special cases for stat & lstat
121             'stat' => OP_STAT,
122             'lstat' => OP_LSTAT,
123              
124             );
125              
126             my %MAP_STAT_T_IX = (
127             st_dev => ST_DEV,
128             st_ino => ST_INO,
129             st_mode => ST_MODE,
130             st_nlink => ST_NLINK,
131             st_uid => ST_UID,
132             st_gid => ST_GID,
133             st_rdev => ST_RDEV,
134             st_size => ST_SIZE,
135             st_atime => ST_ATIME,
136             st_mtime => ST_MTIME,
137             st_ctime => ST_CTIME,
138             st_blksize => ST_BLKSIZE,
139             st_blocks => ST_BLOCKS,
140             );
141              
142             # op_type_id => check
143             my %REVERSE_MAP = reverse %MAP_FC_OP;
144              
145             my %OP_CAN_RETURN_INT = map { $MAP_FC_OP{$_} => 1 } qw{ s };
146             my %OP_RETURNS_NV = map { $MAP_FC_OP{$_} => 1 } qw{ M C A };
147             my %OP_IS_STAT_OR_LSTAT = map { $MAP_FC_OP{$_} => 1 } qw{ stat lstat };
148             #
149             # This is listing the default ERRNO codes
150             # used by each test when the test fails and
151             # the user did not provide one ERRNO error
152             #
153             my %DEFAULT_ERRNO = (
154             'default' => Errno::ENOENT, # default value for any other not listed
155             'x' => Errno::ENOEXEC,
156             'X' => Errno::ENOEXEC,
157              
158             # ...
159             );
160              
161             # this is saving our custom ops
162             # optype_id => sub
163             my $_current_mocks = {};
164              
165             sub import {
166 65     65   1031 my ( $class, @args ) = @_;
167              
168             # mock on import...
169 65         128 my $_next_check;
170             my @for_exporter;
171 65         228 foreach my $check (@args) {
172 89 100 100     1018 if ( !$_next_check && $check !~ qr{^-} ) {
173              
174             # this is a valid arg for exporter
175 79         165 push @for_exporter, $check;
176 79         263 next;
177             }
178 10 100       31 if ( !$_next_check ) {
179              
180             # we found a key like '-e' in '-e => sub {} '
181 6         11 $_next_check = $check;
182             }
183             else {
184             # now this is the value
185 4         6 my $code = $check;
186              
187             # use Overload::FileCheck -from_stat => \&my_stat;
188 4 100 66     19 if ( $_next_check eq q{-from_stat} || $_next_check eq q{-from-stat} ) {
189 2         42 mock_all_from_stat($code);
190             }
191             else {
192 2         5 mock_file_check( $_next_check, $code );
193             }
194              
195 4         15 undef $_next_check;
196             }
197             }
198              
199 65 100       169 if ( defined $_next_check ) {
200 2         292 Carp::croak(qq[Missing CODE ref for mock '$_next_check' in import list]);
201             }
202              
203             # callback the exporter logic
204 63         93099 return __PACKAGE__->export_to_level( 1, $class, @for_exporter );
205             }
206              
207             sub mock_all_file_checks {
208 29     29 0 244899 my ($sub) = @_;
209              
210 29         439 foreach my $check ( sort keys %MAP_FC_OP ) {
211 841 100       2791 next if $check =~ qr{^l?stat$}; # we should not mock stat
212             mock_file_check(
213             $check,
214             sub {
215 526     526   1136 my (@args) = @_;
216 526         1263 return $sub->( $check, @args );
217             }
218 783         2166 );
219             }
220              
221 29         63 return 1;
222             }
223              
224             sub _resolve_check {
225 1634     1634   2245 my ($check) = @_;
226 1634 50       2465 Carp::croak(q[Check is not defined]) unless defined $check;
227 1634         2289 $check =~ s{^-+}{}; # strip any extra dashes
228 1634 50       2720 Carp::croak(qq[Unknown check '$check']) unless defined $MAP_FC_OP{$check};
229 1634         3052 return ( $check, $MAP_FC_OP{$check} );
230             }
231              
232             sub mock_file_check {
233 869     869 1 9913644 my ( $check, $sub ) = @_;
234              
235 869 50       1624 Carp::croak(q[Second arg must be a CODE ref]) unless ref $sub eq 'CODE';
236              
237 869         1229 my ( $name, $optype ) = _resolve_check($check);
238 869 100       1786 Carp::croak(qq[-$name is already mocked by Overload::FileCheck]) if exists $_current_mocks->{$optype};
239              
240 868         1341 $_current_mocks->{$optype} = $sub;
241              
242 868         1666 _xs_mock_op($optype);
243              
244 868         1458 return 1;
245             }
246              
247             sub mock_file_check_guard {
248 6     6 1 245912 my ( $check, $sub ) = @_;
249              
250 6         11 mock_file_check( $check, $sub );
251              
252 6         33 ( my $normalized = $check ) =~ s{^-+}{};
253              
254 6         23 return Overload::FileCheck::Guard->new($normalized);
255             }
256              
257             sub unmock_file_check {
258 111     111 1 14572 my (@checks) = @_;
259              
260 111         269 foreach my $check (@checks) {
261 765         1043 my ( undef, $optype ) = _resolve_check($check);
262              
263 765         1972 delete $_current_mocks->{$optype};
264              
265 765         1328 _xs_unmock_op($optype);
266             }
267              
268 111         383 return 1;
269             }
270              
271             sub mock_all_from_stat {
272 27     27 1 3352119 my ($sub_for_stat) = @_;
273              
274             # then mock all -X checks to our custom
275             mock_all_file_checks(
276             sub {
277 360     360   843 my ( $check, $f_or_fh ) = @_;
278              
279             # the main call
280 360         966 my $return = _check_from_stat( $check, $f_or_fh, $sub_for_stat );
281              
282             # auto remock the OP (it could have been temporary unmocked to use -X _)
283 360         1073 _xs_mock_op( $MAP_FC_OP{$check} );
284              
285 360         891 return $return;
286             }
287 27         179 );
288              
289             # start by mocking 'stat' and 'lstat' call
290 27         100 mock_stat($sub_for_stat);
291              
292 27         72 return 1;
293             }
294              
295             sub _check_from_stat {
296 360     360   721 my ( $check, $f_or_fh, $sub_for_stat ) = @_;
297              
298 360         805 my $optype = $MAP_FC_OP{$check};
299              
300             # stat would need to be called twice
301             # 1/ we first need to check if we are mocking the file
302             # or if we let it fallback to the Perl OP
303             # 2/ doing a second stat call in order to cache _
304              
305             # In Perl, only -l uses lstat (does not follow symlinks).
306             # All other file test ops use stat (follows symlinks).
307 360         707 my $use_lstat = ( $check eq 'l' );
308              
309 360 100       881 my $stat_or_lstat = $use_lstat ? 'lstat' : 'stat';
310              
311 360         1028 my (@mocked_lstat_result) = $sub_for_stat->( $stat_or_lstat, $f_or_fh );
312 360 100 66     8369 if ( scalar @mocked_lstat_result == 1
      100        
313             && !ref $mocked_lstat_result[0]
314             && $mocked_lstat_result[0] == FALLBACK_TO_REAL_OP ) {
315 7         20 return FALLBACK_TO_REAL_OP;
316             }
317              
318             # avoid a second callback to the user hook (do not really happen for now)
319             local $_current_mocks->{ $MAP_FC_OP{$stat_or_lstat} } = sub {
320 353     353   795 return @mocked_lstat_result;
321 353         2253 };
322              
323             # now performing a real stat call [ using the mocked stat function ]
324 353         618 my ( @stat, @lstat );
325              
326 353 100       728 if ($use_lstat) {
327 64     64   12017 no warnings;
  64         142  
  64         4316  
328 14 50       58 @lstat = lstat($f_or_fh) if defined $f_or_fh;
329             }
330             else {
331 64     64   322 no warnings; # throw warnings with Perl <= 5.14
  64         113  
  64         151756  
332 339 50       1669 @stat = stat($f_or_fh) if defined $f_or_fh;
333             }
334              
335             # Dispatch table mapping each check letter to its handler.
336             # Closures capture @stat, @lstat, $optype, and $f_or_fh from the enclosing scope.
337             my %dispatch = (
338              
339             # Unmock then delegate to _ (effective uid/gid checks)
340 19     19   54 r => sub { _xs_unmock_op($optype); _to_bool( scalar -r _ ) }, # readable by effective uid/gid
  19         120  
341 19     19   51 w => sub { _xs_unmock_op($optype); _to_bool( scalar -w _ ) }, # writable by effective uid/gid
  19         121  
342 13     13   36 x => sub { _xs_unmock_op($optype); _to_bool( scalar -x _ ) }, # executable by effective uid/gid
  13         84  
343 19     19   52 o => sub { _xs_unmock_op($optype); _to_bool( scalar -o _ ) }, # owned by effective uid
  19         147  
344              
345             # Unmock then delegate to _ (real uid/gid checks)
346 19     19   47 R => sub { _xs_unmock_op($optype); _to_bool( scalar -R _ ) }, # readable by real uid/gid
  19         119  
347 19     19   52 W => sub { _xs_unmock_op($optype); _to_bool( scalar -W _ ) }, # writable by real uid/gid
  19         114  
348 12     12   39 X => sub { _xs_unmock_op($optype); _to_bool( scalar -X _ ) }, # executable by real uid/gid
  12         78  
349 19     19   54 O => sub { _xs_unmock_op($optype); _to_bool( scalar -O _ ) }, # owned by real uid
  19         112  
350              
351             # Unmock then delegate to _ (other permission/attribute checks)
352 9     9   33 z => sub { _xs_unmock_op($optype); _to_bool( scalar -z _ ) }, # zero size
  9         23  
353 0     0   0 t => sub { _xs_unmock_op($optype); _to_bool( scalar -t _ ) }, # filehandle is a tty
  0         0  
354 1     1   3 u => sub { _xs_unmock_op($optype); _to_bool( scalar -u _ ) }, # setuid bit
  1         9  
355 2     2   5 g => sub { _xs_unmock_op($optype); _to_bool( scalar -g _ ) }, # setgid bit
  2         8  
356 3     3   11 k => sub { _xs_unmock_op($optype); _to_bool( scalar -k _ ) }, # sticky bit
  3         7  
357              
358             # Heuristic text/binary checks (use glob _ to pass the cached stat)
359 5 50   5   10 T => sub { return CHECK_IS_NULL unless @stat; _xs_unmock_op($optype); _to_bool( scalar -T *_ ) }, # ASCII or UTF-8 text (heuristic)
  5         12  
  5         162  
360             B => sub { # binary file (opposite of -T)
361 17 100   17   57 return CHECK_IS_NULL unless @stat; # file not found
362             # Check directory via mode bits directly instead of calling the
363             # mocked -d operator, which would trigger a redundant stat callback.
364 16 100       44 return CHECK_IS_TRUE if _check_mode_type( $stat[ST_MODE], S_IFDIR ) == CHECK_IS_TRUE;
365 5         14 _xs_unmock_op($optype);
366 5         163 return _to_bool( scalar -B *_ );
367             },
368              
369             # Existence and size (computed directly from cached stat)
370 45 100   45   201 e => sub { return CHECK_IS_NULL unless scalar @stat; CHECK_IS_TRUE }, # file exists (stat success implies existence)
  42         1081  
371 19     19   490 s => sub { $stat[ST_SIZE] }, # nonzero size (returns bytes); fallback breaks on symlinks
372              
373             # File type checks via mode bits (using @stat — follows symlinks)
374 24     24   69 f => sub { _check_mode_type( $stat[ST_MODE], S_IFREG ) }, # plain file
375 23     23   52 d => sub { _check_mode_type( $stat[ST_MODE], S_IFDIR ) }, # directory
376              
377             # Symlink check — only op that uses lstat (does not follow symlinks)
378 14     14   35 l => sub { _check_mode_type( $lstat[ST_MODE], S_IFLNK ) }, # symbolic link
379              
380             # File type checks via mode bits (using @stat — follows symlinks like Perl)
381 4     4   11 p => sub { _check_mode_type( $stat[ST_MODE], S_IFIFO ) }, # named pipe (FIFO)
382 8     8   18 S => sub { _check_mode_type( $stat[ST_MODE], S_IFSOCK ) }, # socket
383 4     4   12 b => sub { _check_mode_type( $stat[ST_MODE], S_IFBLK ) }, # block special file
384 4     4   8 c => sub { _check_mode_type( $stat[ST_MODE], S_IFCHR ) }, # character special file
385              
386             # Age calculations: (basetime - timestamp) / seconds_per_day
387             # Returns scalar ref to distinguish real NV values from the
388             # FALLBACK_TO_REAL_OP sentinel (-1) — see _check() NV handling.
389             M => sub {
390 13 100 66 13   112 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_MTIME];
391 12         433 \( ( get_basetime() - $stat[ST_MTIME] ) / 86400.0 ); # days since modification
392             },
393             A => sub {
394 10 100 66 10   64 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_ATIME];
395 9         286 \( ( get_basetime() - $stat[ST_ATIME] ) / 86400.0 ); # days since access
396             },
397             C => sub {
398 9 100 66 9   66 return CHECK_IS_NULL unless scalar @stat && defined $stat[ST_CTIME];
399 8         279 \( ( get_basetime() - $stat[ST_CTIME] ) / 86400.0 ); # days since inode change
400             },
401 353         12707 );
402              
403 353 50       1118 my $handler = $dispatch{$check}
404             or Carp::croak(qq[Unknown check '$check']);
405 353         641 return $handler->();
406             }
407              
408             sub _to_bool {
409 257     257   458 my ($s) = @_;
410              
411 257 100       7092 return ( $s ? CHECK_IS_TRUE : CHECK_IS_FALSE );
412             }
413              
414             sub _check_mode_type {
415 97     97   174 my ( $mode, $type ) = @_;
416              
417 97 100       316 return CHECK_IS_NULL unless defined $mode;
418 93         205 return _to_bool( ( $mode & _S_IFMT ) == $type );
419             }
420              
421             # this is a special case used to mock OP_STAT & OP_LSTAT
422             sub mock_stat {
423 30     30 1 616088 my ($sub) = @_;
424              
425 30 50       95 Carp::croak(q[First arg must be a CODE ref]) unless ref $sub eq 'CODE';
426              
427 30         62 foreach my $opname (qw{stat lstat}) {
428 60         118 my $optype = $MAP_FC_OP{$opname};
429 60 50       116 Carp::croak(qq[No optype found for $opname]) unless $optype;
430              
431             # plug the sub
432             $_current_mocks->{$optype} = sub {
433 44     44   72 my $file_or_handle = shift;
434 44         199 return $sub->( $opname, $file_or_handle );
435 60         212 };
436              
437             # setup the mock for the OP
438 60         168 _xs_mock_op($optype);
439             }
440              
441 30         68 return 1;
442             }
443              
444             # just an alias to unmock stat & lstat at the same time
445             sub unmock_stat {
446 6     6 1 54 return unmock_file_check(qw{stat lstat});
447             }
448              
449             sub unmock_all_file_checks {
450              
451 45     45 1 27476 my @mocks = sort map { $REVERSE_MAP{$_} } keys %$_current_mocks;
  692         1347  
452 45 50       203 return 1 unless scalar @mocks;
453              
454 45         152 return unmock_file_check(@mocks);
455             }
456              
457             # should not be called directly
458             # this is called from XS to check if one OP is mocked
459             # and trigger the callback function when mocked
460             my $_last_call_for;
461              
462             sub _check {
463 1528     1528   1414712 my ( $optype, $file ) = @_;
464              
465             # we have no custom mock at this point
466 1528 50       4940 return FALLBACK_TO_REAL_OP unless defined $_current_mocks->{$optype};
467              
468             # Fall back to the last filename when the current file is undef
469             # and stat is not independently mocked (stacked -X _ scenario).
470 1528 50 66     3795 if ( !defined $file && defined $_last_call_for
      66        
471             && !defined $_current_mocks->{ $MAP_FC_OP{'stat'} } ) {
472 3         5 $file = $_last_call_for;
473             }
474              
475             # Save $_last_call_for before callback dispatch so that re-entrant
476             # calls (e.g. mock_all_from_stat callbacks invoking mocked file tests)
477             # cannot corrupt the outer call's filename context. See GH #68.
478 1528         2256 my $saved_last_call_for = $_last_call_for;
479 1528         3649 my ( $out, @extra ) = $_current_mocks->{$optype}->($file);
480             # Cache string filenames for stacked -X _ ops. When the file is a
481             # reference (filehandle), restore the pre-callback value instead of
482             # clobbering with undef — an inner re-entrant call may have set a
483             # valid filename that a subsequent stacked op needs. See GH #179.
484 1528 100       262547 $_last_call_for = ref($file) ? $saved_last_call_for : $file;
485              
486 1528 100       3194 if ( !defined $out ) {
487             # CHECK_IS_NULL: callback returned undef — propagate as undef
488             # so the OP returns undef to the caller (file not found / unknown)
489 47 100       200 if ( !int($!) ) {
490 2   33     15 $! = $DEFAULT_ERRNO{ $REVERSE_MAP{$optype} || 'default' } || $DEFAULT_ERRNO{'default'};
491             }
492 47         258 return CHECK_IS_NULL;
493             }
494              
495 1481 100       3357 if ( $OP_CAN_RETURN_INT{$optype} ) {
496              
497             # Auto-set errno for falsy returns from int ops (e.g. -s returning 0)
498             # consistent with the boolean op path below. GH #62.
499 44 100 100     146 if ( !$out && !int($!) ) {
500 2   33     25 $! = $DEFAULT_ERRNO{ $REVERSE_MAP{$optype} || 'default' } || $DEFAULT_ERRNO{'default'};
501             }
502 44         637 return $out;
503             }
504              
505             # NV ops (-M, -C, -A) return a (status, value) pair to avoid the -1
506             # sentinel collision: FALLBACK_TO_REAL_OP is -1, but -1.0 is a valid
507             # NV result (file modified exactly 1 day in the future). The XS
508             # handler uses the status code instead of checking value == -1.
509 1437 100       2626 if ( $OP_RETURNS_NV{$optype} ) {
510             # Scalar ref: from _check_from_stat — always a real value, never FALLBACK
511 94 100       208 if ( ref $out eq 'SCALAR' ) {
512 29         268 return ( CHECK_IS_TRUE, $$out );
513             }
514             # Bare scalar: from direct mock_file_check callbacks
515 65 100 66     261 if ( !ref $out && $out == FALLBACK_TO_REAL_OP ) {
516 26         913 return (FALLBACK_TO_REAL_OP);
517             }
518 39         468 return ( CHECK_IS_TRUE, $out );
519             }
520              
521 1343 100       2598 if ( !$out ) {
522              
523             # Set a default ERRNO when the user didn't provide one,
524             # so tests never pass with $! left at zero.
525 272 100       1311 if ( !int($!) ) {
526 31   66     367 $! = $DEFAULT_ERRNO{ $REVERSE_MAP{$optype} || 'default' } || $DEFAULT_ERRNO{'default'};
527             }
528              
529 272         2864 return CHECK_IS_FALSE;
530             }
531              
532 1071 100 100     10088 return FALLBACK_TO_REAL_OP if !ref $out && $out == FALLBACK_TO_REAL_OP;
533              
534             # stat/lstat return a stat array in addition to the status code
535 871 100       1745 if ( $OP_IS_STAT_OR_LSTAT{$optype} ) {
536 385         817 my $stat_as_arrayref = _normalize_stat_result( $out, @extra );
537 377         2603 return ( CHECK_IS_TRUE, $stat_as_arrayref );
538             }
539              
540 486         4694 return CHECK_IS_TRUE;
541             }
542              
543             # Normalize a mock stat return value (array or hash) into a 13-element arrayref.
544             sub _normalize_stat_result {
545 385     385   666 my ( $out, @extra ) = @_;
546              
547 385   33     872 my $stat = $out // $extra[0];
548 385         753 my $stat_is_a = ref $stat;
549 385 100       1034 Carp::croak(q[Your mocked function for stat should return a stat array or hash]) unless $stat_is_a;
550              
551 383         605 my $stat_t_max = STAT_T_MAX;
552              
553 383 100       788 if ( $stat_is_a eq 'ARRAY' ) {
554 371         675 my $av_size = scalar @$stat;
555 371 100 100     1297 if ( $av_size && $av_size != $stat_t_max ) {
556 4         711 Carp::croak(qq[Stat array should contain exactly 0 or $stat_t_max values]);
557             }
558 367         862 return $stat;
559             }
560              
561 12 50       27 if ( $stat_is_a eq 'HASH' ) {
562 12         30 my $stat_as_arrayref = [ (0) x $stat_t_max ];
563 12         32 foreach my $k ( keys %$stat ) {
564 21         62 ( my $normalized = lc($k) ) =~ s{^st_}{};
565 21         49 my $ix = $MAP_STAT_T_IX{"st_$normalized"};
566 21 100       385 Carp::croak(qq[Unknown index for stat_t struct key $k]) unless defined $ix;
567 19         47 $stat_as_arrayref->[$ix] = $stat->{$k};
568             }
569 10         22 return $stat_as_arrayref;
570             }
571              
572 0         0 Carp::croak(q[Your mocked function for stat should return a stat array or hash]);
573             }
574              
575             # accessors for testing purpose mainly
576             sub _get_filecheck_ops_map {
577 42     42   343896 return {%MAP_FC_OP}; # return a copy
578             }
579              
580             ######################################################
581             ### stat helpers
582             ######################################################
583              
584             sub stat_as_directory {
585 5     5 1 267544 my (%opts) = @_;
586              
587 5         35 return _stat_for( S_IFDIR, \%opts );
588             }
589              
590             sub stat_as_file {
591 55     55 1 556650 my (%opts) = @_;
592              
593 55         170 return _stat_for( S_IFREG, \%opts );
594             }
595              
596             sub stat_as_symlink {
597 3     3 1 15 my (%opts) = @_;
598              
599 3         12 return _stat_for( S_IFLNK, \%opts );
600             }
601              
602             sub stat_as_socket {
603 8     8 1 42 my (%opts) = @_;
604              
605 8         19 return _stat_for( S_IFSOCK, \%opts );
606             }
607              
608             sub stat_as_chr {
609 2     2 1 10 my (%opts) = @_;
610              
611 2         7 return _stat_for( S_IFCHR, \%opts );
612             }
613              
614             sub stat_as_block {
615 2     2 1 11 my (%opts) = @_;
616              
617 2         8 return _stat_for( S_IFBLK, \%opts );
618             }
619              
620             sub stat_as_fifo {
621 2     2 1 5 my (%opts) = @_;
622              
623 2         6 return _stat_for( S_IFIFO, \%opts );
624             }
625              
626             sub _stat_for {
627 77     77   134 my ( $type, $opts ) = @_;
628              
629 77         207 my @stat = ( (0) x STAT_T_MAX );
630              
631             # set file type
632 77 50       186 if ( defined $type ) {
633              
634             # _S_IFMT is used as a protection to do not flip outside the mask
635 77         150 $stat[ST_MODE] |= ( $type & _S_IFMT );
636             }
637              
638             # set permission using octal
639 77 100       209 if ( defined $opts->{perms} ) {
640              
641             # _S_IFMT is used as a protection to do not flip outside the mask
642 8         17 $stat[ST_MODE] |= ( $opts->{perms} & ~_S_IFMT );
643             }
644              
645             # deal with UID / GID
646 77 100       189 if ( defined $opts->{uid} ) {
647 4 100       53 if ( $opts->{uid} =~ qr{^[0-9]+$} ) {
648 2         6 $stat[ST_UID] = $opts->{uid};
649             }
650             else {
651 2         291 my $uid = getpwnam( $opts->{uid} );
652 2 100       200 Carp::croak("Unknown user '$opts->{uid}' passed to uid option") unless defined $uid;
653 1         2 $stat[ST_UID] = $uid;
654             }
655             }
656              
657 76 100       147 if ( defined $opts->{gid} ) {
658 4 100       21 if ( $opts->{gid} =~ qr{^[0-9]+$} ) {
659 2         3 $stat[ST_GID] = $opts->{gid};
660             }
661             else {
662 2         145 my $gid = getgrnam( $opts->{gid} );
663 2 100       125 Carp::croak("Unknown group '$opts->{gid}' passed to gid option") unless defined $gid;
664 1         2 $stat[ST_GID] = $gid;
665             }
666             }
667              
668             # options that we can simply copy to a slot
669 75         467 my %name2ix = (
670             dev => ST_DEV,
671             ino => ST_INO,
672             nlink => ST_NLINK,
673             rdev => ST_RDEV,
674             size => ST_SIZE,
675             atime => ST_ATIME,
676             mtime => ST_MTIME,
677             ctime => ST_CTIME,
678             blksize => ST_BLKSIZE,
679             blocks => ST_BLOCKS,
680             );
681              
682             # all valid option names (after normalization: lc + strip st_ prefix)
683 75         229 my %known_opts = ( perms => 1, uid => 1, gid => 1, map { $_ => 1 } keys %name2ix );
  750         1172  
684              
685 75         269 foreach my $orig_key ( keys %$opts ) {
686 58         115 my $k = lc($orig_key);
687 58         101 $k =~ s{^st_}{};
688              
689 58 100       120 unless ( $known_opts{$k} ) {
690 3 100       6 if ( $k eq 'mode' ) {
691 1         94 Carp::croak("Unknown option '$orig_key': use 'perms' for permission bits (file type is set automatically)");
692             }
693 2         198 Carp::croak("Unknown option '$orig_key' passed to stat helper");
694             }
695              
696 55 100       124 next unless defined $name2ix{$k};
697 41         79 $stat[ $name2ix{$k} ] = $opts->{$orig_key};
698             }
699              
700 72         510 return \@stat;
701             }
702              
703             ######################################################
704             ### Scope guard for automatic mock cleanup
705             ######################################################
706              
707             package Overload::FileCheck::Guard;
708              
709             sub new {
710 6     6   30 my ( $class, @checks ) = @_;
711              
712 6         24 return bless { checks => \@checks, active => 1 }, $class;
713             }
714              
715             sub cancel {
716 1     1   2 my ($self) = @_;
717              
718 1         1 $self->{active} = 0;
719 1         3 return;
720             }
721              
722             sub DESTROY {
723 7     7   650 my ($self) = @_;
724              
725 7 100       31 return unless $self->{active};
726 5         8 $self->{active} = 0;
727              
728 5         6 local $@;
729 5         5 eval { Overload::FileCheck::unmock_file_check( @{ $self->{checks} } ) };
  5         6  
  5         10  
730 5         12 return;
731             }
732              
733             1;
734              
735             __END__