Explorar o código

Merge pull request #3918 from ThomasWaldmann/platform-xattr

xattr: move to platform pkg, use cython, use bytes, use fd
TW %!s(int64=7) %!d(string=hai) anos
pai
achega
34cd1b22ec

+ 16 - 33
src/borg/archive.py

@@ -1,4 +1,3 @@
-import errno
 import json
 import os
 import socket
@@ -723,33 +722,9 @@ Utilization of max. archive size: {csize_max:.0%}
         acl_set(path, item, self.numeric_owner)
         # chown removes Linux capabilities, so set the extended attributes at the end, after chown, since they include
         # the Linux capabilities in the "security.capability" attribute.
-        xattrs = item.get('xattrs', {})
-        for k, v in xattrs.items():
-            try:
-                xattr.setxattr(fd or path, k, v, follow_symlinks=False)
-            except OSError as e:
-                if e.errno == errno.E2BIG:
-                    # xattr is too big
-                    logger.warning('%s: Value or key of extended attribute %s is too big for this filesystem' %
-                                   (path, k.decode()))
-                    set_ec(EXIT_WARNING)
-                elif e.errno == errno.ENOTSUP:
-                    # xattrs not supported here
-                    logger.warning('%s: Extended attributes are not supported on this filesystem' % path)
-                    set_ec(EXIT_WARNING)
-                elif e.errno == errno.EACCES:
-                    # permission denied to set this specific xattr (this may happen related to security.* keys)
-                    logger.warning('%s: Permission denied when setting extended attribute %s' % (path, k.decode()))
-                    set_ec(EXIT_WARNING)
-                elif e.errno == errno.ENOSPC:
-                    # no space left on device while setting this specific xattr
-                    # ext4 reports ENOSPC when trying to set an xattr with >4kiB while ext4 can only support 4kiB xattrs
-                    # (in this case, this is NOT a "disk full" error, just a ext4 limitation).
-                    logger.warning('%s: No space left on device while setting extended attribute %s (len = %d)' % (
-                        path, k.decode(), len(v)))
-                    set_ec(EXIT_WARNING)
-                else:
-                    raise
+        warning = xattr.set_all(fd or path, item.get('xattrs', {}), follow_symlinks=False)
+        if warning:
+            set_ec(EXIT_WARNING)
         # bsdflags include the immutable flag and need to be set last:
         if not self.nobsdflags and 'bsdflags' in item:
             try:
@@ -972,11 +947,11 @@ class MetadataCollector:
             attrs['group'] = gid2group(st.st_gid)
         return attrs
 
-    def stat_ext_attrs(self, st, path):
+    def stat_ext_attrs(self, st, path, fd=None):
         attrs = {}
         bsdflags = 0
         with backup_io('extended stat'):
-            xattrs = xattr.get_all(path, follow_symlinks=False)
+            xattrs = xattr.get_all(fd or path, follow_symlinks=False)
             if not self.nobsdflags:
                 bsdflags = get_flags(path, st)
             acl_get(path, attrs, st, self.numeric_owner)
@@ -986,9 +961,9 @@ class MetadataCollector:
             attrs['bsdflags'] = bsdflags
         return attrs
 
-    def stat_attrs(self, st, path):
+    def stat_attrs(self, st, path, fd=None):
         attrs = self.stat_simple_attrs(st)
-        attrs.update(self.stat_ext_attrs(st, path))
+        attrs.update(self.stat_ext_attrs(st, path, fd=fd))
         return attrs
 
 
@@ -1144,6 +1119,7 @@ class FilesystemObjectProcessors:
 
     def process_file(self, path, st, cache):
         with self.create_helper(path, st, None) as (item, status, hardlinked, hardlink_master):  # no status yet
+            md = None
             is_special_file = is_special(st.st_mode)
             if not hardlinked or hardlink_master:
                 if not is_special_file:
@@ -1177,12 +1153,19 @@ class FilesystemObjectProcessors:
                         fh = Archive._open_rb(path)
                     with os.fdopen(fh, 'rb') as fd:
                         self.process_file_chunks(item, cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd, fh)))
+                        md = self.metadata_collector.stat_attrs(st, path, fd=fh)
                     if not is_special_file:
                         # we must not memorize special files, because the contents of e.g. a
                         # block or char device will change without its mtime/size/inode changing.
                         cache.memorize_file(path_hash, st, [c.id for c in item.chunks])
                 self.stats.nfiles += 1
-            item.update(self.metadata_collector.stat_attrs(st, path))
+            if md is None:
+                fh = Archive._open_rb(path)
+                try:
+                    md = self.metadata_collector.stat_attrs(st, path, fd=fh)
+                finally:
+                    os.close(fh)
+            item.update(md)
             item.get_size(memorize=True)
             if is_special_file:
                 # we processed a special file like a regular file. reflect that in mode,

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

@@ -6,6 +6,7 @@ Platform-specific APIs.
 Public APIs are documented in platform.base.
 """
 
+from .base import listxattr, getxattr, setxattr, ENOATTR
 from .base import acl_get, acl_set
 from .base import set_flags, get_flags
 from .base import SaveFile, SyncFile, sync_dir, fdatasync, safe_fadvise
@@ -21,12 +22,15 @@ if not sys.platform.startswith(('win32', )):
 
 if sys.platform.startswith('linux'):  # pragma: linux only
     from .linux import API_VERSION as OS_API_VERSION
+    from .linux import listxattr, getxattr, setxattr
     from .linux import acl_get, acl_set
     from .linux import set_flags, get_flags
     from .linux import SyncFile
 elif sys.platform.startswith('freebsd'):  # pragma: freebsd only
     from .freebsd import API_VERSION as OS_API_VERSION
+    from .freebsd import listxattr, getxattr, setxattr
     from .freebsd import acl_get, acl_set
 elif sys.platform == 'darwin':  # pragma: darwin only
     from .darwin import API_VERSION as OS_API_VERSION
+    from .darwin import listxattr, getxattr, setxattr
     from .darwin import acl_get, acl_set

+ 40 - 1
src/borg/platform/base.py

@@ -17,10 +17,49 @@ platform API: that way platform APIs provided by the platform-specific support m
 are correctly composed into the base functionality.
 """
 
-API_VERSION = '1.1_03'
+API_VERSION = '1.2_01'
 
 fdatasync = getattr(os, 'fdatasync', os.fsync)
 
