diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index acba66258fe8f0..82a5abd657a4d6 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -54,6 +54,9 @@ Directory and files operations *dst* and return *dst* in the most efficient way possible. *src* and *dst* are path-like objects or path names given as strings. + When *src* is a named pipe or a Unix socket, :exc:`SpecialFileError` + is raised. + *dst* must be the complete target file name; look at :func:`~shutil.copy` for a copy that accepts a target directory path. If *src* and *dst* specify the same file, :exc:`SameFileError` is raised. @@ -83,6 +86,11 @@ Directory and files operations copy the file more efficiently. See :ref:`shutil-platform-dependent-efficient-copy-operations` section. + .. versionchanged:: 3.12 + Raise :exc:`~shutil.SpecialFileError` instead of :exc:`OSError` when + copying a Unix socket. Since the former is a subclass of the latter, this + change is backward compatible. + .. exception:: SameFileError This exception is raised if source and destination in :func:`copyfile` @@ -369,7 +377,7 @@ Directory and files operations If *copy_function* is given, it must be a callable that takes two arguments *src* and *dst*, and will be used to copy *src* to *dst* if :func:`os.rename` cannot be used. If the source is a directory, - :func:`copytree` is called, passing it the :func:`copy_function`. The + :func:`copytree` is called, passing it the *copy_function*. The default *copy_function* is :func:`copy2`. Using :func:`~shutil.copy` as the *copy_function* allows the move to succeed when it is not possible to also copy the metadata, at the expense of not copying any of the metadata. diff --git a/Lib/shutil.py b/Lib/shutil.py index b0576407e02ffb..e68068e5748248 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -242,10 +242,13 @@ def copyfile(src, dst, *, follow_symlinks=True): # File most likely does not exist pass else: - # XXX What about other special files? (sockets, devices...) + # XXX What about other special files? (devices...) if stat.S_ISFIFO(st.st_mode): fn = fn.path if isinstance(fn, os.DirEntry) else fn raise SpecialFileError("`%s` is a named pipe" % fn) + elif stat.S_ISSOCK(st.st_mode): + fn = fn.path if isinstance(fn, os.DirEntry) else fn + raise SpecialFileError("`%s` is a socket" % fn) if _WINDOWS and i == 0: file_size = st.st_size diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 1c0589ced9ea89..f9c5ccb1c82219 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -13,6 +13,7 @@ import pathlib import subprocess import random +import socket import string import contextlib import io @@ -910,6 +911,28 @@ def test_copytree_named_pipe(self): shutil.rmtree(TESTFN, ignore_errors=True) shutil.rmtree(TESTFN2, ignore_errors=True) + @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'requires socket.AF_UNIX') + def test_copytree_socket(self): + os.mkdir(TESTFN) + self.addCleanup(shutil.rmtree, TESTFN, ignore_errors=True) + self.addCleanup(shutil.rmtree, TESTFN2, ignore_errors=True) + sock_path = tempfile.mktemp(dir=TESTFN) + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + support.bind_unix_socket(sock, sock_path) + self.addCleanup(support.unlink, sock_path) + + try: + shutil.copytree(TESTFN, TESTFN2) + except shutil.Error as e: + errors = e.args[0] + self.assertEqual(len(errors), 1) + src, dst, error_msg = errors[0] + self.assertEqual("`%s` is a socket" % sock_path, error_msg) + else: + self.fail("shutil.Error should have been raised") + def test_copytree_special_func(self): src_dir = self.mkdtemp() dst_dir = os.path.join(self.mkdtemp(), 'destination') @@ -1453,6 +1476,20 @@ def test_copyfile_named_pipe(self): finally: os.remove(TESTFN) + @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'requires socket.AF_UNIX') + def test_copyfile_socket(self): + tmp_dir = self.mkdtemp() + src = tempfile.mktemp(dir=tmp_dir) + dst = os.path.join(tmp_dir, 'dst') + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + support.bind_unix_socket(sock, src) + self.addCleanup(support.unlink, src) + + self.assertRaises(shutil.SpecialFileError, + shutil.copyfile, src, dst) + def test_copyfile_return_value(self): # copytree returns its destination path. src_dir = self.mkdtemp() diff --git a/Misc/NEWS.d/next/Library/2019-10-03-17-26-27.bpo-37700.pec3ne.rst b/Misc/NEWS.d/next/Library/2019-10-03-17-26-27.bpo-37700.pec3ne.rst new file mode 100644 index 00000000000000..dfdaa7ad574eea --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-10-03-17-26-27.bpo-37700.pec3ne.rst @@ -0,0 +1,2 @@ +:func:`shutil.copyfile` always raises :exc:`shutil.SpecialFileError` +when trying to copy a Unix socket.