mirror of
https://github.com/fadden/ciderpress.git
synced 2024-12-22 05:29:43 +00:00
b97584eeb6
Fix some %ld message in log messages, and update the Linux sample code to match recent changes in NufxLib and DiskImgLib. Also, bump MDC version to 3.0.0 to match Windows version.
3386 lines
106 KiB
C++
3386 lines
106 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 uint8_t* GetCatalogEntryPtr(uint8_t* 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;
|
|
uint8_t 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)
|
|
{
|
|
LOGI(" DOS header test failed (order=%d)", 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]) {
|
|
LOGI(" DOS detected self-reference on cat (%d,%d)",
|
|
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;
|
|
LOGI(" DOS directory links cause a loop (order=%d)", imageOrder);
|
|
goto bail;
|
|
}
|
|
|
|
LOGI(" DOS foundGood=%d order=%d", 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))
|
|
{
|
|
LOGI(" DOS test: bestCount=%d for order=%d", bestCount, bestOrder);
|
|
assert(bestOrder != DiskImg::kSectorOrderUnknown);
|
|
*pOrder = bestOrder;
|
|
*pFormat = DiskImg::kFormatDOS33;
|
|
if (pImg->GetNumSectPerTrack() == 13)
|
|
*pFormat = DiskImg::kFormatDOS32;
|
|
return kDIErrNone;
|
|
}
|
|
|
|
LOGI(" DOS33 didn't find valid DOS3.2 or DOS3.3");
|
|
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) {
|
|
LOGI(" DOS - headerOnly set, skipping file load");
|
|
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(NULL);
|
|
// while (pFile != NULL) {
|
|
// 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()) {
|
|
LOGI(" DOS33 warning: VTOC numtracks %d vs %ld",
|
|
fVTOCNumTracks, fpImg->GetNumTracks());
|
|
}
|
|
if (fVTOCNumSectors != fpImg->GetNumSectPerTrack()) {
|
|
LOGI(" DOS33 warning: VTOC numsect %d vs %d",
|
|
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) {
|
|
LOGI(" NOTE: ignoring VTOC vol (%d) in favor of embedded (%d)",
|
|
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)
|
|
{
|
|
|
|
LOGI("VTOC catalog: track=%d sector=%d",
|
|
fFirstCatTrack, fFirstCatSector);
|
|
LOGI(" volnum=%d numTracks=%d numSects=%d",
|
|
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;
|
|
|
|
//LOGI(" DOS setting usage %d,%d to %d", track, sector, purpose);
|
|
|
|
fVolumeUsage.GetChunkState(track, sector, &cstate);
|
|
if (cstate.isUsed) {
|
|
cstate.purpose = VolumeUsage::kChunkPurposeConflict;
|
|
// LOGI(" DOS conflicting uses for t=%d s=%d", 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;
|
|
|
|
LOGI(" map 0123456789abcdef");
|
|
|
|
for (int i = 0; i < kMaxTracks; i++) {
|
|
uint32_t val, origVal;
|
|
int bit;
|
|
|
|
val = (uint32_t) fVTOC[0x38 + i*4] << 24;
|
|
val |= (uint32_t) fVTOC[0x39 + i*4] << 16;
|
|
val |= (uint32_t) fVTOC[0x3a + i*4] << 8;
|
|
val |= (uint32_t) 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;
|
|
}
|
|
LOGI(" %2d: %s (0x%08x)", 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 uint32_t DiskFSDOS33::GetVTOCEntry(const uint8_t* pVTOC, long track) const
|
|
{
|
|
uint32_t val;
|
|
val = (uint32_t) pVTOC[0x38 + track*4] << 24;
|
|
val |= (uint32_t) pVTOC[0x39 + track*4] << 16;
|
|
val |= (uint32_t) pVTOC[0x3a + track*4] << 8;
|
|
val |= (uint32_t) 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)
|
|
{
|
|
uint32_t val;
|
|
uint32_t 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) {
|
|
LOGI("DOS33 AllocSector unable to find empty sector");
|
|
return kDIErrDiskFull;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We've got the track. Now find the first free sector.
|
|
*/
|
|
int sector;
|
|
sector = numSectPerTrack-1;
|
|
while (sector >= 0) {
|
|
if (val & 0x80000000) {
|
|
//LOGI("+++ allocating T=%d S=%d", 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] = (uint8_t) 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());
|
|
|
|
uint32_t val, mask;
|
|
|
|
val = GetVTOCEntry(fVTOC, track);
|
|
//val = (uint32_t) fVTOC[0x38 + track*4] << 24;
|
|
//val |= (uint32_t) fVTOC[0x39 + track*4] << 16;
|
|
//val |= (uint32_t) fVTOC[0x3a + track*4] << 8;
|
|
//val |= (uint32_t) 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());
|
|
|
|
uint32_t 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] = (uint8_t) (val >> 24);
|
|
fVTOC[0x39 + track*4] = (uint8_t) (val >> 16);
|
|
fVTOC[0x3a + track*4] = (uint8_t) (val >> 8);
|
|
fVTOC[0x3b + track*4] = (uint8_t) 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;
|
|
uint8_t 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);
|
|
|
|
LOGI(" DOS33 reading catalog sector T=%d S=%d", 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]) {
|
|
LOGI(" DOS detected self-reference on cat (%d,%d)",
|
|
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())
|
|
{
|
|
LOGI(" DOS bailing out early on catalog read due to funky T/S");
|
|
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 uint8_t* sctBuf)
|
|
{
|
|
A2FileDOS* pFile;
|
|
const uint8_t* 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? */
|
|
LOGI(" DOS33 peculiar filetype byte 0x%02x", 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 |= (uint16_t) 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 = NULL;
|
|
TrackSector* indexList = NULL;
|
|
int tsCount;
|
|
int indexCount;
|
|
|
|
pFile = (A2FileDOS*) GetNextFile(NULL);
|
|
while (pFile != NULL) {
|
|
DIError dierr;
|
|
dierr = pFile->LoadTSList(&tsList, &tsCount, &indexList, &indexCount);
|
|
if (dierr != kDIErrNone) {
|
|
LOGI("DOS failed loading TS list for '%s'",
|
|
pFile->GetPathName());
|
|
pFile->SetQuality(A2File::kQualityDamaged);
|
|
} else {
|
|
MarkFileUsage(pFile, tsList, tsCount, indexList, indexCount);
|
|
dierr = ComputeLength(pFile, tsList, tsCount);
|
|
if (dierr != kDIErrNone) {
|
|
LOGI("DOS unable to get length for '%s'",
|
|
pFile->GetPathName());
|
|
pFile->SetQuality(A2File::kQualityDamaged);
|
|
}
|
|
}
|
|
|
|
if (pFile->fLengthInSectors != indexCount + tsCount) {
|
|
LOGI("DOS NOTE: file '%s' has len-in-sect=%d but actual=%d",
|
|
pFile->GetPathName(), pFile->fLengthInSectors,
|
|
indexCount + tsCount);
|
|
// expected on sparse random-access text files
|
|
}
|
|
|
|
delete[] tsList;
|
|
delete[] indexList;
|
|
tsList = indexList = NULL;
|
|
|
|
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;
|
|
uint8_t sctBuf[kSctSize];
|
|
|
|
assert(pFile != NULL);
|
|
assert(tsList != NULL);
|
|
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 */
|
|
//LOGI(" DOS reading first file sector");
|
|
dierr = fpImg->ReadTrackSector(tsList[0].track, tsList[0].sector,
|
|
sctBuf);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
if (pFile->fFileType == A2FileDOS::kTypeBinary) {
|
|
pFile->fAuxType =
|
|
sctBuf[0x00] | (uint16_t) sctBuf[0x01] << 8;
|
|
pFile->fLength =
|
|
sctBuf[0x02] | (uint16_t) sctBuf[0x03] << 8;
|
|
pFile->fDataOffset = 4; // take the above into account
|
|
} else {
|
|
pFile->fLength =
|
|
sctBuf[0x00] | (uint16_t) 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, '<') != NULL &&
|
|
strchr(pFile->fFileName, '>') != NULL)
|
|
{
|
|
LOGI(" DOS found probable DDD archive, tweaking '%s' (lis=%u)",
|
|
pFile->GetPathName(), pFile->fLengthInSectors);
|
|
//dierr = TrimLastSectorDown(pFile, tsBuf, WrapperDDD::kMaxDDDZeroCount);
|
|
//if (dierr != kDIErrNone)
|
|
// goto bail;
|
|
//LOGI(" DOS scanned DDD file '%s' to length %ld (tsCount=%d)",
|
|
// 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) {
|
|
LOGI(" DOS33 capping max len from %ld to %d in '%s'",
|
|
(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;
|
|
|
|
LOGI(" DOS scanned text file '%s' down to %d+%ld = %ld",
|
|
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;
|
|
uint8_t sctBuf[kSctSize];
|
|
int i;
|
|
|
|
if (lastTS.track == 0) {
|
|
/* happens on files with lots of "sparse" space at the end */
|
|
return kDIErrNone;
|
|
}
|
|
|
|
//LOGI(" DOS reading LAST file sector");
|
|
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, uint16_t* tsBuf,
|
|
int maxZeroCount)
|
|
{
|
|
DIError dierr;
|
|
uint8_t sctBuf[kSctSize];
|
|
int i;
|
|
|
|
//LOGI(" DOS reading LAST file sector");
|
|
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(uint8_t* 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;
|
|
uint8_t sctBuf[256];
|
|
bool addDOS = false;
|
|
|
|
if (pDiskImg->GetNumTracks() < kMinTracks ||
|
|
pDiskImg->GetNumTracks() > kMaxTracks)
|
|
{
|
|
LOGI(" DOS33 can't format numTracks=%ld", pDiskImg->GetNumTracks());
|
|
return kDIErrInvalidArg;
|
|
}
|
|
if (pDiskImg->GetNumSectPerTrack() != 13 &&
|
|
pDiskImg->GetNumSectPerTrack() != 16 &&
|
|
pDiskImg->GetNumSectPerTrack() != 32)
|
|
{
|
|
LOGI(" DOS33 can't format sectors=%d",
|
|
pDiskImg->GetNumSectPerTrack());
|
|
return kDIErrInvalidArg;
|
|
}
|
|
|
|
if (volName != NULL && strcmp(volName, "DOS") == 0) {
|
|
if (pDiskImg->GetNumSectPerTrack() != 16 &&
|
|
pDiskImg->GetNumSectPerTrack() != 13)
|
|
{
|
|
LOGI("NOTE: numSectPerTrack = %d, can't write DOS tracks",
|
|
pDiskImg->GetNumSectPerTrack());
|
|
return kDIErrInvalidArg;
|
|
}
|
|
addDOS = true;
|
|
}
|
|
|
|
/* set fpImg so calls that rely on it will work; we un-set it later */
|
|
assert(fpImg == NULL);
|
|
SetDiskImg(pDiskImg);
|
|
|
|
LOGI(" DOS33 formatting disk image (sectorOrder=%d)",
|
|
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();
|
|
LOGI(" DOS33 (not zeroing blocks)");
|
|
|
|
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] = (uint8_t) fpImg->GetDOSVolumeNum();
|
|
fVTOC[0x27] = 122; // max T/S pairs
|
|
fVTOC[0x30] = kVTOCTrack+1; // last alloc
|
|
fVTOC[0x31] = 1; // ascending
|
|
fVTOC[0x34] = (uint8_t)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) {
|
|
LOGI(" GLITCH: couldn't read header we just wrote (err=%d)", dierr);
|
|
goto bail;
|
|
}
|
|
|
|
/* don't do this -- assume they're going to call Initialize() later */
|
|
//ScanVolBitmap();
|
|
|
|
bail:
|
|
SetDiskImg(NULL); // shouldn't really be set by us
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* 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 uint8_t* buf = gDOS33Tracks;
|
|
|
|
if (sectPerTrack == 13) {
|
|
LOGI(" DOS33 writing DOS 3.3 tracks");
|
|
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) {
|
|
LOGI(" DOS33 writing DOS 3.3 tracks");
|
|
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 {
|
|
LOGI(" DOS33 *not* writing DOS tracks to %d-sector disk",
|
|
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;
|
|
const char* cp;
|
|
|
|
/* throw out leading pathname, if any */
|
|
if (fssep != '\0') {
|
|
cp = strrchr(name, fssep);
|
|
if (cp != NULL)
|
|
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 = NULL;
|
|
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (!fDiskIsGood)
|
|
return kDIErrBadDiskImage;
|
|
|
|
assert(pParms != NULL);
|
|
assert(pParms->pathName != NULL);
|
|
assert(pParms->storageType == A2FileProDOS::kStorageSeedling);
|
|
LOGI(" DOS33 ---v--- CreateFile '%s'", pParms->pathName);
|
|
|
|
*ppNewFile = NULL;
|
|
|
|
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) != NULL) {
|
|
LOGI(" DOS33 create: normalized name '%s' already exists",
|
|
normalName);
|
|
dierr = kDIErrFileExists;
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
fileType = A2FileDOS::ConvertFileType(pParms->fileType, 0);
|
|
|
|
/*
|
|
* Allocate a directory entry and T/S list.
|
|
*/
|
|
uint8_t 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) {
|
|
LOGI("DOS unable to find an empty entry in the catalog");
|
|
goto bail;
|
|
}
|
|
LOGI(" DOS found free catalog entry T=%d S=%d ent=%d prev=0x%08lx",
|
|
catSect.track, catSect.sector, catEntry, (long) pPrevEntry);
|
|
|
|
/* create the new dir entry at the specified location */
|
|
CreateDirEntry(sctBuf, catEntry, normalName, &tsSect,
|
|
(uint8_t) 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 == NULL) {
|
|
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 = (uint16_t) 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 = NULL;
|
|
|
|
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 != NULL);
|
|
assert(strlen(fileName) <= A2FileDOS::kMaxFileName);
|
|
|
|
if (GetFileByName(fileName) == NULL)
|
|
return kDIErrNone;
|
|
|
|
LOGI(" DOS found duplicate of '%s', making unique", fileName);
|
|
|
|
int nameLen = strlen(fileName);
|
|
int dotOffset=0, dotLen=0;
|
|
char dotBuf[kMaxExtensionLen+1];
|
|
|
|
/* ensure the result will be null-terminated */
|
|
memset(fileName + nameLen, 0, (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 != NULL) {
|
|
int tmpOffset = cp - fileName;
|
|
if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) {
|
|
LOGI(" DOS (keeping extension '%s')", cp);
|
|
assert(strlen(cp) <= kMaxExtensionLen);
|
|
strcpy(dotBuf, cp);
|
|
dotOffset = tmpOffset;
|
|
dotLen = nameLen - dotOffset;
|
|
}
|
|
}
|
|
|
|
const int kMaxDigits = 999;
|
|
int digits = 0;
|
|
int digitLen;
|
|
int copyOffset;
|
|
char digitBuf[4];
|
|
do {
|
|
if (digits == kMaxDigits)
|
|
return kDIErrFileExists;
|
|
digits++;
|
|
|
|
/* not the most efficient way to do this, but it'll do */
|
|
sprintf(digitBuf, "%d", digits);
|
|
digitLen = strlen(digitBuf);
|
|
if (nameLen + digitLen > 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) != NULL);
|
|
|
|
LOGI(" DOS converted to unique name: %s", 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,
|
|
uint8_t* sctBuf, A2FileDOS** ppPrevEntry)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
uint8_t* 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 */
|
|
LOGI("DOS catalog is full");
|
|
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 = NULL;
|
|
if (prevEntry >= 0) {
|
|
A2FileDOS* pFile = (A2FileDOS*) GetNextFile(NULL);
|
|
while (pFile != NULL) {
|
|
if (pFile->fCatTS.track == prevTS.track &&
|
|
pFile->fCatTS.sector == prevTS.sector &&
|
|
pFile->fCatEntryNum == prevEntry)
|
|
{
|
|
*ppPrevEntry = pFile;
|
|
break;
|
|
}
|
|
pFile = (A2FileDOS*) GetNextFile(pFile);
|
|
}
|
|
assert(*ppPrevEntry != NULL);
|
|
}
|
|
}
|
|
|
|
bail:
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Fill out the catalog entry in the location specified.
|
|
*/
|
|
void DiskFSDOS33::CreateDirEntry(uint8_t* sctBuf, int catEntry,
|
|
const char* fileName, TrackSector* pTSSect, uint8_t fileType,
|
|
int access)
|
|
{
|
|
char highName[A2FileDOS::kMaxFileName+1];
|
|
uint8_t* 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] |= (uint8_t) 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 = NULL;
|
|
TrackSector* indexList = NULL;
|
|
int tsCount, indexCount;
|
|
uint8_t sctBuf[kSctSize];
|
|
uint8_t* pEntry;
|
|
|
|
if (pGenericFile == NULL) {
|
|
assert(false);
|
|
return kDIErrInvalidArg;
|
|
}
|
|
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (!fDiskIsGood)
|
|
return kDIErrBadDiskImage;
|
|
if (pGenericFile->IsFileOpen())
|
|
return kDIErrFileOpen;
|
|
|
|
LOGI(" Deleting '%s'", 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) {
|
|
LOGI("Failed loading TS lists while deleting '%s'",
|
|
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)) {
|
|
LOGI("WARNING: freeing unallocated sector T=%d S=%d",
|
|
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];
|
|
uint8_t sctBuf[kSctSize];
|
|
uint8_t* pEntry;
|
|
|
|
if (pFile == NULL || newName == NULL)
|
|
return kDIErrInvalidArg;
|
|
if (!IsValidFileName(newName))
|
|
return kDIErrInvalidArg;
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (!fDiskIsGood)
|
|
return kDIErrBadDiskImage;
|
|
|
|
LOGI(" DOS renaming '%s' to '%s'", 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((uint8_t*)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, uint32_t fileType,
|
|
uint32_t auxType, uint32_t accessFlags)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
A2FileDOS* pFile = (A2FileDOS*) pGenericFile;
|
|
TrackSector* tsList = NULL;
|
|
int tsCount;
|
|
bool nowLocked;
|
|
bool typeChanged;
|
|
|
|
if (pFile == NULL)
|
|
return kDIErrInvalidArg;
|
|
if (fpImg->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
|
|
LOGI("DOS SetFileInfo '%s' type=0x%02x aux=0x%04x access=0x%02x",
|
|
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)) {
|
|
LOGI("DOS SetFileInfo invalid file type");
|
|
dierr = kDIErrInvalidArg;
|
|
goto bail;
|
|
}
|
|
if (auxType != pFile->GetAuxType() && fileType != 0x06) {
|
|
/* this only makes sense for BIN files */
|
|
LOGI("DOS SetFileInfo aux type mismatch; ignoring");
|
|
//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;
|
|
uint8_t sctBuf[kSctSize];
|
|
uint8_t* pEntry;
|
|
|
|
LOGI("Updating file '%s'", 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] = (uint8_t) 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.
|
|
*/
|
|
uint16_t newAuxType;
|
|
newAuxType = (uint16_t) auxType;
|
|
|
|
dierr = pFile->LoadTSList(&tsList, &tsCount);
|
|
if (dierr != kDIErrNone) {
|
|
LOGI(" DOS SFI: unable to load TS list (err=%d)", dierr);
|
|
goto bail;
|
|
}
|
|
|
|
if (fileType == 0x06 && tsCount > 0) {
|
|
uint8_t sctBuf[kSctSize];
|
|
|
|
dierr = fpImg->ReadTrackSector(tsList[0].track,
|
|
tsList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
LOGI("DOS SFI: unable to get first sector of file");
|
|
goto bail;
|
|
}
|
|
|
|
if (auxType == pFile->GetAuxType()) {
|
|
newAuxType = GetShortLE(&sctBuf[0x00]);
|
|
LOGI(" Aux type not changed, extracting from file (0x%04x)",
|
|
newAuxType);
|
|
} else {
|
|
LOGI(" Aux type changed (to 0x%04x), changing file",
|
|
newAuxType);
|
|
|
|
PutShortLE(&sctBuf[0x00], newAuxType);
|
|
dierr = fpImg->WriteTrackSector(tsList[0].track,
|
|
tsList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
LOGI("DOS SFI: unable to write first sector of file");
|
|
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;
|
|
uint8_t 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] = (uint8_t) 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 = NULL;
|
|
}
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
uint32_t 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))
|
|
{
|
|
LOGI(" DOS setting type for large A/I/B file to S");
|
|
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".
|
|
*/
|
|
uint32_t 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((uint8_t*)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 = NULL;
|
|
|
|
if (!readOnly) {
|
|
if (fpDiskFS->GetDiskImg()->GetReadOnly())
|
|
return kDIErrAccessDenied;
|
|
if (fpDiskFS->GetFSDamaged())
|
|
return kDIErrBadDiskImage;
|
|
}
|
|
|
|
if (fpOpenFile != NULL) {
|
|
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) {
|
|
LOGI("DOS33 unable to load TS for '%s' open", 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 = NULL;
|
|
|
|
bail:
|
|
delete pOpenFile;
|
|
return dierr;
|
|
}
|
|
|
|
/*
|
|
* Dump the contents of an A2FileDOS.
|
|
*/
|
|
void A2FileDOS::Dump(void) const
|
|
{
|
|
LOGI("A2FileDOS '%s'", fFileName);
|
|
LOGI(" TS T=%-2d S=%-2d", fTSListTrack, fTSListSector);
|
|
LOGI(" Cat T=%-2d S=%-2d", fCatTS.track, fCatTS.sector);
|
|
LOGI(" type=%d lck=%d slen=%d", fFileType, fLocked, fLengthInSectors);
|
|
LOGI(" auxtype=0x%04x length=%ld",
|
|
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-NULL, 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 = NULL;
|
|
TrackSector* indexList = NULL;
|
|
int tsCount, tsAlloc;
|
|
int indexCount, indexAlloc;
|
|
uint8_t sctBuf[kSctSize];
|
|
int track, sector, iterations;
|
|
|
|
LOGI("--- DOS loading T/S list for '%s'", 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 == NULL || indexList == NULL) {
|
|
dierr = kDIErrMalloc;
|
|
goto bail;
|
|
}
|
|
|
|
assert(fpDiskFS != NULL);
|
|
pDiskImg = fpDiskFS->GetDiskImg();
|
|
assert(pDiskImg != NULL);
|
|
|
|
/* get the first T/S sector for this file */
|
|
track = fTSListTrack;
|
|
sector = fTSListSector;
|
|
if (track >= pDiskImg->GetNumTracks() ||
|
|
sector >= pDiskImg->GetNumSectPerTrack())
|
|
{
|
|
LOGI(" DOS33 invalid initial T/S %d,%d in '%s'", track, sector,
|
|
fFileName);
|
|
dierr = kDIErrBadFile;
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Run through the set of t/s pairs.
|
|
*/
|
|
iterations = 0;
|
|
do {
|
|
uint16_t sectorOffset;
|
|
int lastNonZero;
|
|
|
|
/*
|
|
* Add the current T/S sector to the index list.
|
|
*/
|
|
if (indexCount == indexAlloc) {
|
|
LOGI("+++ expanding index list");
|
|
TrackSector* newList;
|
|
indexAlloc += kDefaultIndexAlloc;
|
|
newList = new TrackSector[indexAlloc];
|
|
if (newList == NULL) {
|
|
dierr = kDIErrMalloc;
|
|
goto bail;
|
|
}
|
|
memcpy(newList, indexList, indexCount * sizeof(TrackSector));
|
|
delete[] indexList;
|
|
indexList = newList;
|
|
}
|
|
indexList[indexCount].track = track;
|
|
indexList[indexCount].sector = sector;
|
|
indexCount++;
|
|
|
|
|
|
//LOGI("+++ scanning T/S at T=%d S=%d", 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
|
|
LOGI(" DOS33 invalid T/S link %d,%d in '%s'", track, sector,
|
|
GetPathName());
|
|
dierr = kDIErrBadFile;
|
|
goto bail;
|
|
}
|
|
if ((sectorOffset % kMaxTSPairs) != 0) {
|
|
LOGI(" DOS33 invalid T/S header sector offset %u in '%s'",
|
|
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) {
|
|
LOGI("+++ expanding ts list");
|
|
TrackSector* newList;
|
|
tsAlloc += kMaxTSPairs * kDefaultTSAlloc;
|
|
newList = new TrackSector[tsAlloc];
|
|
if (newList == NULL) {
|
|
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 */
|
|
//LOGI(" DOS33 odd -- last T/S sector of '%s' was empty",
|
|
// 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 = NULL;
|
|
|
|
if (pIndexList != NULL) {
|
|
*pIndexList = indexList;
|
|
*pIndexCount = indexCount;
|
|
indexList = NULL;
|
|
}
|
|
|
|
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 uint8_t* sctBuf, TrackSector* tsList,
|
|
int* pLastNonZero)
|
|
{
|
|
DIError dierr = kDIErrNone;
|
|
const DiskImg* pDiskImg = fpDiskFS->GetDiskImg();
|
|
const uint8_t* 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)))
|
|
{
|
|
LOGI(" DOS33 invalid T/S %d,%d in '%s'", track, sector,
|
|
fFileName);
|
|
|
|
if (i > 0 && tsList[i-1].track == 0 && tsList[i-1].sector == 0) {
|
|
LOGI(" T/S list looks partially valid");
|
|
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)
|
|
{
|
|
LOGD(" DOS reading %lu bytes from '%s' (offset=%ld)",
|
|
(unsigned long) 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 == NULL)
|
|
return kDIErrDataUnderrun;
|
|
len = (size_t) (fOpenEOF - fOffset);
|
|
}
|
|
if (pActual != NULL)
|
|
*pActual = len;
|
|
long incrLen = len;
|
|
|
|
DIError dierr = kDIErrNone;
|
|
uint8_t 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);
|
|
LOGI(" DOS ran off the end (fTSCount=%d)", fTSCount);
|
|
return kDIErrDataUnderrun;
|
|
}
|
|
|
|
if (fTSList[tsIndex].track == 0 && fTSList[tsIndex].sector == 0) {
|
|
//LOGI(" DOS sparse sector T=%d S=%d",
|
|
// 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) {
|
|
LOGI(" DOS error reading file '%s'", 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();
|
|
uint8_t sctBuf[kSctSize];
|
|
|
|
LOGD(" DOS Write len=%lu %s", (unsigned long) 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 != NULL);
|
|
|
|
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 = NULL;
|
|
|
|
fTSCount = numSectors;
|
|
fTSList = new TrackSector[fTSCount];
|
|
fIndexCount = (numSectors + kMaxTSPairs -1) / kMaxTSPairs;
|
|
assert(fIndexCount > 0);
|
|
fIndexList = new TrackSector[fIndexCount];
|
|
if (fTSList == NULL || fIndexList == NULL) {
|
|
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 uint8_t* curPtr;
|
|
int sectorIdx;
|
|
|
|
curPtr = (const uint8_t*) 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;
|
|
uint8_t sctBuf[kSctSize];
|
|
uint8_t* 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) {
|
|
LOGW("WARNING: DOS Close trimming A/I/B file from %ld to 65535",
|
|
(long) fOpenEOF);
|
|
fOpenEOF = 65535;
|
|
}
|
|
dierr = pDiskFS->GetDiskImg()->ReadTrackSector(fTSList[0].track,
|
|
fTSList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
LOGW("DOS Close: unable to get first sector of file");
|
|
goto bail;
|
|
}
|
|
|
|
if (pFile->fFileType == A2FileDOS::kTypeInteger ||
|
|
pFile->fFileType == A2FileDOS::kTypeApplesoft)
|
|
{
|
|
PutShortLE(&sctBuf[0x00], (uint16_t) fOpenEOF);
|
|
} else {
|
|
PutShortLE(&sctBuf[0x00], pFile->fAuxType);
|
|
PutShortLE(&sctBuf[0x02], (uint16_t) fOpenEOF);
|
|
}
|
|
|
|
dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[0].track,
|
|
fTSList[0].sector, sctBuf);
|
|
if (dierr != kDIErrNone) {
|
|
LOGW("DOS Close: unable to write first sector of file");
|
|
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 = (uint16_t) 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;
|
|
LOGI(" DOS T/S list for '%s' (count=%d)",
|
|
((A2FileDOS*)fpFile)->fFileName, fTSCount);
|
|
|
|
int i;
|
|
for (i = 0; i <= fTSCount; i++) {
|
|
LOGI(" %3d: T=%-2d S=%d", i, fTSList[i].track, fTSList[i].sector);
|
|
}
|
|
}
|