+from .xattr import ENOATTR
+
+
+def listxattr(path, *, follow_symlinks=True):
+    """
+    Return xattr names of a file (list of bytes objects).
+
+    *path* can either be a path (bytes) or an open file descriptor (int).
+    *follow_symlinks* indicates whether symlinks should be followed
+    and only applies when *path* is not an open file descriptor.
+    """
+    return []
+
+
+def getxattr(path, name, *, follow_symlinks=True):
+    """
+    Read xattr and return its value (as bytes).
+
+    *path* can either be a path (bytes) or an open file descriptor (int).
+    *name* is the name of the xattr to read (bytes).
+    *follow_symlinks* indicates whether symlinks should be followed
+    and only applies when *path* is not an open file descriptor.
+    """
+    # as this base dummy implementation returns [] from listxattr,
+    # it must raise here for any given name:
+    raise OSError(ENOATTR, os.strerror(ENOATTR), path)
+
+
+def setxattr(path, name, value, *, follow_symlinks=True):
+    """
+    Write xattr on *path*.
+
+    *path* can either be a path (bytes) or an open file descriptor (int).
+    *name* is the name of the xattr to read (bytes).
+    *value* is the value to write (bytes).
+    *follow_symlinks* indicates whether symlinks should be followed
+    and only applies when *path* is not an open file descriptor.
+    """
+
 
 def acl_get(path, item, st, numeric_owner=False):
     """

+ 59 - 1
src/borg/platform/darwin.pyx

@@ -1,9 +1,26 @@
 import os
 
+from libc.stdint cimport uint32_t
+
 from ..helpers import user2uid, group2gid
 from ..helpers import safe_decode, safe_encode
+from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0
+
+API_VERSION = '1.2_01'
+
+cdef extern from "sys/xattr.h":
+    ssize_t c_listxattr "listxattr" (const char *path, char *list, size_t size, int flags)
+    ssize_t c_flistxattr "flistxattr" (int filedes, char *list, size_t size, int flags)
+
+    ssize_t c_getxattr "getxattr" (const char *path, const char *name, void *value, size_t size, uint32_t pos, int flags)
+    ssize_t c_fgetxattr "fgetxattr" (int filedes, const char *name, void *value, size_t size, uint32_t pos, int flags)
+
+    int c_setxattr "setxattr" (const char *path, const char *name, const void *value, size_t size, uint32_t pos, int flags)
+    int c_fsetxattr "fsetxattr" (int filedes, const char *name, const void *value, size_t size, uint32_t pos, int flags)
+
+    int XATTR_NOFOLLOW
 
-API_VERSION = '1.1_03'
+cdef int XATTR_NOFLAGS = 0x0000
 
 cdef extern from "sys/acl.h":
     ctypedef struct _acl_t:
@@ -18,6 +35,47 @@ cdef extern from "sys/acl.h":
     int ACL_TYPE_EXTENDED
 
 
+def listxattr(path, *, follow_symlinks=True):
+    def func(path, buf, size):
+        if isinstance(path, int):
+            return c_flistxattr(path, <char *> buf, size, XATTR_NOFLAGS)
+        else:
+            if follow_symlinks:
+                return c_listxattr(path, <char *> buf, size, XATTR_NOFLAGS)
+            else:
+                return c_listxattr(path, <char *> buf, size, XATTR_NOFOLLOW)
+
+    n, buf = _listxattr_inner(func, path)
+    return [name for name in split_string0(buf[:n]) if name]
+
+
+def getxattr(path, name, *, follow_symlinks=True):
+    def func(path, name, buf, size):
+        if isinstance(path, int):
+            return c_fgetxattr(path, name, <char *> buf, size, 0, XATTR_NOFLAGS)
+        else:
+            if follow_symlinks:
+                return c_getxattr(path, name, <char *> buf, size, 0, XATTR_NOFLAGS)
+            else:
+                return c_getxattr(path, name, <char *> buf, size, 0, XATTR_NOFOLLOW)
+
+    n, buf = _getxattr_inner(func, path, name)
+    return bytes(buf[:n])
+
+
+def setxattr(path, name, value, *, follow_symlinks=True):
+    def func(path, name, value, size):
+        if isinstance(path, int):
+            return c_fsetxattr(path, name, <char *> value, size, 0, XATTR_NOFLAGS)
+        else:
+            if follow_symlinks:
+                return c_setxattr(path, name, <char *> value, size, 0, XATTR_NOFLAGS)
+            else:
+                return c_setxattr(path, name, <char *> value, size, 0, XATTR_NOFOLLOW)
+
+    _setxattr_inner(func, path, name, value)
+
+
 def _remove_numeric_id_if_possible(acl):
     """Replace the user/group field with the local uid/gid if possible
     """

+ 58 - 1
src/borg/platform/freebsd.pyx

@@ -2,13 +2,29 @@ import os
 
 from ..helpers import posix_acl_use_stored_uid_gid
 from ..helpers import safe_encode, safe_decode
+from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_lstring
 
-API_VERSION = '1.1_03'
+API_VERSION = '1.2_01'
 
 cdef extern from "errno.h":
     int errno
     int EINVAL
 
+cdef extern from "sys/extattr.h":
+    ssize_t c_extattr_list_file "extattr_list_file" (const char *path, int attrnamespace, void *data, size_t nbytes)
+    ssize_t c_extattr_list_link "extattr_list_link" (const char *path, int attrnamespace, void *data, size_t nbytes)
+    ssize_t c_extattr_list_fd "extattr_list_fd" (int fd, int attrnamespace, void *data, size_t nbytes)
+
+    ssize_t c_extattr_get_file "extattr_get_file" (const char *path, int attrnamespace, const char *attrname, void *data, size_t nbytes)
+    ssize_t c_extattr_get_link "extattr_get_link" (const char *path, int attrnamespace, const char *attrname, void *data, size_t nbytes)
+    ssize_t c_extattr_get_fd "extattr_get_fd" (int fd, int attrnamespace, const char *attrname, void *data, size_t nbytes)
+
+    int c_extattr_set_file "extattr_set_file" (const char *path, int attrnamespace, const char *attrname, const void *data, size_t nbytes)
+    int c_extattr_set_link "extattr_set_link" (const char *path, int attrnamespace, const char *attrname, const void *data, size_t nbytes)
+    int c_extattr_set_fd "extattr_set_fd" (int fd, int attrnamespace, const char *attrname, const void *data, size_t nbytes)
+
+    int EXTATTR_NAMESPACE_USER
+
 cdef extern from "sys/types.h":
     int ACL_TYPE_ACCESS
     int ACL_TYPE_DEFAULT
@@ -32,6 +48,47 @@ cdef extern from "unistd.h":
     int _PC_ACL_NFS4
 
 
+def listxattr(path, *, follow_symlinks=True):
+    def func(path, buf, size):
+        if isinstance(path, int):
+            return c_extattr_list_fd(path, EXTATTR_NAMESPACE_USER, <char *> buf, size)
+        else:
+            if follow_symlinks:
+                return c_extattr_list_file(path, EXTATTR_NAMESPACE_USER, <char *> buf, size)
+            else:
+                return c_extattr_list_link(path, EXTATTR_NAMESPACE_USER, <char *> buf, size)
+
+    n, buf = _listxattr_inner(func, path)
+    return [name for name in split_lstring(buf[:n]) if name]
+
+
+def getxattr(path, name, *, follow_symlinks=True):
+    def func(path, name, buf, size):
+        if isinstance(path, int):
+            return c_extattr_get_fd(path, EXTATTR_NAMESPACE_USER, name, <char *> buf, size)
+        else:
+            if follow_symlinks:
+                return c_extattr_get_file(path, EXTATTR_NAMESPACE_USER, name, <char *> buf, size)
+            else:
+                return c_extattr_get_link(path, EXTATTR_NAMESPACE_USER, name, <char *> buf, size)
+
+    n, buf = _getxattr_inner(func, path, name)
+    return bytes(buf[:n])
+
+
+def setxattr(path, name, value, *, follow_symlinks=True):
+    def func(path, name, value, size):
+        if isinstance(path, int):
+            return c_extattr_set_fd(path, EXTATTR_NAMESPACE_USER, name, <char *> value, size)
+        else:
+            if follow_symlinks:
+                return c_extattr_set_file(path, EXTATTR_NAMESPACE_USER, name, <char *> value, size)
+            else:
+                return c_extattr_set_link(path, EXTATTR_NAMESPACE_USER, name, <char *> value, size)
+
+    _setxattr_inner(func, path, name, value)
+
+
 cdef _get_acl(p, type, item, attribute, int flags):
     cdef acl_t acl
     cdef char *text

+ 58 - 1
src/borg/platform/linux.pyx

@@ -8,11 +8,25 @@ from ..helpers import user2uid, group2gid
 from ..helpers import safe_decode, safe_encode
 from .base import SyncFile as BaseSyncFile
 from .base import safe_fadvise
+from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0
 
 from libc cimport errno
 from libc.stdint cimport int64_t
 
-API_VERSION = '1.1_03'
+API_VERSION = '1.2_01'
+
+cdef extern from "attr/xattr.h":
+    ssize_t c_listxattr "listxattr" (const char *path, char *list, size_t size)
+    ssize_t c_llistxattr "llistxattr" (const char *path, char *list, size_t size)
+    ssize_t c_flistxattr "flistxattr" (int filedes, char *list, size_t size)
+
+    ssize_t c_getxattr "getxattr" (const char *path, const char *name, void *value, size_t size)
+    ssize_t c_lgetxattr "lgetxattr" (const char *path, const char *name, void *value, size_t size)
+    ssize_t c_fgetxattr "fgetxattr" (int filedes, const char *name, void *value, size_t size)
+
+    int c_setxattr "setxattr" (const char *path, const char *name, const void *value, size_t size, int flags)
+    int c_lsetxattr "lsetxattr" (const char *path, const char *name, const void *value, size_t size, int flags)
+    int c_fsetxattr "fsetxattr" (int filedes, const char *name, const void *value, size_t size, int flags)
 
 cdef extern from "sys/types.h":
     int ACL_TYPE_ACCESS
@@ -62,6 +76,49 @@ cdef extern from "string.h":
 _comment_re = re.compile(' *#.*', re.M)
 
 
+def listxattr(path, *, follow_symlinks=True):
+    def func(path, buf, size):
+        if isinstance(path, int):
+            return c_flistxattr(path, <char *> buf, size)
+        else:
+            if follow_symlinks:
+                return c_listxattr(path, <char *> buf, size)
+            else:
+                return c_llistxattr(path, <char *> buf, size)
+
+    n, buf = _listxattr_inner(func, path)
+    return [name for name in split_string0(buf[:n])
+            if name and not name.startswith(b'system.posix_acl_')]
+
+
+def getxattr(path, name, *, follow_symlinks=True):
+    def func(path, name, buf, size):
+        if isinstance(path, int):
+            return c_fgetxattr(path, name, <char *> buf, size)
+        else:
+            if follow_symlinks:
+                return c_getxattr(path, name, <char *> buf, size)
+            else:
+                return c_lgetxattr(path, name, <char *> buf, size)
+
+    n, buf = _getxattr_inner(func, path, name)
+    return bytes(buf[:n])
+
+
+def setxattr(path, name, value, *, follow_symlinks=True):
+    def func(path, name, value, size):
+        flags = 0
+        if isinstance(path, int):
+            return c_fsetxattr(path, name, <char *> value, size, flags)
+        else:
+            if follow_symlinks:
+                return c_setxattr(path, name, <char *> value, size, flags)
+            else:
+                return c_lsetxattr(path, name, <char *> value, size, flags)
+
+    _setxattr_inner(func, path, name, value)
+
+
 BSD_TO_LINUX_FLAGS = {
     stat.UF_NODUMP: FS_NODUMP_FL,
     stat.UF_IMMUTABLE: FS_IMMUTABLE_FL,

+ 5 - 0
src/borg/platform/posix.pyx

@@ -1,11 +1,16 @@
 import errno
 import os
 
+from libc.errno cimport errno as c_errno
 
 cdef extern from "wchar.h":
     cdef int wcswidth(const Py_UNICODE *str, size_t n)
 
 
+def get_errno():
+    return c_errno
+
+
 def swidth(s):
     str_len = len(s)
     terminal_width = wcswidth(s, str_len)

+ 96 - 0
src/borg/platform/xattr.py

@@ -0,0 +1,96 @@
+import errno
+import os
+
+from .posix import get_errno
+
+from ..helpers import Buffer
+
+
+try:
+    ENOATTR = errno.ENOATTR
+except AttributeError:
+    # on some platforms, ENOATTR is missing, use ENODATA there
+    ENOATTR = errno.ENODATA
+
+
+buffer = Buffer(bytearray, limit=2**24)
+
+
+def split_string0(buf):
+    """split a list of zero-terminated strings into python not-zero-terminated bytes"""
+    if isinstance(buf, bytearray):
+        buf = bytes(buf)  # use a bytes object, so we return a list of bytes objects
+    return buf.split(b'\0')[:-1]
+
+
+def split_lstring(buf):
+    """split a list of length-prefixed strings into python not-length-prefixed bytes"""
+    result = []
+    mv = memoryview(buf)
+    while mv:
+        length = mv[0]
+        result.append(bytes(mv[1:1 + length]))
+        mv = mv[1 + length:]
+    return result
+
+
+class BufferTooSmallError(Exception):
+    """the buffer given to a xattr function was too small for the result."""
+
+
+def _check(rv, path=None, detect_buffer_too_small=False):
+    if rv < 0:
+        e = get_errno()
+        if detect_buffer_too_small and e == errno.ERANGE:
+            # listxattr and getxattr signal with ERANGE that they need a bigger result buffer.
+            # setxattr signals this way that e.g. a xattr key name is too long / inacceptable.
+            raise BufferTooSmallError
+        else:
+            try:
+                msg = os.strerror(e)
+            except ValueError:
+                msg = ''
+            if isinstance(path, int):
+                path = '<FD %d>' % path
+            raise OSError(e, msg, path)
+    if detect_buffer_too_small and rv >= len(buffer):
+        # freebsd does not error with ERANGE if the buffer is too small,
+        # it just fills the buffer, truncates and returns.
+        # so, we play safe and just assume that result is truncated if
+        # it happens to be a full buffer.
+        raise BufferTooSmallError
+    return rv
+
+
+def _listxattr_inner(func, path):
+    assert isinstance(path, (bytes, int))
+    size = len(buffer)
+    while True:
+        buf = buffer.get(size)
+        try:
+            n = _check(func(path, buf, size), path, detect_buffer_too_small=True)
+        except BufferTooSmallError:
+            size *= 2
+        else:
+            return n, buf
+
+
+def _getxattr_inner(func, path, name):
+    assert isinstance(path, (bytes, int))
+    assert isinstance(name, bytes)
+    size = len(buffer)
+    while True:
+        buf = buffer.get(size)
+        try:
+            n = _check(func(path, name, buf, size), path, detect_buffer_too_small=True)
+        except BufferTooSmallError:
+            size *= 2
+        else:
+            return n, buf
+
+
+def _setxattr_inner(func, path, name, value):
+    assert isinstance(path, (bytes, int))
+    assert isinstance(name, bytes)
+    assert isinstance(value, bytes)
+    _check(func(path, name, value, len(value)), path, detect_buffer_too_small=False)

+ 1 - 1
src/borg/testsuite/__init__.py

@@ -140,7 +140,7 @@ def is_birthtime_fully_supported():
 
 def no_selinux(x):
     # selinux fails our FUSE tests, thus ignore selinux xattrs
-    SELINUX_KEY = 'security.selinux'
+    SELINUX_KEY = b'security.selinux'
     if isinstance(x, dict):
         return {k: v for k, v in x.items() if k != SELINUX_KEY}
     if isinstance(x, list):

+ 16 - 17
src/borg/testsuite/archiver.py

@@ -335,19 +335,20 @@ class ArchiverTestCaseBase(BaseTestCase):
             os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
         self.create_regular_file('fusexattr', size=1)
         if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
+            fn = os.fsencode(os.path.join(self.input_path, 'fusexattr'))
             # ironically, due to the way how fakeroot works, comparing FUSE file xattrs to orig file xattrs
             # will FAIL if fakeroot supports xattrs, thus we only set the xattr if XATTR_FAKEROOT is False.
             # This is because fakeroot with xattr-support does not propagate xattrs of the underlying file
             # into "fakeroot space". Because the xattrs exposed by borgfs are these of an underlying file
             # (from fakeroots point of view) they are invisible to the test process inside the fakeroot.
-            xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.foo', b'bar')
-            xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.empty', b'')
+            xattr.setxattr(fn, b'user.foo', b'bar')
+            xattr.setxattr(fn, b'user.empty', b'')
             # XXX this always fails for me
             # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
             # same for newer ubuntu and centos.
             # if this is supported just on specific platform, platform should be checked first,
             # so that the test setup for all tests using it does not fail here always for others.
-            # xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
+            # xattr.setxattr(os.path.join(self.input_path, 'link1'), b'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
         # FIFO node
         if are_fifos_supported():
             os.mkfifo(os.path.join(self.input_path, 'fifo1'))
@@ -1229,19 +1230,19 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         # We need to manually patch chown to get the behaviour Linux has, since fakeroot does not
         # accurately model the interaction of chown(2) and Linux capabilities, i.e. it does not remove them.
         def patched_fchown(fd, uid, gid):
-            xattr.setxattr(fd, 'security.capability', None, follow_symlinks=False)
+            xattr.setxattr(fd, b'security.capability', b'', follow_symlinks=False)
             fchown(fd, uid, gid)
 
         # The capability descriptor used here is valid and taken from a /usr/bin/ping
         capabilities = b'\x01\x00\x00\x02\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
         self.create_regular_file('file')
-        xattr.setxattr('input/file', 'security.capability', capabilities)
+        xattr.setxattr(b'input/file', b'security.capability', capabilities)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.cmd('create', self.repository_location + '::test', 'input')
         with changedir('output'):
             with patch.object(os, 'fchown', patched_fchown):
                 self.cmd('extract', self.repository_location + '::test')
-            assert xattr.getxattr('input/file', 'security.capability') == capabilities
+            assert xattr.getxattr(b'input/file', b'security.capability') == capabilities
 
     @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of'
                                                          'fakeroot')
@@ -1256,23 +1257,22 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             raise OSError(errno.EACCES, 'EACCES')
 
         self.create_regular_file('file')
-        xattr.setxattr('input/file', 'attribute', 'value')
+        xattr.setxattr(b'input/file', b'attribute', b'value')
         self.cmd('init', self.repository_location, '-e' 'none')
         self.cmd('create', self.repository_location + '::test', 'input')
         with changedir('output'):
             input_abspath = os.path.abspath('input/file')
             with patch.object(xattr, 'setxattr', patched_setxattr_E2BIG):
                 out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING)
-                assert out == (input_abspath + ': Value or key of extended attribute attribute is too big for this '
-                                               'filesystem\n')
+                assert '>: Value or key of extended attribute attribute is too big for this filesystem\n' in out
             os.remove(input_abspath)
             with patch.object(xattr, 'setxattr', patched_setxattr_ENOTSUP):
                 out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING)
-                assert out == (input_abspath + ': Extended attributes are not supported on this filesystem\n')
+                assert '>: Extended attributes are not supported on this filesystem\n' in out
             os.remove(input_abspath)
             with patch.object(xattr, 'setxattr', patched_setxattr_EACCES):
                 out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING)
-                assert out == (input_abspath + ': Permission denied when setting extended attribute attribute\n')
+                assert '>: Permission denied when setting extended attribute attribute\n' in out
             assert os.path.isfile(input_abspath)
 
     def test_path_normalization(self):
@@ -2183,16 +2183,15 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             # list/read xattrs
             try:
                 in_fn = 'input/fusexattr'
-                out_fn = os.path.join(mountpoint, 'input', 'fusexattr')
+                out_fn = os.fsencode(os.path.join(mountpoint, 'input', 'fusexattr'))
                 if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
-                    assert sorted(no_selinux(xattr.listxattr(out_fn))) == ['user.empty', 'user.foo', ]
-                    assert xattr.getxattr(out_fn, 'user.foo') == b'bar'
-                    # Special case: getxattr returns None (not b'') when reading an empty xattr.
-                    assert xattr.getxattr(out_fn, 'user.empty') is None
+                    assert sorted(no_selinux(xattr.listxattr(out_fn))) == [b'user.empty', b'user.foo', ]
+                    assert xattr.getxattr(out_fn, b'user.foo') == b'bar'
+                    assert xattr.getxattr(out_fn, b'user.empty') == b''
                 else:
                     assert xattr.listxattr(out_fn) == []
                     try:
-                        xattr.getxattr(out_fn, 'user.foo')
+                        xattr.getxattr(out_fn, b'user.foo')
                     except OSError as e:
                         assert e.errno == llfuse.ENOATTR
                     else:

+ 27 - 21
src/borg/testsuite/xattr.py

@@ -4,7 +4,8 @@ import unittest
 
 import pytest
 
-from ..xattr import is_enabled, getxattr, setxattr, listxattr, buffer, split_lstring
+from ..platform.xattr import buffer, split_lstring
+from ..xattr import is_enabled, getxattr, setxattr, listxattr
 from . import BaseTestCase
 
 
@@ -21,43 +22,48 @@ class XattrTestCase(BaseTestCase):
 
     def assert_equal_se(self, is_x, want_x):
         # check 2 xattr lists for equality, but ignore security.selinux attr
-        is_x = set(is_x) - {'security.selinux'}
+        is_x = set(is_x) - {b'security.selinux'}
         want_x = set(want_x)
         self.assert_equal(is_x, want_x)
 
     def test(self):
-        self.assert_equal_se(listxattr(self.tmpfile.name), [])
-        self.assert_equal_se(listxattr(self.tmpfile.fileno()), [])
-        self.assert_equal_se(listxattr(self.symlink), [])
-        setxattr(self.tmpfile.name, 'user.foo', b'bar')
-        setxattr(self.tmpfile.fileno(), 'user.bar', b'foo')
-        setxattr(self.tmpfile.name, 'user.empty', None)
-        self.assert_equal_se(listxattr(self.tmpfile.name), ['user.foo', 'user.bar', 'user.empty'])
-        self.assert_equal_se(listxattr(self.tmpfile.fileno()), ['user.foo', 'user.bar', 'user.empty'])
-        self.assert_equal_se(listxattr(self.symlink), ['user.foo', 'user.bar', 'user.empty'])
-        self.assert_equal_se(listxattr(self.symlink, follow_symlinks=False), [])
-        self.assert_equal(getxattr(self.tmpfile.name, 'user.foo'), b'bar')
-        self.assert_equal(getxattr(self.tmpfile.fileno(), 'user.foo'), b'bar')
-        self.assert_equal(getxattr(self.symlink, 'user.foo'), b'bar')
-        self.assert_equal(getxattr(self.tmpfile.name, 'user.empty'), None)
+        tmp_fn = os.fsencode(self.tmpfile.name)
+        tmp_lfn = os.fsencode(self.symlink)
+        tmp_fd = self.tmpfile.fileno()
+        self.assert_equal_se(listxattr(tmp_fn), [])
+        self.assert_equal_se(listxattr(tmp_fd), [])
+        self.assert_equal_se(listxattr(tmp_lfn), [])
+        setxattr(tmp_fn, b'user.foo', b'bar')
+        setxattr(tmp_fd, b'user.bar', b'foo')
+        setxattr(tmp_fn, b'user.empty', b'')
+        self.assert_equal_se(listxattr(tmp_fn), [b'user.foo', b'user.bar', b'user.empty'])
+        self.assert_equal_se(listxattr(tmp_fd), [b'user.foo', b'user.bar', b'user.empty'])
+        self.assert_equal_se(listxattr(tmp_lfn), [b'user.foo', b'user.bar', b'user.empty'])
+        self.assert_equal_se(listxattr(tmp_lfn, follow_symlinks=False), [])
+        self.assert_equal(getxattr(tmp_fn, b'user.foo'), b'bar')
+        self.assert_equal(getxattr(tmp_fd, b'user.foo'), b'bar')
+        self.assert_equal(getxattr(tmp_lfn, b'user.foo'), b'bar')
+        self.assert_equal(getxattr(tmp_fn, b'user.empty'), b'')
 
     def test_listxattr_buffer_growth(self):
+        tmp_fn = os.fsencode(self.tmpfile.name)
         # make it work even with ext4, which imposes rather low limits
         buffer.resize(size=64, init=True)
         # xattr raw key list will be size 9 * (10 + 1), which is > 64
-        keys = ['user.attr%d' % i for i in range(9)]
+        keys = [b'user.attr%d' % i for i in range(9)]
         for key in keys:
-            setxattr(self.tmpfile.name, key, b'x')
-        got_keys = listxattr(self.tmpfile.name)
+            setxattr(tmp_fn, key, b'x')
+        got_keys = listxattr(tmp_fn)
         self.assert_equal_se(got_keys, keys)
         self.assert_equal(len(buffer), 128)
 
     def test_getxattr_buffer_growth(self):
+        tmp_fn = os.fsencode(self.tmpfile.name)
         # make it work even with ext4, which imposes rather low limits
         buffer.resize(size=64, init=True)
         value = b'x' * 126
-        setxattr(self.tmpfile.name, 'user.big', value)
-        got_value = getxattr(self.tmpfile.name, 'user.big')
+        setxattr(tmp_fn, b'user.big', value)
+        got_value = getxattr(tmp_fn, b'user.big')
         self.assert_equal(value, got_value)
         self.assert_equal(len(buffer), 128)
 

+ 69 - 342
src/borg/xattr.py

@@ -2,25 +2,15 @@
 
 import errno
 import os
-import re
-import subprocess
-import sys
 import tempfile
-from ctypes import CDLL, create_string_buffer, c_ssize_t, c_size_t, c_char_p, c_int, c_uint32, get_errno
-from ctypes.util import find_library
-from distutils.version import LooseVersion
 
-from .helpers import Buffer, prepare_subprocess_env
+from .logger import create_logger
 
+logger = create_logger()
 
-try:
-    ENOATTR = errno.ENOATTR
-except AttributeError:
-    # on some platforms, ENOATTR is missing, use ENODATA there
-    ENOATTR = errno.ENODATA
+from .platform import listxattr, getxattr, setxattr, ENOATTR
 
-
-buffer = Buffer(create_string_buffer, limit=2**24)
+XATTR_FAKEROOT = True  # fakeroot with xattr support required (>= 1.20.2?)
 
 
 def is_enabled(path=None):
@@ -28,13 +18,13 @@ def is_enabled(path=None):
     """
     with tempfile.NamedTemporaryFile(dir=path, prefix='borg-tmp') as fd:
         try:
