/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Implementation of DiskFSDOS33 and A2FileDOS classes.
 *
 * Works for DOS 3.2 and "wide DOS" as well.
 *
 * BUG: does not keep VolumeUsage up to date.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"


/*
 * ===========================================================================
 *		DiskFSDOS33
 * ===========================================================================
 */

const int kMaxSectors = 32;

const int kSctSize = 256;
/* do we need a way to override these? */
const int kVTOCTrack = 17;
const int kVTOCSector = 0;
const int kCatalogEntryOffset = 0x0b;	// first entry in cat sect starts here
const int kCatalogEntrySize = 0x23;		// length in bytes of catalog entries
const int kCatalogEntriesPerSect = 7;	// #of entries per catalog sector
const int kEntryDeleted = 0xff;		// this is used for track# of deleted files
const int kEntryUnused = 0x00;		// this is track# in never-used entries
const int kMaxTSPairs = 0x7a;			// 122 entries for 256-byte sectors
const int kTSOffset = 0x0c;				// first T/S entry in a T/S list

const int kMaxTSIterations = 32;

/*
 * Get a pointer to the Nth entry in a catalog sector.
 */
static inline unsigned char*
GetCatalogEntryPtr(unsigned char* basePtr, int entryNum)
{
	assert(entryNum >= 0 && entryNum < kCatalogEntriesPerSect);
	return basePtr + kCatalogEntryOffset + entryNum * kCatalogEntrySize;
}


/*
 * Test this image for DOS3.3-ness.
 *
 * Some notes on tricky disks...
 *
 * DISK019B (Ultima II player master) has a copy of the VTOC in track 11
 * sector 1, which causes a loop back to track 11 sector f.  We may want
 * to be clever here and allow it, but we have to be careful because
 * we must be similarly clever in the VTOC read routines.  (Need a more
 * sophisticated loop detector, since a loop will crank our "foundGood" up.)
 *
 * DISK038B (Congo Bongo) has some "crack" titles and a valid VTOC, but not
 * much else.  Could allow it if the user explicitly told us to use DOS33,
 * but it's a little thin.
 *
 * DISK112B.X (Ultima I player master) has a catalog that jumps around a lot.
 * It's perfectly valid, but we don't really detect it properly.  Forcing
 * DOS interpretation should be acceptable.
 *
 * DISK175A (Standing Stones) has an extremely short but valid catalog track.
 *
 * DISK198B (Aliens+docs) gets 3 and bails with a self-reference.
 */
static DIError
TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, int* pGoodCount)
{
	DIError dierr = kDIErrNone;
	unsigned char sctBuf[kSctSize];
	int numTracks, numSectors;
	int catTrack, catSect;
	int foundGood = 0;
	int iterations = 0;

	*pGoodCount = 0;

	dierr = pImg->ReadTrackSectorSwapped(kVTOCTrack, kVTOCSector,
				sctBuf, imageOrder, DiskImg::kSectorOrderDOS);
	if (dierr != kDIErrNone)
		goto bail;

	catTrack = sctBuf[0x01];
	catSect = sctBuf[0x02];
	numTracks = sctBuf[0x34];
	numSectors = sctBuf[0x35];

	if (!(sctBuf[0x27] == kMaxTSPairs) ||
		/*!(sctBuf[0x36] == 0 && sctBuf[0x37] == 1) ||*/	// bytes per sect
		!(numTracks <= DiskFSDOS33::kMaxTracks) ||
		!(numSectors == 13 || numSectors == 16 || numSectors == 32) ||
		!(catTrack < numTracks && catSect < numSectors) ||
		0)
	{
		WMSG1("  DOS header test failed (order=%d)\n", imageOrder);
		dierr = kDIErrFilesystemNotFound;
		goto bail;
	}

	foundGood++;		// score one for a valid-looking VTOC

	/*
	 * Walk through the catalog track to try to figure out ordering.
	 */
	while (catTrack != 0 && catSect != 0 &&
		iterations < DiskFSDOS33::kMaxCatalogSectors)
	{
		dierr = pImg->ReadTrackSectorSwapped(catTrack, catSect, sctBuf,
					imageOrder, DiskImg::kSectorOrderDOS);
		if (dierr != kDIErrNone) {
			dierr = kDIErrNone;
			break;		/* allow it if earlier stuff was okay */
		}

		if (catTrack == sctBuf[1] && catSect == sctBuf[2] +1)
			foundGood++;
		else if (catTrack == sctBuf[1] && catSect == sctBuf[2]) {
			WMSG2(" DOS detected self-reference on cat (%d,%d)\n",
				catTrack, catSect);
			break;
		}
		catTrack = sctBuf[1];
		catSect = sctBuf[2];
		iterations++;		// watch for infinite loops
	}
	if (iterations >= DiskFSDOS33::kMaxCatalogSectors) {
		/* possible cause: LF->CR conversion screws up link to sector $0a */
		dierr = kDIErrDirectoryLoop;
		WMSG1("  DOS directory links cause a loop (order=%d)\n", imageOrder);
		goto bail;
	}

	WMSG2(" DOS    foundGood=%d order=%d\n", foundGood, imageOrder);
	*pGoodCount = foundGood;

bail:
	return dierr;
}

/*
 * Test to see if the image is a DOS 3.2 or DOS 3.3 disk.
 */
/*static*/ DIError
DiskFSDOS33::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder,
	DiskImg::FSFormat* pFormat, FSLeniency leniency)
{
	if (pImg->GetNumTracks() > kMaxInterestingTracks)
		return kDIErrFilesystemNotFound;

	DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax];
	
	DiskImg::GetSectorOrderArray(ordering, *pOrder);

	DiskImg::SectorOrder bestOrder = DiskImg::kSectorOrderUnknown;
	int bestCount = 0;

	for (int i = 0; i < DiskImg::kSectorOrderMax; i++) {
		int goodCount = 0;

		if (ordering[i] == DiskImg::kSectorOrderUnknown)
			continue;
		if (TestImage(pImg, ordering[i], &goodCount) == kDIErrNone) {
			if (goodCount > bestCount) {
				bestCount = goodCount;
				bestOrder = ordering[i];
			}
		}
	}

	if (bestCount >= 4 ||
		(leniency == kLeniencyVery && bestCount >= 2))
	{
		WMSG2(" DOS test: bestCount=%d for order=%d\n", bestCount, bestOrder);
		assert(bestOrder != DiskImg::kSectorOrderUnknown);
		*pOrder = bestOrder;
		*pFormat = DiskImg::kFormatDOS33;
		if (pImg->GetNumSectPerTrack() == 13)
			*pFormat = DiskImg::kFormatDOS32;
		return kDIErrNone;
	}

	WMSG0(" DOS33 didn't find valid DOS3.2 or DOS3.3\n");
	return kDIErrFilesystemNotFound;
}


/*
 * Get things rolling.
 *
 * Since we're assured that this is a valid disk, errors encountered from here
 * on out must be handled somehow, possibly by claiming that the disk is
 * completely full and has no files on it.
 */
DIError
DiskFSDOS33::Initialize(InitMode initMode)
{
	DIError dierr = kDIErrNone;

	fVolumeUsage.Create(fpImg->GetNumTracks(), fpImg->GetNumSectPerTrack());

	dierr = ReadVTOC();
	if (dierr != kDIErrNone)
		goto bail;
	//DumpVTOC();

	dierr = ScanVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	if (initMode == kInitHeaderOnly) {
		WMSG0(" DOS - headerOnly set, skipping file load\n");
		goto bail;
	}

	/* read the contents of the catalog, creating our A2File list */
	dierr = ReadCatalog();
	if (dierr != kDIErrNone)
		goto bail;

	/* run through and get file lengths and data offsets */
	dierr = GetFileLengths();
	if (dierr != kDIErrNone)
		goto bail;

	/* mark DOS tracks appropriately */
	FixVolumeUsageMap();

	fDiskIsGood = CheckDiskIsGood();

	fVolumeUsage.Dump();

//	A2File* pFile;
//	pFile = GetNextFile(nil);
//	while (pFile != nil) {
//		pFile->Dump();
//		pFile = GetNextFile(pFile);
//	}

bail:
	return dierr;
}

/*
 * Read some fields from the disk Volume Table of Contents.
 */
DIError
DiskFSDOS33::ReadVTOC(void)
{
	DIError dierr;

	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	fFirstCatTrack = fVTOC[0x01];
	fFirstCatSector = fVTOC[0x02];
	fVTOCVolumeNumber = fVTOC[0x06];
	fVTOCNumTracks = fVTOC[0x34];
	fVTOCNumSectors = fVTOC[0x35];

	if (fFirstCatTrack >= fpImg->GetNumTracks())
		return kDIErrBadDiskImage;
	if (fFirstCatSector >= fpImg->GetNumSectPerTrack())
		return kDIErrBadDiskImage;

	if (fVTOCNumTracks != fpImg->GetNumTracks()) {
		WMSG2(" DOS33 warning: VTOC numtracks %d vs %ld\n",
			fVTOCNumTracks, fpImg->GetNumTracks());
	}
	if (fVTOCNumSectors != fpImg->GetNumSectPerTrack()) {
		WMSG2(" DOS33 warning: VTOC numsect %d vs %d\n",
			fVTOCNumSectors, fpImg->GetNumSectPerTrack());
	}

	// call SetDiskVolumeNum with the appropriate thing
	UpdateVolumeNum();

bail:
	FreeVolBitmap();
	return dierr;
}

/*
 * Call this if fpImg's volume num (derived from nibble formats) or
 * the VTOC's volume number changes.
 */
void
DiskFSDOS33::UpdateVolumeNum(void)
{
	/* use the sector-embedded volume number, if available */
	if (fpImg->GetDOSVolumeNum() == DiskImg::kVolumeNumNotSet)
		SetDiskVolumeNum(fVTOCVolumeNumber);
	else
		SetDiskVolumeNum(fpImg->GetDOSVolumeNum());
	if (fDiskVolumeNum != fVTOCVolumeNumber) {
		WMSG2("  NOTE: ignoring VTOC vol (%d) in favor of embedded (%d)\n",
			fVTOCVolumeNumber, fDiskVolumeNum);
	}
}

/*
 * Set the disk volume number (fDiskVolumeNum) and derived fields.
 */
void
DiskFSDOS33::SetDiskVolumeNum(int val)
{
	if (val < 0 || val > 255) {
		// Actual valid range should be 1-254, but it's possible for a
		// sector edit to put invalid stuff here.  It's just one byte
		// though, so 0-255 should be guaranteed.
		assert(false);
		return;
	}
	fDiskVolumeNum = val;
	sprintf(fDiskVolumeName, "DOS%03d", fDiskVolumeNum);
	if (fpImg->GetFSFormat() == DiskImg::kFormatDOS32)
		sprintf(fDiskVolumeID, "DOS 3.2 Volume %03d", fDiskVolumeNum);
	else
		sprintf(fDiskVolumeID, "DOS 3.3 Volume %03d", fDiskVolumeNum);
}


/*
 * Dump some VTOC fields.
 */
void
DiskFSDOS33::DumpVTOC(void)
{

	WMSG2("VTOC catalog: track=%d sector=%d\n",
		fFirstCatTrack, fFirstCatSector);
	WMSG3("  volnum=%d numTracks=%d numSects=%d\n",
		fVTOCVolumeNumber, fVTOCNumTracks, fVTOCNumSectors);
}

/*
 * Update an entry in the VolumeUsage map, watching for conflicts.
 */
void
DiskFSDOS33::SetSectorUsage(long track, long sector,
	VolumeUsage::ChunkPurpose purpose)
{
	VolumeUsage::ChunkState cstate;

	//WMSG3(" DOS setting usage %d,%d to %d\n", track, sector, purpose);

	fVolumeUsage.GetChunkState(track, sector, &cstate);
	if (cstate.isUsed) {
		cstate.purpose = VolumeUsage::kChunkPurposeConflict;
//		WMSG2(" DOS conflicting uses for t=%d s=%d\n", track, sector);
	} else {
		cstate.isUsed = true;
		cstate.purpose = purpose;
	}
	fVolumeUsage.SetChunkState(track, sector, &cstate);
}

/*
 * Examine the volume bitmap, setting fields in the VolumeUsage map
 * as appropriate.  We mark "isMarkedUsed", but leave "isUsed" clear.  The
 * "isUsed" flag gets set by the DOS catalog track processor and the file
 * scanners.
 *
 * We can't mark the DOS tracks, because there's no reliable way to tell by
 * looking at a DOS disk whether it has a bootable DOS image.  It's possible
 * the tracks are marked in-use because files are stored there.  Some
 * tweaked versions of DOS freed up a few sectors on track 2, so partial
 * allocation isn't a good indicator.
 *
 * What we have to do is wait until we have all the information for the
 * various files, and mark the tracks as owned by DOS if nobody else
 * claims them.
 */
DIError
DiskFSDOS33::ScanVolBitmap(void)
{
	DIError dierr;
	VolumeUsage::ChunkState cstate;
	char freemap[32+1] = "--------------------------------";

	cstate.isUsed = false;
	cstate.isMarkedUsed = true;
	cstate.purpose = (VolumeUsage::ChunkPurpose) 0;

	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	WMSG0("  map 0123456789abcdef\n");

	int i;
	for (i = 0; i < kMaxTracks; i++) {
		unsigned long val, origVal;
		int bit;

		val = (unsigned long) fVTOC[0x38 + i*4] << 24;
		val |= (unsigned long) fVTOC[0x39 + i*4] << 16;
		val |= (unsigned long) fVTOC[0x3a + i*4] << 8;
		val |= (unsigned long) fVTOC[0x3b + i*4];
		origVal = val;

		/* init the VolumeUsage stuff */
		for (bit = fpImg->GetNumSectPerTrack()-1; bit >= 0; bit--) {
			freemap[bit] = val & 0x80000000 ? '.' : 'X';

			if (i < fpImg->GetNumTracks() && !(val & 0x80000000)) {
				/* mark the sector as in-use */
				if (fVolumeUsage.SetChunkState(i, bit, &cstate) != kDIErrNone) {
					assert(false);
				}
			}
			val <<= 1;
		}
		WMSG3("  %2d: %s (0x%08lx)\n", i, freemap, origVal);
	}

	/* we know the VTOC is used, so mark it now */
	SetSectorUsage(kVTOCTrack, kVTOCSector, VolumeUsage::kChunkPurposeVolumeDir);

bail:
	FreeVolBitmap();
	return dierr;
}


