Skip to content

Commit f5afa89

Browse files
authored
gh-154726: Fix shutil.copyfile() for symlinks to special files with follow_symlinks=False (GH-154728)
1 parent 7d76013 commit f5afa89

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

Lib/shutil.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,11 @@ def copyfile(src, dst, *, follow_symlinks=True):
292292
if _samefile(src, dst):
293293
raise SameFileError("{!r} and {!r} are the same file".format(src, dst))
294294

295+
copy_symlink = not follow_symlinks and _islink(src)
295296
file_size = 0
296297
for i, fn in enumerate([src, dst]):
298+
if copy_symlink and i == 0:
299+
continue
297300
try:
298301
st = _stat(fn)
299302
except OSError:
@@ -315,7 +318,7 @@ def copyfile(src, dst, *, follow_symlinks=True):
315318
if _WINDOWS and i == 0:
316319
file_size = st.st_size
317320

318-
if not follow_symlinks and _islink(src):
321+
if copy_symlink:
319322
os.symlink(os.readlink(src), dst)
320323
else:
321324
with open(src, 'rb') as fsrc:

Lib/test/test_shutil.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,47 @@ def test_copyfile_socket(self):
15741574
self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket',
15751575
shutil.copyfile, __file__, sock_path)
15761576

1577+
def _check_copyfile_symlink_to_special_file(self, target):
1578+
tmp_dir = self.mkdtemp()
1579+
src = os.path.join(tmp_dir, 'src')
1580+
dst = os.path.join(tmp_dir, 'dst')
1581+
os.symlink(target, src)
1582+
1583+
shutil.copyfile(src, dst, follow_symlinks=False)
1584+
1585+
self.assertTrue(os.path.islink(dst))
1586+
self.assertEqual(os.readlink(dst), target)
1587+
1588+
@os_helper.skip_unless_symlink
1589+
@unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
1590+
def test_copyfile_symlink_to_character_device(self):
1591+
self._check_copyfile_symlink_to_special_file('/dev/null')
1592+
1593+
@os_helper.skip_unless_symlink
1594+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
1595+
@unittest.skipIf(sys.platform == "vxworks",
1596+
"fifo requires special path on VxWorks")
1597+
def test_copyfile_symlink_to_named_pipe(self):
1598+
fifo_path = os.path.join(self.mkdtemp(), 'fifo')
1599+
try:
1600+
os.mkfifo(fifo_path)
1601+
except PermissionError as e:
1602+
self.skipTest('os.mkfifo(): %s' % e)
1603+
self._check_copyfile_symlink_to_special_file(fifo_path)
1604+
1605+
@os_helper.skip_unless_symlink
1606+
@socket_helper.skip_unless_bind_unix_socket
1607+
def test_copyfile_symlink_to_socket(self):
1608+
sock_path = os.path.join(self.mkdtemp(), 'sock')
1609+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1610+
self.addCleanup(sock.close)
1611+
try:
1612+
socket_helper.bind_unix_socket(sock, sock_path)
1613+
except OSError as e:
1614+
self.skipTest(f'cannot bind AF_UNIX socket: {e}')
1615+
self.addCleanup(os_helper.unlink, sock_path)
1616+
self._check_copyfile_symlink_to_special_file(sock_path)
1617+
15771618
@unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
15781619
def test_copyfile_character_device(self):
15791620
self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`shutil.copyfile` to copy a symbolic link to a special file when
2+
``follow_symlinks=False`` instead of raising
3+
:exc:`~shutil.SpecialFileError`.

0 commit comments

Comments
 (0)