From 042448df8505eabdba14c3f86b3e7c253414a88f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 3 Jan 2022 20:12:34 +0200 Subject: [PATCH 1/2] po-46245: Add optional parameter dir_fd in shutil.rmtree() --- Doc/library/shutil.rst | 10 ++++++++-- Doc/whatsnew/3.11.rst | 7 +++++++ Lib/shutil.py | 17 ++++++++++++----- Lib/test/test_shutil.py | 19 +++++++++++++++++++ .../2022-01-03-20-12-14.bpo-46245.3w4RlA.rst | 1 + 5 files changed, 47 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 22d6dba9e1a9c6..16b8d3cdeebc84 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 faa63a93895a20..abdf3f0c269afe 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -258,6 +258,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`.) + + sqlite3 ------- diff --git a/Lib/shutil.py b/Lib/shutil.py index 949e024853c1d2..af86e6f5b99d2f 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -680,9 +680,14 @@ def _rmtree_safe_fd(topfd, path, onerror): 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, + and path should be relative; 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; @@ -691,7 +696,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 @@ -705,12 +710,12 @@ def onerror(*args): # 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) except Exception: onerror(os.open, path, sys.exc_info()) return @@ -718,7 +723,7 @@ def onerror(*args): if os.path.samestat(orig_st, os.fstat(fd)): _rmtree_safe_fd(fd, path, onerror) try: - os.rmdir(path) + os.rmdir(path, dir_fd=dir_fd) except OSError: onerror(os.rmdir, path, sys.exc_info()) else: @@ -730,6 +735,8 @@ def onerror(*args): finally: 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 7669b94ac35a68..e74b2483b6afcc 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -405,6 +405,25 @@ def _raiser(*args, **kwargs): self.assertFalse(shutil._use_fd_functions) self.assertFalse(shutil.rmtree.avoids_symlink_attacks) + 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)) + if shutil._use_fd_functions: + shutil.rmtree(victim, dir_fd=dir_fd) + else: + with self.assertRaises(NotImplementedError): + shutil.rmtree(victim, dir_fd=dir_fd) + self.assertTrue(os.path.exists(fullname)) + shutil.rmtree(fullname) + self.assertFalse(os.path.exists(fullname)) + 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 00000000000000..43e8660b2a3d31 --- /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`. From edb7b1b99202428a6b6e9c25a06d5f928c87852d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 3 Jan 2022 20:50:28 +0200 Subject: [PATCH 2/2] Fix tests on Windows. --- Lib/test/test_shutil.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index e74b2483b6afcc..70033863452805 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -405,6 +405,7 @@ def _raiser(*args, **kwargs): 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' @@ -415,15 +416,16 @@ def test_rmtree_with_dir_fd(self): os.mkdir(os.path.join(fullname, 'subdir')) write_file(os.path.join(fullname, 'subdir', 'somefile'), 'foo') self.assertTrue(os.path.exists(fullname)) - if shutil._use_fd_functions: - shutil.rmtree(victim, dir_fd=dir_fd) - else: - with self.assertRaises(NotImplementedError): - shutil.rmtree(victim, dir_fd=dir_fd) - self.assertTrue(os.path.exists(fullname)) - shutil.rmtree(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())