Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions FileCheck.xs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <XSUB.h>
#include <embed.h>

#include <errno.h>

#include "FileCheck.h"

/*
Expand Down Expand Up @@ -98,9 +100,15 @@ int _overload_ft_ops() {

/* printf ("######## The result is %d /// OPTYPE is %d\n", check_status, optype); */

PUTBACK;
FREETMPS;
LEAVE;
/* Save errno before scope cleanup — FREETMPS/LEAVE can trigger
* DESTROY or other Perl code that clobbers errno set by _check(). */
{
int saved_errno = errno;
PUTBACK;
FREETMPS;
LEAVE;
errno = saved_errno;
}

return check_status;
}
Expand Down Expand Up @@ -135,9 +143,13 @@ SV* _overload_ft_ops_sv() {

/* printf ("######## The result is %d /// OPTYPE is %d\n", check_status, optype); */

PUTBACK;
FREETMPS;
LEAVE;
{
int saved_errno = errno;
PUTBACK;
FREETMPS;
LEAVE;
errno = saved_errno;
}

return status;
}
Expand Down Expand Up @@ -241,9 +253,13 @@ int _overload_ft_stat(Stat_t *stat, int *size) {

}

PUTBACK;
FREETMPS;
LEAVE;
{
int saved_errno = errno;
PUTBACK;
FREETMPS;
LEAVE;
errno = saved_errno;
}

return check_status;
}
Expand Down
123 changes: 123 additions & 0 deletions t/stat-errno-preservation.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/perl -w

# Test that errno ($!) is preserved through XS scope cleanup
# after _check() sets it for failed stat/lstat/file-check operations.
#
# The XS functions _overload_ft_ops() and _overload_ft_stat() call
# FREETMPS/LEAVE after the Perl _check() function returns. Without
# saving/restoring errno, this cleanup can clobber $! values set by
# the mock callback or by _check()'s default errno logic.

use strict;
use warnings;

use Test2::Bundle::Extended;
use Test2::Tools::Explain;
use Test2::Plugin::NoWarnings;

use Overload::FileCheck q{:all};
use Errno ();

my $missing = '/stat/errno/missing';
my $exists = '/stat/errno/exists';

# --- Test with mock_all_from_stat (the -from-stat path) ---

subtest 'errno preserved through stat via mock_all_from_stat' => sub {
mock_all_from_stat(
sub {
my ( $type, $file ) = @_;
if ( $file eq $exists ) {
return stat_as_file();
}
if ( $file eq $missing ) {
$! = Errno::ENOENT();
return 0; # falsy = file not found
}
return FALLBACK_TO_REAL_OP;
}
);

subtest '-e on missing file sets ENOENT' => sub {
$! = 0;
my $check = -e $missing;
ok( !$check, '-e returns false for missing mock' );
is( int($!), Errno::ENOENT(), '$! is ENOENT after -e on missing file' );
};

subtest '-f on missing file sets ENOENT' => sub {
$! = 0;
my $check = -f $missing;
ok( !$check, '-f returns false for missing mock' );
is( int($!), Errno::ENOENT(), '$! is ENOENT after -f on missing file' );
};

subtest 'stat on missing file preserves errno' => sub {
$! = 0;
my @st = stat($missing);
is( scalar @st, 0, 'stat returns empty list for missing mock' );
is( int($!), Errno::ENOENT(), '$! is ENOENT after stat on missing file' );
};

subtest 'lstat on missing file preserves errno' => sub {
$! = 0;
my @st = lstat($missing);
is( scalar @st, 0, 'lstat returns empty list for missing mock' );
is( int($!), Errno::ENOENT(), '$! is ENOENT after lstat on missing file' );
};

subtest '-e on existing file does not set errno' => sub {
$! = 0;
my $check = -e $exists;
ok( $check, '-e returns true for existing mock' );
is( int($!), 0, '$! is not set after successful -e' );
};

unmock_all_file_checks();
};

# --- Test with custom errno values ---

subtest 'custom errno preserved through file check' => sub {
mock_file_check(
'-e' => sub {
my $f = shift;
if ( $f eq $missing ) {
$! = Errno::EACCES();
return CHECK_IS_FALSE;
}
return FALLBACK_TO_REAL_OP;
}
);

$! = 0;
my $check = -e $missing;
ok( !$check, '-e returns false' );
is( int($!), Errno::EACCES(), '$! preserves custom EACCES through XS cleanup' );

unmock_all_file_checks();
};

# --- Test default errno when callback doesn't set one ---

subtest 'default ENOENT set when callback returns false without setting errno' => sub {
mock_all_from_stat(
sub {
my ( $type, $file ) = @_;
if ( $file eq $missing ) {
# Don't set $!, let _check() set the default
return 0;
}
return FALLBACK_TO_REAL_OP;
}
);

$! = 0;
my $check = -e $missing;
ok( !$check, '-e returns false' );
is( int($!), Errno::ENOENT(), '$! gets default ENOENT when callback omits errno' );

unmock_all_file_checks();
};

done_testing;