Commit | Line | Data |
---|---|---|
fbdc9f4a PG |
1 | import logging |
2 | import os | |
3 | import shutil | |
3692fd82 | 4 | import stat |
fbdc9f4a PG |
5 | |
6 | import deltatar.deltatar as deltatar | |
3267933a | 7 | import deltatar.crypto as crypto |
203cb25e | 8 | import deltatar.tarfile as tarfile |
fbdc9f4a PG |
9 | |
10 | from . import BaseTest | |
11 | ||
e25f31ac | 12 | TEST_PASSWORD = "test1234" |
85e7013f | 13 | TEST_VOLSIZ = 2 # MB |
e25f31ac | 14 | TEST_FILESPERVOL = 3 |
85e7013f PG |
15 | VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into |
16 | # volumes; this is black magic | |
20e1d773 | 17 | TEST_BLOCKSIZE = 4096 |
96fe6399 PG |
18 | |
19 | ############################################################################### | |
20 | ## helpers ## | |
21 | ############################################################################### | |
22 | ||
3267933a PG |
23 | def flip_bits (fname, off, b=0x01, n=1): |
24 | """ | |
25 | Open file *fname* at offset *off*, replacing the next *n* bytes with | |
26 | their values xor’ed with *b*. | |
27 | """ | |
28 | fd = os.open (fname, os.O_RDWR) | |
203cb25e | 29 | |
3267933a PG |
30 | try: |
31 | pos = os.lseek (fd, off, os.SEEK_SET) | |
32 | assert pos == off | |
33 | chunk = os.read (fd, n) | |
34 | chunk = bytes (map (lambda v: v ^ b, chunk)) | |
da8996f0 PG |
35 | pos = os.lseek (fd, off, os.SEEK_SET) |
36 | assert pos == off | |
3267933a PG |
37 | os.write (fd, chunk) |
38 | finally: | |
39 | os.close (fd) | |
40 | ||
203cb25e PG |
41 | |
42 | def gz_header_size (fname, off=0): | |
43 | """ | |
44 | Determine the length of the gzip header starting at *off* in file fname. | |
45 | ||
46 | The header is variable length because it may contain the filename as NUL | |
47 | terminated bytes. | |
48 | """ | |
49 | # length so we need to determine where the actual payload starts | |
50 | off = tarfile.GZ_HEADER_SIZE | |
51 | fd = os.open (fname, os.O_RDONLY) | |
52 | ||
53 | try: | |
54 | pos = os.lseek (fd, off, os.SEEK_SET) | |
55 | assert pos == off | |
56 | while os.read (fd, 1)[0] != 0: | |
57 | off += 1 | |
58 | pos = os.lseek (fd, off, os.SEEK_SET) | |
59 | assert pos == off | |
60 | finally: | |
61 | os.close (fd) | |
62 | ||
63 | return off | |
64 | ||
da8996f0 | 65 | |
96fe6399 PG |
66 | def is_pdt_encrypted (fname): |
67 | """ | |
68 | Returns true if the file contains at least one PDT header plus enough | |
69 | space for the object. | |
70 | """ | |
71 | try: | |
72 | with open (fname, "rb") as st: | |
73 | hdr = crypto.hdr_read_stream (st) | |
74 | siz = hdr ["ctsize"] | |
75 | assert (len (st.read (siz)) == siz) | |
76 | except Exception as exn: | |
77 | return False | |
78 | return True | |
79 | ||
80 | ||
3692fd82 PG |
81 | ############################################################################### |
82 | ## corruption simulators ## | |
83 | ############################################################################### | |
84 | ||
00b8c150 PG |
85 | def corrupt_header (_, fname, compress, encrypt): |
86 | """ | |
87 | Modify a significant byte in the object header of the format. | |
88 | """ | |
89 | if encrypt is True: # damage GCM tag | |
90 | flip_bits (fname, crypto.HDR_OFF_TAG + 1) | |
91 | elif compress is True: # invalidate magic | |
92 | flip_bits (fname, 1) | |
93 | else: # Fudge checksum. From tar(5): | |
94 | # | |
95 | # struct header_gnu_tar { | |
96 | # char name[100]; | |
97 | # char mode[8]; | |
98 | # char uid[8]; | |
99 | # char gid[8]; | |
100 | # char size[12]; | |
101 | # char mtime[12]; | |
102 | # char checksum[8]; | |
103 | # … | |
104 | flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1) | |
105 | ||
106 | ||
da8996f0 PG |
107 | def corrupt_entire_header (_, fname, compress, encrypt): |
108 | """ | |
109 | Flip all bits in the first object header. | |
110 | """ | |
111 | if encrypt is True: | |
112 | flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE) | |
113 | elif compress is True: # invalidate magic | |
114 | flip_bits (fname, 0, 0xff, gz_header_size (fname)) | |
115 | else: | |
116 | flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE) | |
117 | ||
118 | ||
00b8c150 PG |
119 | def corrupt_payload_start (_, fname, compress, encrypt): |
120 | """ | |
121 | Modify the byte following the object header structure of the format. | |
122 | """ | |
123 | if encrypt is True: | |
124 | flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1) | |
125 | elif compress is True: | |
126 | flip_bits (fname, gz_header_size (fname) + 1) | |
127 | else: | |
128 | flip_bits (fname, tarfile.BLOCKSIZE + 1) | |
129 | ||
130 | ||
517d35b7 PG |
131 | def corrupt_trailing_data (_, fname, compress, encrypt): |
132 | """ | |
133 | Modify the byte following the object header structure of the format. | |
134 | """ | |
135 | junk = os.urandom (42) | |
136 | fd = os.open (fname, os.O_WRONLY | os.O_APPEND) | |
137 | os.write (fd, junk) | |
138 | os.close (fd) | |
139 | ||
00b8c150 | 140 | |
20e1d773 PG |
141 | def corrupt_volume (_, fname, compress, encrypt): |
142 | """ | |
143 | Zero out an entire volume. | |
144 | """ | |
145 | fd = os.open (fname, os.O_WRONLY) | |
146 | size = os.lseek (fd, 0, os.SEEK_END) | |
147 | assert os.lseek (fd, 0, os.SEEK_SET) == 0 | |
148 | zeros = bytes (b'\x00' * TEST_BLOCKSIZE) | |
149 | while size > 0: | |
150 | todo = min (size, TEST_BLOCKSIZE) | |
151 | os.write (fd, zeros [:todo]) | |
152 | size -= todo | |
153 | os.close (fd) | |
154 | ||
155 | ||
3692fd82 PG |
156 | def corrupt_hole (_, fname, compress, encrypt): |
157 | """ | |
158 | Cut file in three pieces, reassemble without the middle one. | |
159 | """ | |
160 | aname = os.path.abspath (fname) | |
161 | infd = os.open (fname, os.O_RDONLY) | |
162 | size = os.lseek (infd, 0, os.SEEK_END) | |
163 | assert os.lseek (infd, 0, os.SEEK_SET) == 0 | |
164 | assert size > 3 * TEST_BLOCKSIZE | |
165 | hole = (size / 3, size * 2 / 3) | |
166 | outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE, | |
167 | stat.S_IRUSR | stat.S_IWUSR) | |
168 | ||
169 | zeros = bytes (b'\x00' * TEST_BLOCKSIZE) | |
170 | done = 0 | |
171 | while done < size: | |
172 | data = os.read (infd, TEST_BLOCKSIZE) | |
173 | if done < hole [0] or hole [1] < done: | |
174 | # only copy from outside hole | |
175 | os.write (outfd, data) | |
176 | done += len (data) | |
177 | ||
178 | os.close (infd) | |
179 | os.unlink (fname) | |
180 | ||
181 | path = "/proc/self/fd/%d" % outfd | |
182 | os.link (path, aname, src_dir_fd=0, follow_symlinks=True) | |
183 | os.close (outfd) | |
184 | ||
185 | ||
96fe6399 PG |
186 | ############################################################################### |
187 | ## tests ## | |
188 | ############################################################################### | |
203cb25e | 189 | |
0c6682ce | 190 | class DefectiveTest (BaseTest): |
fbdc9f4a PG |
191 | """ |
192 | Disaster recovery: restore corrupt backups. | |
193 | """ | |
194 | ||
96fe6399 PG |
195 | COMPRESSION = None |
196 | PASSWORD = None | |
9d89c237 PG |
197 | FAILURES = 0 # files that could not be restored |
198 | MISMATCHES = 0 # files that were restored but corrupted | |
00b8c150 | 199 | CORRUPT = corrupt_payload_start |
e25f31ac | 200 | VOLUMES = 1 |
4d4925de | 201 | MISSING = None # normally the number of failures |
96fe6399 | 202 | |
fbdc9f4a PG |
203 | |
204 | def setUp(self): | |
205 | ''' | |
206 | Create base test data | |
207 | ''' | |
96fe6399 PG |
208 | self.pwd = os.getcwd() |
209 | self.dst_path = "source_dir" | |
210 | self.src_path = "%s2" % self.dst_path | |
211 | self.hash = dict() | |
212 | ||
fbdc9f4a | 213 | os.system('rm -rf target_dir source_dir* backup_dir* huge') |
96fe6399 | 214 | os.makedirs (self.src_path) |
fbdc9f4a | 215 | |
96fe6399 | 216 | for i in range (5): |
85e7013f | 217 | f = "dummy_%d" % i |
96fe6399 PG |
218 | self.hash [f] = self.create_file ("%s/%s" |
219 | % (self.src_path, f), 5 + i) | |
fbdc9f4a | 220 | |
96fe6399 PG |
221 | |
222 | def tearDown(self): | |
223 | ''' | |
224 | Remove temporal files created by unit tests and reset globals. | |
225 | ''' | |
226 | os.chdir(self.pwd) | |
227 | os.system("rm -rf source_dir source_dir2 backup_dir*") | |
fbdc9f4a PG |
228 | |
229 | ||
0c6682ce PG |
230 | class RecoverTest (DefectiveTest): |
231 | """ | |
232 | Recover: restore corrupt backups from index file information. | |
233 | """ | |
234 | ||
da8996f0 | 235 | def test_recover_corrupt (self): |
fbdc9f4a | 236 | """ |
da8996f0 | 237 | Perform various damaging actions that cause unreadable objects. |
fbdc9f4a PG |
238 | |
239 | Expects the extraction to fail in normal mode. With disaster recovery, | |
240 | extraction must succeed, and exactly one file must be missing. | |
241 | """ | |
96fe6399 | 242 | mode = self.COMPRESSION or "#" |
203cb25e | 243 | bak_path = "backup_dir" |
e25f31ac PG |
244 | backup_file = "the_full_backup_%0.2d.tar" |
245 | backup_full = ("%s/%s" % (bak_path, backup_file)) % 0 | |
96fe6399 PG |
246 | index_file = "the_full_index" |
247 | ||
248 | if self.COMPRESSION is not None: | |
249 | backup_file += ".gz" | |
250 | backup_full += ".gz" | |
251 | index_file += ".gz" | |
252 | ||
253 | if self.PASSWORD is not None: | |
e25f31ac PG |
254 | backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION) |
255 | backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION) | |
256 | index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION) | |
257 | ||
258 | if self.VOLUMES > 1: | |
85e7013f PG |
259 | # add n files for one nth the volume size each, corrected |
260 | # for metadata and tar block overhead | |
261 | fsiz = int ( ( TEST_VOLSIZ | |
262 | / (TEST_FILESPERVOL * VOLUME_OVERHEAD)) | |
263 | * 1024 * 1024) | |
264 | fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL | |
e25f31ac PG |
265 | for i in range (fcnt): |
266 | nvol, invol = divmod(i, TEST_FILESPERVOL) | |
267 | f = "dummy_vol_%d_n_%0.2d" % (nvol, invol) | |
268 | self.hash [f] = self.create_file ("%s/%s" | |
269 | % (self.src_path, f), | |
85e7013f PG |
270 | fsiz, |
271 | random=True) | |
e25f31ac PG |
272 | |
273 | def vname (_x, _y, n, *a, **kwa): | |
274 | return backup_file % n | |
fbdc9f4a | 275 | |
96fe6399 PG |
276 | dtar = deltatar.DeltaTar (mode=mode, |
277 | logger=None, | |
278 | password=self.PASSWORD, | |
203cb25e | 279 | index_name_func=lambda _: index_file, |
3267933a | 280 | volume_name_func=vname) |
fbdc9f4a PG |
281 | |
282 | dtar.create_full_backup \ | |
e25f31ac PG |
283 | (source_path=self.src_path, backup_path=bak_path, |
284 | max_volume_size=1) | |
96fe6399 PG |
285 | |
286 | if self.PASSWORD is not None: | |
287 | # ensure all files are at least superficially in PDT format | |
288 | for f in os.listdir (bak_path): | |
289 | assert is_pdt_encrypted ("%s/%s" % (bak_path, f)) | |
203cb25e PG |
290 | |
291 | # first restore must succeed | |
96fe6399 | 292 | dtar.restore_backup(target_path=self.dst_path, |
f090d35a PG |
293 | backup_indexes_paths=[ |
294 | "%s/%s" % (bak_path, index_file) | |
295 | ]) | |
203cb25e | 296 | for key, value in self.hash.items (): |
96fe6399 | 297 | f = "%s/%s" % (self.dst_path, key) |
b15e549b PG |
298 | assert os.path.exists (f) |
299 | assert value == self.md5sum (f) | |
96fe6399 PG |
300 | shutil.rmtree (self.dst_path) |
301 | shutil.rmtree (self.src_path) | |
203cb25e | 302 | |
00b8c150 PG |
303 | self.CORRUPT (backup_full, |
304 | self.COMPRESSION is not None, | |
305 | self.PASSWORD is not None) | |
203cb25e PG |
306 | |
307 | # normal restore must fail | |
96fe6399 PG |
308 | try: |
309 | dtar.restore_backup(target_path=self.dst_path, | |
203cb25e | 310 | backup_tar_path=backup_full) |
96fe6399 PG |
311 | except tarfile.CompressionError: |
312 | if self.PASSWORD is not None or self.COMPRESSION is not None: | |
313 | pass | |
00b8c150 PG |
314 | else: |
315 | raise | |
96fe6399 | 316 | except tarfile.ReadError: |
00b8c150 PG |
317 | # can happen with all three modes |
318 | pass | |
319 | except tarfile.DecryptionError: | |
320 | if self.PASSWORD is not None: | |
96fe6399 | 321 | pass |
00b8c150 PG |
322 | else: |
323 | raise | |
96fe6399 PG |
324 | |
325 | os.chdir (self.pwd) # not restored due to the error above | |
203cb25e | 326 | # but recover will succeed |
96fe6399 | 327 | failed = dtar.recover_backup(target_path=self.dst_path, |
b15e549b PG |
328 | backup_indexes_paths=[ |
329 | "%s/%s" % (bak_path, index_file) | |
330 | ]) | |
96fe6399 PG |
331 | |
332 | assert len (failed) == self.FAILURES | |
203cb25e PG |
333 | |
334 | # with one file missing | |
9d89c237 PG |
335 | missing = [] |
336 | mismatch = [] | |
203cb25e | 337 | for key, value in self.hash.items (): |
96fe6399 | 338 | kkey = "%s/%s" % (self.dst_path, key) |
b15e549b | 339 | if os.path.exists (kkey): |
9d89c237 PG |
340 | if value != self.md5sum (kkey): |
341 | mismatch.append (key) | |
203cb25e | 342 | else: |
757319dd | 343 | missing.append (key) |
4d4925de PG |
344 | |
345 | # usually, an object whose extraction fails will not be found on | |
346 | # disk afterwards so the number of failures equals that of missing | |
347 | # files. however, some modes will create partial files for objects | |
348 | # spanning multiple volumes that contain the parts whose checksums | |
349 | # were valid. | |
350 | assert len (missing) == (self.MISSING if self.MISSING is not None | |
351 | else self.FAILURES) | |
9d89c237 | 352 | assert len (mismatch) == self.MISMATCHES |
96fe6399 PG |
353 | |
354 | shutil.rmtree (self.dst_path) | |
355 | ||
356 | ||
0c6682ce PG |
357 | class RescueTest (DefectiveTest): |
358 | """ | |
359 | Rescue: restore corrupt backups from backup set that is damaged to a degree | |
360 | that the index file is worthless. | |
361 | """ | |
362 | ||
363 | def test_rescue_corrupt (self): | |
364 | """ | |
365 | Perform various damaging actions that cause unreadable objects, then | |
366 | attempt to extract objects regardless. | |
367 | """ | |
368 | mode = self.COMPRESSION or "#" | |
369 | bak_path = "backup_dir" | |
370 | backup_file = "the_full_backup_%0.2d.tar" | |
371 | backup_full = ("%s/%s" % (bak_path, backup_file)) % 0 | |
372 | index_file = "the_full_index" | |
373 | ||
374 | if self.COMPRESSION is not None: | |
375 | backup_file += ".gz" | |
376 | backup_full += ".gz" | |
377 | index_file += ".gz" | |
378 | ||
379 | if self.PASSWORD is not None: | |
380 | backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION) | |
381 | backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION) | |
382 | index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION) | |
383 | ||
384 | if self.VOLUMES > 1: | |
385 | # add n files for one nth the volume size each, corrected | |
386 | # for metadata and tar block overhead | |
387 | fsiz = int ( ( TEST_VOLSIZ | |
388 | / (TEST_FILESPERVOL * VOLUME_OVERHEAD)) | |
389 | * 1024 * 1024) | |
390 | fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL | |
391 | for i in range (fcnt): | |
392 | nvol, invol = divmod(i, TEST_FILESPERVOL) | |
393 | f = "dummy_vol_%d_n_%0.2d" % (nvol, invol) | |
394 | self.hash [f] = self.create_file ("%s/%s" | |
395 | % (self.src_path, f), | |
396 | fsiz, | |
397 | random=True) | |
398 | ||
399 | def vname (_x, _y, n, *a, **kwa): | |
400 | return backup_file % n | |
401 | ||
402 | dtar = deltatar.DeltaTar (mode=mode, | |
403 | logger=None, | |
404 | password=self.PASSWORD, | |
405 | index_name_func=lambda _: index_file, | |
406 | volume_name_func=vname) | |
407 | ||
408 | dtar.create_full_backup \ | |
409 | (source_path=self.src_path, backup_path=bak_path, | |
410 | max_volume_size=1) | |
411 | ||
412 | if self.PASSWORD is not None: | |
413 | # ensure all files are at least superficially in PDT format | |
414 | for f in os.listdir (bak_path): | |
415 | assert is_pdt_encrypted ("%s/%s" % (bak_path, f)) | |
416 | ||
417 | # first restore must succeed | |
418 | dtar.restore_backup(target_path=self.dst_path, | |
419 | backup_indexes_paths=[ | |
420 | "%s/%s" % (bak_path, index_file) | |
421 | ]) | |
422 | for key, value in self.hash.items (): | |
423 | f = "%s/%s" % (self.dst_path, key) | |
424 | assert os.path.exists (f) | |
425 | assert value == self.md5sum (f) | |
426 | shutil.rmtree (self.dst_path) | |
427 | shutil.rmtree (self.src_path) | |
428 | ||
429 | self.CORRUPT (backup_full, | |
430 | self.COMPRESSION is not None, | |
431 | self.PASSWORD is not None) | |
432 | ||
433 | # normal restore must fail | |
434 | try: | |
435 | dtar.restore_backup(target_path=self.dst_path, | |
436 | backup_tar_path=backup_full) | |
437 | except tarfile.CompressionError: | |
438 | if self.PASSWORD is not None or self.COMPRESSION is not None: | |
439 | pass | |
440 | else: | |
441 | raise | |
442 | except tarfile.ReadError: | |
443 | # can happen with all three modes | |
444 | pass | |
445 | except tarfile.DecryptionError: | |
446 | if self.PASSWORD is not None: | |
447 | pass | |
448 | else: | |
449 | raise | |
450 | ||
451 | os.chdir (self.pwd) # not restored due to the error above | |
452 | # but recover will succeed | |
453 | failed = dtar.rescue_backup(target_path=self.dst_path, | |
454 | backup_indexes_paths=[ | |
455 | "%s/%s" % (bak_path, index_file) | |
456 | ]) | |
457 | ||
458 | assert len (failed) == self.FAILURES | |
459 | ||
460 | # with one file missing | |
461 | missing = [] | |
462 | mismatch = [] | |
463 | for key, value in self.hash.items (): | |
464 | kkey = "%s/%s" % (self.dst_path, key) | |
465 | if os.path.exists (kkey): | |
466 | if value != self.md5sum (kkey): | |
467 | mismatch.append (key) | |
468 | else: | |
469 | missing.append (key) | |
470 | ||
471 | ssert len (missing) == (self.MISSING if self.MISSING is not None | |
472 | else self.FAILURES) | |
473 | assert len (mismatch) == self.MISMATCHES | |
474 | ||
475 | shutil.rmtree (self.dst_path) | |
476 | ||
477 | ||
e25f31ac | 478 | class RecoverCorruptPayloadTestBase (RecoverTest): |
00b8c150 PG |
479 | COMPRESSION = None |
480 | PASSWORD = None | |
9d89c237 PG |
481 | FAILURES = 0 # tarfile will restore but corrupted, as |
482 | MISMATCHES = 1 # revealed by the hash | |
00b8c150 | 483 | |
e25f31ac PG |
484 | class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase): |
485 | VOLUMES = 1 | |
486 | ||
487 | class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase): | |
488 | VOLUMES = 3 | |
489 | ||
00b8c150 | 490 | |
e25f31ac | 491 | class RecoverCorruptPayloadGZTestBase (RecoverTest): |
00b8c150 PG |
492 | COMPRESSION = "#gz" |
493 | PASSWORD = None | |
494 | FAILURES = 1 | |
9d89c237 | 495 | MISMATCHES = 0 |
00b8c150 | 496 | |
e25f31ac PG |
497 | class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase): |
498 | VOLUMES = 1 | |
00b8c150 | 499 | |
e25f31ac PG |
500 | class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase): |
501 | VOLUMES = 3 | |
502 | ||
503 | ||
504 | class RecoverCorruptPayloadGZAESTestBase (RecoverTest): | |
00b8c150 PG |
505 | COMPRESSION = "#gz" |
506 | PASSWORD = TEST_PASSWORD | |
507 | FAILURES = 1 | |
9d89c237 | 508 | MISMATCHES = 0 |
00b8c150 | 509 | |
e25f31ac PG |
510 | class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase): |
511 | VOLUMES = 1 | |
512 | ||
513 | class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase): | |
514 | VOLUMES = 3 | |
00b8c150 | 515 | |
e25f31ac PG |
516 | |
517 | class RecoverCorruptHeaderTestBase (RecoverTest): | |
0349168a PG |
518 | COMPRESSION = None |
519 | PASSWORD = None | |
520 | FAILURES = 1 | |
521 | CORRUPT = corrupt_header | |
9d89c237 | 522 | MISMATCHES = 0 |
0349168a | 523 | |
e25f31ac PG |
524 | class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase): |
525 | VOLUMES = 1 | |
526 | ||
527 | class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase): | |
528 | VOLUMES = 3 | |
529 | ||
0349168a | 530 | |
e25f31ac | 531 | class RecoverCorruptHeaderGZTestBase (RecoverTest): |
96fe6399 PG |
532 | COMPRESSION = "#gz" |
533 | PASSWORD = None | |
534 | FAILURES = 1 | |
00b8c150 | 535 | CORRUPT = corrupt_header |
9d89c237 | 536 | MISMATCHES = 0 |
96fe6399 | 537 | |
e25f31ac PG |
538 | class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase): |
539 | VOLUMES = 1 | |
3267933a | 540 | |
e25f31ac PG |
541 | class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase): |
542 | VOLUMES = 3 | |
543 | ||
544 | ||
545 | class RecoverCorruptHeaderGZAESTestBase (RecoverTest): | |
96fe6399 PG |
546 | COMPRESSION = "#gz" |
547 | PASSWORD = TEST_PASSWORD | |
548 | FAILURES = 1 | |
00b8c150 | 549 | CORRUPT = corrupt_header |
9d89c237 | 550 | MISMATCHES = 0 |
fbdc9f4a | 551 | |
e25f31ac PG |
552 | class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase): |
553 | VOLUMES = 1 | |
554 | ||
555 | class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase): | |
556 | VOLUMES = 3 | |
da8996f0 | 557 | |
e25f31ac PG |
558 | |
559 | class RecoverCorruptEntireHeaderTestBase (RecoverTest): | |
da8996f0 PG |
560 | COMPRESSION = None |
561 | PASSWORD = None | |
562 | FAILURES = 1 | |
563 | CORRUPT = corrupt_entire_header | |
9d89c237 | 564 | MISMATCHES = 0 |
da8996f0 | 565 | |
e25f31ac PG |
566 | class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase): |
567 | VOLUMES = 1 | |
568 | ||
569 | class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase): | |
570 | VOLUMES = 3 | |
571 | ||
da8996f0 | 572 | |
e25f31ac | 573 | class RecoverCorruptEntireHeaderGZTestBase (RecoverTest): |
da8996f0 PG |
574 | COMPRESSION = "#gz" |
575 | PASSWORD = None | |
576 | FAILURES = 1 | |
577 | CORRUPT = corrupt_entire_header | |
9d89c237 | 578 | MISMATCHES = 0 |
da8996f0 | 579 | |
e25f31ac PG |
580 | class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase): |
581 | VOLUMES = 1 | |
da8996f0 | 582 | |
e25f31ac PG |
583 | class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase): |
584 | VOLUMES = 3 | |
585 | ||
586 | ||
587 | class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest): | |
da8996f0 PG |
588 | COMPRESSION = "#gz" |
589 | PASSWORD = TEST_PASSWORD | |
590 | FAILURES = 1 | |
591 | CORRUPT = corrupt_entire_header | |
9d89c237 | 592 | MISMATCHES = 0 |
da8996f0 | 593 | |
e25f31ac PG |
594 | class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase): |
595 | VOLUMES = 1 | |
596 | ||
597 | class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase): | |
598 | VOLUMES = 3 | |
517d35b7 | 599 | |
e25f31ac PG |
600 | |
601 | class RecoverCorruptTrailingDataTestBase (RecoverTest): | |
517d35b7 PG |
602 | # plain Tar is indifferent against traling data and the results |
603 | # are consistent | |
604 | COMPRESSION = None | |
605 | PASSWORD = None | |
606 | FAILURES = 0 | |
607 | CORRUPT = corrupt_trailing_data | |
608 | MISMATCHES = 0 | |
609 | ||
e25f31ac PG |
610 | class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase): |
611 | VOLUMES = 1 | |
612 | ||
613 | class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase): | |
14895f4b PG |
614 | # the last object in first archive has extra bytes somewhere in the |
615 | # middle because tar itself performs no data checksumming. | |
616 | MISMATCHES = 1 | |
e25f31ac PG |
617 | VOLUMES = 3 |
618 | ||
517d35b7 | 619 | |
e25f31ac | 620 | class RecoverCorruptTrailingDataGZTestBase (RecoverTest): |
517d35b7 PG |
621 | # reading past the final object will cause decompression failure; |
622 | # all objects except for the last survive unharmed though | |
623 | COMPRESSION = "#gz" | |
624 | PASSWORD = None | |
625 | FAILURES = 1 | |
626 | CORRUPT = corrupt_trailing_data | |
627 | MISMATCHES = 0 | |
628 | ||
e25f31ac PG |
629 | class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase): |
630 | VOLUMES = 1 | |
517d35b7 | 631 | |
e25f31ac PG |
632 | class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase): |
633 | VOLUMES = 3 | |
14895f4b PG |
634 | # the last file of the first volume will only contain the data of the |
635 | # second part which is contained in the second volume. this happens | |
636 | # because the CRC32 is wrong for the first part so it gets discarded, then | |
637 | # the object is recreated from the first header of the second volume, | |
638 | # containing only the remainder of the data. | |
639 | MISMATCHES = 1 | |
4d4925de | 640 | MISSING = 0 |
e25f31ac PG |
641 | |
642 | ||
643 | class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest): | |
517d35b7 PG |
644 | COMPRESSION = "#gz" |
645 | PASSWORD = TEST_PASSWORD | |
646 | FAILURES = 0 | |
647 | CORRUPT = corrupt_trailing_data | |
648 | MISMATCHES = 0 | |
649 | ||
e25f31ac PG |
650 | class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase): |
651 | VOLUMES = 1 | |
652 | ||
653 | class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase): | |
654 | VOLUMES = 3 | |
517d35b7 | 655 | |
20e1d773 PG |
656 | |
657 | class RecoverCorruptVolumeBaseTest (RecoverTest): | |
658 | COMPRESSION = None | |
659 | PASSWORD = None | |
660 | FAILURES = 8 | |
661 | CORRUPT = corrupt_volume | |
662 | VOLUMES = 3 | |
663 | ||
664 | class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest): | |
665 | pass | |
666 | ||
3692fd82 PG |
667 | class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest): |
668 | COMPRESSION = "#gz" | |
669 | ||
670 | class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest): | |
20e1d773 | 671 | COMPRESSION = "#gz" |
3692fd82 PG |
672 | PASSWORD = TEST_PASSWORD |
673 | ||
674 | ||
675 | class RecoverCorruptHoleBaseTest (RecoverTest): | |
676 | """ | |
677 | Cut bytes from the middle of a volume. | |
678 | ||
679 | Index-based recovery works only up to the hole. | |
680 | """ | |
681 | COMPRESSION = None | |
20e1d773 | 682 | PASSWORD = None |
3692fd82 PG |
683 | FAILURES = 3 |
684 | CORRUPT = corrupt_hole | |
685 | VOLUMES = 2 # request two vols to swell up the first one | |
686 | MISMATCHES = 1 | |
687 | ||
688 | class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest): | |
689 | FAILURES = 2 | |
690 | ||
691 | class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest): | |
692 | COMPRESSION = "#gz" | |
693 | MISSING = 2 | |
20e1d773 | 694 | |
3692fd82 | 695 | class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest): |
20e1d773 PG |
696 | COMPRESSION = "#gz" |
697 | PASSWORD = TEST_PASSWORD | |
3692fd82 | 698 | MISSING = 2 |
20e1d773 | 699 |