ciderpress/diskimg/DOS33.cpp

3448 lines
95 KiB
C++
Raw Normal View History

2007-03-27 17:47:10 +00:00
/*
* 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);
}
}