/*
 * Load the VTOC into the buffer.
 */
DIError
DiskFSDOS33::LoadVolBitmap(void)
{
	DIError dierr;

	assert(!fVTOCLoaded);

	dierr = fpImg->ReadTrackSector(kVTOCTrack, kVTOCSector, fVTOC);
	if (dierr != kDIErrNone)
		return dierr;

	fVTOCLoaded = true;
	return kDIErrNone;
}

/*
 * Save our copy of the volume bitmap.
 */
DIError
DiskFSDOS33::SaveVolBitmap(void)
{
	if (!fVTOCLoaded) {
		assert(false);
		return kDIErrNotReady;
	}

	return fpImg->WriteTrackSector(kVTOCTrack, kVTOCSector, fVTOC);
}

/*
 * Throw away the volume bitmap, discarding any unsaved changes.
 *
 * It's okay to call this if the bitmap isn't loaded.
 */
void
DiskFSDOS33::FreeVolBitmap(void)
{
	fVTOCLoaded = false;

#ifdef _DEBUG
	memset(fVTOC, 0x99, sizeof(fVTOC));
#endif
}

/*
 * Return entry N from the VTOC.
 */
inline unsigned long
DiskFSDOS33::GetVTOCEntry(const unsigned char* pVTOC, long track) const
{
	unsigned long val;
	val = (unsigned long) pVTOC[0x38 + track*4] << 24;
	val |= (unsigned long) pVTOC[0x39 + track*4] << 16;
	val |= (unsigned long) pVTOC[0x3a + track*4] << 8;
	val |= (unsigned long) pVTOC[0x3b + track*4];

	return val;
}

/*
 * Allocate a new sector from the unused pool.
 *
 * Only touches the in-memory copy.
 */
DIError
DiskFSDOS33::AllocSector(TrackSector* pTS)
{
	unsigned long val;
	unsigned long mask;
	long track, numSectPerTrack;

	/* we could compute "mask", but it's faster and easier to do this */
	numSectPerTrack = GetDiskImg()->GetNumSectPerTrack();
	if (numSectPerTrack == 13)
		mask = 0xfff80000;
	else if (numSectPerTrack == 16)
		mask = 0xffff0000;
	else if (numSectPerTrack == 32)
		mask = 0xffffffff;
	else {
		assert(false);
		return kDIErrInternal;
	}

	/*
	 * Start by finding a track with a free sector.  We know it's free
	 * because the bits aren't all zero.
	 *
	 * In theory we don't need "mask", because the DOS format routine is
	 * good about leaving the unused bits clear, and nobody else disturbs
	 * them.  However, it's best not to rely on it.
	 */
	for (track = kVTOCTrack; track > 0; track--) {
		val = GetVTOCEntry(fVTOC, track);
		if ((val & mask) != 0)
			break;
	}
	if (track == 0) {
		long numTracks = GetDiskImg()->GetNumTracks();
		for (track = kVTOCTrack; track < numTracks; track++)
		{
			val = GetVTOCEntry(fVTOC, track);
			if ((val & mask) != 0)
				break;
		}
		if (track == numTracks) {
			WMSG0("DOS33 AllocSector unable to find empty sector\n");
			return kDIErrDiskFull;
		}
	}

	/*
	 * We've got the track.  Now find the first free sector.
	 */
	int sector;
	sector = numSectPerTrack-1;
	while (sector >= 0) {
		if (val & 0x80000000) {
			//WMSG2("+++ allocating T=%d S=%d\n", track, sector);
			SetSectorUseEntry(track, sector, true);
			break;
		}

		val <<= 1;
		sector--;
	}
	if (sector < 0) {
		assert(false);
		return kDIErrInternal;	// should not have failed
	}

	/*
	 * Mostly for fun, update the VTOC allocation thingy.
	 */
	fVTOC[0x30] = (unsigned char) track; // last track where alloc happened
	if (track < kVTOCTrack)
		fVTOC[0x31] = 0xff;		// descending
	else
		fVTOC[0x31] = 0x01;		// ascending

	pTS->track = (char) track;
	pTS->sector = (char) sector;

	return kDIErrNone;
}

/*
 * Create an in-use map for an empty disk.  Sets up the VTOC map only.
 *
 * If "withDOS" is set, mark the first 3 tracks as in-use.
 */
DIError
DiskFSDOS33::CreateEmptyBlockMap(bool withDOS)
{
	DIError dierr;
	long track, sector, maxTrack;

	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		return dierr;

	if (withDOS)
		maxTrack = 3;
	else
		maxTrack = 1;

	/*
	 * Set each bit individually.  Slower, but exercises standard functions.
	 *
	 * Clear all "in use" flags, except for track 0, track 17, and (if
	 * withDOS is set) tracks 1 and 2.
	 */
	for (track = fpImg->GetNumTracks()-1; track >= 0; track--) {
		for (sector = fpImg->GetNumSectPerTrack()-1; sector >= 0; sector--) {
			if (track < maxTrack || track == kVTOCTrack)
				SetSectorUseEntry(track, sector, true);
			else
				SetSectorUseEntry(track, sector, false);
		}
	}

	dierr = SaveVolBitmap();
	FreeVolBitmap();
	if (dierr != kDIErrNone)
		return dierr;

	return kDIErrNone;
}

/*
 * Get the state of an entry in the VTOC sector use map.
 *
 * Returns "true" if it's in use, "false" otherwise.
 */
bool
DiskFSDOS33::GetSectorUseEntry(long track, int sector) const
{
	assert(fVTOCLoaded);
	assert(track >= 0 && track < fpImg->GetNumTracks());
	assert(sector >= 0 && sector < fpImg->GetNumSectPerTrack());

	unsigned long val, mask;

	val = GetVTOCEntry(fVTOC, track);
	//val = (unsigned long) fVTOC[0x38 + track*4] << 24;
	//val |= (unsigned long) fVTOC[0x39 + track*4] << 16;
	//val |= (unsigned long) fVTOC[0x3a + track*4] << 8;
	//val |= (unsigned long) fVTOC[0x3b + track*4];

	/*
	 * The highest-numbered sector is now in the high bit.  If this is a
	 * 16-sector disk, the high bit holds the state of sector 15.
	 *
	 * A '1' indicates the sector is free, '0' indicates it's in use.
	 */
	mask = 1L << (32 - fpImg->GetNumSectPerTrack() + sector);
	return (val & mask) == 0;
}

/*
 * Change the state of an entry in the VTOC sector use map.
 */
void
DiskFSDOS33::SetSectorUseEntry(long track, int sector, bool inUse)
{
	assert(fVTOCLoaded);
	assert(track >= 0 && track < fpImg->GetNumTracks());
	assert(sector >= 0 && sector < fpImg->GetNumSectPerTrack());

	unsigned long val, mask;

	val = GetVTOCEntry(fVTOC, track);

	/* highest sector is always in the high bit */
	mask = 1L << (32 - fpImg->GetNumSectPerTrack() + sector);
	if (inUse)
		val &= ~mask;
	else
		val |= mask;

	fVTOC[0x38 + track*4] = (unsigned char) (val >> 24);
	fVTOC[0x39 + track*4] = (unsigned char) (val >> 16);
	fVTOC[0x3a + track*4] = (unsigned char) (val >> 8);
	fVTOC[0x3b + track*4] = (unsigned char) val;
}


/*
 * Get the amount of free space remaining.
 */
DIError
DiskFSDOS33::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits,
	int* pUnitSize) const
{
	DIError dierr;
	long track, sector, freeSectors;

	dierr = const_cast<DiskFSDOS33*>(this)->LoadVolBitmap();
	if (dierr != kDIErrNone)
		return dierr;

	freeSectors = 0;
	for (track = GetDiskImg()->GetNumTracks()-1; track >= 0; track--) {
		for (sector = GetDiskImg()->GetNumSectPerTrack()-1; sector >= 0; sector--)
		{
			if (!GetSectorUseEntry(track, sector))
				freeSectors++;
		}
	}

	*pTotalUnits = fpImg->GetNumTracks() * fpImg->GetNumSectPerTrack();
	*pFreeUnits = freeSectors;
	*pUnitSize = kSectorSize;

	const_cast<DiskFSDOS33*>(this)->FreeVolBitmap();
	return kDIErrNone;
}


/*
 * Fix up the DOS tracks.
 *
 * Any sectors marked used but not actually in use by a file are marked as
 * in use by the system.  We have to be somewhat careful here because some
 * disks had DOS removed to add space, un-set the last few sectors of track 2
 * that weren't actually used by DOS, or did some other funky thing.
 */
void
DiskFSDOS33::FixVolumeUsageMap(void)
{
	VolumeUsage::ChunkState cstate;
	int track, sector;

	for (track = 0; track < 3; track++) {
		for (sector = 0; sector < fpImg->GetNumSectPerTrack(); sector++) {
			fVolumeUsage.GetChunkState(track, sector, &cstate);
			if (cstate.isMarkedUsed && !cstate.isUsed) {
				cstate.isUsed = true;
				cstate.purpose = VolumeUsage::kChunkPurposeSystem;
				fVolumeUsage.SetChunkState(track, sector, &cstate);
			}
		}
	}
}


/*
 * Read the disk's catalog.
 *
 * NOTE: supposedly DOS stops reading the catalog track when it finds the
 * first entry with a 00 byte, which is why deleted files use ff.  If so,
 * it *might* make sense to mimic this behavior, though on a health disk
 * we shouldn't be finding garbage anyway.
 *
 * Fills out "fCatalogSectors" as it works.
 */
DIError
DiskFSDOS33::ReadCatalog(void)
{
	DIError dierr = kDIErrNone;
	unsigned char sctBuf[kSctSize];
	int catTrack, catSect;
	int iterations;

	catTrack = fFirstCatTrack;
	catSect = fFirstCatSector;
	iterations = 0;

	memset(fCatalogSectors, 0, sizeof(fCatalogSectors));

	while (catTrack != 0 && catSect != 0 && iterations < kMaxCatalogSectors)
	{
		SetSectorUsage(catTrack, catSect, VolumeUsage::kChunkPurposeVolumeDir);

		WMSG2(" DOS33 reading catalog sector T=%d S=%d\n", catTrack, catSect);
		dierr = fpImg->ReadTrackSector(catTrack, catSect, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		/*
		 * Watch for flaws that the DOS detector allows.
		 */
		if (catTrack == sctBuf[0x01] && catSect == sctBuf[0x02]) {
			WMSG2(" DOS detected self-reference on cat (%d,%d)\n",
				catTrack, catSect);
			break;
		}

		/*
		 * Check the next track/sector in the chain.  If the pointer is
		 * broken, there's a very good chance that this isn't really a
		 * catalog sector, so we want to bail out now.
		 */
		if (sctBuf[0x01] >= fpImg->GetNumTracks() ||
			sctBuf[0x02] >= fpImg->GetNumSectPerTrack())
		{
			WMSG0(" DOS bailing out early on catalog read due to funky T/S\n");
			break;
		}

		dierr = ProcessCatalogSector(catTrack, catSect, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		fCatalogSectors[iterations].track = catTrack;
		fCatalogSectors[iterations].sector = catSect;

		catTrack = sctBuf[0x01];
		catSect = sctBuf[0x02];

		iterations++;		// watch for infinite loops

	}
	if (iterations >= kMaxCatalogSectors) {
		dierr = kDIErrDirectoryLoop;
		goto bail;
	}

bail:
	return dierr;
}

/*
 * Process the list of files in one sector of the catalog.
 *
 * Pass in the track, sector, and the contents of that track and sector.
 * (We only use "catTrack" and "catSect" to fill out some fields.)
 */
DIError
DiskFSDOS33::ProcessCatalogSector(int catTrack, int catSect,
	const unsigned char* sctBuf)
{
	A2FileDOS* pFile;
	const unsigned char* pEntry;
	int i;

	pEntry = &sctBuf[kCatalogEntryOffset];

	for (i = 0; i < kCatalogEntriesPerSect; i++) {
		if (pEntry[0x00] != kEntryUnused && pEntry[0x00] != kEntryDeleted) {
			pFile = new A2FileDOS(this);

			pFile->SetQuality(A2File::kQualityGood);

			pFile->fTSListTrack = pEntry[0x00];
			pFile->fTSListSector = pEntry[0x01];
			pFile->fLocked = (pEntry[0x02] & 0x80) != 0;
			switch (pEntry[0x02] & 0x7f) {
			case 0x00:	pFile->fFileType = A2FileDOS::kTypeText;		break;
			case 0x01:	pFile->fFileType = A2FileDOS::kTypeInteger;		break;
			case 0x02:	pFile->fFileType = A2FileDOS::kTypeApplesoft;	break;
			case 0x04:	pFile->fFileType = A2FileDOS::kTypeBinary;		break;
			case 0x08:	pFile->fFileType = A2FileDOS::kTypeS;			break;
			case 0x10:	pFile->fFileType = A2FileDOS::kTypeReloc;		break;
			case 0x20:	pFile->fFileType = A2FileDOS::kTypeA;			break;
			case 0x40:	pFile->fFileType = A2FileDOS::kTypeB;			break;
			default:
				/* some odd arrangement of bit flags? */
				WMSG1(" DOS33 peculiar filetype byte 0x%02x\n", pEntry[0x02]);
				pFile->fFileType = A2FileDOS::kTypeUnknown;
				pFile->SetQuality(A2File::kQualitySuspicious);
				break;
			}

			memcpy(pFile->fFileName, &pEntry[0x03], A2FileDOS::kMaxFileName);
			pFile->fFileName[A2FileDOS::kMaxFileName] = '\0';
			pFile->FixFilename();

			pFile->fLengthInSectors = pEntry[0x21];
			pFile->fLengthInSectors |= (unsigned short) pEntry[0x22] << 8;

			pFile->fCatTS.track = catTrack;
			pFile->fCatTS.sector = catSect;
			pFile->fCatEntryNum = i;

			/* can't do these yet, so just set to defaults */
			pFile->fLength = 0;
			pFile->fSparseLength = 0;
			pFile->fDataOffset = 0;

			AddFileToList(pFile);
		}

		pEntry += kCatalogEntrySize;
	}

	return kDIErrNone;
}


/*
 * Perform consistency checks on the filesystem.
 *
 * Returns "true" if disk appears to be perfect, "false" otherwise.
 */
bool
DiskFSDOS33::CheckDiskIsGood(void)
{
	DIError dierr;
	const DiskImg* pDiskImg = GetDiskImg();
	bool result = true;
	int i;

	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Make sure the VTOC is marked in use, or things could go badly.
	 * Ditto for the catalog tracks.
	 */
	if (!GetSectorUseEntry(kVTOCTrack, kVTOCSector)) {
		fpImg->AddNote(DiskImg::kNoteWarning, "VTOC sector marked as free.");
		result = false;
	}
	for (i = 0; i < kMaxCatalogSectors; i++) {
		if (!GetSectorUseEntry(fCatalogSectors[i].track,
			fCatalogSectors[i].sector))
		{
			fpImg->AddNote(DiskImg::kNoteWarning,
				"Catalog sector %d,%d is marked as free.",
				fCatalogSectors[i].track, fCatalogSectors[i].sector);
			result = false;
		}
	}

	/*
	 * Check for used blocks that aren't marked in-use.
	 *
	 * This requires that VolumeUsage be accurate.  Since this function is
	 * only run during initial startup, any later deviation between VU and
	 * the block use map is irrelevant.
	 */
	VolumeUsage::ChunkState cstate;
	long track, sector;
	long notMarked, extraUsed, conflicts;
	notMarked = extraUsed = conflicts = 0;
	for (track = 0; track < pDiskImg->GetNumTracks(); track++) {
		for (sector = 0; sector < pDiskImg->GetNumSectPerTrack(); sector++) {
			dierr = fVolumeUsage.GetChunkState(track, sector, &cstate);
			if (dierr != kDIErrNone) {
				fpImg->AddNote(DiskImg::kNoteWarning,
					"Internal volume usage error on t=%ld s=%ld.",
					track, sector);
				result = false;
				goto bail;
			}

			if (cstate.isUsed && !cstate.isMarkedUsed)
				notMarked++;
			if (!cstate.isUsed && cstate.isMarkedUsed)
				extraUsed++;
			if (cstate.purpose == VolumeUsage::kChunkPurposeConflict)
				conflicts++;
		}
	}
	if (extraUsed > 0) {
		fpImg->AddNote(DiskImg::kNoteInfo,
			"%ld sector%s marked used but not part of any file.",
			extraUsed, extraUsed == 1 ? " is" : "s are");
		// not a problem, really
	}
	if (notMarked > 0) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"%ld sector%s used by files but not marked used.",
			notMarked, notMarked == 1 ? " is" : "s are");
		result = false;
	}
	if (conflicts > 0) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"%ld sector%s used by more than one file.",
			conflicts, conflicts == 1 ? " is" : "s are");
		result = false;
	}

	/*
	 * Scan for "damaged" files or "suspicious" files diagnosed earlier.
	 */
	bool damaged, suspicious;
	ScanForDamagedFiles(&damaged, &suspicious);

	if (damaged) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"One or more files are damaged.");
		result = false;
	} else if (suspicious) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"One or more files look suspicious.");
		result = false;
	}

bail:
	FreeVolBitmap();
	return result;
}


/*
 * Run through our list of files, computing the lengths and marking file
 * usage in the VolumeUsage object.
 */
DIError
DiskFSDOS33::GetFileLengths(void)
{
	A2FileDOS* pFile;
	TrackSector* tsList = nil;
	TrackSector* indexList = nil;
	int tsCount;
	int indexCount;

	pFile = (A2FileDOS*) GetNextFile(nil);
	while (pFile != nil) {
		DIError dierr;
		dierr = pFile->LoadTSList(&tsList, &tsCount, &indexList, &indexCount);
		if (dierr != kDIErrNone) {
			WMSG1("DOS failed loading TS list for '%s'\n",
				pFile->GetPathName());
			pFile->SetQuality(A2File::kQualityDamaged);
		} else {
			MarkFileUsage(pFile, tsList, tsCount, indexList, indexCount);
			dierr = ComputeLength(pFile, tsList, tsCount);
			if (dierr != kDIErrNone) {
				WMSG1("DOS unable to get length for '%s'\n",
					pFile->GetPathName());
				pFile->SetQuality(A2File::kQualityDamaged);
			}
		}

		if (pFile->fLengthInSectors != indexCount + tsCount) {
			WMSG3("DOS NOTE: file '%s' has len-in-sect=%d but actual=%d\n",
				pFile->GetPathName(), pFile->fLengthInSectors,
				indexCount + tsCount);
			// expected on sparse random-access text files
		}

		delete[] tsList;
		delete[] indexList;
		tsList = indexList = nil;

		pFile = (A2FileDOS*) GetNextFile(pFile);
	}

	return kDIErrNone;
}

/*
 * Compute the length and starting data offset of the file.
 *
 * For Text, there are two situations: sequential and random.  For
 * sequential text files, we just need to find the first 00 byte.  For
 * random, there can be 00s everywhere, and in fact there can be holes
 * in the T/S list.  The plan: since DOS doesn't let you "truncate" a
 * text file, just scan the last sector for 00.  The length is the
 * number of previous T/S entries * 256 plus the sector offset.
 * --> This does the wrong thing for random-access text files, which
 * need to retain their full length, and doesn't work right for sequential
 * text files that (somehow) had their last block over-allocated.  It does
 * the right thing most of the time, but we either need to be more clever
 * here or provide a way to override the default (bool fTrimTextFiles?).
 *
 * For Applesoft and Integer, the file length is stored as the first two
 * bytes of the file.
 *
 * For Binary, the file length is stored in the second two bytes (after
 * the two-byte address).  Some files (with low-memory loaders) used a
 * fake length, and DDD 2.x sets both address and length to zero.
 *
 * For Reloc, S, A2, B2, and "unknown", we just multiply the sector count.
 * We get an accurate sector count from the T/S list (the value in the
 * directory entry might have been tampered with).
 *
 * To handle DDD 2.x files correctly, we need to identify them as such by
 * looking for 'B' with address=0 and length=0, a T/S count of at least 8
 * (the smallest possible compression of a 35-track disk is 2385 bytes),
 * and a '<' in the filename.  If found, we start from offset=0
 * (because DDD Pro 1.x includes the 4 leading bytes) and include all
 * sectors, we'll get the actual file plus at most 256 garbage bytes.
 *
 * On success, we set the following:
 *	pFile->fLength
 *	pFile->fSparseLength
 *	pFile->fDataOffset
 */
DIError
DiskFSDOS33::ComputeLength(A2FileDOS* pFile, const TrackSector* tsList,
	int tsCount)
{
	DIError dierr = kDIErrNone;
	unsigned char sctBuf[kSctSize];

	assert(pFile != nil);
	assert(tsList != nil);
	assert(tsCount >= 0);

	pFile->fDataOffset = 0;

	pFile->fAuxType = 0;
	if (pFile->fFileType == A2FileDOS::kTypeApplesoft)
		pFile->fAuxType = 0x0801;
	/* for text files it's default record length; assume zero */

	if (tsCount == 0) {
		/* no data at all */
		pFile->fLength = 0;
	} else if (pFile->fFileType == A2FileDOS::kTypeApplesoft ||
		pFile->fFileType == A2FileDOS::kTypeInteger ||
		pFile->fFileType == A2FileDOS::kTypeBinary)
	{
		/* read first sector and analyze it */
		//WMSG0(" DOS reading first file sector\n");
		dierr = fpImg->ReadTrackSector(tsList[0].track, tsList[0].sector,
					sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		if (pFile->fFileType == A2FileDOS::kTypeBinary) {
			pFile->fAuxType =
				sctBuf[0x00] | (unsigned short) sctBuf[0x01] << 8;
			pFile->fLength =
				sctBuf[0x02] | (unsigned short) sctBuf[0x03] << 8;
			pFile->fDataOffset = 4;	// take the above into account
		} else {
			pFile->fLength =
				sctBuf[0x00] | (unsigned short) sctBuf[0x01] << 8;
			pFile->fDataOffset = 2;	// take the above into account
		}

		if (pFile->fFileType == A2FileDOS::kTypeBinary &&
			pFile->fLength == 0 && pFile->fAuxType == 0 &&
			tsCount >= 8 &&
			strchr(pFile->fFileName, '<') != nil &&
			strchr(pFile->fFileName, '>') != nil)
		{
			WMSG2(" DOS found probable DDD archive, tweaking '%s' (lis=%u)\n",
				pFile->GetPathName(), pFile->fLengthInSectors);
			//dierr = TrimLastSectorDown(pFile, tsBuf, WrapperDDD::kMaxDDDZeroCount);
			//if (dierr != kDIErrNone)
			//	goto bail;
			//WMSG3(" DOS scanned DDD file '%s' to length %ld (tsCount=%d)\n",
			//	pFile->fFileName, pFile->fLength, pFile->fTSListCount);
			pFile->fLength = tsCount * kSctSize;
			pFile->fDataOffset = 0;
		}

		/* catch bogus lengths in damaged A/I/B files */
		if (pFile->fLength > tsCount * kSctSize) {
			WMSG3(" DOS33 capping max len from %ld to %d in '%s'\n",
				(long) pFile->fLength, tsCount * kSctSize,
				pFile->fFileName);
			pFile->fLength = tsCount * kSctSize - pFile->fDataOffset;
			if (pFile->fLength < 0)		// can't happen here?
				pFile->fLength = 0;

			/*
			 * This could cause a problem, because if the user changes a 'T'
			 * file to 'B', the bogus file length will mark the file as
			 * "suspicious" and we won't allow writing to the disk (which
			 * makes it hard to switch the file type back).  We really don't
			 * want to weaken this test though.
			 */
			pFile->SetQuality(A2File::kQualitySuspicious);
		}

	} else if (pFile->fFileType == A2FileDOS::kTypeText) {
		/* scan text file */
		pFile->fLength = tsCount * kSctSize;
		dierr = TrimLastSectorUp(pFile, tsList[tsCount-1]);
		if (dierr != kDIErrNone)
			goto bail;

		WMSG4(" DOS scanned text file '%s' down to %d+%ld = %ld\n",
			pFile->fFileName,
			(tsCount-1) * kSctSize,
			(long)pFile->fLength - (tsCount-1) * kSctSize,
			(long)pFile->fLength);

		/* TO DO: something clever to discern random access record length? */
	} else {
		/* S/R/A/B: just use the TS count */
		pFile->fLength = tsCount * kSctSize;
	}

	/*
	 * Compute the sparse length for random-access text files.
	 */
	int i, sparseCount;
	sparseCount = 0;
	for (i = 0; i < tsCount; i++) {
		if (tsList[i].track == 0 && tsList[i].sector == 0)
			sparseCount++;
	}
	pFile->fSparseLength = pFile->fLength - sparseCount * kSctSize;
	if (pFile->fSparseLength == -pFile->fDataOffset) {
		/*
		 * This can happen for a completely sparse file.  Looks sort of
		 * stupid to have a length of "-4", so force it to zero.
		 */
		pFile->fSparseLength = 0;
	}

bail:
	return dierr;
}

/*
 * Trim the zeroes off the end of the last sector.  We begin at the start
 * of the sector and stop at the first zero found.
 *
 * Modifies pFile->fLength, which should be set to a roughly accurate
 * value on entry.
 *
 * The caller should endeavor to strip out T=0 S=0 entries that come after
 * the body of the file.  They're valid in the middle for random-access
 * text files.
 */
DIError
DiskFSDOS33::TrimLastSectorUp(A2FileDOS* pFile, TrackSector lastTS)
{
	DIError dierr;
	unsigned char sctBuf[kSctSize];
    int i;

	if (lastTS.track == 0) {
		/* happens on files with lots of "sparse" space at the end */
		return kDIErrNone;
	}

	//WMSG0(" DOS reading LAST file sector\n");
	dierr = fpImg->ReadTrackSector(lastTS.track, lastTS.sector, sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/* start with EOF equal to previous sectors */
	pFile->fLength -= kSctSize;
	for (i = 0; i < kSctSize; i++) {
		if (sctBuf[i] == 0x00)
			break;
		else
			pFile->fLength++;
	}

bail:
	return dierr;
}

/*
 * Given lists of tracks and sector for data and TS index sectors, set the
 * entries in the volume usage map.
 */
void
DiskFSDOS33::MarkFileUsage(A2FileDOS* pFile, TrackSector* tsList, int tsCount,
	TrackSector* indexList, int indexCount)
{
	int i;

	for (i = 0; i < tsCount; i++) {
		/* mark all sectors as in-use by file */
		if (tsList[i].track == 0 && tsList[i].sector == 0) {
			/* sparse sector in random-access text file */
		} else {
			SetSectorUsage(tsList[i].track, tsList[i].sector,
				VolumeUsage::kChunkPurposeUserData);
		}
	}

	for (i = 0; i < indexCount; i++) {
		/* mark the T/S sectors as in-use by file structures */
		SetSectorUsage(indexList[i].track, indexList[i].sector,
			VolumeUsage::kChunkPurposeFileStruct);
	}
}



#if 0
/*
 * Trim the zeroes off the end of the last sector.  We begin at the end
 * of the sector and back up.
 *
 * It is possible (one out of between 128 and 256 times) that we have just
 * the trailing zero in this sector, and we need to back up to the previous
 * sector to find the actual end.  We know a file can end with three zeroes
 * and we suspect it might be possible to end with four, which means we could
 * have between 0 and 3 zeroes in the previous sector, and between 1 and 4
 * in this sector.  If we just tack on three more zeroes, we weaken our
 * length test slightly, because we must allow a "slop" of up to seven bytes.
 * It's a little more work, but scanning the next-to-last sector is probably
 * worthwhile given the otherwise flaky nature of DDD storage.
 */
DIError
DiskFSDOS33::TrimLastSectorDown(A2FileDOS* pFile, unsigned short* tsBuf,
	int maxZeroCount)
{
	DIError dierr;
	unsigned char sctBuf[kSctSize];
    int i;

	//WMSG0(" DOS reading LAST file sector\n");
	dierr = fpImg->ReadTrackSector(
				pFile->TSTrack(tsBuf[pFile->fTSListCount-1]),
				pFile->TSSector(tsBuf[pFile->fTSListCount-1]),
				sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/* find the first trailing zero by finding the last non-zero */
	for (i = kSctSize-1; i >= 0; i--) {
		if (sctBuf[i] != 0x00)
			break;
	}
	if (i < 0) {
		/* sector was nothing but zeroes */
		DebugBreak();
	} else {
		/* peg it at 256; if it went over that, DDD would've added a sector */
		i += maxZeroCount;
		if (i > kSctSize)
			i = kSctSize;
		pFile->fLength = (pFile->fTSListCount-1) * kSctSize + i;
	}

bail:
	return dierr;
}
#endif


/*
 * Convert high ASCII to low ASCII.
 *
 * Some people put inverse and flashing text into filenames, not to mention
 * control characters, so we have to cope with those too.
 *
 * We modify the first "len" bytes of "buf" in place.
 */
/*static*/ void
DiskFSDOS33::LowerASCII(unsigned char* buf, long len)
{
	while (len--) {
		if (*buf & 0x80) {
			if (*buf >= 0xa0)
				*buf &= 0x7f;
			else
				*buf = (*buf & 0x7f) + 0x20;
		} else
			*buf = ((*buf & 0x3f) ^ 0x20) + 0x20;

        buf++;
	}
}


/*
 * Determine whether or not "name" is a valid DOS 3.3 filename.
 *
 * Names can be up to 30 characters and can contain absolutely anything.
 * To make life easier on DOS users, we ban the use of the comma, block
 * control characters and high ASCII, and don't allow completely blank
 * names.  Later on we will later convert to upper case, so we allow lower
 * case letters here.
 *
 * Filenames simply pad out to 30 characters with spaces, so the only
 * "invalid" character is a trailing space.  Because we're using C-style
 * strings, we implicitly ban the use of '\0' in the name.
 */
/*static*/ bool
DiskFSDOS33::IsValidFileName(const char* name)
{
	bool nonSpace = false;
	int len = 0;

	/* count letters, skipping control chars */
	while (*name != '\0') {
		char ch = *name++;

		if (ch < 0x20 || ch >= 0x7f || ch == ',')
			return false;
		if (ch != 0x20)
			nonSpace = true;
		len++;
	}
	if (len == 0 || len > A2FileDOS::kMaxFileName)
		return false;		// can't be empty, can't be huge
	if (!nonSpace)
		return false;		// must have one non-ctrl non-space char
	if (*(name-1) == ' ')
		return false;		// no trailing spaces

	return true;
}

/*
 * Determine whether "name" is a valid volume number.
 */
/*static*/ bool
DiskFSDOS33::IsValidVolumeName(const char* name)
{
	long val;
	char* endp;

	val = strtol(name, &endp, 10);
	if (*endp != '\0' || val < 1 || val > 254)
		return false;

	return true;
}


/*
 * Put a DOS 3.2/3.3 filesystem image on the specified DiskImg.
 *
 * If "volName" is "DOS", a basic DOS image will be written to the first three
 * tracks of the disk, and the in-use map will be updated appropriately.
 *
 * It would seem at first glance that putting the volume number into the
 * volume name string would make the interface more consistent with the
 * rest of the filesystems.  The first glance is substantially correct, but
 * the DOS stuff has a separate "set volume number" interface already, used
 * to deal with the various locations where volume numbers can be stored
 * (2MG header, VTOC, sector address headers) in the various formats.
 *
 * So, instead of stuffing the volume number into "volName" and creating
 * some other path for specifying "add DOS image", I continue to use the
 * defined ways of setting the volume number and abuse "volName" slightly.
 */
DIError
DiskFSDOS33::Format(DiskImg* pDiskImg, const char* volName)
{
	DIError dierr = kDIErrNone;
	unsigned char sctBuf[256];
	bool addDOS = false;

	if (pDiskImg->GetNumTracks() < kMinTracks ||
		pDiskImg->GetNumTracks() > kMaxTracks)
	{
        WMSG1(" DOS33 can't format numTracks=%ld\n", pDiskImg->GetNumTracks());
		return kDIErrInvalidArg;
	}
	if (pDiskImg->GetNumSectPerTrack() != 13 &&
		pDiskImg->GetNumSectPerTrack() != 16 &&
		pDiskImg->GetNumSectPerTrack() != 32)
	{
        WMSG1(" DOS33 can't format sectors=%d\n",
            pDiskImg->GetNumSectPerTrack());
		return kDIErrInvalidArg;
	}

	if (volName != nil && strcmp(volName, "DOS") == 0) {
		if (pDiskImg->GetNumSectPerTrack() != 16 &&
			pDiskImg->GetNumSectPerTrack() != 13)
		{
			WMSG1("NOTE: numSectPerTrack = %d, can't write DOS tracks\n",
				pDiskImg->GetNumSectPerTrack());
			return kDIErrInvalidArg;
		}
		addDOS = true;
	}

	/* set fpImg so calls that rely on it will work; we un-set it later */
	assert(fpImg == nil);
	SetDiskImg(pDiskImg);

	WMSG1(" DOS33 formatting disk image (sectorOrder=%d)\n",
		fpImg->GetSectorOrder());

	/* write DOS sectors */
	dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(),
				DiskImg::kFormatGenericDOSOrd, fpImg->GetSectorOrder());
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * We should now zero out the disk blocks, but on a 32MB volume that can
	 * take a little while.  The blocks are zeroed for us when a disk is
	 * created, so this is really only needed if we're re-formatting an
	 * existing disk.  CiderPress currently doesn't do that, so we're going
	 * to skip it here.
	 */
//	dierr = fpImg->ZeroImage();
	WMSG0(" DOS33  (not zeroing blocks)\n");

	if (addDOS) {
		dierr = WriteDOSTracks(pDiskImg->GetNumSectPerTrack());
		if (dierr != kDIErrNone)
			goto bail;
	}

	/*
	 * Set up the static fields in the VTOC.
	 */
	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;
	fVTOC[0x00] = 0x04;								// (no reason)
	fVTOC[0x01] = kVTOCTrack;						// first cat track
	fVTOC[0x02] = fpImg->GetNumSectPerTrack()-1;	// first cat sector
	fVTOC[0x03] = 3;								// version
	if (fpImg->GetDOSVolumeNum() == DiskImg::kVolumeNumNotSet)
		fVTOC[0x06] = kDefaultVolumeNum;			// VTOC volume number
	else
		fVTOC[0x06] = (unsigned char) fpImg->GetDOSVolumeNum();
	fVTOC[0x27] = 122;								// max T/S pairs
	fVTOC[0x30] = kVTOCTrack+1;						// last alloc
	fVTOC[0x31] = 1;								// ascending
	fVTOC[0x34] = (unsigned char)fpImg->GetNumTracks();	// #of tracks
	fVTOC[0x35] = fpImg->GetNumSectPerTrack();		// #of sectors
	fVTOC[0x36] = 0x00;								// bytes/sector (lo)
	fVTOC[0x37] = 0x01;								// bytes/sector (hi)
	if (pDiskImg->GetNumSectPerTrack() == 13) {
		// minor changes for DOS 3.2
		fVTOC[0x00] = 0x02;
		fVTOC[0x03] = 2;
	}

	dierr = SaveVolBitmap();
	FreeVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Fill the sectors in the catalog track.
	 */
	int sect;
	memset(sctBuf, 0, sizeof(sctBuf));
	sctBuf[0x01] = kVTOCTrack;
	for (sect = fpImg->GetNumSectPerTrack()-1; sect > 1; sect--) {
		sctBuf[0x02] = sect-1;

		dierr = fpImg->WriteTrackSector(kVTOCTrack, sect, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;
	}

	/*
	 * Generate the initial block usage map.  The only entries in use are
	 * right at the start of the disk.
	 */
	CreateEmptyBlockMap(addDOS);

	/* check our work, and set some object fields, by reading what we wrote */
	dierr = ReadVTOC();
	if (dierr != kDIErrNone) {
		WMSG1(" GLITCH: couldn't read header we just wrote (err=%d)\n", dierr);
		goto bail;
	}

	/* don't do this -- assume they're going to call Initialize() later */
	//ScanVolBitmap();

bail:
	SetDiskImg(nil);	// shouldn't really be set by us
	return dierr;
}

/*
 * Write a DOS image into tracks 0-2.
 *
 * This takes the number of sectors per track as an argument so we can figure
 * out which version of DOS to write.  This probably ought to be an enum so
 * we can specify various versions of DOS.
 */
DIError
DiskFSDOS33::WriteDOSTracks(int sectPerTrack)
{
	DIError dierr = kDIErrNone;
	long track, sector;
	const unsigned char* buf = gDOS33Tracks;

	if (sectPerTrack == 13) {
		WMSG0("  DOS33 writing DOS 3.3 tracks\n");
		buf = gDOS32Tracks;

		for (track = 0; track < 3; track++) {
			for (sector = 0; sector < 13; sector++) {
				dierr = fpImg->WriteTrackSector(track, sector, buf);
				if (dierr != kDIErrNone)
					goto bail;
				buf += kSctSize;
			}
		}
	} else if (sectPerTrack == 16) {
		WMSG0("  DOS33 writing DOS 3.3 tracks\n");
		buf = gDOS33Tracks;

		// this should be used for 32-sector disks

		for (track = 0; track < 3; track++) {
			for (sector = 0; sector < 16; sector++) {
				dierr = fpImg->WriteTrackSector(track, sector, buf);
				if (dierr != kDIErrNone)
					goto bail;
				buf += kSctSize;
			}
		}
	} else {
		WMSG1("  DOS33 *not* writing DOS tracks to %d-sector disk\n",
			sectPerTrack);
		assert(false);
	}

bail:
	return dierr;
}

/*
 * Normalize a DOS 3.3 path.  Used when adding files from DiskArchive.
 * The path may contain subdirectory components, which we need to strip away.
 *
 * "*pNormalizedBufLen" is used to pass in the length of the buffer and
 * pass out the length of the string (should the buffer prove inadequate).
 */
DIError
DiskFSDOS33::NormalizePath(const char* path, char fssep,
	char* normalizedBuf, int* pNormalizedBufLen)
{
	DIError dierr = kDIErrNone;
	char tmpBuf[A2FileDOS::kMaxFileName+1];
	int len;

	DoNormalizePath(path, fssep, tmpBuf);
	len = strlen(tmpBuf)+1;

	if (*pNormalizedBufLen < len)
		dierr = kDIErrDataOverrun;
	else
		strcpy(normalizedBuf, tmpBuf);
	*pNormalizedBufLen = len;

	return dierr;
}

/*
 * Normalize a DOS 3.3 pathname.  Lower case becomes upper case, control
 * characters and high ASCII get stripped, and ',' becomes '_'.
 *
 * "outBuf" must be able to hold kMaxFileName+1 characters.
 */
void
DiskFSDOS33::DoNormalizePath(const char* name, char fssep, char* outBuf)
{
	char* outp = outBuf;
	char* cp;

	/* throw out leading pathname, if any */
	if (fssep != '\0') {
		cp = strrchr(name, fssep);
		if (cp != nil)
			name = cp+1;
	}

	while (*name != '\0' && (outp - outBuf) <= A2FileDOS::kMaxFileName) {
		if (*name >= 0x20 && *name < 0x7f) {
			if (*name == ',')
				*outp = '_';
			else
				*outp = toupper(*name);

			outp++;
		}
		name++;
	}
	*outp = '\0';

	if (*outBuf == '\0') {
		/* nothing left */
		strcpy(outBuf, "BLANK");
	}
}

/*
 * Create a file on a DOS 3.2/3.3 disk.
 *
 * The file will be created with an empty T/S list.
 *
 * It is not possible to set the aux type here.  Aux types only apply to 'B'
 * files, and since they're stored in the first data sector (which we don't
 * create), there's nowhere to put it.  We stuff it into the aux type value
 * in the linear file list, on the assumption that somebody will come along
 * and politely Write to the file, even if it's zero bytes long.
 *
 * (Technically speaking, setting the file type here is bogus, because a
 * 'B' file with no data sectors is invalid.  However, we don't want to
 * handle arbitrary changes later -- switching from 'T' to 'B' requires
 * either rewriting the entire file, or confusing the user by changing the
 * type without adjusting the first 4 bytes -- so we set it now.  It's also
 * helpful to set it now because the Write routine needs to know how many
 * bytes offset from the start of the file it needs to be.  We could avoid
 * most of this weirdness by just going ahead and allocating the first
 * sector of the file now, and modifying the Write() function to understand
 * that the first block is already there.  Need to do that someday.)
 */
DIError
DiskFSDOS33::CreateFile(const CreateParms* pParms,	A2File** ppNewFile)
{
	DIError dierr = kDIErrNone;
	const bool createUnique = (GetParameter(kParm_CreateUnique) != 0);
	char normalName[A2FileDOS::kMaxFileName+1];
//	char storageName[A2FileDOS::kMaxFileName+1];
	A2FileDOS::FileType fileType;
	A2FileDOS* pNewFile = nil;

	if (fpImg->GetReadOnly())
		return kDIErrAccessDenied;
	if (!fDiskIsGood)
		return kDIErrBadDiskImage;

	assert(pParms != nil);
	assert(pParms->pathName != nil);
	assert(pParms->storageType == A2FileProDOS::kStorageSeedling);
	WMSG1(" DOS33 ---v--- CreateFile '%s'\n", pParms->pathName);

	*ppNewFile = nil;

	DoNormalizePath(pParms->pathName, pParms->fssep, normalName);

	/*
	 * See if the file already exists.
	 *
	 * If "create unique" is set, we append digits until the name doesn't
	 * match any others.  The name will be modified in place.
	 */
	if (createUnique) {
		MakeFileNameUnique(normalName);
	} else {
		if (GetFileByName(normalName) != nil) {
			WMSG1(" DOS33 create: normalized name '%s' already exists\n",
				normalName);
			dierr = kDIErrFileExists;
			goto bail;
		}
	}

	fileType = A2FileDOS::ConvertFileType(pParms->fileType, 0);

	/*
	 * Allocate a directory entry and T/S list.
	 */
	unsigned char sctBuf[kSctSize];
	TrackSector catSect;
	TrackSector tsSect;
	int catEntry;
	A2FileDOS* pPrevEntry;

	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	/* allocate a sector for the T/S list, and zero it out */
	dierr = AllocSector(&tsSect);
	if (dierr != kDIErrNone)
		goto bail;

	memset(sctBuf, 0, kSctSize);
	dierr = fpImg->WriteTrackSector(tsSect.track, tsSect.sector, sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Find the first free catalog entry.  Also returns a pointer to the
	 * previous entry.
	 */
	dierr = GetFreeCatalogEntry(&catSect, &catEntry, sctBuf, &pPrevEntry);
	if (dierr != kDIErrNone) {
		WMSG0("DOS unable to find an empty entry in the catalog\n");
		goto bail;
	}
	WMSG4(" DOS found free catalog entry T=%d S=%d ent=%d prev=0x%08lx\n",
		catSect.track, catSect.sector, catEntry, (long) pPrevEntry);

	/* create the new dir entry at the specified location */
	CreateDirEntry(sctBuf, catEntry, normalName, &tsSect,
		(unsigned char) fileType, pParms->access);

	/*
	 * Flush everything to disk.
	 */
	dierr = fpImg->WriteTrackSector(catSect.track, catSect.sector, sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	dierr = SaveVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Create a new entry for our file list.
	 */
	pNewFile = new A2FileDOS(this);
	if (pNewFile == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}
	pNewFile->fTSListTrack = tsSect.track;
	pNewFile->fTSListSector = tsSect.sector;
	pNewFile->fLengthInSectors = 1;
	pNewFile->fLocked = false;
	strcpy(pNewFile->fFileName, normalName);
	pNewFile->fFileType = fileType;

	pNewFile->fCatTS.track = catSect.track;
	pNewFile->fCatTS.sector = catSect.sector;
	pNewFile->fCatEntryNum = catEntry;

	pNewFile->fAuxType = (unsigned short) pParms->auxType;
	pNewFile->fDataOffset = 0;
	switch (pNewFile->fFileType) {
	case A2FileDOS::kTypeInteger:
		pNewFile->fDataOffset = 2;
		break;
	case A2FileDOS::kTypeApplesoft:
		pNewFile->fDataOffset = 2;
		pNewFile->fAuxType = 0x0801;
		break;
	case A2FileDOS::kTypeBinary:
		pNewFile->fDataOffset = 4;
		break;
	default:
		break;
	}
	pNewFile->fLength = 0;
	pNewFile->fSparseLength = 0;

	/*
	 * Insert it in the proper place, so that the order of the files matches
	 * the order of entries in the catalog.
	 */
	InsertFileInList(pNewFile, pPrevEntry);

	*ppNewFile = pNewFile;
	pNewFile = nil;

bail:
	delete pNewFile;
	FreeVolBitmap();
	return dierr;
}

/*
 * Make the name pointed to by "fileName" unique.  The name should already
 * be FS-normalized, and be in a buffer that can hold at least kMaxFileName+1
 * bytes.
 *
 * (This is nearly identical to the code in the ProDOS implementation.  I'd
 * like to make it a general DiskFS function, but making the loop condition
 * work requires setting up callbacks, which isn't hard here but is a little
 * annoying in ProDOS because of the subdir buffer.  So it's cut & paste
 * for now.)
 *
 * Returns an error on failure, which should be impossible.
 */
DIError
DiskFSDOS33::MakeFileNameUnique(char* fileName)
{
	assert(fileName != nil);
	assert(strlen(fileName) <= A2FileDOS::kMaxFileName);

	if (GetFileByName(fileName) == nil)
		return kDIErrNone;

	WMSG1(" DOS   found duplicate of '%s', making unique\n", fileName);

	int nameLen = strlen(fileName);
	int dotOffset=0, dotLen=0;
	char dotBuf[kMaxExtensionLen+1];

	/* ensure the result will be null-terminated */
	memset(fileName + nameLen, 0, (A2FileDOS::kMaxFileName - nameLen) +1);

	/*
	 * If this has what looks like a filename extension, grab it.  We want
	 * to preserve ".gif", ".c", etc.
	 */
	const char* cp = strrchr(fileName, '.');
	if (cp != nil) {
		int tmpOffset = cp - fileName;
		if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) {
			WMSG1("  DOS   (keeping extension '%s')\n", cp);
			assert(strlen(cp) <= kMaxExtensionLen);
			strcpy(dotBuf, cp);
			dotOffset = tmpOffset;
			dotLen = nameLen - dotOffset;
		}
	}

	const int kMaxDigits = 999;
	int digits = 0;
	int digitLen;
	int copyOffset;
	char digitBuf[4];
	do {
		if (digits == kMaxDigits)
			return kDIErrFileExists;
		digits++;

		/* not the most efficient way to do this, but it'll do */
		sprintf(digitBuf, "%d", digits);
		digitLen = strlen(digitBuf);
		if (nameLen + digitLen > A2FileDOS::kMaxFileName)
			copyOffset = A2FileDOS::kMaxFileName - dotLen - digitLen;
		else
			copyOffset = nameLen - dotLen;
		memcpy(fileName + copyOffset, digitBuf, digitLen);
		if (dotLen != 0)
			memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen);
	} while (GetFileByName(fileName) != nil);

	WMSG1(" DOS  converted to unique name: %s\n", fileName);

	return kDIErrNone;
}

/*
 * Find the first free entry in the catalog.
 *
 * Also returns an A2File pointer for the previous entry in the catalog.
 *
 * The contents of the catalog sector will be in "sctBuf".
 */
DIError
DiskFSDOS33::GetFreeCatalogEntry(TrackSector* pCatSect, int* pCatEntry,
	unsigned char* sctBuf, A2FileDOS** ppPrevEntry)
{
	DIError dierr = kDIErrNone;
	unsigned char* pEntry;
	int sct, ent;
	bool found = false;

	for (sct = 0; sct < kMaxCatalogSectors; sct++) {
		if (fCatalogSectors[sct].track == 0 &&
			fCatalogSectors[sct].sector == 0)
		{
			/* end of list reached */
			WMSG0("DOS catalog is full\n");
			dierr = kDIErrVolumeDirFull;
			goto bail;
		}
		dierr = fpImg->ReadTrackSector(fCatalogSectors[sct].track,
					fCatalogSectors[sct].sector, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		pEntry = &sctBuf[kCatalogEntryOffset];
		for (ent = 0; ent < kCatalogEntriesPerSect; ent++) {
			if (pEntry[0x00] == 0x00 || pEntry[0x00] == kEntryDeleted) {
				/* winner! */
				*pCatSect = fCatalogSectors[sct];
				*pCatEntry = ent;
				found = true;
				break;
			}

			pEntry += kCatalogEntrySize;
		}

		if (found)
			break;
	}

	if (sct == kMaxCatalogSectors) {
		/* didn't find anything, assume the disk is full */
		dierr = kDIErrVolumeDirFull;
		// fall through to "bail"
	} else {
		/* figure out what the previous entry is */
		TrackSector prevTS;
		int prevEntry;

		if (*pCatEntry != 0) {
			prevTS = *pCatSect;
			prevEntry = *pCatEntry -1;
		} else if (sct != 0) {
			prevTS = fCatalogSectors[sct-1];
			prevEntry = kCatalogEntriesPerSect-1;
		} else {
			/* disk was empty; there's no previous entry */
			prevTS.track = 0;
			prevTS.sector = 0;
			prevEntry = -1;
		}

		/* now find it in the linear file list */
		*ppPrevEntry = nil;
		if (prevEntry >= 0) {
			A2FileDOS* pFile = (A2FileDOS*) GetNextFile(nil);
			while (pFile != nil) {
				if (pFile->fCatTS.track == prevTS.track &&
					pFile->fCatTS.sector == prevTS.sector &&
					pFile->fCatEntryNum == prevEntry)
				{
					*ppPrevEntry = pFile;
					break;
				}
				pFile = (A2FileDOS*) GetNextFile(pFile);
			}
			assert(*ppPrevEntry != nil);
		}
	}

bail:
	return dierr;
}

/*
 * Fill out the catalog entry in the location specified.
 */
void
DiskFSDOS33::CreateDirEntry(unsigned char* sctBuf, int catEntry,
	const char* fileName, TrackSector* pTSSect, unsigned char fileType,
	int access)
{
	char highName[A2FileDOS::kMaxFileName+1];
	unsigned char* pEntry;

	pEntry = GetCatalogEntryPtr(sctBuf, catEntry);
	if (pEntry[0x00] != 0x00 && pEntry[0x00] != kEntryDeleted) {
		/* somebody screwed up */
		assert(false);
		return;
	}

	A2FileDOS::MakeDOSName(highName, fileName);

	pEntry[0x00] = pTSSect->track;
	pEntry[0x01] = pTSSect->sector;
	pEntry[0x02] = fileType;
	if ((access & A2FileProDOS::kAccessWrite) == 0)
		pEntry[0x02] |= (unsigned char) A2FileDOS::kTypeLocked;
	memcpy(&pEntry[0x03], highName, A2FileDOS::kMaxFileName);
	PutShortLE(&pEntry[0x21], 1);		// assume file is 1 sector long
}

/*
 * Delete a file.
 *
 * This entails freeing up the allocated sectors and changing a byte in
 * the directory entry.  We then remove it from the DiskFS file list.
 */
DIError
DiskFSDOS33::DeleteFile(A2File* pGenericFile)
{
	DIError dierr = kDIErrNone;
	A2FileDOS* pFile = (A2FileDOS*) pGenericFile;
	TrackSector* tsList = nil;
	TrackSector* indexList = nil;
	int tsCount, indexCount;
	unsigned char sctBuf[kSctSize];
	unsigned char* pEntry;

	if (pGenericFile == nil) {
		assert(false);
		return kDIErrInvalidArg;
	}

	if (fpImg->GetReadOnly())
		return kDIErrAccessDenied;
	if (!fDiskIsGood)
		return kDIErrBadDiskImage;
	if (pGenericFile->IsFileOpen())
		return kDIErrFileOpen;

	WMSG1("    Deleting '%s'\n", pFile->GetPathName());

	/*
	 * Update the block usage map.  Nothing is permanent until we flush
	 * the data to disk.
	 */
	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	dierr = pFile->LoadTSList(&tsList, &tsCount, &indexList, &indexCount);
	if (dierr != kDIErrNone) {
		WMSG1("Failed loading TS lists while deleting '%s'\n",
			pFile->GetPathName());
		goto bail;
	}

	FreeTrackSectors(tsList, tsCount);
	FreeTrackSectors(indexList, indexCount);

	/*
	 * Mark the entry as deleted.
	 */
	dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
				sctBuf);
	if (dierr != kDIErrNone)
		goto bail;
	pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);
	assert(pEntry[0x00] != 0x00 && pEntry[0x00] != kEntryDeleted);
	pEntry[0x00] = kEntryDeleted;
	dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
				sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Save our updated copy of the volume bitmap to disk.
	 */
	dierr = SaveVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Remove the A2File* from the list.
	 */
	DeleteFileFromList(pFile);

bail:
	FreeVolBitmap();
	delete[] tsList;
	delete[] indexList;
	return dierr;
}

/*
 * Mark all of the track/sector entries in "pList" as free.
 */
void
DiskFSDOS33::FreeTrackSectors(TrackSector* pList, int count)
{
	VolumeUsage::ChunkState cstate;
	int i;

	cstate.isUsed = false;
	cstate.isMarkedUsed = false;
	cstate.purpose = VolumeUsage::kChunkPurposeUnknown;

	for (i = 0; i < count; i++) {
		if (pList[i].track == 0 && pList[i].sector == 0)
			continue;		// sparse file

		if (!GetSectorUseEntry(pList[i].track, pList[i].sector)) {
			WMSG2("WARNING: freeing unallocated sector T=%d S=%d\n",
				pList[i].track, pList[i].sector);
			assert(false);	// impossible unless disk is "damaged"
		}
		SetSectorUseEntry(pList[i].track, pList[i].sector, false);

		fVolumeUsage.SetChunkState(pList[i].track, pList[i].sector, &cstate);
	}
}

/*
 * Rename a file.
 *
 * "newName" must already be normalized.
 */
DIError
DiskFSDOS33::RenameFile(A2File* pGenericFile, const char* newName)
{
	DIError dierr = kDIErrNone;
	A2FileDOS* pFile = (A2FileDOS*) pGenericFile;
	char normalName[A2FileDOS::kMaxFileName+1];
	char dosName[A2FileDOS::kMaxFileName+1];
	unsigned char sctBuf[kSctSize];
	unsigned char* pEntry;

	if (pFile == nil || newName == nil)
		return kDIErrInvalidArg;
	if (!IsValidFileName(newName))
		return kDIErrInvalidArg;
	if (fpImg->GetReadOnly())
		return kDIErrAccessDenied;
	if (!fDiskIsGood)
		return kDIErrBadDiskImage;

	WMSG2(" DOS renaming '%s' to '%s'\n", pFile->GetPathName(), newName);

	/*
	 * Update the disk catalog entry.
	 */
	dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
				sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);

	DoNormalizePath(newName, '\0', normalName);
	A2FileDOS::MakeDOSName(dosName, normalName);
	memcpy(&pEntry[0x03], dosName, A2FileDOS::kMaxFileName);

	dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
				sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Update our internal copy.
	 */
	char storedName[A2FileDOS::kMaxFileName+1];
	strcpy(storedName, dosName);
	LowerASCII((unsigned char*)storedName, A2FileDOS::kMaxFileName);
	A2FileDOS::TrimTrailingSpaces(storedName);

	strcpy(pFile->fFileName, storedName);

bail:
	return dierr;
}

/*
 * Set the file's attributes.
 *
 * We allow the file to be locked or unlocked, and we allow the file type
 * to be changed.  We don't try to rewrite the file if they're changing to or
 * from a format with embedded data (e.g. BAS or BIN); instead, we just
 * change the type letter.  We do need to re-evaluate the end-of-file
 * value afterward.
 *
 * Changing the aux type is only allowed for BIN files.
 */
DIError
DiskFSDOS33::SetFileInfo(A2File* pGenericFile, long fileType, long auxType,
	long accessFlags)
{
	DIError dierr = kDIErrNone;
	A2FileDOS* pFile = (A2FileDOS*) pGenericFile;
	TrackSector* tsList = nil;
	int tsCount;
	bool nowLocked;
	bool typeChanged;

	if (pFile == nil)
		return kDIErrInvalidArg;
	if (fpImg->GetReadOnly())
		return kDIErrAccessDenied;

	WMSG4("DOS SetFileInfo '%s' type=0x%02lx aux=0x%04lx access=0x%02lx\n",
		pFile->GetPathName(), fileType, auxType, accessFlags);

	/*
	 * We can ignore the file/aux type, or we can verify that they're not
	 * trying to change it.  The latter is a little more work but makes
	 * the API a little more communicative.
	 */
	if (!A2FileDOS::IsValidType(fileType)) {
		WMSG0("DOS SetFileInfo invalid file type\n");
		dierr = kDIErrInvalidArg;
		goto bail;
	}
	if (auxType != pFile->GetAuxType() && fileType != 0x06) {
		/* this only makes sense for BIN files */
		WMSG0("DOS SetFileInfo aux type mismatch; ignoring\n");
		//dierr = kDIErrNotSupported;
		//goto bail;
	}

	nowLocked = (accessFlags & A2FileProDOS::kAccessWrite) == 0;
	typeChanged = (fileType != pFile->GetFileType());

	/*
	 * Update the file type and locked status, if necessary.
	 */
	if (nowLocked != pFile->fLocked || typeChanged) {
		A2FileDOS::FileType newFileType;
		unsigned char sctBuf[kSctSize];
		unsigned char* pEntry;

		WMSG1("Updating file '%s'\n", pFile->GetPathName());

		dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
					sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);

		newFileType = A2FileDOS::ConvertFileType(fileType, 0);
		pEntry[0x02] = (unsigned char) newFileType;
		if (nowLocked)
			pEntry[0x02] |= 0x80;

		dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
					sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		/* update our local copy */
		pFile->fLocked = nowLocked;
	}

	if (!typeChanged && auxType == pFile->GetAuxType()) {
		/* only the locked status has changed; skip the rest */
		goto bail;
	}

	/*
	 * If the file has type BIN (either because it was before and we left it
	 * alone, or we changed it to BIN), we need to figure out what the aux
	 * type should be.  There are two situations:
	 *
	 * (1) User specified an aux type.  If the aux type passed in doesn't match
	 *  what's in the A2FileDOS structure, we assume they meant to change it.
	 * (2) User didn't specify an aux type change.  If the file was BIN before,
	 *  we don't need to do anything, but if it was just changed to BIN then
	 *  we need to extract the aux type from the first sector of the file.
	 *
	 * There's also a 3rd situation: they changed the aux type for a non-BIN
	 * file.  This should have been blocked earlier.
	 *
	 * On top of all this, if we changed the file type at all then we need to
	 * re-scan the file length and "data offset" value.
	 */
	unsigned short newAuxType;
	newAuxType = (unsigned short) auxType;

	dierr = pFile->LoadTSList(&tsList, &tsCount);
	if (dierr != kDIErrNone) {
		WMSG1(" DOS SFI: unable to load TS list (err=%d)\n", dierr);
		goto bail;
	}

	if (fileType == 0x06 && tsCount > 0) {
		unsigned char sctBuf[kSctSize];

		dierr = fpImg->ReadTrackSector(tsList[0].track,
					tsList[0].sector, sctBuf);
		if (dierr != kDIErrNone) {
			WMSG0("DOS SFI: unable to get first sector of file\n");
			goto bail;
		}

		if (auxType == pFile->GetAuxType()) {
			newAuxType = GetShortLE(&sctBuf[0x00]);
			WMSG1("  Aux type not changed, extracting from file (0x%04x)\n",
				newAuxType);
		} else {
			WMSG1("  Aux type changed (to 0x%04x), changing file\n",
				newAuxType);

			PutShortLE(&sctBuf[0x00], newAuxType);
			dierr = fpImg->WriteTrackSector(tsList[0].track,
						tsList[0].sector, sctBuf);
			if (dierr != kDIErrNone) {
				WMSG0("DOS SFI: unable to write first sector of file\n");
				goto bail;
			}
		}
	} else {
		/* not BIN or file has no sectors */
		if (pFile->fFileType == A2FileDOS::kTypeApplesoft)
			newAuxType = 0x0801;
		else
			newAuxType = 0x0000;
	}

	/* update our local copy */
	pFile->fFileType = A2FileDOS::ConvertFileType(fileType, 0);
	pFile->fAuxType = newAuxType;

	/*
	 * Recalculate the file's length and "data offset".  This may also mark
	 * the file as "suspicious".  We wouldn't be here if the file was
	 * suspicious when we opened the disk image -- the image would have
	 * been marked read-only -- so if it's suspicious now, it's probably
	 * from a previous file type change attempt in the current session.
	 * Clear the flag so it doesn't "stick".
	 */
	pFile->ResetQuality();
	(void) ComputeLength(pFile, tsList, tsCount);

bail:
	delete[] tsList;
	return dierr;
}

/*
 * Change the disk volume name (number).
 *
 * We can't change the 2MG header, and we can't change the values embedded
 * in the sector headers, so all we do is change the VTOC entry.
 */
DIError
DiskFSDOS33::RenameVolume(const char* newName)
{
	DIError dierr = kDIErrNone;
	unsigned char sctBuf[kSctSize];
	long newNumber;
	char* endp;

	if (!IsValidVolumeName(newName))
		return kDIErrInvalidArg;
	if (fpImg->GetReadOnly())
		return kDIErrAccessDenied;

	// convert the number; we already ascertained that it's valid
	newNumber = strtol(newName, &endp, 10);

	dierr = fpImg->ReadTrackSector(kVTOCTrack, kVTOCSector, sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	sctBuf[0x06] = (unsigned char) newNumber;

	dierr = fpImg->WriteTrackSector(kVTOCTrack, kVTOCSector, sctBuf);
	if (dierr != kDIErrNone)
		goto bail;

	fVTOCVolumeNumber = newNumber;
	UpdateVolumeNum();

bail:
	return dierr;
}


/*
 * ===========================================================================
 *		A2FileDOS
 * ===========================================================================
 */

/*
 * Constructor.
 */
A2FileDOS::A2FileDOS(DiskFS* pDiskFS) : A2File(pDiskFS)
{
	fTSListTrack = -1;
	fTSListSector = -1;
	fLengthInSectors = 0;
	fLocked = false;
	fFileName[0] = '\0';
	fFileType = kTypeUnknown;

	fCatTS.track = fCatTS.sector = 0;
	fCatEntryNum = -1;

	fAuxType = 0;
	fDataOffset = 0;
	fLength = -1;
	fSparseLength = -1;

	fpOpenFile = nil;
}

/*
 * Destructor.  Make sure an "open" file gets "closed".
 */
A2FileDOS::~A2FileDOS(void)
{
	delete fpOpenFile;
}


/*
 * Convert the filetype enum to a ProDOS type.
 *
 * Remember that the DOS filetype field is actually a bit field, so we need
 * to handle situations where more than one bit is set.
 *
 * Ideally this is a reversible transformation, so files copied to ProDOS
 * volumes can be copied back to DOS with no loss of information.  The reverse
 * is *not* true, because of file type reduction and the potential loss of
 * accurate file length info.
 *
 * I'm not entirely certain about the conversion of 'R' to REL, largely
 * because I can't find any information on the REL format.  However, Copy ][+
 * does convert to REL, and the Binary ][ standard says I should as well.
 */
long
A2FileDOS::GetFileType(void) const
{
	long retval;

	switch (fFileType) {
	case kTypeText:			retval = 0x04;	break;	// TXT
	case kTypeInteger:		retval = 0xfa;	break;	// INT
	case kTypeApplesoft:	retval = 0xfc;	break;	// BAS
	case kTypeBinary:		retval = 0x06;	break;	// BIN
	case kTypeS:			retval = 0xf2;	break;	// $f2
	case kTypeReloc:		retval = 0xfe;	break;	// REL
	case kTypeA:			retval = 0xf3;	break;	// $f3
	case kTypeB:			retval = 0xf4;	break;	// $f4
	case kTypeUnknown:
	default:
		retval = 0x00;		// NON
		break;
	}

	return retval;
}

/*
 * Convert a ProDOS 8 file type to its DOS equivalent.
 *
 * We need to know the file length because files over 64K can't fit into
 * DOS A/I/B files.  Text files can be as long as they want, and the
 * other types don't have a length word defined, so they're fine.
 *
 * We can't just convert them later, because by that point they've already
 * got a 2-byte or 4-byte header reserved.
 *
 * Because we don't generally know the eventual length of the file at
 * the time we're creating it, this doesn't work nearly as well as could
 * be hoped.  We can make life a little less confusing for the caller by
 * using type 'S' for any unknown type.
 */
/*static*/ A2FileDOS::FileType
A2FileDOS::ConvertFileType(long prodosType, di_off_t fileLen)
{
	const long kMaxBinary = 65535;
	FileType newType;

	switch (prodosType) {
	case 0xb0:		newType = kTypeText;		break;	// SRC
	case 0x04:		newType = kTypeText;		break;	// TXT
	case 0xfa:		newType = kTypeInteger;		break;	// INT
	case 0xfc:		newType = kTypeApplesoft;	break;	// BAS
	case 0x06:		newType = kTypeBinary;		break;	// BIN
	case 0xf2:		newType = kTypeS;			break;	// $f2
	case 0xfe:		newType = kTypeReloc;		break;	// REL
	case 0xf3:		newType = kTypeA;			break;	// $f3
	case 0xf4:		newType = kTypeB;			break;	// $f4
	default:		newType = kTypeS;			break;
	}

	if (fileLen > kMaxBinary &&
		(newType == kTypeInteger || newType == kTypeApplesoft ||
		newType == kTypeBinary))
	{
		WMSG0("  DOS setting type for large A/I/B file to S\n");
		newType = kTypeS;
	}

	return newType;
}

/*
 * Determine whether the specified type has a valid DOS mapping.
 */
/*static*/ bool
A2FileDOS::IsValidType(long prodosType)
{
	switch (prodosType) {
	case 0xb0:	// SRC
	case 0x04:	// TXT
	case 0xfa:	// INT
	case 0xfc:	// BAS
	case 0x06:	// BIN
	case 0xf2:	// $f2
	case 0xfe:	// REL
	case 0xf3:	// $f3
	case 0xf4:	// $f4
		return true;
	default:
		return false;
	}
}

/*
 * Match the ProDOS equivalents of "locked" and "unlocked".
 */
long
A2FileDOS::GetAccess(void) const
{
	if (fLocked)
		return DiskFS::kFileAccessLocked;	// 0x01 read
	else
		return DiskFS::kFileAccessUnlocked;	// 0xc3 read/write/rename/destroy
}

/*
 * "Fix" a DOS3.3 filename.  Convert DOS-ASCII to normal ASCII, and strip
 * trailing spaces.
 */
void
A2FileDOS::FixFilename(void)
{
	DiskFSDOS33::LowerASCII((unsigned char*)fFileName, kMaxFileName);
	TrimTrailingSpaces(fFileName);
}

/*
 * Trim the spaces off the end of a filename.
 *
 * Assumes the filename has already been converted to low ASCII.
 */
/*static*/ void
A2FileDOS::TrimTrailingSpaces(char* filename)
{
	char* lastspc = filename + strlen(filename);

	assert(*lastspc == '\0');

	while (--lastspc) {
		if (*lastspc != ' ')
			break;
	}

	*(lastspc+1) = '\0';
}

/*
 * Encode a filename into high ASCII, padded out with spaces to
 * kMaxFileName chars.  Lower case is converted to upper case.  This
 * does not filter out control characters or other chunk.
 *
 * "buf" must be able to hold kMaxFileName+1 chars.
 */
/*static*/ void
A2FileDOS::MakeDOSName(char* buf, const char* name)
{
	for (int i = 0; i < kMaxFileName; i++) {
		if (*name == '\0')
			*buf++ = (char) 0xa0;
		else
			*buf++ = toupper(*name++) | 0x80;
	}
	*buf = '\0';
}


/*
 * Set up state for this file.
 */
DIError
A2FileDOS::Open(A2FileDescr** ppOpenFile, bool readOnly,
	bool rsrcFork /*=false*/)
{
	DIError dierr = kDIErrNone;
	A2FDDOS* pOpenFile = nil;

	if (!readOnly) {
		if (fpDiskFS->GetDiskImg()->GetReadOnly())
			return kDIErrAccessDenied;
		if (fpDiskFS->GetFSDamaged())
			return kDIErrBadDiskImage;
	}

	if (fpOpenFile != nil) {
		dierr = kDIErrAlreadyOpen;
		goto bail;
	}

	if (rsrcFork)
		return kDIErrForkNotFound;

	pOpenFile = new A2FDDOS(this);

	dierr = LoadTSList(&pOpenFile->fTSList, &pOpenFile->fTSCount,
				&pOpenFile->fIndexList, &pOpenFile->fIndexCount);
	if (dierr != kDIErrNone) {
		WMSG1("DOS33 unable to load TS for '%s' open\n", GetPathName());
		goto bail;
	}

	pOpenFile->fOffset = 0;
	pOpenFile->fOpenEOF = fLength;
	pOpenFile->fOpenSectorsUsed = fLengthInSectors;

	fpOpenFile = pOpenFile;		// add it to our single-member "open file set"
	*ppOpenFile = pOpenFile;
	pOpenFile = nil;

bail:
	delete pOpenFile;
	return dierr;
}

/*
 * Dump the contents of an A2FileDOS.
 */
void
A2FileDOS::Dump(void) const
{
	WMSG1("A2FileDOS '%s'\n", fFileName);
	WMSG2("  TS T=%-2d S=%-2d\n", fTSListTrack, fTSListSector);
	WMSG2("  Cat T=%-2d S=%-2d\n", fCatTS.track, fCatTS.sector);
	WMSG3("  type=%d lck=%d slen=%d\n", fFileType, fLocked, fLengthInSectors);
	WMSG2("  auxtype=0x%04x length=%ld\n",
		fAuxType, (long) fLength);
}


/*
 * Load the T/S list for this file.
 *
 * A single T/S sector holds 122 entries, enough to store a 30.5K file.
 * It's very unlikely that a file will need more than two, although it's
 * possible for a random-access text file to have a very large number of
 * entries.
 *
 * If "pIndexList" and "pIndexCount" are non-nil, the list of index blocks is
 * also loaded.
 *
 * It's entirely possible to get a large T/S list back that is filled
 * entirely with zeroes.  This can happen if we have a large set of T/S
 * index sectors that are all zero.  We have to leave space for them so
 * that the Write function can use the existing allocated index blocks.
 *
 * THOUGHT: we may want to use the file type to tighten this up a bit.
 * For example, we're currently very careful around random-access text
 * files, but if the file doesn't have type 'T' then random access is
 * impossible.  Currently this isn't a problem, but for e.g. T/S lists
 * with garbage at the end would could deal with the problem more generally.
 */
DIError
A2FileDOS::LoadTSList(TrackSector** pTSList, int* pTSCount,
	TrackSector** pIndexList, int* pIndexCount)
{
	DIError dierr = kDIErrNone;
	DiskImg* pDiskImg;
	const int kDefaultTSAlloc = 2;
	const int kDefaultIndexAlloc = 8;
	TrackSector* tsList = nil;
	TrackSector* indexList = nil;
	int tsCount, tsAlloc;
	int indexCount, indexAlloc;
	unsigned char sctBuf[kSctSize];
	int track, sector, iterations;

	WMSG1("--- DOS loading T/S list for '%s'\n", GetPathName());

	/* over-alloc for small files to reduce reallocs */
	tsAlloc = kMaxTSPairs * kDefaultTSAlloc;
	tsList = new TrackSector[tsAlloc];
	tsCount = 0;

	indexAlloc = kDefaultIndexAlloc;
	indexList = new TrackSector[indexAlloc];
	indexCount = 0;

	if (tsList == nil || indexList == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	assert(fpDiskFS != nil);
	pDiskImg = fpDiskFS->GetDiskImg();
	assert(pDiskImg != nil);

	/* get the first T/S sector for this file */
	track = fTSListTrack;
	sector = fTSListSector;
	if (track >= pDiskImg->GetNumTracks() ||
		sector >= pDiskImg->GetNumSectPerTrack())
	{
		WMSG3(" DOS33 invalid initial T/S %d,%d in '%s'\n", track, sector,
			fFileName);
		dierr = kDIErrBadFile;
		goto bail;
	}

	/*
	 * Run through the set of t/s pairs.
	 */
	iterations = 0;
	do {
		unsigned short sectorOffset;
		int lastNonZero;

		/*
		 * Add the current T/S sector to the index list.
		 */
		if (indexCount == indexAlloc) {
			WMSG0("+++ expanding index list\n");
			TrackSector* newList;
			indexAlloc += kDefaultIndexAlloc;
			newList = new TrackSector[indexAlloc];
			if (newList == nil) {
				dierr = kDIErrMalloc;
				goto bail;
			}
			memcpy(newList, indexList, indexCount * sizeof(TrackSector));
			delete[] indexList;
			indexList = newList;
		}
		indexList[indexCount].track = track;
		indexList[indexCount].sector = sector;
		indexCount++;


		//WMSG2("+++ scanning T/S at T=%d S=%d\n", track, sector);
		dierr = pDiskImg->ReadTrackSector(track, sector, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		/* grab next track/sector */
		track = sctBuf[0x01];
		sector = sctBuf[0x02];
		sectorOffset = GetShortLE(&sctBuf[0x05]);

		/* if T/S link is bogus, whole sector is probably bad */
		if (track >= pDiskImg->GetNumTracks() ||
			sector >= pDiskImg->GetNumSectPerTrack())
		{
			// bogus T/S, mark file as damaged and stop
			WMSG3(" DOS33 invalid T/S link %d,%d in '%s'\n", track, sector,
				GetPathName());
			dierr = kDIErrBadFile;
			goto bail;
		}
		if ((sectorOffset % kMaxTSPairs) != 0) {
			WMSG2(" DOS33 invalid T/S header sector offset %u in '%s'\n",
				sectorOffset, GetPathName());
			// not fatal, just weird
		}

		/*
		 * Make sure we have enough room to hold an entire sector full of
		 * T/S pairs in the list.
		 */
		if (tsCount + kMaxTSPairs > tsAlloc) {
			WMSG0("+++ expanding ts list\n");
			TrackSector* newList;
			tsAlloc += kMaxTSPairs * kDefaultTSAlloc;
			newList = new TrackSector[tsAlloc];
			if (newList == nil) {
				dierr = kDIErrMalloc;
				goto bail;
			}
			memcpy(newList, tsList, tsCount * sizeof(TrackSector));
			delete[] tsList;
			tsList = newList;
		}

		/*
		 * Add the entries.  If there's another T/S list linked, we just
		 * grab the entire sector.  If not, we grab every entry until the
		 * last 0,0.  (Can't stop at the first (0,0), or we'll drop a
		 * piece of a random access text file.)
		 */
		dierr = ExtractTSPairs(sctBuf, &tsList[tsCount], &lastNonZero);
		if (dierr != kDIErrNone)
			goto bail;

		if (track != 0 && sector != 0) {
			/* more T/S lists to come, so we keep all entries */
			tsCount += kMaxTSPairs;
		} else {
			/* this was the last one */
			if (lastNonZero == -1) {
				/* this is ALWAYS the case for a newly-created file */
				//WMSG1(" DOS33 odd -- last T/S sector of '%s' was empty\n",
				//	GetPathName());
			}
			tsCount += lastNonZero +1;
		}

		iterations++;		// watch for infinite loops
	} while (!(track == 0 && sector == 0) && iterations < kMaxTSIterations);

	if (iterations == kMaxTSIterations) {
		dierr = kDIErrFileLoop;
		goto bail;
	}

	*pTSList = tsList;
	*pTSCount = tsCount;
	tsList = nil;

	if (pIndexList != nil) {
		*pIndexList = indexList;
		*pIndexCount = indexCount;
		indexList = nil;
	}

bail:
	delete[] tsList;
	delete[] indexList;
	return dierr;
}

/*
 * Extract the track/sector pairs from the TS list in "sctBuf".  The entries
 * are copied to "tsList", which is assumed to have enough space to hold
 * at least kMaxTSPairs entries.
 *
 * The last non-zero entry will be identified and stored in "*pLastNonZero".
 * If all entries are zero, it will be set to -1.
 *
 * Sometimes files will have junk at the tail end of an otherwise valid
 * T/S list.  We can't just stop when we hit the first (0,0) entry because
 * that'll screw up random-access text file handling.  What we can do is
 * try to detect the situation, and mark the file as "suspicious" without
 * returning an error if we see it.
 *
 * If a TS entry appears to be invalid, this returns an error after all
 * entries have been copied.  If it looks to be partially valid, only the
 * valid parts are copied out, with the rest zeroed.
 */
DIError
A2FileDOS::ExtractTSPairs(const unsigned char* sctBuf, TrackSector* tsList,
	int* pLastNonZero)
{
	DIError dierr = kDIErrNone;
	const DiskImg* pDiskImg = fpDiskFS->GetDiskImg();
	const unsigned char* ptr;
	int i, track, sector;

	*pLastNonZero = -1;
	memset(tsList, 0, sizeof(TrackSector) * kMaxTSPairs);

	ptr = &sctBuf[kTSOffset];		// offset of first T/S entry (0x0c)

	for (i = 0; i < kMaxTSPairs; i++) {
		track = *ptr++;
		sector = *ptr++;

		if (dierr == kDIErrNone &&
			(track >= pDiskImg->GetNumTracks() ||
			 sector >= pDiskImg->GetNumSectPerTrack() ||
			 (track == 0 && sector != 0)))
		{
			WMSG3(" DOS33 invalid T/S %d,%d in '%s'\n", track, sector,
				fFileName);

			if (i > 0 && tsList[i-1].track == 0 && tsList[i-1].sector == 0) {
				WMSG0("  T/S list looks partially valid\n");
				SetQuality(kQualitySuspicious);
				goto bail;	// quit immediately
			} else {
				dierr = kDIErrBadFile;
				// keep going, just so caller has the full set to stare at
			}
		}

		if (track != 0 || sector != 0)
			*pLastNonZero = i;

		tsList[i].track = track;
		tsList[i].sector = sector;
	}

bail:
	return dierr;
}


/*
 * ===========================================================================
 *		A2FDDOS
 * ===========================================================================
 */

/*
 * Read data from the current offset.
 *
 * Files read back as they would from ProDOS, i.e. if you read a binary
 * file you won't see the 4 bytes of length and address.
 */
DIError
A2FDDOS::Read(void* buf, size_t len, size_t* pActual)
{
	WMSG3(" DOS reading %d bytes from '%s' (offset=%ld)\n",
		len, fpFile->GetPathName(), (long) fOffset);

	A2FileDOS* pFile = (A2FileDOS*) fpFile;

	/*
	 * Don't allow them to read past the end of the file.  The length value
	 * stored in pFile->fLength already has pFile->fDataOffset subtracted
	 * from the actual data length, so don't factor it in again.
	 */
	if (fOffset + (long)len > fOpenEOF) {
		if (pActual == nil)
			return kDIErrDataUnderrun;
		len = (size_t) (fOpenEOF - fOffset);
	}
	if (pActual != nil)
		*pActual = len;
	long incrLen = len;

	DIError dierr = kDIErrNone;
	unsigned char sctBuf[kSctSize];
	di_off_t actualOffset = fOffset + pFile->fDataOffset;	// adjust for embedded len
	int tsIndex = (int) (actualOffset / kSctSize);
	int bufOffset = (int) (actualOffset % kSctSize);		// (& 0xff)
	size_t thisCount;

	if (len == 0)
		return kDIErrNone;
	assert(fOpenEOF != 0);

	assert(tsIndex >= 0 && tsIndex < fTSCount);

	/* could be more clever in here and avoid double-buffering */
	while (len) {
		if (tsIndex >= fTSCount) {
			/* should've caught this earlier */
			assert(false);
			WMSG1(" DOS ran off the end (fTSCount=%d)\n", fTSCount);
			return kDIErrDataUnderrun;
		}

		if (fTSList[tsIndex].track == 0 && fTSList[tsIndex].sector == 0) {
			//WMSG2(" DOS sparse sector T=%d S=%d\n",
			//	TSTrack(fTSList[tsIndex]), TSSector(fTSList[tsIndex]));
			memset(sctBuf, 0, sizeof(sctBuf));
		} else {
			dierr = pFile->GetDiskFS()->GetDiskImg()->ReadTrackSector(
						fTSList[tsIndex].track,
						fTSList[tsIndex].sector,
						sctBuf);
			if (dierr != kDIErrNone) {
				WMSG1(" DOS error reading file '%s'\n", pFile->GetPathName());
				return dierr;
			}
		}
		thisCount = kSctSize - bufOffset;
		if (thisCount > len)
			thisCount = len;
		memcpy(buf, sctBuf + bufOffset, thisCount);
		len -= thisCount;
		buf = (char*)buf + thisCount;

		bufOffset = 0;
		tsIndex++;
	}

	fOffset += incrLen;

	return dierr;
}

/*
 * Write data at the current offset.
 *
 * For simplicity, we assume that we're writing a brand-new file in one
 * shot.  As it happens, that's all we're currently required to do, so even
 * if we wrote a more sophisticated function it wouldn't get exercised.
 * Because of the way we write, there's no way to mimic the behavior of
 * random-access text file allocation, so that isn't supported.
 *
 * The data in "buf" should *not* include the 2-4 bytes of header present
 * on A/I/B files.  That's already factored in.
 *
 * Modifies fOpenEOF, fOpenSectorsUsed, and sets fModified.
 */
DIError
A2FDDOS::Write(const void* buf, size_t len, size_t* pActual)
{
	DIError dierr = kDIErrNone;
	A2FileDOS* pFile = (A2FileDOS*) fpFile;
	DiskFSDOS33* pDiskFS = (DiskFSDOS33*) fpFile->GetDiskFS();
	unsigned char sctBuf[kSctSize];

	WMSG2("   DOS Write len=%u %s\n", len, pFile->GetPathName());

	if (len >= 0x01000000) {	// 16MB
		assert(false);
		return kDIErrInvalidArg;
	}
	assert(fOffset == 0);		// big simplifying assumption
	assert(fOpenEOF == 0);		// another one
	assert(fTSCount == 0);		// must hold for our newly-created files
	assert(fIndexCount == 1);	// must hold for our newly-created files
	assert(fOpenSectorsUsed == fTSCount + fIndexCount);
	assert(buf != nil);

	long actualLen = (long) len + pFile->fDataOffset;
	long numSectors = (actualLen + kSctSize -1) / kSctSize;
	TrackSector firstIndex;
	int i;

	/*
	 * Nothing to do for zero-length write; don't even set fModified.  Note,
	 * however, that a zero-length 'B' file is actually 4 bytes long, and
	 * must have a data block allocated.
	 */
	if (actualLen == 0)
		goto bail;
	assert(numSectors > 0);

	dierr = pDiskFS->LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Start by allocating a full T/S list.  The existing T/S list is
	 * empty, but we do have one T/S index sector to fill before we
	 * allocate any others.
	 *
	 * Since we determined above that there was nothing interesting in
	 * our T/S list, we just grab the one allocated block, throw out
	 * the lists, and reallocate them.
	 */
	firstIndex = fIndexList[0];
	delete[] fTSList;
	delete[] fIndexList;
	fTSList = fIndexList = nil;

	fTSCount = numSectors;
	fTSList = new TrackSector[fTSCount];
	fIndexCount = (numSectors + kMaxTSPairs -1) / kMaxTSPairs;
	assert(fIndexCount > 0);
	fIndexList = new TrackSector[fIndexCount];
	if (fTSList == nil || fIndexList == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	/*
	 * Allocate all of the index sectors.  In theory we should to this along
	 * with the file sectors, so that the index and file sectors are
	 * interspersed with the data, but in practice 99% of the file have
	 * only one or two index blocks.  By grouping them together we improve
	 * the performance for emulators and CiderPress.
	 */
	fIndexList[0] = firstIndex;
	for (i = 1; i < fIndexCount; i++) {
		TrackSector allocTS;

		dierr = pDiskFS->AllocSector(&allocTS);
		if (dierr != kDIErrNone)
			goto bail;
		fIndexList[i] = allocTS;
	}
	/*
	 * Allocate the data sectors.
	 */
	for (i = 0; i < fTSCount; i++) {
		TrackSector allocTS;

		dierr = pDiskFS->AllocSector(&allocTS);
		if (dierr != kDIErrNone)
			goto bail;
		fTSList[i] = allocTS;
	}

	/*
	 * Write the sectors into the T/S list.
	 */
	const unsigned char* curPtr;
	int sectorIdx;

	curPtr = (const unsigned char*) buf;
	sectorIdx = 0;

	if (pFile->fDataOffset > 0) {
		/* handle first sector specially */
		assert(pFile->fDataOffset < kSctSize);
		int dataInFirstSct = kSctSize - pFile->fDataOffset;
		if (dataInFirstSct > actualLen - pFile->fDataOffset)
			dataInFirstSct = actualLen - pFile->fDataOffset;

		// dataInFirstSct could be zero (== len)
		memset(sctBuf, 0, sizeof(sctBuf));
		memcpy(sctBuf + pFile->fDataOffset, curPtr,
			dataInFirstSct);
		dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track,
					fTSList[sectorIdx].sector, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		sectorIdx++;
		actualLen -= dataInFirstSct + pFile->fDataOffset;
		curPtr += dataInFirstSct;
	}
	while (actualLen > 0) {
		if (actualLen >= kSctSize) {
			/* write directly from input */
			dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track,
				fTSList[sectorIdx].sector, curPtr);
			if (dierr != kDIErrNone)
				goto bail;
		} else {
			/* make a copy of the partial buffer */
			memset(sctBuf, 0, sizeof(sctBuf));
			memcpy(sctBuf, curPtr, actualLen);
			dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track,
				fTSList[sectorIdx].sector, sctBuf);
			if (dierr != kDIErrNone)
				goto bail;
		}

		sectorIdx++;
		actualLen -= kSctSize;	// goes negative; that's fine
		curPtr += kSctSize;
	}
	assert(sectorIdx == fTSCount);

	/*
	 * Fill out the T/S list sectors.  Failure here presents a potential
	 * problem because, once we've written the first T/S entry, the file
	 * appears to have storage that it actually doesn't.  The easiest way
	 * to handle this safely is to start by writing the last index block
	 * first.
	 */
	for (i = fIndexCount-1; i >= 0; i--) {
		int tsOffset = i * kMaxTSPairs;
		assert(tsOffset < fTSCount);

		memset(sctBuf, 0, kSctSize);
		if (i != fIndexCount-1) {
			sctBuf[0x01] = fIndexList[i+1].track;
			sctBuf[0x02] = fIndexList[i+1].sector;
		}
		PutShortLE(&sctBuf[0x05], kMaxTSPairs * i);

		int ent = i * kMaxTSPairs;		// start here
		for (int j = 0; j < kMaxTSPairs; j++) {
			if (ent == fTSCount)
				break;
			sctBuf[kTSOffset + j*2] = fTSList[ent].track;
			sctBuf[kTSOffset + j*2 +1] = fTSList[ent].sector;
			ent++;
		}

		dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fIndexList[i].track,
					fIndexList[i].sector, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;
	}

	dierr = pDiskFS->SaveVolBitmap();
	if (dierr != kDIErrNone) {
		/*
		 * This is awkward -- we wrote the first T/S list, so the file
		 * now appears to have content, but the blocks aren't marked used.
		 * We read the VTOC successfully though, so it's VERY unlikely
		 * that this will fail.  If it does, it's likely that any attempt
		 * to mitigate the problem will also fail.  (Maybe we could force
		 * the object into read-only mode?)
		 */
		goto bail;
	}

	/* finish up */
	fOpenSectorsUsed = fIndexCount + fTSCount;
	fOpenEOF = len;
	fOffset += len;
	fModified = true;

	if (!UpdateProgress(fOffset))
		dierr = kDIErrCancelled;

bail:
	pDiskFS->FreeVolBitmap();
	return dierr;
}

/*
 * Seek to the specified offset.
 */
DIError
A2FDDOS::Seek(di_off_t offset, DIWhence whence)
{
	//di_off_t fileLength = fpFile->GetDataLength();

	switch (whence) {
	case kSeekSet:
		if (offset < 0 || offset > fOpenEOF)
			return kDIErrInvalidArg;
		fOffset = offset;
		break;
	case kSeekEnd:
		if (offset > 0 || offset < -fOpenEOF)
			return kDIErrInvalidArg;
		fOffset = fOpenEOF + offset;
		break;
	case kSeekCur:
		if (offset < -fOffset ||
			offset >= (fOpenEOF - fOffset))
		{
			return kDIErrInvalidArg;
		}
		fOffset += offset;
		break;
	default:
		assert(false);
		return kDIErrInvalidArg;
	}

	assert(fOffset >= 0 && fOffset <= fOpenEOF);
	return kDIErrNone;
}

/*
 * Return current offset.
 */
di_off_t
A2FDDOS::Tell(void)
{
	return fOffset;
}

/*
 * Release file state.
 *
 * If the file was modified, we need to update the sector usage count in
 * the catalog track, and possibly a length word in the first sector of
 * the file (for A/I/B).
 *
 * Given the current "write all at once" implementation of Write, we could
 * have handled the length word back when initially writing the data, but
 * someday we may fix that and I don't want to have to rewrite this part.
 *
 * Most applications don't check the value of "Close", or call it from a
 * destructor, so we call CloseDescr whether we succeed or not.
 */
DIError
A2FDDOS::Close(void)
{
	DIError dierr = kDIErrNone;

	if (fModified) {
		DiskFSDOS33* pDiskFS = (DiskFSDOS33*) fpFile->GetDiskFS();
		A2FileDOS* pFile = (A2FileDOS*) fpFile;
		unsigned char sctBuf[kSctSize];
		unsigned char* pEntry;

		/*
		 * Fill in the length and address, if needed for this type of file.
		 */
		if (pFile->fFileType == A2FileDOS::kTypeInteger ||
			pFile->fFileType == A2FileDOS::kTypeApplesoft ||
			pFile->fFileType == A2FileDOS::kTypeBinary)
		{
			assert(fTSCount > 0);
			assert(pFile->fDataOffset > 0);
			//assert(fOpenEOF < 65536);
			if (fOpenEOF > 65535) {
				WMSG1("WARNING: DOS Close trimming A/I/B file from %ld to 65535\n",
					(long) fOpenEOF);
				fOpenEOF = 65535;
			}
			dierr = pDiskFS->GetDiskImg()->ReadTrackSector(fTSList[0].track,
						fTSList[0].sector, sctBuf);
			if (dierr != kDIErrNone) {
				WMSG0("DOS Close: unable to get first sector of file\n");
				goto bail;
			}

			if (pFile->fFileType == A2FileDOS::kTypeInteger ||
				pFile->fFileType == A2FileDOS::kTypeApplesoft)
			{
				PutShortLE(&sctBuf[0x00], (unsigned short) fOpenEOF);
			} else {
				PutShortLE(&sctBuf[0x00], pFile->fAuxType);
				PutShortLE(&sctBuf[0x02], (unsigned short) fOpenEOF);
			}

			dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[0].track,
						fTSList[0].sector, sctBuf);
			if (dierr != kDIErrNone) {
				WMSG0("DOS Close: unable to write first sector of file\n");
				goto bail;
			}
		} else if (pFile->fFileType == A2FileDOS::kTypeText) {
			/*
			 * The length of text files can be determined by looking for the
			 * first $00.  A file of exactly 256 bytes occupies only one
			 * sector though, so running out of sectors also works -- the
			 * last $00 is not mandatory.
			 *
			 * Bottom line is that the value we just wrote for fOpenEOF is
			 * *probably* recoverable, so we can stuff it into "fLength"
			 * with some assurance that it will be there when we reopen the
			 * file.
			 */
		} else {
			/*
			 * The remaining file types have a length based solely on
			 * sector count.  We need to round off our length value.
			 */
			fOpenEOF = ((fOpenEOF + kSctSize-1) / kSctSize) * kSctSize;
		}

		/*
		 * Update our internal copies of stuff.
		 */
		pFile->fLength = fOpenEOF;
		pFile->fSparseLength = pFile->fLength;
		pFile->fLengthInSectors = (unsigned short) fOpenSectorsUsed;

		/*
		 * Update the sector count in the directory entry.
		 */
		dierr = pDiskFS->GetDiskImg()->ReadTrackSector(pFile->fCatTS.track,
					pFile->fCatTS.sector, sctBuf);
		if (dierr != kDIErrNone)
			goto bail;

		pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);
		assert(GetShortLE(&pEntry[0x21]) == 1);		// holds for new file
		PutShortLE(&pEntry[0x21], pFile->fLengthInSectors);
		dierr = pDiskFS->GetDiskImg()->WriteTrackSector(pFile->fCatTS.track,
					pFile->fCatTS.sector, sctBuf);
	}

bail:
	fpFile->CloseDescr(this);
	return dierr;
}


