# # 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) fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) fat_mark_clean(rdisk, fsck, newfs, newfs_opts) file_4GB(rdisk, fsck, newfs, newfs_opts) file_4GB_excess_clusters(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) fat_bad_0_or_1(rdisk, fsck, newfs, newfs_opts) fat_mark_clean(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) fat_bad_0_or_1(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(" []] # 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!"