Skip to content

Commit

Permalink
Added handling of missing directory read access under MacOS
Browse files Browse the repository at this point in the history
- changes are for MacOS only, as long as Linux behavior is unclear
- see #496
  • Loading branch information
mrbean-bremen committed Oct 18, 2019
1 parent 71a60a8 commit 26b4c73
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 25 deletions.
31 changes: 23 additions & 8 deletions pyfakefs/fake_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1964,13 +1964,15 @@ def _follow_link(self, link_path_components, link):
# self.cwd.
return self.normpath(link_path)

def get_object_from_normpath(self, file_path):
def get_object_from_normpath(self, file_path, check_read_perm=True):
"""Search for the specified filesystem object within the fake
filesystem.
Args:
file_path: Specifies target FakeFile object to retrieve, with a
path that has already been normalized/resolved.
check_read_perm: If True, raises OSError if a parent directory
does not have read permission
Returns:
The FakeFile object corresponding to file_path.
Expand All @@ -1996,16 +1998,21 @@ def get_object_from_normpath(self, file_path):
self.raise_io_error(errno.ENOTDIR, file_path)
self.raise_io_error(errno.ENOENT, file_path)
target_object = target_object.get_entry(component)
if (self.is_macos and check_read_perm and target_object and
not target_object.st_mode & PERM_READ):
self.raise_os_error(errno.EACCES, target_object.path)
except KeyError:
self.raise_io_error(errno.ENOENT, file_path)
return target_object

def get_object(self, file_path):
def get_object(self, file_path, check_read_perm=True):
"""Search for the specified filesystem object within the fake
filesystem.
Args:
file_path: Specifies the target FakeFile object to retrieve.
check_read_perm: If True, raises OSError if a parent directory
does not have read permission under MacOS
Returns:
The FakeFile object corresponding to `file_path`.
Expand All @@ -2015,16 +2022,19 @@ def get_object(self, file_path):
"""
file_path = make_string_path(file_path)
file_path = self.absnormpath(self._original_path(file_path))
return self.get_object_from_normpath(file_path)
return self.get_object_from_normpath(file_path, check_read_perm)

def resolve(self, file_path, follow_symlinks=True, allow_fd=False):
def resolve(self, file_path, follow_symlinks=True, allow_fd=False,
check_read_perm=True):
"""Search for the specified filesystem object, resolving all links.
Args:
file_path: Specifies the target FakeFile object to retrieve.
follow_symlinks: If `False`, the link itself is resolved,
otherwise the object linked to.
allow_fd: If `True`, `file_path` may be an open file descriptor
check_read_perm: If True, raises OSError if a parent directory
does not have read permission under MacOS
Returns:
The FakeFile object corresponding to `file_path`.
Expand All @@ -2040,7 +2050,8 @@ def resolve(self, file_path, follow_symlinks=True, allow_fd=False):

if follow_symlinks:
file_path = make_string_path(file_path)
return self.get_object_from_normpath(self.resolve_path(file_path))
return self.get_object_from_normpath(self.resolve_path(
file_path, check_read_perm), check_read_perm)
return self.lresolve(file_path)

def lresolve(self, path):
Expand Down Expand Up @@ -2077,6 +2088,8 @@ def lresolve(self, path):
if not self.is_windows_fs and isinstance(parent_obj, FakeFile):
self.raise_io_error(errno.ENOTDIR, path)
self.raise_io_error(errno.ENOENT, path)
if not parent_obj.st_mode & PERM_READ:
self.raise_os_error(errno.EACCES, parent_directory)
return parent_obj.get_entry(child_name)
except KeyError:
self.raise_io_error(errno.ENOENT, path)
Expand Down Expand Up @@ -2291,7 +2304,7 @@ def remove_object(self, file_path):
self.raise_os_error(errno.EBUSY, file_path)
try:
dirname, basename = self.splitpath(file_path)
target_directory = self.resolve(dirname)
target_directory = self.resolve(dirname, check_read_perm=False)
target_directory.remove_entry(basename)
except KeyError:
self.raise_io_error(errno.ENOENT, file_path)
Expand Down Expand Up @@ -2983,6 +2996,8 @@ def confirmdir(self, target_directory):
else:
error_nr = errno.ENOTDIR
self.raise_os_error(error_nr, target_directory, 267)
# if not directory.st_mode & PERM_READ:
# self.raise_os_error(errno.EACCES, directory)
return directory

def remove(self, path):
Expand All @@ -3000,7 +3015,7 @@ def remove(self, path):
if self.ends_with_path_separator(path):
self._handle_broken_link_with_trailing_sep(norm_path)
if self.exists(norm_path):
obj = self.resolve(norm_path)
obj = self.resolve(norm_path, check_read_perm=False)
if S_IFMT(obj.st_mode) == S_IFDIR:
link_obj = self.lresolve(norm_path)
if S_IFMT(link_obj.st_mode) != S_IFLNK:
Expand Down Expand Up @@ -5310,7 +5325,7 @@ def _handle_file_arg(self, file_):
file_path, raw_io=self.raw_io)
if self.filesystem.exists(file_path):
file_object = self.filesystem.get_object_from_normpath(
real_path)
real_path, check_read_perm=False)
return file_object, file_path, filedes, real_path

def _handle_file_mode(self, mode, newline, open_modes):
Expand Down
37 changes: 28 additions & 9 deletions pyfakefs/tests/fake_filesystem_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2003,8 +2003,15 @@ def test_add_existing_real_directory_symlink(self):
with fake_open('/etc/something', 'w') as f:
f.write('good morning')

with self.create_symlinks(symlinks):
self.filesystem.add_real_directory(real_directory, lazy_read=False)
try:
with self.create_symlinks(symlinks):
self.filesystem.add_real_directory(
real_directory, lazy_read=False)
except OSError:
if self.is_windows:
raise unittest.SkipTest(
'Symlinks under Windows need admin privileges')
raise

for link in symlinks:
self.assertTrue(self.filesystem.islink(link[1]))
Expand Down Expand Up @@ -2057,8 +2064,14 @@ def test_add_existing_real_directory_symlink_target_path(self):
('../all_tests.py', os.path.join(real_directory, 'fixtures', 'symlink_file_relative')),
]

with self.create_symlinks(symlinks):
self.filesystem.add_real_directory(real_directory, target_path='/path', lazy_read=False)
try:
with self.create_symlinks(symlinks):
self.filesystem.add_real_directory(real_directory, target_path='/path', lazy_read=False)
except OSError:
if self.is_windows:
raise unittest.SkipTest(
'Symlinks under Windows need admin privileges')
raise

self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_dir_relative'))
self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_dir_relative/all_tests.py'))
Expand All @@ -2073,12 +2086,18 @@ def test_add_existing_real_directory_symlink_lazy_read(self):
('../all_tests.py', os.path.join(real_directory, 'fixtures', 'symlink_file_relative')),
]

with self.create_symlinks(symlinks):
self.filesystem.add_real_directory(real_directory, target_path='/path', lazy_read=True)
try:
with self.create_symlinks(symlinks):
self.filesystem.add_real_directory(real_directory, target_path='/path', lazy_read=True)

self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_dir_relative'))
self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_dir_relative/all_tests.py'))
self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_file_relative'))
self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_dir_relative'))
self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_dir_relative/all_tests.py'))
self.assertTrue(self.filesystem.exists('/path/fixtures/symlink_file_relative'))
except OSError:
if self.is_windows:
raise unittest.SkipTest(
'Symlinks under Windows need admin privileges')
raise

def test_add_existing_real_directory_tree_to_existing_path(self):
self.filesystem.create_dir('/foo/bar')
Expand Down
11 changes: 6 additions & 5 deletions pyfakefs/tests/fake_open_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import unittest

from pyfakefs import fake_filesystem
from pyfakefs.fake_filesystem import is_root
from pyfakefs.fake_filesystem import is_root, PERM_READ
from pyfakefs.tests.test_utils import TestCase, RealFsTestCase


Expand Down Expand Up @@ -440,10 +440,11 @@ def test_read_with_rplus(self):
def create_with_permission(self, file_path, perm_bits):
self.create_file(file_path)
self.os.chmod(file_path, perm_bits)
st = self.os.stat(file_path)
self.assert_mode_equal(perm_bits, st.st_mode)
self.assertTrue(st.st_mode & stat.S_IFREG)
self.assertFalse(st.st_mode & stat.S_IFDIR)
if perm_bits & PERM_READ:
st = self.os.stat(file_path)
self.assert_mode_equal(perm_bits, st.st_mode)
self.assertTrue(st.st_mode & stat.S_IFREG)
self.assertFalse(st.st_mode & stat.S_IFDIR)

