/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Implementation of DiskFSProDOS class.
 *
 * We currently only allow one fork to be open at a time, and each file may
 * only be opened once.
 *
 * BUG: does not keep VolumeUsage up to date.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"

// disable Y2K+ dates when testing w/ProSel-16 vol rep (newer ProSel is OK)
//#define OLD_PRODOS_DATES

#if defined(OLD_PRODOS_DATES) && !(defined(_DEBUG) || defined(_DEBUG_LOG))
# error "don't set OLD_PRODOS_DATES for production"
#endif


/*
 * ===========================================================================
 *		DiskFSProDOS
 * ===========================================================================
 */

const int kBlkSize = 512;
const int kVolHeaderBlock = 2;			// block where Volume Header resides
const int kVolDirExpectedNumBlocks = 4;	// customary #of volume header blocks
const int kMinReasonableBlocks = 16;	// min size for ProDOS volume
const int kExpectedBitmapStart = 6;		// block# where vol bitmap should start
const int kMaxCatalogIterations = 1024;	// theoretical max is 32768?
const int kMaxDirectoryDepth = 64;		// not sure what ProDOS limit is
const int kEntriesPerBlock = 0x0d;		// expected value for entries per blk
const int kEntryLength = 0x27;			// expected value for dir entry len
const int kTypeDIR = 0x0f;


/*
 * Directory header.  All fields not marked as "only for subdirs" also apply
 * to the volume directory header.
 */
//namespace DiskImgLib {
	
typedef struct DiskFSProDOS::DirHeader {
	unsigned char	storageType;
	char			dirName[A2FileProDOS::kMaxFileName+1];
	DiskFSProDOS::ProDate	createWhen;
	unsigned char	version;
	unsigned char	minVersion;
	unsigned char	access;
	unsigned char	entryLength;
	unsigned char	entriesPerBlock;
	unsigned short	fileCount;
	/* the rest are only for subdirs */
	unsigned short	parentPointer;
	unsigned char	parentEntry;
	unsigned char	parentEntryLength;
} DirHeader;

//};	// namespace DiskImgLib


/*
 * See if this looks like a ProDOS volume.
 *
 * We test a few fields in the volume directory header for validity.
 */
static DIError
TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	int volDirEntryLength;
	int volDirEntriesPerBlock;

	dierr = pImg->ReadBlockSwapped(kVolHeaderBlock, blkBuf, imageOrder,
				DiskImg::kSectorOrderProDOS);
	if (dierr != kDIErrNone)
		goto bail;

	volDirEntryLength = blkBuf[0x23];
	volDirEntriesPerBlock = blkBuf[0x24];


	if (!(blkBuf[0x00] == 0 && blkBuf[0x01] == 0) ||
		!((blkBuf[0x04] & 0xf0) == 0xf0) ||
		!((blkBuf[0x04] & 0x0f) != 0) ||
		!(volDirEntryLength * volDirEntriesPerBlock <= kBlkSize) ||
		!(blkBuf[0x05] >= 'A' && blkBuf[0x05] <= 'Z') ||
		0)
	{
		dierr = kDIErrFilesystemNotFound;
		goto bail;
	}

bail:
	return dierr;
}

/*
 * Test to see if the image is a ProDOS disk.
 */
/*static*/ DIError
DiskFSProDOS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder,
    DiskImg::FSFormat* pFormat, FSLeniency leniency)
{
	DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax];
	
	DiskImg::GetSectorOrderArray(ordering, *pOrder);

	for (int i = 0; i < DiskImg::kSectorOrderMax; i++) {
		if (ordering[i] == DiskImg::kSectorOrderUnknown)
			continue;
		if (TestImage(pImg, ordering[i]) == kDIErrNone) {
			*pOrder = ordering[i];
			*pFormat = DiskImg::kFormatProDOS;
			return kDIErrNone;
		}
	}

	WMSG0(" ProDOS didn't find valid FS\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 has
 * no files on it.
 */
DIError
DiskFSProDOS::Initialize(InitMode initMode)
{
	DIError dierr = kDIErrNone;
	char msg[kMaxVolumeName + 32];

	fDiskIsGood = false;		// hosed until proven innocent
	fEarlyDamage = false;

	/*
	 * NOTE: we'd probably be better off with fTotalBlocks, since that's how
	 * big the disk *thinks* it is, especially on a CFFA or MacPart subvol.
	 * However, we know that the image block count is the absolute maximum,
	 * so while it may not be a tight bound it is an upper bound.
	 */
	fVolumeUsage.Create(fpImg->GetNumBlocks());

	dierr = LoadVolHeader();
	if (dierr != kDIErrNone)
		goto bail;
	DumpVolHeader();

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

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

	sprintf(msg, "Scanning %s", fVolumeName);
	if (!fpImg->UpdateScanProgress(msg)) {
		WMSG0(" ProDOS cancelled by user\n");
		dierr = kDIErrCancelled;
		goto bail;
	}

	/* volume dir is guaranteed to come first; if not, we need a lookup func */
	A2FileProDOS* pVolumeDir;
	pVolumeDir = (A2FileProDOS*) GetNextFile(nil);

	dierr = RecursiveDirAdd(pVolumeDir, kVolHeaderBlock, "", 0);
	if (dierr != kDIErrNone) {
		WMSG0(" ProDOS RecursiveDirAdd failed\n");
		goto bail;
	}

	sprintf(msg, "Processing %s", fVolumeName);
	if (!fpImg->UpdateScanProgress(msg)) {
		WMSG0(" ProDOS cancelled by user\n");
		dierr = kDIErrCancelled;
		goto bail;
	}

	dierr = ScanFileUsage();
	if (dierr != kDIErrNone) {
		if (dierr == kDIErrCancelled)
			goto bail;

		/* this might not be fatal; just means that *some* files are bad */
		WMSG1("WARNING: ScanFileUsage returned err=%d\n", dierr);
		dierr = kDIErrNone;
		fpImg->AddNote(DiskImg::kNoteWarning,
			"Some errors were encountered while scanning files.");
		fEarlyDamage = true;	// make sure we know it's damaged
	}

	fDiskIsGood = CheckDiskIsGood();

	if (fScanForSubVolumes != kScanSubDisabled)
		(void) ScanForSubVolumes();

	if (fpImg->GetNumBlocks() <= 1600)
		fVolumeUsage.Dump();

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

bail:
	return dierr;
}

/*
 * Read some interesting fields from the volume header.
 *
 * The "test" function verified certain things, e.g. the storage type
 * is $f and the volume name length is nonzero.
 */
DIError
DiskFSProDOS::LoadVolHeader(void)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	int nameLen;

	dierr = fpImg->ReadBlock(kVolHeaderBlock, blkBuf);
	if (dierr != kDIErrNone)
		goto bail;

	//fPrevBlock = GetShortLE(&blkBuf[0x00]);
	//fNextBlock = GetShortLE(&blkBuf[0x02]);
	nameLen = blkBuf[0x04] & 0x0f;
	memcpy(fVolumeName, &blkBuf[0x05], nameLen);
	fVolumeName[nameLen] = '\0';
	// 0x14-15 reserved
	// undocumented: GS/OS writes the modification date to 0x16-19
	fModWhen = GetLongLE(&blkBuf[0x16]);
	// undocumented: GS/OS uses 0x1a-1b for lower-case handling (see below)
	fCreateWhen = GetLongLE(&blkBuf[0x1c]);
	//fVersion = blkBuf[0x20];
	if (blkBuf[0x21] != 0) {
		/*
		 * We don't care about the MIN_VERSION field, but it looks like GS/OS
		 * rejects anything with a nonzero value here.  We want to add a note
		 * about it.
		 */
		fpImg->AddNote(DiskImg::kNoteInfo,
			"Volume header has nonzero min_version; could confuse GS/OS.");
	}
	fAccess = blkBuf[0x22];
	//fEntryLength = blkBuf[0x23];
	//fEntriesPerBlock = blkBuf[0x24];
	fVolDirFileCount = GetShortLE(&blkBuf[0x25]);
	fBitMapPointer = GetShortLE(&blkBuf[0x27]);
	fTotalBlocks = GetShortLE(&blkBuf[0x29]);

	if (blkBuf[0x1b] & 0x80) {
		/*
		 * Handle lower-case conversion; see GS/OS tech note #8.  Unlike
		 * filenames, volume names are not allowed to contain spaces.  If
		 * they try it we just ignore them.
		 *
		 * Technote 8 doesn't actually talk about volume names.  By
		 * experimentation the field was discovered at offset 0x1a from
		 * the start of the block, which is marked as "reserved" in Beneath
		 * Apple ProDOS.
		 */
		unsigned short lcFlags = GetShortLE(&blkBuf[0x1a]);

		GenerateLowerCaseName(fVolumeName, fVolumeName, lcFlags, false);
	}

	if (fTotalBlocks <= kVolHeaderBlock) {
		/* incr to min; don't use max, or bitmap count may be too large */
		WMSG1(" ProDOS found tiny fTotalBlocks (%d), increasing to minimum\n",
			fTotalBlocks);
		fpImg->AddNote(DiskImg::kNoteWarning,
			"ProDOS filesystem blockcount (%d) too small, setting to %d.",
			fTotalBlocks, kMinReasonableBlocks);
		fTotalBlocks = kMinReasonableBlocks;
		fEarlyDamage = true;
	} else if (fTotalBlocks != fpImg->GetNumBlocks()) {
		if (fTotalBlocks != 65535 || fpImg->GetNumBlocks() != 65536) {
			WMSG2(" ProDOS WARNING: total (%u) != img (%ld)\n",
				fTotalBlocks, fpImg->GetNumBlocks());
			// could AddNote here, but not really necessary
		}

		/*
		 * For safety (esp. vol bitmap read), constrain fTotalBlocks.  We might
		 * consider not doing this for ".hdv", which can start small and then
		 * expand as files are added.  (Check "fExpanded".)
		 */
		if (fTotalBlocks > fpImg->GetNumBlocks()) {
			fpImg->AddNote(DiskImg::kNoteWarning,
				"ProDOS filesystem blockcount (%d) exceeds disk image blocks (%ld).",
				fTotalBlocks, fpImg->GetNumBlocks());
			fTotalBlocks = (unsigned short) fpImg->GetNumBlocks();
			fEarlyDamage = true;
		}
	}

	/*
	 * Test for funky volume bitmap pointer.  Some disks (e.g. /RAM and
	 * ProSel-16) truncate the volume directory to eke a little more storage
	 * out of a disk.  There's nothing wrong with that, but we don't want to
	 * try to use a volume bitmap pointer of zero or 0xffff, because it's
	 * probably garbage.
	 */
	if (fBitMapPointer != kExpectedBitmapStart) {
		if (fBitMapPointer <= kVolHeaderBlock ||
			fBitMapPointer > kExpectedBitmapStart)
		{
			fpImg->AddNote(DiskImg::kNoteWarning,
				"Volume bitmap pointer (%d) is probably invalid.",
				fBitMapPointer);
			fBitMapPointer = 6;		// just fix it and hope for the best
			fEarlyDamage = true;
		} else {
			fpImg->AddNote(DiskImg::kNoteInfo,
				"Unusual volume bitmap start (%d).", fBitMapPointer);
			// try it and see
		}
	}

	SetVolumeID();

	/*
	 * Create a "magic" directory entry for the volume directory.
	 *
	 * Normally these values are pulled out of the file entry in the parent
	 * directory.  Here, we synthesize them from the volume dir header.
	 */
	A2FileProDOS* pFile;
	pFile = new A2FileProDOS(this);
	if (pFile == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	A2FileProDOS::DirEntry* pEntry;
	pEntry = &pFile->fDirEntry;

	int foundStorage;
	foundStorage = (blkBuf[0x04] & 0xf0) >> 4;
	if (foundStorage != A2FileProDOS::kStorageVolumeDirHeader) {
		WMSG1(" ProDOS WARNING: unexpected vol dir file type %d\n",
			pEntry->storageType);
		/* keep going */
	}
	pEntry->storageType = A2FileProDOS::kStorageVolumeDirHeader;
	strcpy(pEntry->fileName, fVolumeName);
	//nameLen = blkBuf[0x04] & 0x0f;
	//memcpy(pEntry->fileName, &blkBuf[0x05], nameLen);
	//pEntry->fileName[nameLen] = '\0';
	pFile->SetPathName(":", pEntry->fileName);
	pEntry->fileName[nameLen] = '\0';
	pEntry->fileType = kTypeDIR;
	pEntry->keyPointer = kVolHeaderBlock;
	pEntry->blocksUsed = kVolDirExpectedNumBlocks;
	pEntry->eof = kVolDirExpectedNumBlocks * 512;
	pEntry->createWhen = GetLongLE(&blkBuf[0x1c]);
	pEntry->version = blkBuf[0x20];
	pEntry->minVersion = blkBuf[0x21];
	pEntry->access = blkBuf[0x22];
	pEntry->auxType = 0;
//	if (blkBuf[0x20] >= 5)
		pEntry->modWhen = GetLongLE(&blkBuf[0x16]);
	pEntry->headerPointer = 0;

	pFile->fSparseDataEof = pEntry->eof;
	pFile->fSparseRsrcEof = -1;

	AddFileToList(pFile);

bail:
	return dierr;
}

/*
 * Set the volume ID field.
 */
void
DiskFSProDOS::SetVolumeID(void)
{
	sprintf(fVolumeID, "ProDOS /%s", fVolumeName);
}


/*
 * Dump what we pulled out of the volume header.
 */
void
DiskFSProDOS::DumpVolHeader(void)
{
	WMSG1(" ProDOS volume header for '%s'\n", fVolumeName);
	WMSG4("  CreateWhen=0x%08lx access=0x%02x bitmap=%d totalbl=%d\n",
		fCreateWhen, fAccess, fBitMapPointer, fTotalBlocks);

	time_t when;
	when = A2FileProDOS::ConvertProDate(fCreateWhen);
	WMSG1("  CreateWhen is %.24s\n", ctime(&when));

	//WMSG4("  prev=%d next=%d bitmap=%d total=%d\n",
	//	fPrevBlock, fNextBlock, fBitMapPointer, fTotalBlocks);
	//WMSG2("  create date=0x%08lx access=0x%02x\n", fCreateWhen, fAccess);
	//WMSG4("  version=%d minVersion=%d entryLen=%d epb=%d\n",
	//	fVersion, fMinVersion, fEntryLength, fEntriesPerBlock);
	//WMSG1("  volume dir fileCount=%d\n", fFileCount);
}


/*
 * Load the disk's volume bitmap into the object's "fBlockUseMap" pointer.
 *
 * Does not attempt to analyze the data.
 */
DIError
DiskFSProDOS::LoadVolBitmap(void)
{
	DIError dierr = kDIErrNone;
	int bitBlock, numBlocks;

	if (fBitMapPointer <= kVolHeaderBlock)
		return kDIErrBadDiskImage;
	if (fTotalBlocks <= kVolHeaderBlock)
		return kDIErrBadDiskImage;

	/* should not already be allocated */
	assert(fBlockUseMap == nil);
	delete[] fBlockUseMap;		// just in case

	bitBlock = fBitMapPointer;

	numBlocks = GetNumBitmapBlocks();	// based on fTotalBlocks
	assert(numBlocks > 0);

	fBlockUseMap = new unsigned char[kBlkSize * numBlocks];
	if (fBlockUseMap == nil)
		return kDIErrMalloc;

	while (numBlocks--) {
		dierr = fpImg->ReadBlock(bitBlock + numBlocks,
					fBlockUseMap + kBlkSize * numBlocks);
		if (dierr != kDIErrNone) {
			delete[] fBlockUseMap;
			fBlockUseMap = nil;
			return dierr;
		}
	}

	return kDIErrNone;
}

/*
 * Save our copy of the volume bitmap.
 */
DIError
DiskFSProDOS::SaveVolBitmap(void)
{
	DIError dierr = kDIErrNone;
	int bitBlock, numBlocks;

	if (fBlockUseMap == nil) {
		assert(false);
		return kDIErrNotReady;
	}
	assert(fBitMapPointer > kVolHeaderBlock);
	assert(fTotalBlocks > kVolHeaderBlock);

	bitBlock = fBitMapPointer;

	numBlocks = GetNumBitmapBlocks();
	assert(numBlocks > 0);

	while (numBlocks--) {
		dierr = fpImg->WriteBlock(bitBlock + numBlocks,
					fBlockUseMap + kBlkSize * numBlocks);
		if (dierr != kDIErrNone)
			return dierr;
	}

	return kDIErrNone;
}

/*
 * Throw away the volume bitmap, discarding any unsaved changes.
 *
 * It's okay to call this if the bitmap isn't loaded.
 */
void
DiskFSProDOS::FreeVolBitmap(void)
{
	delete[] fBlockUseMap;
	fBlockUseMap = nil;
}

/*
 * Examine the volume bitmap, setting fields in the VolumeUsage map
 * as appropriate.
 */
DIError
DiskFSProDOS::ScanVolBitmap(void)
{
	DIError dierr;

	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone) {
		WMSG1(" ProDOS failed to load volume bitmap (err=%d)\n", dierr);
		return dierr;
	}

	assert(fBlockUseMap != nil);

	/* mark the boot blocks as system */
	SetBlockUsage(0, VolumeUsage::kChunkPurposeSystem);
	SetBlockUsage(1, VolumeUsage::kChunkPurposeSystem);

	/* mark the bitmap blocks as system */
	int i;
	for (i = GetNumBitmapBlocks(); i > 0; i--)
		SetBlockUsage(fBitMapPointer + i -1, VolumeUsage::kChunkPurposeSystem);

	/*
	 * Set the "isMarkedUsed" flag in VolumeUsage for all used blocks.
	 */
	VolumeUsage::ChunkState cstate;

	long block = 0;
	long numBytes = (fTotalBlocks + 7) / 8;
	for (i = 0; i < numBytes; i++) {
		unsigned char val = fBlockUseMap[i];

		for (int j = 0; j < 8; j++) {
			if (!(val & 0x80)) {
				/* block is in use, mark it */
				if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone)
				{
					assert(false);
					// keep going, I guess
				}
				cstate.isMarkedUsed = true;
				fVolumeUsage.SetChunkState(block, &cstate);
			}
			val <<= 1;
			block++;

			if (block >= fTotalBlocks)
				break;
		}
		if (block >= fTotalBlocks)
			break;
	}

	FreeVolBitmap();
	return dierr;
}

/*
 * Generate an empty block use map.
 */
DIError
DiskFSProDOS::CreateEmptyBlockMap(void)
{
	DIError dierr;

	/* load from disk; this is just to allocate the data structures */
	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		return dierr;

	/*
	 * Set the bits, block by block.  Not the most efficient way, but it's
	 * fast enough, and it exercises the standard set of functions.
	 */
	long block;
	long firstEmpty =
		kVolHeaderBlock + kVolDirExpectedNumBlocks + GetNumBitmapBlocks();
	for (block = 0; block < firstEmpty; block++)
		SetBlockUseEntry(block, true);
	for ( ; block < fTotalBlocks; block++)
		SetBlockUseEntry(block, false);

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

	return kDIErrNone;
}

/*
 * Get the state of an entry in the block use map.
 *
 * Returns "true" if it's in use, "false" otherwise.
 */
bool
DiskFSProDOS::GetBlockUseEntry(long block) const
{
	assert(block >= 0 && block < fTotalBlocks);
	assert(fBlockUseMap != nil);

	int offset;
	unsigned char mask;

	offset = block / 8;
	mask = 0x80 >> (block & 0x07);
	if (fBlockUseMap[offset] & mask)
		return false;
	else
		return true;
}

/*
 * Change the state of an entry in the block use map.
 */
void
DiskFSProDOS::SetBlockUseEntry(long block, bool inUse)
{
	assert(block >= 0 && block < fTotalBlocks);
	assert(fBlockUseMap != nil);

	if (block == 0 && !inUse) {
		// shouldn't happen
		assert(false);
	}

	int offset;
	unsigned char mask;

	offset = block / 8;
	mask = 0x80 >> (block & 0x07);
	if (!inUse)
		fBlockUseMap[offset] |= mask;
	else
		fBlockUseMap[offset] &= ~mask;
}

/*
 * Check for entries in the block use map past the point where they should be.
 *
 * Returns "true" if bogus entries were found, "false" if all is well.
 */
bool
DiskFSProDOS::ScanForExtraEntries(void) const
{
	assert(fBlockUseMap != nil);

	int offset, endOffset;

	/* sloppy: we're not checking for excess bits within last byte */
	offset = (fTotalBlocks / 8) +1;
	endOffset = GetNumBitmapBlocks() * kBlkSize;

	while (offset < endOffset) {
		if (fBlockUseMap[offset] != 0) {
			WMSG2(" ProDOS found bogus bitmap junk 0x%02x at offset=%d\n",
				fBlockUseMap[offset], offset);
			return true;
		}
		offset++;
	}
	return false;
}

/*
 * Allocate a new block on a ProDOS volume.
 *
 * Only touches the in-memory copy.
 *
 * Returns the block number (0-65535) on success or -1 on failure.
 */
long
DiskFSProDOS::AllocBlock(void)
{
	assert(fBlockUseMap != nil);

#if 0	// whoa... this is REALLY slow
	/*
	 * Run through the entire set of blocks until we find one that's not
	 * allocated.  We could probably make this faster by scanning bytes and
	 * then shifting bits, but this is easier and fast enough.
	 *
	 * We don't scan block 0 because (a) it should never be available and
	 * (b) it has a special meaning in some circumstances.  We could probably
	 * start at kVolHeaderBlock+kVolHeaderNumBlocks.
	 */
	long block;
	for (block = kVolHeaderBlock; block < fTotalBlocks; block++) {
		if (!GetBlockUseEntry(block)) {
			SetBlockUseEntry(block, true);
			return block;
		}
	}
#endif

	int offset;
	int maxOffset = (fTotalBlocks + 7) / 8;

	for (offset = 0; offset < maxOffset; offset++) {
		if (fBlockUseMap[offset] != 0) {
			/* got one, figure out which */
			int subBlock = 0;
			unsigned char uch = fBlockUseMap[offset];
			while ((uch & 0x80) == 0) {
				subBlock++;
				uch <<= 1;
			}

			long block = offset * 8 + subBlock;
			assert(!GetBlockUseEntry(block));
			SetBlockUseEntry(block, true);
			if (block == 0 || block == 1) {
				WMSG0("PRODOS: GLITCH: rejecting alloc of block 0\n");
				continue;
			}
			return block;
		}
	}

	WMSG0("ProDOS: NOTE: AllocBlock just failed!\n");
	return -1;
}

/*
 * Tally up the number of free blocks.
 */
DIError
DiskFSProDOS::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits,
	int* pUnitSize) const
{
	DIError dierr;
	long block, freeBlocks;
	freeBlocks = 0;

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

	for (block = 0; block < fTotalBlocks; block++) {
		if (!GetBlockUseEntry(block))
			freeBlocks++;
	}

	*pTotalUnits = fTotalBlocks;
	*pFreeUnits = freeBlocks;
	*pUnitSize = kBlockSize;

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


/*
 * Update an entry in the VolumeUsage map.
 *
 * The VolumeUsage map spans the range of blocks 
 */
void
DiskFSProDOS::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose)
{
	VolumeUsage::ChunkState cstate;

	fVolumeUsage.GetChunkState(block, &cstate);
	if (cstate.isUsed) {
		cstate.purpose = VolumeUsage::kChunkPurposeConflict;
		WMSG1(" ProDOS conflicting uses for bl=%ld\n", block);
	} else {
		cstate.isUsed = true;
		cstate.purpose = purpose;
	}
	fVolumeUsage.SetChunkState(block, &cstate);
}

/*
 * Pass in the number of the first block of the directory.
 *
 * Start with "pParent" set to the magic entry for the volume dir.
 */
DIError
DiskFSProDOS::RecursiveDirAdd(A2File* pParent, unsigned short dirBlock,
	const char* basePath, int depth)
{
	DIError dierr = kDIErrNone;
	DirHeader header;
	unsigned char blkBuf[kBlkSize];
	int numEntries, iterations, foundCount;
	bool first;

	/* if we get too deep, assume it's a loop */
	if (depth > kMaxDirectoryDepth) {
		dierr = kDIErrDirectoryLoop;
		goto bail;
	}


	if (dirBlock < kVolHeaderBlock || dirBlock >= fpImg->GetNumBlocks()) {
		WMSG1(" ProDOS ERROR: directory block %u out of range\n", dirBlock);
		dierr = kDIErrInvalidBlock;
		goto bail;
	}

	numEntries = 1;
	iterations = 0;
	foundCount = 0;
	first = true;

	while (dirBlock && iterations < kMaxCatalogIterations) {
		dierr = fpImg->ReadBlock(dirBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;
		if (pParent->IsVolumeDirectory())
			SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeVolumeDir);
		else
			SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeSubdir);

		if (first) {
			/* this is the directory header entry */
			dierr = GetDirHeader(blkBuf, &header);
			if (dierr != kDIErrNone)
				goto bail;
			numEntries = header.fileCount;
			//WMSG1("  ProDOS got dir header numEntries = %d\n", numEntries);
		}

		/* slurp the entries out of this block */
		dierr = SlurpEntries(pParent, &header, blkBuf, first, &foundCount,
					basePath, dirBlock, depth);
		if (dierr != kDIErrNone)
			goto bail;

		dirBlock = GetShortLE(&blkBuf[0x02]);
		if (dirBlock != 0 &&
			(dirBlock < 2 || dirBlock >= fpImg->GetNumBlocks()))
		{
			WMSG2(" ProDOS ERROR: invalid dir link block %u in base='%s'\n",
				dirBlock, basePath);
			dierr = kDIErrInvalidBlock;
			goto bail;
		}
		first = false;
		iterations++;
	}
	if (iterations == kMaxCatalogIterations) {
		WMSG0(" ProDOS subdir iteration count exceeded\n");
		dierr = kDIErrDirectoryLoop;
		goto bail;
	}
	if (foundCount != numEntries) {
		/* not significant; just means somebody isn't updating correctly */
		WMSG3(" ProDOS WARNING: numEntries=%d foundCount=%d in base='%s'\n",
			numEntries, foundCount, basePath);
	}

bail:
	return dierr;
}

/*
 * Slurp the entries out of a single ProDOS directory block.
 *
 * Recursively calls RecursiveDirAdd for directories.
 *
 * "*pFound" is increased by the number of valid entries found in this block.
 */
DIError
DiskFSProDOS::SlurpEntries(A2File* pParent, const DirHeader* pHeader,
	const unsigned char* blkBuf, bool skipFirst, int* pCount,
	const char* basePath, unsigned short thisBlock, int depth)
{
	DIError dierr = kDIErrNone;
	int entriesThisBlock = pHeader->entriesPerBlock;
	const unsigned char* entryBuf;
	A2FileProDOS* pFile;

	int idx = 0;
	entryBuf = &blkBuf[0x04];
	if (skipFirst) {
		entriesThisBlock--;
		entryBuf += pHeader->entryLength;
		idx++;
	}

	for ( ; entriesThisBlock > 0 ;
		entriesThisBlock--, idx++, entryBuf += pHeader->entryLength)
	{
		if (entryBuf >= blkBuf + kBlkSize) {
			WMSG0("  ProDOS whoops, just walked out of dirent buffer\n");
			return kDIErrBadDirectory;
		}

		if ((entryBuf[0x00] & 0xf0) == A2FileProDOS::kStorageDeleted) {
			/* skip deleted entries */
			continue;
		}

		pFile = new A2FileProDOS(this);
		if (pFile == nil) {
			dierr = kDIErrMalloc;
			goto bail;
		}

		A2FileProDOS::DirEntry* pEntry;
		pEntry = &pFile->fDirEntry;
		A2FileProDOS::InitDirEntry(pEntry, entryBuf);

		pFile->SetParent(pParent);
		pFile->fParentDirBlock = thisBlock;
		pFile->fParentDirIdx = idx;

		pFile->SetPathName(basePath, pEntry->fileName);

		if (pEntry->keyPointer <= kVolHeaderBlock) {
			WMSG2("ProDOS invalid key pointer %d on '%s'\n",
				pEntry->keyPointer, pFile->GetPathName());
			pFile->SetQuality(A2File::kQualityDamaged);
		} else
		if (pEntry->storageType == A2FileProDOS::kStorageExtended) {
			dierr = ReadExtendedInfo(pFile);
			if (dierr != kDIErrNone) {
				pFile->SetQuality(A2File::kQualityDamaged);
				dierr = kDIErrNone;
			}
		}

		//pFile->Dump();
		AddFileToList(pFile);
		(*pCount)++;

		if (!fpImg->UpdateScanProgress(nil)) {
			WMSG0(" ProDOS cancelled by user\n");
			dierr = kDIErrCancelled;
			goto bail;
		}

		if (pEntry->storageType == A2FileProDOS::kStorageDirectory) {
			// don't need to check for kStorageVolumeDirHeader here
			dierr = RecursiveDirAdd(pFile, pEntry->keyPointer,
						pFile->GetPathName(), depth+1);
			if (dierr != kDIErrNone) {
				if (dierr == kDIErrCancelled)
					goto bail;

				/* mark subdir as damaged and keep going */
				pFile->SetQuality(A2File::kQualityDamaged);
				dierr = kDIErrNone;
			}
		}
	}

bail:
	return dierr;
}


/*
 * Pull the directory header out of the first block of a directory.
 */
DIError
DiskFSProDOS::GetDirHeader(const unsigned char* blkBuf, DirHeader* pHeader)
{
	int nameLen;

	pHeader->storageType = (blkBuf[0x04] & 0xf0) >> 4;
	if (pHeader->storageType != A2FileProDOS::kStorageSubdirHeader &&
		pHeader->storageType != A2FileProDOS::kStorageVolumeDirHeader)
	{
		WMSG1(" ProDOS WARNING: subdir header has wrong storage type (%d)\n",
			pHeader->storageType);
		/* keep going... might be bad idea */
	}
	nameLen = blkBuf[0x04] & 0x0f;
	memcpy(pHeader->dirName, &blkBuf[0x05], nameLen);
	pHeader->dirName[nameLen] = '\0';
	pHeader->createWhen = GetLongLE(&blkBuf[0x1c]);
	pHeader->version = blkBuf[0x20];
	pHeader->minVersion = blkBuf[0x21];
	pHeader->access = blkBuf[0x22];
	pHeader->entryLength = blkBuf[0x23];
	pHeader->entriesPerBlock = blkBuf[0x24];
	pHeader->fileCount = GetShortLE(&blkBuf[0x25]);
	pHeader->parentPointer = GetShortLE(&blkBuf[0x27]);
	pHeader->parentEntry = blkBuf[0x29];
	pHeader->parentEntryLength = blkBuf[0x2a];

	if (pHeader->entryLength * pHeader->entriesPerBlock > kBlkSize ||
		pHeader->entryLength * pHeader->entriesPerBlock == 0)
	{
		WMSG2(" ProDOS invalid subdir header: entryLen=%d, entriesPerBlock=%d\n",
			pHeader->entryLength, pHeader->entriesPerBlock);
		return kDIErrBadDirectory;
	}

	return kDIErrNone;
}

/*
 * Read the information from the key block of an extended file.
 *
 * There's some "HFS Finder information" stuffed into the key block
 * right after the data fork info, but I'm planning to ignore that.
 */
DIError
DiskFSProDOS::ReadExtendedInfo(A2FileProDOS* pFile)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];

	dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, blkBuf);
	if (dierr != kDIErrNone) {
		WMSG1(" ProDOS ReadExtendedInfo: unable to read key block %d\n",
			pFile->fDirEntry.keyPointer);
		goto bail;
	}

	pFile->fExtData.storageType = blkBuf[0x0000] & 0x0f;
	pFile->fExtData.keyBlock = GetShortLE(&blkBuf[0x0001]);
	pFile->fExtData.blocksUsed = GetShortLE(&blkBuf[0x0003]);
	pFile->fExtData.eof = GetLongLE(&blkBuf[0x0005]);
	pFile->fExtData.eof &= 0x00ffffff;

	pFile->fExtRsrc.storageType = blkBuf[0x0100] & 0x0f;
	pFile->fExtRsrc.keyBlock = GetShortLE(&blkBuf[0x0101]);
	pFile->fExtRsrc.blocksUsed = GetShortLE(&blkBuf[0x0103]);
	pFile->fExtRsrc.eof = GetLongLE(&blkBuf[0x0105]);
	pFile->fExtRsrc.eof &= 0x00ffffff;

    if (pFile->fExtData.keyBlock <= kVolHeaderBlock ||
        pFile->fExtRsrc.keyBlock <= kVolHeaderBlock)
    {
		WMSG2(" ProDOS ReadExtendedInfo: found bad extended key blocks %d/%d\n",
			pFile->fExtData.keyBlock, pFile->fExtRsrc.keyBlock);
        return kDIErrBadFile;
    }

bail:
	return dierr;
}

/*
 * Scan all of the files on the disk, reading their block usage into the
 * volume usage map.  This is important for detecting damage, and makes
 * later accesses easier.
 *
 * As a side-effect, we set the "sparse" length for the file.
 */
DIError
DiskFSProDOS::ScanFileUsage(void)
{
	DIError dierr = kDIErrNone;
	A2FileProDOS* pFile;
	long blockCount, indexCount, sparseCount;
	unsigned short* blockList = nil;
	unsigned short* indexList = nil;

	pFile = (A2FileProDOS*) GetNextFile(nil);
	while (pFile != nil) {
		if (!fpImg->UpdateScanProgress(nil)) {
			WMSG0(" ProDOS cancelled by user\n");
			dierr = kDIErrCancelled;
			goto bail;
		}

		//pFile->Dump();
		if (pFile->GetQuality() == A2File::kQualityDamaged)
			goto skip;

		if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) {
			/* resource fork */
			if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) {
				/* not expecting to find a directory here, but it happens */
				dierr = kDIErrBadFile;
			} else {
				dierr = pFile->LoadBlockList(pFile->fExtRsrc.storageType,
							pFile->fExtRsrc.keyBlock, pFile->fExtRsrc.eof,
							&blockCount, &blockList, &indexCount, &indexList);
			}
			if (dierr != kDIErrNone) {
				WMSG1(" ProDOS skipping scan rsrc '%s'\n",
					pFile->fDirEntry.fileName);
				pFile->SetQuality(A2File::kQualityDamaged);
				goto skip;
			}
			ScanBlockList(blockCount, blockList, indexCount, indexList,
				&sparseCount);
			pFile->fSparseRsrcEof =
						(di_off_t) pFile->fExtRsrc.eof - sparseCount * kBlkSize;
			//WMSG3(" SparseCount %d rsrcEof %d '%s'\n",
			//	sparseCount, pFile->fSparseRsrcEof, pFile->fDirEntry.fileName);
			delete[] blockList;
			blockList = nil;
			delete[] indexList;
			indexList = nil;

			/* data fork */
			if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) {
				dierr = kDIErrBadFile;
			} else {
				dierr = pFile->LoadBlockList(pFile->fExtData.storageType,
							pFile->fExtData.keyBlock, pFile->fExtData.eof,
							&blockCount, &blockList, &indexCount, &indexList);
			}
			if (dierr != kDIErrNone) {
				WMSG1(" ProDOS skipping scan data '%s'\n",
					pFile->fDirEntry.fileName);
				pFile->SetQuality(A2File::kQualityDamaged);
				goto skip;
			}
			ScanBlockList(blockCount, blockList, indexCount, indexList,
				&sparseCount);
			pFile->fSparseDataEof =
						(di_off_t) pFile->fExtData.eof - sparseCount * kBlkSize;
			//WMSG3(" SparseCount %d dataEof %d '%s'\n",
			//	sparseCount, pFile->fSparseDataEof, pFile->fDirEntry.fileName);
			delete[] blockList;
			blockList = nil;
			delete[] indexList;
			indexList = nil;

			/* mark the extended key block as in-use */
			SetBlockUsage(pFile->fDirEntry.keyPointer,
				VolumeUsage::kChunkPurposeFileStruct);
		} else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory ||
				   pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader)
		{
			/* we already got these during the recursive descent */
			/* (could do them here if we used "fake" directory entry
			    for volume dir to lead off the recursion) */
			goto skip;
		} else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageSeedling ||
				   pFile->fDirEntry.storageType == A2FileProDOS::kStorageSapling ||
				   pFile->fDirEntry.storageType == A2FileProDOS::kStorageTree)
		{
			/* standard file */
			dierr = pFile->LoadBlockList(pFile->fDirEntry.storageType,
						pFile->fDirEntry.keyPointer, pFile->fDirEntry.eof,
						&blockCount, &blockList, &indexCount, &indexList);
			if (dierr != kDIErrNone) {
				WMSG1(" ProDOS skipping scan '%s'\n",
					pFile->fDirEntry.fileName);
				pFile->SetQuality(A2File::kQualityDamaged);
				goto skip;
			}
			ScanBlockList(blockCount, blockList, indexCount, indexList,
				&sparseCount);
			pFile->fSparseDataEof =
						(di_off_t) pFile->fDirEntry.eof - sparseCount * kBlkSize;
			//WMSG4(" +++ sparseCount=%ld blockCount=%ld sparseDataEof=%ld '%s'\n",
			//	sparseCount, blockCount, (long) pFile->fSparseDataEof,
			//	pFile->fDirEntry.fileName);

			delete[] blockList;
			blockList = nil;
			delete[] indexList;
			indexList = nil;
		} else {
			WMSG2(" ProDOS found weird storage type %d on '%s', ignoring\n",
				pFile->fDirEntry.storageType, pFile->fDirEntry.fileName);
			pFile->SetQuality(A2File::kQualityDamaged);
		}

		/*
		 * A completely empty file written as zero blocks (as opposed to simply
		 * having its EOF extended, e.g. "sparse seedlings") will have zero data
		 * blocks but possibly an EOF that doesn't land on 512 bytes.  This can
		 * result in a slightly negative "sparse length", which we trim to zero
		 * here.
		 */
		//if (stricmp(pFile->fDirEntry.fileName, "EMPTY.SPARSE.R") == 0)
		//	WMSG0("wahoo\n");
		if (pFile->fSparseDataEof < 0)
			pFile->fSparseDataEof = 0;
		if (pFile->fSparseRsrcEof < 0)
			pFile->fSparseRsrcEof = 0;

skip:
		pFile = (A2FileProDOS*) GetNextFile(pFile);
	}

	dierr = kDIErrNone;

bail:
	return dierr;
}

/*
 * Scan a block list into the volume usage map.
 */
void
DiskFSProDOS::ScanBlockList(long blockCount, unsigned short* blockList,
	long indexCount, unsigned short* indexList, long* pSparseCount)
{
	assert(blockList != nil);
	assert(indexCount == 0 || indexList != nil);
	assert(pSparseCount != nil);

	*pSparseCount = 0;

	int i;
	for (i = 0; i < blockCount; i++) {
		if (blockList[i] != 0) {
			SetBlockUsage(blockList[i], VolumeUsage::kChunkPurposeUserData);
		} else {
			(*pSparseCount)++;	// sparse data block
		}
	}

	for (i = 0; i < indexCount; i++) {
		if (indexList[i] != 0) {
			SetBlockUsage(indexList[i], VolumeUsage::kChunkPurposeFileStruct);
		}	// else sparse index block
	}
}


/*
 * ProDOS disks may contain other filesystems.  The typical DOS-in-ProDOS
 * strategy involves marking a bunch of blocks at the end of the disc as
 * "in use" without creating a file to go along with them.
 *
 * We look for certain types of embedded volume by looking for disk
 * usage patterns and then testing those with the standard disk testing
 * facilities.
 */
DIError
DiskFSProDOS::ScanForSubVolumes(void)
{
	DIError dierr = kDIErrNone;
	VolumeUsage::ChunkState cstate;
	int firstBlock, matchCount;
	int block;

	/* this is guaranteed by constraint in volume header read */
	assert(fTotalBlocks <= fpImg->GetNumBlocks());

	if (fTotalBlocks != 1600) {
		WMSG1(" ProDOS ScanForSub: not 800K disk (%ld)\n",
			fpImg->GetNumBlocks());
		return kDIErrNone;		// only scan 800K disks
	}

	matchCount = 0;
	for (block = fTotalBlocks-1; block >= 0; block--) {
		if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) {
			assert(false);
			return kDIErrGeneric;
		}

		if (!cstate.isMarkedUsed || cstate.isUsed)
			break;

		matchCount++;
	}
	firstBlock = block+1;

	WMSG1("MATCH COUNT %d\n", matchCount);
	if (matchCount < 35*8)		// 280 blocks on 35-track floppy
		return kDIErrNone;
	//if (matchCount % 8 != 0) {	// must have 4K tracks
	//	WMSG1(" ProDOS ScanForSub: matchCount %d odd number\n",
	//		matchCount);
	//	return kDIErrNone;
	//}

	/*
	 * Try #1: this is a single DOS 3.3 volume (200K or less).
	 */
	if ((matchCount % 8) == 0 && matchCount <= (50*8)) {	// max 50 tracks
		DiskFS* pNewFS = nil;
		DiskImg* pNewImg = nil;
		WMSG0(" Sub #1: looking for single DOS volume\n");
		dierr = FindSubVolume(firstBlock, matchCount, &pNewImg, &pNewFS);
		if (dierr == kDIErrNone) {
			AddSubVolumeToList(pNewImg, pNewFS);
			MarkSubVolumeBlocks(firstBlock, matchCount);
			return kDIErrNone;
		}
	}


	/*
	 * Try #2: there are multiple 140K DOS 3.3 volumes here.
	 *
	 * We may want to override their volume numbers, but it looks like
	 * DOS Master disks have distinct volume numbers anyway.
	 */
	const int kBlkCount140 = 140*2;
	if ((matchCount % (kBlkCount140)) == 0) {
		int i, count;
		bool found = false;

		count = matchCount / kBlkCount140;
		WMSG1(" Sub #2: looking for %d 140K volumes\n",
			matchCount / kBlkCount140);

		for (i = 0; i < count; i++) {
			DiskFS* pNewFS = nil;
			DiskImg* pNewImg = nil;
			WMSG1(" Sub #2: looking for DOS volume at (%d)\n",
				firstBlock + i * kBlkCount140);
			dierr = FindSubVolume(firstBlock + i * kBlkCount140,
						kBlkCount140, &pNewImg, &pNewFS);
			if (dierr == kDIErrNone) {
				AddSubVolumeToList(pNewImg, pNewFS);
				MarkSubVolumeBlocks(firstBlock + i * kBlkCount140,
						kBlkCount140);
				found = true;
			}
		}
		if (found)
			return kDIErrNone;
	}

	/*
	 * Try #3: there are five 160K DOS 3.3 volumes here (which works out
	 * to exactly 800K).  The first DOS volume loses early tracks as
	 * needed to accommodate the ProDOS directory and up to 28K of
	 * boot files.
	 *
	 * Because the first 160K volume starts at the front of the disk,
	 * we need to restrict this to non-ProDOS sub-volumes, or we'll see
	 * a "ghost" volume in the first position.  This stuff is going to
	 * fail if we test for ProDOS before we check for DOS 3.3.
	 */
	const int kBlkCount160 = 160*2;
	if (matchCount == 1537 || matchCount == 1593) {
		int i, count;
		bool found = false;

		count = 1600 / kBlkCount160;
		WMSG1(" Sub #3: looking for %d 160K volumes\n",
			matchCount / kBlkCount160);

		for (i = 0; i < count; i++) {
			DiskFS* pNewFS = nil;
			DiskImg* pNewImg = nil;
			WMSG1(" Sub #3: looking for DOS volume at (%d)\n",
				i * kBlkCount160);
			dierr = FindSubVolume(i * kBlkCount160,
						kBlkCount160, &pNewImg, &pNewFS);
			if (dierr == kDIErrNone) {
				if (pNewImg->GetFSFormat() == DiskImg::kFormatDOS33) {
					AddSubVolumeToList(pNewImg, pNewFS);
					if (i == 0)
						MarkSubVolumeBlocks(firstBlock, kBlkCount160 - firstBlock);
					else
						MarkSubVolumeBlocks(i * kBlkCount160, kBlkCount160);
				} else {
					delete pNewFS;
					delete pNewImg;
					pNewFS = nil;
					pNewImg = nil;
				}
			}
		}
		if (found)
			return kDIErrNone;
	}

	return kDIErrNone;
}

/*
 * Look for a sub-volume at the specified location.
 *
 * On success, "*ppDiskImg" and "*ppDiskFS" are newly-allocated objects
 * of the appropriate kind.
 */
DIError
DiskFSProDOS::FindSubVolume(long blockStart, long blockCount,
	DiskImg** ppDiskImg, DiskFS** ppDiskFS)
{
	DIError dierr = kDIErrNone;
	DiskFS* pNewFS = nil;
	DiskImg* pNewImg = nil;

	pNewImg = new DiskImg;
	if (pNewImg == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	dierr = pNewImg->OpenImage(fpImg, blockStart, blockCount);
	if (dierr != kDIErrNone) {
		WMSG3(" Sub: OpenImage(%ld,%ld) failed (err=%d)\n",
			blockStart, blockCount, dierr);
		goto bail;
	}

	dierr = pNewImg->AnalyzeImage();
	if (dierr != kDIErrNone) {
		WMSG1(" Sub: analysis failed (err=%d)\n", dierr);
		goto bail;
	}

	if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown ||
		pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown)
	{
		WMSG0(" Sub: unable to identify filesystem\n");
		dierr = kDIErrFilesystemNotFound;
		goto bail;
	}

	/* open a DiskFS for the sub-image */
	WMSG0(" Sub DiskImg succeeded, opening DiskFS\n");
	pNewFS = pNewImg->OpenAppropriateDiskFS();
	if (pNewFS == nil) {
		WMSG0(" Sub: OpenAppropriateDiskFS failed\n");
		dierr = kDIErrUnsupportedFSFmt;
		goto bail;
	}

	/* load the files from the sub-image */
	dierr = pNewFS->Initialize(pNewImg, kInitFull);
	if (dierr != kDIErrNone) {
		WMSG1(" Sub: error %d reading list of files from disk", dierr);
		goto bail;
	}

bail:
	if (dierr != kDIErrNone) {
		delete pNewFS;
		delete pNewImg;
	} else {
		assert(pNewImg != nil && pNewFS != nil);
		*ppDiskImg = pNewImg;
		*ppDiskFS = pNewFS;
	}
	return dierr;
}

/*
 * Mark the blocks used by a sub-volume as in-use.
 */
void
DiskFSProDOS::MarkSubVolumeBlocks(long block, long count)
{
	VolumeUsage::ChunkState cstate;

	while (count--) {
		if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) {
			assert(false);
			return;
		}

		assert(cstate.isMarkedUsed && !cstate.isUsed);
		cstate.isUsed = true;
		cstate.purpose = VolumeUsage::kChunkPurposeEmbedded;
		if (fVolumeUsage.SetChunkState(block, &cstate) != kDIErrNone) {
			assert(false);
			return;
		}

		block++;
	}
}


/*
 * Put a ProDOS filesystem image on the specified DiskImg.
 */
DIError
DiskFSProDOS::Format(DiskImg* pDiskImg, const char* volName)
{
	DIError dierr = kDIErrNone;
	const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0);
	unsigned char blkBuf[kBlkSize];
	long formatBlocks;

	if (!IsValidVolumeName(volName))
		return kDIErrInvalidArg;

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

	WMSG0(" ProDOS formatting disk image\n");

	/* write ProDOS blocks */
	dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(),
				DiskImg::kFormatGenericProDOSOrd, fpImg->GetSectorOrder());
	if (dierr != kDIErrNone)
		goto bail;

	formatBlocks = pDiskImg->GetNumBlocks();
	if (formatBlocks > 65536) {
		WMSG1(" ProDOS: rejecting format req blocks=%ld\n", formatBlocks);
		assert(false);
		return kDIErrInvalidArg;
	}
	if (formatBlocks == 65536) {
		WMSG0(" ProDOS: trimming FS size from 65536 to 65535\n");
		formatBlocks = 65535;
	}

	/*
	 * 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(" ProDOS  (not zeroing blocks)\n");

	/*
	 * Start by writing blocks 0 and 1 (the boot blocks).  This is done from
	 * a standard boot block image that happens to be essentially the same
	 * for all types of disks.  (Apparently these blocks are only used when
	 * booting 5.25" disks?)
	 */
	dierr = WriteBootBlocks();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Write the four-block disk volume entry.  Start by writing the three
	 * empty ones (which only have the prev/next pointers), and finish by
	 * writing the first block, which has the volume directory header.
	 */
	int i;
	memset(blkBuf, 0, sizeof(blkBuf));
	for (i = kVolHeaderBlock+1; i < kVolHeaderBlock+kVolDirExpectedNumBlocks; i++)
	{
		PutShortLE(&blkBuf[0x00], i-1);
		if (i == kVolHeaderBlock+kVolDirExpectedNumBlocks-1)
			PutShortLE(&blkBuf[0x02], 0);
		else
			PutShortLE(&blkBuf[0x02], i+1);

		dierr = fpImg->WriteBlock(i, blkBuf);
		if (dierr != kDIErrNone) {
			WMSG2(" Format: block %d write failed (err=%d)\n", i, dierr);
			goto bail;
		}
	}

	char upperName[A2FileProDOS::kMaxFileName+1];
	unsigned short lcFlags;
	time_t now;

	now = time(nil);

	/*
	 * Compute the lower-case flags, if desired.  The test for "allowLowerCase"
	 * is probably bogus, because in most cases we just got created by the
	 * DiskImg and the app hasn't had time to set the "allow lower" flag.
	 * So it defaults to "enabled", which means the app needs to manually
	 * change the volume name to lower case.
	 */
	UpperCaseName(upperName, volName);
	lcFlags = 0;
	if (allowLowerCase)
		lcFlags = GenerateLowerCaseBits(upperName, volName, false);

	PutShortLE(&blkBuf[0x00], 0);
	PutShortLE(&blkBuf[0x02], kVolHeaderBlock+1);
	blkBuf[0x04] = strlen(upperName) | (A2FileProDOS::kStorageVolumeDirHeader << 4);
	strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName);
	PutLongLE(&blkBuf[0x16], A2FileProDOS::ConvertProDate(now));
	PutShortLE(&blkBuf[0x1a], lcFlags);
	PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(now));
	blkBuf[0x20] = 0;			// GS/OS uses 5?
	/* min_version is zero */
	blkBuf[0x22] = 0xe3;		// access (format/rename/backup/write/read)
	blkBuf[0x23] = 0x27;		// entry_length: always $27
	blkBuf[0x24] = 0x0d;		// entries_per_block: always $0d
	/* file_count is zero - does not include volume dir */
	PutShortLE(&blkBuf[0x27], kVolHeaderBlock + kVolDirExpectedNumBlocks); // bit_map_pointer
	PutShortLE(&blkBuf[0x29], (unsigned short) formatBlocks); // total_blocks
	dierr = fpImg->WriteBlock(kVolHeaderBlock, blkBuf);
	if (dierr != kDIErrNone) {
		WMSG2(" Format: block %d write failed (err=%d)\n",
			kVolHeaderBlock, dierr);
		goto bail;
	}

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

	/*
	 * Generate the initial block usage map.  The only entries in use are
	 * right at the start of the disk.  When we finish, scan what we just
	 * created into
	 */
	CreateEmptyBlockMap();

	/* 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;
}


/*
 * The standard boot block found on ProDOS disks.  The same thing appears
 * to be written to both 5.25" and 3.5" disks, with some modifications
 * made for HD images.
 *
 * This is block 0; block 1 is either zeroed out or filled with a repeating
 * pattern.
 */
const unsigned char gFloppyBlock0[512] = {
    0x01, 0x38, 0xb0, 0x03, 0x4c, 0x32, 0xa1, 0x86, 0x43, 0xc9, 0x03, 0x08,
    0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49, 0xa0,
    0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e, 0xa9,
    0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b, 0x48,
    0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x63, 0xb1, 0x48, 0x99, 0x94, 0x09,
    0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x1d, 0x09, 0xbd, 0x24,
    0x09, 0x99, 0xf2, 0x09, 0xbd, 0x2b, 0x09, 0x9d, 0x7f, 0x0a, 0xca, 0x10,
    0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9, 0xb0,
	0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e, 0x84,
	0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61, 0x85,
	0x4b, 0x20, 0x12, 0x09, 0xb0, 0x68, 0xe6, 0x61, 0xe6, 0x61, 0xe6, 0x46,
	0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01, 0x0c,
	0xd0, 0x6d, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23, 0x0c,
	0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9, 0x0a,
	0xf0, 0x55, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x02, 0x09, 0x29, 0x0f, 0xa8,
	0xb1, 0x4a, 0xd9, 0x02, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0x29, 0xf0,
	0xc9, 0x20, 0xd0, 0x3b, 0xa0, 0x10, 0xb1, 0x4a, 0xc9, 0xff, 0xd0, 0x33,
	0xc8, 0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00,
	0x85, 0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20,
	0x12, 0x09, 0xb0, 0x17, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e,
	0xb1, 0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0xe7,
	0x4c, 0x00, 0x20, 0x4c, 0x3f, 0x09, 0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f,
	0x53, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xa5, 0x60,
	0x85, 0x44, 0xa5, 0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24,
	0x3f, 0x45, 0x47, 0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6,
	0x2b, 0x18, 0x60, 0x4c, 0xbc, 0x09, 0xa9, 0x9f, 0x48, 0xa9, 0xff, 0x48,
	0xa9, 0x01, 0xa2, 0x00, 0x4c, 0x79, 0xf4, 0x20, 0x58, 0xfc, 0xa0, 0x1c,
	0xb9, 0x50, 0x09, 0x99, 0xae, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x4d, 0x09,
	0xaa, 0xaa, 0xaa, 0xa0, 0xd5, 0xce, 0xc1, 0xc2, 0xcc, 0xc5, 0xa0, 0xd4,
	0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0, 0xd2, 0xcf, 0xc4, 0xcf,
	0xd3, 0xa0, 0xaa, 0xaa, 0xaa, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b,
	0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9,
	0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04,
	0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5,
	0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85,
	0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6,
	0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60,
	0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85,
	0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02,
	0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09,
	0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0,
	0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

unsigned char gHDBlock0[] = {
	0x01, 0x38, 0xb0, 0x03, 0x4c, 0x1c, 0x09, 0x78, 0x86, 0x43, 0xc9, 0x03,
	0x08, 0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49,
	0xa0, 0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e,
	0xa9, 0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b,
	0x48, 0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x5e, 0xb1, 0x48, 0x99, 0x94,
	0x09, 0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x32, 0x09, 0xbd,
	0x39, 0x09, 0x99, 0xf2, 0x09, 0xbd, 0x40, 0x09, 0x9d, 0x7f, 0x0a, 0xca,
	0x10, 0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9,
	0xb0, 0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e,
	0x84, 0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61,
	0x85, 0x4b, 0x20, 0x27, 0x09, 0xb0, 0x66, 0xe6, 0x61, 0xe6, 0x61, 0xe6,
	0x46, 0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01,
	0x0c, 0xd0, 0x52, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23,
	0x0c, 0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9,
	0x0a, 0xf0, 0x71, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x20, 0x09, 0x29, 0x0f,
	0xa8, 0xb1, 0x4a, 0xd9, 0x20, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0xa0,
	0x16, 0xb1, 0x4a, 0x4a, 0x6d, 0x1f, 0x09, 0x8d, 0x1f, 0x09, 0xa0, 0x11,
	0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00, 0x85,
	0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20, 0x27,
	0x09, 0xb0, 0x35, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e, 0xb1,
	0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0x18, 0xa2,
	0x01, 0xa9, 0x00, 0xa8, 0x91, 0x60, 0xc8, 0xd0, 0xfb, 0xe6, 0x61, 0xea,
	0xea, 0xca, 0x10, 0xf4, 0xce, 0x1f, 0x09, 0xf0, 0x07, 0xd0, 0xd8, 0xce,
	0x1f, 0x09, 0xd0, 0xca, 0x58, 0x4c, 0x00, 0x20, 0x4c, 0x47, 0x09, 0x02,
	0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f, 0x53, 0xa5, 0x60, 0x85, 0x44, 0xa5,
	0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24, 0x3f, 0x45, 0x47,
	0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6, 0x2b, 0x18, 0x60,
	0x4c, 0xbc, 0x09, 0x20, 0x58, 0xfc, 0xa0, 0x14, 0xb9, 0x58, 0x09, 0x99,
	0xb1, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x55, 0x09, 0xd5, 0xce, 0xc1, 0xc2,
	0xcc, 0xc5, 0xa0, 0xd4, 0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0,
	0xd2, 0xcf, 0xc4, 0xcf, 0xd3, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b,
	0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9,
	0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04,
	0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5,
	0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85,
	0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6,
	0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60,
	0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85,
	0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02,
	0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09,
	0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0,
	0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

/*
 * Write the ProDOS boot blocks onto the disk image.
 */
DIError
DiskFSProDOS::WriteBootBlocks(void)
{
	DIError dierr;
	unsigned char block0[512];
	unsigned char block1[512];
	bool isHD;

	assert(fpImg->GetHasBlocks());

	if (fpImg->GetNumBlocks() == 280 || fpImg->GetNumBlocks() == 1600)
		isHD = false;
	else
		isHD = true;

	if (isHD) {
		memcpy(block0, gHDBlock0, sizeof(block0));
		// repeating 0x42 0x48 pattern
		int i;
		unsigned char* ucp;
		for (i = 0, ucp = block1; i < (int)sizeof(block1); i++)
			*ucp++ = 0x42 + 6 * (i & 0x01);
	} else {
		memcpy(block0, gFloppyBlock0, sizeof(block0));
		memset(block1, 0, sizeof(block1));
	}

    dierr = fpImg->WriteBlock(0, block0);
    if (dierr != kDIErrNone) {
        WMSG1(" WriteBootBlocks: block0 write failed (err=%d)\n", dierr);
        return dierr;
    }
    dierr = fpImg->WriteBlock(1, block1);
    if (dierr != kDIErrNone) {
        WMSG1(" WriteBootBlocks: block1 write failed (err=%d)\n", dierr);
        return dierr;
    }

	return kDIErrNone;
}


/*
 * Create a new, empty file.  There are three different kinds of files we
 * need to be able to handle:
 *	(1) Standard file.  Create the directory entry and an empty "seedling"
 *	  file with one block allocated.  It does not appear that "sparse"
 *	  allocation applies to seedlings.
 *	(2) Extended file.  Create the directory entry, the extended key block,
 *	  and allocate one seedling block for each fork.
 *	(3) Subdirectory.  Allocate a block for the subdir and fill in the
 *	  details in the subdir header.
 *
 * In all cases we need to add a new directory entry as well.
 *
 * By not flushing the updated block usage map and the updated directory
 * block(s) until we're done, we can abort our changes at any time if we
 * encounter a damaged sector or run out of disk space.  We do need to be
 * careful when updating our internal copies of things like file storage
 * types and lengths, updating them only after everything else has
 * succeeded.
 *
 * NOTE: if we detect an empty directory holder, "*ppNewFile" does NOT
 * end up pointing at a file.
 *
 * NOTE: kParm_CreateUnique does *not* apply to creating subdirectories.
 */
DIError
DiskFSProDOS::CreateFile(const CreateParms* pParms,	A2File** ppNewFile)
{
	DIError dierr = kDIErrNone;
	char* normalizedPath = nil;
	char* basePath = nil;
	char* fileName = nil;
	A2FileProDOS* pSubdir = nil;
	A2FileDescr* pOpenSubdir = nil;
	A2FileProDOS* pNewFile = nil;
	unsigned char* subdirBuf = nil;
	const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0);
	const bool createUnique = (GetParameter(kParm_CreateUnique) != 0);
	char upperName[A2FileProDOS::kMaxFileName+1];
	char lowerName[A2FileProDOS::kMaxFileName+1];

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

	assert(pParms != nil);
	assert(pParms->pathName != nil);
	assert(pParms->storageType == A2FileProDOS::kStorageSeedling ||
		   pParms->storageType == A2FileProDOS::kStorageExtended ||
		   pParms->storageType == A2FileProDOS::kStorageDirectory);
	// kStorageVolumeDirHeader not allowed -- that's created by Format
	WMSG1(" ProDOS ---v--- CreateFile '%s'\n", pParms->pathName);
	*ppNewFile = nil;

	/*
	 * Normalize the pathname so that all components are ProDOS-safe
	 * and separated by ':'.
	 */
	assert(pParms->pathName != nil);
	dierr = DoNormalizePath(pParms->pathName, pParms->fssep,
				&normalizedPath);
	if (dierr != kDIErrNone)
		goto bail;
	assert(normalizedPath != nil);

	/*
	 * Split the base path and filename apart.
	 */
	char* cp;
	cp = strrchr(normalizedPath, A2FileProDOS::kFssep);
	if (cp == nil) {
		assert(basePath == nil);
		fileName = normalizedPath;
	} else {
		fileName = new char[strlen(cp+1) +1];
		strcpy(fileName, cp+1);
		*cp = '\0';
		basePath = normalizedPath;
	}
	normalizedPath = nil;	// either fileName or basePath points here now

	assert(fileName != nil);
	//WMSG2(" ProDOS normalized to '%s':'%s'\n",
	//	basePath == nil ? "" : basePath, fileName);

	/*
	 * Open the base path.  If it doesn't exist, create it recursively.
	 */
	if (basePath != nil) {
		WMSG2(" ProDOS  Creating '%s' in '%s'\n", fileName, basePath);
		/* open the named subdir, creating it if it doesn't exist */
		pSubdir = (A2FileProDOS*)GetFileByName(basePath);
		if (pSubdir == nil) {
			WMSG1("  ProDOS  Creating subdir '%s'\n", basePath);
			A2File* pNewSub;
			CreateParms newDirParms;
			newDirParms.pathName = basePath;
			newDirParms.fssep = A2FileProDOS::kFssep;
			newDirParms.storageType = A2FileProDOS::kStorageDirectory;
			newDirParms.fileType = kTypeDIR;	// 0x0f
			newDirParms.auxType = 0;
			newDirParms.access = 0xe3;	// unlocked, backup bit set
			newDirParms.createWhen = newDirParms.modWhen = time(nil);
			dierr = this->CreateFile(&newDirParms, &pNewSub);
			if (dierr != kDIErrNone)
				goto bail;
			assert(pNewSub != nil);

			pSubdir = (A2FileProDOS*) pNewSub;
		}

		/*
		 * And now the annoying part.  We need to reconstruct basePath out
		 * of the filenames actually present, rather than relying on the
		 * argument passed in.  That's because some directories might have
		 * lower-case flags and some might not, and we do case-insensitive
		 * comparisons.  It's not crucial for our inner workings, but the
		 * linear file list in the DiskFS should have accurate strings.
		 * (It'll work just fine, but the display might show the wrong values
		 * for parent directories until they reload the disk.)
		 *
		 * On the bright side, we know exactly how long the string needs
		 * to be, so we can just stomp on it in place.  Assuming, of course,
		 * that the filename created matches up with what the filename
		 * normalizer came up with, which we can guarantee since (a) everybody
		 * uses the same normalizer and (b) the "uniqueify" stuff doesn't
		 * kick in for subdirs because we wouldn't be creating a new subdir
		 * if it didn't already exist.
		 *
		 * This is essentially the same as RegeneratePathName(), but that's
		 * meant for a situation where the filename already exists.
		 */
		A2FileProDOS* pBaseDir = pSubdir;
		int basePathLen = strlen(basePath);
		while (!pBaseDir->IsVolumeDirectory()) {
			const char* fixedName = pBaseDir->GetFileName();
			int fixedLen = strlen(fixedName);
			if (fixedLen > basePathLen) {
				assert(false);
				break;
			}
			assert(basePathLen == fixedLen ||
				   *(basePath + (basePathLen-fixedLen-1)) == kDIFssep);
			memcpy(basePath + (basePathLen-fixedLen), fixedName, fixedLen);
			basePathLen -= fixedLen+1;

			pBaseDir = (A2FileProDOS*) pBaseDir->GetParent();
			assert(pBaseDir != nil);
		}
		// check the math
		if (pSubdir->IsVolumeDirectory())
			assert(basePathLen == 0);
		else
			assert(basePathLen == -1);
	} else {
		/* open the volume directory */
		WMSG1(" ProDOS  Creating '%s' in volume dir\n", fileName);
		/* volume dir must be first in the list */
		pSubdir = (A2FileProDOS*) GetNextFile(nil);
		assert(pSubdir != nil);
		assert(pSubdir->IsVolumeDirectory());
	}
	if (pSubdir == nil) {
		WMSG1(" ProDOS Unable to open subdir '%s'\n", basePath);
		dierr = kDIErrFileNotFound;
		goto bail;
	}

	/*
	 * Load the block usage map into memory.  All changes, to the end of this
	 * function, are made to the in-memory copy and can be "undone" by simply
	 * throwing the temporary map away.
	 */
	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		return dierr;

	/*
	 * Load the subdir or volume dir into memory, and alloc a new directory
	 * entry.
	 */
	dierr = pSubdir->Open(&pOpenSubdir, false);
	if (dierr != kDIErrNone)
		goto bail;

	unsigned char* dirEntryPtr;
	long dirLen;
	unsigned short dirBlock, dirKeyBlock;
	int dirEntrySlot;
	dierr = AllocDirEntry(pOpenSubdir, &subdirBuf, &dirLen, &dirEntryPtr,
				&dirKeyBlock, &dirEntrySlot, &dirBlock);
	if (dierr != kDIErrNone)
		goto bail;

	assert(subdirBuf != nil);
	assert(dirLen > 0);
	assert(dirKeyBlock > 0);
	assert(dirEntrySlot >= 0);
	assert(dirBlock > 0);

	/*
	 * Create a copy of the filename with everything in upper case and spaces
	 * changed to periods.
	 */
	UpperCaseName(upperName, fileName);

	/*
	 * Make the name unique within the current directory.  This requires
	 * appending digits until the name doesn't match any others.
	 *
	 * The filename buffer ("upperName") must be able to hold kMaxFileName+1
	 * chars.  It will be modified in place.
	 */
	if (createUnique &&
		pParms->storageType != A2FileProDOS::kStorageDirectory)
	{
		MakeFileNameUnique(subdirBuf, dirLen, upperName);
	} else {
		/* check to see if it already exists */
		if (NameExistsInDir(subdirBuf, dirLen, upperName)) {
			if (pParms->storageType == A2FileProDOS::kStorageDirectory)
				dierr = kDIErrDirectoryExists;
			else
				dierr = kDIErrFileExists;
			goto bail;
		}
	}

	/*
	 * Allocate file storage and initialize:
	 *	- For directory, a single block with the directory header.
	 *	- For seedling, an empty block.
	 *	- For extended, an extended key block entry and two empty blocks.
	 */
	long keyBlock;
	int blocksUsed;
	int newEOF;
	keyBlock = -1;
	blocksUsed = newEOF = -1;

	dierr = AllocInitialFileStorage(pParms, upperName, dirBlock,
				dirEntrySlot, &keyBlock, &blocksUsed, &newEOF);
	if (dierr != kDIErrNone)
		goto bail;

	assert(blocksUsed > 0);
	assert(keyBlock > 0);
	assert(newEOF >= 0);

	/*
	 * Fill out the newly-created directory entry pointed to by "dirEntryPtr".
	 *
	 * ProDOS filenames are always stored in upper case.  ProDOS 8 v1.8 and
	 * later allow lower-case names with '.' converting to ' '.  We optionally
	 * set the flags here, using the original file name to decide which parts
	 * are lower case.  (Some parts of the original may have been stomped
	 * when the name was made unique, so we need to watch for that.)
	 */
	dirEntryPtr[0x00] = (pParms->storageType << 4) | strlen(upperName);
	strncpy((char*) &dirEntryPtr[0x01], upperName, A2FileProDOS::kMaxFileName);
	if (pParms->fileType >= 0 && pParms->fileType <= 0xff)
		dirEntryPtr[0x10] = (unsigned char) pParms->fileType;
	else
		dirEntryPtr[0x10] = 0;		// HFS long type?
	PutShortLE(&dirEntryPtr[0x11], (unsigned short) keyBlock);
	PutShortLE(&dirEntryPtr[0x13], blocksUsed);
	PutShortLE(&dirEntryPtr[0x15], newEOF);
	dirEntryPtr[0x17] = 0;		// high byte of EOF
	PutLongLE(&dirEntryPtr[0x18], A2FileProDOS::ConvertProDate(pParms->createWhen));
	if (allowLowerCase) {
		unsigned short lcBits;
		lcBits = GenerateLowerCaseBits(upperName, fileName, false);
		GenerateLowerCaseName(upperName, lowerName, lcBits, false);
		lowerName[strlen(upperName)] = '\0';

		PutShortLE(&dirEntryPtr[0x1c], lcBits);
	} else {
		strcpy(lowerName, upperName);
		PutShortLE(&dirEntryPtr[0x1c], 0);	// version, min_version
	}
	dirEntryPtr[0x1e] = pParms->access;
	if (pParms->auxType >= 0 && pParms->auxType <= 0xffff)
		PutShortLE(&dirEntryPtr[0x1f], (unsigned short) pParms->auxType);
	else
		PutShortLE(&dirEntryPtr[0x1f], 0);
	PutLongLE(&dirEntryPtr[0x21], A2FileProDOS::ConvertProDate(pParms->modWhen));
	PutShortLE(&dirEntryPtr[0x25], dirKeyBlock);

	/*
	 * Write updated directory.  If this succeeds, we can no longer undo
	 * what we have done by simply bailing.  If this fails partway through,
	 * we might have a corrupted disk, so it's best to ensure that it's not
	 * going to fail before we call.
	 *
	 * Assuming this isn't a nibble image with I/O errors, the only way we
	 * can really fail is by running out of disk space.  The block has been
	 * pre-allocated, so this should always work.
	 */
	dierr = pOpenSubdir->Write(subdirBuf, dirLen);
	if (dierr != kDIErrNone) {
		WMSG1(" ProDOS directory write failed (dirLen=%ld)\n", dirLen);
		goto bail;
	}

	/*
	 * Flush updated block usage map.
	 */
	dierr = SaveVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Success!
	 *
	 * Create an A2File entry for this, and add it to the list.  The calls
	 * below will re-process some of what we just created, which is slightly
	 * inefficient but helps guarantee that we aren't creating bogus data
	 * structures that won't match what we see when the disk is reloaded.
	 *
	 * - Regen or update internal VolumeUsage map??  Throw it away or mark
	 * it as invalid?
	 */
	pNewFile = new A2FileProDOS(this);

	A2FileProDOS::DirEntry* pEntry;
	pEntry = &pNewFile->fDirEntry;

	A2FileProDOS::InitDirEntry(pEntry, dirEntryPtr);

	pNewFile->fParentDirBlock = dirBlock;
	pNewFile->fParentDirIdx = (dirEntrySlot-1) % kEntriesPerBlock;
	pNewFile->fSparseDataEof = 0;
	pNewFile->fSparseRsrcEof = 0;

	/*
	 * Get the properly-cased filename for the file list.  We already have
	 * a name in "lowerName", but it doesn't take AppleWorks aux type
	 * case stuff into account.  If necessary, deal with it now.
	 */
	if (A2FileProDOS::UsesAppleWorksAuxType(pNewFile->fDirEntry.fileType)) {
		DiskFSProDOS::GenerateLowerCaseName(pNewFile->fDirEntry.fileName,
			lowerName, pNewFile->fDirEntry.auxType, true);
	}
	pNewFile->SetPathName(basePath == nil ? "" : basePath, lowerName);

	if (pEntry->storageType == A2FileProDOS::kStorageExtended) {
		dierr = ReadExtendedInfo(pNewFile);
		if (dierr != kDIErrNone) {
			WMSG0(" ProDOS GLITCH: readback of extended block failed!\n");
			delete pNewFile;
			goto bail;
		}
	}

	pNewFile->SetParent(pSubdir);
	//pNewFile->Dump();

	/*
	 * Because we're hierarchical, and we guarantee that the contents of
	 * subdirectories are grouped together, we must insert the file into an
	 * appropriate place in the list rather than just throwing it onto the
	 * end.
	 *
	 * The proper location for the new file in the linear list is after the
	 * previous file in our subdir.  If we're the first item in the subdir,
	 * we get added right after the parent.  If not, we need to scan, starting
	 * from the parent, for an entry in the file list whose key block pointer
	 * matches that of the previous item in the list.
	 *
	 * We wouldn't be this far if the disk were damaged, so we don't have to
	 * worry too much about weirdness.  The directory entry allocator always
	 * returns the first available, so we know the previous entry is valid.
	 */
	unsigned char* prevDirEntryPtr;
	prevDirEntryPtr = GetPrevDirEntry(subdirBuf, dirEntryPtr);
	if (prevDirEntryPtr == nil) {
		/* previous entry is volume or subdir header */
		InsertFileInList(pNewFile, pNewFile->GetParent());
		WMSG2("Inserted '%s' after '%s'\n",
			pNewFile->GetPathName(), pNewFile->GetParent()->GetPathName());
	} else {
		/* dig out the key block pointer and find the matching file */
		unsigned short prevKeyBlock;
		assert((prevDirEntryPtr[0x00] & 0xf0) != 0);		// verify storage type
		prevKeyBlock = GetShortLE(&prevDirEntryPtr[0x11]);
		A2File* pPrev;
		pPrev = FindFileByKeyBlock(pNewFile->GetParent(), prevKeyBlock);
		if (pPrev == nil) {
			/* should be impossible! */
			assert(false);
			AddFileToList(pNewFile);
		} else {
			/* insert the new file in the list after the previous file */
			InsertFileInList(pNewFile, pPrev);
		}
	}
//	WMSG0("LIST NOW:\n");
//	DumpFileList();

	*ppNewFile = pNewFile;
	pNewFile = nil;

bail:
	delete pNewFile;
	if (pOpenSubdir != nil)
		pOpenSubdir->Close();	// writes updated dir entry in parent dir
	FreeVolBitmap();
	delete[] normalizedPath;
	delete[] subdirBuf;
	delete[] fileName;
	delete[] basePath;
	WMSG1(" ProDOS ---^--- CreateFile '%s' DONE\n", pParms->pathName);
	return dierr;
}

/*
 * Run through the DiskFS file list, looking for an entry with a matching
 * key block.
 */
A2File*
DiskFSProDOS::FindFileByKeyBlock(A2File* pStart, unsigned short keyBlock)
{
	while (pStart != nil) {
		A2FileProDOS* pPro = (A2FileProDOS*) pStart;

		if (pPro->fDirEntry.keyPointer == keyBlock)
			return pStart;

		pStart = GetNextFile(pStart);
	}

	return nil;
}

/*
 * Allocate the initial storage (key blocks, directory header) for a new file.
 *
 * Output values are the key block for the new file, the number of blocks
 * used, and an EOF value.
 *
 * "upperName" is the upper-case name for the file.  "dirBlock" and
 * "dirEntrySlot" refer to the entry in the higher-level directory for this
 * file, and are only needed when creating a new subdir (because the first
 * entry in a subdir points to its entry in the parent dir).
 */
DIError
DiskFSProDOS::AllocInitialFileStorage(const CreateParms* pParms,
	const char* upperName, unsigned short dirBlock, int dirEntrySlot,
	long* pKeyBlock, int* pBlocksUsed, int* pNewEOF)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	long keyBlock;
	int blocksUsed;
	int newEOF;

	blocksUsed = -1;
	keyBlock = -1;
	newEOF = 0;
	memset(blkBuf, 0, sizeof(blkBuf));

	if (pParms->storageType == A2FileProDOS::kStorageSeedling) {
		keyBlock = AllocBlock();
		if (keyBlock == -1) {
			dierr = kDIErrDiskFull;
			goto bail;
		}
		blocksUsed = 1;

		/* write zeroed block */
		dierr = fpImg->WriteBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;
	} else if (pParms->storageType == A2FileProDOS::kStorageExtended) {
		long dataBlock, rsrcBlock;

		dataBlock = AllocBlock();
		rsrcBlock = AllocBlock();
		keyBlock = AllocBlock();
		if (dataBlock < 0 || rsrcBlock < 0 || keyBlock < 0) {
			dierr = kDIErrDiskFull;
			goto bail;
		}
		blocksUsed = 3;
		newEOF = kBlkSize;

		/* write zeroed block */
		dierr = fpImg->WriteBlock(dataBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;
		dierr = fpImg->WriteBlock(rsrcBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;

		/* fill in extended key block details */
		blkBuf[0x00] = blkBuf[0x100] = A2FileProDOS::kStorageSeedling;
		PutShortLE(&blkBuf[0x01], (unsigned short) dataBlock);
		PutShortLE(&blkBuf[0x101], (unsigned short) rsrcBlock);
		blkBuf[0x03] = blkBuf[0x103] = 1;		// blocks used (lo byte)
		/* 3 bytes at 0x05 hold EOF, currently 0 */

		dierr = fpImg->WriteBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;
	} else if (pParms->storageType == A2FileProDOS::kStorageDirectory) {
		keyBlock = AllocBlock();
		if (keyBlock == -1) {
			dierr = kDIErrDiskFull;
			goto bail;
		}
		blocksUsed = 1;
		newEOF = kBlkSize;

		/* fill in directory header fields */
		// 0x00: prev, set to zero
		// 0x02: next, set to zero
		blkBuf[0x04] = (A2FileProDOS::kStorageSubdirHeader << 4) | strlen(upperName);
		strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName);
		blkBuf[0x14] = 0x76;	// 0x75 under old P8, 0x76 under GS/OS
		PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(pParms->createWhen));
		blkBuf[0x20] = 5;		// 0 under 1.0, 3 under v1.4?, 5 under GS/OS
		blkBuf[0x21] = 0;
		blkBuf[0x22] = pParms->access;
		blkBuf[0x23] = kEntryLength;
		blkBuf[0x24] = kEntriesPerBlock;
		PutShortLE(&blkBuf[0x25], 0);		// file count
		PutShortLE(&blkBuf[0x27], dirBlock);
		blkBuf[0x29] = (unsigned char) dirEntrySlot;
		blkBuf[0x2a] = kEntryLength;	// the parent dir's entry length

		dierr = fpImg->WriteBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;
	} else {
		assert(false);
		dierr = kDIErrInternal;
		goto bail;
	}

	*pKeyBlock = keyBlock;
	*pBlocksUsed = blocksUsed;
	*pNewEOF = newEOF;

bail:
	return dierr;
}


/*
 * Scan for damaged files and mysterious or conflicting block usage map
 * entries.
 *
 * Appends some entries to the DiskImg notes, so this should only be run
 * once per DiskFS.
 *
 * This function doesn't set anything; it's effectively "const" except
 * that LoadVolBitmap is inherently non-const.
 *
 * Returns "true" if disk appears to be perfect, "false" otherwise.
 */
bool
DiskFSProDOS::CheckDiskIsGood(void)
{
	DIError dierr;
	bool result = true;
	int i;

	if (fEarlyDamage)
		result = false;

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

	/*
	 * Check the system blocks to see if any of them are marked as free.
	 * If so, refuse to write to this disk.
	 */
	if (!GetBlockUseEntry(0) || !GetBlockUseEntry(1)) {
		fpImg->AddNote(DiskImg::kNoteWarning, "Block 0/1 marked as free.");
		result = false;
	}
	for (i = GetNumBitmapBlocks(); i > 0; i--) {
		if (!GetBlockUseEntry(fBitMapPointer + i -1)) {
			fpImg->AddNote(DiskImg::kNoteWarning,
				"One or more bitmap blocks are marked as free.");
			result = false;
			break;
		}
	}

	/*
	 * 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 blk, notMarked, extraUsed, conflicts;
	notMarked = extraUsed = conflicts = 0;
	for (blk = 0; blk < fVolumeUsage.GetNumChunks(); blk++) {
		dierr = fVolumeUsage.GetChunkState(blk, &cstate);
		if (dierr != kDIErrNone) {
			fpImg->AddNote(DiskImg::kNoteWarning,
				"Internal volume usage error on blk=%ld.", blk);
			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 block%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 block%s used by files but not marked used.",
			notMarked, notMarked == 1 ? " is" : "s are");
		result = false;		// very bad -- any change could trash files
	}
	if (conflicts > 0) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"%ld block%s used by more than one file.",
			conflicts, conflicts == 1 ? " is" : "s are");
		result = false;		// kinda bad -- file deletion leads to trouble
	}

	/*
	 * Check for bits set past the end of the actually-needed bits.  For
	 * some reason P8 and GS/OS both examine these bits, and GS/OS will
	 * freak out completely and claim the disk is unrecognizeable ("would
	 * you like to format?") if they're set.
	 */
	if (ScanForExtraEntries()) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"Blocks past the end of the disk are marked 'in use' in the"
			" volume bitmap.");
		/* don't flunk the disk just for this */
	}

	/*
	 * Scan for "damaged" 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;
}


/*
 * Test a string for validity as a ProDOS volume name.  Syntax is the same as
 * ProDOS file names, but we also disallow spaces.
 */
/*static*/ bool
DiskFSProDOS::IsValidVolumeName(const char* name)
{
	assert((int) A2FileProDOS::kMaxFileName == (int) kMaxVolumeName);
	if (!IsValidFileName(name))
		return false;
	while (*name != '\0') {
		if (*name++ == ' ')
			return false;
	}
	return true;
}

/*
 * Test a string for validity as a ProDOS file name.  Names may be 1-15
 * characters long, must start with a letter, and may contain letters and
 * digits.
 *
 * Lower case and spaces (a/k/a lower-case '.') are accepted.  Trailing
 * spaces are not allowed.
 */
/*static*/ bool
DiskFSProDOS::IsValidFileName(const char* name)
{
	if (name == nil) {
		assert(false);
		return false;
	}

	/* must be 1-15 characters long */
	if (name[0] == '\0')
		return false;
	if (strlen(name) > A2FileProDOS::kMaxFileName)
		return false;

	/* must begin with letter; this also catches zero-length filenames */
	if (toupper(name[0]) < 'A' || toupper(name[0]) > 'Z')
		return false;

	/* no trailing spaces */
	if (name[strlen(name)-1] == ' ')
		return false;

	/* must be A-Za-z 0-9 '.' ' ' */
	name++;
	while (*name != '\0') {
		if (!(	(toupper(*name) >= 'A' && toupper(*name) <= 'Z') ||
				(*name >= '0' && *name <= '9') ||
				(*name == '.') ||
				(*name == ' ')
			 ))
		{
			return false;
		}

		name++;
	}

	return true;
}

/*
 * Generate lower case flags by comparing "upperName" to "lowerName".
 *
 * It's okay for "lowerName" to be longer than "upperName".  The extra chars
 * are just ignored.  Similarly, "lowerName" does not need to be
 * null-terminated.  "lowerName" does need to point to storage with at least
 * as many valid bytes as "upperName", though, or we could crash.
 *
 * Returns the mask to use in a ProDOS dir.  If "forAppleWorks" is set to
 * "true", the mask is modified for use with an AppleWorks aux type.
 */
/*static*/ unsigned short
DiskFSProDOS::GenerateLowerCaseBits(const char* upperName,
	const char* lowerName, bool forAppleWorks)
{
	unsigned short caseMask = 0x8000;
	unsigned short caseBit = 0x8000;
	int len, i;
	char lowch;
	
	len = strlen(upperName);
	assert(len <= A2FileProDOS::kMaxFileName);

	for (i = 0; i < len; i++) {
		caseBit >>= 1;
		lowch = A2FileProDOS::NameToLower(upperName[i]);
		if (lowch == lowerName[i])
			caseMask |= caseBit;
	}

	if (forAppleWorks) {
		unsigned short adjusted;
		caseMask <<= 1;
		adjusted = caseMask << 8 | caseMask >> 8;
		return adjusted;
	} else {
		if (caseMask == 0x8000)
			return 0;	// all upper case, don't freak out pre-v1.8
		else
			return caseMask;
	}
}

/*
 * Generate the lower-case version of a ProDOS filename, using the supplied
 * lower case flags.  "lowerName" must be able to hold 15 chars (enough for
 * a filename or volname).
 *
 * The string will NOT be null-terminated, but the output buffer will be padded
 * with NULs out to the maximum filename len.  This makes it suitable for
 * copying directly into directory block buffers.
 *
 * It's okay to pass the same buffer for "upperName" and "lowerName".
 *
 * "lcFlags" is either ProDOS directory flags or AppleWorks aux type flags,
 * depending on the value of "fromAppleWorks".
 */
/*static*/ void
DiskFSProDOS::GenerateLowerCaseName(const char* upperName, char* lowerName,
	unsigned short lcFlags, bool fromAppleWorks)
{
	int nameLen = strlen(upperName);
	int bit;
	assert(nameLen <= A2FileProDOS::kMaxFileName);

	if (fromAppleWorks) {
		/* handle AppleWorks lower-case-in-auxtype */
		unsigned short caseMask =	// swap bytes
			(lcFlags << 8) | (lcFlags >> 8);
		for (bit = 0; bit < nameLen ; bit++) {
			if ((caseMask & 0x8000) != 0)
				lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]);
			else
				lowerName[bit] = upperName[bit];
			caseMask <<= 1;
		}
		for ( ; bit < A2FileProDOS::kMaxFileName; bit++)
			lowerName[bit] = '\0';
	} else {
		/* handle lower-case conversion; see GS/OS tech note #8 */
		if (lcFlags != 0 && !(lcFlags & 0x8000)) {
			// Should be zero or 0x8000 plus other bits; shouldn't be
			//  bunch of bits without 0x8000 or 0x8000 by itself.  Not
			//  really a problem, just unexpected.
			assert(false);
			memcpy(lowerName, upperName, A2FileProDOS::kMaxFileName);
			return;
		}
		for (bit = 0; bit < nameLen; bit++) {
			lcFlags <<= 1;
			if ((lcFlags & 0x8000) != 0)
				lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]);
			else
				lowerName[bit] = upperName[bit];
		}
	}
	for ( ; bit < A2FileProDOS::kMaxFileName; bit++)
		lowerName[bit] = '\0';
}

/*
 * Normalize a ProDOS path.  Invokes DoNormalizePath and handles the buffer
 * management (if the normalized path doesn't fit in "*pNormalizedBufLen"
 * bytes, we set "*pNormalizedBufLen to the required length).
 *
 * This is invoked from the generalized "add" function in CiderPress, which
 * doesn't want to understand the ins and outs of ProDOS pathnames.
 */
DIError
DiskFSProDOS::NormalizePath(const char* path, char fssep,
	char* normalizedBuf, int* pNormalizedBufLen)
{
	DIError dierr = kDIErrNone;
	char* normalizedPath = nil;
	int len;

	assert(pNormalizedBufLen != nil);
	assert(normalizedBuf != nil || *pNormalizedBufLen == 0);

	dierr = DoNormalizePath(path, fssep, &normalizedPath);
	if (dierr != kDIErrNone)
		goto bail;

	assert(normalizedPath != nil);
	len = strlen(normalizedPath);
	if (normalizedBuf == nil || *pNormalizedBufLen <= len) {
		/* too short */
		dierr = kDIErrDataOverrun;
	} else {
		/* fits */
		strcpy(normalizedBuf, normalizedPath);
	}

	*pNormalizedBufLen = len+1;		// alloc room for the '\0'

bail:
	delete[] normalizedPath;
	return dierr;
}

/*
 * Normalize a ProDOS path.  This requires separating each path component
 * out, making it ProDOS-compliant, and then putting it back in.
 * The fssep could be anything, so we need to change it to kFssep.
 *
 * We don't try to identify duplicates here.  If more than one subdir maps
 * to the same thing, then you're just going to end up with lots of files
 * in the same subdir.  If this is unacceptable then it will have to be
 * fixed at a higher level.
 *
 * Lower-case letters and spaces are left in place.  They're expected to
 * be removed later.
 *
 * The caller must delete[] "*pNormalizedPath".
 */
DIError
DiskFSProDOS::DoNormalizePath(const char* path, char fssep,
	char** pNormalizedPath)
{
	DIError dierr = kDIErrNone;
	char* workBuf = nil;
	char* partBuf = nil;
	char* outputBuf = nil;
	char* start;
	char* end;
	char* outPtr;

	assert(path != nil);
	workBuf = new char[strlen(path)+1];
	partBuf = new char[strlen(path)+1 +1];	// need +1 for prepending letter
	outputBuf = new char[strlen(path) * 2];
	if (workBuf == nil || partBuf == nil || outputBuf == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	strcpy(workBuf, path);
	outputBuf[0] = '\0';

	outPtr = outputBuf;
	start = workBuf;
	while (*start != '\0') {
		//char* origStart = start;	// need for debug msg
		int partIdx;

		if (fssep == '\0') {
			end = nil;
		} else {
			end = strchr(start, fssep);
			if (end != nil)
				*end = '\0';
		}
		partIdx = 0;

		/*
		 * Skip over everything up to the first letter.  If we encounter a
		 * number or a '\0' first, insert a leading letter.
		 */
		while (*start != '\0') {
			if (toupper(*start) >= 'A' && toupper(*start) <= 'Z') {
				partBuf[partIdx++] = *start++;
				break;
			}
			if (*start >= '0' && *start <= '9') {
				partBuf[partIdx++] = 'A';
				break;
			}

			start++;
		}
		if (partIdx == 0)
			partBuf[partIdx++] = 'Z';

		/*
		 * Continue copying, dropping all illegal chars.
		 */
		while (*start != '\0') {
			if ((toupper(*start) >= 'A' && toupper(*start) <= 'Z') ||
				(*start >= '0' && *start <= '9') ||
				(*start == '.') ||
				(*start == ' ') )
			{
				partBuf[partIdx++] = *start++;
			} else {
				start++;
			}
		}

		/*
		 * Truncate at 15 chars, preserving anything that looks like a
		 * filename extension.  "partIdx" represents the length of the
		 * string at this point.  "partBuf" holds the string, which we
		 * want to null-terminate before proceeding.
		 */
		partBuf[partIdx] = '\0';
		if (partIdx > A2FileProDOS::kMaxFileName) {
			const char* pDot = strrchr(partBuf, '.');
			//int DEBUGDOTLEN = pDot - partBuf;
			if (pDot != nil && partIdx - (pDot-partBuf) <= kMaxExtensionLen) {
				int dotLen = partIdx - (pDot-partBuf);
				memmove(partBuf + (A2FileProDOS::kMaxFileName - dotLen),
					pDot, dotLen);		// don't use memcpy, move might overlap
			}
			partIdx = A2FileProDOS::kMaxFileName;
		}
		partBuf[partIdx] = '\0';

		//WMSG2(" ProDOS   Converted component '%s' to '%s'\n",
		//	origStart, partBuf);

		if (outPtr != outputBuf)
			*outPtr++ = A2FileProDOS::kFssep;
		strcpy(outPtr, partBuf);
		outPtr += partIdx;

		/*
		 * Continue with next segment.
		 */
		if (end == nil)
			break;
		start = end+1;
	}

	*outPtr = '\0';

	WMSG3(" ProDOS  Converted path '%s' to '%s' (fssep='%c')\n",
		path, outputBuf, fssep);
	assert(*outputBuf != '\0');

	*pNormalizedPath = outputBuf;
	outputBuf = nil;

bail:
	delete[] workBuf;
	delete[] partBuf;
	delete[] outputBuf;
	return dierr;
}

/*
 * Create a copy of the filename with everything in upper case and spaces
 * changed to periods.
 *
 * "upperName" must be a buffer that holds at least kMaxFileName+1 characters.
 * If "name" is longer than kMaxFileName, it will be truncated.
 */
void
DiskFSProDOS::UpperCaseName(char* upperName, const char* name)
{
    int i;

	for (i = 0; i < A2FileProDOS::kMaxFileName; i++) {
		char ch = name[i];
		if (ch == '\0')
			break;
		else if (ch == ' ')
			upperName[i] = '.';
		else
			upperName[i] = toupper(ch);
	}

	/* null terminate with prejudice -- we memcpy this buffer into subdirs */
	for ( ; i <= A2FileProDOS::kMaxFileName; i++)
		upperName[i] = '\0';
}

/*
 * Allocate a new directory entry.  We start by reading the entire thing
 * into memory.  If the current set of allocated directory blocks is full,
 * and we're not operating on the volume dir, we extend the directory.
 *
 * This just allocates the space; it does not fill in any details, except
 * for the prev/next block pointers and the file count in the header.  (One
 * small exception: if we have to extend the directory, the "prev/next" fields
 * of the new block will be filled in.)
 *
 * The volume in-use block map must be loaded before this is called.  If
 * this needs to extend the directory, a new block will be allocated.
 *
 * Returns a pointer to the new entry, and a whole bunch of other stuff:
 *	"ppDir" gets a pointer to newly-allocated memory with the whole directory
 *	"pDirLen" is the size of the *ppDir buffer
 *	"ppDirEntry" gets a memory pointer to the start of the created entry
 *	"pDirKeyBlock" gets the key block of the directory as a whole
 *	"pDirEntrySlot" gets the slot number within the directory block (first is 1)
 *	"pDirBlock" gets the actual block in which the created entry resides
 *
 * The caller should Write the entire thing to "pOpenSubdir" after filling
 * in the new details for the entry.
 *
 * Possible reasons for failure: disk is out of space, volume dir is out
 * of space, pOpenSubdir is screwy.
 *
 * We guarantee that we will return the first available entry in the current
 * directory.
 */
DIError
DiskFSProDOS::AllocDirEntry(A2FileDescr* pOpenSubdir, unsigned char** ppDir,
	long* pDirLen, unsigned char** ppDirEntry, unsigned short* pDirKeyBlock,
	int* pDirEntrySlot, unsigned short* pDirBlock)
{
	assert(pOpenSubdir != nil);
	*ppDirEntry = nil;
	*pDirLen = -1;
	*pDirKeyBlock = 0;
	*pDirEntrySlot = -1;
	*pDirBlock = 0;

	DIError dierr = kDIErrNone;
	unsigned char* dirBuf = nil;
	long dirLen;
	A2FileProDOS* pFile;
	long newBlock = -1;

	/*
	 * Load the subdir into memory.
	 */
	pFile = (A2FileProDOS*) pOpenSubdir->GetFile();
	dirLen = (long) pFile->GetDataLength();
	if (dirLen < 512 || (dirLen % 512) != 0) {
		WMSG2(" ProDOS GLITCH: funky dir EOF %ld (quality=%d)\n",
			dirLen, pFile->GetQuality());
		dierr = kDIErrBadFile;
		goto bail;
	}
	dirBuf = new unsigned char[dirLen];
	if (dirBuf == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	dierr = pOpenSubdir->Read(dirBuf, dirLen);
	if (dierr != kDIErrNone)
		goto bail;

	if (dirBuf[0x23] != kEntryLength ||
		dirBuf[0x24] != kEntriesPerBlock)
	{
		WMSG1(" ProDOS GLITCH: funky entries per block %d\n", dirBuf[0x24]);
		dierr = kDIErrBadDirectory;
		goto bail;
	}

	/*
	 * Find the first available entry (storage_type is zero).  We need to
	 * step through this by blocks, because the data is block-oriented.
	 * If we run off the end of the last block, (re)alloc a new one.
	 */
	unsigned char* pDirEntry;
	int blockIdx;
	int entryIdx;

    pDirEntry = nil;    // make the compiler happy
    entryIdx = -1;      // make the compiler happy

	for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) {
		pDirEntry = dirBuf + 512*blockIdx + 4;	// skip 4 bytes of prev/next

		for (entryIdx = 0; entryIdx < kEntriesPerBlock;
										entryIdx++, pDirEntry += kEntryLength)
		{
			if ((pDirEntry[0x00] & 0xf0) == 0) {
				WMSG1(" ProDOS  Found empty dir entry in slot %d\n", entryIdx);
				break;		// found one; break out of inner loop
			}
		}
		if (entryIdx < kEntriesPerBlock)
			break;		// out of outer loop
	}
	if (blockIdx == dirLen / 512) {
		if (((dirBuf[0x04] & 0xf0) >> 4) == A2FileProDOS::kStorageVolumeDirHeader)
		{
			/* can't extend the volume dir */
			dierr = kDIErrVolumeDirFull;
			goto bail;
		}

		WMSG0(" ProDOS ran out of directory space, adding another block\n");

		/*
		 * Request an unused block from the system.  Point the "next" pointer
		 * in the last block at it, so that when we go to write this dir
		 * we will know where to put it.
		 */
		unsigned char* pBlock;
		pBlock = dirBuf + 512 * (blockIdx-1);
		if (pBlock[0x02] != 0) {
			WMSG0(" ProDOS GLITCH: adding to block with nonzero next ptr!\n");
			dierr = kDIErrBadDirectory;
			goto bail;
		}

		newBlock = AllocBlock();
		if (newBlock < 0) {
			dierr = kDIErrDiskFull;
			goto bail;
		}

		PutShortLE(&pBlock[0x02], (unsigned short) newBlock);	// set "next"

		/*
		 * Extend our memory buffer to hold the new entry.
		 */
		unsigned char* newSpace = new unsigned char[dirLen + 512];
		if (newSpace == nil) {
			dierr = kDIErrMalloc;
			goto bail;
		}
		memcpy(newSpace, dirBuf, dirLen);
		memset(newSpace + dirLen, 0, 512);
		delete[] dirBuf;
		dirBuf = newSpace;
		dirLen += 512;

		/*
		 * Set the "prev" pointer in the new block to point at the last
		 * block of the existing directory structure.
		 */
		long lastBlock;
		dierr = pOpenSubdir->GetStorage(blockIdx-1, &lastBlock);
		if (dierr != kDIErrNone)
			goto bail;
		pBlock = dirBuf + 512 * blockIdx;
		PutShortLE(&pBlock[0x00], (unsigned short) lastBlock);	// set "prev"
		assert(GetShortLE(&pBlock[0x02]) == 0);				// "next" pointer

		/*
		 * Finally, point pDirEntry at the first entry in the new area.
		 */
		pDirEntry = pBlock + 4;
		entryIdx = 0;
		assert(pDirEntry[0x00] == 0x00);
	}

	/*
	 * Success.  Update the file count in the header.
	 */
	unsigned short count;
	count = GetShortLE(&dirBuf[0x25]);
	count++;
	PutShortLE(&dirBuf[0x25], count);

	long whichBlock;

	*ppDir = dirBuf;
	*pDirLen = dirLen;
	*ppDirEntry = pDirEntry;
	*pDirKeyBlock = pFile->fDirEntry.keyPointer;
	*pDirEntrySlot = entryIdx +1;
	if (blockIdx == ((A2FDProDOS*)pOpenSubdir)->GetBlockCount()) {
		/* not yet added to block list, so can't use GetStorage */
		assert(newBlock > 0);
		*pDirBlock = (unsigned short) newBlock;
	} else {
		assert(newBlock < 0);
		dierr = pOpenSubdir->GetStorage(blockIdx, &whichBlock);
		assert(dierr == kDIErrNone);
		*pDirBlock = (unsigned short) whichBlock;
	}
	dirBuf = nil;

bail:
	delete[] dirBuf;
	return dierr;
}

/*
 * Given a pointer to a directory buffer and a pointer to an entry, find the
 * previous entry.  (This is handy when trying to figure out where to insert
 * a new entry into the DiskFS linear file list.)
 *
 * If the previous entry is the first in the list (i.e. it's a volume or
 * subdir header), this returns nil.
 *
 * This is a little awkward because the directories are chopped up into
 * 512-byte blocks, with 13 entries per block (which doesn't completely fill
 * the block, leaving gaps we have to skip around).  If the previous entry is
 * in the same block we can just return (ptr-0x27), but if it's in a previous
 * block we need to return the last entry in the previous.
 */
unsigned char*
DiskFSProDOS::GetPrevDirEntry(unsigned char* buf, unsigned char* ptr)
{
	assert(buf != nil);
	assert(ptr != nil);

	const int kStartOffset = 4;

	if (ptr == buf + kStartOffset || ptr == buf + kStartOffset + kEntryLength)
		return nil;

	while (ptr - buf > 512)
		buf += 512;

	assert((ptr - buf - kStartOffset) % kEntryLength == 0);

	if (ptr == buf + kStartOffset) {
		/* whoops, went too far */
		buf -= 512;
		return buf + kStartOffset + kEntryLength * (kEntriesPerBlock-1);
	} else {
		return ptr - kEntryLength;
	}
}

/*
 * Make the name pointed to by "fileName" unique within the directory
 * loaded in "subdirBuf".  The name should already be trimmed to 15 chars
 * or less and converted to upper-case only, and be in a buffer that can
 * hold at least kMaxFileName+1 bytes.
 *
 * Returns an error on failure, which should only happen if there are a
 * large number of files with similar names.
 */
DIError
DiskFSProDOS::MakeFileNameUnique(const unsigned char* dirBuf, long dirLen,
	char* fileName)
{
	assert(dirBuf != nil);
	assert(dirLen > 0);
	assert((dirLen % 512) == 0);
	assert(fileName != nil);
	assert(strlen(fileName) <= A2FileProDOS::kMaxFileName);

	if (!NameExistsInDir(dirBuf, dirLen, fileName))
		return kDIErrNone;

	WMSG1(" ProDOS   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, (A2FileProDOS::kMaxFileName - nameLen) +1);

	/*
	 * If this has what looks like a filename extension, grab it.  We want
	 * to preserve ".gif", ".c", etc., since the filetypes don't necessarily
	 * do everything we need.
	 *
	 * This will tend to screw up the upper/lower case stuff, especially
	 * since what we think is a '.' might actually be a ' '.  We could work
	 * around this, but it's probably not necessary.
	 */
	const char* cp = strrchr(fileName, '.');
	if (cp != nil) {
		int tmpOffset = cp - fileName;
		if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) {
			WMSG1("  ProDOS   (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 > A2FileProDOS::kMaxFileName)
			copyOffset = A2FileProDOS::kMaxFileName - dotLen - digitLen;
		else
			copyOffset = nameLen - dotLen;
		memcpy(fileName + copyOffset, digitBuf, digitLen);
		if (dotLen != 0)
			memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen);
	} while (NameExistsInDir(dirBuf, dirLen, fileName));

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

	return kDIErrNone;
}

/*
 * Determine whether the specified file name exists in the raw directory
 * buffer.
 *
 * This should be called with the upper-case-only version of the filename.
 */
bool
DiskFSProDOS::NameExistsInDir(const unsigned char* dirBuf, long dirLen,
	const char* fileName)
{
	const unsigned char* pDirEntry;
	int blockIdx;
	int entryIdx;
	int nameLen = strlen(fileName);

	assert(nameLen <= A2FileProDOS::kMaxFileName);

	for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) {
		pDirEntry = dirBuf + 512*blockIdx + 4;		// skip 4 bytes of prev/next

		for (entryIdx = 0; entryIdx < kEntriesPerBlock;
										entryIdx++, pDirEntry += kEntryLength)
		{
			/* skip directory header */
			if (blockIdx == 0 && entryIdx == 0)
				continue;

			if ((pDirEntry[0x00] & 0xf0) != 0 &&
				(pDirEntry[0x00] & 0x0f) == nameLen &&
				strncmp((char*) &pDirEntry[0x01], fileName, nameLen) == 0)
			{
				return true;
			}
		}
	}

	return false;
}


/*
 * Delete a file.
 *
 * There are three fairly simple steps: (1) mark all blocks used by the file as
 * free, (2) set the storage type in the directory entry to 0, and (3)
 * decrement the file count in the directory header.  We then remove it from
 * the DiskFS file list.
 *
 * We only allow deletion of a subdirectory when the subdir is empty.
 */
DIError
DiskFSProDOS::DeleteFile(A2File* pGenericFile)
{
	DIError dierr = kDIErrNone;
	long blockCount = -1;
	long indexCount = -1;
	unsigned short* blockList = nil;
	unsigned short* indexList = nil;

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

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

	/*
	 * If they try to delete all entries, we don't want to spit back a
	 * failure message over our "fake" volume dir entry.  So we just silently
	 * ignore the request.
	 */
	if (pGenericFile->IsVolumeDirectory()) {
		WMSG0("ProDOS not deleting volume directory\n");
		return kDIErrNone;
	}

	A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile;

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

	dierr = LoadVolBitmap();
	if (dierr != kDIErrNone)
		goto bail;
	switch (pFile->fDirEntry.storageType) {
	case A2FileProDOS::kStorageExtended:
		// handle rsrc fork here, fall out for data fork
		dierr = pFile->LoadBlockList(
					pFile->fExtRsrc.storageType,
					pFile->fExtRsrc.keyBlock,
					pFile->fExtRsrc.eof,
					&blockCount, &blockList,
					&indexCount, &indexList);
		if (dierr != kDIErrNone)
			goto bail;
		FreeBlocks(blockCount, blockList);
		if (indexList != nil)	// no indices for seedling
			FreeBlocks(indexCount, indexList);
		delete[] blockList;
		delete[] indexList;
		indexList = nil;

		// handle the key block "manually"
		blockCount = 1;
		blockList = new unsigned short[blockCount];
		blockList[0] = pFile->fDirEntry.keyPointer;
		FreeBlocks(blockCount, blockList);
		delete[] blockList;
		blockList = nil;

		dierr = pFile->LoadBlockList(
					pFile->fExtData.storageType,
					pFile->fExtData.keyBlock,
					pFile->fExtData.eof,
					&blockCount, &blockList,
					&indexCount, &indexList);
		break;	// fall out

	case A2FileProDOS::kStorageDirectory:
		dierr = pFile->LoadDirectoryBlockList(
					pFile->fDirEntry.keyPointer,
					pFile->fDirEntry.eof,
					&blockCount, &blockList);
		break;	// fall out

	case A2FileProDOS::kStorageSeedling:
	case A2FileProDOS::kStorageSapling:
	case A2FileProDOS::kStorageTree:
		dierr = pFile->LoadBlockList(
					pFile->fDirEntry.storageType,
					pFile->fDirEntry.keyPointer,
					pFile->fDirEntry.eof,
					&blockCount, &blockList,
					&indexCount, &indexList);
		break;	// fall out

	default:
		WMSG1("ProDOS can't delete unknown storage type %d\n",
			pFile->fDirEntry.storageType);
		dierr = kDIErrBadDirectory;
		break;	// fall out
	}

	if (dierr != kDIErrNone)
		goto bail;

	FreeBlocks(blockCount, blockList);
	if (indexList != nil)
		FreeBlocks(indexCount, indexList);

	/*
	 * Update the directory entry.  After this point, failure gets ugly.
	 *
	 * It might be "proper" to open the subdir file, find the correct entry,
	 * and write it back, but the A2FileProDOS structure has the directory
	 * block and entry index stored in it.  Makes it a little easier.
	 */
	unsigned char blkBuf[kBlkSize];
	unsigned char* ptr;
	assert(pFile->fParentDirBlock > 0);
	assert(pFile->fParentDirIdx >= 0 &&
		   pFile->fParentDirIdx < kEntriesPerBlock);
	dierr = fpImg->ReadBlock(pFile->fParentDirBlock, blkBuf);
	if (dierr != kDIErrNone) {
		WMSG1("ProDOS unable to read directory block %u\n",
			pFile->fParentDirBlock);
		goto bail;
	}

	ptr = blkBuf + 4 + pFile->fParentDirIdx * kEntryLength;
	if ((*ptr) >> 4 != pFile->fDirEntry.storageType) {
		WMSG2("ProDOS GLITCH: mismatched storage types (%d vs %d)\n",
			(*ptr) >> 4, pFile->fDirEntry.storageType);
		assert(false);
		dierr = kDIErrBadDirectory;
		goto bail;
	}
	ptr[0x00] = 0;		// zap both storage type and name length
	dierr = fpImg->WriteBlock(pFile->fParentDirBlock, blkBuf);
	if (dierr != kDIErrNone) {
		WMSG1("ProDOS unable to write directory block %u\n",
			pFile->fParentDirBlock);
		goto bail;
	}

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

	/*
	 * One last little thing: decrement the file count in the directory
	 * header.  We can find the appropriate place pretty easily because
	 * we know it's the first block in pFile->fpParent, which for a dir is
	 * always the block pointed to by the key pointer.
	 *
	 * Strictly speaking, failure to update this correctly isn't fatal.  I
	 * doubt most utilities pay any attention to this.  Still, it's important
	 * to keep the filesystem in a consistent state, so we at least must
	 * report the error.  They'll need to run the ProSel volume repair util
	 * to fix it.
	 */
	A2FileProDOS* pParent;
	unsigned short fileCount;
	int storageType;
	pParent = (A2FileProDOS*) pFile->GetParent();
	assert(pParent != nil);
	assert(pParent->fDirEntry.keyPointer >= kVolHeaderBlock);
	dierr = fpImg->ReadBlock(pParent->fDirEntry.keyPointer, blkBuf);
	if (dierr != kDIErrNone) {
		WMSG1("ProDOS unable to read parent dir block %u\n",
			pParent->fDirEntry.keyPointer);
		goto bail;
	}
	ptr = nil;

	storageType = (blkBuf[0x04] & 0xf0) >> 4;
	if (storageType != A2FileProDOS::kStorageSubdirHeader &&
		storageType != A2FileProDOS::kStorageVolumeDirHeader)
	{
		WMSG1("ProDOS invalid storage type %d in dir header block\n",
			storageType);
		DebugBreak();
		dierr = kDIErrBadDirectory;
		goto bail;
	}
	fileCount = GetShortLE(&blkBuf[0x25]);
	if (fileCount > 0)
		fileCount--;
	PutShortLE(&blkBuf[0x25], fileCount);
	dierr = fpImg->WriteBlock(pParent->fDirEntry.keyPointer, blkBuf);
	if (dierr != kDIErrNone) {
		WMSG1("ProDOS unable to write parent dir block %u\n",
			pParent->fDirEntry.keyPointer);
		goto bail;
	}

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

bail:
	FreeVolBitmap();
	delete[] blockList;
	delete[] indexList;
	return kDIErrNone;
}

/*
 * Mark all of the blocks in the blockList as free.
 *
 * The in-use map must already be loaded.
 */
DIError
DiskFSProDOS::FreeBlocks(long blockCount, unsigned short* blockList)
{
	VolumeUsage::ChunkState cstate;
	int i;

	//WMSG2(" +++ FreeBlocks (blockCount=%d blockList=0x%08lx)\n",
	//	blockCount, blockList);
	assert(blockCount >= 0 && blockCount < 65536);
	assert(blockList != nil);

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

	for (i = 0; i < blockCount; i++) {
		if (blockList[i] == 0)	// expected for "sparse" files
			continue;

		if (!GetBlockUseEntry(blockList[i])) {
			WMSG1("WARNING: freeing unallocated block %u\n", blockList[i]);
			assert(false);	// impossible unless disk is "damaged"
		}
		SetBlockUseEntry(blockList[i], false);

		fVolumeUsage.SetChunkState(blockList[i], &cstate);
	}

	return kDIErrNone;
}


/*
 * Rename a file.
 *
 * Pass in a pointer to the file and a string with the new filename (just
 * the filename, not a pathname -- this function doesn't move files
 * between directories).  The new name must already be normalized.
 *
 * Renaming the magic volume directory "file" is not allowed.
 *
 * Things to note:
 *	- Renaming subdirs is annoying.  The name has to be changed in two
 *	  places, and the "pathname" value cached in A2FileProDOS must be
 *	  updated for all children of the subdir.
 *	- Must check for duplicates.
 *	- If it's an AppleWorks file type, we need to change the aux type
 *	  according to the upper/lower case flags.  This holds even if the
 *	  "allow lower case" flag is disabled.
 */
DIError
DiskFSProDOS::RenameFile(A2File* pGenericFile, const char* newName)
{
	DIError dierr = kDIErrNone;
	A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile;
	char upperName[A2FileProDOS::kMaxFileName+1];
	char upperComp[A2FileProDOS::kMaxFileName+1];

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

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

	/*
	 * Check for duplicates.  We do this by getting the parent subdir and
	 * running through it looking for an upper-case-converted match.
	 *
	 * We start in the list at our parent node, knowing that the kids are
	 * grouped together after it.  However, we can't stop right away,
	 * because some of the kids might be subdirectories themselves.  So we
	 * will probably run through a significant chunk of the list.
	 */
	A2File* pParent = pFile->GetParent();
	A2File* pCur;

	UpperCaseName(upperName, newName);
	pCur = GetNextFile(pParent);
	assert(pCur != nil);	// at the very least, pFile is in this dir
	while (pCur != nil) {
		if (pCur != pFile && pCur->GetParent() == pParent) {
			/* one of our siblings; see if the name matches */
			UpperCaseName(upperComp, pCur->GetFileName());
			if (strcmp(upperName, upperComp) == 0) {
				WMSG0(" ProDOS rename dup found\n");
				return kDIErrFileExists;
			}
		}

		pCur = GetNextFile(pCur);
	}

	/*
	 * Grab the directory block and update the filename in the entry.  If this
	 * was a subdir we also need to update its directory header entry.  To
	 * minimize the chances of a partial update, we load both blocks up
	 * front, modify both, then write them both back.
	 */
	unsigned char parentDirBuf[kBlkSize];
	unsigned char thisDirBuf[kBlkSize];

	dierr = fpImg->ReadBlock(pFile->fParentDirBlock, parentDirBuf);
	if (dierr != kDIErrNone)
		goto bail;
	if (pFile->IsDirectory()) {
		dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
		if (dierr != kDIErrNone)
			goto bail;
	}

	/* compute lower case flags as needed */
	unsigned short lcFlags, lcAuxType;
	bool allowLowerCase, isAW;

	allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0;
	isAW = A2FileProDOS::UsesAppleWorksAuxType((unsigned char)pFile->GetFileType());

	if (allowLowerCase)
		lcFlags = GenerateLowerCaseBits(upperName, newName, false);
	else
		lcFlags = 0;
	if (isAW)
		lcAuxType = GenerateLowerCaseBits(upperName, newName, true);
	else
		lcAuxType = 0;

	/*
	 * Possible optimization: if "upperName" matches what's in the block on
	 * disk and the "lcFlags"/"lcAuxType" values match as well, we don't
	 * need to write the blocks back.
	 *
	 * It's difficult to test for this earlier, because we need to do the
	 * update if (a) they're just changing the capitalization or (b) we're
	 * changing the capitalization for them because the "allow lower case"
	 * flag got turned off.
	 */

	/* find the right entry, and copy our filename in */
	unsigned char* ptr;
	assert(pFile->fParentDirIdx >= 0 &&
		   pFile->fParentDirIdx < kEntriesPerBlock);
	ptr = parentDirBuf + 4 + pFile->fParentDirIdx * kEntryLength;
	if ((*ptr) >> 4 != pFile->fDirEntry.storageType) {
		WMSG2("ProDOS GLITCH: mismatched storage types (%d vs %d)\n",
			(*ptr) >> 4, pFile->fDirEntry.storageType);
		assert(false);
		dierr = kDIErrBadDirectory;
		goto bail;
	}
	ptr[0x00] = (ptr[0x00] & 0xf0) | strlen(upperName);
	memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName);
	PutShortLE(&ptr[0x1c], lcFlags);		// version/min_version
	if (isAW)
		PutShortLE(&ptr[0x1f], lcAuxType);

	if (pFile->IsDirectory()) {
		ptr = thisDirBuf + 4;
		if ((*ptr) >> 4 != A2FileProDOS::kStorageSubdirHeader) {
			WMSG1("ProDOS GLITCH: bad storage type in subdir header (%d)\n",
				(*ptr) >> 4);
			assert(false);
			dierr = kDIErrBadDirectory;
			goto bail;
		}
		ptr[0x00] = (ptr[0x00] & 0xf0) | strlen(upperName);
		memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName);
		PutShortLE(&ptr[0x1c], lcFlags);		// version/min_version
	}

	/* write the updated data back to the disk */
	dierr = fpImg->WriteBlock(pFile->fParentDirBlock, parentDirBuf);
	if (dierr != kDIErrNone)
		goto bail;
	if (pFile->IsDirectory()) {
		dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
		if (dierr != kDIErrNone)
			goto bail;
	}

	/*
	 * At this point the ProDOS filesystem is back in a consistent state.
	 * Everything we do from here on is self-inflicted.
	 *
	 * We need to update this entry's A2FileProDOS::fDirEntry.fileName,
	 * as well as the A2FileProDOS::fPathName.  If this was a subdir, then
	 * we need to update A2FileProDOS::fPathName for all files inside the
	 * directory (including children of children).
	 *
	 * The latter is somewhat awkward, so we just re-acquire the pathname
	 * for every file on the disk.  Less efficient but easier to code.
	 */
	if (isAW)
		GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName,
			lcAuxType, true);
	else
		GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName,
			lcFlags, false);
	assert(pFile->fDirEntry.fileName[A2FileProDOS::kMaxFileName] == '\0');

	if (pFile->IsDirectory()) {
		/* do all files that come after us */
		pCur = pFile;
		while (pCur != nil) {
			RegeneratePathName((A2FileProDOS*) pCur);
			pCur = GetNextFile(pCur);
		}
	} else {
		RegeneratePathName(pFile);
	}

	WMSG0("Okay!\n");

bail:
	return dierr;
}

/*
 * Regenerate fPathName for the specified file.
 *
 * Has no effect on the magic volume dir entry.
 *
 * This could be implemented more efficiently, but it's only used when
 * renaming files, so there's not much point.
 */
DIError
DiskFSProDOS::RegeneratePathName(A2FileProDOS* pFile)
{
	A2FileProDOS* pParent;
	char* buf = nil;
	int len;

	/* nothing to do here */
	if (pFile->IsVolumeDirectory())
		return kDIErrNone;

	/* compute the length of the path name */
	len = strlen(pFile->GetFileName());
	pParent = (A2FileProDOS*) pFile->GetParent();
	while (!pParent->IsVolumeDirectory()) {
		len++;		// leave space for the ':'
		len += strlen(pParent->GetFileName());

		pParent = (A2FileProDOS*) pParent->GetParent();
	}

	buf = new char[len+1];
	if (buf == nil)
		return kDIErrMalloc;

	/* generate the new path name */
	int partLen;
	partLen = strlen(pFile->GetFileName());
	strcpy(buf + len - partLen, pFile->GetFileName());
	len -= partLen;

	pParent = (A2FileProDOS*) pFile->GetParent();
	while (!pParent->IsVolumeDirectory()) {
		assert(len > 0);
		buf[--len] = kDIFssep;

		partLen = strlen(pParent->GetFileName());
		strncpy(buf + len - partLen, pParent->GetFileName(), partLen);
		len -= partLen;
		assert(len >= 0);

		pParent = (A2FileProDOS*) pParent->GetParent();
	}

	WMSG2("Replacing '%s' with '%s'\n", pFile->GetPathName(), buf);
	pFile->SetPathName("", buf);
	delete[] buf;

	return kDIErrNone;
}


/*
 * Change the attributes of the specified file.
 *
 * Subdirectories have access bits in the subdir header as well as their
 * file entry.  The BASIC.SYSTEM "lock" command only changes the access
 * bits of the file; the permissions inside the subdir remain 0xe3.  (Which
 * might explain why you can still add files to a locked subdir.)  I'm going
 * to mimic this behavior.
 *
 * This does, of course, mean that there's no meaning in attempts to change
 * the file access permissions of the volume directory.
 */
DIError
DiskFSProDOS::SetFileInfo(A2File* pGenericFile, long fileType, long auxType,
	long accessFlags)
{
	DIError dierr = kDIErrNone;
	A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile;

	if (fpImg->GetReadOnly())
		return kDIErrAccessDenied;
	if (pFile == nil) {
		assert(false);
		return kDIErrInvalidArg;
	}
	if ((fileType & ~(0xff)) != 0 ||
		(auxType & ~(0xffff)) != 0 ||
		(accessFlags & ~(0xff)) != 0)
	{
		return kDIErrInvalidArg;
	}
	if (pFile->IsVolumeDirectory()) {
		WMSG0(" ProDOS refusing to change file info for volume dir\n");
		return kDIErrAccessDenied;		// not quite right
	}

	WMSG4("ProDOS changing values for '%s' to 0x%02lx 0x%04lx 0x%02lx\n",
		pFile->GetPathName(), fileType, auxType, accessFlags);

	/* load the directory block for this file */
	unsigned char thisDirBuf[kBlkSize];
	dierr = fpImg->ReadBlock(pFile->fParentDirBlock, thisDirBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/* find the right entry, and set the fields */
	unsigned char* ptr;
	assert(pFile->fParentDirIdx >= 0 &&
		   pFile->fParentDirIdx < kEntriesPerBlock);
	ptr = thisDirBuf + 4 + pFile->fParentDirIdx * kEntryLength;
	if ((*ptr) >> 4 != pFile->fDirEntry.storageType) {
		WMSG2("ProDOS GLITCH: mismatched storage types (%d vs %d)\n",
			(*ptr) >> 4, pFile->fDirEntry.storageType);
		assert(false);
		dierr = kDIErrBadDirectory;
		goto bail;
	}
	if ((size_t) (*ptr & 0x0f) != strlen(pFile->fDirEntry.fileName)) {
		WMSG2("ProDOS GLITCH: wrong file?  (len=%d vs %d)\n",
			*ptr & 0x0f, strlen(pFile->fDirEntry.fileName));
		assert(false);
		dierr = kDIErrBadDirectory;
		goto bail;
	}

	ptr[0x10] = (unsigned char) fileType;
	ptr[0x1e] = (unsigned char) accessFlags;
	PutShortLE(&ptr[0x1f], (unsigned short) auxType);

	dierr = fpImg->WriteBlock(pFile->fParentDirBlock, thisDirBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/* update our local copy */
	pFile->fDirEntry.fileType = (unsigned char) fileType;
	pFile->fDirEntry.auxType = (unsigned short) auxType;
	pFile->fDirEntry.access = (unsigned char) accessFlags;

bail:
	return dierr;
}


/*
 * Change the disk volume name.
 *
 * This is a lot like renaming a subdirectory, except that there's no parent
 * directory to update, and the name of the volume dir doesn't affect the
 * pathname of anything else.  There's also no risk of a duplicate.
 *
 * Internally we need to update the "fake" entry and the cached copies in
 * fVolumeName and fVolumeID.
 */
DIError
DiskFSProDOS::RenameVolume(const char* newName)
{
	DIError dierr = kDIErrNone;
	char upperName[A2FileProDOS::kMaxFileName+1];
	A2FileProDOS* pFile;

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

	pFile = (A2FileProDOS*) GetNextFile(nil);
	assert(pFile != nil);
	assert(strcmp(pFile->GetFileName(), fVolumeName) == 0);

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

	/*
	 * Figure out the lower-case flags.
	 */
	unsigned short lcFlags;
	bool allowLowerCase;

	UpperCaseName(upperName, newName);
	allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0;
	if (allowLowerCase)
		lcFlags = GenerateLowerCaseBits(upperName, newName, false);
	else
		lcFlags = 0;

	/*
	 * Update the volume dir header.
	 */
	unsigned char thisDirBuf[kBlkSize];
	unsigned char* ptr;
	assert(pFile->fDirEntry.keyPointer == kVolHeaderBlock);

	dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
	if (dierr != kDIErrNone)
		goto bail;

	ptr = thisDirBuf + 4;
	if ((*ptr) >> 4 != A2FileProDOS::kStorageVolumeDirHeader) {
		WMSG1("ProDOS GLITCH: bad storage type in voldir header (%d)\n",
			(*ptr) >> 4);
		assert(false);
		dierr = kDIErrBadDirectory;
		goto bail;
	}
	ptr[0x00] = (ptr[0x00] & 0xf0) | strlen(upperName);
	memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName);
	PutShortLE(&ptr[0x16], lcFlags);		// reserved fields

	dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Set the volume name, based on the upper-case name and lower-case flags
	 * we just wrote.  If "allowLowerCase" was set to false, it may not be
	 * the same as what's in "newName".
	 */
	char lowerName[A2FileProDOS::kMaxFileName+1];
	memset(lowerName, 0, sizeof(lowerName));	// lowerName won't be term'ed
	GenerateLowerCaseName(upperName, lowerName, lcFlags, false);

	strcpy(fVolumeName, lowerName);
	SetVolumeID();
	strcpy(pFile->fDirEntry.fileName, lowerName);

	/* update the entry in the linear file list */
	pFile->SetPathName(":", fVolumeName);

bail:
	return dierr;
}


/*
 * ===========================================================================
 *		A2FileProDOS
 * ===========================================================================
 */

/*
 * Convert from ProDOS compact date format to a time_t.
 *
 *	Byte 0 and 1: yyyyyyymmmmddddd
 *	Byte 2 and 3: 000hhhhh00mmmmmm
 *
 * The field is set entirely to zero if no date was assigned (which cannot
 * be a valid date since "day" ranges from 1 to 31).  If this is found then
 * ((time_t) 0) is returned.
 */
/*static*/ time_t
A2FileProDOS::ConvertProDate(ProDate proDate)
{
	unsigned short prodosDate, prodosTime;
	int year, month, day, hour, minute, second;

	if (proDate == 0)
		return 0;

	prodosDate = (unsigned short) (proDate & 0x0000ffff);
	prodosTime = (unsigned short) ((proDate >> 16) & 0x0000ffff);

	second = 0;
    minute = prodosTime & 0x3f;
    hour = (prodosTime >> 8) & 0x1f;
    day = prodosDate & 0x1f;
    month = (prodosDate >> 5) & 0x0f;
    year = (prodosDate >> 9) & 0x7f;
    if (year < 40)
        year += 100;     /* P8 uses 0-39 for 2000-2039 */

    struct tm tmbuf;
    time_t when;

    tmbuf.tm_sec = second;
    tmbuf.tm_min = minute;
    tmbuf.tm_hour = hour;
    tmbuf.tm_mday = day;
    tmbuf.tm_mon = month-1;		// ProDOS uses 1-12
    tmbuf.tm_year = year;
    tmbuf.tm_wday = 0;
    tmbuf.tm_yday = 0;
    tmbuf.tm_isdst = -1;        // let it figure DST and time zone
    when = mktime(&tmbuf);

	if (when == (time_t) -1)
		when = 0;

	return when;
}

/*
 * Convert a time_t to a ProDOS-format date.
 *
 * CiderPress uses kDateInvalid==-1 and kDateNone==-2.
 */
/*static*/ A2FileProDOS::ProDate
A2FileProDOS::ConvertProDate(time_t unixDate)
{
	ProDate proDate;
	unsigned long prodosDate, prodosTime;
	struct tm* ptm;
	int year;

	if (unixDate == 0 || unixDate == -1 || unixDate == -2)
		return 0;

	ptm = localtime(&unixDate);
	if (ptm == nil)
		return 0;		// must've been invalid or unspecified

	year = ptm->tm_year;
#ifdef OLD_PRODOS_DATES
	/* ProSel-16 volume repair complaints about dates < 1980 and >= Y2K */
	if (year > 100)
		year -= 20;
#endif

	if (year >= 100)
		year -= 100;
	if (year < 0 || year >= 128) {
		WMSG2("WHOOPS: got year %d from %d\n", year, ptm->tm_year);
		year = 70;
	}

	prodosDate = year << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday;
	prodosTime = ptm->tm_hour << 8 | ptm->tm_min;

	proDate = prodosTime << 16 | prodosDate;
	return proDate;
}


/*
 * Return the file creation time as a time_t.
 */
time_t
A2FileProDOS::GetCreateWhen(void) const
{
	return ConvertProDate(fDirEntry.createWhen);
}

/*
 * Return the file modification time as a time_t.
 */
time_t
A2FileProDOS::GetModWhen(void) const
{
	return ConvertProDate(fDirEntry.modWhen);
}

/*
 * Set the full pathname to a combination of the base path and the
 * current file's name.
 *
 * If we're in the volume directory, pass in "" for the base path (not nil).
 */
void
A2FileProDOS::SetPathName(const char* basePath, const char* fileName)
{
	assert(basePath != nil && fileName != nil);
	if (fPathName != nil)
		delete[] fPathName;

	int baseLen = strlen(basePath);
	fPathName = new char[baseLen + 1 + strlen(fileName)+1];
	strcpy(fPathName, basePath);
	if (baseLen != 0 &&
		!(baseLen == 1 && basePath[0] == ':'))
	{
		*(fPathName + baseLen) = kFssep;
		baseLen++;
	}
	strcpy(fPathName + baseLen, fileName);
}


/*
 * Convert a character in a ProDOS name to lower case.
 *
 * This is special in that '.' is considered upper case, with ' ' as its
 * lower-case counterpart.
 */
/*static*/ char
A2FileProDOS::NameToLower(char ch)
{
	if (ch == '.')
		return ' ';
	else
		return tolower(ch);
}

/*
 * Init the fields in the DirEntry struct from the values in the ProDOS
 * directory entry pointed to by "entryBuf".
 *
 * Deals with lower case conversions on the filename.
 */
/*static*/ void
A2FileProDOS::InitDirEntry(A2FileProDOS::DirEntry* pEntry,
	const unsigned char* entryBuf)
{
	int nameLen;

	pEntry->storageType = (entryBuf[0x00] & 0xf0) >> 4;
	nameLen = entryBuf[0x00] & 0x0f;
	memcpy(pEntry->fileName, &entryBuf[0x01], nameLen);
	pEntry->fileName[nameLen] = '\0';
	pEntry->fileType = entryBuf[0x10];
	pEntry->keyPointer = GetShortLE(&entryBuf[0x11]);
	pEntry->blocksUsed = GetShortLE(&entryBuf[0x13]);
	pEntry->eof = GetLongLE(&entryBuf[0x15]);
	pEntry->eof &= 0x00ffffff;
	pEntry->createWhen = GetLongLE(&entryBuf[0x18]);
	pEntry->version = entryBuf[0x1c];
	pEntry->minVersion = entryBuf[0x1d];
	pEntry->access = entryBuf[0x1e];
	pEntry->auxType = GetShortLE(&entryBuf[0x1f]);
	pEntry->modWhen = GetLongLE(&entryBuf[0x21]);
	pEntry->headerPointer = GetShortLE(&entryBuf[0x25]);

	/* generate the name into the buffer; does not null-terminate */
	if (UsesAppleWorksAuxType(pEntry->fileType)) {
		DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName,
			pEntry->auxType, true);
	} else if (pEntry->minVersion & 0x80) {
		DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName,
			GetShortLE(&entryBuf[0x1c]), false);
	}
	pEntry->fileName[sizeof(pEntry->fileName)-1] = '\0';
}


/*
 * Open one fork of this file.
 *
 * I really, really dislike forked files.
 */
DIError
A2FileProDOS::Open(A2FileDescr** ppOpenFile, bool readOnly,
	bool rsrcFork /*= false*/)
{
	DIError dierr = kDIErrNone;
	A2FDProDOS* pOpenFile = nil;

	WMSG3(" ProDOS Open(ro=%d, rsrc=%d) on '%s'\n",
		readOnly, rsrcFork, fPathName);
	//Dump();

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

	if (fpOpenFile != nil) {
		dierr = kDIErrAlreadyOpen;
		goto bail;
	}
	if (rsrcFork && fDirEntry.storageType != kStorageExtended) {
		dierr = kDIErrForkNotFound;
		goto bail;
	}

	pOpenFile = new A2FDProDOS(this);
	if (pOpenFile == nil)
		return kDIErrMalloc;

	pOpenFile->fOpenRsrcFork = false;

	if (fDirEntry.storageType == kStorageExtended) {
		if (rsrcFork) {
			dierr = LoadBlockList(fExtRsrc.storageType, fExtRsrc.keyBlock,
						fExtRsrc.eof, &pOpenFile->fBlockCount,
						&pOpenFile->fBlockList);
			pOpenFile->fOpenEOF = fExtRsrc.eof;
			pOpenFile->fOpenBlocksUsed = fExtRsrc.blocksUsed;
			pOpenFile->fOpenStorageType = fExtRsrc.storageType;
			pOpenFile->fOpenRsrcFork = true;
		} else {
			dierr = LoadBlockList(fExtData.storageType, fExtData.keyBlock,
						fExtData.eof, &pOpenFile->fBlockCount,
						&pOpenFile->fBlockList);
			pOpenFile->fOpenEOF = fExtData.eof;
			pOpenFile->fOpenBlocksUsed = fExtData.blocksUsed;
			pOpenFile->fOpenStorageType = fExtData.storageType;
		}
	} else if (fDirEntry.storageType == kStorageDirectory ||
			   fDirEntry.storageType == kStorageVolumeDirHeader)
	{
		dierr = LoadDirectoryBlockList(fDirEntry.keyPointer,
					fDirEntry.eof, &pOpenFile->fBlockCount,
					&pOpenFile->fBlockList);
		pOpenFile->fOpenEOF = fDirEntry.eof;
		pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed;
		pOpenFile->fOpenStorageType = fDirEntry.storageType;
	} else if (fDirEntry.storageType == kStorageSeedling ||
			   fDirEntry.storageType == kStorageSapling ||
			   fDirEntry.storageType == kStorageTree)
	{
		dierr = LoadBlockList(fDirEntry.storageType, fDirEntry.keyPointer,
					fDirEntry.eof, &pOpenFile->fBlockCount,
					&pOpenFile->fBlockList);
		pOpenFile->fOpenEOF = fDirEntry.eof;
		pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed;
		pOpenFile->fOpenStorageType = fDirEntry.storageType;
	} else {
		WMSG1("PrODOS can't open unknown storage type %d\n",
			fDirEntry.storageType);
		dierr = kDIErrBadDirectory;
		goto bail;
	}
	if (dierr != kDIErrNone) {
		WMSG0(" ProDOS open failed\n");
		goto bail;
	}

	pOpenFile->fOffset = 0;
	//pOpenFile->DumpBlockList();

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

bail:
	delete pOpenFile;
	return dierr;
}


/*
 * Gather a linear, non-sparse list of file blocks into an array.
 *
 * Pass in the storage type and top-level key block.  Separation of
 * extended files should have been handled by the caller.  This loads the
 * list for only one fork.
 *
 * There are two kinds of sparse: sparse *inside* data, and sparse
 * *past* data.  The latter is interesting, because there is no need
 * to create space in index blocks to hold it.  Thus, a sapling could
 * hold a file with an EOF of 16MB.
 *
 * If "pIndexBlockCount" and "pIndexBlockList" are non-nil, then we
 * also accumulate the list of index blocks and return those as well.
 * For a Tree-structured file, the first entry in the index list is
 * the master index block.
 *
 * The caller must delete[] "*pBlockList" and "*pIndexBlockList".
 */
DIError
A2FileProDOS::LoadBlockList(int storageType, unsigned short keyBlock,
	long eof, long* pBlockCount, unsigned short** pBlockList,
	long* pIndexBlockCount, unsigned short** pIndexBlockList)
{
	if (storageType == kStorageDirectory ||
		storageType == kStorageVolumeDirHeader)
	{
		assert(pIndexBlockList == nil && pIndexBlockCount == nil);
		return LoadDirectoryBlockList(keyBlock, eof, pBlockCount, pBlockList);
	}
	
	assert(keyBlock != 0);
	assert(pBlockCount != nil);
	assert(pBlockList != nil);
	assert(*pBlockList == nil);
	if (storageType != kStorageSeedling &&
		storageType != kStorageSapling &&
		storageType != kStorageTree)
	{
		/*
		 * We can get here if somebody puts a bad storage type inside the
		 * extended key block of a forked file.  Bad storage types on other
		 * kinds of files are caught earlier.
		 */
		WMSG2(" ProDOS unexpected storageType %d in '%s'\n",
			storageType, GetPathName());
		return kDIErrNotSupported;
	}

	DIError dierr = kDIErrNone;
	unsigned short* list = nil;
	long count;

	assert(eof < 1024*1024*16);
	count = (eof + kBlkSize -1) / kBlkSize;
	if (count == 0)
		count = 1;
	list = new unsigned short[count+1];
	if (list == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	if (pIndexBlockList != nil) {
		assert(pIndexBlockCount != nil);
		assert(*pIndexBlockList == nil);
	}

	/* this should take care of trailing sparse entries */
	memset(list, 0, sizeof(unsigned short) * count);
	list[count] = kInvalidBlockNum;		// overrun check

	if (storageType == kStorageSeedling) {
		list[0] = keyBlock;

		if (pIndexBlockList != nil) {
			*pIndexBlockCount = 0;
			*pIndexBlockList = nil;
		}
	} else if (storageType == kStorageSapling) {
		dierr = LoadIndexBlock(keyBlock, list, count);
		if (dierr != kDIErrNone)
			goto bail;

		if (pIndexBlockList != nil) {
			*pIndexBlockCount = 1;
			*pIndexBlockList = new unsigned short[1];
			**pIndexBlockList = keyBlock;
		}
	} else if (storageType == kStorageTree) {
		unsigned char blkBuf[kBlkSize];
		unsigned short* listPtr = list;
		unsigned short* outIndexPtr = nil;
		long countDown = count;
		int idx = 0;

		dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;

		if (pIndexBlockList != nil) {
			int numIndices = (count + kMaxBlocksPerIndex-1) / kMaxBlocksPerIndex;
			numIndices++;	// add one for the master index block
			*pIndexBlockList = new unsigned short[numIndices];
			outIndexPtr = *pIndexBlockList;
			*outIndexPtr++ = keyBlock;
			*pIndexBlockCount = 1;
		}

		while (countDown) {
			long blockCount = countDown;
			if (blockCount > kMaxBlocksPerIndex)
				blockCount = kMaxBlocksPerIndex;
			unsigned short idxBlock;

			idxBlock = blkBuf[idx] | (unsigned short) blkBuf[idx+256] << 8;
			if (idxBlock == 0) {
				/* fully sparse index block */
				//WMSG1(" ProDOS that's seriously sparse (%d)!\n", idx);
				memset(listPtr, 0, blockCount * sizeof(unsigned short));
				if (pIndexBlockList != nil) {
					*outIndexPtr++ = idxBlock;
					(*pIndexBlockCount)++;
				}
			} else {
				dierr = LoadIndexBlock(idxBlock, listPtr, blockCount);
				if (dierr != kDIErrNone)
					goto bail;

				if (pIndexBlockList != nil) {
					*outIndexPtr++ = idxBlock;
					(*pIndexBlockCount)++;
				}
			}

			idx++;
			listPtr += blockCount;
			countDown -= blockCount;
		}
	} else {
		assert(false);
	}

	assert(list[count] == kInvalidBlockNum);

	dierr = ValidateBlockList(list, count);
	if (dierr != kDIErrNone)
		goto bail;

	*pBlockCount = count;
	*pBlockList = list;

bail:
	if (dierr != kDIErrNone) {
		delete[] list;
		assert(*pBlockList == nil);

		if (pIndexBlockList != nil && *pIndexBlockList != nil) {
			delete[] *pIndexBlockList;
			*pIndexBlockList = nil;
		}
	}
	return dierr;
}

/*
 * Make sure all values in the block list fall in accepted ranges.
 *
 * We allow zero (used for sparse blocks), but disallow values in the "system"
 * area (block 1 through the end of the usage map).
 *
 * It's hard to say whether we should compare against the DiskImg block count
 * (representing blocks we can physically read but aren't necessarily part
 * of the filesystem) or the filesystem "total blocks" value from the volume
 * header.  Using the one in the volume header is correct, but sometimes the
 * value is off on an otherwise reasonable disk.
 *
 * I'm falling on the side of generosity, allowing files that reference
 * potentially bad data to appear okay.  My main reason is that, except for
 * CFFA volumes that have been tweaked by CiderPress users, very few ProDOS
 * disks will have a large disparity between the two numbers unless somebody
 * has trashed the volume dir header.
 *
 * What we really need is three states for each file: good, suspect, damaged.
 */
DIError
A2FileProDOS::ValidateBlockList(const unsigned short* list, long count)
{
	DiskImg* pImg = fpDiskFS->GetDiskImg();
	bool foundBad = false;

	while (count--) {
		if (*list > pImg->GetNumBlocks() ||
			(*list > 0 && *list <= 2))		// not enough, but it'll do
		{
			WMSG2("Invalid block %d in '%s'\n", *list, fDirEntry.fileName);
			SetQuality(kQualityDamaged);
			return kDIErrBadFile;
		}
		if (*list > fpDiskFS->GetFSNumBlocks())
			foundBad = true;
		list++;
	}

	if (foundBad) {
		WMSG1("  --- found out-of-range block in '%s'\n", GetPathName());
		SetQuality(kQualitySuspicious);
	}

	return kDIErrNone;
}

/*
 * Copy the entries from the index block in "block" to "list", copying
 * at most "maxCount" entries.
 */
DIError
A2FileProDOS::LoadIndexBlock(unsigned short block, unsigned short* list,
	int maxCount)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	int i;

	if (maxCount > kMaxBlocksPerIndex)
		maxCount = kMaxBlocksPerIndex;

	dierr = fpDiskFS->GetDiskImg()->ReadBlock(block, blkBuf);
	if (dierr != kDIErrNone)
		goto bail;

	//WMSG1("LOADING 0x%04x\n", block);
	for (i = 0; i < maxCount; i++) {
		*list++ = blkBuf[i] | (unsigned short) blkBuf[i+256] << 8;
	}

bail:
	return dierr;
}

/*
 * Load the block list from a directory, which is essentially a linear
 * linked list.
 */
DIError
A2FileProDOS::LoadDirectoryBlockList(unsigned short keyBlock,
	long eof, long* pBlockCount, unsigned short** pBlockList)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	unsigned short* list = nil;
	unsigned short* listPtr;
	int iterations;
	long count;

	assert(eof < 1024*1024*16);
	count = (eof + kBlkSize -1) / kBlkSize;
	if (count == 0)
		count = 1;
	list = new unsigned short[count+1];
	if (list == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	/* this should take care of trailing sparse entries */
	memset(list, 0, sizeof(unsigned short) * count);
	list[count] = kInvalidBlockNum;		// overrun check

	iterations = 0;
	listPtr = list;

	while (keyBlock && iterations < kMaxCatalogIterations) {
		if (keyBlock < 2 ||
			keyBlock >= fpDiskFS->GetDiskImg()->GetNumBlocks())
		{
			WMSG1(" ProDOS ERROR: directory block %u out of range\n", keyBlock);
			dierr = kDIErrInvalidBlock;
			goto bail;
		}

		*listPtr++ = keyBlock;

		dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;

		keyBlock = GetShortLE(&blkBuf[0x02]);
		iterations++;
	}
	if (iterations == kMaxCatalogIterations) {
		WMSG0(" ProDOS subdir iteration count exceeded\n");
		dierr = kDIErrDirectoryLoop;
		goto bail;
	}

	assert(list[count] == kInvalidBlockNum);

	*pBlockCount = count;
	*pBlockList = list;

bail:
	if (dierr != kDIErrNone)
		delete list;
	return dierr;
}

/*
 * Dump the contents.
 */
void
A2FileProDOS::Dump(void) const
{
	WMSG2(" ProDOS file '%s' (path='%s')\n",
		fDirEntry.fileName, fPathName);
	WMSG3("   fileType=0x%02x auxType=0x%04x storage=%d\n",
		fDirEntry.fileType, fDirEntry.auxType, fDirEntry.storageType);
	WMSG3("   keyPointer=%d blocksUsed=%d eof=%ld\n",
		fDirEntry.keyPointer, fDirEntry.blocksUsed, fDirEntry.eof);
	WMSG3("   access=0x%02x create=0x%08lx mod=0x%08lx\n",
		fDirEntry.access, fDirEntry.createWhen, fDirEntry.modWhen);
	WMSG3("   version=%d minVersion=%d headerPtr=%d\n",
		fDirEntry.version, fDirEntry.minVersion, fDirEntry.headerPointer);
	if (fDirEntry.storageType == kStorageExtended) {
		WMSG4("   DATA storage=%d keyBlk=%d blkUsed=%d eof=%ld\n",
			fExtData.storageType, fExtData.keyBlock, fExtData.blocksUsed,
			fExtData.eof);
		WMSG4("   RSRC storage=%d keyBlk=%d blkUsed=%d eof=%ld\n",
			fExtRsrc.storageType, fExtRsrc.keyBlock, fExtRsrc.blocksUsed,
			fExtRsrc.eof);
	}
	WMSG2("   * sparseData=%ld  sparseRsrc=%ld\n",
		(long) fSparseDataEof, (long) fSparseRsrcEof);
}


/*
 * ===========================================================================
 *		A2FDProDOS
 * ===========================================================================
 */

/*
 * Read a chunk of data from whichever fork is open.
 */
