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 | ||
0c8baf2b PG |
87 | class UndefinedTest (Exception): |
88 | """No test available for the asked combination of parameters.""" | |
89 | ||
00b8c150 PG |
90 | def 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 |
112 | def 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 |
125 | def 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 |
137 | def 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 |
149 | def 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 |
159 | def 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 |
174 | def 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 |
203 | def immaculate (_, _fname, _compress, _encrypt): |
204 | """ | |
205 | No-op dummy. | |
206 | """ | |
207 | pass | |
3692fd82 | 208 | |
96fe6399 PG |
209 | ############################################################################### |
210 | ## tests ## | |
211 | ############################################################################### | |
203cb25e | 212 | |
0c6682ce | 213 | class 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 | ||
276 | class 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 |
389 | class 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 |
492 | class 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 | 525 | class 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 |
531 | class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase): |
532 | VOLUMES = 1 | |
533 | ||
534 | class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase): | |
535 | VOLUMES = 3 | |
536 | ||
00b8c150 | 537 | |
e25f31ac | 538 | class RecoverCorruptPayloadGZTestBase (RecoverTest): |
00b8c150 PG |
539 | COMPRESSION = "#gz" |
540 | PASSWORD = None | |
541 | FAILURES = 1 | |
9d89c237 | 542 | MISMATCHES = 0 |
00b8c150 | 543 | |
e25f31ac PG |
544 | class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase): |
545 | VOLUMES = 1 | |
00b8c150 | 546 | |
e25f31ac PG |
547 | class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase): |
548 | VOLUMES = 3 | |
549 | ||
550 | ||
551 | class RecoverCorruptPayloadGZAESTestBase (RecoverTest): | |
00b8c150 PG |
552 | COMPRESSION = "#gz" |
553 | PASSWORD = TEST_PASSWORD | |
554 | FAILURES = 1 | |
9d89c237 | 555 | MISMATCHES = 0 |
00b8c150 | 556 | |
e25f31ac PG |
557 | class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase): |
558 | VOLUMES = 1 | |
559 | ||
560 | class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase): | |
561 | VOLUMES = 3 | |
00b8c150 | 562 | |
e25f31ac PG |
563 | |
564 | class 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 |
571 | class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase): |
572 | VOLUMES = 1 | |
573 | ||
574 | class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase): | |
575 | VOLUMES = 3 | |
576 | ||
0349168a | 577 | |
e25f31ac | 578 | class 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 |
585 | class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase): |
586 | VOLUMES = 1 | |
3267933a | 587 | |
e25f31ac PG |
588 | class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase): |
589 | VOLUMES = 3 | |
590 | ||
591 | ||
592 | class 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 |
599 | class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase): |
600 | VOLUMES = 1 | |
601 | ||
602 | class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase): | |
603 | VOLUMES = 3 | |
da8996f0 | 604 | |
e25f31ac PG |
605 | |
606 | class 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 |
613 | class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase): |
614 | VOLUMES = 1 | |
615 | ||
616 | class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase): | |
617 | VOLUMES = 3 | |
618 | ||
da8996f0 | 619 | |
e25f31ac | 620 | class 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 |
627 | class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase): |
628 | VOLUMES = 1 | |
da8996f0 | 629 | |
e25f31ac PG |
630 | class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase): |
631 | VOLUMES = 3 | |
632 | ||
633 | ||
634 | class 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 |
641 | class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase): |
642 | VOLUMES = 1 | |
643 | ||
644 | class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase): | |
645 | VOLUMES = 3 | |
517d35b7 | 646 | |
e25f31ac PG |
647 | |
648 | class 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 |
657 | class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase): |
658 | VOLUMES = 1 | |
659 | ||
660 | class 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 | 667 | class 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 |
676 | class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase): |
677 | VOLUMES = 1 | |
517d35b7 | 678 | |
e25f31ac PG |
679 | class 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 | ||
690 | class 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 |
697 | class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase): |
698 | VOLUMES = 1 | |
699 | ||
700 | class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase): | |
701 | VOLUMES = 3 | |
517d35b7 | 702 | |
20e1d773 PG |
703 | |
704 | class RecoverCorruptVolumeBaseTest (RecoverTest): | |
705 | COMPRESSION = None | |
706 | PASSWORD = None | |
707 | FAILURES = 8 | |
708 | CORRUPT = corrupt_volume | |
709 | VOLUMES = 3 | |
710 | ||
711 | class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest): | |
712 | pass | |
713 | ||
3692fd82 PG |
714 | class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest): |
715 | COMPRESSION = "#gz" | |
716 | ||
717 | class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest): | |
20e1d773 | 718 | COMPRESSION = "#gz" |
3692fd82 PG |
719 | PASSWORD = TEST_PASSWORD |
720 | ||
721 | ||
722 | class 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 | ||
735 | class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest): | |
736 | FAILURES = 2 | |
737 | ||
738 | class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest): | |
739 | COMPRESSION = "#gz" | |
740 | MISSING = 2 | |
20e1d773 | 741 | |
3692fd82 | 742 | class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest): |
20e1d773 PG |
743 | COMPRESSION = "#gz" |
744 | PASSWORD = TEST_PASSWORD | |
3692fd82 | 745 | MISSING = 2 |
20e1d773 | 746 | |
2fe5f6e7 PG |
747 | ############################################################################### |
748 | # rescue | |
749 | ############################################################################### | |
750 | ||
751 | class 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 | |
763 | class RescueCorruptHoleTest (RescueCorruptHoleBaseTest): | |
79bc14cf | 764 | pass |
2fe5f6e7 PG |
765 | |
766 | class 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 | |
772 | class 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 | |
781 | class 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 | ||
793 | class GenIndexIntactBaseTest (GenIndexTest): | |
794 | """ | |
795 | """ | |
796 | COMPRESSION = None | |
797 | PASSWORD = None | |
798 | FAILURES = 0 | |
799 | CORRUPT = immaculate | |
800 | VOLUMES = 1 | |
801 | MISMATCHES = 1 | |
802 | ||
803 | ||
804 | class GenIndexIntactTest (GenIndexIntactBaseTest): | |
805 | pass | |
806 | ||
807 | class GenIndexIntactGZTest (GenIndexIntactBaseTest): | |
808 | COMPRESSION = "#gz" | |
809 | MISSING = 2 | |
810 | ||
811 | class GenIndexIntactGZAESTest (GenIndexIntactBaseTest): | |
812 | COMPRESSION = "#gz" | |
813 | PASSWORD = TEST_PASSWORD | |
814 | MISSING = 2 | |
815 |