4 ===============================================================================
5 test_recover.py – behavior facing file corruption
6 ===============================================================================
8 Corruptors have the signature ``(unittest × string × bool × bool) → void``,
9 where the *string* argument is the name of the file to modify, the *booleans*
10 specialize the operation for compressed and encrypted data. Issues are
11 communicated upward by throwing.
14 Modify the first object header where it hurts. With encryption, the tag
15 is corrupted to cause authentication of the decrypted data to fail. For
16 compressed data, the two byte magic is altered, for uncompressed
17 archives, the tar header checksum field.
20 Modify the *ctsize* field of a PDTCRYPT header. The goal is to have
21 decryption continue past the end of the object, causing data
22 authentication to fail and file reads to be at odds with the offsets in
23 the index. Only applicable to encrypted archives; will raise
24 *UndefinedTest* otherwise.
26 - corrupt_entire_header ():
27 Invert all bits of the first object header (PDTCRYPT, gzip, tar) without
28 affecting the payload. This renders the object unreadable; the file will
29 be resemble one with arbitrary leading data but all the remaining object
30 offsets intact, so the contents can still be extracted with index based
33 - corrupt_payload_start ():
34 For all header variants, skip to the first byte past the header and
35 corrupt it. Encrypted objects will fail to authenticate. Compressed
36 objects will yield a bad CRC32. The Tar layer will take no notice but
37 the extracted object will fail an independent checksum comparison with
38 that of the original file.
40 - corrupt_leading_garbage ():
41 Prepend random data to an otherwise valid file. Creates a situation that
42 index based recovery cannot handle by shifting the offsets of all objects
43 in the file. In rescue mode, these objects must be located and extracted
46 - corrupt_trailing_data ():
47 Append data to an otherwise valid file. Both the recovery and rescue
48 modes must be able to retrieve all objects from that file.
51 Zero out an entire backup file. This is interesting for multivolume
52 tests: all files from the affected volume must be missing but objects
53 that span volume bounds will still be partially recoverable.
56 Remove a region from a file. Following the damaged part, no object can be
57 recovered in index mode, but rescue mode will still find those. The
58 object containing the start of the hole will fail checksum tests because
59 of the missing part and the overlap with the subsequent object.
70 from functools import partial
72 import deltatar.deltatar as deltatar
73 import deltatar.crypto as crypto
74 import deltatar.tarfile as tarfile
76 from . import BaseTest
78 TEST_PASSWORD = "test1234"
81 VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into
82 # volumes; this is black magic
85 ###############################################################################
87 ###############################################################################
89 def flip_bits (fname, off, b=0x01, n=1):
91 Open file *fname* at offset *off*, replacing the next *n* bytes with
92 their values xor’ed with *b*.
94 fd = os.open (fname, os.O_RDWR)
97 pos = os.lseek (fd, off, os.SEEK_SET)
99 chunk = os.read (fd, n)
100 chunk = bytes (map (lambda v: v ^ b, chunk))
101 pos = os.lseek (fd, off, os.SEEK_SET)
108 def gz_header_size (fname, off=0):
110 Determine the length of the gzip header starting at *off* in file fname.
112 The header is variable length because it may contain the filename as NUL
115 # length so we need to determine where the actual payload starts
116 off = tarfile.GZ_HEADER_SIZE
117 fd = os.open (fname, os.O_RDONLY)
120 pos = os.lseek (fd, off, os.SEEK_SET)
122 while os.read (fd, 1)[0] != 0:
124 pos = os.lseek (fd, off, os.SEEK_SET)
132 def is_pdt_encrypted (fname):
134 Returns true if the file contains at least one PDT header plus enough
135 space for the object.
138 with open (fname, "rb") as st:
139 hdr = crypto.hdr_read_stream (st)
141 assert (len (st.read (siz)) == siz)
142 except Exception as exn:
147 ###############################################################################
148 ## corruption simulators ##
149 ###############################################################################
151 class UndefinedTest (Exception):
152 """No test available for the asked combination of parameters."""
154 def corrupt_header (_, fname, compress, encrypt):
156 Modify a significant byte in the object header of the format.
158 if encrypt is True: # damage GCM tag
159 flip_bits (fname, crypto.HDR_OFF_TAG + 1)
160 elif compress is True: # invalidate magic
162 else: # Fudge checksum. From tar(5):
164 # struct header_gnu_tar {
173 flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1)
176 def corrupt_ctsize (_, fname, compress, encrypt):
178 Blow up the size of an object so as to cause its apparent payload to leak
182 # damage lowest bit of second least significant byte of size field;
183 # this effectively sets the ciphertext size to 422, causing it to
184 # extend over the next object into the third one.
185 return flip_bits (fname, crypto.HDR_OFF_CTSIZE + 1, b=0x01)
186 raise UndefinedTest ("corrupt_ctsize %s %s %s" % (fname, compress, encrypt))
189 def corrupt_entire_header (_, fname, compress, encrypt):
191 Flip all bits in the first object header.
194 flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE)
195 elif compress is True:
196 flip_bits (fname, 0, 0xff, gz_header_size (fname))
198 flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE)
201 def corrupt_payload_start (_, fname, compress, encrypt):
203 Modify the byte following the object header structure of the format.
206 flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1)
207 elif compress is True:
208 flip_bits (fname, gz_header_size (fname) + 1)
210 flip_bits (fname, tarfile.BLOCKSIZE + 1)
213 def corrupt_leading_garbage (_, fname, compress, encrypt):
215 Prepend junk to file.
217 aname = os.path.abspath (fname)
218 infd = os.open (fname, os.O_RDONLY)
219 size = os.lseek (infd, 0, os.SEEK_END)
220 assert os.lseek (infd, 0, os.SEEK_SET) == 0
221 outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
222 stat.S_IRUSR | stat.S_IWUSR)
223 junk = os.urandom (42)
225 # write new file with garbage prepended
227 os.write (outfd, junk) # junk first
230 data = os.read (infd, TEST_BLOCKSIZE)
231 os.write (outfd, data)
234 assert os.lseek (outfd, 0, os.SEEK_CUR) == done
236 # close and free old file
240 # install the new file in its place, atomically
241 path = "/proc/self/fd/%d" % outfd
242 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
246 def corrupt_trailing_data (_, fname, compress, encrypt):
248 Append random data to file.
250 junk = os.urandom (42)
251 fd = os.open (fname, os.O_WRONLY | os.O_APPEND)
256 def corrupt_volume (_, fname, compress, encrypt):
258 Zero out an entire volume.
260 fd = os.open (fname, os.O_WRONLY)
261 size = os.lseek (fd, 0, os.SEEK_END)
262 assert os.lseek (fd, 0, os.SEEK_SET) == 0
263 zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
265 todo = min (size, TEST_BLOCKSIZE)
266 os.write (fd, zeros [:todo])
271 def corrupt_hole (_, fname, compress, encrypt):
273 Cut file in three pieces, reassemble without the middle one.
275 aname = os.path.abspath (fname)
276 infd = os.open (fname, os.O_RDONLY)
277 size = os.lseek (infd, 0, os.SEEK_END)
278 assert os.lseek (infd, 0, os.SEEK_SET) == 0
279 assert size > 3 * TEST_BLOCKSIZE
280 hole = (size / 3, size * 2 / 3)
281 outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
282 stat.S_IRUSR | stat.S_IWUSR)
286 data = os.read (infd, TEST_BLOCKSIZE)
287 if done < hole [0] or hole [1] < done:
288 # only copy from outside hole
289 os.write (outfd, data)
295 path = "/proc/self/fd/%d" % outfd
296 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
299 def immaculate (_, _fname, _compress, _encrypt):
305 ###############################################################################
307 ###############################################################################
309 class DefectiveTest (BaseTest):
311 Disaster recovery: restore corrupt backups.
316 FAILURES = 0 # files that could not be restored
317 MISMATCHES = 0 # files that were restored but corrupted
318 CORRUPT = corrupt_payload_start
320 MISSING = None # normally the number of failures
325 Create base test data
327 self.pwd = os.getcwd()
328 self.dst_path = "source_dir"
329 self.src_path = "%s2" % self.dst_path
332 os.system('rm -rf target_dir source_dir* backup_dir* huge')
333 os.makedirs (self.src_path)
337 self.hash [f] = self.create_file ("%s/%s"
338 % (self.src_path, f), 5 + i)
343 Remove temporal files created by unit tests and reset globals.
346 os.system("rm -rf source_dir source_dir2 backup_dir*")
350 def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
351 return backup_file % n
353 def gen_file_names (self, comp, pw):
354 bak_path = "backup_dir"
355 backup_file = "the_full_backup_%0.2d.tar"
356 backup_full = ("%s/%s" % (bak_path, backup_file)) % 0
357 index_file = "the_full_index"
359 if self.COMPRESSION is not None:
364 if self.PASSWORD is not None:
365 backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION)
366 backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION)
367 index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION)
369 return bak_path, backup_file, backup_full, index_file
372 def gen_multivol (self, nvol):
373 # add n files for one nth the volume size each, corrected
374 # for metadata and tar block overhead
375 fsiz = int ( ( TEST_VOLSIZ
376 / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
378 fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
379 for i in range (fcnt):
380 nvol, invol = divmod(i, TEST_FILESPERVOL)
381 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
382 self.hash [f] = self.create_file ("%s/%s"
383 % (self.src_path, f),
388 class RecoverTest (DefectiveTest):
390 Recover: restore corrupt backups from index file information.
393 def test_recover_corrupt (self):
395 Perform various damaging actions that cause unreadable objects.
397 Expects the extraction to fail in normal mode. With disaster recovery,
398 extraction must succeed, and exactly one file must be missing.
400 mode = self.COMPRESSION or "#"
401 bak_path, backup_file, backup_full, index_file = \
402 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
405 self.gen_multivol (self.VOLUMES)
407 vname = partial (self.default_volume_name, backup_file)
408 dtar = deltatar.DeltaTar (mode=mode,
410 password=self.PASSWORD,
411 index_name_func=lambda _: index_file,
412 volume_name_func=vname)
414 dtar.create_full_backup \
415 (source_path=self.src_path, backup_path=bak_path,
418 if self.PASSWORD is not None:
419 # ensure all files are at least superficially in PDT format
420 for f in os.listdir (bak_path):
421 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
423 # first restore must succeed
424 dtar.restore_backup(target_path=self.dst_path,
425 backup_indexes_paths=[
426 "%s/%s" % (bak_path, index_file)
428 for key, value in self.hash.items ():
429 f = "%s/%s" % (self.dst_path, key)
430 assert os.path.exists (f)
431 assert value == self.md5sum (f)
432 shutil.rmtree (self.dst_path)
433 shutil.rmtree (self.src_path)
435 self.CORRUPT (backup_full,
436 self.COMPRESSION is not None,
437 self.PASSWORD is not None)
439 # normal restore must fail
441 dtar.restore_backup(target_path=self.dst_path,
442 backup_tar_path=backup_full)
443 except tarfile.CompressionError:
444 if self.PASSWORD is not None or self.COMPRESSION is not None:
448 except tarfile.ReadError:
449 # can happen with all three modes
451 except tarfile.DecryptionError:
452 if self.PASSWORD is not None:
457 os.chdir (self.pwd) # not restored due to the error above
458 # but recover will succeed
459 failed = dtar.recover_backup(target_path=self.dst_path,
460 backup_indexes_paths=[
461 "%s/%s" % (bak_path, index_file)
464 assert len (failed) == self.FAILURES
466 # with one file missing
469 for key, value in self.hash.items ():
470 kkey = "%s/%s" % (self.dst_path, key)
471 if os.path.exists (kkey):
472 if value != self.md5sum (kkey):
473 mismatch.append (key)
477 # usually, an object whose extraction fails will not be found on
478 # disk afterwards so the number of failures equals that of missing
479 # files. however, some modes will create partial files for objects
480 # spanning multiple volumes that contain the parts whose checksums
482 assert len (missing) == (self.MISSING if self.MISSING is not None
484 assert len (mismatch) == self.MISMATCHES
486 shutil.rmtree (self.dst_path)
489 class RescueTest (DefectiveTest):
491 Rescue: restore corrupt backups from backup set that is damaged to a degree
492 that the index file is worthless.
495 def test_rescue_corrupt (self):
497 Perform various damaging actions that cause unreadable objects, then
498 attempt to extract objects regardless.
500 mode = self.COMPRESSION or "#"
501 bak_path, backup_file, backup_full, index_file = \
502 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
505 self.gen_multivol (self.VOLUMES)
507 vname = partial (self.default_volume_name, backup_file)
508 dtar = deltatar.DeltaTar (mode=mode,
510 password=self.PASSWORD,
511 index_name_func=lambda _: index_file,
512 volume_name_func=vname)
514 dtar.create_full_backup \
515 (source_path=self.src_path, backup_path=bak_path,
518 if self.PASSWORD is not None:
519 # ensure all files are at least superficially in PDT format
520 for f in os.listdir (bak_path):
521 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
523 # first restore must succeed
524 dtar.restore_backup(target_path=self.dst_path,
525 backup_indexes_paths=[
526 "%s/%s" % (bak_path, index_file)
528 for key, value in self.hash.items ():
529 f = "%s/%s" % (self.dst_path, key)
530 assert os.path.exists (f)
531 assert value == self.md5sum (f)
532 shutil.rmtree (self.dst_path)
533 shutil.rmtree (self.src_path)
535 self.CORRUPT (backup_full,
536 self.COMPRESSION is not None,
537 self.PASSWORD is not None)
539 # normal restore must fail
541 dtar.restore_backup(target_path=self.dst_path,
542 backup_tar_path=backup_full)
543 except tarfile.CompressionError:
544 if self.PASSWORD is not None or self.COMPRESSION is not None:
548 except tarfile.ReadError:
549 # can happen with all three modes
551 except tarfile.DecryptionError:
552 if self.PASSWORD is not None:
557 os.chdir (self.pwd) # not restored due to the error above
558 # but recover will succeed
559 failed = dtar.rescue_backup(target_path=self.dst_path,
560 backup_tar_path=backup_full)
561 # with one file missing
564 for key, value in self.hash.items ():
565 kkey = "%s/%s" % (self.dst_path, key)
566 if os.path.exists (kkey):
567 if value != self.md5sum (kkey):
568 mismatch.append (key)
572 assert len (failed) == self.FAILURES
573 assert len (missing) == (self.MISSING if self.MISSING is not None
575 assert len (mismatch) == self.MISMATCHES
577 shutil.rmtree (self.dst_path)
580 class GenIndexTest (DefectiveTest):
582 Deducing an index for a backup with tarfile.
585 def test_gen_index (self):
587 Create backup, leave it unharmed, then generate an index.
589 mode = self.COMPRESSION or "#"
590 bak_path, backup_file, backup_full, index_file = \
591 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
594 self.gen_multivol (self.VOLUMES)
596 vname = partial (self.default_volume_name, backup_file)
597 dtar = deltatar.DeltaTar (mode=mode,
599 password=self.PASSWORD,
600 index_name_func=lambda _: index_file,
601 volume_name_func=vname)
603 dtar.create_full_backup \
604 (source_path=self.src_path, backup_path=bak_path,
607 def gen_volume_name (nvol):
608 return os.path.join (bak_path, vname (backup_full, True, nvol))
610 psidx = tarfile.gen_rescue_index (gen_volume_name,
612 password=self.PASSWORD)
614 # correct for objects spanning volumes: these are treated as separate
616 assert len (psidx) - self.VOLUMES + 1 == len (self.hash)
619 ###############################################################################
621 ###############################################################################
623 class RecoverCorruptPayloadTestBase (RecoverTest):
626 FAILURES = 0 # tarfile will restore but corrupted, as
627 MISMATCHES = 1 # revealed by the hash
629 class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
632 class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
636 class RecoverCorruptPayloadGZTestBase (RecoverTest):
642 class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
645 class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
649 class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
651 PASSWORD = TEST_PASSWORD
655 class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
658 class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
662 class RecoverCorruptHeaderTestBase (RecoverTest):
666 CORRUPT = corrupt_header
669 class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
672 class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
676 class RecoverCorruptHeaderGZTestBase (RecoverTest):
680 CORRUPT = corrupt_header
683 class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
686 class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
690 class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
692 PASSWORD = TEST_PASSWORD
694 CORRUPT = corrupt_header
697 class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
700 class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
704 class RecoverCorruptEntireHeaderTestBase (RecoverTest):
708 CORRUPT = corrupt_entire_header
711 class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
714 class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
718 class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
722 CORRUPT = corrupt_entire_header
725 class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
728 class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
732 class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
734 PASSWORD = TEST_PASSWORD
736 CORRUPT = corrupt_entire_header
739 class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
742 class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
746 class RecoverCorruptTrailingDataTestBase (RecoverTest):
747 # plain Tar is indifferent against traling data and the results
752 CORRUPT = corrupt_trailing_data
755 class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
758 class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
759 # the last object in first archive has extra bytes somewhere in the
760 # middle because tar itself performs no data checksumming.
765 class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
766 # reading past the final object will cause decompression failure;
767 # all objects except for the last survive unharmed though
771 CORRUPT = corrupt_trailing_data
774 class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
777 class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
779 # the last file of the first volume will only contain the data of the
780 # second part which is contained in the second volume. this happens
781 # because the CRC32 is wrong for the first part so it gets discarded, then
782 # the object is recreated from the first header of the second volume,
783 # containing only the remainder of the data.
788 class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
790 PASSWORD = TEST_PASSWORD
792 CORRUPT = corrupt_trailing_data
795 class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
798 class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
802 class RecoverCorruptVolumeBaseTest (RecoverTest):
806 CORRUPT = corrupt_volume
809 class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
812 class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
815 class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
817 PASSWORD = TEST_PASSWORD
820 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
821 class RecoverCorruptHoleBaseTest (RecoverTest):
823 Cut bytes from the middle of a volume.
825 Index-based recovery works only up to the hole.
830 CORRUPT = corrupt_hole
831 VOLUMES = 2 # request two vols to swell up the first one
834 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
835 class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
838 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
839 class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
843 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
844 class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
846 PASSWORD = TEST_PASSWORD
849 ###############################################################################
851 ###############################################################################
853 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
854 class RescueCorruptHoleBaseTest (RescueTest):
856 Cut bytes from the middle of a volume.
861 CORRUPT = corrupt_hole
862 VOLUMES = 2 # request two vols to swell up the first one
863 MISMATCHES = 2 # intersected by hole
864 MISSING = 1 # excised by hole
866 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
867 class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
870 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
871 class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
873 # the decompressor explodes in our face processing the first dummy, nothing
874 # we can do to recover
877 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
878 class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
880 PASSWORD = TEST_PASSWORD
881 # again, ignoring the crypto errors yields a bad zlib stream causing the
882 # decompressor to abort where the hole begins; the file is extracted up
883 # to this point though
887 class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
889 PASSWORD = TEST_PASSWORD
891 CORRUPT = corrupt_ctsize
895 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
896 class RescueCorruptLeadingGarbageTestBase (RescueTest):
897 # plain Tar is indifferent against traling data and the results
902 CORRUPT = corrupt_leading_garbage
905 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
906 class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
909 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
910 class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase):
911 # the last object in first archive has extra bytes somewhere in the
912 # middle because tar itself performs no data checksumming.
917 ###############################################################################
919 ###############################################################################
921 class GenIndexIntactBaseTest (GenIndexTest):
931 class GenIndexIntactSingleTest (GenIndexIntactBaseTest):
934 class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest):
938 class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest):
940 PASSWORD = TEST_PASSWORD
943 class GenIndexIntactMultiTest (GenIndexIntactBaseTest):
947 class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest):
952 class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest):
955 PASSWORD = TEST_PASSWORD
959 class GenIndexCorruptHoleBaseTest (GenIndexTest):
961 Recreate index from file with hole.
966 CORRUPT = corrupt_hole
970 class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest):
973 class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest):
977 class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest):
979 PASSWORD = TEST_PASSWORD
984 class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest):
986 Recreate index from file with hole.
991 CORRUPT = corrupt_entire_header
995 class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest):
998 class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest):
1002 class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest):
1004 PASSWORD = TEST_PASSWORD