/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Implementation of DiskFSPascal class.
 *
 * Currently each file may only be open once.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"


/*
 * ===========================================================================
 *		DiskFSPascal
 * ===========================================================================
 */

const int kBlkSize = 512;
const int kVolHeaderBlock = 2;			// first directory block
const int kMaxCatalogIterations = 64;	// should be short, linear catalog
const int kHugeDir = 32;
static const char* kInvalidNameChars = "$=?,[#:";


/*
 * See if this looks like a Pascal volume.
 *
 * We test a few fields in the volume directory for validity.
 */
static DIError
TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[512];
	unsigned char volName[DiskFSPascal::kMaxVolumeName+1];

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


	if (!(blkBuf[0x00] == 0 && blkBuf[0x01] == 0) ||
		!(blkBuf[0x04] == 0 && blkBuf[0x05] == 0) ||
		!(blkBuf[0x06] > 0 && blkBuf[0x06] <= DiskFSPascal::kMaxVolumeName) ||
		0)
	{
		dierr = kDIErrFilesystemNotFound;
		goto bail;
	}

	/* volume name length is good, check the name itself */
	/* (this may be overly restrictive, but it's probably good to be) */
	memset(volName, 0, sizeof(volName));
	memcpy(volName, &blkBuf[0x07], blkBuf[0x06]);
	if (!DiskFSPascal::IsValidVolumeName((const char*) volName))
		return kDIErrFilesystemNotFound;

bail:
	return dierr;
}

/*
 * Test to see if the image is a Pascal disk.
 */
/*static*/ DIError
DiskFSPascal::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::kFormatPascal;
			return kDIErrNone;
		}
	}

	WMSG0(" Pascal 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 is
 * completely full and has no files on it.
 */
DIError
DiskFSPascal::Initialize(void)
{
	DIError dierr = kDIErrNone;

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

	fVolumeUsage.Create(fpImg->GetNumBlocks());

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

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

	dierr = ScanFileUsage();
	if (dierr != kDIErrNone) {
		/* this might not be fatal; just means that *some* files are bad */
		goto bail;
	}

	fDiskIsGood = CheckDiskIsGood();

	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.
 */
DIError
DiskFSPascal::LoadVolHeader(void)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	int nameLen, maxFiles;

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

	/* vol header is same size as dir entry, but different layout */
	fStartBlock = GetShortLE(&blkBuf[0x00]);
	assert(fStartBlock == 0);		// verified in "TestImage"
	fNextBlock = GetShortLE(&blkBuf[0x02]);
	assert(GetShortLE(&blkBuf[0x04]) == 0);		// type
	nameLen = blkBuf[0x06] & 0x07;
	memcpy(fVolumeName, &blkBuf[0x07], nameLen);
	fVolumeName[nameLen] = '\0';
	fTotalBlocks = GetShortLE(&blkBuf[0x0e]);
	fNumFiles = GetShortLE(&blkBuf[0x10]);
	fAccessWhen = GetShortLE(&blkBuf[0x12]);	// time of last access
	fDateSetWhen = GetShortLE(&blkBuf[0x14]);	// most recent date set
	fStuff1 = GetShortLE(&blkBuf[0x16]);		// filler
	fStuff2 = GetShortLE(&blkBuf[0x18]);		// filler

	if (fTotalBlocks != fpImg->GetNumBlocks()) {
		// saw this most recently on a 40-track .APP image; not a problem
		WMSG2(" Pascal WARNING: total (%u) != img (%ld)\n",
			fTotalBlocks, fpImg->GetNumBlocks());
	}

	/* 
	 * Sanity checks.
	 */
	if (fNextBlock > 34) {
		// directory really shouldn't be more than 6; I'm being generous
		fpImg->AddNote(DiskImg::kNoteWarning,
			"Pascal directory is too big (%d blocks); trimming.",
			fNextBlock - fStartBlock);
	}

	/* max #of file entries, including the vol dir header */
	maxFiles = ((fNextBlock - kVolHeaderBlock) * kBlkSize) / kDirectoryEntryLen;
	if (fNumFiles > maxFiles-1) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"Pascal fNumFiles (%d) exceeds max files (%d); trimming.\n",
			fNumFiles, maxFiles-1);
		fEarlyDamage = true;
	}

	SetVolumeID();

bail:
	return dierr;
}

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

/*
 * Dump what we pulled out of the volume header.
 */
void
DiskFSPascal::DumpVolHeader(void)
{
	time_t access, dateSet;

	WMSG1(" Pascal volume header for '%s'\n", fVolumeName);
	WMSG2("   startBlock=%d nextBlock=%d\n",
		fStartBlock, fNextBlock);
	WMSG4("   totalBlocks=%d numFiles=%d access=0x%04x dateSet=0x%04x\n",
		fTotalBlocks, fNumFiles, fAccessWhen, fDateSetWhen);

	access = A2FilePascal::ConvertPascalDate(fAccessWhen);
	dateSet = A2FilePascal::ConvertPascalDate(fDateSetWhen);
	WMSG1("   -->access %.24s\n", ctime(&access));
	WMSG1("   -->dateSet %.24s\n", ctime(&dateSet));

	//WMSG2("Unconvert access=0x%04x dateSet=0x%04x\n",
	//	A2FilePascal::ConvertPascalDate(access),
	//	A2FilePascal::ConvertPascalDate(dateSet));
}


/*
 * Read the catalog from the disk.
 *
 * No distinction is made for block boundaries, so we want to slurp the
 * entire thing into memory.
 *
 * Sets "fDirectory".
 */
DIError
DiskFSPascal::LoadCatalog(void)
{
	DIError dierr = kDIErrNone;
	unsigned char* dirPtr;
	int block, numBlocks;

	assert(fDirectory == nil);

	numBlocks = fNextBlock - kVolHeaderBlock;
	if (numBlocks <= 0 || numBlocks > kHugeDir) {
		dierr = kDIErrBadDiskImage;
		goto bail;
	}

	fDirectory = new unsigned char[kBlkSize * numBlocks];
	if (fDirectory == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	block = kVolHeaderBlock;
	dirPtr = fDirectory;
	while (numBlocks--) {
		dierr = fpImg->ReadBlock(block, dirPtr);
		if (dierr != kDIErrNone)
			goto bail;

		block++;
		dirPtr += kBlkSize;
	}

bail:
	if (dierr != kDIErrNone) {
		delete[] fDirectory;
		fDirectory = nil;
	}
	return dierr;
}

/*
 * Write our copy of the catalog back out to disk.
 */
DIError
DiskFSPascal::SaveCatalog(void)
{
	DIError dierr = kDIErrNone;
	unsigned char* dirPtr;
	int block, numBlocks;

	assert(fDirectory != nil);

	numBlocks = fNextBlock - kVolHeaderBlock;
	block = kVolHeaderBlock;
	dirPtr = fDirectory;
	while (numBlocks--) {
		dierr = fpImg->WriteBlock(block, dirPtr);
		if (dierr != kDIErrNone)
			goto bail;

		block++;
		dirPtr += kBlkSize;
	}

bail:
	return dierr;
}

/*
 * Free the catalog storage.
 */
void
DiskFSPascal::FreeCatalog(void)
{
	delete[] fDirectory;
	fDirectory = nil;
}


/*
 * Process the catalog into A2File structures.
 */
DIError
DiskFSPascal::ProcessCatalog(void)
{
	DIError dierr = kDIErrNone;
	int i, nameLen;
	A2FilePascal* pFile;
	const unsigned char* dirPtr;
	unsigned short prevNextBlock = fNextBlock;

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

	dirPtr = fDirectory + kDirectoryEntryLen;		// skip vol dir entry
	for (i = 0; i < fNumFiles; i++) {
		pFile = new A2FilePascal(this);

		pFile->fStartBlock = GetShortLE(&dirPtr[0x00]);
		pFile->fNextBlock = GetShortLE(&dirPtr[0x02]);
		pFile->fFileType = (A2FilePascal::FileType) GetShortLE(&dirPtr[0x04]);
		nameLen = dirPtr[0x06] & 0x0f;
		memcpy(pFile->fFileName, &dirPtr[0x07], nameLen);
		pFile->fFileName[nameLen] = '\0';
		pFile->fBytesRemaining = GetShortLE(&dirPtr[0x16]);
		pFile->fModWhen = GetShortLE(&dirPtr[0x18]);

		/* check bytesRem before setting length field */
		if (pFile->fBytesRemaining > kBlkSize) {
			WMSG2(" Pascal found strange bytesRem %u on '%s', trimming\n",
				pFile->fBytesRemaining, pFile->fFileName);
			pFile->fBytesRemaining = kBlkSize;
			pFile->SetQuality(A2File::kQualitySuspicious);
		}

		pFile->fLength = pFile->fBytesRemaining +
			(pFile->fNextBlock - pFile->fStartBlock -1) * kBlkSize;

		/*
		 * Check values.
		 */
		if (pFile->fStartBlock == pFile->fNextBlock) {
			WMSG1(" Pascal found zero-block file '%s'\n", pFile->fFileName);
			pFile->SetQuality(A2File::kQualityDamaged);
		}
		if (pFile->fStartBlock < prevNextBlock) {
			WMSG3(" Pascal start of '%s' (%d) overlaps previous end (%d)\n",
				pFile->fFileName, pFile->fStartBlock, prevNextBlock);
			pFile->SetQuality(A2File::kQualityDamaged);
		}

		if (pFile->fNextBlock > fpImg->GetNumBlocks()) {
			WMSG3(" Pascal invalid 'next' block %d (max %ld) '%s'\n",
				pFile->fNextBlock, fpImg->GetNumBlocks(), pFile->fFileName);
			pFile->fStartBlock = pFile->fNextBlock = 0;
			pFile->fLength = 0;
			pFile->SetQuality(A2File::kQualityDamaged);
		} else if (pFile->fNextBlock > fTotalBlocks) {
			WMSG3(" Pascal 'next' block %d exceeds max (%d) '%s'\n",
				pFile->fNextBlock, fTotalBlocks, pFile->fFileName);
			pFile->SetQuality(A2File::kQualitySuspicious);
		}

		//pFile->Dump();
		AddFileToList(pFile);

		dirPtr += kDirectoryEntryLen;
		prevNextBlock = pFile->fNextBlock;
	}

bail:
	FreeCatalog();
	return dierr;
}


/*
 * Create the volume usage map.  Since UCSD Pascal volumes have neither
 * in-use maps nor index blocks, this is pretty straightforward.
 */
DIError
DiskFSPascal::ScanFileUsage(void)
{
	int block;

	/* start with the boot blocks */
	SetBlockUsage(0, VolumeUsage::kChunkPurposeSystem);
	SetBlockUsage(1, VolumeUsage::kChunkPurposeSystem);

	for (block = kVolHeaderBlock; block < fNextBlock; block++) {
		SetBlockUsage(block, VolumeUsage::kChunkPurposeVolumeDir);
	}

	A2FilePascal* pFile;
	pFile = (A2FilePascal*) GetNextFile(nil);
	while (pFile != nil) {
		for (block = pFile->fStartBlock; block < pFile->fNextBlock; block++)
			SetBlockUsage(block, VolumeUsage::kChunkPurposeUserData);

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

	return kDIErrNone;
}

/*
 * Update an entry in the volume usage map.
 */
void
DiskFSPascal::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose)
{
	VolumeUsage::ChunkState cstate;

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


/*
 * Test a string for validity as a Pascal volume name.
 *
 * Volume names can only be 7 characters long, but otherwise obey the same
 * rules as file names.
 */
/*static*/ bool
DiskFSPascal::IsValidVolumeName(const char* name)
{
	if (name == nil) {
		assert(false);
		return false;
	}
	if (strlen(name) > kMaxVolumeName)
		return false;
	return IsValidFileName(name);
}

/*
 * Test a string for validity as a Pascal file name.
 *
 * Filenames can be up to 15 characters long.  All characters are valid.
 * However, the system filer gets bent out of shape if you use spaces,
 * control characters, any of the wildcards ($=?), or filer meta-characters
 * (,[#:).  It also converts all alpha characters to upper case, but we can
 * take care of that later.
 */
/*static*/ bool
DiskFSPascal::IsValidFileName(const char* name)
{
	assert(name != nil);

	if (name[0] == '\0')
		return false;
	if (strlen(name) > A2FilePascal::kMaxFileName)
		return false;

	/* must be A-Z 0-9 '.' */
	while (*name != '\0') {
		if (*name <= 0x20 || *name >= 0x7f)		// no space, del, or ctrl
			return false;
		//if (*name >= 'a' && *name <= 'z')		// no lower case
		//	return false;
		if (strchr(kInvalidNameChars, *name) != nil) // filer metacharacters
			return false;

		name++;
	}

	return true;
}

/*
 * Put a Pascal filesystem image on the specified DiskImg.
 */
DIError
DiskFSPascal::Format(DiskImg* pDiskImg, const char* volName)
{
	DIError dierr = kDIErrNone;
	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(" Pascal formatting disk image\n");

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

	formatBlocks = pDiskImg->GetNumBlocks();
	if (formatBlocks != 280 && formatBlocks != 1600) {
		WMSG1(" Pascal: rejecting format req blocks=%ld\n", formatBlocks);
		assert(false);
		return kDIErrInvalidArg;
	}

	/*
	 * We should now zero out the disk blocks, but this is done automatically
	 * on new disk images, so there's no need to do it here.
	 */
//	dierr = fpImg->ZeroImage();
	WMSG0(" Pascal  (not zeroing blocks)\n");

	/*
	 * Start by writing blocks 0 and 1 (the boot blocks).  The file
	 * APPLE3:FORMATTER.DATA holds images for 3.5" and 5.25" disks.
	 */
	dierr = WriteBootBlocks();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Write the disk volume entry.
	 */
	memset(blkBuf, 0, sizeof(blkBuf));
	PutShortLE(&blkBuf[0x00], 0);		// start block
	PutShortLE(&blkBuf[0x02], 6);		// next block
	PutShortLE(&blkBuf[0x04], 0);		// "file" type
	blkBuf[0x06] = strlen(volName);
	memcpy(&blkBuf[0x07], volName, strlen(volName));
	PutShortLE(&blkBuf[0x0e], (unsigned short) pDiskImg->GetNumBlocks());
	PutShortLE(&blkBuf[0x10], 0);		// num files
	PutShortLE(&blkBuf[0x12], 0);		// last access date
	PutShortLE(&blkBuf[0x14], 0xa87b);	// last date set (Nov 7 1984)
	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;
	}


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

/*
 * Blocks 0 and 1 of a 5.25" bootable Pascal disk, formatted by
 * APPLE3:FORMATTER from Pascal v1.3.
 */
const unsigned char gPascal525Block0[] = {
	0x01, 0xe0, 0x70, 0xb0, 0x04, 0xe0, 0x40, 0xb0, 0x39, 0xbd, 0x88, 0xc0,
	0x20, 0x20, 0x08, 0xa2, 0x00, 0xbd, 0x25, 0x08, 0x09, 0x80, 0x20, 0xfd,
	0xfb, 0xe8, 0xe0, 0x1d, 0xd0, 0xf3, 0xf0, 0xfe, 0xa9, 0x0a, 0x4c, 0x24,
	0xfc, 0x4d, 0x55, 0x53, 0x54, 0x20, 0x42, 0x4f, 0x4f, 0x54, 0x20, 0x46,
	0x52, 0x4f, 0x4d, 0x20, 0x53, 0x4c, 0x4f, 0x54, 0x20, 0x34, 0x2c, 0x20,
	0x35, 0x20, 0x4f, 0x52, 0x20, 0x36, 0x8a, 0x85, 0x43, 0x4a, 0x4a, 0x4a,
	0x4a, 0x09, 0xc0, 0x85, 0x0d, 0xa9, 0x5c, 0x85, 0x0c, 0xad, 0x00, 0x08,
	0xc9, 0x06, 0xb0, 0x0a, 0x69, 0x02, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0x6c,
	0x0c, 0x00, 0xa9, 0x00, 0x8d, 0x78, 0x04, 0xa9, 0x0a, 0x85, 0x0e, 0xa9,
	0x80, 0x85, 0x3f, 0x85, 0x11, 0xa9, 0x00, 0x85, 0x10, 0xa9, 0x08, 0x85,
	0x02, 0xa9, 0x02, 0x85, 0x0f, 0xa9, 0x00, 0x20, 0x4c, 0x09, 0xa2, 0x4e,
	0xa0, 0x06, 0xb1, 0x10, 0xd9, 0x39, 0x09, 0xf0, 0x2b, 0x18, 0xa5, 0x10,
	0x69, 0x1a, 0x85, 0x10, 0x90, 0x02, 0xe6, 0x11, 0xca, 0xd0, 0xe9, 0xc6,
	0x0e, 0xd0, 0xcc, 0x20, 0x20, 0x08, 0xa6, 0x43, 0xbd, 0x88, 0xc0, 0xa2,
	0x00, 0xbd, 0x2a, 0x09, 0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x15,
	0xd0, 0xf3, 0xf0, 0xfe, 0xc8, 0xc0, 0x13, 0xd0, 0xc9, 0xad, 0x81, 0xc0,
	0xad, 0x81, 0xc0, 0xa9, 0xd0, 0x85, 0x3f, 0xa9, 0x30, 0x85, 0x02, 0xa0,
	0x00, 0xb1, 0x10, 0x85, 0x0f, 0xc8, 0xb1, 0x10, 0x20, 0x4c, 0x09, 0xad,
	0x89, 0xc0, 0xa9, 0xd0, 0x85, 0x3f, 0xa9, 0x10, 0x85, 0x02, 0xa0, 0x00,
	0xb1, 0x10, 0x18, 0x69, 0x18, 0x85, 0x0f, 0xc8, 0xb1, 0x10, 0x69, 0x00,
	0x20, 0x4c, 0x09, 0xa5, 0x43, 0xc9, 0x50, 0xf0, 0x08, 0x90, 0x1a, 0xad,
	0x80, 0xc0, 0x6c, 0xf8, 0xff, 0xa2, 0x00, 0x8e, 0xc4, 0xfe, 0xe8, 0x8e,
	0xc6, 0xfe, 0xe8, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xfb,
	0x08, 0xa2, 0x00, 0x8e, 0xc0, 0xfe, 0xe8, 0x8e, 0xc2, 0xfe, 0xa2, 0x04,
	0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xfb, 0x08, 0x4e, 0x4f,
	0x20, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d,
	0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x0c, 0x53, 0x59, 0x53, 0x54,
	0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x4a, 0x08, 0xa5, 0x0f,
	0x29, 0x07, 0x0a, 0x85, 0x00, 0xa5, 0x0f, 0x28, 0x6a, 0x4a, 0x4a, 0x85,
	0xf0, 0xa9, 0x00, 0x85, 0x3e, 0x4c, 0x78, 0x09, 0xa6, 0x02, 0xf0, 0x22,
	0xc6, 0x02, 0xe6, 0x3f, 0xe6, 0x00, 0xa5, 0x00, 0x49, 0x10, 0xd0, 0x04,
	0x85, 0x00, 0xe6, 0xf0, 0xa4, 0x00, 0xb9, 0x8b, 0x09, 0x85, 0xf1, 0xa2,
	0x00, 0xe4, 0x02, 0xf0, 0x05, 0x20, 0x9b, 0x09, 0x90, 0xda, 0x60, 0x00,
	0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x01, 0x03, 0x05, 0x07, 0x09,
	0x0b, 0x0d, 0x0f, 0xa6, 0x43, 0xa5, 0xf0, 0x0a, 0x0e, 0x78, 0x04, 0x20,
	0xa3, 0x0a, 0x4e, 0x78, 0x04, 0x20, 0x47, 0x0a, 0xb0, 0xfb, 0xa4, 0x2e,
	0x8c, 0x78, 0x04, 0xc4, 0xf0, 0xd0, 0xe6, 0xa5, 0x2d, 0xc5, 0xf1, 0xd0,
	0xec, 0x20, 0xdf, 0x09, 0xb0, 0xe7, 0x20, 0xc7, 0x09, 0x18, 0x60, 0xa0,
	0x00, 0xa2, 0x56, 0xca, 0x30, 0xfb, 0xb9, 0x00, 0x02, 0x5e, 0x00, 0x03,
	0x2a, 0x5e, 0x00, 0x03, 0x2a, 0x91, 0x3e, 0xc8, 0xd0, 0xed, 0x60, 0xa0,
	0x20, 0x88, 0xf0, 0x61, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x49, 0xd5, 0xd0,
	0xf4, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xf2, 0xa0,
	0x56, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xad
};
const unsigned char gPascal525Block1[] = {
	0xd0, 0xe7, 0xa9, 0x00, 0x88, 0x84, 0x26, 0xbc, 0x8c, 0xc0, 0x10, 0xfb,
	0x59, 0xd6, 0x02, 0xa4, 0x26, 0x99, 0x00, 0x03, 0xd0, 0xee, 0x84, 0x26,
	0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0x59, 0xd6, 0x02, 0xa4, 0x26, 0x99, 0x00,
	0x02, 0xc8, 0xd0, 0xee, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0xd9, 0xd6, 0x02,
	0xd0, 0x13, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, 0x0a, 0xea,
	0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xf0, 0x5c, 0x38, 0x60, 0xa0,
	0xfc, 0x84, 0x26, 0xc8, 0xd0, 0x04, 0xe6, 0x26, 0xf0, 0xf3, 0xbd, 0x8c,
	0xc0, 0x10, 0xfb, 0xc9, 0xd5, 0xd0, 0xf0, 0xea, 0xbd, 0x8c, 0xc0, 0x10,
	0xfb, 0xc9, 0xaa, 0xd0, 0xf2, 0xa0, 0x03, 0xbd, 0x8c, 0xc0, 0x10, 0xfb,
	0xc9, 0x96, 0xd0, 0xe7, 0xa9, 0x00, 0x85, 0x27, 0xbd, 0x8c, 0xc0, 0x10,
	0xfb, 0x2a, 0x85, 0x26, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x25, 0x26, 0x99,
	0x2c, 0x00, 0x45, 0x27, 0x88, 0x10, 0xe7, 0xa8, 0xd0, 0xb7, 0xbd, 0x8c,
	0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, 0xae, 0xea, 0xbd, 0x8c, 0xc0, 0x10,
	0xfb, 0xc9, 0xaa, 0xd0, 0xa4, 0x18, 0x60, 0x86, 0x2b, 0x85, 0x2a, 0xcd,
	0x78, 0x04, 0xf0, 0x48, 0xa9, 0x00, 0x85, 0x26, 0xad, 0x78, 0x04, 0x85,
	0x27, 0x38, 0xe5, 0x2a, 0xf0, 0x37, 0xb0, 0x07, 0x49, 0xff, 0xee, 0x78,
	0x04, 0x90, 0x05, 0x69, 0xfe, 0xce, 0x78, 0x04, 0xc5, 0x26, 0x90, 0x02,
	0xa5, 0x26, 0xc9, 0x0c, 0xb0, 0x01, 0xa8, 0x20, 0xf4, 0x0a, 0xb9, 0x15,
	0x0b, 0x20, 0x04, 0x0b, 0xa5, 0x27, 0x29, 0x03, 0x0a, 0x05, 0x2b, 0xaa,
	0xbd, 0x80, 0xc0, 0xb9, 0x21, 0x0b, 0x20, 0x04, 0x0b, 0xe6, 0x26, 0xd0,
	0xbf, 0x20, 0x04, 0x0b, 0xad, 0x78, 0x04, 0x29, 0x03, 0x0a, 0x05, 0x2b,
	0xaa, 0xbd, 0x81, 0xc0, 0xa6, 0x2b, 0x60, 0xea, 0xa2, 0x11, 0xca, 0xd0,
	0xfd, 0xe6, 0x46, 0xd0, 0x02, 0xe6, 0x47, 0x38, 0xe9, 0x01, 0xd0, 0xf0,
	0x60, 0x01, 0x30, 0x28, 0x24, 0x20, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c,
	0x1c, 0x70, 0x2c, 0x26, 0x22, 0x1f, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c,
	0x1c, 0x20, 0x43, 0x4f, 0x50, 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20,
	0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54,
	0x45, 0x52, 0x2c, 0x20, 0x49, 0x4e, 0x43, 0x2e, 0x2c, 0x20, 0x31, 0x39,
	0x38, 0x34, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x35, 0x20, 0x43, 0x2e, 0x4c,
	0x45, 0x55, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x68, 0x03, 0x00, 0x00, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb
};

/*
 * Block 0 of a 3.5" bootable Pascal disk, formatted by
 * APPLE3:FORMATTER from Pascal v1.3.  Block 1 is zeroed out.
 */
unsigned char gPascal35Block0[] = {
	0x01, 0xe0, 0x70, 0xb0, 0x04, 0xe0, 0x40, 0xb0, 0x39, 0xbd, 0x88, 0xc0,
	0x20, 0x20, 0x08, 0xa2, 0x00, 0xbd, 0x25, 0x08, 0x09, 0x80, 0x20, 0xfd,
	0xfb, 0xe8, 0xe0, 0x1d, 0xd0, 0xf3, 0xf0, 0xfe, 0xa9, 0x0a, 0x4c, 0x24,
	0xfc, 0x4d, 0x55, 0x53, 0x54, 0x20, 0x42, 0x4f, 0x4f, 0x54, 0x20, 0x46,
	0x52, 0x4f, 0x4d, 0x20, 0x53, 0x4c, 0x4f, 0x54, 0x20, 0x34, 0x2c, 0x20,
	0x35, 0x20, 0x4f, 0x52, 0x20, 0x36, 0x8a, 0x85, 0x43, 0x4a, 0x4a, 0x4a,
	0x4a, 0x09, 0xc0, 0x85, 0x15, 0x8d, 0x5d, 0x09, 0xa9, 0x00, 0x8d, 0x78,
	0x04, 0x85, 0x14, 0xa9, 0x0a, 0x85, 0x0e, 0xa9, 0x80, 0x85, 0x13, 0x85,
	0x11, 0xa9, 0x00, 0x85, 0x10, 0x85, 0x0b, 0xa9, 0x02, 0x85, 0x0a, 0xa9,
	0x04, 0x85, 0x02, 0x20, 0x40, 0x09, 0xa2, 0x4e, 0xa0, 0x06, 0xb1, 0x10,
	0xd9, 0x2d, 0x09, 0xf0, 0x2b, 0x18, 0xa5, 0x10, 0x69, 0x1a, 0x85, 0x10,
	0x90, 0x02, 0xe6, 0x11, 0xca, 0xd0, 0xe9, 0xc6, 0x0e, 0xd0, 0xcc, 0x20,
	0x20, 0x08, 0xa6, 0x43, 0xbd, 0x88, 0xc0, 0xa2, 0x00, 0xbd, 0x1e, 0x09,
	0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x15, 0xd0, 0xf3, 0xf0, 0xfe,
	0xc8, 0xc0, 0x13, 0xd0, 0xc9, 0xad, 0x83, 0xc0, 0xad, 0x83, 0xc0, 0xa9,
	0xd0, 0x85, 0x13, 0xa0, 0x00, 0xb1, 0x10, 0x85, 0x0a, 0xc8, 0xb1, 0x10,
	0x85, 0x0b, 0xa9, 0x18, 0x85, 0x02, 0x20, 0x40, 0x09, 0xad, 0x8b, 0xc0,
	0xa9, 0xd0, 0x85, 0x13, 0xa0, 0x00, 0xb1, 0x10, 0x18, 0x69, 0x18, 0x85,
	0x0a, 0xc8, 0xb1, 0x10, 0x69, 0x00, 0x85, 0x0b, 0xa9, 0x08, 0x85, 0x02,
	0x20, 0x40, 0x09, 0xa5, 0x43, 0xc9, 0x50, 0xf0, 0x08, 0x90, 0x1a, 0xad,
	0x80, 0xc0, 0x6c, 0xf8, 0xff, 0xa2, 0x00, 0x8e, 0xc4, 0xfe, 0xe8, 0x8e,
	0xc6, 0xfe, 0xe8, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xef,
	0x08, 0xa2, 0x00, 0x8e, 0xc0, 0xfe, 0xe8, 0x8e, 0xc2, 0xfe, 0xa2, 0x04,
	0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xef, 0x08, 0x4e, 0x4f,
	0x20, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d,
	0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x0c, 0x53, 0x59, 0x53, 0x54,
	0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0xa9, 0x01, 0x85, 0x42,
	0xa0, 0xff, 0xb1, 0x14, 0x8d, 0x5c, 0x09, 0xa9, 0x00, 0x85, 0x44, 0xa5,
	0x13, 0x85, 0x45, 0xa5, 0x0a, 0x85, 0x46, 0xa5, 0x0b, 0x85, 0x47, 0x20,
	0x00, 0x00, 0x90, 0x03, 0x4c, 0x5b, 0x08, 0xc6, 0x02, 0xf0, 0x0c, 0xe6,
	0x13, 0xe6, 0x13, 0xe6, 0x0a, 0xd0, 0xdc, 0xe6, 0x0b, 0xd0, 0xd8, 0x60,
	0x20, 0x43, 0x4f, 0x50, 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20, 0x41,
	0x50, 0x50, 0x4c, 0x45, 0x20, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45,
	0x52, 0x2c, 0x20, 0x49, 0x4e, 0x43, 0x2e, 0x2c, 0x20, 0x31, 0x39, 0x38,
	0x34, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x35, 0x20, 0x43, 0x2e, 0x4c, 0x45,
	0x55, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0xb0, 0x01, 0x00, 0x00, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

/*
 * Write the Pascal boot blocks onto the disk image.
 */
DIError
DiskFSPascal::WriteBootBlocks(void)
{
	DIError dierr;
	unsigned char block0[512];
	unsigned char block1[512];
	bool is525 = false;

	assert(fpImg->GetHasBlocks());
	if (fpImg->GetNumBlocks() == 280)
		is525 = true;
	else if (fpImg->GetNumBlocks() == 1600)
		is525 = false;
	else {
		WMSG1(" Pascal boot blocks for blocks=%ld unknown\n",
			fpImg->GetNumBlocks());
        return kDIErrInternal;
	}

	if (is525) {
		memcpy(block0, gPascal525Block0, sizeof(block0));
		memcpy(block1, gPascal525Block1, sizeof(block1));
	} else {
		memcpy(block0, gPascal35Block0, 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;
}


/*
 * Scan for damaged files and conflicting file allocation entries.
 *
 * Appends some entries to the DiskImg notes, so this should only be run
 * once per DiskFS.
 *
 * Returns "true" if disk appears to be perfect, "false" otherwise.
 */
bool
DiskFSPascal::CheckDiskIsGood(void)
{
	//DIError dierr;
	bool result = true;

	if (fEarlyDamage)
		result = false;

	/* (don't need to check to see if the boot blocks or disk catalog are
	   marked in use -- the directory is defined by the set of blocks
	   in the volume header) */

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

	return result;
}

/*
 * Run through the list of files and count up the free blocks.
 */
DIError
DiskFSPascal::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits,
	int* pUnitSize) const
{
	A2FilePascal* pFile;
	long freeBlocks = 0;
	unsigned short prevNextBlock = fNextBlock;

	pFile = (A2FilePascal*) GetNextFile(nil);
	while (pFile != nil) {
		freeBlocks += pFile->fStartBlock - prevNextBlock;
		prevNextBlock = pFile->fNextBlock;

		pFile = (A2FilePascal*) GetNextFile(pFile);
	}
	freeBlocks += fTotalBlocks - prevNextBlock;

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

	return kDIErrNone;
}


/*
 * Normalize a Pascal path.  Used when adding files from DiskArchive.
 *
 * "*pNormalizedBufLen" is used to pass in the length of the buffer and
 * pass out the length of the string (should the buffer prove inadequate).
 */
DIError
DiskFSPascal::NormalizePath(const char* path, char fssep,
	char* normalizedBuf, int* pNormalizedBufLen)
{
	DIError dierr = kDIErrNone;
	char tmpBuf[A2FilePascal::kMaxFileName+1];
	int len;

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

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

	return dierr;
}

/*
 * Normalize a Pascal pathname.  Lower case becomes upper case, invalid
 * characters get stripped.
 *
 * "outBuf" must be able to hold kMaxFileName+1 characters.
 */
void
DiskFSPascal::DoNormalizePath(const char* name, char fssep, char* outBuf)
{
	char* outp = outBuf;
	char* cp;

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

	while (*name != '\0' && (outp - outBuf) < A2FilePascal::kMaxFileName) {
		if (*name > 0x20 && *name < 0x7f &&
			strchr(kInvalidNameChars, *name) == nil)
		{
			*outp++ = toupper(*name);
		}

		name++;
	}

	*outp = '\0';

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


/*
 * Create an empty file.  It doesn't look like pascal normally allows you
 * to create a zero-block file, so we create a 1-block file and set the
 * "data in last block" field to zero.
 *
 * We don't know how big the file will be, so we can't do a "best fit"
 * algorithm for placement.  Instead, we just put it in the largest
 * available free space.
 *
 * NOTE: the Pascal system will auto-delete zero-byte files.  It expects a
 * brand-new 1-block file to have a "bytes remaining" of 512.  The files
 * we create here are expected to be written to, not used as filler, so
 * this behavior is actually a *good* thing.
 */
DIError
DiskFSPascal::CreateFile(const CreateParms* pParms, A2File** ppNewFile)
{
	DIError dierr = kDIErrNone;
	const bool createUnique = (GetParameter(kParm_CreateUnique) != 0);
	char normalName[A2FilePascal::kMaxFileName+1];
	A2FilePascal* pNewFile = nil;

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

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

	/* compute maxFiles, which includes the vol dir header */
	int maxFiles =
		((fNextBlock - kVolHeaderBlock) * kBlkSize) / kDirectoryEntryLen;
	if (fNumFiles >= maxFiles-1) {
		WMSG1("Pascal volume directory full (%d entries)\n", fNumFiles);
		return kDIErrVolumeDirFull;
	}

	*ppNewFile = nil;

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

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

	/*
	 * Find the largest gap in the file space.
	 *
	 * We get an index pointer and A2File pointer to the previous entry.  If
	 * the blank space is at the head of the list, prevIdx will be zero and
	 * pPrevFile will be nil.
	 */
	A2FilePascal* pPrevFile;
	int prevIdx;

	dierr = FindLargestFreeArea(&prevIdx, &pPrevFile);
	if (dierr != kDIErrNone)
		goto bail;
	assert(prevIdx >= 0);

	/*
	 * Make a new entry.
	 */
	time_t now;
	pNewFile = new A2FilePascal(this);
	if (pNewFile == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}
	if (pPrevFile == nil)
		pNewFile->fStartBlock = fNextBlock;
	else
		pNewFile->fStartBlock = pPrevFile->fNextBlock;
	pNewFile->fNextBlock = pNewFile->fStartBlock +1;	// alloc 1 block
	pNewFile->fFileType = A2FilePascal::ConvertFileType(pParms->fileType);
	memset(pNewFile->fFileName, 0, A2FilePascal::kMaxFileName);
	strcpy(pNewFile->fFileName, normalName);
	pNewFile->fBytesRemaining = 0;
	now = time(nil);
	pNewFile->fModWhen = A2FilePascal::ConvertPascalDate(now);

	pNewFile->fLength = 0;

	/*
	 * Make a hole.
	 */
	dierr = LoadCatalog();
	if (dierr != kDIErrNone)
		goto bail;

	if (fNumFiles > prevIdx) {
		WMSG1("  Pascal sliding last %d entries down a slot\n",
			fNumFiles - prevIdx);
		memmove(fDirectory + (prevIdx+2) * kDirectoryEntryLen,
				fDirectory + (prevIdx+1) * kDirectoryEntryLen,
				(fNumFiles - prevIdx) * kDirectoryEntryLen);
	}

	/*
	 * Fill the hole.
	 */
	unsigned char* dirPtr;
	dirPtr = fDirectory + (prevIdx+1) * kDirectoryEntryLen;
	PutShortLE(&dirPtr[0x00], pNewFile->fStartBlock);
	PutShortLE(&dirPtr[0x02], pNewFile->fNextBlock);
	PutShortLE(&dirPtr[0x04], (unsigned short) pNewFile->fFileType);
	dirPtr[0x06] = (unsigned char) strlen(pNewFile->fFileName);
	memcpy(&dirPtr[0x07], pNewFile->fFileName, A2FilePascal::kMaxFileName);
	PutShortLE(&dirPtr[0x16], pNewFile->fBytesRemaining);
	PutShortLE(&dirPtr[0x18], pNewFile->fModWhen);

	/*
	 * Update the #of files.
	 */
	fNumFiles++;
	PutShortLE(&fDirectory[0x10], fNumFiles);

	/*
	 * Flush.
	 */
	dierr = SaveCatalog();
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Add to the linear file list.
	 */
	InsertFileInList(pNewFile, pPrevFile);

	*ppNewFile = pNewFile;
	pNewFile = nil;

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

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

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

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

	/*
	 * If this has what looks like a filename extension, grab it.  We want
	 * to preserve ".gif", ".c", etc.
	 */
	const char* cp = strrchr(fileName, '.');
	if (cp != nil) {
		int tmpOffset = cp - fileName;
		if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) {
			WMSG1("  Pascal   (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 > A2FilePascal::kMaxFileName)
			copyOffset = A2FilePascal::kMaxFileName - dotLen - digitLen;
		else
			copyOffset = nameLen - dotLen;
		memcpy(fileName + copyOffset, digitBuf, digitLen);
		if (dotLen != 0)
			memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen);
	} while (GetFileByName(fileName) != nil);

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

	return kDIErrNone;
}

/*
 * Find the largest chunk of free space on the disk.
 *
 * Returns the index to the directory entry of the file immediately before
 * the chunk (where 0 is the directory header), and the corresponding
 * A2File entry.
 *
 * If there's no free space left, returns kDIErrDiskFull.
 */
DIError
DiskFSPascal::FindLargestFreeArea(int *pPrevIdx, A2FilePascal** ppPrevFile)
{
	A2FilePascal* pFile;
	A2FilePascal* pPrevFile;
	unsigned short prevNextBlock = fNextBlock;
	int gapSize, maxGap, maxIndex, idx;

	maxIndex = -1;
	maxGap = 0;
	idx = 0;
	*ppPrevFile = pPrevFile = nil;

	pFile = (A2FilePascal*) GetNextFile(nil);
	while (pFile != nil) {
		gapSize = pFile->fStartBlock - prevNextBlock;
		if (gapSize > maxGap) {
			maxGap = gapSize;
			maxIndex = idx;
			*ppPrevFile = pPrevFile;
		}

		idx++;
		prevNextBlock = pFile->fNextBlock;
		pPrevFile = pFile;
		pFile = (A2FilePascal*) GetNextFile(pFile);
	}

	gapSize = fTotalBlocks - prevNextBlock;
	if (gapSize > maxGap) {
		maxGap = gapSize;
		maxIndex = idx;
		*ppPrevFile = pPrevFile;
	}

	WMSG3("Pascal largest gap after entry %d '%s' (size=%d)\n",
		maxIndex,
		*ppPrevFile != nil ? (*ppPrevFile)->GetPathName() : "(root)",
		maxGap);
	*pPrevIdx = maxIndex;

	if (maxIndex < 0)
		return kDIErrDiskFull;
	return kDIErrNone;
}


/*
 * Delete a file.  Because Pascal doesn't have a block allocation map, this
 * is a simple matter of crunching the directory entry out.
 */
DIError
DiskFSPascal::DeleteFile(A2File* pGenericFile)
{
	DIError dierr = kDIErrNone;
	A2FilePascal* pFile = (A2FilePascal*) pGenericFile;
	unsigned char* pEntry;
	int dirLen, offsetToNextEntry;

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

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

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

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

	pEntry = FindDirEntry(pFile);
	if (pEntry == nil) {
		assert(false);
		dierr = kDIErrInternal;
		goto bail;
	}
	dirLen = (fNumFiles+1) * kDirectoryEntryLen;
	offsetToNextEntry = (pEntry - fDirectory) + kDirectoryEntryLen;
	if (dirLen == offsetToNextEntry) {
		WMSG0("+++ removing last entry\n");
	} else {
		memmove(pEntry, pEntry+kDirectoryEntryLen, dirLen - offsetToNextEntry);
	}

	assert(fNumFiles > 0);
	fNumFiles--;
	PutShortLE(&fDirectory[0x10], fNumFiles);

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

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

bail:
	FreeCatalog();
	return dierr;
}

/*
 * Rename a file.
 */
DIError
DiskFSPascal::RenameFile(A2File* pGenericFile, const char* newName)
{
	DIError dierr = kDIErrNone;
	A2FilePascal* pFile = (A2FilePascal*) pGenericFile;
	char normalName[A2FilePascal::kMaxFileName+1];
	unsigned char* pEntry;

	if (pFile == nil || newName == nil)
		return kDIErrInvalidArg;
	if (!IsValidFileName(newName))
		return kDIErrInvalidArg;
	if (fpImg->GetReadOnly())
		return kDIErrAccessDenied;
	if (!fDiskIsGood)
		return kDIErrBadDiskImage;
	/* not strictly necessary, but watch sanity check in Close/FindDirEntry */
	if (pGenericFile->IsFileOpen())
		return kDIErrFileOpen;

	DoNormalizePath(newName, '\0', normalName);

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

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

	pEntry = FindDirEntry(pFile);
	if (pEntry == nil) {
		assert(false);
		dierr = kDIErrInternal;
		goto bail;
	}

	pEntry[0x06] = strlen(normalName);
	memcpy(&pEntry[0x07], normalName, A2FilePascal::kMaxFileName);
	strcpy(pFile->fFileName, normalName);

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

bail:
	FreeCatalog();
	return dierr;
}

/*
 * Set file info.
 *
 * Pascal does not have an aux type or access flags.  It has a file type,
 * but we don't allow the full range of ProDOS types.  Attempting to change
 * to an unsupported type results in "PDA" being used.
 */
DIError
DiskFSPascal::SetFileInfo(A2File* pGenericFile, long fileType, long auxType,
	long accessFlags)
{
	DIError dierr = kDIErrNone;
	A2FilePascal* pFile = (A2FilePascal*) pGenericFile;
	unsigned char* pEntry;

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

	WMSG2("Pascal SetFileInfo '%s' fileType=0x%04lx\n",
		pFile->GetPathName(), fileType);

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

	pEntry = FindDirEntry(pFile);
	if (pEntry == nil) {
		assert(false);
		dierr = kDIErrInternal;
		goto bail;
	}

	A2FilePascal::FileType newType;

	newType = A2FilePascal::ConvertFileType(fileType);
	PutShortLE(&pEntry[0x04], (unsigned short) newType);

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

	/* update our local copy */
	pFile->fFileType = newType;

bail:
	FreeCatalog();
	return dierr;
}

/*
 * Change the Pascal volume name.
 */
DIError
DiskFSPascal::RenameVolume(const char* newName)
{
	DIError dierr = kDIErrNone;
	char normalName[A2FilePascal::kMaxFileName+1];

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

	DoNormalizePath(newName, '\0', normalName);

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

	fDirectory[0x06] = strlen(normalName);
	memcpy(&fDirectory[0x07], normalName, fDirectory[0x06]);
	strcpy(fVolumeName, normalName);

	SetVolumeID();

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

bail:
	FreeCatalog();
	return dierr;
}


/*
 * Find "pFile" in "fDirectory".
 */
unsigned char*
DiskFSPascal::FindDirEntry(A2FilePascal* pFile)
{
	unsigned char* ptr;
	int i;

	assert(fDirectory != nil);

	ptr = fDirectory;		// volume header; first iteration skips over it
	for (i = 0; i < fNumFiles; i++) {
		ptr += kDirectoryEntryLen;

		if (GetShortLE(&ptr[0x00]) == pFile->fStartBlock) {
			if (memcmp(&ptr[0x07], pFile->fFileName, ptr[0x06]) != 0) {
				assert(false);
				WMSG2("name/block mismatch on '%s' %d\n",
					pFile->GetPathName(), pFile->fStartBlock);
				return nil;
			}
			return ptr;
		}
	}

	return nil;
}


/*
 * ===========================================================================
 *		A2FilePascal
 * ===========================================================================
 */

/*
 * Convert Pascal file type to ProDOS file type.
 */
long
A2FilePascal::GetFileType(void) const
{
	switch (fFileType) {
	case kTypeUntyped:		return 0x00;		// NON
	case kTypeXdsk:			return 0x01;		// BAD (was 0xf2 in v1.2.2)
	case kTypeCode:			return 0x02;		// PCD
	case kTypeText:			return 0x03;		// PTX
	case kTypeInfo:			return 0xf3;		// no idea
	case kTypeData:			return 0x05;		// PDA
	case kTypeGraf:			return 0xf4;		// no idea
	case kTypeFoto:			return 0x08;		// FOT
	case kTypeSecurdir:		return 0xf5;		// no idea
	default:
		WMSG1("Pascal WARNING: found invalid file type %d\n", fFileType);
		return 0;
	}
}

/*
 * Convert a ProDOS file type to a Pascal file type.
 */
/*static*/ A2FilePascal::FileType
A2FilePascal::ConvertFileType(long prodosType)
{
	FileType newType;

	switch (prodosType) {
	case 0x00:		newType = kTypeUntyped;		break;	// NON
	case 0x01:		newType = kTypeXdsk;		break;	// BAD
	case 0x02:		newType = kTypeCode;		break;	// PCD
	case 0x03:		newType = kTypeText;		break;	// PTX
	case 0xf3:		newType = kTypeInfo;		break;	// ?
	case 0x05:		newType = kTypeData;		break;	// PDA
	case 0xf4:		newType = kTypeGraf;		break;	// ?
	case 0x08:		newType = kTypeFoto;		break;	// FOT
	case 0xf5:		newType = kTypeSecurdir;	break;	// ?
	default:		newType = kTypeData;		break;	// PDA for generic
	}

	return newType;
}


/*
 * Convert from Pascal compact date format to a time_t.
 *
 *	Format yyyyyyydddddmmmm
 *	Month 0..12 (0 indicates invalid date)
 *	Day   0..31
 *	Year  0..100 (1900-1999; 100 will be rejected)
 *
 * We follow the ProDOS protocol of "year < 40 == 1900 + year".  We could
 * probably make that 1970, but the time_t epoch ends before then.
 *
 * The Pascal Filer uses a special date with the year 100 in it to indicate
 * file updates in progress.  If the system comes up and sees a file with
 * the year 100, it will assume that the file was created shortly before the
 * system crashed, and will remove the file.
 */
/*static*/ time_t
A2FilePascal::ConvertPascalDate(PascalDate pascalDate)
{
	int year, month, day;

	month = pascalDate & 0x0f;
	if (!month)
		return 0;
	day = (pascalDate >> 4) & 0x1f;
	year = (pascalDate >> 9) & 0x7f;
	if (year == 100) {
		// ought to mark the file as "suspicious"?
		WMSG0("Pascal WARNING: date with year=100\n");
	}
	if (year < 40)
		year += 100;

    struct tm tmbuf;
    time_t when;

    tmbuf.tm_sec = 0;
    tmbuf.tm_min = 0;
    tmbuf.tm_hour = 0;
    tmbuf.tm_mday = day;
    tmbuf.tm_mon = month-1;
    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 Pascal-format date.
 *
 * CiderPress uses kDateInvalid==-1 and kDateNone==-2.
 */
/*static*/ A2FilePascal::PascalDate
A2FilePascal::ConvertPascalDate(time_t unixDate)
{
	unsigned long date, year;
	struct tm* ptm;

	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;	// years since 1900
	if (year >= 100)
		year -= 100;
	if (year < 0 || year >= 100) {
		WMSG2("WHOOPS: got year %lu from %d\n", year, ptm->tm_year);
		year = 70;
	}
	date = year << 9 | (ptm->tm_mon+1) | ptm->tm_mday << 4;
	return (PascalDate) date;
}


/*
 * Return the file modification date.
 */
time_t
A2FilePascal::GetModWhen(void) const
{
	return ConvertPascalDate(fModWhen);
}

/*
 * Dump the contents of the A2File structure.
 */
void
A2FilePascal::Dump(void) const
{
	WMSG1("A2FilePascal '%s'\n", fFileName);
	WMSG3("  start=%d next=%d type=%d\n",
		fStartBlock, fNextBlock, fFileType);
	WMSG2("  bytesRem=%d modWhen=0x%04x\n",
		fBytesRemaining, fModWhen);
}

/*
 * Not a whole lot to do, since there's no fancy index blocks.
 */
DIError
A2FilePascal::Open(A2FileDescr** ppOpenFile, bool readOnly,
	bool rsrcFork /*=false*/)
{
	A2FDPascal* pOpenFile = nil;

	if (!readOnly) {
		if (fpDiskFS->GetDiskImg()->GetReadOnly())
			return kDIErrAccessDenied;
		if (fpDiskFS->GetFSDamaged())
			return kDIErrBadDiskImage;
	}
	if (fpOpenFile != nil)
		return kDIErrAlreadyOpen;
	if (rsrcFork)
		return kDIErrForkNotFound;

	pOpenFile = new A2FDPascal(this);

	pOpenFile->fOffset = 0;
	pOpenFile->fOpenEOF = fLength;
	pOpenFile->fOpenBlocksUsed = fNextBlock - fStartBlock;
	pOpenFile->fModified = false;

	fpOpenFile = pOpenFile;
	*ppOpenFile = pOpenFile;

	return kDIErrNone;
}


/*
 * ===========================================================================
 *		A2FDPascal
 * ===========================================================================
 */

/*
 * Read a chunk of data from the current offset.
 */
DIError
A2FDPascal::Read(void* buf, size_t len, size_t* pActual)
{
	WMSG3(" Pascal reading %d bytes from '%s' (offset=%ld)\n",
		len, fpFile->GetPathName(), (long) fOffset);

	A2FilePascal* pFile = (A2FilePascal*) fpFile;

	/* don't allow them to read past the end of the file */
	if (fOffset + (long)len > fOpenEOF) {
		if (pActual == nil)
			return kDIErrDataUnderrun;
		len = (size_t) (fOpenEOF - fOffset);
	}
	if (pActual != nil)
		*pActual = len;
	long incrLen = len;

	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	long block = pFile->fStartBlock + (long) (fOffset / kBlkSize);
	int bufOffset = (long) (fOffset % kBlkSize);		// (& 0x01ff)
	size_t thisCount;

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

	while (len) {
		assert(block >= pFile->fStartBlock && block < pFile->fNextBlock);

		dierr = pFile->GetDiskFS()->GetDiskImg()->ReadBlock(block, blkBuf);
		if (dierr != kDIErrNone) {
			WMSG1(" Pascal error reading file '%s'\n", pFile->fFileName);
			return dierr;
		}
		thisCount = kBlkSize - bufOffset;
		if (thisCount > len)
			thisCount = len;

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

		bufOffset = 0;
		block++;
	}

	fOffset += incrLen;

	return dierr;
}

/*
 * Write data at the current offset.
 *
 * We make the customary assumptions here: we're writing to a brand-new file,
 * and writing all data in one shot.  On a Pascal disk, that makes this
 * process almost embarrassingly simple.
 */
DIError
A2FDPascal::Write(const void* buf, size_t len, size_t* pActual)
{
	DIError dierr = kDIErrNone;
	A2FilePascal* pFile = (A2FilePascal*) fpFile;
	DiskFSPascal* pDiskFS = (DiskFSPascal*) fpFile->GetDiskFS();
	unsigned char blkBuf[kBlkSize];
	size_t origLen = len;

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

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

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

	/*
	 * Verify that there's enough room between this file and the next to
	 * hold the contents of the file.
	 */
	long blocksNeeded, blocksAvail;
	A2FilePascal* pNextFile;
	pNextFile = (A2FilePascal*) pDiskFS->GetNextFile(pFile);
	if (pNextFile == nil)
		blocksAvail = pDiskFS->GetTotalBlocks() - pFile->fStartBlock;
	else
		blocksAvail = pNextFile->fStartBlock - pFile->fStartBlock;

	blocksNeeded = (len + kBlkSize -1) / kBlkSize;
	WMSG4("Pascal write '%s' %d bytes: avail=%ld needed=%ld\n",
		pFile->GetPathName(), len, blocksAvail, blocksNeeded);
	if (blocksAvail < blocksNeeded)
		return kDIErrDiskFull;

	/*
	 * Write the data.
	 */
	long block;
	block = pFile->fStartBlock;
	while (len != 0) {
		if (len >= (size_t) kBlkSize) {
			/* full block write */
			dierr = pDiskFS->GetDiskImg()->WriteBlock(block, buf);
			if (dierr != kDIErrNone)
				goto bail;

			len -= kBlkSize;
			buf = (unsigned char*) buf + kBlkSize;
		} else {
			/* partial block write */
			memset(blkBuf, 0, sizeof(blkBuf));
			memcpy(blkBuf, buf, len);
			dierr = pDiskFS->GetDiskImg()->WriteBlock(block, blkBuf);
			if (dierr != kDIErrNone)
				goto bail;

			len = 0;
		}

		block++;
	}

	/*
	 * Update FD state.
	 */
	fOpenBlocksUsed = blocksNeeded;
	fOpenEOF = origLen;
	fOffset = origLen;
	fModified = true;

bail:
	return dierr;
}

/*
 * Seek to a new offset.
 */
DIError
A2FDPascal::Seek(di_off_t offset, DIWhence whence)
{
	//di_off_t fileLen = ((A2FilePascal*) fpFile)->fLength;

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

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

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

/*
 * Release file state, and tell our parent to destroy us.
 *
 * 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
A2FDPascal::Close(void)
{
	DIError dierr = kDIErrNone;
	DiskFSPascal* pDiskFS = (DiskFSPascal*) fpFile->GetDiskFS();

	if (fModified) {
		A2FilePascal* pFile = (A2FilePascal*) fpFile;
		unsigned char* pEntry;

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

		/*
		 * Update our internal copies of stuff.
		 */
		pFile->fLength = fOpenEOF;
		pFile->fNextBlock = pFile->fStartBlock + (unsigned short) fOpenBlocksUsed;
		pFile->fModWhen = A2FilePascal::ConvertPascalDate(time(nil));

		/*
		 * Update the "next block" value and the length-in-last-block.  We
		 * have to scan through the directory to find our entry, rather
		 * than remember an offset at "open" time, on the off chance that
		 * somebody created or deleted a file after we were opened.
		 */
		pEntry = pDiskFS->FindDirEntry(pFile);
		if (pEntry == nil) {
			// we deleted an open file?
			assert(false);
			dierr = kDIErrInternal;
			goto bail;
		}
		unsigned short bytesInLastBlock;
		bytesInLastBlock = (unsigned short)pFile->fLength % kBlkSize;
		if (bytesInLastBlock == 0)
			bytesInLastBlock = 512;		// exactly filled out last block

		PutShortLE(&pEntry[0x02], pFile->fNextBlock);
		PutShortLE(&pEntry[0x16], bytesInLastBlock);
		PutShortLE(&pEntry[0x18], pFile->fModWhen);

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

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

/*
 * Return the #of sectors/blocks in the file.
 */
long
A2FDPascal::GetSectorCount(void) const
{
	A2FilePascal* pFile = (A2FilePascal*) fpFile;
	return (pFile->fNextBlock - pFile->fStartBlock) * 2;
}
long
A2FDPascal::GetBlockCount(void) const
{
	A2FilePascal* pFile = (A2FilePascal*) fpFile;
	return pFile->fNextBlock - pFile->fStartBlock;
}

/*
 * Return the Nth track/sector in this file.
 */
DIError
A2FDPascal::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
{
	A2FilePascal* pFile = (A2FilePascal*) fpFile;
	long pascalIdx = sectorIdx / 2;
	long pascalBlock = pFile->fStartBlock + pascalIdx;
	if (pascalBlock >= pFile->fNextBlock)
		return kDIErrInvalidIndex;

	/* sparse blocks not possible on Pascal volumes */
	BlockToTrackSector(pascalBlock, (sectorIdx & 0x01) != 0, pTrack, pSector);
	return kDIErrNone;
}
/*
 * Return the Nth 512-byte block in this file.
 */
DIError
A2FDPascal::GetStorage(long blockIdx, long* pBlock) const
{
	A2FilePascal* pFile = (A2FilePascal*) fpFile;
	long pascalBlock = pFile->fStartBlock + blockIdx;
	if (pascalBlock >= pFile->fNextBlock)
		return kDIErrInvalidIndex;

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