From fe2b2bc0075c1a8668ad3fda21e66dde7772b4c3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 10 Dec 2022 14:45:01 +0100 Subject: [PATCH 1/7] archive names: validate more strictly, fixes #2290 we want to be able to use an archive name as a directory name, e.g. for the FUSE fs built by borg mount. thus we can not allow "/" in an archive name on linux. on windows, the rules are more restrictive, disallowing quite some more characters (':<>"|*?' plus some more). we do not have FUSE fs / borg mount on windows yet, but we better avoid any issues. we can not avoid ":" though, as our {now} placeholder generates ISO-8601 timestamps, including ":" chars. also, we do not want to have leading/trailing blanks in archive names, neither surrogate-escapes. control chars are disallowed also, including chr(0). we have python str here, thus chr(0) is not expected in there (is not used to terminate a string, like it is in C). --- src/borg/helpers/parseformat.py | 29 ++++++++++++++++++++-- src/borg/testsuite/helpers.py | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 4af8eb5c5..6e3cf3b32 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -540,9 +540,34 @@ def location_validator(proto=None, other=False): def archivename_validator(): 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) - if "/" in text or "::" in text or not text: - raise argparse.ArgumentTypeError('Invalid archive name: "%s"' % 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 diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 6e2039412..b413e9c32 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -32,6 +32,7 @@ from ..helpers import msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH from ..helpers import StableDict, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams +from ..helpers import archivename_validator from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import swidth_slice from ..helpers import chunkit @@ -246,6 +247,49 @@ class TestLocationWithoutEnv: Location("ssh://user@host:/path") +@pytest.mark.parametrize( + "name", + [ + "foobar", + # placeholders + "foobar-{now}", + ], +) +def test_archivename_ok(name): + av = archivename_validator() + av(name) # must not raise an exception + + +@pytest.mark.parametrize( + "name", + [ + "", # too short + "x" * 201, # too long + # invalid chars: + "foo/bar", + "foo\\bar", + ">foo", + " Date: Sat, 10 Dec 2022 15:05:58 +0100 Subject: [PATCH 2/7] transfer: validate archive names before transfer --- src/borg/archiver/transfer_cmd.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index a7506d4ce..bc6718e25 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -5,7 +5,7 @@ from ..archive import Archive from ..constants import * # NOQA from ..crypto.key import uses_same_id_hash, uses_same_chunker_secret from ..helpers import EXIT_SUCCESS, EXIT_ERROR, Error -from ..helpers import location_validator, Location +from ..helpers import location_validator, Location, archivename_validator from ..helpers import format_file_size from ..manifest import Manifest @@ -39,6 +39,19 @@ class TransferMixIn: if not archive_names: return EXIT_SUCCESS + an_errors = [] + av = archivename_validator() + for archive_name in archive_names: + try: + av(archive_name) + except argparse.ArgumentTypeError as err: + an_errors.append(str(err)) + if an_errors: + self.print_error("Invalid archive names detected, please rename them before transfer:") + for err_msg in an_errors: + self.print_error(err_msg) + return EXIT_ERROR + from .. import upgrade as upgrade_mod try: From 99c6afbc61d9f29b62b732d63d6dd18bf7fac744 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Dec 2022 18:01:07 +0100 Subject: [PATCH 3/7] add generic text and comment validator --- src/borg/helpers/__init__.py | 2 +- src/borg/helpers/parseformat.py | 20 ++++++++++++++++++++ src/borg/testsuite/helpers.py | 25 ++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index ccff30d22..8a209c5d8 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -26,7 +26,7 @@ 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 +from .parseformat import Location, location_validator, archivename_validator, comment_validator, text_validator from .parseformat import BaseFormatter, ArchiveFormatter, ItemFormatter, file_status from .parseformat import swidth_slice, ellipsis_truncate from .parseformat import BorgJsonEncoder, basic_json_data, json_print, json_dump, prepare_dump_dict diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 6e3cf3b32..5b347d174 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -573,6 +573,26 @@ def archivename_validator(): 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]') + try: + text.encode("utf-8", errors="strict") + except UnicodeEncodeError: + # looks like text contains surrogate-escapes + raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [contains non-unicode characters]') + return text + + return validator + + +comment_validator = text_validator(name="comment", max_length=10000) + + class BaseFormatter: FIXED_KEYS = { # Formatting aids diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index b413e9c32..76363479d 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -32,7 +32,7 @@ from ..helpers import msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH from ..helpers import StableDict, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams -from ..helpers import archivename_validator +from ..helpers import archivename_validator, text_validator from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import swidth_slice from ..helpers import chunkit @@ -290,6 +290,29 @@ def test_archivename_invalid(name): av(name) +@pytest.mark.parametrize("text", ["", "single line", "multi\nline\ncomment"]) +def test_text_ok(text): + tv = text_validator(max_length=100, name="name") + tv(text) # must not raise an exception + + +@pytest.mark.parametrize( + "text", + [ + "x" * 101, # too long + # invalid chars: + "foo\0bar", + # contains surrogate-escapes + "foo\udc80bar", + "foo\udcffbar", + ], +) +def test_text_invalid(text): + tv = text_validator(max_length=100, name="name") + with pytest.raises(ArgumentTypeError): + tv(text) + + class FormatTimedeltaTestCase(BaseTestCase): def test(self): t0 = datetime(2001, 1, 1, 10, 20, 3, 0) From 4b54b5565e871a72daf5087321e5a082e38b93b2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Dec 2022 18:48:01 +0100 Subject: [PATCH 4/7] use comment validator for cli args and borg transfer --- src/borg/archiver/create_cmd.py | 9 +++++++-- src/borg/archiver/recreate_cmd.py | 9 +++++++-- src/borg/archiver/tar_cmds.py | 9 +++++++-- src/borg/archiver/transfer_cmd.py | 15 ++++++++++++++- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 741e363c5..d56a700f6 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -15,7 +15,7 @@ from ..archive import FilesystemObjectProcessors, MetadataCollector, ChunksProce from ..cache import Cache from ..constants import * # NOQA from ..compress import CompressionSpec -from ..helpers import ChunkerParams +from ..helpers import comment_validator, ChunkerParams from ..helpers import NameSpec, FilesCacheMode from ..helpers import eval_escapes from ..helpers import timestamp, archive_ts_now @@ -811,7 +811,12 @@ class CreateMixIn: archive_group = subparser.add_argument_group("Archive options") archive_group.add_argument( - "--comment", dest="comment", metavar="COMMENT", default="", help="add a comment text to the archive" + "--comment", + metavar="COMMENT", + dest="comment", + type=comment_validator, + default="", + help="add a comment text to the archive", ) archive_group.add_argument( "--timestamp", diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 40f2d6f26..c4d707e13 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -5,7 +5,7 @@ from ._common import build_matcher from ..archive import ArchiveRecreater from ..constants import * # NOQA from ..compress import CompressionSpec -from ..helpers import archivename_validator, ChunkerParams +from ..helpers import archivename_validator, comment_validator, ChunkerParams from ..helpers import timestamp from ..manifest import Manifest @@ -161,7 +161,12 @@ class RecreateMixIn: help="write checkpoint every SECONDS seconds (Default: 1800)", ) archive_group.add_argument( - "--comment", dest="comment", metavar="COMMENT", default=None, help="add a comment text to the archive" + "--comment", + metavar="COMMENT", + dest="comment", + type=comment_validator, + default=None, + help="add a comment text to the archive", ) archive_group.add_argument( "--timestamp", diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index c08c50b8f..9e9dc5162 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -15,7 +15,7 @@ from ..helpers import dash_open from ..helpers import msgpack from ..helpers import create_filter_process from ..helpers import ChunkIteratorFileWrapper -from ..helpers import ChunkerParams +from ..helpers import comment_validator, ChunkerParams from ..helpers import NameSpec from ..helpers import remove_surrogates from ..helpers import timestamp, archive_ts_now @@ -491,7 +491,12 @@ class TarMixIn: archive_group = subparser.add_argument_group("Archive options") archive_group.add_argument( - "--comment", dest="comment", metavar="COMMENT", default="", help="add a comment text to the archive" + "--comment", + metavar="COMMENT", + dest="comment", + type=comment_validator, + default="", + help="add a comment text to the archive", ) archive_group.add_argument( "--timestamp", diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index bc6718e25..0c26bdf73 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -5,7 +5,7 @@ from ..archive import Archive from ..constants import * # NOQA from ..crypto.key import uses_same_id_hash, uses_same_chunker_secret from ..helpers import EXIT_SUCCESS, EXIT_ERROR, Error -from ..helpers import location_validator, Location, archivename_validator +from ..helpers import location_validator, Location, archivename_validator, comment_validator from ..helpers import format_file_size from ..manifest import Manifest @@ -52,6 +52,19 @@ class TransferMixIn: self.print_error(err_msg) return EXIT_ERROR + ac_errors = [] + for archive_name in archive_names: + archive = Archive(other_manifest, archive_name) + try: + comment_validator(archive.metadata.get("comment", "")) + except argparse.ArgumentTypeError as err: + ac_errors.append((archive_name, str(err))) + if ac_errors: + self.print_error("Invalid archive comments detected, please fix them before transfer:") + for archive_name, err_msg in ac_errors: + self.print_error(f"{archive_name}: {err_msg}") + return EXIT_ERROR + from .. import upgrade as upgrade_mod try: From 1517db07ecde6957893e6db3e5306ff761c7d300 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Dec 2022 18:55:14 +0100 Subject: [PATCH 5/7] remove support for bcomment placeholder not needed any more as the comment is now validated pure text. --- src/borg/helpers/parseformat.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 5b347d174..458cbff56 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -629,8 +629,7 @@ class ArchiveFormatter(BaseFormatter): "archive": "archive name interpreted as text (might be missing non-text characters, see barchive)", "name": 'alias of "archive"', "barchive": "verbatim archive name, can contain any character except NUL", - "comment": "archive comment interpreted as text (might be missing non-text characters, see bcomment)", - "bcomment": "verbatim archive comment, can contain any character except NUL", + "comment": "archive comment", # *start* is the key used by borg-info for this timestamp, this makes the formats more compatible "start": "time (start) of creation of the archive", "time": 'alias of "start"', @@ -641,7 +640,7 @@ class ArchiveFormatter(BaseFormatter): "username": "username of user who created this archive", } KEY_GROUPS = ( - ("archive", "name", "barchive", "comment", "bcomment", "id"), + ("archive", "name", "barchive", "comment", "id"), ("start", "time", "end", "command_line"), ("hostname", "username"), ) @@ -692,7 +691,6 @@ class ArchiveFormatter(BaseFormatter): "hostname": partial(self.get_meta, "hostname", rs=True), "username": partial(self.get_meta, "username", rs=True), "comment": partial(self.get_meta, "comment", rs=True), - "bcomment": partial(self.get_meta, "comment", rs=False), "end": self.get_ts_end, "command_line": self.get_cmdline, } From 1e2741ad3d4cb54fceaa436a2d77a529185ab0c7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Dec 2022 19:00:58 +0100 Subject: [PATCH 6/7] remove support for barchive placeholder not needed any more as the archive name is now validated pure text. --- src/borg/archiver/rlist_cmd.py | 3 +-- src/borg/helpers/parseformat.py | 8 +++----- src/borg/testsuite/archiver/rlist_cmd.py | 2 -- src/borg/testsuite/archiver/transfer_cmd.py | 1 + 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/borg/archiver/rlist_cmd.py b/src/borg/archiver/rlist_cmd.py index 0001f5ddb..895d426a1 100644 --- a/src/borg/archiver/rlist_cmd.py +++ b/src/borg/archiver/rlist_cmd.py @@ -114,7 +114,6 @@ class RListMixIn: help="Format output as JSON. " "The form of ``--format`` is ignored, " "but keys used in it are added to the JSON output. " - "Some keys are always present. Note: JSON can only represent text. " - 'A "barchive" key is therefore not available.', + "Some keys are always present. Note: JSON can only represent text.", ) define_archive_filters_group(subparser) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 458cbff56..5656bb0e2 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -616,7 +616,7 @@ class BaseFormatter: return ( "- NEWLINE: OS dependent line separator\n" "- NL: alias of NEWLINE\n" - "- NUL: NUL character for creating print0 / xargs -0 like output, see barchive and bpath keys below\n" + "- NUL: NUL character for creating print0 / xargs -0 like output, see bpath key below\n" "- SPACE\n" "- TAB\n" "- CR\n" @@ -626,9 +626,8 @@ class BaseFormatter: class ArchiveFormatter(BaseFormatter): KEY_DESCRIPTIONS = { - "archive": "archive name interpreted as text (might be missing non-text characters, see barchive)", + "archive": "archive name", "name": 'alias of "archive"', - "barchive": "verbatim archive name, can contain any character except NUL", "comment": "archive comment", # *start* is the key used by borg-info for this timestamp, this makes the formats more compatible "start": "time (start) of creation of the archive", @@ -640,7 +639,7 @@ class ArchiveFormatter(BaseFormatter): "username": "username of user who created this archive", } KEY_GROUPS = ( - ("archive", "name", "barchive", "comment", "id"), + ("archive", "name", "comment", "id"), ("start", "time", "end", "command_line"), ("hostname", "username"), ) @@ -713,7 +712,6 @@ class ArchiveFormatter(BaseFormatter): { "name": remove_surrogates(archive_info.name), "archive": remove_surrogates(archive_info.name), - "barchive": archive_info.name, "id": bin_to_hex(archive_info.id), "time": self.format_time(archive_info.ts), "start": self.format_time(archive_info.ts), diff --git a/src/borg/testsuite/archiver/rlist_cmd.py b/src/borg/testsuite/archiver/rlist_cmd.py index 3c01733e8..f4ceff626 100644 --- a/src/borg/testsuite/archiver/rlist_cmd.py +++ b/src/borg/testsuite/archiver/rlist_cmd.py @@ -35,8 +35,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assertEqual(output_1, output_2) output_1 = self.cmd(f"--repo={self.repository_location}", "rlist", "--short") self.assertEqual(output_1, "test-1\ntest-2\n") - output_1 = self.cmd(f"--repo={self.repository_location}", "rlist", "--format", "{barchive}/") - self.assertEqual(output_1, "test-1/test-2/") output_3 = self.cmd(f"--repo={self.repository_location}", "rlist", "--format", "{name} {comment}{NL}") self.assert_in("test-1 comment 1\n", output_3) self.assert_in("test-2 comment 2\n", output_3) diff --git a/src/borg/testsuite/archiver/transfer_cmd.py b/src/borg/testsuite/archiver/transfer_cmd.py index 202167f1d..acc978101 100644 --- a/src/borg/testsuite/archiver/transfer_cmd.py +++ b/src/borg/testsuite/archiver/transfer_cmd.py @@ -81,6 +81,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): for got_archive, expected_archive in zip(got["archives"], expected["archives"]): del got_archive["id"] del expected_archive["id"] + del expected_archive["barchive"] # timestamps: # borg 1.2 transformed to local time and had microseconds = 0, no tzoffset # borg 2 uses an utc timestamp, with microseconds and with tzoffset From b9dc2be661966411ed9b90d2c4f5e8f242e55b0a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Dec 2022 19:39:52 +0100 Subject: [PATCH 7/7] remove support for bpath placeholder this needs to get done differently later, maybe base64 for json output. --- src/borg/archiver/list_cmd.py | 3 +-- src/borg/helpers/parseformat.py | 8 +++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/borg/archiver/list_cmd.py b/src/borg/archiver/list_cmd.py index 7dfc36775..76a2b7056 100644 --- a/src/borg/archiver/list_cmd.py +++ b/src/borg/archiver/list_cmd.py @@ -114,8 +114,7 @@ class ListMixIn: help="Format output as JSON Lines. " "The form of ``--format`` is ignored, " "but keys used in it are added to the JSON output. " - "Some keys are always present. Note: JSON can only represent text. " - 'A "bpath" key is therefore not available.', + "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( diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 5656bb0e2..2dd921a27 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -616,7 +616,7 @@ class BaseFormatter: return ( "- NEWLINE: OS dependent line separator\n" "- NL: alias of NEWLINE\n" - "- NUL: NUL character for creating print0 / xargs -0 like output, see bpath key below\n" + "- NUL: NUL character for creating print0 / xargs -0 like output\n" "- SPACE\n" "- TAB\n" "- CR\n" @@ -753,8 +753,7 @@ class ItemFormatter(BaseFormatter): # shake_* is not provided because it uses an incompatible .digest() method to support variable length. hash_algorithms = set(hashlib.algorithms_guaranteed).union({"xxh64"}).difference({"shake_128", "shake_256"}) KEY_DESCRIPTIONS = { - "bpath": "verbatim POSIX path, can contain any character except NUL", - "path": "path interpreted as text (might be missing non-text characters, see bpath)", + "path": "file path", "source": "link target for symlinks (identical to linktarget)", "hlid": "hard link identity (same if hardlinking same fs object)", "extra": 'prepends {source} with " -> " for soft links and " link to " for hard links', @@ -765,7 +764,7 @@ class ItemFormatter(BaseFormatter): "health": 'either "healthy" (file ok) or "broken" (if file has all-zero replacement chunks)', } KEY_GROUPS = ( - ("type", "mode", "uid", "gid", "user", "group", "path", "bpath", "source", "linktarget", "hlid", "flags"), + ("type", "mode", "uid", "gid", "user", "group", "path", "source", "linktarget", "hlid", "flags"), ("size", "dsize", "num_chunks", "unique_chunks"), ("mtime", "ctime", "atime", "isomtime", "isoctime", "isoatime"), tuple(sorted(hash_algorithms)), @@ -869,7 +868,6 @@ class ItemFormatter(BaseFormatter): if self.json_lines: item_data["healthy"] = "chunks_healthy" not in item else: - item_data["bpath"] = item.path item_data["extra"] = extra item_data["health"] = "broken" if "chunks_healthy" in item else "healthy" item_data["source"] = source