From 452075db28a31601f8c162d2ba9ed8c4967510c8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 9 May 2026 01:35:20 +0200 Subject: [PATCH] Fix slashdot hack excluding source directory metadata, fixes #9534 When using the slashdot hack (e.g. `borg create ARCHIVE rootfs/./`), the source directory's metadata was being excluded instead of archived as the archive root. This happened because `create_helper` treated the slashdot target directory the same as its parent directories (which should be stripped), rather than recognizing it as the root of the archive. Added a new condition in `create_helper` to detect when the current path matches the strip prefix target exactly (`path + "/" == strip_prefix`) and archive it as `"."` (the archive root) instead of excluding it. --- src/borg/archive.py | 10 +++++++--- src/borg/testsuite/archiver.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f9fdfc129..0fc411d3f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1353,12 +1353,16 @@ class FilesystemObjectProcessors: def create_helper(self, path, st, status=None, hardlinkable=True, strip_prefix=None): if strip_prefix is not None: assert not path.endswith(os.sep) - if strip_prefix.startswith(path + os.sep): + if path + os.sep == strip_prefix: + # this is the directory the slashdot hack points to - archive it as the root. + path = "." + elif strip_prefix.startswith(path + os.sep): # still on a directory level that shall be stripped - do not create an item for this! yield None, 'x', False, False return - # adjust path, remove stripped directory levels - path = path.removeprefix(strip_prefix) + else: + # adjust path, remove stripped directory levels + path = path.removeprefix(strip_prefix) safe_path = make_path_safe(path) item = Item(path=safe_path) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 47815fa31..4f4f39028 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2297,6 +2297,24 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'secondB' in output assert 'secondB/thirdB' in output + def test_create_dotslash_hack_root_metadata(self): + """Test that the slashdot hack archives the source directory metadata as the archive root.""" + os.makedirs(os.path.join(self.input_path, "first", "subdir")) + self.create_regular_file("first/file1", contents=b"hello") + self.cmd('init', '--encryption=none', self.repository_location) + archive = self.repository_location + '::test' + self.cmd('create', archive, 'input/first/./') # slashdot hack + output = self.cmd('list', archive) + # the root directory "." must be in the archive (this was the bug in #9534). + lines = output.splitlines() + assert lines[0].endswith(" .") + # children of the slashdot target must be archived. + assert "subdir" in output + assert "file1" in output + # parent directories must NOT be in the archive. + assert "input" not in output + assert "first" not in output + # def test_cmdline_compatibility(self): # self.create_regular_file('file1', size=1024 * 80) # self.cmd('init', '--encryption=repokey', self.repository_location)