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