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:
codecov-commenter: # [Codecov](https://codecov.io/gh/borgbackup/borg/pull/6528?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) Report
> Merging [#6528](https://codecov.io/gh/borgbackup/borg/pull/6528?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) (fee65ed) into [1.2-maint](https://codecov.io/gh/borgbackup/borg/commit/28c7494455381bed616aeb1408c5049959cc5fbd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) (28c7494) will **decrease** coverage by `0.79%`.
> The diff coverage is `n/a`.

```diff
@@              Coverage Diff              @@
##           1.2-maint    #6528      +/-   ##
=============================================
- Coverage      83.01%   82.22%   -0.80%     
=============================================
  Files             38       38              
  Lines          10463    10463              
  Branches        2049     2040       -9     
=============================================
- Hits            8686     8603      -83     
- Misses          1268     1345      +77     
- Partials         509      515       +6     
```


| [Impacted Files](https://codecov.io/gh/borgbackup/borg/pull/6528?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) | Coverage Δ | |
|---|---|---|
| [src/borg/helpers/parseformat.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy9wYXJzZWZvcm1hdC5weQ==) | `89.70% <ø> (ø)` | |
| [src/borg/fuse\_impl.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvZnVzZV9pbXBsLnB5) | `42.30% <0.00%> (-34.62%)` | :arrow_down: |
| [src/borg/helpers/process.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy9wcm9jZXNzLnB5) | `52.33% <0.00%> (-13.48%)` | :arrow_down: |
| [src/borg/platform/\_\_init\_\_.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvcGxhdGZvcm0vX19pbml0X18ucHk=) | `72.00% <0.00%> (-12.00%)` | :arrow_down: |
| [src/borg/platform/base.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvcGxhdGZvcm0vYmFzZS5weQ==) | `77.69% <0.00%> (-3.08%)` | :arrow_down: |
| [src/borg/helpers/fs.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy9mcy5weQ==) | `76.92% <0.00%> (-2.96%)` | :arrow_down: |
| [src/borg/helpers/msgpack.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy9tc2dwYWNrLnB5) | `83.01% <0.00%> (-1.89%)` | :arrow_down: |
| [src/borg/upgrader.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvdXBncmFkZXIucHk=) | `61.49% <0.00%> (-1.73%)` | :arrow_down: |
| [src/borg/helpers/misc.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy9taXNjLnB5) | `77.09% <0.00%> (-1.68%)` | :arrow_down: |
| [src/borg/xattr.py](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcveGF0dHIucHk=) | `50.00% <0.00%> (-1.20%)` | :arrow_down: |
| ... and [3 more](https://codecov.io/gh/borgbackup/borg/pull/6528/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/borgbackup/borg/pull/6528?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/borgbackup/borg/pull/6528?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup). Last update [e5e360d...fee65ed](https://codecov.io/gh/borgbackup/borg/pull/6528?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup).


Patch:
diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py
index ee32abd7..1c2489cd 100644
--- 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"),


Test patch:
diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py
index d372ad69..c97b806c 100644
--- 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)