Repository: borgbackup/borg

Issue:
xattrs not supported warning during extract fails due to % in filename
<!--
Thank you for reporting an issue.

*IMPORTANT* -  *before* creating a new issue please look around:
 - Borgbackup documentation: http://borgbackup.readthedocs.io/en/stable/index.html
 - FAQ: https://borgbackup.readthedocs.io/en/stable/faq.html
 and
 - open issues in Github tracker: https://github.com/borgbackup/borg/issues
  
If you cannot find a similar problem, then create a new issue.

Please fill in as much of the template as possible.
-->

## Have you checked borgbackup docs, FAQ, and open Github issues?

Yes

## Is this a BUG / ISSUE report or a QUESTION?

BUG

## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).

1.1.15

#### Operating system (distribution) and version.

Ubuntu 20.04

#### Hardware / network configuration, and filesystems used.

Local repo, Ext4

#### How much data is handled by borg?

4.6TiB of Borg repositories total, archive in question is 776GiB but extract fails at 57GiB due to this bug.

#### Full borg commandline that lead to the problem (leave away excludes and passwords)

borg extract /backups/oldeyelow/repo::fluffy-2020-08-22T23:13:37.716949

## Describe the problem you're observing.

Restoring a file with `%p` in the name that includes xattrs (from Windows Subsystem for Linux) onto an ext4 partition causes an exception because the filename gets expanded into the LHS of the `%` formatting operator.  Excerpted log and full stack trace below.


#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.

Yes, same results every time I attempted to extract the archive.  I tried using `--errors` to suppress, but the faulty logic still gets executed.  The version I'm using doesn't yet support `--noxattrs` as now exists in the `1.1-maint` branch, so that wasn't an option.  I therefore edited `/usr/lib/python3/dist-packages/borg/archive.py` to change the logic so that the LHS is always a constant string.  This allowed the archive extract completely while still generating the intended warnings.

#### Include any warning/errors/backtraces from the system logs

```
[... many more "xattrs not supported" warnings like the following line ...]
/data/oldbackups/flufftest/mnt/c/Users/isaac/AppData/Roaming/Mozilla/Firefox/Profiles/2416qrmh.default-release/storage/default/https+++mail.google.com/idb/953658429glmaaviyle-ks-w.files: when setting extended attribute system.wsl_case_sensitive: xattrs not supported on this filesystem
Local Exception
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/borg/archive.py", line 771, in restore_attrs
    xattr.setxattr(fd or path, k, v, follow_symlinks=False)
  File "/usr/lib/python3/dist-packages/borg/xattr.py", line 249, in setxattr
    _setxattr_inner(func, path, name, value)
  File "/usr/lib/python3/dist-packages/borg/xattr.py", line 188, in _setxattr_inner
    _check(func(path, name, value, size), path, detect_buffer_too_small=False)
  File "/usr/lib/python3/dist-packages/borg/xattr.py", line 143, in _check
    raise OSError(e, msg, path)
OSError: [Errno 95] Operation not supported: b'/data/oldbackups/flufftest/mnt/c/Users/isaac/AppData/Roaming/Mozilla/Firefox/Profiles/2416qrmh.default-release/storage/default/https+++mail.google.com/idb/555926040iddano6l%p4u0tcnoermn.files'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4621, in main
    exit_code = archiver.run(args)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4553, in run
    return set_ec(func(args))
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 177, in wrapper
    return method(self, args, repository=repository, **kwargs)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 190, in wrapper
    return method(self, args, repository=repository, manifest=manifest, key=key, archive=archive, **kwargs)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 793, in do_extract
    archive.extract_item(dir_item, stdout=stdout)
  File "/usr/lib/python3/dist-packages/borg/archive.py", line 684, in extract_item
    self.restore_attrs(path, item)
  File "/usr/lib/python3/dist-packages/borg/archive.py", line 788, in restore_attrs
    logger.warning(msg_format % err_str)
ValueError: unsupported format character 'p' (0x70) at index 173

Platform: Linux daneel 5.4.0-90-generic #101-Ubuntu SMP Fri Oct 15 20:00:55 UTC 2021 x86_64
Linux: Unknown Linux  
Borg: 1.1.15  Python: CPython 3.8.10 msgpack: 0.5.6
PID: 203540  CWD: /data/oldbackups/flufftest
sys.argv: ['/usr/bin/borg', 'extract', '/backups/oldeyelow/repo::fluffy-2020-08-22T23:13:37.716949']
SSH_ORIGINAL_COMMAND: None

```


Discussion:

Resolving patch:
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
@@ -3459,6 +3459,38 @@ def test_detect_attic_repo(self):
             output = self.cmd(*args, fork=True, exit_code=2)
             assert 'Attic repository detected.' in output
 
+    # derived from test_extract_xattrs_errors()
+    @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of'
+                                                         'fakeroot')
+    def test_do_not_fail_when_percent_is_in_xattr_name(self):
+        """https://github.com/borgbackup/borg/issues/6063"""
+        def patched_setxattr_EACCES(*args, **kwargs):
+            raise OSError(errno.EACCES, 'EACCES')
+
+        self.create_regular_file('file')
+        xattr.setxattr(b'input/file', b'user.attribute%p', b'value')
+        self.cmd('init', self.repository_location, '-e' 'none')
+        self.cmd('create', self.repository_location + '::test', 'input')
+        with changedir('output'):
+            with patch.object(xattr, 'setxattr', patched_setxattr_EACCES):
+                self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING)
+
+    # derived from test_extract_xattrs_errors()
+    @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of'
+                                                         'fakeroot')
+    def test_do_not_fail_when_percent_is_in_file_name(self):
+        """https://github.com/borgbackup/borg/issues/6063"""
+        def patched_setxattr_EACCES(*args, **kwargs):
+            raise OSError(errno.EACCES, 'EACCES')
+
+        os.makedirs(os.path.join(self.input_path, 'dir%p'))
+        xattr.setxattr(b'input/dir%p', b'user.attribute', b'value')
+        self.cmd('init', self.repository_location, '-e' 'none')
+        self.cmd('create', self.repository_location + '::test', 'input')
+        with changedir('output'):
+            with patch.object(xattr, 'setxattr', patched_setxattr_EACCES):
+                self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING)
+
 
 @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
 class ArchiverTestCaseBinary(ArchiverTestCase):
diff --git a/src/borg/xattr.py b/src/borg/xattr.py
--- a/src/borg/xattr.py
+++ b/src/borg/xattr.py
@@ -132,7 +132,6 @@ def set_all(path, xattrs, follow_symlinks=False):
                 path_str = '<FD %d>' % path
             else:
                 path_str = os.fsdecode(path)
-            msg_format = '%s: when setting extended attribute %s: %%s' % (path_str, k_str)
             if e.errno == errno.E2BIG:
                 err_str = 'too big for this filesystem'
             elif e.errno == errno.ENOTSUP:
@@ -146,5 +145,5 @@ def set_all(path, xattrs, follow_symlinks=False):
                 # EACCES: permission denied to set this specific xattr (this may happen related to security.* keys)
                 # EPERM: operation not permitted
                 err_str = os.strerror(e.errno)
-            logger.warning(msg_format % err_str)
+            logger.warning('%s: when setting extended attribute %s: %s', path_str, k_str, err_str)
     return warning


Test patch: