Skip to content

Commit f8f1464

Browse files
committed
remove the drop_corrupt_tail mechanism, refs #10369
Every check --repair rebuild of the chunk index has an object validator (#10369), so drop_corrupt_tail was only reachable from tests. - PackReader.iter_headers: remove the drop_corrupt_tail parameter. Without a validator, a corrupt object header raises IntegrityError. - build_chunkindex_from_repo: remove the drop_corrupt_tail parameter. - Repository: remove chunkindex_drop_corrupt_tail and chunkindex_validate. Since #10368 the checker hands its index to the repository, so the lazy .chunks rebuild never runs during a check and nothing set either of them. - ArchiveChecker.check: stop passing drop_corrupt_tail. - tests: remove the 5 tests for drop_corrupt_tail, rewrite test_check_without_repair_does_not_drop_a_pack_tail as test_check_without_key_aborts_on_a_corrupt_pack_header.
1 parent 9614abb commit f8f1464

6 files changed

Lines changed: 24 additions & 127 deletions

File tree

src/borg/archive.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2285,14 +2285,11 @@ def check(
22852285
self.chunks = build_chunkindex_from_repo(
22862286
self.repository,
22872287
slow_rebuild=repair,
2288+
# validate is None only without --repair and without the key: a corrupt object header then
2289+
# raises CorruptPack.
22882290
validate=validate,
22892291
# dropped content is a check finding, with or without --repair.
22902292
on_drop=self.note_dropped_objects,
2291-
# without a validator the rebuild can not resync past a corrupt object header. --repair
2292-
# drops the rest of that pack to get on with the repair; without --repair the rebuild
2293-
# raises, so an index missing objects that are still there can not make the check report
2294-
# them as gone.
2295-
drop_corrupt_tail=repair,
22962293
write_immediately=False,
22972294
)
22982295
# clear F_NEW (entry not in the index/ fragments yet), so Repository.close() does not store

src/borg/cache.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -884,20 +884,15 @@ def build_chunkindex_from_repo(
884884
fragments_only=False,
885885
validate=None,
886886
on_drop=None,
887-
drop_corrupt_tail=False,
888887
write_immediately=False,
889888
init_flags=ChunkIndex.F_USED,
890889
):
891890
# fragments_only: build the index from the index/ fragments only, returning None if they cannot be
892891
# read completely, and never write to the repo.
893-
# validate: a repo object validator, handed to PackReader.iter_headers so the rebuild skips the
894-
# objects that fail it.
895-
# on_drop: a callable, handed to PackReader.iter_headers, which calls it once per place where
896-
# the walk skips content. It only reports, it does not change what the walk does.
897-
# drop_corrupt_tail: without a validator, index a pack with a corrupt object header up to that
898-
# header and drop the rest of it, instead of raising, see PackReader.iter_headers.
899-
# With neither of the two, a corrupt object header aborts the rebuild with CorruptPack: the
900-
# index would be missing every object after it.
892+
# validate: a repo object validator or None, passed to PackReader.iter_headers. With a validator,
893+
# the rebuild skips the objects that fail it; without one, a corrupt object header raises CorruptPack.
894+
# on_drop: a callable or None, passed to PackReader.iter_headers, called once per byte range the
895+
# validating walk skips.
901896
assert not (slow_rebuild and fragments_only)
902897
assert not (fragments_only and write_immediately) # fragments_only never writes to the repo
903898
# first, try to build a fresh, mostly complete chunk index from centrally stored index fragments:
@@ -994,9 +989,7 @@ def build_chunkindex_from_repo(
994989
pack_id = hex_to_bin(info.name)
995990
reader = PackReader(repository.store, pack_id)
996991
try:
997-
for chunk_id, obj_offset, obj_size in reader.iter_headers(
998-
validate=validate, on_drop=on_drop, drop_corrupt_tail=drop_corrupt_tail
999-
):
992+
for chunk_id, obj_offset, obj_size in reader.iter_headers(validate=validate, on_drop=on_drop):
1000993
num_chunks += 1
1001994
chunks[chunk_id] = ChunkIndexEntry(
1002995
flags=init_flags, size=0, pack_id=pack_id, obj_offset=obj_offset, obj_size=obj_size

src/borg/repository.py

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def _find_header(self, offset, pack_size, validate):
494494
offset += max(len(buf) - (hdr_size - 1), 1)
495495
return None
496496

497-
def iter_headers(self, validate=None, on_drop=None, drop_corrupt_tail=False):
497+
def iter_headers(self, validate=None, on_drop=None):
498498
"""Yield (chunk_id, offset, size) for each object by walking the fixed object headers.
499499
500500
The walk reads one range per object (or a slice, for a pack in memory), plus one store
@@ -510,13 +510,11 @@ def iter_headers(self, validate=None, on_drop=None, drop_corrupt_tail=False):
510510
validates, the walk yields nothing and raises nothing.
511511
512512
Without a validator a resync is impossible, because payload bytes can look like a header.
513-
A header that _parse_header rejects then raises IntegrityError naming what is wrong with
514-
it, or, with drop_corrupt_tail, ends the walk there and drops the rest of the pack.
513+
A header that _parse_header rejects then raises IntegrityError naming what is wrong with it.
515514
516-
on_drop, if given, is called once per place where the walk discards content: once for the
517-
object with the failed header plus whatever the resync scan skips before the object it
518-
resumes at, once for a tail dropped because the scan found no such object or because there
519-
was no validator to scan with. It only reports, it does not change what the walk does.
515+
on_drop, if given, is called once per byte range a validating walk skips: the object with
516+
the failed header plus the bytes up to the next object validate accepts, or up to the end of
517+
the pack if there is none.
520518
521519
headers_parsed is set to the number of headers _parse_header accepted in this walk, the
522520
candidates the resync scan tried included. A pack whose bytes hold no object header at all
@@ -542,19 +540,9 @@ def iter_headers(self, validate=None, on_drop=None, drop_corrupt_tail=False):
542540
problem = self._validation_problem(hdr, offset, buf, offset, validate)
543541
if problem is not None:
544542
if validate is None:
545-
# no validator, so payload bytes that look like a header can not be told from
546-
# an object: there is no way to resync past this header.
547-
if not drop_corrupt_tail:
548-
# the callers that can say something more useful than "there is corruption
549-
# here" wrap this, see build_chunkindex_from_repo.
550-
raise IntegrityError(f"pack {pack_hex}: {problem} at offset {offset} (pack corruption)")
551-
if on_drop is not None:
552-
on_drop()
553-
logger.warning(
554-
f"pack {pack_hex}: {problem} at offset {offset}, no validator to resync with, "
555-
f"skipping the remaining {pack_size - offset} bytes."
556-
)
557-
break
543+
# without a validator, payload bytes that look like a header can not be told
544+
# apart from an object, so the walk can not continue past this header.
545+
raise IntegrityError(f"pack {pack_hex}: {problem} at offset {offset} (pack corruption)")
558546
if on_drop is not None:
559547
on_drop() # content is discarded either way below: this object, or the tail.
560548
found = self._find_header(offset + 1, pack_size, validate)
@@ -976,12 +964,6 @@ def __init__(
976964
self.exclusive = exclusive
977965
self._pack_writer = None
978966
self._chunks = None # ChunkIndex; loaded lazily on first access to .chunks
979-
# corrupt-header handling for the lazy .chunks rebuild (see PackReader.iter_headers): a
980-
# validate callable makes the rebuild resync past a corrupt object header, drop_corrupt_tail
981-
# makes it index the pack up to that header and drop the rest. Without either, such a header
982-
# aborts the rebuild. TODO(#10378): nothing sets them, remove both.
983-
self.chunkindex_validate = None
984-
self.chunkindex_drop_corrupt_tail = False
985967
# pack_id -> PackReader holding the whole pack; get_many loads into it, get() reuses it
986968
self._pack_cache = LRUCache(capacity=self.PACK_READER_CACHE_SIZE)
987969

@@ -1277,9 +1259,7 @@ def chunks(self):
12771259
if self._chunks is None:
12781260
from .cache import build_chunkindex_from_repo
12791261

1280-
self._chunks = build_chunkindex_from_repo(
1281-
self, validate=self.chunkindex_validate, drop_corrupt_tail=self.chunkindex_drop_corrupt_tail
1282-
)
1262+
self._chunks = build_chunkindex_from_repo(self)
12831263
return self._chunks
12841264

12851265
@chunks.setter

src/borg/testsuite/archiver/check_cmd_test.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -964,12 +964,11 @@ def test_check_repair_validates_index_rebuild(archivers, request):
964964
assert neighbour_id in repository.chunks
965965

966966

967-
def test_check_without_repair_does_not_drop_a_pack_tail(archivers, request, monkeypatch):
968-
"""A check without --repair reports a corrupt object header, it does not index the pack up to it.
967+
def test_check_without_key_aborts_on_a_corrupt_pack_header(archivers, request, monkeypatch):
968+
"""A check without --repair and without the key raises CorruptPack at a corrupt object header.
969969
970-
Without a validator the walk can not resync past a corrupt object header. --repair passes
971-
drop_corrupt_tail, so the rest of that pack is dropped and the repair gets on; a check without
972-
--repair passes drop_corrupt_tail=False and the walk raises instead.
970+
Without the key there is no object validator, and without one the pack walk raises at a corrupt
971+
object header.
973972
974973
The rebuild only walks the packs when the chunk index fragments are unusable, and it only walks
975974
without a validator when the key can not be read, so the test arranges both.
@@ -1013,9 +1012,9 @@ def build_chunkindex_from_repo(repository, **kwargs):
10131012
try:
10141013
index = real_build(repository, **kwargs)
10151014
except Exception as err:
1016-
rebuilds.append((kwargs.get("drop_corrupt_tail"), err))
1015+
rebuilds.append((kwargs.get("validate"), err))
10171016
raise
1018-
rebuilds.append((kwargs.get("drop_corrupt_tail"), index))
1017+
rebuilds.append((kwargs.get("validate"), index))
10191018
return index
10201019

10211020
monkeypatch.setattr(ArchiveChecker, "make_key", make_key)
@@ -1025,10 +1024,9 @@ def build_chunkindex_from_repo(repository, **kwargs):
10251024
with pytest.raises(CorruptPack) as excinfo:
10261025
cmd(archiver, "check", "--archives-only")
10271026
assert f"no object header at offset {damaged_offset} (pack corruption)" in str(excinfo.value)
1028-
drop_corrupt_tail, outcome = rebuilds[0]
1029-
# the rebuild raised, it did not return an index with the pack's tail missing
1027+
validate, outcome = rebuilds[0]
1028+
assert validate is None
10301029
assert isinstance(outcome, CorruptPack)
1031-
assert drop_corrupt_tail is False # a check that only diagnoses does not ask for the drop
10321030

10331031

10341032
def test_repo_list_aborts_cleanly_on_corrupt_pack(archivers, request):

src/borg/testsuite/cache_test.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -546,40 +546,6 @@ def test_build_chunkindex_reports_a_pack_without_any_object_header(tmp_path):
546546
assert len(drops) == 1
547547

548548

549-
def test_build_chunkindex_without_a_validator_drops_the_rest_of_a_damaged_pack(tmp_path):
550-
"""With drop_corrupt_tail and no validator, a corrupt object header ends the pack's walk."""
551-
from .repository_test import fchunk
552-
553-
obj1 = fchunk(b"first", chunk_id=H(90))
554-
obj2 = bytearray(fchunk(b"second", chunk_id=H(91)))
555-
obj2[0] ^= 0xFF # break the magic of the second object's header
556-
obj3 = fchunk(b"third", chunk_id=H(92))
557-
drops = []
558-
with Repository(os.fspath(tmp_path / "repository"), exclusive=True, create=True) as repository:
559-
repository.store_store("packs/" + bin_to_hex(H(93)), obj1 + bytes(obj2) + obj3)
560-
index = build_chunkindex_from_repo(
561-
repository, slow_rebuild=True, on_drop=lambda: drops.append(True), drop_corrupt_tail=True
562-
)
563-
assert H(90) in index # the pack is indexed up to the damaged header
564-
assert H(91) not in index and H(92) not in index # from there on the pack is dropped
565-
assert len(drops) == 1
566-
567-
568-
def test_build_chunkindex_without_drop_corrupt_tail_raises_on_a_damaged_pack(tmp_path):
569-
"""on_drop alone does not let the rebuild past a corrupt object header, it only reports."""
570-
from .repository_test import fchunk
571-
572-
obj1 = fchunk(b"first", chunk_id=H(90))
573-
obj2 = bytearray(fchunk(b"second", chunk_id=H(91)))
574-
obj2[0] ^= 0xFF # break the magic of the second object's header
575-
drops = []
576-
with Repository(os.fspath(tmp_path / "repository"), exclusive=True, create=True) as repository:
577-
repository.store_store("packs/" + bin_to_hex(H(93)), obj1 + bytes(obj2))
578-
with pytest.raises(CorruptPack, match="no object header at offset"):
579-
build_chunkindex_from_repo(repository, slow_rebuild=True, on_drop=lambda: drops.append(True))
580-
assert drops == [] # nothing was discarded: the walk did not get that far
581-
582-
583549
def test_build_chunkindex_drops_a_pack_that_validates_nothing_when_others_do(tmp_path):
584550
"""A single pack of which nothing validates is dropped, the objects of the other packs are indexed."""
585551
from .repository_test import fchunk

src/borg/testsuite/repository_test.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2206,43 +2206,6 @@ def test_pack_reader_raises_on_bad_magic():
22062206
list(reader.iter_headers())
22072207

22082208

2209-
def test_pack_reader_drops_a_corrupt_tail_only_when_asked():
2210-
# without a validator there is nothing to resync with, so the walk can not get past a corrupt
2211-
# header. drop_corrupt_tail alone decides what happens then; on_drop only reports it.
2212-
obj1 = fchunk(b"payload-one", chunk_id=H(1))
2213-
obj2 = bytearray(fchunk(b"payload-two", chunk_id=H(2)))
2214-
obj2[0] ^= 0xFF # break the magic of the second object's header
2215-
pack = obj1 + bytes(obj2)
2216-
drops = []
2217-
reader = PackReader(pack_contents=pack)
2218-
with pytest.raises(IntegrityError, match="no object header at offset"):
2219-
list(reader.iter_headers(on_drop=lambda: drops.append(1)))
2220-
assert drops == [] # nothing was discarded: the walk raised instead
2221-
headers = list(reader.iter_headers(on_drop=lambda: drops.append(1), drop_corrupt_tail=True))
2222-
assert headers == [(H(1), 0, len(obj1))] # up to the corrupt header, the rest of the pack is gone
2223-
assert len(drops) == 1
2224-
2225-
2226-
def test_pack_reader_drops_a_corrupt_tail_without_an_on_drop():
2227-
# drop_corrupt_tail works without an on_drop to report the drop to.
2228-
obj1 = fchunk(b"payload-one", chunk_id=H(1))
2229-
obj2 = bytearray(fchunk(b"payload-two", chunk_id=H(2)))
2230-
obj2[0] ^= 0xFF
2231-
reader = PackReader(pack_contents=obj1 + bytes(obj2))
2232-
assert list(reader.iter_headers(drop_corrupt_tail=True)) == [(H(1), 0, len(obj1))]
2233-
2234-
2235-
def test_pack_reader_drop_corrupt_tail_does_not_affect_a_validating_walk():
2236-
# with a validator the walk resyncs, so drop_corrupt_tail changes nothing.
2237-
obj1 = bytearray(fchunk(b"payload-one", chunk_id=H(1)))
2238-
obj2 = fchunk(b"payload-two", chunk_id=H(2))
2239-
obj1[0] ^= 0xFF
2240-
reader = PackReader(pack_contents=bytes(obj1) + obj2)
2241-
resynced = [(H(2), len(obj1), len(obj2))]
2242-
assert list(reader.iter_headers(validate=accept_all)) == resynced
2243-
assert list(reader.iter_headers(validate=accept_all, drop_corrupt_tail=True)) == resynced
2244-
2245-
22462209
def test_pack_reader_raises_on_bad_magic_through_store(tmp_path):
22472210
obj = bytearray(fchunk(b"FIRST", chunk_id=H(47)))
22482211
obj[0] ^= 0xFF

0 commit comments

Comments
 (0)