Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,11 @@ def copyfile(src, dst, *, follow_symlinks=True):
if _samefile(src, dst):
raise SameFileError("{!r} and {!r} are the same file".format(src, dst))

copy_symlink = not follow_symlinks and _islink(src)
file_size = 0
for i, fn in enumerate([src, dst]):
if copy_symlink and i == 0:
continue
try:
st = _stat(fn)
except OSError:
Expand All @@ -315,7 +318,7 @@ def copyfile(src, dst, *, follow_symlinks=True):
if _WINDOWS and i == 0:
file_size = st.st_size

if not follow_symlinks and _islink(src):
if copy_symlink:
os.symlink(os.readlink(src), dst)
else:
with open(src, 'rb') as fsrc:
Expand Down
46 changes: 46 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,47 @@ def test_copyfile_socket(self):
self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket',
shutil.copyfile, __file__, sock_path)

def _check_copyfile_symlink_to_special_file(self, target):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'src')
dst = os.path.join(tmp_dir, 'dst')
os.symlink(target, src)

shutil.copyfile(src, dst, follow_symlinks=False)

self.assertTrue(os.path.islink(dst))
self.assertEqual(os.readlink(dst), target)

@os_helper.skip_unless_symlink
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
@unittest.skipIf(sys.platform == "vxworks",
"fifo requires special path on VxWorks")
def test_copyfile_symlink_to_named_pipe(self):
fifo_path = os.path.join(self.mkdtemp(), 'fifo')
try:
os.mkfifo(fifo_path)
except PermissionError as e:
self.skipTest('os.mkfifo(): %s' % e)
self._check_copyfile_symlink_to_special_file(fifo_path)

@os_helper.skip_unless_symlink
@socket_helper.skip_unless_bind_unix_socket
def test_copyfile_symlink_to_socket(self):
sock_path = os.path.join(self.mkdtemp(), 'sock')
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
try:
socket_helper.bind_unix_socket(sock, sock_path)
except OSError as e:
if str(e) == "AF_UNIX path too long":
Comment thread
lkk7 marked this conversation as resolved.
Outdated
self.skipTest(
"Pathname {0!a} is too long to serve as an AF_UNIX path"
.format(sock_path))
else:
raise
self.addCleanup(os_helper.unlink, sock_path)
self._check_copyfile_symlink_to_special_file(sock_path)

@unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
def test_copyfile_character_device(self):
self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device',
Expand All @@ -1583,6 +1624,11 @@ def test_copyfile_character_device(self):
self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device',
shutil.copyfile, src_file, '/dev/null')

@os_helper.skip_unless_symlink
@unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
def test_copyfile_symlink_to_character_device(self):
self._check_copyfile_symlink_to_special_file('/dev/null')

def test_copyfile_block_device(self):
block_dev = None
for dev in ['/dev/loop0', '/dev/sda', '/dev/vda', '/dev/disk0']:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`shutil.copyfile` to copy a symbolic link to a special file when
``follow_symlinks=False`` instead of raising
:exc:`~shutil.SpecialFileError`.
Loading