add tests for truncated files
[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
34 affecting the payload. This renders the object unreadable; the file will
35 be resemble one with arbitrary leading data but all the remaining object
36 offsets intact, so the contents can still be extracted with index based
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)
444 ])
203cb25e 445 for key, value in self.hash.items ():
96fe6399 446 f = "%s/%s" % (self.dst_path, key)
b15e549b
PG
447 assert os.path.exists (f)
448 assert value == self.md5sum (f)
96fe6399
PG
449 shutil.rmtree (self.dst_path)
450 shutil.rmtree (self.src_path)
203cb25e 451
00b8c150
PG
452 self.CORRUPT (backup_full,
453 self.COMPRESSION is not None,
454 self.PASSWORD is not None)
203cb25e
PG
455
456 # normal restore must fail
96fe6399
PG
457 try:
458 dtar.restore_backup(target_path=self.dst_path,
203cb25e 459 backup_tar_path=backup_full)
96fe6399
PG
460 except tarfile.CompressionError:
461 if self.PASSWORD is not None or self.COMPRESSION is not None:
462 pass
00b8c150
PG
463 else:
464 raise
96fe6399 465 except tarfile.ReadError:
00b8c150
PG
466 # can happen with all three modes
467 pass
468 except tarfile.DecryptionError:
469 if self.PASSWORD is not None:
96fe6399 470 pass
00b8c150
PG
471 else:
472 raise
96fe6399
PG
473
474 os.chdir (self.pwd) # not restored due to the error above
203cb25e 475 # but recover will succeed
96fe6399 476 failed = dtar.recover_backup(target_path=self.dst_path,
b15e549b
PG
477 backup_indexes_paths=[
478 "%s/%s" % (bak_path, index_file)
479 ])
96fe6399
PG
480
481 assert len (failed) == self.FAILURES
203cb25e
PG
482
483 # with one file missing
9d89c237
PG
484 missing = []
485 mismatch = []
203cb25e 486 for key, value in self.hash.items ():
96fe6399 487 kkey = "%s/%s" % (self.dst_path, key)
b15e549b 488 if os.path.exists (kkey):
9d89c237
PG
489 if value != self.md5sum (kkey):
490 mismatch.append (key)
203cb25e 491 else:
757319dd 492 missing.append (key)
4d4925de
PG
493
494 # usually, an object whose extraction fails will not be found on
495 # disk afterwards so the number of failures equals that of missing
496 # files. however, some modes will create partial files for objects
497 # spanning multiple volumes that contain the parts whose checksums
498 # were valid.
499 assert len (missing) == (self.MISSING if self.MISSING is not None
500 else self.FAILURES)
9d89c237 501 assert len (mismatch) == self.MISMATCHES
96fe6399
PG
502
503 shutil.rmtree (self.dst_path)
504
505
0c6682ce
PG
506class RescueTest (DefectiveTest):
507 """
508 Rescue: restore corrupt backups from backup set that is damaged to a degree
509 that the index file is worthless.
510 """
511
512 def test_rescue_corrupt (self):
513 """
514 Perform various damaging actions that cause unreadable objects, then
515 attempt to extract objects regardless.
516 """
2fe5f6e7
PG
517 mode = self.COMPRESSION or "#"
518 bak_path, backup_file, backup_full, index_file = \
519 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
0c6682ce
PG
520
521 if self.VOLUMES > 1:
047239f3 522 self.gen_multivol (self.VOLUMES)
0c6682ce 523
2fe5f6e7 524 vname = partial (self.default_volume_name, backup_file)
0c6682ce
PG
525 dtar = deltatar.DeltaTar (mode=mode,
526 logger=None,
527 password=self.PASSWORD,
528 index_name_func=lambda _: index_file,
529 volume_name_func=vname)
530
531 dtar.create_full_backup \
532 (source_path=self.src_path, backup_path=bak_path,
533 max_volume_size=1)
534
535 if self.PASSWORD is not None:
536 # ensure all files are at least superficially in PDT format
537 for f in os.listdir (bak_path):
538 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
539
540 # first restore must succeed
541 dtar.restore_backup(target_path=self.dst_path,
542 backup_indexes_paths=[
543 "%s/%s" % (bak_path, index_file)
544 ])
545 for key, value in self.hash.items ():
546 f = "%s/%s" % (self.dst_path, key)
547 assert os.path.exists (f)
548 assert value == self.md5sum (f)
549 shutil.rmtree (self.dst_path)
550 shutil.rmtree (self.src_path)
551
552 self.CORRUPT (backup_full,
553 self.COMPRESSION is not None,
554 self.PASSWORD is not None)
555
556 # normal restore must fail
557 try:
558 dtar.restore_backup(target_path=self.dst_path,
559 backup_tar_path=backup_full)
560 except tarfile.CompressionError:
561 if self.PASSWORD is not None or self.COMPRESSION is not None:
562 pass
563 else:
564 raise
565 except tarfile.ReadError:
566 # can happen with all three modes
567 pass
568 except tarfile.DecryptionError:
569 if self.PASSWORD is not None:
570 pass
571 else:
572 raise
573
574 os.chdir (self.pwd) # not restored due to the error above
575 # but recover will succeed
576 failed = dtar.rescue_backup(target_path=self.dst_path,
2fe5f6e7 577 backup_tar_path=backup_full)
0c6682ce
PG
578 # with one file missing
579 missing = []
580 mismatch = []
581 for key, value in self.hash.items ():
582 kkey = "%s/%s" % (self.dst_path, key)
583 if os.path.exists (kkey):
584 if value != self.md5sum (kkey):
585 mismatch.append (key)
586 else:
587 missing.append (key)
588
79bc14cf 589 assert len (failed) == self.FAILURES
2fe5f6e7
PG
590 assert len (missing) == (self.MISSING if self.MISSING is not None
591 else self.FAILURES)
0c6682ce
PG
592 assert len (mismatch) == self.MISMATCHES
593
594 shutil.rmtree (self.dst_path)
595
596
2fe5f6e7
PG
597class GenIndexTest (DefectiveTest):
598 """
599 Deducing an index for a backup with tarfile.
600 """
601
602 def test_gen_index (self):
603 """
604 Create backup, leave it unharmed, then generate an index.
605 """
606 mode = self.COMPRESSION or "#"
607 bak_path, backup_file, backup_full, index_file = \
608 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
609
047239f3
PG
610 if self.VOLUMES > 1:
611 self.gen_multivol (self.VOLUMES)
612
2fe5f6e7
PG
613 vname = partial (self.default_volume_name, backup_file)
614 dtar = deltatar.DeltaTar (mode=mode,
615 logger=None,
616 password=self.PASSWORD,
617 index_name_func=lambda _: index_file,
618 volume_name_func=vname)
619
620 dtar.create_full_backup \
621 (source_path=self.src_path, backup_path=bak_path,
622 max_volume_size=1)
623
27ee4dd4
PG
624 def gen_volume_name (nvol):
625 return os.path.join (bak_path, vname (backup_full, True, nvol))
626
627 psidx = tarfile.gen_rescue_index (gen_volume_name,
628 mode,
629 password=self.PASSWORD)
2fe5f6e7 630
047239f3
PG
631 # correct for objects spanning volumes: these are treated as separate
632 # in the index!
633 assert len (psidx) - self.VOLUMES + 1 == len (self.hash)
2fe5f6e7
PG
634
635
636###############################################################################
637# rescue
638###############################################################################
639
e25f31ac 640class RecoverCorruptPayloadTestBase (RecoverTest):
00b8c150
PG
641 COMPRESSION = None
642 PASSWORD = None
9d89c237
PG
643 FAILURES = 0 # tarfile will restore but corrupted, as
644 MISMATCHES = 1 # revealed by the hash
00b8c150 645
e25f31ac
PG
646class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
647 VOLUMES = 1
648
649class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
650 VOLUMES = 3
651
00b8c150 652
e25f31ac 653class RecoverCorruptPayloadGZTestBase (RecoverTest):
00b8c150
PG
654 COMPRESSION = "#gz"
655 PASSWORD = None
656 FAILURES = 1
9d89c237 657 MISMATCHES = 0
00b8c150 658
e25f31ac
PG
659class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
660 VOLUMES = 1
00b8c150 661
e25f31ac
PG
662class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
663 VOLUMES = 3
664
665
666class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
00b8c150
PG
667 COMPRESSION = "#gz"
668 PASSWORD = TEST_PASSWORD
669 FAILURES = 1
9d89c237 670 MISMATCHES = 0
00b8c150 671
e25f31ac
PG
672class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
673 VOLUMES = 1
674
675class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
676 VOLUMES = 3
00b8c150 677
e25f31ac
PG
678
679class RecoverCorruptHeaderTestBase (RecoverTest):
0349168a
PG
680 COMPRESSION = None
681 PASSWORD = None
682 FAILURES = 1
683 CORRUPT = corrupt_header
9d89c237 684 MISMATCHES = 0
0349168a 685
e25f31ac
PG
686class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
687 VOLUMES = 1
688
689class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
690 VOLUMES = 3
691
0349168a 692
e25f31ac 693class RecoverCorruptHeaderGZTestBase (RecoverTest):
96fe6399
PG
694 COMPRESSION = "#gz"
695 PASSWORD = None
696 FAILURES = 1
00b8c150 697 CORRUPT = corrupt_header
9d89c237 698 MISMATCHES = 0
96fe6399 699
e25f31ac
PG
700class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
701 VOLUMES = 1
3267933a 702
e25f31ac
PG
703class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
704 VOLUMES = 3
705
706
707class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
96fe6399
PG
708 COMPRESSION = "#gz"
709 PASSWORD = TEST_PASSWORD
710 FAILURES = 1
00b8c150 711 CORRUPT = corrupt_header
9d89c237 712 MISMATCHES = 0
fbdc9f4a 713
e25f31ac
PG
714class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
715 VOLUMES = 1
716
717class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
718 VOLUMES = 3
da8996f0 719
e25f31ac 720
37ccf5bc
PG
721class RecoverCorruptTruncateTestBase (RecoverTest):
722 COMPRESSION = None
723 PASSWORD = None
724 FAILURES = 0
725 CORRUPT = corrupt_truncate
726 MISMATCHES = 0
727
728class RecoverCorruptTruncateTest (RecoverCorruptTruncateTestBase):
729 pass
730
731class RecoverCorruptTruncateGZTest (RecoverCorruptTruncateTestBase):
732 """Two files that failed missing."""
733 COMPRESSION = "#gz"
734 FAILURES = 2
735
736class RecoverCorruptTruncateGZAESTest (RecoverCorruptTruncateTestBase):
737 """Two files that failed missing."""
738 COMPRESSION = "#gz"
739 PASSWORD = TEST_PASSWORD
740 FAILURES = 2
741
742
e25f31ac 743class RecoverCorruptEntireHeaderTestBase (RecoverTest):
da8996f0
PG
744 COMPRESSION = None
745 PASSWORD = None
746 FAILURES = 1
747 CORRUPT = corrupt_entire_header
9d89c237 748 MISMATCHES = 0
da8996f0 749
e25f31ac
PG
750class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
751 VOLUMES = 1
752
753class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
754 VOLUMES = 3
755
da8996f0 756
e25f31ac 757class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
da8996f0
PG
758 COMPRESSION = "#gz"
759 PASSWORD = None
760 FAILURES = 1
761 CORRUPT = corrupt_entire_header
9d89c237 762 MISMATCHES = 0
da8996f0 763
e25f31ac
PG
764class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
765 VOLUMES = 1
da8996f0 766
e25f31ac
PG
767class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
768 VOLUMES = 3
769
770
771class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
da8996f0
PG
772 COMPRESSION = "#gz"
773 PASSWORD = TEST_PASSWORD
774 FAILURES = 1
775 CORRUPT = corrupt_entire_header
9d89c237 776 MISMATCHES = 0
da8996f0 777
e25f31ac
PG
778class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
779 VOLUMES = 1
780
781class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
782 VOLUMES = 3
517d35b7 783
e25f31ac
PG
784
785class RecoverCorruptTrailingDataTestBase (RecoverTest):
517d35b7
PG
786 # plain Tar is indifferent against traling data and the results
787 # are consistent
788 COMPRESSION = None
789 PASSWORD = None
790 FAILURES = 0
791 CORRUPT = corrupt_trailing_data
792 MISMATCHES = 0
793
e25f31ac
PG
794class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
795 VOLUMES = 1
796
797class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
14895f4b
PG
798 # the last object in first archive has extra bytes somewhere in the
799 # middle because tar itself performs no data checksumming.
800 MISMATCHES = 1
e25f31ac
PG
801 VOLUMES = 3
802
517d35b7 803
e25f31ac 804class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
517d35b7
PG
805 # reading past the final object will cause decompression failure;
806 # all objects except for the last survive unharmed though
807 COMPRESSION = "#gz"
808 PASSWORD = None
809 FAILURES = 1
810 CORRUPT = corrupt_trailing_data
811 MISMATCHES = 0
812
e25f31ac
PG
813class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
814 VOLUMES = 1
517d35b7 815
e25f31ac
PG
816class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
817 VOLUMES = 3
14895f4b
PG
818 # the last file of the first volume will only contain the data of the
819 # second part which is contained in the second volume. this happens
820 # because the CRC32 is wrong for the first part so it gets discarded, then
821 # the object is recreated from the first header of the second volume,
822 # containing only the remainder of the data.
823 MISMATCHES = 1
4d4925de 824 MISSING = 0
e25f31ac
PG
825
826
827class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
517d35b7
PG
828 COMPRESSION = "#gz"
829 PASSWORD = TEST_PASSWORD
830 FAILURES = 0
831 CORRUPT = corrupt_trailing_data
832 MISMATCHES = 0
833
e25f31ac
PG
834class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
835 VOLUMES = 1
836
837class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
838 VOLUMES = 3
517d35b7 839
20e1d773
PG
840
841class RecoverCorruptVolumeBaseTest (RecoverTest):
842 COMPRESSION = None
843 PASSWORD = None
844 FAILURES = 8
845 CORRUPT = corrupt_volume
846 VOLUMES = 3
847
848class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
849 pass
850
3692fd82
PG
851class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
852 COMPRESSION = "#gz"
853
854class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
20e1d773 855 COMPRESSION = "#gz"
3692fd82
PG
856 PASSWORD = TEST_PASSWORD
857
858
b9cf4a0f 859@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82
PG
860class RecoverCorruptHoleBaseTest (RecoverTest):
861 """
862 Cut bytes from the middle of a volume.
863
864 Index-based recovery works only up to the hole.
865 """
866 COMPRESSION = None
20e1d773 867 PASSWORD = None
3692fd82
PG
868 FAILURES = 3
869 CORRUPT = corrupt_hole
870 VOLUMES = 2 # request two vols to swell up the first one
871 MISMATCHES = 1
872
b9cf4a0f 873@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82
PG
874class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
875 FAILURES = 2
876
b9cf4a0f 877@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82
PG
878class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
879 COMPRESSION = "#gz"
880 MISSING = 2
20e1d773 881
b9cf4a0f 882@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
3692fd82 883class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
20e1d773
PG
884 COMPRESSION = "#gz"
885 PASSWORD = TEST_PASSWORD
3692fd82 886 MISSING = 2
20e1d773 887
2fe5f6e7
PG
888###############################################################################
889# rescue
890###############################################################################
891
37ccf5bc
PG
892class RescueCorruptTruncateTestBase (RescueTest):
893 COMPRESSION = None
894 PASSWORD = None
895 FAILURES = 0
896 CORRUPT = corrupt_truncate
897 MISMATCHES = 0
898
899class RescueCorruptTruncateTest (RescueCorruptTruncateTestBase):
900 pass
901
902class RescueCorruptTruncateGZTest (RescueCorruptTruncateTestBase):
903 """Two files that failed missing."""
904 COMPRESSION = "#gz"
905 MISSING = 2
906
907class RescueCorruptTruncateGZAESTest (RescueCorruptTruncateTestBase):
908 """Two files missing but didn’t fail on account of their absence."""
909 COMPRESSION = "#gz"
910 PASSWORD = TEST_PASSWORD
911 MISSING = 2
912
913
b9cf4a0f 914@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7
PG
915class RescueCorruptHoleBaseTest (RescueTest):
916 """
917 Cut bytes from the middle of a volume.
918 """
919 COMPRESSION = None
920 PASSWORD = None
79bc14cf 921 FAILURES = 0
2fe5f6e7
PG
922 CORRUPT = corrupt_hole
923 VOLUMES = 2 # request two vols to swell up the first one
79bc14cf
PG
924 MISMATCHES = 2 # intersected by hole
925 MISSING = 1 # excised by hole
2fe5f6e7 926
b9cf4a0f 927@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7 928class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
79bc14cf 929 pass
2fe5f6e7 930
b9cf4a0f 931@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7
PG
932class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
933 COMPRESSION = "#gz"
79bc14cf
PG
934 # the decompressor explodes in our face processing the first dummy, nothing
935 # we can do to recover
936 FAILURES = 1
2fe5f6e7 937
b9cf4a0f 938@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
2fe5f6e7
PG
939class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
940 COMPRESSION = "#gz"
941 PASSWORD = TEST_PASSWORD
79bc14cf
PG
942 # again, ignoring the crypto errors yields a bad zlib stream causing the
943 # decompressor to abort where the hole begins; the file is extracted up
944 # to this point though
945 FAILURES = 1
2fe5f6e7 946
0c8baf2b 947
afb2d647 948class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
0c8baf2b
PG
949 COMPRESSION = "#gz"
950 PASSWORD = TEST_PASSWORD
951 FAILURES = 0
952 CORRUPT = corrupt_ctsize
953 MISMATCHES = 0
954
955
b9cf4a0f 956@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
afb2d647
PG
957class RescueCorruptLeadingGarbageTestBase (RescueTest):
958 # plain Tar is indifferent against traling data and the results
959 # are consistent
960 COMPRESSION = None
961 PASSWORD = None
962 FAILURES = 0
963 CORRUPT = corrupt_leading_garbage
964 MISMATCHES = 0
965
b9cf4a0f 966@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
afb2d647
PG
967class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
968 VOLUMES = 1
969
b9cf4a0f 970@unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
afb2d647
PG
971class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase):
972 # the last object in first archive has extra bytes somewhere in the
973 # middle because tar itself performs no data checksumming.
974 MISMATCHES = 2
975 VOLUMES = 3
976
977
2fe5f6e7
PG
978###############################################################################
979# index
980###############################################################################
981
982class GenIndexIntactBaseTest (GenIndexTest):
983 """
984 """
985 COMPRESSION = None
986 PASSWORD = None
987 FAILURES = 0
988 CORRUPT = immaculate
989 VOLUMES = 1
990 MISMATCHES = 1
991
047239f3
PG
992class GenIndexIntactSingleTest (GenIndexIntactBaseTest):
993 pass
994
995class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest):
996 COMPRESSION = "#gz"
997 MISSING = 2
998
999class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest):
1000 COMPRESSION = "#gz"
1001 PASSWORD = TEST_PASSWORD
1002 MISSING = 2
2fe5f6e7 1003
047239f3
PG
1004class GenIndexIntactMultiTest (GenIndexIntactBaseTest):
1005 VOLUMES = 3
2fe5f6e7
PG
1006 pass
1007
047239f3
PG
1008class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest):
1009 VOLUMES = 3
2fe5f6e7
PG
1010 COMPRESSION = "#gz"
1011 MISSING = 2
1012
047239f3
PG
1013class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest):
1014 VOLUMES = 3
2fe5f6e7
PG
1015 COMPRESSION = "#gz"
1016 PASSWORD = TEST_PASSWORD
1017 MISSING = 2
1018
6e1f5355 1019
37ccf5bc
PG
1020class GenIndexCorruptTruncateBaseTest (GenIndexTest):
1021 """
1022 Recreate index from file that lacks the latter portion.
1023 """
1024 COMPRESSION = None
1025 PASSWORD = None
1026 FAILURES = 0
1027 CORRUPT = corrupt_truncate
1028 MISSING = 2
1029
1030class GenIndexCorruptTruncateTest (GenIndexCorruptTruncateBaseTest):
1031 pass
1032
1033class GenIndexCorruptTruncateGZTest (GenIndexCorruptTruncateBaseTest):
1034 COMPRESSION = "#gz"
1035
1036class GenIndexCorruptTruncateGZAESTest (GenIndexCorruptTruncateBaseTest):
1037 COMPRESSION = "#gz"
1038 PASSWORD = TEST_PASSWORD
1039
1040
6e1f5355
PG
1041class GenIndexCorruptHoleBaseTest (GenIndexTest):
1042 """
1043 Recreate index from file with hole.
1044 """
1045 COMPRESSION = None
1046 PASSWORD = None
1047 FAILURES = 0
1048 CORRUPT = corrupt_hole
1049 VOLUMES = 1
1050 MISMATCHES = 1
1051
1052class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest):
1053 pass
1054
1055class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest):
1056 COMPRESSION = "#gz"
1057 MISSING = 2
1058
1059class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest):
1060 COMPRESSION = "#gz"
1061 PASSWORD = TEST_PASSWORD
1062 MISSING = 2
1063
1064
6e1f5355
PG
1065class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest):
1066 """
1067 Recreate index from file with hole.
1068 """
1069 COMPRESSION = None
1070 PASSWORD = None
1071 FAILURES = 0
1072 CORRUPT = corrupt_entire_header
1073 VOLUMES = 1
1074 MISMATCHES = 1
1075
1076class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest):
1077 pass
1078
1079class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest):
1080 COMPRESSION = "#gz"
1081 MISSING = 2
1082
1083class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest):
1084 COMPRESSION = "#gz"
1085 PASSWORD = TEST_PASSWORD
1086 MISSING = 2
1087