diff --git a/CHANGES.md b/CHANGES.md index aa580382..90532c98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,10 @@ The released versions correspond to PyPI releases. ## Unreleased +### Features +* added possibility to set a path inaccessible under Windows by using `chown()` with + the `force_unix_mode` flag (see [#720](../../issues/720)) + ## [Version 5.1.0](https://pypi.python.org/pypi/pyfakefs/5.1.0) (2023-01-12) New version before Debian freeze diff --git a/docs/usage.rst b/docs/usage.rst index 094632e7..8ea4ee17 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -911,6 +911,29 @@ The following test works both under Windows and Linux: assert os.path.splitdrive(r"C:\foo\bar") == ("C:", r"\foo\bar") assert os.path.ismount("C:") +Set file as inaccessible under Windows +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Normally, if you try to set a file or directory as inaccessible using ``chmod`` under +Windows, the value you provide is masked by a value that always ensures that no read +permissions for any user are removed. In reality, there is the possibility to make +a file or directory unreadable using the Windows ACL API, which is not directly +supported in the Python filesystem API. To make this possible to test, there is the +possibility to use the ``force_unix_mode`` argument to ``FakeFilesystem.chmod``: + +.. code:: python + + def test_is_file_for_unreadable_dir_windows(fs): + fs.os = OSType.WINDOWS + path = pathlib.Path("/foo/bar") + fs.create_file(path) + # normal chmod does not really set the mode to 0 + self.fs.chmod("/foo", 0o000) + assert path.is_file() + # but it does in forced UNIX mode + fs.chmod("/foo", 0o000, force_unix_mode=True) + with pytest.raises(PermissionError): + path.is_file() + .. _`example.py`: https://github.com/pytest-dev/pyfakefs/blob/main/pyfakefs/tests/example.py .. _`example_test.py`: https://github.com/pytest-dev/pyfakefs/blob/main/pyfakefs/tests/example_test.py diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index a294ab58..ae5023fc 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -1438,7 +1438,13 @@ def raise_for_filepath_ending_with_separator( error_nr = errno.EINVAL if self.is_windows_fs else errno.ENOTDIR self.raise_os_error(error_nr, entry_path) - def chmod(self, path: AnyStr, mode: int, follow_symlinks: bool = True) -> None: + def chmod( + self, + path: AnyStr, + mode: int, + follow_symlinks: bool = True, + force_unix_mode: bool = False, + ) -> None: """Change the permissions of a file as encoded in integer mode. Args: @@ -1446,11 +1452,13 @@ def chmod(self, path: AnyStr, mode: int, follow_symlinks: bool = True) -> None: mode: (int) Permissions. follow_symlinks: If `False` and `path` points to a symlink, the link itself is affected instead of the linked object. + force_unix_mode: if True and run under Windows, the mode is not + adapted for Windows to allow making dirs unreadable """ file_object = self.resolve( path, follow_symlinks, allow_fd=True, check_owner=True ) - if self.is_windows_fs: + if self.is_windows_fs and not force_unix_mode: if mode & PERM_WRITE: file_object.st_mode = file_object.st_mode | 0o222 else: @@ -2355,7 +2363,7 @@ def resolve( if follow_symlinks: return self.get_object_from_normpath( - self.resolve_path(file_path, check_read_perm), + self.resolve_path(file_path, allow_fd), check_read_perm, check_owner, ) diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index d405b497..2ad28bb3 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -5366,14 +5366,21 @@ def test_default_path(self): class FakeOsUnreadableDirTest(FakeOsModuleTestBase): def setUp(self): if self.use_real_fs(): - # make sure no dir is created if skipped + # unreadable dirs in Windows are only simulated + # and cannot be created in the real OS using file system + # functions only self.check_posix_only() 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) + self.chmod(self.dir_path, 0o000) + + def chmod(self, path, mode): + if self.is_windows_fs: + self.filesystem.chmod(path, mode, force_unix_mode=True) + else: + self.os.chmod(path, mode) def test_listdir_unreadable_dir(self): if not is_root(): @@ -5382,12 +5389,13 @@ def test_listdir_unreadable_dir(self): self.assertEqual(["some_file"], self.os.listdir(self.dir_path)) def test_listdir_user_readable_dir(self): - self.os.chmod(self.dir_path, 0o600) + self.chmod(self.dir_path, 0o600) self.assertEqual(["some_file"], self.os.listdir(self.dir_path)) - self.os.chmod(self.dir_path, 0o000) + self.chmod(self.dir_path, 0o000) def test_listdir_user_readable_dir_from_other_user(self): self.skip_real_fs() # won't change user in real fs + self.check_posix_only() user_id = USER_ID set_uid(user_id + 1) dir_path = self.make_path("dir1") @@ -5412,6 +5420,7 @@ def test_listdir_group_readable_dir_from_other_user(self): def test_listdir_group_readable_dir_from_other_group(self): self.skip_real_fs() # won't change user in real fs + self.check_posix_only() group_id = GROUP_ID set_gid(group_id + 1) dir_path = self.make_path("dir1") @@ -5438,9 +5447,9 @@ def test_stat_unreadable_dir(self): self.assertEqual(0, self.os.stat(self.dir_path).st_mode & 0o666) def test_chmod_unreadable_dir(self): - self.os.chmod(self.dir_path, 0o666) + self.chmod(self.dir_path, 0o666) self.assertEqual(0o666, self.os.stat(self.dir_path).st_mode & 0o666) - self.os.chmod(self.dir_path, 0o000) + self.chmod(self.dir_path, 0o000) self.assertEqual(0, self.os.stat(self.dir_path).st_mode & 0o666) def test_stat_file_in_unreadable_dir(self): @@ -5450,6 +5459,7 @@ def test_stat_file_in_unreadable_dir(self): self.assertEqual(0, self.os.stat(self.file_path).st_size) def test_remove_unreadable_dir(self): + self.check_posix_only() dir_path = self.make_path("dir1") self.create_dir(dir_path, perm=0o000) self.assertTrue(self.os.path.exists(dir_path)) diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py index fad500a9..fbbde04d 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -29,7 +29,7 @@ from unittest import mock from pyfakefs import fake_pathlib, fake_filesystem, fake_filesystem_unittest -from pyfakefs.fake_filesystem import is_root +from pyfakefs.fake_filesystem import is_root, OSType from pyfakefs.helpers import IS_PYPY from pyfakefs.tests.test_utils import RealFsTestMixin @@ -762,7 +762,6 @@ def use_real_fs(self): return True -@unittest.skipIf(sys.version_info < (3, 6), "path-like objects new in Python 3.6") class FakePathlibUsageInOsFunctionsTest(RealPathlibTestCase): """Test that many os / os.path functions accept a path-like object since Python 3.6. The functionality of these functions is tested @@ -1121,7 +1120,6 @@ def use_real_fs(self): return True -@unittest.skipIf(sys.version_info < (3, 6), "Path-like objects new in Python 3.6") class FakeFilesystemPathLikeObjectTest(unittest.TestCase): def setUp(self): self.filesystem = fake_filesystem.FakeFilesystem(path_separator="/") @@ -1180,5 +1178,23 @@ def test_add_existing_real_directory_with_pathlib_path(self): ) +class FakeFilesystemChmodTest(fake_filesystem_unittest.TestCase): + def setUp(self) -> None: + self.setUpPyfakefs() + + @unittest.skipIf(sys.platform != "win32", "Windows specific test") + def test_is_file_for_unreadable_dir_windows(self): + self.fs.os = OSType.WINDOWS + path = pathlib.Path("/foo/bar") + self.fs.create_file(path) + # normal chmod does not really set the mode to 0 + self.fs.chmod("/foo", 0o000) + self.assertTrue(path.is_file()) + # but it does in forced UNIX mode + self.fs.chmod("/foo", 0o000, force_unix_mode=True) + with self.assertRaises(PermissionError): + path.is_file() + + if __name__ == "__main__": unittest.main(verbosity=2)