-            setxattr(fd.fileno(), 'user.name', b'value')
+            setxattr(fd.fileno(), b'user.name', b'value')
         except OSError:
             return False
-        return getxattr(fd.fileno(), 'user.name') == b'value'
+        return getxattr(fd.fileno(), b'user.name') == b'value'
 
 
-def get_all(path, follow_symlinks=True):
+def get_all(path, follow_symlinks=False):
     """
     Return all extended attributes on *path* as a mapping.
 
@@ -42,349 +32,86 @@ def get_all(path, follow_symlinks=True):
     *follow_symlinks* indicates whether symlinks should be followed
     and only applies when *path* is not an open file descriptor.
 
-    The returned mapping maps xattr names (str) to values (bytes or None).
+    The returned mapping maps xattr names (bytes) to values (bytes or None).
     None indicates, as a xattr value, an empty value, i.e. a value of length zero.
     """
+    if isinstance(path, str):
+        path = os.fsencode(path)
+    result = {}
     try:
-        result = {}
         names = listxattr(path, follow_symlinks=follow_symlinks)
         for name in names:
             try:
-                result[name] = getxattr(path, name, follow_symlinks=follow_symlinks)
+                # xattr name is a bytes object, we directly use it.
+                # if we get an empty xattr value (b''), we store None into the result dict -
+                # borg always did it like that...
+                result[name] = getxattr(path, name, follow_symlinks=follow_symlinks) or None
             except OSError as e:
