ciderpress/diskimg/Gutenberg.cpp

642 lines
17 KiB
C++

/*
* CiderPress
* Copyright (C) 2009 by CiderPress authors. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Implementation of DiskFSGutenberg and A2FileGutenberg classes.
*
*/
#include "StdAfx.h"
#include "DiskImgPriv.h"
/*
* ===========================================================================
* DiskFSGutenberg
* ===========================================================================
*/
const int kMaxSectors = 32;
const int kMaxVolNameLen = 9;
const int kSctSize = 256;
const int kVTOCTrack = 17;
const int kVTOCSector = 7;
const int kCatalogEntryOffset = 0x20; // first entry in cat sect starts here
const int kCatalogEntrySize = 16; // length in bytes of catalog entries
const int kCatalogEntriesPerSect = 15; // #of entries per catalog sector
const int kEntryDeleted = 0x40; // this is used to designate 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 Gutenberg-ness.
*
*/
static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder,
int* pGoodCount)
{
DIError dierr = kDIErrNone;
uint8_t sctBuf[kSctSize];
// int numTracks, numSectors;
int catTrack = kVTOCTrack;
int catSect = kVTOCSector;
int foundGood = 0;
int iterations = 0;
*pGoodCount = 0;
/*
* Walk through the catalog track to try to figure out ordering.
*/
while (iterations < DiskFSGutenberg::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[0] && catSect == sctBuf[1]) {
foundGood++;
if (sctBuf[0x0f] == 0x8d && sctBuf[0x1f] == 0x8d &&
sctBuf[0x2f] == 0x8d && sctBuf[0x3f] == 0x8d &&
sctBuf[0x4f] == 0x8d && sctBuf[0x5f] == 0x8d &&
sctBuf[0x6f] == 0x8d && sctBuf[0x7f] == 0x8d &&
sctBuf[0x8f] == 0x8d && sctBuf[0x9f] == 0x8d)
foundGood++;
}
else if (catTrack >0x80) {
LOGI(" Gutenberg detected end-of-catalog on cat (%d,%d)",
catTrack, catSect);
break;
}
catTrack = sctBuf[0x04];
catSect = sctBuf[0x05];
iterations++; // watch for infinite loops
}
if (iterations >= DiskFSGutenberg::kMaxCatalogSectors) {
/* possible cause: LF->CR conversion screws up link to sector $0a */
dierr = kDIErrDirectoryLoop;
LOGI(" Gutenberg directory links cause a loop (order=%d)", imageOrder);
goto bail;
}
LOGI(" Gutenberg foundGood=%d order=%d", foundGood, imageOrder);
*pGoodCount = foundGood;
bail:
return dierr;
}
/*
* Test to see if the image is a Gutenberg word processor data disk.
*/
/*static*/ DIError DiskFSGutenberg::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 >= 2 ||
(leniency == kLeniencyVery && bestCount >= 1))
{
LOGI(" Gutenberg test: bestCount=%d for order=%d", bestCount, bestOrder);
assert(bestOrder != DiskImg::kSectorOrderUnknown);
*pOrder = bestOrder;
*pFormat = DiskImg::kFormatGutenberg;
return kDIErrNone;
}
LOGI(" Gutenberg didn't find a valid filesystem.");
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 DiskFSGutenberg::Initialize(InitMode initMode)
{
DIError dierr = kDIErrNone;
fVolumeUsage.Create(fpImg->GetNumTracks(), fpImg->GetNumSectPerTrack());
/* 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;
sprintf(fDiskVolumeID, "Gutenberg: %s", fDiskVolumeName);
fDiskIsGood = CheckDiskIsGood();
fVolumeUsage.Dump();
bail:
return dierr;
}
/*
* Get the amount of free space remaining.
*/
DIError DiskFSGutenberg::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits,
int* pUnitSize) const
{
*pTotalUnits = fpImg->GetNumTracks() * fpImg->GetNumSectPerTrack();
*pFreeUnits = 0;
*pUnitSize = kSectorSize;
return kDIErrNone;
}
/*
* Read the disk's catalog.
*/
DIError DiskFSGutenberg::ReadCatalog(void)
{
DIError dierr = kDIErrNone;
uint8_t sctBuf[kSctSize];
int catTrack, catSect;
int iterations;
catTrack = 17;
catSect = 7;
iterations = 0;
memset(fCatalogSectors, 0, sizeof(fCatalogSectors));
while (catTrack < 35 && catSect < 16 && iterations < kMaxCatalogSectors)
{
LOGI(" Gutenberg reading catalog sector T=%d S=%d", catTrack, catSect);
dierr = fpImg->ReadTrackSector(catTrack, catSect, sctBuf);
if (dierr != kDIErrNone)
goto bail;
memcpy(fDiskVolumeName, &sctBuf[6], kMaxVolNameLen); // Copy out the volume name; it should be the same on all catalog sectors.
fDiskVolumeName[kMaxVolNameLen] = 0x00;
DiskFSGutenberg::LowerASCII((uint8_t*)fDiskVolumeName, kMaxVolNameLen);
A2FileGutenberg::TrimTrailingSpaces(fDiskVolumeName);
dierr = ProcessCatalogSector(catTrack, catSect, sctBuf);
if (dierr != kDIErrNone)
goto bail;
fCatalogSectors[iterations].track = catTrack;
fCatalogSectors[iterations].sector = catSect;
catTrack = sctBuf[0x04];
catSect = sctBuf[0x05];
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 DiskFSGutenberg::ProcessCatalogSector(int catTrack, int catSect,
const uint8_t* sctBuf)
{
A2FileGutenberg* pFile;
const uint8_t* pEntry;
int i;
pEntry = &sctBuf[kCatalogEntryOffset];
for (i = 0; i < kCatalogEntriesPerSect; i++) {
if (pEntry[0x0d] != kEntryDeleted && pEntry[0x00] != 0xa0 && pEntry[0x00] != 0x00) {
pFile = new A2FileGutenberg(this);
pFile->SetQuality(A2File::kQualityGood);
pFile->fTrack = pEntry[0x0c];
pFile->fSector = pEntry[0x0d];
memcpy(pFile->fFileName, &pEntry[0x00], A2FileGutenberg::kMaxFileName);
pFile->fFileName[A2FileGutenberg::kMaxFileName] = '\0';
pFile->FixFilename();
//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;
pFile->fLengthInSectors = 0;
pFile->fLengthInSectors = 0;
AddFileToList(pFile);
}
//if (pEntry[0x00] == 0xa0)
// break;
pEntry += kCatalogEntrySize;
}
return kDIErrNone;
}
/*
* Perform consistency checks on the filesystem.
*
* Returns "true" if disk appears to be perfect, "false" otherwise.
*/
bool DiskFSGutenberg::CheckDiskIsGood(void)
{
bool result = true;
return result;
}
/*
* Run through our list of files, computing the lengths and marking file
* usage in the VolumeUsage object.
*/
DIError DiskFSGutenberg::GetFileLengths(void)
{
A2FileGutenberg* pFile;
uint8_t sctBuf[kSctSize];
int tsCount = 0;
uint16_t currentTrack, currentSector;
pFile = (A2FileGutenberg*) GetNextFile(NULL);
while (pFile != NULL) {
DIError dierr;
tsCount = 0;
currentTrack = pFile->fTrack;
currentSector = pFile->fSector;
while (currentTrack < 0x80) {
tsCount ++;
dierr = fpImg->ReadTrackSector(currentTrack, currentSector, sctBuf);
if (dierr != kDIErrNone) {
LOGI("Gutenberg failed loading track/sector for '%s'",
pFile->GetPathName());
goto bail;
}
currentTrack = sctBuf[0x04];
currentSector = sctBuf[0x05];
}
pFile->fLengthInSectors = tsCount;
pFile->fLength = tsCount * 250; // First six bytes of sector are t/s pointers
pFile = (A2FileGutenberg*) GetNextFile(pFile);
}
bail:
return kDIErrNone;
}
/*
* 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 DiskFSGutenberg::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++;
}
}
/*
* ===========================================================================
* A2FileGutenberg
* ===========================================================================
*/
/*
* Constructor.
*/
A2FileGutenberg::A2FileGutenberg(DiskFS* pDiskFS) : A2File(pDiskFS)
{
fTrack = -1;
fSector = -1;
fLengthInSectors = 0;
fLocked = true;
fFileName[0] = '\0';
fFileType = kTypeText;
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".
*/
A2FileGutenberg::~A2FileGutenberg(void)
{
delete fpOpenFile;
}
/*
* Convert the filetype enum to a ProDOS type.
*
*/
long A2FileGutenberg::GetFileType(void) const
{
return 0x04; // TXT;
}
/*
* "Fix" a filename. Convert DOS-ASCII to normal ASCII, and strip
* trailing spaces.
*/
void A2FileGutenberg::FixFilename(void)
{
DiskFSGutenberg::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 A2FileGutenberg::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 A2FileGutenberg::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 A2FileGutenberg::Open(A2FileDescr** ppOpenFile, bool readOnly,
bool rsrcFork /*=false*/)
{
DIError dierr = kDIErrNone;
A2FDGutenberg* 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 A2FDGutenberg(this);
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 A2FileGutenberg.
*/
void A2FileGutenberg::Dump(void) const
{
LOGI("A2FileGutenberg '%s'", fFileName);
LOGI(" TS T=%-2d S=%-2d", fTrack, fSector);
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);
}
/*
* ===========================================================================
* A2FDGutenberg
* ===========================================================================
*/
/*
* Read data from the current offset.
*
*/
DIError A2FDGutenberg::Read(void* buf, size_t len, size_t* pActual)
{
LOGD(" Gutenberg reading %lu bytes from '%s' (offset=%ld)",
(unsigned long) len, fpFile->GetPathName(), (long) fOffset);
A2FileGutenberg* pFile = (A2FileGutenberg*) fpFile;
DIError dierr = kDIErrNone;
uint8_t sctBuf[kSctSize];
short currentTrack, currentSector;
//di_off_t actualOffset = fOffset + pFile->fDataOffset; // adjust for embedded len
int bufOffset = 6;
size_t thisCount;
if (len == 0)
return kDIErrNone;
assert(fOpenEOF != 0);
currentTrack = pFile->fTrack;
currentSector = pFile->fSector;
/* could be more clever in here and avoid double-buffering */
while (len) {
dierr = pFile->GetDiskFS()->GetDiskImg()->ReadTrackSector(
currentTrack,
currentSector,
sctBuf);
if (dierr != kDIErrNone) {
LOGI(" Gutenberg 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;
currentTrack = sctBuf[0x04];
currentSector = sctBuf[0x05];
}
return dierr;
}
/*
* Writing Gutenberg files isn't supported.
*/
DIError A2FDGutenberg::Write(const void* buf, size_t len, size_t* pActual)
{
return kDIErrNotSupported;
}
/*
* Seek to the specified offset.
*/
DIError A2FDGutenberg::Seek(di_off_t offset, DIWhence whence)
{
return kDIErrNotSupported;
}
/*
* Return current offset.
*/
di_off_t A2FDGutenberg::Tell(void)
{
return kDIErrNotSupported;
}
/*
* 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 A2FDGutenberg::Close(void)
{
DIError dierr = kDIErrNone;
fpFile->CloseDescr(this);
return dierr;
}
/*
* Return the #of sectors/blocks in the file.
*/
long A2FDGutenberg::GetSectorCount(void) const
{
return fTSCount;
}
long A2FDGutenberg::GetBlockCount(void) const
{
return (fTSCount+1)/2;
}
/*
* Return the Nth track/sector in this file.
*
* Returns (0,0) for a sparse sector.
*/
DIError A2FDGutenberg::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
{
return kDIErrInvalidIndex;
}
/*
* Unimplemented
*/
DIError A2FDGutenberg::GetStorage(long blockIdx, long* pBlock) const
{
return kDIErrInvalidIndex;
}