| 1 | """ |
| 2 | Intra2net 2017 |
| 3 | |
| 4 | =============================================================================== |
| 5 | test_recover.py – behavior facing file corruption |
| 6 | =============================================================================== |
| 7 | |
| 8 | Corruptors have the signature ``(unittest × string × bool × bool) → void``, |
| 9 | where the *string* argument is the name of the file to modify, the *booleans* |
| 10 | specialize the operation for compressed and encrypted data. Issues are |
| 11 | communicated upward by throwing. |
| 12 | |
| 13 | - corrupt_header (): |
| 14 | Modify the first object header where it hurts. With encryption, the tag |
| 15 | is corrupted to cause authentication of the decrypted data to fail. For |
| 16 | compressed data, the two byte magic is altered, for uncompressed |
| 17 | archives, the tar header checksum field. |
| 18 | |
| 19 | - corrupt_truncate (): |
| 20 | Drop the file’s content after two thirds, causing extraction of later |
| 21 | objects to fail. Since the operation preserves the offsets of objects |
| 22 | before the cutoff, this yields the same results regardless of whether |
| 23 | restore or rescue mode is used. |
| 24 | |
| 25 | - corrupt_ctsize (): |
| 26 | Modify the *ctsize* field of a PDTCRYPT header. The goal is to have |
| 27 | decryption continue past the end of the object, causing data |
| 28 | authentication to fail and file reads to be at odds with the offsets in |
| 29 | the index. Only applicable to encrypted archives; will raise |
| 30 | *UndefinedTest* otherwise. |
| 31 | |
| 32 | - corrupt_entire_header (): |
| 33 | Invert all bits of the first object header (PDTCRYPT, gzip, tar) without |
| 34 | affecting the payload. This renders the object unreadable; the result will |
| 35 | resemble a file with arbitrary leading data but all the remaining object |
| 36 | offsets intact, so the contents can still be extracted with index based |
| 37 | recovery. |
| 38 | |
| 39 | - corrupt_payload_start (): |
| 40 | For all header variants, skip to the first byte past the header and |
| 41 | corrupt it. Encrypted objects will fail to authenticate. Compressed |
| 42 | objects will yield a bad CRC32. The Tar layer will take no notice but |
| 43 | the extracted object will fail an independent checksum comparison with |
| 44 | that of the original file. |
| 45 | |
| 46 | - corrupt_leading_garbage (): |
| 47 | Prepend random data to an otherwise valid file. Creates a situation that |
| 48 | index based recovery cannot handle by shifting the offsets of all objects |
| 49 | in the file. In rescue mode, these objects must be located and extracted |
| 50 | regardless. |
| 51 | |
| 52 | - corrupt_trailing_data (): |
| 53 | Append data to an otherwise valid file. Both the recovery and rescue |
| 54 | modes must be able to retrieve all objects from that file. |
| 55 | |
| 56 | - corrupt_volume (): |
| 57 | Zero out an entire backup file. This is interesting for multivolume |
| 58 | tests: all files from the affected volume must be missing but objects |
| 59 | that span volume bounds will still be partially recoverable. |
| 60 | |
| 61 | - corrupt_hole (): |
| 62 | Remove a region from a file. Following the damaged part, no object can be |
| 63 | recovered in index mode, but rescue mode will still find those. The |
| 64 | object containing the start of the hole will fail checksum tests because |
| 65 | of the missing part and the overlap with the subsequent object. |
| 66 | |
| 67 | """ |
| 68 | |
| 69 | import logging |
| 70 | import os |
| 71 | import shutil |
| 72 | import stat |
| 73 | import sys |
| 74 | import unittest |
| 75 | |
| 76 | from functools import partial |
| 77 | |
| 78 | import deltatar.deltatar as deltatar |
| 79 | import deltatar.crypto as crypto |
| 80 | import deltatar.tarfile as tarfile |
| 81 | |
| 82 | from . import BaseTest |
| 83 | |
| 84 | TEST_PASSWORD = "test1234" |
| 85 | TEST_VOLSIZ = 2 # MB |
| 86 | TEST_FILESPERVOL = 3 |
| 87 | VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into |
| 88 | # volumes; this is black magic |
| 89 | TEST_BLOCKSIZE = 4096 |
| 90 | |
| 91 | ############################################################################### |
| 92 | ## helpers ## |
| 93 | ############################################################################### |
| 94 | |
| 95 | def flip_bits (fname, off, b=0x01, n=1): |
| 96 | """ |
| 97 | Open file *fname* at offset *off*, replacing the next *n* bytes with |
| 98 | their values xor’ed with *b*. |
| 99 | """ |
| 100 | fd = os.open (fname, os.O_RDWR) |
| 101 | |
| 102 | try: |
| 103 | pos = os.lseek (fd, off, os.SEEK_SET) |
| 104 | assert pos == off |
| 105 | chunk = os.read (fd, n) |
| 106 | chunk = bytes (map (lambda v: v ^ b, chunk)) |
| 107 | pos = os.lseek (fd, off, os.SEEK_SET) |
| 108 | assert pos == off |
| 109 | os.write (fd, chunk) |
| 110 | finally: |
| 111 | os.close (fd) |
| 112 | |
| 113 | |
| 114 | def gz_header_size (fname, off=0): |
| 115 | """ |
| 116 | Determine the length of the gzip header starting at *off* in file fname. |
| 117 | |
| 118 | The header is variable length because it may contain the filename as NUL |
| 119 | terminated bytes. |
| 120 | """ |
| 121 | # length so we need to determine where the actual payload starts |
| 122 | off = tarfile.GZ_HEADER_SIZE |
| 123 | fd = os.open (fname, os.O_RDONLY) |
| 124 | |
| 125 | try: |
| 126 | pos = os.lseek (fd, off, os.SEEK_SET) |
| 127 | assert pos == off |
| 128 | while os.read (fd, 1)[0] != 0: |
| 129 | off += 1 |
| 130 | pos = os.lseek (fd, off, os.SEEK_SET) |
| 131 | assert pos == off |
| 132 | finally: |
| 133 | os.close (fd) |
| 134 | |
| 135 | return off |
| 136 | |
| 137 | |
| 138 | def is_pdt_encrypted (fname): |
| 139 | """ |
| 140 | Returns true if the file contains at least one PDT header plus enough |
| 141 | space for the object. |
| 142 | """ |
| 143 | try: |
| 144 | with open (fname, "rb") as st: |
| 145 | hdr = crypto.hdr_read_stream (st) |
| 146 | siz = hdr ["ctsize"] |
| 147 | assert (len (st.read (siz)) == siz) |
| 148 | except Exception as exn: |
| 149 | return False |
| 150 | return True |
| 151 | |
| 152 | |
| 153 | ############################################################################### |
| 154 | ## corruption simulators ## |
| 155 | ############################################################################### |
| 156 | |
| 157 | class UndefinedTest (Exception): |
| 158 | """No test available for the asked combination of parameters.""" |
| 159 | |
| 160 | def corrupt_header (_, fname, compress, encrypt): |
| 161 | """ |
| 162 | Modify a significant byte in the object header of the format. |
| 163 | """ |
| 164 | if encrypt is True: # damage GCM tag |
| 165 | flip_bits (fname, crypto.HDR_OFF_TAG + 1) |
| 166 | elif compress is True: # invalidate magic |
| 167 | flip_bits (fname, 1) |
| 168 | else: # Fudge checksum. From tar(5): |
| 169 | # |
| 170 | # struct header_gnu_tar { |
| 171 | # char name[100]; |
| 172 | # char mode[8]; |
| 173 | # char uid[8]; |
| 174 | # char gid[8]; |
| 175 | # char size[12]; |
| 176 | # char mtime[12]; |
| 177 | # char checksum[8]; |
| 178 | # … |
| 179 | flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1) |
| 180 | |
| 181 | |
| 182 | def corrupt_truncate (_, fname, _compress, _encrypt): |
| 183 | """ |
| 184 | Shorten file by one third. |
| 185 | """ |
| 186 | fd = os.open (fname, os.O_WRONLY) |
| 187 | size = os.lseek (fd, 0, os.SEEK_END) |
| 188 | os.ftruncate (fd, 2 * size // 3) |
| 189 | os.fsync (fd) |
| 190 | os.close (fd) |
| 191 | |
| 192 | |
| 193 | def corrupt_ctsize (_, fname, compress, encrypt): |
| 194 | """ |
| 195 | Blow up the size of an object so as to cause its apparent payload to leak |
| 196 | into the next one. |
| 197 | """ |
| 198 | if encrypt is True: |
| 199 | # damage lowest bit of second least significant byte of size field; |
| 200 | # this effectively sets the ciphertext size to 422, causing it to |
| 201 | # extend over the next object into the third one. |
| 202 | return flip_bits (fname, crypto.HDR_OFF_CTSIZE + 1, b=0x01) |
| 203 | raise UndefinedTest ("corrupt_ctsize %s %s %s" % (fname, compress, encrypt)) |
| 204 | |
| 205 | |
| 206 | def corrupt_entire_header (_, fname, compress, encrypt): |
| 207 | """ |
| 208 | Flip all bits in the first object header. |
| 209 | """ |
| 210 | if encrypt is True: |
| 211 | flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE) |
| 212 | elif compress is True: |
| 213 | flip_bits (fname, 0, 0xff, gz_header_size (fname)) |
| 214 | else: |
| 215 | flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE) |
| 216 | |
| 217 | |
| 218 | def corrupt_payload_start (_, fname, compress, encrypt): |
| 219 | """ |
| 220 | Modify the byte following the object header structure of the format. |
| 221 | """ |
| 222 | if encrypt is True: |
| 223 | flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1) |
| 224 | elif compress is True: |
| 225 | flip_bits (fname, gz_header_size (fname) + 1) |
| 226 | else: |
| 227 | flip_bits (fname, tarfile.BLOCKSIZE + 1) |
| 228 | |
| 229 | |
| 230 | def corrupt_leading_garbage (_, fname, compress, encrypt): |
| 231 | """ |
| 232 | Prepend junk to file. |
| 233 | """ |
| 234 | aname = os.path.abspath (fname) |
| 235 | infd = os.open (fname, os.O_RDONLY) |
| 236 | size = os.lseek (infd, 0, os.SEEK_END) |
| 237 | assert os.lseek (infd, 0, os.SEEK_SET) == 0 |
| 238 | outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE, |
| 239 | stat.S_IRUSR | stat.S_IWUSR) |
| 240 | junk = os.urandom (42) |
| 241 | |
| 242 | # write new file with garbage prepended |
| 243 | done = 0 |
| 244 | os.write (outfd, junk) # junk first |
| 245 | done += len (junk) |
| 246 | while done < size: |
| 247 | data = os.read (infd, TEST_BLOCKSIZE) |
| 248 | os.write (outfd, data) |
| 249 | done += len (data) |
| 250 | |
| 251 | assert os.lseek (outfd, 0, os.SEEK_CUR) == done |
| 252 | |
| 253 | # close and free old file |
| 254 | os.close (infd) |
| 255 | os.unlink (fname) |
| 256 | |
| 257 | # install the new file in its place, atomically |
| 258 | path = "/proc/self/fd/%d" % outfd |
| 259 | os.link (path, aname, src_dir_fd=0, follow_symlinks=True) |
| 260 | os.close (outfd) |
| 261 | |
| 262 | |
| 263 | def corrupt_trailing_data (_, fname, compress, encrypt): |
| 264 | """ |
| 265 | Append random data to file. |
| 266 | """ |
| 267 | junk = os.urandom (42) |
| 268 | fd = os.open (fname, os.O_WRONLY | os.O_APPEND) |
| 269 | os.write (fd, junk) |
| 270 | os.close (fd) |
| 271 | |
| 272 | |
| 273 | def corrupt_volume (_, fname, compress, encrypt): |
| 274 | """ |
| 275 | Zero out an entire volume. |
| 276 | """ |
| 277 | fd = os.open (fname, os.O_WRONLY) |
| 278 | size = os.lseek (fd, 0, os.SEEK_END) |
| 279 | assert os.lseek (fd, 0, os.SEEK_SET) == 0 |
| 280 | zeros = bytes (b'\x00' * TEST_BLOCKSIZE) |
| 281 | while size > 0: |
| 282 | todo = min (size, TEST_BLOCKSIZE) |
| 283 | os.write (fd, zeros [:todo]) |
| 284 | size -= todo |
| 285 | os.close (fd) |
| 286 | |
| 287 | |
| 288 | def corrupt_hole (_, fname, compress, encrypt): |
| 289 | """ |
| 290 | Cut file in three pieces, reassemble without the middle one. |
| 291 | """ |
| 292 | aname = os.path.abspath (fname) |
| 293 | infd = os.open (fname, os.O_RDONLY) |
| 294 | size = os.lseek (infd, 0, os.SEEK_END) |
| 295 | assert os.lseek (infd, 0, os.SEEK_SET) == 0 |
| 296 | assert size > 3 * TEST_BLOCKSIZE |
| 297 | hole = (size / 3, size * 2 / 3) |
| 298 | outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE, |
| 299 | stat.S_IRUSR | stat.S_IWUSR) |
| 300 | |
| 301 | done = 0 |
| 302 | while done < size: |
| 303 | data = os.read (infd, TEST_BLOCKSIZE) |
| 304 | if done < hole [0] or hole [1] < done: |
| 305 | # only copy from outside hole |
| 306 | os.write (outfd, data) |
| 307 | done += len (data) |
| 308 | |
| 309 | os.close (infd) |
| 310 | os.unlink (fname) |
| 311 | |
| 312 | path = "/proc/self/fd/%d" % outfd |
| 313 | os.link (path, aname, src_dir_fd=0, follow_symlinks=True) |
| 314 | os.close (outfd) |
| 315 | |
| 316 | def immaculate (_, _fname, _compress, _encrypt): |
| 317 | """ |
| 318 | No-op dummy. |
| 319 | """ |
| 320 | pass |
| 321 | |
| 322 | ############################################################################### |
| 323 | ## tests ## |
| 324 | ############################################################################### |
| 325 | |
| 326 | class DefectiveTest (BaseTest): |
| 327 | """ |
| 328 | Disaster recovery: restore corrupt backups. |
| 329 | """ |
| 330 | |
| 331 | COMPRESSION = None |
| 332 | PASSWORD = None |
| 333 | FAILURES = 0 # files that could not be restored |
| 334 | MISMATCHES = 0 # files that were restored but corrupted |
| 335 | CORRUPT = corrupt_payload_start |
| 336 | VOLUMES = 1 |
| 337 | MISSING = None # normally the number of failures |
| 338 | |
| 339 | |
| 340 | def setUp(self): |
| 341 | ''' |
| 342 | Create base test data |
| 343 | ''' |
| 344 | self.pwd = os.getcwd() |
| 345 | self.dst_path = "source_dir" |
| 346 | self.src_path = "%s2" % self.dst_path |
| 347 | self.hash = dict() |
| 348 | |
| 349 | os.system('rm -rf target_dir source_dir* backup_dir* huge') |
| 350 | os.makedirs (self.src_path) |
| 351 | |
| 352 | for i in range (5): |
| 353 | f = "dummy_%d" % i |
| 354 | self.hash [f] = self.create_file ("%s/%s" |
| 355 | % (self.src_path, f), 5 + i) |
| 356 | |
| 357 | |
| 358 | def tearDown(self): |
| 359 | ''' |
| 360 | Remove temporal files created by unit tests and reset globals. |
| 361 | ''' |
| 362 | os.chdir(self.pwd) |
| 363 | os.system("rm -rf source_dir source_dir2 backup_dir*") |
| 364 | |
| 365 | |
| 366 | @staticmethod |
| 367 | def default_volume_name (backup_file, _x, _y, n, *a, **kwa): |
| 368 | return backup_file % n |
| 369 | |
| 370 | def gen_file_names (self, comp, pw): |
| 371 | bak_path = "backup_dir" |
| 372 | backup_file = "the_full_backup_%0.2d.tar" |
| 373 | backup_full = ("%s/%s" % (bak_path, backup_file)) % 0 |
| 374 | index_file = "the_full_index" |
| 375 | |
| 376 | if self.COMPRESSION is not None: |
| 377 | backup_file += ".gz" |
| 378 | backup_full += ".gz" |
| 379 | index_file += ".gz" |
| 380 | |
| 381 | if self.PASSWORD is not None: |
| 382 | backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION) |
| 383 | backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION) |
| 384 | index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION) |
| 385 | |
| 386 | return bak_path, backup_file, backup_full, index_file |
| 387 | |
| 388 | |
| 389 | def gen_multivol (self, nvol): |
| 390 | # add n files for one nth the volume size each, corrected |
| 391 | # for metadata and tar block overhead |
| 392 | fsiz = int ( ( TEST_VOLSIZ |
| 393 | / (TEST_FILESPERVOL * VOLUME_OVERHEAD)) |
| 394 | * 1024 * 1024) |
| 395 | fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL |
| 396 | for i in range (fcnt): |
| 397 | nvol, invol = divmod(i, TEST_FILESPERVOL) |
| 398 | f = "dummy_vol_%d_n_%0.2d" % (nvol, invol) |
| 399 | self.hash [f] = self.create_file ("%s/%s" |
| 400 | % (self.src_path, f), |
| 401 | fsiz, |
| 402 | random=True) |
| 403 | |
| 404 | |
| 405 | class RecoverTest (DefectiveTest): |
| 406 | """ |
| 407 | Recover: restore corrupt backups from index file information. |
| 408 | """ |
| 409 | |
| 410 | def test_recover_corrupt (self): |
| 411 | """ |
| 412 | Perform various damaging actions that cause unreadable objects. |
| 413 | |
| 414 | Expects the extraction to fail in normal mode. With disaster recovery, |
| 415 | extraction must succeed, and exactly one file must be missing. |
| 416 | """ |
| 417 | mode = self.COMPRESSION or "#" |
| 418 | bak_path, backup_file, backup_full, index_file = \ |
| 419 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) |
| 420 | |
| 421 | if self.VOLUMES > 1: |
| 422 | self.gen_multivol (self.VOLUMES) |
| 423 | |
| 424 | vname = partial (self.default_volume_name, backup_file) |
| 425 | dtar = deltatar.DeltaTar (mode=mode, |
| 426 | logger=None, |
| 427 | password=self.PASSWORD, |
| 428 | index_name_func=lambda _: index_file, |
| 429 | volume_name_func=vname) |
| 430 | |
| 431 | dtar.create_full_backup \ |
| 432 | (source_path=self.src_path, backup_path=bak_path, |
| 433 | max_volume_size=1) |
| 434 | |
| 435 | if self.PASSWORD is not None: |
| 436 | # ensure all files are at least superficially in PDT format |
| 437 | for f in os.listdir (bak_path): |
| 438 | assert is_pdt_encrypted ("%s/%s" % (bak_path, f)) |
| 439 | |
| 440 | # first restore must succeed |
| 441 | dtar.restore_backup(target_path=self.dst_path, |
| 442 | backup_indexes_paths=[ |
| 443 | "%s/%s" % (bak_path, index_file) |
| 444 | ], |
| 445 | disaster=tarfile.TOLERANCE_RECOVER, |
| 446 | strict_validation=False) |
| 447 | for key, value in self.hash.items (): |
| 448 | f = "%s/%s" % (self.dst_path, key) |
| 449 | assert os.path.exists (f) |
| 450 | assert value == self.md5sum (f) |
| 451 | shutil.rmtree (self.dst_path) |
| 452 | shutil.rmtree (self.src_path) |
| 453 | |
| 454 | self.CORRUPT (backup_full, |
| 455 | self.COMPRESSION is not None, |
| 456 | self.PASSWORD is not None) |
| 457 | |
| 458 | # normal restore must fail |
| 459 | try: |
| 460 | dtar.restore_backup(target_path=self.dst_path, |
| 461 | backup_tar_path=backup_full) |
| 462 | except tarfile.CompressionError: |
| 463 | if self.PASSWORD is not None or self.COMPRESSION is not None: |
| 464 | pass |
| 465 | else: |
| 466 | raise |
| 467 | except tarfile.ReadError: |
| 468 | # can happen with all three modes |
| 469 | pass |
| 470 | except tarfile.DecryptionError: |
| 471 | if self.PASSWORD is not None: |
| 472 | pass |
| 473 | else: |
| 474 | raise |
| 475 | |
| 476 | os.chdir (self.pwd) # not restored due to the error above |
| 477 | # but recover will succeed |
| 478 | failed = dtar.recover_backup(target_path=self.dst_path, |
| 479 | backup_indexes_paths=[ |
| 480 | "%s/%s" % (bak_path, index_file) |
| 481 | ]) |
| 482 | |
| 483 | assert len (failed) == self.FAILURES |
| 484 | |
| 485 | # with one file missing |
| 486 | missing = [] |
| 487 | mismatch = [] |
| 488 | for key, value in self.hash.items (): |
| 489 | kkey = "%s/%s" % (self.dst_path, key) |
| 490 | if os.path.exists (kkey): |
| 491 | if value != self.md5sum (kkey): |
| 492 | mismatch.append (key) |
| 493 | else: |
| 494 | missing.append (key) |
| 495 | |
| 496 | # usually, an object whose extraction fails will not be found on |
| 497 | # disk afterwards so the number of failures equals that of missing |
| 498 | # files. however, some modes will create partial files for objects |
| 499 | # spanning multiple volumes that contain the parts whose checksums |
| 500 | # were valid. |
| 501 | assert len (missing) == (self.MISSING if self.MISSING is not None |
| 502 | else self.FAILURES) |
| 503 | assert len (mismatch) == self.MISMATCHES |
| 504 | |
| 505 | shutil.rmtree (self.dst_path) |
| 506 | |
| 507 | |
| 508 | class RescueTest (DefectiveTest): |
| 509 | """ |
| 510 | Rescue: restore corrupt backups from backup set that is damaged to a degree |
| 511 | that the index file is worthless. |
| 512 | """ |
| 513 | |
| 514 | def test_rescue_corrupt (self): |
| 515 | """ |
| 516 | Perform various damaging actions that cause unreadable objects, then |
| 517 | attempt to extract objects regardless. |
| 518 | """ |
| 519 | mode = self.COMPRESSION or "#" |
| 520 | bak_path, backup_file, backup_full, index_file = \ |
| 521 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) |
| 522 | |
| 523 | if self.VOLUMES > 1: |
| 524 | self.gen_multivol (self.VOLUMES) |
| 525 | |
| 526 | vname = partial (self.default_volume_name, backup_file) |
| 527 | dtar = deltatar.DeltaTar (mode=mode, |
| 528 | logger=None, |
| 529 | password=self.PASSWORD, |
| 530 | index_name_func=lambda _: index_file, |
| 531 | volume_name_func=vname) |
| 532 | |
| 533 | dtar.create_full_backup \ |
| 534 | (source_path=self.src_path, backup_path=bak_path, |
| 535 | max_volume_size=1) |
| 536 | |
| 537 | if self.PASSWORD is not None: |
| 538 | # ensure all files are at least superficially in PDT format |
| 539 | for f in os.listdir (bak_path): |
| 540 | assert is_pdt_encrypted ("%s/%s" % (bak_path, f)) |
| 541 | |
| 542 | # first restore must succeed |
| 543 | dtar.restore_backup(target_path=self.dst_path, |
| 544 | backup_indexes_paths=[ |
| 545 | "%s/%s" % (bak_path, index_file) |
| 546 | ], |
| 547 | disaster=tarfile.TOLERANCE_RECOVER, |
| 548 | strict_validation=False) |
| 549 | for key, value in self.hash.items (): |
| 550 | f = "%s/%s" % (self.dst_path, key) |
| 551 | assert os.path.exists (f) |
| 552 | assert value == self.md5sum (f) |
| 553 | shutil.rmtree (self.dst_path) |
| 554 | shutil.rmtree (self.src_path) |
| 555 | |
| 556 | self.CORRUPT (backup_full, |
| 557 | self.COMPRESSION is not None, |
| 558 | self.PASSWORD is not None) |
| 559 | |
| 560 | # normal restore must fail |
| 561 | try: |
| 562 | dtar.restore_backup(target_path=self.dst_path, |
| 563 | backup_tar_path=backup_full) |
| 564 | except tarfile.CompressionError: |
| 565 | if self.PASSWORD is not None or self.COMPRESSION is not None: |
| 566 | pass |
| 567 | else: |
| 568 | raise |
| 569 | except tarfile.ReadError: |
| 570 | # can happen with all three modes |
| 571 | pass |
| 572 | except tarfile.DecryptionError: |
| 573 | if self.PASSWORD is not None: |
| 574 | pass |
| 575 | else: |
| 576 | raise |
| 577 | |
| 578 | os.chdir (self.pwd) # not restored due to the error above |
| 579 | # but recover will succeed |
| 580 | failed = dtar.rescue_backup(target_path=self.dst_path, |
| 581 | backup_tar_path=backup_full) |
| 582 | # with one file missing |
| 583 | missing = [] |
| 584 | mismatch = [] |
| 585 | for key, value in self.hash.items (): |
| 586 | kkey = "%s/%s" % (self.dst_path, key) |
| 587 | if os.path.exists (kkey): |
| 588 | if value != self.md5sum (kkey): |
| 589 | mismatch.append (key) |
| 590 | else: |
| 591 | missing.append (key) |
| 592 | |
| 593 | assert len (failed) == self.FAILURES |
| 594 | assert len (missing) == (self.MISSING if self.MISSING is not None |
| 595 | else self.FAILURES) |
| 596 | assert len (mismatch) == self.MISMATCHES |
| 597 | |
| 598 | shutil.rmtree (self.dst_path) |
| 599 | |
| 600 | |
| 601 | class GenIndexTest (DefectiveTest): |
| 602 | """ |
| 603 | Deducing an index for a backup with tarfile. |
| 604 | """ |
| 605 | |
| 606 | def test_gen_index (self): |
| 607 | """ |
| 608 | Create backup, leave it unharmed, then generate an index. |
| 609 | """ |
| 610 | mode = self.COMPRESSION or "#" |
| 611 | bak_path, backup_file, backup_full, index_file = \ |
| 612 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) |
| 613 | |
| 614 | if self.VOLUMES > 1: |
| 615 | self.gen_multivol (self.VOLUMES) |
| 616 | |
| 617 | vname = partial (self.default_volume_name, backup_file) |
| 618 | dtar = deltatar.DeltaTar (mode=mode, |
| 619 | logger=None, |
| 620 | password=self.PASSWORD, |
| 621 | index_name_func=lambda _: index_file, |
| 622 | volume_name_func=vname) |
| 623 | |
| 624 | dtar.create_full_backup \ |
| 625 | (source_path=self.src_path, backup_path=bak_path, |
| 626 | max_volume_size=1) |
| 627 | |
| 628 | def gen_volume_name (nvol): |
| 629 | return os.path.join (bak_path, vname (backup_full, True, nvol)) |
| 630 | |
| 631 | psidx = tarfile.gen_rescue_index (gen_volume_name, |
| 632 | mode, |
| 633 | password=self.PASSWORD) |
| 634 | |
| 635 | # correct for objects spanning volumes: these are treated as separate |
| 636 | # in the index! |
| 637 | assert len (psidx) - self.VOLUMES + 1 == len (self.hash) |
| 638 | |
| 639 | |
| 640 | ############################################################################### |
| 641 | # rescue |
| 642 | ############################################################################### |
| 643 | |
| 644 | class RecoverCorruptPayloadTestBase (RecoverTest): |
| 645 | COMPRESSION = None |
| 646 | PASSWORD = None |
| 647 | FAILURES = 0 # tarfile will restore but corrupted, as |
| 648 | MISMATCHES = 1 # revealed by the hash |
| 649 | |
| 650 | class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase): |
| 651 | VOLUMES = 1 |
| 652 | |
| 653 | class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase): |
| 654 | VOLUMES = 3 |
| 655 | |
| 656 | |
| 657 | class RecoverCorruptPayloadGZTestBase (RecoverTest): |
| 658 | COMPRESSION = "#gz" |
| 659 | PASSWORD = None |
| 660 | FAILURES = 1 |
| 661 | MISMATCHES = 0 |
| 662 | |
| 663 | class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase): |
| 664 | VOLUMES = 1 |
| 665 | |
| 666 | class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase): |
| 667 | VOLUMES = 3 |
| 668 | |
| 669 | |
| 670 | class RecoverCorruptPayloadGZAESTestBase (RecoverTest): |
| 671 | COMPRESSION = "#gz" |
| 672 | PASSWORD = TEST_PASSWORD |
| 673 | FAILURES = 1 |
| 674 | MISMATCHES = 0 |
| 675 | |
| 676 | class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase): |
| 677 | VOLUMES = 1 |
| 678 | |
| 679 | class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase): |
| 680 | VOLUMES = 3 |
| 681 | |
| 682 | |
| 683 | class RecoverCorruptHeaderTestBase (RecoverTest): |
| 684 | COMPRESSION = None |
| 685 | PASSWORD = None |
| 686 | FAILURES = 1 |
| 687 | CORRUPT = corrupt_header |
| 688 | MISMATCHES = 0 |
| 689 | |
| 690 | class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase): |
| 691 | VOLUMES = 1 |
| 692 | |
| 693 | class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase): |
| 694 | VOLUMES = 3 |
| 695 | |
| 696 | |
| 697 | class RecoverCorruptHeaderGZTestBase (RecoverTest): |
| 698 | COMPRESSION = "#gz" |
| 699 | PASSWORD = None |
| 700 | FAILURES = 1 |
| 701 | CORRUPT = corrupt_header |
| 702 | MISMATCHES = 0 |
| 703 | |
| 704 | class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase): |
| 705 | VOLUMES = 1 |
| 706 | |
| 707 | class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase): |
| 708 | VOLUMES = 3 |
| 709 | |
| 710 | |
| 711 | class RecoverCorruptHeaderGZAESTestBase (RecoverTest): |
| 712 | COMPRESSION = "#gz" |
| 713 | PASSWORD = TEST_PASSWORD |
| 714 | FAILURES = 1 |
| 715 | CORRUPT = corrupt_header |
| 716 | MISMATCHES = 0 |
| 717 | |
| 718 | class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase): |
| 719 | VOLUMES = 1 |
| 720 | |
| 721 | class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase): |
| 722 | VOLUMES = 3 |
| 723 | |
| 724 | |
| 725 | class RecoverCorruptTruncateTestBase (RecoverTest): |
| 726 | COMPRESSION = None |
| 727 | PASSWORD = None |
| 728 | FAILURES = 0 |
| 729 | CORRUPT = corrupt_truncate |
| 730 | MISMATCHES = 0 |
| 731 | |
| 732 | class RecoverCorruptTruncateTest (RecoverCorruptTruncateTestBase): |
| 733 | pass |
| 734 | |
| 735 | class RecoverCorruptTruncateGZTest (RecoverCorruptTruncateTestBase): |
| 736 | """Two files that failed missing.""" |
| 737 | COMPRESSION = "#gz" |
| 738 | FAILURES = 2 |
| 739 | |
| 740 | class RecoverCorruptTruncateGZAESTest (RecoverCorruptTruncateTestBase): |
| 741 | """Two files that failed missing.""" |
| 742 | COMPRESSION = "#gz" |
| 743 | PASSWORD = TEST_PASSWORD |
| 744 | FAILURES = 2 |
| 745 | |
| 746 | |
| 747 | class RecoverCorruptEntireHeaderTestBase (RecoverTest): |
| 748 | COMPRESSION = None |
| 749 | PASSWORD = None |
| 750 | FAILURES = 1 |
| 751 | CORRUPT = corrupt_entire_header |
| 752 | MISMATCHES = 0 |
| 753 | |
| 754 | class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase): |
| 755 | VOLUMES = 1 |
| 756 | |
| 757 | class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase): |
| 758 | VOLUMES = 3 |
| 759 | |
| 760 | |
| 761 | class RecoverCorruptEntireHeaderGZTestBase (RecoverTest): |
| 762 | COMPRESSION = "#gz" |
| 763 | PASSWORD = None |
| 764 | FAILURES = 1 |
| 765 | CORRUPT = corrupt_entire_header |
| 766 | MISMATCHES = 0 |
| 767 | |
| 768 | class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase): |
| 769 | VOLUMES = 1 |
| 770 | |
| 771 | class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase): |
| 772 | VOLUMES = 3 |
| 773 | |
| 774 | |
| 775 | class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest): |
| 776 | COMPRESSION = "#gz" |
| 777 | PASSWORD = TEST_PASSWORD |
| 778 | FAILURES = 1 |
| 779 | CORRUPT = corrupt_entire_header |
| 780 | MISMATCHES = 0 |
| 781 | |
| 782 | class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase): |
| 783 | VOLUMES = 1 |
| 784 | |
| 785 | class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase): |
| 786 | VOLUMES = 3 |
| 787 | |
| 788 | |
| 789 | class RecoverCorruptTrailingDataTestBase (RecoverTest): |
| 790 | # plain Tar is indifferent against traling data and the results |
| 791 | # are consistent |
| 792 | COMPRESSION = None |
| 793 | PASSWORD = None |
| 794 | FAILURES = 0 |
| 795 | CORRUPT = corrupt_trailing_data |
| 796 | MISMATCHES = 0 |
| 797 | |
| 798 | class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase): |
| 799 | VOLUMES = 1 |
| 800 | |
| 801 | class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase): |
| 802 | # the last object in first archive has extra bytes somewhere in the |
| 803 | # middle because tar itself performs no data checksumming. |
| 804 | MISMATCHES = 1 |
| 805 | VOLUMES = 3 |
| 806 | |
| 807 | |
| 808 | class RecoverCorruptTrailingDataGZTestBase (RecoverTest): |
| 809 | # reading past the final object will cause decompression failure; |
| 810 | # all objects except for the last survive unharmed though |
| 811 | COMPRESSION = "#gz" |
| 812 | PASSWORD = None |
| 813 | FAILURES = 1 |
| 814 | CORRUPT = corrupt_trailing_data |
| 815 | MISMATCHES = 0 |
| 816 | |
| 817 | class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase): |
| 818 | VOLUMES = 1 |
| 819 | |
| 820 | class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase): |
| 821 | VOLUMES = 3 |
| 822 | # the last file of the first volume will only contain the data of the |
| 823 | # second part which is contained in the second volume. this happens |
| 824 | # because the CRC32 is wrong for the first part so it gets discarded, then |
| 825 | # the object is recreated from the first header of the second volume, |
| 826 | # containing only the remainder of the data. |
| 827 | MISMATCHES = 1 |
| 828 | MISSING = 0 |
| 829 | |
| 830 | |
| 831 | class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest): |
| 832 | COMPRESSION = "#gz" |
| 833 | PASSWORD = TEST_PASSWORD |
| 834 | FAILURES = 0 |
| 835 | CORRUPT = corrupt_trailing_data |
| 836 | MISMATCHES = 0 |
| 837 | |
| 838 | class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase): |
| 839 | VOLUMES = 1 |
| 840 | |
| 841 | class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase): |
| 842 | VOLUMES = 3 |
| 843 | |
| 844 | |
| 845 | class RecoverCorruptVolumeBaseTest (RecoverTest): |
| 846 | COMPRESSION = None |
| 847 | PASSWORD = None |
| 848 | FAILURES = 8 |
| 849 | CORRUPT = corrupt_volume |
| 850 | VOLUMES = 3 |
| 851 | |
| 852 | class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest): |
| 853 | pass |
| 854 | |
| 855 | class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest): |
| 856 | COMPRESSION = "#gz" |
| 857 | |
| 858 | class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest): |
| 859 | COMPRESSION = "#gz" |
| 860 | PASSWORD = TEST_PASSWORD |
| 861 | |
| 862 | |
| 863 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 864 | class RecoverCorruptHoleBaseTest (RecoverTest): |
| 865 | """ |
| 866 | Cut bytes from the middle of a volume. |
| 867 | |
| 868 | Index-based recovery works only up to the hole. |
| 869 | """ |
| 870 | COMPRESSION = None |
| 871 | PASSWORD = None |
| 872 | FAILURES = 3 |
| 873 | CORRUPT = corrupt_hole |
| 874 | VOLUMES = 2 # request two vols to swell up the first one |
| 875 | MISMATCHES = 1 |
| 876 | |
| 877 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 878 | class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest): |
| 879 | FAILURES = 2 |
| 880 | |
| 881 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 882 | class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest): |
| 883 | COMPRESSION = "#gz" |
| 884 | MISSING = 2 |
| 885 | |
| 886 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 887 | class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest): |
| 888 | COMPRESSION = "#gz" |
| 889 | PASSWORD = TEST_PASSWORD |
| 890 | MISSING = 2 |
| 891 | |
| 892 | ############################################################################### |
| 893 | # rescue |
| 894 | ############################################################################### |
| 895 | |
| 896 | class RescueCorruptTruncateTestBase (RescueTest): |
| 897 | COMPRESSION = None |
| 898 | PASSWORD = None |
| 899 | FAILURES = 0 |
| 900 | CORRUPT = corrupt_truncate |
| 901 | MISMATCHES = 0 |
| 902 | |
| 903 | class RescueCorruptTruncateTest (RescueCorruptTruncateTestBase): |
| 904 | pass |
| 905 | |
| 906 | class RescueCorruptTruncateGZTest (RescueCorruptTruncateTestBase): |
| 907 | """Two files that failed missing.""" |
| 908 | COMPRESSION = "#gz" |
| 909 | MISSING = 2 |
| 910 | |
| 911 | class RescueCorruptTruncateGZAESTest (RescueCorruptTruncateTestBase): |
| 912 | """Two files missing but didn’t fail on account of their absence.""" |
| 913 | COMPRESSION = "#gz" |
| 914 | PASSWORD = TEST_PASSWORD |
| 915 | MISSING = 2 |
| 916 | |
| 917 | |
| 918 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 919 | class RescueCorruptHoleBaseTest (RescueTest): |
| 920 | """ |
| 921 | Cut bytes from the middle of a volume. |
| 922 | """ |
| 923 | COMPRESSION = None |
| 924 | PASSWORD = None |
| 925 | FAILURES = 0 |
| 926 | CORRUPT = corrupt_hole |
| 927 | VOLUMES = 2 # request two vols to swell up the first one |
| 928 | MISMATCHES = 2 # intersected by hole |
| 929 | MISSING = 1 # excised by hole |
| 930 | |
| 931 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 932 | class RescueCorruptHoleTest (RescueCorruptHoleBaseTest): |
| 933 | pass |
| 934 | |
| 935 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 936 | class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest): |
| 937 | COMPRESSION = "#gz" |
| 938 | # the decompressor explodes in our face processing the first dummy, nothing |
| 939 | # we can do to recover |
| 940 | FAILURES = 1 |
| 941 | |
| 942 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 943 | class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest): |
| 944 | COMPRESSION = "#gz" |
| 945 | PASSWORD = TEST_PASSWORD |
| 946 | # again, ignoring the crypto errors yields a bad zlib stream causing the |
| 947 | # decompressor to abort where the hole begins; the file is extracted up |
| 948 | # to this point though |
| 949 | FAILURES = 1 |
| 950 | |
| 951 | |
| 952 | class RescueCorruptHeaderCTSizeGZAESTest (RescueTest): |
| 953 | COMPRESSION = "#gz" |
| 954 | PASSWORD = TEST_PASSWORD |
| 955 | FAILURES = 0 |
| 956 | CORRUPT = corrupt_ctsize |
| 957 | MISMATCHES = 0 |
| 958 | |
| 959 | |
| 960 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 961 | class RescueCorruptLeadingGarbageTestBase (RescueTest): |
| 962 | # plain Tar is indifferent against traling data and the results |
| 963 | # are consistent |
| 964 | COMPRESSION = None |
| 965 | PASSWORD = None |
| 966 | FAILURES = 0 |
| 967 | CORRUPT = corrupt_leading_garbage |
| 968 | MISMATCHES = 0 |
| 969 | |
| 970 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 971 | class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase): |
| 972 | VOLUMES = 1 |
| 973 | |
| 974 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
| 975 | class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase): |
| 976 | # the last object in first archive has extra bytes somewhere in the |
| 977 | # middle because tar itself performs no data checksumming. |
| 978 | MISMATCHES = 2 |
| 979 | VOLUMES = 3 |
| 980 | |
| 981 | |
| 982 | ############################################################################### |
| 983 | # index |
| 984 | ############################################################################### |
| 985 | |
| 986 | class GenIndexIntactBaseTest (GenIndexTest): |
| 987 | """ |
| 988 | """ |
| 989 | COMPRESSION = None |
| 990 | PASSWORD = None |
| 991 | FAILURES = 0 |
| 992 | CORRUPT = immaculate |
| 993 | VOLUMES = 1 |
| 994 | MISMATCHES = 1 |
| 995 | |
| 996 | class GenIndexIntactSingleTest (GenIndexIntactBaseTest): |
| 997 | pass |
| 998 | |
| 999 | class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest): |
| 1000 | COMPRESSION = "#gz" |
| 1001 | MISSING = 2 |
| 1002 | |
| 1003 | class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest): |
| 1004 | COMPRESSION = "#gz" |
| 1005 | PASSWORD = TEST_PASSWORD |
| 1006 | MISSING = 2 |
| 1007 | |
| 1008 | class GenIndexIntactMultiTest (GenIndexIntactBaseTest): |
| 1009 | VOLUMES = 3 |
| 1010 | pass |
| 1011 | |
| 1012 | class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest): |
| 1013 | VOLUMES = 3 |
| 1014 | COMPRESSION = "#gz" |
| 1015 | MISSING = 2 |
| 1016 | |
| 1017 | class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest): |
| 1018 | VOLUMES = 3 |
| 1019 | COMPRESSION = "#gz" |
| 1020 | PASSWORD = TEST_PASSWORD |
| 1021 | MISSING = 2 |
| 1022 | |
| 1023 | |
| 1024 | class GenIndexCorruptTruncateBaseTest (GenIndexTest): |
| 1025 | """ |
| 1026 | Recreate index from file that lacks the latter portion. |
| 1027 | """ |
| 1028 | COMPRESSION = None |
| 1029 | PASSWORD = None |
| 1030 | FAILURES = 0 |
| 1031 | CORRUPT = corrupt_truncate |
| 1032 | MISSING = 2 |
| 1033 | |
| 1034 | class GenIndexCorruptTruncateTest (GenIndexCorruptTruncateBaseTest): |
| 1035 | pass |
| 1036 | |
| 1037 | class GenIndexCorruptTruncateGZTest (GenIndexCorruptTruncateBaseTest): |
| 1038 | COMPRESSION = "#gz" |
| 1039 | |
| 1040 | class GenIndexCorruptTruncateGZAESTest (GenIndexCorruptTruncateBaseTest): |
| 1041 | COMPRESSION = "#gz" |
| 1042 | PASSWORD = TEST_PASSWORD |
| 1043 | |
| 1044 | |
| 1045 | class GenIndexCorruptHoleBaseTest (GenIndexTest): |
| 1046 | """ |
| 1047 | Recreate index from file with hole. |
| 1048 | """ |
| 1049 | COMPRESSION = None |
| 1050 | PASSWORD = None |
| 1051 | FAILURES = 0 |
| 1052 | CORRUPT = corrupt_hole |
| 1053 | VOLUMES = 1 |
| 1054 | MISMATCHES = 1 |
| 1055 | |
| 1056 | class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest): |
| 1057 | pass |
| 1058 | |
| 1059 | class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest): |
| 1060 | COMPRESSION = "#gz" |
| 1061 | MISSING = 2 |
| 1062 | |
| 1063 | class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest): |
| 1064 | COMPRESSION = "#gz" |
| 1065 | PASSWORD = TEST_PASSWORD |
| 1066 | MISSING = 2 |
| 1067 | |
| 1068 | |
| 1069 | class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest): |
| 1070 | """ |
| 1071 | Recreate index from file with defective headers. |
| 1072 | """ |
| 1073 | COMPRESSION = None |
| 1074 | PASSWORD = None |
| 1075 | FAILURES = 0 |
| 1076 | CORRUPT = corrupt_entire_header |
| 1077 | VOLUMES = 1 |
| 1078 | MISMATCHES = 1 |
| 1079 | |
| 1080 | class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest): |
| 1081 | pass |
| 1082 | |
| 1083 | class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest): |
| 1084 | COMPRESSION = "#gz" |
| 1085 | MISSING = 2 |
| 1086 | |
| 1087 | class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest): |
| 1088 | COMPRESSION = "#gz" |
| 1089 | PASSWORD = TEST_PASSWORD |
| 1090 | MISSING = 2 |
| 1091 | |