-                # if we get ENOATTR, a race has happened: xattr names were deleted after list.
-                # we just ignore the now missing ones. if you want consistency, do snapshots.
-                if e.errno != ENOATTR:
+                name_str = name.decode()
+                if isinstance(path, int):
+                    path_str = '<FD %d>' % path
+                else:
+                    path_str = os.fsdecode(path)
+                if e.errno == ENOATTR:
+                    # if we get ENOATTR, a race has happened: xattr names were deleted after list.
+                    # we just ignore the now missing ones. if you want consistency, do snapshots.
+                    pass
+                elif e.errno == errno.EPERM:
+                    # we were not permitted to read this attribute, still can continue trying to read others
+                    logger.warning('%s: Operation not permitted when reading extended attribute %s' % (
+                                   path_str, name_str))
+                else:
                     raise
-        return result
     except OSError as e:
         if e.errno in (errno.ENOTSUP, errno.EPERM):
-            return {}
-
-
-libc_name = find_library('c')
-if libc_name is None:
-    # find_library didn't work, maybe we are on some minimal system that misses essential
-    # tools used by find_library, like ldconfig, gcc/cc, objdump.
-    # so we can only try some "usual" names for the C library:
-    if sys.platform.startswith('linux'):
-        libc_name = 'libc.so.6'
-    elif sys.platform.startswith(('freebsd', 'netbsd')):
-        libc_name = 'libc.so'
-    elif sys.platform == 'darwin':
-        libc_name = 'libc.dylib'
-    else:
-        msg = "Can't find C library. No fallback known. Try installing ldconfig, gcc/cc or objdump."
-        print(msg, file=sys.stderr)  # logger isn't initialized at this stage
-        raise Exception(msg)
-
-# If we are running with fakeroot on Linux, then use the xattr functions of fakeroot. This is needed by
-# the 'test_extract_capabilities' test, but also allows xattrs to work with fakeroot on Linux in normal use.
-# TODO: Check whether fakeroot supports xattrs on all platforms supported below.
-# TODO: If that's the case then we can make Borg fakeroot-xattr-compatible on these as well.
-XATTR_FAKEROOT = False
-if sys.platform.startswith('linux'):
-    LD_PRELOAD = os.environ.get('LD_PRELOAD', '')
-    preloads = re.split("[ :]", LD_PRELOAD)
-    for preload in preloads:
-        if preload.startswith("libfakeroot"):
-            env = prepare_subprocess_env(system=True)
-            fakeroot_output = subprocess.check_output(['fakeroot', '-v'], env=env)
-            fakeroot_version = LooseVersion(fakeroot_output.decode('ascii').split()[-1])
-            if fakeroot_version >= LooseVersion("1.20.2"):
-                # 1.20.2 has been confirmed to have xattr support
-                # 1.18.2 has been confirmed not to have xattr support
-                # Versions in-between are unknown
-                libc_name = preload
-                XATTR_FAKEROOT = True
-            break
-
-try:
-    libc = CDLL(libc_name, use_errno=True)
-except OSError as e:
-    msg = "Can't find C library [%s]. Try installing ldconfig, gcc/cc or objdump." % e
-    raise Exception(msg)
-
-
-def split_string0(buf):
-    """split a list of zero-terminated strings into python not-zero-terminated bytes"""
-    return buf.split(b'\0')[:-1]
-
-
-def split_lstring(buf):
-    """split a list of length-prefixed strings into python not-length-prefixed bytes"""
-    result = []
-    mv = memoryview(buf)
-    while mv:
-        length = mv[0]
-        result.append(bytes(mv[1:1 + length]))
-        mv = mv[1 + length:]
-    return result
-
-
-class BufferTooSmallError(Exception):
-    """the buffer given to a xattr function was too small for the result."""
-
-
-def _check(rv, path=None, detect_buffer_too_small=False):
-    if rv < 0:
-        e = get_errno()
-        if detect_buffer_too_small and e == errno.ERANGE:
-            # listxattr and getxattr signal with ERANGE that they need a bigger result buffer.
-            # setxattr signals this way that e.g. a xattr key name is too long / inacceptable.
-            raise BufferTooSmallError
+            # if xattrs are not supported on the filesystem, we give up.
+            # EPERM might be raised by listxattr.
+            pass
         else:
