66ba2474652208e8490e7cac9c3db01cc03e1897
[python-delta-tar] / testing / test_recover.py
1 import logging
2 import os
3 import shutil
4 import stat
5
6 from functools import partial
7
8 import deltatar.deltatar as deltatar
9 import deltatar.crypto   as crypto
10 import deltatar.tarfile  as tarfile
11
12 from . import BaseTest
13
14 TEST_PASSWORD     = "test1234"
15 TEST_VOLSIZ       = 2 # MB
16 TEST_FILESPERVOL  = 3
17 VOLUME_OVERHEAD   = 1.4 # account for tar overhead when fitting files into
18                         # volumes; this is black magic
19 TEST_BLOCKSIZE    = 4096
20
21 ###############################################################################
22 ## helpers                                                                   ##
23 ###############################################################################
24
25 def flip_bits (fname, off, b=0x01, n=1):
26     """
27     Open file *fname* at offset *off*, replacing the next *n* bytes with
28     their values xor’ed with *b*.
29     """
30     fd = os.open (fname, os.O_RDWR)
31
32     try:
33         pos = os.lseek (fd, off, os.SEEK_SET)
34         assert pos == off
35         chunk = os.read (fd, n)
36         chunk = bytes (map (lambda v: v ^ b, chunk))
37         pos = os.lseek (fd, off, os.SEEK_SET)
38         assert pos == off
39         os.write (fd, chunk)
40     finally:
41         os.close (fd)
42
43
44 def gz_header_size (fname, off=0):
45     """
46     Determine the length of the gzip header starting at *off* in file fname.
47
48     The header is variable length because it may contain the filename as NUL
49     terminated bytes.
50     """
51     # length so we need to determine where the actual payload starts
52     off = tarfile.GZ_HEADER_SIZE
53     fd = os.open (fname, os.O_RDONLY)
54
55     try:
56         pos = os.lseek (fd, off, os.SEEK_SET)
57         assert pos == off
58         while os.read (fd, 1)[0] != 0:
59             off += 1
60             pos = os.lseek (fd, off, os.SEEK_SET)
61             assert pos == off
62     finally:
63         os.close (fd)
64
65     return off
66
67
68 def is_pdt_encrypted (fname):
69     """
70     Returns true if the file contains at least one PDT header plus enough
71     space for the object.
72     """
73     try:
74         with open (fname, "rb") as st:
75             hdr = crypto.hdr_read_stream (st)
76             siz = hdr ["ctsize"]
77             assert (len (st.read (siz)) == siz)
78     except Exception as exn:
79         return False
80     return True
81
82
83 ###############################################################################
84 ## corruption simulators                                                     ##
85 ###############################################################################
86
87 class UndefinedTest (Exception):
88     """No test available for the asked combination of parameters."""
89
90 def corrupt_header (_, fname, compress, encrypt):
91     """
92     Modify a significant byte in the object header of the format.
93     """
94     if encrypt is True: # damage GCM tag
95         flip_bits (fname, crypto.HDR_OFF_TAG + 1)
96     elif compress is True: # invalidate magic
97         flip_bits (fname, 1)
98     else: # Fudge checksum. From tar(5):
99         #
100         #       struct header_gnu_tar {
101         #               char name[100];
102         #               char mode[8];
103         #               char uid[8];
104         #               char gid[8];
105         #               char size[12];
106         #               char mtime[12];
107         #               char checksum[8];
108         #               …
109         flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1)
110
111
112 def corrupt_ctsize (_, fname, compress, encrypt):
113     """
114     Blow up the size of an object so as to cause its apparent payload to leak
115     into the next one.
116     """
117     if encrypt is True:
118         # damage lowest bit of second least significant byte of size field;
119         # this effectively sets the ciphertext size to 422, causing it to
120         # extend over the next object into the third one.
121         return flip_bits (fname, crypto.HDR_OFF_CTSIZE + 1, b=0x01)
122     raise UndefinedTest ("corrupt_ctsize %s %s %s" % (fname, compress, encrypt))
123
124
125 def corrupt_entire_header (_, fname, compress, encrypt):
126     """
127     Flip all bits in the first object header.
128     """
129     if encrypt is True:
130         flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE)
131     elif compress is True: # invalidate magic
132         flip_bits (fname, 0, 0xff, gz_header_size (fname))
133     else:
134         flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE)
135
136
137 def corrupt_payload_start (_, fname, compress, encrypt):
138     """
139     Modify the byte following the object header structure of the format.
140     """
141     if encrypt is True:
142         flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1)
143     elif compress is True:
144         flip_bits (fname, gz_header_size (fname) + 1)
145     else:
146         flip_bits (fname, tarfile.BLOCKSIZE + 1)
147
148
149 def corrupt_leading_garbage (_, fname, compress, encrypt):
150     """
151     Prepend junk to file.
152     """
153     aname = os.path.abspath (fname)
154     infd  = os.open (fname, os.O_RDONLY)
155     size  = os.lseek (infd, 0, os.SEEK_END)
156     assert os.lseek (infd, 0, os.SEEK_SET) == 0
157     outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
158                      stat.S_IRUSR | stat.S_IWUSR)
159     junk  = os.urandom (42)
160
161     # write new file with garbage prepended
162     done = 0
163     os.write (outfd, junk) # junk first
164     done += len (junk)
165     while done < size:
166         data = os.read (infd, TEST_BLOCKSIZE)
167         os.write (outfd, data)
168         done += len (data)
169
170     assert os.lseek (outfd, 0, os.SEEK_CUR) == done
171
172     # close and free old file
173     os.close (infd)
174     os.unlink (fname)
175
176     # install the new file in its place, atomically
177     path = "/proc/self/fd/%d" % outfd
178     os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
179     os.close (outfd)
180
181
182 def corrupt_trailing_data (_, fname, compress, encrypt):
183     """
184     Modify the byte following the object header structure of the format.
185     """
186     junk = os.urandom (42)
187     fd = os.open (fname, os.O_WRONLY | os.O_APPEND)
188     os.write (fd, junk)
189     os.close (fd)
190
191
192 def corrupt_volume (_, fname, compress, encrypt):
193     """
194     Zero out an entire volume.
195     """
196     fd = os.open (fname, os.O_WRONLY)
197     size = os.lseek (fd, 0, os.SEEK_END)
198     assert os.lseek (fd, 0, os.SEEK_SET) == 0
199     zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
200     while size > 0:
201         todo = min (size, TEST_BLOCKSIZE)
202         os.write (fd, zeros [:todo])
203         size -= todo
204     os.close (fd)
205
206
207 def corrupt_hole (_, fname, compress, encrypt):
208     """
209     Cut file in three pieces, reassemble without the middle one.
210     """
211     aname = os.path.abspath (fname)
212     infd = os.open (fname, os.O_RDONLY)
213     size = os.lseek (infd, 0, os.SEEK_END)
214     assert os.lseek (infd, 0, os.SEEK_SET) == 0
215     assert size > 3 * TEST_BLOCKSIZE
216     hole = (size / 3, size * 2 / 3)
217     outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
218                      stat.S_IRUSR | stat.S_IWUSR)
219     
220     done = 0
221     while done < size:
222         data = os.read (infd, TEST_BLOCKSIZE)
223         if done < hole [0] or hole [1] < done:
224             # only copy from outside hole
225             os.write (outfd, data)
226         done += len (data)
227
228     os.close (infd)
229     os.unlink (fname)
230
231     path = "/proc/self/fd/%d" % outfd
232     os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
233     os.close (outfd)
234
235 def immaculate (_, _fname, _compress, _encrypt):
236     """
237     No-op dummy.
238     """
239     pass
240
241 ###############################################################################
242 ## tests                                                                     ##
243 ###############################################################################
244
245 class DefectiveTest (BaseTest):
246     """
247     Disaster recovery: restore corrupt backups.
248     """
249
250     COMPRESSION = None
251     PASSWORD    = None
252     FAILURES    = 0     # files that could not be restored
253     MISMATCHES  = 0     # files that were restored but corrupted
254     CORRUPT     = corrupt_payload_start
255     VOLUMES     = 1
256     MISSING     = None  # normally the number of failures
257
258
259     def setUp(self):
260         '''
261         Create base test data
262         '''
263         self.pwd      = os.getcwd()
264         self.dst_path = "source_dir"
265         self.src_path = "%s2" % self.dst_path
266         self.hash     = dict()
267
268         os.system('rm -rf target_dir source_dir* backup_dir* huge')
269         os.makedirs (self.src_path)
270
271         for i in range (5):
272             f = "dummy_%d" % i
273             self.hash [f] = self.create_file ("%s/%s"
274                                               % (self.src_path, f), 5 + i)
275
276
277     def tearDown(self):
278         '''
279         Remove temporal files created by unit tests and reset globals.
280         '''
281         os.chdir(self.pwd)
282         os.system("rm -rf source_dir source_dir2 backup_dir*")
283
284
285     @staticmethod
286     def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
287         return backup_file % n
288
289     def gen_file_names (self, comp, pw):
290         bak_path       = "backup_dir"
291         backup_file    = "the_full_backup_%0.2d.tar"
292         backup_full    = ("%s/%s" % (bak_path, backup_file)) % 0
293         index_file     = "the_full_index"
294
295         if self.COMPRESSION is not None:
296             backup_file += ".gz"
297             backup_full += ".gz"
298             index_file  += ".gz"
299
300         if self.PASSWORD is not None:
301             backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION)
302             backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION)
303             index_file  = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION)
304
305         return bak_path, backup_file, backup_full, index_file
306
307
308     def gen_multivol (self, nvol):
309         # add n files for one nth the volume size each, corrected
310         # for metadata and tar block overhead
311         fsiz = int (  (  TEST_VOLSIZ
312                        / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
313                     * 1024 * 1024)
314         fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
315         for i in range (fcnt):
316             nvol, invol = divmod(i, TEST_FILESPERVOL)
317             f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
318             self.hash [f] = self.create_file ("%s/%s"
319                                               % (self.src_path, f),
320                                               fsiz,
321                                               random=True)
322
323
324 class RecoverTest (DefectiveTest):
325     """
326     Recover: restore corrupt backups from index file information.
327     """
328
329     def test_recover_corrupt (self):
330         """
331         Perform various damaging actions that cause unreadable objects.
332
333         Expects the extraction to fail in normal mode. With disaster recovery,
334         extraction must succeed, and exactly one file must be missing.
335         """
336         mode = self.COMPRESSION or "#"
337         bak_path, backup_file, backup_full, index_file = \
338             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
339
340         if self.VOLUMES > 1:
341             self.gen_multivol (self.VOLUMES)
342
343         vname = partial (self.default_volume_name, backup_file)
344         dtar = deltatar.DeltaTar (mode=mode,
345                                   logger=None,
346                                   password=self.PASSWORD,
347                                   index_name_func=lambda _: index_file,
348                                   volume_name_func=vname)
349
350         dtar.create_full_backup \
351             (source_path=self.src_path, backup_path=bak_path,
352              max_volume_size=1)
353
354         if self.PASSWORD is not None:
355             # ensure all files are at least superficially in PDT format
356             for f in os.listdir (bak_path):
357                 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
358
359         # first restore must succeed
360         dtar.restore_backup(target_path=self.dst_path,
361                             backup_indexes_paths=[
362                                 "%s/%s" % (bak_path, index_file)
363                             ])
364         for key, value in self.hash.items ():
365             f = "%s/%s" % (self.dst_path, key)
366             assert os.path.exists (f)
367             assert value == self.md5sum (f)
368         shutil.rmtree (self.dst_path)
369         shutil.rmtree (self.src_path)
370
371         self.CORRUPT (backup_full,
372                       self.COMPRESSION is not None,
373                       self.PASSWORD    is not None)
374
375         # normal restore must fail
376         try:
377             dtar.restore_backup(target_path=self.dst_path,
378                                 backup_tar_path=backup_full)
379         except tarfile.CompressionError:
380             if self.PASSWORD is not None or self.COMPRESSION is not None:
381                 pass
382             else:
383                 raise
384         except tarfile.ReadError:
385             # can happen with all three modes
386             pass
387         except tarfile.DecryptionError:
388             if self.PASSWORD is not None:
389                 pass
390             else:
391                 raise
392
393         os.chdir (self.pwd) # not restored due to the error above
394         # but recover will succeed
395         failed = dtar.recover_backup(target_path=self.dst_path,
396                                      backup_indexes_paths=[
397                                          "%s/%s" % (bak_path, index_file)
398                                      ])
399
400         assert len (failed) == self.FAILURES
401
402         # with one file missing
403         missing  = []
404         mismatch = []
405         for key, value in self.hash.items ():
406             kkey = "%s/%s" % (self.dst_path, key)
407             if os.path.exists (kkey):
408                 if value != self.md5sum (kkey):
409                     mismatch.append (key)
410             else:
411                 missing.append (key)
412
413         # usually, an object whose extraction fails will not be found on
414         # disk afterwards so the number of failures equals that of missing
415         # files. however, some modes will create partial files for objects
416         # spanning multiple volumes that contain the parts whose checksums
417         # were valid.
418         assert len (missing)  == (self.MISSING if self.MISSING is not None
419                                                else self.FAILURES)
420         assert len (mismatch) == self.MISMATCHES
421
422         shutil.rmtree (self.dst_path)
423
424
425 class RescueTest (DefectiveTest):
426     """
427     Rescue: restore corrupt backups from backup set that is damaged to a degree
428     that the index file is worthless.
429     """
430
431     def test_rescue_corrupt (self):
432         """
433         Perform various damaging actions that cause unreadable objects, then
434         attempt to extract objects regardless.
435         """
436         mode = self.COMPRESSION or "#"
437         bak_path, backup_file, backup_full, index_file = \
438             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
439
440         if self.VOLUMES > 1:
441             self.gen_multivol (self.VOLUMES)
442
443         vname = partial (self.default_volume_name, backup_file)
444         dtar = deltatar.DeltaTar (mode=mode,
445                                   logger=None,
446                                   password=self.PASSWORD,
447                                   index_name_func=lambda _: index_file,
448                                   volume_name_func=vname)
449
450         dtar.create_full_backup \
451             (source_path=self.src_path, backup_path=bak_path,
452              max_volume_size=1)
453
454         if self.PASSWORD is not None:
455             # ensure all files are at least superficially in PDT format
456             for f in os.listdir (bak_path):
457                 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
458
459         # first restore must succeed
460         dtar.restore_backup(target_path=self.dst_path,
461                             backup_indexes_paths=[
462                                 "%s/%s" % (bak_path, index_file)
463                             ])
464         for key, value in self.hash.items ():
465             f = "%s/%s" % (self.dst_path, key)
466             assert os.path.exists (f)
467             assert value == self.md5sum (f)
468         shutil.rmtree (self.dst_path)
469         shutil.rmtree (self.src_path)
470
471         self.CORRUPT (backup_full,
472                       self.COMPRESSION is not None,
473                       self.PASSWORD    is not None)
474
475         # normal restore must fail
476         try:
477             dtar.restore_backup(target_path=self.dst_path,
478                                 backup_tar_path=backup_full)
479         except tarfile.CompressionError:
480             if self.PASSWORD is not None or self.COMPRESSION is not None:
481                 pass
482             else:
483                 raise
484         except tarfile.ReadError:
485             # can happen with all three modes
486             pass
487         except tarfile.DecryptionError:
488             if self.PASSWORD is not None:
489                 pass
490             else:
491                 raise
492
493         os.chdir (self.pwd) # not restored due to the error above
494         # but recover will succeed
495         failed = dtar.rescue_backup(target_path=self.dst_path,
496                                     backup_tar_path=backup_full)
497         # with one file missing
498         missing  = []
499         mismatch = []
500         for key, value in self.hash.items ():
501             kkey = "%s/%s" % (self.dst_path, key)
502             if os.path.exists (kkey):
503                 if value != self.md5sum (kkey):
504                     mismatch.append (key)
505             else:
506                 missing.append (key)
507
508         assert len (failed)   == self.FAILURES
509         assert len (missing)  == (self.MISSING if self.MISSING is not None
510                                                else self.FAILURES)
511         assert len (mismatch) == self.MISMATCHES
512
513         shutil.rmtree (self.dst_path)
514
515
516 class GenIndexTest (DefectiveTest):
517     """
518     Deducing an index for a backup with tarfile.
519     """
520
521     def test_gen_index (self):
522         """
523         Create backup, leave it unharmed, then generate an index.
524         """
525         mode = self.COMPRESSION or "#"
526         bak_path, backup_file, backup_full, index_file = \
527             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
528
529         if self.VOLUMES > 1:
530             self.gen_multivol (self.VOLUMES)
531
532         vname = partial (self.default_volume_name, backup_file)
533         dtar = deltatar.DeltaTar (mode=mode,
534                                   logger=None,
535                                   password=self.PASSWORD,
536                                   index_name_func=lambda _: index_file,
537                                   volume_name_func=vname)
538
539         dtar.create_full_backup \
540             (source_path=self.src_path, backup_path=bak_path,
541              max_volume_size=1)
542
543         def gen_volume_name (nvol):
544             return os.path.join (bak_path, vname (backup_full, True, nvol))
545
546         psidx = tarfile.gen_rescue_index (gen_volume_name,
547                                           mode,
548                                           password=self.PASSWORD)
549
550         # correct for objects spanning volumes: these are treated as separate
551         # in the index!
552         assert len (psidx) - self.VOLUMES + 1 == len (self.hash)
553
554
555 ###############################################################################
556 # rescue
557 ###############################################################################
558
559 class RecoverCorruptPayloadTestBase (RecoverTest):
560     COMPRESSION = None
561     PASSWORD    = None
562     FAILURES    = 0 # tarfile will restore but corrupted, as
563     MISMATCHES  = 1 # revealed by the hash
564
565 class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
566     VOLUMES     = 1
567
568 class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
569     VOLUMES     = 3
570
571
572 class RecoverCorruptPayloadGZTestBase (RecoverTest):
573     COMPRESSION = "#gz"
574     PASSWORD    = None
575     FAILURES    = 1
576     MISMATCHES  = 0
577
578 class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
579     VOLUMES     = 1
580
581 class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
582     VOLUMES     = 3
583
584
585 class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
586     COMPRESSION = "#gz"
587     PASSWORD    = TEST_PASSWORD
588     FAILURES    = 1
589     MISMATCHES  = 0
590
591 class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
592     VOLUMES     = 1
593
594 class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
595     VOLUMES     = 3
596
597
598 class RecoverCorruptHeaderTestBase (RecoverTest):
599     COMPRESSION = None
600     PASSWORD    = None
601     FAILURES    = 1
602     CORRUPT     = corrupt_header
603     MISMATCHES  = 0
604
605 class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
606     VOLUMES     = 1
607
608 class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
609     VOLUMES     = 3
610
611
612 class RecoverCorruptHeaderGZTestBase (RecoverTest):
613     COMPRESSION = "#gz"
614     PASSWORD    = None
615     FAILURES    = 1
616     CORRUPT     = corrupt_header
617     MISMATCHES  = 0
618
619 class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
620     VOLUMES     = 1
621
622 class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
623     VOLUMES     = 3
624
625
626 class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
627     COMPRESSION = "#gz"
628     PASSWORD    = TEST_PASSWORD
629     FAILURES    = 1
630     CORRUPT     = corrupt_header
631     MISMATCHES  = 0
632
633 class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
634     VOLUMES     = 1
635
636 class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
637     VOLUMES     = 3
638
639
640 class RecoverCorruptEntireHeaderTestBase (RecoverTest):
641     COMPRESSION = None
642     PASSWORD    = None
643     FAILURES    = 1
644     CORRUPT     = corrupt_entire_header
645     MISMATCHES  = 0
646
647 class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
648     VOLUMES     = 1
649
650 class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
651     VOLUMES     = 3
652
653
654 class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
655     COMPRESSION = "#gz"
656     PASSWORD    = None
657     FAILURES    = 1
658     CORRUPT     = corrupt_entire_header
659     MISMATCHES  = 0
660
661 class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
662     VOLUMES     = 1
663
664 class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
665     VOLUMES     = 3
666
667
668 class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
669     COMPRESSION = "#gz"
670     PASSWORD    = TEST_PASSWORD
671     FAILURES    = 1
672     CORRUPT     = corrupt_entire_header
673     MISMATCHES  = 0
674
675 class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
676     VOLUMES     = 1
677
678 class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
679     VOLUMES     = 3
680
681
682 class RecoverCorruptTrailingDataTestBase (RecoverTest):
683     # plain Tar is indifferent against traling data and the results
684     # are consistent
685     COMPRESSION = None
686     PASSWORD    = None
687     FAILURES    = 0
688     CORRUPT     = corrupt_trailing_data
689     MISMATCHES  = 0
690
691 class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
692     VOLUMES     = 1
693
694 class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
695     # the last object in first archive has extra bytes somewhere in the
696     # middle because tar itself performs no data checksumming.
697     MISMATCHES  = 1
698     VOLUMES     = 3
699
700
701 class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
702     # reading past the final object will cause decompression failure;
703     # all objects except for the last survive unharmed though
704     COMPRESSION = "#gz"
705     PASSWORD    = None
706     FAILURES    = 1
707     CORRUPT     = corrupt_trailing_data
708     MISMATCHES  = 0
709
710 class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
711     VOLUMES     = 1
712
713 class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
714     VOLUMES     = 3
715     # the last file of the first volume will only contain the data of the
716     # second part which is contained in the second volume. this happens
717     # because the CRC32 is wrong for the first part so it gets discarded, then
718     # the object is recreated from the first header of the second volume,
719     # containing only the remainder of the data.
720     MISMATCHES  = 1
721     MISSING     = 0
722
723
724 class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
725     COMPRESSION = "#gz"
726     PASSWORD    = TEST_PASSWORD
727     FAILURES    = 0
728     CORRUPT     = corrupt_trailing_data
729     MISMATCHES  = 0
730
731 class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
732     VOLUMES     = 1
733
734 class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
735     VOLUMES     = 3
736
737
738 class RecoverCorruptVolumeBaseTest (RecoverTest):
739     COMPRESSION = None
740     PASSWORD    = None
741     FAILURES    = 8
742     CORRUPT     = corrupt_volume
743     VOLUMES     = 3
744
745 class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
746     pass
747
748 class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
749     COMPRESSION = "#gz"
750
751 class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
752     COMPRESSION = "#gz"
753     PASSWORD    = TEST_PASSWORD
754
755
756 class RecoverCorruptHoleBaseTest (RecoverTest):
757     """
758     Cut bytes from the middle of a volume.
759
760     Index-based recovery works only up to the hole.
761     """
762     COMPRESSION = None
763     PASSWORD    = None
764     FAILURES    = 3
765     CORRUPT     = corrupt_hole
766     VOLUMES     = 2 # request two vols to swell up the first one
767     MISMATCHES  = 1
768
769 class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
770     FAILURES    = 2
771
772 class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
773     COMPRESSION = "#gz"
774     MISSING     = 2
775
776 class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
777     COMPRESSION = "#gz"
778     PASSWORD    = TEST_PASSWORD
779     MISSING     = 2
780
781 ###############################################################################
782 # rescue
783 ###############################################################################
784
785 class RescueCorruptHoleBaseTest (RescueTest):
786     """
787     Cut bytes from the middle of a volume.
788     """
789     COMPRESSION = None
790     PASSWORD    = None
791     FAILURES    = 0
792     CORRUPT     = corrupt_hole
793     VOLUMES     = 2 # request two vols to swell up the first one
794     MISMATCHES  = 2 # intersected by hole
795     MISSING     = 1 # excised by hole
796
797 class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
798     pass
799
800 class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
801     COMPRESSION = "#gz"
802     # the decompressor explodes in our face processing the first dummy, nothing
803     # we can do to recover
804     FAILURES    = 1
805
806 class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
807     COMPRESSION = "#gz"
808     PASSWORD    = TEST_PASSWORD
809     # again, ignoring the crypto errors yields a bad zlib stream causing the
810     # decompressor to abort where the hole begins; the file is extracted up
811     # to this point though
812     FAILURES    = 1
813
814
815 class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
816     COMPRESSION = "#gz"
817     PASSWORD    = TEST_PASSWORD
818     FAILURES    = 0
819     CORRUPT     = corrupt_ctsize
820     MISMATCHES  = 0
821
822
823 class RescueCorruptLeadingGarbageTestBase (RescueTest):
824     # plain Tar is indifferent against traling data and the results
825     # are consistent
826     COMPRESSION = None
827     PASSWORD    = None
828     FAILURES    = 0
829     CORRUPT     = corrupt_leading_garbage
830     MISMATCHES  = 0
831
832 class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
833     VOLUMES     = 1
834
835 class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase):
836     # the last object in first archive has extra bytes somewhere in the
837     # middle because tar itself performs no data checksumming.
838     MISMATCHES  = 2
839     VOLUMES     = 3
840
841
842 ###############################################################################
843 # index
844 ###############################################################################
845
846 class GenIndexIntactBaseTest (GenIndexTest):
847     """
848     """
849     COMPRESSION = None
850     PASSWORD    = None
851     FAILURES    = 0
852     CORRUPT     = immaculate
853     VOLUMES     = 1
854     MISMATCHES  = 1
855
856 class GenIndexIntactSingleTest (GenIndexIntactBaseTest):
857     pass
858
859 class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest):
860     COMPRESSION = "#gz"
861     MISSING     = 2
862
863 class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest):
864     COMPRESSION = "#gz"
865     PASSWORD    = TEST_PASSWORD
866     MISSING     = 2
867
868 class GenIndexIntactMultiTest (GenIndexIntactBaseTest):
869     VOLUMES     = 3
870     pass
871
872 class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest):
873     VOLUMES     = 3
874     COMPRESSION = "#gz"
875     MISSING     = 2
876
877 class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest):
878     VOLUMES     = 3
879     COMPRESSION = "#gz"
880     PASSWORD    = TEST_PASSWORD
881     MISSING     = 2
882
883
884 class GenIndexCorruptHoleBaseTest (GenIndexTest):
885     """
886     Recreate index from file with hole.
887     """
888     COMPRESSION = None
889     PASSWORD    = None
890     FAILURES    = 0
891     CORRUPT     = corrupt_hole
892     VOLUMES     = 1
893     MISMATCHES  = 1
894
895 class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest):
896     pass
897
898 class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest):
899     COMPRESSION = "#gz"
900     MISSING     = 2
901
902 class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest):
903     COMPRESSION = "#gz"
904     PASSWORD    = TEST_PASSWORD
905     MISSING     = 2
906
907
908
909 class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest):
910     """
911     Recreate index from file with hole.
912     """
913     COMPRESSION = None
914     PASSWORD    = None
915     FAILURES    = 0
916     CORRUPT     = corrupt_entire_header
917     VOLUMES     = 1
918     MISMATCHES  = 1
919
920 class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest):
921     pass
922
923 class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest):
924     COMPRESSION = "#gz"
925     MISSING     = 2
926
927 class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest):
928     COMPRESSION = "#gz"
929     PASSWORD    = TEST_PASSWORD
930     MISSING     = 2
931