Repository: borgbackup/borg

Issue:
scp-style repo URL parsing broken for ip v6 addrs
Reported by @jeanrjc:

Hello, sorry to comment on this issue, but maybe I did not understand something.

The following does not work with borg 1.1.9:

```bash
$ borg check --archives-only backupNC@[2a02:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:/media/pi/myCloudDrive/borgBKP --debug
using builtin fallback logging configuration
35 self tests completed in 1.18 seconds
SSH command line: ['ssh', 'backupNC@2a02', 'borg', 'serve', '--umask=077', '--debug']
Remote: ssh: Could not resolve hostname 2a02: Name or service not known
Connection closed by remote host. Is borg working on the server?
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4455, in main
    exit_code = archiver.run(args)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4387, in run
    return set_ec(func(args))
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 134, in wrapper
    make_parent_dirs=make_parent_dirs, args=args)
  File "/usr/lib/python3/dist-packages/borg/remote.py", line 577, in __init__
    raise ConnectionClosedWithHint('Is borg working on the server?') from None
borg.remote.ConnectionClosedWithHint: Connection closed by remote host. Is borg working on the server?

Platform: Linux nextcloudpi 5.10.103-v7l+ #1529 SMP Tue Mar 8 12:24:00 GMT 2022 armv7l
Linux: debian 10.12 
Borg: 1.1.9  Python: CPython 3.7.3
PID: 4155  CWD: /home/pi
sys.argv: ['/usr/bin/borg', 'check', '--archives-only', 'backupNC@[2a02:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:/media/pi/myCloudDrive/borgBKP', '--debug']
SSH_ORIGINAL_COMMAND: None
```

The "SSH command line" is obviously wrong, what did I miss ? 

Thanks



Discussion:

Resolving patch:
diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py
--- a/src/borg/helpers/parseformat.py
+++ b/src/borg/helpers/parseformat.py
@@ -341,24 +341,33 @@ class Location:
             (?P<archive>[^/]+)                              # archive name must not contain "/"
         )?$"""                                              # must match until the end
 
+    # host NAME, or host IP ADDRESS (v4 or v6, v6 must be in square brackets)
+    host_re = r"""
+        (?P<host>(
+            (?!\[)[^:/]+(?<!\])     # hostname or v4 addr, not containing : or / (does not match v6 addr: no brackets!)
+            |
+            \[[0-9a-fA-F:.]+\])     # ipv6 address in brackets
+        )
+    """
+
     # regexes for misc. kinds of supported location specifiers:
     ssh_re = re.compile(r"""
-        (?P<proto>ssh)://                                   # ssh://
-        """ + optional_user_re + r"""                       # user@  (optional)
-        (?P<host>([^:/]+|\[[0-9a-fA-F:.]+\]))(?::(?P<port>\d+))?  # host or host:port or [ipv6] or [ipv6]:port
-        """ + abs_path_re + optional_archive_re, re.VERBOSE)  # path or path::archive
+        (?P<proto>ssh)://                                       # ssh://
+        """ + optional_user_re + host_re + r"""                 # user@  (optional), host name or address
+        (?::(?P<port>\d+))?                                     # :port (optional)
+        """ + abs_path_re + optional_archive_re, re.VERBOSE)    # path or path::archive
 
     file_re = re.compile(r"""
-        (?P<proto>file)://                                  # file://
-        """ + file_path_re + optional_archive_re, re.VERBOSE)  # servername/path, path or path::archive
+        (?P<proto>file)://                                      # file://
+        """ + file_path_re + optional_archive_re, re.VERBOSE)   # servername/path, path or path::archive
 
-    # note: scp_re is also use for local paths
+    # note: scp_re is also used for local paths
     scp_re = re.compile(r"""
         (
-            """ + optional_user_re + r"""                   # user@  (optional)
-            (?P<host>([^:/]+|\[[0-9a-fA-F:.]+\])):          # host: (don't match / or [ipv6] in host to disambiguate from file:)
-        )?                                                  # user@host: part is optional
-        """ + scp_path_re + optional_archive_re, re.VERBOSE)  # path with optional archive
+            """ + optional_user_re + host_re + r"""             # user@  (optional), host name or address
+            :                                                   # : (required!)
+        )?                                                      # user@host: part is optional
+        """ + scp_path_re + optional_archive_re, re.VERBOSE)    # path with optional archive
 
     # get the repo from BORG_REPO env and the optional archive from param.
     # if the syntax requires giving REPOSITORY (see "borg mount"),
diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py
--- a/src/borg/testsuite/helpers.py
+++ b/src/borg/testsuite/helpers.py
@@ -96,6 +96,10 @@ def test_ssh(self, monkeypatch, keys_dir):
         assert repr(Location('ssh://user@[2001:db8::192.0.2.1]/some/path')) == \
             "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)"
         assert Location('ssh://user@[2001:db8::192.0.2.1]/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path'
+        assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/some/path')) == \
+            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path', archive=None)"
+        assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/some/path')) == \
+            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path', archive=None)"
 
     def test_file(self, monkeypatch, keys_dir):
         monkeypatch.delenv('BORG_REPO', raising=False)
@@ -128,6 +132,8 @@ def test_scp(self, monkeypatch, keys_dir):
         assert repr(Location('user@[2001:db8::192.0.2.1]:/some/path')) == \
             "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)"
         assert Location('user@[2001:db8::192.0.2.1]:/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path'
+        assert repr(Location('user@[2a02:0001:0002:0003:0004:0005:0006:0007]:/some/path')) == \
+            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path', archive=None)"
 
     def test_smb(self, monkeypatch, keys_dir):
         monkeypatch.delenv('BORG_REPO', raising=False)


Test patch: