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