diff --git a/docs/usage/general/repository-urls.rst.inc b/docs/usage/general/repository-urls.rst.inc index a53bb4de9..40b26607b 100644 --- a/docs/usage/general/repository-urls.rst.inc +++ b/docs/usage/general/repository-urls.rst.inc @@ -12,6 +12,10 @@ expanded by your shell). Note: You may also prepend ``file://`` to a filesystem path to use URL style. +Note: UNC paths (``//server/share/path``, ``\\server\share\path``) are not +supported — mount the share (on Windows: map it to a drive letter, e.g. +``net use X: \\server\share``) and use the mounted path instead. + **Remote repositories** accessed via SSH user@host (REST http over stdio): ``rest://user@host:port//abs/path/to/repo`` — absolute path diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index cbc22c6b0..fe2ab1965 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -724,9 +724,9 @@ class Location: BORGSTORE_SCHEMES = ("sftp", "http", "https", "s3", "b2", "rclone") # path may contain any chars. to avoid ambiguities with other regexes, it must not start with - # "//", a "scheme://" or one of the borgstore "scheme:" specifiers (all of which are matched + # a "scheme://" or one of the borgstore "scheme:" specifiers (all of which are matched # before local_re in _parse). the borgstore scheme list is sourced from BORGSTORE_SCHEMES. - local_path_re = r"(?!(//|(?:ssh|file)://|(?:" + "|".join(BORGSTORE_SCHEMES) + r"):))" r"(?P.+)" + local_path_re = r"(?!((?:ssh|file)://|(?:" + "|".join(BORGSTORE_SCHEMES) + r"):))" r"(?P.+)" # abs_path must start with a slash (or drive letter on Windows). abs_path_re = r"(?P[A-Za-z]:/.+)" if is_win32 else r"(?P/.+)" @@ -796,11 +796,15 @@ class Location: self.raw = text # as given by user, might contain placeholders self.processed = replace_placeholders(self.raw, overrides) # after placeholder replacement - valid = self._parse(self.processed) - if valid: - self.valid = True - else: + if not self._parse(self.processed): raise ValueError('Invalid location format: "%s"' % self.processed) + if self.proto == "file" and self.path.startswith("//"): + # UNC path (//server/share/..., \\server\share\...): not supported, fail early with a hint, see #10164. + raise ValueError( + f'UNC paths are not supported: "{self.processed}". ' + "Mount the share (Windows: net use X: \\\\server\\share) and use the mounted path instead." + ) + self.valid = True def _parse(self, text): m = self.ssh_re.match(text) diff --git a/src/borg/testsuite/helpers/parseformat_test.py b/src/borg/testsuite/helpers/parseformat_test.py index 0e1d7cd71..37fdbb69e 100644 --- a/src/borg/testsuite/helpers/parseformat_test.py +++ b/src/borg/testsuite/helpers/parseformat_test.py @@ -264,13 +264,17 @@ class TestLocationWithoutEnv: repr(Location(url)) == f"Location(proto='file', user=None, pass=None, host=None, port=None, path='{path}')" ) - @pytest.mark.skipif(is_win32, reason="still broken") - def test_smb(self, monkeypatch): + def test_unc_rejected(self, monkeypatch): + # UNC paths are not supported (#3141), the parser rejects all spellings early, see #10164. monkeypatch.delenv("BORG_REPO", raising=False) - assert ( - repr(Location("file:////server/share/path")) - == "Location(proto='file', user=None, pass=None, host=None, port=None, path='//server/share/path')" - ) + with pytest.raises(ValueError, match="UNC paths are not supported"): + Location("//server/share/repo") + # on win32, file:/// must be followed by a drive letter, so this is invalid even earlier. + with pytest.raises(ValueError, match="Invalid location" if is_win32 else "UNC paths are not supported"): + Location("file:////server/share/repo") + if is_win32: + with pytest.raises(ValueError, match="UNC paths are not supported"): + Location(r"\\server\share\repo") def test_folder(self, monkeypatch): monkeypatch.delenv("BORG_REPO", raising=False)