Merge branch 'crypto-review'
[python-delta-tar] / testing / test_recover.py
CommitLineData
dbd6ff68
PG
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
37ccf5bc
PG
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
dbd6ff68
PG
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
0b5c1c5e
PG
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
dbd6ff68
PG
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
fbdc9f4a
PG
69import logging
70import os
71import shutil
3692fd82 72import stat
b9cf4a0f
PG
73import sys
74import unittest
fbdc9f4a 75
2fe5f6e7
PG
76from functools import partial
77
fbdc9f4a 78import deltatar.deltatar as deltatar
3267933a 79import deltatar.crypto as crypto
203cb25e 80import deltatar.tarfile as tarfile
fbdc9f4a
PG
81
82from . import BaseTest
83
e25f31ac 84TEST_PASSWORD = "test1234"
85e7013f 85TEST_VOLSIZ = 2 # MB
e25f31ac 86TEST_FILESPERVOL = 3
85e7013f
PG
87VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into
88 # volumes; this is black magic
20e1d773 89TEST_BLOCKSIZE = 4096
96fe6399
PG
90
91###############################################################################
92## helpers ##
93###############################################################################
94
3267933a
PG
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)
203cb25e 101
3267933a
PG
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))
da8996f0
PG
107 pos = os.lseek (fd, off, os.SEEK_SET)
108 assert pos == off
3267933a
PG
109 os.write (fd, chunk)
110 finally:
111 os.close (fd)
112
203cb25e
PG
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
da8996f0 137
96fe6399
PG
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
3692fd82
PG
153###############################################################################
154## corruption simulators ##
155###############################################################################
156
0c8baf2b
PG
157class UndefinedTest (Exception):
158 """No test available for the asked combination of parameters."""
159
00b8c150
PG
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
37ccf5bc
PG
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
0c8baf2b
PG
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
da8996f0
PG
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)
dbd6ff68 212 elif compress is True:
da8996f0
PG
213 flip_bits (fname, 0, 0xff, gz_header_size (fname))
214 else:
215 flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE)
216
217
00b8c150
PG
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
afb2d647
PG
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)
a793ee30 240 junk = os.urandom (42)
afb2d647
PG
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
517d35b7
PG
263def corrupt_trailing_data (_, fname, compress, encrypt):
264 """
dbd6ff68 265 Append random data to file.
517d35b7
PG
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
00b8c150 272
20e1d773
PG
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
3692fd82
PG
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
3692fd82
PG
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
2fe5f6e7
PG
316def immaculate (_, _fname, _compress, _encrypt):
317 """
318 No-op dummy.
319 """
320 pass
3692fd82 321
96fe6399
PG
322###############################################################################
323## tests ##
324###############################################################################
203cb25e 325
0c6682ce 326class DefectiveTest (BaseTest):
fbdc9f4a
PG
327 """
328 Disaster recovery: restore corrupt backups.
329 """
330
96fe6399
PG
331 COMPRESSION = None
332 PASSWORD = None
9d89c237
PG
333 FAILURES = 0 # files that could not be restored
334 MISMATCHES = 0 # files that were restored but corrupted
00b8c150 335 CORRUPT = corrupt_payload_start
e25f31ac 336 VOLUMES = 1
4d4925de 337 MISSING = None # normally the number of failures
96fe6399 338
fbdc9f4a
PG
339
340 def setUp(self):
341 '''
342 Create base test data
343 '''
96fe6399
PG
344 self.pwd = os.getcwd()
345 self.dst_path = "source_dir"
346 self.src_path = "%s2" % self.dst_path
347 self.hash = dict()
348
fbdc9f4a 349 os.system('rm -rf target_dir source_dir* backup_dir* huge')
96fe6399 350 os.makedirs (self.src_path)
fbdc9f4a 351
96fe6399 352 for i in range (5):
85e7013f 353 f = "dummy_%d" % i
96fe6399
PG
354 self.hash [f] = self.create_file ("%s/%s"
355 % (self.src_path, f), 5 + i)
fbdc9f4a 356
96fe6399
PG
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*")
fbdc9f4a
PG
364
365
2fe5f6e7
PG
366 @staticmethod
367 def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
368 return backup_file % n
0c6682ce 369
2fe5f6e7 370 def gen_file_names (self, comp, pw):
203cb25e 371 bak_path = "backup_dir"
e25f31ac
PG
372 backup_file = "the_full_backup_%0.2d.tar"
373 backup_full = ("%s/%s" % (bak_path, backup_file)) % 0
96fe6399
PG
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:
e25f31ac
PG
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
2fe5f6e7
PG
386 return bak_path, backup_file, backup_full, index_file
387
388
047239f3
PG
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
2fe5f6e7
PG
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
e25f31ac 421 if self.VOLUMES > 1:
047239f3 422 self.gen_multivol (self.VOLUMES)
e25f31ac 423
2fe5f6e7 424 vname = partial (self.default_volume_name, backup_file)
96fe6399
PG
425 dtar = deltatar.DeltaTar (mode=mode,
426 logger=None,
427 password=self.PASSWORD,
203cb25e 428 index_name_func=lambda _: index_file,
3267933a 429 volume_name_func=vname)
fbdc9f4a
PG
430
431 dtar.create_full_backup \
e25f31ac
PG
432 (source_path=self.src_path, backup_path=bak_path,
433 max_volume_size=1)
96fe6399
PG
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))
203cb25e
PG
439
440 # first restore must succeed
96fe6399 441 dtar.restore_backup(target_path=self.dst_path,
f090d35a
PG
442 backup_indexes_paths=[
443 "%s/%s" % (bak_path, index_file)
b750b280
PG
444 ],
445 disaster=tarfile.TOLERANCE_RECOVER,
446 strict_validation=False)
203cb25e 447 for key, value in self.hash.items ():
96fe6399 448 f = "%s/%s" % (self.dst_path, key)
b15e549b
PG
449 assert os.path.exists (f)
450 assert value == self.md5sum (f)
96fe6399
PG
451 shutil.rmtree (self.dst_path)
452 shutil.rmtree (self.src_path)
203cb25e 453
00b8c150
PG
454 self.CORRUPT (backup_full,
455 self.COMPRESSION is not None,
456 self.PASSWORD is not None)
203cb25e
PG
457
458 # normal restore must fail
96fe6399
PG
459 try:
460 dtar.restore_backup(target_path=self.dst_path,
203cb25e 461 backup_tar_path=backup_full)
96fe6399
PG
462 except tarfile.CompressionError:
463 if self.PASSWORD is not None or self.COMPRESSION is not None:
464 pass
00b8c150
PG
465 else:
466 raise
96fe6399 467 except tarfile.ReadError:
00b8c150
PG
468 # can happen with all three modes
469 pass
470 except tarfile.DecryptionError:
471 if self.PASSWORD is not None:
96fe6399 472 pass
00b8c150
PG
473 else:
474 raise
96fe6399
PG
475
476 os.chdir (self.pwd) # not restored due to the error above
203cb25e 477 # but recover will succeed
96fe6399 478 failed = dtar.recover_backup(target_path=self.dst_path,
b15e549b
PG
479 backup_indexes_paths=[
480 "%s/%s" % (bak_path, index_file)
481 ])
96fe6399
PG
482
483 assert len (failed) == self.FAILURES
203cb25e
PG
484
485 # with one file missing
9d89c237
PG
486 missing = []
487 mismatch = []
203cb25e 488 for key, value in self.hash.items ():
96fe6399 489 kkey = "%s/%s" % (self.dst_path, key)
b15e549b 490 if os.path.exists (kkey):
9d89c237
PG
491 if value != self.md5sum (kkey):
492 mismatch.append (key)
203cb25e 493 else:
757319dd 494 missing.append (key)
4d4925de
PG
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)
9d89c237 503 assert len (mismatch) == self.MISMATCHES
96fe6399
PG
504
505 shutil.rmtree (self.dst_path)
506
507
0c6682ce
PG
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 """
2fe5f6e7
PG
519 mode = self.COMPRESSION or "#"
520 bak_path, backup_file, backup_full, index_file = \
521 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
0c6682ce
PG
522
523 if self.VOLUMES > 1:
047239f3 524 self.gen_multivol (self.VOLUMES)
0c6682ce 525
2fe5f6e7 526 vname = partial (self.default_volume_name, backup_file)
0c6682ce
PG
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)
b750b280
PG
546 ],
547 disaster=tarfile.TOLERANCE_RECOVER,
548 strict_validation=False)
0c6682ce
PG
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,
2fe5f6e7 581 backup_tar_path=backup_full)
0c6682ce
PG
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
79bc14cf 593 assert len (failed) == self.FAILURES
2fe5f6e7
PG
594 assert len (missing) == (self.MISSING if self.MISSING is not None
595 else self.FAILURES)
0c6682ce
PG
596 assert len (mismatch) == self.MISMATCHES
597
598 shutil.rmtree (self.dst_path)
599
600
2fe5f6e7
PG
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
047239f3
PG
614 if self.VOLUMES > 1:
615 self.gen_multivol (self.VOLUMES)
616
2fe5f6e7
PG
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
27ee4dd4
PG
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)
2fe5f6e7 634
047239f3
PG
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)
2fe5f6e7
PG
638
639
640###############################################################################
641# rescue
642###############################################################################
643
e25f31ac 644class RecoverCorruptPayloadTestBase (RecoverTest):
00b8c150
PG
645 COMPRESSION = None
646 PASSWORD = None
9d89c237
PG
647 FAILURES = 0 # tarfile will restore but corrupted, as
648 MISMATCHES = 1 # revealed by the hash
00b8c150 649
e25f31ac
PG
650class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
651 VOLUMES = 1
652
653class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
654 VOLUMES = 3
655
00b8c150 656
e25f31ac 657class RecoverCorruptPayloadGZTestBase (RecoverTest):
00b8c150
PG
658 COMPRESSION = "#gz"
659 PASSWORD = None
660 FAILURES = 1
9d89c237 661 MISMATCHES = 0
00b8c150 662
e25f31ac
PG
663class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
664 VOLUMES = 1
00b8c150 665
e25f31ac
PG
666class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
667 VOLUMES = 3
668
669
670class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
00b8c150
PG
671 COMPRESSION = "#gz"
672 PASSWORD = TEST_PASSWORD
673 FAILURES = 1
9d89c237 674 MISMATCHES = 0
00b8c150 675
e25f31ac
PG
676class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
677 VOLUMES = 1
678
679class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
680 VOLUMES = 3
00b8c150 681
e25f31ac
PG
682
683class RecoverCorruptHeaderTestBase (RecoverTest):
0349168a
PG
684 COMPRESSION = None
685 PASSWORD = None
686 FAILURES = 1
687 CORRUPT = corrupt_header
9d89c237 688 MISMATCHES = 0
0349168a 689
e25f31ac
PG
690class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
691 VOLUMES = 1
692
693class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
694 VOLUMES = 3
695
0349168a 696
e25f31ac 697class RecoverCorruptHeaderGZTestBase (RecoverTest):
96fe6399
PG
698 COMPRESSION = "#gz"
699 PASSWORD = None
700 FAILURES = 1
00b8c150 701 CORRUPT = corrupt_header
9d89c237 702 MISMATCHES = 0
96fe6399 703
e25f31ac
PG
704class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
705 VOLUMES = 1
3267933a 706
e25f31ac
PG
707class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
708 VOLUMES = 3
709
710
711class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
96fe6399
PG
712 COMPRESSION = "#gz"
713 PASSWORD = TEST_PASSWORD
714 FAILURES = 1
00b8c150 715 CORRUPT = corrupt_header
9d89c237 716 MISMATCHES = 0
fbdc9f4a 717
e25f31ac
PG
718class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
719 VOLUMES = 1
720
721class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
722 VOLUMES = 3
da8996f0 723
e25f31ac 724
37ccf5bc
PG
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
e25f31ac 747class RecoverCorruptEntireHeaderTestBase (RecoverTest):
da8996f0
PG
748 COMPRESSION = None
749 PASSWORD = None
750 FAILURES = 1
751 CORRUPT = corrupt_entire_header
9d89c237 752 MISMATCHES = 0
da8996f0 753
e25f31ac
PG
754class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
755 VOLUMES = 1
756
757class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
758 VOLUMES = 3
759
da8996f0 760
e25f31ac 761class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
da8996f0
PG
762 COMPRESSION = "#gz"
763 PASSWORD = None
764 FAILURES = 1
765 CORRUPT = corrupt_entire_header
9d89c237 766 MISMATCHES = 0
da8996f0 767
e25f31ac
PG
768class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
769 VOLUMES = 1
da8996f0 770
e25f31ac
PG
771class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
772 VOLUMES = 3
773
774
775class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
da8996f0
PG
776 COMPRESSION = "#gz"
777 PASSWORD = TEST_PASSWORD
778 FAILURES = 1
779 CORRUPT = corrupt_entire_header
9d89c237 780 MISMATCHES = 0
da8996f0 781
e25f31ac
PG
782class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
783 VOLUMES = 1
784
785class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
786 VOLUMES = 3
517d35b7 787
e25f31ac
PG
788
789class RecoverCorruptTrailingDataTestBase (RecoverTest):
517d35b7
PG
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
e25f31ac
PG
798class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
799 VOLUMES = 1
800
801class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
14895f4b
PG
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
e25f31ac
PG
805 VOLUMES = 3
806
517d35b7 807
e25f31ac 808class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
517d35b7
PG
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
e25f31ac
PG
817class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
818 VOLUMES = 1
517d35b7 819
e25f31ac
PG
820class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
821 VOLUMES = 3
14895f4b
PG
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
4d4925de 828 MISSING = 0
e25f31ac
PG
829
830
831class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
517d35b7
PG
832 COMPRESSION = "#gz"
833 PASSWORD = TEST_PASSWORD
834 FAILURES = 0
835 CORRUPT = corrupt_trailing_data
836 MISMATCHES = 0
837
e25f31ac
PG
838class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
839 VOLUMES = 1
840
841class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
842 VOLUMES = 3
517d35b7 843
20e1d773
PG
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
3692fd82
PG
855class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
856 COMPRESSION = "#gz"
857
858class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
20e1d773 859 COMPRESSION = "#gz"
3692fd82
PG
860 PASSWORD = TEST_PASSWORD
861
862
b9cf4a0f 863@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82
PG
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
20e1d773 871 PASSWORD = None
3692fd82
PG
872 FAILURES = 3
873 CORRUPT = corrupt_hole
874 VOLUMES = 2 # request two vols to swell up the first one
875 MISMATCHES = 1
876
b9cf4a0f 877@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82
PG
878class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
879 FAILURES = 2
880
b9cf4a0f 881@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82
PG
882class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
883 COMPRESSION = "#gz"
884 MISSING = 2
20e1d773 885
b9cf4a0f 886@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82 887class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
20e1d773
PG
888 COMPRESSION = "#gz"
889 PASSWORD = TEST_PASSWORD
3692fd82 890 MISSING = 2
20e1d773 891
2fe5f6e7
PG
892###############################################################################
893# rescue
894###############################################################################
895
37ccf5bc
PG
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
b9cf4a0f 918@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7
PG
919class RescueCorruptHoleBaseTest (RescueTest):
920 """
921 Cut bytes from the middle of a volume.
922 """
923 COMPRESSION = None
924 PASSWORD = None
79bc14cf 925 FAILURES = 0
2fe5f6e7
PG
926 CORRUPT = corrupt_hole
927 VOLUMES = 2 # request two vols to swell up the first one
79bc14cf
PG
928 MISMATCHES = 2 # intersected by hole
929 MISSING = 1 # excised by hole
2fe5f6e7 930
b9cf4a0f 931@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7 932class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
79bc14cf 933 pass
2fe5f6e7 934
b9cf4a0f 935@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7
PG
936class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
937 COMPRESSION = "#gz"
79bc14cf
PG
938 # the decompressor explodes in our face processing the first dummy, nothing
939 # we can do to recover
940 FAILURES = 1
2fe5f6e7 941
b9cf4a0f 942@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7
PG
943class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
944 COMPRESSION = "#gz"
945 PASSWORD = TEST_PASSWORD
79bc14cf
PG
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
2fe5f6e7 950
0c8baf2b 951
afb2d647 952class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
0c8baf2b
PG
953 COMPRESSION = "#gz"
954 PASSWORD = TEST_PASSWORD
955 FAILURES = 0
956 CORRUPT = corrupt_ctsize
957 MISMATCHES = 0
958
959
b9cf4a0f 960@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
afb2d647
PG
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
b9cf4a0f 970@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
afb2d647
PG
971class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
972 VOLUMES = 1
973
b9cf4a0f 974@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
afb2d647
PG
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
2fe5f6e7
PG
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
047239f3
PG
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
2fe5f6e7 1007
047239f3
PG
1008class GenIndexIntactMultiTest (GenIndexIntactBaseTest):
1009 VOLUMES = 3
2fe5f6e7
PG
1010 pass
1011
047239f3
PG
1012class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest):
1013 VOLUMES = 3
2fe5f6e7
PG
1014 COMPRESSION = "#gz"
1015 MISSING = 2
1016
047239f3
PG
1017class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest):
1018 VOLUMES = 3
2fe5f6e7
PG
1019 COMPRESSION = "#gz"
1020 PASSWORD = TEST_PASSWORD
1021 MISSING = 2
1022
6e1f5355 1023
37ccf5bc
PG
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
6e1f5355
PG
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
6e1f5355
PG
1069class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest):
1070 """
0b5c1c5e 1071 Recreate index from file with defective headers.
6e1f5355
PG
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