mirror of
https://github.com/fadden/ciderpress.git
synced 2024-11-23 11:33:58 +00:00
732cd85ce5
CP/M: correctly identify 3.x disks Updated some copyright notices.
785 lines
20 KiB
C++
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;
|
|
}
|