/*
 * Return the #of sectors/blocks in the file.
 */
long
A2FDDOS::GetSectorCount(void) const
{
	return fTSCount;
}
long
A2FDDOS::GetBlockCount(void) const
{
	return (fTSCount+1)/2;
}

/*
 * Return the Nth track/sector in this file.
 *
 * Returns (0,0) for a sparse sector.
 */
DIError
A2FDDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
{
	if (sectorIdx < 0 || sectorIdx >= fTSCount)
		return kDIErrInvalidIndex;

	*pTrack = fTSList[sectorIdx].track;
	*pSector = fTSList[sectorIdx].sector;
	return kDIErrNone;
}
/*
 * Return the Nth 512-byte block in this file.  Since things aren't stored
 * in 512-byte blocks, we're reduced to finding storage at (tsIndex*2) and
 * converting it to a block number.
 */
DIError
A2FDDOS::GetStorage(long blockIdx, long* pBlock) const
{
	long sectorIdx = blockIdx * 2;
	if (sectorIdx < 0 || sectorIdx >= fTSCount)
		return kDIErrInvalidIndex;

	bool dummy;
	TrackSectorToBlock(fTSList[sectorIdx].track,
		fTSList[sectorIdx].sector, pBlock, &dummy);
	assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks());
	return kDIErrNone;
}


/*
 * Dump the T/S list for an open file.
 */
void
A2FDDOS::DumpTSList(void) const
{
	//A2FileDOS* pFile = (A2FileDOS*) fpFile;
	WMSG2(" DOS T/S list for '%s' (count=%d)\n",
        ((A2FileDOS*)fpFile)->fFileName, fTSCount);

	int i;
	for (i = 0; i <= fTSCount; i++) {
		WMSG3(" %3d: T=%-2d S=%d\n", i, fTSList[i].track, fTSList[i].sector);
	}
}