-            try:
-                msg = os.strerror(e)
-            except ValueError:
-                msg = ''
-            if isinstance(path, int):
-                path = '<FD %d>' % path
-            raise OSError(e, msg, path)
-    if detect_buffer_too_small and rv >= len(buffer):
-        # freebsd does not error with ERANGE if the buffer is too small,
-        # it just fills the buffer, truncates and returns.
-        # so, we play sure and just assume that result is truncated if
-        # it happens to be a full buffer.
-        raise BufferTooSmallError
-    return rv
+            raise
+    return result
 
 
-def _listxattr_inner(func, path):
-    if isinstance(path, str):
-        path = os.fsencode(path)
-    size = len(buffer)
-    while True:
-        buf = buffer.get(size)
-        try:
-            n = _check(func(path, buf, size), path, detect_buffer_too_small=True)
-        except BufferTooSmallError:
-            size *= 2
-        else:
-            return n, buf.raw
+def set_all(path, xattrs, follow_symlinks=False):
+    """
+    Set all extended attributes on *path* from a mapping.
 
+    *path* can either be a path (str or bytes) or an open file descriptor (int).
+    *follow_symlinks* indicates whether symlinks should be followed
+    and only applies when *path* is not an open file descriptor.
+    *xattrs* is mapping maps xattr names (bytes) to values (bytes or None).
+    None indicates, as a xattr value, an empty value, i.e. a value of length zero.
 
-def _getxattr_inner(func, path, name):
+    Return warning status (True means a non-fatal exception has happened and was dealt with).
+    """
     if isinstance(path, str):
         path = os.fsencode(path)
