From 02fbaf4887deaf0207a5805d3736e0124a694c14 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 9 Mar 2022 14:29:33 +0200 Subject: bpo-46245: Add optional parameter dir_fd in shutil.rmtree() (GH-30365) --- Doc/library/shutil.rst | 10 ++++++++-- Doc/whatsnew/3.11.rst | 7 +++++++ Lib/shutil.py | 17 ++++++++++++----- Lib/test/test_shutil.py | 21 +++++++++++++++++++++ .../2022-01-03-20-12-14.bpo-46245.3w4RlA.rst | 1 + 5 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-01-03-20-12-14.bpo-46245.3w4RlA.rst diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 22d6dba..16b8d3c 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -286,7 +286,7 @@ Directory and files operations .. versionadded:: 3.8 The *dirs_exist_ok* parameter. -.. function:: rmtree(path, ignore_errors=False, onerror=None) +.. function:: rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None) .. index:: single: directory; deleting @@ -296,6 +296,9 @@ Directory and files operations handled by calling a handler specified by *onerror* or, if that is omitted, they raise an exception. + This function can support :ref:`paths relative to directory descriptors + `. + .. note:: On platforms that support the necessary fd-based functions a symlink @@ -315,7 +318,7 @@ Directory and files operations *excinfo*, will be the exception information returned by :func:`sys.exc_info`. Exceptions raised by *onerror* will not be caught. - .. audit-event:: shutil.rmtree path shutil.rmtree + .. audit-event:: shutil.rmtree path,dir_fd shutil.rmtree .. versionchanged:: 3.3 Added a symlink attack resistant version that is used automatically @@ -325,6 +328,9 @@ Directory and files operations On Windows, will no longer delete the contents of a directory junction before removing the junction. + .. versionchanged:: 3.11 + The *dir_fd* parameter. + .. attribute:: rmtree.avoids_symlink_attacks Indicates whether the current platform and implementation provides a diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index d9e5d06..628d4c0 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -283,6 +283,13 @@ os (Contributed by Dong-hee Na in :issue:`44611`.) +shutil +------ + +* Add optional parameter *dir_fd* in :func:`shutil.rmtree`. + (Contributed by Serhiy Storchaka in :issue:`46245`.) + + socket ------ diff --git a/Lib/shutil.py b/Lib/shutil.py index eb768f9..22bd86d 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -684,9 +684,14 @@ _use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <= os.scandir in os.supports_fd and os.stat in os.supports_follow_symlinks) -def rmtree(path, ignore_errors=False, onerror=None): +def rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None): """Recursively delete a directory tree. + If dir_fd is not None, it should be a file descriptor open to a directory; + path will then be relative to that directory. + dir_fd may not be implemented on your platform. + If it is unavailable, using it will raise a NotImplementedError. + If ignore_errors is set, errors are ignored; otherwise, if onerror is set, it is called to handle the error with arguments (func, path, exc_info) where func is platform and implementation dependent; @@ -695,7 +700,7 @@ def rmtree(path, ignore_errors=False, onerror=None): is false and onerror is None, an exception is raised. """ - sys.audit("shutil.rmtree", path) + sys.audit("shutil.rmtree", path, dir_fd) if ignore_errors: def onerror(*args): pass @@ -709,12 +714,12 @@ def rmtree(path, ignore_errors=False, onerror=None): # Note: To guard against symlink races, we use the standard # lstat()/open()/fstat() trick. try: - orig_st = os.lstat(path) + orig_st = os.lstat(path, dir_fd=dir_fd) except Exception: onerror(os.lstat, path, sys.exc_info()) return try: - fd = os.open(path, os.O_RDONLY) + fd = os.open(path, os.O_RDONLY, dir_fd=dir_fd) fd_closed = False except Exception: onerror(os.open, path, sys.exc_info()) @@ -725,7 +730,7 @@ def rmtree(path, ignore_errors=False, onerror=None): try: os.close(fd) fd_closed = True - os.rmdir(path) + os.rmdir(path, dir_fd=dir_fd) except OSError: onerror(os.rmdir, path, sys.exc_info()) else: @@ -738,6 +743,8 @@ def rmtree(path, ignore_errors=False, onerror=None): if not fd_closed: os.close(fd) else: + if dir_fd is not None: + raise NotImplementedError("dir_fd unavailable on this platform") try: if _rmtree_islink(path): # symlinks to directories are forbidden, see bug #1669 diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 7669b94..7003386 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -405,6 +405,27 @@ class TestRmTree(BaseTest, unittest.TestCase): self.assertFalse(shutil._use_fd_functions) self.assertFalse(shutil.rmtree.avoids_symlink_attacks) + @unittest.skipUnless(shutil._use_fd_functions, "dir_fd is not supported") + def test_rmtree_with_dir_fd(self): + tmp_dir = self.mkdtemp() + victim = 'killme' + fullname = os.path.join(tmp_dir, victim) + dir_fd = os.open(tmp_dir, os.O_RDONLY) + self.addCleanup(os.close, dir_fd) + os.mkdir(fullname) + os.mkdir(os.path.join(fullname, 'subdir')) + write_file(os.path.join(fullname, 'subdir', 'somefile'), 'foo') + self.assertTrue(os.path.exists(fullname)) + shutil.rmtree(victim, dir_fd=dir_fd) + self.assertFalse(os.path.exists(fullname)) + + @unittest.skipIf(shutil._use_fd_functions, "dir_fd is supported") + def test_rmtree_with_dir_fd_unsupported(self): + tmp_dir = self.mkdtemp() + with self.assertRaises(NotImplementedError): + shutil.rmtree(tmp_dir, dir_fd=0) + self.assertTrue(os.path.exists(tmp_dir)) + def test_rmtree_dont_delete_file(self): # When called on a file instead of a directory, don't delete it. handle, path = tempfile.mkstemp(dir=self.mkdtemp()) diff --git a/Misc/NEWS.d/next/Library/2022-01-03-20-12-14.bpo-46245.3w4RlA.rst b/Misc/NEWS.d/next/Library/2022-01-03-20-12-14.bpo-46245.3w4RlA.rst new file mode 100644 index 0000000..43e8660 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-03-20-12-14.bpo-46245.3w4RlA.rst @@ -0,0 +1 @@ +Add optional parameter *dir_fd* in :func:`shutil.rmtree`. -- cgit v0.12