Repository: borgbackup/borg

Issue:
null value in fuse
```
$ borg mount manual_backup/borg_original/ mounted-backup/ --debug --foreground
using builtin fallback logging configuration
Enter passphrase for key /run/media/rpfannsc/backups/manual_backup/borg_original: 
TAM not found and not required
mount data cache capacity: 8 chunks
Mounting filesystem
Initializing llfuse
Calling fuse_mount
Calling fuse_lowlevel_new
Calling fuse_session_add_chan
Calling fuse_session_loop
handler raised <class 'TypeError'> exception (expected bytes, NoneType found), terminating main loop.
Terminated main loop because request handler raised exception, re-raising..
Calling fuse_session_remove_chan
Calling fuse_session_destroy
Local Exception.
Traceback (most recent call last):
  File "/usr/lib64/python3.5/site-packages/borg/archiver.py", line 2089, in main
    exit_code = archiver.run(args)
  File "/usr/lib64/python3.5/site-packages/borg/archiver.py", line 2025, in run
    return func(args)
  File "/usr/lib64/python3.5/site-packages/borg/archiver.py", line 91, in wrapper
    return method(self, args, repository=repository, **kwargs)
  File "/usr/lib64/python3.5/site-packages/borg/archiver.py", line 566, in do_mount
    operations.mount(args.mountpoint, args.options, args.foreground)
  File "/usr/lib64/python3.5/site-packages/borg/fuse.py", line 101, in mount
    signal = fuse_main()
  File "/usr/lib64/python3.5/site-packages/borg/fuse.py", line 27, in fuse_main
    return llfuse.main(workers=1)
  File "src/fuse_api.pxi", line 343, in llfuse.main (src/llfuse.c:35968)
  File "src/handlers.pxi", line 624, in llfuse.fuse_getxattr (src/llfuse.c:22336)
TypeError: expected bytes, NoneType found

Platform: Linux ronny 4.10.15-200.fc25.x86_64 #1 SMP Mon May 8 18:46:06 UTC 2017 x86_64 x86_64
Linux: Fedora 25 Twenty Five
Borg: 1.0.10  Python: CPython 3.5.3
PID: 2051  CWD: /run/media/rpfannsc/backups
sys.argv: ['/usr/bin/borg', 'mount', 'manual_backup/borg_original/', 'mounted-backup/', '--debug', '--foreground']
SSH_ORIGINAL_COMMAND: None
```


Discussion:
Is this triggered by mounting it alone, or do you need to run some program on the mounted files?
Im running a borg import in the dataset it happpens when backing up certain files

Resolving patch:
diff --git a/src/borg/fuse.py b/src/borg/fuse.py
--- a/src/borg/fuse.py
+++ b/src/borg/fuse.py
@@ -288,7 +288,7 @@ def listxattr(self, inode, ctx=None):
     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
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -326,6 +326,7 @@ def create_test_files(self):
             # 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 @@ def has_noatime(some_file):
                 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
--- 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 @@ def func(path, name, value, size):
 
 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.
+        """


Test patch: