mirror of
https://github.com/fadden/ciderpress.git
synced 2024-11-26 02:49:20 +00:00
b42cc8efe9
gcc is justifiably annoyed by variable initialization crossed by a goto. (issue #51)
5184 lines
173 KiB
C++
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]);
|
|
}
|
|
}
|
|
}
|