ciderpress/diskimg/ProDOS.cpp
Andy McFadden b42cc8efe9 Fix Linux build
gcc is justifiably annoyed by variable initialization crossed by
a goto.

(issue #51)
2021-09-07 13:36:28 -07:00

5184 lines
173 KiB
C++

/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Implementation of DiskFSProDOS class.
*
* We currently only allow one fork to be open at a time, and each file may
* only be opened once.
*
* BUG: does not keep VolumeUsage up to date.
*/
#include "StdAfx.h"
#include "DiskImgPriv.h"
// disable Y2K+ dates when testing w/ProSel-16 vol rep (newer ProSel is OK)
//#define OLD_PRODOS_DATES
#if defined(OLD_PRODOS_DATES) && !(defined(_DEBUG))
# error "don't set OLD_PRODOS_DATES for production"
#endif
/*
* ===========================================================================
* DiskFSProDOS
* ===========================================================================
*/
const int kBlkSize = 512;
const int kVolHeaderBlock = 2; // block where Volume Header resides
const int kFormatVolDirNumBlocks = 4; // #of volume header blocks for new volumes
const int kMinReasonableBlocks = 16; // min size for ProDOS volume
const int kExpectedBitmapStart = 6; // block# where vol bitmap should start
const int kMaxCatalogIterations = 1024; // theoretical max is 32768?
const int kMaxDirectoryDepth = 64; // not sure what ProDOS limit is
const int kEntriesPerBlock = 0x0d; // expected value for entries per blk
const int kEntryLength = 0x27; // expected value for dir entry len
const int kTypeDIR = 0x0f;
/*
* Directory header. All fields not marked as "only for subdirs" also apply
* to the volume directory header.
*/
typedef struct DiskFSProDOS::DirHeader {
uint8_t storageType;
char dirName[A2FileProDOS::kMaxFileName+1];
DiskFSProDOS::ProDate createWhen;
uint8_t version;
uint8_t minVersion;
uint8_t access;
uint8_t entryLength;
uint8_t entriesPerBlock;
uint16_t fileCount;
/* the rest are only for subdirs */
uint16_t parentPointer;
uint8_t parentEntry;
uint8_t parentEntryLength;
} DirHeader;
/*
* See if this looks like a ProDOS volume.
*
* We test a few fields in the volume directory header for validity.
*/
static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder)
{
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
int volDirEntryLength;
int volDirEntriesPerBlock;
dierr = pImg->ReadBlockSwapped(kVolHeaderBlock, blkBuf, imageOrder,
DiskImg::kSectorOrderProDOS);
if (dierr != kDIErrNone)
goto bail;
volDirEntryLength = blkBuf[0x23];
volDirEntriesPerBlock = blkBuf[0x24];
if (!(blkBuf[0x00] == 0 && blkBuf[0x01] == 0) ||
!((blkBuf[0x04] & 0xf0) == 0xf0) ||
!((blkBuf[0x04] & 0x0f) != 0) ||
!(volDirEntryLength * volDirEntriesPerBlock <= kBlkSize) ||
!(blkBuf[0x05] >= 'A' && blkBuf[0x05] <= 'Z') ||
0)
{
dierr = kDIErrFilesystemNotFound;
goto bail;
}
bail:
return dierr;
}
/*
* Test to see if the image is a ProDOS disk.
*/
/*static*/ DIError DiskFSProDOS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder,
DiskImg::FSFormat* pFormat, FSLeniency leniency)
{
DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax];
DiskImg::GetSectorOrderArray(ordering, *pOrder);
for (int i = 0; i < DiskImg::kSectorOrderMax; i++) {
if (ordering[i] == DiskImg::kSectorOrderUnknown)
continue;
if (TestImage(pImg, ordering[i]) == kDIErrNone) {
*pOrder = ordering[i];
*pFormat = DiskImg::kFormatProDOS;
return kDIErrNone;
}
}
LOGI(" ProDOS didn't find valid FS");
return kDIErrFilesystemNotFound;
}
/*
* Get things rolling.
*
* Since we're assured that this is a valid disk, errors encountered from here
* on out must be handled somehow, possibly by claiming that the disk has
* no files on it.
*/
DIError DiskFSProDOS::Initialize(InitMode initMode)
{
DIError dierr = kDIErrNone;
char msg[kMaxVolumeName + 32];
fDiskIsGood = false; // hosed until proven innocent
fEarlyDamage = false;
/*
* NOTE: we'd probably be better off with fTotalBlocks, since that's how
* big the disk *thinks* it is, especially on a CFFA or MacPart subvol.
* However, we know that the image block count is the absolute maximum,
* so while it may not be a tight bound it is an upper bound.
*/
fVolumeUsage.Create(fpImg->GetNumBlocks());
dierr = LoadVolHeader();
if (dierr != kDIErrNone)
goto bail;
DumpVolHeader();
dierr = ScanVolBitmap();
if (dierr != kDIErrNone)
goto bail;
if (initMode == kInitHeaderOnly) {
LOGI(" ProDOS - headerOnly set, skipping file load");
goto bail;
}
sprintf(msg, "Scanning %s", fVolumeName);
if (!fpImg->UpdateScanProgress(msg)) {
LOGI(" ProDOS cancelled by user");
dierr = kDIErrCancelled;
goto bail;
}
/* volume dir is guaranteed to come first; if not, we need a lookup func */
A2FileProDOS* pVolumeDir;
pVolumeDir = (A2FileProDOS*) GetNextFile(NULL);
dierr = RecursiveDirAdd(pVolumeDir, kVolHeaderBlock, "", 0);
if (dierr != kDIErrNone) {
LOGI(" ProDOS RecursiveDirAdd failed");
goto bail;
}
sprintf(msg, "Processing %s", fVolumeName);
if (!fpImg->UpdateScanProgress(msg)) {
LOGI(" ProDOS cancelled by user");
dierr = kDIErrCancelled;
goto bail;
}
dierr = ScanFileUsage();
if (dierr != kDIErrNone) {
if (dierr == kDIErrCancelled)
goto bail;
/* this might not be fatal; just means that *some* files are bad */
LOGI("WARNING: ScanFileUsage returned err=%d", dierr);
dierr = kDIErrNone;
fpImg->AddNote(DiskImg::kNoteWarning,
"Some errors were encountered while scanning files.");
fEarlyDamage = true; // make sure we know it's damaged
}
fDiskIsGood = CheckDiskIsGood();
if (fScanForSubVolumes != kScanSubDisabled)
(void) ScanForSubVolumes();
if (fpImg->GetNumBlocks() <= 1600)
fVolumeUsage.Dump();
// A2File* pFile;
// pFile = GetNextFile(NULL);
// while (pFile != NULL) {
// pFile->Dump();
// pFile = GetNextFile(pFile);
// }
bail:
return dierr;
}
/*
* Read some interesting fields from the volume header.
*
* The "test" function verified certain things, e.g. the storage type
* is $f and the volume name length is nonzero.
*/
DIError DiskFSProDOS::LoadVolHeader(void)
{
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
int nameLen;
dierr = fpImg->ReadBlock(kVolHeaderBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
//fPrevBlock = GetShortLE(&blkBuf[0x00]);
//fNextBlock = GetShortLE(&blkBuf[0x02]);
nameLen = blkBuf[0x04] & 0x0f;
memcpy(fVolumeName, &blkBuf[0x05], nameLen);
fVolumeName[nameLen] = '\0';
// 0x14-15 reserved
// undocumented: GS/OS writes the modification date to 0x16-19
fModWhen = GetLongLE(&blkBuf[0x16]);
// undocumented: GS/OS uses 0x1a-1b for lower-case handling (see below)
fCreateWhen = GetLongLE(&blkBuf[0x1c]);
//fVersion = blkBuf[0x20];
if (blkBuf[0x21] != 0) {
/*
* We don't care about the MIN_VERSION field, but it looks like GS/OS
* rejects anything with a nonzero value here. We want to add a note
* about it.
*/
fpImg->AddNote(DiskImg::kNoteInfo,
"Volume header has nonzero min_version; could confuse GS/OS.");
}
fAccess = blkBuf[0x22];
//fEntryLength = blkBuf[0x23];
//fEntriesPerBlock = blkBuf[0x24];
fVolDirFileCount = GetShortLE(&blkBuf[0x25]);
fBitMapPointer = GetShortLE(&blkBuf[0x27]);
fTotalBlocks = GetShortLE(&blkBuf[0x29]);
if (blkBuf[0x1b] & 0x80) {
/*
* Handle lower-case conversion; see GS/OS tech note #8. Unlike
* filenames, volume names are not allowed to contain spaces. If
* they try it we just ignore them.
*
* Technote 8 doesn't actually talk about volume names. By
* experimentation the field was discovered at offset 0x1a from
* the start of the block, which is marked as "reserved" in Beneath
* Apple ProDOS.
*/
uint16_t lcFlags = GetShortLE(&blkBuf[0x1a]);
GenerateLowerCaseName(fVolumeName, fVolumeName, lcFlags, false);
}
if (fTotalBlocks <= kVolHeaderBlock) {
/* incr to min; don't use max, or bitmap count may be too large */
LOGI(" ProDOS found tiny fTotalBlocks (%d), increasing to minimum",
fTotalBlocks);
fpImg->AddNote(DiskImg::kNoteWarning,
"ProDOS filesystem blockcount (%d) too small, setting to %d.",
fTotalBlocks, kMinReasonableBlocks);
fTotalBlocks = kMinReasonableBlocks;
fEarlyDamage = true;
} else if (fTotalBlocks != fpImg->GetNumBlocks()) {
if (fTotalBlocks != 65535 || fpImg->GetNumBlocks() != 65536) {
LOGI(" ProDOS WARNING: total (%u) != img (%ld)",
fTotalBlocks, fpImg->GetNumBlocks());
// could AddNote here, but not really necessary
}
/*
* For safety (esp. vol bitmap read), constrain fTotalBlocks. We might
* consider not doing this for ".hdv", which can start small and then
* expand as files are added. (Check "fExpanded".)
*/
if (fTotalBlocks > fpImg->GetNumBlocks()) {
fpImg->AddNote(DiskImg::kNoteWarning,
"ProDOS filesystem blockcount (%d) exceeds disk image blocks (%ld).",
fTotalBlocks, fpImg->GetNumBlocks());
fTotalBlocks = (uint16_t) fpImg->GetNumBlocks();
fEarlyDamage = true;
}
}
/*
* Test for funky volume bitmap pointer. Some disks (e.g. /RAM and
* ProSel-16) truncate the volume directory to eke a little more storage
* out of a disk. There's nothing wrong with that, but we don't want to
* try to use a volume bitmap pointer of zero or 0xffff, because it's
* probably garbage.
*/
if (fBitMapPointer != kExpectedBitmapStart) {
if (fBitMapPointer <= kVolHeaderBlock ||
fBitMapPointer > kExpectedBitmapStart)
{
fpImg->AddNote(DiskImg::kNoteWarning,
"Volume bitmap pointer (%d) is probably invalid.",
fBitMapPointer);
fBitMapPointer = 6; // just fix it and hope for the best
fEarlyDamage = true;
} else {
fpImg->AddNote(DiskImg::kNoteInfo,
"Unusual volume bitmap start (%d).", fBitMapPointer);
// try it and see
}
}
SetVolumeID();
/*
* Create a "magic" directory entry for the volume directory.
*
* Normally these values are pulled out of the file entry in the parent
* directory. Here, we synthesize them from the volume dir header.
*/
A2FileProDOS* pFile;
pFile = new A2FileProDOS(this);
if (pFile == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
A2FileProDOS::DirEntry* pEntry;
pEntry = &pFile->fDirEntry;
int foundStorage;
foundStorage = (blkBuf[0x04] & 0xf0) >> 4;
if (foundStorage != A2FileProDOS::kStorageVolumeDirHeader) {
LOGI(" ProDOS WARNING: unexpected vol dir file type %d",
pEntry->storageType);
/* keep going */
}
pEntry->storageType = A2FileProDOS::kStorageVolumeDirHeader;
strcpy(pEntry->fileName, fVolumeName);
//nameLen = blkBuf[0x04] & 0x0f;
//memcpy(pEntry->fileName, &blkBuf[0x05], nameLen);
//pEntry->fileName[nameLen] = '\0';
pFile->SetPathName(":", pEntry->fileName);
pEntry->fileName[nameLen] = '\0';
pEntry->fileType = kTypeDIR;
pEntry->keyPointer = kVolHeaderBlock;
dierr = DetermineVolDirLen(GetShortLE(&blkBuf[0x02]), &pEntry->blocksUsed);
if (dierr != kDIErrNone) {
goto bail;
}
pEntry->eof = pEntry->blocksUsed * 512;
pEntry->createWhen = GetLongLE(&blkBuf[0x1c]);
pEntry->version = blkBuf[0x20];
pEntry->minVersion = blkBuf[0x21];
pEntry->access = blkBuf[0x22];
pEntry->auxType = 0;
// if (blkBuf[0x20] >= 5)
pEntry->modWhen = GetLongLE(&blkBuf[0x16]);
pEntry->headerPointer = 0;
pFile->fSparseDataEof = pEntry->eof;
pFile->fSparseRsrcEof = -1;
AddFileToList(pFile);
pFile = NULL;
bail:
delete pFile;
return dierr;
}
DIError DiskFSProDOS::DetermineVolDirLen(uint16_t nextBlock, uint16_t* pBlocksUsed) {
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
uint16_t blocksUsed = 1;
int iterCount = 0;
// Traverse the volume directory chain, counting blocks. Normally this will have 4, but
// variations are possible.
while (nextBlock != 0) {
blocksUsed++;
if (nextBlock < 2 || nextBlock >= fpImg->GetNumBlocks()) {
LOGI(" ProDOS ERROR: invalid volume dir link block %u", nextBlock);
dierr = kDIErrInvalidBlock;
goto bail;
}
dierr = fpImg->ReadBlock(nextBlock, blkBuf);
if (dierr != kDIErrNone) {
goto bail;
}
nextBlock = GetShortLE(&blkBuf[0x02]);
// Watch for infinite loop.
iterCount++;
if (iterCount > fpImg->GetNumBlocks()) {
LOGI(" ProDOS ERROR: infinite vol directory loop found");
dierr = kDIErrDirectoryLoop;
goto bail;
}
}
bail:
*pBlocksUsed = blocksUsed;
return dierr;
}
/*
* Set the volume ID field.
*/
void DiskFSProDOS::SetVolumeID(void)
{
sprintf(fVolumeID, "ProDOS /%s", fVolumeName);
}
/*
* Dump what we pulled out of the volume header.
*/
void DiskFSProDOS::DumpVolHeader(void)
{
LOGI(" ProDOS volume header for '%s'", fVolumeName);
LOGI(" CreateWhen=0x%08x access=0x%02x bitmap=%d totalbl=%d",
fCreateWhen, fAccess, fBitMapPointer, fTotalBlocks);
time_t when;
when = A2FileProDOS::ConvertProDate(fCreateWhen);
LOGI(" CreateWhen is %.24s", ctime(&when));
//LOGI(" prev=%d next=%d bitmap=%d total=%d",
// fPrevBlock, fNextBlock, fBitMapPointer, fTotalBlocks);
//LOGI(" create date=0x%08lx access=0x%02x", fCreateWhen, fAccess);
//LOGI(" version=%d minVersion=%d entryLen=%d epb=%d",
// fVersion, fMinVersion, fEntryLength, fEntriesPerBlock);
//LOGI(" volume dir fileCount=%d", fFileCount);
}
/*
* Load the disk's volume bitmap into the object's "fBlockUseMap" pointer.
*
* Does not attempt to analyze the data.
*/
DIError DiskFSProDOS::LoadVolBitmap(void)
{
DIError dierr = kDIErrNone;
int bitBlock, numBlocks;
if (fBitMapPointer <= kVolHeaderBlock)
return kDIErrBadDiskImage;
if (fTotalBlocks <= kVolHeaderBlock)
return kDIErrBadDiskImage;
/* should not already be allocated */
assert(fBlockUseMap == NULL);
delete[] fBlockUseMap; // just in case
bitBlock = fBitMapPointer;
numBlocks = GetNumBitmapBlocks(); // based on fTotalBlocks
assert(numBlocks > 0);
fBlockUseMap = new uint8_t[kBlkSize * numBlocks];
if (fBlockUseMap == NULL)
return kDIErrMalloc;
while (numBlocks--) {
dierr = fpImg->ReadBlock(bitBlock + numBlocks,
fBlockUseMap + kBlkSize * numBlocks);
if (dierr != kDIErrNone) {
delete[] fBlockUseMap;
fBlockUseMap = NULL;
return dierr;
}
}
return kDIErrNone;
}
/*
* Save our copy of the volume bitmap.
*/
DIError DiskFSProDOS::SaveVolBitmap(void)
{
DIError dierr = kDIErrNone;
int bitBlock, numBlocks;
if (fBlockUseMap == NULL) {
assert(false);
return kDIErrNotReady;
}
assert(fBitMapPointer > kVolHeaderBlock);
assert(fTotalBlocks > kVolHeaderBlock);
bitBlock = fBitMapPointer;
numBlocks = GetNumBitmapBlocks();
assert(numBlocks > 0);
while (numBlocks--) {
dierr = fpImg->WriteBlock(bitBlock + numBlocks,
fBlockUseMap + kBlkSize * numBlocks);
if (dierr != kDIErrNone)
return dierr;
}
return kDIErrNone;
}
/*
* Throw away the volume bitmap, discarding any unsaved changes.
*
* It's okay to call this if the bitmap isn't loaded.
*/
void DiskFSProDOS::FreeVolBitmap(void)
{
delete[] fBlockUseMap;
fBlockUseMap = NULL;
}
/*
* Examine the volume bitmap, setting fields in the VolumeUsage map
* as appropriate.
*/
DIError DiskFSProDOS::ScanVolBitmap(void)
{
DIError dierr;
dierr = LoadVolBitmap();
if (dierr != kDIErrNone) {
LOGI(" ProDOS failed to load volume bitmap (err=%d)", dierr);
return dierr;
}
assert(fBlockUseMap != NULL);
/* mark the boot blocks as system */
SetBlockUsage(0, VolumeUsage::kChunkPurposeSystem);
SetBlockUsage(1, VolumeUsage::kChunkPurposeSystem);
/* mark the bitmap blocks as system */
int i;
for (i = GetNumBitmapBlocks(); i > 0; i--)
SetBlockUsage(fBitMapPointer + i -1, VolumeUsage::kChunkPurposeSystem);
/*
* Set the "isMarkedUsed" flag in VolumeUsage for all used blocks.
*/
VolumeUsage::ChunkState cstate;
long block = 0;
long numBytes = (fTotalBlocks + 7) / 8;
for (i = 0; i < numBytes; i++) {
uint8_t val = fBlockUseMap[i];
for (int j = 0; j < 8; j++) {
if (!(val & 0x80)) {
/* block is in use, mark it */
if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone)
{
assert(false);
// keep going, I guess
}
cstate.isMarkedUsed = true;
fVolumeUsage.SetChunkState(block, &cstate);
}
val <<= 1;
block++;
if (block >= fTotalBlocks)
break;
}
if (block >= fTotalBlocks)
break;
}
FreeVolBitmap();
return dierr;
}
/*
* Generate an empty block use map. Used by disk formatter.
*/
DIError DiskFSProDOS::CreateEmptyBlockMap(void)
{
DIError dierr;
/* load from disk; this is just to allocate the data structures */
dierr = LoadVolBitmap();
if (dierr != kDIErrNone)
return dierr;
/*
* Set the bits, block by block. Not the most efficient way, but it's
* fast enough, and it exercises the standard set of functions.
*/
long block;
long firstEmpty =
kVolHeaderBlock + kFormatVolDirNumBlocks + GetNumBitmapBlocks();
for (block = 0; block < firstEmpty; block++)
SetBlockUseEntry(block, true);
for ( ; block < fTotalBlocks; block++)
SetBlockUseEntry(block, false);
dierr = SaveVolBitmap();
FreeVolBitmap();
if (dierr != kDIErrNone)
return dierr;
return kDIErrNone;
}
/*
* Get the state of an entry in the block use map.
*
* Returns "true" if it's in use, "false" otherwise.
*/
bool DiskFSProDOS::GetBlockUseEntry(long block) const
{
assert(block >= 0 && block < fTotalBlocks);
assert(fBlockUseMap != NULL);
int offset;
uint8_t mask;
offset = block / 8;
mask = 0x80 >> (block & 0x07);
if (fBlockUseMap[offset] & mask)
return false;
else
return true;
}
/*
* Change the state of an entry in the block use map.
*/
void DiskFSProDOS::SetBlockUseEntry(long block, bool inUse)
{
assert(block >= 0 && block < fTotalBlocks);
assert(fBlockUseMap != NULL);
if (block == 0 && !inUse) {
// shouldn't happen
assert(false);
}
int offset;
uint8_t mask;
offset = block / 8;
mask = 0x80 >> (block & 0x07);
if (!inUse)
fBlockUseMap[offset] |= mask;
else
fBlockUseMap[offset] &= ~mask;
}
/*
* Check for entries in the block use map past the point where they should be.
*
* Returns "true" if bogus entries were found, "false" if all is well.
*/
bool DiskFSProDOS::ScanForExtraEntries(void) const
{
assert(fBlockUseMap != NULL);
int offset, endOffset;
/* sloppy: we're not checking for excess bits within last byte */
offset = (fTotalBlocks / 8) +1;
endOffset = GetNumBitmapBlocks() * kBlkSize;
while (offset < endOffset) {
if (fBlockUseMap[offset] != 0) {
LOGI(" ProDOS found bogus bitmap junk 0x%02x at offset=%d",
fBlockUseMap[offset], offset);
return true;
}
offset++;
}
return false;
}
/*
* Allocate a new block on a ProDOS volume.
*
* Only touches the in-memory copy.
*
* Returns the block number (0-65535) on success or -1 on failure.
*/
long DiskFSProDOS::AllocBlock(void)
{
assert(fBlockUseMap != NULL);
#if 0 // whoa... this is REALLY slow
/*
* Run through the entire set of blocks until we find one that's not
* allocated. We could probably make this faster by scanning bytes and
* then shifting bits, but this is easier and fast enough.
*
* We don't scan block 0 because (a) it should never be available and
* (b) it has a special meaning in some circumstances. We could probably
* start at kVolHeaderBlock+kVolHeaderNumBlocks.
*/
long block;
for (block = kVolHeaderBlock; block < fTotalBlocks; block++) {
if (!GetBlockUseEntry(block)) {
SetBlockUseEntry(block, true);
return block;
}
}
#endif
int offset;
int maxOffset = (fTotalBlocks + 7) / 8;
for (offset = 0; offset < maxOffset; offset++) {
if (fBlockUseMap[offset] != 0) {
/* got one, figure out which */
int subBlock = 0;
uint8_t uch = fBlockUseMap[offset];
while ((uch & 0x80) == 0) {
subBlock++;
uch <<= 1;
}
long block = offset * 8 + subBlock;
assert(!GetBlockUseEntry(block));
SetBlockUseEntry(block, true);
if (block == 0 || block == 1) {
LOGI("PRODOS: GLITCH: rejecting alloc of block 0");
continue;
}
return block;
}
}
LOGI("ProDOS: NOTE: AllocBlock just failed!");
return -1;
}
/*
* Tally up the number of free blocks.
*/
DIError DiskFSProDOS::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits,
int* pUnitSize) const
{
DIError dierr;
long block, freeBlocks;
freeBlocks = 0;
dierr = const_cast<DiskFSProDOS*>(this)->LoadVolBitmap();
if (dierr != kDIErrNone)
return dierr;
for (block = 0; block < fTotalBlocks; block++) {
if (!GetBlockUseEntry(block))
freeBlocks++;
}
*pTotalUnits = fTotalBlocks;
*pFreeUnits = freeBlocks;
*pUnitSize = kBlockSize;
const_cast<DiskFSProDOS*>(this)->FreeVolBitmap();
return kDIErrNone;
}
/*
* Update an entry in the VolumeUsage map.
*
* The VolumeUsage map spans the range of blocks
*/
void DiskFSProDOS::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose)
{
VolumeUsage::ChunkState cstate;
fVolumeUsage.GetChunkState(block, &cstate);
if (cstate.isUsed) {
cstate.purpose = VolumeUsage::kChunkPurposeConflict;
LOGI(" ProDOS conflicting uses for bl=%ld", block);
} else {
cstate.isUsed = true;
cstate.purpose = purpose;
}
fVolumeUsage.SetChunkState(block, &cstate);
}
/*
* Pass in the number of the first block of the directory.
*
* Start with "pParent" set to the magic entry for the volume dir.
*/
DIError DiskFSProDOS::RecursiveDirAdd(A2File* pParent, uint16_t dirBlock,
const char* basePath, int depth)
{
DIError dierr = kDIErrNone;
DirHeader header;
uint8_t blkBuf[kBlkSize];
int numEntries, iterations, foundCount;
bool first;
/* if we get too deep, assume it's a loop */
if (depth > kMaxDirectoryDepth) {
dierr = kDIErrDirectoryLoop;
goto bail;
}
if (dirBlock < kVolHeaderBlock || dirBlock >= fpImg->GetNumBlocks()) {
LOGI(" ProDOS ERROR: directory block %u out of range", dirBlock);
dierr = kDIErrInvalidBlock;
goto bail;
}
numEntries = 1;
iterations = 0;
foundCount = 0;
first = true;
while (dirBlock && iterations < kMaxCatalogIterations) {
dierr = fpImg->ReadBlock(dirBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
if (pParent->IsVolumeDirectory())
SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeVolumeDir);
else
SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeSubdir);
if (first) {
/* this is the directory header entry */
dierr = GetDirHeader(blkBuf, &header);
if (dierr != kDIErrNone)
goto bail;
numEntries = header.fileCount;
//LOGI(" ProDOS got dir header numEntries = %d", numEntries);
}
/* slurp the entries out of this block */
dierr = SlurpEntries(pParent, &header, blkBuf, first, &foundCount,
basePath, dirBlock, depth);
if (dierr != kDIErrNone)
goto bail;
dirBlock = GetShortLE(&blkBuf[0x02]);
if (dirBlock != 0 &&
(dirBlock < 2 || dirBlock >= fpImg->GetNumBlocks()))
{
LOGI(" ProDOS ERROR: invalid dir link block %u in base='%s'",
dirBlock, basePath);
dierr = kDIErrInvalidBlock;
goto bail;
}
first = false;
iterations++;
}
if (iterations == kMaxCatalogIterations) {
LOGI(" ProDOS subdir iteration count exceeded");
dierr = kDIErrDirectoryLoop;
goto bail;
}
if (foundCount != numEntries) {
/* not significant; just means somebody isn't updating correctly */
LOGI(" ProDOS WARNING: numEntries=%d foundCount=%d in base='%s'",
numEntries, foundCount, basePath);
}
bail:
return dierr;
}
/*
* Slurp the entries out of a single ProDOS directory block.
*
* Recursively calls RecursiveDirAdd for directories.
*
* "*pFound" is increased by the number of valid entries found in this block.
*/
DIError DiskFSProDOS::SlurpEntries(A2File* pParent, const DirHeader* pHeader,
const uint8_t* blkBuf, bool skipFirst, int* pCount,
const char* basePath, uint16_t thisBlock, int depth)
{
DIError dierr = kDIErrNone;
int entriesThisBlock = pHeader->entriesPerBlock;
const uint8_t* entryBuf;
A2FileProDOS* pFile;
int idx = 0;
entryBuf = &blkBuf[0x04];
if (skipFirst) {
entriesThisBlock--;
entryBuf += pHeader->entryLength;
idx++;
}
for ( ; entriesThisBlock > 0 ;
entriesThisBlock--, idx++, entryBuf += pHeader->entryLength)
{
if (entryBuf >= blkBuf + kBlkSize) {
LOGI(" ProDOS whoops, just walked out of dirent buffer");
return kDIErrBadDirectory;
}
if ((entryBuf[0x00] & 0xf0) == A2FileProDOS::kStorageDeleted) {
/* skip deleted entries */
continue;
}
pFile = new A2FileProDOS(this);
if (pFile == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
A2FileProDOS::DirEntry* pEntry;
pEntry = &pFile->fDirEntry;
A2FileProDOS::InitDirEntry(pEntry, entryBuf);
pFile->SetParent(pParent);
pFile->fParentDirBlock = thisBlock;
pFile->fParentDirIdx = idx;
pFile->SetPathName(basePath, pEntry->fileName);
if (pEntry->keyPointer <= kVolHeaderBlock) {
LOGI("ProDOS invalid key pointer %d on '%s'",
pEntry->keyPointer, pFile->GetPathName());
pFile->SetQuality(A2File::kQualityDamaged);
} else
if (pEntry->storageType == A2FileProDOS::kStorageExtended) {
dierr = ReadExtendedInfo(pFile);
if (dierr != kDIErrNone) {
pFile->SetQuality(A2File::kQualityDamaged);
dierr = kDIErrNone;
}
}
//pFile->Dump();
AddFileToList(pFile);
(*pCount)++;
if (!fpImg->UpdateScanProgress(NULL)) {
LOGI(" ProDOS cancelled by user");
dierr = kDIErrCancelled;
goto bail;
}
if (pEntry->storageType == A2FileProDOS::kStorageDirectory) {
// don't need to check for kStorageVolumeDirHeader here
dierr = RecursiveDirAdd(pFile, pEntry->keyPointer,
pFile->GetPathName(), depth+1);
if (dierr != kDIErrNone) {
if (dierr == kDIErrCancelled)
goto bail;
/* mark subdir as damaged and keep going */
pFile->SetQuality(A2File::kQualityDamaged);
dierr = kDIErrNone;
}
}
}
bail:
return dierr;
}
/*
* Pull the directory header out of the first block of a directory.
*/
DIError DiskFSProDOS::GetDirHeader(const uint8_t* blkBuf, DirHeader* pHeader)
{
int nameLen;
pHeader->storageType = (blkBuf[0x04] & 0xf0) >> 4;
if (pHeader->storageType != A2FileProDOS::kStorageSubdirHeader &&
pHeader->storageType != A2FileProDOS::kStorageVolumeDirHeader)
{
LOGI(" ProDOS WARNING: subdir header has wrong storage type (%d)",
pHeader->storageType);
/* keep going... might be bad idea */
}
nameLen = blkBuf[0x04] & 0x0f;
memcpy(pHeader->dirName, &blkBuf[0x05], nameLen);
pHeader->dirName[nameLen] = '\0';
pHeader->createWhen = GetLongLE(&blkBuf[0x1c]);
pHeader->version = blkBuf[0x20];
pHeader->minVersion = blkBuf[0x21];
pHeader->access = blkBuf[0x22];
pHeader->entryLength = blkBuf[0x23];
pHeader->entriesPerBlock = blkBuf[0x24];
pHeader->fileCount = GetShortLE(&blkBuf[0x25]);
pHeader->parentPointer = GetShortLE(&blkBuf[0x27]);
pHeader->parentEntry = blkBuf[0x29];
pHeader->parentEntryLength = blkBuf[0x2a];
if (pHeader->entryLength * pHeader->entriesPerBlock > kBlkSize ||
pHeader->entryLength * pHeader->entriesPerBlock == 0)
{
LOGI(" ProDOS invalid subdir header: entryLen=%d, entriesPerBlock=%d",
pHeader->entryLength, pHeader->entriesPerBlock);
return kDIErrBadDirectory;
}
return kDIErrNone;
}
/*
* Read the information from the key block of an extended file.
*
* There's some "HFS Finder information" stuffed into the key block
* right after the data fork info, but I'm planning to ignore that.
*/
DIError DiskFSProDOS::ReadExtendedInfo(A2FileProDOS* pFile)
{
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, blkBuf);
if (dierr != kDIErrNone) {
LOGI(" ProDOS ReadExtendedInfo: unable to read key block %d",
pFile->fDirEntry.keyPointer);
goto bail;
}
pFile->fExtData.storageType = blkBuf[0x0000] & 0x0f;
pFile->fExtData.keyBlock = GetShortLE(&blkBuf[0x0001]);
pFile->fExtData.blocksUsed = GetShortLE(&blkBuf[0x0003]);
pFile->fExtData.eof = GetLongLE(&blkBuf[0x0005]);
pFile->fExtData.eof &= 0x00ffffff;
pFile->fExtRsrc.storageType = blkBuf[0x0100] & 0x0f;
pFile->fExtRsrc.keyBlock = GetShortLE(&blkBuf[0x0101]);
pFile->fExtRsrc.blocksUsed = GetShortLE(&blkBuf[0x0103]);
pFile->fExtRsrc.eof = GetLongLE(&blkBuf[0x0105]);
pFile->fExtRsrc.eof &= 0x00ffffff;
if (pFile->fExtData.keyBlock <= kVolHeaderBlock ||
pFile->fExtRsrc.keyBlock <= kVolHeaderBlock)
{
LOGI(" ProDOS ReadExtendedInfo: found bad extended key blocks %d/%d",
pFile->fExtData.keyBlock, pFile->fExtRsrc.keyBlock);
return kDIErrBadFile;
}
bail:
return dierr;
}
/*
* Scan all of the files on the disk, reading their block usage into the
* volume usage map. This is important for detecting damage, and makes
* later accesses easier.
*
* As a side-effect, we set the "sparse" length for the file.
*/
DIError DiskFSProDOS::ScanFileUsage(void)
{
DIError dierr = kDIErrNone;
A2FileProDOS* pFile;
long blockCount, indexCount, sparseCount;
uint16_t* blockList = NULL;
uint16_t* indexList = NULL;
pFile = (A2FileProDOS*) GetNextFile(NULL);
while (pFile != NULL) {
if (!fpImg->UpdateScanProgress(NULL)) {
LOGI(" ProDOS cancelled by user");
dierr = kDIErrCancelled;
goto bail;
}
//pFile->Dump();
if (pFile->GetQuality() == A2File::kQualityDamaged)
goto skip;
if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) {
/* resource fork */
if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) {
/* not expecting to find a directory here, but it happens */
dierr = kDIErrBadFile;
} else {
dierr = pFile->LoadBlockList(pFile->fExtRsrc.storageType,
pFile->fExtRsrc.keyBlock, pFile->fExtRsrc.eof,
&blockCount, &blockList, &indexCount, &indexList);
}
if (dierr != kDIErrNone) {
LOGI(" ProDOS skipping scan rsrc '%s'",
pFile->fDirEntry.fileName);
pFile->SetQuality(A2File::kQualityDamaged);
goto skip;
}
ScanBlockList(blockCount, blockList, indexCount, indexList,
&sparseCount);
pFile->fSparseRsrcEof =
(di_off_t) pFile->fExtRsrc.eof - sparseCount * kBlkSize;
//LOGI(" SparseCount %d rsrcEof %d '%s'",
// sparseCount, pFile->fSparseRsrcEof, pFile->fDirEntry.fileName);
delete[] blockList;
blockList = NULL;
delete[] indexList;
indexList = NULL;
/* data fork */
if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) {
dierr = kDIErrBadFile;
} else {
dierr = pFile->LoadBlockList(pFile->fExtData.storageType,
pFile->fExtData.keyBlock, pFile->fExtData.eof,
&blockCount, &blockList, &indexCount, &indexList);
}
if (dierr != kDIErrNone) {
LOGI(" ProDOS skipping scan data '%s'",
pFile->fDirEntry.fileName);
pFile->SetQuality(A2File::kQualityDamaged);
goto skip;
}
ScanBlockList(blockCount, blockList, indexCount, indexList,
&sparseCount);
pFile->fSparseDataEof =
(di_off_t) pFile->fExtData.eof - sparseCount * kBlkSize;
//LOGI(" SparseCount %ld dataEof %ld -> %lld '%s'",
// sparseCount, pFile->fExtData.eof, pFile->fSparseDataEof,
// pFile->fDirEntry.fileName);
delete[] blockList;
blockList = NULL;
delete[] indexList;
indexList = NULL;
/* mark the extended key block as in-use */
SetBlockUsage(pFile->fDirEntry.keyPointer,
VolumeUsage::kChunkPurposeFileStruct);
} else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory ||
pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader)
{
/* we already got these during the recursive descent */
/* (could do them here if we used "fake" directory entry
for volume dir to lead off the recursion) */
goto skip;
} else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageSeedling ||
pFile->fDirEntry.storageType == A2FileProDOS::kStorageSapling ||
pFile->fDirEntry.storageType == A2FileProDOS::kStorageTree)
{
/* standard file */
dierr = pFile->LoadBlockList(pFile->fDirEntry.storageType,
pFile->fDirEntry.keyPointer, pFile->fDirEntry.eof,
&blockCount, &blockList, &indexCount, &indexList);
if (dierr != kDIErrNone) {
LOGI(" ProDOS skipping scan '%s'",
pFile->fDirEntry.fileName);
pFile->SetQuality(A2File::kQualityDamaged);
goto skip;
}
ScanBlockList(blockCount, blockList, indexCount, indexList,
&sparseCount);
pFile->fSparseDataEof =
(di_off_t) pFile->fDirEntry.eof - sparseCount * kBlkSize;
//LOGI(" +++ sparseCount=%ld blockCount=%ld sparseDataEof=%lld '%s'",
// sparseCount, blockCount, pFile->fSparseDataEof,
// pFile->fDirEntry.fileName);
delete[] blockList;
blockList = NULL;
delete[] indexList;
indexList = NULL;
} else {
LOGI(" ProDOS found weird storage type %d on '%s', ignoring",
pFile->fDirEntry.storageType, pFile->fDirEntry.fileName);
pFile->SetQuality(A2File::kQualityDamaged);
}
/*
* A completely empty file written as zero blocks (as opposed to simply
* having its EOF extended, e.g. "sparse seedlings") will have zero data
* blocks but possibly an EOF that doesn't land on 512 bytes. This can
* result in a slightly negative "sparse length", which we trim to zero
* here.
*/
//if (stricmp(pFile->fDirEntry.fileName, "EMPTY.SPARSE.R") == 0)
// LOGI("wahoo");
if (pFile->fSparseDataEof < 0)
pFile->fSparseDataEof = 0;
if (pFile->fSparseRsrcEof < 0)
pFile->fSparseRsrcEof = 0;
skip:
pFile = (A2FileProDOS*) GetNextFile(pFile);
}
dierr = kDIErrNone;
bail:
return dierr;
}
/*
* Scan a block list into the volume usage map.
*/
void DiskFSProDOS::ScanBlockList(long blockCount, uint16_t* blockList,
long indexCount, uint16_t* indexList, long* pSparseCount)
{
assert(blockList != NULL);
assert(indexCount == 0 || indexList != NULL);
assert(pSparseCount != NULL);
*pSparseCount = 0;
int i;
for (i = 0; i < blockCount; i++) {
if (blockList[i] != 0) {
SetBlockUsage(blockList[i], VolumeUsage::kChunkPurposeUserData);
} else {
(*pSparseCount)++; // sparse data block
}
}
for (i = 0; i < indexCount; i++) {
if (indexList[i] != 0) {
SetBlockUsage(indexList[i], VolumeUsage::kChunkPurposeFileStruct);
} // else sparse index block
}
}
/*
* ProDOS disks may contain other filesystems. The typical DOS-in-ProDOS
* strategy involves marking a bunch of blocks at the end of the disc as
* "in use" without creating a file to go along with them.
*
* We look for certain types of embedded volume by looking for disk
* usage patterns and then testing those with the standard disk testing
* facilities.
*/
DIError DiskFSProDOS::ScanForSubVolumes(void)
{
DIError dierr = kDIErrNone;
VolumeUsage::ChunkState cstate;
int firstBlock, matchCount;
int block;
/* this is guaranteed by constraint in volume header read */
assert(fTotalBlocks <= fpImg->GetNumBlocks());
if (fTotalBlocks != 1600) {
LOGI(" ProDOS ScanForSub: not 800K disk (%ld)",
fpImg->GetNumBlocks());
return kDIErrNone; // only scan 800K disks
}
matchCount = 0;
for (block = fTotalBlocks-1; block >= 0; block--) {
if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) {
assert(false);
return kDIErrGeneric;
}
if (!cstate.isMarkedUsed || cstate.isUsed)
break;
matchCount++;
}
firstBlock = block+1;
LOGI("MATCH COUNT %d", matchCount);
if (matchCount < 35*8) // 280 blocks on 35-track floppy
return kDIErrNone;
//if (matchCount % 8 != 0) { // must have 4K tracks
// LOGI(" ProDOS ScanForSub: matchCount %d odd number",
// matchCount);
// return kDIErrNone;
//}
/*
* Try #1: this is a single DOS 3.3 volume (200K or less).
*/
if ((matchCount % 8) == 0 && matchCount <= (50*8)) { // max 50 tracks
DiskFS* pNewFS = NULL;
DiskImg* pNewImg = NULL;
LOGI(" Sub #1: looking for single DOS volume");
dierr = FindSubVolume(firstBlock, matchCount, &pNewImg, &pNewFS);
if (dierr == kDIErrNone) {
AddSubVolumeToList(pNewImg, pNewFS);
MarkSubVolumeBlocks(firstBlock, matchCount);
return kDIErrNone;
}
}
/*
* Try #2: there are multiple 140K DOS 3.3 volumes here.
*
* We may want to override their volume numbers, but it looks like
* DOS Master disks have distinct volume numbers anyway.
*/
const int kBlkCount140 = 140*2;
if ((matchCount % (kBlkCount140)) == 0) {
int i, count;
bool found = false;
count = matchCount / kBlkCount140;
LOGI(" Sub #2: looking for %d 140K volumes",
matchCount / kBlkCount140);
for (i = 0; i < count; i++) {
DiskFS* pNewFS = NULL;
DiskImg* pNewImg = NULL;
LOGI(" Sub #2: looking for DOS volume at (%d)",
firstBlock + i * kBlkCount140);
dierr = FindSubVolume(firstBlock + i * kBlkCount140,
kBlkCount140, &pNewImg, &pNewFS);
if (dierr == kDIErrNone) {
AddSubVolumeToList(pNewImg, pNewFS);
MarkSubVolumeBlocks(firstBlock + i * kBlkCount140,
kBlkCount140);
found = true;
}
}
if (found)
return kDIErrNone;
}
/*
* Try #3: there are five 160K DOS 3.3 volumes here (which works out
* to exactly 800K). The first DOS volume loses early tracks as
* needed to accommodate the ProDOS directory and up to 28K of
* boot files.
*
* Because the first 160K volume starts at the front of the disk,
* we need to restrict this to non-ProDOS sub-volumes, or we'll see
* a "ghost" volume in the first position. This stuff is going to
* fail if we test for ProDOS before we check for DOS 3.3.
*/
const int kBlkCount160 = 160*2;
if (matchCount == 1537 || matchCount == 1593) {
int i, count;
bool found = false;
count = 1600 / kBlkCount160;
LOGI(" Sub #3: looking for %d 160K volumes",
matchCount / kBlkCount160);
for (i = 0; i < count; i++) {
DiskFS* pNewFS = NULL;
DiskImg* pNewImg = NULL;
LOGI(" Sub #3: looking for DOS volume at (%d)",
i * kBlkCount160);
dierr = FindSubVolume(i * kBlkCount160,
kBlkCount160, &pNewImg, &pNewFS);
if (dierr == kDIErrNone) {
if (pNewImg->GetFSFormat() == DiskImg::kFormatDOS33) {
AddSubVolumeToList(pNewImg, pNewFS);
if (i == 0)
MarkSubVolumeBlocks(firstBlock, kBlkCount160 - firstBlock);
else
MarkSubVolumeBlocks(i * kBlkCount160, kBlkCount160);
} else {
delete pNewFS;
delete pNewImg;
pNewFS = NULL;
pNewImg = NULL;
}
}
}
if (found)
return kDIErrNone;
}
return kDIErrNone;
}
/*
* Look for a sub-volume at the specified location.
*
* On success, "*ppDiskImg" and "*ppDiskFS" are newly-allocated objects
* of the appropriate kind.
*/
DIError DiskFSProDOS::FindSubVolume(long blockStart, long blockCount,
DiskImg** ppDiskImg, DiskFS** ppDiskFS)
{
DIError dierr = kDIErrNone;
DiskFS* pNewFS = NULL;
DiskImg* pNewImg = NULL;
pNewImg = new DiskImg;
if (pNewImg == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
dierr = pNewImg->OpenImage(fpImg, blockStart, blockCount);
if (dierr != kDIErrNone) {
LOGI(" Sub: OpenImage(%ld,%ld) failed (err=%d)",
blockStart, blockCount, dierr);
goto bail;
}
dierr = pNewImg->AnalyzeImage();
if (dierr != kDIErrNone) {
LOGI(" Sub: analysis failed (err=%d)", dierr);
goto bail;
}
if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown ||
pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown)
{
LOGI(" Sub: unable to identify filesystem");
dierr = kDIErrFilesystemNotFound;
goto bail;
}
/* open a DiskFS for the sub-image */
LOGI(" Sub DiskImg succeeded, opening DiskFS");
pNewFS = pNewImg->OpenAppropriateDiskFS();
if (pNewFS == NULL) {
LOGI(" Sub: OpenAppropriateDiskFS failed");
dierr = kDIErrUnsupportedFSFmt;
goto bail;
}
/* load the files from the sub-image */
dierr = pNewFS->Initialize(pNewImg, kInitFull);
if (dierr != kDIErrNone) {
LOGE(" Sub: error %d reading list of files from disk", dierr);
goto bail;
}
bail:
if (dierr != kDIErrNone) {
delete pNewFS;
delete pNewImg;
} else {
assert(pNewImg != NULL && pNewFS != NULL);
*ppDiskImg = pNewImg;
*ppDiskFS = pNewFS;
}
return dierr;
}
/*
* Mark the blocks used by a sub-volume as in-use.
*/
void DiskFSProDOS::MarkSubVolumeBlocks(long block, long count)
{
VolumeUsage::ChunkState cstate;
while (count--) {
if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) {
assert(false);
return;
}
assert(cstate.isMarkedUsed && !cstate.isUsed);
cstate.isUsed = true;
cstate.purpose = VolumeUsage::kChunkPurposeEmbedded;
if (fVolumeUsage.SetChunkState(block, &cstate) != kDIErrNone) {
assert(false);
return;
}
block++;
}
}
/*
* Put a ProDOS filesystem image on the specified DiskImg.
*/
DIError DiskFSProDOS::Format(DiskImg* pDiskImg, const char* volName)
{
DIError dierr = kDIErrNone;
const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0);
uint8_t 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 == NULL);
SetDiskImg(pDiskImg);
LOGI(" ProDOS formatting disk image");
/* write ProDOS blocks */
dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(),
DiskImg::kFormatGenericProDOSOrd, fpImg->GetSectorOrder());
if (dierr != kDIErrNone)
goto bail;
formatBlocks = pDiskImg->GetNumBlocks();
if (formatBlocks > 65536) {
LOGI(" ProDOS: rejecting format req blocks=%ld", formatBlocks);
assert(false);
return kDIErrInvalidArg;
}
if (formatBlocks == 65536) {
LOGI(" ProDOS: trimming FS size from 65536 to 65535");
formatBlocks = 65535;
}
/*
* We should now zero out the disk blocks, but on a 32MB volume that can
* take a little while. The blocks are zeroed for us when a disk is
* created, so this is really only needed if we're re-formatting an
* existing disk. CiderPress currently doesn't do that, so we're going
* to skip it here.
*/
// dierr = fpImg->ZeroImage();
LOGI(" ProDOS (not zeroing blocks)");
/*
* Start by writing blocks 0 and 1 (the boot blocks). This is done from
* a standard boot block image that happens to be essentially the same
* for all types of disks. (Apparently these blocks are only used when
* booting 5.25" disks?)
*/
dierr = WriteBootBlocks();
if (dierr != kDIErrNone)
goto bail;
/*
* Write the four-block disk volume entry. Start by writing the three
* empty ones (which only have the prev/next pointers), and finish by
* writing the first block, which has the volume directory header.
*/
int i;
memset(blkBuf, 0, sizeof(blkBuf));
for (i = kVolHeaderBlock+1; i < kVolHeaderBlock+kFormatVolDirNumBlocks; i++)
{
PutShortLE(&blkBuf[0x00], i-1);
if (i == kVolHeaderBlock+kFormatVolDirNumBlocks-1)
PutShortLE(&blkBuf[0x02], 0);
else
PutShortLE(&blkBuf[0x02], i+1);
dierr = fpImg->WriteBlock(i, blkBuf);
if (dierr != kDIErrNone) {
LOGI(" Format: block %d write failed (err=%d)", i, dierr);
goto bail;
}
}
char upperName[A2FileProDOS::kMaxFileName+1];
uint16_t lcFlags;
time_t now;
now = time(NULL);
/*
* Compute the lower-case flags, if desired. The test for "allowLowerCase"
* is probably bogus, because in most cases we just got created by the
* DiskImg and the app hasn't had time to set the "allow lower" flag.
* So it defaults to "enabled", which means the app needs to manually
* change the volume name to lower case.
*/
UpperCaseName(upperName, volName);
lcFlags = 0;
if (allowLowerCase)
lcFlags = GenerateLowerCaseBits(upperName, volName, false);
PutShortLE(&blkBuf[0x00], 0);
PutShortLE(&blkBuf[0x02], kVolHeaderBlock+1);
blkBuf[0x04] = (uint8_t)(strlen(upperName) | (A2FileProDOS::kStorageVolumeDirHeader << 4));
strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName);
PutLongLE(&blkBuf[0x16], A2FileProDOS::ConvertProDate(now));
PutShortLE(&blkBuf[0x1a], lcFlags);
PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(now));
blkBuf[0x20] = 0; // GS/OS uses 5?
/* min_version is zero */
blkBuf[0x22] = 0xe3; // access (format/rename/backup/write/read)
blkBuf[0x23] = 0x27; // entry_length: always $27
blkBuf[0x24] = 0x0d; // entries_per_block: always $0d
/* file_count is zero - does not include volume dir */
PutShortLE(&blkBuf[0x27], kVolHeaderBlock + kFormatVolDirNumBlocks); // bit_map_pointer
PutShortLE(&blkBuf[0x29], (uint16_t) formatBlocks); // total_blocks
dierr = fpImg->WriteBlock(kVolHeaderBlock, blkBuf);
if (dierr != kDIErrNone) {
LOGI(" Format: block %d write failed (err=%d)",
kVolHeaderBlock, dierr);
goto bail;
}
/* check our work, and set some object fields, by reading what we wrote */
dierr = LoadVolHeader();
if (dierr != kDIErrNone) {
LOGI(" GLITCH: couldn't read header we just wrote (err=%d)", dierr);
goto bail;
}
/*
* Generate the initial block usage map. The only entries in use are
* right at the start of the disk.
*/
CreateEmptyBlockMap();
/* don't do this -- assume they're going to call Initialize() later */
//ScanVolBitmap();
bail:
SetDiskImg(NULL); // shouldn't really be set by us
return dierr;
}
/*
* The standard boot block found on ProDOS disks. The same thing appears
* to be written to both 5.25" and 3.5" disks, with some modifications
* made for HD images.
*
* This is block 0; block 1 is either zeroed out or filled with a repeating
* pattern.
*/
const uint8_t gFloppyBlock0[512] = {
0x01, 0x38, 0xb0, 0x03, 0x4c, 0x32, 0xa1, 0x86, 0x43, 0xc9, 0x03, 0x08,
0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49, 0xa0,
0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e, 0xa9,
0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b, 0x48,
0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x63, 0xb1, 0x48, 0x99, 0x94, 0x09,
0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x1d, 0x09, 0xbd, 0x24,
0x09, 0x99, 0xf2, 0x09, 0xbd, 0x2b, 0x09, 0x9d, 0x7f, 0x0a, 0xca, 0x10,
0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9, 0xb0,
0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e, 0x84,
0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61, 0x85,
0x4b, 0x20, 0x12, 0x09, 0xb0, 0x68, 0xe6, 0x61, 0xe6, 0x61, 0xe6, 0x46,
0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01, 0x0c,
0xd0, 0x6d, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23, 0x0c,
0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9, 0x0a,
0xf0, 0x55, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x02, 0x09, 0x29, 0x0f, 0xa8,
0xb1, 0x4a, 0xd9, 0x02, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0x29, 0xf0,
0xc9, 0x20, 0xd0, 0x3b, 0xa0, 0x10, 0xb1, 0x4a, 0xc9, 0xff, 0xd0, 0x33,
0xc8, 0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00,
0x85, 0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20,
0x12, 0x09, 0xb0, 0x17, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e,
0xb1, 0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0xe7,
0x4c, 0x00, 0x20, 0x4c, 0x3f, 0x09, 0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f,
0x53, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xa5, 0x60,
0x85, 0x44, 0xa5, 0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24,
0x3f, 0x45, 0x47, 0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6,
0x2b, 0x18, 0x60, 0x4c, 0xbc, 0x09, 0xa9, 0x9f, 0x48, 0xa9, 0xff, 0x48,
0xa9, 0x01, 0xa2, 0x00, 0x4c, 0x79, 0xf4, 0x20, 0x58, 0xfc, 0xa0, 0x1c,
0xb9, 0x50, 0x09, 0x99, 0xae, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x4d, 0x09,
0xaa, 0xaa, 0xaa, 0xa0, 0xd5, 0xce, 0xc1, 0xc2, 0xcc, 0xc5, 0xa0, 0xd4,
0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0, 0xd2, 0xcf, 0xc4, 0xcf,
0xd3, 0xa0, 0xaa, 0xaa, 0xaa, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b,
0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9,
0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04,
0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5,
0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85,
0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6,
0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60,
0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85,
0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02,
0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09,
0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0,
0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t gHDBlock0[] = {
0x01, 0x38, 0xb0, 0x03, 0x4c, 0x1c, 0x09, 0x78, 0x86, 0x43, 0xc9, 0x03,
0x08, 0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49,
0xa0, 0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e,
0xa9, 0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b,
0x48, 0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x5e, 0xb1, 0x48, 0x99, 0x94,
0x09, 0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x32, 0x09, 0xbd,
0x39, 0x09, 0x99, 0xf2, 0x09, 0xbd, 0x40, 0x09, 0x9d, 0x7f, 0x0a, 0xca,
0x10, 0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9,
0xb0, 0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e,
0x84, 0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61,
0x85, 0x4b, 0x20, 0x27, 0x09, 0xb0, 0x66, 0xe6, 0x61, 0xe6, 0x61, 0xe6,
0x46, 0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01,
0x0c, 0xd0, 0x52, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23,
0x0c, 0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9,
0x0a, 0xf0, 0x71, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x20, 0x09, 0x29, 0x0f,
0xa8, 0xb1, 0x4a, 0xd9, 0x20, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0xa0,
0x16, 0xb1, 0x4a, 0x4a, 0x6d, 0x1f, 0x09, 0x8d, 0x1f, 0x09, 0xa0, 0x11,
0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00, 0x85,
0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20, 0x27,
0x09, 0xb0, 0x35, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e, 0xb1,
0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0x18, 0xa2,
0x01, 0xa9, 0x00, 0xa8, 0x91, 0x60, 0xc8, 0xd0, 0xfb, 0xe6, 0x61, 0xea,
0xea, 0xca, 0x10, 0xf4, 0xce, 0x1f, 0x09, 0xf0, 0x07, 0xd0, 0xd8, 0xce,
0x1f, 0x09, 0xd0, 0xca, 0x58, 0x4c, 0x00, 0x20, 0x4c, 0x47, 0x09, 0x02,
0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f, 0x53, 0xa5, 0x60, 0x85, 0x44, 0xa5,
0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24, 0x3f, 0x45, 0x47,
0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6, 0x2b, 0x18, 0x60,
0x4c, 0xbc, 0x09, 0x20, 0x58, 0xfc, 0xa0, 0x14, 0xb9, 0x58, 0x09, 0x99,
0xb1, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x55, 0x09, 0xd5, 0xce, 0xc1, 0xc2,
0xcc, 0xc5, 0xa0, 0xd4, 0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0,
0xd2, 0xcf, 0xc4, 0xcf, 0xd3, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b,
0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9,
0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04,
0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5,
0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85,
0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6,
0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60,
0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85,
0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02,
0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09,
0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0,
0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
/*
* Write the ProDOS boot blocks onto the disk image.
*/
DIError DiskFSProDOS::WriteBootBlocks(void)
{
DIError dierr;
uint8_t block0[512];
uint8_t block1[512];
bool isHD;
assert(fpImg->GetHasBlocks());
if (fpImg->GetNumBlocks() == 280 || fpImg->GetNumBlocks() == 1600)
isHD = false;
else
isHD = true;
if (isHD) {
memcpy(block0, gHDBlock0, sizeof(block0));
// repeating 0x42 0x48 pattern
int i;
uint8_t* ucp;
for (i = 0, ucp = block1; i < (int)sizeof(block1); i++)
*ucp++ = 0x42 + 6 * (i & 0x01);
} else {
memcpy(block0, gFloppyBlock0, sizeof(block0));
memset(block1, 0, sizeof(block1));
}
dierr = fpImg->WriteBlock(0, block0);
if (dierr != kDIErrNone) {
LOGI(" WriteBootBlocks: block0 write failed (err=%d)", dierr);
return dierr;
}
dierr = fpImg->WriteBlock(1, block1);
if (dierr != kDIErrNone) {
LOGI(" WriteBootBlocks: block1 write failed (err=%d)", dierr);
return dierr;
}
return kDIErrNone;
}
/*
* Create a new, empty file. There are three different kinds of files we
* need to be able to handle:
* (1) Standard file. Create the directory entry and an empty "seedling"
* file with one block allocated. It does not appear that "sparse"
* allocation applies to seedlings.
* (2) Extended file. Create the directory entry, the extended key block,
* and allocate one seedling block for each fork.
* (3) Subdirectory. Allocate a block for the subdir and fill in the
* details in the subdir header.
*
* In all cases we need to add a new directory entry as well.
*
* By not flushing the updated block usage map and the updated directory
* block(s) until we're done, we can abort our changes at any time if we
* encounter a damaged sector or run out of disk space. We do need to be
* careful when updating our internal copies of things like file storage
* types and lengths, updating them only after everything else has
* succeeded.
*
* NOTE: if we detect an empty directory holder, "*ppNewFile" does NOT
* end up pointing at a file.
*
* NOTE: kParm_CreateUnique does *not* apply to creating subdirectories.
*/
DIError DiskFSProDOS::CreateFile(const CreateParms* pParms, A2File** ppNewFile)
{
DIError dierr = kDIErrNone;
char* normalizedPath = NULL;
char* basePath = NULL;
char* fileName = NULL;
A2FileProDOS* pSubdir = NULL;
A2FileDescr* pOpenSubdir = NULL;
A2FileProDOS* pNewFile = NULL;
uint8_t* subdirBuf = NULL;
const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0);
const bool createUnique = (GetParameter(kParm_CreateUnique) != 0);
char upperName[A2FileProDOS::kMaxFileName+1];
char lowerName[A2FileProDOS::kMaxFileName+1];
if (fpImg->GetReadOnly())
return kDIErrAccessDenied;
if (!fDiskIsGood)
return kDIErrBadDiskImage;
assert(pParms != NULL);
assert(pParms->pathName != NULL);
assert(pParms->storageType == A2FileProDOS::kStorageSeedling ||
pParms->storageType == A2FileProDOS::kStorageExtended ||
pParms->storageType == A2FileProDOS::kStorageDirectory);
// kStorageVolumeDirHeader not allowed -- that's created by Format
LOGI(" ProDOS ---v--- CreateFile '%s'", pParms->pathName);
*ppNewFile = NULL;
/*
* Normalize the pathname so that all components are ProDOS-safe
* and separated by ':'.
*/
assert(pParms->pathName != NULL);
dierr = DoNormalizePath(pParms->pathName, pParms->fssep,
&normalizedPath);
if (dierr != kDIErrNone)
goto bail;
assert(normalizedPath != NULL);
/*
* Split the base path and filename apart.
*/
char* cp;
cp = strrchr(normalizedPath, A2FileProDOS::kFssep);
if (cp == NULL) {
assert(basePath == NULL);
fileName = normalizedPath;
} else {
fileName = new char[strlen(cp+1) +1];
strcpy(fileName, cp+1);
*cp = '\0';
basePath = normalizedPath;
}
normalizedPath = NULL; // either fileName or basePath points here now
assert(fileName != NULL);
//LOGI(" ProDOS normalized to '%s':'%s'",
// basePath == NULL ? "" : basePath, fileName);
/*
* Open the base path. If it doesn't exist, create it recursively.
*/
if (basePath != NULL) {
LOGI(" ProDOS Creating '%s' in '%s'", fileName, basePath);
/* open the named subdir, creating it if it doesn't exist */
pSubdir = (A2FileProDOS*)GetFileByName(basePath);
if (pSubdir == NULL) {
LOGI(" ProDOS Creating subdir '%s'", basePath);
A2File* pNewSub;
CreateParms newDirParms;
newDirParms.pathName = basePath;
newDirParms.fssep = A2FileProDOS::kFssep;
newDirParms.storageType = A2FileProDOS::kStorageDirectory;
newDirParms.fileType = kTypeDIR; // 0x0f
newDirParms.auxType = 0;
newDirParms.access = 0xe3; // unlocked, backup bit set
newDirParms.createWhen = newDirParms.modWhen = time(NULL);
dierr = this->CreateFile(&newDirParms, &pNewSub);
if (dierr != kDIErrNone)
goto bail;
assert(pNewSub != NULL);
pSubdir = (A2FileProDOS*) pNewSub;
}
/*
* And now the annoying part. We need to reconstruct basePath out
* of the filenames actually present, rather than relying on the
* argument passed in. That's because some directories might have
* lower-case flags and some might not, and we do case-insensitive
* comparisons. It's not crucial for our inner workings, but the
* linear file list in the DiskFS should have accurate strings.
* (It'll work just fine, but the display might show the wrong values
* for parent directories until they reload the disk.)
*
* On the bright side, we know exactly how long the string needs
* to be, so we can just stomp on it in place. Assuming, of course,
* that the filename created matches up with what the filename
* normalizer came up with, which we can guarantee since (a) everybody
* uses the same normalizer and (b) the "uniqueify" stuff doesn't
* kick in for subdirs because we wouldn't be creating a new subdir
* if it didn't already exist.
*
* This is essentially the same as RegeneratePathName(), but that's
* meant for a situation where the filename already exists.
*/
A2FileProDOS* pBaseDir = pSubdir;
int basePathLen = strlen(basePath);
while (!pBaseDir->IsVolumeDirectory()) {
const char* fixedName = pBaseDir->GetFileName();
int fixedLen = strlen(fixedName);
if (fixedLen > basePathLen) {
assert(false);
break;
}
assert(basePathLen == fixedLen ||
*(basePath + (basePathLen-fixedLen-1)) == kDIFssep);
memcpy(basePath + (basePathLen-fixedLen), fixedName, fixedLen);
basePathLen -= fixedLen+1;
pBaseDir = (A2FileProDOS*) pBaseDir->GetParent();
assert(pBaseDir != NULL);
}
// check the math
if (pSubdir->IsVolumeDirectory())
assert(basePathLen == 0);
else
assert(basePathLen == -1);
} else {
/* open the volume directory */
LOGI(" ProDOS Creating '%s' in volume dir", fileName);
/* volume dir must be first in the list */
pSubdir = (A2FileProDOS*) GetNextFile(NULL);
assert(pSubdir != NULL);
assert(pSubdir->IsVolumeDirectory());
}
if (pSubdir == NULL) {
LOGI(" ProDOS Unable to open subdir '%s'", basePath);
dierr = kDIErrFileNotFound;
goto bail;
}
/*
* Load the block usage map into memory. All changes, to the end of this
* function, are made to the in-memory copy and can be "undone" by simply
* throwing the temporary map away.
*/
dierr = LoadVolBitmap();
if (dierr != kDIErrNone)
return dierr;
/*
* Load the subdir or volume dir into memory, and alloc a new directory
* entry.
*/
dierr = pSubdir->Open(&pOpenSubdir, false);
if (dierr != kDIErrNone)
goto bail;
uint8_t* dirEntryPtr;
long dirLen;
uint16_t dirBlock, dirKeyBlock;
int dirEntrySlot;
dierr = AllocDirEntry(pOpenSubdir, &subdirBuf, &dirLen, &dirEntryPtr,
&dirKeyBlock, &dirEntrySlot, &dirBlock);
if (dierr != kDIErrNone)
goto bail;
assert(subdirBuf != NULL);
assert(dirLen > 0);
assert(dirKeyBlock > 0);
assert(dirEntrySlot >= 0);
assert(dirBlock > 0);
/*
* Create a copy of the filename with everything in upper case and spaces
* changed to periods.
*/
UpperCaseName(upperName, fileName);
/*
* Make the name unique within the current directory. This requires
* appending digits until the name doesn't match any others.
*
* The filename buffer ("upperName") must be able to hold kMaxFileName+1
* chars. It will be modified in place.
*/
if (createUnique &&
pParms->storageType != A2FileProDOS::kStorageDirectory)
{
MakeFileNameUnique(subdirBuf, dirLen, upperName);
} else {
/* check to see if it already exists */
if (NameExistsInDir(subdirBuf, dirLen, upperName)) {
if (pParms->storageType == A2FileProDOS::kStorageDirectory)
dierr = kDIErrDirectoryExists;
else
dierr = kDIErrFileExists;
goto bail;
}
}
/*
* Allocate file storage and initialize:
* - For directory, a single block with the directory header.
* - For seedling, an empty block.
* - For extended, an extended key block entry and two empty blocks.
*/
long keyBlock;
int blocksUsed;
int newEOF;
keyBlock = -1;
blocksUsed = newEOF = -1;
dierr = AllocInitialFileStorage(pParms, upperName, dirBlock,
dirEntrySlot, &keyBlock, &blocksUsed, &newEOF);
if (dierr != kDIErrNone)
goto bail;
assert(blocksUsed > 0);
assert(keyBlock > 0);
assert(newEOF >= 0);
/*
* Fill out the newly-created directory entry pointed to by "dirEntryPtr".
*
* ProDOS filenames are always stored in upper case. ProDOS 8 v1.8 and
* later allow lower-case names with '.' converting to ' '. We optionally
* set the flags here, using the original file name to decide which parts
* are lower case. (Some parts of the original may have been stomped
* when the name was made unique, so we need to watch for that.)
*/
dirEntryPtr[0x00] = (uint8_t)((pParms->storageType << 4) | strlen(upperName));
strncpy((char*) &dirEntryPtr[0x01], upperName, A2FileProDOS::kMaxFileName);
if (pParms->fileType >= 0 && pParms->fileType <= 0xff)
dirEntryPtr[0x10] = (uint8_t) pParms->fileType;
else
dirEntryPtr[0x10] = 0; // HFS long type?
PutShortLE(&dirEntryPtr[0x11], (uint16_t) keyBlock);
PutShortLE(&dirEntryPtr[0x13], blocksUsed);
PutShortLE(&dirEntryPtr[0x15], newEOF);
dirEntryPtr[0x17] = 0; // high byte of EOF
PutLongLE(&dirEntryPtr[0x18], A2FileProDOS::ConvertProDate(pParms->createWhen));
if (allowLowerCase) {
uint16_t lcBits;
lcBits = GenerateLowerCaseBits(upperName, fileName, false);
GenerateLowerCaseName(upperName, lowerName, lcBits, false);
lowerName[strlen(upperName)] = '\0';
PutShortLE(&dirEntryPtr[0x1c], lcBits);
} else {
strcpy(lowerName, upperName);
PutShortLE(&dirEntryPtr[0x1c], 0); // version, min_version
}
dirEntryPtr[0x1e] = pParms->access;
if (pParms->auxType >= 0 && pParms->auxType <= 0xffff)
PutShortLE(&dirEntryPtr[0x1f], (uint16_t) pParms->auxType);
else
PutShortLE(&dirEntryPtr[0x1f], 0);
PutLongLE(&dirEntryPtr[0x21], A2FileProDOS::ConvertProDate(pParms->modWhen));
PutShortLE(&dirEntryPtr[0x25], dirKeyBlock);
/*
* Write updated directory. If this succeeds, we can no longer undo
* what we have done by simply bailing. If this fails partway through,
* we might have a corrupted disk, so it's best to ensure that it's not
* going to fail before we call.
*
* Assuming this isn't a nibble image with I/O errors, the only way we
* can really fail is by running out of disk space. The block has been
* pre-allocated, so this should always work.
*/
dierr = pOpenSubdir->Write(subdirBuf, dirLen);
if (dierr != kDIErrNone) {
LOGI(" ProDOS directory write failed (dirLen=%ld)", dirLen);
goto bail;
}
/*
* Flush updated block usage map.
*/
dierr = SaveVolBitmap();
if (dierr != kDIErrNone)
goto bail;
/*
* Success!
*
* Create an A2File entry for this, and add it to the list. The calls
* below will re-process some of what we just created, which is slightly
* inefficient but helps guarantee that we aren't creating bogus data
* structures that won't match what we see when the disk is reloaded.
*
* - Regen or update internal VolumeUsage map?? Throw it away or mark
* it as invalid?
*/
pNewFile = new A2FileProDOS(this);
A2FileProDOS::DirEntry* pEntry;
pEntry = &pNewFile->fDirEntry;
A2FileProDOS::InitDirEntry(pEntry, dirEntryPtr);
pNewFile->fParentDirBlock = dirBlock;
pNewFile->fParentDirIdx = (dirEntrySlot-1) % kEntriesPerBlock;
pNewFile->fSparseDataEof = 0;
pNewFile->fSparseRsrcEof = 0;
/*
* Get the properly-cased filename for the file list. We already have
* a name in "lowerName", but it doesn't take AppleWorks aux type
* case stuff into account. If necessary, deal with it now.
*/
if (A2FileProDOS::UsesAppleWorksAuxType(pNewFile->fDirEntry.fileType)) {
DiskFSProDOS::GenerateLowerCaseName(pNewFile->fDirEntry.fileName,
lowerName, pNewFile->fDirEntry.auxType, true);
}
pNewFile->SetPathName(basePath == NULL ? "" : basePath, lowerName);
if (pEntry->storageType == A2FileProDOS::kStorageExtended) {
dierr = ReadExtendedInfo(pNewFile);
if (dierr != kDIErrNone) {
LOGI(" ProDOS GLITCH: readback of extended block failed!");
delete pNewFile;
goto bail;
}
}
pNewFile->SetParent(pSubdir);
//pNewFile->Dump();
/*
* Because we're hierarchical, and we guarantee that the contents of
* subdirectories are grouped together, we must insert the file into an
* appropriate place in the list rather than just throwing it onto the
* end.
*
* The proper location for the new file in the linear list is after the
* previous file in our subdir. If we're the first item in the subdir,
* we get added right after the parent. If not, we need to scan, starting
* from the parent, for an entry in the file list whose key block pointer
* matches that of the previous item in the list.
*
* We wouldn't be this far if the disk were damaged, so we don't have to
* worry too much about weirdness. The directory entry allocator always
* returns the first available, so we know the previous entry is valid.
*/
uint8_t* prevDirEntryPtr;
prevDirEntryPtr = GetPrevDirEntry(subdirBuf, dirEntryPtr);
if (prevDirEntryPtr == NULL) {
/* previous entry is volume or subdir header */
InsertFileInList(pNewFile, pNewFile->GetParent());
LOGI("Inserted '%s' after '%s'",
pNewFile->GetPathName(), pNewFile->GetParent()->GetPathName());
} else {
/* dig out the key block pointer and find the matching file */
uint16_t prevKeyBlock;
assert((prevDirEntryPtr[0x00] & 0xf0) != 0); // verify storage type
prevKeyBlock = GetShortLE(&prevDirEntryPtr[0x11]);
A2File* pPrev;
pPrev = FindFileByKeyBlock(pNewFile->GetParent(), prevKeyBlock);
if (pPrev == NULL) {
/* should be impossible! */
assert(false);
AddFileToList(pNewFile);
} else {
/* insert the new file in the list after the previous file */
InsertFileInList(pNewFile, pPrev);
}
}
// LOGI("LIST NOW:");
// DumpFileList();
*ppNewFile = pNewFile;
pNewFile = NULL;
bail:
delete pNewFile;
if (pOpenSubdir != NULL)
pOpenSubdir->Close(); // writes updated dir entry in parent dir
FreeVolBitmap();
delete[] normalizedPath;
delete[] subdirBuf;
delete[] fileName;
delete[] basePath;
LOGI(" ProDOS ---^--- CreateFile '%s' DONE", pParms->pathName);
return dierr;
}
/*
* Run through the DiskFS file list, looking for an entry with a matching
* key block.
*/
A2File* DiskFSProDOS::FindFileByKeyBlock(A2File* pStart, uint16_t keyBlock)
{
while (pStart != NULL) {
A2FileProDOS* pPro = (A2FileProDOS*) pStart;
if (pPro->fDirEntry.keyPointer == keyBlock)
return pStart;
pStart = GetNextFile(pStart);
}
return NULL;
}
/*
* Allocate the initial storage (key blocks, directory header) for a new file.
*
* Output values are the key block for the new file, the number of blocks
* used, and an EOF value.
*
* "upperName" is the upper-case name for the file. "dirBlock" and
* "dirEntrySlot" refer to the entry in the higher-level directory for this
* file, and are only needed when creating a new subdir (because the first
* entry in a subdir points to its entry in the parent dir).
*/
DIError DiskFSProDOS::AllocInitialFileStorage(const CreateParms* pParms,
const char* upperName, uint16_t dirBlock, int dirEntrySlot,
long* pKeyBlock, int* pBlocksUsed, int* pNewEOF)
{
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
long keyBlock;
int blocksUsed;
int newEOF;
blocksUsed = -1;
keyBlock = -1;
newEOF = 0;
memset(blkBuf, 0, sizeof(blkBuf));
if (pParms->storageType == A2FileProDOS::kStorageSeedling) {
keyBlock = AllocBlock();
if (keyBlock == -1) {
dierr = kDIErrDiskFull;
goto bail;
}
blocksUsed = 1;
/* write zeroed block */
dierr = fpImg->WriteBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
} else if (pParms->storageType == A2FileProDOS::kStorageExtended) {
long dataBlock, rsrcBlock;
dataBlock = AllocBlock();
rsrcBlock = AllocBlock();
keyBlock = AllocBlock();
if (dataBlock < 0 || rsrcBlock < 0 || keyBlock < 0) {
dierr = kDIErrDiskFull;
goto bail;
}
blocksUsed = 3;
newEOF = kBlkSize;
/* write zeroed block */
dierr = fpImg->WriteBlock(dataBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
dierr = fpImg->WriteBlock(rsrcBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
/* fill in extended key block details */
blkBuf[0x00] = blkBuf[0x100] = A2FileProDOS::kStorageSeedling;
PutShortLE(&blkBuf[0x01], (uint16_t) dataBlock);
PutShortLE(&blkBuf[0x101], (uint16_t) rsrcBlock);
blkBuf[0x03] = blkBuf[0x103] = 1; // blocks used (lo byte)
/* 3 bytes at 0x05 hold EOF, currently 0 */
dierr = fpImg->WriteBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
} else if (pParms->storageType == A2FileProDOS::kStorageDirectory) {
keyBlock = AllocBlock();
if (keyBlock == -1) {
dierr = kDIErrDiskFull;
goto bail;
}
blocksUsed = 1;
newEOF = kBlkSize;
/* fill in directory header fields */
// 0x00: prev, set to zero
// 0x02: next, set to zero
blkBuf[0x04] = (uint8_t)((A2FileProDOS::kStorageSubdirHeader << 4) | strlen(upperName));
strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName);
blkBuf[0x14] = 0x76; // 0x75 under old P8, 0x76 under GS/OS
PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(pParms->createWhen));
blkBuf[0x20] = 5; // 0 under 1.0, 3 under v1.4?, 5 under GS/OS
blkBuf[0x21] = 0;
blkBuf[0x22] = pParms->access;
blkBuf[0x23] = kEntryLength;
blkBuf[0x24] = kEntriesPerBlock;
PutShortLE(&blkBuf[0x25], 0); // file count
PutShortLE(&blkBuf[0x27], dirBlock);
blkBuf[0x29] = (uint8_t) dirEntrySlot;
blkBuf[0x2a] = kEntryLength; // the parent dir's entry length
dierr = fpImg->WriteBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
} else {
assert(false);
dierr = kDIErrInternal;
goto bail;
}
*pKeyBlock = keyBlock;
*pBlocksUsed = blocksUsed;
*pNewEOF = newEOF;
bail:
return dierr;
}
/*
* Scan for damaged files and mysterious or conflicting block usage map
* entries.
*
* Appends some entries to the DiskImg notes, so this should only be run
* once per DiskFS.
*
* This function doesn't set anything; it's effectively "const" except
* that LoadVolBitmap is inherently non-const.
*
* Returns "true" if disk appears to be perfect, "false" otherwise.
*/
bool DiskFSProDOS::CheckDiskIsGood(void)
{
DIError dierr;
bool result = true;
int i;
if (fEarlyDamage)
result = false;
dierr = LoadVolBitmap();
if (dierr != kDIErrNone)
goto bail;
/*
* Check the system blocks to see if any of them are marked as free.
* If so, refuse to write to this disk.
*/
if (!GetBlockUseEntry(0) || !GetBlockUseEntry(1)) {
fpImg->AddNote(DiskImg::kNoteWarning, "Block 0/1 marked as free.");
result = false;
}
for (i = GetNumBitmapBlocks(); i > 0; i--) {
if (!GetBlockUseEntry(fBitMapPointer + i -1)) {
fpImg->AddNote(DiskImg::kNoteWarning,
"One or more bitmap blocks are marked as free.");
result = false;
break;
}
}
/*
* Check for used blocks that aren't marked in-use.
*
* This requires that VolumeUsage be accurate. Since this function is
* only run during initial startup, any later deviation between VU and
* the block use map is irrelevant.
*/
VolumeUsage::ChunkState cstate;
long blk, notMarked, extraUsed, conflicts;
notMarked = extraUsed = conflicts = 0;
for (blk = 0; blk < fVolumeUsage.GetNumChunks(); blk++) {
dierr = fVolumeUsage.GetChunkState(blk, &cstate);
if (dierr != kDIErrNone) {
fpImg->AddNote(DiskImg::kNoteWarning,
"Internal volume usage error on blk=%ld.", blk);
result = false;
goto bail;
}
if (cstate.isUsed && !cstate.isMarkedUsed)
notMarked++;
if (!cstate.isUsed && cstate.isMarkedUsed)
extraUsed++;
if (cstate.purpose == VolumeUsage::kChunkPurposeConflict)
conflicts++;
}
if (extraUsed > 0) {
fpImg->AddNote(DiskImg::kNoteInfo,
"%ld block%s marked used but not part of any file.",
extraUsed, extraUsed == 1 ? " is" : "s are");
// not a problem, really
}
if (notMarked > 0) {
fpImg->AddNote(DiskImg::kNoteWarning,
"%ld block%s used by files but not marked used.",
notMarked, notMarked == 1 ? " is" : "s are");
result = false; // very bad -- any change could trash files
}
if (conflicts > 0) {
fpImg->AddNote(DiskImg::kNoteWarning,
"%ld block%s used by more than one file.",
conflicts, conflicts == 1 ? " is" : "s are");
result = false; // kinda bad -- file deletion leads to trouble
}
/*
* Check for bits set past the end of the actually-needed bits. For
* some reason P8 and GS/OS both examine these bits, and GS/OS will
* freak out completely and claim the disk is unrecognizeable ("would
* you like to format?") if they're set.
*/
if (ScanForExtraEntries()) {
fpImg->AddNote(DiskImg::kNoteWarning,
"Blocks past the end of the disk are marked 'in use' in the"
" volume bitmap.");
/* don't flunk the disk just for this */
}
/*
* Scan for "damaged" or "suspicious" files diagnosed earlier.
*/
bool damaged, suspicious;
ScanForDamagedFiles(&damaged, &suspicious);
if (damaged) {
fpImg->AddNote(DiskImg::kNoteWarning,
"One or more files are damaged.");
result = false;
} else if (suspicious) {
fpImg->AddNote(DiskImg::kNoteWarning,
"One or more files look suspicious.");
result = false;
}
bail:
FreeVolBitmap();
return result;
}
/*
* Test a string for validity as a ProDOS volume name. Syntax is the same as
* ProDOS file names, but we also disallow spaces.
*/
/*static*/ bool DiskFSProDOS::IsValidVolumeName(const char* name)
{
assert((int) A2FileProDOS::kMaxFileName == (int) kMaxVolumeName);
if (!IsValidFileName(name))
return false;
while (*name != '\0') {
if (*name++ == ' ')
return false;
}
return true;
}
/*
* Test a string for validity as a ProDOS file name. Names may be 1-15
* characters long, must start with a letter, and may contain letters and
* digits.
*
* Lower case and spaces (a/k/a lower-case '.') are accepted. Trailing
* spaces are not allowed.
*/
/*static*/ bool DiskFSProDOS::IsValidFileName(const char* name)
{
if (name == NULL) {
assert(false);
return false;
}
/* must be 1-15 characters long */
if (name[0] == '\0')
return false;
if (strlen(name) > A2FileProDOS::kMaxFileName)
return false;
/* must begin with letter; this also catches zero-length filenames */
if (toupper(name[0]) < 'A' || toupper(name[0]) > 'Z')
return false;
/* no trailing spaces */
if (name[strlen(name)-1] == ' ')
return false;
/* must be A-Za-z 0-9 '.' ' ' */
name++;
while (*name != '\0') {
if (!( (toupper(*name) >= 'A' && toupper(*name) <= 'Z') ||
(*name >= '0' && *name <= '9') ||
(*name == '.') ||
(*name == ' ')
))
{
return false;
}
name++;
}
return true;
}
/*
* Generate lower case flags by comparing "upperName" to "lowerName".
*
* It's okay for "lowerName" to be longer than "upperName". The extra chars
* are just ignored. Similarly, "lowerName" does not need to be
* null-terminated. "lowerName" does need to point to storage with at least
* as many valid bytes as "upperName", though, or we could crash.
*
* Returns the mask to use in a ProDOS dir. If "forAppleWorks" is set to
* "true", the mask is modified for use with an AppleWorks aux type.
*/
/*static*/ uint16_t DiskFSProDOS::GenerateLowerCaseBits(const char* upperName,
const char* lowerName, bool forAppleWorks)
{
uint16_t caseMask = 0x8000;
uint16_t caseBit = 0x8000;
int len, i;
char lowch;
len = strlen(upperName);
assert(len <= A2FileProDOS::kMaxFileName);
for (i = 0; i < len; i++) {
caseBit >>= 1;
lowch = A2FileProDOS::NameToLower(upperName[i]);
if (lowch == lowerName[i])
caseMask |= caseBit;
}
if (forAppleWorks) {
uint16_t adjusted;
caseMask <<= 1;
adjusted = caseMask << 8 | caseMask >> 8;
return adjusted;
} else {
if (caseMask == 0x8000)
return 0; // all upper case, don't freak out pre-v1.8
else
return caseMask;
}
}
/*
* Generate the lower-case version of a ProDOS filename, using the supplied
* lower case flags. "lowerName" must be able to hold 15 chars (enough for
* a filename or volname).
*
* The string will NOT be null-terminated, but the output buffer will be padded
* with NULs out to the maximum filename len. This makes it suitable for
* copying directly into directory block buffers.
*
* It's okay to pass the same buffer for "upperName" and "lowerName".
*
* "lcFlags" is either ProDOS directory flags or AppleWorks aux type flags,
* depending on the value of "fromAppleWorks".
*/
/*static*/ void DiskFSProDOS::GenerateLowerCaseName(const char* upperName,
char* lowerName, uint16_t lcFlags, bool fromAppleWorks)
{
int nameLen = strlen(upperName);
int bit;
assert(nameLen <= A2FileProDOS::kMaxFileName);
if (fromAppleWorks) {
/* handle AppleWorks lower-case-in-auxtype */
uint16_t caseMask = // swap bytes
(lcFlags << 8) | (lcFlags >> 8);
for (bit = 0; bit < nameLen ; bit++) {
if ((caseMask & 0x8000) != 0)
lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]);
else
lowerName[bit] = upperName[bit];
caseMask <<= 1;
}
for ( ; bit < A2FileProDOS::kMaxFileName; bit++)
lowerName[bit] = '\0';
} else {
/* handle lower-case conversion; see GS/OS tech note #8 */
if (lcFlags != 0 && !(lcFlags & 0x8000)) {
// Should be zero or 0x8000 plus other bits; shouldn't be
// bunch of bits without 0x8000 or 0x8000 by itself. Not
// really a problem, just unexpected.
assert(false);
memcpy(lowerName, upperName, A2FileProDOS::kMaxFileName);
return;
}
for (bit = 0; bit < nameLen; bit++) {
lcFlags <<= 1;
if ((lcFlags & 0x8000) != 0)
lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]);
else
lowerName[bit] = upperName[bit];
}
}
for ( ; bit < A2FileProDOS::kMaxFileName; bit++)
lowerName[bit] = '\0';
}
/*
* Normalize a ProDOS path. Invokes DoNormalizePath and handles the buffer
* management (if the normalized path doesn't fit in "*pNormalizedBufLen"
* bytes, we set "*pNormalizedBufLen to the required length).
*
* This is invoked from the generalized "add" function in CiderPress, which
* doesn't want to understand the ins and outs of ProDOS pathnames.
*/
DIError DiskFSProDOS::NormalizePath(const char* path, char fssep,
char* normalizedBuf, int* pNormalizedBufLen)
{
DIError dierr = kDIErrNone;
char* normalizedPath = NULL;
int len;
assert(pNormalizedBufLen != NULL);
assert(normalizedBuf != NULL || *pNormalizedBufLen == 0);
dierr = DoNormalizePath(path, fssep, &normalizedPath);
if (dierr != kDIErrNone)
goto bail;
assert(normalizedPath != NULL);
len = strlen(normalizedPath);
if (normalizedBuf == NULL || *pNormalizedBufLen <= len) {
/* too short */
dierr = kDIErrDataOverrun;
} else {
/* fits */
strcpy(normalizedBuf, normalizedPath);
}
*pNormalizedBufLen = len+1; // alloc room for the '\0'
bail:
delete[] normalizedPath;
return dierr;
}
/*
* Normalize a ProDOS path. This requires separating each path component
* out, making it ProDOS-compliant, and then putting it back in.
* The fssep could be anything, so we need to change it to kFssep.
*
* We don't try to identify duplicates here. If more than one subdir maps
* to the same thing, then you're just going to end up with lots of files
* in the same subdir. If this is unacceptable then it will have to be
* fixed at a higher level.
*
* Lower-case letters and spaces are left in place. They're expected to
* be removed later.
*
* The caller must delete[] "*pNormalizedPath".
*/
DIError DiskFSProDOS::DoNormalizePath(const char* path, char fssep,
char** pNormalizedPath)
{
DIError dierr = kDIErrNone;
char* workBuf = NULL;
char* partBuf = NULL;
char* outputBuf = NULL;
char* start;
char* end;
char* outPtr;
assert(path != NULL);
workBuf = new char[strlen(path)+1];
partBuf = new char[strlen(path)+1 +1]; // need +1 for prepending letter
outputBuf = new char[strlen(path) * 2];
if (workBuf == NULL || partBuf == NULL || outputBuf == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
strcpy(workBuf, path);
outputBuf[0] = '\0';
outPtr = outputBuf;
start = workBuf;
while (*start != '\0') {
//char* origStart = start; // need for debug msg
int partIdx;
if (fssep == '\0') {
end = NULL;
} else {
end = strchr(start, fssep);
if (end != NULL)
*end = '\0';
}
partIdx = 0;
/*
* Skip over everything up to the first letter. If we encounter a
* number or a '\0' first, insert a leading letter.
*/
while (*start != '\0') {
if (toupper(*start) >= 'A' && toupper(*start) <= 'Z') {
partBuf[partIdx++] = *start++;
break;
}
if (*start >= '0' && *start <= '9') {
partBuf[partIdx++] = 'A';
break;