bpo-32713: Fix tarfile.itn for large/negative float values. (GH-5434)
[python-delta-tar] / testing / test_recover.py
... / ...
CommitLineData
1"""
2Intra2net 2017
3
4===============================================================================
5 test_recover.py – behavior facing file corruption
6===============================================================================
7
8Corruptors have the signature ``(unittest × string × bool × bool) → void``,
9where the *string* argument is the name of the file to modify, the *booleans*
10specialize the operation for compressed and encrypted data. Issues are
11communicated upward by throwing.
12
13 - corrupt_header ():
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.
18
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.
24
25 - corrupt_ctsize ():
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.
31
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 result will
35 resemble a file with arbitrary leading data but all the remaining object
36 offsets intact, so the contents can still be extracted with index based
37 recovery.
38
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.
45
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
50 regardless.
51
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.
55
56 - corrupt_volume ():
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.
60
61 - corrupt_hole ():
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.
66
67"""
68
69import logging
70import os
71import shutil
72import stat
73import sys
74import unittest
75
76from functools import partial
77
78import deltatar.deltatar as deltatar
79import deltatar.crypto as crypto
80import deltatar.tarfile as tarfile
81
82from . import BaseTest
83
84TEST_PASSWORD = "test1234"
85TEST_VOLSIZ = 2 # MB
86TEST_FILESPERVOL = 3
87VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into
88 # volumes; this is black magic
89TEST_BLOCKSIZE = 4096
90
91###############################################################################
92## helpers ##
93###############################################################################
94
95def flip_bits (fname, off, b=0x01, n=1):
96 """
97 Open file *fname* at offset *off*, replacing the next *n* bytes with
98 their values xor’ed with *b*.
99 """
100 fd = os.open (fname, os.O_RDWR)
101
102 try:
103 pos = os.lseek (fd, off, os.SEEK_SET)
104 assert pos == off
105 chunk = os.read (fd, n)
106 chunk = bytes (map (lambda v: v ^ b, chunk))
107 pos = os.lseek (fd, off, os.SEEK_SET)
108 assert pos == off
109 os.write (fd, chunk)
110 finally:
111 os.close (fd)
112
113
114def gz_header_size (fname, off=0):
115 """
116 Determine the length of the gzip header starting at *off* in file fname.
117
118 The header is variable length because it may contain the filename as NUL
119 terminated bytes.
120 """
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)
124
125 try:
126 pos = os.lseek (fd, off, os.SEEK_SET)
127 assert pos == off
128 while os.read (fd, 1)[0] != 0:
129 off += 1
130 pos = os.lseek (fd, off, os.SEEK_SET)
131 assert pos == off
132 finally:
133 os.close (fd)
134
135 return off
136
137
138def is_pdt_encrypted (fname):
139 """
140 Returns true if the file contains at least one PDT header plus enough
141 space for the object.
142 """
143 try:
144 with open (fname, "rb") as st:
145 hdr = crypto.hdr_read_stream (st)
146 siz = hdr ["ctsize"]
147 assert (len (st.read (siz)) == siz)
148 except Exception as exn:
149 return False
150 return True
151
152
153###############################################################################
154## corruption simulators ##
155###############################################################################
156
157class UndefinedTest (Exception):
158 """No test available for the asked combination of parameters."""
159
160def corrupt_header (_, fname, compress, encrypt):
161 """
162 Modify a significant byte in the object header of the format.
163 """
164 if encrypt is True: # damage GCM tag
165 flip_bits (fname, crypto.HDR_OFF_TAG + 1)
166 elif compress is True: # invalidate magic
167 flip_bits (fname, 1)
168 else: # Fudge checksum. From tar(5):
169 #
170 # struct header_gnu_tar {
171 # char name[100];
172 # char mode[8];
173 # char uid[8];
174 # char gid[8];
175 # char size[12];
176 # char mtime[12];
177 # char checksum[8];
178 # …
179 flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1)
180
181
182def corrupt_truncate (_, fname, _compress, _encrypt):
183 """
184 Shorten file by one third.
185 """
186 fd = os.open (fname, os.O_WRONLY)
187 size = os.lseek (fd, 0, os.SEEK_END)
188 os.ftruncate (fd, 2 * size // 3)
189 os.fsync (fd)
190 os.close (fd)
191
192
193def corrupt_ctsize (_, fname, compress, encrypt):
194 """
195 Blow up the size of an object so as to cause its apparent payload to leak
196 into the next one.
197 """
198 if encrypt is True:
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))
204
205
206def corrupt_entire_header (_, fname, compress, encrypt):
207 """
208 Flip all bits in the first object header.
209 """
210 if encrypt is True:
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))
214 else:
215 flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE)
216
217
218def corrupt_payload_start (_, fname, compress, encrypt):
219 """
220 Modify the byte following the object header structure of the format.
221 """
222 if encrypt is True:
223 flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1)
224 elif compress is True:
225 flip_bits (fname, gz_header_size (fname) + 1)
226 else:
227 flip_bits (fname, tarfile.BLOCKSIZE + 1)
228
229
230def corrupt_leading_garbage (_, fname, compress, encrypt):
231 """
232 Prepend junk to file.
233 """
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)
241
242 # write new file with garbage prepended
243 done = 0
244 os.write (outfd, junk) # junk first
245 done += len (junk)
246 while done < size:
247 data = os.read (infd, TEST_BLOCKSIZE)
248 os.write (outfd, data)
249 done += len (data)
250
251 assert os.lseek (outfd, 0, os.SEEK_CUR) == done
252
253 # close and free old file
254 os.close (infd)
255 os.unlink (fname)
256
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)
260 os.close (outfd)
261
262
263def corrupt_trailing_data (_, fname, compress, encrypt):
264 """
265 Append random data to file.
266 """
267 junk = os.urandom (42)
268 fd = os.open (fname, os.O_WRONLY | os.O_APPEND)
269 os.write (fd, junk)
270 os.close (fd)
271
272
273def corrupt_volume (_, fname, compress, encrypt):
274 """
275 Zero out an entire volume.
276 """
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)
281 while size > 0:
282 todo = min (size, TEST_BLOCKSIZE)
283 os.write (fd, zeros [:todo])
284 size -= todo
285 os.close (fd)
286
287
288def corrupt_hole (_, fname, compress, encrypt):
289 """
290 Cut file in three pieces, reassemble without the middle one.
291 """
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)
300
301 done = 0
302 while done < size:
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)
307 done += len (data)
308
309 os.close (infd)
310 os.unlink (fname)
311
312 path = "/proc/self/fd/%d" % outfd
313 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
314 os.close (outfd)
315
316def immaculate (_, _fname, _compress, _encrypt):
317 """
318 No-op dummy.
319 """
320 pass
321
322###############################################################################
323## tests ##
324###############################################################################
325
326class DefectiveTest (BaseTest):
327 """
328 Disaster recovery: restore corrupt backups.
329 """
330
331 COMPRESSION = None
332 PASSWORD = None
333 FAILURES = 0 # files that could not be restored
334 MISMATCHES = 0 # files that were restored but corrupted
335 CORRUPT = corrupt_payload_start
336 VOLUMES = 1
337 MISSING = None # normally the number of failures
338
339
340 def setUp(self):
341 '''
342 Create base test data
343 '''
344 self.pwd = os.getcwd()
345 self.dst_path = "source_dir"
346 self.src_path = "%s2" % self.dst_path
347 self.hash = dict()
348
349 os.system('rm -rf target_dir source_dir* backup_dir* huge')
350 os.makedirs (self.src_path)
351
352 for i in range (5):
353 f = "dummy_%d" % i
354 self.hash [f] = self.create_file ("%s/%s"
355 % (self.src_path, f), 5 + i)
356
357
358 def tearDown(self):
359 '''
360 Remove temporal files created by unit tests and reset globals.
361 '''
362 os.chdir(self.pwd)
363 os.system("rm -rf source_dir source_dir2 backup_dir*")
364
365
366 @staticmethod
367 def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
368 return backup_file % n
369
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"
375
376 if self.COMPRESSION is not None:
377 backup_file += ".gz"
378 backup_full += ".gz"
379 index_file += ".gz"
380
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)
385
386 return bak_path, backup_file, backup_full, index_file
387
388
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))
394 * 1024 * 1024)
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),
401 fsiz,
402 random=True)
403
404
405class RecoverTest (DefectiveTest):
406 """
407 Recover: restore corrupt backups from index file information.
408 """
409
410 def test_recover_corrupt (self):
411 """
412 Perform various damaging actions that cause unreadable objects.
413
414 Expects the extraction to fail in normal mode. With disaster recovery,
415 extraction must succeed, and exactly one file must be missing.
416 """
417 mode = self.COMPRESSION or "#"
418 bak_path, backup_file, backup_full, index_file = \
419 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
420
421 if self.VOLUMES > 1:
422 self.gen_multivol (self.VOLUMES)
423
424 vname = partial (self.default_volume_name, backup_file)
425 dtar = deltatar.DeltaTar (mode=mode,
426 logger=None,
427 password=self.PASSWORD,
428 index_name_func=lambda _: index_file,
429 volume_name_func=vname)
430
431 dtar.create_full_backup \
432 (source_path=self.src_path, backup_path=bak_path,
433 max_volume_size=1)
434
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))
439
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)
444 ],
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)
453
454 self.CORRUPT (backup_full,
455 self.COMPRESSION is not None,
456 self.PASSWORD is not None)
457
458 # normal restore must fail
459 try:
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:
464 pass
465 else:
466 raise
467 except tarfile.ReadError:
468 # can happen with all three modes
469 pass
470 except tarfile.DecryptionError:
471 if self.PASSWORD is not None:
472 pass
473 else:
474 raise
475
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)
481 ])
482
483 assert len (failed) == self.FAILURES
484
485 # with one file missing
486 missing = []
487 mismatch = []
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)
493 else:
494 missing.append (key)
495
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
500 # were valid.
501 assert len (missing) == (self.MISSING if self.MISSING is not None
502 else self.FAILURES)
503 assert len (mismatch) == self.MISMATCHES
504
505 shutil.rmtree (self.dst_path)
506
507
508class RescueTest (DefectiveTest):
509 """
510 Rescue: restore corrupt backups from backup set that is damaged to a degree
511 that the index file is worthless.
512 """
513
514 def test_rescue_corrupt (self):
515 """
516 Perform various damaging actions that cause unreadable objects, then
517 attempt to extract objects regardless.
518 """
519 mode = self.COMPRESSION or "#"
520 bak_path, backup_file, backup_full, index_file = \
521 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
522
523 if self.VOLUMES > 1:
524 self.gen_multivol (self.VOLUMES)
525
526 vname = partial (self.default_volume_name, backup_file)
527 dtar = deltatar.DeltaTar (mode=mode,
528 logger=None,
529 password=self.PASSWORD,
530 index_name_func=lambda _: index_file,
531 volume_name_func=vname)
532
533 dtar.create_full_backup \
534 (source_path=self.src_path, backup_path=bak_path,
535 max_volume_size=1)
536
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))
541
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)
546 ],
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)
555
556 self.CORRUPT (backup_full,
557 self.COMPRESSION is not None,
558 self.PASSWORD is not None)
559
560 # normal restore must fail
561 try:
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:
566 pass
567 else:
568 raise
569 except tarfile.ReadError:
570 # can happen with all three modes
571 pass
572 except tarfile.DecryptionError:
573 if self.PASSWORD is not None:
574 pass
575 else:
576 raise
577
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
583 missing = []
584 mismatch = []
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)
590 else:
591 missing.append (key)
592
593 assert len (failed) == self.FAILURES
594 assert len (missing) == (self.MISSING if self.MISSING is not None
595 else self.FAILURES)
596 assert len (mismatch) == self.MISMATCHES
597
598 shutil.rmtree (self.dst_path)
599
600
601class GenIndexTest (DefectiveTest):
602 """
603 Deducing an index for a backup with tarfile.
604 """
605
606 def test_gen_index (self):
607 """
608 Create backup, leave it unharmed, then generate an index.
609 """
610 mode = self.COMPRESSION or "#"
611 bak_path, backup_file, backup_full, index_file = \
612 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
613
614 if self.VOLUMES > 1:
615 self.gen_multivol (self.VOLUMES)
616
617 vname = partial (self.default_volume_name, backup_file)
618 dtar = deltatar.DeltaTar (mode=mode,
619 logger=None,
620 password=self.PASSWORD,
621 index_name_func=lambda _: index_file,
622 volume_name_func=vname)
623
624 dtar.create_full_backup \
625 (source_path=self.src_path, backup_path=bak_path,
626 max_volume_size=1)
627
628 def gen_volume_name (nvol):
629 return os.path.join (bak_path, vname (backup_full, True, nvol))
630
631 psidx = tarfile.gen_rescue_index (gen_volume_name,
632 mode,
633 password=self.PASSWORD)
634
635 # correct for objects spanning volumes: these are treated as separate
636 # in the index!
637 assert len (psidx) - self.VOLUMES + 1 == len (self.hash)
638
639
640###############################################################################
641# rescue
642###############################################################################
643
644class RecoverCorruptPayloadTestBase (RecoverTest):
645 COMPRESSION = None
646 PASSWORD = None
647 FAILURES = 0 # tarfile will restore but corrupted, as
648 MISMATCHES = 1 # revealed by the hash
649
650class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
651 VOLUMES = 1
652
653class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
654 VOLUMES = 3
655
656
657class RecoverCorruptPayloadGZTestBase (RecoverTest):
658 COMPRESSION = "#gz"
659 PASSWORD = None
660 FAILURES = 1
661 MISMATCHES = 0
662
663class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
664 VOLUMES = 1
665
666class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
667 VOLUMES = 3
668
669
670class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
671 COMPRESSION = "#gz"
672 PASSWORD = TEST_PASSWORD
673 FAILURES = 1
674 MISMATCHES = 0
675
676class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
677 VOLUMES = 1
678
679class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
680 VOLUMES = 3
681
682
683class RecoverCorruptHeaderTestBase (RecoverTest):
684 COMPRESSION = None
685 PASSWORD = None
686 FAILURES = 1
687 CORRUPT = corrupt_header
688 MISMATCHES = 0
689
690class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
691 VOLUMES = 1
692
693class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
694 VOLUMES = 3
695
696
697class RecoverCorruptHeaderGZTestBase (RecoverTest):
698 COMPRESSION = "#gz"
699 PASSWORD = None
700 FAILURES = 1
701 CORRUPT = corrupt_header
702 MISMATCHES = 0
703
704class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
705 VOLUMES = 1
706
707class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
708 VOLUMES = 3
709
710
711class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
712 COMPRESSION = "#gz"
713 PASSWORD = TEST_PASSWORD
714 FAILURES = 1
715 CORRUPT = corrupt_header
716 MISMATCHES = 0
717
718class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
719 VOLUMES = 1
720
721class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
722 VOLUMES = 3
723
724
725class RecoverCorruptTruncateTestBase (RecoverTest):
726 COMPRESSION = None
727 PASSWORD = None
728 FAILURES = 0
729 CORRUPT = corrupt_truncate
730 MISMATCHES = 0
731
732class RecoverCorruptTruncateTest (RecoverCorruptTruncateTestBase):
733 pass
734
735class RecoverCorruptTruncateGZTest (RecoverCorruptTruncateTestBase):
736 """Two files that failed missing."""
737 COMPRESSION = "#gz"
738 FAILURES = 2
739
740class RecoverCorruptTruncateGZAESTest (RecoverCorruptTruncateTestBase):
741 """Two files that failed missing."""
742 COMPRESSION = "#gz"
743 PASSWORD = TEST_PASSWORD
744 FAILURES = 2
745
746
747class RecoverCorruptEntireHeaderTestBase (RecoverTest):
748 COMPRESSION = None
749 PASSWORD = None
750 FAILURES = 1
751 CORRUPT = corrupt_entire_header
752 MISMATCHES = 0
753
754class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
755 VOLUMES = 1
756
757class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
758 VOLUMES = 3
759
760
761class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
762 COMPRESSION = "#gz"
763 PASSWORD = None
764 FAILURES = 1
765 CORRUPT = corrupt_entire_header
766 MISMATCHES = 0
767
768class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
769 VOLUMES = 1
770
771class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
772 VOLUMES = 3
773
774
775class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
776 COMPRESSION = "#gz"
777 PASSWORD = TEST_PASSWORD
778 FAILURES = 1
779 CORRUPT = corrupt_entire_header
780 MISMATCHES = 0
781
782class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
783 VOLUMES = 1
784
785class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
786 VOLUMES = 3
787
788
789class RecoverCorruptTrailingDataTestBase (RecoverTest):
790 # plain Tar is indifferent against traling data and the results
791 # are consistent
792 COMPRESSION = None
793 PASSWORD = None
794 FAILURES = 0
795 CORRUPT = corrupt_trailing_data
796 MISMATCHES = 0
797
798class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
799 VOLUMES = 1
800
801class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
802 # the last object in first archive has extra bytes somewhere in the
803 # middle because tar itself performs no data checksumming.
804 MISMATCHES = 1
805 VOLUMES = 3
806
807
808class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
809 # reading past the final object will cause decompression failure;
810 # all objects except for the last survive unharmed though
811 COMPRESSION = "#gz"
812 PASSWORD = None
813 FAILURES = 1
814 CORRUPT = corrupt_trailing_data
815 MISMATCHES = 0
816
817class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
818 VOLUMES = 1
819
820class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
821 VOLUMES = 3
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.
827 MISMATCHES = 1
828 MISSING = 0
829
830
831class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
832 COMPRESSION = "#gz"
833 PASSWORD = TEST_PASSWORD
834 FAILURES = 0
835 CORRUPT = corrupt_trailing_data
836 MISMATCHES = 0
837
838class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
839 VOLUMES = 1
840
841class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
842 VOLUMES = 3
843
844
845class RecoverCorruptVolumeBaseTest (RecoverTest):
846 COMPRESSION = None
847 PASSWORD = None
848 FAILURES = 8
849 CORRUPT = corrupt_volume
850 VOLUMES = 3
851
852class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
853 pass
854
855class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
856 COMPRESSION = "#gz"
857
858class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
859 COMPRESSION = "#gz"
860 PASSWORD = TEST_PASSWORD
861
862
863@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
864class RecoverCorruptHoleBaseTest (RecoverTest):
865 """
866 Cut bytes from the middle of a volume.
867
868 Index-based recovery works only up to the hole.
869 """
870 COMPRESSION = None
871 PASSWORD = None
872 FAILURES = 3
873 CORRUPT = corrupt_hole
874 VOLUMES = 2 # request two vols to swell up the first one
875 MISMATCHES = 1
876
877@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
878class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
879 FAILURES = 2
880
881@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
882class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
883 COMPRESSION = "#gz"
884 MISSING = 2
885
886@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
887class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
888 COMPRESSION = "#gz"
889 PASSWORD = TEST_PASSWORD
890 MISSING = 2
891
892###############################################################################
893# rescue
894###############################################################################
895
896class RescueCorruptTruncateTestBase (RescueTest):
897 COMPRESSION = None
898 PASSWORD = None
899 FAILURES = 0
900 CORRUPT = corrupt_truncate
901 MISMATCHES = 0
902
903class RescueCorruptTruncateTest (RescueCorruptTruncateTestBase):
904 pass
905
906class RescueCorruptTruncateGZTest (RescueCorruptTruncateTestBase):
907 """Two files that failed missing."""
908 COMPRESSION = "#gz"
909 MISSING = 2
910
911class RescueCorruptTruncateGZAESTest (RescueCorruptTruncateTestBase):
912 """Two files missing but didn’t fail on account of their absence."""
913 COMPRESSION = "#gz"
914 PASSWORD = TEST_PASSWORD
915 MISSING = 2
916
917
918@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
919class RescueCorruptHoleBaseTest (RescueTest):
920 """
921 Cut bytes from the middle of a volume.
922 """
923 COMPRESSION = None
924 PASSWORD = None
925 FAILURES = 0
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
930
931@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
932class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
933 pass
934
935@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
936class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
937 COMPRESSION = "#gz"
938 # the decompressor explodes in our face processing the first dummy, nothing
939 # we can do to recover
940 FAILURES = 1
941
942@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
943class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
944 COMPRESSION = "#gz"
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
949 FAILURES = 1
950
951
952class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
953 COMPRESSION = "#gz"
954 PASSWORD = TEST_PASSWORD
955 FAILURES = 0
956 CORRUPT = corrupt_ctsize
957 MISMATCHES = 0
958
959
960@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
961class RescueCorruptLeadingGarbageTestBase (RescueTest):
962 # plain Tar is indifferent against traling data and the results
963 # are consistent
964 COMPRESSION = None
965 PASSWORD = None
966 FAILURES = 0
967 CORRUPT = corrupt_leading_garbage
968 MISMATCHES = 0
969
970@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
971class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
972 VOLUMES = 1
973
974@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
975class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase):
976 # the last object in first archive has extra bytes somewhere in the
977 # middle because tar itself performs no data checksumming.
978 MISMATCHES = 2
979 VOLUMES = 3
980
981
982###############################################################################
983# index
984###############################################################################
985
986class GenIndexIntactBaseTest (GenIndexTest):
987 """
988 """
989 COMPRESSION = None
990 PASSWORD = None
991 FAILURES = 0
992 CORRUPT = immaculate
993 VOLUMES = 1
994 MISMATCHES = 1
995
996class GenIndexIntactSingleTest (GenIndexIntactBaseTest):
997 pass
998
999class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest):
1000 COMPRESSION = "#gz"
1001 MISSING = 2
1002
1003class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest):
1004 COMPRESSION = "#gz"
1005 PASSWORD = TEST_PASSWORD
1006 MISSING = 2
1007
1008class GenIndexIntactMultiTest (GenIndexIntactBaseTest):
1009 VOLUMES = 3
1010 pass
1011
1012class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest):
1013 VOLUMES = 3
1014 COMPRESSION = "#gz"
1015 MISSING = 2
1016
1017class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest):
1018 VOLUMES = 3
1019 COMPRESSION = "#gz"
1020 PASSWORD = TEST_PASSWORD
1021 MISSING = 2
1022
1023
1024class GenIndexCorruptTruncateBaseTest (GenIndexTest):
1025 """
1026 Recreate index from file that lacks the latter portion.
1027 """
1028 COMPRESSION = None
1029 PASSWORD = None
1030 FAILURES = 0
1031 CORRUPT = corrupt_truncate
1032 MISSING = 2
1033
1034class GenIndexCorruptTruncateTest (GenIndexCorruptTruncateBaseTest):
1035 pass
1036
1037class GenIndexCorruptTruncateGZTest (GenIndexCorruptTruncateBaseTest):
1038 COMPRESSION = "#gz"
1039
1040class GenIndexCorruptTruncateGZAESTest (GenIndexCorruptTruncateBaseTest):
1041 COMPRESSION = "#gz"
1042 PASSWORD = TEST_PASSWORD
1043
1044
1045class GenIndexCorruptHoleBaseTest (GenIndexTest):
1046 """
1047 Recreate index from file with hole.
1048 """
1049 COMPRESSION = None
1050 PASSWORD = None
1051 FAILURES = 0
1052 CORRUPT = corrupt_hole
1053 VOLUMES = 1
1054 MISMATCHES = 1
1055
1056class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest):
1057 pass
1058
1059class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest):
1060 COMPRESSION = "#gz"
1061 MISSING = 2
1062
1063class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest):
1064 COMPRESSION = "#gz"
1065 PASSWORD = TEST_PASSWORD
1066 MISSING = 2
1067
1068
1069class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest):
1070 """
1071 Recreate index from file with defective headers.
1072 """
1073 COMPRESSION = None
1074 PASSWORD = None
1075 FAILURES = 0
1076 CORRUPT = corrupt_entire_header
1077 VOLUMES = 1
1078 MISMATCHES = 1
1079
1080class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest):
1081 pass
1082
1083class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest):
1084 COMPRESSION = "#gz"
1085 MISSING = 2
1086
1087class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest):
1088 COMPRESSION = "#gz"
1089 PASSWORD = TEST_PASSWORD
1090 MISSING = 2
1091