diff --git a/CHANGES.md b/CHANGES.md index 710fadc2..376b1ada 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,10 @@ The released versions correspond to PyPI releases. * fixed handling of `dirfd` in `os.symlink` (see [#968](../../issues/968)) * add missing `follow_symlink` argument to `os.link` (see [#973](../../issues/973)) +### Enhancements +* added support for `O_NOFOLLOW` and `O_DIRECTORY` flags in `os.open` + (see [#972](../../issues/972) and [#974](../../issues/974)) + ## [Version 5.3.5](https://pypi.python.org/pypi/pyfakefs/5.3.5) (2024-01-30) Fixes a regression. diff --git a/pyfakefs/fake_os.py b/pyfakefs/fake_os.py index 1bf1ea46..68f7752f 100644 --- a/pyfakefs/fake_os.py +++ b/pyfakefs/fake_os.py @@ -247,6 +247,18 @@ def open( else: mode = 0o777 & ~self._umask() + has_directory_flag = ( + hasattr(os, "O_DIRECTORY") and flags & os.O_DIRECTORY == os.O_DIRECTORY + ) + if has_directory_flag and not self.filesystem.isdir(path): + raise OSError(errno.ENOTDIR, "path is not a directory", path) + + has_follow_flag = ( + hasattr(os, "O_NOFOLLOW") and flags & os.O_NOFOLLOW == os.O_NOFOLLOW + ) + if has_follow_flag and self.filesystem.islink(path): + raise OSError(errno.ELOOP, "path is a symlink", path) + has_tmpfile_flag = ( hasattr(os, "O_TMPFILE") and flags & os.O_TMPFILE == os.O_TMPFILE ) diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index 708e82ff..0240b3fa 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -565,6 +565,42 @@ def test_open_raises_with_trailing_separator_windows(self): self.check_windows_only() self.check_open_raises_with_trailing_separator(errno.EINVAL) + @unittest.skipIf(not hasattr(os, "O_DIRECTORY"), "opening directory not supported") + def test_open_raises_if_not_dir(self): + self.check_posix_only() + file_path = self.make_path("file.txt") + self.create_file(file_path, contents="foo") + with self.assertRaises(NotADirectoryError): + self.os.open(file_path, os.O_RDONLY | os.O_DIRECTORY) + dir_path = self.make_path("dir") + self.create_dir(dir_path) + with self.assertRaises(IsADirectoryError): + self.os.open(dir_path, os.O_RDWR | os.O_DIRECTORY) + + @unittest.skipIf(not hasattr(os, "O_NOFOLLOW"), "NOFOLLOW attribute not supported") + def test_open_nofollow_symlink_raises(self): + self.skip_if_symlink_not_supported() + file_path = self.make_path("file.txt") + self.create_file(file_path, contents="foo") + link_path = self.make_path("link") + self.create_symlink(link_path, file_path) + with self.assertRaises(OSError) as cm: + self.os.open(link_path, os.O_RDONLY | os.O_NOFOLLOW) + assert cm.exception.errno == errno.ELOOP + + @unittest.skipIf(not hasattr(os, "O_NOFOLLOW"), "NOFOLLOW attribute not supported") + def test_open_nofollow_symlink_as_parent_works(self): + self.skip_if_symlink_not_supported() + dir_path = self.make_path("dir") + self.create_dir(dir_path) + link_path = self.make_path("link") + self.create_symlink(link_path, dir_path) + file_path = self.os.path.join(link_path, "file.txt") + self.create_file(file_path, contents="foo") + fd = self.os.open(file_path, os.O_RDONLY | os.O_NOFOLLOW) + self.assertGreater(fd, 0) + self.os.close(fd) + def test_lexists_with_trailing_separator_linux_windows(self): self.check_linux_and_windows() self.skip_if_symlink_not_supported() @@ -5298,7 +5334,6 @@ def test_scandir_stat_nlink(self): self.assertEqual(1, self.os.stat(self.file_path).st_nlink) @unittest.skipIf(not hasattr(os, "O_DIRECTORY"), "opening directory not supported") - @unittest.skipIf(sys.version_info < (3, 7), "fd not supported for scandir") def test_scandir_with_fd(self): # regression test for #723 temp_dir = self.make_path("tmp", "dir")