ciderpress/diskimg/CPM.cpp
Andy McFadden 732cd85ce5 CP/M: added support for Microsoft Softcard "data only" disks.
CP/M: correctly identify 3.x disks
Updated some copyright notices.
2009-01-03 19:02:13 +00:00

785 lines
20 KiB
C++

/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Apple II CP/M disk format.
*
* Limitations:
* - Read-only.
* - Does not do much with user numbers.
* - Rumor has it that "sparse" files are possible. Not handled.
* - I'm currently treating the directory as fixed-length. This may
* not be correct.
* - Not handling special entries (volume label, date stamps,
* password control).
*
* As I have no practical experience with CP/M, this is the weakest of the
* filesystem implementations.
*/
#include "StdAfx.h"
#include "DiskImgPriv.h"
/*
* ===========================================================================
* DiskFSCPM
* ===========================================================================
*/
const int kBlkSize = 512; // really ought to be 1024
const int kVolDirBlock = 24; // track 3 sector 0
const int kVolDirCount = 4; // 4 prodos blocks
const int kNoDataByte = 0xe5;
const int kMaxUserNumber = 31; // 0-15 on some systems, 0-31 on others
const int kMaxSpecialUserNumber = 0x21; // 0x20 and 0x21 have special meanings
const int kMaxExtent = 31; // extent counter, 0-31
/*
* See if this looks like a CP/M 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 dirBuf[kBlkSize * kVolDirCount];
unsigned char* dptr;
int i;
assert(sizeof(dirBuf) == DiskFSCPM::kFullDirSize);
for (i = 0; i < kVolDirCount; i++) {
dierr = pImg->ReadBlockSwapped(kVolDirBlock + i, dirBuf + kBlkSize*i,
imageOrder, DiskImg::kSectorOrderCPM);
if (dierr != kDIErrNone)
goto bail;
}
dptr = dirBuf;
for (i = 0; i < DiskFSCPM::kFullDirSize/DiskFSCPM::kDirectoryEntryLen; i++)
{
if (*dptr != kNoDataByte) {
/*
* Usually userNumber is 0, but sometimes not. It's expected to
* be < 0x20 for a normal file, may be 0x21 or 0x22 for special
* entries (volume label, date stamps).
*/
if (*dptr > kMaxSpecialUserNumber) {
dierr = kDIErrFilesystemNotFound;
break;
}
/* extent counter, 0-31 */
if (dptr[12] > kMaxExtent) {
dierr = kDIErrFilesystemNotFound;
break;
}
/* check for a valid filename here; high bit may be set on some bytes */
unsigned char firstLet = *(dptr+1) & 0x7f;
if (firstLet < 0x20) {
dierr = kDIErrFilesystemNotFound;
break;
}
}
dptr += DiskFSCPM::kDirectoryEntryLen;
}
if (dierr == kDIErrNone) {
WMSG1(" CPM found clean directory, imageOrder=%d\n", imageOrder);
}
bail:
return dierr;
}
/*
* Test to see if the image is a CP/M disk.
*
* On the Apple II, these were always on 5.25" disks. However, it's possible
* to create hard drive volumes up to 8MB.
*/
/*static*/ DIError
DiskFSCPM::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder,
DiskImg::FSFormat* pFormat, FSLeniency leniency)
{
/* CP/M disks use 1K blocks, so ignore anything with odd count */
if (pImg->GetNumBlocks() == 0 ||
(pImg->GetNumBlocks() & 0x01) != 0)
{
WMSG1(" CPM rejecting image with numBlocks=%ld\n",
pImg->GetNumBlocks());
return kDIErrFilesystemNotFound;
}
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::kFormatCPM;
return kDIErrNone;
}
}
WMSG0(" CPM 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
DiskFSCPM::Initialize(void)
{
DIError dierr = kDIErrNone;
dierr = ReadCatalog();
if (dierr != kDIErrNone)
goto bail;
fVolumeUsage.Create(fpImg->GetNumBlocks());
dierr = ScanFileUsage();
if (dierr != kDIErrNone) {
/* this might not be fatal; just means that *some* files are bad */
dierr = kDIErrNone;
goto bail;
}
fDiskIsGood = CheckDiskIsGood();
fVolumeUsage.Dump();
//A2File* pFile;
//pFile = GetNextFile(nil);
//while (pFile != nil) {
// pFile->Dump();
// pFile = GetNextFile(pFile);
//}
bail:
return dierr;
}
/*
* Read the entire CP/M catalog (all 2K of it) into memory, and parse
* out the individual files.
*
* A single file can have more than one directory entry. We only want
* to create an A2File object for the first one.
*/
DIError
DiskFSCPM::ReadCatalog(void)
{
DIError dierr = kDIErrNone;
unsigned char dirBuf[kFullDirSize];
unsigned char* dptr;
int i;
for (i = 0; i < kVolDirCount; i++) {
dierr = fpImg->ReadBlock(kVolDirBlock + i, dirBuf + kBlkSize*i);
if (dierr != kDIErrNone)
goto bail;
}
dptr = dirBuf;
for (i = 0; i < kNumDirEntries; i++) {
fDirEntry[i].userNumber = dptr[0x00];
/* copy the filename, stripping the high bits off */
for (int j = 0; j < kDirFileNameLen; j++)
fDirEntry[i].fileName[j] = dptr[0x01 + j] & 0x7f;
fDirEntry[i].fileName[kDirFileNameLen] = '\0';
fDirEntry[i].extent = dptr[0x0c] + dptr[0x0e] * kExtentsInLowByte;
fDirEntry[i].S1 = dptr[0x0d];
fDirEntry[i].records = dptr[0x0f];
memcpy(fDirEntry[i].blocks, &dptr[0x10], kDirEntryBlockCount);
fDirEntry[i].readOnly = (dptr[0x09] & 0x80) != 0;
fDirEntry[i].system = (dptr[0x0a] & 0x80) != 0;
fDirEntry[i].badBlockList = false; // set if block list is bad
dptr += kDirectoryEntryLen;
}
/* create an entry for the first extent of each file */
for (i = 0; i < kNumDirEntries; i++) {
A2FileCPM* pFile;
if (fDirEntry[i].userNumber == kNoDataByte || fDirEntry[i].extent != 0)
continue;
if (fDirEntry[i].userNumber > kMaxUserNumber) {
/* skip over volume label, date stamps, etc */
WMSG1("Skipping entry with userNumber=0x%02x\n",
fDirEntry[i].userNumber);
}
pFile = new A2FileCPM(this, fDirEntry);
FormatName(pFile->fFileName, (char*)fDirEntry[i].fileName);
pFile->fReadOnly = fDirEntry[i].readOnly;
pFile->fDirIdx = i;
pFile->fLength = 0;
dierr = ComputeLength(pFile);
if (dierr != kDIErrNone) {
pFile->SetQuality(A2File::kQualityDamaged);
dierr = kDIErrNone;
}
AddFileToList(pFile);
}
/*
* Validate the list of blocks.
*/
int maxCpmBlock;
maxCpmBlock = (fpImg->GetNumBlocks() - kVolDirBlock) / 2;
for (i = 0; i < kNumDirEntries; i++) {
if (fDirEntry[i].userNumber == kNoDataByte)
continue;
for (int j = 0; j < kDirEntryBlockCount; j++) {
if (fDirEntry[i].blocks[j] >= maxCpmBlock) {
WMSG2(" CPM invalid block %d in file '%s'\n",
fDirEntry[i].blocks[j], fDirEntry[i].fileName);
//pFile->SetQuality(A2File::kQualityDamaged);
fDirEntry[i].badBlockList = true;
break;
}
}
}
bail:
return dierr;
}
/*
* Reformat from 11 chars with spaces into clean xxxxx.yyy format.
*/
void
DiskFSCPM::FormatName(char* dstBuf, const char* srcBuf)
{
char workBuf[kDirFileNameLen+1];
char* cp;
assert(strlen(srcBuf) < sizeof(workBuf));
strcpy(workBuf, srcBuf);
cp = workBuf;
while (*cp != '\0') {
//*cp &= 0x7f; // [no longer necessary]
if (*cp == ' ')
*cp = '\0';
if (*cp == ':') // don't think this is allowed, but check
*cp = 'X'; // for it anyway
cp++;
}
strcpy(dstBuf, workBuf);
dstBuf[8] = '\0'; // in case filename part is full 8 chars
strcat(dstBuf, ".");
strcat(dstBuf, workBuf+8);
assert(strlen(dstBuf) <= A2FileCPM::kMaxFileName);
}
/*
* Compute the length of a file. Sets "pFile->fLength".
*
* This requires walking through the list of extents and looking for the
* last one. We use the "records" field of the last extent to determine
* the file length.
*
* (Should probably just get the block list and then walk that, rather than
* having directory parse code in two places.)
*/
DIError
DiskFSCPM::ComputeLength(A2FileCPM* pFile)
{
int i;
int best, maxExtent;
best = maxExtent = -1;
for (i = 0; i < DiskFSCPM::kNumDirEntries; i++) {
if (fDirEntry[i].userNumber == kNoDataByte)
continue;
if (strcmp((const char*)fDirEntry[i].fileName,
(const char*)fDirEntry[pFile->fDirIdx].fileName) == 0 &&
fDirEntry[i].userNumber == fDirEntry[pFile->fDirIdx].userNumber)
{
/* this entry is part of the file */
if (fDirEntry[i].extent > maxExtent) {
best = i;
maxExtent = fDirEntry[i].extent;
}
}
}
if (maxExtent < 0 || best < 0) {
WMSG1(" CPM couldn't find existing file '%s'!\n", pFile->fFileName);
assert(false);
return kDIErrInternal;
}
pFile->fLength = kDirEntryBlockCount * 1024 * maxExtent +
fDirEntry[best].records * 128;
return kDIErrNone;
}
/*
* Scan file usage into the volume usage map.
*
* Tracks 0, 1, and 2 are always used by the boot loader. The volume directory
* is on the first half of track 3 (blocks 0 and 1).
*/
DIError
DiskFSCPM::ScanFileUsage(void)
{
int cpmBlock;
int i, j;
for (i = 0; i < kVolDirBlock; i++)
SetBlockUsage(i, VolumeUsage::kChunkPurposeSystem);
for (i = kVolDirBlock; i < kVolDirBlock + kVolDirCount; i++)
SetBlockUsage(i, VolumeUsage::kChunkPurposeVolumeDir);
for (i = 0; i < kNumDirEntries; i++) {
if (fDirEntry[i].userNumber == kNoDataByte)
continue;
if (fDirEntry[i].badBlockList)
continue;
for (j = 0; j < kDirEntryBlockCount; j++) {
cpmBlock = fDirEntry[i].blocks[j];
if (cpmBlock == 0)
break;
SetBlockUsage(CPMToProDOSBlock(cpmBlock),
VolumeUsage::kChunkPurposeUserData);
SetBlockUsage(CPMToProDOSBlock(cpmBlock)+1,
VolumeUsage::kChunkPurposeUserData);
}
}
return kDIErrNone;
}
/*
* Update an entry in the usage map.
*
* "block" is a 512-byte block, so you will have to call here twice for every
* 1K CP/M block.
*/
void
DiskFSCPM::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose)
{
VolumeUsage::ChunkState cstate;
if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) {
WMSG1(" CPM ERROR: unable to set state on block %ld\n", block);
return;
}
if (cstate.isUsed) {
cstate.purpose = VolumeUsage::kChunkPurposeConflict;
WMSG1(" CPM conflicting uses for block=%ld\n", block);
} else {
cstate.isUsed = true;
cstate.isMarkedUsed = true; // no volume bitmap
cstate.purpose = purpose;
}
fVolumeUsage.SetChunkState(block, &cstate);
}
/*
* 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
DiskFSCPM::CheckDiskIsGood(void)
{
//DIError dierr;
bool result = true;
//if (fEarlyDamage)
// result = false;
/*
* TO DO: look for multiple files occupying the same blocks.
*/
/*
* 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;
}
/*
* ===========================================================================
* A2FileCPM
* ===========================================================================
*/
/*
* Not a whole lot to do, since there's no fancy index blocks.
*
* Calling GetBlockList twice is probably not the best way to go through life.
* This needs an overhaul.
*/
DIError
A2FileCPM::Open(A2FileDescr** ppOpenFile, bool readOnly,
bool rsrcFork /*=false*/)
{
DIError dierr;
A2FDCPM* pOpenFile = nil;
if (fpOpenFile != nil)
return kDIErrAlreadyOpen;
if (rsrcFork)
return kDIErrForkNotFound;
assert(readOnly);
pOpenFile = new A2FDCPM(this);
dierr = GetBlockList(&pOpenFile->fBlockCount, nil);
if (dierr != kDIErrNone)
goto bail;
pOpenFile->fBlockList = new unsigned char[pOpenFile->fBlockCount+1];
pOpenFile->fBlockList[pOpenFile->fBlockCount] = 0xff;
dierr = GetBlockList(&pOpenFile->fBlockCount, pOpenFile->fBlockList);
if (dierr != kDIErrNone)
goto bail;
assert(pOpenFile->fBlockList[pOpenFile->fBlockCount] == 0xff);
pOpenFile->fOffset = 0;
//fOpen = true;
fpOpenFile = pOpenFile;
*ppOpenFile = pOpenFile;
pOpenFile = nil;
bail:
delete pOpenFile;
return dierr;
}
/*
* Get the complete block list for a file. This will involve reading
* one or more directory entries.
*
* Call this once with "blockBuf" equal to "nil" to get the block count,
* then call a second time after allocating blockBuf.
*/
DIError
A2FileCPM::GetBlockList(long* pBlockCount, unsigned char* blockBuf) const
{
di_off_t length = fLength;
int blockCount = 0;
int i, j;
/*
* Run through the entries, pulling blocks out until we account for the
* entire length of the file.
*
* [Should probably pay more attention to extent numbers, making sure
* that they make sense. Not vital until we allow writes.]
*/
for (i = 0; i < DiskFSCPM::kNumDirEntries; i++) {
if (length <= 0)
break;
if (fpDirEntry[i].userNumber == kNoDataByte)
continue;
if (strcmp((const char*)fpDirEntry[i].fileName,
(const char*)fpDirEntry[fDirIdx].fileName) == 0 &&
fpDirEntry[i].userNumber == fpDirEntry[fDirIdx].userNumber)
{
/* this entry is part of the file */
for (j = 0; j < DiskFSCPM::kDirEntryBlockCount; j++) {
if (fpDirEntry[i].blocks[j] == 0) {
WMSG2(" CPM found sparse block %d/%d\n", i, j);
}
blockCount++;
if (blockBuf != nil) {
long listOffset = j +
fpDirEntry[i].extent * DiskFSCPM::kDirEntryBlockCount;
blockBuf[listOffset] = fpDirEntry[i].blocks[j];
}
length -= 1024;
if (length <= 0)
break;
}
}
}
if (length > 0) {
WMSG1(" CPM WARNING: can't account for %ld bytes!\n", (long) length);
//assert(false);
}
//WMSG2(" Returning blockCount=%d for '%s'\n", blockCount,
// fpDirEntry[fDirIdx].fileName);
if (pBlockCount != nil) {
assert(blockBuf == nil || *pBlockCount == blockCount);
*pBlockCount = blockCount;
}
return kDIErrNone;
}
/*
* Dump the contents of the A2File structure.
*/
void
A2FileCPM::Dump(void) const
{
WMSG2("A2FileCPM '%s' length=%ld\n", fFileName, (long) fLength);
}
/*
* ===========================================================================
* A2FDCPM
* ===========================================================================
*/
/*
* Read a chunk of data from the current offset.
*/
DIError
A2FDCPM::Read(void* buf, size_t len, size_t* pActual)
{
WMSG3(" CP/M reading %d bytes from '%s' (offset=%ld)\n",
len, fpFile->GetPathName(), (long) fOffset);
A2FileCPM* pFile = (A2FileCPM*) fpFile;
/* don't allow them to read past the end of the file */
if (fOffset + (long)len > pFile->fLength) {
if (pActual == nil)
return kDIErrDataUnderrun;
len = (size_t) (pFile->fLength - fOffset);
}
if (pActual != nil)
*pActual = len;
long incrLen = len;
DIError dierr = kDIErrNone;
const int kCPMBlockSize = kBlkSize*2;
assert(kCPMBlockSize == 1024);
unsigned char blkBuf[kCPMBlockSize];
int blkIndex = (int) (fOffset / kCPMBlockSize);
int bufOffset = (int) (fOffset % kCPMBlockSize); // (& 0x3ff)
size_t thisCount;
long prodosBlock;
if (len == 0)
return kDIErrNone;
assert(pFile->fLength != 0);
while (len) {
if (blkIndex >= fBlockCount) {
/* ran out of data */
return kDIErrDataUnderrun;
}
if (fBlockList[blkIndex] == 0) {
/*
* Sparse block.
*/
memset(blkBuf, kNoDataByte, sizeof(blkBuf));
} else {
/*
* Read one CP/M block (two ProDOS blocks) and pull out the
* set of data that the user wants.
*
* On some Microsoft Softcard disks, the first three tracks hold
* file data rather than the system image.
*/
prodosBlock = DiskFSCPM::CPMToProDOSBlock(fBlockList[blkIndex]);
if (prodosBlock >= 280)
prodosBlock -= 280;
dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(prodosBlock,
blkBuf);
if (dierr != kDIErrNone) {
WMSG1(" CP/M error1 reading file '%s'\n", pFile->fFileName);
return dierr;
}
dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(prodosBlock+1,
blkBuf + kBlkSize);
if (dierr != kDIErrNone) {
WMSG1(" CP/M error2 reading file '%s'\n", pFile->fFileName);
return dierr;
}
}
thisCount = kCPMBlockSize - bufOffset;
if (thisCount > len)
thisCount = len;
memcpy(buf, blkBuf + bufOffset, thisCount);
len -= thisCount;
buf = (char*)buf + thisCount;
bufOffset = 0;
blkIndex++;
}
fOffset += incrLen;
return dierr;
}
/*
* Write data at the current offset.
*/
DIError
A2FDCPM::Write(const void* buf, size_t len, size_t* pActual)
{
return kDIErrNotSupported;
}
/*
* Seek to a new offset.
*/
DIError
A2FDCPM::Seek(di_off_t offset, DIWhence whence)
{
di_off_t fileLength = ((A2FileCPM*) fpFile)->fLength;
switch (whence) {
case kSeekSet:
if (offset < 0 || offset > fileLength)
return kDIErrInvalidArg;
fOffset = offset;
break;
case kSeekEnd:
if (offset > 0 || offset < -fileLength)
return kDIErrInvalidArg;
fOffset = fileLength + offset;
break;
case kSeekCur:
if (offset < -fOffset ||
offset >= (fileLength - fOffset))
{
return kDIErrInvalidArg;
}
fOffset += offset;
break;
default:
assert(false);
return kDIErrInvalidArg;
}
assert(fOffset >= 0 && fOffset <= fileLength);
return kDIErrNone;
}
/*
* Return current offset.
*/
di_off_t
A2FDCPM::Tell(void)
{
return fOffset;
}
/*
* Release file state, such as it is.
*/
DIError
A2FDCPM::Close(void)
{
fpFile->CloseDescr(this);
return kDIErrNone;
}
/*
* Return the #of sectors/blocks in the file.
*/
long
A2FDCPM::GetSectorCount(void) const
{
return fBlockCount * 4;
}
long
A2FDCPM::GetBlockCount(void) const
{
return fBlockCount * 2;
}
/*
* Return the Nth track/sector in this file.
*/
DIError
A2FDCPM::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
{
long cpmIdx = sectorIdx / 4; // 4 256-byte sectors per 1K CP/M block
if (cpmIdx >= fBlockCount)
return kDIErrInvalidIndex; // CP/M files can have *no* storage
long cpmBlock = fBlockList[cpmIdx];
long prodosBlock = DiskFSCPM::CPMToProDOSBlock(cpmBlock);
if (sectorIdx & 0x02)
prodosBlock++;
BlockToTrackSector(prodosBlock, (sectorIdx & 0x01) != 0, pTrack, pSector);
return kDIErrNone;
}
/*
* Return the Nth 512-byte block in this file. Since things aren't stored
* in 512-byte blocks, we grab the appropriate 1K block and pick half.
*/
DIError
A2FDCPM::GetStorage(long blockIdx, long* pBlock) const
{
long cpmIdx = blockIdx / 2; // 4 256-byte sectors per 1K CP/M block
if (cpmIdx >= fBlockCount)
return kDIErrInvalidIndex;
long cpmBlock = fBlockList[cpmIdx];
long prodosBlock = DiskFSCPM::CPMToProDOSBlock(cpmBlock);
if (blockIdx & 0x01)
prodosBlock++;
*pBlock = prodosBlock;
assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks());
return kDIErrNone;
}