-    name = os.fsencode(name)
-    size = len(buffer)
-    while True:
-        buf = buffer.get(size)
+    warning = False
+    for k, v in xattrs.items():
         try:
-            n = _check(func(path, name, buf, size), path, detect_buffer_too_small=True)
-        except BufferTooSmallError:
-            size *= 2
-        else:
-            return n, buf.raw
-
-
-def _setxattr_inner(func, path, name, value):
-    if isinstance(path, str):
-        path = os.fsencode(path)
-    name = os.fsencode(name)
-    value = value and os.fsencode(value)
-    size = len(value) if value else 0
-    _check(func(path, name, value, size), path, detect_buffer_too_small=False)
-
-
-if sys.platform.startswith('linux'):  # pragma: linux only
-    libc.llistxattr.argtypes = (c_char_p, c_char_p, c_size_t)
-    libc.llistxattr.restype = c_ssize_t
-    libc.flistxattr.argtypes = (c_int, c_char_p, c_size_t)
-    libc.flistxattr.restype = c_ssize_t
-    libc.lsetxattr.argtypes = (c_char_p, c_char_p, c_char_p, c_size_t, c_int)
-    libc.lsetxattr.restype = c_int
-    libc.fsetxattr.argtypes = (c_int, c_char_p, c_char_p, c_size_t, c_int)
-    libc.fsetxattr.restype = c_int
-    libc.lgetxattr.argtypes = (c_char_p, c_char_p, c_char_p, c_size_t)
-    libc.lgetxattr.restype = c_ssize_t
-    libc.fgetxattr.argtypes = (c_int, c_char_p, c_char_p, c_size_t)
-    libc.fgetxattr.restype = c_ssize_t
-
-    def listxattr(path, *, follow_symlinks=True):
-        def func(path, buf, size):
+            # the key k is a bytes object due to msgpack unpacking it as such.
+            # if we have a None value, it means "empty", so give b'' to setxattr in that case:
+            setxattr(path, k, v or b'', follow_symlinks=follow_symlinks)
+        except OSError as e:
+            warning = True
+            k_str = k.decode()
             if isinstance(path, int):
