1# 2# Run a variety of tests against fsck_msdos 3# 4# Usage: 5# python test_fsck.py [<fsck_msdos> [<tmp_dir>]] 6# 7# where <tmp_dir> is a path to a directory where disk images will be 8# temporarily created. If <path_to_fsck> is specified, it is used instead 9# of 'fsck_msdos' to invoke the fsck_msdos program (for example, to test 10# a new build that has not been installed). 11# 12 13from __future__ import with_statement 14 15import sys 16import os 17import subprocess 18import struct 19from msdosfs import * 20from HexDump import HexDump 21 22class LaunchError(Exception): 23 def __init__(self, returncode): 24 self.returncode = returncode 25 if returncode < 0: 26 self.message = "Program exited with signal %d" % -returncode 27 else: 28 self.message = "Program exited with status %d" % returncode 29 30 def __str__(self): 31 return self.message 32 33class FailureExpected(Exception): 34 def __init__(self, s): 35 self.s = s 36 def __str__(self): 37 return self.s 38 39# 40# launch -- A helper to run another process and collect the standard output 41# and standard error streams. If the process returns a non-zero exit 42# status, then raise an exception. 43# 44def launch(args, **kwargs): 45 print "launch:", args, kwargs 46 p = subprocess.Popen(args, **kwargs) 47 stdout, stderr = p.communicate() 48 if p.returncode != 0: 49 raise LaunchError(p.returncode) 50 return stdout, stderr 51 52# 53# 1. Make a disk image file 54# 2. Attach the image file, without mounting 55# ---- Begin per-test stuff ---- 56# 3. newfs_msdos the image 57# 4. Fill image with content 58# 5. fsck_msdos -n the image 59# 6. fsck_msdos -y the image 60# 7. Run /sbin/fsck_msdos against image 61# ---- End per-test stuff ---- 62# 8. Detach the image 63# 9. Delete the image file 64# 65 66# 67# Run tests on 20GiB FAT32 sparse disk image 68# 69def test_fat32(dir, fsck, newfs): 70 # 71 # Create a 20GB disk image in @dir 72 # 73 dmg = os.path.join(dir, 'Test20GB.sparseimage') 74 launch('hdiutil create -size 20g -type SPARSE -layout NONE'.split()+[dmg]) 75 newfs_opts = "-F 32 -b 4096 -v TEST20GB".split() 76 77 # 78 # Attach the image 79 # 80 disk = launch(['hdiutil', 'attach', '-nomount', dmg], stdout=subprocess.PIPE)[0].rstrip() 81 rdisk = disk.replace('/dev/disk', '/dev/rdisk') 82 83 # 84 # Run tests 85 # 86 # TODO: Known good disk 87 # empty file 88 # one cluster file 89 # larger file 90 # one cluster directory 91 # larger directory 92 # 93 test_quick(rdisk, fsck, newfs, newfs_opts) 94 test_bad_args(rdisk, fsck, newfs, newfs_opts) 95 test_maxmem(rdisk, fsck, newfs, newfs_opts) 96 test_empty(rdisk, fsck, newfs, newfs_opts) 97 test_boot_sector(rdisk, fsck, newfs, newfs_opts) 98 test_boot_fat32(rdisk, fsck, newfs, newfs_opts) # FAT32 only! 99 test_fsinfo(rdisk, fsck, newfs, newfs_opts) # FAT32 only! 100 fat_too_small(rdisk, fsck, newfs, newfs_opts) 101 orphan_clusters(rdisk, fsck, newfs, newfs_opts) 102 file_excess_clusters(rdisk, fsck, newfs, newfs_opts) 103 file_bad_clusters(rdisk, fsck, newfs, newfs_opts) 104 dir_bad_start(rdisk, fsck, newfs, newfs_opts) 105 root_bad_start(rdisk, fsck, newfs, newfs_opts) # FAT32 only! 106 root_bad_first_cluster(rdisk, fsck, newfs, newfs_opts) # FAT32 only! 107 dir_size_dots(rdisk, fsck, newfs, newfs_opts) 108 long_name(rdisk, fsck, newfs, newfs_opts) 109 past_end_of_dir(rdisk, fsck, newfs, newfs_opts) 110 past_end_of_dir(rdisk, fsck, newfs, newfs_opts, True) 111 fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) 112 fat_mark_clean_corrupt(rdisk, fsck, newfs, newfs_opts) 113 fat_mark_clean_ok(rdisk, fsck, newfs, newfs_opts) 114 file_4GB(rdisk, fsck, newfs, newfs_opts) 115 file_4GB_excess_clusters(rdisk, fsck, newfs, newfs_opts) 116 directory_garbage(rdisk, fsck, newfs, newfs_opts) 117 118 # 119 # Detach the image 120 # 121 launch(['diskutil', 'eject', disk]) 122 123 # 124 # Delete the image file 125 # 126 os.remove(dmg) 127 128# 129# Run tests on 160MiB FAT16 image 130# 131def test_fat16(dir, fsck, newfs): 132 # 133 # Create a 160MB disk image in @dir 134 # 135 dmg = os.path.join(dir, 'Test160MB.dmg') 136 f = file(dmg, "w") 137 f.truncate(160*1024*1024) 138 f.close 139 newfs_opts = "-F 16 -b 4096 -v TEST160MB".split() 140 141 # 142 # Attach the image 143 # 144 disk = launch(['hdiutil', 'attach', '-nomount', dmg], stdout=subprocess.PIPE)[0].rstrip() 145 rdisk = disk.replace('/dev/disk', '/dev/rdisk') 146 147 # 148 # Run tests 149 # 150 # TODO: Known good disk 151 # empty file 152 # one cluster file 153 # larger file 154 # one cluster directory 155 # larger directory 156 # 157 test_quick(rdisk, fsck, newfs, newfs_opts) 158 test_bad_args(rdisk, fsck, newfs, newfs_opts) 159 test_maxmem(rdisk, fsck, newfs, newfs_opts) 160 test_empty(rdisk, fsck, newfs, newfs_opts) 161 test_boot_sector(rdisk, fsck, newfs, newfs_opts) 162 fat_too_small(rdisk, fsck, newfs, newfs_opts) 163 orphan_clusters(rdisk, fsck, newfs, newfs_opts) 164 file_excess_clusters(rdisk, fsck, newfs, newfs_opts) 165 file_bad_clusters(rdisk, fsck, newfs, newfs_opts) 166 dir_bad_start(rdisk, fsck, newfs, newfs_opts) 167 dir_size_dots(rdisk, fsck, newfs, newfs_opts) 168 long_name(rdisk, fsck, newfs, newfs_opts) 169 past_end_of_dir(rdisk, fsck, newfs, newfs_opts) 170 past_end_of_dir(rdisk, fsck, newfs, newfs_opts, True) 171 fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) 172 fat_mark_clean_corrupt(rdisk, fsck, newfs, newfs_opts) 173 fat_mark_clean_ok(rdisk, fsck, newfs, newfs_opts) 174 directory_garbage(rdisk, fsck, newfs, newfs_opts) 175 176 # 177 # Detach the image 178 # 179 launch(['diskutil', 'eject', disk]) 180 181 # 182 # Delete the image file 183 # 184 os.remove(dmg) 185 186# 187# Run tests on 15MiB FAT12 image 188# 189def test_fat12(dir, fsck, newfs): 190 # 191 # Create a 15MB disk image in @dir 192 # 193 dmg = os.path.join(dir, 'Test15MB.dmg') 194 f = file(dmg, "w") 195 f.truncate(15*1024*1024) 196 f.close 197 newfs_opts = "-F 12 -b 4096 -v TEST15MB".split() 198 199 # 200 # Attach the image 201 # 202 disk = launch(['hdiutil', 'attach', '-nomount', dmg], stdout=subprocess.PIPE)[0].rstrip() 203 rdisk = disk.replace('/dev/disk', '/dev/rdisk') 204 205 # 206 # Run tests 207 # 208 # TODO: Known good disk 209 # empty file 210 # one cluster file 211 # larger file 212 # one cluster directory 213 # larger directory 214 # 215 test_quick(rdisk, fsck, newfs, newfs_opts) 216 test_bad_args(rdisk, fsck, newfs, newfs_opts) 217 test_maxmem(rdisk, fsck, newfs, newfs_opts) 218 test_empty(rdisk, fsck, newfs, newfs_opts) 219 test_boot_sector(rdisk, fsck, newfs, newfs_opts) 220 fat_too_small(rdisk, fsck, newfs, newfs_opts) 221 orphan_clusters(rdisk, fsck, newfs, newfs_opts) 222 file_excess_clusters(rdisk, fsck, newfs, newfs_opts) 223 file_bad_clusters(rdisk, fsck, newfs, newfs_opts) 224 dir_bad_start(rdisk, fsck, newfs, newfs_opts) 225 dir_size_dots(rdisk, fsck, newfs, newfs_opts) 226 long_name(rdisk, fsck, newfs, newfs_opts) 227 past_end_of_dir(rdisk, fsck, newfs, newfs_opts) 228 past_end_of_dir(rdisk, fsck, newfs, newfs_opts, True) 229 fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) 230 directory_garbage(rdisk, fsck, newfs, newfs_opts) 231 232 # 233 # Detach the image 234 # 235 launch(['diskutil', 'eject', disk]) 236 237 # 238 # Delete the image file 239 # 240 os.remove(dmg) 241 242# 243# A minimal test -- make sure fsck_msdos runs on an empty image 244# 245def test_empty(disk, fsck, newfs, newfs_opts): 246 # 247 # newfs the disk 248 # 249 launch([newfs]+newfs_opts+[disk]) 250 251 # 252 # fsck the disk 253 # 254 launch([fsck, '-n', disk]) 255 256# 257# Make a volume with allocated but unreferenced cluster chains 258# 259def orphan_clusters(disk, fsck, newfs, newfs_opts): 260 launch([newfs]+newfs_opts+[disk]) 261 262 # 263 # Create some cluster chains not referenced by any file or directory 264 # 265 f = file(disk, "r+") 266 v = msdosfs(f) 267 v.allocate(7, 100) 268 v.allocate(23, 150) 269 v.allocate(1, 190) 270 v.flush() 271 del v 272 f.close() 273 del f 274 275 try: 276 launch([fsck, '-n', disk]) 277 except LaunchError: 278 pass 279 launch([fsck, '-p', disk]) 280 launch(['/sbin/fsck_msdos', '-n', disk]) 281 282# 283# Make a file with excess clusters allocated 284# One file with EOF == 0 285# One file with EOF != 0 286# Files with excess clusters that are cross-linked 287# First excess cluster is cross-linked 288# Other excess cluster is cross-linked 289# Excess clusters end with free/bad/reserved cluster 290# First excess cluster is free/bad/reserved 291# Other excess cluster is free/bad/reserved 292# 293def file_excess_clusters(disk, fsck, newfs, newfs_opts): 294 launch([newfs]+newfs_opts+[disk]) 295 296 # 297 # Create files with too many clusters for their size 298 # 299 f = file(disk, "r+") 300 v = msdosfs(f) 301 head=v.allocate(7) 302 v.root().mkfile('FOO', head=head, length=6*v.bytesPerCluster) 303 head=v.allocate(1) 304 v.root().mkfile('BAR', head=head, length=0) 305 306 # 307 # LINK1 is OK. 308 # LINK2 contains excess clusters; the first is cross-linked with LINK1 309 # LINK3 contains excess clusters; the second is cross-linked with LINK1 310 # 311 clusters = v.fat.find(9) 312 head = v.fat.chain(clusters) 313 v.root().mkfile('LINK1', head=head, length=8*v.bytesPerCluster+1) 314 head = v.fat.allocate(3, last=clusters[7]) 315 v.root().mkfile('LINK2', head=head, length=2*v.bytesPerCluster+3) 316 head = v.fat.allocate(5, last=clusters[8]) 317 v.root().mkfile('LINK3', head=head, length=3*v.bytesPerCluster+5) 318 if v.fsinfo: 319 v.fsinfo.allocate(9+3+5) 320 321 # 322 # FREE1 has its first excess cluster marked free 323 # BAD3 has its third excess cluster marked bad 324 # 325 head = v.allocate(11, last=CLUST_BAD) 326 v.root().mkfile('BAD3', head=head, length=8*v.bytesPerCluster+300) 327 head = v.allocate(8, last=CLUST_FREE) 328 v.root().mkfile('FREE1', head=head, length=6*v.bytesPerCluster+100) 329 330 v.flush() 331 del v 332 f.close() 333 del f 334 335 try: 336 launch([fsck, '-n', disk]) 337 except LaunchError: 338 pass 339 launch([fsck, '-y', disk]) 340 launch(['/sbin/fsck_msdos', '-n', disk]) 341 342# 343# Make files with bad clusters in their chains 344# FILE1 file with middle cluster free 345# FILE2 file with middle cluster bad/reserved 346# FILE3 file with middle cluster points to out of range cluster 347# FILE4 file with middle cluster that is cross-linked (to same file) 348# FILE5 file whose head is "free" 349# FILE6 file whose head is "bad" 350# FILE7 file whose head is out of range 351# FILE8 file whose head is cross-linked 352# 353def file_bad_clusters(disk, fsck, newfs, newfs_opts): 354 launch([newfs]+newfs_opts+[disk]) 355 356 f = file(disk, "r+") 357 v = msdosfs(f) 358 359 clusters = v.fat.find(5) 360 to_free = clusters[2] 361 head = v.fat.chain(clusters) 362 v.root().mkfile('FILE1', head=head, length=6*v.bytesPerCluster+111) 363 if v.fsinfo: 364 v.fsinfo.allocate(5) 365 366 clusters = v.fat.find(5) 367 head = v.fat.chain(clusters) 368 v.root().mkfile('FILE2', head=head, length=4*v.bytesPerCluster+222) 369 v.fat[clusters[2]] = CLUST_RSRVD 370 if v.fsinfo: 371 v.fsinfo.allocate(5) 372 373 clusters = v.fat.find(5) 374 head = v.fat.chain(clusters) 375 v.root().mkfile('FILE3', head=head, length=4*v.bytesPerCluster+333) 376 v.fat[clusters[2]] = 1 377 if v.fsinfo: 378 v.fsinfo.allocate(5) 379 380 clusters = v.fat.find(5) 381 head = v.fat.chain(clusters) 382 v.root().mkfile('FILE4', head=head, length=4*v.bytesPerCluster+44) 383 v.fat[clusters[2]] = clusters[1] 384 if v.fsinfo: 385 v.fsinfo.allocate(5) 386 387 v.root().mkfile('FILE5', head=CLUST_FREE, length=4*v.bytesPerCluster+55) 388 389 v.root().mkfile('FILE6', head=CLUST_BAD, length=4*v.bytesPerCluster+66) 390 391 v.root().mkfile('FILE7', head=CLUST_RSRVD-1, length=4*v.bytesPerCluster+77) 392 393 head = v.allocate(5) 394 v.root().mkfile('FOO', head=head, length=4*v.bytesPerCluster+99) 395 v.root().mkfile('FILE8', head=head, length=4*v.bytesPerCluster+88) 396 397 # Free the middle cluster of FILE1 now that we've finished allocating 398 v.fat[to_free] = CLUST_FREE 399 400 v.flush() 401 del v 402 f.close() 403 del f 404 405 try: 406 launch([fsck, '-n', disk]) 407 except LaunchError: 408 pass 409 launch([fsck, '-y', disk]) 410 launch(['/sbin/fsck_msdos', '-n', disk]) 411 412# 413# Make directories whose starting cluster number is free/bad/reserved/out of range 414# DIR1 start cluster is free 415# DIR2 start cluster is reserved 416# DIR3 start cluster is bad 417# DIR4 start cluster is EOF 418# DIR5 start cluster is 1 419# DIR6 start cluster is one more than max valid cluster 420# 421def dir_bad_start(disk, fsck, newfs, newfs_opts): 422 def mkdir(parent, name, head): 423 bytes = make_long_dirent(name, ATTR_DIRECTORY, head=head) 424 slots = len(bytes)/32 425 slot = parent.find_slots(slots, grow=True) 426 parent.write_slots(slot, bytes) 427 428 launch([newfs]+newfs_opts+[disk]) 429 430 f = file(disk, "r+") 431 v = msdosfs(f) 432 root = v.root() 433 434 mkdir(root, 'DIR1', CLUST_FREE) 435 mkdir(root, 'DIR2', CLUST_RSRVD) 436 mkdir(root, 'DIR3', CLUST_BAD) 437 mkdir(root, 'DIR4', CLUST_EOF) 438 mkdir(root, 'DIR5', 1) 439 mkdir(root, 'DIR6', v.clusters+2) 440 441 v.flush() 442 del v 443 f.close() 444 del f 445 446 try: 447 launch([fsck, '-n', disk]) 448 except LaunchError: 449 pass 450 launch([fsck, '-y', disk]) 451 launch(['/sbin/fsck_msdos', '-n', disk]) 452 453# 454# Root dir's starting cluster number is free/bad/reserved/out of range 455# 456# NOTE: This test is only applicable to FAT32! 457# 458def root_bad_start(disk, fsck, newfs, newfs_opts): 459 def set_root_start(disk, head): 460 dev = file(disk, "r+") 461 dev.seek(0) 462 bytes = dev.read(512) 463 bytes = bytes[0:44] + struct.pack("<I", head) + bytes[48:] 464 dev.seek(0) 465 dev.write(bytes) 466 dev.close() 467 del dev 468 469 launch([newfs]+newfs_opts+[disk]) 470 471 f = file(disk, "r+") 472 v = msdosfs(f) 473 clusters = v.clusters 474 v.flush() 475 del v 476 f.close() 477 del f 478 479 for head in [CLUST_FREE, CLUST_RSRVD, CLUST_BAD, CLUST_EOF, 1, clusters+2]: 480 set_root_start(disk, head) 481 482 try: 483 launch([fsck, '-n', disk]) 484 except LaunchError: 485 pass 486 try: 487 launch([fsck, '-y', disk]) 488 except LaunchError: 489 pass 490 try: 491 launch(['/sbin/fsck_msdos', '-n', disk]) 492 except LaunchError: 493 pass 494 495# 496# Root dir's first cluster is free/bad/reserved 497# 498# NOTE: This test is only applicable to FAT32! 499# 500def root_bad_first_cluster(disk, fsck, newfs, newfs_opts): 501 for link in [CLUST_FREE, CLUST_RSRVD, CLUST_BAD]: 502 launch([newfs]+newfs_opts+[disk]) 503 504 f = file(disk, "r+") 505 v = msdosfs(f) 506 v.fat[v.rootCluster] = link 507 v.flush() 508 del v 509 f.close() 510 del f 511 512 try: 513 launch([fsck, '-n', disk]) 514 except LaunchError: 515 pass 516 launch([fsck, '-y', disk]) 517 launch(['/sbin/fsck_msdos', '-n', disk]) 518 519# 520# Create subdirectories with the following problems: 521# Size (length) field is non-zero 522# "." entry has wrong starting cluster 523# ".." entry start cluster is non-zero, and parent is root 524# ".." entry start cluster is zero, and parent is not root 525# ".." entry start cluster is incorrect 526# 527def dir_size_dots(disk, fsck, newfs, newfs_opts): 528 launch([newfs]+newfs_opts+[disk]) 529 530 f = file(disk, "r+") 531 v = msdosfs(f) 532 root = v.root() 533 534 # Make a couple of directories without any problems 535 child = root.mkdir('CHILD') 536 grand = child.mkdir('GRAND') 537 538 # Directory has non-zero size 539 dir = root.mkdir('BADSIZE', length=666) 540 541 # "." entry has incorrect start cluster 542 dir = root.mkdir('BADDOT') 543 fields = parse_dirent(dir.read_slots(0)) 544 fields['head'] = fields['head'] + 30 545 dir.write_slots(0, make_dirent(**fields)) 546 547 # ".." entry has non-zero start cluster, but parent is root 548 dir = root.mkdir('DOTDOT.NZ') 549 fields = parse_dirent(dir.read_slots(0)) 550 fields['head'] = 47 551 dir.write_slots(0, make_dirent(**fields)) 552 553 # ".." entry has zero start cluster, but parent is not root 554 dir = child.mkdir('DOTDOT.ZER') 555 fields = parse_dirent(dir.read_slots(0)) 556 fields['head'] = 0 557 dir.write_slots(0, make_dirent(**fields)) 558 559 # ".." entry start cluster is incorrect (parent is not root) 560 dir = grand.mkdir('DOTDOT.BAD') 561 fields = parse_dirent(dir.read_slots(0)) 562 fields['head'] = fields['head'] + 30 563 dir.write_slots(0, make_dirent(**fields)) 564 565 v.flush() 566 del v 567 f.close() 568 del f 569 570 try: 571 launch([fsck, '-n', disk]) 572 except LaunchError: 573 pass 574 launch([fsck, '-y', disk]) 575 launch(['/sbin/fsck_msdos', '-n', disk]) 576 577def long_name(disk, fsck, newfs, newfs_opts): 578 launch([newfs]+newfs_opts+[disk]) 579 580 f = file(disk, "r+") 581 v = msdosfs(f) 582 root = v.root() 583 584 # Long name entries (valid or not!) preceding volume label 585 bytes = make_long_dirent('Test1GB', ATTR_VOLUME_ID) 586 root.write_slots(0, bytes) 587 588 # Create a file with a known good long name 589 root.mkfile('The quick brown fox jumped over the lazy dog') 590 591 # Create a file with a known good short name 592 root.mkfile('foo.bar') 593 594 # Create a file with invalid long name entries (bad checksums) 595 bytes = make_long_dirent('Greetings and felicitations my friends', ATTR_ARCHIVE) 596 bytes = bytes[0:-32] + 'HELLO ' + bytes[-21:] 597 assert len(bytes) % 32 == 0 598 slots = len(bytes) / 32 599 slot = root.find_slots(slots) 600 root.write_slots(slot, bytes) 601 602 subdir = root.mkdir('SubDir') 603 604 # Create a file with incomplete long name entries 605 # Missing first (LONG_NAME_LAST) entry 606 bytes = make_long_dirent('To be or not to be', ATTR_ARCHIVE)[32:] 607 slots = len(bytes) / 32 608 slot = subdir.find_slots(slots) 609 subdir.write_slots(slot, bytes) 610 611 # Missing middle (second) long entry 612 bytes = make_long_dirent('A Man a Plan a Canal Panama', ATTR_ARCHIVE) 613 bytes = bytes[:32] + bytes[64:] 614 slots = len(bytes) / 32 615 slot = subdir.find_slots(slots) 616 subdir.write_slots(slot, bytes) 617 618 # Missing last long entry 619 bytes = make_long_dirent('We the People in order to form a more perfect union', ATTR_ARCHIVE) 620 bytes = bytes[0:-64] + bytes[-32:] 621 slots = len(bytes) / 32 622 slot = subdir.find_slots(slots) 623 subdir.write_slots(slot, bytes) 624 625 subdir = root.mkdir('Bad Orders') 626 627 # Bad order value: first 628 bytes = make_long_dirent('One is the loneliest number', ATTR_ARCHIVE) 629 bytes = chr(ord(bytes[0])+7) + bytes[1:] 630 slots = len(bytes) / 32 631 slot = subdir.find_slots(slots) 632 subdir.write_slots(slot, bytes) 633 634 # Bad order value: middle 635 bytes = make_long_dirent('It takes two to tango or so they say', ATTR_ARCHIVE) 636 bytes = bytes[:32] + chr(ord(bytes[32])+7) + bytes[33:] 637 slots = len(bytes) / 32 638 slot = subdir.find_slots(slots) 639 subdir.write_slots(slot, bytes) 640 641 # Bad order value: last 642 bytes = make_long_dirent('Threes Company becomes Threes A Crowd', ATTR_ARCHIVE) 643 bytes = bytes[:-64] + chr(ord(bytes[-64])+7) + bytes[-63:] 644 slots = len(bytes) / 32 645 slot = subdir.find_slots(slots) 646 subdir.write_slots(slot, bytes) 647 648 # Long name entries (valid or not, with no short entry) at end of directory 649 bytes = make_long_dirent('Four score and seven years ago', ATTR_ARCHIVE) 650 bytes = bytes[0:-32] # Remove the short name entry 651 assert len(bytes) % 32 == 0 652 slots = len(bytes) / 32 653 slot = root.find_slots(slots) 654 root.write_slots(slot, bytes) 655 656 v.flush() 657 del v 658 f.close() 659 del f 660 661 try: 662 launch([fsck, '-n', disk]) 663 except LaunchError: 664 pass 665 launch([fsck, '-y', disk]) 666 launch(['/sbin/fsck_msdos', '-n', disk]) 667 668def past_end_of_dir(disk, fsck, newfs, newfs_opts, multiple_clusters=False): 669 launch([newfs]+newfs_opts+[disk]) 670 671 f = file(disk, "r+") 672 v = msdosfs(f) 673 root = v.root() 674 675 if multiple_clusters: 676 subdir_clusters = v.fat.find(10) 677 else: 678 subdir_clusters = None 679 subdir = root.mkdir('SubDir', subdir_clusters) 680 subdir.mkfile('Good Sub File') 681 root.mkfile('Good Root File') 682 683 # Make an entry that will be replaced by end-of-directory 684 slotEOF = root.find_slots(1) 685 root.mkfile('EOF') 686 687 # Make some valid file entries past end of directory 688 root.mkfile('BADFILE') 689 root.mkdir('Bad Dir') 690 root.mkfile('Bad File 2') 691 692 # Overwrite 'EOF' entry with end-of-directory marker 693 root.write_slots(slotEOF, '\x00' * 32) 694 695 # Make an entry that will be replaced by end-of-directory 696 slotEOF = subdir.find_slots(1) 697 subdir.mkfile('EOF') 698 699 # Make some valid file entries past end of directory 700 subdir.mkfile('BADFILE') 701 subdir.mkdir('Bad Dir') 702 subdir.mkfile('Bad File 2') 703 704 # If desired, make a whole bunch more entries that will cause 705 # the directory to grow into at least one more cluster. 706 # See Radar #xxxx. 707 if multiple_clusters: 708 base_name = "This file name is long so that it can take up plenty of room in the directory " 709 entry_length = len(make_long_dirent(base_name, 0)) 710 num_entries = 4 * (v.bytesPerCluster // entry_length) 711 for i in xrange(num_entries): 712 subdir.mkfile(base_name+str(i)) 713 714 # Overwrite 'EOF' entry with end-of-directory marker 715 subdir.write_slots(slotEOF, '\x00' * 32) 716 717 v.flush() 718 del v 719 f.close() 720 del f 721 722 try: 723 launch([fsck, '-n', disk]) 724 except LaunchError: 725 pass 726 launch([fsck, '-y', disk]) 727 launch(['/sbin/fsck_msdos', '-n', disk]) 728 729# 730# Stomp the first two FAT entries. 731# 732def fat_bad_0_or_1(disk, fsck, newfs, newfs_opts): 733 launch([newfs]+newfs_opts+[disk]) 734 735 f = file(disk, "r+") 736 v = msdosfs(f) 737 738 v.fat[0] = 0 739 v.fat[1] = 1 740 741 v.flush() 742 del v 743 f.close() 744 del f 745 746 try: 747 launch([fsck, '-n', disk]) 748 except LaunchError: 749 pass 750 launch([fsck, '-y', disk]) 751 launch(['/sbin/fsck_msdos', '-n', disk]) 752 753# 754# Mark the volume dirty, and cause some minor damage (orphan clusters). 755# Make sure the volume gets marked clean afterwards. 756# 757def fat_mark_clean_corrupt(disk, fsck, newfs, newfs_opts): 758 launch([newfs]+newfs_opts+[disk]) 759 760 f = file(disk, "r+") 761 v = msdosfs(f) 762 763 # Mark the volume "dirty" by clearing the "clean" bit. 764 if v.type == 32: 765 v.fat[1] = v.fat[1] & 0x07FFFFFF 766 else: 767 v.fat[1] = v.fat[1] & 0x7FFF 768 769 # Allocate some clusters, so there is something to repair. 770 v.allocate(3) 771 772 v.flush() 773 del v 774 f.close() 775 del f 776 777 try: 778 launch([fsck, '-n', disk]) 779 except LaunchError: 780 pass 781 launch([fsck, '-y', disk]) 782 783 f = file(disk, "r") 784 v = msdosfs(f) 785 786 # Make sure the "clean" bit is now set. 787 if v.type == 32: 788 clean = v.fat[1] & 0x08000000 789 else: 790 clean = v.fat[1] & 0x8000 791 if not clean: 792 raise RuntimeError("Volume still dirty!") 793 794 v.flush() 795 del v 796 f.close() 797 del f 798 799 launch(['/sbin/fsck_msdos', '-n', disk]) 800 801# 802# Mark the volume dirty (with no corruption). 803# Make sure the volume gets marked clean afterwards. 804# Make sure the exit status is 0, even with "-n". 805# 806def fat_mark_clean_ok(disk, fsck, newfs, newfs_opts): 807 launch([newfs]+newfs_opts+[disk]) 808 809 f = file(disk, "r+") 810 v = msdosfs(f) 811 812 # Mark the volume "dirty" by clearing the "clean" bit. 813 if v.type == 32: 814 v.fat[1] = v.fat[1] & 0x07FFFFFF 815 else: 816 v.fat[1] = v.fat[1] & 0x7FFF 817 818 v.flush() 819 del v 820 f.close() 821 del f 822 823 # Make sure that we ask the user to mark the disk clean, but don't return 824 # a non-zero exit status if the user declines. 825 stdout, stderr = launch([fsck, '-n', disk], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 826 assert "\nMARK FILE SYSTEM CLEAN? no\n" in stdout 827 assert "\n***** FILE SYSTEM IS LEFT MARKED AS DIRTY *****\n" in stdout 828 829 stdout, stderr = launch([fsck, '-y', disk], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 830 assert "\nMARK FILE SYSTEM CLEAN? yes\n" in stdout 831 assert "\nMARKING FILE SYSTEM CLEAN\n" in stdout 832 833 f = file(disk, "r") 834 v = msdosfs(f) 835 836 # Make sure the "clean" bit is now set. 837 if v.type == 32: 838 clean = v.fat[1] & 0x08000000 839 else: 840 clean = v.fat[1] & 0x8000 841 if not clean: 842 raise RuntimeError("Volume still dirty!") 843 844 v.flush() 845 del v 846 f.close() 847 del f 848 849 launch(['/sbin/fsck_msdos', '-n', disk]) 850 851# 852# Make a file whose physical size is 4GB. The logical size is 4GB-100. 853# This is actually NOT corrupt; it's here to verify that fsck_msdos does not 854# try to truncate the file due to overflow of the physical size. [4988133] 855# 856def file_4GB(disk, fsck, newfs, newfs_opts): 857 launch([newfs]+newfs_opts+[disk]) 858 859 # 860 # Create a file whose size is 4GB-100. That means its physical size will 861 # be rounded up to the next multiple of the cluster size, meaning the 862 # physical size will be 4GB. 863 # 864 print "# Creating a 4GiB file. This may take some time." 865 f = file(disk, "r+") 866 v = msdosfs(f) 867 four_GB = 4*1024*1024*1024 868 clusters = four_GB / v.bytesPerCluster 869 head = v.allocate(clusters) 870 v.root().mkfile('4GB', head=head, length=four_GB-100) 871 872 v.flush() 873 del v 874 f.close() 875 del f 876 877 launch([fsck, '-n', disk]) 878 879# 880# Make a file with excess clusters allocated: over 4GB worth of clusters 881# 882# TODO: What combination of files do we want to test with? 883# TODO: A smallish logical size 884# TODO: A logical size just under 4GB 885# TODO: Cross-linked files? 886# TODO: Cross linked beyond 4GB? 887# TODO: Cross linked before 4GB? 888# 889def file_4GB_excess_clusters(disk, fsck, newfs, newfs_opts): 890 launch([newfs]+newfs_opts+[disk]) 891 892 # 893 # Create files with too many clusters for their size 894 # 895 print "# Creating a 4GiB+ file. This may take some time." 896 f = file(disk, "r+") 897 v = msdosfs(f) 898 four_GB = 4*1024*1024*1024 899 clusters = four_GB / v.bytesPerCluster 900 head=v.allocate(clusters+7) 901 v.root().mkfile('FOO', head=head, length=5*v.bytesPerCluster-100) 902 head=v.allocate(clusters+3) 903 v.root().mkfile('BAR', head=head, length=four_GB-30) 904 905 v.flush() 906 del v 907 f.close() 908 del f 909 910 # TODO: Need a better way to assert that the disk is corrupt to start with 911 try: 912 launch([fsck, '-n', disk]) 913 except LaunchError: 914 pass 915 launch([fsck, '-y', disk]) 916 launch([fsck, '-n', disk]) 917 918# 919# Test the "-q" ("quick") option which reports whether the "dirty" flag in 920# the FAT has been set. The dirty flag is only defined for FAT16 and FAT32. 921# For FAT12, we actually do a full verify of the volume and return that the 922# volume is clean if it has no problems, dirty if a problem was detected. 923# 924# NOTE: Assumes newfs_opts[1] is "12", "16" or "32" to indicate which FAT 925# type is being tested. 926# 927def test_quick(disk, fsck, newfs, newfs_opts): 928 assert newfs_opts[1] in ["12", "16", "32"] 929 launch([newfs]+newfs_opts+[disk]) 930 931 # Try a quick check of a volume that is clean 932 launch([fsck, '-q', disk]) 933 934 # Make the volume dirty 935 f = file(disk, "r+") 936 v = msdosfs(f) 937 if newfs_opts[1] in ["16", "32"]: 938 if newfs_opts[1] == "16": 939 v.fat[1] &= 0x7FFF 940 else: 941 v.fat[1] &= 0x07FFFFFF 942 else: 943 # Corrupt a FAT12 volume so that it looks dirty. 944 # Allocate some clusters, so there is something to repair. 945 v.allocate(3) 946 v.flush() 947 del v 948 f.close() 949 del f 950 951 # Quick check a dirty volume 952 try: 953 launch([fsck, '-q', disk]) 954 except LaunchError: 955 pass 956 else: 957 raise FailureExpected("Volume not dirty?") 958 959# 960# Test the "-M" (memory limit) option. This just tests the argument parsing. 961# It does not verify that fsck_msdos actually limits its memory usage. 962# 963def test_maxmem(disk, fsck, newfs, newfs_opts): 964 launch([newfs]+newfs_opts+[disk]) 965 launch([fsck, '-M', '1m', disk]) 966 launch([fsck, '-M', '900k', disk]) 967 968# 969# Test several combinations of bad arguments. 970# 971def test_bad_args(disk, fsck, newfs, newfs_opts): 972 launch([newfs]+newfs_opts+[disk]) 973 974 try: 975 launch([fsck, '-M', 'foo', disk]) 976 except LaunchError: 977 pass 978 else: 979 raise FailureExpected("Expected bad argument: -M foo") 980 981 try: 982 launch([fsck, '-p', '-M', 'foo', disk]) 983 except LaunchError: 984 pass 985 else: 986 raise FailureExpected("Expected bad argument: -p -M foo") 987 988 try: 989 launch([fsck, '-M', '1x', disk]) 990 except LaunchError: 991 pass 992 else: 993 raise FailureExpected("Expected bad argument: -M 1x") 994 995 try: 996 launch([fsck, '-z', disk]) 997 except LaunchError: 998 pass 999 else: 1000 raise FailureExpected("Expected bad argument: -z") 1001 1002 try: 1003 launch([fsck]) 1004 except LaunchError: 1005 pass 1006 else: 1007 raise FailureExpected("Expected usage (no disk given)") 1008 1009# 1010# Test several cases of bad values in the boot sector. 1011# Assumes testing on a disk with 512 bytes per sector. 1012# These are all fatal, so don't try repairing. 1013# 1014def test_boot_sector(disk, fsck, newfs, newfs_opts): 1015 # Corrupt the jump instruction 1016 def bad_jump(bytes): 1017 return 'H+' + bytes[2:] 1018 1019 # Corrupt the sector size (set it to 0x03FF) 1020 def bad_sector_size(bytes): 1021 return bytes[0:11] + '\xff\x03' + bytes[13:] 1022 1023 # Corrupt the sectors per cluster value 1024 def bad_sec_per_clust(bytes): 1025 return bytes[0:13] + '\x07' + bytes[14:] 1026 1027 for func, reason in [(bad_jump,"Bad boot jump"), 1028 (bad_sector_size, "Bad sector size"), 1029 (bad_sec_per_clust, "Bad sectors per cluster")]: 1030 launch([newfs]+newfs_opts+[disk]) 1031 with open(disk, "r+") as f: 1032 bytes = f.read(512) 1033 f.seek(0) 1034 bytes = func(bytes) 1035 f.write(bytes) 1036 try: 1037 launch([fsck, '-n', disk]) 1038 except LaunchError: 1039 pass 1040 else: 1041 raise FailureExpected(reason) 1042 1043# 1044# Test several cases of bad values in the boot sector (FAT32 only). 1045# These are all fatal, so don't try repairing. 1046# 1047def test_boot_fat32(disk, fsck, newfs, newfs_opts): 1048 # Non-zero number of root directory entries 1049 def bad_root_count(bytes): 1050 return bytes[0:17] + '\x00\x02' + bytes[19:] 1051 1052 # Non-zero file system version 1053 def bad_version(bytes): 1054 return bytes[0:42] + '\x00\x01' + bytes[44:] 1055 1056 for func, reason in [(bad_root_count,"Bad root entry count"), 1057 (bad_version, "Bad filesystem version")]: 1058 launch([newfs]+newfs_opts+[disk]) 1059 with open(disk, "r+") as f: 1060 bytes = f.read(512) 1061 f.seek(0) 1062 bytes = func(bytes) 1063 f.write(bytes) 1064 try: 1065 launch([fsck, '-n', disk]) 1066 except LaunchError: 1067 pass 1068 else: 1069 raise FailureExpected(reason) 1070 1071# 1072# Test several cases of bad values in the boot sector (FAT32 only). 1073# 1074def test_fsinfo(disk, fsck, newfs, newfs_opts): 1075 def bad_leading_sig(bytes): 1076 return 'RRAA' + bytes[4:] 1077 1078 def bad_sig2(bytes): 1079 return bytes[0:484] + 'rraa' + bytes[488:] 1080 1081 def bad_trailing_sig(bytes): 1082 return bytes[0:508] + '\xff\x00\xaa\x55' + bytes[512:] 1083 1084 def bad_free_count(bytes): 1085 return bytes[0:488] + '\xfe\xed\xfa\xce' + bytes[492:] 1086 1087 # Figure out where the FSInfo sector ended up 1088 launch([newfs]+newfs_opts+[disk]) 1089 with open(disk, "r+") as f: 1090 bytes = f.read(512) 1091 fsinfo = ord(bytes[48]) + 256 * ord(bytes[49]) 1092 1093 # Test each of the FSInfo corruptions 1094 for func, reason in [(bad_leading_sig, "Bad leading signature"), 1095 (bad_sig2, "Bad structure signature"), 1096 (bad_trailing_sig, "Bad trailing signature"), 1097 (bad_free_count, "Bad free cluster count")]: 1098 launch([newfs]+newfs_opts+[disk]) 1099 with open(disk, "r+") as f: 1100 f.seek(fsinfo * 512) 1101 bytes = f.read(512) 1102 f.seek(fsinfo * 512) 1103 bytes = func(bytes) 1104 f.write(bytes) 1105 try: 1106 launch([fsck, '-n', disk]) 1107 except LaunchError: 1108 pass 1109 else: 1110 raise FailureExpected(reason) 1111 1112 launch([fsck, '-y', disk]) 1113 launch([fsck, '-n', disk]) 1114 1115# 1116# Test when the FAT has too few sectors for the number of clusters. 1117# 1118# NOTE: Relies on the fact that newfs_msdos places a FAT32 root 1119# directory in cluster #2 (immediately following the FATs). 1120# 1121def fat_too_small(disk, fsck, newfs, newfs_opts): 1122 launch([newfs]+newfs_opts+[disk]) 1123 1124 with open(disk, "r+") as f: 1125 bytes = f.read(512) 1126 numFATs = ord(bytes[16]) 1127 reserved = ord(bytes[14]) + 256 * ord(bytes[15]) 1128 1129 # Decrement the number of sectors per FAT 1130 if bytes[22:24] != '\x00\x00': 1131 fat_sectors = struct.unpack("<H", bytes[22:24])[0] 1132 bytes = bytes[0:22] + struct.pack("<H", fat_sectors - 1) + bytes[24:] 1133 else: 1134 fat_sectors = struct.unpack("<I", bytes[36:40])[0] 1135 bytes = bytes[0:36] + struct.pack("<I", fat_sectors - 1) + bytes[40:] 1136 f.seek(0) 1137 f.write(bytes) 1138 1139 # Copy the root directory from old location to new location 1140 f.seek(512 * (reserved + numFATs * fat_sectors)) 1141 bytes = f.read(65536) 1142 f.seek(512 * (reserved + numFATs * fat_sectors - numFATs)) 1143 f.write(bytes) 1144 1145 # NOTE: FAT too small is NOT a fatal error 1146 launch([fsck, '-y', disk]) # Need a way to test for expected output 1147 launch([fsck, '-n', disk]) 1148 1149# 1150# Test trying to repair a disk where a directory has been overwritten by 1151# garbage (and therefore shouldn't be parsed as a directory). See 1152# <rdar://problem/15384158> Better repairs for directories containing garbage 1153# 1154def directory_garbage(disk, fsck, newfs, newfs_opts): 1155 launch([newfs]+newfs_opts+[disk]) 1156 1157 f = file(disk, "r+") 1158 v = msdosfs(f) 1159 root = v.root() 1160 1161 # Create a subdirectory with some children 1162 subdir = root.mkdir('EFI', v.fat.find(2)) 1163 for i in range(10): 1164 subdir.mkfile("Child {}".format(i), content="This is child number {}".format(i)) 1165 1166 # 1167 # Now clobber the contents of the subdirectory by overwriting it with text. 1168 # 1169 # Note: the length of this text is 887 bytes, which could be larger than a 1170 # cluster (minimum cluster size is 512). Be sure the directory is large enough! 1171 # 1172 subdir.pwrite(0, """Here's to the crazy ones. The misfits. The rebels. The troublemakers. The round pegs in the square holes. 1173 1174The ones who see things differently. They're not fond of rules. And they have no respect for the status quo. You can quote them, disagree with them, glorify or vilify them. 1175 1176About the only thing you can't do is ignore them. Because they change things. They invent. They imagine. They heal. They explore. They create. They inspire. They push the human race forward. 1177 1178Maybe they have to be crazy. 1179 1180How else can you stare at an empty canvas and see a work of art? Or sit in silence and hear a song that's never been written? Or gaze at a red planet and see a laboratory on wheels? 1181 1182We make tools for these kinds of people. 1183 1184While some see them as the crazy ones, we see genius. Because the people who are crazy enough to think they can change the world, are the ones who do. 1185""") 1186 1187 v.flush() 1188 f.close() 1189 1190 try: 1191 launch([fsck, '-n', disk]) 1192 except LaunchError: 1193 pass 1194 else: 1195 raise FailureExpected("Subdirectory /EFI should be corrupt") 1196 1197 launch([fsck, '-y', disk]) 1198 launch([fsck, '-n', disk]) 1199 1200 # Make sure /EFI has been changed to a file 1201 f = file(disk, "r+") 1202 v = msdosfs(f) 1203 root = v.root() 1204 efi = root.pread(32, 32) # The directory entry for /EFI 1205 assert(efi[0:11] == "EFI ") 1206 assert(efi[11] == "\x00") # No longer has ATTR_DIRECTORY set 1207 f.close() 1208 1209# 1210# When run as a script, run the test suite. 1211# 1212# Usage: 1213# python test_fsck.py [<fsck_msdos> [<tmp_dir>]] 1214# 1215if __name__ == '__main__': 1216 # 1217 # Set up defaults 1218 # 1219 dir = '/tmp' 1220 fsck = 'fsck_msdos' 1221 newfs = 'newfs_msdos' 1222 1223 if len(sys.argv) > 1: 1224 fsck = sys.argv[1] 1225 if len(sys.argv) > 2: 1226 dir = sys.argv[2] 1227 if len(sys.argv) > 3: 1228 print "%s: Too many arguments!" % sys.argv[0] 1229 print "Usage: %s [<fsck_msdos> [<tmp_dir>]]" 1230 sys.exit(1) 1231 1232 # 1233 # Run the test suite 1234 # 1235 test_fat32(dir, fsck, newfs) 1236 test_fat16(dir, fsck, newfs) 1237 test_fat12(dir, fsck, newfs) 1238 1239 print "\nSuccess!" 1240