Browse Source

Merge pull request #7212 from ThomasWaldmann/archivename-validator

use archivename_validator everywhere
TW 2 years ago
parent
commit
3202f68fb3

+ 2 - 2
src/borg/archiver/create_cmd.py

@@ -16,7 +16,7 @@ from ..cache import Cache
 from ..constants import *  # NOQA
 from ..compress import CompressionSpec
 from ..helpers import comment_validator, ChunkerParams
-from ..helpers import NameSpec, FilesCacheMode
+from ..helpers import archivename_validator, FilesCacheMode
 from ..helpers import eval_escapes
 from ..helpers import timestamp, archive_ts_now
 from ..helpers import get_cache_dir, os_stat
@@ -861,5 +861,5 @@ class CreateMixIn:
             help="select compression algorithm, see the output of the " '"borg help compression" command for details.',
         )
 
-        subparser.add_argument("name", metavar="NAME", type=NameSpec, help="specify the archive name")
+        subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
         subparser.add_argument("paths", metavar="PATH", nargs="*", type=str, action="extend", help="paths to archive")

+ 3 - 3
src/borg/archiver/debug_cmd.py

@@ -12,7 +12,7 @@ from ..helpers import sysinfo
 from ..helpers import bin_to_hex, prepare_dump_dict
 from ..helpers import dash_open
 from ..helpers import StableDict
-from ..helpers import positive_int_validator, NameSpec
+from ..helpers import positive_int_validator, archivename_validator
 from ..manifest import Manifest
 from ..platform import get_process_id
 from ..repository import Repository, LIST_SCAN_LIMIT, TAG_PUT, TAG_DELETE, TAG_COMMIT
@@ -387,7 +387,7 @@ class DebugMixIn:
             help="dump archive items (metadata) (debug)",
         )
         subparser.set_defaults(func=self.do_debug_dump_archive_items)