-                return libc.flistxattr(path, buf, size)
+                path_str = '<FD %d>' % path
             else:
-                if follow_symlinks:
-                    return libc.listxattr(path, buf, size)
-                else:
-                    return libc.llistxattr(path, buf, size)
-
-        n, buf = _listxattr_inner(func, path)
-        return [os.fsdecode(name) for name in split_string0(buf[:n])
-                if name and not name.startswith(b'system.posix_acl_')]
-
-    def getxattr(path, name, *, follow_symlinks=True):
-        def func(path, name, buf, size):
-            if isinstance(path, int):
-                return libc.fgetxattr(path, name, buf, size)
+                path_str = os.fsdecode(path)
+            if e.errno == errno.E2BIG:
+                logger.warning('%s: Value or key of extended attribute %s is too big for this filesystem' % (
+                               path_str, k_str))
+            elif e.errno == errno.ENOTSUP:
+                logger.warning('%s: Extended attributes are not supported on this filesystem' % path_str)
+            elif e.errno == errno.EACCES:
+                # permission denied to set this specific xattr (this may happen related to security.* keys)
+                logger.warning('%s: Permission denied when setting extended attribute %s' % (path_str, k_str))
+            elif e.errno == errno.ENOSPC:
+                # ext4 reports ENOSPC when trying to set an xattr with >4kiB while ext4 can only support 4kiB xattrs
+                # (in this case, this is NOT a "disk full" error, just a ext4 limitation).
+                logger.warning('%s: No space left on device while setting extended attribute %s (len = %d)' % (
+                               path_str, k_str, len(v)))
             else:
