Переглянути джерело

Merge pull request #9115 from ThomasWaldmann/fix/macos-flags-9090

extract: fs flags: use get/set to influence only specific flags, #9039, macOS only
TW 21 годин тому
батько
коміт
631046c63c

+ 1 - 0
src/borg/platform/__init__.py

@@ -41,6 +41,7 @@ elif is_darwin:  # pragma: darwin only
     from .darwin import listxattr, getxattr, setxattr
     from .darwin import acl_get, acl_set
     from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns
+    from .darwin import set_flags
 
 
 def get_birthtime_ns(st, path, fd=None):

+ 64 - 0
src/borg/platform/darwin.pyx

@@ -195,3 +195,67 @@ def _get_birthtime_ns(path, follow_symlinks=False):
     if result != 0:
         raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
     return stat_info.st_birthtimespec.tv_sec * 1_000_000_000 + stat_info.st_birthtimespec.tv_nsec
+
+
+# macOS flags handling: only modify flags documented as settable; preserve all others, see #9090.
+# The man page states UF_COMPRESSED and SF_DATALESS are internal flags and must not be modified
+# from user space. We therefore only modify flags that are documented to be settable by owner or
+# super-user and preserve everything else (including unknown or future flags).
+
+cdef extern from "sys/stat.h":
+    int chflags(const char *path, uint32_t flags)
+    int lchflags(const char *path, uint32_t flags)
+    int fchflags(int fd, uint32_t flags)
+
+
+# Known-good settable flags from macOS chflags(2). We intentionally do NOT include
+# internal flags like UF_COMPRESSED and SF_DATALESS. Resolved once at import time.
+# getattr(..., 0) keeps this importable on non-Darwin platforms or Python versions
+# missing some constants.
+import stat as stat_mod
+
+SETTABLE_FLAG_NAMES = (
+    # Owner-settable (UF_*)
+    'UF_NODUMP',
+    'UF_IMMUTABLE',
+    'UF_APPEND',
+    'UF_OPAQUE',
+    'UF_NOUNLINK',
+    'UF_HIDDEN',
+    # Super-user-settable (SF_*)
+    'SF_ARCHIVED',
+    'SF_IMMUTABLE',
+    'SF_APPEND',
+    # SF_NOUNLINK exists on some BSDs; include defensively
+    'SF_NOUNLINK',
+)
+
+cdef uint32_t SETTABLE_FLAGS_MASK = 0
+for _name in SETTABLE_FLAG_NAMES:
+    SETTABLE_FLAGS_MASK |= <uint32_t> getattr(stat_mod, _name, 0)
+
+
+def set_flags(path, bsd_flags, fd=None):
+    """Set BSD-style flags on macOS, preserving system-managed read-only flags."""
+    # Determine current flags.
+    try:
+        if fd is not None:
+            st = os.fstat(fd)
+        else:
+            st = os.lstat(path)
+        current = st.st_flags
+    except (OSError, AttributeError):
+        # We can't determine the current flags, so better give up than corrupting anything.
+        return
+
+    new_flags = (current & ~SETTABLE_FLAGS_MASK) | (bsd_flags & SETTABLE_FLAGS_MASK)
+
+    # Apply flags.
+    cdef uint32_t c_flags = <uint32_t> new_flags
+    if fd is not None:
+        if fchflags(fd, c_flags) == -1:
+            raise OSError(errno.errno, os.strerror(errno.errno), path)
+    else:
+        path_bytes = os.fsencode(path)
+        if lchflags(path_bytes, c_flags) == -1:
+            raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path_bytes))

+ 23 - 0
src/borg/testsuite/archiver.py

@@ -1534,6 +1534,29 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         assert same_ts_ns(mtime_extracted, mtime_expected)
         # assert same_ts_ns(atime_extracted, atime_expected)  # still broken, but not really important.
 
+    @pytest.mark.skipif(not is_darwin, reason='only for macOS')
+    def test_extract_restores_append_flag(self):
+        if not has_lchflags or not hasattr(stat, 'UF_APPEND'):
+            pytest.skip('BSD flags or UF_APPEND not supported on this platform')
+        # create a file and set the append flag on it
+        self.create_regular_file('appendflag', size=1)
+        src_path = os.path.join(self.input_path, 'appendflag')
+        platform.set_flags(src_path, stat.UF_APPEND)
+        # Verify the flag actually got set; otherwise skip (filesystem may not support it)
+        st = os.lstat(src_path)
+        if (platform.get_flags(src_path, st) & stat.UF_APPEND) == 0:
+            pytest.skip('UF_APPEND not settable on this filesystem')
+        # archive and extract
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        archive = self.repository_location + '::test'
+        self.cmd('create', archive, 'input')
+        with changedir('output'):
+            self.cmd('extract', archive)
+            out_path = os.path.join('input', 'appendflag')
+            st2 = os.lstat(out_path)
+            flags = platform.get_flags(out_path, st2)
+            assert (flags & stat.UF_APPEND) == stat.UF_APPEND
+
     def test_path_normalization(self):
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.create_regular_file('dir1/dir2/file', size=1024 * 80)