-        subparser.add_argument("name", metavar="NAME", type=NameSpec, help="specify the archive name")
+        subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
 
         debug_dump_archive_epilog = process_epilog(
             """
@@ -404,7 +404,7 @@ class DebugMixIn:
             help="dump decoded archive metadata (debug)",
         )
         subparser.set_defaults(func=self.do_debug_dump_archive)
-        subparser.add_argument("name", metavar="NAME", type=NameSpec, help="specify the archive name")
+        subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
         subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into")
 
         debug_dump_manifest_epilog = process_epilog(

+ 2 - 2
src/borg/archiver/diff_cmd.py

@@ -106,8 +106,8 @@ class DiffMixIn:
         )
         subparser.add_argument("--sort", dest="sort", action="store_true", help="Sort the output lines by file path.")
         subparser.add_argument("--json-lines", action="store_true", help="Format output as JSON Lines. ")
-        subparser.add_argument("name", metavar="ARCHIVE1", type=archivename_validator(), help="ARCHIVE1 name")
-        subparser.add_argument("other_name", metavar="ARCHIVE2", type=archivename_validator(), help="ARCHIVE2 name")
+        subparser.add_argument("name", metavar="ARCHIVE1", type=archivename_validator, help="ARCHIVE1 name")
+        subparser.add_argument("other_name", metavar="ARCHIVE2", type=archivename_validator, help="ARCHIVE2 name")
         subparser.add_argument(
             "paths",
             metavar="PATH",

+ 2 - 2
src/borg/archiver/extract_cmd.py

@@ -8,7 +8,7 @@ from ._common import with_repository, with_archive
 from ._common import build_filter, build_matcher
 from ..archive import BackupError, BackupOSError
 from ..constants import *  # NOQA
-from ..helpers import NameSpec
+from ..helpers import archivename_validator
 from ..helpers import remove_surrogates
 from ..helpers import HardLinkManager
 from ..helpers import ProgressIndicatorPercent
@@ -175,7 +175,7 @@ class ExtractMixIn:
             action="store_true",
             help="create holes in output sparse file from all-zero chunks",
         )
-        subparser.add_argument("name", metavar="NAME", type=NameSpec, help="specify the archive name")
+        subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
         subparser.add_argument(
             "paths", metavar="PATH", nargs="*", type=str, help="paths to extract; patterns are supported"
         )

+ 2 - 2
src/borg/archiver/list_cmd.py

@@ -6,7 +6,7 @@ from ._common import with_repository, build_matcher
 from ..archive import Archive
 from ..cache import Cache
 from ..constants import *  # NOQA
-from ..helpers import ItemFormatter, BaseFormatter, NameSpec
+from ..helpers import ItemFormatter, BaseFormatter, archivename_validator
 from ..manifest import Manifest
 
 from ..logger import create_logger
@@ -116,7 +116,7 @@ class ListMixIn:
             "but keys used in it are added to the JSON output. "
             "Some keys are always present. Note: JSON can only represent text.",
         )
-        subparser.add_argument("name", metavar="NAME", type=NameSpec, help="specify the archive name")
+        subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
         subparser.add_argument(
             "paths", metavar="PATH", nargs="*", type=str, help="paths to list; patterns are supported"
         )

+ 1 - 1
src/borg/archiver/recreate_cmd.py

@@ -147,7 +147,7 @@ class RecreateMixIn:
             dest="target",
             metavar="TARGET",
             default=None,
-            type=archivename_validator(),
+            type=archivename_validator,
             help="create a new archive with the name ARCHIVE, do not replace existing archive "
             "(only applies for a single archive)",
         )

+ 2 - 2
src/borg/archiver/rename_cmd.py

@@ -41,7 +41,7 @@ class RenameMixIn:
             help="rename archive",
         )
         subparser.set_defaults(func=self.do_rename)
-        subparser.add_argument("name", metavar="OLDNAME", type=archivename_validator(), help="specify the archive name")
+        subparser.add_argument("name", metavar="OLDNAME", type=archivename_validator, help="specify the archive name")
         subparser.add_argument(
-            "newname", metavar="NEWNAME", type=archivename_validator(), help="specify the new archive name"
+            "newname", metavar="NEWNAME", type=archivename_validator, help="specify the new archive name"
         )

+ 3 - 4
src/borg/archiver/tar_cmds.py

@@ -15,8 +15,7 @@ from ..helpers import dash_open
 from ..helpers import msgpack
 from ..helpers import create_filter_process
 from ..helpers import ChunkIteratorFileWrapper
-from ..helpers import comment_validator, ChunkerParams
-from ..helpers import NameSpec
+from ..helpers import archivename_validator, comment_validator, ChunkerParams
 from ..helpers import remove_surrogates
 from ..helpers import timestamp, archive_ts_now
 from ..helpers import basic_json_data, json_print
@@ -404,7 +403,7 @@ class TarMixIn:
             choices=("BORG", "PAX", "GNU"),
             help="select tar format: BORG, PAX or GNU",
         )
-        subparser.add_argument("name", metavar="NAME", type=NameSpec, help="specify the archive name")
+        subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
         subparser.add_argument("tarfile", metavar="FILE", help='output tar file. "-" to write to stdout instead.')
         subparser.add_argument(
             "paths", metavar="PATH", nargs="*", type=str, help="paths to extract; patterns are supported"
@@ -536,5 +535,5 @@ class TarMixIn:
             help="select compression algorithm, see the output of the " '"borg help compression" command for details.',
         )
 
-        subparser.add_argument("name", metavar="NAME", type=NameSpec, help="specify the archive name")
+        subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
         subparser.add_argument("tarfile", metavar="TARFILE", help='input tar file. "-" to read from stdin instead.')

+ 1 - 2
src/borg/archiver/transfer_cmd.py

@@ -40,10 +40,9 @@ class TransferMixIn:
             return EXIT_SUCCESS
 
         an_errors = []
-        av = archivename_validator()
         for archive_name in archive_names:
             try:
-                av(archive_name)
+                archivename_validator(archive_name)
             except argparse.ArgumentTypeError as err:
                 an_errors.append(str(err))
         if an_errors:

+ 1 - 2
src/borg/helpers/__init__.py

@@ -20,11 +20,10 @@ from .misc import sysinfo, log_multi, consume
 from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper
 from .parseformat import bin_to_hex, safe_encode, safe_decode
 from .parseformat import remove_surrogates, eval_escapes, decode_dict, positive_int_validator, interval
-from .parseformat import ChunkerParams, FilesCacheMode, partial_format, DatetimeWrapper
+from .parseformat import SortBySpec, ChunkerParams, FilesCacheMode, partial_format, DatetimeWrapper
 from .parseformat import format_file_size, parse_file_size, FileSize, parse_storage_quota
 from .parseformat import sizeof_fmt, sizeof_fmt_iec, sizeof_fmt_decimal
 from .parseformat import format_line, replace_placeholders, PlaceholderError
-from .parseformat import SortBySpec, NameSpec
 from .parseformat import format_archive, parse_stringified_list, clean_lines
 from .parseformat import Location, location_validator, archivename_validator, comment_validator, text_validator
 from .parseformat import BaseFormatter, ArchiveFormatter, ItemFormatter, file_status

+ 32 - 40
src/borg/helpers/parseformat.py

@@ -227,8 +227,6 @@ class PlaceholderReplacer:
 
 replace_placeholders = PlaceholderReplacer()
 
-NameSpec = str
-
 
 def SortBySpec(text):
     from ..manifest import AI_HUMAN_SORT_KEYS
@@ -538,48 +536,21 @@ def location_validator(proto=None, other=False):
     return validator
 
 
-def archivename_validator():
+def text_validator(*, name, max_length, min_length=0, invalid_ctrl_chars="\0", invalid_chars="", no_blanks=False):
     def validator(text):
         assert isinstance(text, str)
-        # we make sure that the archive name can be used as directory name (for borg mount)
-        text = replace_placeholders(text)
-        MAX_PATH = 260  # Windows default. Since Win10, there is a registry setting LongPathsEnabled to get more.
-        MAX_DIRNAME = MAX_PATH - len("12345678.123")
-        SAFETY_MARGIN = 48  # borgfs path: mountpoint / archivename / dir / dir / ... / file
-        MAX_ARCHIVENAME = MAX_DIRNAME - SAFETY_MARGIN
-        if not (0 < len(text) <= MAX_ARCHIVENAME):
-            raise argparse.ArgumentTypeError(f'Invalid archive name: "{text}" [0 < length <= {MAX_ARCHIVENAME}]')
-        # note: ":" is also a invalid path char on windows, but we can not blacklist it,
-        # because e.g. our {now} placeholder creates ISO-8601 like output like 2022-12-10T20:47:42 .
-        invalid_chars = r"/" + r"\"<|>?*"  # posix + windows
-        if re.search(f"[{re.escape(invalid_chars)}]", text):
-            raise argparse.ArgumentTypeError(
-                f'Invalid archive name: "{text}" [invalid chars detected matching "{invalid_chars}"]'
-            )
-        invalid_ctrl_chars = "".join(chr(i) for i in range(32))
-        if re.search(f"[{re.escape(invalid_ctrl_chars)}]", text):
+        if len(text) < min_length:
+            raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [length < {min_length}]')
+        if len(text) > max_length:
+            raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [length > {max_length}]')
+        if invalid_ctrl_chars and re.search(f"[{re.escape(invalid_ctrl_chars)}]", text):
+            raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [invalid control chars detected]')
+        if invalid_chars and re.search(f"[{re.escape(invalid_chars)}]", text):
             raise argparse.ArgumentTypeError(
-                f'Invalid archive name: "{text}" [invalid control chars detected, ASCII < 32]'
+                f'Invalid {name}: "{text}" [invalid chars detected matching "{invalid_chars}"]'
             )
-        if text.startswith(" ") or text.endswith(" "):
-            raise argparse.ArgumentTypeError(f'Invalid archive name: "{text}" [leading or trailing blanks]')
-        try:
-            text.encode("utf-8", errors="strict")
-        except UnicodeEncodeError:
-            # looks like text contains surrogate-escapes
-            raise argparse.ArgumentTypeError(f'Invalid archive name: "{text}" [contains non-unicode characters]')
-        return text
-
-    return validator
-
-
-def text_validator(*, name, max_length, invalid_ctrl_chars="\0"):
-    def validator(text):
-        assert isinstance(text, str)
-        if not (len(text) <= max_length):
-            raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [length <= {max_length}]')
-        if re.search(f"[{re.escape(invalid_ctrl_chars)}]", text):
-            raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [invalid control chars detected]')
+        if no_blanks and (text.startswith(" ") or text.endswith(" ")):
+            raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [leading or trailing blanks detected]')
         try:
             text.encode("utf-8", errors="strict")
         except UnicodeEncodeError:
@@ -593,6 +564,27 @@ def text_validator(*, name, max_length, invalid_ctrl_chars="\0"):
 comment_validator = text_validator(name="comment", max_length=10000)
 
 
+def archivename_validator(text):
+    # we make sure that the archive name can be used as directory name (for borg mount)
+    MAX_PATH = 260  # Windows default. Since Win10, there is a registry setting LongPathsEnabled to get more.
+    MAX_DIRNAME = MAX_PATH - len("12345678.123")
+    SAFETY_MARGIN = 48  # borgfs path: mountpoint / archivename / dir / dir / ... / file
+    MAX_ARCHIVENAME = MAX_DIRNAME - SAFETY_MARGIN
+    invalid_ctrl_chars = "".join(chr(i) for i in range(32))
+    # note: ":" is also an invalid path char on windows, but we can not blacklist it,
+    # because e.g. our {now} placeholder creates ISO-8601 like output like 2022-12-10T20:47:42 .
+    invalid_chars = r"/" + r"\"<|>?*"  # posix + windows
+    validate_text = text_validator(
+        name="archive name",
+        min_length=1,
+        max_length=MAX_ARCHIVENAME,
+        invalid_ctrl_chars=invalid_ctrl_chars,
+        invalid_chars=invalid_chars,
+        no_blanks=True,
+    )
+    return validate_text(text)
+
+
 class BaseFormatter:
     FIXED_KEYS = {
         # Formatting aids

+ 2 - 4
src/borg/testsuite/helpers.py

@@ -256,8 +256,7 @@ class TestLocationWithoutEnv:
     ],
 )
 def test_archivename_ok(name):
-    av = archivename_validator()
-    av(name)  # must not raise an exception
+    archivename_validator(name)  # must not raise an exception
 
 
 @pytest.mark.parametrize(
@@ -285,9 +284,8 @@ def test_archivename_ok(name):
     ],
 )
 def test_archivename_invalid(name):
-    av = archivename_validator()
     with pytest.raises(ArgumentTypeError):
-        av(name)
+        archivename_validator(name)
 
 
 @pytest.mark.parametrize("text", ["", "single line", "multi\nline\ncomment"])