a769d916a724127a11f02dcc18b4a549ccb93893
[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 (512) # tar block sized
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 class RecoverTest (DefectiveTest):
309     """
310     Recover: restore corrupt backups from index file information.
311     """
312
313     def test_recover_corrupt (self):
314         """
315         Perform various damaging actions that cause unreadable objects.
316
317         Expects the extraction to fail in normal mode. With disaster recovery,
318         extraction must succeed, and exactly one file must be missing.
319         """
320         mode = self.COMPRESSION or "#"
321         bak_path, backup_file, backup_full, index_file = \
322             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
323
324         if self.VOLUMES > 1:
325             # add n files for one nth the volume size each, corrected
326             # for metadata and tar block overhead
327             fsiz = int (  (  TEST_VOLSIZ
328                            / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
329                         * 1024 * 1024)
330             fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
331             for i in range (fcnt):
332                 nvol, invol = divmod(i, TEST_FILESPERVOL)
333                 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
334                 self.hash [f] = self.create_file ("%s/%s"
335                                                   % (self.src_path, f),
336                                                   fsiz,
337                                                   random=True)
338
339         vname = partial (self.default_volume_name, backup_file)
340         dtar = deltatar.DeltaTar (mode=mode,
341                                   logger=None,
342                                   password=self.PASSWORD,
343                                   index_name_func=lambda _: index_file,
344                                   volume_name_func=vname)
345
346         dtar.create_full_backup \
347             (source_path=self.src_path, backup_path=bak_path,
348              max_volume_size=1)
349
350         if self.PASSWORD is not None:
351             # ensure all files are at least superficially in PDT format
352             for f in os.listdir (bak_path):
353                 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
354
355         # first restore must succeed
356         dtar.restore_backup(target_path=self.dst_path,
357                             backup_indexes_paths=[
358                                 "%s/%s" % (bak_path, index_file)
359                             ])
360         for key, value in self.hash.items ():
361             f = "%s/%s" % (self.dst_path, key)
362             assert os.path.exists (f)
363             assert value == self.md5sum (f)
364         shutil.rmtree (self.dst_path)
365         shutil.rmtree (self.src_path)
366
367         self.CORRUPT (backup_full,
368                       self.COMPRESSION is not None,
369                       self.PASSWORD    is not None)
370
371         # normal restore must fail
372         try:
373             dtar.restore_backup(target_path=self.dst_path,
374                                 backup_tar_path=backup_full)
375         except tarfile.CompressionError:
376             if self.PASSWORD is not None or self.COMPRESSION is not None:
377                 pass
378             else:
379                 raise
380         except tarfile.ReadError:
381             # can happen with all three modes
382             pass
383         except tarfile.DecryptionError:
384             if self.PASSWORD is not None:
385                 pass
386             else:
387                 raise
388
389         os.chdir (self.pwd) # not restored due to the error above
390         # but recover will succeed
391         failed = dtar.recover_backup(target_path=self.dst_path,
392                                      backup_indexes_paths=[
393                                          "%s/%s" % (bak_path, index_file)
394                                      ])
395
396         assert len (failed) == self.FAILURES
397
398         # with one file missing
399         missing  = []
400         mismatch = []
401         for key, value in self.hash.items ():
402             kkey = "%s/%s" % (self.dst_path, key)
403             if os.path.exists (kkey):
404                 if value != self.md5sum (kkey):
405                     mismatch.append (key)
406             else:
407                 missing.append (key)
408
409         # usually, an object whose extraction fails will not be found on
410         # disk afterwards so the number of failures equals that of missing
411         # files. however, some modes will create partial files for objects
412         # spanning multiple volumes that contain the parts whose checksums
413         # were valid.
414         assert len (missing)  == (self.MISSING if self.MISSING is not None
415                                                else self.FAILURES)
416         assert len (mismatch) == self.MISMATCHES
417
418         shutil.rmtree (self.dst_path)
419
420
421 class RescueTest (DefectiveTest):
422     """
423     Rescue: restore corrupt backups from backup set that is damaged to a degree
424     that the index file is worthless.
425     """
426
427     def test_rescue_corrupt (self):
428         """
429         Perform various damaging actions that cause unreadable objects, then
430         attempt to extract objects regardless.
431         """
432         mode = self.COMPRESSION or "#"
433         bak_path, backup_file, backup_full, index_file = \
434             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
435
436         if self.VOLUMES > 1:
437             # add n files for one nth the volume size each, corrected
438             # for metadata and tar block overhead
439             fsiz = int (  (  TEST_VOLSIZ
440                            / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
441                         * 1024 * 1024)
442             fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
443             for i in range (fcnt):
444                 nvol, invol = divmod(i, TEST_FILESPERVOL)
445                 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
446                 self.hash [f] = self.create_file ("%s/%s"
447                                                   % (self.src_path, f),
448                                                   fsiz,
449                                                   random=True)
450
451         vname = partial (self.default_volume_name, backup_file)
452         dtar = deltatar.DeltaTar (mode=mode,
453                                   logger=None,
454                                   password=self.PASSWORD,
455                                   index_name_func=lambda _: index_file,
456                                   volume_name_func=vname)
457
458         dtar.create_full_backup \
459             (source_path=self.src_path, backup_path=bak_path,
460              max_volume_size=1)
461
462         if self.PASSWORD is not None:
463             # ensure all files are at least superficially in PDT format
464             for f in os.listdir (bak_path):
465                 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
466
467         # first restore must succeed
468         dtar.restore_backup(target_path=self.dst_path,
469                             backup_indexes_paths=[
470                                 "%s/%s" % (bak_path, index_file)
471                             ])
472         for key, value in self.hash.items ():
473             f = "%s/%s" % (self.dst_path, key)
474             assert os.path.exists (f)
475             assert value == self.md5sum (f)
476         shutil.rmtree (self.dst_path)
477         shutil.rmtree (self.src_path)
478
479         self.CORRUPT (backup_full,
480                       self.COMPRESSION is not None,
481                       self.PASSWORD    is not None)
482
483         # normal restore must fail
484         try:
485             dtar.restore_backup(target_path=self.dst_path,
486                                 backup_tar_path=backup_full)
487         except tarfile.CompressionError:
488             if self.PASSWORD is not None or self.COMPRESSION is not None:
489                 pass
490             else:
491                 raise
492         except tarfile.ReadError:
493             # can happen with all three modes
494             pass
495         except tarfile.DecryptionError:
496             if self.PASSWORD is not None:
497                 pass
498             else:
499                 raise
500
501         os.chdir (self.pwd) # not restored due to the error above
502         # but recover will succeed
503         failed = dtar.rescue_backup(target_path=self.dst_path,
504                                     backup_tar_path=backup_full)
505         # with one file missing
506         missing  = []
507         mismatch = []
508         for key, value in self.hash.items ():
509             kkey = "%s/%s" % (self.dst_path, key)
510             if os.path.exists (kkey):
511                 if value != self.md5sum (kkey):
512                     mismatch.append (key)
513             else:
514                 missing.append (key)
515
516         assert len (failed)   == self.FAILURES
517         assert len (missing)  == (self.MISSING if self.MISSING is not None
518                                                else self.FAILURES)
519         assert len (mismatch) == self.MISMATCHES
520
521         shutil.rmtree (self.dst_path)
522
523
524 class GenIndexTest (DefectiveTest):
525     """
526     Deducing an index for a backup with tarfile.
527     """
528
529     def test_gen_index (self):
530         """
531         Create backup, leave it unharmed, then generate an index.
532         """
533         mode = self.COMPRESSION or "#"
534         bak_path, backup_file, backup_full, index_file = \
535             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
536
537         vname = partial (self.default_volume_name, backup_file)
538         dtar = deltatar.DeltaTar (mode=mode,
539                                   logger=None,
540                                   password=self.PASSWORD,
541                                   index_name_func=lambda _: index_file,
542                                   volume_name_func=vname)
543
544         dtar.create_full_backup \
545             (source_path=self.src_path, backup_path=bak_path,
546              max_volume_size=1)
547
548         psidx = tarfile.gen_rescue_index (backup_full, mode, password=self.PASSWORD)
549
550         assert len (psidx) == len (self.hash)
551
552
553 ###############################################################################
554 # rescue
555 ###############################################################################
556
557 class RecoverCorruptPayloadTestBase (RecoverTest):
558     COMPRESSION = None
559     PASSWORD    = None
560     FAILURES    = 0 # tarfile will restore but corrupted, as
561     MISMATCHES  = 1 # revealed by the hash
562
563 class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
564     VOLUMES     = 1
565
566 class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
567     VOLUMES     = 3
568
569
570 class RecoverCorruptPayloadGZTestBase (RecoverTest):
571     COMPRESSION = "#gz"
572     PASSWORD    = None
573     FAILURES    = 1
574     MISMATCHES  = 0
575
576 class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
577     VOLUMES     = 1
578
579 class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
580     VOLUMES     = 3
581
582
583 class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
584     COMPRESSION = "#gz"
585     PASSWORD    = TEST_PASSWORD
586     FAILURES    = 1
587     MISMATCHES  = 0
588
589 class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
590     VOLUMES     = 1
591
592 class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
593     VOLUMES     = 3
594
595
596 class RecoverCorruptHeaderTestBase (RecoverTest):
597     COMPRESSION = None
598     PASSWORD    = None
599     FAILURES    = 1
600     CORRUPT     = corrupt_header
601     MISMATCHES  = 0
602
603 class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
604     VOLUMES     = 1
605
606 class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
607     VOLUMES     = 3
608
609
610 class RecoverCorruptHeaderGZTestBase (RecoverTest):
611     COMPRESSION = "#gz"
612     PASSWORD    = None
613     FAILURES    = 1
614     CORRUPT     = corrupt_header
615     MISMATCHES  = 0
616
617 class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
618     VOLUMES     = 1
619
620 class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
621     VOLUMES     = 3
622
623
624 class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
625     COMPRESSION = "#gz"
626     PASSWORD    = TEST_PASSWORD
627     FAILURES    = 1
628     CORRUPT     = corrupt_header
629     MISMATCHES  = 0
630
631 class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
632     VOLUMES     = 1
633
634 class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
635     VOLUMES     = 3
636
637
638 class RecoverCorruptEntireHeaderTestBase (RecoverTest):
639     COMPRESSION = None
640     PASSWORD    = None
641     FAILURES    = 1
642     CORRUPT     = corrupt_entire_header
643     MISMATCHES  = 0
644
645 class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
646     VOLUMES     = 1
647
648 class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
649     VOLUMES     = 3
650
651
652 class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
653     COMPRESSION = "#gz"
654     PASSWORD    = None
655     FAILURES    = 1
656     CORRUPT     = corrupt_entire_header
657     MISMATCHES  = 0
658
659 class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
660     VOLUMES     = 1
661
662 class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
663     VOLUMES     = 3
664
665
666 class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
667     COMPRESSION = "#gz"
668     PASSWORD    = TEST_PASSWORD
669     FAILURES    = 1
670     CORRUPT     = corrupt_entire_header
671     MISMATCHES  = 0
672
673 class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
674     VOLUMES     = 1
675
676 class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
677     VOLUMES     = 3
678
679
680 class RecoverCorruptTrailingDataTestBase (RecoverTest):
681     # plain Tar is indifferent against traling data and the results
682     # are consistent
683     COMPRESSION = None
684     PASSWORD    = None
685     FAILURES    = 0
686     CORRUPT     = corrupt_trailing_data
687     MISMATCHES  = 0
688
689 class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
690     VOLUMES     = 1
691
692 class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
693     # the last object in first archive has extra bytes somewhere in the
694     # middle because tar itself performs no data checksumming.
695     MISMATCHES  = 1
696     VOLUMES     = 3
697
698
699 class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
700     # reading past the final object will cause decompression failure;
701     # all objects except for the last survive unharmed though
702     COMPRESSION = "#gz"
703     PASSWORD    = None
704     FAILURES    = 1
705     CORRUPT     = corrupt_trailing_data
706     MISMATCHES  = 0
707
708 class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
709     VOLUMES     = 1
710
711 class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
712     VOLUMES     = 3
713     # the last file of the first volume will only contain the data of the
714     # second part which is contained in the second volume. this happens
715     # because the CRC32 is wrong for the first part so it gets discarded, then
716     # the object is recreated from the first header of the second volume,
717     # containing only the remainder of the data.
718     MISMATCHES  = 1
719     MISSING     = 0
720
721
722 class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
723     COMPRESSION = "#gz"
724     PASSWORD    = TEST_PASSWORD
725     FAILURES    = 0
726     CORRUPT     = corrupt_trailing_data
727     MISMATCHES  = 0
728
729 class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
730     VOLUMES     = 1
731
732 class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
733     VOLUMES     = 3
734
735
736 class RecoverCorruptVolumeBaseTest (RecoverTest):
737     COMPRESSION = None
738     PASSWORD    = None
739     FAILURES    = 8
740     CORRUPT     = corrupt_volume
741     VOLUMES     = 3
742
743 class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
744     pass
745
746 class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
747     COMPRESSION = "#gz"
748
749 class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
750     COMPRESSION = "#gz"
751     PASSWORD    = TEST_PASSWORD
752
753
754 class RecoverCorruptHoleBaseTest (RecoverTest):
755     """
756     Cut bytes from the middle of a volume.
757
758     Index-based recovery works only up to the hole.
759     """
760     COMPRESSION = None
761     PASSWORD    = None
762     FAILURES    = 3
763     CORRUPT     = corrupt_hole
764     VOLUMES     = 2 # request two vols to swell up the first one
765     MISMATCHES  = 1
766
767 class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
768     FAILURES    = 2
769
770 class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
771     COMPRESSION = "#gz"
772     MISSING     = 2
773
774 class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
775     COMPRESSION = "#gz"
776     PASSWORD    = TEST_PASSWORD
777     MISSING     = 2
778
779 ###############################################################################
780 # rescue
781 ###############################################################################
782
783 class RescueCorruptHoleBaseTest (RescueTest):
784     """
785     Cut bytes from the middle of a volume.
786     """
787     COMPRESSION = None
788     PASSWORD    = None
789     FAILURES    = 0
790     CORRUPT     = corrupt_hole
791     VOLUMES     = 2 # request two vols to swell up the first one
792     MISMATCHES  = 2 # intersected by hole
793     MISSING     = 1 # excised by hole
794
795 class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
796     pass
797
798 class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
799     COMPRESSION = "#gz"
800     # the decompressor explodes in our face processing the first dummy, nothing
801     # we can do to recover
802     FAILURES    = 1
803
804 class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
805     COMPRESSION = "#gz"
806     PASSWORD    = TEST_PASSWORD
807     # again, ignoring the crypto errors yields a bad zlib stream causing the
808     # decompressor to abort where the hole begins; the file is extracted up
809     # to this point though
810     FAILURES    = 1
811
812
813 class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
814     COMPRESSION = "#gz"
815     PASSWORD    = TEST_PASSWORD
816     FAILURES    = 0
817     CORRUPT     = corrupt_ctsize
818     MISMATCHES  = 0
819
820
821 class RescueCorruptLeadingGarbageTestBase (RescueTest):
822     # plain Tar is indifferent against traling data and the results
823     # are consistent
824     COMPRESSION = None
825     PASSWORD    = None
826     FAILURES    = 0
827     CORRUPT     = corrupt_leading_garbage
828     MISMATCHES  = 0
829
830 class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
831     VOLUMES     = 1
832
833 class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase):
834     # the last object in first archive has extra bytes somewhere in the
835     # middle because tar itself performs no data checksumming.
836     MISMATCHES  = 2
837     VOLUMES     = 3
838
839
840 ###############################################################################
841 # index
842 ###############################################################################
843
844 class GenIndexIntactBaseTest (GenIndexTest):
845     """
846     """
847     COMPRESSION = None
848     PASSWORD    = None
849     FAILURES    = 0
850     CORRUPT     = immaculate
851     VOLUMES     = 1
852     MISMATCHES  = 1
853
854
855 class GenIndexIntactTest (GenIndexIntactBaseTest):
856     pass
857
858 class GenIndexIntactGZTest (GenIndexIntactBaseTest):
859     COMPRESSION = "#gz"
860     MISSING     = 2
861
862 class GenIndexIntactGZAESTest (GenIndexIntactBaseTest):
863     COMPRESSION = "#gz"
864     PASSWORD    = TEST_PASSWORD
865     MISSING     = 2
866