Merge pull request #7212 from ThomasWaldmann/archivename-validator

use archivename_validator everywhere
This commit is contained in:
TW 2022-12-15 23:30:33 +01:00 committed by GitHub
commit 3202f68fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 54 additions and 67 deletions

View File

@ -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")

View File

@ -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(

View File

@ -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",

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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)",
)

View File

@ -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"
)

View File

@ -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.')

View File

@ -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:

View File

@ -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

View File

@ -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):
raise argparse.ArgumentTypeError(
f'Invalid archive name: "{text}" [invalid control chars detected, ASCII < 32]'
)
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):
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 {name}: "{text}" [invalid chars detected matching "{invalid_chars}"]'
)
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

View File

@ -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"])