diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 20782544d..28e09689d 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -288,7 +288,7 @@ class FuseOperations(llfuse.Operations): def getxattr(self, inode, name, ctx=None): item = self.get_item(inode) try: - return item.get('xattrs', {})[name] + return item.get('xattrs', {})[name] or b'' except KeyError: raise llfuse.FUSEError(llfuse.ENOATTR) from None diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 8f47b0b96..6d8c23f9f 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -326,6 +326,7 @@ class ArchiverTestCaseBase(BaseTestCase): # 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'') # 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. @@ -1874,8 +1875,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): in_fn = 'input/fusexattr' out_fn = os.path.join(mountpoint, 'input', 'fusexattr') if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): - assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ] + 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 else: assert xattr.listxattr(out_fn) == [] try: diff --git a/src/borg/xattr.py b/src/borg/xattr.py index c37d1a819..03c5eae26 100644 --- a/src/borg/xattr.py +++ b/src/borg/xattr.py @@ -35,6 +35,16 @@ def is_enabled(path=None): def get_all(path, follow_symlinks=True): + """ + Return all extended attributes on *path* as 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. + + The returned mapping maps xattr names (str) to values (bytes or None). + None indicates, as a xattr value, an empty value, i.e. a value of length zero. + """ try: result = {} names = listxattr(path, follow_symlinks=follow_symlinks) @@ -111,7 +121,7 @@ def split_lstring(buf): class BufferTooSmallError(Exception): - """the buffer given to an xattr function was too small for the result.""" + """the buffer given to a xattr function was too small for the result.""" def _check(rv, path=None, detect_buffer_too_small=False): @@ -346,10 +356,33 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only 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): - pass + """ + 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): - pass + """ + 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. + """