check: test a full --repair with a corrupt index and a corrupt pack, refs #10434

This commit is contained in:
Mrityunjay Raj
2026-09-30 01:08:40 +05:30
parent 8d0ab1b3e1
commit e178bc3902
3 changed files with 62 additions and 4 deletions
+5 -4
View File
@@ -1489,9 +1489,9 @@ class Repository:
rebuilds and persists it (see ArchiveChecker.check and ArchiveChecker.finish), refs #10434. Packs
are verified by the store hash, which is content-addressing rather than a MAC, so that check
detects accidental corruption but not tampering; the rebuild therefore checks every object with
validate, see below, refs #9901, #10026. If any pack is corrupt the index is not rebuilt, refs
#10026. Pack ids found corrupt are kept in cache/checked-packs, refs #9696. That object is stored
in the key's envelope, too, so check() needs the key (see set_key).
validate, see below, refs #9901, #10026. With repo_only, if any pack is corrupt, the index is not
rebuilt, refs #10026. Pack ids found corrupt are kept in cache/checked-packs, refs #9696. That
object is stored in the key's envelope, too, so check() needs the key (see set_key).
A pack recorded corrupt fails the check, also on a partial run that stops before re-reaching
it. The record clears at the check that finds the pack intact again or gone (removed by
@@ -1810,7 +1810,8 @@ class Repository:
else:
# a full check's archives phase reads archive/item metadata (and file content with
# --verify-data), so it repairs a corrupt pack holding such objects; warn rather than fail.
logger.warning(f"{done} {mode} repository check, corrupt pack(s) found{so_far}.")
deferred = "; index corrupt, the archives check rebuilds it from the packs" if index_deferred else ""
logger.warning(f"{done} {mode} repository check, corrupt pack(s) found{deferred}{so_far}.")
elif drops:
# drops come from a repository-only rebuild only; no archives phase follows it.
logger.error(
@@ -951,6 +951,32 @@ def test_check_repair_rebuilds_corrupt_index(archivers, request, mode, message):
assert "archive1" in cmd(archiver, "repo-list") # and remains usable
def test_check_repair_rebuilds_corrupt_index_with_corrupt_pack(archivers, request):
# A corrupt index and a corrupt pack: a full --repair rebuilds and stores the index in the archives check.
# The corrupt pack stays recorded corrupt, so a following check still fails on it, refs #10434.
archiver = request.getfixturevalue(archivers)
check_cmd_setup(archiver)
archive, repository = open_archive(archiver.repository_path, "archive1")
with repository:
bad_pack = sorted(info.name for info in repository.store_list("packs"))[0]
name = f"packs/{bad_pack}"
repository.store_store(name, corrupt(repository.store_load(name), -1))
for info in repository.store_list("index"): # rot every index fragment
name = f"index/{info.name}"
repository.store_store(name, corrupt(repository.store_load(name), 0))
output = cmd(archiver, "check", "-v", "--repair", exit_code=0)
assert "corrupt pack(s) found; index corrupt, the archives check rebuilds it from the packs." in output
archive, repository = open_archive(archiver.repository_path, "archive1")
with repository:
index_infos = list(repository.store_list("index"))
assert index_infos # a fresh index was stored
for info in index_infos: # each fragment's content matches its store hash name
assert repository.store.hash(f"index/{info.name}", algorithm=STORE_HASH_NAME) == info.name
output = cmd(archiver, "check", "-v", exit_code=1)
assert "Checked 1 index files (0 errors)" in output
assert f"Corrupt pack: {bad_pack}" in output
def test_check_repair_walks_packs_once_for_corrupt_index(archiver, monkeypatch):
# A full --repair with a corrupt index walks the objects of each pack once, for the index rebuild in the
# archives check, refs #10434.
+31
View File
@@ -1938,6 +1938,37 @@ def test_check_repair_refuses_when_pack_corrupt(tmp_path):
assert repository.check(repair=False) is False # index was not rebuilt; still corrupt
def test_check_full_repair_defers_corrupt_index_with_corrupt_pack(tmp_path, caplog):
# check(repair=True, repo_only=False) with a corrupt index and a corrupt pack succeeds and stores no
# index: the archives phase rebuilds the index and repairs what the corrupt pack held, refs #10434.
location = os.fspath(tmp_path / "repo")
with Repository(location, exclusive=True, create=True) as repository:
repository.put(H(1), fchunk(b"GOOD-CHUNK", chunk_id=H(1)))
repository.flush() # seal a pack holding H(1)
repository.put(H(2), fchunk(b"LOST-CHUNK", chunk_id=H(2)))
repository.flush() # seal a separate pack holding H(2)
with reopen(repository) as repository:
bad_pack_id = repository.chunks[H(2)].pack_id
bad_pack_name = "packs/" + bin_to_hex(bad_pack_id)
data = bytearray(repository.store_load(bad_pack_name))
data[-1] ^= 0xFF # rot the pack holding H(2): its content no longer matches its store hash name
repository.store_store(bad_pack_name, bytes(data))
for info in repository.store_list("index"): # rot every fragment
name = f"index/{info.name}"
idata = bytearray(repository.store_load(name))
idata[0] ^= 0xFF
repository.store_store(name, bytes(idata))
with reopen(repository) as repository:
index_before = {info.name for info in repository.store_list("index")}
with caplog.at_level(logging.INFO, logger="borg.repository"):
assert repository.check(repair=True, repo_only=False, validate=validate_any) is True
assert "and 2 packs (1 errors)." in caplog.text
assert "corrupt pack(s) found; index corrupt, the archives check rebuilds it from the packs." in caplog.text
assert {info.name for info in repository.store_list("index")} == index_before # nothing stored
assert bad_pack_name in [f"packs/{info.name}" for info in repository.store_list("packs")] # not dropped
assert PackTracker.load(repository).corrupt_ids() == [bad_pack_id] # recorded corrupt
@pytest.mark.parametrize("repo_only", [True, False])
def test_check_repair_leaves_index_when_interrupted(tmp_path, caplog, monkeypatch, repo_only):
# an interrupted repair (SIGINT before every pack is verified) must not rebuild the index from