-                if follow_symlinks:
-                    return libc.getxattr(path, name, buf, size)
-                else:
-                    return libc.lgetxattr(path, name, buf, size)
-
-        n, buf = _getxattr_inner(func, path, name)
-        return buf[:n] or None
-
-    def setxattr(path, name, value, *, follow_symlinks=True):
-        def func(path, name, value, size):
-            flags = 0
-            if isinstance(path, int):
-                return libc.fsetxattr(path, name, value, size, flags)
-            else:
-                if follow_symlinks:
-                    return libc.setxattr(path, name, value, size, flags)
-                else:
-                    return libc.lsetxattr(path, name, value, size, flags)
-
-        _setxattr_inner(func, path, name, value)
-
-elif sys.platform == 'darwin':  # pragma: darwin only
-    libc.listxattr.argtypes = (c_char_p, c_char_p, c_size_t, c_int)
-    libc.listxattr.restype = c_ssize_t
-    libc.flistxattr.argtypes = (c_int, c_char_p, c_size_t)
-    libc.flistxattr.restype = c_ssize_t
-    libc.setxattr.argtypes = (c_char_p, c_char_p, c_char_p, c_size_t, c_uint32, c_int)
-    libc.setxattr.restype = c_int
-    libc.fsetxattr.argtypes = (c_int, c_char_p, c_char_p, c_size_t, c_uint32, c_int)
-    libc.fsetxattr.restype = c_int
-    libc.getxattr.argtypes = (c_char_p, c_char_p, c_char_p, c_size_t, c_uint32, c_int)
-    libc.getxattr.restype = c_ssize_t
-    libc.fgetxattr.argtypes = (c_int, c_char_p, c_char_p, c_size_t, c_uint32, c_int)
-    libc.fgetxattr.restype = c_ssize_t
-
-    XATTR_NOFLAGS = 0x0000
-    XATTR_NOFOLLOW = 0x0001
-
-    def listxattr(path, *, follow_symlinks=True):
-        def func(path, buf, size):
-            if isinstance(path, int):
-                return libc.flistxattr(path, buf, size, XATTR_NOFLAGS)
-            else:
-                if follow_symlinks:
-                    return libc.listxattr(path, buf, size, XATTR_NOFLAGS)
-                else:
-                    return libc.listxattr(path, buf, size, XATTR_NOFOLLOW)
-
-        n, buf = _listxattr_inner(func, path)
-        return [os.fsdecode(name) for name in split_string0(buf[:n]) if name]
-
-    def getxattr(path, name, *, follow_symlinks=True):
-        def func(path, name, buf, size):
-            if isinstance(path, int):
-                return libc.fgetxattr(path, name, buf, size, 0, XATTR_NOFLAGS)
-            else:
-                if follow_symlinks:
-                    return libc.getxattr(path, name, buf, size, 0, XATTR_NOFLAGS)
-                else:
-                    return libc.getxattr(path, name, buf, size, 0, XATTR_NOFOLLOW)
-
-        n, buf = _getxattr_inner(func, path, name)
-        return buf[:n] or None
-
-    def setxattr(path, name, value, *, follow_symlinks=True):
-        def func(path, name, value, size):
-            if isinstance(path, int):
-                return libc.fsetxattr(path, name, value, size, 0, XATTR_NOFLAGS)
-            else:
-                if follow_symlinks:
-                    return libc.setxattr(path, name, value, size, 0, XATTR_NOFLAGS)
-                else:
-                    return libc.setxattr(path, name, value, size, 0, XATTR_NOFOLLOW)
-
-        _setxattr_inner(func, path, name, value)
-
-elif sys.platform.startswith('freebsd'):  # pragma: freebsd only
-    libc.extattr_list_fd.argtypes = (c_int, c_int, c_char_p, c_size_t)
-    libc.extattr_list_fd.restype = c_ssize_t
-    libc.extattr_list_link.argtypes = (c_char_p, c_int, c_char_p, c_size_t)
-    libc.extattr_list_link.restype = c_ssize_t
-    libc.extattr_list_file.argtypes = (c_char_p, c_int, c_char_p, c_size_t)
-    libc.extattr_list_file.restype = c_ssize_t
-    libc.extattr_get_fd.argtypes = (c_int, c_int, c_char_p, c_char_p, c_size_t)
-    libc.extattr_get_fd.restype = c_ssize_t
-    libc.extattr_get_link.argtypes = (c_char_p, c_int, c_char_p, c_char_p, c_size_t)
-    libc.extattr_get_link.restype = c_ssize_t
-    libc.extattr_get_file.argtypes = (c_char_p, c_int, c_char_p, c_char_p, c_size_t)
-    libc.extattr_get_file.restype = c_ssize_t
-    libc.extattr_set_fd.argtypes = (c_int, c_int, c_char_p, c_char_p, c_size_t)
-    libc.extattr_set_fd.restype = c_int
-    libc.extattr_set_link.argtypes = (c_char_p, c_int, c_char_p, c_char_p, c_size_t)
-    libc.extattr_set_link.restype = c_int
-    libc.extattr_set_file.argtypes = (c_char_p, c_int, c_char_p, c_char_p, c_size_t)
-    libc.extattr_set_file.restype = c_int
-    ns = EXTATTR_NAMESPACE_USER = 0x0001
-
-    def listxattr(path, *, follow_symlinks=True):
-        def func(path, buf, size):
-            if isinstance(path, int):
-                return libc.extattr_list_fd(path, ns, buf, size)
-            else:
-                if follow_symlinks:
-                    return libc.extattr_list_file(path, ns, buf, size)
-                else:
-                    return libc.extattr_list_link(path, ns, buf, size)
-
-        n, buf = _listxattr_inner(func, path)
-        return [os.fsdecode(name) for name in split_lstring(buf[:n]) if name]
-
-    def getxattr(path, name, *, follow_symlinks=True):
-        def func(path, name, buf, size):
-            if isinstance(path, int):
-                return libc.extattr_get_fd(path, ns, name, buf, size)
-            else:
-                if follow_symlinks:
-                    return libc.extattr_get_file(path, ns, name, buf, size)
-                else:
-                    return libc.extattr_get_link(path, ns, name, buf, size)
-
-        n, buf = _getxattr_inner(func, path, name)
-        return buf[:n] or None
-
-    def setxattr(path, name, value, *, follow_symlinks=True):
-        def func(path, name, value, size):
-            if isinstance(path, int):
-                return libc.extattr_set_fd(path, ns, name, value, size)
-            else:
-                if follow_symlinks:
-                    return libc.extattr_set_file(path, ns, name, value, size)
-                else:
-                    return libc.extattr_set_link(path, ns, name, value, size)
-
-        _setxattr_inner(func, path, name, value)
-
-else:  # pragma: unknown platform only
-    def listxattr(path, *, follow_symlinks=True):
-        """
-        Return list of xattr names on a file.
-
-        *path* can either be a path (str or bytes) or an open file descriptor (int).
-        *follow_symlinks* indicates whether symlinks should be followed
-        and only applies when *path* is not an open file descriptor.
-        """
-        return []
-
-    def getxattr(path, name, *, follow_symlinks=True):
-        """
-        Read xattr and return its value (as bytes) or None if its empty.
-
-        *path* can either be a path (str or bytes) or an open file descriptor (int).
-        *name* is the name of the xattr to read (str).
-        *follow_symlinks* indicates whether symlinks should be followed
-        and only applies when *path* is not an open file descriptor.
-        """
-
-    def setxattr(path, name, value, *, follow_symlinks=True):
-        """
-        Write xattr on *path*.
-
-        *path* can either be a path (str or bytes) or an open file descriptor (int).
-        *name* is the name of the xattr to read (str).
-        *value* is the value to write. It is either bytes or None. The latter
-        signals that the value shall be empty (size equals zero).
-        *follow_symlinks* indicates whether symlinks should be followed
-        and only applies when *path* is not an open file descriptor.
-        """
+                raise
+    return warning