# # Run a variety of tests against fsck_msdos # # Usage: # python test_fsck.py [ []] # # where is a path to a directory where disk images will be # temporarily created. If is specified, it is used instead # of 'fsck_msdos' to invoke the fsck_msdos program (for example, to test # a new build that has not been installed). # from __future__ import with_statement import sys import os import subprocess import struct from msdosfs import * from HexDump import HexDump class LaunchError(Exception): def __init__(self, returncode): self.returncode = returncode if returncode < 0: self.message = "Program exited with signal %d" % -returncode else: self.message = "Program exited with status %d" % returncode def __str__(self): return self.message class FailureExpected(Exception): def __init__(self, s): self.s = s def __str__(self): return self.s # # launch -- A helper to run another process and collect the standard output # and standard error streams. If the process returns a non-zero exit # status, then raise an exception. # def launch(args, **kwargs): print "launch:", args, kwargs p = subprocess.Popen(args, **kwargs) stdout, stderr = p.communicate() if p.returncode != 0: raise LaunchError(p.returncode) return stdout, stderr # # 1. Make a disk image file # 2. Attach the image file, without mounting # ---- Begin per-test stuff ---- # 3. newfs_msdos the image # 4. Fill image with content # 5. fsck_msdos -n the image # 6. fsck_msdos -y the image # 7. Run /sbin/fsck_msdos against image # ---- End per-test stuff ---- # 8. Detach the image # 9. Delete the image file # # # Run tests on 20GiB FAT32 sparse disk image # def test_fat32(dir, fsck, newfs): # # Create a 20GB disk image in @dir # dmg = os.path.join(dir, 'Test20GB.sparseimage') launch('hdiutil create -size 20g -type SPARSE -layout NONE'.split()+[dmg]) newfs_opts = "-F 32 -b 4096 -v TEST20GB".split() # # Attach the image # disk = launch(['hdiutil', 'attach', '-nomount', dmg], stdout=subprocess.PIPE)[0].rstrip() rdisk = disk.replace('/dev/disk', '/dev/rdisk') # # Run tests # # TODO: Known good disk # empty file # one cluster file # larger file # one cluster directory # larger directory # test_quick(rdisk, fsck, newfs, newfs_opts) test_bad_args(rdisk, fsck, newfs, newfs_opts) test_maxmem(rdisk, fsck, newfs, newfs_opts) test_empty(rdisk, fsck, newfs, newfs_opts) test_boot_sector(rdisk, fsck, newfs, newfs_opts) test_boot_fat32(rdisk, fsck, newfs, newfs_opts) # FAT32 only! test_fsinfo(rdisk, fsck, newfs, newfs_opts) # FAT32 only! fat_too_small(rdisk, fsck, newfs, newfs_opts) orphan_clusters(rdisk, fsck, newfs, newfs_opts) file_excess_clusters(rdisk, fsck, newfs, newfs_opts) file_bad_clusters(rdisk, fsck, newfs, newfs_opts) dir_bad_start(rdisk, fsck, newfs, newfs_opts) root_bad_start(rdisk, fsck, newfs, newfs_opts) # FAT32 only! root_bad_first_cluster(rdisk, fsck, newfs, newfs_opts) # FAT32 only! dir_size_dots(rdisk, fsck, newfs, newfs_opts) long_name(rdisk, fsck, newfs, newfs_opts) past_end_of_dir(rdisk, fsck, newfs, newfs_opts) past_end_of_dir(rdisk, fsck, newfs, newfs_opts, True) fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) fat_mark_clean_corrupt(rdisk, fsck, newfs, newfs_opts) fat_mark_clean_ok(rdisk, fsck, newfs, newfs_opts) file_4GB(rdisk, fsck, newfs, newfs_opts) file_4GB_excess_clusters(rdisk, fsck, newfs, newfs_opts) directory_garbage(rdisk, fsck, newfs, newfs_opts) # # Detach the image # launch(['diskutil', 'eject', disk]) # # Delete the image file # os.remove(dmg) # # Run tests on 160MiB FAT16 image # def test_fat16(dir, fsck, newfs): # # Create a 160MB disk image in @dir # dmg = os.path.join(dir, 'Test160MB.dmg') f = file(dmg, "w") f.truncate(160*1024*1024) f.close newfs_opts = "-F 16 -b 4096 -v TEST160MB".split() # # Attach the image # disk = launch(['hdiutil', 'attach', '-nomount', dmg], stdout=subprocess.PIPE)[0].rstrip() rdisk = disk.replace('/dev/disk', '/dev/rdisk') # # Run tests # # TODO: Known good disk # empty file # one cluster file # larger file # one cluster directory # larger directory # test_quick(rdisk, fsck, newfs, newfs_opts) test_bad_args(rdisk, fsck, newfs, newfs_opts) test_maxmem(rdisk, fsck, newfs, newfs_opts) test_empty(rdisk, fsck, newfs, newfs_opts) test_boot_sector(rdisk, fsck, newfs, newfs_opts) fat_too_small(rdisk, fsck, newfs, newfs_opts) orphan_clusters(rdisk, fsck, newfs, newfs_opts) file_excess_clusters(rdisk, fsck, newfs, newfs_opts) file_bad_clusters(rdisk, fsck, newfs, newfs_opts) dir_bad_start(rdisk, fsck, newfs, newfs_opts) dir_size_dots(rdisk, fsck, newfs, newfs_opts) long_name(rdisk, fsck, newfs, newfs_opts) past_end_of_dir(rdisk, fsck, newfs, newfs_opts) past_end_of_dir(rdisk, fsck, newfs, newfs_opts, True) fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) fat_mark_clean_corrupt(rdisk, fsck, newfs, newfs_opts) fat_mark_clean_ok(rdisk, fsck, newfs, newfs_opts) directory_garbage(rdisk, fsck, newfs, newfs_opts) # # Detach the image # launch(['diskutil', 'eject', disk]) # # Delete the image file # os.remove(dmg) # # Run tests on 15MiB FAT12 image # def test_fat12(dir, fsck, newfs): # # Create a 15MB disk image in @dir # dmg = os.path.join(dir, 'Test15MB.dmg') f = file(dmg, "w") f.truncate(15*1024*1024) f.close newfs_opts = "-F 12 -b 4096 -v TEST15MB".split() # # Attach the image # disk = launch(['hdiutil', 'attach', '-nomount', dmg], stdout=subprocess.PIPE)[0].rstrip() rdisk = disk.replace('/dev/disk', '/dev/rdisk') # # Run tests # # TODO: Known good disk # empty file # one cluster file # larger file # one cluster directory # larger directory # test_quick(rdisk, fsck, newfs, newfs_opts) test_bad_args(rdisk, fsck, newfs, newfs_opts) test_maxmem(rdisk, fsck, newfs, newfs_opts) test_empty(rdisk, fsck, newfs, newfs_opts) test_boot_sector(rdisk, fsck, newfs, newfs_opts) fat_too_small(rdisk, fsck, newfs, newfs_opts) orphan_clusters(rdisk, fsck, newfs, newfs_opts) file_excess_clusters(rdisk, fsck, newfs, newfs_opts) file_bad_clusters(rdisk, fsck, newfs, newfs_opts) dir_bad_start(rdisk, fsck, newfs, newfs_opts) dir_size_dots(rdisk, fsck, newfs, newfs_opts) long_name(rdisk, fsck, newfs, newfs_opts) past_end_of_dir(rdisk, fsck, newfs, newfs_opts) past_end_of_dir(rdisk, fsck, newfs, newfs_opts, True) fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) directory_garbage(rdisk, fsck, newfs, newfs_opts) # # Detach the image # launch(['diskutil', 'eject', disk]) # # Delete the image file # os.remove(dmg) # # A minimal test -- make sure fsck_msdos runs on an empty image # def test_empty(disk, fsck, newfs, newfs_opts): # # newfs the disk # launch([newfs]+newfs_opts+[disk]) # # fsck the disk # launch([fsck, '-n', disk]) # # Make a volume with allocated but unreferenced cluster chains # def orphan_clusters(disk, fsck, newfs, newfs_opts): launch([newfs]+newfs_opts+[disk]) # # Create some cluster chains not referenced by any file or directory # f = file(disk, "r+") v = msdosfs(f) v.allocate(7, 100) v.allocate(23, 150) v.allocate(1, 190) v.flush() del v f.close() del f try: launch([fsck, '-n', disk]) except LaunchError: pass launch([fsck, '-p', disk]) launch(['/sbin/fsck_msdos', '-n', disk]) # # Make a file with excess clusters allocated # One file with EOF == 0 # One file with EOF != 0 # Files with excess clusters that are cross-linked # First excess cluster is cross-linked # Other excess cluster is cross-linked # Excess clusters end with free/bad/reserved cluster # First excess cluster is free/bad/reserved # Other excess cluster is free/bad/reserved # def file_excess_clusters(disk, fsck, newfs, newfs_opts): launch([newfs]+newfs_opts+[disk]) # # Create files with too many clusters for their size # f = file(disk, "r+") v = msdosfs(f) head=v.allocate(7) v.root().mkfile('FOO', head=head, length=6*v.bytesPerCluster) head=v.allocate(1) v.root().mkfile('BAR', head=head, length=0) # # LINK1 is OK. # LINK2 contains excess clusters; the first is cross-linked with LINK1 # LINK3 contains excess clusters; the second is cross-linked with LINK1 # clusters = v.fat.find(9) head = v.fat.chain(clusters) v.root().mkfile('LINK1', head=head, length=8*v.bytesPerCluster+1) head = v.fat.allocate(3, last=clusters[7]) v.root().mkfile('LINK2', head=head, length=2*v.bytesPerCluster+3) head = v.fat.allocate(5, last=clusters[8]) v.root().mkfile('LINK3', head=head, length=3*v.bytesPerCluster+5) if v.fsinfo: v.fsinfo.allocate(9+3+5) # # FREE1 has its first excess cluster marked free # BAD3 has its third excess cluster marked bad # head = v.allocate(11, last=CLUST_BAD) v.root().mkfile('BAD3', head=head, length=8*v.bytesPerCluster+300) head = v.allocate(8, last=CLUST_FREE) v.root().mkfile('FREE1', head=head, length=6*v.bytesPerCluster+100) v.flush() del v f.close() del f try: launch([fsck, '-n', disk]) except LaunchError: pass launch([fsck, '-y', disk]) launch(['/sbin/fsck_msdos', '-n', disk]) # # Make files with bad clusters in their chains # FILE1 file with middle cluster free # FILE2 file with middle cluster bad/reserved # FILE3 file with middle cluster points to out of range cluster # FILE4 file with middle cluster that is cross-linked (to same file) # FILE5 file whose head is "free" # FILE6 file whose head is "bad" # FILE7 file whose head is out of range # FILE8 file whose head is cross-linked # def file_bad_clusters(disk, fsck, newfs, newfs_opts): launch([newfs]+newfs_opts+[disk]) f = file(disk, "r+") v = msdosfs(f) clusters = v.fat.find(5) to_free = clusters[2] head = v.fat.chain(clusters) v.root().mkfile('FILE1', head=head, length=6*v.bytesPerCluster+111) if v.fsinfo: v.fsinfo.allocate(5) clusters = v.fat.find(5) head = v.fat.chain(clusters) v.root().mkfile('FILE2', head=head, length=4*v.bytesPerCluster+222) v.fat[clusters[2]] = CLUST_RSRVD if v.fsinfo: v.fsinfo.allocate(5) clusters = v.fat.find(5) head = v.fat.chain(clusters) v.root().mkfile('FILE3', head=head, length=4*v.bytesPerCluster+333) v.fat[clusters[2]] = 1 if v.fsinfo: v.fsinfo.allocate(5) clusters = v.fat.find(5) head = v.fat.chain(clusters) v.root().mkfile('FILE4', head=head, length=4*v.bytesPerCluster+44) v.fat[clusters[2]] = clusters[1] if v.fsinfo: v.fsinfo.allocate(5) v.root().mkfile('FILE5', head=CLUST_FREE, length=4*v.bytesPerCluster+55) v.root().mkfile('FILE6', head=CLUST_BAD, length=4*v.bytesPerCluster+66) v.root().mkfile('FILE7', head=CLUST_RSRVD-1, length=4*v.bytesPerCluster+77) head = v.allocate(5) v.root().mkfile('FOO', head=head, length=4*v.bytesPerCluster+99) v.root().mkfile('FILE8', head=head, length=4*v.bytesPerCluster+88) # Free the middle cluster of FILE1 now that we've finished allocating v.fat[to_free] = CLUST_FREE v.flush() del v f.close() del f try: launch([fsck, '-n', disk]) except LaunchError: pass launch([fsck, '-y', disk]) launch(['/sbin/fsck_msdos', '-n', disk]) # # Make directories whose starting cluster number is free/bad/reserved/out of range # DIR1 start cluster is free # DIR2 start cluster is reserved # DIR3 start cluster is bad # DIR4 start cluster is EOF # DIR5 start cluster is 1 # DIR6 start cluster is one more than max valid cluster # def dir_bad_start(disk, fsck, newfs, newfs_opts): def mkdir(parent, name, head): bytes = make_long_dirent(name, ATTR_DIRECTORY, head=head) slots = len(bytes)/32 slot = parent.find_slots(slots, grow=True) parent.write_slots(slot, bytes) launch([newfs]+newfs_opts+[disk]) f = file(disk, "r+") v = msdosfs(f) root = v.root() mkdir(root, 'DIR1', CLUST_FREE) mkdir(root, 'DIR2', CLUST_RSRVD) mkdir(root, 'DIR3', CLUST_BAD) mkdir(root, 'DIR4', CLUST_EOF) mkdir(root, 'DIR5', 1) mkdir(root, 'DIR6', v.clusters+2) v.flush() del v f.close() del f try: launch([fsck, '-n', disk]) except LaunchError: pass launch([fsck, '-y', disk]) launch(['/sbin/fsck_msdos', '-n', disk]) # # Root dir's starting cluster number is free/bad/reserved/out of range # # NOTE: This test is only applicable to FAT32! # def root_bad_start(disk, fsck, newfs, newfs_opts): def set_root_start(disk, head): dev = file(disk, "r+") dev.seek(0) bytes = dev.read(512) bytes = bytes[0:44] + struct.pack(" Better repairs for directories containing garbage # def directory_garbage(disk, fsck, newfs, newfs_opts): launch([newfs]+newfs_opts+[disk]) f = file(disk, "r+") v = msdosfs(f) root = v.root() # Create a subdirectory with some children subdir = root.mkdir('EFI', v.fat.find(2)) for i in range(10): subdir.mkfile("Child {}".format(i), content="This is child number {}".format(i)) # # Now clobber the contents of the subdirectory by overwriting it with text. # # Note: the length of this text is 887 bytes, which could be larger than a # cluster (minimum cluster size is 512). Be sure the directory is large enough! # subdir.pwrite(0, """Here's to the crazy ones. The misfits. The rebels. The troublemakers. The round pegs in the square holes. The 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. About 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. Maybe they have to be crazy. How 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? We make tools for these kinds of people. While 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. """) v.flush() f.close() try: launch([fsck, '-n', disk]) except LaunchError: pass else: raise FailureExpected("Subdirectory /EFI should be corrupt") launch([fsck, '-y', disk]) launch([fsck, '-n', disk]) # Make sure /EFI has been changed to a file f = file(disk, "r+") v = msdosfs(f) root = v.root() efi = root.pread(32, 32) # The directory entry for /EFI assert(efi[0:11] == "EFI ") assert(efi[11] == "\x00") # No longer has ATTR_DIRECTORY set f.close() # # When run as a script, run the test suite. # # Usage: # python test_fsck.py [ []] # if __name__ == '__main__': # # Set up defaults # dir = '/tmp' fsck = 'fsck_msdos' newfs = 'newfs_msdos' if len(sys.argv) > 1: fsck = sys.argv[1] if len(sys.argv) > 2: dir = sys.argv[2] if len(sys.argv) > 3: print "%s: Too many arguments!" % sys.argv[0] print "Usage: %s [ []]" sys.exit(1) # # Run the test suite # test_fat32(dir, fsck, newfs) test_fat16(dir, fsck, newfs) test_fat12(dir, fsck, newfs) print "\nSuccess!"