Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
29 changes: 28 additions & 1 deletion Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import os
import platform
import sys
import stat
import fnmatch
Expand Down Expand Up @@ -311,6 +312,31 @@ def _copyxattr(src, dst, *, follow_symlinks=True):
def _copyxattr(*args, **kwargs):
pass

if sys.platform == 'darwin':
# As of macOS 10.10, the `copyfile` API changed to not copy certain flags.
# This corresponds to kernel version 14.0.
mac_ver = tuple(int(x) for x in os.uname().release.split('.'))
if mac_ver >= (14, 0, 0):
def _fix_flags(dst, flags, follow_symlinks):
"""Perform any modifications to `flags` before they can be applied
to `dst` with `chflags()`.

"""
# Based on copyfile's copyfile_stat()
omit_flags = (stat.UF_TRACKED | stat.SF_RESTRICTED)
add_flags = 0

# If the kernel automatically put SF_RESTRICTED on the destination
# already, we don't want to clear it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is needed, will macOS make files restricted when copying into special locations? The SIP feature only protects selected system files. Without this code the signature for this helper function could be simplified.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic is the same as Apple's code, so I think it's better to keep it.

st = os.stat(dst, follow_symlinks=follow_symlinks)
add_flags |= (st.st_flags & stat.SF_RESTRICTED)

return (flags & ~omit_flags) | add_flags

if '_fix_flags' not in vars():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use "else:" instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I used else:, it would make the code more complicated. If it matched with if sys.platform == 'darwin', then _fix_flags would not be defined for old versions of macOS. If I matched it with if mac_ver >= (10, 10, 0), then it would not be defined for other platforms. If I wanted to do both, I'd have to repeat code. This seemed the least complex.

def _fix_flags(dst, flags, *args, **kwargs):
return flags

def copystat(src, dst, *, follow_symlinks=True):
"""Copy all stat info (mode bits, atime, mtime, flags) from src to dst.

Expand Down Expand Up @@ -356,7 +382,8 @@ def lookup(name):
pass
if hasattr(st, 'st_flags'):
try:
lookup("chflags")(dst, st.st_flags, follow_symlinks=follow)
flags = _fix_flags(dst, st.st_flags, follow_symlinks=follow)
lookup("chflags")(dst, flags, follow_symlinks=follow)
except OSError as why:
for err in 'EOPNOTSUPP', 'ENOTSUP':
if hasattr(errno, err) and why.errno == getattr(errno, err):
Expand Down
6 changes: 4 additions & 2 deletions Lib/stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,13 @@ def S_ISSOCK(mode):
UF_APPEND = 0x00000004 # file may only be appended to
UF_OPAQUE = 0x00000008 # directory is opaque when viewed through a union stack
UF_NOUNLINK = 0x00000010 # file may not be renamed or deleted
UF_COMPRESSED = 0x00000020 # OS X: file is hfs-compressed
UF_HIDDEN = 0x00008000 # OS X: file should not be displayed
UF_COMPRESSED = 0x00000020 # macOS: file is compressed (some file-systems)
UF_TRACKED = 0x00000040 # macOS: file renames and deletes are tracked
UF_HIDDEN = 0x00008000 # macOS: file should not be displayed
SF_ARCHIVED = 0x00010000 # file may be archived
SF_IMMUTABLE = 0x00020000 # file may not be changed
SF_APPEND = 0x00040000 # file may only be appended to
SF_RESTRICTED = 0x00080000 # macOS: entitlement required for writing
SF_NOUNLINK = 0x00100000 # file may not be renamed or deleted
SF_SNAPSHOT = 0x00200000 # file is a snapshot file

Expand Down
17 changes: 17 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
from test.support import TESTFN, FakePath

TESTFN2 = TESTFN + "2"

MACOS = sys.platform.startswith("darwin")
if MACOS:
MACOS_VERS = tuple(int(x) for x in os.uname().release.split('.'))

try:
import grp
import pwd
Expand Down Expand Up @@ -472,6 +476,19 @@ def _chflags_raiser(path, flags, *, follow_symlinks=True):
finally:
os.chflags = old_chflags

@unittest.skipUnless(MACOS and MACOS_VERS >= (14, 0, 0),
"requires macOS 10.10 or newer")
def test_copystat_macos_restricted(self):
# A sample restricted file. This file has always been present on macOS.
src = '/System/Library/CoreServices/SystemVersion.plist'
dst = os.path.join(self.mkdtemp(), 'file1')
write_file(dst, 'foo')

# Check that the file is restricted befre, but not after copying
self.assertNotEqual(os.stat(src).st_flags & stat.SF_RESTRICTED, 0)
shutil.copystat(src, dst)
self.assertEqual(os.stat(dst).st_flags & stat.SF_RESTRICTED, 0)

@support.skip_unless_xattr
def test_copyxattr(self):
tmp_dir = self.mkdtemp()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``shutil.copystat`` now implements flag copy behavior from ``copyfile(3)``
on macOS 10.10 and above. Patch by Ryan Govostes.