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.
19 - corrupt_truncate ():
20 Drop the file’s content after two thirds, causing extraction of later
21 objects to fail. Since the operation preserves the offsets of objects
22 before the cutoff, this yields the same results regardless of whether
23 restore or rescue mode is used.
26 Modify the *ctsize* field of a PDTCRYPT header. The goal is to have
27 decryption continue past the end of the object, causing data
28 authentication to fail and file reads to be at odds with the offsets in
29 the index. Only applicable to encrypted archives; will raise
30 *UndefinedTest* otherwise.
32 - corrupt_entire_header ():
33 Invert all bits of the first object header (PDTCRYPT, gzip, tar) without
34 affecting the payload. This renders the object unreadable; the file will
35 be resemble one with arbitrary leading data but all the remaining object
36 offsets intact, so the contents can still be extracted with index based
39 - corrupt_payload_start ():
40 For all header variants, skip to the first byte past the header and
41 corrupt it. Encrypted objects will fail to authenticate. Compressed
42 objects will yield a bad CRC32. The Tar layer will take no notice but
43 the extracted object will fail an independent checksum comparison with
44 that of the original file.
46 - corrupt_leading_garbage ():
47 Prepend random data to an otherwise valid file. Creates a situation that
48 index based recovery cannot handle by shifting the offsets of all objects
49 in the file. In rescue mode, these objects must be located and extracted
52 - corrupt_trailing_data ():
53 Append data to an otherwise valid file. Both the recovery and rescue
54 modes must be able to retrieve all objects from that file.
57 Zero out an entire backup file. This is interesting for multivolume
58 tests: all files from the affected volume must be missing but objects
59 that span volume bounds will still be partially recoverable.
62 Remove a region from a file. Following the damaged part, no object can be
63 recovered in index mode, but rescue mode will still find those. The
64 object containing the start of the hole will fail checksum tests because
65 of the missing part and the overlap with the subsequent object.
76 from functools import partial
78 import deltatar.deltatar as deltatar
79 import deltatar.crypto as crypto
80 import deltatar.tarfile as tarfile
82 from . import BaseTest
84 TEST_PASSWORD = "test1234"
87 VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into
88 # volumes; this is black magic
91 ###############################################################################
93 ###############################################################################
95 def flip_bits (fname, off, b=0x01, n=1):
97 Open file *fname* at offset *off*, replacing the next *n* bytes with
98 their values xor’ed with *b*.
100 fd = os.open (fname, os.O_RDWR)
103 pos = os.lseek (fd, off, os.SEEK_SET)
105 chunk = os.read (fd, n)
106 chunk = bytes (map (lambda v: v ^ b, chunk))
107 pos = os.lseek (fd, off, os.SEEK_SET)
114 def gz_header_size (fname, off=0):
116 Determine the length of the gzip header starting at *off* in file fname.
118 The header is variable length because it may contain the filename as NUL
121 # length so we need to determine where the actual payload starts
122 off = tarfile.GZ_HEADER_SIZE
123 fd = os.open (fname, os.O_RDONLY)
126 pos = os.lseek (fd, off, os.SEEK_SET)
128 while os.read (fd, 1)[0] != 0:
130 pos = os.lseek (fd, off, os.SEEK_SET)
138 def is_pdt_encrypted (fname):
140 Returns true if the file contains at least one PDT header plus enough
141 space for the object.
144 with open (fname, "rb") as st:
145 hdr = crypto.hdr_read_stream (st)
147 assert (len (st.read (siz)) == siz)
148 except Exception as exn:
153 ###############################################################################
154 ## corruption simulators ##
155 ###############################################################################
157 class UndefinedTest (Exception):
158 """No test available for the asked combination of parameters."""
160 def corrupt_header (_, fname, compress, encrypt):
162 Modify a significant byte in the object header of the format.
164 if encrypt is True: # damage GCM tag
165 flip_bits (fname, crypto.HDR_OFF_TAG + 1)
166 elif compress is True: # invalidate magic
168 else: # Fudge checksum. From tar(5):
170 # struct header_gnu_tar {
179 flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1)
182 def corrupt_truncate (_, fname, _compress, _encrypt):
184 Shorten file by one third.
186 fd = os.open (fname, os.O_WRONLY)
187 size = os.lseek (fd, 0, os.SEEK_END)
188 os.ftruncate (fd, 2 * size // 3)
193 def corrupt_ctsize (_, fname, compress, encrypt):
195 Blow up the size of an object so as to cause its apparent payload to leak
199 # damage lowest bit of second least significant byte of size field;
200 # this effectively sets the ciphertext size to 422, causing it to
201 # extend over the next object into the third one.
202 return flip_bits (fname, crypto.HDR_OFF_CTSIZE + 1, b=0x01)
203 raise UndefinedTest ("corrupt_ctsize %s %s %s" % (fname, compress, encrypt))
206 def corrupt_entire_header (_, fname, compress, encrypt):
208 Flip all bits in the first object header.
211 flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE)
212 elif compress is True:
213 flip_bits (fname, 0, 0xff, gz_header_size (fname))
215 flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE)
218 def corrupt_payload_start (_, fname, compress, encrypt):
220 Modify the byte following the object header structure of the format.
223 flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1)
224 elif compress is True:
225 flip_bits (fname, gz_header_size (fname) + 1)
227 flip_bits (fname, tarfile.BLOCKSIZE + 1)
230 def corrupt_leading_garbage (_, fname, compress, encrypt):
232 Prepend junk to file.
234 aname = os.path.abspath (fname)
235 infd = os.open (fname, os.O_RDONLY)
236 size = os.lseek (infd, 0, os.SEEK_END)
237 assert os.lseek (infd, 0, os.SEEK_SET) == 0
238 outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
239 stat.S_IRUSR | stat.S_IWUSR)
240 junk = os.urandom (42)
242 # write new file with garbage prepended
244 os.write (outfd, junk) # junk first
247 data = os.read (infd, TEST_BLOCKSIZE)
248 os.write (outfd, data)
251 assert os.lseek (outfd, 0, os.SEEK_CUR) == done
253 # close and free old file
257 # install the new file in its place, atomically
258 path = "/proc/self/fd/%d" % outfd
259 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
263 def corrupt_trailing_data (_, fname, compress, encrypt):
265 Append random data to file.
267 junk = os.urandom (42)
268 fd = os.open (fname, os.O_WRONLY | os.O_APPEND)
273 def corrupt_volume (_, fname, compress, encrypt):
275 Zero out an entire volume.
277 fd = os.open (fname, os.O_WRONLY)
278 size = os.lseek (fd, 0, os.SEEK_END)
279 assert os.lseek (fd, 0, os.SEEK_SET) == 0
280 zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
282 todo = min (size, TEST_BLOCKSIZE)
283 os.write (fd, zeros [:todo])
288 def corrupt_hole (_, fname, compress, encrypt):
290 Cut file in three pieces, reassemble without the middle one.
292 aname = os.path.abspath (fname)
293 infd = os.open (fname, os.O_RDONLY)
294 size = os.lseek (infd, 0, os.SEEK_END)
295 assert os.lseek (infd, 0, os.SEEK_SET) == 0
296 assert size > 3 * TEST_BLOCKSIZE
297 hole = (size / 3, size * 2 / 3)
298 outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
299 stat.S_IRUSR | stat.S_IWUSR)
303 data = os.read (infd, TEST_BLOCKSIZE)
304 if done < hole [0] or hole [1] < done:
305 # only copy from outside hole
306 os.write (outfd, data)
312 path = "/proc/self/fd/%d" % outfd
313 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
316 def immaculate (_, _fname, _compress, _encrypt):
322 ###############################################################################
324 ###############################################################################
326 class DefectiveTest (BaseTest):
328 Disaster recovery: restore corrupt backups.
333 FAILURES = 0 # files that could not be restored
334 MISMATCHES = 0 # files that were restored but corrupted
335 CORRUPT = corrupt_payload_start
337 MISSING = None # normally the number of failures
342 Create base test data
344 self.pwd = os.getcwd()
345 self.dst_path = "source_dir"
346 self.src_path = "%s2" % self.dst_path
349 os.system('rm -rf target_dir source_dir* backup_dir* huge')
350 os.makedirs (self.src_path)
354 self.hash [f] = self.create_file ("%s/%s"
355 % (self.src_path, f), 5 + i)
360 Remove temporal files created by unit tests and reset globals.
363 os.system("rm -rf source_dir source_dir2 backup_dir*")
367 def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
368 return backup_file % n
370 def gen_file_names (self, comp, pw):
371 bak_path = "backup_dir"
372 backup_file = "the_full_backup_%0.2d.tar"
373 backup_full = ("%s/%s" % (bak_path, backup_file)) % 0
374 index_file = "the_full_index"
376 if self.COMPRESSION is not None:
381 if self.PASSWORD is not None:
382 backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION)
383 backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION)
384 index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION)
386 return bak_path, backup_file, backup_full, index_file
389 def gen_multivol (self, nvol):
390 # add n files for one nth the volume size each, corrected
391 # for metadata and tar block overhead
392 fsiz = int ( ( TEST_VOLSIZ
393 / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
395 fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
396 for i in range (fcnt):
397 nvol, invol = divmod(i, TEST_FILESPERVOL)
398 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
399 self.hash [f] = self.create_file ("%s/%s"
400 % (self.src_path, f),
405 class RecoverTest (DefectiveTest):
407 Recover: restore corrupt backups from index file information.
410 def test_recover_corrupt (self):
412 Perform various damaging actions that cause unreadable objects.
414 Expects the extraction to fail in normal mode. With disaster recovery,
415 extraction must succeed, and exactly one file must be missing.
417 mode = self.COMPRESSION or "#"
418 bak_path, backup_file, backup_full, index_file = \
419 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
422 self.gen_multivol (self.VOLUMES)
424 vname = partial (self.default_volume_name, backup_file)
425 dtar = deltatar.DeltaTar (mode=mode,
427 password=self.PASSWORD,
428 index_name_func=lambda _: index_file,
429 volume_name_func=vname)
431 dtar.create_full_backup \
432 (source_path=self.src_path, backup_path=bak_path,
435 if self.PASSWORD is not None:
436 # ensure all files are at least superficially in PDT format
437 for f in os.listdir (bak_path):
438 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
440 # first restore must succeed
441 dtar.restore_backup(target_path=self.dst_path,
442 backup_indexes_paths=[
443 "%s/%s" % (bak_path, index_file)
445 disaster=tarfile.TOLERANCE_RECOVER,
446 strict_validation=False)
447 for key, value in self.hash.items ():
448 f = "%s/%s" % (self.dst_path, key)
449 assert os.path.exists (f)
450 assert value == self.md5sum (f)
451 shutil.rmtree (self.dst_path)
452 shutil.rmtree (self.src_path)
454 self.CORRUPT (backup_full,
455 self.COMPRESSION is not None,
456 self.PASSWORD is not None)
458 # normal restore must fail
460 dtar.restore_backup(target_path=self.dst_path,
461 backup_tar_path=backup_full)
462 except tarfile.CompressionError:
463 if self.PASSWORD is not None or self.COMPRESSION is not None:
467 except tarfile.ReadError:
468 # can happen with all three modes
470 except tarfile.DecryptionError:
471 if self.PASSWORD is not None:
476 os.chdir (self.pwd) # not restored due to the error above
477 # but recover will succeed
478 failed = dtar.recover_backup(target_path=self.dst_path,
479 backup_indexes_paths=[
480 "%s/%s" % (bak_path, index_file)
483 assert len (failed) == self.FAILURES
485 # with one file missing
488 for key, value in self.hash.items ():
489 kkey = "%s/%s" % (self.dst_path, key)
490 if os.path.exists (kkey):
491 if value != self.md5sum (kkey):
492 mismatch.append (key)
496 # usually, an object whose extraction fails will not be found on
497 # disk afterwards so the number of failures equals that of missing
498 # files. however, some modes will create partial files for objects
499 # spanning multiple volumes that contain the parts whose checksums
501 assert len (missing) == (self.MISSING if self.MISSING is not None
503 assert len (mismatch) == self.MISMATCHES
505 shutil.rmtree (self.dst_path)
508 class RescueTest (DefectiveTest):
510 Rescue: restore corrupt backups from backup set that is damaged to a degree
511 that the index file is worthless.
514 def test_rescue_corrupt (self):
516 Perform various damaging actions that cause unreadable objects, then
517 attempt to extract objects regardless.
519 mode = self.COMPRESSION or "#"
520 bak_path, backup_file, backup_full, index_file = \
521 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
524 self.gen_multivol (self.VOLUMES)
526 vname = partial (self.default_volume_name, backup_file)
527 dtar = deltatar.DeltaTar (mode=mode,
529 password=self.PASSWORD,
530 index_name_func=lambda _: index_file,
531 volume_name_func=vname)
533 dtar.create_full_backup \
534 (source_path=self.src_path, backup_path=bak_path,
537 if self.PASSWORD is not None:
538 # ensure all files are at least superficially in PDT format
539 for f in os.listdir (bak_path):
540 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
542 # first restore must succeed
543 dtar.restore_backup(target_path=self.dst_path,
544 backup_indexes_paths=[
545 "%s/%s" % (bak_path, index_file)
547 disaster=tarfile.TOLERANCE_RECOVER,
548 strict_validation=False)
549 for key, value in self.hash.items ():
550 f = "%s/%s" % (self.dst_path, key)
551 assert os.path.exists (f)
552 assert value == self.md5sum (f)
553 shutil.rmtree (self.dst_path)
554 shutil.rmtree (self.src_path)
556 self.CORRUPT (backup_full,
557 self.COMPRESSION is not None,
558 self.PASSWORD is not None)
560 # normal restore must fail
562 dtar.restore_backup(target_path=self.dst_path,
563 backup_tar_path=backup_full)
564 except tarfile.CompressionError:
565 if self.PASSWORD is not None or self.COMPRESSION is not None:
569 except tarfile.ReadError:
570 # can happen with all three modes
572 except tarfile.DecryptionError:
573 if self.PASSWORD is not None:
578 os.chdir (self.pwd) # not restored due to the error above
579 # but recover will succeed
580 failed = dtar.rescue_backup(target_path=self.dst_path,
581 backup_tar_path=backup_full)
582 # with one file missing
585 for key, value in self.hash.items ():
586 kkey = "%s/%s" % (self.dst_path, key)
587 if os.path.exists (kkey):
588 if value != self.md5sum (kkey):
589 mismatch.append (key)
593 assert len (failed) == self.FAILURES
594 assert len (missing) == (self.MISSING if self.MISSING is not None
596 assert len (mismatch) == self.MISMATCHES
598 shutil.rmtree (self.dst_path)
601 class GenIndexTest (DefectiveTest):
603 Deducing an index for a backup with tarfile.
606 def test_gen_index (self):
608 Create backup, leave it unharmed, then generate an index.
610 mode = self.COMPRESSION or "#"
611 bak_path, backup_file, backup_full, index_file = \
612 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
615 self.gen_multivol (self.VOLUMES)
617 vname = partial (self.default_volume_name, backup_file)
618 dtar = deltatar.DeltaTar (mode=mode,
620 password=self.PASSWORD,
621 index_name_func=lambda _: index_file,
622 volume_name_func=vname)
624 dtar.create_full_backup \
625 (source_path=self.src_path, backup_path=bak_path,
628 def gen_volume_name (nvol):
629 return os.path.join (bak_path, vname (backup_full, True, nvol))
631 psidx = tarfile.gen_rescue_index (gen_volume_name,
633 password=self.PASSWORD)
635 # correct for objects spanning volumes: these are treated as separate
637 assert len (psidx) - self.VOLUMES + 1 == len (self.hash)
640 ###############################################################################
642 ###############################################################################
644 class RecoverCorruptPayloadTestBase (RecoverTest):
647 FAILURES = 0 # tarfile will restore but corrupted, as
648 MISMATCHES = 1 # revealed by the hash
650 class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
653 class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
657 class RecoverCorruptPayloadGZTestBase (RecoverTest):
663 class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
666 class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
670 class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
672 PASSWORD = TEST_PASSWORD
676 class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
679 class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
683 class RecoverCorruptHeaderTestBase (RecoverTest):
687 CORRUPT = corrupt_header
690 class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
693 class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
697 class RecoverCorruptHeaderGZTestBase (RecoverTest):
701 CORRUPT = corrupt_header
704 class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
707 class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
711 class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
713 PASSWORD = TEST_PASSWORD
715 CORRUPT = corrupt_header
718 class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
721 class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
725 class RecoverCorruptTruncateTestBase (RecoverTest):
729 CORRUPT = corrupt_truncate
732 class RecoverCorruptTruncateTest (RecoverCorruptTruncateTestBase):
735 class RecoverCorruptTruncateGZTest (RecoverCorruptTruncateTestBase):
736 """Two files that failed missing."""
740 class RecoverCorruptTruncateGZAESTest (RecoverCorruptTruncateTestBase):
741 """Two files that failed missing."""
743 PASSWORD = TEST_PASSWORD
747 class RecoverCorruptEntireHeaderTestBase (RecoverTest):
751 CORRUPT = corrupt_entire_header
754 class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
757 class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
761 class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
765 CORRUPT = corrupt_entire_header
768 class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
771 class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
775 class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
777 PASSWORD = TEST_PASSWORD
779 CORRUPT = corrupt_entire_header
782 class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
785 class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
789 class RecoverCorruptTrailingDataTestBase (RecoverTest):
790 # plain Tar is indifferent against traling data and the results
795 CORRUPT = corrupt_trailing_data
798 class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
801 class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
802 # the last object in first archive has extra bytes somewhere in the
803 # middle because tar itself performs no data checksumming.
808 class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
809 # reading past the final object will cause decompression failure;
810 # all objects except for the last survive unharmed though
814 CORRUPT = corrupt_trailing_data
817 class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
820 class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
822 # the last file of the first volume will only contain the data of the
823 # second part which is contained in the second volume. this happens
824 # because the CRC32 is wrong for the first part so it gets discarded, then
825 # the object is recreated from the first header of the second volume,
826 # containing only the remainder of the data.
831 class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
833 PASSWORD = TEST_PASSWORD
835 CORRUPT = corrupt_trailing_data
838 class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
841 class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
845 class RecoverCorruptVolumeBaseTest (RecoverTest):
849 CORRUPT = corrupt_volume
852 class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
855 class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
858 class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
860 PASSWORD = TEST_PASSWORD
863 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
864 class RecoverCorruptHoleBaseTest (RecoverTest):
866 Cut bytes from the middle of a volume.
868 Index-based recovery works only up to the hole.
873 CORRUPT = corrupt_hole
874 VOLUMES = 2 # request two vols to swell up the first one
877 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
878 class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
881 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
882 class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
886 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
887 class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
889 PASSWORD = TEST_PASSWORD
892 ###############################################################################
894 ###############################################################################
896 class RescueCorruptTruncateTestBase (RescueTest):
900 CORRUPT = corrupt_truncate
903 class RescueCorruptTruncateTest (RescueCorruptTruncateTestBase):
906 class RescueCorruptTruncateGZTest (RescueCorruptTruncateTestBase):
907 """Two files that failed missing."""
911 class RescueCorruptTruncateGZAESTest (RescueCorruptTruncateTestBase):
912 """Two files missing but didn’t fail on account of their absence."""
914 PASSWORD = TEST_PASSWORD
918 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
919 class RescueCorruptHoleBaseTest (RescueTest):
921 Cut bytes from the middle of a volume.
926 CORRUPT = corrupt_hole
927 VOLUMES = 2 # request two vols to swell up the first one
928 MISMATCHES = 2 # intersected by hole
929 MISSING = 1 # excised by hole
931 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
932 class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
935 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
936 class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
938 # the decompressor explodes in our face processing the first dummy, nothing
939 # we can do to recover
942 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
943 class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
945 PASSWORD = TEST_PASSWORD
946 # again, ignoring the crypto errors yields a bad zlib stream causing the
947 # decompressor to abort where the hole begins; the file is extracted up
948 # to this point though
952 class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
954 PASSWORD = TEST_PASSWORD
956 CORRUPT = corrupt_ctsize
960 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
961 class RescueCorruptLeadingGarbageTestBase (RescueTest):
962 # plain Tar is indifferent against traling data and the results
967 CORRUPT = corrupt_leading_garbage
970 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
971 class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
974 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
975 class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase):
976 # the last object in first archive has extra bytes somewhere in the
977 # middle because tar itself performs no data checksumming.
982 ###############################################################################
984 ###############################################################################
986 class GenIndexIntactBaseTest (GenIndexTest):
996 class GenIndexIntactSingleTest (GenIndexIntactBaseTest):
999 class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest):
1003 class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest):
1005 PASSWORD = TEST_PASSWORD
1008 class GenIndexIntactMultiTest (GenIndexIntactBaseTest):
1012 class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest):
1017 class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest):
1020 PASSWORD = TEST_PASSWORD
1024 class GenIndexCorruptTruncateBaseTest (GenIndexTest):
1026 Recreate index from file that lacks the latter portion.
1031 CORRUPT = corrupt_truncate
1034 class GenIndexCorruptTruncateTest (GenIndexCorruptTruncateBaseTest):
1037 class GenIndexCorruptTruncateGZTest (GenIndexCorruptTruncateBaseTest):
1040 class GenIndexCorruptTruncateGZAESTest (GenIndexCorruptTruncateBaseTest):
1042 PASSWORD = TEST_PASSWORD
1045 class GenIndexCorruptHoleBaseTest (GenIndexTest):
1047 Recreate index from file with hole.
1052 CORRUPT = corrupt_hole
1056 class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest):
1059 class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest):
1063 class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest):
1065 PASSWORD = TEST_PASSWORD
1069 class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest):
1071 Recreate index from file with hole.
1076 CORRUPT = corrupt_entire_header
1080 class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest):
1083 class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest):
1087 class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest):
1089 PASSWORD = TEST_PASSWORD