mirror of
https://github.com/fadden/ciderpress.git
synced 2024-12-22 05:29:43 +00:00
3448 lines
95 KiB
C++
3448 lines
95 KiB
C++
/*
|
|
* CiderPress
|
|
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
|
|
* See the file LICENSE for distribution terms.
|
|
*/
|
|
/*
|
|
* Implementation of DiskFSDOS33 and A2FileDOS classes.
|
|
*
|
|
* Works for DOS 3.2 and "wide DOS" as well.
|
|
*
|
|
* BUG: does not keep VolumeUsage up to date.
|
|
*/
|
|
#include "StdAfx.h"
|
|
#include "DiskImgPriv.h"
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* DiskFSDOS33
|
|
* ===========================================================================
|
|
*/
|
|
|
|
const int kMaxSectors = 32;
|
|
|
|
const int kSctSize = 256;
|
|
/* do we need a way to override these? */
|
|
const int kVTOCTrack = 17;
|
|
const int kVTOCSector = 0;
|
|
const int kCatalogEntryOffset = 0x0b; // first entry in cat sect starts here
|
|
const int kCatalogEntrySize = 0x23; // length in bytes of catalog entries
|
|
const int kCatalogEntriesPerSect = 7; // #of entries per catalog sector
|
|
const int kEntryDeleted = 0xff; // this is used for track# of deleted files
|
|
const int kEntryUnused = 0x00; // this is track# in never-used entries
|
|
const int kMaxTSPairs = 0x7a; // 122 entries for 256-byte sectors
|
|
const int kTSOffset = 0x0c; // first T/S entry in a T/S list
|
|
|
|
const int kMaxTSIterations = 32;
|
|
|
|
/*
|
|
* Get a pointer to the Nth entry in a catalog sector.
|
|
*/
|
|
static inline unsigned char*
|
|
GetCatalogEntryPtr(unsigned char* basePtr, int entryNum)
|
|
{
|
|
assert(entryNum >= 0 && entryNum < kCatalogEntriesPerSect);
|
|
return basePtr + kCatalogEntryOffset + entryNum * kCatalogEntrySize;
|
|
}
|
|
|
|
|
|
/*
|
|
* Test this image for DOS3.3-ness.
|
|
*
|
|
* Some notes on tricky disks...
|
|
*
|
|
* DISK019B (Ultima II player master) has a copy of the VTOC in track 11
|
|
* sector 1, which causes a loop back to track 11 sector f. We may want
|
|
* to be clever here and allow it, but we have to be careful because
|
|
* we must be similarly clever in the VTOC read routines. (Need a more
|
|
* sophisticated loop detector, since a loop will crank our "foundGood" up.)
|
|
*
|
|
* DISK038B (Congo Bongo) has some "crack" titles and a valid VTOC, but not
|
|
* much else. Could allow it if the user explicitly told us to use DOS33,
|
|
* but it's a little thin.
|
|
*
|
|
* DISK112B.X (Ultima I player master) has a catalog that jumps around a lot.
|
|
* It's perfectly valid, but we don't really detect it properly. Forcing
|
|
* DOS interpretation should be acceptable.
|
|
*
|
|
* DISK175A (Standing Stones) has an extremely short but valid catalog track.
|
|
*
|
|
* DISK198B (Aliens+docs) gets 3 and bails with a self-reference.
|
|
*/
|
|
static DIError
|
|
TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, int* pGoodCount)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
unsigned char sctBuf[kSctSize];
|
|
int numTracks, numSectors;
|
|
int catTrack, catSect;
|
|
int foundGood = 0;
|
|
int iterations = 0;
|
|
|
|
*pGoodCount = 0;
|
|
|
|
dierr = pImg->ReadTrackSectorSwapped(kVTOCTrack, kVTOCSector,
|
|
sctBuf, imageOrder, DiskImg::kSectorOrderDOS);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
catTrack = sctBuf[0x01];
|
|
catSect = sctBuf[0x02];
|
|
numTracks = sctBuf[0x34];
|
|
numSectors = sctBuf[0x35];
|
|
|
|
if (!(sctBuf[0x27] == kMaxTSPairs) ||
|
|
/*!(sctBuf[0x36] == 0 && sctBuf[0x37] == 1) ||*/ // bytes per sect
|
|
!(numTracks <= DiskFSDOS33::kMaxTracks) ||
|
|
!(numSectors == 13 || numSectors == 16 || numSectors == 32) ||
|
|
!(catTrack < numTracks && catSect < numSectors) ||
|
|
0)
|
|
{
|
|
WMSG1(" DOS header test failed (order=%d)\n", imageOrder);
|
|
dierr = kDIErrFilesystemNotFound;
|
|
goto bail;
|
|
}
|
|
|
|
foundGood++; // score one for a valid-looking VTOC
|
|
|
|
/*
|
|
* Walk through the catalog track to try to figure out ordering.
|
|
*/
|
|
while (catTrack != 0 && catSect != 0 &&
|
|
iterations < DiskFSDOS33::kMaxCatalogSectors)
|
|
{
|
|
dierr = pImg->ReadTrackSectorSwapped(catTrack, catSect, sctBuf,
|
|
imageOrder, DiskImg::kSectorOrderDOS);
|
|
if (dierr != kDIErrNone) {
|
|
dierr = kDIErrNone;
|
|
break; /* allow it if earlier stuff was okay */
|
|
}
|
|
|
|
if (catTrack == sctBuf[1] && catSect == sctBuf[2] +1)
|
|
foundGood++;
|
|
else if (catTrack == sctBuf[1] && catSect == sctBuf[2]) {
|
|
WMSG2(" DOS detected self-reference on cat (%d,%d)\n",
|
|
catTrack, catSect);
|
|
break;
|
|
}
|
|
catTrack = sctBuf[1];
|
|
catSect = sctBuf[2];
|
|
iterations++; // watch for infinite loops
|
|
}
|
|
if (iterations >= DiskFSDOS33::kMaxCatalogSectors) {
|
|
/* possible cause: LF->CR conversion screws up link to sector $0a */
|
|
dierr = kDIErrDirectoryLoop;
|
|
WMSG1(" DOS directory links cause a loop (order=%d)\n", imageOrder);
|
|
goto bail;
|
|
}
|
|
|
|
WMSG2(" DOS foundGood=%d order=%d\n", foundGood, imageOrder);
|
|
*pGoodCount = foundGood;
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Test to see if the image is a DOS 3.2 or DOS 3.3 disk.
|
|
*/
|
|
/*static*/ DIError
|
|
DiskFSDOS33::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder,
|
|
DiskImg::FSFormat* pFormat, FSLeniency leniency)
|
|
{
|
|
if (pImg->GetNumTracks() > kMaxInterestingTracks)
|
|
return kDIErrFilesystemNotFound;
|
|
|
|
DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax];
|
|
|
|
DiskImg::GetSectorOrderArray(ordering, *pOrder);
|
|
|
|
DiskImg::SectorOrder bestOrder = DiskImg::kSectorOrderUnknown;
|
|
int bestCount = 0;
|
|
|
|
for (int i = 0; i < DiskImg::kSectorOrderMax; i++) {
|
|
int goodCount = 0;
|
|
|
|
if (ordering[i] == DiskImg::kSectorOrderUnknown)
|
|
continue;
|
|
if (TestImage(pImg, ordering[i], &goodCount) == kDIErrNone) {
|
|
if (goodCount > bestCount) {
|
|
bestCount = goodCount;
|
|
bestOrder = ordering[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestCount >= 4 ||
|
|
(leniency == kLeniencyVery && bestCount >= 2))
|
|
{
|
|
WMSG2(" DOS test: bestCount=%d for order=%d\n", bestCount, bestOrder);
|
|
assert(bestOrder != DiskImg::kSectorOrderUnknown);
|
|
*pOrder = bestOrder;
|
|
*pFormat = DiskImg::kFormatDOS33;
|
|
if (pImg->GetNumSectPerTrack() == 13)
|
|
*pFormat = DiskImg::kFormatDOS32;
|
|
return kDIErrNone;
|
|
}
|
|
|
|
WMSG0(" DOS33 didn't find valid DOS3.2 or DOS3.3\n");
|
|
return kDIErrFilesystemNotFound;
|
|
}
|
|
|
|
|
|
/*
|
|
* Get things rolling.
|
|
*
|
|
* Since we're assured that this is a valid disk, errors encountered from here
|
|
* on out must be handled somehow, possibly by claiming that the disk is
|
|
* completely full and has no files on it.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::Initialize(InitMode initMode)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
|
|
fVolumeUsage.Create(fpImg->GetNumTracks(), fpImg->GetNumSectPerTrack());
|
|
|
|
dierr = ReadVTOC();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
//DumpVTOC();
|
|
|
|
dierr = ScanVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
if (initMode == kInitHeaderOnly) {
|
|
WMSG0(" DOS - headerOnly set, skipping file load\n");
|
|
goto bail;
|
|
}
|
|
|
|
/* read the contents of the catalog, creating our A2File list */
|
|
dierr = ReadCatalog();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/* run through and get file lengths and data offsets */
|
|
dierr = GetFileLengths();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/* mark DOS tracks appropriately */
|
|
FixVolumeUsageMap();
|
|
|
|
fDiskIsGood = CheckDiskIsGood();
|
|
|
|
fVolumeUsage.Dump();
|
|
|
|
// A2File* pFile;
|
|
// pFile = GetNextFile(nil);
|
|
// while (pFile != nil) {
|
|
// pFile->Dump();
|
|
// pFile = GetNextFile(pFile);
|
|
// }
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Read some fields from the disk Volume Table of Contents.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::ReadVTOC(void)
|
|
{
|
|
DIError dierr;
|
|
|
|
dierr = LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
fFirstCatTrack = fVTOC[0x01];
|
|
fFirstCatSector = fVTOC[0x02];
|
|
fVTOCVolumeNumber = fVTOC[0x06];
|
|
fVTOCNumTracks = fVTOC[0x34];
|
|
fVTOCNumSectors = fVTOC[0x35];
|
|
|
|
if (fFirstCatTrack >= fpImg->GetNumTracks())
|
|
return kDIErrBadDiskImage;
|
|
if (fFirstCatSector >= fpImg->GetNumSectPerTrack())
|
|
return kDIErrBadDiskImage;
|
|
|
|
if (fVTOCNumTracks != fpImg->GetNumTracks()) {
|
|
WMSG2(" DOS33 warning: VTOC numtracks %d vs %ld\n",
|
|
fVTOCNumTracks, fpImg->GetNumTracks());
|
|
}
|
|
if (fVTOCNumSectors != fpImg->GetNumSectPerTrack()) {
|
|
WMSG2(" DOS33 warning: VTOC numsect %d vs %d\n",
|
|
fVTOCNumSectors, fpImg->GetNumSectPerTrack());
|
|
}
|
|
|
|
// call SetDiskVolumeNum with the appropriate thing
|
|
UpdateVolumeNum();
|
|
|
|
bail:
|
|
FreeVolBitmap();
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Call this if fpImg's volume num (derived from nibble formats) or
|
|
* the VTOC's volume number changes.
|
|
*/
|
|
void
|
|
DiskFSDOS33::UpdateVolumeNum(void)
|
|
{
|
|
/* use the sector-embedded volume number, if available */
|
|
if (fpImg->GetDOSVolumeNum() == DiskImg::kVolumeNumNotSet)
|
|
SetDiskVolumeNum(fVTOCVolumeNumber);
|
|
else
|
|
SetDiskVolumeNum(fpImg->GetDOSVolumeNum());
|
|
if (fDiskVolumeNum != fVTOCVolumeNumber) {
|
|
WMSG2(" NOTE: ignoring VTOC vol (%d) in favor of embedded (%d)\n",
|
|
fVTOCVolumeNumber, fDiskVolumeNum);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Set the disk volume number (fDiskVolumeNum) and derived fields.
|
|
*/
|
|
void
|
|
DiskFSDOS33::SetDiskVolumeNum(int val)
|
|
{
|
|
if (val < 0 || val > 255) {
|
|
// Actual valid range should be 1-254, but it's possible for a
|
|
// sector edit to put invalid stuff here. It's just one byte
|
|
// though, so 0-255 should be guaranteed.
|
|
assert(false);
|
|
return;
|
|
}
|
|
fDiskVolumeNum = val;
|
|
sprintf(fDiskVolumeName, "DOS%03d", fDiskVolumeNum);
|
|
if (fpImg->GetFSFormat() == DiskImg::kFormatDOS32)
|
|
sprintf(fDiskVolumeID, "DOS 3.2 Volume %03d", fDiskVolumeNum);
|
|
else
|
|
sprintf(fDiskVolumeID, "DOS 3.3 Volume %03d", fDiskVolumeNum);
|
|
}
|
|
|
|
|
|
/*
|
|
* Dump some VTOC fields.
|
|
*/
|
|
void
|
|
DiskFSDOS33::DumpVTOC(void)
|
|
{
|
|
|
|
WMSG2("VTOC catalog: track=%d sector=%d\n",
|
|
fFirstCatTrack, fFirstCatSector);
|
|
WMSG3(" volnum=%d numTracks=%d numSects=%d\n",
|
|
fVTOCVolumeNumber, fVTOCNumTracks, fVTOCNumSectors);
|
|
}
|
|
|
|
/*
|
|
* Update an entry in the VolumeUsage map, watching for conflicts.
|
|
*/
|
|
void
|
|
DiskFSDOS33::SetSectorUsage(long track, long sector,
|
|
VolumeUsage::ChunkPurpose purpose)
|
|
{
|
|
VolumeUsage::ChunkState cstate;
|
|
|
|
//WMSG3(" DOS setting usage %d,%d to %d\n", track, sector, purpose);
|
|
|
|
fVolumeUsage.GetChunkState(track, sector, &cstate);
|
|
if (cstate.isUsed) {
|
|
cstate.purpose = VolumeUsage::kChunkPurposeConflict;
|
|
// WMSG2(" DOS conflicting uses for t=%d s=%d\n", track, sector);
|
|
} else {
|
|
cstate.isUsed = true;
|
|
cstate.purpose = purpose;
|
|
}
|
|
fVolumeUsage.SetChunkState(track, sector, &cstate);
|
|
}
|
|
|
|
/*
|
|
* Examine the volume bitmap, setting fields in the VolumeUsage map
|
|
* as appropriate. We mark "isMarkedUsed", but leave "isUsed" clear. The
|
|
* "isUsed" flag gets set by the DOS catalog track processor and the file
|
|
* scanners.
|
|
*
|
|
* We can't mark the DOS tracks, because there's no reliable way to tell by
|
|
* looking at a DOS disk whether it has a bootable DOS image. It's possible
|
|
* the tracks are marked in-use because files are stored there. Some
|
|
* tweaked versions of DOS freed up a few sectors on track 2, so partial
|
|
* allocation isn't a good indicator.
|
|
*
|
|
* What we have to do is wait until we have all the information for the
|
|
* various files, and mark the tracks as owned by DOS if nobody else
|
|
* claims them.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::ScanVolBitmap(void)
|
|
{
|
|
DIError dierr;
|
|
VolumeUsage::ChunkState cstate;
|
|
char freemap[32+1] = "--------------------------------";
|
|
|
|
cstate.isUsed = false;
|
|
cstate.isMarkedUsed = true;
|
|
cstate.purpose = (VolumeUsage::ChunkPurpose) 0;
|
|
|
|
dierr = LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
WMSG0(" map 0123456789abcdef\n");
|
|
|
|
int i;
|
|
for (i = 0; i < kMaxTracks; i++) {
|
|
unsigned long val, origVal;
|
|
int bit;
|
|
|
|
val = (unsigned long) fVTOC[0x38 + i*4] << 24;
|
|
val |= (unsigned long) fVTOC[0x39 + i*4] << 16;
|
|
val |= (unsigned long) fVTOC[0x3a + i*4] << 8;
|
|
val |= (unsigned long) fVTOC[0x3b + i*4];
|
|
origVal = val;
|
|
|
|
/* init the VolumeUsage stuff */
|
|
for (bit = fpImg->GetNumSectPerTrack()-1; bit >= 0; bit--) {
|
|
freemap[bit] = val & 0x80000000 ? '.' : 'X';
|
|
|
|
if (i < fpImg->GetNumTracks() && !(val & 0x80000000)) {
|
|
/* mark the sector as in-use */
|
|
if (fVolumeUsage.SetChunkState(i, bit, &cstate) != kDIErrNone) {
|
|
assert(false);
|
|
}
|
|
}
|
|
val <<= 1;
|
|
}
|
|
WMSG3(" %2d: %s (0x%08lx)\n", i, freemap, origVal);
|
|
}
|
|
|
|
/* we know the VTOC is used, so mark it now */
|
|
SetSectorUsage(kVTOCTrack, kVTOCSector, VolumeUsage::kChunkPurposeVolumeDir);
|
|
|
|
bail:
|
|
FreeVolBitmap();
|
|
return dierr;
|
|
}
|
|
|
|
|
|
/*
|
|
* Load the VTOC into the buffer.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::LoadVolBitmap(void)
|
|
{
|
|
DIError dierr;
|
|
|
|
assert(!fVTOCLoaded);
|
|
|
|
dierr = fpImg->ReadTrackSector(kVTOCTrack, kVTOCSector, fVTOC);
|
|
if (dierr != kDIErrNone)
|
|
return dierr;
|
|
|
|
fVTOCLoaded = true;
|
|
return kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* Save our copy of the volume bitmap.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::SaveVolBitmap(void)
|
|
{
|
|
if (!fVTOCLoaded) {
|
|
assert(false);
|
|
return kDIErrNotReady;
|
|
}
|
|
|
|
return fpImg->WriteTrackSector(kVTOCTrack, kVTOCSector, fVTOC);
|
|
}
|
|
|
|
/*
|
|
* Throw away the volume bitmap, discarding any unsaved changes.
|
|
*
|
|
* It's okay to call this if the bitmap isn't loaded.
|
|
*/
|
|
void
|
|
DiskFSDOS33::FreeVolBitmap(void)
|
|
{
|
|
fVTOCLoaded = false;
|
|
|
|
#ifdef _DEBUG
|
|
memset(fVTOC, 0x99, sizeof(fVTOC));
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Return entry N from the VTOC.
|
|
*/
|
|
inline unsigned long
|
|
DiskFSDOS33::GetVTOCEntry(const unsigned char* pVTOC, long track) const
|
|
{
|
|
unsigned long val;
|
|
val = (unsigned long) pVTOC[0x38 + track*4] << 24;
|
|
val |= (unsigned long) pVTOC[0x39 + track*4] << 16;
|
|
val |= (unsigned long) pVTOC[0x3a + track*4] << 8;
|
|
val |= (unsigned long) pVTOC[0x3b + track*4];
|
|
|
|
return val;
|
|
}
|
|
|
|
/*
|
|
* Allocate a new sector from the unused pool.
|
|
*
|
|
* Only touches the in-memory copy.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::AllocSector(TrackSector* pTS)
|
|
{
|
|
unsigned long val;
|
|
unsigned long mask;
|
|
long track, numSectPerTrack;
|
|
|
|
/* we could compute "mask", but it's faster and easier to do this */
|
|
numSectPerTrack = GetDiskImg()->GetNumSectPerTrack();
|
|
if (numSectPerTrack == 13)
|
|
mask = 0xfff80000;
|
|
else if (numSectPerTrack == 16)
|
|
mask = 0xffff0000;
|
|
else if (numSectPerTrack == 32)
|
|
mask = 0xffffffff;
|
|
else {
|
|
assert(false);
|
|
return kDIErrInternal;
|
|
}
|
|
|
|
/*
|
|
* Start by finding a track with a free sector. We know it's free
|
|
* because the bits aren't all zero.
|
|
*
|
|
* In theory we don't need "mask", because the DOS format routine is
|
|
* good about leaving the unused bits clear, and nobody else disturbs
|
|
* them. However, it's best not to rely on it.
|
|
*/
|
|
for (track = kVTOCTrack; track > 0; track--) {
|
|
val = GetVTOCEntry(fVTOC, track);
|
|
if ((val & mask) != 0)
|
|
break;
|
|
}
|
|
if (track == 0) {
|
|
long numTracks = GetDiskImg()->GetNumTracks();
|
|
for (track = kVTOCTrack; track < numTracks; track++)
|
|
{
|
|
val = GetVTOCEntry(fVTOC, track);
|
|
if ((val & mask) != 0)
|
|
break;
|
|
}
|
|
if (track == numTracks) {
|
|
WMSG0("DOS33 AllocSector unable to find empty sector\n");
|
|
return kDIErrDiskFull;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We've got the track. Now find the first free sector.
|
|
*/
|
|
int sector;
|
|
sector = numSectPerTrack-1;
|
|
while (sector >= 0) {
|
|
if (val & 0x80000000) {
|
|
//WMSG2("+++ allocating T=%d S=%d\n", track, sector);
|
|
SetSectorUseEntry(track, sector, true);
|
|
break;
|
|
}
|
|
|
|
val <<= 1;
|
|
sector--;
|
|
}
|
|
if (sector < 0) {
|
|
assert(false);
|
|
return kDIErrInternal; // should not have failed
|
|
}
|
|
|
|
/*
|
|
* Mostly for fun, update the VTOC allocation thingy.
|
|
*/
|
|
fVTOC[0x30] = (unsigned char) track; // last track where alloc happened
|
|
if (track < kVTOCTrack)
|
|
fVTOC[0x31] = 0xff; // descending
|
|
else
|
|
fVTOC[0x31] = 0x01; // ascending
|
|
|
|
pTS->track = (char) track;
|
|
pTS->sector = (char) sector;
|
|
|
|
return kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* Create an in-use map for an empty disk. Sets up the VTOC map only.
|
|
*
|
|
* If "withDOS" is set, mark the first 3 tracks as in-use.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::CreateEmptyBlockMap(bool withDOS)
|
|
{
|
|
DIError dierr;
|
|
long track, sector, maxTrack;
|
|
|
|
dierr = LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
return dierr;
|
|
|
|
if (withDOS)
|
|
maxTrack = 3;
|
|
else
|
|
maxTrack = 1;
|
|
|
|
/*
|
|
* Set each bit individually. Slower, but exercises standard functions.
|
|
*
|
|
* Clear all "in use" flags, except for track 0, track 17, and (if
|
|
* withDOS is set) tracks 1 and 2.
|
|
*/
|
|
for (track = fpImg->GetNumTracks()-1; track >= 0; track--) {
|
|
for (sector = fpImg->GetNumSectPerTrack()-1; sector >= 0; sector--) {
|
|
if (track < maxTrack || track == kVTOCTrack)
|
|
SetSectorUseEntry(track, sector, true);
|
|
else
|
|
SetSectorUseEntry(track, sector, false);
|
|
}
|
|
}
|
|
|
|
dierr = SaveVolBitmap();
|
|
FreeVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
return dierr;
|
|
|
|
return kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* Get the state of an entry in the VTOC sector use map.
|
|
*
|
|
* Returns "true" if it's in use, "false" otherwise.
|
|
*/
|
|
bool
|
|
DiskFSDOS33::GetSectorUseEntry(long track, int sector) const
|
|
{
|
|
assert(fVTOCLoaded);
|
|
assert(track >= 0 && track < fpImg->GetNumTracks());
|
|
assert(sector >= 0 && sector < fpImg->GetNumSectPerTrack());
|
|
|
|
unsigned long val, mask;
|
|
|
|
val = GetVTOCEntry(fVTOC, track);
|
|
//val = (unsigned long) fVTOC[0x38 + track*4] << 24;
|
|
//val |= (unsigned long) fVTOC[0x39 + track*4] << 16;
|
|
//val |= (unsigned long) fVTOC[0x3a + track*4] << 8;
|
|
//val |= (unsigned long) fVTOC[0x3b + track*4];
|
|
|
|
/*
|
|
* The highest-numbered sector is now in the high bit. If this is a
|
|
* 16-sector disk, the high bit holds the state of sector 15.
|
|
*
|
|
* A '1' indicates the sector is free, '0' indicates it's in use.
|
|
*/
|
|
mask = 1L << (32 - fpImg->GetNumSectPerTrack() + sector);
|
|
return (val & mask) == 0;
|
|
}
|
|
|
|
/*
|
|
* Change the state of an entry in the VTOC sector use map.
|
|
*/
|
|
void
|
|
DiskFSDOS33::SetSectorUseEntry(long track, int sector, bool inUse)
|
|
{
|
|
assert(fVTOCLoaded);
|
|
assert(track >= 0 && track < fpImg->GetNumTracks());
|
|
assert(sector >= 0 && sector < fpImg->GetNumSectPerTrack());
|
|
|
|
unsigned long val, mask;
|
|
|
|
val = GetVTOCEntry(fVTOC, track);
|
|
|
|
/* highest sector is always in the high bit */
|
|
mask = 1L << (32 - fpImg->GetNumSectPerTrack() + sector);
|
|
if (inUse)
|
|
val &= ~mask;
|
|
else
|
|
val |= mask;
|
|
|
|
fVTOC[0x38 + track*4] = (unsigned char) (val >> 24);
|
|
fVTOC[0x39 + track*4] = (unsigned char) (val >> 16);
|
|
fVTOC[0x3a + track*4] = (unsigned char) (val >> 8);
|
|
fVTOC[0x3b + track*4] = (unsigned char) val;
|
|
}
|
|
|
|
|
|
/*
|
|
* Get the amount of free space remaining.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits,
|
|
int* pUnitSize) const
|
|
{
|
|
DIError dierr;
|
|
long track, sector, freeSectors;
|
|
|
|
dierr = const_cast<DiskFSDOS33*>(this)->LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
return dierr;
|
|
|
|
freeSectors = 0;
|
|
for (track = GetDiskImg()->GetNumTracks()-1; track >= 0; track--) {
|
|
for (sector = GetDiskImg()->GetNumSectPerTrack()-1; sector >= 0; sector--)
|
|
{
|
|
if (!GetSectorUseEntry(track, sector))
|
|
freeSectors++;
|
|
}
|
|
}
|
|
|
|
*pTotalUnits = fpImg->GetNumTracks() * fpImg->GetNumSectPerTrack();
|
|
*pFreeUnits = freeSectors;
|
|
*pUnitSize = kSectorSize;
|
|
|
|
const_cast<DiskFSDOS33*>(this)->FreeVolBitmap();
|
|
return kDIErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Fix up the DOS tracks.
|
|
*
|
|
* Any sectors marked used but not actually in use by a file are marked as
|
|
* in use by the system. We have to be somewhat careful here because some
|
|
* disks had DOS removed to add space, un-set the last few sectors of track 2
|
|
* that weren't actually used by DOS, or did some other funky thing.
|
|
*/
|
|
void
|
|
DiskFSDOS33::FixVolumeUsageMap(void)
|
|
{
|
|
VolumeUsage::ChunkState cstate;
|
|
int track, sector;
|
|
|
|
for (track = 0; track < 3; track++) {
|
|
for (sector = 0; sector < fpImg->GetNumSectPerTrack(); sector++) {
|
|
fVolumeUsage.GetChunkState(track, sector, &cstate);
|
|
if (cstate.isMarkedUsed && !cstate.isUsed) {
|
|
cstate.isUsed = true;
|
|
cstate.purpose = VolumeUsage::kChunkPurposeSystem;
|
|
fVolumeUsage.SetChunkState(track, sector, &cstate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Read the disk's catalog.
|
|
*
|
|
* NOTE: supposedly DOS stops reading the catalog track when it finds the
|
|
* first entry with a 00 byte, which is why deleted files use ff. If so,
|
|
* it *might* make sense to mimic this behavior, though on a health disk
|
|
* we shouldn't be finding garbage anyway.
|
|
*
|
|
* Fills out "fCatalogSectors" as it works.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::ReadCatalog(void)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
unsigned char sctBuf[kSctSize];
|
|
int catTrack, catSect;
|
|
int iterations;
|
|
|
|
catTrack = fFirstCatTrack;
|
|
catSect = fFirstCatSector;
|
|
iterations = 0;
|
|
|
|
memset(fCatalogSectors, 0, sizeof(fCatalogSectors));
|
|
|
|
while (catTrack != 0 && catSect != 0 && iterations < kMaxCatalogSectors)
|
|
{
|
|
SetSectorUsage(catTrack, catSect, VolumeUsage::kChunkPurposeVolumeDir);
|
|
|
|
WMSG2(" DOS33 reading catalog sector T=%d S=%d\n", catTrack, catSect);
|
|
dierr = fpImg->ReadTrackSector(catTrack, catSect, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Watch for flaws that the DOS detector allows.
|
|
*/
|
|
if (catTrack == sctBuf[0x01] && catSect == sctBuf[0x02]) {
|
|
WMSG2(" DOS detected self-reference on cat (%d,%d)\n",
|
|
catTrack, catSect);
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Check the next track/sector in the chain. If the pointer is
|
|
* broken, there's a very good chance that this isn't really a
|
|
* catalog sector, so we want to bail out now.
|
|
*/
|
|
if (sctBuf[0x01] >= fpImg->GetNumTracks() ||
|
|
sctBuf[0x02] >= fpImg->GetNumSectPerTrack())
|
|
{
|
|
WMSG0(" DOS bailing out early on catalog read due to funky T/S\n");
|
|
break;
|
|
}
|
|
|
|
dierr = ProcessCatalogSector(catTrack, catSect, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
fCatalogSectors[iterations].track = catTrack;
|
|
fCatalogSectors[iterations].sector = catSect;
|
|
|
|
catTrack = sctBuf[0x01];
|
|
catSect = sctBuf[0x02];
|
|
|
|
iterations++; // watch for infinite loops
|
|
|
|
}
|
|
if (iterations >= kMaxCatalogSectors) {
|
|
dierr = kDIErrDirectoryLoop;
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Process the list of files in one sector of the catalog.
|
|
*
|
|
* Pass in the track, sector, and the contents of that track and sector.
|
|
* (We only use "catTrack" and "catSect" to fill out some fields.)
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::ProcessCatalogSector(int catTrack, int catSect,
|
|
const unsigned char* sctBuf)
|
|
{
|
|
A2FileDOS* pFile;
|
|
const unsigned char* pEntry;
|
|
int i;
|
|
|
|
pEntry = &sctBuf[kCatalogEntryOffset];
|
|
|
|
for (i = 0; i < kCatalogEntriesPerSect; i++) {
|
|
if (pEntry[0x00] != kEntryUnused && pEntry[0x00] != kEntryDeleted) {
|
|
pFile = new A2FileDOS(this);
|
|
|
|
pFile->SetQuality(A2File::kQualityGood);
|
|
|
|
pFile->fTSListTrack = pEntry[0x00];
|
|
pFile->fTSListSector = pEntry[0x01];
|
|
pFile->fLocked = (pEntry[0x02] & 0x80) != 0;
|
|
switch (pEntry[0x02] & 0x7f) {
|
|
case 0x00: pFile->fFileType = A2FileDOS::kTypeText; break;
|
|
case 0x01: pFile->fFileType = A2FileDOS::kTypeInteger; break;
|
|
case 0x02: pFile->fFileType = A2FileDOS::kTypeApplesoft; break;
|
|
case 0x04: pFile->fFileType = A2FileDOS::kTypeBinary; break;
|
|
case 0x08: pFile->fFileType = A2FileDOS::kTypeS; break;
|
|
case 0x10: pFile->fFileType = A2FileDOS::kTypeReloc; break;
|
|
case 0x20: pFile->fFileType = A2FileDOS::kTypeA; break;
|
|
case 0x40: pFile->fFileType = A2FileDOS::kTypeB; break;
|
|
default:
|
|
/* some odd arrangement of bit flags? */
|
|
WMSG1(" DOS33 peculiar filetype byte 0x%02x\n", pEntry[0x02]);
|
|
pFile->fFileType = A2FileDOS::kTypeUnknown;
|
|
pFile->SetQuality(A2File::kQualitySuspicious);
|
|
break;
|
|
}
|
|
|
|
memcpy(pFile->fFileName, &pEntry[0x03], A2FileDOS::kMaxFileName);
|
|
pFile->fFileName[A2FileDOS::kMaxFileName] = '\0';
|
|
pFile->FixFilename();
|
|
|
|
pFile->fLengthInSectors = pEntry[0x21];
|
|
pFile->fLengthInSectors |= (unsigned short) pEntry[0x22] << 8;
|
|
|
|
pFile->fCatTS.track = catTrack;
|
|
pFile->fCatTS.sector = catSect;
|
|
pFile->fCatEntryNum = i;
|
|
|
|
/* can't do these yet, so just set to defaults */
|
|
pFile->fLength = 0;
|
|
pFile->fSparseLength = 0;
|
|
pFile->fDataOffset = 0;
|
|
|
|
AddFileToList(pFile);
|
|
}
|
|
|
|
pEntry += kCatalogEntrySize;
|
|
}
|
|
|
|
return kDIErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Perform consistency checks on the filesystem.
|
|
*
|
|
* Returns "true" if disk appears to be perfect, "false" otherwise.
|
|
*/
|
|
bool
|
|
DiskFSDOS33::CheckDiskIsGood(void)
|
|
{
|
|
DIError dierr;
|
|
const DiskImg* pDiskImg = GetDiskImg();
|
|
bool result = true;
|
|
int i;
|
|
|
|
dierr = LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Make sure the VTOC is marked in use, or things could go badly.
|
|
* Ditto for the catalog tracks.
|
|
*/
|
|
if (!GetSectorUseEntry(kVTOCTrack, kVTOCSector)) {
|
|
fpImg->AddNote(DiskImg::kNoteWarning, "VTOC sector marked as free.");
|
|
result = false;
|
|
}
|
|
for (i = 0; i < kMaxCatalogSectors; i++) {
|
|
if (!GetSectorUseEntry(fCatalogSectors[i].track,
|
|
fCatalogSectors[i].sector))
|
|
{
|
|
fpImg->AddNote(DiskImg::kNoteWarning,
|
|
"Catalog sector %d,%d is marked as free.",
|
|
fCatalogSectors[i].track, fCatalogSectors[i].sector);
|
|
result = false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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 track, sector;
|
|
long notMarked, extraUsed, conflicts;
|
|
notMarked = extraUsed = conflicts = 0;
|
|
for (track = 0; track < pDiskImg->GetNumTracks(); track++) {
|
|
for (sector = 0; sector < pDiskImg->GetNumSectPerTrack(); sector++) {
|
|
dierr = fVolumeUsage.GetChunkState(track, sector, &cstate);
|
|
if (dierr != kDIErrNone) {
|
|
fpImg->AddNote(DiskImg::kNoteWarning,
|
|
"Internal volume usage error on t=%ld s=%ld.",
|
|
track, sector);
|
|
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 sector%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 sector%s used by files but not marked used.",
|
|
notMarked, notMarked == 1 ? " is" : "s are");
|
|
result = false;
|
|
}
|
|
if (conflicts > 0) {
|
|
fpImg->AddNote(DiskImg::kNoteWarning,
|
|
"%ld sector%s used by more than one file.",
|
|
conflicts, conflicts == 1 ? " is" : "s are");
|
|
result = false;
|
|
}
|
|
|
|
/*
|
|
* Scan for "damaged" files 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;
|
|
}
|
|
|
|
|
|
/*
|
|
* Run through our list of files, computing the lengths and marking file
|
|
* usage in the VolumeUsage object.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::GetFileLengths(void)
|
|
{
|
|
A2FileDOS* pFile;
|
|
TrackSector* tsList = nil;
|
|
TrackSector* indexList = nil;
|
|
int tsCount;
|
|
int indexCount;
|
|
|
|
pFile = (A2FileDOS*) GetNextFile(nil);
|
|
while (pFile != nil) {
|
|
DIError dierr;
|
|
dierr = pFile->LoadTSList(&tsList, &tsCount, &indexList, &indexCount);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG1("DOS failed loading TS list for '%s'\n",
|
|
pFile->GetPathName());
|
|
pFile->SetQuality(A2File::kQualityDamaged);
|
|
} else {
|
|
MarkFileUsage(pFile, tsList, tsCount, indexList, indexCount);
|
|
dierr = ComputeLength(pFile, tsList, tsCount);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG1("DOS unable to get length for '%s'\n",
|
|
pFile->GetPathName());
|
|
pFile->SetQuality(A2File::kQualityDamaged);
|
|
}
|
|
}
|
|
|
|
if (pFile->fLengthInSectors != indexCount + tsCount) {
|
|
WMSG3("DOS NOTE: file '%s' has len-in-sect=%d but actual=%d\n",
|
|
pFile->GetPathName(), pFile->fLengthInSectors,
|
|
indexCount + tsCount);
|
|
// expected on sparse random-access text files
|
|
}
|
|
|
|
delete[] tsList;
|
|
delete[] indexList;
|
|
tsList = indexList = nil;
|
|
|
|
pFile = (A2FileDOS*) GetNextFile(pFile);
|
|
}
|
|
|
|
return kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* Compute the length and starting data offset of the file.
|
|
*
|
|
* For Text, there are two situations: sequential and random. For
|
|
* sequential text files, we just need to find the first 00 byte. For
|
|
* random, there can be 00s everywhere, and in fact there can be holes
|
|
* in the T/S list. The plan: since DOS doesn't let you "truncate" a
|
|
* text file, just scan the last sector for 00. The length is the
|
|
* number of previous T/S entries * 256 plus the sector offset.
|
|
* --> This does the wrong thing for random-access text files, which
|
|
* need to retain their full length, and doesn't work right for sequential
|
|
* text files that (somehow) had their last block over-allocated. It does
|
|
* the right thing most of the time, but we either need to be more clever
|
|
* here or provide a way to override the default (bool fTrimTextFiles?).
|
|
*
|
|
* For Applesoft and Integer, the file length is stored as the first two
|
|
* bytes of the file.
|
|
*
|
|
* For Binary, the file length is stored in the second two bytes (after
|
|
* the two-byte address). Some files (with low-memory loaders) used a
|
|
* fake length, and DDD 2.x sets both address and length to zero.
|
|
*
|
|
* For Reloc, S, A2, B2, and "unknown", we just multiply the sector count.
|
|
* We get an accurate sector count from the T/S list (the value in the
|
|
* directory entry might have been tampered with).
|
|
*
|
|
* To handle DDD 2.x files correctly, we need to identify them as such by
|
|
* looking for 'B' with address=0 and length=0, a T/S count of at least 8
|
|
* (the smallest possible compression of a 35-track disk is 2385 bytes),
|
|
* and a '<' in the filename. If found, we start from offset=0
|
|
* (because DDD Pro 1.x includes the 4 leading bytes) and include all
|
|
* sectors, we'll get the actual file plus at most 256 garbage bytes.
|
|
*
|
|
* On success, we set the following:
|
|
* pFile->fLength
|
|
* pFile->fSparseLength
|
|
* pFile->fDataOffset
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::ComputeLength(A2FileDOS* pFile, const TrackSector* tsList,
|
|
int tsCount)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
unsigned char sctBuf[kSctSize];
|
|
|
|
assert(pFile != nil);
|
|
assert(tsList != nil);
|
|
assert(tsCount >= 0);
|
|
|
|
pFile->fDataOffset = 0;
|
|
|
|
pFile->fAuxType = 0;
|
|
if (pFile->fFileType == A2FileDOS::kTypeApplesoft)
|
|
pFile->fAuxType = 0x0801;
|
|
/* for text files it's default record length; assume zero */
|
|
|
|
if (tsCount == 0) {
|
|
/* no data at all */
|
|
pFile->fLength = 0;
|
|
} else if (pFile->fFileType == A2FileDOS::kTypeApplesoft ||
|
|
pFile->fFileType == A2FileDOS::kTypeInteger ||
|
|
pFile->fFileType == A2FileDOS::kTypeBinary)
|
|
{
|
|
/* read first sector and analyze it */
|
|
//WMSG0(" DOS reading first file sector\n");
|
|
dierr = fpImg->ReadTrackSector(tsList[0].track, tsList[0].sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
if (pFile->fFileType == A2FileDOS::kTypeBinary) {
|
|
pFile->fAuxType =
|
|
sctBuf[0x00] | (unsigned short) sctBuf[0x01] << 8;
|
|
pFile->fLength =
|
|
sctBuf[0x02] | (unsigned short) sctBuf[0x03] << 8;
|
|
pFile->fDataOffset = 4; // take the above into account
|
|
} else {
|
|
pFile->fLength =
|
|
sctBuf[0x00] | (unsigned short) sctBuf[0x01] << 8;
|
|
pFile->fDataOffset = 2; // take the above into account
|
|
}
|
|
|
|
if (pFile->fFileType == A2FileDOS::kTypeBinary &&
|
|
pFile->fLength == 0 && pFile->fAuxType == 0 &&
|
|
tsCount >= 8 &&
|
|
strchr(pFile->fFileName, '<') != nil &&
|
|
strchr(pFile->fFileName, '>') != nil)
|
|
{
|
|
WMSG2(" DOS found probable DDD archive, tweaking '%s' (lis=%u)\n",
|
|
pFile->GetPathName(), pFile->fLengthInSectors);
|
|
//dierr = TrimLastSectorDown(pFile, tsBuf, WrapperDDD::kMaxDDDZeroCount);
|
|
//if (dierr != kDIErrNone)
|
|
// goto bail;
|
|
//WMSG3(" DOS scanned DDD file '%s' to length %ld (tsCount=%d)\n",
|
|
// pFile->fFileName, pFile->fLength, pFile->fTSListCount);
|
|
pFile->fLength = tsCount * kSctSize;
|
|
pFile->fDataOffset = 0;
|
|
}
|
|
|
|
/* catch bogus lengths in damaged A/I/B files */
|
|
if (pFile->fLength > tsCount * kSctSize) {
|
|
WMSG3(" DOS33 capping max len from %ld to %d in '%s'\n",
|
|
(long) pFile->fLength, tsCount * kSctSize,
|
|
pFile->fFileName);
|
|
pFile->fLength = tsCount * kSctSize - pFile->fDataOffset;
|
|
if (pFile->fLength < 0) // can't happen here?
|
|
pFile->fLength = 0;
|
|
|
|
/*
|
|
* This could cause a problem, because if the user changes a 'T'
|
|
* file to 'B', the bogus file length will mark the file as
|
|
* "suspicious" and we won't allow writing to the disk (which
|
|
* makes it hard to switch the file type back). We really don't
|
|
* want to weaken this test though.
|
|
*/
|
|
pFile->SetQuality(A2File::kQualitySuspicious);
|
|
}
|
|
|
|
} else if (pFile->fFileType == A2FileDOS::kTypeText) {
|
|
/* scan text file */
|
|
pFile->fLength = tsCount * kSctSize;
|
|
dierr = TrimLastSectorUp(pFile, tsList[tsCount-1]);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
WMSG4(" DOS scanned text file '%s' down to %d+%ld = %ld\n",
|
|
pFile->fFileName,
|
|
(tsCount-1) * kSctSize,
|
|
(long)pFile->fLength - (tsCount-1) * kSctSize,
|
|
(long)pFile->fLength);
|
|
|
|
/* TO DO: something clever to discern random access record length? */
|
|
} else {
|
|
/* S/R/A/B: just use the TS count */
|
|
pFile->fLength = tsCount * kSctSize;
|
|
}
|
|
|
|
/*
|
|
* Compute the sparse length for random-access text files.
|
|
*/
|
|
int i, sparseCount;
|
|
sparseCount = 0;
|
|
for (i = 0; i < tsCount; i++) {
|
|
if (tsList[i].track == 0 && tsList[i].sector == 0)
|
|
sparseCount++;
|
|
}
|
|
pFile->fSparseLength = pFile->fLength - sparseCount * kSctSize;
|
|
if (pFile->fSparseLength == -pFile->fDataOffset) {
|
|
/*
|
|
* This can happen for a completely sparse file. Looks sort of
|
|
* stupid to have a length of "-4", so force it to zero.
|
|
*/
|
|
pFile->fSparseLength = 0;
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Trim the zeroes off the end of the last sector. We begin at the start
|
|
* of the sector and stop at the first zero found.
|
|
*
|
|
* Modifies pFile->fLength, which should be set to a roughly accurate
|
|
* value on entry.
|
|
*
|
|
* The caller should endeavor to strip out T=0 S=0 entries that come after
|
|
* the body of the file. They're valid in the middle for random-access
|
|
* text files.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::TrimLastSectorUp(A2FileDOS* pFile, TrackSector lastTS)
|
|
{
|
|
DIError dierr;
|
|
unsigned char sctBuf[kSctSize];
|
|
int i;
|
|
|
|
if (lastTS.track == 0) {
|
|
/* happens on files with lots of "sparse" space at the end */
|
|
return kDIErrNone;
|
|
}
|
|
|
|
//WMSG0(" DOS reading LAST file sector\n");
|
|
dierr = fpImg->ReadTrackSector(lastTS.track, lastTS.sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/* start with EOF equal to previous sectors */
|
|
pFile->fLength -= kSctSize;
|
|
for (i = 0; i < kSctSize; i++) {
|
|
if (sctBuf[i] == 0x00)
|
|
break;
|
|
else
|
|
pFile->fLength++;
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Given lists of tracks and sector for data and TS index sectors, set the
|
|
* entries in the volume usage map.
|
|
*/
|
|
void
|
|
DiskFSDOS33::MarkFileUsage(A2FileDOS* pFile, TrackSector* tsList, int tsCount,
|
|
TrackSector* indexList, int indexCount)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < tsCount; i++) {
|
|
/* mark all sectors as in-use by file */
|
|
if (tsList[i].track == 0 && tsList[i].sector == 0) {
|
|
/* sparse sector in random-access text file */
|
|
} else {
|
|
SetSectorUsage(tsList[i].track, tsList[i].sector,
|
|
VolumeUsage::kChunkPurposeUserData);
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < indexCount; i++) {
|
|
/* mark the T/S sectors as in-use by file structures */
|
|
SetSectorUsage(indexList[i].track, indexList[i].sector,
|
|
VolumeUsage::kChunkPurposeFileStruct);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#if 0
|
|
/*
|
|
* Trim the zeroes off the end of the last sector. We begin at the end
|
|
* of the sector and back up.
|
|
*
|
|
* It is possible (one out of between 128 and 256 times) that we have just
|
|
* the trailing zero in this sector, and we need to back up to the previous
|
|
* sector to find the actual end. We know a file can end with three zeroes
|
|
* and we suspect it might be possible to end with four, which means we could
|
|
* have between 0 and 3 zeroes in the previous sector, and between 1 and 4
|
|
* in this sector. If we just tack on three more zeroes, we weaken our
|
|
* length test slightly, because we must allow a "slop" of up to seven bytes.
|
|
* It's a little more work, but scanning the next-to-last sector is probably
|
|
* worthwhile given the otherwise flaky nature of DDD storage.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::TrimLastSectorDown(A2FileDOS* pFile, unsigned short* tsBuf,
|
|
int maxZeroCount)
|
|
{
|
|
DIError dierr;
|
|
unsigned char sctBuf[kSctSize];
|
|
int i;
|
|
|
|
//WMSG0(" DOS reading LAST file sector\n");
|
|
dierr = fpImg->ReadTrackSector(
|
|
pFile->TSTrack(tsBuf[pFile->fTSListCount-1]),
|
|
pFile->TSSector(tsBuf[pFile->fTSListCount-1]),
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/* find the first trailing zero by finding the last non-zero */
|
|
for (i = kSctSize-1; i >= 0; i--) {
|
|
if (sctBuf[i] != 0x00)
|
|
break;
|
|
}
|
|
if (i < 0) {
|
|
/* sector was nothing but zeroes */
|
|
DebugBreak();
|
|
} else {
|
|
/* peg it at 256; if it went over that, DDD would've added a sector */
|
|
i += maxZeroCount;
|
|
if (i > kSctSize)
|
|
i = kSctSize;
|
|
pFile->fLength = (pFile->fTSListCount-1) * kSctSize + i;
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
* Convert high ASCII to low ASCII.
|
|
*
|
|
* Some people put inverse and flashing text into filenames, not to mention
|
|
* control characters, so we have to cope with those too.
|
|
*
|
|
* We modify the first "len" bytes of "buf" in place.
|
|
*/
|
|
/*static*/ void
|
|
DiskFSDOS33::LowerASCII(unsigned char* buf, long len)
|
|
{
|
|
while (len--) {
|
|
if (*buf & 0x80) {
|
|
if (*buf >= 0xa0)
|
|
*buf &= 0x7f;
|
|
else
|
|
*buf = (*buf & 0x7f) + 0x20;
|
|
} else
|
|
*buf = ((*buf & 0x3f) ^ 0x20) + 0x20;
|
|
|
|
buf++;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Determine whether or not "name" is a valid DOS 3.3 filename.
|
|
*
|
|
* Names can be up to 30 characters and can contain absolutely anything.
|
|
* To make life easier on DOS users, we ban the use of the comma, block
|
|
* control characters and high ASCII, and don't allow completely blank
|
|
* names. Later on we will later convert to upper case, so we allow lower
|
|
* case letters here.
|
|
*
|
|
* Filenames simply pad out to 30 characters with spaces, so the only
|
|
* "invalid" character is a trailing space. Because we're using C-style
|
|
* strings, we implicitly ban the use of '\0' in the name.
|
|
*/
|
|
/*static*/ bool
|
|
DiskFSDOS33::IsValidFileName(const char* name)
|
|
{
|
|
bool nonSpace = false;
|
|
int len = 0;
|
|
|
|
/* count letters, skipping control chars */
|
|
while (*name != '\0') {
|
|
char ch = *name++;
|
|
|
|
if (ch < 0x20 || ch >= 0x7f || ch == ',')
|
|
return false;
|
|
if (ch != 0x20)
|
|
nonSpace = true;
|
|
len++;
|
|
}
|
|
if (len == 0 || len > A2FileDOS::kMaxFileName)
|
|
return false; // can't be empty, can't be huge
|
|
if (!nonSpace)
|
|
return false; // must have one non-ctrl non-space char
|
|
if (*(name-1) == ' ')
|
|
return false; // no trailing spaces
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Determine whether "name" is a valid volume number.
|
|
*/
|
|
/*static*/ bool
|
|
DiskFSDOS33::IsValidVolumeName(const char* name)
|
|
{
|
|
long val;
|
|
char* endp;
|
|
|
|
val = strtol(name, &endp, 10);
|
|
if (*endp != '\0' || val < 1 || val > 254)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Put a DOS 3.2/3.3 filesystem image on the specified DiskImg.
|
|
*
|
|
* If "volName" is "DOS", a basic DOS image will be written to the first three
|
|
* tracks of the disk, and the in-use map will be updated appropriately.
|
|
*
|
|
* It would seem at first glance that putting the volume number into the
|
|
* volume name string would make the interface more consistent with the
|
|
* rest of the filesystems. The first glance is substantially correct, but
|
|
* the DOS stuff has a separate "set volume number" interface already, used
|
|
* to deal with the various locations where volume numbers can be stored
|
|
* (2MG header, VTOC, sector address headers) in the various formats.
|
|
*
|
|
* So, instead of stuffing the volume number into "volName" and creating
|
|
* some other path for specifying "add DOS image", I continue to use the
|
|
* defined ways of setting the volume number and abuse "volName" slightly.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::Format(DiskImg* pDiskImg, const char* volName)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
unsigned char sctBuf[256];
|
|
bool addDOS = false;
|
|
|
|
if (pDiskImg->GetNumTracks() < kMinTracks ||
|
|
pDiskImg->GetNumTracks() > kMaxTracks)
|
|
{
|
|
WMSG1(" DOS33 can't format numTracks=%ld\n", pDiskImg->GetNumTracks());
|
|
return kDIErrInvalidArg;
|
|
}
|
|
if (pDiskImg->GetNumSectPerTrack() != 13 &&
|
|
pDiskImg->GetNumSectPerTrack() != 16 &&
|
|
pDiskImg->GetNumSectPerTrack() != 32)
|
|
{
|
|
WMSG1(" DOS33 can't format sectors=%d\n",
|
|
pDiskImg->GetNumSectPerTrack());
|
|
return kDIErrInvalidArg;
|
|
}
|
|
|
|
if (volName != nil && strcmp(volName, "DOS") == 0) {
|
|
if (pDiskImg->GetNumSectPerTrack() != 16 &&
|
|
pDiskImg->GetNumSectPerTrack() != 13)
|
|
{
|
|
WMSG1("NOTE: numSectPerTrack = %d, can't write DOS tracks\n",
|
|
pDiskImg->GetNumSectPerTrack());
|
|
return kDIErrInvalidArg;
|
|
}
|
|
addDOS = true;
|
|
}
|
|
|
|
/* set fpImg so calls that rely on it will work; we un-set it later */
|
|
assert(fpImg == nil);
|
|
SetDiskImg(pDiskImg);
|
|
|
|
WMSG1(" DOS33 formatting disk image (sectorOrder=%d)\n",
|
|
fpImg->GetSectorOrder());
|
|
|
|
/* write DOS sectors */
|
|
dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(),
|
|
DiskImg::kFormatGenericDOSOrd, fpImg->GetSectorOrder());
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* 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();
|
|
WMSG0(" DOS33 (not zeroing blocks)\n");
|
|
|
|
if (addDOS) {
|
|
dierr = WriteDOSTracks(pDiskImg->GetNumSectPerTrack());
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Set up the static fields in the VTOC.
|
|
*/
|
|
dierr = LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
fVTOC[0x00] = 0x04; // (no reason)
|
|
fVTOC[0x01] = kVTOCTrack; // first cat track
|
|
fVTOC[0x02] = fpImg->GetNumSectPerTrack()-1; // first cat sector
|
|
fVTOC[0x03] = 3; // version
|
|
if (fpImg->GetDOSVolumeNum() == DiskImg::kVolumeNumNotSet)
|
|
fVTOC[0x06] = kDefaultVolumeNum; // VTOC volume number
|
|
else
|
|
fVTOC[0x06] = (unsigned char) fpImg->GetDOSVolumeNum();
|
|
fVTOC[0x27] = 122; // max T/S pairs
|
|
fVTOC[0x30] = kVTOCTrack+1; // last alloc
|
|
fVTOC[0x31] = 1; // ascending
|
|
fVTOC[0x34] = (unsigned char)fpImg->GetNumTracks(); // #of tracks
|
|
fVTOC[0x35] = fpImg->GetNumSectPerTrack(); // #of sectors
|
|
fVTOC[0x36] = 0x00; // bytes/sector (lo)
|
|
fVTOC[0x37] = 0x01; // bytes/sector (hi)
|
|
if (pDiskImg->GetNumSectPerTrack() == 13) {
|
|
// minor changes for DOS 3.2
|
|
fVTOC[0x00] = 0x02;
|
|
fVTOC[0x03] = 2;
|
|
}
|
|
|
|
dierr = SaveVolBitmap();
|
|
FreeVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Fill the sectors in the catalog track.
|
|
*/
|
|
int sect;
|
|
memset(sctBuf, 0, sizeof(sctBuf));
|
|
sctBuf[0x01] = kVTOCTrack;
|
|
for (sect = fpImg->GetNumSectPerTrack()-1; sect > 1; sect--) {
|
|
sctBuf[0x02] = sect-1;
|
|
|
|
dierr = fpImg->WriteTrackSector(kVTOCTrack, sect, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Generate the initial block usage map. The only entries in use are
|
|
* right at the start of the disk.
|
|
*/
|
|
CreateEmptyBlockMap(addDOS);
|
|
|
|
/* check our work, and set some object fields, by reading what we wrote */
|
|
dierr = ReadVTOC();
|
|
if (dierr != kDIErrNone) {
|
|
WMSG1(" GLITCH: couldn't read header we just wrote (err=%d)\n", dierr);
|
|
goto bail;
|
|
}
|
|
|
|
/* don't do this -- assume they're going to call Initialize() later */
|
|
//ScanVolBitmap();
|
|
|
|
bail:
|
|
SetDiskImg(nil); // shouldn't really be set by us
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Write a DOS image into tracks 0-2.
|
|
*
|
|
* This takes the number of sectors per track as an argument so we can figure
|
|
* out which version of DOS to write. This probably ought to be an enum so
|
|
* we can specify various versions of DOS.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::WriteDOSTracks(int sectPerTrack)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
long track, sector;
|
|
const unsigned char* buf = gDOS33Tracks;
|
|
|
|
if (sectPerTrack == 13) {
|
|
WMSG0(" DOS33 writing DOS 3.3 tracks\n");
|
|
buf = gDOS32Tracks;
|
|
|
|
for (track = 0; track < 3; track++) {
|
|
for (sector = 0; sector < 13; sector++) {
|
|
dierr = fpImg->WriteTrackSector(track, sector, buf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
buf += kSctSize;
|
|
}
|
|
}
|
|
} else if (sectPerTrack == 16) {
|
|
WMSG0(" DOS33 writing DOS 3.3 tracks\n");
|
|
buf = gDOS33Tracks;
|
|
|
|
// this should be used for 32-sector disks
|
|
|
|
for (track = 0; track < 3; track++) {
|
|
for (sector = 0; sector < 16; sector++) {
|
|
dierr = fpImg->WriteTrackSector(track, sector, buf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
buf += kSctSize;
|
|
}
|
|
}
|
|
} else {
|
|
WMSG1(" DOS33 *not* writing DOS tracks to %d-sector disk\n",
|
|
sectPerTrack);
|
|
assert(false);
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Normalize a DOS 3.3 path. Used when adding files from DiskArchive.
|
|
* The path may contain subdirectory components, which we need to strip away.
|
|
*
|
|
* "*pNormalizedBufLen" is used to pass in the length of the buffer and
|
|
* pass out the length of the string (should the buffer prove inadequate).
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::NormalizePath(const char* path, char fssep,
|
|
char* normalizedBuf, int* pNormalizedBufLen)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
char tmpBuf[A2FileDOS::kMaxFileName+1];
|
|
int len;
|
|
|
|
DoNormalizePath(path, fssep, tmpBuf);
|
|
len = strlen(tmpBuf)+1;
|
|
|
|
if (*pNormalizedBufLen < len)
|
|
dierr = kDIErrDataOverrun;
|
|
else
|
|
strcpy(normalizedBuf, tmpBuf);
|
|
*pNormalizedBufLen = len;
|
|
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Normalize a DOS 3.3 pathname. Lower case becomes upper case, control
|
|
* characters and high ASCII get stripped, and ',' becomes '_'.
|
|
*
|
|
* "outBuf" must be able to hold kMaxFileName+1 characters.
|
|
*/
|
|
void
|
|
DiskFSDOS33::DoNormalizePath(const char* name, char fssep, char* outBuf)
|
|
{
|
|
char* outp = outBuf;
|
|
char* cp;
|
|
|
|
/* throw out leading pathname, if any */
|
|
if (fssep != '\0') {
|
|
cp = strrchr(name, fssep);
|
|
if (cp != nil)
|
|
name = cp+1;
|
|
}
|
|
|
|
while (*name != '\0' && (outp - outBuf) <= A2FileDOS::kMaxFileName) {
|
|
if (*name >= 0x20 && *name < 0x7f) {
|
|
if (*name == ',')
|
|
*outp = '_';
|
|
else
|
|
*outp = toupper(*name);
|
|
|
|
outp++;
|
|
}
|
|
name++;
|
|
}
|
|
*outp = '\0';
|
|
|
|
if (*outBuf == '\0') {
|
|
/* nothing left */
|
|
strcpy(outBuf, "BLANK");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Create a file on a DOS 3.2/3.3 disk.
|
|
*
|
|
* The file will be created with an empty T/S list.
|
|
*
|
|
* It is not possible to set the aux type here. Aux types only apply to 'B'
|
|
* files, and since they're stored in the first data sector (which we don't
|
|
* create), there's nowhere to put it. We stuff it into the aux type value
|
|
* in the linear file list, on the assumption that somebody will come along
|
|
* and politely Write to the file, even if it's zero bytes long.
|
|
*
|
|
* (Technically speaking, setting the file type here is bogus, because a
|
|
* 'B' file with no data sectors is invalid. However, we don't want to
|
|
* handle arbitrary changes later -- switching from 'T' to 'B' requires
|
|
* either rewriting the entire file, or confusing the user by changing the
|
|
* type without adjusting the first 4 bytes -- so we set it now. It's also
|
|
* helpful to set it now because the Write routine needs to know how many
|
|
* bytes offset from the start of the file it needs to be. We could avoid
|
|
* most of this weirdness by just going ahead and allocating the first
|
|
* sector of the file now, and modifying the Write() function to understand
|
|
* that the first block is already there. Need to do that someday.)
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::CreateFile(const CreateParms* pParms, A2File** ppNewFile)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
const bool createUnique = (GetParameter(kParm_CreateUnique) != 0);
|
|
char normalName[A2FileDOS::kMaxFileName+1];
|
|
// char storageName[A2FileDOS::kMaxFileName+1];
|
|
A2FileDOS::FileType fileType;
|
|
A2FileDOS* pNewFile = nil;
|
|
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (!fDiskIsGood)
|
|
return kDIErrBadDiskImage;
|
|
|
|
assert(pParms != nil);
|
|
assert(pParms->pathName != nil);
|
|
assert(pParms->storageType == A2FileProDOS::kStorageSeedling);
|
|
WMSG1(" DOS33 ---v--- CreateFile '%s'\n", pParms->pathName);
|
|
|
|
*ppNewFile = nil;
|
|
|
|
DoNormalizePath(pParms->pathName, pParms->fssep, normalName);
|
|
|
|
/*
|
|
* See if the file already exists.
|
|
*
|
|
* If "create unique" is set, we append digits until the name doesn't
|
|
* match any others. The name will be modified in place.
|
|
*/
|
|
if (createUnique) {
|
|
MakeFileNameUnique(normalName);
|
|
} else {
|
|
if (GetFileByName(normalName) != nil) {
|
|
WMSG1(" DOS33 create: normalized name '%s' already exists\n",
|
|
normalName);
|
|
dierr = kDIErrFileExists;
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
fileType = A2FileDOS::ConvertFileType(pParms->fileType, 0);
|
|
|
|
/*
|
|
* Allocate a directory entry and T/S list.
|
|
*/
|
|
unsigned char sctBuf[kSctSize];
|
|
TrackSector catSect;
|
|
TrackSector tsSect;
|
|
int catEntry;
|
|
A2FileDOS* pPrevEntry;
|
|
|
|
dierr = LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/* allocate a sector for the T/S list, and zero it out */
|
|
dierr = AllocSector(&tsSect);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
memset(sctBuf, 0, kSctSize);
|
|
dierr = fpImg->WriteTrackSector(tsSect.track, tsSect.sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Find the first free catalog entry. Also returns a pointer to the
|
|
* previous entry.
|
|
*/
|
|
dierr = GetFreeCatalogEntry(&catSect, &catEntry, sctBuf, &pPrevEntry);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG0("DOS unable to find an empty entry in the catalog\n");
|
|
goto bail;
|
|
}
|
|
WMSG4(" DOS found free catalog entry T=%d S=%d ent=%d prev=0x%08lx\n",
|
|
catSect.track, catSect.sector, catEntry, (long) pPrevEntry);
|
|
|
|
/* create the new dir entry at the specified location */
|
|
CreateDirEntry(sctBuf, catEntry, normalName, &tsSect,
|
|
(unsigned char) fileType, pParms->access);
|
|
|
|
/*
|
|
* Flush everything to disk.
|
|
*/
|
|
dierr = fpImg->WriteTrackSector(catSect.track, catSect.sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
dierr = SaveVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Create a new entry for our file list.
|
|
*/
|
|
pNewFile = new A2FileDOS(this);
|
|
if (pNewFile == nil) {
|
|
dierr = kDIErrMalloc;
|
|
goto bail;
|
|
}
|
|
pNewFile->fTSListTrack = tsSect.track;
|
|
pNewFile->fTSListSector = tsSect.sector;
|
|
pNewFile->fLengthInSectors = 1;
|
|
pNewFile->fLocked = false;
|
|
strcpy(pNewFile->fFileName, normalName);
|
|
pNewFile->fFileType = fileType;
|
|
|
|
pNewFile->fCatTS.track = catSect.track;
|
|
pNewFile->fCatTS.sector = catSect.sector;
|
|
pNewFile->fCatEntryNum = catEntry;
|
|
|
|
pNewFile->fAuxType = (unsigned short) pParms->auxType;
|
|
pNewFile->fDataOffset = 0;
|
|
switch (pNewFile->fFileType) {
|
|
case A2FileDOS::kTypeInteger:
|
|
pNewFile->fDataOffset = 2;
|
|
break;
|
|
case A2FileDOS::kTypeApplesoft:
|
|
pNewFile->fDataOffset = 2;
|
|
pNewFile->fAuxType = 0x0801;
|
|
break;
|
|
case A2FileDOS::kTypeBinary:
|
|
pNewFile->fDataOffset = 4;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
pNewFile->fLength = 0;
|
|
pNewFile->fSparseLength = 0;
|
|
|
|
/*
|
|
* Insert it in the proper place, so that the order of the files matches
|
|
* the order of entries in the catalog.
|
|
*/
|
|
InsertFileInList(pNewFile, pPrevEntry);
|
|
|
|
*ppNewFile = pNewFile;
|
|
pNewFile = nil;
|
|
|
|
bail:
|
|
delete pNewFile;
|
|
FreeVolBitmap();
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Make the name pointed to by "fileName" unique. The name should already
|
|
* be FS-normalized, and be in a buffer that can hold at least kMaxFileName+1
|
|
* bytes.
|
|
*
|
|
* (This is nearly identical to the code in the ProDOS implementation. I'd
|
|
* like to make it a general DiskFS function, but making the loop condition
|
|
* work requires setting up callbacks, which isn't hard here but is a little
|
|
* annoying in ProDOS because of the subdir buffer. So it's cut & paste
|
|
* for now.)
|
|
*
|
|
* Returns an error on failure, which should be impossible.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::MakeFileNameUnique(char* fileName)
|
|
{
|
|
assert(fileName != nil);
|
|
assert(strlen(fileName) <= A2FileDOS::kMaxFileName);
|
|
|
|
if (GetFileByName(fileName) == nil)
|
|
return kDIErrNone;
|
|
|
|
WMSG1(" DOS found duplicate of '%s', making unique\n", 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, (A2FileDOS::kMaxFileName - nameLen) +1);
|
|
|
|
/*
|
|
* If this has what looks like a filename extension, grab it. We want
|
|
* to preserve ".gif", ".c", etc.
|
|
*/
|
|
const char* cp = strrchr(fileName, '.');
|
|
if (cp != nil) {
|
|
int tmpOffset = cp - fileName;
|
|
if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) {
|
|
WMSG1(" DOS (keeping extension '%s')\n", 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 > A2FileDOS::kMaxFileName)
|
|
copyOffset = A2FileDOS::kMaxFileName - dotLen - digitLen;
|
|
else
|
|
copyOffset = nameLen - dotLen;
|
|
memcpy(fileName + copyOffset, digitBuf, digitLen);
|
|
if (dotLen != 0)
|
|
memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen);
|
|
} while (GetFileByName(fileName) != nil);
|
|
|
|
WMSG1(" DOS converted to unique name: %s\n", fileName);
|
|
|
|
return kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* Find the first free entry in the catalog.
|
|
*
|
|
* Also returns an A2File pointer for the previous entry in the catalog.
|
|
*
|
|
* The contents of the catalog sector will be in "sctBuf".
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::GetFreeCatalogEntry(TrackSector* pCatSect, int* pCatEntry,
|
|
unsigned char* sctBuf, A2FileDOS** ppPrevEntry)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
unsigned char* pEntry;
|
|
int sct, ent;
|
|
bool found = false;
|
|
|
|
for (sct = 0; sct < kMaxCatalogSectors; sct++) {
|
|
if (fCatalogSectors[sct].track == 0 &&
|
|
fCatalogSectors[sct].sector == 0)
|
|
{
|
|
/* end of list reached */
|
|
WMSG0("DOS catalog is full\n");
|
|
dierr = kDIErrVolumeDirFull;
|
|
goto bail;
|
|
}
|
|
dierr = fpImg->ReadTrackSector(fCatalogSectors[sct].track,
|
|
fCatalogSectors[sct].sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
pEntry = &sctBuf[kCatalogEntryOffset];
|
|
for (ent = 0; ent < kCatalogEntriesPerSect; ent++) {
|
|
if (pEntry[0x00] == 0x00 || pEntry[0x00] == kEntryDeleted) {
|
|
/* winner! */
|
|
*pCatSect = fCatalogSectors[sct];
|
|
*pCatEntry = ent;
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
pEntry += kCatalogEntrySize;
|
|
}
|
|
|
|
if (found)
|
|
break;
|
|
}
|
|
|
|
if (sct == kMaxCatalogSectors) {
|
|
/* didn't find anything, assume the disk is full */
|
|
dierr = kDIErrVolumeDirFull;
|
|
// fall through to "bail"
|
|
} else {
|
|
/* figure out what the previous entry is */
|
|
TrackSector prevTS;
|
|
int prevEntry;
|
|
|
|
if (*pCatEntry != 0) {
|
|
prevTS = *pCatSect;
|
|
prevEntry = *pCatEntry -1;
|
|
} else if (sct != 0) {
|
|
prevTS = fCatalogSectors[sct-1];
|
|
prevEntry = kCatalogEntriesPerSect-1;
|
|
} else {
|
|
/* disk was empty; there's no previous entry */
|
|
prevTS.track = 0;
|
|
prevTS.sector = 0;
|
|
prevEntry = -1;
|
|
}
|
|
|
|
/* now find it in the linear file list */
|
|
*ppPrevEntry = nil;
|
|
if (prevEntry >= 0) {
|
|
A2FileDOS* pFile = (A2FileDOS*) GetNextFile(nil);
|
|
while (pFile != nil) {
|
|
if (pFile->fCatTS.track == prevTS.track &&
|
|
pFile->fCatTS.sector == prevTS.sector &&
|
|
pFile->fCatEntryNum == prevEntry)
|
|
{
|
|
*ppPrevEntry = pFile;
|
|
break;
|
|
}
|
|
pFile = (A2FileDOS*) GetNextFile(pFile);
|
|
}
|
|
assert(*ppPrevEntry != nil);
|
|
}
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Fill out the catalog entry in the location specified.
|
|
*/
|
|
void
|
|
DiskFSDOS33::CreateDirEntry(unsigned char* sctBuf, int catEntry,
|
|
const char* fileName, TrackSector* pTSSect, unsigned char fileType,
|
|
int access)
|
|
{
|
|
char highName[A2FileDOS::kMaxFileName+1];
|
|
unsigned char* pEntry;
|
|
|
|
pEntry = GetCatalogEntryPtr(sctBuf, catEntry);
|
|
if (pEntry[0x00] != 0x00 && pEntry[0x00] != kEntryDeleted) {
|
|
/* somebody screwed up */
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
A2FileDOS::MakeDOSName(highName, fileName);
|
|
|
|
pEntry[0x00] = pTSSect->track;
|
|
pEntry[0x01] = pTSSect->sector;
|
|
pEntry[0x02] = fileType;
|
|
if ((access & A2FileProDOS::kAccessWrite) == 0)
|
|
pEntry[0x02] |= (unsigned char) A2FileDOS::kTypeLocked;
|
|
memcpy(&pEntry[0x03], highName, A2FileDOS::kMaxFileName);
|
|
PutShortLE(&pEntry[0x21], 1); // assume file is 1 sector long
|
|
}
|
|
|
|
/*
|
|
* Delete a file.
|
|
*
|
|
* This entails freeing up the allocated sectors and changing a byte in
|
|
* the directory entry. We then remove it from the DiskFS file list.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::DeleteFile(A2File* pGenericFile)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
A2FileDOS* pFile = (A2FileDOS*) pGenericFile;
|
|
TrackSector* tsList = nil;
|
|
TrackSector* indexList = nil;
|
|
int tsCount, indexCount;
|
|
unsigned char sctBuf[kSctSize];
|
|
unsigned char* pEntry;
|
|
|
|
if (pGenericFile == nil) {
|
|
assert(false);
|
|
return kDIErrInvalidArg;
|
|
}
|
|
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (!fDiskIsGood)
|
|
return kDIErrBadDiskImage;
|
|
if (pGenericFile->IsFileOpen())
|
|
return kDIErrFileOpen;
|
|
|
|
WMSG1(" Deleting '%s'\n", pFile->GetPathName());
|
|
|
|
/*
|
|
* Update the block usage map. Nothing is permanent until we flush
|
|
* the data to disk.
|
|
*/
|
|
dierr = LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
dierr = pFile->LoadTSList(&tsList, &tsCount, &indexList, &indexCount);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG1("Failed loading TS lists while deleting '%s'\n",
|
|
pFile->GetPathName());
|
|
goto bail;
|
|
}
|
|
|
|
FreeTrackSectors(tsList, tsCount);
|
|
FreeTrackSectors(indexList, indexCount);
|
|
|
|
/*
|
|
* Mark the entry as deleted.
|
|
*/
|
|
dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);
|
|
assert(pEntry[0x00] != 0x00 && pEntry[0x00] != kEntryDeleted);
|
|
pEntry[0x00] = kEntryDeleted;
|
|
dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Save our updated copy of the volume bitmap to disk.
|
|
*/
|
|
dierr = SaveVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Remove the A2File* from the list.
|
|
*/
|
|
DeleteFileFromList(pFile);
|
|
|
|
bail:
|
|
FreeVolBitmap();
|
|
delete[] tsList;
|
|
delete[] indexList;
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Mark all of the track/sector entries in "pList" as free.
|
|
*/
|
|
void
|
|
DiskFSDOS33::FreeTrackSectors(TrackSector* pList, int count)
|
|
{
|
|
VolumeUsage::ChunkState cstate;
|
|
int i;
|
|
|
|
cstate.isUsed = false;
|
|
cstate.isMarkedUsed = false;
|
|
cstate.purpose = VolumeUsage::kChunkPurposeUnknown;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
if (pList[i].track == 0 && pList[i].sector == 0)
|
|
continue; // sparse file
|
|
|
|
if (!GetSectorUseEntry(pList[i].track, pList[i].sector)) {
|
|
WMSG2("WARNING: freeing unallocated sector T=%d S=%d\n",
|
|
pList[i].track, pList[i].sector);
|
|
assert(false); // impossible unless disk is "damaged"
|
|
}
|
|
SetSectorUseEntry(pList[i].track, pList[i].sector, false);
|
|
|
|
fVolumeUsage.SetChunkState(pList[i].track, pList[i].sector, &cstate);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Rename a file.
|
|
*
|
|
* "newName" must already be normalized.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::RenameFile(A2File* pGenericFile, const char* newName)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
A2FileDOS* pFile = (A2FileDOS*) pGenericFile;
|
|
char normalName[A2FileDOS::kMaxFileName+1];
|
|
char dosName[A2FileDOS::kMaxFileName+1];
|
|
unsigned char sctBuf[kSctSize];
|
|
unsigned char* pEntry;
|
|
|
|
if (pFile == nil || newName == nil)
|
|
return kDIErrInvalidArg;
|
|
if (!IsValidFileName(newName))
|
|
return kDIErrInvalidArg;
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (!fDiskIsGood)
|
|
return kDIErrBadDiskImage;
|
|
|
|
WMSG2(" DOS renaming '%s' to '%s'\n", pFile->GetPathName(), newName);
|
|
|
|
/*
|
|
* Update the disk catalog entry.
|
|
*/
|
|
dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);
|
|
|
|
DoNormalizePath(newName, '\0', normalName);
|
|
A2FileDOS::MakeDOSName(dosName, normalName);
|
|
memcpy(&pEntry[0x03], dosName, A2FileDOS::kMaxFileName);
|
|
|
|
dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Update our internal copy.
|
|
*/
|
|
char storedName[A2FileDOS::kMaxFileName+1];
|
|
strcpy(storedName, dosName);
|
|
LowerASCII((unsigned char*)storedName, A2FileDOS::kMaxFileName);
|
|
A2FileDOS::TrimTrailingSpaces(storedName);
|
|
|
|
strcpy(pFile->fFileName, storedName);
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Set the file's attributes.
|
|
*
|
|
* We allow the file to be locked or unlocked, and we allow the file type
|
|
* to be changed. We don't try to rewrite the file if they're changing to or
|
|
* from a format with embedded data (e.g. BAS or BIN); instead, we just
|
|
* change the type letter. We do need to re-evaluate the end-of-file
|
|
* value afterward.
|
|
*
|
|
* Changing the aux type is only allowed for BIN files.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::SetFileInfo(A2File* pGenericFile, long fileType, long auxType,
|
|
long accessFlags)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
A2FileDOS* pFile = (A2FileDOS*) pGenericFile;
|
|
TrackSector* tsList = nil;
|
|
int tsCount;
|
|
bool nowLocked;
|
|
bool typeChanged;
|
|
|
|
if (pFile == nil)
|
|
return kDIErrInvalidArg;
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
|
|
WMSG4("DOS SetFileInfo '%s' type=0x%02lx aux=0x%04lx access=0x%02lx\n",
|
|
pFile->GetPathName(), fileType, auxType, accessFlags);
|
|
|
|
/*
|
|
* We can ignore the file/aux type, or we can verify that they're not
|
|
* trying to change it. The latter is a little more work but makes
|
|
* the API a little more communicative.
|
|
*/
|
|
if (!A2FileDOS::IsValidType(fileType)) {
|
|
WMSG0("DOS SetFileInfo invalid file type\n");
|
|
dierr = kDIErrInvalidArg;
|
|
goto bail;
|
|
}
|
|
if (auxType != pFile->GetAuxType() && fileType != 0x06) {
|
|
/* this only makes sense for BIN files */
|
|
WMSG0("DOS SetFileInfo aux type mismatch; ignoring\n");
|
|
//dierr = kDIErrNotSupported;
|
|
//goto bail;
|
|
}
|
|
|
|
nowLocked = (accessFlags & A2FileProDOS::kAccessWrite) == 0;
|
|
typeChanged = (fileType != pFile->GetFileType());
|
|
|
|
/*
|
|
* Update the file type and locked status, if necessary.
|
|
*/
|
|
if (nowLocked != pFile->fLocked || typeChanged) {
|
|
A2FileDOS::FileType newFileType;
|
|
unsigned char sctBuf[kSctSize];
|
|
unsigned char* pEntry;
|
|
|
|
WMSG1("Updating file '%s'\n", pFile->GetPathName());
|
|
|
|
dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);
|
|
|
|
newFileType = A2FileDOS::ConvertFileType(fileType, 0);
|
|
pEntry[0x02] = (unsigned char) newFileType;
|
|
if (nowLocked)
|
|
pEntry[0x02] |= 0x80;
|
|
|
|
dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/* update our local copy */
|
|
pFile->fLocked = nowLocked;
|
|
}
|
|
|
|
if (!typeChanged && auxType == pFile->GetAuxType()) {
|
|
/* only the locked status has changed; skip the rest */
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* If the file has type BIN (either because it was before and we left it
|
|
* alone, or we changed it to BIN), we need to figure out what the aux
|
|
* type should be. There are two situations:
|
|
*
|
|
* (1) User specified an aux type. If the aux type passed in doesn't match
|
|
* what's in the A2FileDOS structure, we assume they meant to change it.
|
|
* (2) User didn't specify an aux type change. If the file was BIN before,
|
|
* we don't need to do anything, but if it was just changed to BIN then
|
|
* we need to extract the aux type from the first sector of the file.
|
|
*
|
|
* There's also a 3rd situation: they changed the aux type for a non-BIN
|
|
* file. This should have been blocked earlier.
|
|
*
|
|
* On top of all this, if we changed the file type at all then we need to
|
|
* re-scan the file length and "data offset" value.
|
|
*/
|
|
unsigned short newAuxType;
|
|
newAuxType = (unsigned short) auxType;
|
|
|
|
dierr = pFile->LoadTSList(&tsList, &tsCount);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG1(" DOS SFI: unable to load TS list (err=%d)\n", dierr);
|
|
goto bail;
|
|
}
|
|
|
|
if (fileType == 0x06 && tsCount > 0) {
|
|
unsigned char sctBuf[kSctSize];
|
|
|
|
dierr = fpImg->ReadTrackSector(tsList[0].track,
|
|
tsList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG0("DOS SFI: unable to get first sector of file\n");
|
|
goto bail;
|
|
}
|
|
|
|
if (auxType == pFile->GetAuxType()) {
|
|
newAuxType = GetShortLE(&sctBuf[0x00]);
|
|
WMSG1(" Aux type not changed, extracting from file (0x%04x)\n",
|
|
newAuxType);
|
|
} else {
|
|
WMSG1(" Aux type changed (to 0x%04x), changing file\n",
|
|
newAuxType);
|
|
|
|
PutShortLE(&sctBuf[0x00], newAuxType);
|
|
dierr = fpImg->WriteTrackSector(tsList[0].track,
|
|
tsList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG0("DOS SFI: unable to write first sector of file\n");
|
|
goto bail;
|
|
}
|
|
}
|
|
} else {
|
|
/* not BIN or file has no sectors */
|
|
if (pFile->fFileType == A2FileDOS::kTypeApplesoft)
|
|
newAuxType = 0x0801;
|
|
else
|
|
newAuxType = 0x0000;
|
|
}
|
|
|
|
/* update our local copy */
|
|
pFile->fFileType = A2FileDOS::ConvertFileType(fileType, 0);
|
|
pFile->fAuxType = newAuxType;
|
|
|
|
/*
|
|
* Recalculate the file's length and "data offset". This may also mark
|
|
* the file as "suspicious". We wouldn't be here if the file was
|
|
* suspicious when we opened the disk image -- the image would have
|
|
* been marked read-only -- so if it's suspicious now, it's probably
|
|
* from a previous file type change attempt in the current session.
|
|
* Clear the flag so it doesn't "stick".
|
|
*/
|
|
pFile->ResetQuality();
|
|
(void) ComputeLength(pFile, tsList, tsCount);
|
|
|
|
bail:
|
|
delete[] tsList;
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Change the disk volume name (number).
|
|
*
|
|
* We can't change the 2MG header, and we can't change the values embedded
|
|
* in the sector headers, so all we do is change the VTOC entry.
|
|
*/
|
|
DIError
|
|
DiskFSDOS33::RenameVolume(const char* newName)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
unsigned char sctBuf[kSctSize];
|
|
long newNumber;
|
|
char* endp;
|
|
|
|
if (!IsValidVolumeName(newName))
|
|
return kDIErrInvalidArg;
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
|
|
// convert the number; we already ascertained that it's valid
|
|
newNumber = strtol(newName, &endp, 10);
|
|
|
|
dierr = fpImg->ReadTrackSector(kVTOCTrack, kVTOCSector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
sctBuf[0x06] = (unsigned char) newNumber;
|
|
|
|
dierr = fpImg->WriteTrackSector(kVTOCTrack, kVTOCSector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
fVTOCVolumeNumber = newNumber;
|
|
UpdateVolumeNum();
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* A2FileDOS
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Constructor.
|
|
*/
|
|
A2FileDOS::A2FileDOS(DiskFS* pDiskFS) : A2File(pDiskFS)
|
|
{
|
|
fTSListTrack = -1;
|
|
fTSListSector = -1;
|
|
fLengthInSectors = 0;
|
|
fLocked = false;
|
|
fFileName[0] = '\0';
|
|
fFileType = kTypeUnknown;
|
|
|
|
fCatTS.track = fCatTS.sector = 0;
|
|
fCatEntryNum = -1;
|
|
|
|
fAuxType = 0;
|
|
fDataOffset = 0;
|
|
fLength = -1;
|
|
fSparseLength = -1;
|
|
|
|
fpOpenFile = nil;
|
|
}
|
|
|
|
/*
|
|
* Destructor. Make sure an "open" file gets "closed".
|
|
*/
|
|
A2FileDOS::~A2FileDOS(void)
|
|
{
|
|
delete fpOpenFile;
|
|
}
|
|
|
|
|
|
/*
|
|
* Convert the filetype enum to a ProDOS type.
|
|
*
|
|
* Remember that the DOS filetype field is actually a bit field, so we need
|
|
* to handle situations where more than one bit is set.
|
|
*
|
|
* Ideally this is a reversible transformation, so files copied to ProDOS
|
|
* volumes can be copied back to DOS with no loss of information. The reverse
|
|
* is *not* true, because of file type reduction and the potential loss of
|
|
* accurate file length info.
|
|
*
|
|
* I'm not entirely certain about the conversion of 'R' to REL, largely
|
|
* because I can't find any information on the REL format. However, Copy ][+
|
|
* does convert to REL, and the Binary ][ standard says I should as well.
|
|
*/
|
|
long
|
|
A2FileDOS::GetFileType(void) const
|
|
{
|
|
long retval;
|
|
|
|
switch (fFileType) {
|
|
case kTypeText: retval = 0x04; break; // TXT
|
|
case kTypeInteger: retval = 0xfa; break; // INT
|
|
case kTypeApplesoft: retval = 0xfc; break; // BAS
|
|
case kTypeBinary: retval = 0x06; break; // BIN
|
|
case kTypeS: retval = 0xf2; break; // $f2
|
|
case kTypeReloc: retval = 0xfe; break; // REL
|
|
case kTypeA: retval = 0xf3; break; // $f3
|
|
case kTypeB: retval = 0xf4; break; // $f4
|
|
case kTypeUnknown:
|
|
default:
|
|
retval = 0x00; // NON
|
|
break;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
/*
|
|
* Convert a ProDOS 8 file type to its DOS equivalent.
|
|
*
|
|
* We need to know the file length because files over 64K can't fit into
|
|
* DOS A/I/B files. Text files can be as long as they want, and the
|
|
* other types don't have a length word defined, so they're fine.
|
|
*
|
|
* We can't just convert them later, because by that point they've already
|
|
* got a 2-byte or 4-byte header reserved.
|
|
*
|
|
* Because we don't generally know the eventual length of the file at
|
|
* the time we're creating it, this doesn't work nearly as well as could
|
|
* be hoped. We can make life a little less confusing for the caller by
|
|
* using type 'S' for any unknown type.
|
|
*/
|
|
/*static*/ A2FileDOS::FileType
|
|
A2FileDOS::ConvertFileType(long prodosType, di_off_t fileLen)
|
|
{
|
|
const long kMaxBinary = 65535;
|
|
FileType newType;
|
|
|
|
switch (prodosType) {
|
|
case 0xb0: newType = kTypeText; break; // SRC
|
|
case 0x04: newType = kTypeText; break; // TXT
|
|
case 0xfa: newType = kTypeInteger; break; // INT
|
|
case 0xfc: newType = kTypeApplesoft; break; // BAS
|
|
case 0x06: newType = kTypeBinary; break; // BIN
|
|
case 0xf2: newType = kTypeS; break; // $f2
|
|
case 0xfe: newType = kTypeReloc; break; // REL
|
|
case 0xf3: newType = kTypeA; break; // $f3
|
|
case 0xf4: newType = kTypeB; break; // $f4
|
|
default: newType = kTypeS; break;
|
|
}
|
|
|
|
if (fileLen > kMaxBinary &&
|
|
(newType == kTypeInteger || newType == kTypeApplesoft ||
|
|
newType == kTypeBinary))
|
|
{
|
|
WMSG0(" DOS setting type for large A/I/B file to S\n");
|
|
newType = kTypeS;
|
|
}
|
|
|
|
return newType;
|
|
}
|
|
|
|
/*
|
|
* Determine whether the specified type has a valid DOS mapping.
|
|
*/
|
|
/*static*/ bool
|
|
A2FileDOS::IsValidType(long prodosType)
|
|
{
|
|
switch (prodosType) {
|
|
case 0xb0: // SRC
|
|
case 0x04: // TXT
|
|
case 0xfa: // INT
|
|
case 0xfc: // BAS
|
|
case 0x06: // BIN
|
|
case 0xf2: // $f2
|
|
case 0xfe: // REL
|
|
case 0xf3: // $f3
|
|
case 0xf4: // $f4
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Match the ProDOS equivalents of "locked" and "unlocked".
|
|
*/
|
|
long
|
|
A2FileDOS::GetAccess(void) const
|
|
{
|
|
if (fLocked)
|
|
return DiskFS::kFileAccessLocked; // 0x01 read
|
|
else
|
|
return DiskFS::kFileAccessUnlocked; // 0xc3 read/write/rename/destroy
|
|
}
|
|
|
|
/*
|
|
* "Fix" a DOS3.3 filename. Convert DOS-ASCII to normal ASCII, and strip
|
|
* trailing spaces.
|
|
*/
|
|
void
|
|
A2FileDOS::FixFilename(void)
|
|
{
|
|
DiskFSDOS33::LowerASCII((unsigned char*)fFileName, kMaxFileName);
|
|
TrimTrailingSpaces(fFileName);
|
|
}
|
|
|
|
/*
|
|
* Trim the spaces off the end of a filename.
|
|
*
|
|
* Assumes the filename has already been converted to low ASCII.
|
|
*/
|
|
/*static*/ void
|
|
A2FileDOS::TrimTrailingSpaces(char* filename)
|
|
{
|
|
char* lastspc = filename + strlen(filename);
|
|
|
|
assert(*lastspc == '\0');
|
|
|
|
while (--lastspc) {
|
|
if (*lastspc != ' ')
|
|
break;
|
|
}
|
|
|
|
*(lastspc+1) = '\0';
|
|
}
|
|
|
|
/*
|
|
* Encode a filename into high ASCII, padded out with spaces to
|
|
* kMaxFileName chars. Lower case is converted to upper case. This
|
|
* does not filter out control characters or other chunk.
|
|
*
|
|
* "buf" must be able to hold kMaxFileName+1 chars.
|
|
*/
|
|
/*static*/ void
|
|
A2FileDOS::MakeDOSName(char* buf, const char* name)
|
|
{
|
|
for (int i = 0; i < kMaxFileName; i++) {
|
|
if (*name == '\0')
|
|
*buf++ = (char) 0xa0;
|
|
else
|
|
*buf++ = toupper(*name++) | 0x80;
|
|
}
|
|
*buf = '\0';
|
|
}
|
|
|
|
|
|
/*
|
|
* Set up state for this file.
|
|
*/
|
|
DIError
|
|
A2FileDOS::Open(A2FileDescr** ppOpenFile, bool readOnly,
|
|
bool rsrcFork /*=false*/)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
A2FDDOS* pOpenFile = nil;
|
|
|
|
if (!readOnly) {
|
|
if (fpDiskFS->GetDiskImg()->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (fpDiskFS->GetFSDamaged())
|
|
return kDIErrBadDiskImage;
|
|
}
|
|
|
|
if (fpOpenFile != nil) {
|
|
dierr = kDIErrAlreadyOpen;
|
|
goto bail;
|
|
}
|
|
|
|
if (rsrcFork)
|
|
return kDIErrForkNotFound;
|
|
|
|
pOpenFile = new A2FDDOS(this);
|
|
|
|
dierr = LoadTSList(&pOpenFile->fTSList, &pOpenFile->fTSCount,
|
|
&pOpenFile->fIndexList, &pOpenFile->fIndexCount);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG1("DOS33 unable to load TS for '%s' open\n", GetPathName());
|
|
goto bail;
|
|
}
|
|
|
|
pOpenFile->fOffset = 0;
|
|
pOpenFile->fOpenEOF = fLength;
|
|
pOpenFile->fOpenSectorsUsed = fLengthInSectors;
|
|
|
|
fpOpenFile = pOpenFile; // add it to our single-member "open file set"
|
|
*ppOpenFile = pOpenFile;
|
|
pOpenFile = nil;
|
|
|
|
bail:
|
|
delete pOpenFile;
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Dump the contents of an A2FileDOS.
|
|
*/
|
|
void
|
|
A2FileDOS::Dump(void) const
|
|
{
|
|
WMSG1("A2FileDOS '%s'\n", fFileName);
|
|
WMSG2(" TS T=%-2d S=%-2d\n", fTSListTrack, fTSListSector);
|
|
WMSG2(" Cat T=%-2d S=%-2d\n", fCatTS.track, fCatTS.sector);
|
|
WMSG3(" type=%d lck=%d slen=%d\n", fFileType, fLocked, fLengthInSectors);
|
|
WMSG2(" auxtype=0x%04x length=%ld\n",
|
|
fAuxType, (long) fLength);
|
|
}
|
|
|
|
|
|
/*
|
|
* Load the T/S list for this file.
|
|
*
|
|
* A single T/S sector holds 122 entries, enough to store a 30.5K file.
|
|
* It's very unlikely that a file will need more than two, although it's
|
|
* possible for a random-access text file to have a very large number of
|
|
* entries.
|
|
*
|
|
* If "pIndexList" and "pIndexCount" are non-nil, the list of index blocks is
|
|
* also loaded.
|
|
*
|
|
* It's entirely possible to get a large T/S list back that is filled
|
|
* entirely with zeroes. This can happen if we have a large set of T/S
|
|
* index sectors that are all zero. We have to leave space for them so
|
|
* that the Write function can use the existing allocated index blocks.
|
|
*
|
|
* THOUGHT: we may want to use the file type to tighten this up a bit.
|
|
* For example, we're currently very careful around random-access text
|
|
* files, but if the file doesn't have type 'T' then random access is
|
|
* impossible. Currently this isn't a problem, but for e.g. T/S lists
|
|
* with garbage at the end would could deal with the problem more generally.
|
|
*/
|
|
DIError
|
|
A2FileDOS::LoadTSList(TrackSector** pTSList, int* pTSCount,
|
|
TrackSector** pIndexList, int* pIndexCount)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
DiskImg* pDiskImg;
|
|
const int kDefaultTSAlloc = 2;
|
|
const int kDefaultIndexAlloc = 8;
|
|
TrackSector* tsList = nil;
|
|
TrackSector* indexList = nil;
|
|
int tsCount, tsAlloc;
|
|
int indexCount, indexAlloc;
|
|
unsigned char sctBuf[kSctSize];
|
|
int track, sector, iterations;
|
|
|
|
WMSG1("--- DOS loading T/S list for '%s'\n", GetPathName());
|
|
|
|
/* over-alloc for small files to reduce reallocs */
|
|
tsAlloc = kMaxTSPairs * kDefaultTSAlloc;
|
|
tsList = new TrackSector[tsAlloc];
|
|
tsCount = 0;
|
|
|
|
indexAlloc = kDefaultIndexAlloc;
|
|
indexList = new TrackSector[indexAlloc];
|
|
indexCount = 0;
|
|
|
|
if (tsList == nil || indexList == nil) {
|
|
dierr = kDIErrMalloc;
|
|
goto bail;
|
|
}
|
|
|
|
assert(fpDiskFS != nil);
|
|
pDiskImg = fpDiskFS->GetDiskImg();
|
|
assert(pDiskImg != nil);
|
|
|
|
/* get the first T/S sector for this file */
|
|
track = fTSListTrack;
|
|
sector = fTSListSector;
|
|
if (track >= pDiskImg->GetNumTracks() ||
|
|
sector >= pDiskImg->GetNumSectPerTrack())
|
|
{
|
|
WMSG3(" DOS33 invalid initial T/S %d,%d in '%s'\n", track, sector,
|
|
fFileName);
|
|
dierr = kDIErrBadFile;
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Run through the set of t/s pairs.
|
|
*/
|
|
iterations = 0;
|
|
do {
|
|
unsigned short sectorOffset;
|
|
int lastNonZero;
|
|
|
|
/*
|
|
* Add the current T/S sector to the index list.
|
|
*/
|
|
if (indexCount == indexAlloc) {
|
|
WMSG0("+++ expanding index list\n");
|
|
TrackSector* newList;
|
|
indexAlloc += kDefaultIndexAlloc;
|
|
newList = new TrackSector[indexAlloc];
|
|
if (newList == nil) {
|
|
dierr = kDIErrMalloc;
|
|
goto bail;
|
|
}
|
|
memcpy(newList, indexList, indexCount * sizeof(TrackSector));
|
|
delete[] indexList;
|
|
indexList = newList;
|
|
}
|
|
indexList[indexCount].track = track;
|
|
indexList[indexCount].sector = sector;
|
|
indexCount++;
|
|
|
|
|
|
//WMSG2("+++ scanning T/S at T=%d S=%d\n", track, sector);
|
|
dierr = pDiskImg->ReadTrackSector(track, sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/* grab next track/sector */
|
|
track = sctBuf[0x01];
|
|
sector = sctBuf[0x02];
|
|
sectorOffset = GetShortLE(&sctBuf[0x05]);
|
|
|
|
/* if T/S link is bogus, whole sector is probably bad */
|
|
if (track >= pDiskImg->GetNumTracks() ||
|
|
sector >= pDiskImg->GetNumSectPerTrack())
|
|
{
|
|
// bogus T/S, mark file as damaged and stop
|
|
WMSG3(" DOS33 invalid T/S link %d,%d in '%s'\n", track, sector,
|
|
GetPathName());
|
|
dierr = kDIErrBadFile;
|
|
goto bail;
|
|
}
|
|
if ((sectorOffset % kMaxTSPairs) != 0) {
|
|
WMSG2(" DOS33 invalid T/S header sector offset %u in '%s'\n",
|
|
sectorOffset, GetPathName());
|
|
// not fatal, just weird
|
|
}
|
|
|
|
/*
|
|
* Make sure we have enough room to hold an entire sector full of
|
|
* T/S pairs in the list.
|
|
*/
|
|
if (tsCount + kMaxTSPairs > tsAlloc) {
|
|
WMSG0("+++ expanding ts list\n");
|
|
TrackSector* newList;
|
|
tsAlloc += kMaxTSPairs * kDefaultTSAlloc;
|
|
newList = new TrackSector[tsAlloc];
|
|
if (newList == nil) {
|
|
dierr = kDIErrMalloc;
|
|
goto bail;
|
|
}
|
|
memcpy(newList, tsList, tsCount * sizeof(TrackSector));
|
|
delete[] tsList;
|
|
tsList = newList;
|
|
}
|
|
|
|
/*
|
|
* Add the entries. If there's another T/S list linked, we just
|
|
* grab the entire sector. If not, we grab every entry until the
|
|
* last 0,0. (Can't stop at the first (0,0), or we'll drop a
|
|
* piece of a random access text file.)
|
|
*/
|
|
dierr = ExtractTSPairs(sctBuf, &tsList[tsCount], &lastNonZero);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
if (track != 0 && sector != 0) {
|
|
/* more T/S lists to come, so we keep all entries */
|
|
tsCount += kMaxTSPairs;
|
|
} else {
|
|
/* this was the last one */
|
|
if (lastNonZero == -1) {
|
|
/* this is ALWAYS the case for a newly-created file */
|
|
//WMSG1(" DOS33 odd -- last T/S sector of '%s' was empty\n",
|
|
// GetPathName());
|
|
}
|
|
tsCount += lastNonZero +1;
|
|
}
|
|
|
|
iterations++; // watch for infinite loops
|
|
} while (!(track == 0 && sector == 0) && iterations < kMaxTSIterations);
|
|
|
|
if (iterations == kMaxTSIterations) {
|
|
dierr = kDIErrFileLoop;
|
|
goto bail;
|
|
}
|
|
|
|
*pTSList = tsList;
|
|
*pTSCount = tsCount;
|
|
tsList = nil;
|
|
|
|
if (pIndexList != nil) {
|
|
*pIndexList = indexList;
|
|
*pIndexCount = indexCount;
|
|
indexList = nil;
|
|
}
|
|
|
|
bail:
|
|
delete[] tsList;
|
|
delete[] indexList;
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Extract the track/sector pairs from the TS list in "sctBuf". The entries
|
|
* are copied to "tsList", which is assumed to have enough space to hold
|
|
* at least kMaxTSPairs entries.
|
|
*
|
|
* The last non-zero entry will be identified and stored in "*pLastNonZero".
|
|
* If all entries are zero, it will be set to -1.
|
|
*
|
|
* Sometimes files will have junk at the tail end of an otherwise valid
|
|
* T/S list. We can't just stop when we hit the first (0,0) entry because
|
|
* that'll screw up random-access text file handling. What we can do is
|
|
* try to detect the situation, and mark the file as "suspicious" without
|
|
* returning an error if we see it.
|
|
*
|
|
* If a TS entry appears to be invalid, this returns an error after all
|
|
* entries have been copied. If it looks to be partially valid, only the
|
|
* valid parts are copied out, with the rest zeroed.
|
|
*/
|
|
DIError
|
|
A2FileDOS::ExtractTSPairs(const unsigned char* sctBuf, TrackSector* tsList,
|
|
int* pLastNonZero)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
const DiskImg* pDiskImg = fpDiskFS->GetDiskImg();
|
|
const unsigned char* ptr;
|
|
int i, track, sector;
|
|
|
|
*pLastNonZero = -1;
|
|
memset(tsList, 0, sizeof(TrackSector) * kMaxTSPairs);
|
|
|
|
ptr = &sctBuf[kTSOffset]; // offset of first T/S entry (0x0c)
|
|
|
|
for (i = 0; i < kMaxTSPairs; i++) {
|
|
track = *ptr++;
|
|
sector = *ptr++;
|
|
|
|
if (dierr == kDIErrNone &&
|
|
(track >= pDiskImg->GetNumTracks() ||
|
|
sector >= pDiskImg->GetNumSectPerTrack() ||
|
|
(track == 0 && sector != 0)))
|
|
{
|
|
WMSG3(" DOS33 invalid T/S %d,%d in '%s'\n", track, sector,
|
|
fFileName);
|
|
|
|
if (i > 0 && tsList[i-1].track == 0 && tsList[i-1].sector == 0) {
|
|
WMSG0(" T/S list looks partially valid\n");
|
|
SetQuality(kQualitySuspicious);
|
|
goto bail; // quit immediately
|
|
} else {
|
|
dierr = kDIErrBadFile;
|
|
// keep going, just so caller has the full set to stare at
|
|
}
|
|
}
|
|
|
|
if (track != 0 || sector != 0)
|
|
*pLastNonZero = i;
|
|
|
|
tsList[i].track = track;
|
|
tsList[i].sector = sector;
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* A2FDDOS
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Read data from the current offset.
|
|
*
|
|
* Files read back as they would from ProDOS, i.e. if you read a binary
|
|
* file you won't see the 4 bytes of length and address.
|
|
*/
|
|
DIError
|
|
A2FDDOS::Read(void* buf, size_t len, size_t* pActual)
|
|
{
|
|
WMSG3(" DOS reading %d bytes from '%s' (offset=%ld)\n",
|
|
len, fpFile->GetPathName(), (long) fOffset);
|
|
|
|
A2FileDOS* pFile = (A2FileDOS*) fpFile;
|
|
|
|
/*
|
|
* Don't allow them to read past the end of the file. The length value
|
|
* stored in pFile->fLength already has pFile->fDataOffset subtracted
|
|
* from the actual data length, so don't factor it in again.
|
|
*/
|
|
if (fOffset + (long)len > fOpenEOF) {
|
|
if (pActual == nil)
|
|
return kDIErrDataUnderrun;
|
|
len = (size_t) (fOpenEOF - fOffset);
|
|
}
|
|
if (pActual != nil)
|
|
*pActual = len;
|
|
long incrLen = len;
|
|
|
|
DIError dierr = kDIErrNone;
|
|
unsigned char sctBuf[kSctSize];
|
|
di_off_t actualOffset = fOffset + pFile->fDataOffset; // adjust for embedded len
|
|
int tsIndex = (int) (actualOffset / kSctSize);
|
|
int bufOffset = (int) (actualOffset % kSctSize); // (& 0xff)
|
|
size_t thisCount;
|
|
|
|
if (len == 0)
|
|
return kDIErrNone;
|
|
assert(fOpenEOF != 0);
|
|
|
|
assert(tsIndex >= 0 && tsIndex < fTSCount);
|
|
|
|
/* could be more clever in here and avoid double-buffering */
|
|
while (len) {
|
|
if (tsIndex >= fTSCount) {
|
|
/* should've caught this earlier */
|
|
assert(false);
|
|
WMSG1(" DOS ran off the end (fTSCount=%d)\n", fTSCount);
|
|
return kDIErrDataUnderrun;
|
|
}
|
|
|
|
if (fTSList[tsIndex].track == 0 && fTSList[tsIndex].sector == 0) {
|
|
//WMSG2(" DOS sparse sector T=%d S=%d\n",
|
|
// TSTrack(fTSList[tsIndex]), TSSector(fTSList[tsIndex]));
|
|
memset(sctBuf, 0, sizeof(sctBuf));
|
|
} else {
|
|
dierr = pFile->GetDiskFS()->GetDiskImg()->ReadTrackSector(
|
|
fTSList[tsIndex].track,
|
|
fTSList[tsIndex].sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG1(" DOS error reading file '%s'\n", pFile->GetPathName());
|
|
return dierr;
|
|
}
|
|
}
|
|
thisCount = kSctSize - bufOffset;
|
|
if (thisCount > len)
|
|
thisCount = len;
|
|
memcpy(buf, sctBuf + bufOffset, thisCount);
|
|
len -= thisCount;
|
|
buf = (char*)buf + thisCount;
|
|
|
|
bufOffset = 0;
|
|
tsIndex++;
|
|
}
|
|
|
|
fOffset += incrLen;
|
|
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Write data at the current offset.
|
|
*
|
|
* For simplicity, we assume that we're writing a brand-new file in one
|
|
* shot. As it happens, that's all we're currently required to do, so even
|
|
* if we wrote a more sophisticated function it wouldn't get exercised.
|
|
* Because of the way we write, there's no way to mimic the behavior of
|
|
* random-access text file allocation, so that isn't supported.
|
|
*
|
|
* The data in "buf" should *not* include the 2-4 bytes of header present
|
|
* on A/I/B files. That's already factored in.
|
|
*
|
|
* Modifies fOpenEOF, fOpenSectorsUsed, and sets fModified.
|
|
*/
|
|
DIError
|
|
A2FDDOS::Write(const void* buf, size_t len, size_t* pActual)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
A2FileDOS* pFile = (A2FileDOS*) fpFile;
|
|
DiskFSDOS33* pDiskFS = (DiskFSDOS33*) fpFile->GetDiskFS();
|
|
unsigned char sctBuf[kSctSize];
|
|
|
|
WMSG2(" DOS Write len=%u %s\n", len, pFile->GetPathName());
|
|
|
|
if (len >= 0x01000000) { // 16MB
|
|
assert(false);
|
|
return kDIErrInvalidArg;
|
|
}
|
|
assert(fOffset == 0); // big simplifying assumption
|
|
assert(fOpenEOF == 0); // another one
|
|
assert(fTSCount == 0); // must hold for our newly-created files
|
|
assert(fIndexCount == 1); // must hold for our newly-created files
|
|
assert(fOpenSectorsUsed == fTSCount + fIndexCount);
|
|
assert(buf != nil);
|
|
|
|
long actualLen = (long) len + pFile->fDataOffset;
|
|
long numSectors = (actualLen + kSctSize -1) / kSctSize;
|
|
TrackSector firstIndex;
|
|
int i;
|
|
|
|
/*
|
|
* Nothing to do for zero-length write; don't even set fModified. Note,
|
|
* however, that a zero-length 'B' file is actually 4 bytes long, and
|
|
* must have a data block allocated.
|
|
*/
|
|
if (actualLen == 0)
|
|
goto bail;
|
|
assert(numSectors > 0);
|
|
|
|
dierr = pDiskFS->LoadVolBitmap();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* Start by allocating a full T/S list. The existing T/S list is
|
|
* empty, but we do have one T/S index sector to fill before we
|
|
* allocate any others.
|
|
*
|
|
* Since we determined above that there was nothing interesting in
|
|
* our T/S list, we just grab the one allocated block, throw out
|
|
* the lists, and reallocate them.
|
|
*/
|
|
firstIndex = fIndexList[0];
|
|
delete[] fTSList;
|
|
delete[] fIndexList;
|
|
fTSList = fIndexList = nil;
|
|
|
|
fTSCount = numSectors;
|
|
fTSList = new TrackSector[fTSCount];
|
|
fIndexCount = (numSectors + kMaxTSPairs -1) / kMaxTSPairs;
|
|
assert(fIndexCount > 0);
|
|
fIndexList = new TrackSector[fIndexCount];
|
|
if (fTSList == nil || fIndexList == nil) {
|
|
dierr = kDIErrMalloc;
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Allocate all of the index sectors. In theory we should to this along
|
|
* with the file sectors, so that the index and file sectors are
|
|
* interspersed with the data, but in practice 99% of the file have
|
|
* only one or two index blocks. By grouping them together we improve
|
|
* the performance for emulators and CiderPress.
|
|
*/
|
|
fIndexList[0] = firstIndex;
|
|
for (i = 1; i < fIndexCount; i++) {
|
|
TrackSector allocTS;
|
|
|
|
dierr = pDiskFS->AllocSector(&allocTS);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
fIndexList[i] = allocTS;
|
|
}
|
|
/*
|
|
* Allocate the data sectors.
|
|
*/
|
|
for (i = 0; i < fTSCount; i++) {
|
|
TrackSector allocTS;
|
|
|
|
dierr = pDiskFS->AllocSector(&allocTS);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
fTSList[i] = allocTS;
|
|
}
|
|
|
|
/*
|
|
* Write the sectors into the T/S list.
|
|
*/
|
|
const unsigned char* curPtr;
|
|
int sectorIdx;
|
|
|
|
curPtr = (const unsigned char*) buf;
|
|
sectorIdx = 0;
|
|
|
|
if (pFile->fDataOffset > 0) {
|
|
/* handle first sector specially */
|
|
assert(pFile->fDataOffset < kSctSize);
|
|
int dataInFirstSct = kSctSize - pFile->fDataOffset;
|
|
if (dataInFirstSct > actualLen - pFile->fDataOffset)
|
|
dataInFirstSct = actualLen - pFile->fDataOffset;
|
|
|
|
// dataInFirstSct could be zero (== len)
|
|
memset(sctBuf, 0, sizeof(sctBuf));
|
|
memcpy(sctBuf + pFile->fDataOffset, curPtr,
|
|
dataInFirstSct);
|
|
dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track,
|
|
fTSList[sectorIdx].sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
sectorIdx++;
|
|
actualLen -= dataInFirstSct + pFile->fDataOffset;
|
|
curPtr += dataInFirstSct;
|
|
}
|
|
while (actualLen > 0) {
|
|
if (actualLen >= kSctSize) {
|
|
/* write directly from input */
|
|
dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track,
|
|
fTSList[sectorIdx].sector, curPtr);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
} else {
|
|
/* make a copy of the partial buffer */
|
|
memset(sctBuf, 0, sizeof(sctBuf));
|
|
memcpy(sctBuf, curPtr, actualLen);
|
|
dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track,
|
|
fTSList[sectorIdx].sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
}
|
|
|
|
sectorIdx++;
|
|
actualLen -= kSctSize; // goes negative; that's fine
|
|
curPtr += kSctSize;
|
|
}
|
|
assert(sectorIdx == fTSCount);
|
|
|
|
/*
|
|
* Fill out the T/S list sectors. Failure here presents a potential
|
|
* problem because, once we've written the first T/S entry, the file
|
|
* appears to have storage that it actually doesn't. The easiest way
|
|
* to handle this safely is to start by writing the last index block
|
|
* first.
|
|
*/
|
|
for (i = fIndexCount-1; i >= 0; i--) {
|
|
int tsOffset = i * kMaxTSPairs;
|
|
assert(tsOffset < fTSCount);
|
|
|
|
memset(sctBuf, 0, kSctSize);
|
|
if (i != fIndexCount-1) {
|
|
sctBuf[0x01] = fIndexList[i+1].track;
|
|
sctBuf[0x02] = fIndexList[i+1].sector;
|
|
}
|
|
PutShortLE(&sctBuf[0x05], kMaxTSPairs * i);
|
|
|
|
int ent = i * kMaxTSPairs; // start here
|
|
for (int j = 0; j < kMaxTSPairs; j++) {
|
|
if (ent == fTSCount)
|
|
break;
|
|
sctBuf[kTSOffset + j*2] = fTSList[ent].track;
|
|
sctBuf[kTSOffset + j*2 +1] = fTSList[ent].sector;
|
|
ent++;
|
|
}
|
|
|
|
dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fIndexList[i].track,
|
|
fIndexList[i].sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
}
|
|
|
|
dierr = pDiskFS->SaveVolBitmap();
|
|
if (dierr != kDIErrNone) {
|
|
/*
|
|
* This is awkward -- we wrote the first T/S list, so the file
|
|
* now appears to have content, but the blocks aren't marked used.
|
|
* We read the VTOC successfully though, so it's VERY unlikely
|
|
* that this will fail. If it does, it's likely that any attempt
|
|
* to mitigate the problem will also fail. (Maybe we could force
|
|
* the object into read-only mode?)
|
|
*/
|
|
goto bail;
|
|
}
|
|
|
|
/* finish up */
|
|
fOpenSectorsUsed = fIndexCount + fTSCount;
|
|
fOpenEOF = len;
|
|
fOffset += len;
|
|
fModified = true;
|
|
|
|
if (!UpdateProgress(fOffset))
|
|
dierr = kDIErrCancelled;
|
|
|
|
bail:
|
|
pDiskFS->FreeVolBitmap();
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Seek to the specified offset.
|
|
*/
|
|
DIError
|
|
A2FDDOS::Seek(di_off_t offset, DIWhence whence)
|
|
{
|
|
//di_off_t fileLength = fpFile->GetDataLength();
|
|
|
|
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 kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* Return current offset.
|
|
*/
|
|
di_off_t
|
|
A2FDDOS::Tell(void)
|
|
{
|
|
return fOffset;
|
|
}
|
|
|
|
/*
|
|
* Release file state.
|
|
*
|
|
* If the file was modified, we need to update the sector usage count in
|
|
* the catalog track, and possibly a length word in the first sector of
|
|
* the file (for A/I/B).
|
|
*
|
|
* Given the current "write all at once" implementation of Write, we could
|
|
* have handled the length word back when initially writing the data, but
|
|
* someday we may fix that and I don't want to have to rewrite this part.
|
|
*
|
|
* 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
|
|
A2FDDOS::Close(void)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
|
|
if (fModified) {
|
|
DiskFSDOS33* pDiskFS = (DiskFSDOS33*) fpFile->GetDiskFS();
|
|
A2FileDOS* pFile = (A2FileDOS*) fpFile;
|
|
unsigned char sctBuf[kSctSize];
|
|
unsigned char* pEntry;
|
|
|
|
/*
|
|
* Fill in the length and address, if needed for this type of file.
|
|
*/
|
|
if (pFile->fFileType == A2FileDOS::kTypeInteger ||
|
|
pFile->fFileType == A2FileDOS::kTypeApplesoft ||
|
|
pFile->fFileType == A2FileDOS::kTypeBinary)
|
|
{
|
|
assert(fTSCount > 0);
|
|
assert(pFile->fDataOffset > 0);
|
|
//assert(fOpenEOF < 65536);
|
|
if (fOpenEOF > 65535) {
|
|
WMSG1("WARNING: DOS Close trimming A/I/B file from %ld to 65535\n",
|
|
(long) fOpenEOF);
|
|
fOpenEOF = 65535;
|
|
}
|
|
dierr = pDiskFS->GetDiskImg()->ReadTrackSector(fTSList[0].track,
|
|
fTSList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG0("DOS Close: unable to get first sector of file\n");
|
|
goto bail;
|
|
}
|
|
|
|
if (pFile->fFileType == A2FileDOS::kTypeInteger ||
|
|
pFile->fFileType == A2FileDOS::kTypeApplesoft)
|
|
{
|
|
PutShortLE(&sctBuf[0x00], (unsigned short) fOpenEOF);
|
|
} else {
|
|
PutShortLE(&sctBuf[0x00], pFile->fAuxType);
|
|
PutShortLE(&sctBuf[0x02], (unsigned short) fOpenEOF);
|
|
}
|
|
|
|
dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[0].track,
|
|
fTSList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
WMSG0("DOS Close: unable to write first sector of file\n");
|
|
goto bail;
|
|
}
|
|
} else if (pFile->fFileType == A2FileDOS::kTypeText) {
|
|
/*
|
|
* The length of text files can be determined by looking for the
|
|
* first $00. A file of exactly 256 bytes occupies only one
|
|
* sector though, so running out of sectors also works -- the
|
|
* last $00 is not mandatory.
|
|
*
|
|
* Bottom line is that the value we just wrote for fOpenEOF is
|
|
* *probably* recoverable, so we can stuff it into "fLength"
|
|
* with some assurance that it will be there when we reopen the
|
|
* file.
|
|
*/
|
|
} else {
|
|
/*
|
|
* The remaining file types have a length based solely on
|
|
* sector count. We need to round off our length value.
|
|
*/
|
|
fOpenEOF = ((fOpenEOF + kSctSize-1) / kSctSize) * kSctSize;
|
|
}
|
|
|
|
/*
|
|
* Update our internal copies of stuff.
|
|
*/
|
|
pFile->fLength = fOpenEOF;
|
|
pFile->fSparseLength = pFile->fLength;
|
|
pFile->fLengthInSectors = (unsigned short) fOpenSectorsUsed;
|
|
|
|
/*
|
|
* Update the sector count in the directory entry.
|
|
*/
|
|
dierr = pDiskFS->GetDiskImg()->ReadTrackSector(pFile->fCatTS.track,
|
|
pFile->fCatTS.sector, sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum);
|
|
assert(GetShortLE(&pEntry[0x21]) == 1); // holds for new file
|
|
PutShortLE(&pEntry[0x21], pFile->fLengthInSectors);
|
|
dierr = pDiskFS->GetDiskImg()->WriteTrackSector(pFile->fCatTS.track,
|
|
pFile->fCatTS.sector, sctBuf);
|
|
}
|
|
|
|
bail:
|
|
fpFile->CloseDescr(this);
|
|
return dierr;
|
|
}
|
|
|
|
|
|
/*
|
|
* Return the #of sectors/blocks in the file.
|
|
*/
|
|
long
|
|
A2FDDOS::GetSectorCount(void) const
|
|
{
|
|
return fTSCount;
|
|
}
|
|
long
|
|
A2FDDOS::GetBlockCount(void) const
|
|
{
|
|
return (fTSCount+1)/2;
|
|
}
|
|
|
|
/*
|
|
* Return the Nth track/sector in this file.
|
|
*
|
|
* Returns (0,0) for a sparse sector.
|
|
*/
|
|
DIError
|
|
A2FDDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
|
|
{
|
|
if (sectorIdx < 0 || sectorIdx >= fTSCount)
|
|
return kDIErrInvalidIndex;
|
|
|
|
*pTrack = fTSList[sectorIdx].track;
|
|
*pSector = fTSList[sectorIdx].sector;
|
|
return kDIErrNone;
|
|
}
|
|
/*
|
|
* Return the Nth 512-byte block in this file. Since things aren't stored
|
|
* in 512-byte blocks, we're reduced to finding storage at (tsIndex*2) and
|
|
* converting it to a block number.
|
|
*/
|
|
DIError
|
|
A2FDDOS::GetStorage(long blockIdx, long* pBlock) const
|
|
{
|
|
long sectorIdx = blockIdx * 2;
|
|
if (sectorIdx < 0 || sectorIdx >= fTSCount)
|
|
return kDIErrInvalidIndex;
|
|
|
|
bool dummy;
|
|
TrackSectorToBlock(fTSList[sectorIdx].track,
|
|
fTSList[sectorIdx].sector, pBlock, &dummy);
|
|
assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks());
|
|
return kDIErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Dump the T/S list for an open file.
|
|
*/
|
|
void
|
|
A2FDDOS::DumpTSList(void) const
|
|
{
|
|
//A2FileDOS* pFile = (A2FileDOS*) fpFile;
|
|
WMSG2(" DOS T/S list for '%s' (count=%d)\n",
|
|
((A2FileDOS*)fpFile)->fFileName, fTSCount);
|
|
|
|
int i;
|
|
for (i = 0; i <= fTSCount; i++) {
|
|
WMSG3(" %3d: T=%-2d S=%d\n", i, fTSList[i].track, fTSList[i].sector);
|
|
}
|
|
}
|