diff --git a/lib/Test/MockFile.pm b/lib/Test/MockFile.pm index 6c5d4cf..8bbbb0f 100644 --- a/lib/Test/MockFile.pm +++ b/lib/Test/MockFile.pm @@ -4041,6 +4041,20 @@ sub __rename ($$) { # Renaming to self is a no-op (POSIX rename(2)) return 1 if $mock_old == $mock_new; + # Permission check: rename needs write+execute on both parent dirs (POSIX rename(2)) + if ( defined $_mock_uid ) { + if ( !_check_parent_perms( $mock_old->{'path'}, 2 | 1 ) ) { + $! = EACCES; + _maybe_throw_autodie( 'rename', @_ ); + return 0; + } + if ( !_check_parent_perms( $mock_new->{'path'}, 2 | 1 ) ) { + $! = EACCES; + _maybe_throw_autodie( 'rename', @_ ); + return 0; + } + } + # Can't overwrite a directory with a non-directory if ( $mock_new->exists && $mock_new->is_dir && !$mock_old->is_dir ) { $! = EISDIR; diff --git a/t/rename.t b/t/rename.t index 439da47..d936012 100644 --- a/t/rename.t +++ b/t/rename.t @@ -7,7 +7,7 @@ use Test2::Bundle::Extended; use Test2::Tools::Explain; use Test2::Plugin::NoWarnings; -use Errno qw/ENOENT EISDIR ENOTDIR ENOTEMPTY/; +use Errno qw/ENOENT EISDIR ENOTDIR ENOTEMPTY EACCES/; use Test::MockFile qw< nostrict >; @@ -239,4 +239,44 @@ note "-------------- rename: preserves inode and nlink --------------"; is( $st[3], 3, 'nlink preserved after rename' ); } +note "-------------- rename: permission check on source parent dir --------------"; +{ + my $parent = Test::MockFile->new_dir( '/mock/srcperm', { mode => 0555 } ); + my $old = Test::MockFile->file( '/mock/srcperm/file', 'data' ); + my $dst = Test::MockFile->file('/mock/dst_perm'); + + Test::MockFile->set_user( 1000, 1000 ); + ok( !rename( '/mock/srcperm/file', '/mock/dst_perm' ), 'rename fails when source parent is read-only' ); + is( $! + 0, EACCES, 'errno is EACCES' ); + ok( $old->exists, 'source file still exists after denied rename' ); + Test::MockFile->clear_user(); +} + +note "-------------- rename: permission check on dest parent dir --------------"; +{ + my $src_parent = Test::MockFile->new_dir( '/mock/src_ok', { mode => 0755, uid => 1000 } ); + my $old = Test::MockFile->file( '/mock/src_ok/file', 'data' ); + my $dst_parent = Test::MockFile->new_dir( '/mock/dstperm', { mode => 0555 } ); + my $dst = Test::MockFile->file('/mock/dstperm/target'); + + Test::MockFile->set_user( 1000, 1000 ); + ok( !rename( '/mock/src_ok/file', '/mock/dstperm/target' ), 'rename fails when dest parent is read-only' ); + is( $! + 0, EACCES, 'errno is EACCES' ); + ok( $old->exists, 'source file still exists after denied rename' ); + Test::MockFile->clear_user(); +} + +note "-------------- rename: succeeds when both parents are writable --------------"; +{ + my $src_dir = Test::MockFile->new_dir( '/mock/src_w', { mode => 0755, uid => 1000 } ); + my $old = Test::MockFile->file( '/mock/src_w/file', 'data' ); + my $dst_dir = Test::MockFile->new_dir( '/mock/dst_w', { mode => 0755, uid => 1000 } ); + my $dst = Test::MockFile->file('/mock/dst_w/target'); + + Test::MockFile->set_user( 1000, 1000 ); + ok( rename( '/mock/src_w/file', '/mock/dst_w/target' ), 'rename succeeds when both parents are writable' ); + is( $dst->contents, 'data', 'file moved successfully' ); + Test::MockFile->clear_user(); +} + done_testing();