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