DIError
A2FDProDOS::Read(void* buf, size_t len, size_t* pActual)
{
	WMSG3(" ProDOS reading %d bytes from '%s' (offset=%ld)\n",
		len, fpFile->GetPathName(), (long) fOffset);
	//if (fBlockList == nil)
	//	return kDIErrNotReady;

	if (fOffset + (long)len > fOpenEOF) {
		if (pActual == nil)
			return kDIErrDataUnderrun;
		len = (long) (fOpenEOF - fOffset);
	}
	if (pActual != nil)
		*pActual = len;
//
	long incrLen = len;

	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	long blockIndex = (long) (fOffset / kBlkSize);
	int bufOffset = (int) (fOffset % kBlkSize);		// (& 0x01ff)
	size_t thisCount;
	long progressCounter = 0;

	if (len == 0) {
		///* one block allocated for empty file */
		//SetLastBlock(fBlockList[0], true);
		return kDIErrNone;
	}
	assert(fOpenEOF != 0);

	assert(blockIndex >= 0 && blockIndex < fBlockCount);

	while (len) {
		if (fBlockList[blockIndex] == 0) {
			//WMSG1(" ProDOS sparse index %d\n", blockIndex);
			memset(blkBuf, 0, sizeof(blkBuf));
		} else {
			//WMSG1(" ProDOS non-sparse index %d\n", blockIndex);
			dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(fBlockList[blockIndex],
						blkBuf);
			if (dierr != kDIErrNone) {
				WMSG3(" ProDOS error reading block [%ld]=%d of '%s'\n",
					blockIndex, fBlockList[blockIndex], fpFile->GetPathName());
				return dierr;
			}
		}
		thisCount = kBlkSize - bufOffset;
		if (thisCount > len)
			thisCount = len;

		memcpy(buf, blkBuf + bufOffset, thisCount);
		len -= thisCount;
		buf = (char*)buf + thisCount;

		bufOffset = 0;
		blockIndex++;

		progressCounter++;
		if (progressCounter > 100 && len) {
			progressCounter = 0;
			/*
			 * Show progress within the current read request.  This only
			 * kicks in for large reads, e.g. reformatting the entire file.
			 * For smaller reads, used when we're extracting w/o reformatting,
			 * "progressCounter" never gets large enough.
			 */
			if (!UpdateProgress(fOffset + incrLen - len)) {
				dierr = kDIErrCancelled;
				return dierr;
			}
			//::Sleep(100);		// DEBUG DEBUG
		}
	}

	fOffset += incrLen;

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

	return dierr;
}

/*
 * Write data at the current offset.
 *
 * For simplicity, we assume that there can only be one of two situations:
 *	(1) We're writing a directory, which might expand by one block; or
 *	(2) We're writing all of a brand-new file in one shot.
 *
 * Modifies fOpenEOF, fOpenBlocksUsed, fStorageType, and sets fModified.
 *
 * HEY: ProSel-16 describes these as fragmented, and it's probably right.
 * The correct way to do this is to allocate index blocks before allocating
 * the blocks they refer to, so that we don't have to jump all over the disk
 * to read the indexes (which, at the moment, appear at the end of the file).
 * A bit tricky, but doable.
 */
DIError
A2FDProDOS::Write(const void* buf, size_t len, size_t* pActual)
{
	DIError dierr = kDIErrNone;
	A2FileProDOS* pFile = (A2FileProDOS*) fpFile;
	DiskFSProDOS* pDiskFS = (DiskFSProDOS*) fpFile->GetDiskFS();
	bool allocSparse = (pDiskFS->GetParameter(DiskFS::kParmProDOS_AllocSparse) != 0);
	unsigned char blkBuf[kBlkSize];
	unsigned short keyBlock;

	if (len >= 0x01000000) {	// 16MB
		assert(false);
		return kDIErrInvalidArg;
	}

	/* use separate function for directories */
	if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory ||
		pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader)
	{
		return WriteDirectory(buf, len, pActual);
	}

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

	assert(fOffset == 0);		// big simplifying assumption
	assert(fOpenEOF == 0);		// another one
	assert(fOpenBlocksUsed == 1);
	assert(buf != nil);

	/* nothing to do for zero-length write; don't even set fModified */
	if (len == 0)
		goto bail;

	if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended)
		keyBlock = pFile->fDirEntry.keyPointer;
	else {
		if (fOpenRsrcFork)
			keyBlock = pFile->fExtRsrc.keyBlock;
		else
			keyBlock = pFile->fExtData.keyBlock;
	}

	/*
	 * Special-case seedling files.  Just write the data into the key block
	 * and we're done.
	 */
	if (len <= (size_t)kBlkSize) {
		memset(blkBuf, 0, sizeof(blkBuf));
		memcpy(blkBuf, buf, len);
		dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;

		fOpenEOF = len;
		fOpenBlocksUsed = 1;
		assert(fOpenStorageType == A2FileProDOS::kStorageSeedling);
		fOffset += len;
		fModified = true;
		goto bail;
	}

	/*
	 * Start by allocating space for the block list.  The list is always the
	 * same size, regardless of sparse allocations.
	 *
	 * We over-alloc by one so we can have an overrun detection entry.
	 */
	fBlockCount = (len + kBlkSize-1) / kBlkSize;
	assert(fBlockCount > 0);
	delete[] fBlockList;
	fBlockList = new unsigned short[fBlockCount+1];
	if (fBlockList == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}
	fBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum;

	/*
	 * Write the data blocks to disk, allocating as we go.  We have to treat
	 * the last entry specially because it might not fill an entire block.
	 */
	const unsigned char* blkPtr;
	long blockIdx;
	bool allZero;
	long progressCounter;

	progressCounter = 0;
	allZero = true;
	blkPtr = (const unsigned char*) buf;
	for (blockIdx = 0; blockIdx < fBlockCount; blockIdx++) {
		long newBlock;

		if (blockIdx == fBlockCount-1) {
			/* for last block, copy partial and move blkPtr */
			int copyLen = len - (blockIdx * kBlkSize);
			assert(copyLen > 0 && copyLen <= kBlkSize);
			memset(blkBuf, 0, sizeof(blkBuf));
			memcpy(blkBuf, blkPtr, copyLen);
			blkPtr = blkBuf;
		}

		if (allocSparse && IsEmptyBlock(blkPtr))
			newBlock = 0;
		else {
			newBlock = pDiskFS->AllocBlock();
			fOpenBlocksUsed++;
			allZero = false;
		}

		if (newBlock < 0) {
			WMSG0(" ProDOS disk full during write!\n");
			dierr = kDIErrDiskFull;
			goto bail;
		}

		fBlockList[blockIdx] = (unsigned short) newBlock;

		if (newBlock != 0) {
			dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkPtr);
			if (dierr != kDIErrNone)
				goto bail;
		}

		blkPtr += kBlkSize;

		/*
		 * Update the progress counter and check to see if the "cancel" button
		 * has been hit.  We don't call UpdateProgress on the last block
		 * because we could be passing an offset value larger than "len".
		 * Also, we don't want the progress bar to hit 100% until we've
		 * actually finished.
		 *
		 * We do NOT want to check this after we start writing index blocks.
		 * If we do, we need to make sure that whatever index blocks the file
		 * has match up with what we've allocated in the disk block map.
		 *
		 * We don't want to save the disk block map if the user cancels here,
		 * because then the blocks will be marked as "used" even though the
		 * index blocks for this file haven't been written yet.
		 *
		 * It's tricky to get this right, which is why we allocate space
		 * for the index blocks now -- running out of disk space and
		 * user cancellation are handled the same way.  Once we get to the
		 * point where we're updating the file structure, we can neither be
		 * cancelled nor run out of space.  (We can still hit a bad block,
		 * though, which we currently don't handle.)
		 */
		progressCounter++;	// update every N blocks
		if (progressCounter > 100 && blockIdx != fBlockCount) {
			progressCounter = 0;
			if (!UpdateProgress(blockIdx * kBlkSize)) {
				dierr = kDIErrCancelled;
				goto bail;
			}
		}
	}

	assert(fBlockList[fBlockCount] == A2FileProDOS::kInvalidBlockNum);

	/*
	 * Now we have a full block map.  Allocate any needed index blocks and
	 * write them.
	 *
	 * If our block map is empty, i.e. the entire file is sparse, then
	 * there's no need to create a sapling.  We just leave the file in
	 * seedling form.  This can only happen for a completely empty file.
	 */
	if (allZero) {
		WMSG0("+++ ProDOS storing large but empty file as seedling\n");
		/* make sure key block is empty */
		memset(blkBuf, 0, sizeof(blkBuf));
		dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;
		fOpenStorageType = A2FileProDOS::kStorageSeedling;
		fBlockList[0] = keyBlock;
	} else if (fBlockCount <= 256) {
		/* sapling file, write an index block into the key block */
		bool allzero = true;
		assert(fBlockCount > 1);
		memset(blkBuf, 0, sizeof(blkBuf));
		int i;
		for (i = 0; i < fBlockCount; i++) {
			if (fBlockList[i] != 0)
				allzero = false;
			blkBuf[i] = fBlockList[i] & 0xff;
			blkBuf[256 + i] = (fBlockList[i] >> 8) & 0xff;
		}

		dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf);
		if (dierr != kDIErrNone)
			goto bail;
		fOpenStorageType = A2FileProDOS::kStorageSapling;
	} else {
		/* tree file, write two or more indexes and write master into key */
		unsigned char masterBlk[kBlkSize];
		int idx;

		memset(masterBlk, 0, sizeof(masterBlk));

		for (idx = 0; idx < fBlockCount; ) {
			long newBlock;
			int i;

			memset(blkBuf, 0, sizeof(blkBuf));
			for (i = 0; i < 256 && idx < fBlockCount; i++, idx++) {
				blkBuf[i] = fBlockList[idx] & 0xff;
				blkBuf[256+i] = (fBlockList[idx] >> 8) & 0xff;
			}

			/* allocate a new index block, if needed */
			if (allocSparse && IsEmptyBlock(blkBuf))
				newBlock = 0;
			else {
				newBlock = pDiskFS->AllocBlock();
				fOpenBlocksUsed++;
			}
			if (newBlock != 0) {
				dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkBuf);
				if (dierr != kDIErrNone)
					goto bail;
			}

			masterBlk[(idx-1) / 256] = (unsigned char) newBlock;
			masterBlk[256 + (idx-1)/256] = (unsigned char) (newBlock >> 8);
		}

		dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, masterBlk);
		if (dierr != kDIErrNone)
			goto bail;
		fOpenStorageType = A2FileProDOS::kStorageTree;
	}

	fOpenEOF = len;
	fOffset += len;
	fModified = true;

bail:
	if (dierr == kDIErrNone)
		dierr = pDiskFS->SaveVolBitmap();

	/*
	 * We need to check UpdateProgress *after* the volume bitmap has been
	 * saved.  Otherwise we'll have blocks allocated in the file's structure
	 * but not marked in-use in the map when the "dierr" check above fails.
	 */
	if (dierr == kDIErrNone) {
		if (!UpdateProgress(fOffset))
			dierr = kDIErrCancelled;
	}

	pDiskFS->FreeVolBitmap();
	return dierr;
}

/*
 * Determine whether a block is filled entirely with zeroes.
 */
bool
A2FDProDOS::IsEmptyBlock(const unsigned char* blk)
{
	int i;

	for (i = 0; i < kBlkSize; i++) {
		if (*blk++ != 0)
			return false;
	}

	return true;
}

/*
 * Write a directory, possibly extending it by one block.
 *
 * If we're growing, the extra block will already have been allocated, and is
 * pointed to by the "next" pointer in the next-to-last block.  (This
 * pre-allocation makes our lives easier, and avoids a situation where we
 * would have to update the volume bitmap when another function is already
 * making lots of changes to it.)
 */
DIError
A2FDProDOS::WriteDirectory(const void* buf, size_t len, size_t* pActual)
{
	DIError dierr = kDIErrNone;

	WMSG2("ProDOS  writing %d bytes to directory '%s'\n",
		len, fpFile->GetPathName());

	assert(len >= (size_t)kBlkSize);
	assert((len % kBlkSize) == 0);
	assert(len == (size_t)fOpenEOF || len == (size_t)fOpenEOF + kBlkSize);

	if (len > (size_t)fOpenEOF) {
		/*
		 * Extend the block list, remembering that we add an extra item
		 * on the end to check for overruns.
		 */
		unsigned short* newBlockList;

		fBlockCount++;
		newBlockList = new unsigned short[fBlockCount+1];
		memcpy(newBlockList, fBlockList,
			sizeof(unsigned short) * fBlockCount);
		newBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum;

		unsigned char* blkPtr;
		blkPtr = (unsigned char*)buf + fOpenEOF - kBlkSize;
		assert(blkPtr >= buf);
		assert(GetShortLE(&blkPtr[0x02]) != 0);
		newBlockList[fBlockCount-1] = GetShortLE(&blkPtr[0x02]);

		delete[] fBlockList;
		fBlockList = newBlockList;

		WMSG0(" ProDOS updated block list for subdir:\n");
		DumpBlockList();
	}

	/*
	 * Now just run down the block list writing the directory.
	 */
	assert(len == (size_t)fBlockCount * kBlkSize);
	int idx;
	for (idx = 0; idx < fBlockCount; idx++) {
		assert(fBlockList[idx] >= kVolHeaderBlock);
		dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(fBlockList[idx],
					(unsigned char*)buf + idx * kBlkSize);
		if (dierr != kDIErrNone) {
			WMSG1(" ProDOS failed writing dir, block=%d\n", fBlockList[idx]);
			goto bail;
		}
	}

	fOpenEOF = len;
	fOpenBlocksUsed = (unsigned short) fBlockCount;	// very simple for subdirs
	//fOpenStorageType
	fModified = true;

bail:
	return dierr;
}

/*
 * Seek to a new position within the file.
 */
DIError
A2FDProDOS::Seek(di_off_t offset, DIWhence whence)
{
	DIError dierr = kDIErrNone;
	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 dierr;
}

/*
 * Return current offset.
 */
di_off_t
A2FDProDOS::Tell(void)
{
	//if (fBlockList == nil)
	//	return kDIErrNotReady;

	return fOffset;
}

/*
 * Release file state.
 *
 * 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
A2FDProDOS::Close(void)
{
	DIError dierr = kDIErrNone;

	if (fModified) {
		A2FileProDOS* pFile = (A2FileProDOS*) fpFile;
		unsigned char blkBuf[kBlkSize];
		unsigned char newStorageType = fOpenStorageType;
		unsigned short newBlocksUsed = fOpenBlocksUsed;
		unsigned long newEOF = (unsigned long) fOpenEOF;
		unsigned short combinedBlocksUsed;
		unsigned long combinedEOF;

		/*
		 * If this is an extended file, fix the entries in the extended
		 * key block, and adjust the values to be stored in the directory.
		 */
		if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) {
			/* these two don't change */
			newStorageType = pFile->fDirEntry.storageType;

			dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(
						pFile->fDirEntry.keyPointer, blkBuf);
			if (dierr != kDIErrNone)
				goto bail;

			int offset = 0;
			if (fOpenRsrcFork)
				offset = 256;

			blkBuf[0x00 + offset] = fOpenStorageType;
			// key block doesn't change
			PutShortLE(&blkBuf[0x03 + offset], newBlocksUsed);
			blkBuf[0x05 + offset] = (unsigned char) newEOF;
			blkBuf[0x06 + offset] = (unsigned char) (newEOF >> 8);
			blkBuf[0x07 + offset] = (unsigned char) (newEOF >> 16);

			dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(
						pFile->fDirEntry.keyPointer, blkBuf);
			if (dierr != kDIErrNone)
				goto bail;

			// file blocks used is sum of data and rsrc block counts +1 for key
			combinedBlocksUsed =
				GetShortLE(&blkBuf[0x03]) + GetShortLE(&blkBuf[0x103]) +1;
			combinedEOF = 512;	// for some reason this gets stuffed in
		} else {
			combinedBlocksUsed = newBlocksUsed;
			combinedEOF = newEOF;
		}

		/*
		 * Update fields in the file's directory entry.  Unless, of course,
		 * this is the volume directory itself.
		 */
		if (pFile->fParentDirBlock != 0) {
			dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(
						pFile->fParentDirBlock, blkBuf);
			if (dierr != kDIErrNone)
				goto bail;

			unsigned char* pParentPtr;
			pParentPtr = blkBuf + 0x04 + pFile->fParentDirIdx * kEntryLength;
			assert(pParentPtr + kEntryLength < blkBuf + kBlkSize);
			if (toupper(pParentPtr[0x01]) != toupper(pFile->fDirEntry.fileName[0]))
			{
				WMSG0("ProDOS ERROR: parent pointer has wrong entry??\n");
				assert(false);
				dierr = kDIErrInternal;
				goto bail;
			}

			/* update the fields from the open file */
			pParentPtr[0x00] =
				(pParentPtr[0x00] & 0x0f) | (newStorageType << 4);
			PutShortLE(&pParentPtr[0x13], combinedBlocksUsed);
			if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended)
			{
				PutShortLE(&pParentPtr[0x15], (unsigned short) newEOF);
				pParentPtr[0x17] = (unsigned char) (newEOF >> 16);
			}
			/* don't update the mod date for now */
			//PutLongLE(&pParentPtr[0x21], A2FileProDOS::ConvertProDate(time(nil)));

			dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(
						pFile->fParentDirBlock, blkBuf);
			if (dierr != kDIErrNone)
				goto bail;
		}

		/*
		 * Find the #of sparse blocks.
		 */
		int sparseCount = 0;
		for (int i = 0; i < fBlockCount; i++) {
			if (fBlockList[i] == 0)
				sparseCount++;
		}

		/*
		 * Update our internal copies of stuff.  The EOFs have changed, and
		 * in theory we'd want to update the modification date.  In practice
		 * we're usually shuffling data from one archive to another and want
		 * to preserve the mod date.  (Could be a DiskFS global pref?)
		 */
		pFile->fDirEntry.storageType = newStorageType;
		pFile->fDirEntry.blocksUsed = combinedBlocksUsed;
		pFile->fDirEntry.eof = combinedEOF;

		if (newStorageType == A2FileProDOS::kStorageExtended) {
			if (!fOpenRsrcFork) {
				pFile->fExtData.storageType = fOpenStorageType;
				pFile->fExtData.blocksUsed = newBlocksUsed;
				pFile->fExtData.eof = newEOF;
				pFile->fSparseDataEof = (di_off_t) newEOF - (sparseCount * kBlkSize);
				if (pFile->fSparseDataEof < 0)
					pFile->fSparseDataEof = 0;
			} else {
				pFile->fExtRsrc.storageType = fOpenStorageType;
				pFile->fExtRsrc.blocksUsed = newBlocksUsed;
				pFile->fExtRsrc.eof = newEOF;
				pFile->fSparseRsrcEof = (di_off_t) newEOF - (sparseCount * kBlkSize);
				if (pFile->fSparseRsrcEof < 0)
					pFile->fSparseRsrcEof = 0;
			}
		} else {
			pFile->fSparseDataEof = (di_off_t) newEOF - (sparseCount * kBlkSize);
			if (pFile->fSparseDataEof < 0)
				pFile->fSparseDataEof = 0;
		}
		// update mod date?
		
		//WMSG1("File '%s' closed\n", pFile->GetPathName());
		//pFile->Dump();
	}

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


/*
 * Return the #of sectors/blocks in the file.
 */
long
A2FDProDOS::GetSectorCount(void) const
{
	//if (fBlockList == nil)
	//	return kDIErrNotReady;
	return fBlockCount * 2;
}
long
A2FDProDOS::GetBlockCount(void) const
{
	//if (fBlockList == nil)
	//	return kDIErrNotReady;
	return fBlockCount;
}

/*
 * Return the Nth track/sector in this file.
 */
DIError
A2FDProDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
{
	//if (fBlockList == nil)
	//	return kDIErrNotReady;
	long prodosIdx = sectorIdx / 2;
	if (prodosIdx < 0 || prodosIdx >= fBlockCount)
		return kDIErrInvalidIndex;
	long prodosBlock = fBlockList[prodosIdx];

	if (prodosBlock == 0)
		*pTrack = *pSector = 0;		// special-case to avoid returning (0,1)
	else
		BlockToTrackSector(prodosBlock, (sectorIdx & 0x01) != 0, pTrack, pSector);
	return kDIErrNone;
}
/*
 * Return the Nth 512-byte block in this file.
 */
DIError
A2FDProDOS::GetStorage(long blockIdx, long* pBlock) const
{
	//if (fBlockList == nil)
	//	return kDIErrNotReady;
	if (blockIdx < 0 || blockIdx >= fBlockCount)
		return kDIErrInvalidIndex;
	long prodosBlock = fBlockList[blockIdx];

	*pBlock = prodosBlock;
	assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks());
	return kDIErrNone;
}

/*
 * Dump the list of blocks from an open file, skipping over
 * "sparsed-out" entries.
 */
void
A2FDProDOS::DumpBlockList(void) const
{
	long ll;

	WMSG1(" ProDOS file block list (count=%ld)\n", fBlockCount);
	for (ll = 0; ll <= fBlockCount; ll++) {
		if (fBlockList[ll] != 0) {
			WMSG2(" %5ld: 0x%04x\n", ll, fBlockList[ll]);
		}
	}
}