def test_open_flags700(self):
# set up
Expand Down
42 changes: 39 additions & 3 deletions pyfakefs/tests/fake_os_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,8 @@ def test_rename_preserves_stat(self):
new_file = self.filesystem.get_object(new_file_path)
self.assertNotEqual(new_file.st_mtime, old_file.st_mtime)
self.os.rename(old_file_path, new_file_path)
new_file = self.filesystem.get_object(new_file_path)
new_file = self.filesystem.get_object(
new_file_path, check_read_perm=False)
self.assertEqual(new_file.st_mtime, old_file.st_mtime)
self.assertEqual(new_file.st_mode, old_file.st_mode)
self.assertEqual(new_file.st_uid, old_file.st_uid)
Expand Down Expand Up @@ -1950,9 +1951,9 @@ def test_chmod_dir(self):
path = self.make_path('some_dir')
self.createTestDirectory(path)
# actual tests
self.os.chmod(path, 0o1234)
self.os.chmod(path, 0o1434)
st = self.os.stat(path)
self.assert_mode_equal(0o1234, st.st_mode)
self.assert_mode_equal(0o1434, st.st_mode)
self.assertFalse(st.st_mode & stat.S_IFREG)
self.assertTrue(st.st_mode & stat.S_IFDIR)

Expand Down Expand Up @@ -5036,3 +5037,38 @@ def test_default_path(self):
self.os.setxattr(self.dir_path, b'test', b'value')
self.assertEqual(['test'], self.os.listxattr())
self.assertEqual(b'value', self.os.getxattr(self.dir_path, 'test'))


class FakeOsUnreadableDirTest(FakeOsModuleTestBase):
def setUp(self):
super(FakeOsUnreadableDirTest, self).setUp()
self.check_posix_only()
self.dir_path = self.make_path('some_dir')
self.file_path = self.os.path.join(self.dir_path, 'some_file')
self.create_file(self.file_path)
self.os.chmod(self.dir_path, 0o000)

@unittest.skipIf(TestCase.is_macos, 'Linux behavior')
def test_listdir_unreadable_dir_linux(self):
self.assertEqual(['some_file'], self.os.listdir(self.dir_path))

@unittest.skipIf(not TestCase.is_macos, 'MacOS behavior')
def test_listdir_unreadable_dir_macos(self):
self.assert_raises_os_error(
errno.EACCES, self.os.listdir, self.dir_path)

@unittest.skipIf(TestCase.is_macos, 'Linux behavior')
def test_stat_file_in_unreadable_dir_linux(self):
self.assertEqual(0, self.os.stat(self.file_path).st_size)

@unittest.skipIf(not TestCase.is_macos, 'MacOS behavior')
def test_stat_file_in_unreadable_dir_macos(self):
self.assert_raises_os_error(
errno.EACCES, self.os.stat, self.file_path)


class RealOsUnreadableDirTest(FakeOsUnreadableDirTest):
def use_real_fs(self):
return True


21 changes: 21 additions & 0 deletions pyfakefs/tests/fake_pathlib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,27 @@ def test_resolve_nonexisting_file(self):
path = self.path('/foo/bar')
self.assert_raises_os_error(errno.ENOENT, path.resolve)

def test_stat_file_in_unreadable_dir(self):
self.check_posix_only()
dir_path = self.make_path('some_dir')
file_path = self.os.path.join(dir_path, 'some_file')
self.create_file(file_path)
self.os.chmod(dir_path, 0o000)
self.assertEqual(0, self.path(file_path).stat().st_size)

def test_iterdir_in_unreadable_dir(self):
self.check_posix_only()
dir_path = self.make_path('some_dir')
file_path = self.os.path.join(dir_path, 'some_file')
self.create_file(file_path)
self.os.chmod(dir_path, 0o000)
iter = self.path(dir_path).iterdir()
if self.is_macos:
self.assert_raises_os_error(errno.EACCES, list, iter)
else:
path = str(list(iter)[0])
self.assertTrue(path.endswith('some_file'))

@unittest.skipIf(not is_windows, 'Windows specific behavior')
def test_resolve_file_as_parent_windows(self):
skip_if_pathlib_36_is_available()
Expand Down

0 comments on commit 26b4c73

Please sign in to comment.