ciderpress/diskimg/ProDOS.cpp

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;
}
start++;
}
if (partIdx == 0)
partBuf[partIdx++] = 'Z';
/*
* Continue copying, dropping all illegal chars.
*/
while (*start != '\0') {
if ((toupper(*start) >= 'A' && toupper(*start) <= 'Z') ||
(*start >= '0' && *start <= '9') ||
(*start == '.') ||
(*start == ' ') )
{
partBuf[partIdx++] = *start++;
} else {
start++;
}
}
/*
* Truncate at 15 chars, preserving anything that looks like a
* filename extension. "partIdx" represents the length of the
* string at this point. "partBuf" holds the string, which we
* want to null-terminate before proceeding.
*/
partBuf[partIdx] = '\0';
if (partIdx > A2FileProDOS::kMaxFileName) {
const char* pDot = strrchr(partBuf, '.');
//int DEBUGDOTLEN = pDot - partBuf;
if (pDot != NULL && partIdx - (pDot-partBuf) <= kMaxExtensionLen) {
int dotLen = partIdx - (pDot-partBuf);
memmove(partBuf + (A2FileProDOS::kMaxFileName - dotLen),
pDot, dotLen); // don't use memcpy, move might overlap
}
partIdx = A2FileProDOS::kMaxFileName;
}
partBuf[partIdx] = '\0';
//LOGI(" ProDOS Converted component '%s' to '%s'",
// origStart, partBuf);
if (outPtr != outputBuf)
*outPtr++ = A2FileProDOS::kFssep;
strcpy(outPtr, partBuf);
outPtr += partIdx;
/*
* Continue with next segment.
*/
if (end == NULL)
break;
start = end+1;
}
*outPtr = '\0';
LOGI(" ProDOS Converted path '%s' to '%s' (fssep='%c')",
path, outputBuf, fssep);
assert(*outputBuf != '\0');
*pNormalizedPath = outputBuf;
outputBuf = NULL;
bail:
delete[] workBuf;
delete[] partBuf;
delete[] outputBuf;
return dierr;
}
/*
* Create a copy of the filename with everything in upper case and spaces
* changed to periods.
*
* "upperName" must be a buffer that holds at least kMaxFileName+1 characters.
* If "name" is longer than kMaxFileName, it will be truncated.
*/
void DiskFSProDOS::UpperCaseName(char* upperName, const char* name)
{
int i;
for (i = 0; i < A2FileProDOS::kMaxFileName; i++) {
char ch = name[i];
if (ch == '\0')
break;
else if (ch == ' ')
upperName[i] = '.';
else
upperName[i] = toupper(ch);
}
/* null terminate with prejudice -- we memcpy this buffer into subdirs */
for ( ; i <= A2FileProDOS::kMaxFileName; i++)
upperName[i] = '\0';
}
/*
* Allocate a new directory entry. We start by reading the entire thing
* into memory. If the current set of allocated directory blocks is full,
* and we're not operating on the volume dir, we extend the directory.
*
* This just allocates the space; it does not fill in any details, except
* for the prev/next block pointers and the file count in the header. (One
* small exception: if we have to extend the directory, the "prev/next" fields
* of the new block will be filled in.)
*
* The volume in-use block map must be loaded before this is called. If
* this needs to extend the directory, a new block will be allocated.
*
* Returns a pointer to the new entry, and a whole bunch of other stuff:
* "ppDir" gets a pointer to newly-allocated memory with the whole directory
* "pDirLen" is the size of the *ppDir buffer
* "ppDirEntry" gets a memory pointer to the start of the created entry
* "pDirKeyBlock" gets the key block of the directory as a whole
* "pDirEntrySlot" gets the slot number within the directory block (first is 1)
* "pDirBlock" gets the actual block in which the created entry resides
*
* The caller should Write the entire thing to "pOpenSubdir" after filling
* in the new details for the entry.
*
* Possible reasons for failure: disk is out of space, volume dir is out
* of space, pOpenSubdir is screwy.
*
* We guarantee that we will return the first available entry in the current
* directory.
*/
DIError DiskFSProDOS::AllocDirEntry(A2FileDescr* pOpenSubdir, uint8_t** ppDir,
long* pDirLen, uint8_t** ppDirEntry, uint16_t* pDirKeyBlock,
int* pDirEntrySlot, uint16_t* pDirBlock)
{
assert(pOpenSubdir != NULL);
*ppDirEntry = NULL;
*pDirLen = -1;
*pDirKeyBlock = 0;
*pDirEntrySlot = -1;
*pDirBlock = 0;
DIError dierr = kDIErrNone;
uint8_t* dirBuf = NULL;
long dirLen;
A2FileProDOS* pFile;
long newBlock = -1;
/*
* Load the subdir into memory.
*/
pFile = (A2FileProDOS*) pOpenSubdir->GetFile();
dirLen = (long) pFile->GetDataLength();
if (dirLen < 512 || (dirLen % 512) != 0) {
LOGI(" ProDOS GLITCH: funky dir EOF %ld (quality=%d)",
dirLen, pFile->GetQuality());
dierr = kDIErrBadFile;
goto bail;
}
dirBuf = new uint8_t[dirLen];
if (dirBuf == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
dierr = pOpenSubdir->Read(dirBuf, dirLen);
if (dierr != kDIErrNone)
goto bail;
if (dirBuf[0x23] != kEntryLength ||
dirBuf[0x24] != kEntriesPerBlock)
{
LOGI(" ProDOS GLITCH: funky entries per block %d", dirBuf[0x24]);
dierr = kDIErrBadDirectory;
goto bail;
}
/*
* Find the first available entry (storage_type is zero). We need to
* step through this by blocks, because the data is block-oriented.
* If we run off the end of the last block, (re)alloc a new one.
*/
uint8_t* pDirEntry;
int blockIdx;
int entryIdx;
pDirEntry = NULL; // make the compiler happy
entryIdx = -1; // make the compiler happy
for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) {
pDirEntry = dirBuf + 512*blockIdx + 4; // skip 4 bytes of prev/next
for (entryIdx = 0; entryIdx < kEntriesPerBlock;
entryIdx++, pDirEntry += kEntryLength)
{
if ((pDirEntry[0x00] & 0xf0) == 0) {
LOGI(" ProDOS Found empty dir entry in slot %d", entryIdx);
break; // found one; break out of inner loop
}
}
if (entryIdx < kEntriesPerBlock)
break; // out of outer loop
}
if (blockIdx == dirLen / 512) {
if (((dirBuf[0x04] & 0xf0) >> 4) == A2FileProDOS::kStorageVolumeDirHeader)
{
/* can't extend the volume dir */
dierr = kDIErrVolumeDirFull;
goto bail;
}
LOGI(" ProDOS ran out of directory space, adding another block");
/*
* Request an unused block from the system. Point the "next" pointer
* in the last block at it, so that when we go to write this dir
* we will know where to put it.
*/
uint8_t* pBlock;
pBlock = dirBuf + 512 * (blockIdx-1);
if (pBlock[0x02] != 0) {
LOGI(" ProDOS GLITCH: adding to block with nonzero next ptr!");
dierr = kDIErrBadDirectory;
goto bail;
}
newBlock = AllocBlock();
if (newBlock < 0) {
dierr = kDIErrDiskFull;
goto bail;
}
PutShortLE(&pBlock[0x02], (uint16_t) newBlock); // set "next"
/*
* Extend our memory buffer to hold the new entry.
*/
uint8_t* newSpace = new uint8_t[dirLen + 512];
if (newSpace == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
memcpy(newSpace, dirBuf, dirLen);
memset(newSpace + dirLen, 0, 512);
delete[] dirBuf;
dirBuf = newSpace;
dirLen += 512;
/*
* Set the "prev" pointer in the new block to point at the last
* block of the existing directory structure.
*/
long lastBlock;
dierr = pOpenSubdir->GetStorage(blockIdx-1, &lastBlock);
if (dierr != kDIErrNone)
goto bail;
pBlock = dirBuf + 512 * blockIdx;
PutShortLE(&pBlock[0x00], (uint16_t) lastBlock); // set "prev"
assert(GetShortLE(&pBlock[0x02]) == 0); // "next" pointer
/*
* Finally, point pDirEntry at the first entry in the new area.
*/
pDirEntry = pBlock + 4;
entryIdx = 0;
assert(pDirEntry[0x00] == 0x00);
}
/*
* Success. Update the file count in the header.
*/
uint16_t count;
count = GetShortLE(&dirBuf[0x25]);
count++;
PutShortLE(&dirBuf[0x25], count);
long whichBlock;
*ppDir = dirBuf;
*pDirLen = dirLen;
*ppDirEntry = pDirEntry;
*pDirKeyBlock = pFile->fDirEntry.keyPointer;
*pDirEntrySlot = entryIdx +1;
if (blockIdx == ((A2FDProDOS*)pOpenSubdir)->GetBlockCount()) {
/* not yet added to block list, so can't use GetStorage */
assert(newBlock > 0);
*pDirBlock = (uint16_t) newBlock;
} else {
assert(newBlock < 0);
dierr = pOpenSubdir->GetStorage(blockIdx, &whichBlock);
assert(dierr == kDIErrNone);
*pDirBlock = (uint16_t) whichBlock;
}
dirBuf = NULL;
bail:
delete[] dirBuf;
return dierr;
}
/*
* Given a pointer to a directory buffer and a pointer to an entry, find the
* previous entry. (This is handy when trying to figure out where to insert
* a new entry into the DiskFS linear file list.)
*
* If the previous entry is the first in the list (i.e. it's a volume or
* subdir header), this returns NULL.
*
* This is a little awkward because the directories are chopped up into
* 512-byte blocks, with 13 entries per block (which doesn't completely fill
* the block, leaving gaps we have to skip around). If the previous entry is
* in the same block we can just return (ptr-0x27), but if it's in a previous
* block we need to return the last entry in the previous.
*/
uint8_t* DiskFSProDOS::GetPrevDirEntry(uint8_t* buf, uint8_t* ptr)
{
assert(buf != NULL);
assert(ptr != NULL);
const int kStartOffset = 4;
if (ptr == buf + kStartOffset || ptr == buf + kStartOffset + kEntryLength)
return NULL;
while (ptr - buf > 512)
buf += 512;
assert((ptr - buf - kStartOffset) % kEntryLength == 0);
if (ptr == buf + kStartOffset) {
/* whoops, went too far */
buf -= 512;
return buf + kStartOffset + kEntryLength * (kEntriesPerBlock-1);
} else {
return ptr - kEntryLength;
}
}
/*
* Make the name pointed to by "fileName" unique within the directory
* loaded in "subdirBuf". The name should already be trimmed to 15 chars
* or less and converted to upper-case only, and be in a buffer that can
* hold at least kMaxFileName+1 bytes.
*
* Returns an error on failure, which should only happen if there are a
* large number of files with similar names.
*/
DIError DiskFSProDOS::MakeFileNameUnique(const uint8_t* dirBuf, long dirLen,
char* fileName)
{
assert(dirBuf != NULL);
assert(dirLen > 0);
assert((dirLen % 512) == 0);
assert(fileName != NULL);
assert(strlen(fileName) <= A2FileProDOS::kMaxFileName);
if (!NameExistsInDir(dirBuf, dirLen, fileName))
return kDIErrNone;
LOGI(" ProDOS found duplicate of '%s', making unique", fileName);
int nameLen = strlen(fileName);
int dotOffset=0, dotLen=0;
char dotBuf[kMaxExtensionLen+1];
/* ensure the result will be null-terminated */
memset(fileName + nameLen, 0, (A2FileProDOS::kMaxFileName - nameLen) +1);
/*
* If this has what looks like a filename extension, grab it. We want
* to preserve ".gif", ".c", etc., since the filetypes don't necessarily
* do everything we need.
*
* This will tend to screw up the upper/lower case stuff, especially
* since what we think is a '.' might actually be a ' '. We could work
* around this, but it's probably not necessary.
*/
const char* cp = strrchr(fileName, '.');
if (cp != NULL) {
int tmpOffset = cp - fileName;
if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) {
LOGI(" ProDOS (keeping extension '%s')", cp);
assert(strlen(cp) <= kMaxExtensionLen);
strcpy(dotBuf, cp);
dotOffset = tmpOffset;
dotLen = nameLen - dotOffset;
}
}
const int kMaxDigits = 999;
int digits = 0;
int digitLen;
int copyOffset;
char digitBuf[4];
do {
if (digits == kMaxDigits)
return kDIErrFileExists;
digits++;
/* not the most efficient way to do this, but it'll do */
sprintf(digitBuf, "%d", digits);
digitLen = strlen(digitBuf);
if (nameLen + digitLen > A2FileProDOS::kMaxFileName)
copyOffset = A2FileProDOS::kMaxFileName - dotLen - digitLen;
else
copyOffset = nameLen - dotLen;
memcpy(fileName + copyOffset, digitBuf, digitLen);
if (dotLen != 0)
memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen);
} while (NameExistsInDir(dirBuf, dirLen, fileName));
LOGI(" ProDOS converted to unique name: %s", fileName);
return kDIErrNone;
}
/*
* Determine whether the specified file name exists in the raw directory
* buffer.
*
* This should be called with the upper-case-only version of the filename.
*/
bool DiskFSProDOS::NameExistsInDir(const uint8_t* dirBuf, long dirLen,
const char* fileName)
{
const uint8_t* pDirEntry;
int blockIdx;
int entryIdx;
int nameLen = strlen(fileName);
assert(nameLen <= A2FileProDOS::kMaxFileName);
for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) {
pDirEntry = dirBuf + 512*blockIdx + 4; // skip 4 bytes of prev/next
for (entryIdx = 0; entryIdx < kEntriesPerBlock;
entryIdx++, pDirEntry += kEntryLength)
{
/* skip directory header */
if (blockIdx == 0 && entryIdx == 0)
continue;
if ((pDirEntry[0x00] & 0xf0) != 0 &&
(pDirEntry[0x00] & 0x0f) == nameLen &&
strncmp((char*) &pDirEntry[0x01], fileName, nameLen) == 0)
{
return true;
}
}
}
return false;
}
/*
* Delete a file.
*
* There are three fairly simple steps: (1) mark all blocks used by the file as
* free, (2) set the storage type in the directory entry to 0, and (3)
* decrement the file count in the directory header. We then remove it from
* the DiskFS file list.
*
* We only allow deletion of a subdirectory when the subdir is empty.
*/
DIError DiskFSProDOS::DeleteFile(A2File* pGenericFile)
{
DIError dierr = kDIErrNone;
long blockCount = -1;
long indexCount = -1;
uint16_t* blockList = NULL;
uint16_t* indexList = NULL;
if (pGenericFile == NULL) {
assert(false);
return kDIErrInvalidArg;
}
if (fpImg->GetReadOnly())
return kDIErrAccessDenied;
if (!fDiskIsGood)
return kDIErrBadDiskImage;
if (pGenericFile->IsFileOpen())
return kDIErrFileOpen;
/*
* If they try to delete all entries, we don't want to spit back a
* failure message over our "fake" volume dir entry. So we just silently
* ignore the request.
*/
if (pGenericFile->IsVolumeDirectory()) {
LOGI("ProDOS not deleting volume directory");
return kDIErrNone;
}
A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile;
LOGI(" Deleting '%s'", pFile->GetPathName());
dierr = LoadVolBitmap();
if (dierr != kDIErrNone)
goto bail;
switch (pFile->fDirEntry.storageType) {
case A2FileProDOS::kStorageExtended:
// handle rsrc fork here, fall out for data fork
dierr = pFile->LoadBlockList(
pFile->fExtRsrc.storageType,
pFile->fExtRsrc.keyBlock,
pFile->fExtRsrc.eof,
&blockCount, &blockList,
&indexCount, &indexList);
if (dierr != kDIErrNone)
goto bail;
FreeBlocks(blockCount, blockList);
if (indexList != NULL) // no indices for seedling
FreeBlocks(indexCount, indexList);
delete[] blockList;
delete[] indexList;
indexList = NULL;
// handle the key block "manually"
blockCount = 1;
blockList = new uint16_t[blockCount];
blockList[0] = pFile->fDirEntry.keyPointer;
FreeBlocks(blockCount, blockList);
delete[] blockList;
blockList = NULL;
dierr = pFile->LoadBlockList(
pFile->fExtData.storageType,
pFile->fExtData.keyBlock,
pFile->fExtData.eof,
&blockCount, &blockList,
&indexCount, &indexList);
break; // fall out
case A2FileProDOS::kStorageDirectory:
dierr = pFile->LoadDirectoryBlockList(
pFile->fDirEntry.keyPointer,
pFile->fDirEntry.eof,
&blockCount, &blockList);
break; // fall out
case A2FileProDOS::kStorageSeedling:
case A2FileProDOS::kStorageSapling:
case A2FileProDOS::kStorageTree:
dierr = pFile->LoadBlockList(
pFile->fDirEntry.storageType,
pFile->fDirEntry.keyPointer,
pFile->fDirEntry.eof,
&blockCount, &blockList,
&indexCount, &indexList);
break; // fall out
default:
LOGI("ProDOS can't delete unknown storage type %d",
pFile->fDirEntry.storageType);
dierr = kDIErrBadDirectory;
break; // fall out
}
if (dierr != kDIErrNone)
goto bail;
FreeBlocks(blockCount, blockList);
if (indexList != NULL)
FreeBlocks(indexCount, indexList);
/*
* Update the directory entry. After this point, failure gets ugly.
*
* It might be "proper" to open the subdir file, find the correct entry,
* and write it back, but the A2FileProDOS structure has the directory
* block and entry index stored in it. Makes it a little easier.
*/
uint8_t blkBuf[kBlkSize];
uint8_t* ptr;
assert(pFile->fParentDirBlock > 0);
assert(pFile->fParentDirIdx >= 0 &&
pFile->fParentDirIdx < kEntriesPerBlock);
dierr = fpImg->ReadBlock(pFile->fParentDirBlock, blkBuf);
if (dierr != kDIErrNone) {
LOGI("ProDOS unable to read directory block %u",
pFile->fParentDirBlock);
goto bail;
}
ptr = blkBuf + 4 + pFile->fParentDirIdx * kEntryLength;
if ((*ptr) >> 4 != pFile->fDirEntry.storageType) {
LOGI("ProDOS GLITCH: mismatched storage types (%d vs %d)",
(*ptr) >> 4, pFile->fDirEntry.storageType);
assert(false);
dierr = kDIErrBadDirectory;
goto bail;
}
ptr[0x00] = 0; // zap both storage type and name length
dierr = fpImg->WriteBlock(pFile->fParentDirBlock, blkBuf);
if (dierr != kDIErrNone) {
LOGI("ProDOS unable to write directory block %u",
pFile->fParentDirBlock);
goto bail;
}
/*
* Save our updated copy of the volume bitmap to disk.
*/
dierr = SaveVolBitmap();
if (dierr != kDIErrNone)
goto bail;
/*
* One last little thing: decrement the file count in the directory
* header. We can find the appropriate place pretty easily because
* we know it's the first block in pFile->fpParent, which for a dir is
* always the block pointed to by the key pointer.
*
* Strictly speaking, failure to update this correctly isn't fatal. I
* doubt most utilities pay any attention to this. Still, it's important
* to keep the filesystem in a consistent state, so we at least must
* report the error. They'll need to run the ProSel volume repair util
* to fix it.
*/
A2FileProDOS* pParent;
uint16_t fileCount;
int storageType;
pParent = (A2FileProDOS*) pFile->GetParent();
assert(pParent != NULL);
assert(pParent->fDirEntry.keyPointer >= kVolHeaderBlock);
dierr = fpImg->ReadBlock(pParent->fDirEntry.keyPointer, blkBuf);
if (dierr != kDIErrNone) {
LOGI("ProDOS unable to read parent dir block %u",
pParent->fDirEntry.keyPointer);
goto bail;
}
ptr = NULL;
storageType = (blkBuf[0x04] & 0xf0) >> 4;
if (storageType != A2FileProDOS::kStorageSubdirHeader &&
storageType != A2FileProDOS::kStorageVolumeDirHeader)
{
LOGI("ProDOS invalid storage type %d in dir header block",
storageType);
DebugBreak();
dierr = kDIErrBadDirectory;
goto bail;
}
fileCount = GetShortLE(&blkBuf[0x25]);
if (fileCount > 0)
fileCount--;
PutShortLE(&blkBuf[0x25], fileCount);
dierr = fpImg->WriteBlock(pParent->fDirEntry.keyPointer, blkBuf);
if (dierr != kDIErrNone) {
LOGI("ProDOS unable to write parent dir block %u",
pParent->fDirEntry.keyPointer);
goto bail;
}
/*
* Remove the A2File* from the list.
*/
DeleteFileFromList(pFile);
bail:
FreeVolBitmap();
delete[] blockList;
delete[] indexList;
return kDIErrNone;
}
/*
* Mark all of the blocks in the blockList as free.
*
* The in-use map must already be loaded.
*/
DIError DiskFSProDOS::FreeBlocks(long blockCount, uint16_t* blockList)
{
VolumeUsage::ChunkState cstate;
int i;
//LOGI(" +++ FreeBlocks (blockCount=%d blockList=0x%08lx)",
// blockCount, blockList);
assert(blockCount >= 0 && blockCount < 65536);
assert(blockList != NULL);
cstate.isUsed = false;
cstate.isMarkedUsed = false;
cstate.purpose = VolumeUsage::kChunkPurposeUnknown;
for (i = 0; i < blockCount; i++) {
if (blockList[i] == 0) // expected for "sparse" files
continue;
if (!GetBlockUseEntry(blockList[i])) {
LOGI("WARNING: freeing unallocated block %u", blockList[i]);
assert(false); // impossible unless disk is "damaged"
}
SetBlockUseEntry(blockList[i], false);
fVolumeUsage.SetChunkState(blockList[i], &cstate);
}
return kDIErrNone;
}
/*
* Rename a file.
*
* Pass in a pointer to the file and a string with the new filename (just
* the filename, not a pathname -- this function doesn't move files
* between directories). The new name must already be normalized.
*
* Renaming the magic volume directory "file" is not allowed.
*
* Things to note:
* - Renaming subdirs is annoying. The name has to be changed in two
* places, and the "pathname" value cached in A2FileProDOS must be
* updated for all children of the subdir.
* - Must check for duplicates.
* - If it's an AppleWorks file type, we need to change the aux type
* according to the upper/lower case flags. This holds even if the
* "allow lower case" flag is disabled.
*/
DIError DiskFSProDOS::RenameFile(A2File* pGenericFile, const char* newName)
{
DIError dierr = kDIErrNone;
A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile;
char upperName[A2FileProDOS::kMaxFileName+1];
char upperComp[A2FileProDOS::kMaxFileName+1];
if (pFile == NULL || newName == NULL)
return kDIErrInvalidArg;
if (!IsValidFileName(newName))
return kDIErrInvalidArg;
if (pFile->IsVolumeDirectory())
return kDIErrInvalidArg;
if (fpImg->GetReadOnly())
return kDIErrAccessDenied;
if (!fDiskIsGood)
return kDIErrBadDiskImage;
LOGI(" ProDOS renaming '%s' to '%s'", pFile->GetPathName(), newName);
/*
* Check for duplicates. We do this by getting the parent subdir and
* running through it looking for an upper-case-converted match.
*
* We start in the list at our parent node, knowing that the kids are
* grouped together after it. However, we can't stop right away,
* because some of the kids might be subdirectories themselves. So we
* will probably run through a significant chunk of the list.
*/
A2File* pParent = pFile->GetParent();
A2File* pCur;
UpperCaseName(upperName, newName);
pCur = GetNextFile(pParent);
assert(pCur != NULL); // at the very least, pFile is in this dir
while (pCur != NULL) {
if (pCur != pFile && pCur->GetParent() == pParent) {
/* one of our siblings; see if the name matches */
UpperCaseName(upperComp, pCur->GetFileName());
if (strcmp(upperName, upperComp) == 0) {
LOGI(" ProDOS rename dup found");
return kDIErrFileExists;
}
}
pCur = GetNextFile(pCur);
}
/*
* Grab the directory block and update the filename in the entry. If this
* was a subdir we also need to update its directory header entry. To
* minimize the chances of a partial update, we load both blocks up
* front, modify both, then write them both back.
*/
uint8_t parentDirBuf[kBlkSize];
uint8_t thisDirBuf[kBlkSize];
dierr = fpImg->ReadBlock(pFile->fParentDirBlock, parentDirBuf);
if (dierr != kDIErrNone)
goto bail;
if (pFile->IsDirectory()) {
dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
if (dierr != kDIErrNone)
goto bail;
}
/* compute lower case flags as needed */
uint16_t lcFlags, lcAuxType;
bool allowLowerCase, isAW;
allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0;
isAW = A2FileProDOS::UsesAppleWorksAuxType((uint8_t)pFile->GetFileType());
if (allowLowerCase)
lcFlags = GenerateLowerCaseBits(upperName, newName, false);
else
lcFlags = 0;
if (isAW)
lcAuxType = GenerateLowerCaseBits(upperName, newName, true);
else
lcAuxType = 0;
/*
* Possible optimization: if "upperName" matches what's in the block on
* disk and the "lcFlags"/"lcAuxType" values match as well, we don't
* need to write the blocks back.
*
* It's difficult to test for this earlier, because we need to do the
* update if (a) they're just changing the capitalization or (b) we're
* changing the capitalization for them because the "allow lower case"
* flag got turned off.
*/
/* find the right entry, and copy our filename in */
uint8_t* ptr;
assert(pFile->fParentDirIdx >= 0 &&
pFile->fParentDirIdx < kEntriesPerBlock);
ptr = parentDirBuf + 4 + pFile->fParentDirIdx * kEntryLength;
if ((*ptr) >> 4 != pFile->fDirEntry.storageType) {
LOGI("ProDOS GLITCH: mismatched storage types (%d vs %d)",
(*ptr) >> 4, pFile->fDirEntry.storageType);
assert(false);
dierr = kDIErrBadDirectory;
goto bail;
}
ptr[0x00] = (uint8_t)((ptr[0x00] & 0xf0) | strlen(upperName));
memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName);
PutShortLE(&ptr[0x1c], lcFlags); // version/min_version
if (isAW)
PutShortLE(&ptr[0x1f], lcAuxType);
if (pFile->IsDirectory()) {
ptr = thisDirBuf + 4;
if ((*ptr) >> 4 != A2FileProDOS::kStorageSubdirHeader) {
LOGI("ProDOS GLITCH: bad storage type in subdir header (%d)",
(*ptr) >> 4);
assert(false);
dierr = kDIErrBadDirectory;
goto bail;
}
ptr[0x00] = (uint8_t)((ptr[0x00] & 0xf0) | strlen(upperName));
memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName);
PutShortLE(&ptr[0x1c], lcFlags); // version/min_version
}
/* write the updated data back to the disk */
dierr = fpImg->WriteBlock(pFile->fParentDirBlock, parentDirBuf);
if (dierr != kDIErrNone)
goto bail;
if (pFile->IsDirectory()) {
dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
if (dierr != kDIErrNone)
goto bail;
}
/*
* At this point the ProDOS filesystem is back in a consistent state.
* Everything we do from here on is self-inflicted.
*
* We need to update this entry's A2FileProDOS::fDirEntry.fileName,
* as well as the A2FileProDOS::fPathName. If this was a subdir, then
* we need to update A2FileProDOS::fPathName for all files inside the
* directory (including children of children).
*
* The latter is somewhat awkward, so we just re-acquire the pathname
* for every file on the disk. Less efficient but easier to code.
*/
if (isAW)
GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName,
lcAuxType, true);
else
GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName,
lcFlags, false);
assert(pFile->fDirEntry.fileName[A2FileProDOS::kMaxFileName] == '\0');
if (pFile->IsDirectory()) {
/* do all files that come after us */
pCur = pFile;
while (pCur != NULL) {
RegeneratePathName((A2FileProDOS*) pCur);
pCur = GetNextFile(pCur);
}
} else {
RegeneratePathName(pFile);
}
LOGI("Okay!");
bail:
return dierr;
}
/*
* Regenerate fPathName for the specified file.
*
* Has no effect on the magic volume dir entry.
*
* This could be implemented more efficiently, but it's only used when
* renaming files, so there's not much point.
*/
DIError DiskFSProDOS::RegeneratePathName(A2FileProDOS* pFile)
{
A2FileProDOS* pParent;
char* buf = NULL;
int len;
/* nothing to do here */
if (pFile->IsVolumeDirectory())
return kDIErrNone;
/* compute the length of the path name */
len = strlen(pFile->GetFileName());
pParent = (A2FileProDOS*) pFile->GetParent();
while (!pParent->IsVolumeDirectory()) {
len++; // leave space for the ':'
len += strlen(pParent->GetFileName());
pParent = (A2FileProDOS*) pParent->GetParent();
}
buf = new char[len+1];
if (buf == NULL)
return kDIErrMalloc;
/* generate the new path name */
int partLen;
partLen = strlen(pFile->GetFileName());
strcpy(buf + len - partLen, pFile->GetFileName());
len -= partLen;
pParent = (A2FileProDOS*) pFile->GetParent();
while (!pParent->IsVolumeDirectory()) {
assert(len > 0);
buf[--len] = kDIFssep;
partLen = strlen(pParent->GetFileName());
strncpy(buf + len - partLen, pParent->GetFileName(), partLen);
len -= partLen;
assert(len >= 0);
pParent = (A2FileProDOS*) pParent->GetParent();
}
LOGI("Replacing '%s' with '%s'", pFile->GetPathName(), buf);
pFile->SetPathName("", buf);
delete[] buf;
return kDIErrNone;
}
/*
* Change the attributes of the specified file.
*
* Subdirectories have access bits in the subdir header as well as their
* file entry. The BASIC.SYSTEM "lock" command only changes the access
* bits of the file; the permissions inside the subdir remain 0xe3. (Which
* might explain why you can still add files to a locked subdir.) I'm going
* to mimic this behavior.
*
* This does, of course, mean that there's no meaning in attempts to change
* the file access permissions of the volume directory.
*/
DIError DiskFSProDOS::SetFileInfo(A2File* pGenericFile, uint32_t fileType,
uint32_t auxType, uint32_t accessFlags)
{
DIError dierr = kDIErrNone;
A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile;
if (fpImg->GetReadOnly())
return kDIErrAccessDenied;
if (pFile == NULL) {
assert(false);
return kDIErrInvalidArg;
}
if ((fileType & ~(0xff)) != 0 ||
(auxType & ~(0xffff)) != 0 ||
(accessFlags & ~(0xff)) != 0)
{
return kDIErrInvalidArg;
}
if (pFile->IsVolumeDirectory()) {
LOGI(" ProDOS refusing to change file info for volume dir");
return kDIErrAccessDenied; // not quite right
}
LOGI("ProDOS changing values for '%s' to 0x%02x 0x%04x 0x%02x",
pFile->GetPathName(), fileType, auxType, accessFlags);
/* load the directory block for this file */
uint8_t thisDirBuf[kBlkSize];
dierr = fpImg->ReadBlock(pFile->fParentDirBlock, thisDirBuf);
if (dierr != kDIErrNone)
goto bail;
/* find the right entry, and set the fields */
uint8_t* ptr;
assert(pFile->fParentDirIdx >= 0 &&
pFile->fParentDirIdx < kEntriesPerBlock);
ptr = thisDirBuf + 4 + pFile->fParentDirIdx * kEntryLength;
if ((*ptr) >> 4 != pFile->fDirEntry.storageType) {
LOGI("ProDOS GLITCH: mismatched storage types (%d vs %d)",
(*ptr) >> 4, pFile->fDirEntry.storageType);
assert(false);
dierr = kDIErrBadDirectory;
goto bail;
}
if ((size_t) (*ptr & 0x0f) != strlen(pFile->fDirEntry.fileName)) {
LOGW("ProDOS GLITCH: wrong file? (len=%d vs %u)",
*ptr & 0x0f, (unsigned int) strlen(pFile->fDirEntry.fileName));
assert(false);
dierr = kDIErrBadDirectory;
goto bail;
}
ptr[0x10] = (uint8_t) fileType;
ptr[0x1e] = (uint8_t) accessFlags;
PutShortLE(&ptr[0x1f], (uint16_t) auxType);
dierr = fpImg->WriteBlock(pFile->fParentDirBlock, thisDirBuf);
if (dierr != kDIErrNone)
goto bail;
/* update our local copy */
pFile->fDirEntry.fileType = (uint8_t) fileType;
pFile->fDirEntry.auxType = (uint16_t) auxType;
pFile->fDirEntry.access = (uint8_t) accessFlags;
bail:
return dierr;
}
/*
* Change the disk volume name.
*
* This is a lot like renaming a subdirectory, except that there's no parent
* directory to update, and the name of the volume dir doesn't affect the
* pathname of anything else. There's also no risk of a duplicate.
*
* Internally we need to update the "fake" entry and the cached copies in
* fVolumeName and fVolumeID.
*/
DIError DiskFSProDOS::RenameVolume(const char* newName)
{
DIError dierr = kDIErrNone;
char upperName[A2FileProDOS::kMaxFileName+1];
A2FileProDOS* pFile;
if (!IsValidVolumeName(newName))
return kDIErrInvalidArg;
if (fpImg->GetReadOnly())
return kDIErrAccessDenied;
pFile = (A2FileProDOS*) GetNextFile(NULL);
assert(pFile != NULL);
assert(strcmp(pFile->GetFileName(), fVolumeName) == 0);
LOGI(" ProDOS renaming volume '%s' to '%s'",
pFile->GetPathName(), newName);
/*
* Figure out the lower-case flags.
*/
uint16_t lcFlags;
bool allowLowerCase;
UpperCaseName(upperName, newName);
allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0;
if (allowLowerCase)
lcFlags = GenerateLowerCaseBits(upperName, newName, false);
else
lcFlags = 0;
/*
* Update the volume dir header.
*/
uint8_t thisDirBuf[kBlkSize];
uint8_t* ptr;
assert(pFile->fDirEntry.keyPointer == kVolHeaderBlock);
dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
if (dierr != kDIErrNone)
goto bail;
ptr = thisDirBuf + 4;
if ((*ptr) >> 4 != A2FileProDOS::kStorageVolumeDirHeader) {
LOGI("ProDOS GLITCH: bad storage type in voldir header (%d)",
(*ptr) >> 4);
assert(false);
dierr = kDIErrBadDirectory;
goto bail;
}
ptr[0x00] = (uint8_t)((ptr[0x00] & 0xf0) | strlen(upperName));
memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName);
PutShortLE(&ptr[0x16], lcFlags); // reserved fields
dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf);
if (dierr != kDIErrNone)
goto bail;
/*
* Set the volume name, based on the upper-case name and lower-case flags
* we just wrote. If "allowLowerCase" was set to false, it may not be
* the same as what's in "newName".
*/
char lowerName[A2FileProDOS::kMaxFileName+1];
memset(lowerName, 0, sizeof(lowerName)); // lowerName won't be term'ed
GenerateLowerCaseName(upperName, lowerName, lcFlags, false);
strcpy(fVolumeName, lowerName);
SetVolumeID();
strcpy(pFile->fDirEntry.fileName, lowerName);
/* update the entry in the linear file list */
pFile->SetPathName(":", fVolumeName);
bail:
return dierr;
}
/*
* ===========================================================================
* A2FileProDOS
* ===========================================================================
*/
/*
* Convert from ProDOS compact date format to a time_t.
*
* Byte 0 and 1: yyyyyyymmmmddddd
* Byte 2 and 3: 000hhhhh00mmmmmm
*
* The field is set entirely to zero if no date was assigned (which cannot
* be a valid date since "day" ranges from 1 to 31). If this is found then
* ((time_t) 0) is returned.
*/
/*static*/ time_t A2FileProDOS::ConvertProDate(ProDate proDate)
{
uint16_t prodosDate, prodosTime;
int year, month, day, hour, minute, second;
if (proDate == 0)
return 0;
prodosDate = (uint16_t) (proDate & 0x0000ffff);
prodosTime = (uint16_t) ((proDate >> 16) & 0x0000ffff);
second = 0;
minute = prodosTime & 0x3f;
hour = (prodosTime >> 8) & 0x1f;
day = prodosDate & 0x1f;
month = (prodosDate >> 5) & 0x0f;
year = (prodosDate >> 9) & 0x7f;
if (year < 40)
year += 100; /* P8 uses 0-39 for 2000-2039 */
struct tm tmbuf;
time_t when;
tmbuf.tm_sec = second;
tmbuf.tm_min = minute;
tmbuf.tm_hour = hour;
tmbuf.tm_mday = day;
tmbuf.tm_mon = month-1; // ProDOS uses 1-12
tmbuf.tm_year = year;
tmbuf.tm_wday = 0;
tmbuf.tm_yday = 0;
tmbuf.tm_isdst = -1; // let it figure DST and time zone
when = mktime(&tmbuf);
if (when == (time_t) -1)
when = 0;
return when;
}
/*
* Convert a time_t to a ProDOS-format date.
*
* CiderPress uses kDateInvalid==-1 and kDateNone==-2.
*/
/*static*/ A2FileProDOS::ProDate A2FileProDOS::ConvertProDate(time_t unixDate)
{
ProDate proDate;
uint32_t prodosDate, prodosTime;
struct tm* ptm;
int year;
if (unixDate == 0 || unixDate == -1 || unixDate == -2)
return 0;
ptm = localtime(&unixDate);
if (ptm == NULL)
return 0; // must've been invalid or unspecified
year = ptm->tm_year;
#ifdef OLD_PRODOS_DATES
/* ProSel-16 volume repair complaints about dates < 1980 and >= Y2K */
if (year > 100)
year -= 20;
#endif
if (year >= 100)
year -= 100;
if (year < 0 || year >= 128) {
LOGI("WHOOPS: got year %d from %d", year, ptm->tm_year);
year = 70;
}
prodosDate = year << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday;
prodosTime = ptm->tm_hour << 8 | ptm->tm_min;
proDate = prodosTime << 16 | prodosDate;
return proDate;
}
/*
* Return the file creation time as a time_t.
*/
time_t A2FileProDOS::GetCreateWhen(void) const
{
return ConvertProDate(fDirEntry.createWhen);
}
/*
* Return the file modification time as a time_t.
*/
time_t A2FileProDOS::GetModWhen(void) const
{
return ConvertProDate(fDirEntry.modWhen);
}
/*
* Set the full pathname to a combination of the base path and the
* current file's name.
*
* If we're in the volume directory, pass in "" for the base path (not NULL).
*/
void A2FileProDOS::SetPathName(const char* basePath, const char* fileName)
{
assert(basePath != NULL && fileName != NULL);
if (fPathName != NULL)
delete[] fPathName;
int baseLen = strlen(basePath);
fPathName = new char[baseLen + 1 + strlen(fileName)+1];
strcpy(fPathName, basePath);
if (baseLen != 0 &&
!(baseLen == 1 && basePath[0] == ':'))
{
*(fPathName + baseLen) = kFssep;
baseLen++;
}
strcpy(fPathName + baseLen, fileName);
}
/*
* Convert a character in a ProDOS name to lower case.
*
* This is special in that '.' is considered upper case, with ' ' as its
* lower-case counterpart.
*/
/*static*/ char A2FileProDOS::NameToLower(char ch)
{
if (ch == '.')
return ' ';
else
return tolower(ch);
}
/*
* Init the fields in the DirEntry struct from the values in the ProDOS
* directory entry pointed to by "entryBuf".
*
* Deals with lower case conversions on the filename.
*/
/*static*/ void A2FileProDOS::InitDirEntry(A2FileProDOS::DirEntry* pEntry,
const uint8_t* entryBuf)
{
int nameLen;
pEntry->storageType = (entryBuf[0x00] & 0xf0) >> 4;
nameLen = entryBuf[0x00] & 0x0f;
memcpy(pEntry->fileName, &entryBuf[0x01], nameLen);
pEntry->fileName[nameLen] = '\0';
pEntry->fileType = entryBuf[0x10];
pEntry->keyPointer = GetShortLE(&entryBuf[0x11]);
pEntry->blocksUsed = GetShortLE(&entryBuf[0x13]);
pEntry->eof = GetLongLE(&entryBuf[0x15]);
pEntry->eof &= 0x00ffffff;
pEntry->createWhen = GetLongLE(&entryBuf[0x18]);
pEntry->version = entryBuf[0x1c];
pEntry->minVersion = entryBuf[0x1d];
pEntry->access = entryBuf[0x1e];
pEntry->auxType = GetShortLE(&entryBuf[0x1f]);
pEntry->modWhen = GetLongLE(&entryBuf[0x21]);
pEntry->headerPointer = GetShortLE(&entryBuf[0x25]);
/* generate the name into the buffer; does not null-terminate */
if (UsesAppleWorksAuxType(pEntry->fileType)) {
DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName,
pEntry->auxType, true);
} else if (pEntry->minVersion & 0x80) {
DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName,
GetShortLE(&entryBuf[0x1c]), false);
}
pEntry->fileName[sizeof(pEntry->fileName)-1] = '\0';
}
/*
* Open one fork of this file.
*
* I really, really dislike forked files.
*/
DIError A2FileProDOS::Open(A2FileDescr** ppOpenFile, bool readOnly,
bool rsrcFork /*= false*/)
{
DIError dierr = kDIErrNone;
A2FDProDOS* pOpenFile = NULL;
LOGI(" ProDOS Open(ro=%d, rsrc=%d) on '%s'",
readOnly, rsrcFork, fPathName);
//Dump();
if (!readOnly) {
if (fpDiskFS->GetDiskImg()->GetReadOnly())
return kDIErrAccessDenied;
if (fpDiskFS->GetFSDamaged())
return kDIErrBadDiskImage;
}
if (fpOpenFile != NULL) {
dierr = kDIErrAlreadyOpen;
goto bail;
}
if (rsrcFork && fDirEntry.storageType != kStorageExtended) {
dierr = kDIErrForkNotFound;
goto bail;
}
pOpenFile = new A2FDProDOS(this);
if (pOpenFile == NULL)
return kDIErrMalloc;
pOpenFile->fOpenRsrcFork = false;
if (fDirEntry.storageType == kStorageExtended) {
if (rsrcFork) {
dierr = LoadBlockList(fExtRsrc.storageType, fExtRsrc.keyBlock,
fExtRsrc.eof, &pOpenFile->fBlockCount,
&pOpenFile->fBlockList);
pOpenFile->fOpenEOF = fExtRsrc.eof;
pOpenFile->fOpenBlocksUsed = fExtRsrc.blocksUsed;
pOpenFile->fOpenStorageType = fExtRsrc.storageType;
pOpenFile->fOpenRsrcFork = true;
} else {
dierr = LoadBlockList(fExtData.storageType, fExtData.keyBlock,
fExtData.eof, &pOpenFile->fBlockCount,
&pOpenFile->fBlockList);
pOpenFile->fOpenEOF = fExtData.eof;
pOpenFile->fOpenBlocksUsed = fExtData.blocksUsed;
pOpenFile->fOpenStorageType = fExtData.storageType;
}
} else if (fDirEntry.storageType == kStorageDirectory ||
fDirEntry.storageType == kStorageVolumeDirHeader)
{
dierr = LoadDirectoryBlockList(fDirEntry.keyPointer,
fDirEntry.eof, &pOpenFile->fBlockCount,
&pOpenFile->fBlockList);
pOpenFile->fOpenEOF = fDirEntry.eof;
pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed;
pOpenFile->fOpenStorageType = fDirEntry.storageType;
} else if (fDirEntry.storageType == kStorageSeedling ||
fDirEntry.storageType == kStorageSapling ||
fDirEntry.storageType == kStorageTree)
{
dierr = LoadBlockList(fDirEntry.storageType, fDirEntry.keyPointer,
fDirEntry.eof, &pOpenFile->fBlockCount,
&pOpenFile->fBlockList);
pOpenFile->fOpenEOF = fDirEntry.eof;
pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed;
pOpenFile->fOpenStorageType = fDirEntry.storageType;
} else {
LOGI("PrODOS can't open unknown storage type %d",
fDirEntry.storageType);
dierr = kDIErrBadDirectory;
goto bail;
}
if (dierr != kDIErrNone) {
LOGI(" ProDOS open failed");
goto bail;
}
pOpenFile->fOffset = 0;
//pOpenFile->DumpBlockList();
fpOpenFile = pOpenFile; // add it to our single-member "open file set"
*ppOpenFile = pOpenFile;
pOpenFile = NULL;
bail:
delete pOpenFile;
return dierr;
}
/*
* Gather a linear, non-sparse list of file blocks into an array.
*
* Pass in the storage type and top-level key block. Separation of
* extended files should have been handled by the caller. This loads the
* list for only one fork.
*
* There are two kinds of sparse: sparse *inside* data, and sparse
* *past* data. The latter is interesting, because there is no need
* to create space in index blocks to hold it. Thus, a sapling could
* hold a file with an EOF of 16MB.
*
* If "pIndexBlockCount" and "pIndexBlockList" are non-NULL, then we
* also accumulate the list of index blocks and return those as well.
* For a Tree-structured file, the first entry in the index list is
* the master index block.
*
* The caller must delete[] "*pBlockList" and "*pIndexBlockList".
*/
DIError A2FileProDOS::LoadBlockList(int storageType, uint16_t keyBlock,
long eof, long* pBlockCount, uint16_t** pBlockList,
long* pIndexBlockCount, uint16_t** pIndexBlockList)
{
if (storageType == kStorageDirectory ||
storageType == kStorageVolumeDirHeader)
{
assert(pIndexBlockList == NULL && pIndexBlockCount == NULL);
return LoadDirectoryBlockList(keyBlock, eof, pBlockCount, pBlockList);
}
assert(keyBlock != 0);
assert(pBlockCount != NULL);
assert(pBlockList != NULL);
assert(*pBlockList == NULL);
if (storageType != kStorageSeedling &&
storageType != kStorageSapling &&
storageType != kStorageTree)
{
/*
* We can get here if somebody puts a bad storage type inside the
* extended key block of a forked file. Bad storage types on other
* kinds of files are caught earlier.
*/
LOGI(" ProDOS unexpected storageType %d in '%s'",
storageType, GetPathName());
return kDIErrNotSupported;
}
DIError dierr = kDIErrNone;
uint16_t* list = NULL;
long count;
assert(eof < 1024*1024*16);
count = (eof + kBlkSize -1) / kBlkSize;
if (count == 0)
count = 1;
list = new uint16_t[count+1];
if (list == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
if (pIndexBlockList != NULL) {
assert(pIndexBlockCount != NULL);
assert(*pIndexBlockList == NULL);
}
/* this should take care of trailing sparse entries */
memset(list, 0, sizeof(uint16_t) * count);
list[count] = kInvalidBlockNum; // overrun check
if (storageType == kStorageSeedling) {
list[0] = keyBlock;
if (pIndexBlockList != NULL) {
*pIndexBlockCount = 0;
*pIndexBlockList = NULL;
}
} else if (storageType == kStorageSapling) {
dierr = LoadIndexBlock(keyBlock, list, count);
if (dierr != kDIErrNone)
goto bail;
if (pIndexBlockList != NULL) {
*pIndexBlockCount = 1;
*pIndexBlockList = new uint16_t[1];
**pIndexBlockList = keyBlock;
}
} else if (storageType == kStorageTree) {
uint8_t blkBuf[kBlkSize];
uint16_t* listPtr = list;
uint16_t* outIndexPtr = NULL;
long countDown = count;
int idx = 0;
dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
if (pIndexBlockList != NULL) {
int numIndices = (count + kMaxBlocksPerIndex-1) / kMaxBlocksPerIndex;
numIndices++; // add one for the master index block
*pIndexBlockList = new uint16_t[numIndices];
outIndexPtr = *pIndexBlockList;
*outIndexPtr++ = keyBlock;
*pIndexBlockCount = 1;
}
while (countDown) {
long blockCount = countDown;
if (blockCount > kMaxBlocksPerIndex)
blockCount = kMaxBlocksPerIndex;
uint16_t idxBlock;
idxBlock = blkBuf[idx] | (uint16_t) blkBuf[idx+256] << 8;
if (idxBlock == 0) {
/* fully sparse index block */
//LOGI(" ProDOS that's seriously sparse (%d)!", idx);
memset(listPtr, 0, blockCount * sizeof(uint16_t));
if (pIndexBlockList != NULL) {
*outIndexPtr++ = idxBlock;
(*pIndexBlockCount)++;
}
} else {
dierr = LoadIndexBlock(idxBlock, listPtr, blockCount);
if (dierr != kDIErrNone)
goto bail;
if (pIndexBlockList != NULL) {
*outIndexPtr++ = idxBlock;
(*pIndexBlockCount)++;
}
}
idx++;
listPtr += blockCount;
countDown -= blockCount;
}
} else {
assert(false);
}
assert(list[count] == kInvalidBlockNum);
dierr = ValidateBlockList(list, count);
if (dierr != kDIErrNone)
goto bail;
*pBlockCount = count;
*pBlockList = list;
bail:
if (dierr != kDIErrNone) {
delete[] list;
assert(*pBlockList == NULL);
if (pIndexBlockList != NULL && *pIndexBlockList != NULL) {
delete[] *pIndexBlockList;
*pIndexBlockList = NULL;
}
}
return dierr;
}
/*
* Make sure all values in the block list fall in accepted ranges.
*
* We allow zero (used for sparse blocks), but disallow values in the "system"
* area (block 1 through the end of the usage map).
*
* It's hard to say whether we should compare against the DiskImg block count
* (representing blocks we can physically read but aren't necessarily part
* of the filesystem) or the filesystem "total blocks" value from the volume
* header. Using the one in the volume header is correct, but sometimes the
* value is off on an otherwise reasonable disk.
*
* I'm falling on the side of generosity, allowing files that reference
* potentially bad data to appear okay. My main reason is that, except for
* CFFA volumes that have been tweaked by CiderPress users, very few ProDOS
* disks will have a large disparity between the two numbers unless somebody
* has trashed the volume dir header.
*
* What we really need is three states for each file: good, suspect, damaged.
*/
DIError A2FileProDOS::ValidateBlockList(const uint16_t* list, long count)
{
DiskImg* pImg = fpDiskFS->GetDiskImg();
bool foundBad = false;
while (count--) {
if (*list > pImg->GetNumBlocks() ||
(*list > 0 && *list <= 2)) // not enough, but it'll do
{
LOGI("Invalid block %d in '%s'", *list, fDirEntry.fileName);
SetQuality(kQualityDamaged);
return kDIErrBadFile;
}
if (*list > fpDiskFS->GetFSNumBlocks())
foundBad = true;
list++;
}
if (foundBad) {
LOGI(" --- found out-of-range block in '%s'", GetPathName());
SetQuality(kQualitySuspicious);
}
return kDIErrNone;
}
/*
* Copy the entries from the index block in "block" to "list", copying
* at most "maxCount" entries.
*/
DIError A2FileProDOS::LoadIndexBlock(uint16_t block, uint16_t* list,
int maxCount)
{
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
int i;
if (maxCount > kMaxBlocksPerIndex)
maxCount = kMaxBlocksPerIndex;
dierr = fpDiskFS->GetDiskImg()->ReadBlock(block, blkBuf);
if (dierr != kDIErrNone)
goto bail;
//LOGI("LOADING 0x%04x", block);
for (i = 0; i < maxCount; i++) {
*list++ = blkBuf[i] | (uint16_t) blkBuf[i+256] << 8;
}
bail:
return dierr;
}
/*
* Load the block list from a directory, which is essentially a linear
* linked list.
*/
DIError A2FileProDOS::LoadDirectoryBlockList(uint16_t keyBlock,
long eof, long* pBlockCount, uint16_t** pBlockList)
{
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
uint16_t* list = NULL;
uint16_t* listPtr;
int iterations;
long count;
assert(eof < 1024*1024*16);
count = (eof + kBlkSize -1) / kBlkSize;
if (count == 0)
count = 1;
list = new uint16_t[count+1];
if (list == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
/* this should take care of trailing sparse entries */
memset(list, 0, sizeof(uint16_t) * count);
list[count] = kInvalidBlockNum; // overrun check
iterations = 0;
listPtr = list;
while (keyBlock && iterations < kMaxCatalogIterations) {
if (keyBlock < 2 ||
keyBlock >= fpDiskFS->GetDiskImg()->GetNumBlocks())
{
LOGI(" ProDOS ERROR: directory block %u out of range", keyBlock);
dierr = kDIErrInvalidBlock;
goto bail;
}
*listPtr++ = keyBlock;
dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
keyBlock = GetShortLE(&blkBuf[0x02]);
iterations++;
}
if (iterations == kMaxCatalogIterations) {
LOGI(" ProDOS subdir iteration count exceeded");
dierr = kDIErrDirectoryLoop;
goto bail;
}
assert(list[count] == kInvalidBlockNum);
*pBlockCount = count;
*pBlockList = list;
bail:
if (dierr != kDIErrNone)
delete[] list;
return dierr;
}
/*
* Dump the contents.
*/
void A2FileProDOS::Dump(void) const
{
LOGI(" ProDOS file '%s' (path='%s')",
fDirEntry.fileName, fPathName);
LOGI(" fileType=0x%02x auxType=0x%04x storage=%d",
fDirEntry.fileType, fDirEntry.auxType, fDirEntry.storageType);
LOGI(" keyPointer=%d blocksUsed=%d eof=%d",
fDirEntry.keyPointer, fDirEntry.blocksUsed, fDirEntry.eof);
LOGI(" access=0x%02x create=0x%08x mod=0x%08x",
fDirEntry.access, fDirEntry.createWhen, fDirEntry.modWhen);
LOGI(" version=%d minVersion=%d headerPtr=%d",
fDirEntry.version, fDirEntry.minVersion, fDirEntry.headerPointer);
if (fDirEntry.storageType == kStorageExtended) {
LOGI(" DATA storage=%d keyBlk=%d blkUsed=%d eof=%d",
fExtData.storageType, fExtData.keyBlock, fExtData.blocksUsed,
fExtData.eof);
LOGI(" RSRC storage=%d keyBlk=%d blkUsed=%d eof=%d",
fExtRsrc.storageType, fExtRsrc.keyBlock, fExtRsrc.blocksUsed,
fExtRsrc.eof);
}
LOGI(" * sparseData=%ld sparseRsrc=%ld",
(long) fSparseDataEof, (long) fSparseRsrcEof);
}
/*
* ===========================================================================
* A2FDProDOS
* ===========================================================================
*/
/*
* Read a chunk of data from whichever fork is open.
*/
DIError A2FDProDOS::Read(void* buf, size_t len, size_t* pActual)
{
LOGD(" ProDOS reading %lu bytes from '%s' (offset=%ld)",
(unsigned long) len, fpFile->GetPathName(), (long) fOffset);
//if (fBlockList == NULL)
// return kDIErrNotReady;
if (fOffset + (long)len > fOpenEOF) {
if (pActual == NULL)
return kDIErrDataUnderrun;
len = (long) (fOpenEOF - fOffset);
}
if (pActual != NULL)
*pActual = len;
//
long incrLen = len;
DIError dierr = kDIErrNone;
uint8_t blkBuf[kBlkSize];
long blockIndex = (long) (fOffset / kBlkSize);
int bufOffset = (int) (fOffset % kBlkSize); // (& 0x01ff)
size_t thisCount;
long progressCounter = 0;
if (len == 0) {
///* one block allocated for empty file */
//SetLastBlock(fBlockList[0], true);
return kDIErrNone;
}
assert(fOpenEOF != 0);
assert(blockIndex >= 0 && blockIndex < fBlockCount);
while (len) {
if (fBlockList[blockIndex] == 0) {
//LOGI(" ProDOS sparse index %d", blockIndex);
memset(blkBuf, 0, sizeof(blkBuf));
} else {
//LOGI(" ProDOS non-sparse index %d", blockIndex);
dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(fBlockList[blockIndex],
blkBuf);
if (dierr != kDIErrNone) {
LOGI(" ProDOS error reading block [%ld]=%d of '%s'",
blockIndex, fBlockList[blockIndex], fpFile->GetPathName());
return dierr;
}
}
thisCount = kBlkSize - bufOffset;
if (thisCount > len)
thisCount = len;
memcpy(buf, blkBuf + bufOffset, thisCount);
len -= thisCount;
buf = (char*)buf + thisCount;
bufOffset = 0;
blockIndex++;
progressCounter++;
if (progressCounter > 100 && len) {
progressCounter = 0;
/*
* Show progress within the current read request. This only
* kicks in for large reads, e.g. reformatting the entire file.
* For smaller reads, used when we're extracting w/o reformatting,
* "progressCounter" never gets large enough.
*/
if (!UpdateProgress(fOffset + incrLen - len)) {
dierr = kDIErrCancelled;
return dierr;
}
//::Sleep(100); // DEBUG DEBUG
}
}
fOffset += incrLen;
if (!UpdateProgress(fOffset))
dierr = kDIErrCancelled;
return dierr;
}
/*
* Write data at the current offset.
*
* For simplicity, we assume that there can only be one of two situations:
* (1) We're writing a directory, which might expand by one block; or
* (2) We're writing all of a brand-new file in one shot.
*
* Modifies fOpenEOF, fOpenBlocksUsed, fStorageType, and sets fModified.
*
* HEY: ProSel-16 describes these as fragmented, and it's probably right.
* The correct way to do this is to allocate index blocks before allocating
* the blocks they refer to, so that we don't have to jump all over the disk
* to read the indexes (which, at the moment, appear at the end of the file).
* A bit tricky, but doable.
*/
DIError A2FDProDOS::Write(const void* buf, size_t len, size_t* pActual)
{
DIError dierr = kDIErrNone;
A2FileProDOS* pFile = (A2FileProDOS*) fpFile;
DiskFSProDOS* pDiskFS = (DiskFSProDOS*) fpFile->GetDiskFS();
bool allocSparse = (pDiskFS->GetParameter(DiskFS::kParmProDOS_AllocSparse) != 0);
uint8_t blkBuf[kBlkSize];
uint16_t keyBlock;
bool allZero = true;
const uint8_t* scanPtr = (const uint8_t*)buf;
if (len >= 0x01000000) { // 16MB
assert(false);
return kDIErrInvalidArg;
}
/* use separate function for directories */
if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory ||
pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader)
{
return WriteDirectory(buf, len, pActual);
}
dierr = pDiskFS->LoadVolBitmap();
if (dierr != kDIErrNone)
goto bail;
assert(fOffset == 0); // big simplifying assumption
assert(fOpenEOF == 0); // another one
assert(fOpenBlocksUsed == 1);
assert(buf != NULL);
/* nothing to do for zero-length write; don't even set fModified */
if (len == 0)
goto bail;
if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended)
keyBlock = pFile->fDirEntry.keyPointer;
else {
if (fOpenRsrcFork)
keyBlock = pFile->fExtRsrc.keyBlock;
else
keyBlock = pFile->fExtData.keyBlock;
}
/*
* See if the file is completely empty. This lets us do an optimization
* where we store it as a seedling. (GS/OS seems to do this, ProDOS 8
* v2.0.3 tends to allocate the first block.)
*/
for (unsigned int i = 0; i < len; ++i, ++scanPtr) {
if (*scanPtr != 0x00) {
allZero = false;
break;
}
}
if (allZero) {
LOGI("+++ found file filled with %zd zeroes", len);
}
/*
* Special-case seedling files. Just write the data into the key block
* and we're done.
*/
if (allZero || len <= (size_t)kBlkSize) {
memset(blkBuf, 0, sizeof(blkBuf));
if (!allZero) {
memcpy(blkBuf, buf, len);
} else {
LOGI("+++ ProDOS storing large but empty file as seedling");
}
dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
fOpenEOF = len;
fOpenBlocksUsed = 1;
assert(fOpenStorageType == A2FileProDOS::kStorageSeedling);
fOffset += len;
fModified = true;
goto bail;
}
/*
* Start by allocating space for the block list. The list is always the
* same size, regardless of sparse allocations.
*
* We over-alloc by one so we can have an overrun detection entry.
*/
fBlockCount = (len + kBlkSize-1) / kBlkSize;
assert(fBlockCount > 0);
delete[] fBlockList;
fBlockList = new uint16_t[fBlockCount+1];
if (fBlockList == NULL) {
dierr = kDIErrMalloc;
goto bail;
}
fBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum;
/*
* Write the data blocks to disk, allocating as we go. We have to treat
* the last entry specially because it might not fill an entire block.
*/
const uint8_t* blkPtr;
long blockIdx;
long progressCounter;
progressCounter = 0;
blkPtr = (const uint8_t*) buf;
for (blockIdx = 0; blockIdx < fBlockCount; blockIdx++) {
long newBlock;
if (blockIdx == fBlockCount-1) {
/* for last block, copy partial and move blkPtr */
int copyLen = len - (blockIdx * kBlkSize);
assert(copyLen > 0 && copyLen <= kBlkSize);
memset(blkBuf, 0, sizeof(blkBuf));
memcpy(blkBuf, blkPtr, copyLen);
blkPtr = blkBuf;
}
if (allocSparse && IsEmptyBlock(blkPtr)) {
if (blockIdx == 0) {
// Fix for issues #18 and #49. GS/OS appears to get confused
// if the first entry in the master index block for a "tree"
// file is zero. We can avoid the problem by always allocating
// the first data block, which causes allocation of the first
// index block. (The "all zeroes" case was handled earlier,
// so if we got here we know this won't be an empty seedling.)
LOGI("+++ allocating storage for empty first block");
newBlock = pDiskFS->AllocBlock();
fOpenBlocksUsed++;
} else {
// Sparse.
newBlock = 0;
}
} else {
newBlock = pDiskFS->AllocBlock();
fOpenBlocksUsed++;
}
if (newBlock < 0) {
LOGI(" ProDOS disk full during write!");
dierr = kDIErrDiskFull;
goto bail;
}
fBlockList[blockIdx] = (uint16_t) newBlock;
if (newBlock != 0) {
dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkPtr);
if (dierr != kDIErrNone)
goto bail;
}
blkPtr += kBlkSize;
/*
* Update the progress counter and check to see if the "cancel" button
* has been hit. We don't call UpdateProgress on the last block
* because we could be passing an offset value larger than "len".
* Also, we don't want the progress bar to hit 100% until we've
* actually finished.
*
* We do NOT want to check this after we start writing index blocks.
* If we do, we need to make sure that whatever index blocks the file
* has match up with what we've allocated in the disk block map.
*
* We don't want to save the disk block map if the user cancels here,
* because then the blocks will be marked as "used" even though the
* index blocks for this file haven't been written yet.
*
* It's tricky to get this right, which is why we allocate space
* for the index blocks now -- running out of disk space and
* user cancellation are handled the same way. Once we get to the
* point where we're updating the file structure, we can neither be
* cancelled nor run out of space. (We can still hit a bad block,
* though, which we currently don't handle.)
*/
progressCounter++; // update every N blocks
if (progressCounter > 100 && blockIdx != fBlockCount) {
progressCounter = 0;
if (!UpdateProgress(blockIdx * kBlkSize)) {
dierr = kDIErrCancelled;
goto bail;
}
}
}
assert(fBlockList[fBlockCount] == A2FileProDOS::kInvalidBlockNum);
/*
* Now we have a full block map. Allocate any needed index blocks and
* write them.
*/
#if 0 // now done earlier
/*
* If our block map is empty, i.e. the entire file is sparse, then
* there's no need to create a sapling. We just leave the file in
* seedling form. This can only happen for a completely empty file.
*/
if (allZero) {
LOGI("+++ ProDOS storing large but empty file as seedling");
/* make sure key block is empty */
memset(blkBuf, 0, sizeof(blkBuf));
dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
fOpenStorageType = A2FileProDOS::kStorageSeedling;
fBlockList[0] = keyBlock;
} else
#endif
if (fBlockCount <= 256) {
/* sapling file, write an index block into the key block */
//bool allzero = true; <-- should this be getting used?
assert(fBlockCount > 1);
memset(blkBuf, 0, sizeof(blkBuf));
int i;
for (i = 0; i < fBlockCount; i++) {
//if (fBlockList[i] != 0)
// allzero = false;
blkBuf[i] = fBlockList[i] & 0xff;
blkBuf[256 + i] = (fBlockList[i] >> 8) & 0xff;
}
dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
fOpenStorageType = A2FileProDOS::kStorageSapling;
} else {
/* tree file, write two or more indexes and write master into key */
uint8_t masterBlk[kBlkSize];
int idx;
memset(masterBlk, 0, sizeof(masterBlk));
for (idx = 0; idx < fBlockCount; ) {
long newBlock;
int i;
memset(blkBuf, 0, sizeof(blkBuf));
for (i = 0; i < 256 && idx < fBlockCount; i++, idx++) {
blkBuf[i] = fBlockList[idx] & 0xff;
blkBuf[256+i] = (fBlockList[idx] >> 8) & 0xff;
}
/* allocate a new index block, if needed */
if (allocSparse && IsEmptyBlock(blkBuf))
newBlock = 0;
else {
newBlock = pDiskFS->AllocBlock();
fOpenBlocksUsed++;
}
if (newBlock != 0) {
dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
}
masterBlk[(idx-1) / 256] = (uint8_t) newBlock;
masterBlk[256 + (idx-1)/256] = (uint8_t) (newBlock >> 8);
}
dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, masterBlk);
if (dierr != kDIErrNone)
goto bail;
fOpenStorageType = A2FileProDOS::kStorageTree;
}
fOpenEOF = len;
fOffset += len;
fModified = true;
bail:
if (dierr == kDIErrNone)
dierr = pDiskFS->SaveVolBitmap();
/*
* We need to check UpdateProgress *after* the volume bitmap has been
* saved. Otherwise we'll have blocks allocated in the file's structure
* but not marked in-use in the map when the "dierr" check above fails.
*/
if (dierr == kDIErrNone) {
if (!UpdateProgress(fOffset))
dierr = kDIErrCancelled;
}
pDiskFS->FreeVolBitmap();
return dierr;
}
/*
* Determine whether a block is filled entirely with zeroes.
*/
bool A2FDProDOS::IsEmptyBlock(const uint8_t* blk)
{
int i;
for (i = 0; i < kBlkSize; i++) {
if (*blk++ != 0)
return false;
}
return true;
}
/*
* Write a directory, possibly extending it by one block.
*
* If we're growing, the extra block will already have been allocated, and is
* pointed to by the "next" pointer in the next-to-last block. (This
* pre-allocation makes our lives easier, and avoids a situation where we
* would have to update the volume bitmap when another function is already
* making lots of changes to it.)
*/
DIError A2FDProDOS::WriteDirectory(const void* buf, size_t len, size_t* pActual)
{
DIError dierr = kDIErrNone;
LOGD("ProDOS writing %lu bytes to directory '%s'",
(unsigned long) len, fpFile->GetPathName());
assert(len >= (size_t)kBlkSize);
assert((len % kBlkSize) == 0);
assert(len == (size_t)fOpenEOF || len == (size_t)fOpenEOF + kBlkSize);
if (len > (size_t)fOpenEOF) {
/*
* Extend the block list, remembering that we add an extra item
* on the end to check for overruns.
*/
uint16_t* newBlockList;
fBlockCount++;
newBlockList = new uint16_t[fBlockCount+1];
memcpy(newBlockList, fBlockList,
sizeof(uint16_t) * fBlockCount);
newBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum;
uint8_t* blkPtr;
blkPtr = (uint8_t*)buf + fOpenEOF - kBlkSize;
assert(blkPtr >= buf);
assert(GetShortLE(&blkPtr[0x02]) != 0);
newBlockList[fBlockCount-1] = GetShortLE(&blkPtr[0x02]);
delete[] fBlockList;
fBlockList = newBlockList;
LOGI(" ProDOS updated block list for subdir:");
DumpBlockList();
}
/*
* Now just run down the block list writing the directory.
*/
assert(len == (size_t)fBlockCount * kBlkSize);
int idx;
for (idx = 0; idx < fBlockCount; idx++) {
assert(fBlockList[idx] >= kVolHeaderBlock);
dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(fBlockList[idx],
(uint8_t*)buf + idx * kBlkSize);
if (dierr != kDIErrNone) {
LOGI(" ProDOS failed writing dir, block=%d", fBlockList[idx]);
goto bail;
}
}
fOpenEOF = len;
fOpenBlocksUsed = (uint16_t) fBlockCount; // very simple for subdirs
//fOpenStorageType
fModified = true;
bail:
return dierr;
}
/*
* Seek to a new position within the file.
*/
DIError A2FDProDOS::Seek(di_off_t offset, DIWhence whence)
{
DIError dierr = kDIErrNone;
switch (whence) {
case kSeekSet:
if (offset < 0 || offset > fOpenEOF)
return kDIErrInvalidArg;
fOffset = offset;
break;
case kSeekEnd:
if (offset > 0 || offset < -fOpenEOF)
return kDIErrInvalidArg;
fOffset = fOpenEOF + offset;
break;
case kSeekCur:
if (offset < -fOffset ||
offset >= (fOpenEOF - fOffset))
{
return kDIErrInvalidArg;
}
fOffset += offset;
break;
default:
assert(false);
return kDIErrInvalidArg;
}
assert(fOffset >= 0 && fOffset <= fOpenEOF);
return dierr;
}
/*
* Return current offset.
*/
di_off_t A2FDProDOS::Tell(void)
{
//if (fBlockList == NULL)
// return kDIErrNotReady;
return fOffset;
}
/*
* Release file state.
*
* Most applications don't check the value of "Close", or call it from a
* destructor, so we call CloseDescr whether we succeed or not.
*/
DIError A2FDProDOS::Close(void)
{
DIError dierr = kDIErrNone;
if (fModified) {
A2FileProDOS* pFile = (A2FileProDOS*) fpFile;
uint8_t blkBuf[kBlkSize];
uint8_t newStorageType = fOpenStorageType;
uint16_t newBlocksUsed = fOpenBlocksUsed;
uint32_t newEOF = (uint32_t) fOpenEOF; // TODO: assert range
uint16_t combinedBlocksUsed;
uint32_t combinedEOF;
/*
* If this is an extended file, fix the entries in the extended
* key block, and adjust the values to be stored in the directory.
*/
if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) {
/* these two don't change */
newStorageType = pFile->fDirEntry.storageType;
dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(
pFile->fDirEntry.keyPointer, blkBuf);
if (dierr != kDIErrNone)
goto bail;
int offset = 0;
if (fOpenRsrcFork)
offset = 256;
blkBuf[0x00 + offset] = fOpenStorageType;
// key block doesn't change
PutShortLE(&blkBuf[0x03 + offset], newBlocksUsed);
blkBuf[0x05 + offset] = (uint8_t) newEOF;
blkBuf[0x06 + offset] = (uint8_t) (newEOF >> 8);
blkBuf[0x07 + offset] = (uint8_t) (newEOF >> 16);
dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(
pFile->fDirEntry.keyPointer, blkBuf);
if (dierr != kDIErrNone)
goto bail;
// file blocks used is sum of data and rsrc block counts +1 for key
combinedBlocksUsed =
GetShortLE(&blkBuf[0x03]) + GetShortLE(&blkBuf[0x103]) +1;
combinedEOF = 512; // for some reason this gets stuffed in
} else {
combinedBlocksUsed = newBlocksUsed;
combinedEOF = newEOF;
}
/*
* Update fields in the file's directory entry. Unless, of course,
* this is the volume directory itself.
*/
if (pFile->fParentDirBlock != 0) {
dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(
pFile->fParentDirBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
uint8_t* pParentPtr;
pParentPtr = blkBuf + 0x04 + pFile->fParentDirIdx * kEntryLength;
assert(pParentPtr + kEntryLength < blkBuf + kBlkSize);
if (toupper(pParentPtr[0x01]) != toupper(pFile->fDirEntry.fileName[0]))
{
LOGW("ProDOS ERROR: parent pointer has wrong entry??");
assert(false);
dierr = kDIErrInternal;
goto bail;
}
/* update the fields from the open file */
pParentPtr[0x00] =
(pParentPtr[0x00] & 0x0f) | (newStorageType << 4);
PutShortLE(&pParentPtr[0x13], combinedBlocksUsed);
if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended)
{
PutShortLE(&pParentPtr[0x15], (uint16_t) newEOF);
pParentPtr[0x17] = (uint8_t) (newEOF >> 16);
}
/* don't update the mod date for now */
//PutLongLE(&pParentPtr[0x21], A2FileProDOS::ConvertProDate(time(NULL)));
dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(
pFile->fParentDirBlock, blkBuf);
if (dierr != kDIErrNone)
goto bail;
}
/*
* Find the #of sparse blocks. We do this to update the "sparse EOF",
* which determines the "compressed" size shown in the file list. We
* have two cases: normal file with sparse contents, and seedling file
* that is entirely sparse except for the first block.
*
* In the normal case, we walk through the list of data blocks,
* looking for gaps. In the seedling case, we just use the EOF.
*
* This is just for display. The value seen after adding a file should
* not change if you reload the disk image.
*/
int sparseBlocks = 0;
if (fBlockCount == 1 && fOpenEOF > kBlkSize) {
// 1023/1024 = 2 blocks = 1 sparse
// 1025 = 3 blocks = 2 sparse
sparseBlocks = (int)((fOpenEOF-1) / kBlkSize);
} else {
for (int i = 0; i < fBlockCount; i++) {
if (fBlockList[i] == 0)
sparseBlocks++;
}
}
/*
* Update our internal copies of stuff. The EOFs have changed, and
* in theory we'd want to update the modification date. In practice
* we're usually shuffling data from one archive to another and want
* to preserve the mod date. (Could be a DiskFS global pref?)
*/
pFile->fDirEntry.storageType = newStorageType;
pFile->fDirEntry.blocksUsed = combinedBlocksUsed;
pFile->fDirEntry.eof = combinedEOF;
if (newStorageType == A2FileProDOS::kStorageExtended) {
if (!fOpenRsrcFork) {
pFile->fExtData.storageType = fOpenStorageType;
pFile->fExtData.blocksUsed = newBlocksUsed;
pFile->fExtData.eof = newEOF;
pFile->fSparseDataEof = (di_off_t) newEOF - (sparseBlocks * kBlkSize);
if (pFile->fSparseDataEof < 0)
pFile->fSparseDataEof = 0;
} else {
pFile->fExtRsrc.storageType = fOpenStorageType;
pFile->fExtRsrc.blocksUsed = newBlocksUsed;
pFile->fExtRsrc.eof = newEOF;
pFile->fSparseRsrcEof = (di_off_t) newEOF - (sparseBlocks * kBlkSize);
if (pFile->fSparseRsrcEof < 0)
pFile->fSparseRsrcEof = 0;
}
} else {
pFile->fSparseDataEof = (di_off_t) newEOF - (sparseBlocks * kBlkSize);
if (pFile->fSparseDataEof < 0)
pFile->fSparseDataEof = 0;
}
// update mod date?
//LOGI("File '%s' closed", pFile->GetPathName());
//pFile->Dump();
}
bail:
fpFile->CloseDescr(this);
return dierr;
}
/*
* Return the #of sectors/blocks in the file.
*/
long A2FDProDOS::GetSectorCount(void) const
{
//if (fBlockList == NULL)
// return kDIErrNotReady;
return fBlockCount * 2;
}
long A2FDProDOS::GetBlockCount(void) const
{
//if (fBlockList == NULL)
// return kDIErrNotReady;
return fBlockCount;
}
/*
* Return the Nth track/sector in this file.
*/
DIError A2FDProDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
{
//if (fBlockList == NULL)
// return kDIErrNotReady;
long prodosIdx = sectorIdx / 2;
if (prodosIdx < 0 || prodosIdx >= fBlockCount)
return kDIErrInvalidIndex;
long prodosBlock = fBlockList[prodosIdx];
if (prodosBlock == 0)
*pTrack = *pSector = 0; // special-case to avoid returning (0,1)
else
BlockToTrackSector(prodosBlock, (sectorIdx & 0x01) != 0, pTrack, pSector);
return kDIErrNone;
}
/*
* Return the Nth 512-byte block in this file.
*/
DIError A2FDProDOS::GetStorage(long blockIdx, long* pBlock) const
{
//if (fBlockList == NULL)
// return kDIErrNotReady;
if (blockIdx < 0 || blockIdx >= fBlockCount)
return kDIErrInvalidIndex;
long prodosBlock = fBlockList[blockIdx];
*pBlock = prodosBlock;
assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks());
return kDIErrNone;
}
/*
* Dump the list of blocks from an open file, skipping over
* "sparsed-out" entries.
*/
void A2FDProDOS::DumpBlockList(void) const
{
long ll;
LOGI(" ProDOS file block list (count=%ld)", fBlockCount);
for (ll = 0; ll <= fBlockCount; ll++) {
if (fBlockList[ll] != 0) {
LOGI(" %5ld: 0x%04x", ll, fBlockList[ll]);
}
}
}