3030from .storelocking import Lock
3131from .logger import create_logger
3232from .manifest import NoManifestError
33- from .repoobj import RepoObj , OBJ_MAGIC
33+ from .repoobj import RepoObj , OBJ_MAGIC , SUPPORTED_OBJ_VERSIONS
3434from .crypto .key import is_keyfile
3535
3636logger = create_logger (__name__ )
3737
3838# an object name is its sha256 as 64 lowercase hex digits.
3939_valid_object_name = re .compile (r"[0-9a-f]{64}" ).fullmatch
4040
41+ # how much of a pack PackReader reads at once when searching for the next object header.
42+ RESYNC_WINDOW_SIZE = 1024 * 1024
43+
4144
4245def repo_lister (repository , * , limit = None ):
4346 marker = None
@@ -362,24 +365,73 @@ def read(self, offset, size):
362365 return self .store .load (self .key , offset = offset , size = size )
363366
364367 def size (self ):
365- """Return the pack size in bytes; for a store-backed pack this is one metadata lookup ."""
368+ """Return the pack size in bytes ( a store metadata lookup, unless the pack is in memory) ."""
366369 if self .pack_contents is not None :
367370 return len (self .pack_contents )
368371 return self .store .info (self .key ).size
369372
370- def iter_headers (self ):
373+ @staticmethod
374+ def _parse_header (hdr_data , offset , pack_size ):
375+ """Return the ObjHeader in hdr_data if it is a valid header at offset, None otherwise.
376+
377+ Valid means: OBJ_MAGIC, a supported version, and an object that fits into the pack.
378+ """
379+ hdr = RepoObj .ObjHeader (* RepoObj .obj_header .unpack (hdr_data ))
380+ if hdr .magic != OBJ_MAGIC or hdr .version not in SUPPORTED_OBJ_VERSIONS :
381+ return None
382+ if offset + RepoObj .obj_header .size + hdr .meta_size + hdr .data_size > pack_size :
383+ return None
384+ return hdr
385+
386+ def _find_header (self , offset , pack_size , validate ):
387+ """Scan forward from offset for the next object validate accepts, return its offset or None.
388+
389+ A pack has no framing besides the object headers, so this searches for OBJ_MAGIC. That byte
390+ sequence also occurs inside payloads, so a candidate is accepted only when its header parses
391+ and validate confirms it.
392+ """
393+ hdr_size = RepoObj .obj_header .size
394+ while offset + hdr_size <= pack_size :
395+ # a window at a time, so the scan costs one store request per RESYNC_WINDOW_SIZE bytes.
396+ buf = bytes (self .read (offset , min (RESYNC_WINDOW_SIZE , pack_size - offset )))
397+ if len (buf ) < hdr_size :
398+ break
399+ pos = 0
400+ while True :
401+ pos = buf .find (OBJ_MAGIC , pos )
402+ if pos < 0 or pos + hdr_size > len (buf ):
403+ break # not in this window, or a header overlapping its end: the next window has it
404+ hdr = self ._parse_header (buf [pos : pos + hdr_size ], offset + pos , pack_size )
405+ if hdr is not None :
406+ obj_size = hdr_size + hdr .meta_size + hdr .data_size
407+ # an object is at most MAX_DATA_SIZE bytes (Repository.put), so a larger candidate is a
408+ # false match on OBJ_MAGIC in a payload.
409+ if obj_size <= MAX_DATA_SIZE :
410+ size = obj_size if validate .needs_data else hdr_size + hdr .meta_size
411+ end = pos + size
412+ # the window holds these bytes, unless the candidate crosses its end.
413+ obj = buf [pos :end ] if end <= len (buf ) else self .read (offset + pos , size )
414+ if validate (hdr .chunk_id , obj ):
415+ return offset + pos
416+ pos += 1
417+ # step by the window less one header, so a magic straddling the boundary is still found.
418+ offset += max (len (buf ) - (hdr_size - 1 ), 1 )
419+ return None
420+
421+ def iter_headers (self , validate = None ):
371422 """Yield (chunk_id, offset, size) for each object by walking the fixed object headers.
372423
373- Only the headers are read, not the payloads, so locating every object costs one short
374- range read per object (or just a slice, when the pack is already in memory), plus one
375- store metadata lookup for the pack size.
424+ The walk reads a header per object: one short range read each (or a slice, for a pack in
425+ memory), plus one store metadata lookup for the pack size.
426+
427+ A header must have OBJ_MAGIC, a supported version and describe an object that fits into
428+ the pack, otherwise the pack is corrupt and IntegrityError is raised. A read shorter than
429+ a header ends the walk: that is the end of the pack.
376430
377- Each full header must have OBJ_MAGIC and describe an object that fits into the pack,
378- otherwise the pack is corrupt and IntegrityError is raised. Ending the walk instead
379- would be worse than raising: the chunks index rebuilt from these headers would just be
380- missing the rest of the pack, and borg check --repair would then "fix" the archives by
381- dropping chunks that are there.
382- A trailing partial header is the clean end of the pack, not corruption.
431+ validate(chunk_id, obj): returns whether obj is a repo object with id chunk_id, where obj is
432+ its header and metadata, plus its data when validate.needs_data is set. When validate is
433+ given, a corrupt header makes the walk resync: it scans for the next object validate accepts
434+ (see _find_header), continues there, and logs the skipped bytes.
383435 """
384436 pack_hex = bin_to_hex (self .pack_id ) if self .pack_id is not None else "<no id>"
385437 pack_size = self .size ()
@@ -389,17 +441,26 @@ def iter_headers(self):
389441 hdr_data = self .read (offset , hdr_size )
390442 if len (hdr_data ) < hdr_size :
391443 break # clean EOF, or trailing partial bytes
392- hdr = RepoObj .ObjHeader (* RepoObj .obj_header .unpack (hdr_data ))
393- if hdr .magic != OBJ_MAGIC :
394- raise IntegrityError (
395- f'pack { pack_hex } : no object header at offset { offset } (pack corruption), run "borg check"'
444+ hdr = self ._parse_header (hdr_data , offset , pack_size )
445+ if hdr is None :
446+ if validate is None :
447+ raise IntegrityError (
448+ f'pack { pack_hex } : invalid object header at offset { offset } (pack corruption), run "borg check"'
449+ )
450+ next_offset = self ._find_header (offset + 1 , pack_size , validate )
451+ if next_offset is None :
452+ logger .warning (
453+ f"pack { pack_hex } : invalid object header at offset { offset } and none after it, "
454+ f"skipping the remaining { pack_size - offset } bytes."
455+ )
456+ break
457+ logger .warning (
458+ f"pack { pack_hex } : invalid object header at offset { offset } , "
459+ f"skipping { next_offset - offset } bytes to the next one."
396460 )
461+ offset = next_offset
462+ continue
397463 obj_size = hdr_size + hdr .meta_size + hdr .data_size
398- if offset + obj_size > pack_size :
399- raise IntegrityError (
400- f"pack { pack_hex } : object extends past end of file at offset { offset } "
401- f'(pack corruption), run "borg check"'
402- )
403464 yield hdr .chunk_id , offset , obj_size
404465 offset += obj_size
405466
0 commit comments