add unit test for borked ciphertext size
[python-delta-tar] / testing / test_recover.py
CommitLineData
fbdc9f4a
PG
1import logging
2import os
3import shutil
3692fd82 4import stat
fbdc9f4a 5
2fe5f6e7
PG
6from functools import partial
7
fbdc9f4a 8import deltatar.deltatar as deltatar
3267933a 9import deltatar.crypto as crypto
203cb25e 10import deltatar.tarfile as tarfile
fbdc9f4a
PG
11
12from . import BaseTest
13
e25f31ac 14TEST_PASSWORD = "test1234"
85e7013f 15TEST_VOLSIZ = 2 # MB
e25f31ac 16TEST_FILESPERVOL = 3
85e7013f
PG
17VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into
18 # volumes; this is black magic
20e1d773 19TEST_BLOCKSIZE = 4096
96fe6399
PG
20
21###############################################################################
22## helpers ##
23###############################################################################
24
3267933a
PG
25def 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)
203cb25e 31
3267933a
PG
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))
da8996f0
PG
37 pos = os.lseek (fd, off, os.SEEK_SET)
38 assert pos == off
3267933a
PG
39 os.write (fd, chunk)
40 finally:
41 os.close (fd)
42
203cb25e
PG
43
44def 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
da8996f0 67
96fe6399
PG
68def 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
3692fd82
PG
83###############################################################################
84## corruption simulators ##
85###############################################################################
86
0c8baf2b
PG
87class UndefinedTest (Exception):
88 """No test available for the asked combination of parameters."""
89
00b8c150
PG
90def 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
0c8baf2b
PG
112def 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
da8996f0
PG
125def 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
00b8c150
PG
137def 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
517d35b7
PG
149def corrupt_trailing_data (_, fname, compress, encrypt):
150 """
151 Modify the byte following the object header structure of the format.
152 """
153 junk = os.urandom (42)
154 fd = os.open (fname, os.O_WRONLY | os.O_APPEND)
155 os.write (fd, junk)
156 os.close (fd)
157
00b8c150 158
20e1d773
PG
159def corrupt_volume (_, fname, compress, encrypt):
160 """
161 Zero out an entire volume.
162 """
163 fd = os.open (fname, os.O_WRONLY)
164 size = os.lseek (fd, 0, os.SEEK_END)
165 assert os.lseek (fd, 0, os.SEEK_SET) == 0
166 zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
167 while size > 0:
168 todo = min (size, TEST_BLOCKSIZE)
169 os.write (fd, zeros [:todo])
170 size -= todo
171 os.close (fd)
172
173
3692fd82
PG
174def corrupt_hole (_, fname, compress, encrypt):
175 """
176 Cut file in three pieces, reassemble without the middle one.
177 """
178 aname = os.path.abspath (fname)
179 infd = os.open (fname, os.O_RDONLY)
180 size = os.lseek (infd, 0, os.SEEK_END)
181 assert os.lseek (infd, 0, os.SEEK_SET) == 0
182 assert size > 3 * TEST_BLOCKSIZE
183 hole = (size / 3, size * 2 / 3)
184 outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
185 stat.S_IRUSR | stat.S_IWUSR)
186
187 zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
188 done = 0
189 while done < size:
190 data = os.read (infd, TEST_BLOCKSIZE)
191 if done < hole [0] or hole [1] < done:
192 # only copy from outside hole
193 os.write (outfd, data)
194 done += len (data)
195
196 os.close (infd)
197 os.unlink (fname)
198
199 path = "/proc/self/fd/%d" % outfd
200 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
201 os.close (outfd)
202
2fe5f6e7
PG
203def immaculate (_, _fname, _compress, _encrypt):
204 """
205 No-op dummy.
206 """
207 pass
3692fd82 208
96fe6399
PG
209###############################################################################
210## tests ##
211###############################################################################
203cb25e 212
0c6682ce 213class DefectiveTest (BaseTest):
fbdc9f4a
PG
214 """
215 Disaster recovery: restore corrupt backups.
216 """
217
96fe6399
PG
218 COMPRESSION = None
219 PASSWORD = None
9d89c237
PG
220 FAILURES = 0 # files that could not be restored
221 MISMATCHES = 0 # files that were restored but corrupted
00b8c150 222 CORRUPT = corrupt_payload_start
e25f31ac 223 VOLUMES = 1
4d4925de 224 MISSING = None # normally the number of failures
96fe6399 225
fbdc9f4a
PG
226
227 def setUp(self):
228 '''
229 Create base test data
230 '''
96fe6399
PG
231 self.pwd = os.getcwd()
232 self.dst_path = "source_dir"
233 self.src_path = "%s2" % self.dst_path
234 self.hash = dict()
235
fbdc9f4a 236 os.system('rm -rf target_dir source_dir* backup_dir* huge')
96fe6399 237 os.makedirs (self.src_path)
fbdc9f4a 238
96fe6399 239 for i in range (5):
85e7013f 240 f = "dummy_%d" % i
96fe6399
PG
241 self.hash [f] = self.create_file ("%s/%s"
242 % (self.src_path, f), 5 + i)
fbdc9f4a 243
96fe6399
PG
244
245 def tearDown(self):
246 '''
247 Remove temporal files created by unit tests and reset globals.
248 '''
249 os.chdir(self.pwd)
250 os.system("rm -rf source_dir source_dir2 backup_dir*")
fbdc9f4a
PG
251
252
2fe5f6e7
PG
253 @staticmethod
254 def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
255 return backup_file % n
0c6682ce 256
2fe5f6e7 257 def gen_file_names (self, comp, pw):
203cb25e 258 bak_path = "backup_dir"
e25f31ac
PG
259 backup_file = "the_full_backup_%0.2d.tar"
260 backup_full = ("%s/%s" % (bak_path, backup_file)) % 0
96fe6399
PG
261 index_file = "the_full_index"
262
263 if self.COMPRESSION is not None:
264 backup_file += ".gz"
265 backup_full += ".gz"
266 index_file += ".gz"
267
268 if self.PASSWORD is not None:
e25f31ac
PG
269 backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION)
270 backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION)
271 index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION)
272
2fe5f6e7
PG
273 return bak_path, backup_file, backup_full, index_file
274
275
276class RecoverTest (DefectiveTest):
277 """
278 Recover: restore corrupt backups from index file information.
279 """
280
281 def test_recover_corrupt (self):
282 """
283 Perform various damaging actions that cause unreadable objects.
284
285 Expects the extraction to fail in normal mode. With disaster recovery,
286 extraction must succeed, and exactly one file must be missing.
287 """
288 mode = self.COMPRESSION or "#"
289 bak_path, backup_file, backup_full, index_file = \
290 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
291
e25f31ac 292 if self.VOLUMES > 1:
85e7013f
PG
293 # add n files for one nth the volume size each, corrected
294 # for metadata and tar block overhead
295 fsiz = int ( ( TEST_VOLSIZ
296 / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
297 * 1024 * 1024)
298 fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
e25f31ac
PG
299 for i in range (fcnt):
300 nvol, invol = divmod(i, TEST_FILESPERVOL)
301 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
302 self.hash [f] = self.create_file ("%s/%s"
303 % (self.src_path, f),
85e7013f
PG
304 fsiz,
305 random=True)
e25f31ac 306
2fe5f6e7 307 vname = partial (self.default_volume_name, backup_file)
96fe6399
PG
308 dtar = deltatar.DeltaTar (mode=mode,
309 logger=None,
310 password=self.PASSWORD,
203cb25e 311 index_name_func=lambda _: index_file,
3267933a 312 volume_name_func=vname)
fbdc9f4a
PG
313
314 dtar.create_full_backup \
e25f31ac
PG
315 (source_path=self.src_path, backup_path=bak_path,
316 max_volume_size=1)
96fe6399
PG
317
318 if self.PASSWORD is not None:
319 # ensure all files are at least superficially in PDT format
320 for f in os.listdir (bak_path):
321 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
203cb25e
PG
322
323 # first restore must succeed
96fe6399 324 dtar.restore_backup(target_path=self.dst_path,
f090d35a
PG
325 backup_indexes_paths=[
326 "%s/%s" % (bak_path, index_file)
327 ])
203cb25e 328 for key, value in self.hash.items ():
96fe6399 329 f = "%s/%s" % (self.dst_path, key)
b15e549b
PG
330 assert os.path.exists (f)
331 assert value == self.md5sum (f)
96fe6399
PG
332 shutil.rmtree (self.dst_path)
333 shutil.rmtree (self.src_path)
203cb25e 334
00b8c150
PG
335 self.CORRUPT (backup_full,
336 self.COMPRESSION is not None,
337 self.PASSWORD is not None)
203cb25e
PG
338
339 # normal restore must fail
96fe6399
PG
340 try:
341 dtar.restore_backup(target_path=self.dst_path,
203cb25e 342 backup_tar_path=backup_full)
96fe6399
PG
343 except tarfile.CompressionError:
344 if self.PASSWORD is not None or self.COMPRESSION is not None:
345 pass
00b8c150
PG
346 else:
347 raise
96fe6399 348 except tarfile.ReadError:
00b8c150
PG
349 # can happen with all three modes
350 pass
351 except tarfile.DecryptionError:
352 if self.PASSWORD is not None:
96fe6399 353 pass
00b8c150
PG
354 else:
355 raise
96fe6399
PG
356
357 os.chdir (self.pwd) # not restored due to the error above
203cb25e 358 # but recover will succeed
96fe6399 359 failed = dtar.recover_backup(target_path=self.dst_path,
b15e549b
PG
360 backup_indexes_paths=[
361 "%s/%s" % (bak_path, index_file)
362 ])
96fe6399
PG
363
364 assert len (failed) == self.FAILURES
203cb25e
PG
365
366 # with one file missing
9d89c237
PG
367 missing = []
368 mismatch = []
203cb25e 369 for key, value in self.hash.items ():
96fe6399 370 kkey = "%s/%s" % (self.dst_path, key)
b15e549b 371 if os.path.exists (kkey):
9d89c237
PG
372 if value != self.md5sum (kkey):
373 mismatch.append (key)
203cb25e 374 else:
757319dd 375 missing.append (key)
4d4925de
PG
376
377 # usually, an object whose extraction fails will not be found on
378 # disk afterwards so the number of failures equals that of missing
379 # files. however, some modes will create partial files for objects
380 # spanning multiple volumes that contain the parts whose checksums
381 # were valid.
382 assert len (missing) == (self.MISSING if self.MISSING is not None
383 else self.FAILURES)
9d89c237 384 assert len (mismatch) == self.MISMATCHES
96fe6399
PG
385
386 shutil.rmtree (self.dst_path)
387
388
0c6682ce
PG
389class RescueTest (DefectiveTest):
390 """
391 Rescue: restore corrupt backups from backup set that is damaged to a degree
392 that the index file is worthless.
393 """
394
395 def test_rescue_corrupt (self):
396 """
397 Perform various damaging actions that cause unreadable objects, then
398 attempt to extract objects regardless.
399 """
2fe5f6e7
PG
400 mode = self.COMPRESSION or "#"
401 bak_path, backup_file, backup_full, index_file = \
402 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
0c6682ce
PG
403
404 if self.VOLUMES > 1:
405 # add n files for one nth the volume size each, corrected
406 # for metadata and tar block overhead
407 fsiz = int ( ( TEST_VOLSIZ
408 / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
409 * 1024 * 1024)
410 fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
411 for i in range (fcnt):
412 nvol, invol = divmod(i, TEST_FILESPERVOL)
413 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
414 self.hash [f] = self.create_file ("%s/%s"
415 % (self.src_path, f),
416 fsiz,
417 random=True)
418
2fe5f6e7 419 vname = partial (self.default_volume_name, backup_file)
0c6682ce
PG
420 dtar = deltatar.DeltaTar (mode=mode,
421 logger=None,
422 password=self.PASSWORD,
423 index_name_func=lambda _: index_file,
424 volume_name_func=vname)
425
426 dtar.create_full_backup \
427 (source_path=self.src_path, backup_path=bak_path,
428 max_volume_size=1)
429
430 if self.PASSWORD is not None:
431 # ensure all files are at least superficially in PDT format
432 for f in os.listdir (bak_path):
433 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
434
435 # first restore must succeed
436 dtar.restore_backup(target_path=self.dst_path,
437 backup_indexes_paths=[
438 "%s/%s" % (bak_path, index_file)
439 ])
440 for key, value in self.hash.items ():
441 f = "%s/%s" % (self.dst_path, key)
442 assert os.path.exists (f)
443 assert value == self.md5sum (f)
444 shutil.rmtree (self.dst_path)
445 shutil.rmtree (self.src_path)
446
447 self.CORRUPT (backup_full,
448 self.COMPRESSION is not None,
449 self.PASSWORD is not None)
450
451 # normal restore must fail
452 try:
453 dtar.restore_backup(target_path=self.dst_path,
454 backup_tar_path=backup_full)
455 except tarfile.CompressionError:
456 if self.PASSWORD is not None or self.COMPRESSION is not None:
457 pass
458 else:
459 raise
460 except tarfile.ReadError:
461 # can happen with all three modes
462 pass
463 except tarfile.DecryptionError:
464 if self.PASSWORD is not None:
465 pass
466 else:
467 raise
468
469 os.chdir (self.pwd) # not restored due to the error above
470 # but recover will succeed
471 failed = dtar.rescue_backup(target_path=self.dst_path,
2fe5f6e7 472 backup_tar_path=backup_full)
0c6682ce
PG
473 # with one file missing
474 missing = []
475 mismatch = []
476 for key, value in self.hash.items ():
477 kkey = "%s/%s" % (self.dst_path, key)
478 if os.path.exists (kkey):
479 if value != self.md5sum (kkey):
480 mismatch.append (key)
481 else:
482 missing.append (key)
483
79bc14cf 484 assert len (failed) == self.FAILURES
2fe5f6e7
PG
485 assert len (missing) == (self.MISSING if self.MISSING is not None
486 else self.FAILURES)
0c6682ce
PG
487 assert len (mismatch) == self.MISMATCHES
488
489 shutil.rmtree (self.dst_path)
490
491
2fe5f6e7
PG
492class GenIndexTest (DefectiveTest):
493 """
494 Deducing an index for a backup with tarfile.
495 """
496
497 def test_gen_index (self):
498 """
499 Create backup, leave it unharmed, then generate an index.
500 """
501 mode = self.COMPRESSION or "#"
502 bak_path, backup_file, backup_full, index_file = \
503 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
504
505 vname = partial (self.default_volume_name, backup_file)
506 dtar = deltatar.DeltaTar (mode=mode,
507 logger=None,
508 password=self.PASSWORD,
509 index_name_func=lambda _: index_file,
510 volume_name_func=vname)
511
512 dtar.create_full_backup \
513 (source_path=self.src_path, backup_path=bak_path,
514 max_volume_size=1)
515
516 psidx = tarfile.gen_rescue_index (backup_full, mode, password=self.PASSWORD)
517
518 assert len (psidx) == len (self.hash)
519
520
521###############################################################################
522# rescue
523###############################################################################
524
e25f31ac 525class RecoverCorruptPayloadTestBase (RecoverTest):
00b8c150
PG
526 COMPRESSION = None
527 PASSWORD = None
9d89c237
PG
528 FAILURES = 0 # tarfile will restore but corrupted, as
529 MISMATCHES = 1 # revealed by the hash
00b8c150 530
e25f31ac
PG
531class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
532 VOLUMES = 1
533
534class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
535 VOLUMES = 3
536
00b8c150 537
e25f31ac 538class RecoverCorruptPayloadGZTestBase (RecoverTest):
00b8c150
PG
539 COMPRESSION = "#gz"
540 PASSWORD = None
541 FAILURES = 1
9d89c237 542 MISMATCHES = 0
00b8c150 543
e25f31ac
PG
544class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
545 VOLUMES = 1
00b8c150 546
e25f31ac
PG
547class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
548 VOLUMES = 3
549
550
551class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
00b8c150
PG
552 COMPRESSION = "#gz"
553 PASSWORD = TEST_PASSWORD
554 FAILURES = 1
9d89c237 555 MISMATCHES = 0
00b8c150 556
e25f31ac
PG
557class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
558 VOLUMES = 1
559
560class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
561 VOLUMES = 3
00b8c150 562
e25f31ac
PG
563
564class RecoverCorruptHeaderTestBase (RecoverTest):
0349168a
PG
565 COMPRESSION = None
566 PASSWORD = None
567 FAILURES = 1
568 CORRUPT = corrupt_header
9d89c237 569 MISMATCHES = 0
0349168a 570
e25f31ac
PG
571class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
572 VOLUMES = 1
573
574class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
575 VOLUMES = 3
576
0349168a 577
e25f31ac 578class RecoverCorruptHeaderGZTestBase (RecoverTest):
96fe6399
PG
579 COMPRESSION = "#gz"
580 PASSWORD = None
581 FAILURES = 1
00b8c150 582 CORRUPT = corrupt_header
9d89c237 583 MISMATCHES = 0
96fe6399 584
e25f31ac
PG
585class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
586 VOLUMES = 1
3267933a 587
e25f31ac
PG
588class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
589 VOLUMES = 3
590
591
592class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
96fe6399
PG
593 COMPRESSION = "#gz"
594 PASSWORD = TEST_PASSWORD
595 FAILURES = 1
00b8c150 596 CORRUPT = corrupt_header
9d89c237 597 MISMATCHES = 0
fbdc9f4a 598
e25f31ac
PG
599class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
600 VOLUMES = 1
601
602class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
603 VOLUMES = 3
da8996f0 604
e25f31ac
PG
605
606class RecoverCorruptEntireHeaderTestBase (RecoverTest):
da8996f0
PG
607 COMPRESSION = None
608 PASSWORD = None
609 FAILURES = 1
610 CORRUPT = corrupt_entire_header
9d89c237 611 MISMATCHES = 0
da8996f0 612
e25f31ac
PG
613class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
614 VOLUMES = 1
615
616class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
617 VOLUMES = 3
618
da8996f0 619
e25f31ac 620class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
da8996f0
PG
621 COMPRESSION = "#gz"
622 PASSWORD = None
623 FAILURES = 1
624 CORRUPT = corrupt_entire_header
9d89c237 625 MISMATCHES = 0
da8996f0 626
e25f31ac
PG
627class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
628 VOLUMES = 1
da8996f0 629
e25f31ac
PG
630class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
631 VOLUMES = 3
632
633
634class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
da8996f0
PG
635 COMPRESSION = "#gz"
636 PASSWORD = TEST_PASSWORD
637 FAILURES = 1
638 CORRUPT = corrupt_entire_header
9d89c237 639 MISMATCHES = 0
da8996f0 640
e25f31ac
PG
641class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
642 VOLUMES = 1
643
644class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
645 VOLUMES = 3
517d35b7 646
e25f31ac
PG
647
648class RecoverCorruptTrailingDataTestBase (RecoverTest):
517d35b7
PG
649 # plain Tar is indifferent against traling data and the results
650 # are consistent
651 COMPRESSION = None
652 PASSWORD = None
653 FAILURES = 0
654 CORRUPT = corrupt_trailing_data
655 MISMATCHES = 0
656
e25f31ac
PG
657class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
658 VOLUMES = 1
659
660class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
14895f4b
PG
661 # the last object in first archive has extra bytes somewhere in the
662 # middle because tar itself performs no data checksumming.
663 MISMATCHES = 1
e25f31ac
PG
664 VOLUMES = 3
665
517d35b7 666
e25f31ac 667class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
517d35b7
PG
668 # reading past the final object will cause decompression failure;
669 # all objects except for the last survive unharmed though
670 COMPRESSION = "#gz"
671 PASSWORD = None
672 FAILURES = 1
673 CORRUPT = corrupt_trailing_data
674 MISMATCHES = 0
675
e25f31ac
PG
676class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
677 VOLUMES = 1
517d35b7 678
e25f31ac
PG
679class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
680 VOLUMES = 3
14895f4b
PG
681 # the last file of the first volume will only contain the data of the
682 # second part which is contained in the second volume. this happens
683 # because the CRC32 is wrong for the first part so it gets discarded, then
684 # the object is recreated from the first header of the second volume,
685 # containing only the remainder of the data.
686 MISMATCHES = 1
4d4925de 687 MISSING = 0
e25f31ac
PG
688
689
690class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
517d35b7
PG
691 COMPRESSION = "#gz"
692 PASSWORD = TEST_PASSWORD
693 FAILURES = 0
694 CORRUPT = corrupt_trailing_data
695 MISMATCHES = 0
696
e25f31ac
PG
697class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
698 VOLUMES = 1
699
700class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
701 VOLUMES = 3
517d35b7 702
20e1d773
PG
703
704class RecoverCorruptVolumeBaseTest (RecoverTest):
705 COMPRESSION = None
706 PASSWORD = None
707 FAILURES = 8
708 CORRUPT = corrupt_volume
709 VOLUMES = 3
710
711class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
712 pass
713
3692fd82
PG
714class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
715 COMPRESSION = "#gz"
716
717class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
20e1d773 718 COMPRESSION = "#gz"
3692fd82
PG
719 PASSWORD = TEST_PASSWORD
720
721
722class RecoverCorruptHoleBaseTest (RecoverTest):
723 """
724 Cut bytes from the middle of a volume.
725
726 Index-based recovery works only up to the hole.
727 """
728 COMPRESSION = None
20e1d773 729 PASSWORD = None
3692fd82
PG
730 FAILURES = 3
731 CORRUPT = corrupt_hole
732 VOLUMES = 2 # request two vols to swell up the first one
733 MISMATCHES = 1
734
735class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
736 FAILURES = 2
737
738class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
739 COMPRESSION = "#gz"
740 MISSING = 2
20e1d773 741
3692fd82 742class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
20e1d773
PG
743 COMPRESSION = "#gz"
744 PASSWORD = TEST_PASSWORD
3692fd82 745 MISSING = 2
20e1d773 746
2fe5f6e7
PG
747###############################################################################
748# rescue
749###############################################################################
750
751class RescueCorruptHoleBaseTest (RescueTest):
752 """
753 Cut bytes from the middle of a volume.
754 """
755 COMPRESSION = None
756 PASSWORD = None
79bc14cf 757 FAILURES = 0
2fe5f6e7
PG
758 CORRUPT = corrupt_hole
759 VOLUMES = 2 # request two vols to swell up the first one
79bc14cf
PG
760 MISMATCHES = 2 # intersected by hole
761 MISSING = 1 # excised by hole
2fe5f6e7
PG
762
763class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
79bc14cf 764 pass
2fe5f6e7
PG
765
766class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
767 COMPRESSION = "#gz"
79bc14cf
PG
768 # the decompressor explodes in our face processing the first dummy, nothing
769 # we can do to recover
770 FAILURES = 1
2fe5f6e7
PG
771
772class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
773 COMPRESSION = "#gz"
774 PASSWORD = TEST_PASSWORD
79bc14cf
PG
775 # again, ignoring the crypto errors yields a bad zlib stream causing the
776 # decompressor to abort where the hole begins; the file is extracted up
777 # to this point though
778 FAILURES = 1
2fe5f6e7 779
0c8baf2b
PG
780
781class RecoverCorruptHeaderCTSizeGZAESTest (RescueTest):
782 COMPRESSION = "#gz"
783 PASSWORD = TEST_PASSWORD
784 FAILURES = 0
785 CORRUPT = corrupt_ctsize
786 MISMATCHES = 0
787
788
2fe5f6e7
PG
789###############################################################################
790# index
791###############################################################################
792
793class GenIndexIntactBaseTest (GenIndexTest):
794 """
795 """
796 COMPRESSION = None
797 PASSWORD = None
798 FAILURES = 0
799 CORRUPT = immaculate
800 VOLUMES = 1
801 MISMATCHES = 1
802
803
804class GenIndexIntactTest (GenIndexIntactBaseTest):
805 pass
806
807class GenIndexIntactGZTest (GenIndexIntactBaseTest):
808 COMPRESSION = "#gz"
809 MISSING = 2
810
811class GenIndexIntactGZAESTest (GenIndexIntactBaseTest):
812 COMPRESSION = "#gz"
813 PASSWORD = TEST_PASSWORD
814 MISSING = 2
815