From abd3515424ad8da8b6efe07788f42f6ffcba96f9 Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Sun, 4 Jan 2009 22:23:04 +0000 Subject: [PATCH] Adding read capability for Gutenberg word processor formatted disks --- app/DiskArchive.cpp | 6312 +++++++++++++++++------------------ diskimg/DiskImg.cpp | 7040 ++++++++++++++++++++------------------- diskimg/DiskImg.h | 3321 +++++++++--------- diskimg/DiskImgDetail.h | 6187 +++++++++++++++++----------------- diskimg/Gutenberg.cpp | 671 ++++ diskimg/diskimg.dsp | 592 ++-- 6 files changed, 12533 insertions(+), 11590 deletions(-) create mode 100644 diskimg/Gutenberg.cpp diff --git a/app/DiskArchive.cpp b/app/DiskArchive.cpp index c3cbf93..017b290 100644 --- a/app/DiskArchive.cpp +++ b/app/DiskArchive.cpp @@ -1,3155 +1,3157 @@ -/* - * CiderPress - * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. - * See the file LICENSE for distribution terms. - */ -/* - * Bridge between DiskImg and GenericArchive. - */ -#include "stdafx.h" -#include "DiskArchive.h" -#include "NufxArchive.h" -#include "Preferences.h" -#include "Main.h" -#include "ImageFormatDialog.h" -#include "RenameEntryDialog.h" -#include "ConfirmOverwriteDialog.h" -#include "../diskimg/DiskImgDetail.h" - -static const char* kEmptyFolderMarker = ".$$EmptyFolder"; - - -/* - * =========================================================================== - * DiskEntry - * =========================================================================== - */ - -/* - * Extract data from a disk image. - * - * If "*ppText" is non-nil, the data will be read into the pointed-to buffer - * so long as it's shorter than *pLength bytes. The value in "*pLength" - * will be set to the actual length used. - * - * If "*ppText" is nil, the uncompressed data will be placed into a buffer - * allocated with "new[]". - * - * Returns IDOK on success, IDCANCEL if the operation was cancelled by the - * user, and -1 value on failure. On failure, "*pErrMsg" holds an error - * message. - * - * "which" is an anonymous GenericArchive enum (e.g. "kDataThread"). - */ -int -DiskEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength, - CString* pErrMsg) const -{ - DIError dierr; - A2FileDescr* pOpenFile = nil; - char* dataBuf = nil; - bool rsrcFork; - bool needAlloc = true; - int result = -1; - - ASSERT(fpFile != nil); - ASSERT(pErrMsg != nil); - *pErrMsg = ""; - - if (*ppText != nil) - needAlloc = false; - - if (GetDamaged()) { - *pErrMsg = "File is damaged"; - goto bail; - } - - if (which == kDataThread) - rsrcFork = false; - else if (which == kRsrcThread) - rsrcFork = true; - else { - *pErrMsg = "No such fork"; - goto bail; - } - - LONGLONG len; - if (rsrcFork) - len = fpFile->GetRsrcLength(); - else - len = fpFile->GetDataLength(); - - if (len == 0) { - if (needAlloc) { - *ppText = new char[1]; - **ppText = '\0'; - } - *pLength = 0; - result = IDOK; - goto bail; - } else if (len < 0) { - assert(rsrcFork); // forked files always have a data fork - *pErrMsg = "That fork doesn't exist"; - goto bail; - } - - dierr = fpFile->Open(&pOpenFile, true, rsrcFork); - if (dierr != kDIErrNone) { - *pErrMsg = "File open failed"; - goto bail; - } - - SET_PROGRESS_BEGIN(); - pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, len, nil); - - if (needAlloc) { - dataBuf = new char[(int) len]; - if (dataBuf == nil) { - pErrMsg->Format("ERROR: allocation of %ld bytes failed", len); - goto bail; - } - } else { - if (*pLength < (long) len) { - pErrMsg->Format("ERROR: buf size %ld too short (%ld)", - *pLength, (long) len); - goto bail; - } - dataBuf = *ppText; - } - - dierr = pOpenFile->Read(dataBuf, (size_t) len); - if (dierr != kDIErrNone) { - if (dierr == kDIErrCancelled) { - result = IDCANCEL; - } else { - pErrMsg->Format("File read failed: %s", - DiskImgLib::DIStrError(dierr)); - } - goto bail; - } - - if (needAlloc) - *ppText = dataBuf; - *pLength = (long) len; - result = IDOK; - -bail: - if (pOpenFile != nil) - pOpenFile->Close(); - if (result == IDOK) { - SET_PROGRESS_END(); - ASSERT(pErrMsg->IsEmpty()); - } else { - ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty()); - if (needAlloc) { - delete[] dataBuf; - ASSERT(*ppText == nil); - } - } - return result; -} - -/* - * Extract data from a thread to a file. Since we're not copying to memory, - * we can't assume that we're able to hold the entire file all at once. - * - * Returns IDOK on success, IDCANCEL if the operation was cancelled by the - * user, and -1 value on failure. On failure, "*pMsg" holds an - * error message. - */ -int -DiskEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, - ConvertHighASCII convHA, CString* pErrMsg) const -{ - A2FileDescr* pOpenFile = nil; - bool rsrcFork; - int result = -1; - - ASSERT(IDOK != -1 && IDCANCEL != -1); - ASSERT(fpFile != nil); - - if (which == kDataThread) - rsrcFork = false; - else if (which == kRsrcThread) - rsrcFork = true; - else { - /* if we handle disk images, make sure we disable "conv" */ - *pErrMsg = "No such fork"; - goto bail; - } - - LONGLONG len; - if (rsrcFork) - len = fpFile->GetRsrcLength(); - else - len = fpFile->GetDataLength(); - - if (len == 0) { - WMSG0("Empty fork\n"); - result = IDOK; - goto bail; - } else if (len < 0) { - assert(rsrcFork); // forked files always have a data fork - *pErrMsg = "That fork doesn't exist"; - goto bail; - } - - DIError dierr; - dierr = fpFile->Open(&pOpenFile, true, rsrcFork); - if (dierr != kDIErrNone) { - *pErrMsg = "Unable to open file on disk image"; - goto bail; - } - - dierr = CopyData(pOpenFile, outfp, conv, convHA, pErrMsg); - if (dierr != kDIErrNone) { - if (pErrMsg->IsEmpty()) { - pErrMsg->Format("Failed while copying data: %s\n", - DiskImgLib::DIStrError(dierr)); - } - goto bail; - } - - result = IDOK; - -bail: - if (pOpenFile != nil) - pOpenFile->Close(); - return result; -} - -/* - * Copy data from the open A2File to outfp, possibly converting EOL along - * the way. - */ -DIError -DiskEntry::CopyData(A2FileDescr* pOpenFile, FILE* outfp, ConvertEOL conv, - ConvertHighASCII convHA, CString* pMsg) const -{ - DIError dierr = kDIErrNone; - const int kChunkSize = 16384; - char buf[kChunkSize]; - //bool firstChunk = true; - //EOLType sourceType; - bool lastCR = false; - LONGLONG srcLen, dataRem; - - /* get the length of the open file */ - dierr = pOpenFile->Seek(0, DiskImgLib::kSeekEnd); - if (dierr != kDIErrNone) - goto bail; - srcLen = pOpenFile->Tell(); - dierr = pOpenFile->Rewind(); - if (dierr != kDIErrNone) - goto bail; - ASSERT(srcLen > 0); // empty files should've been caught earlier - - SET_PROGRESS_BEGIN(); - pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, srcLen, nil); - - /* - * Loop until all data copied. - */ - dataRem = srcLen; - while (dataRem) { - int chunkLen; - - if (dataRem > kChunkSize) - chunkLen = kChunkSize; - else - chunkLen = (int) dataRem; - - /* read a chunk from the source file */ - dierr = pOpenFile->Read(buf, chunkLen); - if (dierr != kDIErrNone) { - pMsg->Format("File read failed: %s", - DiskImgLib::DIStrError(dierr)); - goto bail; - } - - /* write chunk to destination file */ - int err = GenericEntry::WriteConvert(outfp, buf, chunkLen, &conv, - &convHA, &lastCR); - if (err != 0) { - pMsg->Format("File write failed: %s", strerror(err)); - dierr = kDIErrGeneric; - goto bail; - } - - dataRem -= chunkLen; - //SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen)); - } - -bail: - pOpenFile->ClearProgressUpdater(); - SET_PROGRESS_END(); - return dierr; -} - - -/* - * Figure out whether or not we're allowed to change a file's type and - * aux type. - */ -bool -DiskEntry::GetFeatureFlag(Feature feature) const -{ - DiskImg::FSFormat format; - - format = fpFile->GetDiskFS()->GetDiskImg()->GetFSFormat(); - - switch (feature) { - case kFeatureCanChangeType: - { - //if (GetRecordKind() == kRecordKindVolumeDir) - // return false; - - switch (format) { - case DiskImg::kFormatProDOS: - case DiskImg::kFormatPascal: - case DiskImg::kFormatMacHFS: - case DiskImg::kFormatDOS32: - case DiskImg::kFormatDOS33: - return true; - default: - return false; - } - } - case kFeaturePascalTypes: - { - switch (format) { - case DiskImg::kFormatPascal: - return true; - default: - return false; - } - } - case kFeatureDOSTypes: - { - switch (format) { - case DiskImg::kFormatDOS32: - case DiskImg::kFormatDOS33: - return true; - default: - return false; - } - } - case kFeatureHFSTypes: - { - switch (format) { - case DiskImg::kFormatMacHFS: - return true; - default: - return false; - } - } - case kFeatureHasFullAccess: - { - switch (format) { - case DiskImg::kFormatProDOS: - return true; - default: - return false; - } - } - case kFeatureHasSimpleAccess: - { - switch (format) { - case DiskImg::kFormatDOS33: - case DiskImg::kFormatDOS32: - case DiskImg::kFormatCPM: - case DiskImg::kFormatMacHFS: - return true; - default: - return false; - } - } - case kFeatureHasInvisibleFlag: - { - switch(format) { - case DiskImg::kFormatProDOS: - case DiskImg::kFormatMacHFS: - return true; - default: - return false; - } - } - default: - WMSG1("Unexpected feature flag %d\n", feature); - assert(false); - return false; - } - - assert(false); - return false; -} - - -/* - * =========================================================================== - * DiskArchive - * =========================================================================== - */ - -/* - * Perform one-time initialization of the DiskLib library. - */ -/*static*/ CString -DiskArchive::AppInit(void) -{ - CString result(""); - DIError dierr; - long major, minor, bug; - - WMSG0("Initializing DiskImg library\n"); - - // set this before initializing, so we can see init debug msgs - DiskImgLib::Global::SetDebugMsgHandler(DebugMsgHandler); - - dierr = DiskImgLib::Global::AppInit(); - if (dierr != kDIErrNone) { - result.Format("DiskImg DLL failed to initialize: %s\n", - DiskImgLib::DIStrError(dierr)); - goto bail; - } - - DiskImgLib::Global::GetVersion(&major, &minor, &bug); - if (major != kDiskImgVersionMajor || minor < kDiskImgVersionMinor) { - result.Format("Older or incompatible version of DiskImg DLL found.\r\r" - "Wanted v%d.%d.x, found %ld.%ld.%ld.", - kDiskImgVersionMajor, kDiskImgVersionMinor, - major, minor, bug); - goto bail; - } - -bail: - return result; -} - -/* - * Perform one-time cleanup of DiskImgLib at shutdown time. - */ -/*static*/ void -DiskArchive::AppCleanup(void) -{ - DiskImgLib::Global::AppCleanup(); -} - - -/* - * Handle a debug message from the DiskImg library. - */ -/*static*/ void -DiskArchive::DebugMsgHandler(const char* file, int line, const char* msg) -{ - ASSERT(file != nil); - ASSERT(msg != nil); - -#if defined(_DEBUG_LOG) - //fprintf(gLog, "%s(%d) : %s", file, line, msg); - fprintf(gLog, "%05u %s", gPid, msg); -#elif defined(_DEBUG) - _CrtDbgReport(_CRT_WARN, file, line, NULL, "%s", msg); -#else - /* do nothing */ -#endif -} - -/* - * Progress update callback, called from DiskImgLib during read/write - * operations. - * - * Returns "true" if we should continue; - */ -/*static*/ bool -DiskArchive::ProgressCallback(DiskImgLib::A2FileDescr* pFile, - DiskImgLib::di_off_t max, DiskImgLib::di_off_t current, void* state) -{ - int status; - - //::Sleep(10); - status = SET_PROGRESS_UPDATE(ComputePercent(current, max)); - if (status == IDCANCEL) { - WMSG0("IDCANCEL returned from Main progress updater\n"); - return false; - } - - return true; // tell DiskImgLib to continue what it's doing -} - -/* - * Progress update callback, called from DiskImgLib while scanning a volume - * during Open(). - * - * Returns "true" if we should continue; - */ -/*static*/ bool -DiskArchive::ScanProgressCallback(void* cookie, const char* str, int count) -{ - CString fmt; - bool cont; - - if (count == 0) - fmt = str; - else - fmt.Format("%s (%%d)", str); - cont = SET_PROGRESS_COUNTER_2(fmt, count); - - if (!cont) { - WMSG0("cancelled\n"); - } - - return cont; -} - - -/* - * Finish instantiating a DiskArchive object by opening an existing file. - */ -GenericArchive::OpenResult -DiskArchive::Open(const char* filename, bool readOnly, CString* pErrMsg) -{ - DIError dierr; - CString errMsg; - OpenResult result = kResultUnknown; - const Preferences* pPreferences = GET_PREFERENCES(); - - ASSERT(fpPrimaryDiskFS == nil); - ASSERT(filename != nil); - //ASSERT(ext != nil); - - ASSERT(pPreferences != nil); - - fIsReadOnly = readOnly; - - // special case for volume open - bool isVolume = false; - if (filename[0] >= 'A' && filename[0] <= 'Z' && - filename[1] == ':' && filename[2] == '\\' && - filename[3] == '\0') - { - isVolume = true; - } - - /* - * Open the image. This can be very slow for compressed images, - * especially 3.5" FDI images. - */ - { - CWaitCursor waitc; - - dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly); - if (dierr == kDIErrAccessDenied && !readOnly && !isVolume) { - // retry file open with read-only set - // don't do that for volumes -- assume they know what they want - WMSG0(" Retrying open with read-only set\n"); - fIsReadOnly = readOnly = true; - dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly); - } - if (dierr != kDIErrNone) { - if (dierr == kDIErrFileArchive) - result = kResultFileArchive; - else { - result = kResultFailure; - errMsg.Format("Unable to open '%s': %s.", filename, - DiskImgLib::DIStrError(dierr)); - } - goto bail; - } - } - - dierr = fDiskImg.AnalyzeImage(); - if (dierr != kDIErrNone) { - result = kResultFailure; - errMsg.Format("Analysis of '%s' failed: %s", filename, - DiskImgLib::DIStrError(dierr)); - goto bail; - } - - /* allow them to override sector order and filesystem, if requested */ - if (pPreferences->GetPrefBool(kPrQueryImageFormat)) { - ImageFormatDialog imf; - - imf.InitializeValues(&fDiskImg); - imf.fFileSource = filename; - imf.SetQueryDisplayFormat(false); - imf.SetAllowGenericFormats(false); - - if (imf.DoModal() != IDOK) { - WMSG0("User bailed on IMF dialog\n"); - result = kResultCancel; - goto bail; - } - - if (imf.fSectorOrder != fDiskImg.GetSectorOrder() || - imf.fFSFormat != fDiskImg.GetFSFormat()) - { - WMSG0("Initial values overridden, forcing img format\n"); - dierr = fDiskImg.OverrideFormat(fDiskImg.GetPhysicalFormat(), - imf.fFSFormat, imf.fSectorOrder); - if (dierr != kDIErrNone) { - result = kResultFailure; - errMsg.Format("Unable to access disk image using selected" - " parameters. Error: %s.", - DiskImgLib::DIStrError(dierr)); - goto bail; - } - } - } - - if (fDiskImg.GetFSFormat() == DiskImg::kFormatUnknown || - fDiskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) - { - result = kResultFailure; - errMsg.Format("Unable to identify filesystem on '%s'", filename); - goto bail; - } - - /* create an appropriate DiskFS object */ - fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS(); - if (fpPrimaryDiskFS == nil) { - /* unknown FS should've been caught above! */ - ASSERT(false); - result = kResultFailure; - errMsg.Format("Format of '%s' not recognized.", filename); - goto bail; - } - - fpPrimaryDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled); - - /* - * Scan all files and on the disk image, and recursively descend into - * sub-volumes. Can be slow on physical volumes. - * - * This is really only useful for ProDOS and HFS disks. Nothing else - * can be large enough to really get slow, and nothing else is likely - * to show up in a large multi-partition image. - * - * THOUGHT: only show the dialog if the volume is over a certain size. - */ - { - MainWindow* pMain = GET_MAIN_WINDOW(); - ProgressCounterDialog* pProgress; - - pProgress = new ProgressCounterDialog; - pProgress->Create(_T("Examining contents, please wait..."), pMain); - pProgress->SetCounterFormat("Scanning..."); - pProgress->CenterWindow(); - //pMain->PeekAndPump(); // redraw - CWaitCursor waitc; - - /* set up progress dialog and scan all files */ - pMain->SetProgressCounterDialog(pProgress); - fDiskImg.SetScanProgressCallback(ScanProgressCallback, this); - - dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull); - - fDiskImg.SetScanProgressCallback(nil, nil); - pMain->SetProgressCounterDialog(nil); - pProgress->DestroyWindow(); - - if (dierr != kDIErrNone) { - if (dierr == kDIErrCancelled) { - result = kResultCancel; - } else { - result = kResultFailure; - errMsg.Format("Error reading list of files from disk: %s", - DiskImgLib::DIStrError(dierr)); - } - goto bail; - } - } - - if (LoadContents() != 0) { - result = kResultFailure; - errMsg.Format("Failed while loading contents of disk image."); - goto bail; - } - - /* - * Force read-only flag if underlying FS doesn't allow RW. We need to - * consider embedded filesystems, so we only set RO if none of the - * filesystems are writable. - * - * BUG: this only checks the first level. Should be fully recursive. - */ - if (!fpPrimaryDiskFS->GetReadWriteSupported()) { - const DiskFS::SubVolume* pSubVol; - - fIsReadOnly = true; - pSubVol = fpPrimaryDiskFS->GetNextSubVolume(nil); - while (pSubVol != nil) { - if (pSubVol->GetDiskFS()->GetReadWriteSupported()) { - fIsReadOnly = false; - break; - } - - pSubVol = fpPrimaryDiskFS->GetNextSubVolume(pSubVol); - } - } - - /* force read-only if the primary is damaged */ - if (fpPrimaryDiskFS->GetFSDamaged()) - fIsReadOnly = true; - /* force read-only if the DiskImg thinks a wrapper is damaged */ - if (fpPrimaryDiskFS->GetDiskImg()->GetReadOnly()) - fIsReadOnly = true; - -// /* force read-only on .gz/.zip unless pref allows */ -// if (fDiskImg.GetOuterFormat() != DiskImg::kOuterFormatNone) { -// if (pPreferences->GetPrefBool(kPrWriteZips) == 0) -// fIsReadOnly = true; -// } - - SetPathName(filename); - result = kResultSuccess; - - /* set any preferences-based settings */ - PreferencesChanged(); - -bail: - *pErrMsg = errMsg; - if (!errMsg.IsEmpty()) { - assert(result == kResultFailure); - delete fpPrimaryDiskFS; - fpPrimaryDiskFS = nil; - } else { - assert(result != kResultFailure); - } - return result; -} - - -/* - * Finish instantiating a DiskArchive object by creating a new archive. - * - * Returns an error string on failure, or "" on success. - */ -CString -DiskArchive::New(const char* fileName, const void* vOptions) -{ - const Preferences* pPreferences = GET_PREFERENCES(); - NewOptions* pOptions = (NewOptions*) vOptions; - CString volName; - long numBlocks = -1; - long numTracks = -1; - int numSectors; - CString retmsg; - DIError dierr; - bool allowLowerCase; - - ASSERT(fileName != nil); - ASSERT(pOptions != nil); - - allowLowerCase = pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0; - - switch (pOptions->base.format) { - case DiskImg::kFormatUnknown: - numBlocks = pOptions->blank.numBlocks; - break; - case DiskImg::kFormatProDOS: - volName = pOptions->prodos.volName; - numBlocks = pOptions->prodos.numBlocks; - break; - case DiskImg::kFormatPascal: - volName = pOptions->pascalfs.volName; - numBlocks = pOptions->pascalfs.numBlocks; - break; - case DiskImg::kFormatMacHFS: - volName = pOptions->hfs.volName; - numBlocks = pOptions->hfs.numBlocks; - break; - case DiskImg::kFormatDOS32: - numTracks = pOptions->dos.numTracks; - numSectors = pOptions->dos.numSectors; - - if (numTracks < DiskFSDOS33::kMinTracks || - numTracks > DiskFSDOS33::kMaxTracks) - { - retmsg.Format("Invalid DOS32 track count"); - goto bail; - } - if (numSectors != 13) { - retmsg.Format("Invalid DOS32 sector count"); - goto bail; - } - if (pOptions->dos.allocDOSTracks) - volName = "DOS"; - break; - case DiskImg::kFormatDOS33: - numTracks = pOptions->dos.numTracks; - numSectors = pOptions->dos.numSectors; - - if (numTracks < DiskFSDOS33::kMinTracks || - numTracks > DiskFSDOS33::kMaxTracks) - { - retmsg.Format("Invalid DOS33 track count"); - goto bail; - } - if (numSectors != 16 && numSectors != 32) { // no 13-sector (yet) - retmsg.Format("Invalid DOS33 sector count"); - goto bail; - } - if (pOptions->dos.allocDOSTracks) - volName = "DOS"; - break; - default: - retmsg.Format("Unsupported disk format"); - goto bail; - } - - WMSG4("DiskArchive: new '%s' %ld %s in '%s'\n", - (const char*)volName, numBlocks, - DiskImg::ToString(pOptions->base.format), fileName); - - bool canSkipFormat; - if (IsWin9x()) - canSkipFormat = false; - else - canSkipFormat = true; - - /* - * Create an image with the appropriate characteristics. We set - * "skipFormat" because we know this will be a brand-new file, and - * we're not currently creating nibble images. - * - * GLITCH: under Win98/ME, brand-new files contain the previous contents - * of the hard drive. We need to explicitly zero them out. We don't - * want to do it under Win2K/XP because it can be slow for larger - * volumes. - */ - if (numBlocks > 0) { - dierr = fDiskImg.CreateImage(fileName, nil, - DiskImg::kOuterFormatNone, - DiskImg::kFileFormatUnadorned, - DiskImg::kPhysicalFormatSectors, - nil, - pOptions->base.sectorOrder, - DiskImg::kFormatGenericProDOSOrd, // arg must be generic - numBlocks, - canSkipFormat); - } else { - ASSERT(numTracks > 0); - dierr = fDiskImg.CreateImage(fileName, nil, - DiskImg::kOuterFormatNone, - DiskImg::kFileFormatUnadorned, - DiskImg::kPhysicalFormatSectors, - nil, - pOptions->base.sectorOrder, - DiskImg::kFormatGenericProDOSOrd, // arg must be generic - numTracks, numSectors, - canSkipFormat); - } - if (dierr != kDIErrNone) { - retmsg.Format("Unable to create disk image: %s.", - DiskImgLib::DIStrError(dierr)); - goto bail; - } - - if (pOptions->base.format == DiskImg::kFormatUnknown) - goto skip_format; - - if (pOptions->base.format == DiskImg::kFormatDOS33 || - pOptions->base.format == DiskImg::kFormatDOS32) - fDiskImg.SetDOSVolumeNum(pOptions->dos.volumeNum); - - /* - * If we don't allow lower case in ProDOS filenames, don't allow them - * in volume names either. This works because we don't allow ' ' in - * volume names; otherwise we'd need to invoke a ProDOS-specific call - * to convert the ' ' to '.'. (Or we could just do it ourselves.) - * - * We can't ask the ProDOS DiskFS to force upper case for us because - * the ProDOS DiskFS object doesn't yet exist. - */ - if (pOptions->base.format == DiskImg::kFormatProDOS && !allowLowerCase) - volName.MakeUpper(); - - /* format it */ - dierr = fDiskImg.FormatImage(pOptions->base.format, volName); - if (dierr != kDIErrNone) { - retmsg.Format("Unable to format disk image: %s.", - DiskImgLib::DIStrError(dierr)); - goto bail; - } - fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS(false); - if (fpPrimaryDiskFS == nil) { - retmsg.Format("Unable to create DiskFS."); - goto bail; - } - - /* prep it */ - dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull); - if (dierr != kDIErrNone) { - retmsg.Format("Error reading list of files from disk: %s", - DiskImgLib::DIStrError(dierr)); - goto bail; - } - - /* this is pretty meaningless, but do it to ensure we're initialized */ - if (LoadContents() != 0) { - retmsg.Format("Failed while loading contents of disk image."); - goto bail; - } - -skip_format: - SetPathName(fileName); - - /* set any preferences-based settings */ - PreferencesChanged(); - -bail: - return retmsg; -} - -/* - * Close the DiskArchive ojbect. - */ -CString -DiskArchive::Close(void) -{ - if (fpPrimaryDiskFS != nil) { - WMSG0("DiskArchive shutdown closing disk image\n"); - delete fpPrimaryDiskFS; - fpPrimaryDiskFS = nil; - } - - DIError dierr; - dierr = fDiskImg.CloseImage(); - if (dierr != kDIErrNone) { - MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); - CString msg, failed; - - msg.Format("Failed while closing disk image: %s.", - DiskImgLib::DIStrError(dierr)); - failed.LoadString(IDS_FAILED); - WMSG1("During close: %s\n", (const char*) msg); - - pMainWin->MessageBox(msg, failed, MB_OK); - } - - return ""; -} - -/* - * Flush the DiskArchive object. - * - * Most of the stuff we do with disk images goes straight through, but in - * the case of compressed disks we don't normally re-compress them until - * it's time to close them. This forces us to update the copy on disk. - * - * Returns an empty string on success, or an error message on failure. - */ -CString -DiskArchive::Flush(void) -{ - DIError dierr; - CWaitCursor waitc; - - assert(fpPrimaryDiskFS != nil); - - dierr = fpPrimaryDiskFS->Flush(DiskImg::kFlushAll); - if (dierr != kDIErrNone) { - CString errMsg; - - errMsg.Format("Attempt to flush the current archive failed: %s.", - DiskImgLib::DIStrError(dierr)); - return errMsg; - } - - return ""; -} - -/* - * Returns "true" if the archive has un-flushed modifications pending. - */ -bool -DiskArchive::IsModified(void) const -{ - assert(fpPrimaryDiskFS != nil); - - return fpPrimaryDiskFS->GetDiskImg()->GetDirtyFlag(); -} - -/* - * Return an description of the disk archive, suitable for display in the - * main title bar. - */ -void -DiskArchive::GetDescription(CString* pStr) const -{ - if (fpPrimaryDiskFS == nil) - return; - - if (fpPrimaryDiskFS->GetVolumeID() != nil) - pStr->Format("Disk Image - %s", fpPrimaryDiskFS->GetVolumeID()); -} - - -/* - * Load the contents of a "disk archive". - * - * Returns 0 on success. - */ -int -DiskArchive::LoadContents(void) -{ - int result; - - WMSG0("DiskArchive LoadContents\n"); - ASSERT(fpPrimaryDiskFS != nil); - - { - MainWindow* pMain = GET_MAIN_WINDOW(); - ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog; - pWaitDlg->Create(IDD_LOADING, pMain); - pWaitDlg->CenterWindow(); - pMain->PeekAndPump(); // redraw - CWaitCursor waitc; - - result = LoadDiskFSContents(fpPrimaryDiskFS, ""); - - SET_PROGRESS_COUNTER(-1); - - pWaitDlg->DestroyWindow(); - //pMain->PeekAndPump(); // redraw - } - - return result; -} - -/* - * Reload the stuff from the underlying DiskFS. - * - * This also does a "lite" flush of the disk data. For files that are - * essentially being written as we go, this does little more than clear - * the "dirty" flag. Files that need to be recompressed or have some - * other slow operation remain dirty. - * - * We don't need to do the flush as part of the reload -- we can load the - * contents with everything in a perfectly dirty state. We don't need to - * do it at all. We do it to keep the "dirty" flag clear when nothing is - * really dirty, and we do it here because almost all of our functions call - * "reload" after making changes, which makes it convenient to call from here. - */ -CString -DiskArchive::Reload() -{ - fReloadFlag = true; // tell everybody that cached data is invalid - - (void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly); - - DeleteEntries(); // a GenericArchive operation - - if (LoadContents() != 0) - return "Disk image reload failed."; - - return ""; -} - -/* - * Reload the contents of the archive, showing an error message if the - * reload fails. - * - * Returns 0 on success, -1 on failure. - */ -int -DiskArchive::InternalReload(CWnd* pMsgWnd) -{ - CString errMsg; - - errMsg = Reload(); - - if (!errMsg.IsEmpty()) { - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - return -1; - } - - return 0; -} - -/* - * Load the contents of a DiskFS. - * - * Recursively handle sub-volumes. "volName" holds the name of the - * sub-volume as it should appear in the list. - */ -int -DiskArchive::LoadDiskFSContents(DiskFS* pDiskFS, const char* volName) -{ - static const char* kBlankFileName = ""; - A2File* pFile; - DiskEntry* pNewEntry; - DiskFS::SubVolume* pSubVol; - const Preferences* pPreferences = GET_PREFERENCES(); - bool wantCoerceDOSFilenames = false; - CString ourSubVolName; - - wantCoerceDOSFilenames = pPreferences->GetPrefBool(kPrCoerceDOSFilenames); - - WMSG2("Notes for disk image '%s':\n%s", - volName, pDiskFS->GetDiskImg()->GetNotes()); - - ASSERT(pDiskFS != nil); - pFile = pDiskFS->GetNextFile(nil); - while (pFile != nil) { - pNewEntry = new DiskEntry(pFile); - if (pNewEntry == nil) - return -1; - - CString path(pFile->GetPathName()); - if (path.IsEmpty()) - path = kBlankFileName; - if (DiskImg::UsesDOSFileStructure(pFile->GetFSFormat()) && - wantCoerceDOSFilenames) - { - InjectLowercase(&path); - } - pNewEntry->SetPathName(path); - if (volName[0] != '\0') - pNewEntry->SetSubVolName(volName); - pNewEntry->SetFssep(pFile->GetFssep()); - pNewEntry->SetFileType(pFile->GetFileType()); - pNewEntry->SetAuxType(pFile->GetAuxType()); - pNewEntry->SetAccess(pFile->GetAccess()); - if (pFile->GetCreateWhen() == 0) - pNewEntry->SetCreateWhen(kDateNone); - else - pNewEntry->SetCreateWhen(pFile->GetCreateWhen()); - if (pFile->GetModWhen() == 0) - pNewEntry->SetModWhen(kDateNone); - else - pNewEntry->SetModWhen(pFile->GetModWhen()); - pNewEntry->SetSourceFS(pFile->GetFSFormat()); - pNewEntry->SetHasDataFork(true); - if (pFile->IsVolumeDirectory()) { - /* volume directory entry; only on ProDOS/HFS */ - ASSERT(pFile->GetRsrcLength() < 0); - pNewEntry->SetRecordKind(GenericEntry::kRecordKindVolumeDir); - //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); - pNewEntry->SetDataForkLen(pFile->GetDataLength()); - pNewEntry->SetCompressedLen(pFile->GetDataLength()); - } else if (pFile->IsDirectory()) { - /* directory entry */ - ASSERT(pFile->GetRsrcLength() < 0); - pNewEntry->SetRecordKind(GenericEntry::kRecordKindDirectory); - //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); - pNewEntry->SetDataForkLen(pFile->GetDataLength()); - pNewEntry->SetCompressedLen(pFile->GetDataLength()); - } else if (pFile->GetRsrcLength() >= 0) { - /* has resource fork */ - pNewEntry->SetRecordKind(GenericEntry::kRecordKindForkedFile); - pNewEntry->SetDataForkLen(pFile->GetDataLength()); - pNewEntry->SetRsrcForkLen(pFile->GetRsrcLength()); - //pNewEntry->SetUncompressedLen( - // pFile->GetDataLength() + pFile->GetRsrcLength() ); - pNewEntry->SetCompressedLen( - pFile->GetDataSparseLength() + pFile->GetRsrcSparseLength() ); - pNewEntry->SetHasRsrcFork(true); - } else { - /* just data fork */ - pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile); - //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); - pNewEntry->SetDataForkLen(pFile->GetDataLength()); - pNewEntry->SetCompressedLen(pFile->GetDataSparseLength()); - } - - switch (pNewEntry->GetSourceFS()) { - case DiskImg::kFormatDOS33: - case DiskImg::kFormatDOS32: - case DiskImg::kFormatUNIDOS: - pNewEntry->SetFormatStr("DOS"); - break; - case DiskImg::kFormatProDOS: - pNewEntry->SetFormatStr("ProDOS"); - break; - case DiskImg::kFormatPascal: - pNewEntry->SetFormatStr("Pascal"); - break; - case DiskImg::kFormatCPM: - pNewEntry->SetFormatStr("CP/M"); - break; - case DiskImg::kFormatMSDOS: - pNewEntry->SetFormatStr("MS-DOS"); - break; - case DiskImg::kFormatRDOS33: - case DiskImg::kFormatRDOS32: - case DiskImg::kFormatRDOS3: - pNewEntry->SetFormatStr("RDOS"); - break; - case DiskImg::kFormatMacHFS: - pNewEntry->SetFormatStr("HFS"); - break; - default: - pNewEntry->SetFormatStr("???"); - break; - } - - pNewEntry->SetDamaged(pFile->GetQuality() == A2File::kQualityDamaged); - pNewEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious); - - AddEntry(pNewEntry); - - /* this is not very useful -- all the heavy lifting was done earlier */ - if ((GetNumEntries() % 100) == 0) - SET_PROGRESS_COUNTER(GetNumEntries()); - - pFile = pDiskFS->GetNextFile(pFile); - } - - /* - * Load all sub-volumes. - * - * We define the sub-volume name to use for the next layer down. We - * prepend an underscore to the unmodified name. So long as the volume - * name is a valid Windows path -- which should hold true for most disks, - * though possibly not for Pascal -- it can be extracted directly with - * its full path with no risk of conflict. (The extraction code relies - * on this, so don't put a ':' in the subvol name or Windows will choke.) - */ - pSubVol = pDiskFS->GetNextSubVolume(nil); - while (pSubVol != nil) { - CString concatSubVolName; - const char* subVolName; - int ret; - - subVolName = pSubVol->GetDiskFS()->GetVolumeName(); - if (subVolName == nil) - subVolName = "+++"; // call it *something* - - if (volName[0] == '\0') - concatSubVolName.Format("_%s", subVolName); - else - concatSubVolName.Format("%s_%s", volName, subVolName); - ret = LoadDiskFSContents(pSubVol->GetDiskFS(), concatSubVolName); - if (ret != 0) - return ret; - pSubVol = pDiskFS->GetNextSubVolume(pSubVol); - } - - return 0; -} - - -/* - * User has updated their preferences. Take note. - * - * Setting preferences in a DiskFS causes those prefs to be pushed down - * to all sub-volumes. - */ -void -DiskArchive::PreferencesChanged(void) -{ - const Preferences* pPreferences = GET_PREFERENCES(); - - if (fpPrimaryDiskFS != nil) { - fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase, - pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0); - fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse, - pPreferences->GetPrefBool(kPrProDOSUseSparse) != 0); - } -} - - -/* - * Report on what this disk image is capable of. - */ -long -DiskArchive::GetCapability(Capability cap) -{ - switch (cap) { - case kCapCanTest: - return false; - break; - case kCapCanRenameFullPath: - return false; - break; - case kCapCanRecompress: - return false; - break; - case kCapCanEditComment: - return false; - break; - case kCapCanAddDisk: - return false; - break; - case kCapCanConvEOLOnAdd: - return true; - break; - case kCapCanCreateSubdir: - return true; - break; - case kCapCanRenameVolume: - return true; - break; - default: - ASSERT(false); - return -1; - break; - } -} - - -/* - * =========================================================================== - * DiskArchive -- add files - * =========================================================================== - */ - -/* - * Process a bulk "add" request. - * - * Returns "true" on success, "false" on failure. - */ -bool -DiskArchive::BulkAdd(ActionProgressDialog* pActionProgress, - const AddFilesDialog* pAddOpts) -{ - NuError nerr; - CString errMsg; - char curDir[MAX_PATH] = ""; - bool retVal = false; - - WMSG2("Opts: '%s' typePres=%d\n", - pAddOpts->fStoragePrefix, pAddOpts->fTypePreservation); - WMSG3(" sub=%d strip=%d ovwr=%d\n", - pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames, - pAddOpts->fOverwriteExisting); - - ASSERT(fpAddDataHead == nil); - - /* these reset on every new add */ - fOverwriteExisting = false; - fOverwriteNoAsk = false; - - /* we screen for clashes with existing files later; this just ensures - "batch uniqueness" */ - fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true); - - /* - * Save the current directory and change to the one from the file dialog. - */ - const char* buf = pAddOpts->GetFileNames(); - WMSG2("Selected path = '%s' (offset=%d)\n", buf, - pAddOpts->GetFileNameOffset()); - - if (GetCurrentDirectory(sizeof(curDir), curDir) == 0) { - errMsg = "Unable to get current directory.\n"; - ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); - goto bail; - } - if (SetCurrentDirectory(buf) == false) { - errMsg.Format("Unable to set current directory to '%s'.\n", buf); - ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); - goto bail; - } - - buf += pAddOpts->GetFileNameOffset(); - while (*buf != '\0') { - WMSG1(" file '%s'\n", buf); - - /* add the file, calling DoAddFile via the generic AddFile */ - nerr = AddFile(pAddOpts, buf, &errMsg); - if (nerr != kNuErrNone) { - if (errMsg.IsEmpty()) - errMsg.Format("Failed while adding file '%s': %s.", - (LPCTSTR) buf, NuStrError(nerr)); - if (nerr != kNuErrAborted) { - ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); - } - goto bail; - } - - buf += strlen(buf)+1; - } - - if (fpAddDataHead == nil) { - CString title; - title.LoadString(IDS_MB_APP_NAME); - errMsg = "No files added.\n"; - pActionProgress->MessageBox(errMsg, title, MB_OK | MB_ICONWARNING); - } else { - /* add all pending files */ - retVal = true; - errMsg = ProcessFileAddData(pAddOpts->fpTargetDiskFS, - pAddOpts->fConvEOL); - if (!errMsg.IsEmpty()) { - CString title; - ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); - retVal = false; - } - - /* success or failure, reload the contents */ - errMsg = Reload(); - if (!errMsg.IsEmpty()) - retVal = false; - } - -bail: - FreeAddDataList(); - if (SetCurrentDirectory(curDir) == false) { - errMsg.Format("Unable to reset current directory to '%s'.\n", buf); - ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); - // bummer, but don't signal failure - } - return retVal; -} - -/* - * Add a file to a disk image. - * - * Unfortunately we can't just add the files here. We need to figure out - * which pairs of files should be combined into a single "extended" file. - * (Yes, the cursed forked files strike again.) - * - * The way you tell if two files should be one is by comparing their - * filenames and type info. If they match, and one is a data fork and - * one is a resource fork, we have a single split file. - * - * We have to be careful here because we don't know which will be seen - * first and whether they'll be adjacent. We have to dig through the - * list of previously-added files for a match (O(n^2) behavior currently). - * - * We also have to compare the right filename. Comparing the Windows - * filename is a bad idea, because by definition one of them has a resource - * fork tag on it. We need to compare the normalized filename before - * the ProDOS normalizer/uniqifier gets a chance to mangle it. As luck - * would have it, that's exactly what we have in "storageName". - * - * For a NuFX archive, NufxLib does all this nonsense for us, but we have - * to manage it ourselves here. The good news is that, since we have to - * wade through all the filenames, we have an opportunity to make the names - * unique. So long as we ensure that the names we have don't clash with - * anything currently on the disk, we know that anything we add that does - * clash is running into something we just added, which means we can turn - * on CreateFile's "make unique" feature and let the filesystem-specific - * code handle uniqueness. - * - * Any fields we want to keep from the NuFileDetails struct need to be - * copied out. It's a "hairy" struct, so we need to duplicate the strings. - */ -NuError -DiskArchive::DoAddFile(const AddFilesDialog* pAddOpts, - FileDetails* pDetails) -{ - NuError nuerr = kNuErrNone; - DiskFS* pDiskFS = pAddOpts->fpTargetDiskFS; - - DIError dierr; - int neededLen = 64; // reasonable guess - char* fsNormalBuf = nil; - - WMSG2(" +++ ADD file: orig='%s' stor='%s'\n", - pDetails->origName, pDetails->storageName); - -retry: - /* - * Convert "storageName" to a filesystem-normalized path. - */ - delete[] fsNormalBuf; - fsNormalBuf = new char[neededLen]; - dierr = pDiskFS->NormalizePath(pDetails->storageName, - PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen); - if (dierr == kDIErrDataOverrun) { - /* not long enough, try again *once* */ - delete[] fsNormalBuf; - fsNormalBuf = new char[neededLen]; - dierr = pDiskFS->NormalizePath(pDetails->storageName, - PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen); - } - if (dierr != kDIErrNone) { - nuerr = kNuErrInternal; - goto bail; - } - - /* - * Test to see if the file already exists. If it does, give the user - * the opportunity to rename it, overwrite the original, or skip - * adding it. - * - * The FS-normalized path may not reflect the actual storage name, - * because some features (like ProDOS "allow lower case") aren't - * factored in until later. However, it should be close enough -- it - * has to be, or we'd be in trouble for saying it's going to overwrite - * the file in the archive. - */ - A2File* pExisting; - pExisting = pDiskFS->GetFileByName(fsNormalBuf); - if (pExisting != nil) { - NuResult result; - - result = HandleReplaceExisting(pExisting, pDetails); - if (result == kNuAbort) { - nuerr = kNuErrAborted; - goto bail; - } else if (result == kNuSkip) { - nuerr = kNuErrSkipped; - goto bail; - } else if (result == kNuRename) { - goto retry; - } else if (result == kNuOverwrite) { - /* delete the existing file immediately */ - WMSG1(" Deleting existing file '%s'\n", fsNormalBuf); - dierr = pDiskFS->DeleteFile(pExisting); - if (dierr != kDIErrNone) { - // Would be nice to show a dialog and explain *why*, but - // I'm not sure we have a window here. - WMSG1(" Deletion failed (err=%d)\n", dierr); - goto bail; - } - } else { - WMSG1("GLITCH: bad return %d from HandleReplaceExisting\n",result); - assert(false); - nuerr = kNuErrInternal; - goto bail; - } - } - - /* - * Put all the goodies into a new FileAddData object, and add it to - * the end of the list. - */ - FileAddData* pAddData; - pAddData = new FileAddData(pDetails, fsNormalBuf); - if (pAddData == nil) { - nuerr = kNuErrMalloc; - goto bail; - } - - WMSG1("FSNormalized is '%s'\n", pAddData->GetFSNormalPath()); - - AddToAddDataList(pAddData); - -bail: - delete[] fsNormalBuf; - return nuerr; -} - -/* - * A file we're adding clashes with an existing file. Decide what to do - * about it. - * - * Returns one of the following: - * kNuOverwrite - overwrite the existing file - * kNuSkip - skip adding the existing file - * kNuRename - user wants to rename the file - * kNuAbort - cancel out of the entire add process - * - * Side effects: - * Sets fOverwriteExisting and fOverwriteNoAsk if a "to all" button is hit - * Replaces pDetails->storageName if the user elects to rename - */ -NuResult -DiskArchive::HandleReplaceExisting(const A2File* pExisting, - FileDetails* pDetails) -{ - NuResult result; - - if (fOverwriteNoAsk) { - if (fOverwriteExisting) - return kNuOverwrite; - else - return kNuSkip; - } - - ConfirmOverwriteDialog confOvwr; - - confOvwr.fExistingFile = pExisting->GetPathName(); - confOvwr.fExistingFileModWhen = pExisting->GetModWhen(); - - PathName srcPath(pDetails->origName); - confOvwr.fNewFileSource = pDetails->origName; // or storageName? - confOvwr.fNewFileModWhen = srcPath.GetModWhen(); - - if (confOvwr.DoModal() == IDCANCEL) { - WMSG0("User cancelled out of add-to-diskimg replace-existing\n"); - return kNuAbort; - } - - if (confOvwr.fResultRename) { - /* - * Replace the name in FileDetails. They were asked to modify - * the already-normalized version of the filename. We will run - * it back through the FS-specific normalizer, which will handle - * any oddities they type in. - * - * We don't want to run it through PathProposal.LocalToArchive - * because that'll strip out ':' in the pathnames. - * - * Ideally the rename dialog would have a way to validate the - * full path and reject "OK" if it's not valid. Instead, we just - * allow the FS normalizer to force the filename to be valid. - */ - pDetails->storageName = confOvwr.fExistingFile; - WMSG1("Trying rename to '%s'\n", pDetails->storageName); - return kNuRename; - } - - if (confOvwr.fResultApplyToAll) { - fOverwriteNoAsk = true; - if (confOvwr.fResultOverwrite) - fOverwriteExisting = true; - else - fOverwriteExisting = false; - } - if (confOvwr.fResultOverwrite) - result = kNuOverwrite; - else - result = kNuSkip; - - return result; -} - - -/* - * Process the list of pending file adds. - * - * This is where the rubber (finally!) meets the road. - */ -CString -DiskArchive::ProcessFileAddData(DiskFS* pDiskFS, int addOptsConvEOL) -{ - CString errMsg; - FileAddData* pData; - unsigned char* dataBuf = nil; - unsigned char* rsrcBuf = nil; - long dataLen, rsrcLen; - MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); - - WMSG0("--- ProcessFileAddData\n"); - - /* map the EOL conversion to something we can use */ - GenericEntry::ConvertEOL convEOL; - - switch (addOptsConvEOL) { - case AddFilesDialog::kConvEOLNone: - convEOL = GenericEntry::kConvertEOLOff; - break; - case AddFilesDialog::kConvEOLType: - // will be adjusted each time through the loop - convEOL = GenericEntry::kConvertEOLOff; - break; - case AddFilesDialog::kConvEOLAuto: - convEOL = GenericEntry::kConvertEOLAuto; - break; - case AddFilesDialog::kConvEOLAll: - convEOL = GenericEntry::kConvertEOLOn; - break; - default: - assert(false); - convEOL = GenericEntry::kConvertEOLOff; - break; - } - - - pData = fpAddDataHead; - while (pData != nil) { - const FileDetails* pDataDetails = nil; - const FileDetails* pRsrcDetails = nil; - const FileDetails* pDetails = pData->GetDetails(); - const char* typeStr = "????"; - - switch (pDetails->entryKind) { - case FileDetails::kFileKindDataFork: - pDataDetails = pDetails; - typeStr = "data"; - break; - case FileDetails::kFileKindRsrcFork: - pRsrcDetails = pDetails; - typeStr = "rsrc"; - break; - case FileDetails::kFileKindDiskImage: - pDataDetails = pDetails; - typeStr = "disk"; - break; - case FileDetails::kFileKindBothForks: - case FileDetails::kFileKindDirectory: - default: - assert(false); - return "internal error"; - } - - if (pData->GetOtherFork() != nil) { - pDetails = pData->GetOtherFork()->GetDetails(); - typeStr = "both"; - - switch (pDetails->entryKind) { - case FileDetails::kFileKindDataFork: - assert(pDataDetails == nil); - pDataDetails = pDetails; - break; - case FileDetails::kFileKindRsrcFork: - assert(pRsrcDetails == nil); - pRsrcDetails = pDetails; - break; - case FileDetails::kFileKindDiskImage: - assert(false); - return "(internal) add other disk error"; - case FileDetails::kFileKindBothForks: - case FileDetails::kFileKindDirectory: - default: - assert(false); - return "internal error"; - } - } - - WMSG2("Adding file '%s' (%s)\n", - pDetails->storageName, typeStr); - ASSERT(pDataDetails != nil || pRsrcDetails != nil); - - /* - * The current implementation of DiskImg/DiskFS requires writing each - * fork in one shot. This means loading the entire thing into - * memory. Not so bad for ProDOS, with its 16MB maximum file size, - * but it could be awkward for HFS (not to mention HFS Plus!). - */ - DiskFS::CreateParms parms; - ConvertFDToCP(pData->GetDetails(), &parms); - if (pRsrcDetails != nil) - parms.storageType = kNuStorageExtended; - else - parms.storageType = kNuStorageSeedling; - /* use the FS-normalized path here */ - parms.pathName = pData->GetFSNormalPath(); - - dataLen = rsrcLen = -1; - if (pDataDetails != nil) { - /* figure out text conversion, including high ASCII for DOS */ - /* (HA conversion only happens if text conversion happens) */ - GenericEntry::ConvertHighASCII convHA; - if (addOptsConvEOL == AddFilesDialog::kConvEOLType) { - if (pDataDetails->fileType == kFileTypeTXT || - pDataDetails->fileType == kFileTypeSRC) - { - WMSG0("Enabling text conversion by type\n"); - convEOL = GenericEntry::kConvertEOLOn; - } else { - convEOL = GenericEntry::kConvertEOLOff; - } - } - if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat())) - convHA = GenericEntry::kConvertHAOn; - else - convHA = GenericEntry::kConvertHAOff; - - errMsg = LoadFile(pDataDetails->origName, &dataBuf, &dataLen, - convEOL, convHA); - if (!errMsg.IsEmpty()) - goto bail; - } - if (pRsrcDetails != nil) { - /* no text conversion on resource forks */ - errMsg = LoadFile(pRsrcDetails->origName, &rsrcBuf, &rsrcLen, - GenericEntry::kConvertEOLOff, GenericEntry::kConvertHAOff); - if (!errMsg.IsEmpty()) - goto bail; - } - - /* really ought to do this separately for each thread */ - SET_PROGRESS_BEGIN(); - SET_PROGRESS_UPDATE2(0, pDetails->origName, - parms.pathName); - - DIError dierr; - dierr = AddForksToDisk(pDiskFS, &parms, dataBuf, dataLen, - rsrcBuf, rsrcLen); - SET_PROGRESS_END(); - if (dierr != kDIErrNone) { - errMsg.Format("Unable to add '%s' to image: %s.", - parms.pathName, DiskImgLib::DIStrError(dierr)); - goto bail; - } - delete[] dataBuf; - delete[] rsrcBuf; - dataBuf = rsrcBuf = nil; - - pData = pData->GetNext(); - } - -bail: - delete[] dataBuf; - delete[] rsrcBuf; - return errMsg; -} - -#define kCharLF '\n' -#define kCharCR '\r' - -/* - * Load a file into a buffer, possibly converting EOL markers and setting - * "high ASCII" along the way. - * - * Returns a pointer to a newly-allocated buffer (new[]) and the data length. - * If the file is empty, no buffer will be allocated. - * - * Returns an empty string on success, or an error message on failure. - * - * HEY: really ought to update the progress counter, especially when reading - * really large files. - */ -CString -DiskArchive::LoadFile(const char* pathName, unsigned char** pBuf, long* pLen, - GenericEntry::ConvertEOL conv, GenericEntry::ConvertHighASCII convHA) const -{ - CString errMsg; - FILE* fp; - long fileLen; - - ASSERT(convHA == GenericEntry::kConvertHAOn || - convHA == GenericEntry::kConvertHAOff); - ASSERT(conv == GenericEntry::kConvertEOLOn || - conv == GenericEntry::kConvertEOLOff || - conv == GenericEntry::kConvertEOLAuto); - ASSERT(pathName != nil); - ASSERT(pBuf != nil); - ASSERT(pLen != nil); - - fp = fopen(pathName, "rb"); - if (fp == nil) { - errMsg.Format("Unable to open '%s': %s.", pathName, - strerror(errno)); - goto bail; - } - - if (fseek(fp, 0, SEEK_END) != 0) { - errMsg.Format("Unable to seek to end of '%s': %s", pathName, - strerror(errno)); - goto bail; - } - fileLen = ftell(fp); - if (fileLen < 0) { - errMsg.Format("Unable to determine length of '%s': %s", pathName, - strerror(errno)); - goto bail; - } - rewind(fp); - - if (fileLen == 0) { // handle zero-length files - *pBuf = nil; - *pLen = 0; - goto bail; - } else if (fileLen > 0x00ffffff) { - errMsg = "Cannot add files larger than 16MB to a disk image."; - goto bail; - } - - *pBuf = new unsigned char[fileLen]; - if (*pBuf == nil) { - errMsg.Format("Unable to allocate %ld bytes for '%s'.", - fileLen, pathName); - goto bail; - } - - /* - * We're ready to load the file. We need to sort out EOL conversion. - * Since we always convert to CR, we know the file will stay the same - * size or get smaller, which means the buffer we've allocated is - * guaranteed to hold the file even if we convert it. - * - * If the text mode is "auto", we need to load a piece of the file and - * analyze it. - */ - if (conv == GenericEntry::kConvertEOLAuto) { - GenericEntry::EOLType eolType; - GenericEntry::ConvertHighASCII dummy; - - int chunkLen = 16384; // nice big chunk - if (chunkLen > fileLen) - chunkLen = fileLen; - - if (fread(*pBuf, chunkLen, 1, fp) != 1) { - errMsg.Format("Unable to read initial chunk of '%s': %s.", - pathName, strerror(errno)); - delete[] *pBuf; - *pBuf = nil; - goto bail; - } - rewind(fp); - - conv = GenericEntry::DetermineConversion(*pBuf, chunkLen, - &eolType, &dummy); - WMSG2("LoadFile DetermineConv returned conv=%d eolType=%d\n", - conv, eolType); - if (conv == GenericEntry::kConvertEOLOn && - eolType == GenericEntry::kEOLCR) - { - WMSG0(" (skipping conversion due to matching eolType)\n"); - conv = GenericEntry::kConvertEOLOff; - } - } - ASSERT(conv != GenericEntry::kConvertEOLAuto); - - /* - * The "high ASCII" conversion is either on or off. In this context, - * "on" means "convert all text files", and "off" means "don't convert - * text files". We never convert non-text files. Conversion should - * always be "on" for DOS 3.2/3.3, and "off" for everything else (except - * RDOS, should we choose to make that writeable). - */ - if (conv == GenericEntry::kConvertEOLOff) { - /* fast path */ - WMSG1(" +++ NOT converting text '%s'\n", pathName); - if (fread(*pBuf, fileLen, 1, fp) != 1) { - errMsg.Format("Unable to read '%s': %s.", pathName, strerror(errno)); - delete[] *pBuf; - *pBuf = nil; - goto bail; - } - } else { - /* - * Convert as we go. - * - * Observation: if we copy a binary file to a DOS disk, and force - * the text conversion, we will convert 0x0a to 0x0d, and thence - * to 0x8d. However, we may still have some 0x8a bytes lying around, - * because we don't convert 0x8a in the original file to anything. - * This means that a CR->CRLF or LF->CRLF conversion can't be - * "undone" on a DOS disk. (Not that anyone cares.) - */ - long count = fileLen; - int mask, ic; - bool lastCR = false; - unsigned char* buf = *pBuf; - - if (convHA == GenericEntry::kConvertHAOn) - mask = 0x80; - else - mask = 0x00; - - WMSG2(" +++ Converting text '%s', mask=0x%02x\n", pathName, mask); - - while (count--) { - ic = getc(fp); - - if (ic == kCharCR) { - *buf++ = (unsigned char) (kCharCR | mask); - lastCR = true; - } else if (ic == kCharLF) { - if (!lastCR) - *buf++ = (unsigned char) (kCharCR | mask); - lastCR = false; - } else { - if (ic == '\0') - *buf++ = (unsigned char) ic; // don't conv 0x00 - else - *buf++ = (unsigned char) (ic | mask); - lastCR = false; - } - } - fileLen = buf - *pBuf; - } - - (void) fclose(fp); - - *pLen = fileLen; - -bail: - return errMsg; -} - -/* - * Add a file with the supplied data to the disk image. - * - * Forks that exist but are empty have a length of zero. Forks that don't - * exist have a length of -1. - * - * Called by XferFile and ProcessFileAddData. - */ -DIError -DiskArchive::AddForksToDisk(DiskFS* pDiskFS, const DiskFS::CreateParms* pParms, - const unsigned char* dataBuf, long dataLen, - const unsigned char* rsrcBuf, long rsrcLen) const -{ - DIError dierr = kDIErrNone; - CString replacementFileName; - const int kFileTypeBIN = 0x06; - const int kFileTypeINT = 0xfa; - const int kFileTypeBAS = 0xfc; - A2File* pNewFile = nil; - A2FileDescr* pOpenFile = nil; - DiskFS::CreateParms parmCopy; - - /* - * Make a temporary copy, pointers and all, so we can rewrite some of - * the fields. This is sort of bad, because we're making copies of a - * const char* filename pointer whose underlying storage we're not - * really familiar with. However, so long as we don't try to retain - * it after this function returns we should be fine. - * - * Might be better to make CreateParms a class instead of a struct, - * make the pathName field new[] storage, and write a copy constructor - * for the operation below. This will be fine for now. - */ - memcpy(&parmCopy, pParms, sizeof(parmCopy)); - - if (rsrcLen >= 0) { - ASSERT(parmCopy.storageType == kNuStorageExtended); - } - - /* - * Look for "empty directory holders" that we put into NuFX archives - * when doing disk-to-archive conversions. These make no sense if - * there's no fssep (because it's coming from DOS), or if there's no - * base path, so we can ignore those cases. We can also ignore it if - * the file is forked or is already a directory. - */ - if (parmCopy.fssep != '\0' && parmCopy.storageType == kNuStorageSeedling) { - const char* cp; - cp = strrchr(parmCopy.pathName, parmCopy.fssep); - if (cp != nil) { - if (strcmp(cp+1, kEmptyFolderMarker) == 0 && dataLen == 0) { - /* drop the junk on the end */ - parmCopy.storageType = kNuStorageDirectory; - replacementFileName = parmCopy.pathName; - replacementFileName = - replacementFileName.Left(cp - parmCopy.pathName -1); - parmCopy.pathName = replacementFileName; - parmCopy.fileType = 0x0f; // DIR - parmCopy.access &= ~(A2FileProDOS::kAccessInvisible); - dataLen = -1; - } - } - } - - /* - * If this is a subdir create request (from the clipboard or an "empty - * directory placeholder" in a NuFX archive), handle it here. If we're - * on a filesystem that doesn't have subdirectories, just skip it. - */ - if (parmCopy.storageType == kNuStorageDirectory) { - A2File* pDummyFile; - ASSERT(dataLen < 0 && rsrcLen < 0); - - if (DiskImg::IsHierarchical(pDiskFS->GetDiskImg()->GetFSFormat())) { - dierr = pDiskFS->CreateFile(&parmCopy, &pDummyFile); - if (dierr == kDIErrDirectoryExists) - dierr = kDIErrNone; // dirs are not made unique - goto bail; - } else { - WMSG0(" Ignoring subdir create req on non-hierarchic FS\n"); - goto bail; - } - } - - /* don't try to put resource forks onto a DOS disk */ - if (!DiskImg::HasResourceForks(pDiskFS->GetDiskImg()->GetFSFormat())) { - if (rsrcLen >= 0) { - rsrcLen = -1; - parmCopy.storageType = kNuStorageSeedling; - - if (dataLen < 0) { - /* this was a resource-fork-only file */ - WMSG1("--- nothing left to write for '%s'\n", - parmCopy.pathName); - goto bail; - } - } else { - ASSERT(parmCopy.storageType == kNuStorageSeedling); - } - } - - /* quick kluge to get the right file type on large DOS files */ - if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()) && - dataLen >= 65536) - { - if (parmCopy.fileType == kFileTypeBIN || - parmCopy.fileType == kFileTypeINT || - parmCopy.fileType == kFileTypeBAS) - { - WMSG0("+++ switching DOS file type to $f2\n"); - parmCopy.fileType = 0xf2; // DOS 'S' file - } - } - - /* - * Create the file on the disk. The storage type determines whether - * it has data+rsrc forks or just data (there's no such thing in - * ProDOS as "just a resource fork"). There's no need to open the - * fork if we're not going to write to it. - * - * This holds for resource forks as well, because the storage type - * determines whether or not the file is forked, and we've asserted - * that a file with a non-(-1) rsrcLen is forked. - */ - dierr = pDiskFS->CreateFile(&parmCopy, &pNewFile); - if (dierr != kDIErrNone) { - WMSG1(" CreateFile failed: %s\n", DiskImgLib::DIStrError(dierr)); - goto bail; - } - - /* - * Note: if this was an empty directory holder, pNewFile will be set - * to nil. We used to avoid handling this by just not opening the file - * if it had a length of zero. However, DOS 3.3 needs to write some - * kinds of zero-length files, because e.g. a zero-length 'B' file - * actually has 4 bytes of data in it. - * - * Of course, if dataLen is zero then dataBuf is nil, so we need to - * supply a dummy write buffer. None of this is an issue for resource - * forks, because DOS 3.3 doesn't have those. - */ - - if (dataLen > 0 || - (dataLen == 0 && pNewFile != nil)) - { - ASSERT(pNewFile != nil); - unsigned char dummyBuf[1] = { '\0' }; - - dierr = pNewFile->Open(&pOpenFile, false, false); - if (dierr != kDIErrNone) - goto bail; - - pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, - dataLen, nil); - - dierr = pOpenFile->Write(dataBuf != nil ? dataBuf : dummyBuf, dataLen); - if (dierr != kDIErrNone) - goto bail; - - dierr = pOpenFile->Close(); - if (dierr != kDIErrNone) - goto bail; - pOpenFile = nil; - } - - if (rsrcLen > 0) { - ASSERT(pNewFile != nil); - - dierr = pNewFile->Open(&pOpenFile, false, true); - if (dierr != kDIErrNone) - goto bail; - - pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, - rsrcLen, nil); - - dierr = pOpenFile->Write(rsrcBuf, rsrcLen); - if (dierr != kDIErrNone) - goto bail; - - dierr = pOpenFile->Close(); - if (dierr != kDIErrNone) - goto bail; - pOpenFile = nil; - } - -bail: - if (pOpenFile != nil) - pOpenFile->Close(); - if (dierr != kDIErrNone && pNewFile != nil) { - /* - * Clean up the partially-written file. This does not, of course, - * erase any subdirectories that were created to contain this file. - * Not worth worrying about. - */ - WMSG1(" Deleting newly-created file '%s'\n", parmCopy.pathName); - (void) pDiskFS->DeleteFile(pNewFile); - } - return dierr; -} - -/* - * Fill out a CreateParms structure from a FileDetails structure. - * - * The NuStorageType values correspond exactly to ProDOS storage types, so - * there's no need to convert them. - */ -void -DiskArchive::ConvertFDToCP(const FileDetails* pDetails, - DiskFS::CreateParms* pCreateParms) -{ - pCreateParms->pathName = pDetails->storageName; - pCreateParms->fssep = (char) pDetails->fileSysInfo; - pCreateParms->storageType = pDetails->storageType; - pCreateParms->fileType = pDetails->fileType; - pCreateParms->auxType = pDetails->extraType; - pCreateParms->access = pDetails->access; - pCreateParms->createWhen = NufxArchive::DateTimeToSeconds(&pDetails->createWhen); - pCreateParms->modWhen = NufxArchive::DateTimeToSeconds(&pDetails->modWhen); -} - - -/* - * Add an entry to the end of the FileAddData list. - * - * If "storeName" (the Windows filename with type goodies stripped, but - * without filesystem normalization) matches an entry already in the list, - * we check to see if these are forks of the same file. If they are - * different forks and we don't already have both forks, we put the - * pointer into the "fork pointer" of the existing file rather than adding - * it to the end of the list. - */ -void -DiskArchive::AddToAddDataList(FileAddData* pData) -{ - ASSERT(pData != nil); - ASSERT(pData->GetNext() == nil); - - /* - * Run through the entire existing list, looking for a match. This is - * O(n^2) behavior, but I'm expecting N to be relatively small (under - * 1000 in almost all cases). - */ - //if (strcasecmp(pData->GetDetails()->storageName, "system\\finder") == 0) - // WMSG0("whee\n"); - FileAddData* pSearch = fpAddDataHead; - FileDetails::FileKind dataKind, listKind; - - dataKind = pData->GetDetails()->entryKind; - while (pSearch != nil) { - if (pSearch->GetOtherFork() == nil && - strcmp(pSearch->GetDetails()->storageName, - pData->GetDetails()->storageName) == 0) - { - //NuThreadID dataID = pData->GetDetails()->threadID; - //NuThreadID listID = pSearch->GetDetails()->threadID; - - listKind = pSearch->GetDetails()->entryKind; - - /* got a name match */ - if (dataKind != listKind && - (dataKind == FileDetails::kFileKindDataFork || dataKind == FileDetails::kFileKindRsrcFork) && - (listKind == FileDetails::kFileKindDataFork || listKind == FileDetails::kFileKindRsrcFork)) - { - /* looks good, hook it in here instead of the list */ - WMSG2("--- connecting forks of '%s' and '%s'\n", - pData->GetDetails()->origName, - pSearch->GetDetails()->origName); - pSearch->SetOtherFork(pData); - return; - } - } - - pSearch = pSearch->GetNext(); - } - - if (fpAddDataHead == nil) { - assert(fpAddDataTail == nil); - fpAddDataHead = fpAddDataTail = pData; - } else { - fpAddDataTail->SetNext(pData); - fpAddDataTail = pData; - } -} - -/* - * Free all entries in the FileAddData list. - */ -void -DiskArchive::FreeAddDataList(void) -{ - FileAddData* pData; - FileAddData* pNext; - - pData = fpAddDataHead; - while (pData != nil) { - pNext = pData->GetNext(); - delete pData->GetOtherFork(); - delete pData; - pData = pNext; - } - - fpAddDataHead = fpAddDataTail = nil; -} - - -/* - * =========================================================================== - * DiskArchive -- create subdir - * =========================================================================== - */ - -/* - * Create a subdirectory named "newName" in "pParentEntry". - */ -bool -DiskArchive::CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, - const char* newName) -{ - ASSERT(newName != nil && strlen(newName) > 0); - DiskEntry* pEntry = (DiskEntry*) pParentEntry; - ASSERT(pEntry != nil); - A2File* pFile = pEntry->GetA2File(); - ASSERT(pFile != nil); - DiskFS* pDiskFS = pFile->GetDiskFS(); - ASSERT(pDiskFS != nil); - - if (!pFile->IsDirectory()) { - ASSERT(false); - return false; - } - - DIError dierr; - A2File* pNewFile = nil; - DiskFS::CreateParms parms; - CString pathName; - time_t now = time(nil); - - /* - * Create the full path. - */ - if (pFile->IsVolumeDirectory()) { - pathName = newName; - } else { - pathName = pParentEntry->GetPathName(); - pathName += pParentEntry->GetFssep(); - pathName += newName; - } - ASSERT(strchr(newName, pParentEntry->GetFssep()) == nil); - - /* using NufxLib constants; they match with ProDOS */ - memset(&parms, 0, sizeof(parms)); - parms.pathName = pathName; - parms.fssep = pParentEntry->GetFssep(); - parms.storageType = kNuStorageDirectory; - parms.fileType = 0x0f; // ProDOS DIR - parms.auxType = 0; - parms.access = kNuAccessUnlocked; - parms.createWhen = now; - parms.modWhen = now; - - dierr = pDiskFS->CreateFile(&parms, &pNewFile); - if (dierr != kDIErrNone) { - CString errMsg; - errMsg.Format("Unable to create subdirectory: %s.\n", - DiskImgLib::DIStrError(dierr)); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - return false; - } - - if (InternalReload(pMsgWnd) != 0) - return false; - - return true; -} - - -/* - * =========================================================================== - * DiskArchive -- delete selection - * =========================================================================== - */ - -/* - * Compare DiskEntry display names in descending order (Z-A). - */ -/*static*/ int -DiskArchive::CompareDisplayNamesDesc(const void* ventry1, const void* ventry2) -{ - const DiskEntry* pEntry1 = *((const DiskEntry**) ventry1); - const DiskEntry* pEntry2 = *((const DiskEntry**) ventry2); - - return strcasecmp(pEntry2->GetDisplayName(), pEntry1->GetDisplayName()); -} - -/* - * Delete the records listed in the selection set. - * - * The DiskFS DeleteFile() function will not delete a subdirectory unless - * it is empty. This complicates matters somewhat for us, because the - * selection set isn't in any particular order. We need to sort on the - * pathname and then delete bottom-up. - * - * CiderPress does work to ensure that, if a subdir is selected, everything - * in that subdir is also selected. So if we just delete everything in the - * right order, we should be okay. - */ -bool -DiskArchive::DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) -{ - CString errMsg; - SelectionEntry* pSelEntry; - DiskEntry* pEntry; - DIError dierr; - bool retVal = false; - - SET_PROGRESS_BEGIN(); - - /* - * Start by copying the DiskEntry pointers out of the selection set and - * into an array. The selection set was created such that there is one - * entry in the set for each file. (The file viewer likes to have one - * entry for each thread.) - */ - int numEntries = pSelSet->GetNumEntries(); - ASSERT(numEntries > 0); - DiskEntry** entryArray = new DiskEntry*[numEntries]; - int idx = 0; - - pSelEntry = pSelSet->IterNext(); - while (pSelEntry != nil) { - pEntry = (DiskEntry*) pSelEntry->GetEntry(); - ASSERT(pEntry != nil); - - entryArray[idx++] = pEntry; - WMSG2("Added 0x%08lx '%s'\n", (long) entryArray[idx-1], - entryArray[idx-1]->GetDisplayName()); - - pSelEntry = pSelSet->IterNext(); - } - ASSERT(idx == numEntries); - - /* - * Sort the file array by descending filename. - */ - ::qsort(entryArray, numEntries, sizeof(DiskEntry*), CompareDisplayNamesDesc); - - /* - * Run through the sorted list, deleting each entry. - */ - for (idx = 0; idx < numEntries; idx++) { - A2File* pFile; - - pEntry = entryArray[idx]; - pFile = pEntry->GetA2File(); - - /* - * We shouldn't be here at all if the main volume were opened - * read-only. However, it's possible that the main is read-write - * and our sub-volumes are read-only (probably because we don't - * support write access to the filesystem). - */ - if (!pFile->GetDiskFS()->GetReadWriteSupported()) { - errMsg.Format("Unable to delete '%s' on '%s': operation not supported.", - pEntry->GetDisplayName(), pFile->GetDiskFS()->GetVolumeName()); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - goto bail; - } - - WMSG2(" Deleting '%s' from '%s'\n", pEntry->GetPathName(), - pFile->GetDiskFS()->GetVolumeName()); - SET_PROGRESS_UPDATE2(0, pEntry->GetPathName(), nil); - - /* - * Ask the DiskFS to delete the file. As soon as this completes, - * "pFile" is invalid and must not be dereferenced. - */ - dierr = pFile->GetDiskFS()->DeleteFile(pFile); - if (dierr != kDIErrNone) { - errMsg.Format("Unable to delete '%s' on '%s': %s.", - pEntry->GetDisplayName(), pFile->GetDiskFS()->GetVolumeName(), - DiskImgLib::DIStrError(dierr)); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - goto bail; - } - SET_PROGRESS_UPDATE(100); - - /* - * Be paranoid and zap the pointer, on the off chance somebody - * tries to redraw the content list from the deleted data. - * - * In practice we don't work this way -- the stuff that gets drawn - * on the screen comes out of GenericEntry, not A2File. If this - * changes we'll need to raise the "reload" flag here, before the - * reload, to prevent the ContentList from chasing a bad pointer. - */ - pEntry->SetA2File(nil); - } - - retVal = true; - -bail: - SET_PROGRESS_END(); - delete[] entryArray; - if (InternalReload(pMsgWnd) != 0) - retVal = false; - - return retVal; -} - -/* - * =========================================================================== - * DiskArchive -- rename files - * =========================================================================== - */ - - /* - * Rename a set of files, one at a time. - * - * If we rename a subdirectory, it could affect the next thing we try to - * rename (because we show the full path). We have to reload our file - * list from the DiskFS after each renamed subdir. The trouble is that - * this invalidates the data displayed in the ContentList, and we won't - * redraw the screen correctly. We can work around the problem by getting - * the pathname directly from the DiskFS instead of from DiskEntry, though - * it's not immediately obvious which is less confusing. - */ -bool -DiskArchive::RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) -{ - CString errMsg; - bool retVal = false; - - WMSG1("Renaming %d entries\n", pSelSet->GetNumEntries()); - - /* - * For each item in the selection set, bring up the "rename" dialog, - * and ask the GenericEntry to process it. - * - * If they hit "cancel" or there's an error, we still flush the - * previous changes. This is so that we don't have to create the - * same sort of deferred-write feature when renaming things in other - * sorts of archives (e.g. disk archives). - */ - SelectionEntry* pSelEntry = pSelSet->IterNext(); - while (pSelEntry != nil) { - RenameEntryDialog renameDlg(pMsgWnd); - DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry(); - - WMSG1(" Renaming '%s'\n", pEntry->GetPathName()); - if (!SetRenameFields(pMsgWnd, pEntry, &renameDlg)) - break; - - int result; - if (pEntry->GetA2File()->IsVolumeDirectory()) - result = IDIGNORE; // don't allow rename of volume dir - else - result = renameDlg.DoModal(); - if (result == IDOK) { - DIError dierr; - DiskFS* pDiskFS; - A2File* pFile; - - pFile = pEntry->GetA2File(); - pDiskFS = pFile->GetDiskFS(); - dierr = pDiskFS->RenameFile(pFile, renameDlg.fNewName); - if (dierr != kDIErrNone) { - errMsg.Format("Unable to rename '%s' to '%s': %s.", - pEntry->GetPathName(), renameDlg.fNewName, - DiskImgLib::DIStrError(dierr)); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - goto bail; - } - WMSG2("Rename of '%s' to '%s' succeeded\n", - pEntry->GetDisplayName(), renameDlg.fNewName); - } else if (result == IDCANCEL) { - WMSG0("Canceling out of remaining renames\n"); - break; - } else { - /* 3rd possibility is IDIGNORE, i.e. skip this entry */ - WMSG1("Skipping rename of '%s'\n", pEntry->GetDisplayName()); - } - - pSelEntry = pSelSet->IterNext(); - } - - /* reload GenericArchive from disk image */ - if (InternalReload(pMsgWnd) == kNuErrNone) - retVal = true; - -bail: - return retVal; -} - -/* - * Set up a RenameEntryDialog for the entry in "*pEntry". - * - * Returns "true" on success, "false" on failure. - */ -bool -DiskArchive::SetRenameFields(CWnd* pMsgWnd, DiskEntry* pEntry, - RenameEntryDialog* pDialog) -{ - DiskFS* pDiskFS; - - ASSERT(pEntry != nil); - - /* - * Figure out if we're allowed to change the entire path. (This is - * doing it the hard way, but what the hell.) - */ - long cap = GetCapability(GenericArchive::kCapCanRenameFullPath); - bool renameFullPath = (cap != 0); - - // a bit round-about, but it works - pDiskFS = pEntry->GetA2File()->GetDiskFS(); - - /* - * Make sure rename is allowed. It's nice to do these *before* putting - * up the rename dialog, so that the user doesn't do a bunch of typing - * before being told that it's pointless. - */ - if (!pDiskFS->GetReadWriteSupported()) { - CString errMsg; - errMsg.Format("Unable to rename '%s': operation not supported.", - pEntry->GetPathName()); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - return false; - } - if (pDiskFS->GetFSDamaged()) { - CString errMsg; - errMsg.Format("Unable to rename '%s': the disk it's on appears to be damaged.", - pEntry->GetPathName()); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - return false; - } - - pDialog->SetCanRenameFullPath(renameFullPath); - pDialog->fOldName = pEntry->GetPathName(); - pDialog->fFssep = pEntry->GetFssep(); - pDialog->fpArchive = this; - pDialog->fpEntry = pEntry; - - return true; -} - -/* - * Verify that the a name is suitable. Called by RenameEntryDialog and - * CreateSubdirDialog. - * - * Tests for context-specific syntax and checks for duplicates. - * - * Returns an empty string on success, or an error message on failure. - */ -CString -DiskArchive::TestPathName(const GenericEntry* pGenericEntry, - const CString& basePath, const CString& newName, char newFssep) const -{ - const DiskEntry* pEntry = (DiskEntry*) pGenericEntry; - DiskImg::FSFormat format; - CString pathName, errMsg; - DiskFS* pDiskFS; - - if (basePath.IsEmpty()) { - pathName = newName; - } else { - pathName = basePath; - pathName += newFssep; - pathName += newName; - } - - pDiskFS = pEntry->GetA2File()->GetDiskFS(); - format = pDiskFS->GetDiskImg()->GetFSFormat(); - - /* look for an existing file, but don't compare against self */ - A2File* existingFile; - existingFile = pDiskFS->GetFileByName(pathName); - if (existingFile != nil && existingFile != pEntry->GetA2File()) { - errMsg = "A file with that name already exists."; - goto bail; - } - - switch (format) { - case DiskImg::kFormatProDOS: - if (!DiskFSProDOS::IsValidFileName(newName)) - errMsg.LoadString(IDS_VALID_FILENAME_PRODOS); - break; - case DiskImg::kFormatDOS33: - case DiskImg::kFormatDOS32: - if (!DiskFSDOS33::IsValidFileName(newName)) - errMsg.LoadString(IDS_VALID_FILENAME_DOS); - break; - case DiskImg::kFormatPascal: - if (!DiskFSPascal::IsValidFileName(newName)) - errMsg.LoadString(IDS_VALID_FILENAME_PASCAL); - break; - case DiskImg::kFormatMacHFS: - if (!DiskFSHFS::IsValidFileName(newName)) - errMsg.LoadString(IDS_VALID_FILENAME_HFS); - break; - default: - errMsg = "Not supported by TestPathName!"; - } - -bail: - return errMsg; -} - - -/* - * =========================================================================== - * DiskArchive -- rename a volume - * =========================================================================== - */ - -/* - * Ask a DiskFS to change its volume name. - * - * Returns "true" on success, "false" on failure. - */ -bool -DiskArchive::RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, - const char* newName) -{ - DIError dierr; - CString errMsg; - bool retVal = true; - - dierr = pDiskFS->RenameVolume(newName); - if (dierr != kDIErrNone) { - errMsg.Format("Unable to rename volume: %s.\n", - DiskImgLib::DIStrError(dierr)); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - retVal = false; - /* fall through to reload anyway */ - } - - /* reload GenericArchive from disk image */ - if (InternalReload(pMsgWnd) != 0) - retVal = false; - - return retVal; -} - -/* - * Test a volume name for validity. - */ -CString -DiskArchive::TestVolumeName(const DiskFS* pDiskFS, - const char* newName) const -{ - DiskImg::FSFormat format; - CString errMsg; - - ASSERT(pDiskFS != nil); - ASSERT(newName != nil); - - format = pDiskFS->GetDiskImg()->GetFSFormat(); - - switch (format) { - case DiskImg::kFormatProDOS: - if (!DiskFSProDOS::IsValidVolumeName(newName)) - errMsg.LoadString(IDS_VALID_VOLNAME_PRODOS); - break; - case DiskImg::kFormatDOS33: - case DiskImg::kFormatDOS32: - if (!DiskFSDOS33::IsValidVolumeName(newName)) - errMsg.LoadString(IDS_VALID_VOLNAME_DOS); - break; - case DiskImg::kFormatPascal: - if (!DiskFSPascal::IsValidVolumeName(newName)) - errMsg.LoadString(IDS_VALID_VOLNAME_PASCAL); - break; - case DiskImg::kFormatMacHFS: - if (!DiskFSHFS::IsValidVolumeName(newName)) - errMsg.LoadString(IDS_VALID_VOLNAME_HFS); - break; - default: - errMsg = "Not supported by TestVolumeName!"; - } - - return errMsg; -} - - -/* - * =========================================================================== - * DiskArchive -- set file properties - * =========================================================================== - */ - -/* - * Set the properties of "pEntry" to what's in "pProps". - * - * [currently only supports file type, aux type, and access flags] - * - * Technically we should reload the GenericArchive from the NufxArchive, - * but the set of changes is pretty small, so we just make them here. - */ -bool -DiskArchive::SetProps(CWnd* pMsgWnd, GenericEntry* pGenericEntry, - const FileProps* pProps) -{ - DIError dierr; - DiskEntry* pEntry = (DiskEntry*) pGenericEntry; - A2File* pFile = pEntry->GetA2File(); - - dierr = pFile->GetDiskFS()->SetFileInfo(pFile, pProps->fileType, - pProps->auxType, pProps->access); - if (dierr != kDIErrNone) { - CString errMsg; - errMsg.Format("Unable to set file info: %s.\n", - DiskImgLib::DIStrError(dierr)); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - return false; - } - - /* do this in lieu of reloading GenericArchive */ - pEntry->SetFileType(pFile->GetFileType()); - pEntry->SetAuxType(pFile->GetAuxType()); - pEntry->SetAccess(pFile->GetAccess()); - - /* DOS 3.2/3.3 may change these as well */ - DiskImg::FSFormat fsFormat; - fsFormat = pFile->GetDiskFS()->GetDiskImg()->GetFSFormat(); - if (fsFormat == DiskImg::kFormatDOS32 || fsFormat == DiskImg::kFormatDOS33) { - WMSG0(" (reloading additional fields after DOS SFI)\n"); - pEntry->SetDataForkLen(pFile->GetDataLength()); - pEntry->SetCompressedLen(pFile->GetDataSparseLength()); - pEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious); - } - - /* clear the dirty flag in trivial cases */ - (void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly); - - return true; -} - - -/* - * =========================================================================== - * DiskArchive -- transfer files to another archive - * =========================================================================== - */ - -/* - * Transfer the selected files out of this archive and into another. - * - * In this case, it's files on a disk (with unspecified filesystem) to a NuFX - * archive. We get the open archive pointer and some options from "pXferOpts". - * - * The selection set was created with the "any" selection criteria, which - * means there's only one entry for each file regardless of whether it's - * forked or not. - */ -GenericArchive::XferStatus -DiskArchive::XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, - ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts) -{ - WMSG0("DiskArchive XferSelection!\n"); - unsigned char* dataBuf = nil; - unsigned char* rsrcBuf = nil; - FileDetails fileDetails; - CString errMsg, extractErrMsg, cmpStr; - CString fixedPathName; - XferStatus retval = kXferFailed; - - pXferOpts->fTarget->XferPrepare(pXferOpts); - - SelectionEntry* pSelEntry = pSelSet->IterNext(); - for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) { - long dataLen=-1, rsrcLen=-1; - DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry(); - int typeOverride = -1; - int result; - - ASSERT(dataBuf == nil); - ASSERT(rsrcBuf == nil); - - if (pEntry->GetDamaged()) { - WMSG1(" XFER skipping damaged entry '%s'\n", - pEntry->GetDisplayName()); - continue; - } - - /* - * Do a quick de-colonizing pass for non-ProDOS volumes, then prepend - * the subvolume name (if any). - */ - fixedPathName = pEntry->GetPathName(); - if (fixedPathName.IsEmpty()) - fixedPathName = _T("(no filename)"); - if (pEntry->GetFSFormat() != DiskImg::kFormatProDOS) - fixedPathName.Replace(PathProposal::kDefaultStoredFssep, '.'); - if (pEntry->GetSubVolName() != nil) { - CString tmpStr; - tmpStr = pEntry->GetSubVolName(); - tmpStr += (char)PathProposal::kDefaultStoredFssep; - tmpStr += fixedPathName; - fixedPathName = tmpStr; - } - - if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) { - /* this is the volume dir */ - WMSG1(" XFER not transferring volume dir '%s'\n", - fixedPathName); - continue; - } else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) { - if (pXferOpts->fPreserveEmptyFolders) { - /* if this is an empty directory, create a fake entry */ - cmpStr = fixedPathName; - cmpStr += (char)PathProposal::kDefaultStoredFssep; - - if (pSelSet->CountMatchingPrefix(cmpStr) == 0) { - WMSG1("FOUND empty dir '%s'\n", fixedPathName); - cmpStr += kEmptyFolderMarker; - dataBuf = new unsigned char[1]; - dataLen = 0; - fileDetails.entryKind = FileDetails::kFileKindDataFork; - fileDetails.storageName = cmpStr; - fileDetails.fileType = 0; // NON - fileDetails.access = - pEntry->GetAccess() | GenericEntry::kAccessInvisible; - goto have_stuff2; - } else { - WMSG1("NOT empty dir '%s'\n", fixedPathName); - } - } - - WMSG1(" XFER not transferring directory '%s'\n", - fixedPathName); - continue; - } - - WMSG3(" Xfer '%s' (data=%d rsrc=%d)\n", - fixedPathName, pEntry->GetHasDataFork(), - pEntry->GetHasRsrcFork()); - - dataBuf = nil; - dataLen = 0; - result = pEntry->ExtractThreadToBuffer(GenericEntry::kDataThread, - (char**) &dataBuf, &dataLen, &extractErrMsg); - if (result == IDCANCEL) { - WMSG0("Cancelled during data extract!\n"); - goto bail; /* abort anything that was pending */ - } else if (result != IDOK) { - errMsg.Format("Failed while extracting '%s': %s.", - fixedPathName, extractErrMsg); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - goto bail; - } - ASSERT(dataBuf != nil); - ASSERT(dataLen >= 0); - -#if 0 - if (pXferOpts->fConvDOSText && - DiskImg::UsesDOSFileStructure(pEntry->GetFSFormat()) && - pEntry->GetFileType() == kFileTypeTXT) - { - /* don't need to convert EOL, so just strip in place */ - long len; - unsigned char* ucp; - - WMSG1(" Converting DOS text in '%s'\n", fixedPathName); - for (ucp = dataBuf, len = dataLen; len > 0; len--, ucp++) - *ucp = *ucp & 0x7f; - } -#endif - -#if 0 // annoying to invoke PTX reformatter from here... ReformatHolder, etc. - if (pXferOpts->fConvPascalText && - pEntry->GetFSFormat() == DiskImg::kFormatPascal && - pEntry->GetFileType() == kFileTypePTX) - { - WMSG1("WOULD CONVERT ptx '%s'\n", fixedPathName); - } -#endif - - if (pEntry->GetHasRsrcFork()) { - rsrcBuf = nil; - rsrcLen = 0; - result = pEntry->ExtractThreadToBuffer(GenericEntry::kRsrcThread, - (char**) &rsrcBuf, &rsrcLen, &extractErrMsg); - if (result == IDCANCEL) { - WMSG0("Cancelled during rsrc extract!\n"); - goto bail; /* abort anything that was pending */ - } else if (result != IDOK) { - errMsg.Format("Failed while extracting '%s': %s.", - fixedPathName, extractErrMsg); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - goto bail; - } - } else { - ASSERT(rsrcBuf == nil); - } - - if (pEntry->GetHasDataFork() && pEntry->GetHasRsrcFork()) - fileDetails.entryKind = FileDetails::kFileKindBothForks; - else if (pEntry->GetHasDataFork()) - fileDetails.entryKind = FileDetails::kFileKindDataFork; - else if (pEntry->GetHasRsrcFork()) - fileDetails.entryKind = FileDetails::kFileKindRsrcFork; - else { - ASSERT(false); - fileDetails.entryKind = FileDetails::kFileKindUnknown; - } - - /* - * Set up the FileDetails. - */ - fileDetails.storageName = fixedPathName; - fileDetails.fileType = pEntry->GetFileType(); - fileDetails.access = pEntry->GetAccess(); -have_stuff2: - fileDetails.fileSysFmt = pEntry->GetSourceFS(); - fileDetails.fileSysInfo = PathProposal::kDefaultStoredFssep; - fileDetails.extraType = pEntry->GetAuxType(); - fileDetails.storageType = kNuStorageUnknown; /* let NufxLib deal */ - - time_t when; - when = time(nil); - UNIXTimeToDateTime(&when, &fileDetails.archiveWhen); - when = pEntry->GetModWhen(); - UNIXTimeToDateTime(&when, &fileDetails.modWhen); - when = pEntry->GetCreateWhen(); - UNIXTimeToDateTime(&when, &fileDetails.createWhen); - - pActionProgress->SetArcName(fileDetails.storageName); - if (pActionProgress->SetProgress(0) == IDCANCEL) { - retval = kXferCancelled; - goto bail; - } - - errMsg = pXferOpts->fTarget->XferFile(&fileDetails, &dataBuf, dataLen, - &rsrcBuf, rsrcLen); - if (!errMsg.IsEmpty()) { - WMSG0("XferFile failed!\n"); - errMsg.Format("Failed while transferring '%s': %s.", - pEntry->GetDisplayName(), (const char*) errMsg); - ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); - goto bail; - } - ASSERT(dataBuf == nil); - ASSERT(rsrcBuf == nil); - - if (pActionProgress->SetProgress(100) == IDCANCEL) { - retval = kXferCancelled; - goto bail; - } - } - - //MainWindow* pMainWin; - //pMainWin = (MainWindow*)::AfxGetMainWnd(); - //pMainWin->EventPause(1000); - - retval = kXferOK; - -bail: - if (retval != kXferOK) - pXferOpts->fTarget->XferAbort(pMsgWnd); - else - pXferOpts->fTarget->XferFinish(pMsgWnd); - delete[] dataBuf; - delete[] rsrcBuf; - return retval; -} - -/* - * Prepare for file transfers. - */ -void -DiskArchive::XferPrepare(const XferFileOptions* pXferOpts) -{ - WMSG0("DiskArchive::XferPrepare\n"); - - //fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase, - // pXferOpts->fAllowLowerCase); - //fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse, - // pXferOpts->fUseSparseBlocks); - fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true); - - //fXferStoragePrefix = pXferOpts->fStoragePrefix; - fpXferTargetFS = pXferOpts->fpTargetFS; -} - -/* - * Transfer a file to the disk image. Called from NufxArchive's XferSelection - * and clipboard "paste". - * - * "dataLen" and "rsrcLen" will be -1 if the corresponding fork doesn't - * exist. - * - * Returns 0 on success, nonzero on failure. - * - * On success, *pDataBuf and *pRsrcBuf are freed and set to nil. (It's - * necessary for the interface to work this way because the NufxArchive - * version just tucks the pointers into NufxLib structures.) - */ -CString -DiskArchive::XferFile(FileDetails* pDetails, unsigned char** pDataBuf, - long dataLen, unsigned char** pRsrcBuf, long rsrcLen) -{ - //const int kFileTypeTXT = 0x04; - DiskFS::CreateParms createParms; - DiskFS* pDiskFS; - CString errMsg; - DIError dierr = kDIErrNone; - - WMSG3(" XFER: transfer '%s' (dataLen=%ld rsrcLen=%ld)\n", - pDetails->storageName, dataLen, rsrcLen); - - ASSERT(pDataBuf != nil); - ASSERT(pRsrcBuf != nil); - - /* fill out CreateParms from FileDetails */ - ConvertFDToCP(pDetails, &createParms); - - if (fpXferTargetFS == nil) - pDiskFS = fpPrimaryDiskFS; - else - pDiskFS = fpXferTargetFS; - - /* - * Strip the high ASCII from DOS and RDOS text files, unless we're adding - * them to a DOS disk. Likewise, if we're adding non-DOS text files to - * a DOS disk, we need to add the high bit. - * - * DOS converts both TXT and SRC to 'T', so we have to handle both here. - * Ideally we'd just ask DOS, "do you think this is a text file?", but it's - * not worth adding a new interface just for that. - */ - bool srcIsDOS, dstIsDOS; - srcIsDOS = DiskImg::UsesDOSFileStructure(pDetails->fileSysFmt); - dstIsDOS = DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()); - if (dataLen > 0 && - (pDetails->fileType == kFileTypeTXT || pDetails->fileType == kFileTypeSRC)) - { - unsigned char* ucp = *pDataBuf; - long len = dataLen; - - if (srcIsDOS && !dstIsDOS) { - WMSG1(" Stripping high ASCII from '%s'\n", pDetails->storageName); - - while (len--) - *ucp++ &= 0x7f; - } else if (!srcIsDOS && dstIsDOS) { - WMSG1(" Adding high ASCII to '%s'\n", pDetails->storageName); - - while (len--) { - if (*ucp != '\0') - *ucp |= 0x80; - ucp++; - } - } else if (srcIsDOS && dstIsDOS) { - WMSG1(" --- not altering DOS-to-DOS text '%s'\n", - pDetails->storageName); - } else { - WMSG1(" --- non-DOS transfer '%s'\n", pDetails->storageName); - } - } - - /* add a file with one or two forks */ - if (createParms.storageType == kNuStorageDirectory) { - ASSERT(dataLen < 0 && rsrcLen < 0); - } else { - ASSERT(dataLen >= 0 || rsrcLen >= 0); // at least one fork - } - - /* if we still have something to write, write it */ - dierr = AddForksToDisk(pDiskFS, &createParms, *pDataBuf, dataLen, - *pRsrcBuf, rsrcLen); - if (dierr != kDIErrNone) { - errMsg.Format("%s", DiskImgLib::DIStrError(dierr)); - goto bail; - } - - /* clean up */ - delete[] *pDataBuf; - *pDataBuf = nil; - delete[] *pRsrcBuf; - *pRsrcBuf = nil; - -bail: - return errMsg; -} - - -/* - * Abort our progress. Not really possible, except by throwing the disk - * image away. - */ -void -DiskArchive::XferAbort(CWnd* pMsgWnd) -{ - WMSG0("DiskArchive::XferAbort\n"); - InternalReload(pMsgWnd); -} - -/* - * Transfer is finished. - */ -void -DiskArchive::XferFinish(CWnd* pMsgWnd) -{ - WMSG0("DiskArchive::XferFinish\n"); - InternalReload(pMsgWnd); -} +/* + * CiderPress + * Copyright (C) 2009 by CiderPress authors. All Rights Reserved. + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Bridge between DiskImg and GenericArchive. + */ +#include "stdafx.h" +#include "DiskArchive.h" +#include "NufxArchive.h" +#include "Preferences.h" +#include "Main.h" +#include "ImageFormatDialog.h" +#include "RenameEntryDialog.h" +#include "ConfirmOverwriteDialog.h" +#include "../diskimg/DiskImgDetail.h" + +static const char* kEmptyFolderMarker = ".$$EmptyFolder"; + + +/* + * =========================================================================== + * DiskEntry + * =========================================================================== + */ + +/* + * Extract data from a disk image. + * + * If "*ppText" is non-nil, the data will be read into the pointed-to buffer + * so long as it's shorter than *pLength bytes. The value in "*pLength" + * will be set to the actual length used. + * + * If "*ppText" is nil, the uncompressed data will be placed into a buffer + * allocated with "new[]". + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pErrMsg" holds an error + * message. + * + * "which" is an anonymous GenericArchive enum (e.g. "kDataThread"). + */ +int +DiskEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const +{ + DIError dierr; + A2FileDescr* pOpenFile = nil; + char* dataBuf = nil; + bool rsrcFork; + bool needAlloc = true; + int result = -1; + + ASSERT(fpFile != nil); + ASSERT(pErrMsg != nil); + *pErrMsg = ""; + + if (*ppText != nil) + needAlloc = false; + + if (GetDamaged()) { + *pErrMsg = "File is damaged"; + goto bail; + } + + if (which == kDataThread) + rsrcFork = false; + else if (which == kRsrcThread) + rsrcFork = true; + else { + *pErrMsg = "No such fork"; + goto bail; + } + + LONGLONG len; + if (rsrcFork) + len = fpFile->GetRsrcLength(); + else + len = fpFile->GetDataLength(); + + if (len == 0) { + if (needAlloc) { + *ppText = new char[1]; + **ppText = '\0'; + } + *pLength = 0; + result = IDOK; + goto bail; + } else if (len < 0) { + assert(rsrcFork); // forked files always have a data fork + *pErrMsg = "That fork doesn't exist"; + goto bail; + } + + dierr = fpFile->Open(&pOpenFile, true, rsrcFork); + if (dierr != kDIErrNone) { + *pErrMsg = "File open failed"; + goto bail; + } + + SET_PROGRESS_BEGIN(); + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, len, nil); + + if (needAlloc) { + dataBuf = new char[(int) len]; + if (dataBuf == nil) { + pErrMsg->Format("ERROR: allocation of %ld bytes failed", len); + goto bail; + } + } else { + if (*pLength < (long) len) { + pErrMsg->Format("ERROR: buf size %ld too short (%ld)", + *pLength, (long) len); + goto bail; + } + dataBuf = *ppText; + } + + dierr = pOpenFile->Read(dataBuf, (size_t) len); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) { + result = IDCANCEL; + } else { + pErrMsg->Format("File read failed: %s", + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + + if (needAlloc) + *ppText = dataBuf; + *pLength = (long) len; + result = IDOK; + +bail: + if (pOpenFile != nil) + pOpenFile->Close(); + if (result == IDOK) { + SET_PROGRESS_END(); + ASSERT(pErrMsg->IsEmpty()); + } else { + ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty()); + if (needAlloc) { + delete[] dataBuf; + ASSERT(*ppText == nil); + } + } + return result; +} + +/* + * Extract data from a thread to a file. Since we're not copying to memory, + * we can't assume that we're able to hold the entire file all at once. + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pMsg" holds an + * error message. + */ +int +DiskEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const +{ + A2FileDescr* pOpenFile = nil; + bool rsrcFork; + int result = -1; + + ASSERT(IDOK != -1 && IDCANCEL != -1); + ASSERT(fpFile != nil); + + if (which == kDataThread) + rsrcFork = false; + else if (which == kRsrcThread) + rsrcFork = true; + else { + /* if we handle disk images, make sure we disable "conv" */ + *pErrMsg = "No such fork"; + goto bail; + } + + LONGLONG len; + if (rsrcFork) + len = fpFile->GetRsrcLength(); + else + len = fpFile->GetDataLength(); + + if (len == 0) { + WMSG0("Empty fork\n"); + result = IDOK; + goto bail; + } else if (len < 0) { + assert(rsrcFork); // forked files always have a data fork + *pErrMsg = "That fork doesn't exist"; + goto bail; + } + + DIError dierr; + dierr = fpFile->Open(&pOpenFile, true, rsrcFork); + if (dierr != kDIErrNone) { + *pErrMsg = "Unable to open file on disk image"; + goto bail; + } + + dierr = CopyData(pOpenFile, outfp, conv, convHA, pErrMsg); + if (dierr != kDIErrNone) { + if (pErrMsg->IsEmpty()) { + pErrMsg->Format("Failed while copying data: %s\n", + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + + result = IDOK; + +bail: + if (pOpenFile != nil) + pOpenFile->Close(); + return result; +} + +/* + * Copy data from the open A2File to outfp, possibly converting EOL along + * the way. + */ +DIError +DiskEntry::CopyData(A2FileDescr* pOpenFile, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pMsg) const +{ + DIError dierr = kDIErrNone; + const int kChunkSize = 16384; + char buf[kChunkSize]; + //bool firstChunk = true; + //EOLType sourceType; + bool lastCR = false; + LONGLONG srcLen, dataRem; + + /* get the length of the open file */ + dierr = pOpenFile->Seek(0, DiskImgLib::kSeekEnd); + if (dierr != kDIErrNone) + goto bail; + srcLen = pOpenFile->Tell(); + dierr = pOpenFile->Rewind(); + if (dierr != kDIErrNone) + goto bail; + ASSERT(srcLen > 0); // empty files should've been caught earlier + + SET_PROGRESS_BEGIN(); + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, srcLen, nil); + + /* + * Loop until all data copied. + */ + dataRem = srcLen; + while (dataRem) { + int chunkLen; + + if (dataRem > kChunkSize) + chunkLen = kChunkSize; + else + chunkLen = (int) dataRem; + + /* read a chunk from the source file */ + dierr = pOpenFile->Read(buf, chunkLen); + if (dierr != kDIErrNone) { + pMsg->Format("File read failed: %s", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* write chunk to destination file */ + int err = GenericEntry::WriteConvert(outfp, buf, chunkLen, &conv, + &convHA, &lastCR); + if (err != 0) { + pMsg->Format("File write failed: %s", strerror(err)); + dierr = kDIErrGeneric; + goto bail; + } + + dataRem -= chunkLen; + //SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen)); + } + +bail: + pOpenFile->ClearProgressUpdater(); + SET_PROGRESS_END(); + return dierr; +} + + +/* + * Figure out whether or not we're allowed to change a file's type and + * aux type. + */ +bool +DiskEntry::GetFeatureFlag(Feature feature) const +{ + DiskImg::FSFormat format; + + format = fpFile->GetDiskFS()->GetDiskImg()->GetFSFormat(); + + switch (feature) { + case kFeatureCanChangeType: + { + //if (GetRecordKind() == kRecordKindVolumeDir) + // return false; + + switch (format) { + case DiskImg::kFormatProDOS: + case DiskImg::kFormatPascal: + case DiskImg::kFormatMacHFS: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatDOS33: + return true; + default: + return false; + } + } + case kFeaturePascalTypes: + { + switch (format) { + case DiskImg::kFormatPascal: + return true; + default: + return false; + } + } + case kFeatureDOSTypes: + { + switch (format) { + case DiskImg::kFormatDOS32: + case DiskImg::kFormatDOS33: + return true; + default: + return false; + } + } + case kFeatureHFSTypes: + { + switch (format) { + case DiskImg::kFormatMacHFS: + return true; + default: + return false; + } + } + case kFeatureHasFullAccess: + { + switch (format) { + case DiskImg::kFormatProDOS: + return true; + default: + return false; + } + } + case kFeatureHasSimpleAccess: + { + switch (format) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatCPM: + case DiskImg::kFormatMacHFS: + return true; + default: + return false; + } + } + case kFeatureHasInvisibleFlag: + { + switch(format) { + case DiskImg::kFormatProDOS: + case DiskImg::kFormatMacHFS: + return true; + default: + return false; + } + } + default: + WMSG1("Unexpected feature flag %d\n", feature); + assert(false); + return false; + } + + assert(false); + return false; +} + + +/* + * =========================================================================== + * DiskArchive + * =========================================================================== + */ + +/* + * Perform one-time initialization of the DiskLib library. + */ +/*static*/ CString +DiskArchive::AppInit(void) +{ + CString result(""); + DIError dierr; + long major, minor, bug; + + WMSG0("Initializing DiskImg library\n"); + + // set this before initializing, so we can see init debug msgs + DiskImgLib::Global::SetDebugMsgHandler(DebugMsgHandler); + + dierr = DiskImgLib::Global::AppInit(); + if (dierr != kDIErrNone) { + result.Format("DiskImg DLL failed to initialize: %s\n", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + DiskImgLib::Global::GetVersion(&major, &minor, &bug); + if (major != kDiskImgVersionMajor || minor < kDiskImgVersionMinor) { + result.Format("Older or incompatible version of DiskImg DLL found.\r\r" + "Wanted v%d.%d.x, found %ld.%ld.%ld.", + kDiskImgVersionMajor, kDiskImgVersionMinor, + major, minor, bug); + goto bail; + } + +bail: + return result; +} + +/* + * Perform one-time cleanup of DiskImgLib at shutdown time. + */ +/*static*/ void +DiskArchive::AppCleanup(void) +{ + DiskImgLib::Global::AppCleanup(); +} + + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +DiskArchive::DebugMsgHandler(const char* file, int line, const char* msg) +{ + ASSERT(file != nil); + ASSERT(msg != nil); + +#if defined(_DEBUG_LOG) + //fprintf(gLog, "%s(%d) : %s", file, line, msg); + fprintf(gLog, "%05u %s", gPid, msg); +#elif defined(_DEBUG) + _CrtDbgReport(_CRT_WARN, file, line, NULL, "%s", msg); +#else + /* do nothing */ +#endif +} + +/* + * Progress update callback, called from DiskImgLib during read/write + * operations. + * + * Returns "true" if we should continue; + */ +/*static*/ bool +DiskArchive::ProgressCallback(DiskImgLib::A2FileDescr* pFile, + DiskImgLib::di_off_t max, DiskImgLib::di_off_t current, void* state) +{ + int status; + + //::Sleep(10); + status = SET_PROGRESS_UPDATE(ComputePercent(current, max)); + if (status == IDCANCEL) { + WMSG0("IDCANCEL returned from Main progress updater\n"); + return false; + } + + return true; // tell DiskImgLib to continue what it's doing +} + +/* + * Progress update callback, called from DiskImgLib while scanning a volume + * during Open(). + * + * Returns "true" if we should continue; + */ +/*static*/ bool +DiskArchive::ScanProgressCallback(void* cookie, const char* str, int count) +{ + CString fmt; + bool cont; + + if (count == 0) + fmt = str; + else + fmt.Format("%s (%%d)", str); + cont = SET_PROGRESS_COUNTER_2(fmt, count); + + if (!cont) { + WMSG0("cancelled\n"); + } + + return cont; +} + + +/* + * Finish instantiating a DiskArchive object by opening an existing file. + */ +GenericArchive::OpenResult +DiskArchive::Open(const char* filename, bool readOnly, CString* pErrMsg) +{ + DIError dierr; + CString errMsg; + OpenResult result = kResultUnknown; + const Preferences* pPreferences = GET_PREFERENCES(); + + ASSERT(fpPrimaryDiskFS == nil); + ASSERT(filename != nil); + //ASSERT(ext != nil); + + ASSERT(pPreferences != nil); + + fIsReadOnly = readOnly; + + // special case for volume open + bool isVolume = false; + if (filename[0] >= 'A' && filename[0] <= 'Z' && + filename[1] == ':' && filename[2] == '\\' && + filename[3] == '\0') + { + isVolume = true; + } + + /* + * Open the image. This can be very slow for compressed images, + * especially 3.5" FDI images. + */ + { + CWaitCursor waitc; + + dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly); + if (dierr == kDIErrAccessDenied && !readOnly && !isVolume) { + // retry file open with read-only set + // don't do that for volumes -- assume they know what they want + WMSG0(" Retrying open with read-only set\n"); + fIsReadOnly = readOnly = true; + dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly); + } + if (dierr != kDIErrNone) { + if (dierr == kDIErrFileArchive) + result = kResultFileArchive; + else { + result = kResultFailure; + errMsg.Format("Unable to open '%s': %s.", filename, + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + } + + dierr = fDiskImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + result = kResultFailure; + errMsg.Format("Analysis of '%s' failed: %s", filename, + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* allow them to override sector order and filesystem, if requested */ + if (pPreferences->GetPrefBool(kPrQueryImageFormat)) { + ImageFormatDialog imf; + + imf.InitializeValues(&fDiskImg); + imf.fFileSource = filename; + imf.SetQueryDisplayFormat(false); + imf.SetAllowGenericFormats(false); + + if (imf.DoModal() != IDOK) { + WMSG0("User bailed on IMF dialog\n"); + result = kResultCancel; + goto bail; + } + + if (imf.fSectorOrder != fDiskImg.GetSectorOrder() || + imf.fFSFormat != fDiskImg.GetFSFormat()) + { + WMSG0("Initial values overridden, forcing img format\n"); + dierr = fDiskImg.OverrideFormat(fDiskImg.GetPhysicalFormat(), + imf.fFSFormat, imf.fSectorOrder); + if (dierr != kDIErrNone) { + result = kResultFailure; + errMsg.Format("Unable to access disk image using selected" + " parameters. Error: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + } + } + + if (fDiskImg.GetFSFormat() == DiskImg::kFormatUnknown || + fDiskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + result = kResultFailure; + errMsg.Format("Unable to identify filesystem on '%s'", filename); + goto bail; + } + + /* create an appropriate DiskFS object */ + fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS(); + if (fpPrimaryDiskFS == nil) { + /* unknown FS should've been caught above! */ + ASSERT(false); + result = kResultFailure; + errMsg.Format("Format of '%s' not recognized.", filename); + goto bail; + } + + fpPrimaryDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled); + + /* + * Scan all files and on the disk image, and recursively descend into + * sub-volumes. Can be slow on physical volumes. + * + * This is really only useful for ProDOS and HFS disks. Nothing else + * can be large enough to really get slow, and nothing else is likely + * to show up in a large multi-partition image. + * + * THOUGHT: only show the dialog if the volume is over a certain size. + */ + { + MainWindow* pMain = GET_MAIN_WINDOW(); + ProgressCounterDialog* pProgress; + + pProgress = new ProgressCounterDialog; + pProgress->Create(_T("Examining contents, please wait..."), pMain); + pProgress->SetCounterFormat("Scanning..."); + pProgress->CenterWindow(); + //pMain->PeekAndPump(); // redraw + CWaitCursor waitc; + + /* set up progress dialog and scan all files */ + pMain->SetProgressCounterDialog(pProgress); + fDiskImg.SetScanProgressCallback(ScanProgressCallback, this); + + dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull); + + fDiskImg.SetScanProgressCallback(nil, nil); + pMain->SetProgressCounterDialog(nil); + pProgress->DestroyWindow(); + + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) { + result = kResultCancel; + } else { + result = kResultFailure; + errMsg.Format("Error reading list of files from disk: %s", + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + } + + if (LoadContents() != 0) { + result = kResultFailure; + errMsg.Format("Failed while loading contents of disk image."); + goto bail; + } + + /* + * Force read-only flag if underlying FS doesn't allow RW. We need to + * consider embedded filesystems, so we only set RO if none of the + * filesystems are writable. + * + * BUG: this only checks the first level. Should be fully recursive. + */ + if (!fpPrimaryDiskFS->GetReadWriteSupported()) { + const DiskFS::SubVolume* pSubVol; + + fIsReadOnly = true; + pSubVol = fpPrimaryDiskFS->GetNextSubVolume(nil); + while (pSubVol != nil) { + if (pSubVol->GetDiskFS()->GetReadWriteSupported()) { + fIsReadOnly = false; + break; + } + + pSubVol = fpPrimaryDiskFS->GetNextSubVolume(pSubVol); + } + } + + /* force read-only if the primary is damaged */ + if (fpPrimaryDiskFS->GetFSDamaged()) + fIsReadOnly = true; + /* force read-only if the DiskImg thinks a wrapper is damaged */ + if (fpPrimaryDiskFS->GetDiskImg()->GetReadOnly()) + fIsReadOnly = true; + +// /* force read-only on .gz/.zip unless pref allows */ +// if (fDiskImg.GetOuterFormat() != DiskImg::kOuterFormatNone) { +// if (pPreferences->GetPrefBool(kPrWriteZips) == 0) +// fIsReadOnly = true; +// } + + SetPathName(filename); + result = kResultSuccess; + + /* set any preferences-based settings */ + PreferencesChanged(); + +bail: + *pErrMsg = errMsg; + if (!errMsg.IsEmpty()) { + assert(result == kResultFailure); + delete fpPrimaryDiskFS; + fpPrimaryDiskFS = nil; + } else { + assert(result != kResultFailure); + } + return result; +} + + +/* + * Finish instantiating a DiskArchive object by creating a new archive. + * + * Returns an error string on failure, or "" on success. + */ +CString +DiskArchive::New(const char* fileName, const void* vOptions) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + NewOptions* pOptions = (NewOptions*) vOptions; + CString volName; + long numBlocks = -1; + long numTracks = -1; + int numSectors; + CString retmsg; + DIError dierr; + bool allowLowerCase; + + ASSERT(fileName != nil); + ASSERT(pOptions != nil); + + allowLowerCase = pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0; + + switch (pOptions->base.format) { + case DiskImg::kFormatUnknown: + numBlocks = pOptions->blank.numBlocks; + break; + case DiskImg::kFormatProDOS: + volName = pOptions->prodos.volName; + numBlocks = pOptions->prodos.numBlocks; + break; + case DiskImg::kFormatPascal: + volName = pOptions->pascalfs.volName; + numBlocks = pOptions->pascalfs.numBlocks; + break; + case DiskImg::kFormatMacHFS: + volName = pOptions->hfs.volName; + numBlocks = pOptions->hfs.numBlocks; + break; + case DiskImg::kFormatDOS32: + numTracks = pOptions->dos.numTracks; + numSectors = pOptions->dos.numSectors; + + if (numTracks < DiskFSDOS33::kMinTracks || + numTracks > DiskFSDOS33::kMaxTracks) + { + retmsg.Format("Invalid DOS32 track count"); + goto bail; + } + if (numSectors != 13) { + retmsg.Format("Invalid DOS32 sector count"); + goto bail; + } + if (pOptions->dos.allocDOSTracks) + volName = "DOS"; + break; + case DiskImg::kFormatDOS33: + numTracks = pOptions->dos.numTracks; + numSectors = pOptions->dos.numSectors; + + if (numTracks < DiskFSDOS33::kMinTracks || + numTracks > DiskFSDOS33::kMaxTracks) + { + retmsg.Format("Invalid DOS33 track count"); + goto bail; + } + if (numSectors != 16 && numSectors != 32) { // no 13-sector (yet) + retmsg.Format("Invalid DOS33 sector count"); + goto bail; + } + if (pOptions->dos.allocDOSTracks) + volName = "DOS"; + break; + default: + retmsg.Format("Unsupported disk format"); + goto bail; + } + + WMSG4("DiskArchive: new '%s' %ld %s in '%s'\n", + (const char*)volName, numBlocks, + DiskImg::ToString(pOptions->base.format), fileName); + + bool canSkipFormat; + if (IsWin9x()) + canSkipFormat = false; + else + canSkipFormat = true; + + /* + * Create an image with the appropriate characteristics. We set + * "skipFormat" because we know this will be a brand-new file, and + * we're not currently creating nibble images. + * + * GLITCH: under Win98/ME, brand-new files contain the previous contents + * of the hard drive. We need to explicitly zero them out. We don't + * want to do it under Win2K/XP because it can be slow for larger + * volumes. + */ + if (numBlocks > 0) { + dierr = fDiskImg.CreateImage(fileName, nil, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + nil, + pOptions->base.sectorOrder, + DiskImg::kFormatGenericProDOSOrd, // arg must be generic + numBlocks, + canSkipFormat); + } else { + ASSERT(numTracks > 0); + dierr = fDiskImg.CreateImage(fileName, nil, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + nil, + pOptions->base.sectorOrder, + DiskImg::kFormatGenericProDOSOrd, // arg must be generic + numTracks, numSectors, + canSkipFormat); + } + if (dierr != kDIErrNone) { + retmsg.Format("Unable to create disk image: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + if (pOptions->base.format == DiskImg::kFormatUnknown) + goto skip_format; + + if (pOptions->base.format == DiskImg::kFormatDOS33 || + pOptions->base.format == DiskImg::kFormatDOS32) + fDiskImg.SetDOSVolumeNum(pOptions->dos.volumeNum); + + /* + * If we don't allow lower case in ProDOS filenames, don't allow them + * in volume names either. This works because we don't allow ' ' in + * volume names; otherwise we'd need to invoke a ProDOS-specific call + * to convert the ' ' to '.'. (Or we could just do it ourselves.) + * + * We can't ask the ProDOS DiskFS to force upper case for us because + * the ProDOS DiskFS object doesn't yet exist. + */ + if (pOptions->base.format == DiskImg::kFormatProDOS && !allowLowerCase) + volName.MakeUpper(); + + /* format it */ + dierr = fDiskImg.FormatImage(pOptions->base.format, volName); + if (dierr != kDIErrNone) { + retmsg.Format("Unable to format disk image: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS(false); + if (fpPrimaryDiskFS == nil) { + retmsg.Format("Unable to create DiskFS."); + goto bail; + } + + /* prep it */ + dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull); + if (dierr != kDIErrNone) { + retmsg.Format("Error reading list of files from disk: %s", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* this is pretty meaningless, but do it to ensure we're initialized */ + if (LoadContents() != 0) { + retmsg.Format("Failed while loading contents of disk image."); + goto bail; + } + +skip_format: + SetPathName(fileName); + + /* set any preferences-based settings */ + PreferencesChanged(); + +bail: + return retmsg; +} + +/* + * Close the DiskArchive ojbect. + */ +CString +DiskArchive::Close(void) +{ + if (fpPrimaryDiskFS != nil) { + WMSG0("DiskArchive shutdown closing disk image\n"); + delete fpPrimaryDiskFS; + fpPrimaryDiskFS = nil; + } + + DIError dierr; + dierr = fDiskImg.CloseImage(); + if (dierr != kDIErrNone) { + MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); + CString msg, failed; + + msg.Format("Failed while closing disk image: %s.", + DiskImgLib::DIStrError(dierr)); + failed.LoadString(IDS_FAILED); + WMSG1("During close: %s\n", (const char*) msg); + + pMainWin->MessageBox(msg, failed, MB_OK); + } + + return ""; +} + +/* + * Flush the DiskArchive object. + * + * Most of the stuff we do with disk images goes straight through, but in + * the case of compressed disks we don't normally re-compress them until + * it's time to close them. This forces us to update the copy on disk. + * + * Returns an empty string on success, or an error message on failure. + */ +CString +DiskArchive::Flush(void) +{ + DIError dierr; + CWaitCursor waitc; + + assert(fpPrimaryDiskFS != nil); + + dierr = fpPrimaryDiskFS->Flush(DiskImg::kFlushAll); + if (dierr != kDIErrNone) { + CString errMsg; + + errMsg.Format("Attempt to flush the current archive failed: %s.", + DiskImgLib::DIStrError(dierr)); + return errMsg; + } + + return ""; +} + +/* + * Returns "true" if the archive has un-flushed modifications pending. + */ +bool +DiskArchive::IsModified(void) const +{ + assert(fpPrimaryDiskFS != nil); + + return fpPrimaryDiskFS->GetDiskImg()->GetDirtyFlag(); +} + +/* + * Return an description of the disk archive, suitable for display in the + * main title bar. + */ +void +DiskArchive::GetDescription(CString* pStr) const +{ + if (fpPrimaryDiskFS == nil) + return; + + if (fpPrimaryDiskFS->GetVolumeID() != nil) + pStr->Format("Disk Image - %s", fpPrimaryDiskFS->GetVolumeID()); +} + + +/* + * Load the contents of a "disk archive". + * + * Returns 0 on success. + */ +int +DiskArchive::LoadContents(void) +{ + int result; + + WMSG0("DiskArchive LoadContents\n"); + ASSERT(fpPrimaryDiskFS != nil); + + { + MainWindow* pMain = GET_MAIN_WINDOW(); + ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog; + pWaitDlg->Create(IDD_LOADING, pMain); + pWaitDlg->CenterWindow(); + pMain->PeekAndPump(); // redraw + CWaitCursor waitc; + + result = LoadDiskFSContents(fpPrimaryDiskFS, ""); + + SET_PROGRESS_COUNTER(-1); + + pWaitDlg->DestroyWindow(); + //pMain->PeekAndPump(); // redraw + } + + return result; +} + +/* + * Reload the stuff from the underlying DiskFS. + * + * This also does a "lite" flush of the disk data. For files that are + * essentially being written as we go, this does little more than clear + * the "dirty" flag. Files that need to be recompressed or have some + * other slow operation remain dirty. + * + * We don't need to do the flush as part of the reload -- we can load the + * contents with everything in a perfectly dirty state. We don't need to + * do it at all. We do it to keep the "dirty" flag clear when nothing is + * really dirty, and we do it here because almost all of our functions call + * "reload" after making changes, which makes it convenient to call from here. + */ +CString +DiskArchive::Reload() +{ + fReloadFlag = true; // tell everybody that cached data is invalid + + (void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly); + + DeleteEntries(); // a GenericArchive operation + + if (LoadContents() != 0) + return "Disk image reload failed."; + + return ""; +} + +/* + * Reload the contents of the archive, showing an error message if the + * reload fails. + * + * Returns 0 on success, -1 on failure. + */ +int +DiskArchive::InternalReload(CWnd* pMsgWnd) +{ + CString errMsg; + + errMsg = Reload(); + + if (!errMsg.IsEmpty()) { + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return -1; + } + + return 0; +} + +/* + * Load the contents of a DiskFS. + * + * Recursively handle sub-volumes. "volName" holds the name of the + * sub-volume as it should appear in the list. + */ +int +DiskArchive::LoadDiskFSContents(DiskFS* pDiskFS, const char* volName) +{ + static const char* kBlankFileName = ""; + A2File* pFile; + DiskEntry* pNewEntry; + DiskFS::SubVolume* pSubVol; + const Preferences* pPreferences = GET_PREFERENCES(); + bool wantCoerceDOSFilenames = false; + CString ourSubVolName; + + wantCoerceDOSFilenames = pPreferences->GetPrefBool(kPrCoerceDOSFilenames); + + WMSG2("Notes for disk image '%s':\n%s", + volName, pDiskFS->GetDiskImg()->GetNotes()); + + ASSERT(pDiskFS != nil); + pFile = pDiskFS->GetNextFile(nil); + while (pFile != nil) { + pNewEntry = new DiskEntry(pFile); + if (pNewEntry == nil) + return -1; + + CString path(pFile->GetPathName()); + if (path.IsEmpty()) + path = kBlankFileName; + if (DiskImg::UsesDOSFileStructure(pFile->GetFSFormat()) && + wantCoerceDOSFilenames) + { + InjectLowercase(&path); + } + pNewEntry->SetPathName(path); + if (volName[0] != '\0') + pNewEntry->SetSubVolName(volName); + pNewEntry->SetFssep(pFile->GetFssep()); + pNewEntry->SetFileType(pFile->GetFileType()); + pNewEntry->SetAuxType(pFile->GetAuxType()); + pNewEntry->SetAccess(pFile->GetAccess()); + if (pFile->GetCreateWhen() == 0) + pNewEntry->SetCreateWhen(kDateNone); + else + pNewEntry->SetCreateWhen(pFile->GetCreateWhen()); + if (pFile->GetModWhen() == 0) + pNewEntry->SetModWhen(kDateNone); + else + pNewEntry->SetModWhen(pFile->GetModWhen()); + pNewEntry->SetSourceFS(pFile->GetFSFormat()); + pNewEntry->SetHasDataFork(true); + if (pFile->IsVolumeDirectory()) { + /* volume directory entry; only on ProDOS/HFS */ + ASSERT(pFile->GetRsrcLength() < 0); + pNewEntry->SetRecordKind(GenericEntry::kRecordKindVolumeDir); + //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetCompressedLen(pFile->GetDataLength()); + } else if (pFile->IsDirectory()) { + /* directory entry */ + ASSERT(pFile->GetRsrcLength() < 0); + pNewEntry->SetRecordKind(GenericEntry::kRecordKindDirectory); + //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetCompressedLen(pFile->GetDataLength()); + } else if (pFile->GetRsrcLength() >= 0) { + /* has resource fork */ + pNewEntry->SetRecordKind(GenericEntry::kRecordKindForkedFile); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetRsrcForkLen(pFile->GetRsrcLength()); + //pNewEntry->SetUncompressedLen( + // pFile->GetDataLength() + pFile->GetRsrcLength() ); + pNewEntry->SetCompressedLen( + pFile->GetDataSparseLength() + pFile->GetRsrcSparseLength() ); + pNewEntry->SetHasRsrcFork(true); + } else { + /* just data fork */ + pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile); + //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetCompressedLen(pFile->GetDataSparseLength()); + } + + switch (pNewEntry->GetSourceFS()) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatUNIDOS: + case DiskImg::kFormatGutenberg: + pNewEntry->SetFormatStr("DOS"); + break; + case DiskImg::kFormatProDOS: + pNewEntry->SetFormatStr("ProDOS"); + break; + case DiskImg::kFormatPascal: + pNewEntry->SetFormatStr("Pascal"); + break; + case DiskImg::kFormatCPM: + pNewEntry->SetFormatStr("CP/M"); + break; + case DiskImg::kFormatMSDOS: + pNewEntry->SetFormatStr("MS-DOS"); + break; + case DiskImg::kFormatRDOS33: + case DiskImg::kFormatRDOS32: + case DiskImg::kFormatRDOS3: + pNewEntry->SetFormatStr("RDOS"); + break; + case DiskImg::kFormatMacHFS: + pNewEntry->SetFormatStr("HFS"); + break; + default: + pNewEntry->SetFormatStr("???"); + break; + } + + pNewEntry->SetDamaged(pFile->GetQuality() == A2File::kQualityDamaged); + pNewEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious); + + AddEntry(pNewEntry); + + /* this is not very useful -- all the heavy lifting was done earlier */ + if ((GetNumEntries() % 100) == 0) + SET_PROGRESS_COUNTER(GetNumEntries()); + + pFile = pDiskFS->GetNextFile(pFile); + } + + /* + * Load all sub-volumes. + * + * We define the sub-volume name to use for the next layer down. We + * prepend an underscore to the unmodified name. So long as the volume + * name is a valid Windows path -- which should hold true for most disks, + * though possibly not for Pascal -- it can be extracted directly with + * its full path with no risk of conflict. (The extraction code relies + * on this, so don't put a ':' in the subvol name or Windows will choke.) + */ + pSubVol = pDiskFS->GetNextSubVolume(nil); + while (pSubVol != nil) { + CString concatSubVolName; + const char* subVolName; + int ret; + + subVolName = pSubVol->GetDiskFS()->GetVolumeName(); + if (subVolName == nil) + subVolName = "+++"; // call it *something* + + if (volName[0] == '\0') + concatSubVolName.Format("_%s", subVolName); + else + concatSubVolName.Format("%s_%s", volName, subVolName); + ret = LoadDiskFSContents(pSubVol->GetDiskFS(), concatSubVolName); + if (ret != 0) + return ret; + pSubVol = pDiskFS->GetNextSubVolume(pSubVol); + } + + return 0; +} + + +/* + * User has updated their preferences. Take note. + * + * Setting preferences in a DiskFS causes those prefs to be pushed down + * to all sub-volumes. + */ +void +DiskArchive::PreferencesChanged(void) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + + if (fpPrimaryDiskFS != nil) { + fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase, + pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0); + fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse, + pPreferences->GetPrefBool(kPrProDOSUseSparse) != 0); + } +} + + +/* + * Report on what this disk image is capable of. + */ +long +DiskArchive::GetCapability(Capability cap) +{ + switch (cap) { + case kCapCanTest: + return false; + break; + case kCapCanRenameFullPath: + return false; + break; + case kCapCanRecompress: + return false; + break; + case kCapCanEditComment: + return false; + break; + case kCapCanAddDisk: + return false; + break; + case kCapCanConvEOLOnAdd: + return true; + break; + case kCapCanCreateSubdir: + return true; + break; + case kCapCanRenameVolume: + return true; + break; + default: + ASSERT(false); + return -1; + break; + } +} + + +/* + * =========================================================================== + * DiskArchive -- add files + * =========================================================================== + */ + +/* + * Process a bulk "add" request. + * + * Returns "true" on success, "false" on failure. + */ +bool +DiskArchive::BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) +{ + NuError nerr; + CString errMsg; + char curDir[MAX_PATH] = ""; + bool retVal = false; + + WMSG2("Opts: '%s' typePres=%d\n", + pAddOpts->fStoragePrefix, pAddOpts->fTypePreservation); + WMSG3(" sub=%d strip=%d ovwr=%d\n", + pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames, + pAddOpts->fOverwriteExisting); + + ASSERT(fpAddDataHead == nil); + + /* these reset on every new add */ + fOverwriteExisting = false; + fOverwriteNoAsk = false; + + /* we screen for clashes with existing files later; this just ensures + "batch uniqueness" */ + fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true); + + /* + * Save the current directory and change to the one from the file dialog. + */ + const char* buf = pAddOpts->GetFileNames(); + WMSG2("Selected path = '%s' (offset=%d)\n", buf, + pAddOpts->GetFileNameOffset()); + + if (GetCurrentDirectory(sizeof(curDir), curDir) == 0) { + errMsg = "Unable to get current directory.\n"; + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + goto bail; + } + if (SetCurrentDirectory(buf) == false) { + errMsg.Format("Unable to set current directory to '%s'.\n", buf); + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + goto bail; + } + + buf += pAddOpts->GetFileNameOffset(); + while (*buf != '\0') { + WMSG1(" file '%s'\n", buf); + + /* add the file, calling DoAddFile via the generic AddFile */ + nerr = AddFile(pAddOpts, buf, &errMsg); + if (nerr != kNuErrNone) { + if (errMsg.IsEmpty()) + errMsg.Format("Failed while adding file '%s': %s.", + (LPCTSTR) buf, NuStrError(nerr)); + if (nerr != kNuErrAborted) { + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + } + goto bail; + } + + buf += strlen(buf)+1; + } + + if (fpAddDataHead == nil) { + CString title; + title.LoadString(IDS_MB_APP_NAME); + errMsg = "No files added.\n"; + pActionProgress->MessageBox(errMsg, title, MB_OK | MB_ICONWARNING); + } else { + /* add all pending files */ + retVal = true; + errMsg = ProcessFileAddData(pAddOpts->fpTargetDiskFS, + pAddOpts->fConvEOL); + if (!errMsg.IsEmpty()) { + CString title; + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + retVal = false; + } + + /* success or failure, reload the contents */ + errMsg = Reload(); + if (!errMsg.IsEmpty()) + retVal = false; + } + +bail: + FreeAddDataList(); + if (SetCurrentDirectory(curDir) == false) { + errMsg.Format("Unable to reset current directory to '%s'.\n", buf); + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + // bummer, but don't signal failure + } + return retVal; +} + +/* + * Add a file to a disk image. + * + * Unfortunately we can't just add the files here. We need to figure out + * which pairs of files should be combined into a single "extended" file. + * (Yes, the cursed forked files strike again.) + * + * The way you tell if two files should be one is by comparing their + * filenames and type info. If they match, and one is a data fork and + * one is a resource fork, we have a single split file. + * + * We have to be careful here because we don't know which will be seen + * first and whether they'll be adjacent. We have to dig through the + * list of previously-added files for a match (O(n^2) behavior currently). + * + * We also have to compare the right filename. Comparing the Windows + * filename is a bad idea, because by definition one of them has a resource + * fork tag on it. We need to compare the normalized filename before + * the ProDOS normalizer/uniqifier gets a chance to mangle it. As luck + * would have it, that's exactly what we have in "storageName". + * + * For a NuFX archive, NufxLib does all this nonsense for us, but we have + * to manage it ourselves here. The good news is that, since we have to + * wade through all the filenames, we have an opportunity to make the names + * unique. So long as we ensure that the names we have don't clash with + * anything currently on the disk, we know that anything we add that does + * clash is running into something we just added, which means we can turn + * on CreateFile's "make unique" feature and let the filesystem-specific + * code handle uniqueness. + * + * Any fields we want to keep from the NuFileDetails struct need to be + * copied out. It's a "hairy" struct, so we need to duplicate the strings. + */ +NuError +DiskArchive::DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails) +{ + NuError nuerr = kNuErrNone; + DiskFS* pDiskFS = pAddOpts->fpTargetDiskFS; + + DIError dierr; + int neededLen = 64; // reasonable guess + char* fsNormalBuf = nil; + + WMSG2(" +++ ADD file: orig='%s' stor='%s'\n", + pDetails->origName, pDetails->storageName); + +retry: + /* + * Convert "storageName" to a filesystem-normalized path. + */ + delete[] fsNormalBuf; + fsNormalBuf = new char[neededLen]; + dierr = pDiskFS->NormalizePath(pDetails->storageName, + PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen); + if (dierr == kDIErrDataOverrun) { + /* not long enough, try again *once* */ + delete[] fsNormalBuf; + fsNormalBuf = new char[neededLen]; + dierr = pDiskFS->NormalizePath(pDetails->storageName, + PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen); + } + if (dierr != kDIErrNone) { + nuerr = kNuErrInternal; + goto bail; + } + + /* + * Test to see if the file already exists. If it does, give the user + * the opportunity to rename it, overwrite the original, or skip + * adding it. + * + * The FS-normalized path may not reflect the actual storage name, + * because some features (like ProDOS "allow lower case") aren't + * factored in until later. However, it should be close enough -- it + * has to be, or we'd be in trouble for saying it's going to overwrite + * the file in the archive. + */ + A2File* pExisting; + pExisting = pDiskFS->GetFileByName(fsNormalBuf); + if (pExisting != nil) { + NuResult result; + + result = HandleReplaceExisting(pExisting, pDetails); + if (result == kNuAbort) { + nuerr = kNuErrAborted; + goto bail; + } else if (result == kNuSkip) { + nuerr = kNuErrSkipped; + goto bail; + } else if (result == kNuRename) { + goto retry; + } else if (result == kNuOverwrite) { + /* delete the existing file immediately */ + WMSG1(" Deleting existing file '%s'\n", fsNormalBuf); + dierr = pDiskFS->DeleteFile(pExisting); + if (dierr != kDIErrNone) { + // Would be nice to show a dialog and explain *why*, but + // I'm not sure we have a window here. + WMSG1(" Deletion failed (err=%d)\n", dierr); + goto bail; + } + } else { + WMSG1("GLITCH: bad return %d from HandleReplaceExisting\n",result); + assert(false); + nuerr = kNuErrInternal; + goto bail; + } + } + + /* + * Put all the goodies into a new FileAddData object, and add it to + * the end of the list. + */ + FileAddData* pAddData; + pAddData = new FileAddData(pDetails, fsNormalBuf); + if (pAddData == nil) { + nuerr = kNuErrMalloc; + goto bail; + } + + WMSG1("FSNormalized is '%s'\n", pAddData->GetFSNormalPath()); + + AddToAddDataList(pAddData); + +bail: + delete[] fsNormalBuf; + return nuerr; +} + +/* + * A file we're adding clashes with an existing file. Decide what to do + * about it. + * + * Returns one of the following: + * kNuOverwrite - overwrite the existing file + * kNuSkip - skip adding the existing file + * kNuRename - user wants to rename the file + * kNuAbort - cancel out of the entire add process + * + * Side effects: + * Sets fOverwriteExisting and fOverwriteNoAsk if a "to all" button is hit + * Replaces pDetails->storageName if the user elects to rename + */ +NuResult +DiskArchive::HandleReplaceExisting(const A2File* pExisting, + FileDetails* pDetails) +{ + NuResult result; + + if (fOverwriteNoAsk) { + if (fOverwriteExisting) + return kNuOverwrite; + else + return kNuSkip; + } + + ConfirmOverwriteDialog confOvwr; + + confOvwr.fExistingFile = pExisting->GetPathName(); + confOvwr.fExistingFileModWhen = pExisting->GetModWhen(); + + PathName srcPath(pDetails->origName); + confOvwr.fNewFileSource = pDetails->origName; // or storageName? + confOvwr.fNewFileModWhen = srcPath.GetModWhen(); + + if (confOvwr.DoModal() == IDCANCEL) { + WMSG0("User cancelled out of add-to-diskimg replace-existing\n"); + return kNuAbort; + } + + if (confOvwr.fResultRename) { + /* + * Replace the name in FileDetails. They were asked to modify + * the already-normalized version of the filename. We will run + * it back through the FS-specific normalizer, which will handle + * any oddities they type in. + * + * We don't want to run it through PathProposal.LocalToArchive + * because that'll strip out ':' in the pathnames. + * + * Ideally the rename dialog would have a way to validate the + * full path and reject "OK" if it's not valid. Instead, we just + * allow the FS normalizer to force the filename to be valid. + */ + pDetails->storageName = confOvwr.fExistingFile; + WMSG1("Trying rename to '%s'\n", pDetails->storageName); + return kNuRename; + } + + if (confOvwr.fResultApplyToAll) { + fOverwriteNoAsk = true; + if (confOvwr.fResultOverwrite) + fOverwriteExisting = true; + else + fOverwriteExisting = false; + } + if (confOvwr.fResultOverwrite) + result = kNuOverwrite; + else + result = kNuSkip; + + return result; +} + + +/* + * Process the list of pending file adds. + * + * This is where the rubber (finally!) meets the road. + */ +CString +DiskArchive::ProcessFileAddData(DiskFS* pDiskFS, int addOptsConvEOL) +{ + CString errMsg; + FileAddData* pData; + unsigned char* dataBuf = nil; + unsigned char* rsrcBuf = nil; + long dataLen, rsrcLen; + MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); + + WMSG0("--- ProcessFileAddData\n"); + + /* map the EOL conversion to something we can use */ + GenericEntry::ConvertEOL convEOL; + + switch (addOptsConvEOL) { + case AddFilesDialog::kConvEOLNone: + convEOL = GenericEntry::kConvertEOLOff; + break; + case AddFilesDialog::kConvEOLType: + // will be adjusted each time through the loop + convEOL = GenericEntry::kConvertEOLOff; + break; + case AddFilesDialog::kConvEOLAuto: + convEOL = GenericEntry::kConvertEOLAuto; + break; + case AddFilesDialog::kConvEOLAll: + convEOL = GenericEntry::kConvertEOLOn; + break; + default: + assert(false); + convEOL = GenericEntry::kConvertEOLOff; + break; + } + + + pData = fpAddDataHead; + while (pData != nil) { + const FileDetails* pDataDetails = nil; + const FileDetails* pRsrcDetails = nil; + const FileDetails* pDetails = pData->GetDetails(); + const char* typeStr = "????"; + + switch (pDetails->entryKind) { + case FileDetails::kFileKindDataFork: + pDataDetails = pDetails; + typeStr = "data"; + break; + case FileDetails::kFileKindRsrcFork: + pRsrcDetails = pDetails; + typeStr = "rsrc"; + break; + case FileDetails::kFileKindDiskImage: + pDataDetails = pDetails; + typeStr = "disk"; + break; + case FileDetails::kFileKindBothForks: + case FileDetails::kFileKindDirectory: + default: + assert(false); + return "internal error"; + } + + if (pData->GetOtherFork() != nil) { + pDetails = pData->GetOtherFork()->GetDetails(); + typeStr = "both"; + + switch (pDetails->entryKind) { + case FileDetails::kFileKindDataFork: + assert(pDataDetails == nil); + pDataDetails = pDetails; + break; + case FileDetails::kFileKindRsrcFork: + assert(pRsrcDetails == nil); + pRsrcDetails = pDetails; + break; + case FileDetails::kFileKindDiskImage: + assert(false); + return "(internal) add other disk error"; + case FileDetails::kFileKindBothForks: + case FileDetails::kFileKindDirectory: + default: + assert(false); + return "internal error"; + } + } + + WMSG2("Adding file '%s' (%s)\n", + pDetails->storageName, typeStr); + ASSERT(pDataDetails != nil || pRsrcDetails != nil); + + /* + * The current implementation of DiskImg/DiskFS requires writing each + * fork in one shot. This means loading the entire thing into + * memory. Not so bad for ProDOS, with its 16MB maximum file size, + * but it could be awkward for HFS (not to mention HFS Plus!). + */ + DiskFS::CreateParms parms; + ConvertFDToCP(pData->GetDetails(), &parms); + if (pRsrcDetails != nil) + parms.storageType = kNuStorageExtended; + else + parms.storageType = kNuStorageSeedling; + /* use the FS-normalized path here */ + parms.pathName = pData->GetFSNormalPath(); + + dataLen = rsrcLen = -1; + if (pDataDetails != nil) { + /* figure out text conversion, including high ASCII for DOS */ + /* (HA conversion only happens if text conversion happens) */ + GenericEntry::ConvertHighASCII convHA; + if (addOptsConvEOL == AddFilesDialog::kConvEOLType) { + if (pDataDetails->fileType == kFileTypeTXT || + pDataDetails->fileType == kFileTypeSRC) + { + WMSG0("Enabling text conversion by type\n"); + convEOL = GenericEntry::kConvertEOLOn; + } else { + convEOL = GenericEntry::kConvertEOLOff; + } + } + if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat())) + convHA = GenericEntry::kConvertHAOn; + else + convHA = GenericEntry::kConvertHAOff; + + errMsg = LoadFile(pDataDetails->origName, &dataBuf, &dataLen, + convEOL, convHA); + if (!errMsg.IsEmpty()) + goto bail; + } + if (pRsrcDetails != nil) { + /* no text conversion on resource forks */ + errMsg = LoadFile(pRsrcDetails->origName, &rsrcBuf, &rsrcLen, + GenericEntry::kConvertEOLOff, GenericEntry::kConvertHAOff); + if (!errMsg.IsEmpty()) + goto bail; + } + + /* really ought to do this separately for each thread */ + SET_PROGRESS_BEGIN(); + SET_PROGRESS_UPDATE2(0, pDetails->origName, + parms.pathName); + + DIError dierr; + dierr = AddForksToDisk(pDiskFS, &parms, dataBuf, dataLen, + rsrcBuf, rsrcLen); + SET_PROGRESS_END(); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to add '%s' to image: %s.", + parms.pathName, DiskImgLib::DIStrError(dierr)); + goto bail; + } + delete[] dataBuf; + delete[] rsrcBuf; + dataBuf = rsrcBuf = nil; + + pData = pData->GetNext(); + } + +bail: + delete[] dataBuf; + delete[] rsrcBuf; + return errMsg; +} + +#define kCharLF '\n' +#define kCharCR '\r' + +/* + * Load a file into a buffer, possibly converting EOL markers and setting + * "high ASCII" along the way. + * + * Returns a pointer to a newly-allocated buffer (new[]) and the data length. + * If the file is empty, no buffer will be allocated. + * + * Returns an empty string on success, or an error message on failure. + * + * HEY: really ought to update the progress counter, especially when reading + * really large files. + */ +CString +DiskArchive::LoadFile(const char* pathName, unsigned char** pBuf, long* pLen, + GenericEntry::ConvertEOL conv, GenericEntry::ConvertHighASCII convHA) const +{ + CString errMsg; + FILE* fp; + long fileLen; + + ASSERT(convHA == GenericEntry::kConvertHAOn || + convHA == GenericEntry::kConvertHAOff); + ASSERT(conv == GenericEntry::kConvertEOLOn || + conv == GenericEntry::kConvertEOLOff || + conv == GenericEntry::kConvertEOLAuto); + ASSERT(pathName != nil); + ASSERT(pBuf != nil); + ASSERT(pLen != nil); + + fp = fopen(pathName, "rb"); + if (fp == nil) { + errMsg.Format("Unable to open '%s': %s.", pathName, + strerror(errno)); + goto bail; + } + + if (fseek(fp, 0, SEEK_END) != 0) { + errMsg.Format("Unable to seek to end of '%s': %s", pathName, + strerror(errno)); + goto bail; + } + fileLen = ftell(fp); + if (fileLen < 0) { + errMsg.Format("Unable to determine length of '%s': %s", pathName, + strerror(errno)); + goto bail; + } + rewind(fp); + + if (fileLen == 0) { // handle zero-length files + *pBuf = nil; + *pLen = 0; + goto bail; + } else if (fileLen > 0x00ffffff) { + errMsg = "Cannot add files larger than 16MB to a disk image."; + goto bail; + } + + *pBuf = new unsigned char[fileLen]; + if (*pBuf == nil) { + errMsg.Format("Unable to allocate %ld bytes for '%s'.", + fileLen, pathName); + goto bail; + } + + /* + * We're ready to load the file. We need to sort out EOL conversion. + * Since we always convert to CR, we know the file will stay the same + * size or get smaller, which means the buffer we've allocated is + * guaranteed to hold the file even if we convert it. + * + * If the text mode is "auto", we need to load a piece of the file and + * analyze it. + */ + if (conv == GenericEntry::kConvertEOLAuto) { + GenericEntry::EOLType eolType; + GenericEntry::ConvertHighASCII dummy; + + int chunkLen = 16384; // nice big chunk + if (chunkLen > fileLen) + chunkLen = fileLen; + + if (fread(*pBuf, chunkLen, 1, fp) != 1) { + errMsg.Format("Unable to read initial chunk of '%s': %s.", + pathName, strerror(errno)); + delete[] *pBuf; + *pBuf = nil; + goto bail; + } + rewind(fp); + + conv = GenericEntry::DetermineConversion(*pBuf, chunkLen, + &eolType, &dummy); + WMSG2("LoadFile DetermineConv returned conv=%d eolType=%d\n", + conv, eolType); + if (conv == GenericEntry::kConvertEOLOn && + eolType == GenericEntry::kEOLCR) + { + WMSG0(" (skipping conversion due to matching eolType)\n"); + conv = GenericEntry::kConvertEOLOff; + } + } + ASSERT(conv != GenericEntry::kConvertEOLAuto); + + /* + * The "high ASCII" conversion is either on or off. In this context, + * "on" means "convert all text files", and "off" means "don't convert + * text files". We never convert non-text files. Conversion should + * always be "on" for DOS 3.2/3.3, and "off" for everything else (except + * RDOS, should we choose to make that writeable). + */ + if (conv == GenericEntry::kConvertEOLOff) { + /* fast path */ + WMSG1(" +++ NOT converting text '%s'\n", pathName); + if (fread(*pBuf, fileLen, 1, fp) != 1) { + errMsg.Format("Unable to read '%s': %s.", pathName, strerror(errno)); + delete[] *pBuf; + *pBuf = nil; + goto bail; + } + } else { + /* + * Convert as we go. + * + * Observation: if we copy a binary file to a DOS disk, and force + * the text conversion, we will convert 0x0a to 0x0d, and thence + * to 0x8d. However, we may still have some 0x8a bytes lying around, + * because we don't convert 0x8a in the original file to anything. + * This means that a CR->CRLF or LF->CRLF conversion can't be + * "undone" on a DOS disk. (Not that anyone cares.) + */ + long count = fileLen; + int mask, ic; + bool lastCR = false; + unsigned char* buf = *pBuf; + + if (convHA == GenericEntry::kConvertHAOn) + mask = 0x80; + else + mask = 0x00; + + WMSG2(" +++ Converting text '%s', mask=0x%02x\n", pathName, mask); + + while (count--) { + ic = getc(fp); + + if (ic == kCharCR) { + *buf++ = (unsigned char) (kCharCR | mask); + lastCR = true; + } else if (ic == kCharLF) { + if (!lastCR) + *buf++ = (unsigned char) (kCharCR | mask); + lastCR = false; + } else { + if (ic == '\0') + *buf++ = (unsigned char) ic; // don't conv 0x00 + else + *buf++ = (unsigned char) (ic | mask); + lastCR = false; + } + } + fileLen = buf - *pBuf; + } + + (void) fclose(fp); + + *pLen = fileLen; + +bail: + return errMsg; +} + +/* + * Add a file with the supplied data to the disk image. + * + * Forks that exist but are empty have a length of zero. Forks that don't + * exist have a length of -1. + * + * Called by XferFile and ProcessFileAddData. + */ +DIError +DiskArchive::AddForksToDisk(DiskFS* pDiskFS, const DiskFS::CreateParms* pParms, + const unsigned char* dataBuf, long dataLen, + const unsigned char* rsrcBuf, long rsrcLen) const +{ + DIError dierr = kDIErrNone; + CString replacementFileName; + const int kFileTypeBIN = 0x06; + const int kFileTypeINT = 0xfa; + const int kFileTypeBAS = 0xfc; + A2File* pNewFile = nil; + A2FileDescr* pOpenFile = nil; + DiskFS::CreateParms parmCopy; + + /* + * Make a temporary copy, pointers and all, so we can rewrite some of + * the fields. This is sort of bad, because we're making copies of a + * const char* filename pointer whose underlying storage we're not + * really familiar with. However, so long as we don't try to retain + * it after this function returns we should be fine. + * + * Might be better to make CreateParms a class instead of a struct, + * make the pathName field new[] storage, and write a copy constructor + * for the operation below. This will be fine for now. + */ + memcpy(&parmCopy, pParms, sizeof(parmCopy)); + + if (rsrcLen >= 0) { + ASSERT(parmCopy.storageType == kNuStorageExtended); + } + + /* + * Look for "empty directory holders" that we put into NuFX archives + * when doing disk-to-archive conversions. These make no sense if + * there's no fssep (because it's coming from DOS), or if there's no + * base path, so we can ignore those cases. We can also ignore it if + * the file is forked or is already a directory. + */ + if (parmCopy.fssep != '\0' && parmCopy.storageType == kNuStorageSeedling) { + const char* cp; + cp = strrchr(parmCopy.pathName, parmCopy.fssep); + if (cp != nil) { + if (strcmp(cp+1, kEmptyFolderMarker) == 0 && dataLen == 0) { + /* drop the junk on the end */ + parmCopy.storageType = kNuStorageDirectory; + replacementFileName = parmCopy.pathName; + replacementFileName = + replacementFileName.Left(cp - parmCopy.pathName -1); + parmCopy.pathName = replacementFileName; + parmCopy.fileType = 0x0f; // DIR + parmCopy.access &= ~(A2FileProDOS::kAccessInvisible); + dataLen = -1; + } + } + } + + /* + * If this is a subdir create request (from the clipboard or an "empty + * directory placeholder" in a NuFX archive), handle it here. If we're + * on a filesystem that doesn't have subdirectories, just skip it. + */ + if (parmCopy.storageType == kNuStorageDirectory) { + A2File* pDummyFile; + ASSERT(dataLen < 0 && rsrcLen < 0); + + if (DiskImg::IsHierarchical(pDiskFS->GetDiskImg()->GetFSFormat())) { + dierr = pDiskFS->CreateFile(&parmCopy, &pDummyFile); + if (dierr == kDIErrDirectoryExists) + dierr = kDIErrNone; // dirs are not made unique + goto bail; + } else { + WMSG0(" Ignoring subdir create req on non-hierarchic FS\n"); + goto bail; + } + } + + /* don't try to put resource forks onto a DOS disk */ + if (!DiskImg::HasResourceForks(pDiskFS->GetDiskImg()->GetFSFormat())) { + if (rsrcLen >= 0) { + rsrcLen = -1; + parmCopy.storageType = kNuStorageSeedling; + + if (dataLen < 0) { + /* this was a resource-fork-only file */ + WMSG1("--- nothing left to write for '%s'\n", + parmCopy.pathName); + goto bail; + } + } else { + ASSERT(parmCopy.storageType == kNuStorageSeedling); + } + } + + /* quick kluge to get the right file type on large DOS files */ + if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()) && + dataLen >= 65536) + { + if (parmCopy.fileType == kFileTypeBIN || + parmCopy.fileType == kFileTypeINT || + parmCopy.fileType == kFileTypeBAS) + { + WMSG0("+++ switching DOS file type to $f2\n"); + parmCopy.fileType = 0xf2; // DOS 'S' file + } + } + + /* + * Create the file on the disk. The storage type determines whether + * it has data+rsrc forks or just data (there's no such thing in + * ProDOS as "just a resource fork"). There's no need to open the + * fork if we're not going to write to it. + * + * This holds for resource forks as well, because the storage type + * determines whether or not the file is forked, and we've asserted + * that a file with a non-(-1) rsrcLen is forked. + */ + dierr = pDiskFS->CreateFile(&parmCopy, &pNewFile); + if (dierr != kDIErrNone) { + WMSG1(" CreateFile failed: %s\n", DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* + * Note: if this was an empty directory holder, pNewFile will be set + * to nil. We used to avoid handling this by just not opening the file + * if it had a length of zero. However, DOS 3.3 needs to write some + * kinds of zero-length files, because e.g. a zero-length 'B' file + * actually has 4 bytes of data in it. + * + * Of course, if dataLen is zero then dataBuf is nil, so we need to + * supply a dummy write buffer. None of this is an issue for resource + * forks, because DOS 3.3 doesn't have those. + */ + + if (dataLen > 0 || + (dataLen == 0 && pNewFile != nil)) + { + ASSERT(pNewFile != nil); + unsigned char dummyBuf[1] = { '\0' }; + + dierr = pNewFile->Open(&pOpenFile, false, false); + if (dierr != kDIErrNone) + goto bail; + + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, + dataLen, nil); + + dierr = pOpenFile->Write(dataBuf != nil ? dataBuf : dummyBuf, dataLen); + if (dierr != kDIErrNone) + goto bail; + + dierr = pOpenFile->Close(); + if (dierr != kDIErrNone) + goto bail; + pOpenFile = nil; + } + + if (rsrcLen > 0) { + ASSERT(pNewFile != nil); + + dierr = pNewFile->Open(&pOpenFile, false, true); + if (dierr != kDIErrNone) + goto bail; + + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, + rsrcLen, nil); + + dierr = pOpenFile->Write(rsrcBuf, rsrcLen); + if (dierr != kDIErrNone) + goto bail; + + dierr = pOpenFile->Close(); + if (dierr != kDIErrNone) + goto bail; + pOpenFile = nil; + } + +bail: + if (pOpenFile != nil) + pOpenFile->Close(); + if (dierr != kDIErrNone && pNewFile != nil) { + /* + * Clean up the partially-written file. This does not, of course, + * erase any subdirectories that were created to contain this file. + * Not worth worrying about. + */ + WMSG1(" Deleting newly-created file '%s'\n", parmCopy.pathName); + (void) pDiskFS->DeleteFile(pNewFile); + } + return dierr; +} + +/* + * Fill out a CreateParms structure from a FileDetails structure. + * + * The NuStorageType values correspond exactly to ProDOS storage types, so + * there's no need to convert them. + */ +void +DiskArchive::ConvertFDToCP(const FileDetails* pDetails, + DiskFS::CreateParms* pCreateParms) +{ + pCreateParms->pathName = pDetails->storageName; + pCreateParms->fssep = (char) pDetails->fileSysInfo; + pCreateParms->storageType = pDetails->storageType; + pCreateParms->fileType = pDetails->fileType; + pCreateParms->auxType = pDetails->extraType; + pCreateParms->access = pDetails->access; + pCreateParms->createWhen = NufxArchive::DateTimeToSeconds(&pDetails->createWhen); + pCreateParms->modWhen = NufxArchive::DateTimeToSeconds(&pDetails->modWhen); +} + + +/* + * Add an entry to the end of the FileAddData list. + * + * If "storeName" (the Windows filename with type goodies stripped, but + * without filesystem normalization) matches an entry already in the list, + * we check to see if these are forks of the same file. If they are + * different forks and we don't already have both forks, we put the + * pointer into the "fork pointer" of the existing file rather than adding + * it to the end of the list. + */ +void +DiskArchive::AddToAddDataList(FileAddData* pData) +{ + ASSERT(pData != nil); + ASSERT(pData->GetNext() == nil); + + /* + * Run through the entire existing list, looking for a match. This is + * O(n^2) behavior, but I'm expecting N to be relatively small (under + * 1000 in almost all cases). + */ + //if (strcasecmp(pData->GetDetails()->storageName, "system\\finder") == 0) + // WMSG0("whee\n"); + FileAddData* pSearch = fpAddDataHead; + FileDetails::FileKind dataKind, listKind; + + dataKind = pData->GetDetails()->entryKind; + while (pSearch != nil) { + if (pSearch->GetOtherFork() == nil && + strcmp(pSearch->GetDetails()->storageName, + pData->GetDetails()->storageName) == 0) + { + //NuThreadID dataID = pData->GetDetails()->threadID; + //NuThreadID listID = pSearch->GetDetails()->threadID; + + listKind = pSearch->GetDetails()->entryKind; + + /* got a name match */ + if (dataKind != listKind && + (dataKind == FileDetails::kFileKindDataFork || dataKind == FileDetails::kFileKindRsrcFork) && + (listKind == FileDetails::kFileKindDataFork || listKind == FileDetails::kFileKindRsrcFork)) + { + /* looks good, hook it in here instead of the list */ + WMSG2("--- connecting forks of '%s' and '%s'\n", + pData->GetDetails()->origName, + pSearch->GetDetails()->origName); + pSearch->SetOtherFork(pData); + return; + } + } + + pSearch = pSearch->GetNext(); + } + + if (fpAddDataHead == nil) { + assert(fpAddDataTail == nil); + fpAddDataHead = fpAddDataTail = pData; + } else { + fpAddDataTail->SetNext(pData); + fpAddDataTail = pData; + } +} + +/* + * Free all entries in the FileAddData list. + */ +void +DiskArchive::FreeAddDataList(void) +{ + FileAddData* pData; + FileAddData* pNext; + + pData = fpAddDataHead; + while (pData != nil) { + pNext = pData->GetNext(); + delete pData->GetOtherFork(); + delete pData; + pData = pNext; + } + + fpAddDataHead = fpAddDataTail = nil; +} + + +/* + * =========================================================================== + * DiskArchive -- create subdir + * =========================================================================== + */ + +/* + * Create a subdirectory named "newName" in "pParentEntry". + */ +bool +DiskArchive::CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const char* newName) +{ + ASSERT(newName != nil && strlen(newName) > 0); + DiskEntry* pEntry = (DiskEntry*) pParentEntry; + ASSERT(pEntry != nil); + A2File* pFile = pEntry->GetA2File(); + ASSERT(pFile != nil); + DiskFS* pDiskFS = pFile->GetDiskFS(); + ASSERT(pDiskFS != nil); + + if (!pFile->IsDirectory()) { + ASSERT(false); + return false; + } + + DIError dierr; + A2File* pNewFile = nil; + DiskFS::CreateParms parms; + CString pathName; + time_t now = time(nil); + + /* + * Create the full path. + */ + if (pFile->IsVolumeDirectory()) { + pathName = newName; + } else { + pathName = pParentEntry->GetPathName(); + pathName += pParentEntry->GetFssep(); + pathName += newName; + } + ASSERT(strchr(newName, pParentEntry->GetFssep()) == nil); + + /* using NufxLib constants; they match with ProDOS */ + memset(&parms, 0, sizeof(parms)); + parms.pathName = pathName; + parms.fssep = pParentEntry->GetFssep(); + parms.storageType = kNuStorageDirectory; + parms.fileType = 0x0f; // ProDOS DIR + parms.auxType = 0; + parms.access = kNuAccessUnlocked; + parms.createWhen = now; + parms.modWhen = now; + + dierr = pDiskFS->CreateFile(&parms, &pNewFile); + if (dierr != kDIErrNone) { + CString errMsg; + errMsg.Format("Unable to create subdirectory: %s.\n", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + + if (InternalReload(pMsgWnd) != 0) + return false; + + return true; +} + + +/* + * =========================================================================== + * DiskArchive -- delete selection + * =========================================================================== + */ + +/* + * Compare DiskEntry display names in descending order (Z-A). + */ +/*static*/ int +DiskArchive::CompareDisplayNamesDesc(const void* ventry1, const void* ventry2) +{ + const DiskEntry* pEntry1 = *((const DiskEntry**) ventry1); + const DiskEntry* pEntry2 = *((const DiskEntry**) ventry2); + + return strcasecmp(pEntry2->GetDisplayName(), pEntry1->GetDisplayName()); +} + +/* + * Delete the records listed in the selection set. + * + * The DiskFS DeleteFile() function will not delete a subdirectory unless + * it is empty. This complicates matters somewhat for us, because the + * selection set isn't in any particular order. We need to sort on the + * pathname and then delete bottom-up. + * + * CiderPress does work to ensure that, if a subdir is selected, everything + * in that subdir is also selected. So if we just delete everything in the + * right order, we should be okay. + */ +bool +DiskArchive::DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + CString errMsg; + SelectionEntry* pSelEntry; + DiskEntry* pEntry; + DIError dierr; + bool retVal = false; + + SET_PROGRESS_BEGIN(); + + /* + * Start by copying the DiskEntry pointers out of the selection set and + * into an array. The selection set was created such that there is one + * entry in the set for each file. (The file viewer likes to have one + * entry for each thread.) + */ + int numEntries = pSelSet->GetNumEntries(); + ASSERT(numEntries > 0); + DiskEntry** entryArray = new DiskEntry*[numEntries]; + int idx = 0; + + pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = (DiskEntry*) pSelEntry->GetEntry(); + ASSERT(pEntry != nil); + + entryArray[idx++] = pEntry; + WMSG2("Added 0x%08lx '%s'\n", (long) entryArray[idx-1], + entryArray[idx-1]->GetDisplayName()); + + pSelEntry = pSelSet->IterNext(); + } + ASSERT(idx == numEntries); + + /* + * Sort the file array by descending filename. + */ + ::qsort(entryArray, numEntries, sizeof(DiskEntry*), CompareDisplayNamesDesc); + + /* + * Run through the sorted list, deleting each entry. + */ + for (idx = 0; idx < numEntries; idx++) { + A2File* pFile; + + pEntry = entryArray[idx]; + pFile = pEntry->GetA2File(); + + /* + * We shouldn't be here at all if the main volume were opened + * read-only. However, it's possible that the main is read-write + * and our sub-volumes are read-only (probably because we don't + * support write access to the filesystem). + */ + if (!pFile->GetDiskFS()->GetReadWriteSupported()) { + errMsg.Format("Unable to delete '%s' on '%s': operation not supported.", + pEntry->GetDisplayName(), pFile->GetDiskFS()->GetVolumeName()); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + WMSG2(" Deleting '%s' from '%s'\n", pEntry->GetPathName(), + pFile->GetDiskFS()->GetVolumeName()); + SET_PROGRESS_UPDATE2(0, pEntry->GetPathName(), nil); + + /* + * Ask the DiskFS to delete the file. As soon as this completes, + * "pFile" is invalid and must not be dereferenced. + */ + dierr = pFile->GetDiskFS()->DeleteFile(pFile); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to delete '%s' on '%s': %s.", + pEntry->GetDisplayName(), pFile->GetDiskFS()->GetVolumeName(), + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + SET_PROGRESS_UPDATE(100); + + /* + * Be paranoid and zap the pointer, on the off chance somebody + * tries to redraw the content list from the deleted data. + * + * In practice we don't work this way -- the stuff that gets drawn + * on the screen comes out of GenericEntry, not A2File. If this + * changes we'll need to raise the "reload" flag here, before the + * reload, to prevent the ContentList from chasing a bad pointer. + */ + pEntry->SetA2File(nil); + } + + retVal = true; + +bail: + SET_PROGRESS_END(); + delete[] entryArray; + if (InternalReload(pMsgWnd) != 0) + retVal = false; + + return retVal; +} + +/* + * =========================================================================== + * DiskArchive -- rename files + * =========================================================================== + */ + + /* + * Rename a set of files, one at a time. + * + * If we rename a subdirectory, it could affect the next thing we try to + * rename (because we show the full path). We have to reload our file + * list from the DiskFS after each renamed subdir. The trouble is that + * this invalidates the data displayed in the ContentList, and we won't + * redraw the screen correctly. We can work around the problem by getting + * the pathname directly from the DiskFS instead of from DiskEntry, though + * it's not immediately obvious which is less confusing. + */ +bool +DiskArchive::RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + CString errMsg; + bool retVal = false; + + WMSG1("Renaming %d entries\n", pSelSet->GetNumEntries()); + + /* + * For each item in the selection set, bring up the "rename" dialog, + * and ask the GenericEntry to process it. + * + * If they hit "cancel" or there's an error, we still flush the + * previous changes. This is so that we don't have to create the + * same sort of deferred-write feature when renaming things in other + * sorts of archives (e.g. disk archives). + */ + SelectionEntry* pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + RenameEntryDialog renameDlg(pMsgWnd); + DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry(); + + WMSG1(" Renaming '%s'\n", pEntry->GetPathName()); + if (!SetRenameFields(pMsgWnd, pEntry, &renameDlg)) + break; + + int result; + if (pEntry->GetA2File()->IsVolumeDirectory()) + result = IDIGNORE; // don't allow rename of volume dir + else + result = renameDlg.DoModal(); + if (result == IDOK) { + DIError dierr; + DiskFS* pDiskFS; + A2File* pFile; + + pFile = pEntry->GetA2File(); + pDiskFS = pFile->GetDiskFS(); + dierr = pDiskFS->RenameFile(pFile, renameDlg.fNewName); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to rename '%s' to '%s': %s.", + pEntry->GetPathName(), renameDlg.fNewName, + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + WMSG2("Rename of '%s' to '%s' succeeded\n", + pEntry->GetDisplayName(), renameDlg.fNewName); + } else if (result == IDCANCEL) { + WMSG0("Canceling out of remaining renames\n"); + break; + } else { + /* 3rd possibility is IDIGNORE, i.e. skip this entry */ + WMSG1("Skipping rename of '%s'\n", pEntry->GetDisplayName()); + } + + pSelEntry = pSelSet->IterNext(); + } + + /* reload GenericArchive from disk image */ + if (InternalReload(pMsgWnd) == kNuErrNone) + retVal = true; + +bail: + return retVal; +} + +/* + * Set up a RenameEntryDialog for the entry in "*pEntry". + * + * Returns "true" on success, "false" on failure. + */ +bool +DiskArchive::SetRenameFields(CWnd* pMsgWnd, DiskEntry* pEntry, + RenameEntryDialog* pDialog) +{ + DiskFS* pDiskFS; + + ASSERT(pEntry != nil); + + /* + * Figure out if we're allowed to change the entire path. (This is + * doing it the hard way, but what the hell.) + */ + long cap = GetCapability(GenericArchive::kCapCanRenameFullPath); + bool renameFullPath = (cap != 0); + + // a bit round-about, but it works + pDiskFS = pEntry->GetA2File()->GetDiskFS(); + + /* + * Make sure rename is allowed. It's nice to do these *before* putting + * up the rename dialog, so that the user doesn't do a bunch of typing + * before being told that it's pointless. + */ + if (!pDiskFS->GetReadWriteSupported()) { + CString errMsg; + errMsg.Format("Unable to rename '%s': operation not supported.", + pEntry->GetPathName()); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + if (pDiskFS->GetFSDamaged()) { + CString errMsg; + errMsg.Format("Unable to rename '%s': the disk it's on appears to be damaged.", + pEntry->GetPathName()); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + + pDialog->SetCanRenameFullPath(renameFullPath); + pDialog->fOldName = pEntry->GetPathName(); + pDialog->fFssep = pEntry->GetFssep(); + pDialog->fpArchive = this; + pDialog->fpEntry = pEntry; + + return true; +} + +/* + * Verify that the a name is suitable. Called by RenameEntryDialog and + * CreateSubdirDialog. + * + * Tests for context-specific syntax and checks for duplicates. + * + * Returns an empty string on success, or an error message on failure. + */ +CString +DiskArchive::TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const +{ + const DiskEntry* pEntry = (DiskEntry*) pGenericEntry; + DiskImg::FSFormat format; + CString pathName, errMsg; + DiskFS* pDiskFS; + + if (basePath.IsEmpty()) { + pathName = newName; + } else { + pathName = basePath; + pathName += newFssep; + pathName += newName; + } + + pDiskFS = pEntry->GetA2File()->GetDiskFS(); + format = pDiskFS->GetDiskImg()->GetFSFormat(); + + /* look for an existing file, but don't compare against self */ + A2File* existingFile; + existingFile = pDiskFS->GetFileByName(pathName); + if (existingFile != nil && existingFile != pEntry->GetA2File()) { + errMsg = "A file with that name already exists."; + goto bail; + } + + switch (format) { + case DiskImg::kFormatProDOS: + if (!DiskFSProDOS::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_PRODOS); + break; + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + if (!DiskFSDOS33::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_DOS); + break; + case DiskImg::kFormatPascal: + if (!DiskFSPascal::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_PASCAL); + break; + case DiskImg::kFormatMacHFS: + if (!DiskFSHFS::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_HFS); + break; + default: + errMsg = "Not supported by TestPathName!"; + } + +bail: + return errMsg; +} + + +/* + * =========================================================================== + * DiskArchive -- rename a volume + * =========================================================================== + */ + +/* + * Ask a DiskFS to change its volume name. + * + * Returns "true" on success, "false" on failure. + */ +bool +DiskArchive::RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const char* newName) +{ + DIError dierr; + CString errMsg; + bool retVal = true; + + dierr = pDiskFS->RenameVolume(newName); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to rename volume: %s.\n", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + retVal = false; + /* fall through to reload anyway */ + } + + /* reload GenericArchive from disk image */ + if (InternalReload(pMsgWnd) != 0) + retVal = false; + + return retVal; +} + +/* + * Test a volume name for validity. + */ +CString +DiskArchive::TestVolumeName(const DiskFS* pDiskFS, + const char* newName) const +{ + DiskImg::FSFormat format; + CString errMsg; + + ASSERT(pDiskFS != nil); + ASSERT(newName != nil); + + format = pDiskFS->GetDiskImg()->GetFSFormat(); + + switch (format) { + case DiskImg::kFormatProDOS: + if (!DiskFSProDOS::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_PRODOS); + break; + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + if (!DiskFSDOS33::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_DOS); + break; + case DiskImg::kFormatPascal: + if (!DiskFSPascal::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_PASCAL); + break; + case DiskImg::kFormatMacHFS: + if (!DiskFSHFS::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_HFS); + break; + default: + errMsg = "Not supported by TestVolumeName!"; + } + + return errMsg; +} + + +/* + * =========================================================================== + * DiskArchive -- set file properties + * =========================================================================== + */ + +/* + * Set the properties of "pEntry" to what's in "pProps". + * + * [currently only supports file type, aux type, and access flags] + * + * Technically we should reload the GenericArchive from the NufxArchive, + * but the set of changes is pretty small, so we just make them here. + */ +bool +DiskArchive::SetProps(CWnd* pMsgWnd, GenericEntry* pGenericEntry, + const FileProps* pProps) +{ + DIError dierr; + DiskEntry* pEntry = (DiskEntry*) pGenericEntry; + A2File* pFile = pEntry->GetA2File(); + + dierr = pFile->GetDiskFS()->SetFileInfo(pFile, pProps->fileType, + pProps->auxType, pProps->access); + if (dierr != kDIErrNone) { + CString errMsg; + errMsg.Format("Unable to set file info: %s.\n", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + + /* do this in lieu of reloading GenericArchive */ + pEntry->SetFileType(pFile->GetFileType()); + pEntry->SetAuxType(pFile->GetAuxType()); + pEntry->SetAccess(pFile->GetAccess()); + + /* DOS 3.2/3.3 may change these as well */ + DiskImg::FSFormat fsFormat; + fsFormat = pFile->GetDiskFS()->GetDiskImg()->GetFSFormat(); + if (fsFormat == DiskImg::kFormatDOS32 || fsFormat == DiskImg::kFormatDOS33) { + WMSG0(" (reloading additional fields after DOS SFI)\n"); + pEntry->SetDataForkLen(pFile->GetDataLength()); + pEntry->SetCompressedLen(pFile->GetDataSparseLength()); + pEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious); + } + + /* clear the dirty flag in trivial cases */ + (void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly); + + return true; +} + + +/* + * =========================================================================== + * DiskArchive -- transfer files to another archive + * =========================================================================== + */ + +/* + * Transfer the selected files out of this archive and into another. + * + * In this case, it's files on a disk (with unspecified filesystem) to a NuFX + * archive. We get the open archive pointer and some options from "pXferOpts". + * + * The selection set was created with the "any" selection criteria, which + * means there's only one entry for each file regardless of whether it's + * forked or not. + */ +GenericArchive::XferStatus +DiskArchive::XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts) +{ + WMSG0("DiskArchive XferSelection!\n"); + unsigned char* dataBuf = nil; + unsigned char* rsrcBuf = nil; + FileDetails fileDetails; + CString errMsg, extractErrMsg, cmpStr; + CString fixedPathName; + XferStatus retval = kXferFailed; + + pXferOpts->fTarget->XferPrepare(pXferOpts); + + SelectionEntry* pSelEntry = pSelSet->IterNext(); + for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) { + long dataLen=-1, rsrcLen=-1; + DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry(); + int typeOverride = -1; + int result; + + ASSERT(dataBuf == nil); + ASSERT(rsrcBuf == nil); + + if (pEntry->GetDamaged()) { + WMSG1(" XFER skipping damaged entry '%s'\n", + pEntry->GetDisplayName()); + continue; + } + + /* + * Do a quick de-colonizing pass for non-ProDOS volumes, then prepend + * the subvolume name (if any). + */ + fixedPathName = pEntry->GetPathName(); + if (fixedPathName.IsEmpty()) + fixedPathName = _T("(no filename)"); + if (pEntry->GetFSFormat() != DiskImg::kFormatProDOS) + fixedPathName.Replace(PathProposal::kDefaultStoredFssep, '.'); + if (pEntry->GetSubVolName() != nil) { + CString tmpStr; + tmpStr = pEntry->GetSubVolName(); + tmpStr += (char)PathProposal::kDefaultStoredFssep; + tmpStr += fixedPathName; + fixedPathName = tmpStr; + } + + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) { + /* this is the volume dir */ + WMSG1(" XFER not transferring volume dir '%s'\n", + fixedPathName); + continue; + } else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) { + if (pXferOpts->fPreserveEmptyFolders) { + /* if this is an empty directory, create a fake entry */ + cmpStr = fixedPathName; + cmpStr += (char)PathProposal::kDefaultStoredFssep; + + if (pSelSet->CountMatchingPrefix(cmpStr) == 0) { + WMSG1("FOUND empty dir '%s'\n", fixedPathName); + cmpStr += kEmptyFolderMarker; + dataBuf = new unsigned char[1]; + dataLen = 0; + fileDetails.entryKind = FileDetails::kFileKindDataFork; + fileDetails.storageName = cmpStr; + fileDetails.fileType = 0; // NON + fileDetails.access = + pEntry->GetAccess() | GenericEntry::kAccessInvisible; + goto have_stuff2; + } else { + WMSG1("NOT empty dir '%s'\n", fixedPathName); + } + } + + WMSG1(" XFER not transferring directory '%s'\n", + fixedPathName); + continue; + } + + WMSG3(" Xfer '%s' (data=%d rsrc=%d)\n", + fixedPathName, pEntry->GetHasDataFork(), + pEntry->GetHasRsrcFork()); + + dataBuf = nil; + dataLen = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kDataThread, + (char**) &dataBuf, &dataLen, &extractErrMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during data extract!\n"); + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + errMsg.Format("Failed while extracting '%s': %s.", + fixedPathName, extractErrMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + ASSERT(dataBuf != nil); + ASSERT(dataLen >= 0); + +#if 0 + if (pXferOpts->fConvDOSText && + DiskImg::UsesDOSFileStructure(pEntry->GetFSFormat()) && + pEntry->GetFileType() == kFileTypeTXT) + { + /* don't need to convert EOL, so just strip in place */ + long len; + unsigned char* ucp; + + WMSG1(" Converting DOS text in '%s'\n", fixedPathName); + for (ucp = dataBuf, len = dataLen; len > 0; len--, ucp++) + *ucp = *ucp & 0x7f; + } +#endif + +#if 0 // annoying to invoke PTX reformatter from here... ReformatHolder, etc. + if (pXferOpts->fConvPascalText && + pEntry->GetFSFormat() == DiskImg::kFormatPascal && + pEntry->GetFileType() == kFileTypePTX) + { + WMSG1("WOULD CONVERT ptx '%s'\n", fixedPathName); + } +#endif + + if (pEntry->GetHasRsrcFork()) { + rsrcBuf = nil; + rsrcLen = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kRsrcThread, + (char**) &rsrcBuf, &rsrcLen, &extractErrMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during rsrc extract!\n"); + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + errMsg.Format("Failed while extracting '%s': %s.", + fixedPathName, extractErrMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + } else { + ASSERT(rsrcBuf == nil); + } + + if (pEntry->GetHasDataFork() && pEntry->GetHasRsrcFork()) + fileDetails.entryKind = FileDetails::kFileKindBothForks; + else if (pEntry->GetHasDataFork()) + fileDetails.entryKind = FileDetails::kFileKindDataFork; + else if (pEntry->GetHasRsrcFork()) + fileDetails.entryKind = FileDetails::kFileKindRsrcFork; + else { + ASSERT(false); + fileDetails.entryKind = FileDetails::kFileKindUnknown; + } + + /* + * Set up the FileDetails. + */ + fileDetails.storageName = fixedPathName; + fileDetails.fileType = pEntry->GetFileType(); + fileDetails.access = pEntry->GetAccess(); +have_stuff2: + fileDetails.fileSysFmt = pEntry->GetSourceFS(); + fileDetails.fileSysInfo = PathProposal::kDefaultStoredFssep; + fileDetails.extraType = pEntry->GetAuxType(); + fileDetails.storageType = kNuStorageUnknown; /* let NufxLib deal */ + + time_t when; + when = time(nil); + UNIXTimeToDateTime(&when, &fileDetails.archiveWhen); + when = pEntry->GetModWhen(); + UNIXTimeToDateTime(&when, &fileDetails.modWhen); + when = pEntry->GetCreateWhen(); + UNIXTimeToDateTime(&when, &fileDetails.createWhen); + + pActionProgress->SetArcName(fileDetails.storageName); + if (pActionProgress->SetProgress(0) == IDCANCEL) { + retval = kXferCancelled; + goto bail; + } + + errMsg = pXferOpts->fTarget->XferFile(&fileDetails, &dataBuf, dataLen, + &rsrcBuf, rsrcLen); + if (!errMsg.IsEmpty()) { + WMSG0("XferFile failed!\n"); + errMsg.Format("Failed while transferring '%s': %s.", + pEntry->GetDisplayName(), (const char*) errMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + ASSERT(dataBuf == nil); + ASSERT(rsrcBuf == nil); + + if (pActionProgress->SetProgress(100) == IDCANCEL) { + retval = kXferCancelled; + goto bail; + } + } + + //MainWindow* pMainWin; + //pMainWin = (MainWindow*)::AfxGetMainWnd(); + //pMainWin->EventPause(1000); + + retval = kXferOK; + +bail: + if (retval != kXferOK) + pXferOpts->fTarget->XferAbort(pMsgWnd); + else + pXferOpts->fTarget->XferFinish(pMsgWnd); + delete[] dataBuf; + delete[] rsrcBuf; + return retval; +} + +/* + * Prepare for file transfers. + */ +void +DiskArchive::XferPrepare(const XferFileOptions* pXferOpts) +{ + WMSG0("DiskArchive::XferPrepare\n"); + + //fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase, + // pXferOpts->fAllowLowerCase); + //fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse, + // pXferOpts->fUseSparseBlocks); + fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true); + + //fXferStoragePrefix = pXferOpts->fStoragePrefix; + fpXferTargetFS = pXferOpts->fpTargetFS; +} + +/* + * Transfer a file to the disk image. Called from NufxArchive's XferSelection + * and clipboard "paste". + * + * "dataLen" and "rsrcLen" will be -1 if the corresponding fork doesn't + * exist. + * + * Returns 0 on success, nonzero on failure. + * + * On success, *pDataBuf and *pRsrcBuf are freed and set to nil. (It's + * necessary for the interface to work this way because the NufxArchive + * version just tucks the pointers into NufxLib structures.) + */ +CString +DiskArchive::XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen) +{ + //const int kFileTypeTXT = 0x04; + DiskFS::CreateParms createParms; + DiskFS* pDiskFS; + CString errMsg; + DIError dierr = kDIErrNone; + + WMSG3(" XFER: transfer '%s' (dataLen=%ld rsrcLen=%ld)\n", + pDetails->storageName, dataLen, rsrcLen); + + ASSERT(pDataBuf != nil); + ASSERT(pRsrcBuf != nil); + + /* fill out CreateParms from FileDetails */ + ConvertFDToCP(pDetails, &createParms); + + if (fpXferTargetFS == nil) + pDiskFS = fpPrimaryDiskFS; + else + pDiskFS = fpXferTargetFS; + + /* + * Strip the high ASCII from DOS and RDOS text files, unless we're adding + * them to a DOS disk. Likewise, if we're adding non-DOS text files to + * a DOS disk, we need to add the high bit. + * + * DOS converts both TXT and SRC to 'T', so we have to handle both here. + * Ideally we'd just ask DOS, "do you think this is a text file?", but it's + * not worth adding a new interface just for that. + */ + bool srcIsDOS, dstIsDOS; + srcIsDOS = DiskImg::UsesDOSFileStructure(pDetails->fileSysFmt); + dstIsDOS = DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()); + if (dataLen > 0 && + (pDetails->fileType == kFileTypeTXT || pDetails->fileType == kFileTypeSRC)) + { + unsigned char* ucp = *pDataBuf; + long len = dataLen; + + if (srcIsDOS && !dstIsDOS) { + WMSG1(" Stripping high ASCII from '%s'\n", pDetails->storageName); + + while (len--) + *ucp++ &= 0x7f; + } else if (!srcIsDOS && dstIsDOS) { + WMSG1(" Adding high ASCII to '%s'\n", pDetails->storageName); + + while (len--) { + if (*ucp != '\0') + *ucp |= 0x80; + ucp++; + } + } else if (srcIsDOS && dstIsDOS) { + WMSG1(" --- not altering DOS-to-DOS text '%s'\n", + pDetails->storageName); + } else { + WMSG1(" --- non-DOS transfer '%s'\n", pDetails->storageName); + } + } + + /* add a file with one or two forks */ + if (createParms.storageType == kNuStorageDirectory) { + ASSERT(dataLen < 0 && rsrcLen < 0); + } else { + ASSERT(dataLen >= 0 || rsrcLen >= 0); // at least one fork + } + + /* if we still have something to write, write it */ + dierr = AddForksToDisk(pDiskFS, &createParms, *pDataBuf, dataLen, + *pRsrcBuf, rsrcLen); + if (dierr != kDIErrNone) { + errMsg.Format("%s", DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* clean up */ + delete[] *pDataBuf; + *pDataBuf = nil; + delete[] *pRsrcBuf; + *pRsrcBuf = nil; + +bail: + return errMsg; +} + + +/* + * Abort our progress. Not really possible, except by throwing the disk + * image away. + */ +void +DiskArchive::XferAbort(CWnd* pMsgWnd) +{ + WMSG0("DiskArchive::XferAbort\n"); + InternalReload(pMsgWnd); +} + +/* + * Transfer is finished. + */ +void +DiskArchive::XferFinish(CWnd* pMsgWnd) +{ + WMSG0("DiskArchive::XferFinish\n"); + InternalReload(pMsgWnd); +} diff --git a/diskimg/DiskImg.cpp b/diskimg/DiskImg.cpp index d164e83..17d7931 100644 --- a/diskimg/DiskImg.cpp +++ b/diskimg/DiskImg.cpp @@ -1,3515 +1,3525 @@ -/* - * CiderPress - * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. - * See the file LICENSE for distribution terms. - */ -/* - * Implementation of the DiskImg class. - */ -#include "StdAfx.h" -#include "DiskImgPriv.h" -#include "TwoImg.h" - - -/* - * =========================================================================== - * DiskImg - * =========================================================================== - */ - -/* - * Standard NibbleDescr profiles. - * - * These will be tried in the order in which they appear here. - * - * IMPORTANT: if you add or remove an entry, update the StdNibbleDescr enum - * in DiskImg.h. - * - * Formats that allow the data checksum to be ignored should NOT be written. - * It's possible that the DOS on the disk is ignoring the checksums, but - * it's more likely that they're using a non-standard seed, and the newly- - * written sectors will have the wrong checksum value. - * - * Non-standard headers are usually okay, because we don't rewrite the - * headers, just the sector contents. - */ -/*static*/ const DiskImg::NibbleDescr DiskImg::kStdNibbleDescrs[] = { - { - "DOS 3.3 Standard", - 16, - { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, - 0x00, // checksum seed - true, // verify checksum - true, // verify track - 2, // epilog verify count - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, // checksum seed - true, // verify checksum - 2, // epilog verify count - kNibbleEnc62, - kNibbleSpecialNone, - }, - { - "DOS 3.3 Patched", - 16, - { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, - 0x00, // checksum seed - false, // verify checksum - false, // verify track - 0, // epilog verify count - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, // checksum seed - true, // verify checksum - 0, // epilog verify count - kNibbleEnc62, - kNibbleSpecialNone, - }, - { - "DOS 3.3 Ignore Checksum", - 16, - { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, - 0x00, // checksum seed - false, // verify checksum - false, // verify track - 0, // epilog verify count - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, // checksum seed - false, // verify checksum - 0, // epilog verify count - kNibbleEnc62, - kNibbleSpecialNone, - }, - { - "DOS 3.2 Standard", - 13, - { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - true, - 2, - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - 2, - kNibbleEnc53, - kNibbleSpecialNone, - }, - { - "DOS 3.2 Patched", - 13, - { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, - 0x00, - false, - false, - 0, - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - 0, - kNibbleEnc53, - kNibbleSpecialNone, - }, - { - "Muse DOS 3.2", // standard DOS 3.2 with doubled sectors - 13, - { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - true, - 2, - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - 2, - kNibbleEnc53, - kNibbleSpecialMuse, - }, - { - "RDOS 3.3", // SSI 16-sector RDOS, with altered headers - 16, - { 0xd4, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - true, - 0, // epilog verify count - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - 2, - kNibbleEnc62, - kNibbleSpecialSkipFirstAddrByte, - /* odd tracks use d4aa96, even tracks use d5aa96 */ - }, - { - "RDOS 3.2", // SSI 13-sector RDOS, with altered headers - 13, - { 0xd4, 0xaa, 0xb7 }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - true, - 2, - { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, - 0x00, - true, - 2, - kNibbleEnc53, - kNibbleSpecialNone, - }, - { - "Custom", // reserve space for empty slot - 0, - }, -}; -/*static*/ const DiskImg::NibbleDescr* -DiskImg::GetStdNibbleDescr(StdNibbleDescr idx) -{ - if ((int)idx < 0 || (int)idx >= (int) NELEM(kStdNibbleDescrs)) - return nil; - return &kStdNibbleDescrs[(int)idx]; -} - - -/* - * Initialize the members during construction. - */ -DiskImg::DiskImg(void) -{ - assert(Global::GetAppInitCalled()); - - fOuterFormat = kOuterFormatUnknown; - fFileFormat = kFileFormatUnknown; - fPhysical = kPhysicalFormatUnknown; - fpNibbleDescr = nil; - fOrder = kSectorOrderUnknown; - fFormat = kFormatUnknown; - - fFileSysOrder = kSectorOrderUnknown; - fSectorPairing = false; - fSectorPairOffset = -1; - - fpOuterGFD = nil; - fpWrapperGFD = nil; - fpDataGFD = nil; - fpOuterWrapper = nil; - fpImageWrapper = nil; - fpParentImg = nil; - fDOSVolumeNum = kVolumeNumNotSet; - fOuterLength = -1; - fWrappedLength = -1; - fLength = -1; - fExpandable = false; - fReadOnly = true; - fDirty = false; - - fHasSectors = false; - fHasBlocks = false; - fHasNibbles = false; - - fNumTracks = -1; - fNumSectPerTrack = -1; - fNumBlocks = -1; - - fpScanProgressCallback = NULL; - - /* - * Create a working copy of the nibble descr table. We want to leave - * open the possibility of applications editing or discarding entries, - * so we work off of a copy. - * - * Ideally we'd allow these to be set per-track, so that certain odd - * formats could be handled transparently (e.g. Muse tweaked DOS 3.2) - * for formatting as well as reading. - */ - assert(kStdNibbleDescrs[kNibbleDescrCustom].numSectors == 0); - assert(kNibbleDescrCustom == NELEM(kStdNibbleDescrs)-1); - fpNibbleDescrTable = new NibbleDescr[NELEM(kStdNibbleDescrs)]; - fNumNibbleDescrEntries = NELEM(kStdNibbleDescrs); - memcpy(fpNibbleDescrTable, kStdNibbleDescrs, sizeof(kStdNibbleDescrs)); - - fNibbleTrackBuf = nil; - fNibbleTrackLoaded = -1; - - fNuFXCompressType = kNuThreadFormatLZW2; - - fNotes = nil; - fpBadBlockMap = nil; - fDiskFSRefCnt = 0; -} - -/* - * Throw away local storage. - */ -DiskImg::~DiskImg(void) -{ - if (fpDataGFD != nil) { - WMSG0("~DiskImg closing GenericFD(s)\n"); - } - (void) CloseImage(); - delete[] fpNibbleDescrTable; - delete[] fNibbleTrackBuf; - delete[] fNotes; - delete fpBadBlockMap; - - /* normally these will be closed, but perhaps not if something failed */ - if (fpOuterGFD != nil) - delete fpOuterGFD; - if (fpWrapperGFD != nil) - delete fpWrapperGFD; - if (fpDataGFD != nil) - delete fpDataGFD; - if (fpOuterWrapper != nil) - delete fpOuterWrapper; - if (fpImageWrapper != nil) - delete fpImageWrapper; - - fDiskFSRefCnt = 100; // flag as freed -} - - -/* - * Set the nibble descr pointer. - */ -void -DiskImg::SetNibbleDescr(int idx) -{ - assert(idx >= 0 && idx < kNibbleDescrMAX); - fpNibbleDescr = &fpNibbleDescrTable[idx]; -} - -/* - * Set up a custom nibble descriptor. - */ -void -DiskImg::SetCustomNibbleDescr(const NibbleDescr* pDescr) -{ - if (pDescr == NULL) { - fpNibbleDescr = NULL; - } else { - assert(fpNibbleDescrTable != NULL); - //WMSG2("Overwriting entry %d with new value (special=%d)\n", - // kNibbleDescrCustom, pDescr->special); - fpNibbleDescrTable[kNibbleDescrCustom] = *pDescr; - fpNibbleDescr = &fpNibbleDescrTable[kNibbleDescrCustom]; - } -} - - -/* - * Open a volume or a file on disk. - * - * For Windows, we need to handle logical/physical volumes specially. If - * the filename matches the appropriate pattern, use a different GFD. - */ -DIError -DiskImg::OpenImage(const char* pathName, char fssep, bool readOnly) -{ - DIError dierr = kDIErrNone; - bool isWinDevice = false; - - if (fpDataGFD != nil) { - WMSG0(" DI already open!\n"); - return kDIErrAlreadyOpen; - } - WMSG3(" DI OpenImage '%s' '%.1s' ro=%d\n", pathName, &fssep, readOnly); - - fReadOnly = readOnly; - -#ifdef _WIN32 - if ((fssep == '\0' || fssep == '\\') && - pathName[0] >= 'A' && pathName[0] <= 'Z' && - pathName[1] == ':' && pathName[2] == '\\' && - pathName[3] == '\0') - { - isWinDevice = true; // logical volume ("A:\") - } - if ((fssep == '\0' || fssep == '\\') && - isdigit(pathName[0]) && isdigit(pathName[1]) && - pathName[2] == ':' && pathName[3] == '\\' && - pathName[4] == '\0') - { - isWinDevice = true; // physical volume ("80:\") - } - if ((fssep == '\0' || fssep == '\\') && - strncmp(pathName, kASPIDev, strlen(kASPIDev)) == 0 && - pathName[strlen(pathName)-1] == '\\') - { - isWinDevice = true; // ASPI volume ("ASPI:x:y:z\") - } -#endif - - if (isWinDevice) { -#ifdef _WIN32 - GFDWinVolume* pGFDWinVolume = new GFDWinVolume; - - dierr = pGFDWinVolume->Open(pathName, fReadOnly); - if (dierr != kDIErrNone) { - delete pGFDWinVolume; - goto bail; - } - - fpWrapperGFD = pGFDWinVolume; - // Use a unique extension to skip some of the probing. - dierr = AnalyzeImageFile("CPDevice.cp-win-vol", '\0'); - if (dierr != kDIErrNone) - goto bail; -#endif - } else { - GFDFile* pGFDFile = new GFDFile; - - dierr = pGFDFile->Open(pathName, fReadOnly); - if (dierr != kDIErrNone) { - delete pGFDFile; - goto bail; - } - - //fImageFileName = new char[strlen(pathName) + 1]; - //strcpy(fImageFileName, pathName); - - fpWrapperGFD = pGFDFile; - pGFDFile = nil; - - dierr = AnalyzeImageFile(pathName, fssep); - if (dierr != kDIErrNone) - goto bail; - } - - - assert(fpDataGFD != nil); - -bail: - return dierr; -} - -/* - * Open from a buffer, which could point to unadorned ready-to-go content - * or to a preloaded image file. - */ -DIError -DiskImg::OpenImage(const void* buffer, long length, bool readOnly) -{ - if (fpDataGFD != nil) { - WMSG0(" DI already open!\n"); - return kDIErrAlreadyOpen; - } - WMSG3(" DI OpenImage %08lx %ld ro=%d\n", (long) buffer, length, readOnly); - - DIError dierr; - GFDBuffer* pGFDBuffer; - - fReadOnly = readOnly; - pGFDBuffer = new GFDBuffer; - - dierr = pGFDBuffer->Open(const_cast(buffer), length, false, false, - readOnly); - if (dierr != kDIErrNone) { - delete pGFDBuffer; - return dierr; - } - - fpWrapperGFD = pGFDBuffer; - pGFDBuffer = nil; - - dierr = AnalyzeImageFile("", '\0'); - if (dierr != kDIErrNone) - return dierr; - - assert(fpDataGFD != nil); - return kDIErrNone; -} - -/* - * Open a range of blocks from an already-open disk image. This is only - * useful for things like UNIDOS volumes, which don't have an associated - * file in the image and are linear. - * - * The "read only" flag is inherited from the parent. - * - * For embedded images with visible file structure, we should be using - * an EmbeddedFD instead. [Note these were never implemented.] - * - * NOTE: there is an implicit ProDOS block ordering imposed on the parent - * image. It turns out that all of our current embedded parents use - * ProDOS-ordered blocks, so it works out okay, but the "linear" requirement - * above goes beyond just having contiguous blocks. - */ -DIError -DiskImg::OpenImage(DiskImg* pParent, long firstBlock, long numBlocks) -{ - WMSG3(" DI OpenImage parent=0x%08lx %ld %ld\n", (long) pParent, firstBlock, - numBlocks); - if (fpDataGFD != nil) { - WMSG0(" DI already open!\n"); - return kDIErrAlreadyOpen; - } - - if (pParent == nil || firstBlock < 0 || numBlocks <= 0 || - firstBlock + numBlocks > pParent->GetNumBlocks()) - { - assert(false); - return kDIErrInvalidArg; - } - - fReadOnly = pParent->GetReadOnly(); // very important - - DIError dierr; - GFDGFD* pGFDGFD; - - pGFDGFD = new GFDGFD; - dierr = pGFDGFD->Open(pParent->fpDataGFD, firstBlock * kBlockSize, fReadOnly); - if (dierr != kDIErrNone) { - delete pGFDGFD; - return dierr; - } - - fpDataGFD = pGFDGFD; - assert(fpWrapperGFD == nil); - - /* - * This replaces the call to "analyze image file" because we know we - * already have an open file with specific characteristics. - */ - //fOffset = pParent->fOffset + kBlockSize * firstBlock; - fLength = numBlocks * kBlockSize; - fOuterLength = fWrappedLength = fLength; - fFileFormat = kFileFormatUnadorned; - fPhysical = pParent->fPhysical; - fOrder = pParent->fOrder; - - fpParentImg = pParent; - - return dierr; -} -DIError -DiskImg::OpenImage(DiskImg* pParent, long firstTrack, long firstSector, - long numSectors) -{ - WMSG4(" DI OpenImage parent=0x%08lx %ld %ld %ld\n", (long) pParent, - firstTrack, firstSector, numSectors); - if (fpDataGFD != nil) { - WMSG0(" DI already open!\n"); - return kDIErrAlreadyOpen; - } - - if (pParent == nil) - return kDIErrInvalidArg; - - int prntSectPerTrack = pParent->GetNumSectPerTrack(); - int lastTrack = firstTrack + - (numSectors + prntSectPerTrack-1) / prntSectPerTrack; - if (firstTrack < 0 || numSectors <= 0 || - lastTrack > pParent->GetNumTracks()) - { - return kDIErrInvalidArg; - } - - fReadOnly = pParent->GetReadOnly(); // very important - - DIError dierr; - GFDGFD* pGFDGFD; - - pGFDGFD = new GFDGFD; - dierr = pGFDGFD->Open(pParent->fpDataGFD, - kSectorSize * firstTrack * prntSectPerTrack, fReadOnly); - if (dierr != kDIErrNone) { - delete pGFDGFD; - return dierr; - } - - fpDataGFD = pGFDGFD; - assert(fpWrapperGFD == nil); - - /* - * This replaces the call to "analyze image file" because we know we - * already have an open file with specific characteristics. - */ - assert(firstSector == 0); // else fOffset calculation breaks - //fOffset = pParent->fOffset + kSectorSize * firstTrack * prntSectPerTrack; - fLength = numSectors * kSectorSize; - fOuterLength = fWrappedLength = fLength; - fFileFormat = kFileFormatUnadorned; - fPhysical = pParent->fPhysical; - fOrder = pParent->fOrder; - - fpParentImg = pParent; - - return dierr; -} - - -/* - * Enable sector pairing. Useful for OzDOS. - */ -void -DiskImg::SetPairedSectors(bool enable, int idx) -{ - fSectorPairing = enable; - fSectorPairOffset = idx; - - if (enable) { - assert(idx == 0 || idx == 1); - } -} - -/* - * Close the image, freeing resources. - * - * If we write to a child DiskImg, it's responsible for setting the "dirty" - * flag in its parent (and so on up the chain). That's necessary so that, - * when we close the file, changes made to a child DiskImg cause the parent - * to do any necessary recompression. - * - * [ This is getting called even when image creation failed with an error. - * This is probably the correct behavior, but we may want to be aborting the - * image creation instead of completing it. That's a higher-level decision - * though. ++ATM 20040506 ] - */ -DIError -DiskImg::CloseImage(void) -{ - DIError dierr; - - WMSG1("CloseImage %p\n", this); - - /* check for DiskFS objects that still point to us */ - if (fDiskFSRefCnt != 0) { - WMSG1("ERROR: CloseImage: fDiskFSRefCnt=%d\n", fDiskFSRefCnt); - assert(false); //DebugBreak(); - } - - /* - * Flush any changes. - */ - dierr = FlushImage(kFlushAll); - if (dierr != kDIErrNone) - return dierr; - - /* - * Clean up. Close GFD, OrigGFD, and OuterGFD. Delete ImageWrapper - * and OuterWrapper. - * - * In some cases we will have the file open more than once (e.g. a - * NuFX archive, which must be opened on disk). - */ - if (fpDataGFD != nil) { - fpDataGFD->Close(); - delete fpDataGFD; - fpDataGFD = nil; - } - if (fpWrapperGFD != nil) { - fpWrapperGFD->Close(); - delete fpWrapperGFD; - fpWrapperGFD = nil; - } - if (fpOuterGFD != nil) { - fpOuterGFD->Close(); - delete fpOuterGFD; - fpOuterGFD = nil; - } - delete fpImageWrapper; - fpImageWrapper = nil; - delete fpOuterWrapper; - fpOuterWrapper = nil; - - return dierr; -} - - -/* - * Flush data to disk. - * - * The only time this really needs to do anything on a disk image file is - * when we have compressed data (NuFX, DDD, .gz, .zip). The uncompressed - * wrappers either don't do anything ("unadorned") or just update some - * header fields (DiskCopy42). - * - * If "mode" is kFlushFastOnly, we only flush the formats that don't really - * need flushing. This is part of a scheme to keep the disk contents in a - * reasonable state on the off chance we crash with a modified file open. - * It also helps the user understand when changes are being made immediately - * vs. when they're written to memory and compressed later. We could just - * refuse to raise the "dirty" flag when modifying "simple" file formats, - * but that would change the meaning of the flag from "something has been - * changed" to "what's in the file and what's in memory differ". I want it - * to be a "dirty" flag. - */ -DIError -DiskImg::FlushImage(FlushMode mode) -{ - DIError dierr = kDIErrNone; - - WMSG2(" DI FlushImage (dirty=%d mode=%d)\n", fDirty, mode); - if (!fDirty) - return kDIErrNone; - if (fpDataGFD == nil) { - /* - * This can happen if we tried to create a disk image but failed, e.g. - * couldn't create the output file because of access denied on the - * directory. There's no data, therefore nothing to flush, but the - * "dirty" flag is set because CreateImageCommon sets it almost - * immediately. - */ - WMSG0(" (disk must've failed during creation)\n"); - fDirty = false; - return kDIErrNone; - } - - if (mode == kFlushFastOnly && - ((fpImageWrapper != nil && !fpImageWrapper->HasFastFlush()) || - (fpOuterWrapper != nil && !fpOuterWrapper->HasFastFlush()) )) - { - WMSG0("DI fast flush requested, but one or both wrappers are slow\n"); - return kDIErrNone; - } - - /* - * Step 1: make sure any local caches have been flushed. - */ - /* (none) */ - - /* - * Step 2: push changes from fpDataGFD to fpWrapperGFD. This will - * cause ImageWrapper to rebuild itself (SHK, DDD, whatever). In - * some cases this amounts to copying the data on top of itself, - * which we can avoid easily. - * - * Embedded volumes don't have wrappers; when you write to an - * embedded volume, it passes straight through to the parent. - * - * (Note to self: formats like NuFX that write to a temp file and then - * rename over the old will close fpWrapperGFD and just access it - * directly. This is bad, because it doesn't allow them to have an - * "outer" format, but it's the way life is. The point is that it's - * okay for fpWrapperGFD to be non-nil but represent a closed file, - * so long as the "Flush" function has it figured out.) - */ - if (fpWrapperGFD != nil) { - WMSG2(" DI flushing data changes to wrapper (fLen=%ld fWrapLen=%ld)\n", - (long) fLength, (long) fWrappedLength); - dierr = fpImageWrapper->Flush(fpWrapperGFD, fpDataGFD, fLength, - &fWrappedLength); - if (dierr != kDIErrNone) { - WMSG1(" ERROR: wrapper flush failed (err=%d)\n", dierr); - return dierr; - } - /* flush the GFD in case it's a Win32 volume with block caching */ - dierr = fpWrapperGFD->Flush(); - } else { - assert(fpParentImg != nil); - } - - /* - * Step 3: if we have an fpOuterGFD, rebuild the file with the data - * in fpWrapperGFD. - */ - if (fpOuterWrapper != nil) { - WMSG1(" DI saving wrapper to outer, fWrapLen=%ld\n", - (long) fWrappedLength); - assert(fpOuterGFD != nil); - dierr = fpOuterWrapper->Save(fpOuterGFD, fpWrapperGFD, - fWrappedLength); - if (dierr != kDIErrNone) { - WMSG1(" ERROR: outer save failed (err=%d)\n", dierr); - return dierr; - } - } - - fDirty = false; - return kDIErrNone; -} - - - -/* - * Given the filename extension and a GFD, figure out what's inside. - * - * The filename extension should give us some idea what to expect: - * SHK, SDK, BXY - ShrinkIt compressed disk image - * GZ - gzip-compressed file (with something else inside) - * ZIP - ZIP archive with a single disk image inside - * DDD - DDD, DDD Pro, or DDD5.0 compressed image - * DSK - DiskCopy 4.2 or DO/PO - * DC - DiskCopy 4.2 (or 6?) - * DC6 - DiskCopy 6 (usually just raw sectors) - * DO, PO, D13, RAW? - DOS-order or ProDOS-order uncompressed - * IMG - Copy ][+ image (unadorned, physical sector order) - * HDV - virtual hard drive image - * NIB, RAW? - nibblized image - * (no extension) uncompressed - * cp-win-vol - our "magic" extension to indicate a Windows logical volume - * - * We can also examine the file length to see if it's a standard size - * (140K, 800K) and look for magic values in the header. - * - * If we can access the contents directly from disk, we do so. It's - * possibly more efficient to load the whole thing into memory, but if - * we have that much memory then the OS should cache it for us. (I have - * some 20MB disk images from my hard drive that shouldn't be loaded - * in their entirety. Certainly don't want to load a 512MB CFFA image.) - * - * On input, the following fields must be set: - * fpWrapperGFD - GenericFD for the file pointed to by "pathname" (or for a - * memory buffer if this is a sub-volume) - * - * On success, the following fields will be set: - * fWrappedLength, fOuterLength - set appropriately - * fpDataGFD - GFD for the raw data, possibly just a GFDGFD with an offset - * fLength - length of unadorned data in the file, or the length of - * data stored in fBuffer (test for fBuffer!=nil) - * fFileFormat - set to the overall file format, mostly interesting - * for identification of the file "wrapper" - * fPhysicalFormat - set to the type of data this holds - * (maybe) fOrder - set when the file format or extension dictates, e.g. - * 2MG or *.po; not always reliable - * (maybe) fDOSVolumeNum - set to DOS volume number from wrapper - * - * This may set fReadOnly if one of the wrappers looks okay but is reporting - * a bad checksum. - */ -DIError -DiskImg::AnalyzeImageFile(const char* pathName, char fssep) -{ - DIError dierr = kDIErrNone; - FileFormat probableFormat; - bool reliableExt; - const char* ext = FindExtension(pathName, fssep); - char* extBuf = nil; // uses malloc/free - bool needExtFromOuter = false; - - if (ext != nil) { - assert(*ext == '.'); - ext++; - } else - ext = ""; - - WMSG1(" DI AnalyzeImageFile ext='%s'\n", ext); - - /* sanity check: nobody should have configured these yet */ - assert(fOuterFormat == kOuterFormatUnknown); - assert(fFileFormat == kFileFormatUnknown); - assert(fOrder == kSectorOrderUnknown); - assert(fFormat == kFormatUnknown); - fLength = -1; - dierr = fpWrapperGFD->Seek(0, kSeekEnd); - if (dierr != kDIErrNone) { - WMSG0(" DI Couldn't seek to end of wrapperGFD\n"); - goto bail; - } - fWrappedLength = fOuterLength = fpWrapperGFD->Tell(); - - /* quick test for zero-length files */ - if (fWrappedLength == 0) - return kDIErrUnrecognizedFileFmt; - - /* - * Start by checking for a zip/gzip "wrapper wrapper". We want to strip - * that away before we do anything else. Because web sites tend to - * gzip everything in sight whether it needs it or not, we treat this - * as a special case and assume that anything could be inside. - * - * Some cases are difficult to handle, e.g. ".SDK", since NufxLib - * doesn't let us open an archive that is sitting in memory. - * - * We could also handle disk images stored as ordinary files stored - * inside SHK. Not much point in handling multiple files down at - * this level though. - */ - if (strcasecmp(ext, "gz") == 0 && - OuterGzip::Test(fpWrapperGFD, fOuterLength) == kDIErrNone) - { - WMSG0(" DI found gz outer wrapper\n"); - - fpOuterWrapper = new OuterGzip(); - if (fpOuterWrapper == nil) { - dierr = kDIErrMalloc; - goto bail; - } - fOuterFormat = kOuterFormatGzip; - - /* drop the ".gz" and get down to the next extension */ - ext = ""; - extBuf = strdup(pathName); - if (extBuf != nil) { - char* localExt; - - localExt = (char*) FindExtension(extBuf, fssep); - if (localExt != nil) - *localExt = '\0'; - localExt = (char*) FindExtension(extBuf, fssep); - if (localExt != nil) { - ext = localExt; - assert(*ext == '.'); - ext++; - } - } - WMSG1(" DI after gz, ext='%s'\n", ext == nil ? "(nil)" : ext); - - } else if (strcasecmp(ext, "zip") == 0) { - dierr = OuterZip::Test(fpWrapperGFD, fOuterLength); - if (dierr != kDIErrNone) - goto bail; - - WMSG0(" DI found ZIP outer wrapper\n"); - - fpOuterWrapper = new OuterZip(); - if (fpOuterWrapper == nil) { - dierr = kDIErrMalloc; - goto bail; - } - fOuterFormat = kOuterFormatZip; - - needExtFromOuter = true; - - } else { - fOuterFormat = kOuterFormatNone; - } - - /* finish up outer wrapper stuff */ - if (fOuterFormat != kOuterFormatNone) { - GenericFD* pNewGFD = nil; - dierr = fpOuterWrapper->Load(fpWrapperGFD, fOuterLength, fReadOnly, - &fWrappedLength, &pNewGFD); - if (dierr != kDIErrNone) { - WMSG0(" DI outer prep failed\n"); - /* extensions are "reliable", so failure is unavoidable */ - goto bail; - } - - /* Load() sets this */ - if (fpOuterWrapper->IsDamaged()) { - AddNote(kNoteWarning, "The zip/gzip wrapper appears to be damaged."); - fReadOnly = true; - } - - /* shift GFDs */ - fpOuterGFD = fpWrapperGFD; - fpWrapperGFD = pNewGFD; - - if (needExtFromOuter) { - ext = fpOuterWrapper->GetExtension(); - if (ext == nil) - ext = ""; - } - } - - /* - * Try to figure out what format the file is in. - * - * First pass, try only what the filename says it is. This way, if - * two file formats look alike, we have a good chance of getting it - * right. - * - * The "Test" functions have the complete file at their disposal. The - * file's length is stored in "fWrappedLength" for convenience. - */ - reliableExt = false; - probableFormat = kFileFormatUnknown; - if (strcasecmp(ext, "2mg") == 0 || strcasecmp(ext, "2img") == 0) { - reliableExt = true; - if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - probableFormat = kFileFormat2MG; - } else if (strcasecmp(ext, "shk") == 0 || strcasecmp(ext, "sdk") == 0 || - strcasecmp(ext, "bxy") == 0) - { - DIError dierr2; - reliableExt = true; - dierr2 = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength); - if (dierr2 == kDIErrNone) - probableFormat = kFileFormatNuFX; - else if (dierr2 == kDIErrFileArchive) { - WMSG0(" AnalyzeImageFile thinks it found a NuFX file archive\n"); - dierr = dierr2; - goto bail; - } - } else if (strcasecmp(ext, "hdv") == 0) { - /* usually just a "raw" disk, but check for Sim //e */ - if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - probableFormat = kFileFormatSim2eHDV; - - /* ProDOS .hdv volumes can expand */ - fExpandable = true; - } else if (strcasecmp(ext, "dsk") == 0 || strcasecmp(ext, "dc") == 0) { - /* might be DiskCopy */ - if (WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - probableFormat = kFileFormatDiskCopy42; - } else if (strcasecmp(ext, "ddd") == 0) { - /* do this after compressed formats but before unadorned */ - reliableExt = true; - if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - probableFormat = kFileFormatDDD; - } else if (strcasecmp(ext, "app") == 0) { - reliableExt = true; - if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - probableFormat = kFileFormatTrackStar; - } else if (strcasecmp(ext, "fdi") == 0) { - reliableExt = true; - if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - probableFormat = kFileFormatFDI; - } else if (strcasecmp(ext, "img") == 0) { - if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - { - probableFormat = kFileFormatUnadorned; - fPhysical = kPhysicalFormatSectors; - fOrder = kSectorOrderPhysical; - } - } else if (strcasecmp(ext, "nib") == 0 || strcasecmp(ext, "raw") == 0) { - if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - { - probableFormat = kFileFormatUnadorned; - fPhysical = kPhysicalFormatNib525_6656; - /* figure out NibbleFormat later */ - } - } else if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "po") == 0 || - strcasecmp(ext, "d13") == 0 || strcasecmp(ext, "dc6") == 0) - { - if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - { - probableFormat = kFileFormatUnadorned; - fPhysical = kPhysicalFormatSectors; - if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "d13") == 0) - fOrder = kSectorOrderDOS; - else - fOrder = kSectorOrderProDOS; // po, dc6 - WMSG1(" DI guessing order is %d by extension\n", fOrder); - } - } else if (strcasecmp(ext, "cp-win-vol") == 0) { - /* this is a Windows logical volume */ - reliableExt = true; - probableFormat = kFileFormatUnadorned; - fPhysical = kPhysicalFormatSectors; - fOrder = kSectorOrderProDOS; - } else { - /* no match on the filename extension; start guessing */ - } - - if (probableFormat != kFileFormatUnknown) { - /* - * Found a match. Use "probableFormat" to open the file. - */ - WMSG1(" DI scored hit on extension '%s'\n", ext); - } else { - /* - * Didn't work. If the file extension was marked "reliable", then - * either we have the wrong extension on the file, or the contents - * are damaged. - * - * If the extension isn't reliable, or simply absent, then we have - * to probe through the formats we know and just hope for the best. - * - * If the "test" function returns with a checksum failure, we take - * it to mean that the format was positively identified, but the - * data inside is corrupted. This results in an immediate return - * with the checksum failure noted. Only a few wrapper formats - * have checksums embedded. (The "test" functions should only - * be looking at header checksums.) - */ - if (reliableExt) { - WMSG1(" DI file extension '%s' did not match contents\n", ext); - dierr = kDIErrBadFileFormat; - goto bail; - } else { - WMSG1(" DI extension '%s' not useful, probing formats\n", ext); - dierr = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength); - if (dierr == kDIErrNone) { - probableFormat = kFileFormatNuFX; - goto gotit; - } else if (dierr == kDIErrFileArchive) - goto bail; // we know it's NuFX, we know we can't use it - else if (dierr == kDIErrBadChecksum) - goto bail; // right file type, bad data - - dierr = WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength); - if (dierr == kDIErrNone) { - probableFormat = kFileFormatDiskCopy42; - goto gotit; - } else if (dierr == kDIErrBadChecksum) - goto bail; // right file type, bad data - - if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { - probableFormat = kFileFormat2MG; - } else if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { - probableFormat = kFileFormatDDD; - } else if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - { - probableFormat = kFileFormatSim2eHDV; - } else if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - { - probableFormat = kFileFormatTrackStar; - } else if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) - { - probableFormat = kFileFormatFDI; - } else if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { - probableFormat = kFileFormatUnadorned; - fPhysical = kPhysicalFormatNib525_6656; // placeholder - } else if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { - probableFormat = kFileFormatUnadorned; - fPhysical = kPhysicalFormatSectors; - } -gotit: ; - } - } - - /* - * Either we recognize it or we don't. Finish opening the file by - * setting up "fLength" and "fPhysical" values, extracting data - * into a memory buffer if necessary. fpDataGFD is set up by the - * "prep" function. - * - * If we're lucky, this will also configure "fOrder" for us, which is - * important when we can't recognize the filesystem format (for correct - * operation of disk tools). - */ - switch (probableFormat) { - case kFileFormat2MG: - fpImageWrapper = new Wrapper2MG(); - break; - case kFileFormatDiskCopy42: - fpImageWrapper = new WrapperDiskCopy42(); - break; - case kFileFormatSim2eHDV: - fpImageWrapper = new WrapperSim2eHDV(); - break; - case kFileFormatTrackStar: - fpImageWrapper = new WrapperTrackStar(); - break; - case kFileFormatFDI: - fpImageWrapper = new WrapperFDI(); - fReadOnly = true; // writing to FDI not yet supported - break; - case kFileFormatNuFX: - fpImageWrapper = new WrapperNuFX(); - ((WrapperNuFX*)fpImageWrapper)->SetCompressType( - (NuThreadFormat) fNuFXCompressType); - break; - case kFileFormatDDD: - fpImageWrapper = new WrapperDDD(); - break; - case kFileFormatUnadorned: - if (IsSectorFormat(fPhysical)) - fpImageWrapper = new WrapperUnadornedSector(); - else if (IsNibbleFormat(fPhysical)) - fpImageWrapper = new WrapperUnadornedNibble(); - else { - assert(false); - } - break; - default: - WMSG0(" DI couldn't figure out the file format\n"); - dierr = kDIErrUnrecognizedFileFmt; - break; - } - if (fpImageWrapper != nil) { - assert(fpDataGFD == nil); - dierr = fpImageWrapper->Prep(fpWrapperGFD, fWrappedLength, fReadOnly, - &fLength, &fPhysical, &fOrder, &fDOSVolumeNum, - &fpBadBlockMap, &fpDataGFD); - } else { - /* could be a mem alloc failure that didn't set dierr */ - if (dierr == kDIErrNone) - dierr = kDIErrGeneric; - } - - if (dierr != kDIErrNone) { - WMSG1(" DI wrapper prep failed (err=%d)\n", dierr); - goto bail; - } - - /* check for non-fatal checksum failures, e.g. DiskCopy42 */ - if (fpImageWrapper->IsDamaged()) { - AddNote(kNoteWarning, "File checksum didn't match."); - fReadOnly = true; - } - - fFileFormat = probableFormat; - - assert(fLength >= 0); - assert(fpDataGFD != nil); - assert(fOuterFormat != kOuterFormatUnknown); - assert(fFileFormat != kFileFormatUnknown); - assert(fPhysical != kPhysicalFormatUnknown); - -bail: - free(extBuf); - return dierr; -} - - -/* - * Try to figure out what we're looking at. - * - * Returns an error if we don't think this is even a disk image. If we - * just can't figure it out, we return success but with the format value - * set to "unknown". This gives the caller a chance to use "override" - * to help us find our way. - * - * On entry: - * fpDataGFD, fLength, and fFileFormat are defined - * fSectorPairing is specified - * fOrder has a semi-reliable guess at sector ordering - * On exit: - * fOrder and fFormat are set to the best of our ability - * fNumTracks, fNumSectPerTrack, and fNumBlocks are set - * fHasSectors, fHasTracks, and fHasNibbles are set - * fFileSysOrder is set - * fpNibbleDescr will be set for nibble images - */ -DIError -DiskImg::AnalyzeImage(void) -{ - assert(fLength >= 0); - assert(fpDataGFD != nil); - assert(fFileFormat != kFileFormatUnknown); - assert(fPhysical != kPhysicalFormatUnknown); - assert(fFormat == kFormatUnknown); - assert(fFileSysOrder == kSectorOrderUnknown); - assert(fNumTracks == -1); - assert(fNumSectPerTrack == -1); - assert(fNumBlocks == -1); - if (fpDataGFD == nil) - return kDIErrInternal; - - /* - * Figure out how many tracks and sectors the image has. - * - * For an odd-sized ProDOS image, there will be no tracks and sectors. - */ - if (IsSectorFormat(fPhysical)) { - if (!fLength) { - WMSG0(" DI zero-length disk images not allowed\n"); - return kDIErrOddLength; - } - - if (fLength == kD13Length) { - /* 13-sector .d13 image */ - fHasSectors = true; - fNumSectPerTrack = 13; - fNumTracks = kTrackCount525; - assert(!fHasBlocks); - } else if (fLength % (16 * kSectorSize) == 0) { - /* looks like a collection of 16-sector tracks */ - fHasSectors = true; - - fNumSectPerTrack = 16; - fNumTracks = (int) (fLength / (fNumSectPerTrack * kSectorSize)); - - /* sector pairing effectively cuts #of tracks in half */ - if (fSectorPairing) { - if ((fNumTracks & 0x01) != 0) { - WMSG0(" DI error: bad attempt at sector pairing\n"); - assert(false); - fSectorPairing = false; - } - } - - if (fSectorPairing) - fNumTracks /= 2; - } else { - if (fSectorPairing) { - WMSG1("GLITCH: sector pairing enabled, but fLength=%ld\n", - (long) fLength); - return kDIErrOddLength; - } - - assert(fNumTracks == -1); - assert(fNumSectPerTrack == -1); - assert((fLength % kBlockSize) == 0); - - fHasBlocks = true; - fNumBlocks = (long) (fLength / kBlockSize); - } - } else if (IsNibbleFormat(fPhysical)) { - fHasNibbles = fHasSectors = true; - - /* - * Figure out if it's 13-sector or 16-sector (or garbage). We - * have to make an assessment of the entire disk so we can declare - * it to be 13-sector or 16-sector, which is useful for DiskFS - * which will want to scan for DOS VTOCs and other goodies. We - * also want to provide a default NibbleDescr. - * - * Failing that, we still allow it to be opened for raw track access. - * - * This also sets fNumTracks, which could be more than 35 if we're - * working with a TrackStar or FDI image. - */ - DIError dierr; - dierr = AnalyzeNibbleData(); // sets nibbleDescr and DOS vol num - if (dierr == kDIErrNone) { - assert(fpNibbleDescr != nil); - fNumSectPerTrack = fpNibbleDescr->numSectors; - fOrder = kSectorOrderPhysical; - - if (!fReadOnly && !fpNibbleDescr->dataVerifyChecksum) { - WMSG0("DI nibbleDescr does not verify data checksum, disabling writes\n"); - AddNote(kNoteInfo, - "Sectors use non-standard data checksums; writing disabled."); - fReadOnly = true; - } - } else { - //assert(fpNibbleDescr == nil); - fNumSectPerTrack = -1; - fOrder = kSectorOrderPhysical; - fHasSectors = false; - } - } else { - WMSG1("Unsupported physical %d\n", fPhysical); - assert(false); - return kDIErrGeneric; - } - - /* - * Compute the number of blocks. For a 13-sector disk, block access - * is not possible. - * - * For nibble formats, we have to base the block count on the number - * of sectors rather than the file length. - */ - if (fHasSectors) { - assert(fNumSectPerTrack > 0); - if ((fNumSectPerTrack & 0x01) == 0) { - /* not a 13-sector disk, so define blocks in terms of sectors */ - /* (effects of sector pairing are already taken into account) */ - fHasBlocks = true; - fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2; - } - } else if (fHasBlocks) { - if ((fLength % kBlockSize) == 0) { - /* not sector-oriented, so define blocks based on length */ - fHasBlocks = true; - fNumBlocks = (long) (fLength / kBlockSize); - - if (fSectorPairing) { - if ((fNumBlocks & 0x01) != 0) { - WMSG0(" DI error: bad attempt at sector pairing (blk)\n"); - assert(false); - fSectorPairing = false; - } else - fNumBlocks /= 2; - } - - } else { - assert(false); - return kDIErrGeneric; - } - } else if (fHasNibbles) { - assert(fNumBlocks == -1); - } else { - WMSG0(" DI none of fHasSectors/fHasBlocks/fHasNibbles are set\n"); - assert(false); - return kDIErrInternal; - } - - /* - * We've got the track/sector/block layout sorted out; now figure out - * what kind of filesystem we're dealing with. - */ - AnalyzeImageFS(); - - WMSG4(" DI AnalyzeImage tracks=%ld sectors=%d blocks=%ld fileSysOrder=%d\n", - fNumTracks, fNumSectPerTrack, fNumBlocks, fFileSysOrder); - WMSG3(" hasBlocks=%d hasSectors=%d hasNibbles=%d\n", - fHasBlocks, fHasSectors, fHasNibbles); - - return kDIErrNone; -} - -/* - * Try to figure out what filesystem exists on this disk image. - * - * We want to test for DOS before ProDOS, because sometimes they overlap (e.g. - * 800K ProDOS disk with five 160K DOS volumes on it). - * - * Sets fFormat, fOrder, and fFileSysOrder. - */ -void -DiskImg::AnalyzeImageFS(void) -{ - /* - * In some circumstances it would be useful to have a set describing - * what filesystems we might expect to find, e.g. we're not likely to - * encounter RDOS embedded in a CF card. - */ - if (DiskFSMacPart::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatMacPart); - WMSG1(" DI found MacPart, order=%d\n", fOrder); - } else if (DiskFSMicroDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatMicroDrive); - WMSG1(" DI found MicroDrive, order=%d\n", fOrder); - } else if (DiskFSFocusDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatFocusDrive); - WMSG1(" DI found FocusDrive, order=%d\n", fOrder); - } else if (DiskFSCFFA::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - // The CFFA format doesn't have a partition map, but we do insist - // on finding multiple volumes. It needs to come after MicroDrive, - // because a disk formatted for CFFA then subsequently partitioned - // for MicroDrive will still look like valid CFFA unless you zero - // out the blocks. - assert(fFormat == kFormatCFFA4 || fFormat == kFormatCFFA8); - WMSG1(" DI found CFFA, order=%d\n", fOrder); - } else if (DiskFSFAT::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - // This is really just a trap to catch CFFA cards that were formatted - // for ProDOS and then re-formatted for MSDOS. As such it needs to - // come before the ProDOS test. It only works on larger volumes, - // and can be overridden, so it's pretty safe. - assert(fFormat == kFormatMSDOS); - WMSG1(" DI found MSDOS, order=%d\n", fOrder); - } else if (DiskFSDOS33::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatDOS32 || fFormat == kFormatDOS33); - WMSG1(" DI found DOS3.x, order=%d\n", fOrder); - if (fNumSectPerTrack == 13) - fFormat = kFormatDOS32; - } else if (DiskFSUNIDOS::TestWideFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - // Should only succeed on 400K embedded chunks. - assert(fFormat == kFormatDOS33); - fNumSectPerTrack = 32; - fNumTracks /= 2; - WMSG1(" DI found 'wide' DOS3.3, order=%d\n", fOrder); - } else if (DiskFSUNIDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatUNIDOS); - fNumSectPerTrack = 32; - fNumTracks /= 2; - WMSG1(" DI found UNIDOS, order=%d\n", fOrder); - } else if (DiskFSOzDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatOzDOS); - fNumSectPerTrack = 32; - fNumTracks /= 2; - WMSG1(" DI found OzDOS, order=%d\n", fOrder); - } else if (DiskFSProDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatProDOS); - WMSG1(" DI found ProDOS, order=%d\n", fOrder); - } else if (DiskFSPascal::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatPascal); - WMSG1(" DI found Pascal, order=%d\n", fOrder); - } else if (DiskFSCPM::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatCPM); - WMSG1(" DI found CP/M, order=%d\n", fOrder); - } else if (DiskFSRDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatRDOS33 || - fFormat == kFormatRDOS32 || - fFormat == kFormatRDOS3); - WMSG1(" DI found RDOS 3.3, order=%d\n", fOrder); - } else if (DiskFSHFS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) - { - assert(fFormat == kFormatMacHFS); - WMSG1(" DI found HFS, order=%d\n", fOrder); - } else { - fFormat = kFormatUnknown; - WMSG1(" DI no recognizeable filesystem found (fOrder=%d)\n", - fOrder); - } - - fFileSysOrder = CalcFSSectorOrder(); -} - - -/* - * Override the format determined by the analyzer. - * - * If they insist on the presence of a valid filesystem, check to make sure - * that filesystem actually exists. - * - * Note that this does not allow overriding the file structure, which must - * be clearly identifiable to be at all useful. If the file has no "wrapper" - * structure, the "unadorned" format should be specified, and the contents - * identified by the PhysicalFormat. - */ -DIError -DiskImg::OverrideFormat(PhysicalFormat physical, FSFormat format, - SectorOrder order) -{ - DIError dierr = kDIErrNone; - SectorOrder newOrder; - FSFormat newFormat; - - WMSG3(" DI override: physical=%d format=%d order=%d\n", - physical, format, order); - - if (!IsSectorFormat(physical) && !IsNibbleFormat(physical)) - return kDIErrUnsupportedPhysicalFmt; - - /* don't allow forcing physical format change */ - if (physical != fPhysical) - return kDIErrInvalidArg; - - /* optimization */ - if (physical == fPhysical && format == fFormat && order == fOrder) { - WMSG0(" DI override matches existing, ignoring\n"); - return kDIErrNone; - } - - newOrder = order; - newFormat = format; - - switch (format) { - case kFormatDOS33: - case kFormatDOS32: - dierr = DiskFSDOS33::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - // Go ahead and allow the override even if the DOS version is wrong. - // So long as the sector count is correct, it's okay. - break; - case kFormatProDOS: - dierr = DiskFSProDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatPascal: - dierr = DiskFSPascal::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatMacHFS: - dierr = DiskFSHFS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatUNIDOS: - dierr = DiskFSUNIDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatOzDOS: - dierr = DiskFSOzDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatCFFA4: - case kFormatCFFA8: - dierr = DiskFSCFFA::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - // So long as it's CFFA, we allow the user to force it to be 4-mode - // or 8-mode. Don't require newFormat==format. - break; - case kFormatMacPart: - dierr = DiskFSMacPart::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatMicroDrive: - dierr = DiskFSMicroDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatFocusDrive: - dierr = DiskFSFocusDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatCPM: - dierr = DiskFSCPM::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatMSDOS: - dierr = DiskFSFAT::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - break; - case kFormatRDOS33: - case kFormatRDOS32: - case kFormatRDOS3: - dierr = DiskFSRDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); - if (newFormat != format) - dierr = kDIErrFilesystemNotFound; // found RDOS, but wrong flavor - break; - case kFormatGenericPhysicalOrd: - case kFormatGenericProDOSOrd: - case kFormatGenericDOSOrd: - case kFormatGenericCPMOrd: - /* no discussion possible, since there's no FS to validate */ - newFormat = format; - newOrder = order; - break; - case kFormatUnknown: - /* only valid in rare situations, e.g. CFFA CreatePlaceholder */ - newFormat = format; - newOrder = order; - break; - default: - dierr = kDIErrUnsupportedFSFmt; - break; - } - - if (dierr != kDIErrNone) { - WMSG0(" DI override failed\n"); - goto bail; - } - - /* - * We passed in "order" to TestFS. If it came back with something - * different, it means that it didn't like the new order value even - * when "leniency" was granted. - */ - if (newOrder != order) { - dierr = kDIErrBadOrdering; - goto bail; - } - - fFormat = format; - fOrder = newOrder; - fFileSysOrder = CalcFSSectorOrder(); - - WMSG0(" DI override accepted\n"); - -bail: - return dierr; -} - -/* - * Figure out the sector ordering for this filesystem, so we can decide - * how the sectors need to be re-arranged when we're reading them. - * - * If the value returned by this function matches fOrder, then no swapping - * will be done. - * - * NOTE: this table is redundant with some knowledge embedded in the - * individual "TestFS" functions. - */ -DiskImg::SectorOrder -DiskImg::CalcFSSectorOrder(void) const -{ - /* in the absence of information, just leave it alone */ - if (fFormat == kFormatUnknown || fOrder == kSectorOrderUnknown) { - WMSG0(" DI WARNING: FindSectorOrder but format not known\n"); - return fOrder; - } - - assert(fOrder == kSectorOrderPhysical || fOrder == kSectorOrderCPM || - fOrder == kSectorOrderProDOS || fOrder == kSectorOrderDOS); - - switch (fFormat) { - case kFormatGenericPhysicalOrd: - case kFormatRDOS32: - case kFormatRDOS3: - return kSectorOrderPhysical; - - case kFormatGenericDOSOrd: - case kFormatDOS33: - case kFormatDOS32: - case kFormatUNIDOS: - case kFormatOzDOS: - return kSectorOrderDOS; - - case kFormatGenericCPMOrd: - case kFormatCPM: - return kSectorOrderCPM; - - case kFormatGenericProDOSOrd: - case kFormatProDOS: - case kFormatRDOS33: - case kFormatPascal: - case kFormatMacHFS: - case kFormatMacMFS: - case kFormatLisa: - case kFormatMSDOS: - case kFormatISO9660: - case kFormatCFFA4: - case kFormatCFFA8: - case kFormatMacPart: - case kFormatMicroDrive: - case kFormatFocusDrive: - return kSectorOrderProDOS; - - default: - assert(false); - return fOrder; - } -} - -/* - * Based on the disk format, figure out if we should prefer blocks or - * sectors when examining disk contents. - */ -bool -DiskImg::ShowAsBlocks(void) const -{ - if (!fHasBlocks) - return false; - - /* in the absence of information, assume sectors */ - if (fFormat == kFormatUnknown) { - if (fOrder == kSectorOrderProDOS) - return true; - else - return false; - } - - switch (fFormat) { - case kFormatGenericPhysicalOrd: - case kFormatGenericDOSOrd: - case kFormatDOS33: - case kFormatDOS32: - case kFormatRDOS3: - case kFormatRDOS33: - case kFormatUNIDOS: - case kFormatOzDOS: - return false; - - case kFormatGenericProDOSOrd: - case kFormatGenericCPMOrd: - case kFormatProDOS: - case kFormatPascal: - case kFormatMacHFS: - case kFormatMacMFS: - case kFormatLisa: - case kFormatCPM: - case kFormatMSDOS: - case kFormatISO9660: - case kFormatCFFA4: - case kFormatCFFA8: - case kFormatMacPart: - case kFormatMicroDrive: - case kFormatFocusDrive: - return true; - - default: - assert(false); - return false; - } -} - - -/* - * Format an image with the requested fileystem format. This only works if - * the matching DiskFS supports formatting of disks. - */ -DIError -DiskImg::FormatImage(FSFormat format, const char* volName) -{ - DIError dierr = kDIErrNone; - DiskFS* pDiskFS = nil; - FSFormat savedFormat; - - WMSG1(" DI FormatImage '%s'\n", volName); - - /* - * Open a temporary DiskFS for the requested format. We do this via the - * standard OpenAppropriate call, so we temporarily switch our format - * out. (We will eventually replace it, but we want to make sure that - * local error handling works correctly, so we restore it for now.) - */ - savedFormat = fFormat; - fFormat = format; - pDiskFS = OpenAppropriateDiskFS(false); - fFormat = savedFormat; - - if (pDiskFS == nil) { - dierr = kDIErrUnsupportedFSFmt; - goto bail; - } - - dierr = pDiskFS->Format(this, volName); - if (dierr != kDIErrNone) - goto bail; - - WMSG0("DI format successful\n"); - fFormat = format; - -bail: - delete pDiskFS; - return dierr; -} - -/* - * Clear an image to zeros, usually done as a prelude to a higher-level format. - * - * BUG: this should also handle the track/sector case. - * - * HEY: this is awfully slow on large disks... should have some sort of - * optimized path that just writes to the GFD or something. Maybe even just - * a "ZeroBlock" instead of "WriteBlock" so we can memset instead of memcpy? - */ -DIError -DiskImg::ZeroImage(void) -{ - DIError dierr = kDIErrNone; - unsigned char blkBuf[kBlockSize]; - long block; - - WMSG1(" DI ZeroImage (%ld blocks)\n", GetNumBlocks()); - memset(blkBuf, 0, sizeof(blkBuf)); - - for (block = 0; block < GetNumBlocks(); block++) { - dierr = WriteBlock(block, blkBuf); - if (dierr != kDIErrNone) - break; - } - - return dierr; -} - - -/* - * Set the "scan progress" function. - * - * We want to use the same function for our sub-volumes too. - */ -void -DiskImg::SetScanProgressCallback(ScanProgressCallback func, void* cookie) -{ - if (fpParentImg != nil) { - /* unexpected, but perfectly okay */ - DebugBreak(); - } - - fpScanProgressCallback = func; - fScanProgressCookie = cookie; - fScanCount = 0; - fScanMsg[0] = '\0'; - fScanLastMsgWhen = time(nil); -} - -/* - * Update the progress. Call with a string at the start of a volume, then - * call with a NULL pointer every time we add a file. - */ -bool -DiskImg::UpdateScanProgress(const char* newStr) -{ - ScanProgressCallback func = fpScanProgressCallback; - DiskImg* pImg = this; - bool result = true; - - /* search up the tree to find a progress updater */ - while (func == nil) { - pImg = pImg->fpParentImg; - if (pImg == nil) - return result; // none defined, bail out - func = pImg->fpScanProgressCallback; - } - - time_t now = time(nil); - - if (newStr == NULL) { - fScanCount++; - //if ((fScanCount % 100) == 0) - if (fScanLastMsgWhen != now) { - result = (*func)(fScanProgressCookie, - fScanMsg, fScanCount); - fScanLastMsgWhen = now; - } - } else { - fScanCount = 0; - strncpy(fScanMsg, newStr, sizeof(fScanMsg)); - fScanMsg[sizeof(fScanMsg)-1] = '\0'; - result = (*func)(fScanProgressCookie, fScanMsg, - fScanCount); - fScanLastMsgWhen = now; - } - - return result; -} - - -/* - * ========================================================================== - * Block/track/sector I/O - * ========================================================================== - */ - -/* - * Handle sector order conversions. - */ -DIError -DiskImg::CalcSectorAndOffset(long track, int sector, SectorOrder imageOrder, - SectorOrder fsOrder, di_off_t* pOffset, int* pNewSector) -{ - if (!fHasSectors) - return kDIErrUnsupportedAccess; - - /* - * Sector order conversions. No table is needed for Copy ][+ format, - * which is equivalent to "physical". - */ - static const int raw2dos[16] = { - 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 - }; - static const int dos2raw[16] = { - 0, 13, 11, 9, 7, 5, 3, 1, 14, 12, 10, 8, 6, 4, 2, 15 - }; - static const int raw2prodos[16] = { - 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15 - }; - static const int prodos2raw[16] = { - 0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15 - }; - static const int raw2cpm[16] = { - 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5 - }; - static const int cpm2raw[16] = { - 0, 3, 6, 9, 12, 15, 2, 5, 8, 11, 14, 1, 4, 7, 10, 13 - }; - - if (track < 0 || track >= fNumTracks) { - WMSG1(" DI read invalid track %ld\n", track); - return kDIErrInvalidTrack; - } - if (sector < 0 || sector >= fNumSectPerTrack) { - WMSG1(" DI read invalid sector %d\n", sector); - return kDIErrInvalidSector; - } - - di_off_t offset; - int newSector = -1; - - /* - * 16-sector disks write sectors in ascending order and then remap - * them with a translation table. - */ - if (fNumSectPerTrack == 16 || fNumSectPerTrack == 32) { - if (fSectorPairing) { - assert(fSectorPairOffset == 0 || fSectorPairOffset == 1); - // this pushes "track" beyond fNumTracks - track *= 2; - if (sector >= 16) { - track++; - sector -= 16; - } - offset = track * fNumSectPerTrack * kSectorSize; - - sector = sector * 2 + fSectorPairOffset; - if (sector >= 16) { - offset += 16*kSectorSize; - sector -= 16; - } - } else { - offset = track * fNumSectPerTrack * kSectorSize; - if (sector >= 16) { - offset += 16*kSectorSize; - sector -= 16; - } - } - assert(sector >= 0 && sector < 16); - - /* convert request to "raw" sector number */ - switch (fsOrder) { - case kSectorOrderProDOS: - newSector = prodos2raw[sector]; - break; - case kSectorOrderDOS: - newSector = dos2raw[sector]; - break; - case kSectorOrderCPM: - newSector = cpm2raw[sector]; - break; - case kSectorOrderPhysical: // used for Copy ][+ - newSector = sector; - break; - case kSectorOrderUnknown: - // should never happen; fall through to "default" - default: - assert(false); - newSector = sector; - break; - } - - /* convert "raw" request to the image's ordering */ - switch (imageOrder) { - case kSectorOrderProDOS: - newSector = raw2prodos[newSector]; - break; - case kSectorOrderDOS: - newSector = raw2dos[newSector]; - break; - case kSectorOrderCPM: - newSector = raw2cpm[newSector]; - break; - case kSectorOrderPhysical: - //newSector = newSector; - break; - case kSectorOrderUnknown: - // should never happen; fall through to "default" - default: - assert(false); - //newSector = newSector; - break; - } - - if (imageOrder == fsOrder) { - assert(sector == newSector); - } - - offset += newSector * kSectorSize; - } else if (fNumSectPerTrack == 13) { - /* sector skew has no meaning, so assume no translation */ - offset = track * fNumSectPerTrack * kSectorSize; - newSector = sector; - offset += newSector * kSectorSize; - if (imageOrder != fsOrder) { - /* translation expected */ - WMSG2("NOTE: CalcSectorAndOffset for nspt=13 with img=%d fs=%d\n", - imageOrder, fsOrder); - } - } else { - assert(false); // should not be here - - /* try to do something reasonable */ - assert(imageOrder == fsOrder); - offset = (di_off_t)track * fNumSectPerTrack * kSectorSize; - offset += sector * kSectorSize; - } - - *pOffset = offset; - *pNewSector = newSector; - return kDIErrNone; -} - -/* - * Determine whether an image uses a linear mapping. This allows us to - * optimize block reads & writes, very useful when dealing with logical - * volumes under Windows (which also use 512-byte blocks). - * - * The "imageOrder" argument usually comes from fOrder, and "fsOrder" - * comes from "fFileSysOrder". - */ -inline bool -DiskImg::IsLinearBlocks(SectorOrder imageOrder, SectorOrder fsOrder) -{ - /* - * Any time fOrder==fFileSysOrder, we know that we have a linear - * mapping. This holds true for reading ProDOS blocks from a ".po" - * file or reading DOS sectors from a ".do" file. - */ - return (IsSectorFormat(fPhysical) && fHasBlocks && - imageOrder == fsOrder); -} - -/* - * Read the specified track and sector, adjusting for sector ordering as - * appropriate. - * - * Copies 256 bytes into "*buf". - * - * Returns 0 on success, nonzero on failure. - */ -DIError -DiskImg::ReadTrackSectorSwapped(long track, int sector, void* buf, - SectorOrder imageOrder, SectorOrder fsOrder) -{ - DIError dierr; - di_off_t offset; - int newSector = -1; - - if (buf == nil) - return kDIErrInvalidArg; - -#if 0 // Pre-d13 - if (fNumSectPerTrack == 13) { - /* no sector skewing possible for 13-sector disks */ - assert(fHasNibbles); - - return ReadNibbleSector(track, sector, buf, fpNibbleDescr); - } -#endif - - dierr = CalcSectorAndOffset(track, sector, imageOrder, fsOrder, - &offset, &newSector); - if (dierr != kDIErrNone) - return dierr; - - if (IsSectorFormat(fPhysical)) { - assert(offset+kSectorSize <= fLength); - - //WMSG2(" DI t=%d s=%d\n", track, - // (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize); - - dierr = CopyBytesOut(buf, offset, kSectorSize); - } else if (IsNibbleFormat(fPhysical)) { - if (imageOrder != kSectorOrderPhysical) { - WMSG2(" NOTE: nibble imageOrder is %d (expected %d)\n", - imageOrder, kSectorOrderPhysical); - } - dierr = ReadNibbleSector(track, newSector, buf, fpNibbleDescr); - } else { - assert(false); - dierr = kDIErrInternal; - } - - return dierr; -} - -/* - * Write the specified track and sector, adjusting for sector ordering as - * appropriate. - * - * Copies 256 bytes out of "buf". - * - * Returns 0 on success, nonzero on failure. - */ -DIError -DiskImg::WriteTrackSector(long track, int sector, const void* buf) -{ - DIError dierr; - di_off_t offset; - int newSector = -1; - - if (buf == nil) - return kDIErrInvalidArg; - if (fReadOnly) - return kDIErrAccessDenied; - -#if 0 // Pre-d13 - if (fNumSectPerTrack == 13) { - /* no sector skewing possible for 13-sector disks */ - assert(fHasNibbles); - - return WriteNibbleSector(track, sector, buf, fpNibbleDescr); - } -#endif - - dierr = CalcSectorAndOffset(track, sector, fOrder, fFileSysOrder, - &offset, &newSector); - if (dierr != kDIErrNone) - return dierr; - - if (IsSectorFormat(fPhysical)) { - assert(offset+kSectorSize <= fLength); - - //WMSG2(" DI t=%d s=%d\n", track, - // (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize); - - dierr = CopyBytesIn(buf, offset, kSectorSize); - } else if (IsNibbleFormat(fPhysical)) { - if (fOrder != kSectorOrderPhysical) { - WMSG2(" NOTE: nibble fOrder is %d (expected %d)\n", - fOrder, kSectorOrderPhysical); - } - dierr = WriteNibbleSector(track, newSector, buf, fpNibbleDescr); - } else { - assert(false); - dierr = kDIErrInternal; - } - - return dierr; -} - -/* - * Read a 512-byte block. - * - * Copies 512 bytes into "*buf". - */ -DIError -DiskImg::ReadBlockSwapped(long block, void* buf, SectorOrder imageOrder, - SectorOrder fsOrder) -{ - if (!fHasBlocks) - return kDIErrUnsupportedAccess; - if (block < 0 || block >= fNumBlocks) - return kDIErrInvalidBlock; - if (buf == nil) - return kDIErrInvalidArg; - - DIError dierr; - long track, blkInTrk; - - /* if we have a bad block map, check it */ - if (CheckForBadBlocks(block, 1)) { - dierr = kDIErrReadFailed; - goto bail; - } - - if (fHasSectors && !IsLinearBlocks(imageOrder, fsOrder)) { - /* run it through the t/s call so we handle DOS ordering */ - track = block / (fNumSectPerTrack/2); - blkInTrk = block - (track * (fNumSectPerTrack/2)); - dierr = ReadTrackSectorSwapped(track, blkInTrk*2, buf, - imageOrder, fsOrder); - if (dierr != kDIErrNone) - return dierr; - dierr = ReadTrackSectorSwapped(track, blkInTrk*2+1, - (char*)buf+kSectorSize, imageOrder, fsOrder); - } else if (fHasBlocks) { - /* no sectors, so no swapping; must be linear blocks */ - if (imageOrder != fsOrder) { - WMSG2(" DI NOTE: ReadBlockSwapped on non-sector (%d/%d)\n", - imageOrder, fsOrder); - } - dierr = CopyBytesOut(buf, (di_off_t) block * kBlockSize, kBlockSize); - } else { - assert(false); - dierr = kDIErrInternal; - } - -bail: - return dierr; -} - -/* - * Read multiple blocks. - * - * IMPORTANT: this returns immediately when a read fails. The buffer will - * probably not contain data from all readable sectors. The application is - * expected to retry the blocks individually. - */ -DIError -DiskImg::ReadBlocks(long startBlock, int numBlocks, void* buf) -{ - DIError dierr = kDIErrNone; - - assert(fHasBlocks); - assert(startBlock >= 0); - assert(numBlocks > 0); - assert(buf != nil); - - if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) { - assert(false); - return kDIErrInvalidArg; - } - - /* if we have a bad block map, check it */ - if (CheckForBadBlocks(startBlock, numBlocks)) { - dierr = kDIErrReadFailed; - goto bail; - } - - if (!IsLinearBlocks(fOrder, fFileSysOrder)) { - /* - * This isn't a collection of linear blocks, so we need to read it one - * block at a time with sector swapping. This almost certainly means - * that we're not reading from physical media, so performance shouldn't - * be an issue. - */ - if (startBlock == 0) { - WMSG0(" ReadBlocks: nonlinear, not trying\n"); - } - while (numBlocks--) { - dierr = ReadBlock(startBlock, buf); - if (dierr != kDIErrNone) - goto bail; - startBlock++; - buf = (unsigned char*)buf + kBlockSize; - } - } else { - if (startBlock == 0) { - WMSG0(" ReadBlocks: doing big linear reads\n"); - } - dierr = CopyBytesOut(buf, - (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize); - } - -bail: - return dierr; -} - -/* - * Check to see if any blocks in a range of blocks show up in the bad - * block map. This is primarily useful for 3.5" disk images converted - * from nibble images, because we convert them directly to "cooked" - * 512-byte blocks. - * - * Returns "true" if we found bad blocks, "false" if not. - */ -bool -DiskImg::CheckForBadBlocks(long startBlock, int numBlocks) -{ - int i; - - if (fpBadBlockMap == nil) - return false; - - for (i = startBlock; i < startBlock+numBlocks; i++) { - if (fpBadBlockMap->IsSet(i)) - return true; - } - return false; -} - -/* - * Write a block of data to a DiskImg. - * - * Returns immediately when a block write fails. Does not try to write all - * blocks before returning failure. - */ -DIError -DiskImg::WriteBlock(long block, const void* buf) -{ - if (!fHasBlocks) - return kDIErrUnsupportedAccess; - if (block < 0 || block >= fNumBlocks) - return kDIErrInvalidBlock; - if (buf == nil) - return kDIErrInvalidArg; - if (fReadOnly) - return kDIErrAccessDenied; - - DIError dierr; - long track, blkInTrk; - - if (fHasSectors && !IsLinearBlocks(fOrder, fFileSysOrder)) { - /* run it through the t/s call so we handle DOS ordering */ - track = block / (fNumSectPerTrack/2); - blkInTrk = block - (track * (fNumSectPerTrack/2)); - dierr = WriteTrackSector(track, blkInTrk*2, buf); - if (dierr != kDIErrNone) - return dierr; - dierr = WriteTrackSector(track, blkInTrk*2+1, (char*)buf+kSectorSize); - } else if (fHasBlocks) { - /* no sectors, so no swapping; must be linear blocks */ - if (fOrder != fFileSysOrder) { - WMSG2(" DI NOTE: WriteBlock on non-sector (%d/%d)\n", - fOrder, fFileSysOrder); - } - dierr = CopyBytesIn(buf, (di_off_t)block * kBlockSize, kBlockSize); - } else { - assert(false); - dierr = kDIErrInternal; - } - return dierr; -} - -/* - * Write multiple blocks. - */ -DIError -DiskImg::WriteBlocks(long startBlock, int numBlocks, const void* buf) -{ - DIError dierr = kDIErrNone; - - assert(fHasBlocks); - assert(startBlock >= 0); - assert(numBlocks > 0); - assert(buf != nil); - - if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) { - assert(false); - return kDIErrInvalidArg; - } - - if (!IsLinearBlocks(fOrder, fFileSysOrder)) { - /* - * This isn't a collection of linear blocks, so we need to write it - * one block at a time with sector swapping. This almost certainly - * means that we're not reading from physical media, so performance - * shouldn't be an issue. - */ - if (startBlock == 0) { - WMSG0(" WriteBlocks: nonlinear, not trying\n"); - } - while (numBlocks--) { - dierr = WriteBlock(startBlock, buf); - if (dierr != kDIErrNone) - goto bail; - startBlock++; - buf = (unsigned char*)buf + kBlockSize; - } - } else { - if (startBlock == 0) { - WMSG0(" WriteBlocks: doing big linear writes\n"); - } - dierr = CopyBytesIn(buf, - (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize); - } - -bail: - return dierr; -} - - -/* - * Copy a chunk of bytes out of the disk image. - * - * (This is the lowest-level read routine in this class.) - */ -DIError -DiskImg::CopyBytesOut(void* buf, di_off_t offset, int size) const -{ - DIError dierr; - - dierr = fpDataGFD->Seek(offset, kSeekSet); - if (dierr != kDIErrNone) { - WMSG2(" DI seek off=%ld failed (err=%d)\n", (long) offset, dierr); - return dierr; - } - - dierr = fpDataGFD->Read(buf, size); - if (dierr != kDIErrNone) { - WMSG3(" DI read off=%ld size=%d failed (err=%d)\n", - (long) offset, size, dierr); - return dierr; - } - - return kDIErrNone; -} - -/* - * Copy a chunk of bytes into the disk image. - * - * Sets the "dirty" flag. - * - * (This is the lowest-level write routine in DiskImg.) - */ -DIError -DiskImg::CopyBytesIn(const void* buf, di_off_t offset, int size) -{ - DIError dierr; - - if (fReadOnly) { - DebugBreak(); - return kDIErrAccessDenied; - } - assert(fpDataGFD != nil); // somebody closed the image? - - dierr = fpDataGFD->Seek(offset, kSeekSet); - if (dierr != kDIErrNone) { - WMSG2(" DI seek off=%ld failed (err=%d)\n", (long) offset, dierr); - return dierr; - } - - dierr = fpDataGFD->Write(buf, size); - if (dierr != kDIErrNone) { - WMSG3(" DI write off=%ld size=%d failed (err=%d)\n", - (long) offset, size, dierr); - return dierr; - } - - /* set the dirty flag here and everywhere above */ - DiskImg* pImg = this; - while (pImg != nil) { - pImg->fDirty = true; - pImg = pImg->fpParentImg; - } - - return kDIErrNone; -} - - -/* - * =========================================================================== - * Image creation - * =========================================================================== - */ - -/* - * Create a disk image with the specified parameters. - * - * "storageName" and "pNibbleDescr" may be nil. - */ -DIError -DiskImg::CreateImage(const char* pathName, const char* storageName, - OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical, - const NibbleDescr* pNibbleDescr, SectorOrder order, - FSFormat format, long numBlocks, bool skipFormat) -{ - assert(fpDataGFD == nil); // should not be open already! - - if (numBlocks <= 0) { - WMSG1("ERROR: bad numBlocks %ld\n", numBlocks); - assert(false); - return kDIErrInvalidCreateReq; - } - - fOuterFormat = outerFormat; - fFileFormat = fileFormat; - fPhysical = physical; - SetCustomNibbleDescr(pNibbleDescr); - fOrder = order; - fFormat = format; - - fNumBlocks = numBlocks; - fHasBlocks = true; - - return CreateImageCommon(pathName, storageName, skipFormat); -} -DIError -DiskImg::CreateImage(const char* pathName, const char* storageName, - OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical, - const NibbleDescr* pNibbleDescr, SectorOrder order, - FSFormat format, long numTracks, long numSectPerTrack, bool skipFormat) -{ - assert(fpDataGFD == nil); // should not be open already! - - if (numTracks <= 0 || numSectPerTrack == 0) { - WMSG2("ERROR: bad tracks/sectors %ld/%ld\n", numTracks, numSectPerTrack); - assert(false); - return kDIErrInvalidCreateReq; - } - - fOuterFormat = outerFormat; - fFileFormat = fileFormat; - fPhysical = physical; - SetCustomNibbleDescr(pNibbleDescr); - fOrder = order; - fFormat = format; - - fNumTracks = numTracks; - fNumSectPerTrack = numSectPerTrack; - fHasSectors = true; - if (numSectPerTrack < 0) { - /* nibble image with non-standard formatting */ - if (!IsNibbleFormat(fPhysical)) { - WMSG0("Whoa: expected nibble format here\n"); - assert(false); - return kDIErrInvalidCreateReq; - } - WMSG0("Sector image w/o sectors, switching to nibble mode\n"); - fHasNibbles = true; - fHasSectors = false; - fpNibbleDescr = nil; - } - - return CreateImageCommon(pathName, storageName, skipFormat); -} - -/* - * Do the actual disk image creation. - */ -DIError -DiskImg::CreateImageCommon(const char* pathName, const char* storageName, - bool skipFormat) -{ - DIError dierr; - - /* - * Step 1: figure out fHasBlocks/fHasSectors/fHasNibbles and any - * other misc fields. - * - * If the disk is a nibble image expected to have a particular - * volume number, it should have already been set by the application. - */ - if (fHasBlocks) { - if ((fNumBlocks % 8) == 0) { - fHasSectors = true; - fNumSectPerTrack = 16; - fNumTracks = fNumBlocks / 8; - } else { - WMSG0("NOTE: sector access to new image not possible\n"); - } - } else if (fHasSectors) { - if ((fNumSectPerTrack & 0x01) == 0) { - fHasBlocks = true; - fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2; - } else { - WMSG0("NOTE: block access to new image not possible\n"); - } - } - if (fHasSectors && fPhysical != kPhysicalFormatSectors) - fHasNibbles = true; - assert(fHasBlocks || fHasSectors || fHasNibbles); - - fFileSysOrder = CalcFSSectorOrder(); - fReadOnly = false; - fDirty = true; - - /* - * Step 2: check for invalid arguments and bad combinations. - */ - dierr = ValidateCreateFormat(); - if (dierr != kDIErrNone) { - WMSG0("ERROR: CIC arg validation failed, bailing\n"); - goto bail; - } - - /* - * Step 3: create the destination file. Put this into fpWrapperGFD - * or fpOuterGFD. - * - * The file must not already exist. - * - * THOUGHT: should allow creation of an in-memory disk image. This won't - * work for NuFX, but will work for pretty much everything else. - */ - WMSG1(" CIC: creating '%s'\n", pathName); - int fd; - fd = open(pathName, O_CREAT | O_EXCL, 0644); - if (fd < 0) { - dierr = (DIError) errno; - WMSG2("ERROR: unable to create file '%s' (errno=%d)\n", - pathName, dierr); - goto bail; - } - close(fd); - - GFDFile* pGFDFile; - pGFDFile = new GFDFile; - - dierr = pGFDFile->Open(pathName, false); - if (dierr != kDIErrNone) { - delete pGFDFile; - goto bail; - } - - if (fOuterFormat == kOuterFormatNone) - fpWrapperGFD = pGFDFile; - else - fpOuterGFD = pGFDFile; - pGFDFile = nil; - - /* - * Step 4: if we have an outer GFD and therefore don't currently have - * an fpWrapperGFD, create an expandable memory buffer to use. - * - * We want to take a guess at how big the image will be, so compute - * fLength now. - * - * Create an OuterWrapper as needed. - */ - if (IsSectorFormat(fPhysical)) { - if (fHasBlocks) - fLength = (di_off_t) GetNumBlocks() * kBlockSize; - else - fLength = (di_off_t) GetNumTracks() * GetNumSectPerTrack() * kSectorSize; - } else { - assert(IsNibbleFormat(fPhysical)); - fLength = GetNumTracks() * GetNibbleTrackAllocLength(); - } - assert(fLength > 0); - - if (fpWrapperGFD == nil) { - /* shift GFDs and create a new memory GFD, pre-sized */ - GFDBuffer* pGFDBuffer = new GFDBuffer; - - /* use fLength as a starting point for buffer size; this may expand */ - dierr = pGFDBuffer->Open(nil, fLength, true, true, false); - if (dierr != kDIErrNone) { - delete pGFDBuffer; - goto bail; - } - - fpWrapperGFD = pGFDBuffer; - pGFDBuffer = nil; - } - - /* create an fpOuterWrapper struct */ - switch (fOuterFormat) { - case kOuterFormatNone: - break; - case kOuterFormatGzip: - fpOuterWrapper = new OuterGzip; - if (fpOuterWrapper == nil) { - dierr = kDIErrMalloc; - goto bail; - } - break; - case kOuterFormatZip: - fpOuterWrapper = new OuterZip; - if (fpOuterWrapper == nil) { - dierr = kDIErrMalloc; - goto bail; - } - break; - default: - assert(false); - dierr = kDIErrInternal; - goto bail; - } - - /* - * Step 5: tell the ImageWrapper to write itself into the GFD, passing - * in the blank memory buffer. - * - * - Unadorned formats copy from memory buffer to fpWrapperGFD on disk. - * (With gz, fpWrapperGFD is actually a memory buffer.) fpDataGFD - * becomes an offset into the file. - * - 2MG writes header into GFD and follows it with all data; DC42 - * and Sim2e do similar things. - * - NuFX reopens pathName as SHK file (fpWrapperGFD must point to a - * file) and accesses the archive through an fpArchive. fpDataGFD - * is created as a memory buffer and the blank image is copied in. - * - DDD leaves fpWrapperGFD alone and copies the blank image into a - * new buffer for fpDataGFD. - * - * Sets fWrappedLength when possible, determined from fPhysical and - * either fNumBlocks or fNumTracks. Creates fpDataGFD, often as a - * GFDGFD offset into fpWrapperGFD. - */ - switch (fFileFormat) { - case kFileFormat2MG: - fpImageWrapper = new Wrapper2MG(); - break; - case kFileFormatDiskCopy42: - fpImageWrapper = new WrapperDiskCopy42(); - fpImageWrapper->SetStorageName(storageName); - break; - case kFileFormatSim2eHDV: - fpImageWrapper = new WrapperSim2eHDV(); - break; - case kFileFormatTrackStar: - fpImageWrapper = new WrapperTrackStar(); - fpImageWrapper->SetStorageName(storageName); - break; - case kFileFormatFDI: - fpImageWrapper = new WrapperFDI(); - break; - case kFileFormatNuFX: - fpImageWrapper = new WrapperNuFX(); - fpImageWrapper->SetStorageName(storageName); - ((WrapperNuFX*)fpImageWrapper)->SetCompressType( - (NuThreadFormat) fNuFXCompressType); - break; - case kFileFormatDDD: - fpImageWrapper = new WrapperDDD(); - break; - case kFileFormatUnadorned: - if (IsSectorFormat(fPhysical)) - fpImageWrapper = new WrapperUnadornedSector(); - else if (IsNibbleFormat(fPhysical)) - fpImageWrapper = new WrapperUnadornedNibble(); - else { - assert(false); - } - break; - default: - assert(fpImageWrapper == nil); - break; - } - - if (fpImageWrapper == nil) { - WMSG0(" DI couldn't figure out the file format\n"); - dierr = kDIErrUnrecognizedFileFmt; - goto bail; - } - - /* create the wrapper, write the header, and create fpDataGFD */ - assert(fpDataGFD == nil); - dierr = fpImageWrapper->Create(fLength, fPhysical, fOrder, - fDOSVolumeNum, fpWrapperGFD, &fWrappedLength, &fpDataGFD); - if (dierr != kDIErrNone) { - WMSG1("ImageWrapper Create failed, err=%d\n", dierr); - goto bail; - } - assert(fpDataGFD != nil); - - /* - * Step 6: "format" fpDataGFD. - * - * Note we don't specify an ordering to the "create blank" functions. - * Either it's sectors, in which case it's all zeroes, or it's nibbles, - * in which case it's always in physical order. - * - * If we're formatting for nibbles, and the application hasn't specified - * a disk volume number, use the default (254). - */ - if (fPhysical == kPhysicalFormatSectors) - dierr = FormatSectors(fpDataGFD, skipFormat); // zero out the image - else { - assert(!skipFormat); // don't skip low-level nibble formatting! - if (fDOSVolumeNum == kVolumeNumNotSet) { - fDOSVolumeNum = kDefaultNibbleVolumeNum; - WMSG0(" Using default nibble volume num\n"); - } - - dierr = FormatNibbles(fpDataGFD); // write basic nibble stuff - } - - - /* - * We're done! - * - * Quick sanity check... - */ - if (fOuterFormat != kOuterFormatNone) { - assert(fpOuterGFD != nil); - assert(fpWrapperGFD != nil); - assert(fpDataGFD != nil); - } - -bail: - return dierr; -} - -/* - * Check that the requested format is one we can create. - * - * We don't allow .SDK.GZ or 6384-byte nibble 2MG. 2MG sector images - * must be in DOS or ProDOS order. - * - * Only "generic" FS formats may be used. The application may choose - * to call AnalyzeImage later on to set the actual FS once data has - * been written. - */ -DIError -DiskImg::ValidateCreateFormat(void) const -{ - /* - * Check for invalid arguments. - */ - if (fHasBlocks && fNumBlocks >= 4194304) { // 2GB or larger? - if (fFileFormat != kFileFormatUnadorned) { - WMSG0("CreateImage: images >= 2GB can only be unadorned\n"); - return kDIErrInvalidCreateReq; - } - } - if (fOuterFormat == kOuterFormatUnknown || - fFileFormat == kFileFormatUnknown || - fPhysical == kPhysicalFormatUnknown || - fOrder == kSectorOrderUnknown || - fFormat == kFormatUnknown) - { - WMSG0("CreateImage: ambiguous format\n"); - return kDIErrInvalidCreateReq; - } - if (fOuterFormat != kOuterFormatNone && - fOuterFormat != kOuterFormatGzip && - fOuterFormat != kOuterFormatZip) - { - WMSG1("CreateImage: unsupported outer format %d\n", fOuterFormat); - return kDIErrInvalidCreateReq; - } - if (fFileFormat != kFileFormatUnadorned && - fFileFormat != kFileFormat2MG && - fFileFormat != kFileFormatDiskCopy42 && - fFileFormat != kFileFormatSim2eHDV && - fFileFormat != kFileFormatTrackStar && - fFileFormat != kFileFormatFDI && - fFileFormat != kFileFormatNuFX && - fFileFormat != kFileFormatDDD) - { - WMSG1("CreateImage: unsupported file format %d\n", fFileFormat); - return kDIErrInvalidCreateReq; - } - if (fFormat != kFormatGenericPhysicalOrd && - fFormat != kFormatGenericProDOSOrd && - fFormat != kFormatGenericDOSOrd && - fFormat != kFormatGenericCPMOrd) - { - WMSG0("CreateImage: may only use 'generic' formats\n"); - return kDIErrInvalidCreateReq; - } - - /* - * Check for invalid combinations. - */ - if (fPhysical != kPhysicalFormatSectors) { - if (fOrder != kSectorOrderPhysical) { - WMSG0("CreateImage: nibble images are always 'physical' order\n"); - return kDIErrInvalidCreateReq; - } - - if (GetHasSectors() == false && GetHasNibbles() == false) { - WMSG2("CreateImage: must set hasSectors(%d) or hasNibbles(%d)\n", - GetHasSectors(), GetHasNibbles()); - return kDIErrInvalidCreateReq; - } - - if (fpNibbleDescr == nil && GetNumSectPerTrack() > 0) { - WMSG0("CreateImage: must provide NibbleDescr for non-sector\n"); - return kDIErrInvalidCreateReq; - } - - if (fpNibbleDescr != nil && - fpNibbleDescr->numSectors != GetNumSectPerTrack()) - { - WMSG2("CreateImage: ?? nd->numSectors=%d, GetNumSectPerTrack=%d\n", - fpNibbleDescr->numSectors, GetNumSectPerTrack()); - return kDIErrInvalidCreateReq; - } - - if (fpNibbleDescr != nil && ( - (fpNibbleDescr->numSectors == 13 && - fpNibbleDescr->encoding != kNibbleEnc53) || - (fpNibbleDescr->numSectors == 16 && - fpNibbleDescr->encoding != kNibbleEnc62)) - ) - { - WMSG0("CreateImage: sector count/encoding mismatch\n"); - return kDIErrInvalidCreateReq; - } - - if (GetNumTracks() != kTrackCount525 && - !(GetNumTracks() == 40 && fFileFormat == kFileFormatTrackStar)) - { - WMSG1("CreateImage: unexpected track count %ld\n", GetNumTracks()); - return kDIErrInvalidCreateReq; - } - } - if (fFileFormat == kFileFormat2MG) { - if (fPhysical != kPhysicalFormatSectors && - fPhysical != kPhysicalFormatNib525_6656) - { - WMSG1("CreateImage: 2MG can't handle physical %d\n", fPhysical); - return kDIErrInvalidCreateReq; - } - - if (fPhysical == kPhysicalFormatSectors && - (fOrder != kSectorOrderProDOS && - fOrder != kSectorOrderDOS)) - { - WMSG0("CreateImage: 2MG requires DOS or ProDOS ordering\n"); - return kDIErrInvalidCreateReq; - } - } - if (fFileFormat == kFileFormatNuFX) { - if (fOuterFormat != kOuterFormatNone) { - WMSG0("CreateImage: can't mix NuFX and outer wrapper\n"); - return kDIErrInvalidCreateReq; - } - if (fPhysical != kPhysicalFormatSectors) { - WMSG0("CreateImage: NuFX physical must be sectors\n"); - return kDIErrInvalidCreateReq; - } - if (fOrder != kSectorOrderProDOS) { - WMSG0("CreateImage: NuFX is always ProDOS-order\n"); - return kDIErrInvalidCreateReq; - } - } - if (fFileFormat == kFileFormatDiskCopy42) { - if (fPhysical != kPhysicalFormatSectors) { - WMSG0("CreateImage: DC42 physical must be sectors\n"); - return kDIErrInvalidCreateReq; - } - if ((GetHasBlocks() && GetNumBlocks() != 1600) || - GetHasSectors() && - (GetNumTracks() != 200 || GetNumSectPerTrack() != 16)) - { - WMSG0("CreateImage: DC42 only for 800K disks\n"); - return kDIErrInvalidCreateReq; - } - if (fOrder != kSectorOrderProDOS && - fOrder != kSectorOrderDOS) // used for UNIDOS disks?? - { - WMSG0("CreateImage: DC42 is always ProDOS or DOS\n"); - return kDIErrInvalidCreateReq; - } - } - if (fFileFormat == kFileFormatSim2eHDV) { - if (fPhysical != kPhysicalFormatSectors) { - WMSG0("CreateImage: Sim2eHDV physical must be sectors\n"); - return kDIErrInvalidCreateReq; - } - if (fOrder != kSectorOrderProDOS) { - WMSG0("CreateImage: Sim2eHDV is always ProDOS-order\n"); - return kDIErrInvalidCreateReq; - } - } - if (fFileFormat == kFileFormatTrackStar) { - if (fPhysical != kPhysicalFormatNib525_Var) { - WMSG0("CreateImage: TrackStar physical must be var-nibbles\n"); - return kDIErrInvalidCreateReq; - } - } - if (fFileFormat == kFileFormatFDI) { - if (fPhysical != kPhysicalFormatNib525_Var) { - WMSG0("CreateImage: FDI physical must be var-nibbles\n"); - return kDIErrInvalidCreateReq; - } - } - if (fFileFormat == kFileFormatDDD) { - if (fPhysical != kPhysicalFormatSectors) { - WMSG0("CreateImage: DDD physical must be sectors\n"); - return kDIErrInvalidCreateReq; - } - if (fOrder != kSectorOrderDOS) { - WMSG0("CreateImage: DDD is always DOS-order\n"); - return kDIErrInvalidCreateReq; - } - if (!GetHasSectors() || GetNumTracks() != 35 || - GetNumSectPerTrack() != 16) - { - WMSG0("CreateImage: DDD is only for 16-sector 35-track disks\n"); - return kDIErrInvalidCreateReq; - } - } - - return kDIErrNone; -} - -/* - * Create a blank image for physical=="sectors". - * - * fLength must be a multiple of 256. - * - * If "quickFormat" is set, only the very last sector is written (to set - * the EOF on the file). - */ -DIError -DiskImg::FormatSectors(GenericFD* pGFD, bool quickFormat) const -{ - DIError dierr = kDIErrNone; - char sctBuf[kSectorSize]; - di_off_t length; - - assert(fLength > 0 && (fLength & 0xff) == 0); - - //if (!(fLength & 0x01)) - // return FormatBlocks(pGFD); - - memset(sctBuf, 0, sizeof(sctBuf)); - pGFD->Rewind(); - - if (quickFormat) { - dierr = pGFD->Seek(fLength - sizeof(sctBuf), kSeekSet); - if (dierr != kDIErrNone) { - WMSG2(" FormatSectors: GFD seek %ld failed (err=%d)\n", - (long) fLength - sizeof(sctBuf), dierr); - goto bail; - } - dierr = pGFD->Write(sctBuf, sizeof(sctBuf), nil); - if (dierr != kDIErrNone) { - WMSG1(" FormatSectors: GFD quick write failed (err=%d)\n", dierr); - goto bail; - } - } else { - for (length = fLength ; length > 0; length -= sizeof(sctBuf)) { - dierr = pGFD->Write(sctBuf, sizeof(sctBuf), nil); - if (dierr != kDIErrNone) { - WMSG1(" FormatSectors: GFD write failed (err=%d)\n", dierr); - goto bail; - } - } - assert(length == 0); - } - - -bail: - return dierr; -} - -#if 0 // didn't help -/* - * Create a blank image for physical=="sectors". This is called from - * FormatSectors when it looks like we're formatting entire blocks. - */ -DIError -DiskImg::FormatBlocks(GenericFD* pGFD) const -{ - DIError dierr; - char blkBuf[kBlockSize]; - long length; - time_t start, end; - - assert(fLength > 0 && (fLength & 0x1ff) == 0); - - start = time(nil); - - memset(blkBuf, 0, sizeof(blkBuf)); - pGFD->Rewind(); - - for (length = fLength ; length > 0; length -= sizeof(blkBuf)) { - dierr = pGFD->Write(blkBuf, sizeof(blkBuf), nil); - if (dierr != kDIErrNone) { - WMSG1(" FormatBlocks: GFD write failed (err=%d)\n", dierr); - return dierr; - } - } - assert(length == 0); - - end = time(nil); - WMSG1("FormatBlocks complete, time=%ld\n", end - start); - - return kDIErrNone; -} -#endif - - -/* - * =========================================================================== - * Utility functions - * =========================================================================== - */ - -/* - * Add a note to this disk image. - * - * This is how we communicate cautions and warnings to the user. Use - * linefeeds ('\n') to indicate line breaks. - * - * The maximum length of a single note is set by the size of "buf". - */ -void -DiskImg::AddNote(NoteType type, const char* fmt, ...) -{ - char buf[512]; - char* cp = buf; - int maxLen = sizeof(buf); - va_list args; - int len; - - /* - * Prepend a string that highlights the note. - */ - switch (type) { - case kNoteWarning: - strcpy(cp, "- WARNING: "); - break; - default: - strcpy(cp, "- "); - break; - } - len = strlen(cp); - cp += len; - maxLen -= len; - - /* - * Add the note. - */ - va_start(args, fmt); -#if defined(HAVE_VSNPRINTF) - (void) vsnprintf(cp, maxLen, fmt, args); -#elif defined(HAVE__VSNPRINTF) - (void) _vsnprintf(cp, maxLen, fmt, args); -#else -# error "hosed" -#endif - va_end(args); - - buf[sizeof(buf)-2] = '\0'; // leave room for additional '\n' - len = strlen(buf); - if (len > 0 && buf[len-1] != '\n') { - buf[len] = '\n'; - buf[len+1] = '\0'; - len++; - } - - WMSG1("+++ adding note '%s'\n", buf); - - if (fNotes == nil) { - fNotes = new char[len +1]; - if (fNotes == nil) { - WMSG1("Unable to create notes[%d]\n", len+1); - assert(false); - return; - } - strcpy(fNotes, buf); - } else { - int existingLen = strlen(fNotes); - char* newNotes = new char[existingLen + len +1]; - if (newNotes == nil) { - WMSG1("Unable to create newNotes[%d]\n", existingLen+len+1); - assert(false); - return; - } - strcpy(newNotes, fNotes); - strcpy(newNotes + existingLen, buf); - delete[] fNotes; - fNotes = newNotes; - } -} - -/* - * Return a string with the notes in it. - */ -const char* -DiskImg::GetNotes(void) const -{ - if (fNotes == nil) - return ""; - else - return fNotes; -} - - -/* - * Get length and offset of tracks in a nibble image. This is necessary - * because of formats with variable-length tracks (e.g. TrackStar). - */ -int -DiskImg::GetNibbleTrackLength(long track) const -{ - assert(fpImageWrapper != NULL); - return fpImageWrapper->GetNibbleTrackLength(fPhysical, track); -} -int -DiskImg::GetNibbleTrackOffset(long track) const -{ - assert(fpImageWrapper != NULL); - return fpImageWrapper->GetNibbleTrackOffset(fPhysical, track); -} - - -/* - * Return a new object with the appropriate DiskFS sub-class. - * - * If the image hasn't been analyzed, or was analyzed to no avail, "nil" - * is returned unless "allowUnknown" is set to "true". In that case, a - * DiskFSUnknown is returned. - * - * This doesn't inspire the DiskFS to do any processing, just creates the - * new object. - */ -DiskFS* -DiskImg::OpenAppropriateDiskFS(bool allowUnknown) -{ - DiskFS* pDiskFS = nil; - - /* - * Create an appropriate DiskFS object. - */ - switch (GetFSFormat()) { - case DiskImg::kFormatDOS33: - case DiskImg::kFormatDOS32: - pDiskFS = new DiskFSDOS33(); - break; - case DiskImg::kFormatProDOS: - pDiskFS = new DiskFSProDOS(); - break; - case DiskImg::kFormatPascal: - pDiskFS = new DiskFSPascal(); - break; - case DiskImg::kFormatMacHFS: - pDiskFS = new DiskFSHFS(); - break; - case DiskImg::kFormatUNIDOS: - pDiskFS = new DiskFSUNIDOS(); - break; - case DiskImg::kFormatOzDOS: - pDiskFS = new DiskFSOzDOS(); - break; - case DiskImg::kFormatCFFA4: - case DiskImg::kFormatCFFA8: - pDiskFS = new DiskFSCFFA(); - break; - case DiskImg::kFormatMacPart: - pDiskFS = new DiskFSMacPart(); - break; - case DiskImg::kFormatMicroDrive: - pDiskFS = new DiskFSMicroDrive(); - break; - case DiskImg::kFormatFocusDrive: - pDiskFS = new DiskFSFocusDrive(); - break; - case DiskImg::kFormatCPM: - pDiskFS = new DiskFSCPM(); - break; - case DiskImg::kFormatMSDOS: - pDiskFS = new DiskFSFAT(); - break; - case DiskImg::kFormatRDOS33: - case DiskImg::kFormatRDOS32: - case DiskImg::kFormatRDOS3: - pDiskFS = new DiskFSRDOS(); - break; - - default: - WMSG1("WARNING: unhandled DiskFS case %d\n", GetFSFormat()); - assert(false); - /* fall through */ - case DiskImg::kFormatGenericPhysicalOrd: - case DiskImg::kFormatGenericProDOSOrd: - case DiskImg::kFormatGenericDOSOrd: - case DiskImg::kFormatGenericCPMOrd: - case DiskImg::kFormatUnknown: - if (allowUnknown) { - pDiskFS = new DiskFSUnknown(); - break; - } - } - - return pDiskFS; -} - - -/* - * Fill an array with SectorOrder values. The ordering specified by "first" - * will come first. Unused entries will be set to "unknown" and should be - * ignored. - * - * "orderArray" must have kSectorOrderMax elements. - */ -/*static*/ void -DiskImg::GetSectorOrderArray(SectorOrder* orderArray, SectorOrder first) -{ - // init array - for (int i = 0; i < kSectorOrderMax; i++) - orderArray[i] = (SectorOrder) i; - - // pull the best-guess ordering to the front - assert(orderArray[0] == kSectorOrderUnknown); - - orderArray[0] = first; - orderArray[(int) first] = kSectorOrderUnknown; - - // don't bother checking CP/M sector order - orderArray[kSectorOrderCPM] = kSectorOrderUnknown; -} - - -/* - * Return a short string describing "format". - * - * These are semi-duplicated in ImageFormatDialog.cpp in CiderPress. - */ -/*static*/ const char* -DiskImg::ToStringCommon(int format, const ToStringLookup* pTable, - int tableSize) -{ - for (int i = 0; i < tableSize; i++) { - if (pTable[i].format == format) - return pTable[i].str; - } - - assert(false); - return "(unknown)"; -} - -/*static*/ const char* -DiskImg::ToString(OuterFormat format) -{ - static const ToStringLookup kOuterFormats[] = { - { DiskImg::kOuterFormatUnknown, "Unknown format" }, - { DiskImg::kOuterFormatNone, "(none)" }, - { DiskImg::kOuterFormatCompress, "UNIX compress" }, - { DiskImg::kOuterFormatGzip, "gzip" }, - { DiskImg::kOuterFormatBzip2, "bzip2" }, - { DiskImg::kOuterFormatZip, "Zip archive" }, - }; - - return ToStringCommon(format, kOuterFormats, NELEM(kOuterFormats)); -} -/*static*/ const char* -DiskImg::ToString(FileFormat format) -{ - static const ToStringLookup kFileFormats[] = { - { DiskImg::kFileFormatUnknown, "Unknown format" }, - { DiskImg::kFileFormatUnadorned, "Unadorned raw data" }, - { DiskImg::kFileFormat2MG, "2MG" }, - { DiskImg::kFileFormatNuFX, "NuFX (ShrinkIt)" }, - { DiskImg::kFileFormatDiskCopy42, "DiskCopy 4.2" }, - { DiskImg::kFileFormatDiskCopy60, "DiskCopy 6.0" }, - { DiskImg::kFileFormatDavex, "Davex volume image" }, - { DiskImg::kFileFormatSim2eHDV, "Sim //e HDV" }, - { DiskImg::kFileFormatTrackStar, "TrackStar image" }, - { DiskImg::kFileFormatFDI, "FDI image" }, - { DiskImg::kFileFormatDDD, "DDD" }, - { DiskImg::kFileFormatDDDDeluxe, "DDDDeluxe" }, - }; - - return ToStringCommon(format, kFileFormats, NELEM(kFileFormats)); -}; -/*static*/ const char* -DiskImg::ToString(PhysicalFormat format) -{ - static const ToStringLookup kPhysicalFormats[] = { - { DiskImg::kPhysicalFormatUnknown, "Unknown format" }, - { DiskImg::kPhysicalFormatSectors, "Sectors" }, - { DiskImg::kPhysicalFormatNib525_6656, "Raw nibbles (6656-byte)" }, - { DiskImg::kPhysicalFormatNib525_6384, "Raw nibbles (6384-byte)" }, - { DiskImg::kPhysicalFormatNib525_Var, "Raw nibbles (variable len)" }, - }; - - return ToStringCommon(format, kPhysicalFormats, NELEM(kPhysicalFormats)); -}; -/*static*/ const char* -DiskImg::ToString(SectorOrder format) -{ - static const ToStringLookup kSectorOrders[] = { - { DiskImg::kSectorOrderUnknown, "Unknown ordering" }, - { DiskImg::kSectorOrderProDOS, "ProDOS block ordering" }, - { DiskImg::kSectorOrderDOS, "DOS sector ordering" }, - { DiskImg::kSectorOrderCPM, "CP/M block ordering" }, - { DiskImg::kSectorOrderPhysical, "Physical sector ordering" }, - }; - - return ToStringCommon(format, kSectorOrders, NELEM(kSectorOrders)); -}; -/*static*/ const char* -DiskImg::ToString(FSFormat format) -{ - static const ToStringLookup kFSFormats[] = { - { DiskImg::kFormatUnknown, "Unknown" }, - { DiskImg::kFormatProDOS, "ProDOS" }, - { DiskImg::kFormatDOS33, "DOS 3.3" }, - { DiskImg::kFormatDOS32, "DOS 3.2" }, - { DiskImg::kFormatPascal, "Pascal" }, - { DiskImg::kFormatMacHFS, "HFS" }, - { DiskImg::kFormatMacMFS, "MFS" }, - { DiskImg::kFormatLisa, "Lisa" }, - { DiskImg::kFormatCPM, "CP/M" }, - { DiskImg::kFormatMSDOS, "MS-DOS FAT" }, - { DiskImg::kFormatISO9660, "ISO-9660" }, - { DiskImg::kFormatRDOS33, "RDOS 3.3 (16-sector)" }, - { DiskImg::kFormatRDOS32, "RDOS 3.2 (13-sector)" }, - { DiskImg::kFormatRDOS3, "RDOS 3 (cracked 13-sector)" }, - { DiskImg::kFormatGenericDOSOrd, "Generic DOS sectors" }, - { DiskImg::kFormatGenericProDOSOrd, "Generic ProDOS blocks" }, - { DiskImg::kFormatGenericPhysicalOrd, "Generic raw sectors" }, - { DiskImg::kFormatGenericCPMOrd, "Generic CP/M blocks" }, - { DiskImg::kFormatUNIDOS, "UNIDOS (400K DOS x2)" }, - { DiskImg::kFormatOzDOS, "OzDOS (400K DOS x2)" }, - { DiskImg::kFormatCFFA4, "CFFA (4 or 6 partitions)" }, - { DiskImg::kFormatCFFA8, "CFFA (8 partitions)" }, - { DiskImg::kFormatMacPart, "Macintosh partitioned disk" }, - { DiskImg::kFormatMicroDrive, "MicroDrive partitioned disk" }, - { DiskImg::kFormatFocusDrive, "FocusDrive partitioned disk" }, - }; - - return ToStringCommon(format, kFSFormats, NELEM(kFSFormats)); -}; - - -/* - * strerror() equivalent for DiskImg errors. - */ -const char* -DiskImgLib::DIStrError(DIError dierr) -{ - if (dierr > 0) { - const char* msg; - msg = strerror(dierr); - if (msg != nil) - return msg; - } - - /* - * BUG: this should be set up as per-thread storage in an MT environment. - * I would be more inclined to worry about this if I was expecting - * to hit "default:". So long as valid values are passed in, and the - * switch statement is kept up to date, we should never have cause - * to return this. - * - * An easier solution, should this present a problem for someone, would - * be to have the function return nil or "unknown error" when the - * error value isn't recognized. I'd recommend leaving it as-is for - * debug builds, though, as it's helpful to know *which* error is not - * recognized. - */ - static char defaultMsg[32]; - - switch (dierr) { - case kDIErrNone: - return "(no error)"; - - case kDIErrAccessDenied: - return "access denied"; - case kDIErrVWAccessForbidden: - return "for safety, write access to this volume is forbidden"; - case kDIErrSharingViolation: - return "file is already open and cannot be shared"; - case kDIErrNoExclusiveAccess: - return "couldn't get exclusive access"; - case kDIErrWriteProtected: - return "write protected"; - case kDIErrCDROMNotSupported: - return "access to CD-ROM drives is not supported"; - case kDIErrASPIFailure: - return "an ASPI request failed"; - case kDIErrSPTIFailure: - return "an SPTI request failed"; - case kDIErrSCSIFailure: - return "a SCSI request failed"; - case kDIErrDeviceNotReady: - return "device not ready"; - - case kDIErrFileNotFound: - return "file not found"; - case kDIErrForkNotFound: - return "fork not found"; - case kDIErrAlreadyOpen: - return "an image is already open"; - case kDIErrFileOpen: - return "file is open"; - case kDIErrNotReady: - return "object not ready"; - case kDIErrFileExists: - return "file already exists"; - case kDIErrDirectoryExists: - return "directory already exists"; - - case kDIErrEOF: - return "end of file reached"; - case kDIErrReadFailed: - return "read failed"; - case kDIErrWriteFailed: - return "write failed"; - case kDIErrDataUnderrun: - return "tried to read past end of file"; - case kDIErrDataOverrun: - return "tried to write past end of file"; - case kDIErrGenericIO: - return "I/O error"; - - case kDIErrOddLength: - return "image size is wrong"; - case kDIErrUnrecognizedFileFmt: - return "not a recognized disk image format"; - case kDIErrBadFileFormat: - return "image file contents aren't in expected format"; - case kDIErrUnsupportedFileFmt: - return "file format not supported"; - case kDIErrUnsupportedPhysicalFmt: - return "physical format not supported"; - case kDIErrUnsupportedFSFmt: - return "filesystem type not supported"; - case kDIErrBadOrdering: - return "bad sector ordering"; - case kDIErrFilesystemNotFound: - return "specified filesystem not found"; - case kDIErrUnsupportedAccess: - return "the method of access used isn't supported for this image"; - case kDIErrUnsupportedImageFeature: - return "image file uses features that CiderPress doesn't support"; - - case kDIErrInvalidTrack: - return "invalid track number"; - case kDIErrInvalidSector: - return "invalid sector number"; - case kDIErrInvalidBlock: - return "invalid block number"; - case kDIErrInvalidIndex: - return "invalid index number"; - - case kDIErrDirectoryLoop: - return "disk directory structure has an infinite loop"; - case kDIErrFileLoop: - return "file structure has an infinite loop"; - case kDIErrBadDiskImage: - return "the filesystem on this image appears damaged"; - case kDIErrBadFile: - return "file structure appears damaged"; - case kDIErrBadDirectory: - return "a directory appears damaged"; - case kDIErrBadPartition: - return "bad partition"; - - case kDIErrFileArchive: - return "this looks like a file archive, not a disk archive"; - case kDIErrUnsupportedCompression: - return "compression method not supported"; - case kDIErrBadChecksum: - return "checksum doesn't match, data may be corrupted"; - case kDIErrBadCompressedData: - return "the compressed data is corrupted"; - case kDIErrBadArchiveStruct: - return "archive may be damaged"; - - case kDIErrBadNibbleSectors: - return "couldn't read sectors from this image"; - case kDIErrSectorUnreadable: - return "sector not readable"; - case kDIErrInvalidDiskByte: - return "found invalid nibble image disk byte"; - case kDIErrBadRawData: - return "couldn't convert raw data to nibble data"; - - case kDIErrInvalidFileName: - return "invalid file name"; - case kDIErrDiskFull: - return "disk full"; - case kDIErrVolumeDirFull: - return "volume directory is full"; - case kDIErrInvalidCreateReq: - return "invalid disk image create request"; - case kDIErrTooBig: - return "size is larger than we can handle"; - - case kDIErrGeneric: - return "DiskImg generic error"; - case kDIErrInternal: - return "DiskImg internal error"; - case kDIErrMalloc: - return "memory allocation failure"; - case kDIErrInvalidArg: - return "invalid argument"; - case kDIErrNotSupported: - return "feature not supported"; - case kDIErrCancelled: - return "cancelled by user"; - - case kDIErrNufxLibInitFailed: - return "NufxLib initialization failed"; - - default: - sprintf(defaultMsg, "(error=%d)", dierr); - return defaultMsg; - } -} - -/* - * High ASCII conversion table, from Technical Note PT515, - * "Apple File Exchange Q&As". The table is available in a hopelessly - * blurry PDF or a pair of GIFs created with small fonts, but I think I - * have mostly captured it. - */ -/*static*/ const unsigned char DiskImg::kMacHighASCII[128+1] = - "AACENOUaaaaaaceeeeiiiinooooouuuu" // 0x80 - 0x9f - "tocL$oPBrct'.=AO%+<>YudsPpSaoOao" // 0xa0 - 0xbf - "?!-vf=d<>. AAOOo--\"\"''/oyY/o<> f" // 0xc0 - 0xdf - "|*,,%AEAEEIIIIOOaOUUUi^~-,**,\"? "; // 0xe0 - 0xff - - -/* - * Hack for Win32 systems. See Win32BlockIO.cpp for commentary. - */ -bool DiskImgLib::gAllowWritePhys0 = false; -/*static*/ void DiskImg::SetAllowWritePhys0(bool val) { - DiskImgLib::gAllowWritePhys0 = val; -} +/* + * CiderPress + * Copyright (C) 2009 by CiderPress authors. All Rights Reserved. + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of the DiskImg class. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" +#include "TwoImg.h" + + +/* + * =========================================================================== + * DiskImg + * =========================================================================== + */ + +/* + * Standard NibbleDescr profiles. + * + * These will be tried in the order in which they appear here. + * + * IMPORTANT: if you add or remove an entry, update the StdNibbleDescr enum + * in DiskImg.h. + * + * Formats that allow the data checksum to be ignored should NOT be written. + * It's possible that the DOS on the disk is ignoring the checksums, but + * it's more likely that they're using a non-standard seed, and the newly- + * written sectors will have the wrong checksum value. + * + * Non-standard headers are usually okay, because we don't rewrite the + * headers, just the sector contents. + */ +/*static*/ const DiskImg::NibbleDescr DiskImg::kStdNibbleDescrs[] = { + { + "DOS 3.3 Standard", + 16, + { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + true, // verify track + 2, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + 2, // epilog verify count + kNibbleEnc62, + kNibbleSpecialNone, + }, + { + "DOS 3.3 Patched", + 16, + { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + false, // verify checksum + false, // verify track + 0, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + 0, // epilog verify count + kNibbleEnc62, + kNibbleSpecialNone, + }, + { + "DOS 3.3 Ignore Checksum", + 16, + { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + false, // verify checksum + false, // verify track + 0, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + false, // verify checksum + 0, // epilog verify count + kNibbleEnc62, + kNibbleSpecialNone, + }, + { + "DOS 3.2 Standard", + 13, + { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 2, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc53, + kNibbleSpecialNone, + }, + { + "DOS 3.2 Patched", + 13, + { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, + 0x00, + false, + false, + 0, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 0, + kNibbleEnc53, + kNibbleSpecialNone, + }, + { + "Muse DOS 3.2", // standard DOS 3.2 with doubled sectors + 13, + { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 2, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc53, + kNibbleSpecialMuse, + }, + { + "RDOS 3.3", // SSI 16-sector RDOS, with altered headers + 16, + { 0xd4, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 0, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc62, + kNibbleSpecialSkipFirstAddrByte, + /* odd tracks use d4aa96, even tracks use d5aa96 */ + }, + { + "RDOS 3.2", // SSI 13-sector RDOS, with altered headers + 13, + { 0xd4, 0xaa, 0xb7 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 2, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc53, + kNibbleSpecialNone, + }, + { + "Custom", // reserve space for empty slot + 0, + }, +}; +/*static*/ const DiskImg::NibbleDescr* +DiskImg::GetStdNibbleDescr(StdNibbleDescr idx) +{ + if ((int)idx < 0 || (int)idx >= (int) NELEM(kStdNibbleDescrs)) + return nil; + return &kStdNibbleDescrs[(int)idx]; +} + + +/* + * Initialize the members during construction. + */ +DiskImg::DiskImg(void) +{ + assert(Global::GetAppInitCalled()); + + fOuterFormat = kOuterFormatUnknown; + fFileFormat = kFileFormatUnknown; + fPhysical = kPhysicalFormatUnknown; + fpNibbleDescr = nil; + fOrder = kSectorOrderUnknown; + fFormat = kFormatUnknown; + + fFileSysOrder = kSectorOrderUnknown; + fSectorPairing = false; + fSectorPairOffset = -1; + + fpOuterGFD = nil; + fpWrapperGFD = nil; + fpDataGFD = nil; + fpOuterWrapper = nil; + fpImageWrapper = nil; + fpParentImg = nil; + fDOSVolumeNum = kVolumeNumNotSet; + fOuterLength = -1; + fWrappedLength = -1; + fLength = -1; + fExpandable = false; + fReadOnly = true; + fDirty = false; + + fHasSectors = false; + fHasBlocks = false; + fHasNibbles = false; + + fNumTracks = -1; + fNumSectPerTrack = -1; + fNumBlocks = -1; + + fpScanProgressCallback = NULL; + + /* + * Create a working copy of the nibble descr table. We want to leave + * open the possibility of applications editing or discarding entries, + * so we work off of a copy. + * + * Ideally we'd allow these to be set per-track, so that certain odd + * formats could be handled transparently (e.g. Muse tweaked DOS 3.2) + * for formatting as well as reading. + */ + assert(kStdNibbleDescrs[kNibbleDescrCustom].numSectors == 0); + assert(kNibbleDescrCustom == NELEM(kStdNibbleDescrs)-1); + fpNibbleDescrTable = new NibbleDescr[NELEM(kStdNibbleDescrs)]; + fNumNibbleDescrEntries = NELEM(kStdNibbleDescrs); + memcpy(fpNibbleDescrTable, kStdNibbleDescrs, sizeof(kStdNibbleDescrs)); + + fNibbleTrackBuf = nil; + fNibbleTrackLoaded = -1; + + fNuFXCompressType = kNuThreadFormatLZW2; + + fNotes = nil; + fpBadBlockMap = nil; + fDiskFSRefCnt = 0; +} + +/* + * Throw away local storage. + */ +DiskImg::~DiskImg(void) +{ + if (fpDataGFD != nil) { + WMSG0("~DiskImg closing GenericFD(s)\n"); + } + (void) CloseImage(); + delete[] fpNibbleDescrTable; + delete[] fNibbleTrackBuf; + delete[] fNotes; + delete fpBadBlockMap; + + /* normally these will be closed, but perhaps not if something failed */ + if (fpOuterGFD != nil) + delete fpOuterGFD; + if (fpWrapperGFD != nil) + delete fpWrapperGFD; + if (fpDataGFD != nil) + delete fpDataGFD; + if (fpOuterWrapper != nil) + delete fpOuterWrapper; + if (fpImageWrapper != nil) + delete fpImageWrapper; + + fDiskFSRefCnt = 100; // flag as freed +} + + +/* + * Set the nibble descr pointer. + */ +void +DiskImg::SetNibbleDescr(int idx) +{ + assert(idx >= 0 && idx < kNibbleDescrMAX); + fpNibbleDescr = &fpNibbleDescrTable[idx]; +} + +/* + * Set up a custom nibble descriptor. + */ +void +DiskImg::SetCustomNibbleDescr(const NibbleDescr* pDescr) +{ + if (pDescr == NULL) { + fpNibbleDescr = NULL; + } else { + assert(fpNibbleDescrTable != NULL); + //WMSG2("Overwriting entry %d with new value (special=%d)\n", + // kNibbleDescrCustom, pDescr->special); + fpNibbleDescrTable[kNibbleDescrCustom] = *pDescr; + fpNibbleDescr = &fpNibbleDescrTable[kNibbleDescrCustom]; + } +} + + +/* + * Open a volume or a file on disk. + * + * For Windows, we need to handle logical/physical volumes specially. If + * the filename matches the appropriate pattern, use a different GFD. + */ +DIError +DiskImg::OpenImage(const char* pathName, char fssep, bool readOnly) +{ + DIError dierr = kDIErrNone; + bool isWinDevice = false; + + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + WMSG3(" DI OpenImage '%s' '%.1s' ro=%d\n", pathName, &fssep, readOnly); + + fReadOnly = readOnly; + +#ifdef _WIN32 + if ((fssep == '\0' || fssep == '\\') && + pathName[0] >= 'A' && pathName[0] <= 'Z' && + pathName[1] == ':' && pathName[2] == '\\' && + pathName[3] == '\0') + { + isWinDevice = true; // logical volume ("A:\") + } + if ((fssep == '\0' || fssep == '\\') && + isdigit(pathName[0]) && isdigit(pathName[1]) && + pathName[2] == ':' && pathName[3] == '\\' && + pathName[4] == '\0') + { + isWinDevice = true; // physical volume ("80:\") + } + if ((fssep == '\0' || fssep == '\\') && + strncmp(pathName, kASPIDev, strlen(kASPIDev)) == 0 && + pathName[strlen(pathName)-1] == '\\') + { + isWinDevice = true; // ASPI volume ("ASPI:x:y:z\") + } +#endif + + if (isWinDevice) { +#ifdef _WIN32 + GFDWinVolume* pGFDWinVolume = new GFDWinVolume; + + dierr = pGFDWinVolume->Open(pathName, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDWinVolume; + goto bail; + } + + fpWrapperGFD = pGFDWinVolume; + // Use a unique extension to skip some of the probing. + dierr = AnalyzeImageFile("CPDevice.cp-win-vol", '\0'); + if (dierr != kDIErrNone) + goto bail; +#endif + } else { + GFDFile* pGFDFile = new GFDFile; + + dierr = pGFDFile->Open(pathName, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDFile; + goto bail; + } + + //fImageFileName = new char[strlen(pathName) + 1]; + //strcpy(fImageFileName, pathName); + + fpWrapperGFD = pGFDFile; + pGFDFile = nil; + + dierr = AnalyzeImageFile(pathName, fssep); + if (dierr != kDIErrNone) + goto bail; + } + + + assert(fpDataGFD != nil); + +bail: + return dierr; +} + +/* + * Open from a buffer, which could point to unadorned ready-to-go content + * or to a preloaded image file. + */ +DIError +DiskImg::OpenImage(const void* buffer, long length, bool readOnly) +{ + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + WMSG3(" DI OpenImage %08lx %ld ro=%d\n", (long) buffer, length, readOnly); + + DIError dierr; + GFDBuffer* pGFDBuffer; + + fReadOnly = readOnly; + pGFDBuffer = new GFDBuffer; + + dierr = pGFDBuffer->Open(const_cast(buffer), length, false, false, + readOnly); + if (dierr != kDIErrNone) { + delete pGFDBuffer; + return dierr; + } + + fpWrapperGFD = pGFDBuffer; + pGFDBuffer = nil; + + dierr = AnalyzeImageFile("", '\0'); + if (dierr != kDIErrNone) + return dierr; + + assert(fpDataGFD != nil); + return kDIErrNone; +} + +/* + * Open a range of blocks from an already-open disk image. This is only + * useful for things like UNIDOS volumes, which don't have an associated + * file in the image and are linear. + * + * The "read only" flag is inherited from the parent. + * + * For embedded images with visible file structure, we should be using + * an EmbeddedFD instead. [Note these were never implemented.] + * + * NOTE: there is an implicit ProDOS block ordering imposed on the parent + * image. It turns out that all of our current embedded parents use + * ProDOS-ordered blocks, so it works out okay, but the "linear" requirement + * above goes beyond just having contiguous blocks. + */ +DIError +DiskImg::OpenImage(DiskImg* pParent, long firstBlock, long numBlocks) +{ + WMSG3(" DI OpenImage parent=0x%08lx %ld %ld\n", (long) pParent, firstBlock, + numBlocks); + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + + if (pParent == nil || firstBlock < 0 || numBlocks <= 0 || + firstBlock + numBlocks > pParent->GetNumBlocks()) + { + assert(false); + return kDIErrInvalidArg; + } + + fReadOnly = pParent->GetReadOnly(); // very important + + DIError dierr; + GFDGFD* pGFDGFD; + + pGFDGFD = new GFDGFD; + dierr = pGFDGFD->Open(pParent->fpDataGFD, firstBlock * kBlockSize, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDGFD; + return dierr; + } + + fpDataGFD = pGFDGFD; + assert(fpWrapperGFD == nil); + + /* + * This replaces the call to "analyze image file" because we know we + * already have an open file with specific characteristics. + */ + //fOffset = pParent->fOffset + kBlockSize * firstBlock; + fLength = numBlocks * kBlockSize; + fOuterLength = fWrappedLength = fLength; + fFileFormat = kFileFormatUnadorned; + fPhysical = pParent->fPhysical; + fOrder = pParent->fOrder; + + fpParentImg = pParent; + + return dierr; +} +DIError +DiskImg::OpenImage(DiskImg* pParent, long firstTrack, long firstSector, + long numSectors) +{ + WMSG4(" DI OpenImage parent=0x%08lx %ld %ld %ld\n", (long) pParent, + firstTrack, firstSector, numSectors); + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + + if (pParent == nil) + return kDIErrInvalidArg; + + int prntSectPerTrack = pParent->GetNumSectPerTrack(); + int lastTrack = firstTrack + + (numSectors + prntSectPerTrack-1) / prntSectPerTrack; + if (firstTrack < 0 || numSectors <= 0 || + lastTrack > pParent->GetNumTracks()) + { + return kDIErrInvalidArg; + } + + fReadOnly = pParent->GetReadOnly(); // very important + + DIError dierr; + GFDGFD* pGFDGFD; + + pGFDGFD = new GFDGFD; + dierr = pGFDGFD->Open(pParent->fpDataGFD, + kSectorSize * firstTrack * prntSectPerTrack, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDGFD; + return dierr; + } + + fpDataGFD = pGFDGFD; + assert(fpWrapperGFD == nil); + + /* + * This replaces the call to "analyze image file" because we know we + * already have an open file with specific characteristics. + */ + assert(firstSector == 0); // else fOffset calculation breaks + //fOffset = pParent->fOffset + kSectorSize * firstTrack * prntSectPerTrack; + fLength = numSectors * kSectorSize; + fOuterLength = fWrappedLength = fLength; + fFileFormat = kFileFormatUnadorned; + fPhysical = pParent->fPhysical; + fOrder = pParent->fOrder; + + fpParentImg = pParent; + + return dierr; +} + + +/* + * Enable sector pairing. Useful for OzDOS. + */ +void +DiskImg::SetPairedSectors(bool enable, int idx) +{ + fSectorPairing = enable; + fSectorPairOffset = idx; + + if (enable) { + assert(idx == 0 || idx == 1); + } +} + +/* + * Close the image, freeing resources. + * + * If we write to a child DiskImg, it's responsible for setting the "dirty" + * flag in its parent (and so on up the chain). That's necessary so that, + * when we close the file, changes made to a child DiskImg cause the parent + * to do any necessary recompression. + * + * [ This is getting called even when image creation failed with an error. + * This is probably the correct behavior, but we may want to be aborting the + * image creation instead of completing it. That's a higher-level decision + * though. ++ATM 20040506 ] + */ +DIError +DiskImg::CloseImage(void) +{ + DIError dierr; + + WMSG1("CloseImage %p\n", this); + + /* check for DiskFS objects that still point to us */ + if (fDiskFSRefCnt != 0) { + WMSG1("ERROR: CloseImage: fDiskFSRefCnt=%d\n", fDiskFSRefCnt); + assert(false); //DebugBreak(); + } + + /* + * Flush any changes. + */ + dierr = FlushImage(kFlushAll); + if (dierr != kDIErrNone) + return dierr; + + /* + * Clean up. Close GFD, OrigGFD, and OuterGFD. Delete ImageWrapper + * and OuterWrapper. + * + * In some cases we will have the file open more than once (e.g. a + * NuFX archive, which must be opened on disk). + */ + if (fpDataGFD != nil) { + fpDataGFD->Close(); + delete fpDataGFD; + fpDataGFD = nil; + } + if (fpWrapperGFD != nil) { + fpWrapperGFD->Close(); + delete fpWrapperGFD; + fpWrapperGFD = nil; + } + if (fpOuterGFD != nil) { + fpOuterGFD->Close(); + delete fpOuterGFD; + fpOuterGFD = nil; + } + delete fpImageWrapper; + fpImageWrapper = nil; + delete fpOuterWrapper; + fpOuterWrapper = nil; + + return dierr; +} + + +/* + * Flush data to disk. + * + * The only time this really needs to do anything on a disk image file is + * when we have compressed data (NuFX, DDD, .gz, .zip). The uncompressed + * wrappers either don't do anything ("unadorned") or just update some + * header fields (DiskCopy42). + * + * If "mode" is kFlushFastOnly, we only flush the formats that don't really + * need flushing. This is part of a scheme to keep the disk contents in a + * reasonable state on the off chance we crash with a modified file open. + * It also helps the user understand when changes are being made immediately + * vs. when they're written to memory and compressed later. We could just + * refuse to raise the "dirty" flag when modifying "simple" file formats, + * but that would change the meaning of the flag from "something has been + * changed" to "what's in the file and what's in memory differ". I want it + * to be a "dirty" flag. + */ +DIError +DiskImg::FlushImage(FlushMode mode) +{ + DIError dierr = kDIErrNone; + + WMSG2(" DI FlushImage (dirty=%d mode=%d)\n", fDirty, mode); + if (!fDirty) + return kDIErrNone; + if (fpDataGFD == nil) { + /* + * This can happen if we tried to create a disk image but failed, e.g. + * couldn't create the output file because of access denied on the + * directory. There's no data, therefore nothing to flush, but the + * "dirty" flag is set because CreateImageCommon sets it almost + * immediately. + */ + WMSG0(" (disk must've failed during creation)\n"); + fDirty = false; + return kDIErrNone; + } + + if (mode == kFlushFastOnly && + ((fpImageWrapper != nil && !fpImageWrapper->HasFastFlush()) || + (fpOuterWrapper != nil && !fpOuterWrapper->HasFastFlush()) )) + { + WMSG0("DI fast flush requested, but one or both wrappers are slow\n"); + return kDIErrNone; + } + + /* + * Step 1: make sure any local caches have been flushed. + */ + /* (none) */ + + /* + * Step 2: push changes from fpDataGFD to fpWrapperGFD. This will + * cause ImageWrapper to rebuild itself (SHK, DDD, whatever). In + * some cases this amounts to copying the data on top of itself, + * which we can avoid easily. + * + * Embedded volumes don't have wrappers; when you write to an + * embedded volume, it passes straight through to the parent. + * + * (Note to self: formats like NuFX that write to a temp file and then + * rename over the old will close fpWrapperGFD and just access it + * directly. This is bad, because it doesn't allow them to have an + * "outer" format, but it's the way life is. The point is that it's + * okay for fpWrapperGFD to be non-nil but represent a closed file, + * so long as the "Flush" function has it figured out.) + */ + if (fpWrapperGFD != nil) { + WMSG2(" DI flushing data changes to wrapper (fLen=%ld fWrapLen=%ld)\n", + (long) fLength, (long) fWrappedLength); + dierr = fpImageWrapper->Flush(fpWrapperGFD, fpDataGFD, fLength, + &fWrappedLength); + if (dierr != kDIErrNone) { + WMSG1(" ERROR: wrapper flush failed (err=%d)\n", dierr); + return dierr; + } + /* flush the GFD in case it's a Win32 volume with block caching */ + dierr = fpWrapperGFD->Flush(); + } else { + assert(fpParentImg != nil); + } + + /* + * Step 3: if we have an fpOuterGFD, rebuild the file with the data + * in fpWrapperGFD. + */ + if (fpOuterWrapper != nil) { + WMSG1(" DI saving wrapper to outer, fWrapLen=%ld\n", + (long) fWrappedLength); + assert(fpOuterGFD != nil); + dierr = fpOuterWrapper->Save(fpOuterGFD, fpWrapperGFD, + fWrappedLength); + if (dierr != kDIErrNone) { + WMSG1(" ERROR: outer save failed (err=%d)\n", dierr); + return dierr; + } + } + + fDirty = false; + return kDIErrNone; +} + + + +/* + * Given the filename extension and a GFD, figure out what's inside. + * + * The filename extension should give us some idea what to expect: + * SHK, SDK, BXY - ShrinkIt compressed disk image + * GZ - gzip-compressed file (with something else inside) + * ZIP - ZIP archive with a single disk image inside + * DDD - DDD, DDD Pro, or DDD5.0 compressed image + * DSK - DiskCopy 4.2 or DO/PO + * DC - DiskCopy 4.2 (or 6?) + * DC6 - DiskCopy 6 (usually just raw sectors) + * DO, PO, D13, RAW? - DOS-order or ProDOS-order uncompressed + * IMG - Copy ][+ image (unadorned, physical sector order) + * HDV - virtual hard drive image + * NIB, RAW? - nibblized image + * (no extension) uncompressed + * cp-win-vol - our "magic" extension to indicate a Windows logical volume + * + * We can also examine the file length to see if it's a standard size + * (140K, 800K) and look for magic values in the header. + * + * If we can access the contents directly from disk, we do so. It's + * possibly more efficient to load the whole thing into memory, but if + * we have that much memory then the OS should cache it for us. (I have + * some 20MB disk images from my hard drive that shouldn't be loaded + * in their entirety. Certainly don't want to load a 512MB CFFA image.) + * + * On input, the following fields must be set: + * fpWrapperGFD - GenericFD for the file pointed to by "pathname" (or for a + * memory buffer if this is a sub-volume) + * + * On success, the following fields will be set: + * fWrappedLength, fOuterLength - set appropriately + * fpDataGFD - GFD for the raw data, possibly just a GFDGFD with an offset + * fLength - length of unadorned data in the file, or the length of + * data stored in fBuffer (test for fBuffer!=nil) + * fFileFormat - set to the overall file format, mostly interesting + * for identification of the file "wrapper" + * fPhysicalFormat - set to the type of data this holds + * (maybe) fOrder - set when the file format or extension dictates, e.g. + * 2MG or *.po; not always reliable + * (maybe) fDOSVolumeNum - set to DOS volume number from wrapper + * + * This may set fReadOnly if one of the wrappers looks okay but is reporting + * a bad checksum. + */ +DIError +DiskImg::AnalyzeImageFile(const char* pathName, char fssep) +{ + DIError dierr = kDIErrNone; + FileFormat probableFormat; + bool reliableExt; + const char* ext = FindExtension(pathName, fssep); + char* extBuf = nil; // uses malloc/free + bool needExtFromOuter = false; + + if (ext != nil) { + assert(*ext == '.'); + ext++; + } else + ext = ""; + + WMSG1(" DI AnalyzeImageFile ext='%s'\n", ext); + + /* sanity check: nobody should have configured these yet */ + assert(fOuterFormat == kOuterFormatUnknown); + assert(fFileFormat == kFileFormatUnknown); + assert(fOrder == kSectorOrderUnknown); + assert(fFormat == kFormatUnknown); + fLength = -1; + dierr = fpWrapperGFD->Seek(0, kSeekEnd); + if (dierr != kDIErrNone) { + WMSG0(" DI Couldn't seek to end of wrapperGFD\n"); + goto bail; + } + fWrappedLength = fOuterLength = fpWrapperGFD->Tell(); + + /* quick test for zero-length files */ + if (fWrappedLength == 0) + return kDIErrUnrecognizedFileFmt; + + /* + * Start by checking for a zip/gzip "wrapper wrapper". We want to strip + * that away before we do anything else. Because web sites tend to + * gzip everything in sight whether it needs it or not, we treat this + * as a special case and assume that anything could be inside. + * + * Some cases are difficult to handle, e.g. ".SDK", since NufxLib + * doesn't let us open an archive that is sitting in memory. + * + * We could also handle disk images stored as ordinary files stored + * inside SHK. Not much point in handling multiple files down at + * this level though. + */ + if (strcasecmp(ext, "gz") == 0 && + OuterGzip::Test(fpWrapperGFD, fOuterLength) == kDIErrNone) + { + WMSG0(" DI found gz outer wrapper\n"); + + fpOuterWrapper = new OuterGzip(); + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + fOuterFormat = kOuterFormatGzip; + + /* drop the ".gz" and get down to the next extension */ + ext = ""; + extBuf = strdup(pathName); + if (extBuf != nil) { + char* localExt; + + localExt = (char*) FindExtension(extBuf, fssep); + if (localExt != nil) + *localExt = '\0'; + localExt = (char*) FindExtension(extBuf, fssep); + if (localExt != nil) { + ext = localExt; + assert(*ext == '.'); + ext++; + } + } + WMSG1(" DI after gz, ext='%s'\n", ext == nil ? "(nil)" : ext); + + } else if (strcasecmp(ext, "zip") == 0) { + dierr = OuterZip::Test(fpWrapperGFD, fOuterLength); + if (dierr != kDIErrNone) + goto bail; + + WMSG0(" DI found ZIP outer wrapper\n"); + + fpOuterWrapper = new OuterZip(); + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + fOuterFormat = kOuterFormatZip; + + needExtFromOuter = true; + + } else { + fOuterFormat = kOuterFormatNone; + } + + /* finish up outer wrapper stuff */ + if (fOuterFormat != kOuterFormatNone) { + GenericFD* pNewGFD = nil; + dierr = fpOuterWrapper->Load(fpWrapperGFD, fOuterLength, fReadOnly, + &fWrappedLength, &pNewGFD); + if (dierr != kDIErrNone) { + WMSG0(" DI outer prep failed\n"); + /* extensions are "reliable", so failure is unavoidable */ + goto bail; + } + + /* Load() sets this */ + if (fpOuterWrapper->IsDamaged()) { + AddNote(kNoteWarning, "The zip/gzip wrapper appears to be damaged."); + fReadOnly = true; + } + + /* shift GFDs */ + fpOuterGFD = fpWrapperGFD; + fpWrapperGFD = pNewGFD; + + if (needExtFromOuter) { + ext = fpOuterWrapper->GetExtension(); + if (ext == nil) + ext = ""; + } + } + + /* + * Try to figure out what format the file is in. + * + * First pass, try only what the filename says it is. This way, if + * two file formats look alike, we have a good chance of getting it + * right. + * + * The "Test" functions have the complete file at their disposal. The + * file's length is stored in "fWrappedLength" for convenience. + */ + reliableExt = false; + probableFormat = kFileFormatUnknown; + if (strcasecmp(ext, "2mg") == 0 || strcasecmp(ext, "2img") == 0) { + reliableExt = true; + if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormat2MG; + } else if (strcasecmp(ext, "shk") == 0 || strcasecmp(ext, "sdk") == 0 || + strcasecmp(ext, "bxy") == 0) + { + DIError dierr2; + reliableExt = true; + dierr2 = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength); + if (dierr2 == kDIErrNone) + probableFormat = kFileFormatNuFX; + else if (dierr2 == kDIErrFileArchive) { + WMSG0(" AnalyzeImageFile thinks it found a NuFX file archive\n"); + dierr = dierr2; + goto bail; + } + } else if (strcasecmp(ext, "hdv") == 0) { + /* usually just a "raw" disk, but check for Sim //e */ + if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatSim2eHDV; + + /* ProDOS .hdv volumes can expand */ + fExpandable = true; + } else if (strcasecmp(ext, "dsk") == 0 || strcasecmp(ext, "dc") == 0) { + /* might be DiskCopy */ + if (WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatDiskCopy42; + } else if (strcasecmp(ext, "ddd") == 0) { + /* do this after compressed formats but before unadorned */ + reliableExt = true; + if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatDDD; + } else if (strcasecmp(ext, "app") == 0) { + reliableExt = true; + if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatTrackStar; + } else if (strcasecmp(ext, "fdi") == 0) { + reliableExt = true; + if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatFDI; + } else if (strcasecmp(ext, "img") == 0) { + if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + fOrder = kSectorOrderPhysical; + } + } else if (strcasecmp(ext, "nib") == 0 || strcasecmp(ext, "raw") == 0) { + if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatNib525_6656; + /* figure out NibbleFormat later */ + } + } else if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "po") == 0 || + strcasecmp(ext, "d13") == 0 || strcasecmp(ext, "dc6") == 0) + { + if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "d13") == 0) + fOrder = kSectorOrderDOS; + else + fOrder = kSectorOrderProDOS; // po, dc6 + WMSG1(" DI guessing order is %d by extension\n", fOrder); + } + } else if (strcasecmp(ext, "cp-win-vol") == 0) { + /* this is a Windows logical volume */ + reliableExt = true; + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + fOrder = kSectorOrderProDOS; + } else { + /* no match on the filename extension; start guessing */ + } + + if (probableFormat != kFileFormatUnknown) { + /* + * Found a match. Use "probableFormat" to open the file. + */ + WMSG1(" DI scored hit on extension '%s'\n", ext); + } else { + /* + * Didn't work. If the file extension was marked "reliable", then + * either we have the wrong extension on the file, or the contents + * are damaged. + * + * If the extension isn't reliable, or simply absent, then we have + * to probe through the formats we know and just hope for the best. + * + * If the "test" function returns with a checksum failure, we take + * it to mean that the format was positively identified, but the + * data inside is corrupted. This results in an immediate return + * with the checksum failure noted. Only a few wrapper formats + * have checksums embedded. (The "test" functions should only + * be looking at header checksums.) + */ + if (reliableExt) { + WMSG1(" DI file extension '%s' did not match contents\n", ext); + dierr = kDIErrBadFileFormat; + goto bail; + } else { + WMSG1(" DI extension '%s' not useful, probing formats\n", ext); + dierr = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength); + if (dierr == kDIErrNone) { + probableFormat = kFileFormatNuFX; + goto gotit; + } else if (dierr == kDIErrFileArchive) + goto bail; // we know it's NuFX, we know we can't use it + else if (dierr == kDIErrBadChecksum) + goto bail; // right file type, bad data + + dierr = WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength); + if (dierr == kDIErrNone) { + probableFormat = kFileFormatDiskCopy42; + goto gotit; + } else if (dierr == kDIErrBadChecksum) + goto bail; // right file type, bad data + + if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormat2MG; + } else if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormatDDD; + } else if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatSim2eHDV; + } else if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatTrackStar; + } else if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatFDI; + } else if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatNib525_6656; // placeholder + } else if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + } +gotit: ; + } + } + + /* + * Either we recognize it or we don't. Finish opening the file by + * setting up "fLength" and "fPhysical" values, extracting data + * into a memory buffer if necessary. fpDataGFD is set up by the + * "prep" function. + * + * If we're lucky, this will also configure "fOrder" for us, which is + * important when we can't recognize the filesystem format (for correct + * operation of disk tools). + */ + switch (probableFormat) { + case kFileFormat2MG: + fpImageWrapper = new Wrapper2MG(); + break; + case kFileFormatDiskCopy42: + fpImageWrapper = new WrapperDiskCopy42(); + break; + case kFileFormatSim2eHDV: + fpImageWrapper = new WrapperSim2eHDV(); + break; + case kFileFormatTrackStar: + fpImageWrapper = new WrapperTrackStar(); + break; + case kFileFormatFDI: + fpImageWrapper = new WrapperFDI(); + fReadOnly = true; // writing to FDI not yet supported + break; + case kFileFormatNuFX: + fpImageWrapper = new WrapperNuFX(); + ((WrapperNuFX*)fpImageWrapper)->SetCompressType( + (NuThreadFormat) fNuFXCompressType); + break; + case kFileFormatDDD: + fpImageWrapper = new WrapperDDD(); + break; + case kFileFormatUnadorned: + if (IsSectorFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedSector(); + else if (IsNibbleFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedNibble(); + else { + assert(false); + } + break; + default: + WMSG0(" DI couldn't figure out the file format\n"); + dierr = kDIErrUnrecognizedFileFmt; + break; + } + if (fpImageWrapper != nil) { + assert(fpDataGFD == nil); + dierr = fpImageWrapper->Prep(fpWrapperGFD, fWrappedLength, fReadOnly, + &fLength, &fPhysical, &fOrder, &fDOSVolumeNum, + &fpBadBlockMap, &fpDataGFD); + } else { + /* could be a mem alloc failure that didn't set dierr */ + if (dierr == kDIErrNone) + dierr = kDIErrGeneric; + } + + if (dierr != kDIErrNone) { + WMSG1(" DI wrapper prep failed (err=%d)\n", dierr); + goto bail; + } + + /* check for non-fatal checksum failures, e.g. DiskCopy42 */ + if (fpImageWrapper->IsDamaged()) { + AddNote(kNoteWarning, "File checksum didn't match."); + fReadOnly = true; + } + + fFileFormat = probableFormat; + + assert(fLength >= 0); + assert(fpDataGFD != nil); + assert(fOuterFormat != kOuterFormatUnknown); + assert(fFileFormat != kFileFormatUnknown); + assert(fPhysical != kPhysicalFormatUnknown); + +bail: + free(extBuf); + return dierr; +} + + +/* + * Try to figure out what we're looking at. + * + * Returns an error if we don't think this is even a disk image. If we + * just can't figure it out, we return success but with the format value + * set to "unknown". This gives the caller a chance to use "override" + * to help us find our way. + * + * On entry: + * fpDataGFD, fLength, and fFileFormat are defined + * fSectorPairing is specified + * fOrder has a semi-reliable guess at sector ordering + * On exit: + * fOrder and fFormat are set to the best of our ability + * fNumTracks, fNumSectPerTrack, and fNumBlocks are set + * fHasSectors, fHasTracks, and fHasNibbles are set + * fFileSysOrder is set + * fpNibbleDescr will be set for nibble images + */ +DIError +DiskImg::AnalyzeImage(void) +{ + assert(fLength >= 0); + assert(fpDataGFD != nil); + assert(fFileFormat != kFileFormatUnknown); + assert(fPhysical != kPhysicalFormatUnknown); + assert(fFormat == kFormatUnknown); + assert(fFileSysOrder == kSectorOrderUnknown); + assert(fNumTracks == -1); + assert(fNumSectPerTrack == -1); + assert(fNumBlocks == -1); + if (fpDataGFD == nil) + return kDIErrInternal; + + /* + * Figure out how many tracks and sectors the image has. + * + * For an odd-sized ProDOS image, there will be no tracks and sectors. + */ + if (IsSectorFormat(fPhysical)) { + if (!fLength) { + WMSG0(" DI zero-length disk images not allowed\n"); + return kDIErrOddLength; + } + + if (fLength == kD13Length) { + /* 13-sector .d13 image */ + fHasSectors = true; + fNumSectPerTrack = 13; + fNumTracks = kTrackCount525; + assert(!fHasBlocks); + } else if (fLength % (16 * kSectorSize) == 0) { + /* looks like a collection of 16-sector tracks */ + fHasSectors = true; + + fNumSectPerTrack = 16; + fNumTracks = (int) (fLength / (fNumSectPerTrack * kSectorSize)); + + /* sector pairing effectively cuts #of tracks in half */ + if (fSectorPairing) { + if ((fNumTracks & 0x01) != 0) { + WMSG0(" DI error: bad attempt at sector pairing\n"); + assert(false); + fSectorPairing = false; + } + } + + if (fSectorPairing) + fNumTracks /= 2; + } else { + if (fSectorPairing) { + WMSG1("GLITCH: sector pairing enabled, but fLength=%ld\n", + (long) fLength); + return kDIErrOddLength; + } + + assert(fNumTracks == -1); + assert(fNumSectPerTrack == -1); + assert((fLength % kBlockSize) == 0); + + fHasBlocks = true; + fNumBlocks = (long) (fLength / kBlockSize); + } + } else if (IsNibbleFormat(fPhysical)) { + fHasNibbles = fHasSectors = true; + + /* + * Figure out if it's 13-sector or 16-sector (or garbage). We + * have to make an assessment of the entire disk so we can declare + * it to be 13-sector or 16-sector, which is useful for DiskFS + * which will want to scan for DOS VTOCs and other goodies. We + * also want to provide a default NibbleDescr. + * + * Failing that, we still allow it to be opened for raw track access. + * + * This also sets fNumTracks, which could be more than 35 if we're + * working with a TrackStar or FDI image. + */ + DIError dierr; + dierr = AnalyzeNibbleData(); // sets nibbleDescr and DOS vol num + if (dierr == kDIErrNone) { + assert(fpNibbleDescr != nil); + fNumSectPerTrack = fpNibbleDescr->numSectors; + fOrder = kSectorOrderPhysical; + + if (!fReadOnly && !fpNibbleDescr->dataVerifyChecksum) { + WMSG0("DI nibbleDescr does not verify data checksum, disabling writes\n"); + AddNote(kNoteInfo, + "Sectors use non-standard data checksums; writing disabled."); + fReadOnly = true; + } + } else { + //assert(fpNibbleDescr == nil); + fNumSectPerTrack = -1; + fOrder = kSectorOrderPhysical; + fHasSectors = false; + } + } else { + WMSG1("Unsupported physical %d\n", fPhysical); + assert(false); + return kDIErrGeneric; + } + + /* + * Compute the number of blocks. For a 13-sector disk, block access + * is not possible. + * + * For nibble formats, we have to base the block count on the number + * of sectors rather than the file length. + */ + if (fHasSectors) { + assert(fNumSectPerTrack > 0); + if ((fNumSectPerTrack & 0x01) == 0) { + /* not a 13-sector disk, so define blocks in terms of sectors */ + /* (effects of sector pairing are already taken into account) */ + fHasBlocks = true; + fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2; + } + } else if (fHasBlocks) { + if ((fLength % kBlockSize) == 0) { + /* not sector-oriented, so define blocks based on length */ + fHasBlocks = true; + fNumBlocks = (long) (fLength / kBlockSize); + + if (fSectorPairing) { + if ((fNumBlocks & 0x01) != 0) { + WMSG0(" DI error: bad attempt at sector pairing (blk)\n"); + assert(false); + fSectorPairing = false; + } else + fNumBlocks /= 2; + } + + } else { + assert(false); + return kDIErrGeneric; + } + } else if (fHasNibbles) { + assert(fNumBlocks == -1); + } else { + WMSG0(" DI none of fHasSectors/fHasBlocks/fHasNibbles are set\n"); + assert(false); + return kDIErrInternal; + } + + /* + * We've got the track/sector/block layout sorted out; now figure out + * what kind of filesystem we're dealing with. + */ + AnalyzeImageFS(); + + WMSG4(" DI AnalyzeImage tracks=%ld sectors=%d blocks=%ld fileSysOrder=%d\n", + fNumTracks, fNumSectPerTrack, fNumBlocks, fFileSysOrder); + WMSG3(" hasBlocks=%d hasSectors=%d hasNibbles=%d\n", + fHasBlocks, fHasSectors, fHasNibbles); + + return kDIErrNone; +} + +/* + * Try to figure out what filesystem exists on this disk image. + * + * We want to test for DOS before ProDOS, because sometimes they overlap (e.g. + * 800K ProDOS disk with five 160K DOS volumes on it). + * + * Sets fFormat, fOrder, and fFileSysOrder. + */ +void +DiskImg::AnalyzeImageFS(void) +{ + /* + * In some circumstances it would be useful to have a set describing + * what filesystems we might expect to find, e.g. we're not likely to + * encounter RDOS embedded in a CF card. + */ + if (DiskFSMacPart::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatMacPart); + WMSG1(" DI found MacPart, order=%d\n", fOrder); + } else if (DiskFSMicroDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatMicroDrive); + WMSG1(" DI found MicroDrive, order=%d\n", fOrder); + } else if (DiskFSFocusDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatFocusDrive); + WMSG1(" DI found FocusDrive, order=%d\n", fOrder); + } else if (DiskFSCFFA::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + // The CFFA format doesn't have a partition map, but we do insist + // on finding multiple volumes. It needs to come after MicroDrive, + // because a disk formatted for CFFA then subsequently partitioned + // for MicroDrive will still look like valid CFFA unless you zero + // out the blocks. + assert(fFormat == kFormatCFFA4 || fFormat == kFormatCFFA8); + WMSG1(" DI found CFFA, order=%d\n", fOrder); + } else if (DiskFSFAT::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + // This is really just a trap to catch CFFA cards that were formatted + // for ProDOS and then re-formatted for MSDOS. As such it needs to + // come before the ProDOS test. It only works on larger volumes, + // and can be overridden, so it's pretty safe. + assert(fFormat == kFormatMSDOS); + WMSG1(" DI found MSDOS, order=%d\n", fOrder); + } else if (DiskFSDOS33::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatDOS32 || fFormat == kFormatDOS33); + WMSG1(" DI found DOS3.x, order=%d\n", fOrder); + if (fNumSectPerTrack == 13) + fFormat = kFormatDOS32; + } else if (DiskFSUNIDOS::TestWideFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + // Should only succeed on 400K embedded chunks. + assert(fFormat == kFormatDOS33); + fNumSectPerTrack = 32; + fNumTracks /= 2; + WMSG1(" DI found 'wide' DOS3.3, order=%d\n", fOrder); + } else if (DiskFSUNIDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatUNIDOS); + fNumSectPerTrack = 32; + fNumTracks /= 2; + WMSG1(" DI found UNIDOS, order=%d\n", fOrder); + } else if (DiskFSOzDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatOzDOS); + fNumSectPerTrack = 32; + fNumTracks /= 2; + WMSG1(" DI found OzDOS, order=%d\n", fOrder); + } else if (DiskFSProDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatProDOS); + WMSG1(" DI found ProDOS, order=%d\n", fOrder); + } else if (DiskFSPascal::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatPascal); + WMSG1(" DI found Pascal, order=%d\n", fOrder); + } else if (DiskFSCPM::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatCPM); + WMSG1(" DI found CP/M, order=%d\n", fOrder); + } else if (DiskFSRDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatRDOS33 || + fFormat == kFormatRDOS32 || + fFormat == kFormatRDOS3); + WMSG1(" DI found RDOS 3.3, order=%d\n", fOrder); + } else if (DiskFSHFS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatMacHFS); + WMSG1(" DI found HFS, order=%d\n", fOrder); + } else if (DiskFSGutenberg::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatGutenberg); + WMSG1(" DI found Gutenberg, order=%d\n", fOrder); + } else { + fFormat = kFormatUnknown; + WMSG1(" DI no recognizeable filesystem found (fOrder=%d)\n", + fOrder); + } + + fFileSysOrder = CalcFSSectorOrder(); +} + + +/* + * Override the format determined by the analyzer. + * + * If they insist on the presence of a valid filesystem, check to make sure + * that filesystem actually exists. + * + * Note that this does not allow overriding the file structure, which must + * be clearly identifiable to be at all useful. If the file has no "wrapper" + * structure, the "unadorned" format should be specified, and the contents + * identified by the PhysicalFormat. + */ +DIError +DiskImg::OverrideFormat(PhysicalFormat physical, FSFormat format, + SectorOrder order) +{ + DIError dierr = kDIErrNone; + SectorOrder newOrder; + FSFormat newFormat; + + WMSG3(" DI override: physical=%d format=%d order=%d\n", + physical, format, order); + + if (!IsSectorFormat(physical) && !IsNibbleFormat(physical)) + return kDIErrUnsupportedPhysicalFmt; + + /* don't allow forcing physical format change */ + if (physical != fPhysical) + return kDIErrInvalidArg; + + /* optimization */ + if (physical == fPhysical && format == fFormat && order == fOrder) { + WMSG0(" DI override matches existing, ignoring\n"); + return kDIErrNone; + } + + newOrder = order; + newFormat = format; + + switch (format) { + case kFormatDOS33: + case kFormatDOS32: + dierr = DiskFSDOS33::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + // Go ahead and allow the override even if the DOS version is wrong. + // So long as the sector count is correct, it's okay. + break; + case kFormatProDOS: + dierr = DiskFSProDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatPascal: + dierr = DiskFSPascal::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatMacHFS: + dierr = DiskFSHFS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatUNIDOS: + dierr = DiskFSUNIDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatOzDOS: + dierr = DiskFSOzDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatCFFA4: + case kFormatCFFA8: + dierr = DiskFSCFFA::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + // So long as it's CFFA, we allow the user to force it to be 4-mode + // or 8-mode. Don't require newFormat==format. + break; + case kFormatMacPart: + dierr = DiskFSMacPart::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatMicroDrive: + dierr = DiskFSMicroDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatFocusDrive: + dierr = DiskFSFocusDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatCPM: + dierr = DiskFSCPM::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatMSDOS: + dierr = DiskFSFAT::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatRDOS33: + case kFormatRDOS32: + case kFormatRDOS3: + dierr = DiskFSRDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + if (newFormat != format) + dierr = kDIErrFilesystemNotFound; // found RDOS, but wrong flavor + break; + case kFormatGenericPhysicalOrd: + case kFormatGenericProDOSOrd: + case kFormatGenericDOSOrd: + case kFormatGenericCPMOrd: + /* no discussion possible, since there's no FS to validate */ + newFormat = format; + newOrder = order; + break; + case kFormatUnknown: + /* only valid in rare situations, e.g. CFFA CreatePlaceholder */ + newFormat = format; + newOrder = order; + break; + default: + dierr = kDIErrUnsupportedFSFmt; + break; + } + + if (dierr != kDIErrNone) { + WMSG0(" DI override failed\n"); + goto bail; + } + + /* + * We passed in "order" to TestFS. If it came back with something + * different, it means that it didn't like the new order value even + * when "leniency" was granted. + */ + if (newOrder != order) { + dierr = kDIErrBadOrdering; + goto bail; + } + + fFormat = format; + fOrder = newOrder; + fFileSysOrder = CalcFSSectorOrder(); + + WMSG0(" DI override accepted\n"); + +bail: + return dierr; +} + +/* + * Figure out the sector ordering for this filesystem, so we can decide + * how the sectors need to be re-arranged when we're reading them. + * + * If the value returned by this function matches fOrder, then no swapping + * will be done. + * + * NOTE: this table is redundant with some knowledge embedded in the + * individual "TestFS" functions. + */ +DiskImg::SectorOrder +DiskImg::CalcFSSectorOrder(void) const +{ + /* in the absence of information, just leave it alone */ + if (fFormat == kFormatUnknown || fOrder == kSectorOrderUnknown) { + WMSG0(" DI WARNING: FindSectorOrder but format not known\n"); + return fOrder; + } + + assert(fOrder == kSectorOrderPhysical || fOrder == kSectorOrderCPM || + fOrder == kSectorOrderProDOS || fOrder == kSectorOrderDOS); + + switch (fFormat) { + case kFormatGenericPhysicalOrd: + case kFormatRDOS32: + case kFormatRDOS3: + return kSectorOrderPhysical; + + case kFormatGenericDOSOrd: + case kFormatDOS33: + case kFormatDOS32: + case kFormatUNIDOS: + case kFormatOzDOS: + case kFormatGutenberg: + return kSectorOrderDOS; + + case kFormatGenericCPMOrd: + case kFormatCPM: + return kSectorOrderCPM; + + case kFormatGenericProDOSOrd: + case kFormatProDOS: + case kFormatRDOS33: + case kFormatPascal: + case kFormatMacHFS: + case kFormatMacMFS: + case kFormatLisa: + case kFormatMSDOS: + case kFormatISO9660: + case kFormatCFFA4: + case kFormatCFFA8: + case kFormatMacPart: + case kFormatMicroDrive: + case kFormatFocusDrive: + return kSectorOrderProDOS; + + default: + assert(false); + return fOrder; + } +} + +/* + * Based on the disk format, figure out if we should prefer blocks or + * sectors when examining disk contents. + */ +bool +DiskImg::ShowAsBlocks(void) const +{ + if (!fHasBlocks) + return false; + + /* in the absence of information, assume sectors */ + if (fFormat == kFormatUnknown) { + if (fOrder == kSectorOrderProDOS) + return true; + else + return false; + } + + switch (fFormat) { + case kFormatGenericPhysicalOrd: + case kFormatGenericDOSOrd: + case kFormatDOS33: + case kFormatDOS32: + case kFormatRDOS3: + case kFormatRDOS33: + case kFormatUNIDOS: + case kFormatOzDOS: + case kFormatGutenberg: + return false; + + case kFormatGenericProDOSOrd: + case kFormatGenericCPMOrd: + case kFormatProDOS: + case kFormatPascal: + case kFormatMacHFS: + case kFormatMacMFS: + case kFormatLisa: + case kFormatCPM: + case kFormatMSDOS: + case kFormatISO9660: + case kFormatCFFA4: + case kFormatCFFA8: + case kFormatMacPart: + case kFormatMicroDrive: + case kFormatFocusDrive: + return true; + + default: + assert(false); + return false; + } +} + + +/* + * Format an image with the requested fileystem format. This only works if + * the matching DiskFS supports formatting of disks. + */ +DIError +DiskImg::FormatImage(FSFormat format, const char* volName) +{ + DIError dierr = kDIErrNone; + DiskFS* pDiskFS = nil; + FSFormat savedFormat; + + WMSG1(" DI FormatImage '%s'\n", volName); + + /* + * Open a temporary DiskFS for the requested format. We do this via the + * standard OpenAppropriate call, so we temporarily switch our format + * out. (We will eventually replace it, but we want to make sure that + * local error handling works correctly, so we restore it for now.) + */ + savedFormat = fFormat; + fFormat = format; + pDiskFS = OpenAppropriateDiskFS(false); + fFormat = savedFormat; + + if (pDiskFS == nil) { + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + dierr = pDiskFS->Format(this, volName); + if (dierr != kDIErrNone) + goto bail; + + WMSG0("DI format successful\n"); + fFormat = format; + +bail: + delete pDiskFS; + return dierr; +} + +/* + * Clear an image to zeros, usually done as a prelude to a higher-level format. + * + * BUG: this should also handle the track/sector case. + * + * HEY: this is awfully slow on large disks... should have some sort of + * optimized path that just writes to the GFD or something. Maybe even just + * a "ZeroBlock" instead of "WriteBlock" so we can memset instead of memcpy? + */ +DIError +DiskImg::ZeroImage(void) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlockSize]; + long block; + + WMSG1(" DI ZeroImage (%ld blocks)\n", GetNumBlocks()); + memset(blkBuf, 0, sizeof(blkBuf)); + + for (block = 0; block < GetNumBlocks(); block++) { + dierr = WriteBlock(block, blkBuf); + if (dierr != kDIErrNone) + break; + } + + return dierr; +} + + +/* + * Set the "scan progress" function. + * + * We want to use the same function for our sub-volumes too. + */ +void +DiskImg::SetScanProgressCallback(ScanProgressCallback func, void* cookie) +{ + if (fpParentImg != nil) { + /* unexpected, but perfectly okay */ + DebugBreak(); + } + + fpScanProgressCallback = func; + fScanProgressCookie = cookie; + fScanCount = 0; + fScanMsg[0] = '\0'; + fScanLastMsgWhen = time(nil); +} + +/* + * Update the progress. Call with a string at the start of a volume, then + * call with a NULL pointer every time we add a file. + */ +bool +DiskImg::UpdateScanProgress(const char* newStr) +{ + ScanProgressCallback func = fpScanProgressCallback; + DiskImg* pImg = this; + bool result = true; + + /* search up the tree to find a progress updater */ + while (func == nil) { + pImg = pImg->fpParentImg; + if (pImg == nil) + return result; // none defined, bail out + func = pImg->fpScanProgressCallback; + } + + time_t now = time(nil); + + if (newStr == NULL) { + fScanCount++; + //if ((fScanCount % 100) == 0) + if (fScanLastMsgWhen != now) { + result = (*func)(fScanProgressCookie, + fScanMsg, fScanCount); + fScanLastMsgWhen = now; + } + } else { + fScanCount = 0; + strncpy(fScanMsg, newStr, sizeof(fScanMsg)); + fScanMsg[sizeof(fScanMsg)-1] = '\0'; + result = (*func)(fScanProgressCookie, fScanMsg, + fScanCount); + fScanLastMsgWhen = now; + } + + return result; +} + + +/* + * ========================================================================== + * Block/track/sector I/O + * ========================================================================== + */ + +/* + * Handle sector order conversions. + */ +DIError +DiskImg::CalcSectorAndOffset(long track, int sector, SectorOrder imageOrder, + SectorOrder fsOrder, di_off_t* pOffset, int* pNewSector) +{ + if (!fHasSectors) + return kDIErrUnsupportedAccess; + + /* + * Sector order conversions. No table is needed for Copy ][+ format, + * which is equivalent to "physical". + */ + static const int raw2dos[16] = { + 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 + }; + static const int dos2raw[16] = { + 0, 13, 11, 9, 7, 5, 3, 1, 14, 12, 10, 8, 6, 4, 2, 15 + }; + static const int raw2prodos[16] = { + 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15 + }; + static const int prodos2raw[16] = { + 0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15 + }; + static const int raw2cpm[16] = { + 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5 + }; + static const int cpm2raw[16] = { + 0, 3, 6, 9, 12, 15, 2, 5, 8, 11, 14, 1, 4, 7, 10, 13 + }; + + if (track < 0 || track >= fNumTracks) { + WMSG1(" DI read invalid track %ld\n", track); + return kDIErrInvalidTrack; + } + if (sector < 0 || sector >= fNumSectPerTrack) { + WMSG1(" DI read invalid sector %d\n", sector); + return kDIErrInvalidSector; + } + + di_off_t offset; + int newSector = -1; + + /* + * 16-sector disks write sectors in ascending order and then remap + * them with a translation table. + */ + if (fNumSectPerTrack == 16 || fNumSectPerTrack == 32) { + if (fSectorPairing) { + assert(fSectorPairOffset == 0 || fSectorPairOffset == 1); + // this pushes "track" beyond fNumTracks + track *= 2; + if (sector >= 16) { + track++; + sector -= 16; + } + offset = track * fNumSectPerTrack * kSectorSize; + + sector = sector * 2 + fSectorPairOffset; + if (sector >= 16) { + offset += 16*kSectorSize; + sector -= 16; + } + } else { + offset = track * fNumSectPerTrack * kSectorSize; + if (sector >= 16) { + offset += 16*kSectorSize; + sector -= 16; + } + } + assert(sector >= 0 && sector < 16); + + /* convert request to "raw" sector number */ + switch (fsOrder) { + case kSectorOrderProDOS: + newSector = prodos2raw[sector]; + break; + case kSectorOrderDOS: + newSector = dos2raw[sector]; + break; + case kSectorOrderCPM: + newSector = cpm2raw[sector]; + break; + case kSectorOrderPhysical: // used for Copy ][+ + newSector = sector; + break; + case kSectorOrderUnknown: + // should never happen; fall through to "default" + default: + assert(false); + newSector = sector; + break; + } + + /* convert "raw" request to the image's ordering */ + switch (imageOrder) { + case kSectorOrderProDOS: + newSector = raw2prodos[newSector]; + break; + case kSectorOrderDOS: + newSector = raw2dos[newSector]; + break; + case kSectorOrderCPM: + newSector = raw2cpm[newSector]; + break; + case kSectorOrderPhysical: + //newSector = newSector; + break; + case kSectorOrderUnknown: + // should never happen; fall through to "default" + default: + assert(false); + //newSector = newSector; + break; + } + + if (imageOrder == fsOrder) { + assert(sector == newSector); + } + + offset += newSector * kSectorSize; + } else if (fNumSectPerTrack == 13) { + /* sector skew has no meaning, so assume no translation */ + offset = track * fNumSectPerTrack * kSectorSize; + newSector = sector; + offset += newSector * kSectorSize; + if (imageOrder != fsOrder) { + /* translation expected */ + WMSG2("NOTE: CalcSectorAndOffset for nspt=13 with img=%d fs=%d\n", + imageOrder, fsOrder); + } + } else { + assert(false); // should not be here + + /* try to do something reasonable */ + assert(imageOrder == fsOrder); + offset = (di_off_t)track * fNumSectPerTrack * kSectorSize; + offset += sector * kSectorSize; + } + + *pOffset = offset; + *pNewSector = newSector; + return kDIErrNone; +} + +/* + * Determine whether an image uses a linear mapping. This allows us to + * optimize block reads & writes, very useful when dealing with logical + * volumes under Windows (which also use 512-byte blocks). + * + * The "imageOrder" argument usually comes from fOrder, and "fsOrder" + * comes from "fFileSysOrder". + */ +inline bool +DiskImg::IsLinearBlocks(SectorOrder imageOrder, SectorOrder fsOrder) +{ + /* + * Any time fOrder==fFileSysOrder, we know that we have a linear + * mapping. This holds true for reading ProDOS blocks from a ".po" + * file or reading DOS sectors from a ".do" file. + */ + return (IsSectorFormat(fPhysical) && fHasBlocks && + imageOrder == fsOrder); +} + +/* + * Read the specified track and sector, adjusting for sector ordering as + * appropriate. + * + * Copies 256 bytes into "*buf". + * + * Returns 0 on success, nonzero on failure. + */ +DIError +DiskImg::ReadTrackSectorSwapped(long track, int sector, void* buf, + SectorOrder imageOrder, SectorOrder fsOrder) +{ + DIError dierr; + di_off_t offset; + int newSector = -1; + + if (buf == nil) + return kDIErrInvalidArg; + +#if 0 // Pre-d13 + if (fNumSectPerTrack == 13) { + /* no sector skewing possible for 13-sector disks */ + assert(fHasNibbles); + + return ReadNibbleSector(track, sector, buf, fpNibbleDescr); + } +#endif + + dierr = CalcSectorAndOffset(track, sector, imageOrder, fsOrder, + &offset, &newSector); + if (dierr != kDIErrNone) + return dierr; + + if (IsSectorFormat(fPhysical)) { + assert(offset+kSectorSize <= fLength); + + //WMSG2(" DI t=%d s=%d\n", track, + // (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize); + + dierr = CopyBytesOut(buf, offset, kSectorSize); + } else if (IsNibbleFormat(fPhysical)) { + if (imageOrder != kSectorOrderPhysical) { + WMSG2(" NOTE: nibble imageOrder is %d (expected %d)\n", + imageOrder, kSectorOrderPhysical); + } + dierr = ReadNibbleSector(track, newSector, buf, fpNibbleDescr); + } else { + assert(false); + dierr = kDIErrInternal; + } + + return dierr; +} + +/* + * Write the specified track and sector, adjusting for sector ordering as + * appropriate. + * + * Copies 256 bytes out of "buf". + * + * Returns 0 on success, nonzero on failure. + */ +DIError +DiskImg::WriteTrackSector(long track, int sector, const void* buf) +{ + DIError dierr; + di_off_t offset; + int newSector = -1; + + if (buf == nil) + return kDIErrInvalidArg; + if (fReadOnly) + return kDIErrAccessDenied; + +#if 0 // Pre-d13 + if (fNumSectPerTrack == 13) { + /* no sector skewing possible for 13-sector disks */ + assert(fHasNibbles); + + return WriteNibbleSector(track, sector, buf, fpNibbleDescr); + } +#endif + + dierr = CalcSectorAndOffset(track, sector, fOrder, fFileSysOrder, + &offset, &newSector); + if (dierr != kDIErrNone) + return dierr; + + if (IsSectorFormat(fPhysical)) { + assert(offset+kSectorSize <= fLength); + + //WMSG2(" DI t=%d s=%d\n", track, + // (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize); + + dierr = CopyBytesIn(buf, offset, kSectorSize); + } else if (IsNibbleFormat(fPhysical)) { + if (fOrder != kSectorOrderPhysical) { + WMSG2(" NOTE: nibble fOrder is %d (expected %d)\n", + fOrder, kSectorOrderPhysical); + } + dierr = WriteNibbleSector(track, newSector, buf, fpNibbleDescr); + } else { + assert(false); + dierr = kDIErrInternal; + } + + return dierr; +} + +/* + * Read a 512-byte block. + * + * Copies 512 bytes into "*buf". + */ +DIError +DiskImg::ReadBlockSwapped(long block, void* buf, SectorOrder imageOrder, + SectorOrder fsOrder) +{ + if (!fHasBlocks) + return kDIErrUnsupportedAccess; + if (block < 0 || block >= fNumBlocks) + return kDIErrInvalidBlock; + if (buf == nil) + return kDIErrInvalidArg; + + DIError dierr; + long track, blkInTrk; + + /* if we have a bad block map, check it */ + if (CheckForBadBlocks(block, 1)) { + dierr = kDIErrReadFailed; + goto bail; + } + + if (fHasSectors && !IsLinearBlocks(imageOrder, fsOrder)) { + /* run it through the t/s call so we handle DOS ordering */ + track = block / (fNumSectPerTrack/2); + blkInTrk = block - (track * (fNumSectPerTrack/2)); + dierr = ReadTrackSectorSwapped(track, blkInTrk*2, buf, + imageOrder, fsOrder); + if (dierr != kDIErrNone) + return dierr; + dierr = ReadTrackSectorSwapped(track, blkInTrk*2+1, + (char*)buf+kSectorSize, imageOrder, fsOrder); + } else if (fHasBlocks) { + /* no sectors, so no swapping; must be linear blocks */ + if (imageOrder != fsOrder) { + WMSG2(" DI NOTE: ReadBlockSwapped on non-sector (%d/%d)\n", + imageOrder, fsOrder); + } + dierr = CopyBytesOut(buf, (di_off_t) block * kBlockSize, kBlockSize); + } else { + assert(false); + dierr = kDIErrInternal; + } + +bail: + return dierr; +} + +/* + * Read multiple blocks. + * + * IMPORTANT: this returns immediately when a read fails. The buffer will + * probably not contain data from all readable sectors. The application is + * expected to retry the blocks individually. + */ +DIError +DiskImg::ReadBlocks(long startBlock, int numBlocks, void* buf) +{ + DIError dierr = kDIErrNone; + + assert(fHasBlocks); + assert(startBlock >= 0); + assert(numBlocks > 0); + assert(buf != nil); + + if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) { + assert(false); + return kDIErrInvalidArg; + } + + /* if we have a bad block map, check it */ + if (CheckForBadBlocks(startBlock, numBlocks)) { + dierr = kDIErrReadFailed; + goto bail; + } + + if (!IsLinearBlocks(fOrder, fFileSysOrder)) { + /* + * This isn't a collection of linear blocks, so we need to read it one + * block at a time with sector swapping. This almost certainly means + * that we're not reading from physical media, so performance shouldn't + * be an issue. + */ + if (startBlock == 0) { + WMSG0(" ReadBlocks: nonlinear, not trying\n"); + } + while (numBlocks--) { + dierr = ReadBlock(startBlock, buf); + if (dierr != kDIErrNone) + goto bail; + startBlock++; + buf = (unsigned char*)buf + kBlockSize; + } + } else { + if (startBlock == 0) { + WMSG0(" ReadBlocks: doing big linear reads\n"); + } + dierr = CopyBytesOut(buf, + (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize); + } + +bail: + return dierr; +} + +/* + * Check to see if any blocks in a range of blocks show up in the bad + * block map. This is primarily useful for 3.5" disk images converted + * from nibble images, because we convert them directly to "cooked" + * 512-byte blocks. + * + * Returns "true" if we found bad blocks, "false" if not. + */ +bool +DiskImg::CheckForBadBlocks(long startBlock, int numBlocks) +{ + int i; + + if (fpBadBlockMap == nil) + return false; + + for (i = startBlock; i < startBlock+numBlocks; i++) { + if (fpBadBlockMap->IsSet(i)) + return true; + } + return false; +} + +/* + * Write a block of data to a DiskImg. + * + * Returns immediately when a block write fails. Does not try to write all + * blocks before returning failure. + */ +DIError +DiskImg::WriteBlock(long block, const void* buf) +{ + if (!fHasBlocks) + return kDIErrUnsupportedAccess; + if (block < 0 || block >= fNumBlocks) + return kDIErrInvalidBlock; + if (buf == nil) + return kDIErrInvalidArg; + if (fReadOnly) + return kDIErrAccessDenied; + + DIError dierr; + long track, blkInTrk; + + if (fHasSectors && !IsLinearBlocks(fOrder, fFileSysOrder)) { + /* run it through the t/s call so we handle DOS ordering */ + track = block / (fNumSectPerTrack/2); + blkInTrk = block - (track * (fNumSectPerTrack/2)); + dierr = WriteTrackSector(track, blkInTrk*2, buf); + if (dierr != kDIErrNone) + return dierr; + dierr = WriteTrackSector(track, blkInTrk*2+1, (char*)buf+kSectorSize); + } else if (fHasBlocks) { + /* no sectors, so no swapping; must be linear blocks */ + if (fOrder != fFileSysOrder) { + WMSG2(" DI NOTE: WriteBlock on non-sector (%d/%d)\n", + fOrder, fFileSysOrder); + } + dierr = CopyBytesIn(buf, (di_off_t)block * kBlockSize, kBlockSize); + } else { + assert(false); + dierr = kDIErrInternal; + } + return dierr; +} + +/* + * Write multiple blocks. + */ +DIError +DiskImg::WriteBlocks(long startBlock, int numBlocks, const void* buf) +{ + DIError dierr = kDIErrNone; + + assert(fHasBlocks); + assert(startBlock >= 0); + assert(numBlocks > 0); + assert(buf != nil); + + if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) { + assert(false); + return kDIErrInvalidArg; + } + + if (!IsLinearBlocks(fOrder, fFileSysOrder)) { + /* + * This isn't a collection of linear blocks, so we need to write it + * one block at a time with sector swapping. This almost certainly + * means that we're not reading from physical media, so performance + * shouldn't be an issue. + */ + if (startBlock == 0) { + WMSG0(" WriteBlocks: nonlinear, not trying\n"); + } + while (numBlocks--) { + dierr = WriteBlock(startBlock, buf); + if (dierr != kDIErrNone) + goto bail; + startBlock++; + buf = (unsigned char*)buf + kBlockSize; + } + } else { + if (startBlock == 0) { + WMSG0(" WriteBlocks: doing big linear writes\n"); + } + dierr = CopyBytesIn(buf, + (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize); + } + +bail: + return dierr; +} + + +/* + * Copy a chunk of bytes out of the disk image. + * + * (This is the lowest-level read routine in this class.) + */ +DIError +DiskImg::CopyBytesOut(void* buf, di_off_t offset, int size) const +{ + DIError dierr; + + dierr = fpDataGFD->Seek(offset, kSeekSet); + if (dierr != kDIErrNone) { + WMSG2(" DI seek off=%ld failed (err=%d)\n", (long) offset, dierr); + return dierr; + } + + dierr = fpDataGFD->Read(buf, size); + if (dierr != kDIErrNone) { + WMSG3(" DI read off=%ld size=%d failed (err=%d)\n", + (long) offset, size, dierr); + return dierr; + } + + return kDIErrNone; +} + +/* + * Copy a chunk of bytes into the disk image. + * + * Sets the "dirty" flag. + * + * (This is the lowest-level write routine in DiskImg.) + */ +DIError +DiskImg::CopyBytesIn(const void* buf, di_off_t offset, int size) +{ + DIError dierr; + + if (fReadOnly) { + DebugBreak(); + return kDIErrAccessDenied; + } + assert(fpDataGFD != nil); // somebody closed the image? + + dierr = fpDataGFD->Seek(offset, kSeekSet); + if (dierr != kDIErrNone) { + WMSG2(" DI seek off=%ld failed (err=%d)\n", (long) offset, dierr); + return dierr; + } + + dierr = fpDataGFD->Write(buf, size); + if (dierr != kDIErrNone) { + WMSG3(" DI write off=%ld size=%d failed (err=%d)\n", + (long) offset, size, dierr); + return dierr; + } + + /* set the dirty flag here and everywhere above */ + DiskImg* pImg = this; + while (pImg != nil) { + pImg->fDirty = true; + pImg = pImg->fpParentImg; + } + + return kDIErrNone; +} + + +/* + * =========================================================================== + * Image creation + * =========================================================================== + */ + +/* + * Create a disk image with the specified parameters. + * + * "storageName" and "pNibbleDescr" may be nil. + */ +DIError +DiskImg::CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical, + const NibbleDescr* pNibbleDescr, SectorOrder order, + FSFormat format, long numBlocks, bool skipFormat) +{ + assert(fpDataGFD == nil); // should not be open already! + + if (numBlocks <= 0) { + WMSG1("ERROR: bad numBlocks %ld\n", numBlocks); + assert(false); + return kDIErrInvalidCreateReq; + } + + fOuterFormat = outerFormat; + fFileFormat = fileFormat; + fPhysical = physical; + SetCustomNibbleDescr(pNibbleDescr); + fOrder = order; + fFormat = format; + + fNumBlocks = numBlocks; + fHasBlocks = true; + + return CreateImageCommon(pathName, storageName, skipFormat); +} +DIError +DiskImg::CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical, + const NibbleDescr* pNibbleDescr, SectorOrder order, + FSFormat format, long numTracks, long numSectPerTrack, bool skipFormat) +{ + assert(fpDataGFD == nil); // should not be open already! + + if (numTracks <= 0 || numSectPerTrack == 0) { + WMSG2("ERROR: bad tracks/sectors %ld/%ld\n", numTracks, numSectPerTrack); + assert(false); + return kDIErrInvalidCreateReq; + } + + fOuterFormat = outerFormat; + fFileFormat = fileFormat; + fPhysical = physical; + SetCustomNibbleDescr(pNibbleDescr); + fOrder = order; + fFormat = format; + + fNumTracks = numTracks; + fNumSectPerTrack = numSectPerTrack; + fHasSectors = true; + if (numSectPerTrack < 0) { + /* nibble image with non-standard formatting */ + if (!IsNibbleFormat(fPhysical)) { + WMSG0("Whoa: expected nibble format here\n"); + assert(false); + return kDIErrInvalidCreateReq; + } + WMSG0("Sector image w/o sectors, switching to nibble mode\n"); + fHasNibbles = true; + fHasSectors = false; + fpNibbleDescr = nil; + } + + return CreateImageCommon(pathName, storageName, skipFormat); +} + +/* + * Do the actual disk image creation. + */ +DIError +DiskImg::CreateImageCommon(const char* pathName, const char* storageName, + bool skipFormat) +{ + DIError dierr; + + /* + * Step 1: figure out fHasBlocks/fHasSectors/fHasNibbles and any + * other misc fields. + * + * If the disk is a nibble image expected to have a particular + * volume number, it should have already been set by the application. + */ + if (fHasBlocks) { + if ((fNumBlocks % 8) == 0) { + fHasSectors = true; + fNumSectPerTrack = 16; + fNumTracks = fNumBlocks / 8; + } else { + WMSG0("NOTE: sector access to new image not possible\n"); + } + } else if (fHasSectors) { + if ((fNumSectPerTrack & 0x01) == 0) { + fHasBlocks = true; + fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2; + } else { + WMSG0("NOTE: block access to new image not possible\n"); + } + } + if (fHasSectors && fPhysical != kPhysicalFormatSectors) + fHasNibbles = true; + assert(fHasBlocks || fHasSectors || fHasNibbles); + + fFileSysOrder = CalcFSSectorOrder(); + fReadOnly = false; + fDirty = true; + + /* + * Step 2: check for invalid arguments and bad combinations. + */ + dierr = ValidateCreateFormat(); + if (dierr != kDIErrNone) { + WMSG0("ERROR: CIC arg validation failed, bailing\n"); + goto bail; + } + + /* + * Step 3: create the destination file. Put this into fpWrapperGFD + * or fpOuterGFD. + * + * The file must not already exist. + * + * THOUGHT: should allow creation of an in-memory disk image. This won't + * work for NuFX, but will work for pretty much everything else. + */ + WMSG1(" CIC: creating '%s'\n", pathName); + int fd; + fd = open(pathName, O_CREAT | O_EXCL, 0644); + if (fd < 0) { + dierr = (DIError) errno; + WMSG2("ERROR: unable to create file '%s' (errno=%d)\n", + pathName, dierr); + goto bail; + } + close(fd); + + GFDFile* pGFDFile; + pGFDFile = new GFDFile; + + dierr = pGFDFile->Open(pathName, false); + if (dierr != kDIErrNone) { + delete pGFDFile; + goto bail; + } + + if (fOuterFormat == kOuterFormatNone) + fpWrapperGFD = pGFDFile; + else + fpOuterGFD = pGFDFile; + pGFDFile = nil; + + /* + * Step 4: if we have an outer GFD and therefore don't currently have + * an fpWrapperGFD, create an expandable memory buffer to use. + * + * We want to take a guess at how big the image will be, so compute + * fLength now. + * + * Create an OuterWrapper as needed. + */ + if (IsSectorFormat(fPhysical)) { + if (fHasBlocks) + fLength = (di_off_t) GetNumBlocks() * kBlockSize; + else + fLength = (di_off_t) GetNumTracks() * GetNumSectPerTrack() * kSectorSize; + } else { + assert(IsNibbleFormat(fPhysical)); + fLength = GetNumTracks() * GetNibbleTrackAllocLength(); + } + assert(fLength > 0); + + if (fpWrapperGFD == nil) { + /* shift GFDs and create a new memory GFD, pre-sized */ + GFDBuffer* pGFDBuffer = new GFDBuffer; + + /* use fLength as a starting point for buffer size; this may expand */ + dierr = pGFDBuffer->Open(nil, fLength, true, true, false); + if (dierr != kDIErrNone) { + delete pGFDBuffer; + goto bail; + } + + fpWrapperGFD = pGFDBuffer; + pGFDBuffer = nil; + } + + /* create an fpOuterWrapper struct */ + switch (fOuterFormat) { + case kOuterFormatNone: + break; + case kOuterFormatGzip: + fpOuterWrapper = new OuterGzip; + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + break; + case kOuterFormatZip: + fpOuterWrapper = new OuterZip; + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + break; + default: + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + /* + * Step 5: tell the ImageWrapper to write itself into the GFD, passing + * in the blank memory buffer. + * + * - Unadorned formats copy from memory buffer to fpWrapperGFD on disk. + * (With gz, fpWrapperGFD is actually a memory buffer.) fpDataGFD + * becomes an offset into the file. + * - 2MG writes header into GFD and follows it with all data; DC42 + * and Sim2e do similar things. + * - NuFX reopens pathName as SHK file (fpWrapperGFD must point to a + * file) and accesses the archive through an fpArchive. fpDataGFD + * is created as a memory buffer and the blank image is copied in. + * - DDD leaves fpWrapperGFD alone and copies the blank image into a + * new buffer for fpDataGFD. + * + * Sets fWrappedLength when possible, determined from fPhysical and + * either fNumBlocks or fNumTracks. Creates fpDataGFD, often as a + * GFDGFD offset into fpWrapperGFD. + */ + switch (fFileFormat) { + case kFileFormat2MG: + fpImageWrapper = new Wrapper2MG(); + break; + case kFileFormatDiskCopy42: + fpImageWrapper = new WrapperDiskCopy42(); + fpImageWrapper->SetStorageName(storageName); + break; + case kFileFormatSim2eHDV: + fpImageWrapper = new WrapperSim2eHDV(); + break; + case kFileFormatTrackStar: + fpImageWrapper = new WrapperTrackStar(); + fpImageWrapper->SetStorageName(storageName); + break; + case kFileFormatFDI: + fpImageWrapper = new WrapperFDI(); + break; + case kFileFormatNuFX: + fpImageWrapper = new WrapperNuFX(); + fpImageWrapper->SetStorageName(storageName); + ((WrapperNuFX*)fpImageWrapper)->SetCompressType( + (NuThreadFormat) fNuFXCompressType); + break; + case kFileFormatDDD: + fpImageWrapper = new WrapperDDD(); + break; + case kFileFormatUnadorned: + if (IsSectorFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedSector(); + else if (IsNibbleFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedNibble(); + else { + assert(false); + } + break; + default: + assert(fpImageWrapper == nil); + break; + } + + if (fpImageWrapper == nil) { + WMSG0(" DI couldn't figure out the file format\n"); + dierr = kDIErrUnrecognizedFileFmt; + goto bail; + } + + /* create the wrapper, write the header, and create fpDataGFD */ + assert(fpDataGFD == nil); + dierr = fpImageWrapper->Create(fLength, fPhysical, fOrder, + fDOSVolumeNum, fpWrapperGFD, &fWrappedLength, &fpDataGFD); + if (dierr != kDIErrNone) { + WMSG1("ImageWrapper Create failed, err=%d\n", dierr); + goto bail; + } + assert(fpDataGFD != nil); + + /* + * Step 6: "format" fpDataGFD. + * + * Note we don't specify an ordering to the "create blank" functions. + * Either it's sectors, in which case it's all zeroes, or it's nibbles, + * in which case it's always in physical order. + * + * If we're formatting for nibbles, and the application hasn't specified + * a disk volume number, use the default (254). + */ + if (fPhysical == kPhysicalFormatSectors) + dierr = FormatSectors(fpDataGFD, skipFormat); // zero out the image + else { + assert(!skipFormat); // don't skip low-level nibble formatting! + if (fDOSVolumeNum == kVolumeNumNotSet) { + fDOSVolumeNum = kDefaultNibbleVolumeNum; + WMSG0(" Using default nibble volume num\n"); + } + + dierr = FormatNibbles(fpDataGFD); // write basic nibble stuff + } + + + /* + * We're done! + * + * Quick sanity check... + */ + if (fOuterFormat != kOuterFormatNone) { + assert(fpOuterGFD != nil); + assert(fpWrapperGFD != nil); + assert(fpDataGFD != nil); + } + +bail: + return dierr; +} + +/* + * Check that the requested format is one we can create. + * + * We don't allow .SDK.GZ or 6384-byte nibble 2MG. 2MG sector images + * must be in DOS or ProDOS order. + * + * Only "generic" FS formats may be used. The application may choose + * to call AnalyzeImage later on to set the actual FS once data has + * been written. + */ +DIError +DiskImg::ValidateCreateFormat(void) const +{ + /* + * Check for invalid arguments. + */ + if (fHasBlocks && fNumBlocks >= 4194304) { // 2GB or larger? + if (fFileFormat != kFileFormatUnadorned) { + WMSG0("CreateImage: images >= 2GB can only be unadorned\n"); + return kDIErrInvalidCreateReq; + } + } + if (fOuterFormat == kOuterFormatUnknown || + fFileFormat == kFileFormatUnknown || + fPhysical == kPhysicalFormatUnknown || + fOrder == kSectorOrderUnknown || + fFormat == kFormatUnknown) + { + WMSG0("CreateImage: ambiguous format\n"); + return kDIErrInvalidCreateReq; + } + if (fOuterFormat != kOuterFormatNone && + fOuterFormat != kOuterFormatGzip && + fOuterFormat != kOuterFormatZip) + { + WMSG1("CreateImage: unsupported outer format %d\n", fOuterFormat); + return kDIErrInvalidCreateReq; + } + if (fFileFormat != kFileFormatUnadorned && + fFileFormat != kFileFormat2MG && + fFileFormat != kFileFormatDiskCopy42 && + fFileFormat != kFileFormatSim2eHDV && + fFileFormat != kFileFormatTrackStar && + fFileFormat != kFileFormatFDI && + fFileFormat != kFileFormatNuFX && + fFileFormat != kFileFormatDDD) + { + WMSG1("CreateImage: unsupported file format %d\n", fFileFormat); + return kDIErrInvalidCreateReq; + } + if (fFormat != kFormatGenericPhysicalOrd && + fFormat != kFormatGenericProDOSOrd && + fFormat != kFormatGenericDOSOrd && + fFormat != kFormatGenericCPMOrd) + { + WMSG0("CreateImage: may only use 'generic' formats\n"); + return kDIErrInvalidCreateReq; + } + + /* + * Check for invalid combinations. + */ + if (fPhysical != kPhysicalFormatSectors) { + if (fOrder != kSectorOrderPhysical) { + WMSG0("CreateImage: nibble images are always 'physical' order\n"); + return kDIErrInvalidCreateReq; + } + + if (GetHasSectors() == false && GetHasNibbles() == false) { + WMSG2("CreateImage: must set hasSectors(%d) or hasNibbles(%d)\n", + GetHasSectors(), GetHasNibbles()); + return kDIErrInvalidCreateReq; + } + + if (fpNibbleDescr == nil && GetNumSectPerTrack() > 0) { + WMSG0("CreateImage: must provide NibbleDescr for non-sector\n"); + return kDIErrInvalidCreateReq; + } + + if (fpNibbleDescr != nil && + fpNibbleDescr->numSectors != GetNumSectPerTrack()) + { + WMSG2("CreateImage: ?? nd->numSectors=%d, GetNumSectPerTrack=%d\n", + fpNibbleDescr->numSectors, GetNumSectPerTrack()); + return kDIErrInvalidCreateReq; + } + + if (fpNibbleDescr != nil && ( + (fpNibbleDescr->numSectors == 13 && + fpNibbleDescr->encoding != kNibbleEnc53) || + (fpNibbleDescr->numSectors == 16 && + fpNibbleDescr->encoding != kNibbleEnc62)) + ) + { + WMSG0("CreateImage: sector count/encoding mismatch\n"); + return kDIErrInvalidCreateReq; + } + + if (GetNumTracks() != kTrackCount525 && + !(GetNumTracks() == 40 && fFileFormat == kFileFormatTrackStar)) + { + WMSG1("CreateImage: unexpected track count %ld\n", GetNumTracks()); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormat2MG) { + if (fPhysical != kPhysicalFormatSectors && + fPhysical != kPhysicalFormatNib525_6656) + { + WMSG1("CreateImage: 2MG can't handle physical %d\n", fPhysical); + return kDIErrInvalidCreateReq; + } + + if (fPhysical == kPhysicalFormatSectors && + (fOrder != kSectorOrderProDOS && + fOrder != kSectorOrderDOS)) + { + WMSG0("CreateImage: 2MG requires DOS or ProDOS ordering\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatNuFX) { + if (fOuterFormat != kOuterFormatNone) { + WMSG0("CreateImage: can't mix NuFX and outer wrapper\n"); + return kDIErrInvalidCreateReq; + } + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: NuFX physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderProDOS) { + WMSG0("CreateImage: NuFX is always ProDOS-order\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatDiskCopy42) { + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: DC42 physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if ((GetHasBlocks() && GetNumBlocks() != 1600) || + GetHasSectors() && + (GetNumTracks() != 200 || GetNumSectPerTrack() != 16)) + { + WMSG0("CreateImage: DC42 only for 800K disks\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderProDOS && + fOrder != kSectorOrderDOS) // used for UNIDOS disks?? + { + WMSG0("CreateImage: DC42 is always ProDOS or DOS\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatSim2eHDV) { + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: Sim2eHDV physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderProDOS) { + WMSG0("CreateImage: Sim2eHDV is always ProDOS-order\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatTrackStar) { + if (fPhysical != kPhysicalFormatNib525_Var) { + WMSG0("CreateImage: TrackStar physical must be var-nibbles\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatFDI) { + if (fPhysical != kPhysicalFormatNib525_Var) { + WMSG0("CreateImage: FDI physical must be var-nibbles\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatDDD) { + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: DDD physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderDOS) { + WMSG0("CreateImage: DDD is always DOS-order\n"); + return kDIErrInvalidCreateReq; + } + if (!GetHasSectors() || GetNumTracks() != 35 || + GetNumSectPerTrack() != 16) + { + WMSG0("CreateImage: DDD is only for 16-sector 35-track disks\n"); + return kDIErrInvalidCreateReq; + } + } + + return kDIErrNone; +} + +/* + * Create a blank image for physical=="sectors". + * + * fLength must be a multiple of 256. + * + * If "quickFormat" is set, only the very last sector is written (to set + * the EOF on the file). + */ +DIError +DiskImg::FormatSectors(GenericFD* pGFD, bool quickFormat) const +{ + DIError dierr = kDIErrNone; + char sctBuf[kSectorSize]; + di_off_t length; + + assert(fLength > 0 && (fLength & 0xff) == 0); + + //if (!(fLength & 0x01)) + // return FormatBlocks(pGFD); + + memset(sctBuf, 0, sizeof(sctBuf)); + pGFD->Rewind(); + + if (quickFormat) { + dierr = pGFD->Seek(fLength - sizeof(sctBuf), kSeekSet); + if (dierr != kDIErrNone) { + WMSG2(" FormatSectors: GFD seek %ld failed (err=%d)\n", + (long) fLength - sizeof(sctBuf), dierr); + goto bail; + } + dierr = pGFD->Write(sctBuf, sizeof(sctBuf), nil); + if (dierr != kDIErrNone) { + WMSG1(" FormatSectors: GFD quick write failed (err=%d)\n", dierr); + goto bail; + } + } else { + for (length = fLength ; length > 0; length -= sizeof(sctBuf)) { + dierr = pGFD->Write(sctBuf, sizeof(sctBuf), nil); + if (dierr != kDIErrNone) { + WMSG1(" FormatSectors: GFD write failed (err=%d)\n", dierr); + goto bail; + } + } + assert(length == 0); + } + + +bail: + return dierr; +} + +#if 0 // didn't help +/* + * Create a blank image for physical=="sectors". This is called from + * FormatSectors when it looks like we're formatting entire blocks. + */ +DIError +DiskImg::FormatBlocks(GenericFD* pGFD) const +{ + DIError dierr; + char blkBuf[kBlockSize]; + long length; + time_t start, end; + + assert(fLength > 0 && (fLength & 0x1ff) == 0); + + start = time(nil); + + memset(blkBuf, 0, sizeof(blkBuf)); + pGFD->Rewind(); + + for (length = fLength ; length > 0; length -= sizeof(blkBuf)) { + dierr = pGFD->Write(blkBuf, sizeof(blkBuf), nil); + if (dierr != kDIErrNone) { + WMSG1(" FormatBlocks: GFD write failed (err=%d)\n", dierr); + return dierr; + } + } + assert(length == 0); + + end = time(nil); + WMSG1("FormatBlocks complete, time=%ld\n", end - start); + + return kDIErrNone; +} +#endif + + +/* + * =========================================================================== + * Utility functions + * =========================================================================== + */ + +/* + * Add a note to this disk image. + * + * This is how we communicate cautions and warnings to the user. Use + * linefeeds ('\n') to indicate line breaks. + * + * The maximum length of a single note is set by the size of "buf". + */ +void +DiskImg::AddNote(NoteType type, const char* fmt, ...) +{ + char buf[512]; + char* cp = buf; + int maxLen = sizeof(buf); + va_list args; + int len; + + /* + * Prepend a string that highlights the note. + */ + switch (type) { + case kNoteWarning: + strcpy(cp, "- WARNING: "); + break; + default: + strcpy(cp, "- "); + break; + } + len = strlen(cp); + cp += len; + maxLen -= len; + + /* + * Add the note. + */ + va_start(args, fmt); +#if defined(HAVE_VSNPRINTF) + (void) vsnprintf(cp, maxLen, fmt, args); +#elif defined(HAVE__VSNPRINTF) + (void) _vsnprintf(cp, maxLen, fmt, args); +#else +# error "hosed" +#endif + va_end(args); + + buf[sizeof(buf)-2] = '\0'; // leave room for additional '\n' + len = strlen(buf); + if (len > 0 && buf[len-1] != '\n') { + buf[len] = '\n'; + buf[len+1] = '\0'; + len++; + } + + WMSG1("+++ adding note '%s'\n", buf); + + if (fNotes == nil) { + fNotes = new char[len +1]; + if (fNotes == nil) { + WMSG1("Unable to create notes[%d]\n", len+1); + assert(false); + return; + } + strcpy(fNotes, buf); + } else { + int existingLen = strlen(fNotes); + char* newNotes = new char[existingLen + len +1]; + if (newNotes == nil) { + WMSG1("Unable to create newNotes[%d]\n", existingLen+len+1); + assert(false); + return; + } + strcpy(newNotes, fNotes); + strcpy(newNotes + existingLen, buf); + delete[] fNotes; + fNotes = newNotes; + } +} + +/* + * Return a string with the notes in it. + */ +const char* +DiskImg::GetNotes(void) const +{ + if (fNotes == nil) + return ""; + else + return fNotes; +} + + +/* + * Get length and offset of tracks in a nibble image. This is necessary + * because of formats with variable-length tracks (e.g. TrackStar). + */ +int +DiskImg::GetNibbleTrackLength(long track) const +{ + assert(fpImageWrapper != NULL); + return fpImageWrapper->GetNibbleTrackLength(fPhysical, track); +} +int +DiskImg::GetNibbleTrackOffset(long track) const +{ + assert(fpImageWrapper != NULL); + return fpImageWrapper->GetNibbleTrackOffset(fPhysical, track); +} + + +/* + * Return a new object with the appropriate DiskFS sub-class. + * + * If the image hasn't been analyzed, or was analyzed to no avail, "nil" + * is returned unless "allowUnknown" is set to "true". In that case, a + * DiskFSUnknown is returned. + * + * This doesn't inspire the DiskFS to do any processing, just creates the + * new object. + */ +DiskFS* +DiskImg::OpenAppropriateDiskFS(bool allowUnknown) +{ + DiskFS* pDiskFS = nil; + + /* + * Create an appropriate DiskFS object. + */ + switch (GetFSFormat()) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + pDiskFS = new DiskFSDOS33(); + break; + case DiskImg::kFormatProDOS: + pDiskFS = new DiskFSProDOS(); + break; + case DiskImg::kFormatPascal: + pDiskFS = new DiskFSPascal(); + break; + case DiskImg::kFormatMacHFS: + pDiskFS = new DiskFSHFS(); + break; + case DiskImg::kFormatUNIDOS: + pDiskFS = new DiskFSUNIDOS(); + break; + case DiskImg::kFormatOzDOS: + pDiskFS = new DiskFSOzDOS(); + break; + case DiskImg::kFormatCFFA4: + case DiskImg::kFormatCFFA8: + pDiskFS = new DiskFSCFFA(); + break; + case DiskImg::kFormatMacPart: + pDiskFS = new DiskFSMacPart(); + break; + case DiskImg::kFormatMicroDrive: + pDiskFS = new DiskFSMicroDrive(); + break; + case DiskImg::kFormatFocusDrive: + pDiskFS = new DiskFSFocusDrive(); + break; + case DiskImg::kFormatCPM: + pDiskFS = new DiskFSCPM(); + break; + case DiskImg::kFormatMSDOS: + pDiskFS = new DiskFSFAT(); + break; + case DiskImg::kFormatRDOS33: + case DiskImg::kFormatRDOS32: + case DiskImg::kFormatRDOS3: + pDiskFS = new DiskFSRDOS(); + break; + case DiskImg::kFormatGutenberg: + pDiskFS = new DiskFSGutenberg(); + break; + + default: + WMSG1("WARNING: unhandled DiskFS case %d\n", GetFSFormat()); + assert(false); + /* fall through */ + case DiskImg::kFormatGenericPhysicalOrd: + case DiskImg::kFormatGenericProDOSOrd: + case DiskImg::kFormatGenericDOSOrd: + case DiskImg::kFormatGenericCPMOrd: + case DiskImg::kFormatUnknown: + if (allowUnknown) { + pDiskFS = new DiskFSUnknown(); + break; + } + } + + return pDiskFS; +} + + +/* + * Fill an array with SectorOrder values. The ordering specified by "first" + * will come first. Unused entries will be set to "unknown" and should be + * ignored. + * + * "orderArray" must have kSectorOrderMax elements. + */ +/*static*/ void +DiskImg::GetSectorOrderArray(SectorOrder* orderArray, SectorOrder first) +{ + // init array + for (int i = 0; i < kSectorOrderMax; i++) + orderArray[i] = (SectorOrder) i; + + // pull the best-guess ordering to the front + assert(orderArray[0] == kSectorOrderUnknown); + + orderArray[0] = first; + orderArray[(int) first] = kSectorOrderUnknown; + + // don't bother checking CP/M sector order + orderArray[kSectorOrderCPM] = kSectorOrderUnknown; +} + + +/* + * Return a short string describing "format". + * + * These are semi-duplicated in ImageFormatDialog.cpp in CiderPress. + */ +/*static*/ const char* +DiskImg::ToStringCommon(int format, const ToStringLookup* pTable, + int tableSize) +{ + for (int i = 0; i < tableSize; i++) { + if (pTable[i].format == format) + return pTable[i].str; + } + + assert(false); + return "(unknown)"; +} + +/*static*/ const char* +DiskImg::ToString(OuterFormat format) +{ + static const ToStringLookup kOuterFormats[] = { + { DiskImg::kOuterFormatUnknown, "Unknown format" }, + { DiskImg::kOuterFormatNone, "(none)" }, + { DiskImg::kOuterFormatCompress, "UNIX compress" }, + { DiskImg::kOuterFormatGzip, "gzip" }, + { DiskImg::kOuterFormatBzip2, "bzip2" }, + { DiskImg::kOuterFormatZip, "Zip archive" }, + }; + + return ToStringCommon(format, kOuterFormats, NELEM(kOuterFormats)); +} +/*static*/ const char* +DiskImg::ToString(FileFormat format) +{ + static const ToStringLookup kFileFormats[] = { + { DiskImg::kFileFormatUnknown, "Unknown format" }, + { DiskImg::kFileFormatUnadorned, "Unadorned raw data" }, + { DiskImg::kFileFormat2MG, "2MG" }, + { DiskImg::kFileFormatNuFX, "NuFX (ShrinkIt)" }, + { DiskImg::kFileFormatDiskCopy42, "DiskCopy 4.2" }, + { DiskImg::kFileFormatDiskCopy60, "DiskCopy 6.0" }, + { DiskImg::kFileFormatDavex, "Davex volume image" }, + { DiskImg::kFileFormatSim2eHDV, "Sim //e HDV" }, + { DiskImg::kFileFormatTrackStar, "TrackStar image" }, + { DiskImg::kFileFormatFDI, "FDI image" }, + { DiskImg::kFileFormatDDD, "DDD" }, + { DiskImg::kFileFormatDDDDeluxe, "DDDDeluxe" }, + }; + + return ToStringCommon(format, kFileFormats, NELEM(kFileFormats)); +}; +/*static*/ const char* +DiskImg::ToString(PhysicalFormat format) +{ + static const ToStringLookup kPhysicalFormats[] = { + { DiskImg::kPhysicalFormatUnknown, "Unknown format" }, + { DiskImg::kPhysicalFormatSectors, "Sectors" }, + { DiskImg::kPhysicalFormatNib525_6656, "Raw nibbles (6656-byte)" }, + { DiskImg::kPhysicalFormatNib525_6384, "Raw nibbles (6384-byte)" }, + { DiskImg::kPhysicalFormatNib525_Var, "Raw nibbles (variable len)" }, + }; + + return ToStringCommon(format, kPhysicalFormats, NELEM(kPhysicalFormats)); +}; +/*static*/ const char* +DiskImg::ToString(SectorOrder format) +{ + static const ToStringLookup kSectorOrders[] = { + { DiskImg::kSectorOrderUnknown, "Unknown ordering" }, + { DiskImg::kSectorOrderProDOS, "ProDOS block ordering" }, + { DiskImg::kSectorOrderDOS, "DOS sector ordering" }, + { DiskImg::kSectorOrderCPM, "CP/M block ordering" }, + { DiskImg::kSectorOrderPhysical, "Physical sector ordering" }, + }; + + return ToStringCommon(format, kSectorOrders, NELEM(kSectorOrders)); +}; +/*static*/ const char* +DiskImg::ToString(FSFormat format) +{ + static const ToStringLookup kFSFormats[] = { + { DiskImg::kFormatUnknown, "Unknown" }, + { DiskImg::kFormatProDOS, "ProDOS" }, + { DiskImg::kFormatDOS33, "DOS 3.3" }, + { DiskImg::kFormatDOS32, "DOS 3.2" }, + { DiskImg::kFormatPascal, "Pascal" }, + { DiskImg::kFormatMacHFS, "HFS" }, + { DiskImg::kFormatMacMFS, "MFS" }, + { DiskImg::kFormatLisa, "Lisa" }, + { DiskImg::kFormatCPM, "CP/M" }, + { DiskImg::kFormatMSDOS, "MS-DOS FAT" }, + { DiskImg::kFormatISO9660, "ISO-9660" }, + { DiskImg::kFormatRDOS33, "RDOS 3.3 (16-sector)" }, + { DiskImg::kFormatRDOS32, "RDOS 3.2 (13-sector)" }, + { DiskImg::kFormatRDOS3, "RDOS 3 (cracked 13-sector)" }, + { DiskImg::kFormatGenericDOSOrd, "Generic DOS sectors" }, + { DiskImg::kFormatGenericProDOSOrd, "Generic ProDOS blocks" }, + { DiskImg::kFormatGenericPhysicalOrd, "Generic raw sectors" }, + { DiskImg::kFormatGenericCPMOrd, "Generic CP/M blocks" }, + { DiskImg::kFormatUNIDOS, "UNIDOS (400K DOS x2)" }, + { DiskImg::kFormatOzDOS, "OzDOS (400K DOS x2)" }, + { DiskImg::kFormatCFFA4, "CFFA (4 or 6 partitions)" }, + { DiskImg::kFormatCFFA8, "CFFA (8 partitions)" }, + { DiskImg::kFormatMacPart, "Macintosh partitioned disk" }, + { DiskImg::kFormatMicroDrive, "MicroDrive partitioned disk" }, + { DiskImg::kFormatFocusDrive, "FocusDrive partitioned disk" }, + }; + + return ToStringCommon(format, kFSFormats, NELEM(kFSFormats)); +}; + + +/* + * strerror() equivalent for DiskImg errors. + */ +const char* +DiskImgLib::DIStrError(DIError dierr) +{ + if (dierr > 0) { + const char* msg; + msg = strerror(dierr); + if (msg != nil) + return msg; + } + + /* + * BUG: this should be set up as per-thread storage in an MT environment. + * I would be more inclined to worry about this if I was expecting + * to hit "default:". So long as valid values are passed in, and the + * switch statement is kept up to date, we should never have cause + * to return this. + * + * An easier solution, should this present a problem for someone, would + * be to have the function return nil or "unknown error" when the + * error value isn't recognized. I'd recommend leaving it as-is for + * debug builds, though, as it's helpful to know *which* error is not + * recognized. + */ + static char defaultMsg[32]; + + switch (dierr) { + case kDIErrNone: + return "(no error)"; + + case kDIErrAccessDenied: + return "access denied"; + case kDIErrVWAccessForbidden: + return "for safety, write access to this volume is forbidden"; + case kDIErrSharingViolation: + return "file is already open and cannot be shared"; + case kDIErrNoExclusiveAccess: + return "couldn't get exclusive access"; + case kDIErrWriteProtected: + return "write protected"; + case kDIErrCDROMNotSupported: + return "access to CD-ROM drives is not supported"; + case kDIErrASPIFailure: + return "an ASPI request failed"; + case kDIErrSPTIFailure: + return "an SPTI request failed"; + case kDIErrSCSIFailure: + return "a SCSI request failed"; + case kDIErrDeviceNotReady: + return "device not ready"; + + case kDIErrFileNotFound: + return "file not found"; + case kDIErrForkNotFound: + return "fork not found"; + case kDIErrAlreadyOpen: + return "an image is already open"; + case kDIErrFileOpen: + return "file is open"; + case kDIErrNotReady: + return "object not ready"; + case kDIErrFileExists: + return "file already exists"; + case kDIErrDirectoryExists: + return "directory already exists"; + + case kDIErrEOF: + return "end of file reached"; + case kDIErrReadFailed: + return "read failed"; + case kDIErrWriteFailed: + return "write failed"; + case kDIErrDataUnderrun: + return "tried to read past end of file"; + case kDIErrDataOverrun: + return "tried to write past end of file"; + case kDIErrGenericIO: + return "I/O error"; + + case kDIErrOddLength: + return "image size is wrong"; + case kDIErrUnrecognizedFileFmt: + return "not a recognized disk image format"; + case kDIErrBadFileFormat: + return "image file contents aren't in expected format"; + case kDIErrUnsupportedFileFmt: + return "file format not supported"; + case kDIErrUnsupportedPhysicalFmt: + return "physical format not supported"; + case kDIErrUnsupportedFSFmt: + return "filesystem type not supported"; + case kDIErrBadOrdering: + return "bad sector ordering"; + case kDIErrFilesystemNotFound: + return "specified filesystem not found"; + case kDIErrUnsupportedAccess: + return "the method of access used isn't supported for this image"; + case kDIErrUnsupportedImageFeature: + return "image file uses features that CiderPress doesn't support"; + + case kDIErrInvalidTrack: + return "invalid track number"; + case kDIErrInvalidSector: + return "invalid sector number"; + case kDIErrInvalidBlock: + return "invalid block number"; + case kDIErrInvalidIndex: + return "invalid index number"; + + case kDIErrDirectoryLoop: + return "disk directory structure has an infinite loop"; + case kDIErrFileLoop: + return "file structure has an infinite loop"; + case kDIErrBadDiskImage: + return "the filesystem on this image appears damaged"; + case kDIErrBadFile: + return "file structure appears damaged"; + case kDIErrBadDirectory: + return "a directory appears damaged"; + case kDIErrBadPartition: + return "bad partition"; + + case kDIErrFileArchive: + return "this looks like a file archive, not a disk archive"; + case kDIErrUnsupportedCompression: + return "compression method not supported"; + case kDIErrBadChecksum: + return "checksum doesn't match, data may be corrupted"; + case kDIErrBadCompressedData: + return "the compressed data is corrupted"; + case kDIErrBadArchiveStruct: + return "archive may be damaged"; + + case kDIErrBadNibbleSectors: + return "couldn't read sectors from this image"; + case kDIErrSectorUnreadable: + return "sector not readable"; + case kDIErrInvalidDiskByte: + return "found invalid nibble image disk byte"; + case kDIErrBadRawData: + return "couldn't convert raw data to nibble data"; + + case kDIErrInvalidFileName: + return "invalid file name"; + case kDIErrDiskFull: + return "disk full"; + case kDIErrVolumeDirFull: + return "volume directory is full"; + case kDIErrInvalidCreateReq: + return "invalid disk image create request"; + case kDIErrTooBig: + return "size is larger than we can handle"; + + case kDIErrGeneric: + return "DiskImg generic error"; + case kDIErrInternal: + return "DiskImg internal error"; + case kDIErrMalloc: + return "memory allocation failure"; + case kDIErrInvalidArg: + return "invalid argument"; + case kDIErrNotSupported: + return "feature not supported"; + case kDIErrCancelled: + return "cancelled by user"; + + case kDIErrNufxLibInitFailed: + return "NufxLib initialization failed"; + + default: + sprintf(defaultMsg, "(error=%d)", dierr); + return defaultMsg; + } +} + +/* + * High ASCII conversion table, from Technical Note PT515, + * "Apple File Exchange Q&As". The table is available in a hopelessly + * blurry PDF or a pair of GIFs created with small fonts, but I think I + * have mostly captured it. + */ +/*static*/ const unsigned char DiskImg::kMacHighASCII[128+1] = + "AACENOUaaaaaaceeeeiiiinooooouuuu" // 0x80 - 0x9f + "tocL$oPBrct'.=AO%+<>YudsPpSaoOao" // 0xa0 - 0xbf + "?!-vf=d<>. AAOOo--\"\"''/oyY/o<> f" // 0xc0 - 0xdf + "|*,,%AEAEEIIIIOOaOUUUi^~-,**,\"? "; // 0xe0 - 0xff + + +/* + * Hack for Win32 systems. See Win32BlockIO.cpp for commentary. + */ +bool DiskImgLib::gAllowWritePhys0 = false; +/*static*/ void DiskImg::SetAllowWritePhys0(bool val) { + DiskImgLib::gAllowWritePhys0 = val; +} diff --git a/diskimg/DiskImg.h b/diskimg/DiskImg.h index a6afe5a..9b900d1 100644 --- a/diskimg/DiskImg.h +++ b/diskimg/DiskImg.h @@ -1,1659 +1,1662 @@ -/* - * CiderPress - * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. - * See the file LICENSE for distribution terms. - */ -/* - * Public declarations for the DiskImg library. - * - * Everything is wrapped in the "DiskImgLib" namespace. Either prefix - * all references with "DiskImgLib::", or add "using namespace DiskImgLib" - * to all C++ source files that make use of it. - * - * Under Linux, this should be compiled with -D_FILE_OFFSET_BITS=64. - * - * These classes are not thread-safe with respect to access to a single - * disk image. Writing to the same disk image from multiple threads - * simultaneously is bound to end in disaster. Simultaneous access to - * different objects will work, though modifying the same disk image - * file from multiple objects will lead to unpredictable results. - */ -#ifndef __DISK_IMG__ -#define __DISK_IMG__ - -#include -#include -#include - -//#define EXCISE_GPL_CODE - -/* Windows DLL stuff */ -#ifdef _WIN32 -# ifdef DISKIMG_EXPORTS -# define DISKIMG_API __declspec(dllexport) -# else -# define DISKIMG_API __declspec(dllimport) -# endif -#else -# define DISKIMG_API -#endif - -namespace DiskImgLib { - -/* compiled-against versions; call DiskImg::GetVersion for linked-against */ -#define kDiskImgVersionMajor 4 -#define kDiskImgVersionMinor 6 -#define kDiskImgVersionBug 0 - - -/* - * Errors from the various disk image classes. - */ -typedef enum DIError { - kDIErrNone = 0, - - /* I/O request errors (should renumber/rename to match GS/OS errors?) */ - kDIErrAccessDenied = -10, - kDIErrVWAccessForbidden = -11, // write access to volume forbidden - kDIErrSharingViolation = -12, // file is in use and not shareable - kDIErrNoExclusiveAccess = -13, // couldn't get exclusive access - kDIErrWriteProtected = -14, // disk is write protected - kDIErrCDROMNotSupported = -15, // access to CD-ROM drives not supptd - kDIErrASPIFailure = -16, // generic ASPI failure result - kDIErrSPTIFailure = -17, // generic SPTI failure result - kDIErrSCSIFailure = -18, // generic SCSI failure result - kDIErrDeviceNotReady = -19, // floppy or CD-ROM drive has no media - - kDIErrFileNotFound = -20, - kDIErrForkNotFound = -21, // requested fork does not exist - kDIErrAlreadyOpen = -22, // already open, can't open a 2nd time - kDIErrFileOpen = -23, // file is open, can't delete it - kDIErrNotReady = -24, - kDIErrFileExists = -25, // file already exists - kDIErrDirectoryExists = -26, // directory already exists - - kDIErrEOF = -30, // end-of-file reached - kDIErrReadFailed = -31, - kDIErrWriteFailed = -32, - kDIErrDataUnderrun = -33, // tried to read off end of the image - kDIErrDataOverrun = -34, // tried to write off end of the image - kDIErrGenericIO = -35, // generic I/O error - - kDIErrOddLength = -40, // image size not multiple of sectors - kDIErrUnrecognizedFileFmt = -41, // file format just not recognized - kDIErrBadFileFormat = -42, // filename ext doesn't match contents - kDIErrUnsupportedFileFmt = -43, // recognized but not supported - kDIErrUnsupportedPhysicalFmt = -44, // (same) - kDIErrUnsupportedFSFmt = -45, // (and again) - kDIErrBadOrdering = -46, // requested sector ordering no good - kDIErrFilesystemNotFound = -47, // requested filesystem isn't there - kDIErrUnsupportedAccess = -48, // e.g. read sectors from blocks-only - kDIErrUnsupportedImageFeature = -49, // e.g. FDI image w/Amiga sectors - - kDIErrInvalidTrack = -50, // request for invalid track number - kDIErrInvalidSector = -51, // request for invalid sector number - kDIErrInvalidBlock = -52, // request for invalid block number - kDIErrInvalidIndex = -53, // request with an invalid index - - kDIErrDirectoryLoop = -60, // directory chain points into itself - kDIErrFileLoop = -61, // file sector or block alloc loops - kDIErrBadDiskImage = -62, // the FS on the disk image is damaged - kDIErrBadFile = -63, // bad file on disk image - kDIErrBadDirectory = -64, // bad dir on disk image - kDIErrBadPartition = -65, // bad partition on multi-part format - - kDIErrFileArchive = -70, // file archive, not disk archive - kDIErrUnsupportedCompression = -71, // compression method is not supported - kDIErrBadChecksum = -72, // image file's checksum is bad - kDIErrBadCompressedData = -73, // data can't even be unpacked - kDIErrBadArchiveStruct = -74, // bad archive structure - - kDIErrBadNibbleSectors = -80, // can't read sectors from this image - kDIErrSectorUnreadable = -81, // requested sector not readable - kDIErrInvalidDiskByte = -82, // invalid byte for encoding type - kDIErrBadRawData = -83, // couldn't get correct nibbles - - kDIErrInvalidFileName = -90, // tried to create file with bad name - kDIErrDiskFull = -91, // no space left on disk - kDIErrVolumeDirFull = -92, // no more entries in volume dir - kDIErrInvalidCreateReq = -93, // CreateImage request was flawed - kDIErrTooBig = -94, // larger than we want to handle - - /* higher-level errors */ - kDIErrGeneric = -101, - kDIErrInternal = -102, - kDIErrMalloc = -103, - kDIErrInvalidArg = -104, - kDIErrNotSupported = -105, // feature not currently supported - kDIErrCancelled = -106, // an operation was cancelled by user - - kDIErrNufxLibInitFailed = -110, -} DIError; - -/* return a string describing the error */ -DISKIMG_API const char* DIStrError(DIError dierr); - - -/* exact definition of off_t varies, so just define our own */ -#ifdef _ULONGLONG_ - typedef LONGLONG di_off_t; -#else - typedef off_t di_off_t; -#endif - -/* common definition of "whence" for seeks */ -typedef enum DIWhence { - kSeekSet = SEEK_SET, - kSeekCur = SEEK_CUR, - kSeekEnd = SEEK_END -}; - -/* try to load ASPI under Win2K; if successful, SPTI should be disabled */ -const bool kAlwaysTryASPI = false; -/* ASPI device "filenames" look like "ASPI:x:y:z\" */ -DISKIMG_API extern const char* kASPIDev; - -/* some nibble-encoding constants */ -const int kTrackLenNib525 = 6656; -const int kTrackLenNb2525 = 6384; -const int kTrackLenTrackStar525 = 6525; // max len of data in TS image -const int kTrackAllocSize = 6656; // max 5.25 nibble track len; for buffers -const int kTrackCount525 = 35; // expected #of tracks on 5.25 img -const int kMaxNibbleTracks525 = 40; // max #of tracks on 5.25 nibble img -const int kDefaultNibbleVolumeNum = 254; -const int kBlockSize = 512; // block size for DiskImg interfaces -const int kSectorSize = 256; // sector size (1/2 block) -const int kD13Length = 256 * 13 * 35; // length of a .d13 image - -/* largest expanse we allow access to on a volume (8GB in 512-byte blocks) */ -const long kVolumeMaxBlocks = 8*1024*(1024*1024 / kBlockSize); - -/* largest .gz file we'll open (uncompressed size) */ -const long kGzipMax = 32*1024*1024; - -/* forward and external class definitions */ -class DiskFS; -class A2File; -class A2FileDescr; -class GenericFD; -class OuterWrapper; -class ImageWrapper; -class CircularBufferAccess; -class ASPI; -class LinearBitmap; - - -/* - * Library-global data functions. - * - * This class is just a namespace clumper. Do not instantiate. - */ -class DISKIMG_API Global { -public: - // one-time DLL initialization; use SetDebugMsgHandler first - static DIError AppInit(void); - // one-time DLL cleanup - static DIError AppCleanup(void); - - // return the DiskImg version number - static void GetVersion(long* pMajor, long* pMinor, long* pBug); - - static bool GetAppInitCalled(void) { return fAppInitCalled; } - static bool GetHasSPTI(void); - static bool GetHasASPI(void); - - // return a pointer to our global ASPI instance - static ASPI* GetASPI(void) { return fpASPI; } - // shortcut for fpASPI->GetVersion() - static unsigned long GetASPIVersion(void); - - // pointer to the debug message handler - typedef void (*DebugMsgHandler)(const char* file, int line, const char* msg); - static DebugMsgHandler gDebugMsgHandler; - - static DebugMsgHandler SetDebugMsgHandler(DebugMsgHandler handler); - static void PrintDebugMsg(const char* file, int line, const char* fmt, ...) - #if defined(__GNUC__) - __attribute__ ((format(printf, 3, 4))) - #endif - ; - -private: - // no instantiation allowed - Global(void) {} - ~Global(void) {} - - // make sure app calls AppInit - static bool fAppInitCalled; - - static ASPI* fpASPI; -}; - -extern bool gAllowWritePhys0; // ugh -- see Win32BlockIO.cpp - - -/* - * Disk I/O class, roughly equivalent to a GS/OS disk device driver. - * - * Abstracts away the file's source (file on disk, file in memory) and - * storage format (DOS order, ProDOS order, nibble). Will also cope - * with common disk compression and wrapper formats (Mac DiskCopy, 2MG, - * ShrinkIt, etc) if handed a file on disk. - * - * Images may be embedded within other images, e.g. UNIDOS and storage - * type $04 pascal volumes. - * - * THOUGHT: we need a list(?) of pointers from here back to the DiskFS - * so that any modifications here will "wake" the DiskFS and sub-volumes. - * We also need a "dirty" flag so things like CloseNufx can know not to - * re-do work when Closing after a Flush. Also DiskFS can alert us to - * any locally cached stuff, and we can tell them to flush everything. - * (Possibly useful when doing disk updates, so stuff can be trivially - * un-done. Currently CiderPress checks the filename manually after - * each write, but that's generally less reliable than having the knowledge - * contained in the DiskImg.) - * - * THOUGHT: need a ReadRawTrack that gets raw nibblized data. For a - * nibblized image it returns the data, for a sector image it generates - * the raw data. - * - * THOUGHT: we could reduce the risk of problems and increase performance - * for physical media with a "copy on write" scheme. We'd create a shadow - * array of modified blocks, and write them at Flush time. This would - * provide an instantaneous "revert" feature, and prevent formats like - * DiskCopy42 (which has a CRC in its header) from being inconsistent for - * long stretches. - */ -class DISKIMG_API DiskImg { -public: - // create DiskImg object - DiskImg(void); - virtual ~DiskImg(void); - - /* - * Types describing an image file. - * - * The file itself is described by an external parameter ("file source") - * that is either the name of the file, a memory buffer, or an EFD - * (EmbeddedFileDescriptor). - */ - typedef enum { // format of the "wrapper wrapper" - kOuterFormatUnknown = 0, - kOuterFormatNone = 1, // (plain) - kOuterFormatCompress = 2, // .xx.Z - kOuterFormatGzip = 3, // .xx.gz - kOuterFormatBzip2 = 4, // .xx.bz2 - kOuterFormatZip = 10, // .zip - } OuterFormat; - typedef enum { // format of the image "wrapper" - kFileFormatUnknown = 0, - kFileFormatUnadorned = 1, // .po, .do, ,nib, .raw, .d13 - kFileFormat2MG = 2, // .2mg, .2img, $e0/0130 - kFileFormatDiskCopy42 = 3, // .dsk/.disk, maybe .dc - kFileFormatDiskCopy60 = 4, // .dc6 (often just raw format) - kFileFormatDavex = 5, // $e0/8004 - kFileFormatSim2eHDV = 6, // .hdv - kFileFormatTrackStar = 7, // .app (40-track or 80-track) - kFileFormatFDI = 8, // .fdi (5.25" or 3.5") - kFileFormatNuFX = 20, // .shk, .sdk, .bxy - kFileFormatDDD = 21, // .ddd - kFileFormatDDDDeluxe = 22, // $DD, .ddd - } FileFormat; - typedef enum { // format of the image data stream - kPhysicalFormatUnknown = 0, - kPhysicalFormatSectors = 1, // sequential 256-byte sectors (13/16/32) - kPhysicalFormatNib525_6656 = 2, // 5.25" disk ".nib" (6656 bytes/track) - kPhysicalFormatNib525_6384 = 3, // 5.25" disk ".nb2" (6384 bytes/track) - kPhysicalFormatNib525_Var = 4, // 5.25" disk (variable len, e.g. ".app") - } PhysicalFormat; - typedef enum { // sector ordering for "sector" format images - kSectorOrderUnknown = 0, - kSectorOrderProDOS = 1, // written as series of ProDOS blocks - kSectorOrderDOS = 2, // written as series of DOS sectors - kSectorOrderCPM = 3, // written as series of 1K CP/M blocks - kSectorOrderPhysical = 4, // written as un-interleaved sectors - kSectorOrderMax, // (used for array sizing) - } SectorOrder; - typedef enum { // main filesystem format (based on NuFX enum) - kFormatUnknown = 0, - kFormatProDOS = 1, - kFormatDOS33 = 2, - kFormatDOS32 = 3, - kFormatPascal = 4, - kFormatMacHFS = 5, - kFormatMacMFS = 6, - kFormatLisa = 7, - kFormatCPM = 8, - //kFormatCharFST - kFormatMSDOS = 10, // any FAT filesystem - //kFormatHighSierra - kFormatISO9660 = 12, - //kFormatAppleShare - kFormatRDOS33 = 20, // 16-sector RDOS disk - kFormatRDOS32 = 21, // 13-sector RDOS disk - kFormatRDOS3 = 22, // 13-sector RDOS disk converted to 16 - // "generic" formats *must* be in their own "decade" - kFormatGenericPhysicalOrd = 30, // unknown, but physical-sector-ordered - kFormatGenericProDOSOrd = 31, // unknown, but ProDOS-block-ordered - kFormatGenericDOSOrd = 32, // unknown, but DOS-sector-ordered - kFormatGenericCPMOrd = 33, // unknown, but CP/M-block-ordered - kFormatUNIDOS = 40, // two 400K DOS 3.3 volumes - kFormatOzDOS = 41, // two 400K DOS 3.3 volumes, weird order - kFormatCFFA4 = 42, // CFFA image with 4 or 6 partitions - kFormatCFFA8 = 43, // CFFA image with 8 partitions - kFormatMacPart = 44, // Macintosh-style partitioned disk - kFormatMicroDrive = 45, // ///SHH Systeme's MicroDrive format - kFormatFocusDrive = 46, // Parsons Engineering FocusDrive format - - // try to keep this in an unsigned char, e.g. for CP clipboard - } FSFormat; - - /* - * Nibble encode/decode description. Use no pointers here, so we - * store as an array and resize at will. - * - * Should we define an enum to describe whether address and data - * headers are standard or some wacky variant? - */ - typedef enum { - kNibbleAddrPrologLen = 3, // d5 aa 96 - kNibbleAddrEpilogLen = 3, // de aa eb - kNibbleDataPrologLen = 3, // d5 aa ad - kNibbleDataEpilogLen = 3, // de aa eb - }; - typedef enum { - kNibbleEncUnknown = 0, - kNibbleEnc44, - kNibbleEnc53, - kNibbleEnc62, - } NibbleEnc; - typedef enum { - kNibbleSpecialNone = 0, - kNibbleSpecialMuse, // doubled sector numbers on tracks > 2 - kNibbleSpecialSkipFirstAddrByte, - } NibbleSpecial; - typedef struct { - char description[32]; - short numSectors; // 13 or 16 (or 18?) - - unsigned char addrProlog[kNibbleAddrPrologLen]; - unsigned char addrEpilog[kNibbleAddrEpilogLen]; - unsigned char addrChecksumSeed; - bool addrVerifyChecksum; - bool addrVerifyTrack; - int addrEpilogVerifyCount; - - unsigned char dataProlog[kNibbleDataPrologLen]; - unsigned char dataEpilog[kNibbleDataEpilogLen]; - unsigned char dataChecksumSeed; - bool dataVerifyChecksum; - int dataEpilogVerifyCount; - - NibbleEnc encoding; - NibbleSpecial special; - } NibbleDescr; - - - static inline bool IsSectorFormat(PhysicalFormat fmt) { - return (fmt == kPhysicalFormatSectors); - } - static inline bool IsNibbleFormat(PhysicalFormat fmt) { - return (fmt == kPhysicalFormatNib525_6656 || - fmt == kPhysicalFormatNib525_6384 || - fmt == kPhysicalFormatNib525_Var); - } - - // file is on disk; stuff like 2MG headers will be identified and stripped - DIError OpenImage(const char* filename, char fssep, bool readOnly); - // file is in memory; provide a pointer to the data start and buffer size - DIError OpenImage(const void* buffer, long length, bool readOnly); - // file is a range of blocks on an open block-oriented disk image - DIError OpenImage(DiskImg* pParent, long firstBlock, long numBlocks); - // file is a range of tracks/sectors on an open sector-oriented disk image - DIError OpenImage(DiskImg* pParent, long firstTrack, long firstSector, - long numSectors); - - // create a new, blank image file - DIError CreateImage(const char* pathName, const char* storageName, - OuterFormat outerFormat, FileFormat fileFormat, - PhysicalFormat physical, const NibbleDescr* pNibbleDescr, - SectorOrder order, FSFormat format, - long numBlocks, bool skipFormat); - DIError CreateImage(const char* pathName, const char* storageName, - OuterFormat outerFormat, FileFormat fileFormat, - PhysicalFormat physical, const NibbleDescr* pNibbleDescr, - SectorOrder order, FSFormat format, - long numTracks, long numSectPerTrack, bool skipFormat); - - // flush any changes to disk; slow recompress only for "kFlushAll" - typedef enum { kFlushUnknown=0, kFlushFastOnly=1, kFlushAll=2 } FlushMode; - DIError FlushImage(FlushMode mode); - // close the image, freeing up any resources in use - DIError CloseImage(void); - // raise/lower refCnt (may want to track pointers someday) - void AddDiskFS(DiskFS* pDiskFS) { fDiskFSRefCnt++; } - void RemoveDiskFS(DiskFS* pDiskFS) { - assert(fDiskFSRefCnt > 0); - fDiskFSRefCnt--; - } - - // (re-)format this image in the specified FS format - DIError FormatImage(FSFormat format, const char* volName); - // reset all blocks/sectors to zeroes - DIError ZeroImage(void); - - // configure for paired sectors (OzDOS) - void SetPairedSectors(bool enable, int idx); - - // identify sector ordering and disk format - // (may want a version that takes "hints" for special disks?) - DIError AnalyzeImage(void); - // figure out what FS and sector ordering is on the disk image - void AnalyzeImageFS(void); - bool ShowAsBlocks(void) const; - // overrule the analyzer (generally not recommended) -- does not - // override FileFormat, which is very reliable - DIError OverrideFormat(PhysicalFormat physical, FSFormat format, - SectorOrder order); - - // Create a DiskFS that matches this DiskImg. Must be called after - // AnalayzeImage, or you will always get a DiskFSUnknown. The DiskFS - // must be freed with "delete" when no longer needed. - DiskFS* OpenAppropriateDiskFS(bool allowUnknown = false); - - // Add information or a warning to the list of notes. Use linefeeds to - // indicate line breaks. This is currently append-only. - typedef enum { kNoteInfo, kNoteWarning } NoteType; - void AddNote(NoteType type, const char* fmt, ...) - #if defined(__GNUC__) - __attribute__ ((format(printf, 3, 4))) - #endif - ; - const char* GetNotes(void) const; - - // simple accessors - OuterFormat GetOuterFormat(void) const { return fOuterFormat; } - FileFormat GetFileFormat(void) const { return fFileFormat; } - PhysicalFormat GetPhysicalFormat(void) const { return fPhysical; } - SectorOrder GetSectorOrder(void) const { return fOrder; } - FSFormat GetFSFormat(void) const { return fFormat; } - long GetNumTracks(void) const { return fNumTracks; } - int GetNumSectPerTrack(void) const { return fNumSectPerTrack; } - long GetNumBlocks(void) const { return fNumBlocks; } - bool GetReadOnly(void) const { return fReadOnly; } - bool GetDirtyFlag(void) const { return fDirty; } - - // set read-only flag; don't use this (open with correct setting; - // this was added as safety hack for the volume copier) - void SetReadOnly(bool val) { fReadOnly = val; } - - // read a 256-byte sector - // NOTE to self: this function should not be available for odd-sized - // volumes, e.g. a ProDOS /RAM or /RAM5 stored with Davex. Need some way - // to communicate that to disk editor so it knows to grey-out the - // selection checkbox and/or not use "show as sectors" as default. - virtual DIError ReadTrackSector(long track, int sector, void* buf) { - return ReadTrackSectorSwapped(track, sector, buf, fOrder, - fFileSysOrder); - } - DIError ReadTrackSectorSwapped(long track, int sector, - void* buf, SectorOrder imageOrder, SectorOrder fsOrder); - // write a 256-byte sector - virtual DIError WriteTrackSector(long track, int sector, const void* buf); - - // read a 512-byte block - virtual DIError ReadBlock(long block, void* buf) { - return ReadBlockSwapped(block, buf, fOrder, fFileSysOrder); - } - DIError ReadBlockSwapped(long block, void* buf, SectorOrder imageOrder, - SectorOrder fsOrder); - // read multiple blocks - virtual DIError ReadBlocks(long startBlock, int numBlocks, void* buf); - // check our virtual bad block map - bool CheckForBadBlocks(long startBlock, int numBlocks); - // write a 512-byte block - virtual DIError WriteBlock(long block, const void* buf); - // write multiple blocks - virtual DIError WriteBlocks(long startBlock, int numBlocks, const void* buf); - - // read an entire nibblized track - virtual DIError ReadNibbleTrack(long track, unsigned char* buf, - long* pTrackLen); - // write a track; trackLen must be <= those in image - virtual DIError WriteNibbleTrack(long track, const unsigned char* buf, - long trackLen); - - // save the current image as a 2MG file - //DIError Write2MG(const char* filename); - - // need to treat the DOS volume number as meta-data for some disks - short GetDOSVolumeNum(void) const { return fDOSVolumeNum; } - void SetDOSVolumeNum(short val) { fDOSVolumeNum = val; } - enum { kVolumeNumNotSet = -1 }; - - // some simple getters - bool GetHasSectors(void) const { return fHasSectors; } - bool GetHasBlocks(void) const { return fHasBlocks; } - bool GetHasNibbles(void) const { return fHasNibbles; } - bool GetIsEmbedded(void) const { return fpParentImg != NULL; } - - // return the current NibbleDescr - const NibbleDescr* GetNibbleDescr(void) const { return fpNibbleDescr; } - // set the NibbleDescr; we do this by copying the entry into our table - // (could improve by doing memcmp on available entries?) - void SetNibbleDescr(int idx); - void SetCustomNibbleDescr(const NibbleDescr* pDescr); - const NibbleDescr* GetNibbleDescrTable(int* pCount) const { - *pCount = fNumNibbleDescrEntries; - return fpNibbleDescrTable; - } - - // set the NuFX compression type, used when compressing or re-compressing; - // must be set before image is opened or created - void SetNuFXCompressionType(int val) { fNuFXCompressType = val; } - - /* - * Set up a progress callback to use when scanning a disk volume. Pass - * nil for "func" to disable. - * - * The callback function is expected to return "true" if all is well. - * If it returns false, kDIErrCancelled will eventually come back. - */ - typedef bool (*ScanProgressCallback)(void* cookie, const char* str, - int count); - void SetScanProgressCallback(ScanProgressCallback func, void* cookie); - /* update status dialog during disk scan; called from DiskFS code */ - bool UpdateScanProgress(const char* newStr); - - /* - * Static utility functions. - */ - // returns "true" if the files on this image have DOS structure, i.e. - // simple file types and high-ASCII text files - static bool UsesDOSFileStructure(FSFormat format) { - return (format == kFormatDOS33 || - format == kFormatDOS32 || - format == kFormatUNIDOS || - format == kFormatOzDOS || - format == kFormatRDOS33 || - format == kFormatRDOS32 || - format == kFormatRDOS3); - } - // returns "true" if we can open files on the specified filesystem - static bool CanOpenFiles(FSFormat format) { - return (format == kFormatProDOS || - format == kFormatDOS33 || - format == kFormatDOS32 || - format == kFormatPascal || - format == kFormatCPM || - format == kFormatRDOS33 || - format == kFormatRDOS32 || - format == kFormatRDOS3); - } - // returns "true" if we can create subdirectories on this filesystem - static bool IsHierarchical(FSFormat format) { - return (format == kFormatProDOS || - format == kFormatMacHFS || - format == kFormatMSDOS); - } - // returns "true" if we can create resource forks on this filesystem - static bool HasResourceForks(FSFormat format) { - return (format == kFormatProDOS || - format == kFormatMacHFS); - } - // returns "true" if the format is one of the "generics" - static bool IsGenericFormat(FSFormat format) { - return (format / 10 == DiskImg::kFormatGenericDOSOrd / 10); - } - - /* this must match DiskImg::kStdNibbleDescrs table */ - typedef enum StdNibbleDescr { - kNibbleDescrDOS33Std = 0, - kNibbleDescrDOS33Patched, - kNibbleDescrDOS33IgnoreChecksum, - kNibbleDescrDOS32Std, - kNibbleDescrDOS32Patched, - kNibbleDescrMuse32, - kNibbleDescrRDOS33, - kNibbleDescrRDOS32, - kNibbleDescrCustom, // slot for custom entry - - kNibbleDescrMAX // must be last - }; - static const NibbleDescr* GetStdNibbleDescr(StdNibbleDescr idx); - // call this once, at DLL initialization time - static void CalcNibbleInvTables(void); - // calculate block number from cyl/head/sect on 3.5" disk - static int CylHeadSect35ToBlock(int cyl, int head, int sect); - // unpack nibble data from a 3.5" disk track - static DIError UnpackNibbleTrack35(const unsigned char* nibbleBuf, - long nibbleLen, unsigned char* outputBuf, int cyl, int head, - LinearBitmap* pBadBlockMap); - // compute the #of sectors per track for cylinder N (0-79) - static int SectorsPerTrack35(int cylinder); - - // get the order in which we test for sector ordering - static void GetSectorOrderArray(SectorOrder* orderArray, SectorOrder first); - - // utility function used by HFS filename normalizer; available to apps - static inline unsigned char MacToASCII(unsigned char uch) { - if (uch < 0x20) - return '?'; - else if (uch < 0x80) - return uch; - else - return kMacHighASCII[uch - 0x80]; - } - - // Allow write access to physical disk 0. This is usually the boot disk, - // but with some BIOS the first IDE drive is always physical 0 even if - // you're booting from SATA. This only has meaning under Win32. - static void SetAllowWritePhys0(bool val); - - /* - * Get string constants for enumerated values. - */ - typedef struct { int format; const char* str; } ToStringLookup; - static const char* ToStringCommon(int format, const ToStringLookup* pTable, - int tableSize); - static const char* ToString(OuterFormat format); - static const char* ToString(FileFormat format); - static const char* ToString(PhysicalFormat format); - static const char* ToString(SectorOrder format); - static const char* ToString(FSFormat format); - -private: - /* - * Fundamental disk image identification. - */ - OuterFormat fOuterFormat; // e.g. gzip - FileFormat fFileFormat; - PhysicalFormat fPhysical; - const NibbleDescr* fpNibbleDescr; // only used for "nibble" images - SectorOrder fOrder; // only used for "sector" images - FSFormat fFormat; - - /* - * This affects how the DiskImg responds to requests for reading - * a track or sector. - * - * "fFileSysOrder", together with with "fOrder", determines how - * sector numbers are translated. It describes the order that the - * DiskFS filesystem expects things to be in. If the image isn't - * sector-addressable, then it is assumed to be in linear block order. - * - * If "fSectorPairing" is set, the DiskImg treats the disk as if - * it were in OzDOS format, with one sector from two different - * volumes in a single 512-byte block. - */ - SectorOrder fFileSysOrder; - bool fSectorPairing; - int fSectorPairOffset; // which image (should be 0 or 1) - - /* - * Internal state. - */ - GenericFD* fpOuterGFD; // outer wrapper, if any (.gz only) - GenericFD* fpWrapperGFD; // Apple II image file - GenericFD* fpDataGFD; // raw Apple II data - OuterWrapper* fpOuterWrapper; // needed for outer .gz wrapper - ImageWrapper* fpImageWrapper; // disk image wrapper (2MG, SHK, etc) - DiskImg* fpParentImg; // set for embedded volumes - short fDOSVolumeNum; // specified by some wrapper formats - di_off_t fOuterLength; // total len of file - di_off_t fWrappedLength; // len of file after Outer wrapper removed - di_off_t fLength; // len of disk image (w/o wrappers) - bool fExpandable; // ProDOS .hdv can expand - bool fReadOnly; // allow writes to this image? - bool fDirty; // have we modified this image? - //bool fIsEmbedded; // is this image embedded in another? - - bool fHasSectors; // image is sector-addressable - bool fHasBlocks; // image is block-addressable - bool fHasNibbles; // image is nibble-addressable - - long fNumTracks; // for sector-addressable images - int fNumSectPerTrack; // (ditto) - long fNumBlocks; // for 512-byte block-addressable images - - unsigned char* fNibbleTrackBuf; // allocated on heap - int fNibbleTrackLoaded; // track currently in buffer - - int fNuFXCompressType; // used when compressing a NuFX image - - char* fNotes; // warnings and FYIs about DiskImg/DiskFS - - LinearBitmap* fpBadBlockMap; // used for 3.5" nibble images - - int fDiskFSRefCnt; // #of DiskFS objects pointing at us - - /* - * NibbleDescr entries. There are several standard ones, and we want - * to allow applications to define additional ones. - */ - NibbleDescr* fpNibbleDescrTable; - int fNumNibbleDescrEntries; - - /* static table of default values */ - static const NibbleDescr kStdNibbleDescrs[]; - - DIError CreateImageCommon(const char* pathName, const char* storageName, - bool skipFormat); - DIError ValidateCreateFormat(void) const; - DIError FormatSectors(GenericFD* pGFD, bool quickFormat) const; - //DIError FormatBlocks(GenericFD* pGFD) const; - - DIError CopyBytesOut(void* buf, di_off_t offset, int size) const; - DIError CopyBytesIn(const void* buf, di_off_t offset, int size); - DIError AnalyzeImageFile(const char* pathName, char fssep); - // Figure out the sector ordering for this filesystem, so we can decide - // how the sectors need to be re-arranged when we're reading them. - SectorOrder CalcFSSectorOrder(void) const; - // Handle sector order calculations. - DIError CalcSectorAndOffset(long track, int sector, SectorOrder ImageOrder, - SectorOrder fsOrder, di_off_t* pOffset, int* pNewSector); - inline bool IsLinearBlocks(SectorOrder imageOrder, SectorOrder fsOrder); - - /* - * Progress update during the filesystem scan. This only exists in the - * topmost DiskImg. (This is arguably more appropriate in DiskFS, but - * DiskFS objects don't have a notion of "parent" and are somewhat more - * ephemeral.) - */ - ScanProgressCallback fpScanProgressCallback; - void* fScanProgressCookie; - int fScanCount; - char fScanMsg[128]; - time_t fScanLastMsgWhen; - - /* - * 5.25" nibble image access. - */ - enum { - kDataSize62 = 343, // 342 bytes + checksum byte - kChunkSize62 = 86, // (0x56) - - kDataSize53 = 411, // 410 bytes + checksum byte - kChunkSize53 = 51, // (0x33) - kThreeSize = (kChunkSize53 * 3) + 1, // same as 410 - 256 - }; - DIError ReadNibbleSector(long track, int sector, void* buf, - const NibbleDescr* pNibbleDescr); - DIError WriteNibbleSector(long track, int sector, const void* buf, - const NibbleDescr* pNibbleDescr); - void DumpNibbleDescr(const NibbleDescr* pNibDescr) const; - int GetNibbleTrackLength(long track) const; - int GetNibbleTrackOffset(long track) const; - int GetNibbleTrackFormatLength(void) const { - /* return length to use when formatting for 16 sectors */ - if (fPhysical == kPhysicalFormatNib525_6656) - return kTrackLenNib525; - else if (fPhysical == kPhysicalFormatNib525_6384) - return kTrackLenNb2525; - else if (fPhysical == kPhysicalFormatNib525_Var) { - if (fFileFormat == kFileFormatTrackStar || - fFileFormat == kFileFormatFDI) - { - return kTrackLenNb2525; // use minimum possible - } - } - assert(false); - return -1; - } - int GetNibbleTrackAllocLength(void) const { - /* return length to allocate when creating an image */ - if (fPhysical == kPhysicalFormatNib525_Var && - (fFileFormat == kFileFormatTrackStar || - fFileFormat == kFileFormatFDI)) - { - // use maximum possible - return kTrackLenTrackStar525; - } - return GetNibbleTrackFormatLength(); - } - DIError LoadNibbleTrack(long track, long* pTrackLen); - DIError SaveNibbleTrack(void); - int FindNibbleSectorStart(const CircularBufferAccess& buffer, - int track, int sector, const NibbleDescr* pNibbleDescr, int* pVol); - void DecodeAddr(const CircularBufferAccess& buffer, int offset, - short* pVol, short* pTrack, short* pSector, short* pChksum); - inline unsigned int ConvFrom44(unsigned char val1, unsigned char val2) { - return ((val1 << 1) | 0x01) & val2; - } - DIError DecodeNibbleData(const CircularBufferAccess& buffer, int idx, - unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); - void EncodeNibbleData(const CircularBufferAccess& buffer, int idx, - const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; - DIError DecodeNibble62(const CircularBufferAccess& buffer, int idx, - unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); - void EncodeNibble62(const CircularBufferAccess& buffer, int idx, - const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; - DIError DecodeNibble53(const CircularBufferAccess& buffer, int idx, - unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); - void EncodeNibble53(const CircularBufferAccess& buffer, int idx, - const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; - int TestNibbleTrack(int track, const NibbleDescr* pNibbleDescr, int* pVol); - DIError AnalyzeNibbleData(void); - inline unsigned char Conv44(unsigned short val, bool first) const { - if (first) - return (val >> 1) | 0xaa; - else - return val | 0xaa; - } - DIError FormatNibbles(GenericFD* pGFD) const; - - static const unsigned char kMacHighASCII[]; - - /* - * 3.5" nibble access - */ - static int FindNextSector35(const CircularBufferAccess& buffer, int start, - int cyl, int head, int* pSector); - static bool DecodeNibbleSector35(const CircularBufferAccess& buffer, - int start, unsigned char* sectorBuf, unsigned char* readChecksum, - unsigned char* calcChecksum); - static bool UnpackChecksum35(const CircularBufferAccess& buffer, - int offset, unsigned char* checksumBuf); - static void EncodeNibbleSector35(const unsigned char* sectorData, - unsigned char* outBuf); - - /* static data tables */ - static unsigned char kDiskBytes53[32]; - static unsigned char kDiskBytes62[64]; - static unsigned char kInvDiskBytes53[256]; - static unsigned char kInvDiskBytes62[256]; - enum { kInvInvalidValue = 0xff }; - -private: // some C++ stuff to block behavior we don't support - DiskImg& operator=(const DiskImg&); - DiskImg(const DiskImg&); -}; - - - -/* - * Disk filesystem class, roughly equivalent to a GS/OS FST. This is an - * abstract base class. - * - * Static functions know how to access a DiskImg and figure out what kind - * of image we have. Once known, the appropriate sub-class can be - * instantiated. - * - * We maintain a linear list of files to make it easy for applications to - * traverse the full set of files. Sometimes this makes it hard for us to - * update internally (especially HFS). With some minor cleverness it - * should be possible to switch to a tree structure while retaining the - * linear "get next" API. This would be a big help for ProDOS and HFS. - * - * NEED: some notification mechanism for changes to files and/or block - * editing of the disk (especially with regard to open sub-volumes). If - * a disk volume open for file-by-file viewing is modified with the disk - * editor, we should close the file when the disk editor exits. - * - * NEED? disk utilities, such as "disk crunch", for Pascal volumes. Could - * be written externally, but might as well keep fs knowledge embedded. - * - * MISSING: there is no way to override the image analyzer when working - * with sub-volumes. Actually, there is; it just has to happen *after* - * the DiskFS has been created. We should provide an approach that either - * stifles the DiskFS creation, or allows us to override and replace the - * internal DiskFS so we can pop up a sub-volume list, show what we *think* - * is there, and then let the user pick a volume and pick overrides (mainly - * for use in the disk editor). Not sure if we want the changes to "stick"; - * we probably do. Q: does the "scan for sub-volumes" attribute propagate - * recursively to each sub-sub-volume? Probably. - * - * NOTE to self: should make "test" functions more lax when called - * from here, on the assumption that the caller is knowledgeable. Perhaps - * an independent "strictness" variable that can be cranked down through - * multiple calls to AnalyzeImage?? - */ -class DISKIMG_API DiskFS { -public: - /* - * Information about volume usage. - * - * Each "chunk" is a track/sector on a DOS disk or a block on a ProDOS - * or Pascal disk. CP/M really ought to use 1K blocks, but for - * convenience we're just using 512-byte blocks (it's up to the CP/M - * code to set two "chunks" per block). - * - * NOTE: the current DOS/ProDOS/Pascal code is sloppy when it comes to - * keeping this structure up to date. HFS doesn't use it at all. This - * has always been a low-priority feature. - */ - class DISKIMG_API VolumeUsage { - public: - VolumeUsage(void) { - fByBlocks = false; - fTotalChunks = -1; - fNumSectors = -1; - //fFreeChunks = -1; - fList = NULL; - fListSize = -1; - } - ~VolumeUsage(void) { - delete[] fList; - } - - /* - * These values MUST fit in five bits. - * - * Suggested disk map colors: - * 0 = unknown (color-key pink) - * 1 = conflict (medium-red) - * 2 = boot loader (dark-blue) - * 3 = volume dir (light-blue) - * 4 = subdir (medium-blue) - * 5 = user data (medium-green) - * 6 = user index blocks (light-green) - * 7 = embedded filesys (yellow) - * - * THOUGHT: Add flag for I/O error (nibble images) -- requires - * automatic disk verify pass. (Or maybe could be done manually - * on request?) - * - * unused --> black - * marked-used-but-not-used --> dark-red - * used-but-not-marked-used --> orange - */ - typedef enum { - kChunkPurposeUnknown = 0, - kChunkPurposeConflict = 1, // two or more different things - kChunkPurposeSystem = 2, // boot blocks, volume bitmap - kChunkPurposeVolumeDir = 3, // volume dir (or only dir) - kChunkPurposeSubdir = 4, // ProDOS sub-directory - kChunkPurposeUserData = 5, // file on this filesystem - kChunkPurposeFileStruct = 6, // index blocks, T/S lists - kChunkPurposeEmbedded = 7, // embedded filesystem - // how about: outside range claimed by disk, e.g. fTotalBlocks on - // 800K ProDOS disk in a 32MB CFFA volume? - } ChunkPurpose; - - typedef struct ChunkState { - bool isUsed; - bool isMarkedUsed; - ChunkPurpose purpose; // only valid if isUsed is set - } ChunkState; - - // initialize, configuring for either blocks or sectors - DIError Create(long numBlocks); - DIError Create(long numTracks, long numSectors); - bool GetInitialized(void) const { return (fList != NULL); } - - // return the number of chunks on this disk - long GetNumChunks(void) const { return fTotalChunks; } - - // return the number of unallocated chunks, taking into account - // both the free-chunk bitmap (if any) and the actual usage - long GetActualFreeChunks(void) const; - - // return the state of the specified chunk - DIError GetChunkState(long block, ChunkState* pState) const; - DIError GetChunkState(long track, long sector, - ChunkState* pState) const; - - // set the state of a particular chunk (should only be done by - // the DiskFS sub-classes) - DIError SetChunkState(long block, const ChunkState* pState); - DIError SetChunkState(long track, long sector, - const ChunkState* pState); - - void Dump(void) const; // debugging - - private: - DIError GetChunkStateIdx(int idx, ChunkState* pState) const; - DIError SetChunkStateIdx(int idx, const ChunkState* pState); - inline char StateToChar(ChunkState* pState) const; - - /* - * Chunk state is stored as a set of bits in one byte: - * - * 0-4: how is block used (only has meaning if bit 6 is set) - * 5: for nibble images, indicates the block or sector is unreadable - * 6: is block used by something (0=no, 1=yes) - * 7: is block marked "in use" by system map (0=no, 1=yes) - * - * [ Consider reducing "purpose" to 0-3 and adding bad block bit for - * nibble images and physical media.] - */ - enum { - kChunkPurposeMask = 0x1f, // ChunkPurpose enum - kChunkDamagedFlag = 0x20, - kChunkUsedFlag = 0x40, - kChunkMarkedUsedFlag = 0x80, - }; - - bool fByBlocks; - long fTotalChunks; - long fNumSectors; // only valid if !fByBlocks - //long fFreeChunks; - unsigned char* fList; - int fListSize; - }; // end of VolumeUsage class - - /* - * List of sub-volumes. The SubVolume owns the DiskImg and DiskFS - * that are handed to it, so they can be deleted when the SubVolume - * is deleted as part of destroying the parent. - */ - class SubVolume { - public: - SubVolume(void) : fpDiskImg(NULL), fpDiskFS(NULL), - fpPrev(NULL), fpNext(NULL) {} - ~SubVolume(void) { - delete fpDiskFS; // must close first; may need flush to DiskImg - delete fpDiskImg; - } - - void Create(DiskImg* pDiskImg, DiskFS* pDiskFS) { - assert(pDiskImg != NULL); - assert(pDiskFS != NULL); - fpDiskImg = pDiskImg; - fpDiskFS = pDiskFS; - } - - DiskImg* GetDiskImg(void) const { return fpDiskImg; } - DiskFS* GetDiskFS(void) const { return fpDiskFS; } - - SubVolume* GetPrev(void) const { return fpPrev; } - void SetPrev(SubVolume* pSubVol) { fpPrev = pSubVol; } - SubVolume* GetNext(void) const { return fpNext; } - void SetNext(SubVolume* pSubVol) { fpNext = pSubVol; } - - private: - DiskImg* fpDiskImg; - DiskFS* fpDiskFS; - - SubVolume* fpPrev; - SubVolume* fpNext; - }; // end of SubVolume class - - - - /* - * Start of DiskFS declarations. - */ -public: - typedef enum SubScanMode { - kScanSubUnknown = 0, - kScanSubDisabled, - kScanSubEnabled, - kScanSubContainerOnly, - } SubScanMode; - - - DiskFS(void) { - fpA2Head = fpA2Tail = NULL; - fpSubVolumeHead = fpSubVolumeTail = NULL; - fpImg = NULL; - fScanForSubVolumes = kScanSubDisabled; - - fParmTable[kParm_CreateUnique] = 0; - fParmTable[kParmProDOS_AllowLowerCase] = 1; - fParmTable[kParmProDOS_AllocSparse] = 1; - } - virtual ~DiskFS(void) { - DeleteSubVolumeList(); - DeleteFileList(); - SetDiskImg(NULL); - } - - /* - * Static FSFormat-analysis functions, called by the DiskImg AnalyzeImage - * function. Capable of determining with a high degree of accuracy - * what format the disk is in, yet remaining flexible enough to - * function correctly with variations (like DOS3.3 disks with - * truncated TOCs and ProDOS images from hard drives). - * - * The "pOrder" and "pFormat" arguments are in/out. Set them to the - * appropriate "unknown" enum value on entry. If something is known - * of the order or format, put that in instead; in some cases this will - * bias the proceedings, which is useful for efficiency and for making - * overrides work correctly. - * - * On success, these return kDIErrNone and set "*pOrder". On failure, - * they return nonzero and leave "*pOrder" unmodified. - * - * Each DiskFS sub-class should declare a TestFS function. It's not - * virtual void here because, since it's called before the DiskFS is - * created, it must be static. - */ - typedef enum FSLeniency { kLeniencyNot, kLeniencyVery } FSLeniency; - //static DIError TestFS(const DiskImg* pImg, DiskImg::SectorOrder* pOrder, - // DiskImg::FSFormat* pFormat, FSLeniency leniency); - - /* - * Load the disk contents and (if enabled) scan for sub-volumes. - * - * If "headerOnly" is set, we just do a quick scan of the volume header - * to get basic information. The deep file scan is skipped (but can - * be done later). (Sub-classes can choose to ignore the flag and - * always do the full scan; this is an optimization.) Guaranteed to - * set the volume name and volume block/sector count. - * - * If a progress callback is set up, this can return with a "cancelled" - * result, which should not be treated as a failure. - */ - typedef enum { kInitUnknown = 0, kInitHeaderOnly, kInitFull } InitMode; - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) = 0; - - /* - * Format the disk with the appropriate filesystem, creating all filesystem - * structures and (when appropriate) boot blocks. - */ - virtual DIError Format(DiskImg* pDiskImg, const char* volName) - { return kDIErrNotSupported; } - - /* - * Pass in a full path to normalize, and a buffer to copy the output - * into. On entry "pNormalizedBufLen" holds the length of the buffer. - * On exit, it holds the size of the buffer required to hold the - * normalized string. If the buffer is nil or isn't big enough, no part - * of the normalized path will be copied into the buffer, and a specific - * error (kDIErrDataOverrun) will be returned. - */ - virtual DIError NormalizePath(const char* path, char fssep, - char* normalizedBuf, int* pNormalizedBufLen) - { return kDIErrNotSupported; } - - - /* - * Create a file. The CreateParms struct specifies the full set of file - * details. To remain FS-agnostic, use the NufxLib constants - * (kNuStorageDirectory, kNuAccessUnlocked, etc). They match up with - * their ProDOS equivalents, and I promise to make them work right. - * - * On success, the file exists as a fully-formed, zero-length file. A - * pointer to the relevant A2File structure is returned. - */ - enum { - /* valid values for CreateParms; must match ProDOS enum */ - kStorageSeedling = 1, - kStorageExtended = 5, - kStorageDirectory = 13, - }; - typedef struct CreateParms { - const char* pathName; // full pathname - char fssep; - int storageType; // determines normal, subdir, or forked - long fileType; - long auxType; - int access; - time_t createWhen; - time_t modWhen; - } CreateParms; - virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile) - { return kDIErrNotSupported; } - - /* - * Delete a file from the disk. - */ - virtual DIError DeleteFile(A2File* pFile) - { return kDIErrNotSupported; } - - /* - * Rename a file. - */ - virtual DIError RenameFile(A2File* pFile, const char* newName) - { return kDIErrNotSupported; } - - /* - * Alter file attributes. - */ - virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, - long accessFlags) - { return kDIErrNotSupported; } - - /* - * Rename a volume. Also works for changing the disk volume number. - */ - virtual DIError RenameVolume(const char* newName) - { return kDIErrNotSupported; } - - - // Accessor - DiskImg* GetDiskImg(void) const { return fpImg; } - - // Test file and volume names (and volume numbers) - // [these need to be static functions for some things... hmm] - //virtual bool IsValidFileName(const char* name) const { return false; } - //virtual bool IsValidVolumeName(const char* name) const { return false; } - - // Return the disk volume name, or NULL if there isn't one. - virtual const char* GetVolumeName(void) const = 0; - - // Return a printable string identifying the FS type and volume - virtual const char* GetVolumeID(void) const = 0; - - // Return the "bare" volume name. For formats where the volume name - // is actually a number (e.g. DOS 3.3), this returns just the number. - // For formats without a volume name or number (e.g. CP/M), this returns - // nil, indicating that any attempt to change the volume name will fail. - virtual const char* GetBareVolumeName(void) const = 0; - - // Returns "false" if we only support read-only access to this FS type - virtual bool GetReadWriteSupported(void) const = 0; - - // Returns "true" if the filesystem shows evidence of damage. - virtual bool GetFSDamaged(void) const = 0; - - // Returns number of blocks recognized by the filesystem, or -1 if the - // FS isn't block-oriented (e.g. DOS 3.2/3.3) - virtual long GetFSNumBlocks(void) const { return -1; } - - // Get the next file in the list. Start by passing in NULL to get the - // head of the list. Returns NULL when the end of the list is reached. - A2File* GetNextFile(A2File* pCurrent) const; - - // Get a count of the files and directories on this disk. - long GetFileCount(void) const; - - /* - * Find a file by case-insensitive pathname. Assumes fssep=':'. The - * compare function can be overridden for systems like HFS, where "case - * insensitive" has a different meaning because of the native - * character set. - * - * The A2File* returned should not be deleted. - */ - typedef int (*StringCompareFunc)(const char* str1, const char* str2); - A2File* GetFileByName(const char* pathName, StringCompareFunc func = NULL); - - // This controls scanning for sub-volumes; must be set before Initialize(). - SubScanMode GetScanForSubVolumes(void) const { return fScanForSubVolumes; } - void SetScanForSubVolumes(SubScanMode val) { fScanForSubVolumes = val; } - - // some constants for non-ProDOS filesystems to use - enum { kFileAccessLocked = 0x01, kFileAccessUnlocked = 0xc3 }; - - - /* - * We use this as a filename separator character (i.e. between pathname - * components) in all filenames. It's useful to standardize on this - * so that behavior is consistent across all disk and archive formats. - * - * The choice of ':' is good because it's invalid in filenames on - * Windows, Mac OS, GS/OS, and pretty much anywhere else we could be - * running except for UNIX. It's valid under DOS 3.3, but since you - * can't have subdirectories under DOS there's no risk of confusion. - */ - enum { kDIFssep = ':' }; - - - /* - * Return the volume use map. This is a non-const function because - * it might need to do a "just-in-time" usage map update. It returns - * const to keep non-DiskFS classes from altering the map. - */ - const VolumeUsage* GetVolumeUsageMap(void) { - if (fVolumeUsage.GetInitialized()) - return &fVolumeUsage; - else - return NULL; - } - - /* - * Return the total space and free space, in either blocks or sectors - * as appropriate. "*pUnitSize" will be kBlockSize or kSectorSize. - */ - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const = 0; - - - /* - * Get the next volume in the list. Start by passing in NULL to get the - * head of the list. Returns NULL when the end of the list is reached. - */ - SubVolume* GetNextSubVolume(const SubVolume* pCurrent) const; - - /* - * Set some parameters to tell the DiskFS how to operate. Some of - * these are FS-specific, some may be general. - * - * Parameters are set in the current object and all sub-volume objects. - * - * The enum is part of the interface and must be rigidly defined, but - * it is also used to size an array so it can't be too sparse. - */ - typedef enum DiskFSParameter { - kParmUnknown = 0, - - kParm_CreateUnique = 1, // make new filenames unique - - kParmProDOS_AllowLowerCase = 10, // allow lower case and spaces - kParmProDOS_AllocSparse = 11, // don't store empty blocks - - kParmMax // must be last entry - } DiskFSParameter; - long GetParameter(DiskFSParameter parm); - void SetParameter(DiskFSParameter parm, long val); - - /* - * Flush changed data. - * - * The individual filesystems shouldn't generally do any caching; if - * they do, we would want a virtual "FlushFS()" that gets called by - * Flush. The better answer is to cache in DiskImg, which works for - * everything, and knows if the underlying storage is already in RAM. - * - * For the most part this just needs to recursively flush the DiskImg - * objects in all of the sub-volumes and then the current volume. This - * is a no-op in most cases, but if the archive is compressed this will - * cause a new, compressed archive to be created. - * - * The "mode" value determines whether or not we do "heavy" flushes. It's - * very handy to be able to do "slow" flushes for anything that is being - * written directly to disk (as opposed to being run through Deflate), - * so that the UI doesn't indicate that they're partially written when - * in fact they're fully written. - */ - DIError Flush(DiskImg::FlushMode mode); - - /* - * Set the read-only flag on our DiskImg and those of our subvolumes. - * Used to ensure that a DiskFS with un-flushed data can be deleted - * without corrupting the volume. - */ - void SetAllReadOnly(bool val); - - // debug dump - void DumpFileList(void); - -protected: - /* - * Set the DiskImg pointer. Updates the reference count in DiskImg. - */ - void SetDiskImg(DiskImg* pImg); - - // once added, we own the pDiskImg and the pDiskFS (DO NOT pass the - // same DiskImg or DiskFS in more than once!). Note this copies the - // fParmTable and other stuff (fScanForSubVolumes) from parent to child. - void AddSubVolumeToList(DiskImg* pDiskImg, DiskFS* pDiskFS); - // add files to fpA2Head/fpA2Tail - void AddFileToList(A2File* pFile); - // only need for hierarchical filesystems; insert file after pPrev - void InsertFileInList(A2File* pFile, A2File* pPrev); - // delete an entry - void DeleteFileFromList(A2File* pFile); - - // scan for damaged or suspicious files - void ScanForDamagedFiles(bool* pDamaged, bool* pSuspicious); - - // pointer to the DiskImg structure underlying this filesystem - DiskImg* fpImg; - - VolumeUsage fVolumeUsage; - SubScanMode fScanForSubVolumes; - - -private: - A2File* SkipSubdir(A2File* pSubdir); - void CopyInheritables(DiskFS* pNewFS); - void DeleteFileList(void); - void DeleteSubVolumeList(void); - - long fParmTable[kParmMax]; // for DiskFSParameter - - A2File* fpA2Head; - A2File* fpA2Tail; - SubVolume* fpSubVolumeHead; - SubVolume* fpSubVolumeTail; - -private: - DiskFS& operator=(const DiskFS&); - DiskFS(const DiskFS&); -}; - - -/* - * Apple II file class, representing a file on an Apple II volume. This is an - * abstract base class. - * - * There is a different sub-class for each filesystem type. The A2File object - * encapsulates all of the knowledge required to read a file from a disk - * image. - * - * The prev/next pointers, used to maintain a linked list of files, are only - * accessible from DiskFS functions. At some point we may want to rearrange - * the way this is handled, e.g. by not maintaining a list at all, so it's - * important that everything go through DiskFS requests. - * - * The FSFormat is made an explicit member, because sub-classes may not - * understand exactly where the file came from (e.g. was it DOS3.2 or - * DOS 3.3). Somebody might care. - * - * - * NOTE TO SELF: open files need to be generalized. Right now the internal - * implementations only allow a single open, which is okay for our purposes - * but bad for a general FS implementation. As it stands, you can't even - * open both forks at the same time on ProDOS/HFS. - * - * UMMM: The handling of "damaged" files could be more consistent. - */ -class DISKIMG_API A2File { -public: - friend class DiskFS; - - A2File(DiskFS* pDiskFS) : fpDiskFS(pDiskFS) { - fpPrev = fpNext = NULL; - fFileQuality = kQualityGood; - } - virtual ~A2File(void) {} - - /* - * All Apple II files have certain characteristics, of which ProDOS - * is roughly a superset. (Yes, you can have HFS on a IIgs, but - * all that fancy creator type stuff is decidedly Mac-centric. Still, - * we want to assume 4-byte file and aux types.) - * - * NEED: something distinguishing between files and disk images? - * - * NOTE: there is no guarantee that GetPathName will return unique values - * (duplicates are possible). We don't guarantee that you won't get an - * empty string back (it's valid to have an empty filename in the dir in - * DOS 3.3, and it's possible for other filesystems to be damaged). The - * pathname may receive some minor sanitizing, e.g. removal or conversion - * of high ASCII and control characters, but some filesystems (like HFS) - * make use of them. - * - * We do guarantee that the contents of subdirectories are grouped - * together. This makes it much easier to construct a hierarchy out of - * the linear list. This becomes important when dynamically adding - * files to a disk. - */ - virtual const char* GetFileName(void) const = 0; // name of this file - virtual const char* GetPathName(void) const = 0; // full path - virtual char GetFssep(void) const = 0; // '\0' if none - virtual long GetFileType(void) const = 0; - virtual long GetAuxType(void) const = 0; - virtual long GetAccess(void) const = 0; // ProDOS-style perms - virtual time_t GetCreateWhen(void) const = 0; - virtual time_t GetModWhen(void) const = 0; - virtual di_off_t GetDataLength(void) const = 0; // len of data fork - virtual di_off_t GetDataSparseLength(void) const = 0; // len w/o sparse areas - virtual di_off_t GetRsrcLength(void) const = 0; // len or -1 if no rsrc - virtual di_off_t GetRsrcSparseLength(void) const = 0; // len or -1 if no rsrc - virtual DiskImg::FSFormat GetFSFormat(void) const { - return fpDiskFS->GetDiskImg()->GetFSFormat(); - } - virtual bool IsDirectory(void) const { return false; } - virtual bool IsVolumeDirectory(void) const { return false; } - - /* - * Open a file. Treat the A2FileDescr like an fd. - */ - virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, - bool rsrcFork = false) = 0; - - /* - * This is called by the A2FileDescr object when somebody invokes Close(). - * The A2File object should remove the A2FileDescr from its list of open - * files and delete the storage. The implementation must not call the - * A2FileDescr's Close function, since that would cause a recursive loop. - * - * This should not be called by an application. - */ - virtual void CloseDescr(A2FileDescr* pOpenFile) = 0; - - /* - * This is only useful for hierarchical filesystems like ProDOS, - * where the order of items in the linear list is significant. It - * allows an unambiguous determination of which subdir a file resides - * in, even if somebody has sector-edited the filesystem so that two - * subdirs have the same name. (It's also a bit speedier to compare - * than pathname substrings would be.) - */ - virtual A2File* GetParent(void) const { return NULL; } - - /* - * Returns "true" if either fork of the file is open, "false" if not. - */ - virtual bool IsFileOpen(void) const = 0; - - virtual void Dump(void) const = 0; // debugging - - typedef enum FileQuality { - kQualityUnknown = 0, - kQualityGood, - kQualitySuspicious, - kQualityDamaged, - } FileQuality; - virtual FileQuality GetQuality(void) const { return fFileQuality; } - virtual void SetQuality(FileQuality quality); - virtual void ResetQuality(void); - - DiskFS* GetDiskFS(void) const { return fpDiskFS; } - -protected: - DiskFS* fpDiskFS; - virtual void SetParent(A2File* pParent) { /* do nothing */ } - -private: - A2File* GetPrev(void) const { return fpPrev; } - void SetPrev(A2File* pFile) { fpPrev = pFile; } - A2File* GetNext(void) const { return fpNext; } - void SetNext(A2File* pFile) { fpNext = pFile; } - - // Set when file structure is damaged and application should not try - // to open the file. - FileQuality fFileQuality; - - A2File* fpPrev; - A2File* fpNext; - - -private: - A2File& operator=(const A2File&); - A2File(const A2File&); -}; - - -/* - * Abstract representation of an open file. This contains all active state - * and all information required to read and write a file. - * - * Do not delete these objects; instead, invoke the Close method, so that they - * can be removed from the parents' list of open files. - * TODO: consider making the destructor "protected" - */ -class DISKIMG_API A2FileDescr { -public: - A2FileDescr(A2File* pFile) : fpFile(pFile), fProgressUpdateFunc(NULL) {} - virtual ~A2FileDescr(void) { fpFile = NULL; /*paranoia*/ } - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL) = 0; - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL) = 0; - virtual DIError Seek(di_off_t offset, DIWhence whence) = 0; - virtual di_off_t Tell(void) = 0; - virtual DIError Close(void) = 0; - - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) - const = 0; - virtual DIError GetStorage(long blockIdx, long* pBlock) - const = 0; - virtual long GetSectorCount(void) const = 0; - virtual long GetBlockCount(void) const = 0; - - virtual DIError Rewind(void) { return Seek(0, kSeekSet); } - - A2File* GetFile(void) const { return fpFile; } - - /* - * Progress update callback mechanism. Pass in the length or (for writes) - * expected length of the file. This invokes the callback with the - * lengths and some pointers. - * - * If the progress callback returns "true", progress continues. If it - * returns "false", the read or write function will return with - * kDIErrCancelled. - */ - typedef bool (*ProgressUpdater)(A2FileDescr* pFile, di_off_t max, - di_off_t current, void* vState); - void SetProgressUpdater(ProgressUpdater func, di_off_t max, void* state) { - fProgressUpdateFunc = func; - fProgressUpdateMax = max; - fProgressUpdateState = state; - } - void ClearProgressUpdater(void) { - fProgressUpdateFunc = NULL; - } - -protected: - A2File* fpFile; - - /* - * Internal utility functions for mapping blocks to sectors and vice-versa. - */ - virtual void TrackSectorToBlock(long track, long sector, long* pBlock, - bool* pSecondHalf) const - { - int numSectPerTrack = fpFile->GetDiskFS()->GetDiskImg()->GetNumSectPerTrack(); - assert(track < fpFile->GetDiskFS()->GetDiskImg()->GetNumTracks()); - assert(sector < numSectPerTrack); - long dblBlock = track * numSectPerTrack + sector; - *pBlock = dblBlock / 2; - *pSecondHalf = (dblBlock & 0x01) != 0; - } - virtual void BlockToTrackSector(long block, bool secondHalf, long* pTrack, - long* pSector) const - { - assert(block < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); - int numSectPerTrack = fpFile->GetDiskFS()->GetDiskImg()->GetNumSectPerTrack(); - int dblBlock = block * 2; - if (secondHalf) - dblBlock++; - *pTrack = dblBlock / numSectPerTrack; - *pSector = dblBlock % numSectPerTrack; - } - - /* - * Call this from FS-specific read/write functions on successful - * completion (and perhaps more often for filesystems with potentially - * large files, e.g. ProDOS/HFS). - * - * Test the return value; if "false", user wishes to cancel the op, and - * long read or write calls should return immediately. - */ - inline bool UpdateProgress(di_off_t current) { - if (fProgressUpdateFunc != NULL) { - return (*fProgressUpdateFunc)(this, fProgressUpdateMax, current, - fProgressUpdateState); - } else { - return true; - } - } - -private: - A2FileDescr& operator=(const A2FileDescr&); - A2FileDescr(const A2FileDescr&); - - /* storage for progress update goodies */ - ProgressUpdater fProgressUpdateFunc; - di_off_t fProgressUpdateMax; - void* fProgressUpdateState; -}; - -}; // namespace DiskImgLib - -#endif /*__DISK_IMG__*/ +/* + * CiderPress + * Copyright (C) 2009 by CiderPress authors. All Rights Reserved. + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Public declarations for the DiskImg library. + * + * Everything is wrapped in the "DiskImgLib" namespace. Either prefix + * all references with "DiskImgLib::", or add "using namespace DiskImgLib" + * to all C++ source files that make use of it. + * + * Under Linux, this should be compiled with -D_FILE_OFFSET_BITS=64. + * + * These classes are not thread-safe with respect to access to a single + * disk image. Writing to the same disk image from multiple threads + * simultaneously is bound to end in disaster. Simultaneous access to + * different objects will work, though modifying the same disk image + * file from multiple objects will lead to unpredictable results. + */ +#ifndef __DISK_IMG__ +#define __DISK_IMG__ + +#include +#include +#include + +//#define EXCISE_GPL_CODE + +/* Windows DLL stuff */ +#ifdef _WIN32 +# ifdef DISKIMG_EXPORTS +# define DISKIMG_API __declspec(dllexport) +# else +# define DISKIMG_API __declspec(dllimport) +# endif +#else +# define DISKIMG_API +#endif + +namespace DiskImgLib { + +/* compiled-against versions; call DiskImg::GetVersion for linked-against */ +#define kDiskImgVersionMajor 4 +#define kDiskImgVersionMinor 6 +#define kDiskImgVersionBug 0 + + +/* + * Errors from the various disk image classes. + */ +typedef enum DIError { + kDIErrNone = 0, + + /* I/O request errors (should renumber/rename to match GS/OS errors?) */ + kDIErrAccessDenied = -10, + kDIErrVWAccessForbidden = -11, // write access to volume forbidden + kDIErrSharingViolation = -12, // file is in use and not shareable + kDIErrNoExclusiveAccess = -13, // couldn't get exclusive access + kDIErrWriteProtected = -14, // disk is write protected + kDIErrCDROMNotSupported = -15, // access to CD-ROM drives not supptd + kDIErrASPIFailure = -16, // generic ASPI failure result + kDIErrSPTIFailure = -17, // generic SPTI failure result + kDIErrSCSIFailure = -18, // generic SCSI failure result + kDIErrDeviceNotReady = -19, // floppy or CD-ROM drive has no media + + kDIErrFileNotFound = -20, + kDIErrForkNotFound = -21, // requested fork does not exist + kDIErrAlreadyOpen = -22, // already open, can't open a 2nd time + kDIErrFileOpen = -23, // file is open, can't delete it + kDIErrNotReady = -24, + kDIErrFileExists = -25, // file already exists + kDIErrDirectoryExists = -26, // directory already exists + + kDIErrEOF = -30, // end-of-file reached + kDIErrReadFailed = -31, + kDIErrWriteFailed = -32, + kDIErrDataUnderrun = -33, // tried to read off end of the image + kDIErrDataOverrun = -34, // tried to write off end of the image + kDIErrGenericIO = -35, // generic I/O error + + kDIErrOddLength = -40, // image size not multiple of sectors + kDIErrUnrecognizedFileFmt = -41, // file format just not recognized + kDIErrBadFileFormat = -42, // filename ext doesn't match contents + kDIErrUnsupportedFileFmt = -43, // recognized but not supported + kDIErrUnsupportedPhysicalFmt = -44, // (same) + kDIErrUnsupportedFSFmt = -45, // (and again) + kDIErrBadOrdering = -46, // requested sector ordering no good + kDIErrFilesystemNotFound = -47, // requested filesystem isn't there + kDIErrUnsupportedAccess = -48, // e.g. read sectors from blocks-only + kDIErrUnsupportedImageFeature = -49, // e.g. FDI image w/Amiga sectors + + kDIErrInvalidTrack = -50, // request for invalid track number + kDIErrInvalidSector = -51, // request for invalid sector number + kDIErrInvalidBlock = -52, // request for invalid block number + kDIErrInvalidIndex = -53, // request with an invalid index + + kDIErrDirectoryLoop = -60, // directory chain points into itself + kDIErrFileLoop = -61, // file sector or block alloc loops + kDIErrBadDiskImage = -62, // the FS on the disk image is damaged + kDIErrBadFile = -63, // bad file on disk image + kDIErrBadDirectory = -64, // bad dir on disk image + kDIErrBadPartition = -65, // bad partition on multi-part format + + kDIErrFileArchive = -70, // file archive, not disk archive + kDIErrUnsupportedCompression = -71, // compression method is not supported + kDIErrBadChecksum = -72, // image file's checksum is bad + kDIErrBadCompressedData = -73, // data can't even be unpacked + kDIErrBadArchiveStruct = -74, // bad archive structure + + kDIErrBadNibbleSectors = -80, // can't read sectors from this image + kDIErrSectorUnreadable = -81, // requested sector not readable + kDIErrInvalidDiskByte = -82, // invalid byte for encoding type + kDIErrBadRawData = -83, // couldn't get correct nibbles + + kDIErrInvalidFileName = -90, // tried to create file with bad name + kDIErrDiskFull = -91, // no space left on disk + kDIErrVolumeDirFull = -92, // no more entries in volume dir + kDIErrInvalidCreateReq = -93, // CreateImage request was flawed + kDIErrTooBig = -94, // larger than we want to handle + + /* higher-level errors */ + kDIErrGeneric = -101, + kDIErrInternal = -102, + kDIErrMalloc = -103, + kDIErrInvalidArg = -104, + kDIErrNotSupported = -105, // feature not currently supported + kDIErrCancelled = -106, // an operation was cancelled by user + + kDIErrNufxLibInitFailed = -110, +} DIError; + +/* return a string describing the error */ +DISKIMG_API const char* DIStrError(DIError dierr); + + +/* exact definition of off_t varies, so just define our own */ +#ifdef _ULONGLONG_ + typedef LONGLONG di_off_t; +#else + typedef off_t di_off_t; +#endif + +/* common definition of "whence" for seeks */ +typedef enum DIWhence { + kSeekSet = SEEK_SET, + kSeekCur = SEEK_CUR, + kSeekEnd = SEEK_END +}; + +/* try to load ASPI under Win2K; if successful, SPTI should be disabled */ +const bool kAlwaysTryASPI = false; +/* ASPI device "filenames" look like "ASPI:x:y:z\" */ +DISKIMG_API extern const char* kASPIDev; + +/* some nibble-encoding constants */ +const int kTrackLenNib525 = 6656; +const int kTrackLenNb2525 = 6384; +const int kTrackLenTrackStar525 = 6525; // max len of data in TS image +const int kTrackAllocSize = 6656; // max 5.25 nibble track len; for buffers +const int kTrackCount525 = 35; // expected #of tracks on 5.25 img +const int kMaxNibbleTracks525 = 40; // max #of tracks on 5.25 nibble img +const int kDefaultNibbleVolumeNum = 254; +const int kBlockSize = 512; // block size for DiskImg interfaces +const int kSectorSize = 256; // sector size (1/2 block) +const int kD13Length = 256 * 13 * 35; // length of a .d13 image + +/* largest expanse we allow access to on a volume (8GB in 512-byte blocks) */ +const long kVolumeMaxBlocks = 8*1024*(1024*1024 / kBlockSize); + +/* largest .gz file we'll open (uncompressed size) */ +const long kGzipMax = 32*1024*1024; + +/* forward and external class definitions */ +class DiskFS; +class A2File; +class A2FileDescr; +class GenericFD; +class OuterWrapper; +class ImageWrapper; +class CircularBufferAccess; +class ASPI; +class LinearBitmap; + + +/* + * Library-global data functions. + * + * This class is just a namespace clumper. Do not instantiate. + */ +class DISKIMG_API Global { +public: + // one-time DLL initialization; use SetDebugMsgHandler first + static DIError AppInit(void); + // one-time DLL cleanup + static DIError AppCleanup(void); + + // return the DiskImg version number + static void GetVersion(long* pMajor, long* pMinor, long* pBug); + + static bool GetAppInitCalled(void) { return fAppInitCalled; } + static bool GetHasSPTI(void); + static bool GetHasASPI(void); + + // return a pointer to our global ASPI instance + static ASPI* GetASPI(void) { return fpASPI; } + // shortcut for fpASPI->GetVersion() + static unsigned long GetASPIVersion(void); + + // pointer to the debug message handler + typedef void (*DebugMsgHandler)(const char* file, int line, const char* msg); + static DebugMsgHandler gDebugMsgHandler; + + static DebugMsgHandler SetDebugMsgHandler(DebugMsgHandler handler); + static void PrintDebugMsg(const char* file, int line, const char* fmt, ...) + #if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) + #endif + ; + +private: + // no instantiation allowed + Global(void) {} + ~Global(void) {} + + // make sure app calls AppInit + static bool fAppInitCalled; + + static ASPI* fpASPI; +}; + +extern bool gAllowWritePhys0; // ugh -- see Win32BlockIO.cpp + + +/* + * Disk I/O class, roughly equivalent to a GS/OS disk device driver. + * + * Abstracts away the file's source (file on disk, file in memory) and + * storage format (DOS order, ProDOS order, nibble). Will also cope + * with common disk compression and wrapper formats (Mac DiskCopy, 2MG, + * ShrinkIt, etc) if handed a file on disk. + * + * Images may be embedded within other images, e.g. UNIDOS and storage + * type $04 pascal volumes. + * + * THOUGHT: we need a list(?) of pointers from here back to the DiskFS + * so that any modifications here will "wake" the DiskFS and sub-volumes. + * We also need a "dirty" flag so things like CloseNufx can know not to + * re-do work when Closing after a Flush. Also DiskFS can alert us to + * any locally cached stuff, and we can tell them to flush everything. + * (Possibly useful when doing disk updates, so stuff can be trivially + * un-done. Currently CiderPress checks the filename manually after + * each write, but that's generally less reliable than having the knowledge + * contained in the DiskImg.) + * + * THOUGHT: need a ReadRawTrack that gets raw nibblized data. For a + * nibblized image it returns the data, for a sector image it generates + * the raw data. + * + * THOUGHT: we could reduce the risk of problems and increase performance + * for physical media with a "copy on write" scheme. We'd create a shadow + * array of modified blocks, and write them at Flush time. This would + * provide an instantaneous "revert" feature, and prevent formats like + * DiskCopy42 (which has a CRC in its header) from being inconsistent for + * long stretches. + */ +class DISKIMG_API DiskImg { +public: + // create DiskImg object + DiskImg(void); + virtual ~DiskImg(void); + + /* + * Types describing an image file. + * + * The file itself is described by an external parameter ("file source") + * that is either the name of the file, a memory buffer, or an EFD + * (EmbeddedFileDescriptor). + */ + typedef enum { // format of the "wrapper wrapper" + kOuterFormatUnknown = 0, + kOuterFormatNone = 1, // (plain) + kOuterFormatCompress = 2, // .xx.Z + kOuterFormatGzip = 3, // .xx.gz + kOuterFormatBzip2 = 4, // .xx.bz2 + kOuterFormatZip = 10, // .zip + } OuterFormat; + typedef enum { // format of the image "wrapper" + kFileFormatUnknown = 0, + kFileFormatUnadorned = 1, // .po, .do, ,nib, .raw, .d13 + kFileFormat2MG = 2, // .2mg, .2img, $e0/0130 + kFileFormatDiskCopy42 = 3, // .dsk/.disk, maybe .dc + kFileFormatDiskCopy60 = 4, // .dc6 (often just raw format) + kFileFormatDavex = 5, // $e0/8004 + kFileFormatSim2eHDV = 6, // .hdv + kFileFormatTrackStar = 7, // .app (40-track or 80-track) + kFileFormatFDI = 8, // .fdi (5.25" or 3.5") + kFileFormatNuFX = 20, // .shk, .sdk, .bxy + kFileFormatDDD = 21, // .ddd + kFileFormatDDDDeluxe = 22, // $DD, .ddd + } FileFormat; + typedef enum { // format of the image data stream + kPhysicalFormatUnknown = 0, + kPhysicalFormatSectors = 1, // sequential 256-byte sectors (13/16/32) + kPhysicalFormatNib525_6656 = 2, // 5.25" disk ".nib" (6656 bytes/track) + kPhysicalFormatNib525_6384 = 3, // 5.25" disk ".nb2" (6384 bytes/track) + kPhysicalFormatNib525_Var = 4, // 5.25" disk (variable len, e.g. ".app") + } PhysicalFormat; + typedef enum { // sector ordering for "sector" format images + kSectorOrderUnknown = 0, + kSectorOrderProDOS = 1, // written as series of ProDOS blocks + kSectorOrderDOS = 2, // written as series of DOS sectors + kSectorOrderCPM = 3, // written as series of 1K CP/M blocks + kSectorOrderPhysical = 4, // written as un-interleaved sectors + kSectorOrderMax, // (used for array sizing) + } SectorOrder; + typedef enum { // main filesystem format (based on NuFX enum) + kFormatUnknown = 0, + kFormatProDOS = 1, + kFormatDOS33 = 2, + kFormatDOS32 = 3, + kFormatPascal = 4, + kFormatMacHFS = 5, + kFormatMacMFS = 6, + kFormatLisa = 7, + kFormatCPM = 8, + //kFormatCharFST + kFormatMSDOS = 10, // any FAT filesystem + //kFormatHighSierra + kFormatISO9660 = 12, + //kFormatAppleShare + kFormatRDOS33 = 20, // 16-sector RDOS disk + kFormatRDOS32 = 21, // 13-sector RDOS disk + kFormatRDOS3 = 22, // 13-sector RDOS disk converted to 16 + // "generic" formats *must* be in their own "decade" + kFormatGenericPhysicalOrd = 30, // unknown, but physical-sector-ordered + kFormatGenericProDOSOrd = 31, // unknown, but ProDOS-block-ordered + kFormatGenericDOSOrd = 32, // unknown, but DOS-sector-ordered + kFormatGenericCPMOrd = 33, // unknown, but CP/M-block-ordered + kFormatUNIDOS = 40, // two 400K DOS 3.3 volumes + kFormatOzDOS = 41, // two 400K DOS 3.3 volumes, weird order + kFormatCFFA4 = 42, // CFFA image with 4 or 6 partitions + kFormatCFFA8 = 43, // CFFA image with 8 partitions + kFormatMacPart = 44, // Macintosh-style partitioned disk + kFormatMicroDrive = 45, // ///SHH Systeme's MicroDrive format + kFormatFocusDrive = 46, // Parsons Engineering FocusDrive format + kFormatGutenberg = 47, // Gutenberg word processor format + + // try to keep this in an unsigned char, e.g. for CP clipboard + } FSFormat; + + /* + * Nibble encode/decode description. Use no pointers here, so we + * store as an array and resize at will. + * + * Should we define an enum to describe whether address and data + * headers are standard or some wacky variant? + */ + typedef enum { + kNibbleAddrPrologLen = 3, // d5 aa 96 + kNibbleAddrEpilogLen = 3, // de aa eb + kNibbleDataPrologLen = 3, // d5 aa ad + kNibbleDataEpilogLen = 3, // de aa eb + }; + typedef enum { + kNibbleEncUnknown = 0, + kNibbleEnc44, + kNibbleEnc53, + kNibbleEnc62, + } NibbleEnc; + typedef enum { + kNibbleSpecialNone = 0, + kNibbleSpecialMuse, // doubled sector numbers on tracks > 2 + kNibbleSpecialSkipFirstAddrByte, + } NibbleSpecial; + typedef struct { + char description[32]; + short numSectors; // 13 or 16 (or 18?) + + unsigned char addrProlog[kNibbleAddrPrologLen]; + unsigned char addrEpilog[kNibbleAddrEpilogLen]; + unsigned char addrChecksumSeed; + bool addrVerifyChecksum; + bool addrVerifyTrack; + int addrEpilogVerifyCount; + + unsigned char dataProlog[kNibbleDataPrologLen]; + unsigned char dataEpilog[kNibbleDataEpilogLen]; + unsigned char dataChecksumSeed; + bool dataVerifyChecksum; + int dataEpilogVerifyCount; + + NibbleEnc encoding; + NibbleSpecial special; + } NibbleDescr; + + + static inline bool IsSectorFormat(PhysicalFormat fmt) { + return (fmt == kPhysicalFormatSectors); + } + static inline bool IsNibbleFormat(PhysicalFormat fmt) { + return (fmt == kPhysicalFormatNib525_6656 || + fmt == kPhysicalFormatNib525_6384 || + fmt == kPhysicalFormatNib525_Var); + } + + // file is on disk; stuff like 2MG headers will be identified and stripped + DIError OpenImage(const char* filename, char fssep, bool readOnly); + // file is in memory; provide a pointer to the data start and buffer size + DIError OpenImage(const void* buffer, long length, bool readOnly); + // file is a range of blocks on an open block-oriented disk image + DIError OpenImage(DiskImg* pParent, long firstBlock, long numBlocks); + // file is a range of tracks/sectors on an open sector-oriented disk image + DIError OpenImage(DiskImg* pParent, long firstTrack, long firstSector, + long numSectors); + + // create a new, blank image file + DIError CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, + PhysicalFormat physical, const NibbleDescr* pNibbleDescr, + SectorOrder order, FSFormat format, + long numBlocks, bool skipFormat); + DIError CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, + PhysicalFormat physical, const NibbleDescr* pNibbleDescr, + SectorOrder order, FSFormat format, + long numTracks, long numSectPerTrack, bool skipFormat); + + // flush any changes to disk; slow recompress only for "kFlushAll" + typedef enum { kFlushUnknown=0, kFlushFastOnly=1, kFlushAll=2 } FlushMode; + DIError FlushImage(FlushMode mode); + // close the image, freeing up any resources in use + DIError CloseImage(void); + // raise/lower refCnt (may want to track pointers someday) + void AddDiskFS(DiskFS* pDiskFS) { fDiskFSRefCnt++; } + void RemoveDiskFS(DiskFS* pDiskFS) { + assert(fDiskFSRefCnt > 0); + fDiskFSRefCnt--; + } + + // (re-)format this image in the specified FS format + DIError FormatImage(FSFormat format, const char* volName); + // reset all blocks/sectors to zeroes + DIError ZeroImage(void); + + // configure for paired sectors (OzDOS) + void SetPairedSectors(bool enable, int idx); + + // identify sector ordering and disk format + // (may want a version that takes "hints" for special disks?) + DIError AnalyzeImage(void); + // figure out what FS and sector ordering is on the disk image + void AnalyzeImageFS(void); + bool ShowAsBlocks(void) const; + // overrule the analyzer (generally not recommended) -- does not + // override FileFormat, which is very reliable + DIError OverrideFormat(PhysicalFormat physical, FSFormat format, + SectorOrder order); + + // Create a DiskFS that matches this DiskImg. Must be called after + // AnalayzeImage, or you will always get a DiskFSUnknown. The DiskFS + // must be freed with "delete" when no longer needed. + DiskFS* OpenAppropriateDiskFS(bool allowUnknown = false); + + // Add information or a warning to the list of notes. Use linefeeds to + // indicate line breaks. This is currently append-only. + typedef enum { kNoteInfo, kNoteWarning } NoteType; + void AddNote(NoteType type, const char* fmt, ...) + #if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) + #endif + ; + const char* GetNotes(void) const; + + // simple accessors + OuterFormat GetOuterFormat(void) const { return fOuterFormat; } + FileFormat GetFileFormat(void) const { return fFileFormat; } + PhysicalFormat GetPhysicalFormat(void) const { return fPhysical; } + SectorOrder GetSectorOrder(void) const { return fOrder; } + FSFormat GetFSFormat(void) const { return fFormat; } + long GetNumTracks(void) const { return fNumTracks; } + int GetNumSectPerTrack(void) const { return fNumSectPerTrack; } + long GetNumBlocks(void) const { return fNumBlocks; } + bool GetReadOnly(void) const { return fReadOnly; } + bool GetDirtyFlag(void) const { return fDirty; } + + // set read-only flag; don't use this (open with correct setting; + // this was added as safety hack for the volume copier) + void SetReadOnly(bool val) { fReadOnly = val; } + + // read a 256-byte sector + // NOTE to self: this function should not be available for odd-sized + // volumes, e.g. a ProDOS /RAM or /RAM5 stored with Davex. Need some way + // to communicate that to disk editor so it knows to grey-out the + // selection checkbox and/or not use "show as sectors" as default. + virtual DIError ReadTrackSector(long track, int sector, void* buf) { + return ReadTrackSectorSwapped(track, sector, buf, fOrder, + fFileSysOrder); + } + DIError ReadTrackSectorSwapped(long track, int sector, + void* buf, SectorOrder imageOrder, SectorOrder fsOrder); + // write a 256-byte sector + virtual DIError WriteTrackSector(long track, int sector, const void* buf); + + // read a 512-byte block + virtual DIError ReadBlock(long block, void* buf) { + return ReadBlockSwapped(block, buf, fOrder, fFileSysOrder); + } + DIError ReadBlockSwapped(long block, void* buf, SectorOrder imageOrder, + SectorOrder fsOrder); + // read multiple blocks + virtual DIError ReadBlocks(long startBlock, int numBlocks, void* buf); + // check our virtual bad block map + bool CheckForBadBlocks(long startBlock, int numBlocks); + // write a 512-byte block + virtual DIError WriteBlock(long block, const void* buf); + // write multiple blocks + virtual DIError WriteBlocks(long startBlock, int numBlocks, const void* buf); + + // read an entire nibblized track + virtual DIError ReadNibbleTrack(long track, unsigned char* buf, + long* pTrackLen); + // write a track; trackLen must be <= those in image + virtual DIError WriteNibbleTrack(long track, const unsigned char* buf, + long trackLen); + + // save the current image as a 2MG file + //DIError Write2MG(const char* filename); + + // need to treat the DOS volume number as meta-data for some disks + short GetDOSVolumeNum(void) const { return fDOSVolumeNum; } + void SetDOSVolumeNum(short val) { fDOSVolumeNum = val; } + enum { kVolumeNumNotSet = -1 }; + + // some simple getters + bool GetHasSectors(void) const { return fHasSectors; } + bool GetHasBlocks(void) const { return fHasBlocks; } + bool GetHasNibbles(void) const { return fHasNibbles; } + bool GetIsEmbedded(void) const { return fpParentImg != NULL; } + + // return the current NibbleDescr + const NibbleDescr* GetNibbleDescr(void) const { return fpNibbleDescr; } + // set the NibbleDescr; we do this by copying the entry into our table + // (could improve by doing memcmp on available entries?) + void SetNibbleDescr(int idx); + void SetCustomNibbleDescr(const NibbleDescr* pDescr); + const NibbleDescr* GetNibbleDescrTable(int* pCount) const { + *pCount = fNumNibbleDescrEntries; + return fpNibbleDescrTable; + } + + // set the NuFX compression type, used when compressing or re-compressing; + // must be set before image is opened or created + void SetNuFXCompressionType(int val) { fNuFXCompressType = val; } + + /* + * Set up a progress callback to use when scanning a disk volume. Pass + * nil for "func" to disable. + * + * The callback function is expected to return "true" if all is well. + * If it returns false, kDIErrCancelled will eventually come back. + */ + typedef bool (*ScanProgressCallback)(void* cookie, const char* str, + int count); + void SetScanProgressCallback(ScanProgressCallback func, void* cookie); + /* update status dialog during disk scan; called from DiskFS code */ + bool UpdateScanProgress(const char* newStr); + + /* + * Static utility functions. + */ + // returns "true" if the files on this image have DOS structure, i.e. + // simple file types and high-ASCII text files + static bool UsesDOSFileStructure(FSFormat format) { + return (format == kFormatDOS33 || + format == kFormatDOS32 || + format == kFormatGutenberg || + format == kFormatUNIDOS || + format == kFormatOzDOS || + format == kFormatRDOS33 || + format == kFormatRDOS32 || + format == kFormatRDOS3); + } + // returns "true" if we can open files on the specified filesystem + static bool CanOpenFiles(FSFormat format) { + return (format == kFormatProDOS || + format == kFormatDOS33 || + format == kFormatDOS32 || + format == kFormatPascal || + format == kFormatCPM || + format == kFormatRDOS33 || + format == kFormatRDOS32 || + format == kFormatRDOS3); + } + // returns "true" if we can create subdirectories on this filesystem + static bool IsHierarchical(FSFormat format) { + return (format == kFormatProDOS || + format == kFormatMacHFS || + format == kFormatMSDOS); + } + // returns "true" if we can create resource forks on this filesystem + static bool HasResourceForks(FSFormat format) { + return (format == kFormatProDOS || + format == kFormatMacHFS); + } + // returns "true" if the format is one of the "generics" + static bool IsGenericFormat(FSFormat format) { + return (format / 10 == DiskImg::kFormatGenericDOSOrd / 10); + } + + /* this must match DiskImg::kStdNibbleDescrs table */ + typedef enum StdNibbleDescr { + kNibbleDescrDOS33Std = 0, + kNibbleDescrDOS33Patched, + kNibbleDescrDOS33IgnoreChecksum, + kNibbleDescrDOS32Std, + kNibbleDescrDOS32Patched, + kNibbleDescrMuse32, + kNibbleDescrRDOS33, + kNibbleDescrRDOS32, + kNibbleDescrCustom, // slot for custom entry + + kNibbleDescrMAX // must be last + }; + static const NibbleDescr* GetStdNibbleDescr(StdNibbleDescr idx); + // call this once, at DLL initialization time + static void CalcNibbleInvTables(void); + // calculate block number from cyl/head/sect on 3.5" disk + static int CylHeadSect35ToBlock(int cyl, int head, int sect); + // unpack nibble data from a 3.5" disk track + static DIError UnpackNibbleTrack35(const unsigned char* nibbleBuf, + long nibbleLen, unsigned char* outputBuf, int cyl, int head, + LinearBitmap* pBadBlockMap); + // compute the #of sectors per track for cylinder N (0-79) + static int SectorsPerTrack35(int cylinder); + + // get the order in which we test for sector ordering + static void GetSectorOrderArray(SectorOrder* orderArray, SectorOrder first); + + // utility function used by HFS filename normalizer; available to apps + static inline unsigned char MacToASCII(unsigned char uch) { + if (uch < 0x20) + return '?'; + else if (uch < 0x80) + return uch; + else + return kMacHighASCII[uch - 0x80]; + } + + // Allow write access to physical disk 0. This is usually the boot disk, + // but with some BIOS the first IDE drive is always physical 0 even if + // you're booting from SATA. This only has meaning under Win32. + static void SetAllowWritePhys0(bool val); + + /* + * Get string constants for enumerated values. + */ + typedef struct { int format; const char* str; } ToStringLookup; + static const char* ToStringCommon(int format, const ToStringLookup* pTable, + int tableSize); + static const char* ToString(OuterFormat format); + static const char* ToString(FileFormat format); + static const char* ToString(PhysicalFormat format); + static const char* ToString(SectorOrder format); + static const char* ToString(FSFormat format); + +private: + /* + * Fundamental disk image identification. + */ + OuterFormat fOuterFormat; // e.g. gzip + FileFormat fFileFormat; + PhysicalFormat fPhysical; + const NibbleDescr* fpNibbleDescr; // only used for "nibble" images + SectorOrder fOrder; // only used for "sector" images + FSFormat fFormat; + + /* + * This affects how the DiskImg responds to requests for reading + * a track or sector. + * + * "fFileSysOrder", together with with "fOrder", determines how + * sector numbers are translated. It describes the order that the + * DiskFS filesystem expects things to be in. If the image isn't + * sector-addressable, then it is assumed to be in linear block order. + * + * If "fSectorPairing" is set, the DiskImg treats the disk as if + * it were in OzDOS format, with one sector from two different + * volumes in a single 512-byte block. + */ + SectorOrder fFileSysOrder; + bool fSectorPairing; + int fSectorPairOffset; // which image (should be 0 or 1) + + /* + * Internal state. + */ + GenericFD* fpOuterGFD; // outer wrapper, if any (.gz only) + GenericFD* fpWrapperGFD; // Apple II image file + GenericFD* fpDataGFD; // raw Apple II data + OuterWrapper* fpOuterWrapper; // needed for outer .gz wrapper + ImageWrapper* fpImageWrapper; // disk image wrapper (2MG, SHK, etc) + DiskImg* fpParentImg; // set for embedded volumes + short fDOSVolumeNum; // specified by some wrapper formats + di_off_t fOuterLength; // total len of file + di_off_t fWrappedLength; // len of file after Outer wrapper removed + di_off_t fLength; // len of disk image (w/o wrappers) + bool fExpandable; // ProDOS .hdv can expand + bool fReadOnly; // allow writes to this image? + bool fDirty; // have we modified this image? + //bool fIsEmbedded; // is this image embedded in another? + + bool fHasSectors; // image is sector-addressable + bool fHasBlocks; // image is block-addressable + bool fHasNibbles; // image is nibble-addressable + + long fNumTracks; // for sector-addressable images + int fNumSectPerTrack; // (ditto) + long fNumBlocks; // for 512-byte block-addressable images + + unsigned char* fNibbleTrackBuf; // allocated on heap + int fNibbleTrackLoaded; // track currently in buffer + + int fNuFXCompressType; // used when compressing a NuFX image + + char* fNotes; // warnings and FYIs about DiskImg/DiskFS + + LinearBitmap* fpBadBlockMap; // used for 3.5" nibble images + + int fDiskFSRefCnt; // #of DiskFS objects pointing at us + + /* + * NibbleDescr entries. There are several standard ones, and we want + * to allow applications to define additional ones. + */ + NibbleDescr* fpNibbleDescrTable; + int fNumNibbleDescrEntries; + + /* static table of default values */ + static const NibbleDescr kStdNibbleDescrs[]; + + DIError CreateImageCommon(const char* pathName, const char* storageName, + bool skipFormat); + DIError ValidateCreateFormat(void) const; + DIError FormatSectors(GenericFD* pGFD, bool quickFormat) const; + //DIError FormatBlocks(GenericFD* pGFD) const; + + DIError CopyBytesOut(void* buf, di_off_t offset, int size) const; + DIError CopyBytesIn(const void* buf, di_off_t offset, int size); + DIError AnalyzeImageFile(const char* pathName, char fssep); + // Figure out the sector ordering for this filesystem, so we can decide + // how the sectors need to be re-arranged when we're reading them. + SectorOrder CalcFSSectorOrder(void) const; + // Handle sector order calculations. + DIError CalcSectorAndOffset(long track, int sector, SectorOrder ImageOrder, + SectorOrder fsOrder, di_off_t* pOffset, int* pNewSector); + inline bool IsLinearBlocks(SectorOrder imageOrder, SectorOrder fsOrder); + + /* + * Progress update during the filesystem scan. This only exists in the + * topmost DiskImg. (This is arguably more appropriate in DiskFS, but + * DiskFS objects don't have a notion of "parent" and are somewhat more + * ephemeral.) + */ + ScanProgressCallback fpScanProgressCallback; + void* fScanProgressCookie; + int fScanCount; + char fScanMsg[128]; + time_t fScanLastMsgWhen; + + /* + * 5.25" nibble image access. + */ + enum { + kDataSize62 = 343, // 342 bytes + checksum byte + kChunkSize62 = 86, // (0x56) + + kDataSize53 = 411, // 410 bytes + checksum byte + kChunkSize53 = 51, // (0x33) + kThreeSize = (kChunkSize53 * 3) + 1, // same as 410 - 256 + }; + DIError ReadNibbleSector(long track, int sector, void* buf, + const NibbleDescr* pNibbleDescr); + DIError WriteNibbleSector(long track, int sector, const void* buf, + const NibbleDescr* pNibbleDescr); + void DumpNibbleDescr(const NibbleDescr* pNibDescr) const; + int GetNibbleTrackLength(long track) const; + int GetNibbleTrackOffset(long track) const; + int GetNibbleTrackFormatLength(void) const { + /* return length to use when formatting for 16 sectors */ + if (fPhysical == kPhysicalFormatNib525_6656) + return kTrackLenNib525; + else if (fPhysical == kPhysicalFormatNib525_6384) + return kTrackLenNb2525; + else if (fPhysical == kPhysicalFormatNib525_Var) { + if (fFileFormat == kFileFormatTrackStar || + fFileFormat == kFileFormatFDI) + { + return kTrackLenNb2525; // use minimum possible + } + } + assert(false); + return -1; + } + int GetNibbleTrackAllocLength(void) const { + /* return length to allocate when creating an image */ + if (fPhysical == kPhysicalFormatNib525_Var && + (fFileFormat == kFileFormatTrackStar || + fFileFormat == kFileFormatFDI)) + { + // use maximum possible + return kTrackLenTrackStar525; + } + return GetNibbleTrackFormatLength(); + } + DIError LoadNibbleTrack(long track, long* pTrackLen); + DIError SaveNibbleTrack(void); + int FindNibbleSectorStart(const CircularBufferAccess& buffer, + int track, int sector, const NibbleDescr* pNibbleDescr, int* pVol); + void DecodeAddr(const CircularBufferAccess& buffer, int offset, + short* pVol, short* pTrack, short* pSector, short* pChksum); + inline unsigned int ConvFrom44(unsigned char val1, unsigned char val2) { + return ((val1 << 1) | 0x01) & val2; + } + DIError DecodeNibbleData(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); + void EncodeNibbleData(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; + DIError DecodeNibble62(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); + void EncodeNibble62(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; + DIError DecodeNibble53(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); + void EncodeNibble53(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; + int TestNibbleTrack(int track, const NibbleDescr* pNibbleDescr, int* pVol); + DIError AnalyzeNibbleData(void); + inline unsigned char Conv44(unsigned short val, bool first) const { + if (first) + return (val >> 1) | 0xaa; + else + return val | 0xaa; + } + DIError FormatNibbles(GenericFD* pGFD) const; + + static const unsigned char kMacHighASCII[]; + + /* + * 3.5" nibble access + */ + static int FindNextSector35(const CircularBufferAccess& buffer, int start, + int cyl, int head, int* pSector); + static bool DecodeNibbleSector35(const CircularBufferAccess& buffer, + int start, unsigned char* sectorBuf, unsigned char* readChecksum, + unsigned char* calcChecksum); + static bool UnpackChecksum35(const CircularBufferAccess& buffer, + int offset, unsigned char* checksumBuf); + static void EncodeNibbleSector35(const unsigned char* sectorData, + unsigned char* outBuf); + + /* static data tables */ + static unsigned char kDiskBytes53[32]; + static unsigned char kDiskBytes62[64]; + static unsigned char kInvDiskBytes53[256]; + static unsigned char kInvDiskBytes62[256]; + enum { kInvInvalidValue = 0xff }; + +private: // some C++ stuff to block behavior we don't support + DiskImg& operator=(const DiskImg&); + DiskImg(const DiskImg&); +}; + + + +/* + * Disk filesystem class, roughly equivalent to a GS/OS FST. This is an + * abstract base class. + * + * Static functions know how to access a DiskImg and figure out what kind + * of image we have. Once known, the appropriate sub-class can be + * instantiated. + * + * We maintain a linear list of files to make it easy for applications to + * traverse the full set of files. Sometimes this makes it hard for us to + * update internally (especially HFS). With some minor cleverness it + * should be possible to switch to a tree structure while retaining the + * linear "get next" API. This would be a big help for ProDOS and HFS. + * + * NEED: some notification mechanism for changes to files and/or block + * editing of the disk (especially with regard to open sub-volumes). If + * a disk volume open for file-by-file viewing is modified with the disk + * editor, we should close the file when the disk editor exits. + * + * NEED? disk utilities, such as "disk crunch", for Pascal volumes. Could + * be written externally, but might as well keep fs knowledge embedded. + * + * MISSING: there is no way to override the image analyzer when working + * with sub-volumes. Actually, there is; it just has to happen *after* + * the DiskFS has been created. We should provide an approach that either + * stifles the DiskFS creation, or allows us to override and replace the + * internal DiskFS so we can pop up a sub-volume list, show what we *think* + * is there, and then let the user pick a volume and pick overrides (mainly + * for use in the disk editor). Not sure if we want the changes to "stick"; + * we probably do. Q: does the "scan for sub-volumes" attribute propagate + * recursively to each sub-sub-volume? Probably. + * + * NOTE to self: should make "test" functions more lax when called + * from here, on the assumption that the caller is knowledgeable. Perhaps + * an independent "strictness" variable that can be cranked down through + * multiple calls to AnalyzeImage?? + */ +class DISKIMG_API DiskFS { +public: + /* + * Information about volume usage. + * + * Each "chunk" is a track/sector on a DOS disk or a block on a ProDOS + * or Pascal disk. CP/M really ought to use 1K blocks, but for + * convenience we're just using 512-byte blocks (it's up to the CP/M + * code to set two "chunks" per block). + * + * NOTE: the current DOS/ProDOS/Pascal code is sloppy when it comes to + * keeping this structure up to date. HFS doesn't use it at all. This + * has always been a low-priority feature. + */ + class DISKIMG_API VolumeUsage { + public: + VolumeUsage(void) { + fByBlocks = false; + fTotalChunks = -1; + fNumSectors = -1; + //fFreeChunks = -1; + fList = NULL; + fListSize = -1; + } + ~VolumeUsage(void) { + delete[] fList; + } + + /* + * These values MUST fit in five bits. + * + * Suggested disk map colors: + * 0 = unknown (color-key pink) + * 1 = conflict (medium-red) + * 2 = boot loader (dark-blue) + * 3 = volume dir (light-blue) + * 4 = subdir (medium-blue) + * 5 = user data (medium-green) + * 6 = user index blocks (light-green) + * 7 = embedded filesys (yellow) + * + * THOUGHT: Add flag for I/O error (nibble images) -- requires + * automatic disk verify pass. (Or maybe could be done manually + * on request?) + * + * unused --> black + * marked-used-but-not-used --> dark-red + * used-but-not-marked-used --> orange + */ + typedef enum { + kChunkPurposeUnknown = 0, + kChunkPurposeConflict = 1, // two or more different things + kChunkPurposeSystem = 2, // boot blocks, volume bitmap + kChunkPurposeVolumeDir = 3, // volume dir (or only dir) + kChunkPurposeSubdir = 4, // ProDOS sub-directory + kChunkPurposeUserData = 5, // file on this filesystem + kChunkPurposeFileStruct = 6, // index blocks, T/S lists + kChunkPurposeEmbedded = 7, // embedded filesystem + // how about: outside range claimed by disk, e.g. fTotalBlocks on + // 800K ProDOS disk in a 32MB CFFA volume? + } ChunkPurpose; + + typedef struct ChunkState { + bool isUsed; + bool isMarkedUsed; + ChunkPurpose purpose; // only valid if isUsed is set + } ChunkState; + + // initialize, configuring for either blocks or sectors + DIError Create(long numBlocks); + DIError Create(long numTracks, long numSectors); + bool GetInitialized(void) const { return (fList != NULL); } + + // return the number of chunks on this disk + long GetNumChunks(void) const { return fTotalChunks; } + + // return the number of unallocated chunks, taking into account + // both the free-chunk bitmap (if any) and the actual usage + long GetActualFreeChunks(void) const; + + // return the state of the specified chunk + DIError GetChunkState(long block, ChunkState* pState) const; + DIError GetChunkState(long track, long sector, + ChunkState* pState) const; + + // set the state of a particular chunk (should only be done by + // the DiskFS sub-classes) + DIError SetChunkState(long block, const ChunkState* pState); + DIError SetChunkState(long track, long sector, + const ChunkState* pState); + + void Dump(void) const; // debugging + + private: + DIError GetChunkStateIdx(int idx, ChunkState* pState) const; + DIError SetChunkStateIdx(int idx, const ChunkState* pState); + inline char StateToChar(ChunkState* pState) const; + + /* + * Chunk state is stored as a set of bits in one byte: + * + * 0-4: how is block used (only has meaning if bit 6 is set) + * 5: for nibble images, indicates the block or sector is unreadable + * 6: is block used by something (0=no, 1=yes) + * 7: is block marked "in use" by system map (0=no, 1=yes) + * + * [ Consider reducing "purpose" to 0-3 and adding bad block bit for + * nibble images and physical media.] + */ + enum { + kChunkPurposeMask = 0x1f, // ChunkPurpose enum + kChunkDamagedFlag = 0x20, + kChunkUsedFlag = 0x40, + kChunkMarkedUsedFlag = 0x80, + }; + + bool fByBlocks; + long fTotalChunks; + long fNumSectors; // only valid if !fByBlocks + //long fFreeChunks; + unsigned char* fList; + int fListSize; + }; // end of VolumeUsage class + + /* + * List of sub-volumes. The SubVolume owns the DiskImg and DiskFS + * that are handed to it, so they can be deleted when the SubVolume + * is deleted as part of destroying the parent. + */ + class SubVolume { + public: + SubVolume(void) : fpDiskImg(NULL), fpDiskFS(NULL), + fpPrev(NULL), fpNext(NULL) {} + ~SubVolume(void) { + delete fpDiskFS; // must close first; may need flush to DiskImg + delete fpDiskImg; + } + + void Create(DiskImg* pDiskImg, DiskFS* pDiskFS) { + assert(pDiskImg != NULL); + assert(pDiskFS != NULL); + fpDiskImg = pDiskImg; + fpDiskFS = pDiskFS; + } + + DiskImg* GetDiskImg(void) const { return fpDiskImg; } + DiskFS* GetDiskFS(void) const { return fpDiskFS; } + + SubVolume* GetPrev(void) const { return fpPrev; } + void SetPrev(SubVolume* pSubVol) { fpPrev = pSubVol; } + SubVolume* GetNext(void) const { return fpNext; } + void SetNext(SubVolume* pSubVol) { fpNext = pSubVol; } + + private: + DiskImg* fpDiskImg; + DiskFS* fpDiskFS; + + SubVolume* fpPrev; + SubVolume* fpNext; + }; // end of SubVolume class + + + + /* + * Start of DiskFS declarations. + */ +public: + typedef enum SubScanMode { + kScanSubUnknown = 0, + kScanSubDisabled, + kScanSubEnabled, + kScanSubContainerOnly, + } SubScanMode; + + + DiskFS(void) { + fpA2Head = fpA2Tail = NULL; + fpSubVolumeHead = fpSubVolumeTail = NULL; + fpImg = NULL; + fScanForSubVolumes = kScanSubDisabled; + + fParmTable[kParm_CreateUnique] = 0; + fParmTable[kParmProDOS_AllowLowerCase] = 1; + fParmTable[kParmProDOS_AllocSparse] = 1; + } + virtual ~DiskFS(void) { + DeleteSubVolumeList(); + DeleteFileList(); + SetDiskImg(NULL); + } + + /* + * Static FSFormat-analysis functions, called by the DiskImg AnalyzeImage + * function. Capable of determining with a high degree of accuracy + * what format the disk is in, yet remaining flexible enough to + * function correctly with variations (like DOS3.3 disks with + * truncated TOCs and ProDOS images from hard drives). + * + * The "pOrder" and "pFormat" arguments are in/out. Set them to the + * appropriate "unknown" enum value on entry. If something is known + * of the order or format, put that in instead; in some cases this will + * bias the proceedings, which is useful for efficiency and for making + * overrides work correctly. + * + * On success, these return kDIErrNone and set "*pOrder". On failure, + * they return nonzero and leave "*pOrder" unmodified. + * + * Each DiskFS sub-class should declare a TestFS function. It's not + * virtual void here because, since it's called before the DiskFS is + * created, it must be static. + */ + typedef enum FSLeniency { kLeniencyNot, kLeniencyVery } FSLeniency; + //static DIError TestFS(const DiskImg* pImg, DiskImg::SectorOrder* pOrder, + // DiskImg::FSFormat* pFormat, FSLeniency leniency); + + /* + * Load the disk contents and (if enabled) scan for sub-volumes. + * + * If "headerOnly" is set, we just do a quick scan of the volume header + * to get basic information. The deep file scan is skipped (but can + * be done later). (Sub-classes can choose to ignore the flag and + * always do the full scan; this is an optimization.) Guaranteed to + * set the volume name and volume block/sector count. + * + * If a progress callback is set up, this can return with a "cancelled" + * result, which should not be treated as a failure. + */ + typedef enum { kInitUnknown = 0, kInitHeaderOnly, kInitFull } InitMode; + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) = 0; + + /* + * Format the disk with the appropriate filesystem, creating all filesystem + * structures and (when appropriate) boot blocks. + */ + virtual DIError Format(DiskImg* pDiskImg, const char* volName) + { return kDIErrNotSupported; } + + /* + * Pass in a full path to normalize, and a buffer to copy the output + * into. On entry "pNormalizedBufLen" holds the length of the buffer. + * On exit, it holds the size of the buffer required to hold the + * normalized string. If the buffer is nil or isn't big enough, no part + * of the normalized path will be copied into the buffer, and a specific + * error (kDIErrDataOverrun) will be returned. + */ + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen) + { return kDIErrNotSupported; } + + + /* + * Create a file. The CreateParms struct specifies the full set of file + * details. To remain FS-agnostic, use the NufxLib constants + * (kNuStorageDirectory, kNuAccessUnlocked, etc). They match up with + * their ProDOS equivalents, and I promise to make them work right. + * + * On success, the file exists as a fully-formed, zero-length file. A + * pointer to the relevant A2File structure is returned. + */ + enum { + /* valid values for CreateParms; must match ProDOS enum */ + kStorageSeedling = 1, + kStorageExtended = 5, + kStorageDirectory = 13, + }; + typedef struct CreateParms { + const char* pathName; // full pathname + char fssep; + int storageType; // determines normal, subdir, or forked + long fileType; + long auxType; + int access; + time_t createWhen; + time_t modWhen; + } CreateParms; + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile) + { return kDIErrNotSupported; } + + /* + * Delete a file from the disk. + */ + virtual DIError DeleteFile(A2File* pFile) + { return kDIErrNotSupported; } + + /* + * Rename a file. + */ + virtual DIError RenameFile(A2File* pFile, const char* newName) + { return kDIErrNotSupported; } + + /* + * Alter file attributes. + */ + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags) + { return kDIErrNotSupported; } + + /* + * Rename a volume. Also works for changing the disk volume number. + */ + virtual DIError RenameVolume(const char* newName) + { return kDIErrNotSupported; } + + + // Accessor + DiskImg* GetDiskImg(void) const { return fpImg; } + + // Test file and volume names (and volume numbers) + // [these need to be static functions for some things... hmm] + //virtual bool IsValidFileName(const char* name) const { return false; } + //virtual bool IsValidVolumeName(const char* name) const { return false; } + + // Return the disk volume name, or NULL if there isn't one. + virtual const char* GetVolumeName(void) const = 0; + + // Return a printable string identifying the FS type and volume + virtual const char* GetVolumeID(void) const = 0; + + // Return the "bare" volume name. For formats where the volume name + // is actually a number (e.g. DOS 3.3), this returns just the number. + // For formats without a volume name or number (e.g. CP/M), this returns + // nil, indicating that any attempt to change the volume name will fail. + virtual const char* GetBareVolumeName(void) const = 0; + + // Returns "false" if we only support read-only access to this FS type + virtual bool GetReadWriteSupported(void) const = 0; + + // Returns "true" if the filesystem shows evidence of damage. + virtual bool GetFSDamaged(void) const = 0; + + // Returns number of blocks recognized by the filesystem, or -1 if the + // FS isn't block-oriented (e.g. DOS 3.2/3.3) + virtual long GetFSNumBlocks(void) const { return -1; } + + // Get the next file in the list. Start by passing in NULL to get the + // head of the list. Returns NULL when the end of the list is reached. + A2File* GetNextFile(A2File* pCurrent) const; + + // Get a count of the files and directories on this disk. + long GetFileCount(void) const; + + /* + * Find a file by case-insensitive pathname. Assumes fssep=':'. The + * compare function can be overridden for systems like HFS, where "case + * insensitive" has a different meaning because of the native + * character set. + * + * The A2File* returned should not be deleted. + */ + typedef int (*StringCompareFunc)(const char* str1, const char* str2); + A2File* GetFileByName(const char* pathName, StringCompareFunc func = NULL); + + // This controls scanning for sub-volumes; must be set before Initialize(). + SubScanMode GetScanForSubVolumes(void) const { return fScanForSubVolumes; } + void SetScanForSubVolumes(SubScanMode val) { fScanForSubVolumes = val; } + + // some constants for non-ProDOS filesystems to use + enum { kFileAccessLocked = 0x01, kFileAccessUnlocked = 0xc3 }; + + + /* + * We use this as a filename separator character (i.e. between pathname + * components) in all filenames. It's useful to standardize on this + * so that behavior is consistent across all disk and archive formats. + * + * The choice of ':' is good because it's invalid in filenames on + * Windows, Mac OS, GS/OS, and pretty much anywhere else we could be + * running except for UNIX. It's valid under DOS 3.3, but since you + * can't have subdirectories under DOS there's no risk of confusion. + */ + enum { kDIFssep = ':' }; + + + /* + * Return the volume use map. This is a non-const function because + * it might need to do a "just-in-time" usage map update. It returns + * const to keep non-DiskFS classes from altering the map. + */ + const VolumeUsage* GetVolumeUsageMap(void) { + if (fVolumeUsage.GetInitialized()) + return &fVolumeUsage; + else + return NULL; + } + + /* + * Return the total space and free space, in either blocks or sectors + * as appropriate. "*pUnitSize" will be kBlockSize or kSectorSize. + */ + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const = 0; + + + /* + * Get the next volume in the list. Start by passing in NULL to get the + * head of the list. Returns NULL when the end of the list is reached. + */ + SubVolume* GetNextSubVolume(const SubVolume* pCurrent) const; + + /* + * Set some parameters to tell the DiskFS how to operate. Some of + * these are FS-specific, some may be general. + * + * Parameters are set in the current object and all sub-volume objects. + * + * The enum is part of the interface and must be rigidly defined, but + * it is also used to size an array so it can't be too sparse. + */ + typedef enum DiskFSParameter { + kParmUnknown = 0, + + kParm_CreateUnique = 1, // make new filenames unique + + kParmProDOS_AllowLowerCase = 10, // allow lower case and spaces + kParmProDOS_AllocSparse = 11, // don't store empty blocks + + kParmMax // must be last entry + } DiskFSParameter; + long GetParameter(DiskFSParameter parm); + void SetParameter(DiskFSParameter parm, long val); + + /* + * Flush changed data. + * + * The individual filesystems shouldn't generally do any caching; if + * they do, we would want a virtual "FlushFS()" that gets called by + * Flush. The better answer is to cache in DiskImg, which works for + * everything, and knows if the underlying storage is already in RAM. + * + * For the most part this just needs to recursively flush the DiskImg + * objects in all of the sub-volumes and then the current volume. This + * is a no-op in most cases, but if the archive is compressed this will + * cause a new, compressed archive to be created. + * + * The "mode" value determines whether or not we do "heavy" flushes. It's + * very handy to be able to do "slow" flushes for anything that is being + * written directly to disk (as opposed to being run through Deflate), + * so that the UI doesn't indicate that they're partially written when + * in fact they're fully written. + */ + DIError Flush(DiskImg::FlushMode mode); + + /* + * Set the read-only flag on our DiskImg and those of our subvolumes. + * Used to ensure that a DiskFS with un-flushed data can be deleted + * without corrupting the volume. + */ + void SetAllReadOnly(bool val); + + // debug dump + void DumpFileList(void); + +protected: + /* + * Set the DiskImg pointer. Updates the reference count in DiskImg. + */ + void SetDiskImg(DiskImg* pImg); + + // once added, we own the pDiskImg and the pDiskFS (DO NOT pass the + // same DiskImg or DiskFS in more than once!). Note this copies the + // fParmTable and other stuff (fScanForSubVolumes) from parent to child. + void AddSubVolumeToList(DiskImg* pDiskImg, DiskFS* pDiskFS); + // add files to fpA2Head/fpA2Tail + void AddFileToList(A2File* pFile); + // only need for hierarchical filesystems; insert file after pPrev + void InsertFileInList(A2File* pFile, A2File* pPrev); + // delete an entry + void DeleteFileFromList(A2File* pFile); + + // scan for damaged or suspicious files + void ScanForDamagedFiles(bool* pDamaged, bool* pSuspicious); + + // pointer to the DiskImg structure underlying this filesystem + DiskImg* fpImg; + + VolumeUsage fVolumeUsage; + SubScanMode fScanForSubVolumes; + + +private: + A2File* SkipSubdir(A2File* pSubdir); + void CopyInheritables(DiskFS* pNewFS); + void DeleteFileList(void); + void DeleteSubVolumeList(void); + + long fParmTable[kParmMax]; // for DiskFSParameter + + A2File* fpA2Head; + A2File* fpA2Tail; + SubVolume* fpSubVolumeHead; + SubVolume* fpSubVolumeTail; + +private: + DiskFS& operator=(const DiskFS&); + DiskFS(const DiskFS&); +}; + + +/* + * Apple II file class, representing a file on an Apple II volume. This is an + * abstract base class. + * + * There is a different sub-class for each filesystem type. The A2File object + * encapsulates all of the knowledge required to read a file from a disk + * image. + * + * The prev/next pointers, used to maintain a linked list of files, are only + * accessible from DiskFS functions. At some point we may want to rearrange + * the way this is handled, e.g. by not maintaining a list at all, so it's + * important that everything go through DiskFS requests. + * + * The FSFormat is made an explicit member, because sub-classes may not + * understand exactly where the file came from (e.g. was it DOS3.2 or + * DOS 3.3). Somebody might care. + * + * + * NOTE TO SELF: open files need to be generalized. Right now the internal + * implementations only allow a single open, which is okay for our purposes + * but bad for a general FS implementation. As it stands, you can't even + * open both forks at the same time on ProDOS/HFS. + * + * UMMM: The handling of "damaged" files could be more consistent. + */ +class DISKIMG_API A2File { +public: + friend class DiskFS; + + A2File(DiskFS* pDiskFS) : fpDiskFS(pDiskFS) { + fpPrev = fpNext = NULL; + fFileQuality = kQualityGood; + } + virtual ~A2File(void) {} + + /* + * All Apple II files have certain characteristics, of which ProDOS + * is roughly a superset. (Yes, you can have HFS on a IIgs, but + * all that fancy creator type stuff is decidedly Mac-centric. Still, + * we want to assume 4-byte file and aux types.) + * + * NEED: something distinguishing between files and disk images? + * + * NOTE: there is no guarantee that GetPathName will return unique values + * (duplicates are possible). We don't guarantee that you won't get an + * empty string back (it's valid to have an empty filename in the dir in + * DOS 3.3, and it's possible for other filesystems to be damaged). The + * pathname may receive some minor sanitizing, e.g. removal or conversion + * of high ASCII and control characters, but some filesystems (like HFS) + * make use of them. + * + * We do guarantee that the contents of subdirectories are grouped + * together. This makes it much easier to construct a hierarchy out of + * the linear list. This becomes important when dynamically adding + * files to a disk. + */ + virtual const char* GetFileName(void) const = 0; // name of this file + virtual const char* GetPathName(void) const = 0; // full path + virtual char GetFssep(void) const = 0; // '\0' if none + virtual long GetFileType(void) const = 0; + virtual long GetAuxType(void) const = 0; + virtual long GetAccess(void) const = 0; // ProDOS-style perms + virtual time_t GetCreateWhen(void) const = 0; + virtual time_t GetModWhen(void) const = 0; + virtual di_off_t GetDataLength(void) const = 0; // len of data fork + virtual di_off_t GetDataSparseLength(void) const = 0; // len w/o sparse areas + virtual di_off_t GetRsrcLength(void) const = 0; // len or -1 if no rsrc + virtual di_off_t GetRsrcSparseLength(void) const = 0; // len or -1 if no rsrc + virtual DiskImg::FSFormat GetFSFormat(void) const { + return fpDiskFS->GetDiskImg()->GetFSFormat(); + } + virtual bool IsDirectory(void) const { return false; } + virtual bool IsVolumeDirectory(void) const { return false; } + + /* + * Open a file. Treat the A2FileDescr like an fd. + */ + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false) = 0; + + /* + * This is called by the A2FileDescr object when somebody invokes Close(). + * The A2File object should remove the A2FileDescr from its list of open + * files and delete the storage. The implementation must not call the + * A2FileDescr's Close function, since that would cause a recursive loop. + * + * This should not be called by an application. + */ + virtual void CloseDescr(A2FileDescr* pOpenFile) = 0; + + /* + * This is only useful for hierarchical filesystems like ProDOS, + * where the order of items in the linear list is significant. It + * allows an unambiguous determination of which subdir a file resides + * in, even if somebody has sector-edited the filesystem so that two + * subdirs have the same name. (It's also a bit speedier to compare + * than pathname substrings would be.) + */ + virtual A2File* GetParent(void) const { return NULL; } + + /* + * Returns "true" if either fork of the file is open, "false" if not. + */ + virtual bool IsFileOpen(void) const = 0; + + virtual void Dump(void) const = 0; // debugging + + typedef enum FileQuality { + kQualityUnknown = 0, + kQualityGood, + kQualitySuspicious, + kQualityDamaged, + } FileQuality; + virtual FileQuality GetQuality(void) const { return fFileQuality; } + virtual void SetQuality(FileQuality quality); + virtual void ResetQuality(void); + + DiskFS* GetDiskFS(void) const { return fpDiskFS; } + +protected: + DiskFS* fpDiskFS; + virtual void SetParent(A2File* pParent) { /* do nothing */ } + +private: + A2File* GetPrev(void) const { return fpPrev; } + void SetPrev(A2File* pFile) { fpPrev = pFile; } + A2File* GetNext(void) const { return fpNext; } + void SetNext(A2File* pFile) { fpNext = pFile; } + + // Set when file structure is damaged and application should not try + // to open the file. + FileQuality fFileQuality; + + A2File* fpPrev; + A2File* fpNext; + + +private: + A2File& operator=(const A2File&); + A2File(const A2File&); +}; + + +/* + * Abstract representation of an open file. This contains all active state + * and all information required to read and write a file. + * + * Do not delete these objects; instead, invoke the Close method, so that they + * can be removed from the parents' list of open files. + * TODO: consider making the destructor "protected" + */ +class DISKIMG_API A2FileDescr { +public: + A2FileDescr(A2File* pFile) : fpFile(pFile), fProgressUpdateFunc(NULL) {} + virtual ~A2FileDescr(void) { fpFile = NULL; /*paranoia*/ } + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL) = 0; + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL) = 0; + virtual DIError Seek(di_off_t offset, DIWhence whence) = 0; + virtual di_off_t Tell(void) = 0; + virtual DIError Close(void) = 0; + + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) + const = 0; + virtual DIError GetStorage(long blockIdx, long* pBlock) + const = 0; + virtual long GetSectorCount(void) const = 0; + virtual long GetBlockCount(void) const = 0; + + virtual DIError Rewind(void) { return Seek(0, kSeekSet); } + + A2File* GetFile(void) const { return fpFile; } + + /* + * Progress update callback mechanism. Pass in the length or (for writes) + * expected length of the file. This invokes the callback with the + * lengths and some pointers. + * + * If the progress callback returns "true", progress continues. If it + * returns "false", the read or write function will return with + * kDIErrCancelled. + */ + typedef bool (*ProgressUpdater)(A2FileDescr* pFile, di_off_t max, + di_off_t current, void* vState); + void SetProgressUpdater(ProgressUpdater func, di_off_t max, void* state) { + fProgressUpdateFunc = func; + fProgressUpdateMax = max; + fProgressUpdateState = state; + } + void ClearProgressUpdater(void) { + fProgressUpdateFunc = NULL; + } + +protected: + A2File* fpFile; + + /* + * Internal utility functions for mapping blocks to sectors and vice-versa. + */ + virtual void TrackSectorToBlock(long track, long sector, long* pBlock, + bool* pSecondHalf) const + { + int numSectPerTrack = fpFile->GetDiskFS()->GetDiskImg()->GetNumSectPerTrack(); + assert(track < fpFile->GetDiskFS()->GetDiskImg()->GetNumTracks()); + assert(sector < numSectPerTrack); + long dblBlock = track * numSectPerTrack + sector; + *pBlock = dblBlock / 2; + *pSecondHalf = (dblBlock & 0x01) != 0; + } + virtual void BlockToTrackSector(long block, bool secondHalf, long* pTrack, + long* pSector) const + { + assert(block < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); + int numSectPerTrack = fpFile->GetDiskFS()->GetDiskImg()->GetNumSectPerTrack(); + int dblBlock = block * 2; + if (secondHalf) + dblBlock++; + *pTrack = dblBlock / numSectPerTrack; + *pSector = dblBlock % numSectPerTrack; + } + + /* + * Call this from FS-specific read/write functions on successful + * completion (and perhaps more often for filesystems with potentially + * large files, e.g. ProDOS/HFS). + * + * Test the return value; if "false", user wishes to cancel the op, and + * long read or write calls should return immediately. + */ + inline bool UpdateProgress(di_off_t current) { + if (fProgressUpdateFunc != NULL) { + return (*fProgressUpdateFunc)(this, fProgressUpdateMax, current, + fProgressUpdateState); + } else { + return true; + } + } + +private: + A2FileDescr& operator=(const A2FileDescr&); + A2FileDescr(const A2FileDescr&); + + /* storage for progress update goodies */ + ProgressUpdater fProgressUpdateFunc; + di_off_t fProgressUpdateMax; + void* fProgressUpdateState; +}; + +}; // namespace DiskImgLib + +#endif /*__DISK_IMG__*/ diff --git a/diskimg/DiskImgDetail.h b/diskimg/DiskImgDetail.h index 8e6e336..c295abc 100644 --- a/diskimg/DiskImgDetail.h +++ b/diskimg/DiskImgDetail.h @@ -1,2967 +1,3220 @@ -/* - * CiderPress - * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. - * See the file LICENSE for distribution terms. - */ -/* - * Sub-classes of the base classes defined in DiskImg.h. - * - * Most applications will not need to include this file, because the - * polymorphic interfaces do everything they need. If something needs to - * examine the actual directory structure of a file, it can do so through - * these declarations. - */ -#ifndef __DISKIMGDETAIL__ -#define __DISKIMGDETAIL__ - -#include "../prebuilt/NufxLib.h" -#define ZLIB_DLL -#include "../prebuilt/zlib.h" - -#include "DiskImg.h" - -#ifndef EXCISE_GPL_CODE -# include "libhfs/hfs.h" -#endif - -namespace DiskImgLib { - -/* - * =========================================================================== - * Outer wrappers - * =========================================================================== - */ - -/* - * Outer wrapper class, representing a compression utility or archive - * format that must be stripped away so we can get to the Apple II stuff. - * - * Outer wrappers usually have a filename embedded in them, representing - * the original name of the file. We want to use the extension from this - * name when evaluating the file contents. Usually. - */ -class OuterWrapper { -public: - OuterWrapper(void) {} - virtual ~OuterWrapper(void) {} - - // all sub-classes should have one of these - //static DIError Test(GenericFD* pGFD, long outerLength); - - // open the file and prepare to access it; fills out return values - // NOTE: pGFD must be a GFDFile. - virtual DIError Load(GenericFD* pOuterGFD, di_off_t outerLength, bool readOnly, - di_off_t* pWrapperLength, GenericFD** ppWrapperGFD) = 0; - - virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, - di_off_t wrapperLength) = 0; - - // set on recoverable errors, like a CRC failure - virtual bool IsDamaged(void) const = 0; - - // indicate that we don't have a "fast" flush - virtual bool HasFastFlush(void) const { return false; } - - virtual const char* GetExtension(void) const = 0; - -private: - OuterWrapper& operator=(const OuterWrapper&); - OuterWrapper(const OuterWrapper&); -}; - -class OuterGzip : public OuterWrapper { -public: - OuterGzip(void) { fWrapperDamaged = false; } - virtual ~OuterGzip(void) {} - - static DIError Test(GenericFD* pGFD, di_off_t outerLength); - virtual DIError Load(GenericFD* pGFD, di_off_t outerLength, bool readOnly, - di_off_t* pTotalLength, GenericFD** ppNewGFD); - virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, - di_off_t wrapperLength); - - virtual bool IsDamaged(void) const { return fWrapperDamaged; } - - virtual const char* GetExtension(void) const { return NULL; } - -private: - DIError ExtractGzipImage(gzFile gzfp, char** pBuf, di_off_t* pLength); - DIError CloseGzip(void); - - // Largest possible ProDOS volume; quite a bit to hold in RAM. Add a - // little extra for .hdv format. - enum { kMaxUncompressedSize = kGzipMax +256 }; - - bool fWrapperDamaged; -}; - -class OuterZip : public OuterWrapper { -public: - OuterZip(void) : fStoredFileName(NULL), fExtension(NULL) {} - virtual ~OuterZip(void) { - delete[] fStoredFileName; - delete[] fExtension; - } - - static DIError Test(GenericFD* pGFD, di_off_t outerLength); - virtual DIError Load(GenericFD* pGFD, di_off_t outerLength, bool readOnly, - di_off_t* pTotalLength, GenericFD** ppNewGFD); - virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, - di_off_t wrapperLength); - - virtual bool IsDamaged(void) const { return false; } - - virtual const char* GetExtension(void) const { return fExtension; } - -private: - class LocalFileHeader { - public: - LocalFileHeader(void) : - fVersionToExtract(0), - fGPBitFlag(0), - fCompressionMethod(0), - fLastModFileTime(0), - fLastModFileDate(0), - fCRC32(0), - fCompressedSize(0), - fUncompressedSize(0), - fFileNameLength(0), - fExtraFieldLength(0), - fFileName(NULL) - {} - virtual ~LocalFileHeader(void) { delete[] fFileName; } - - DIError Read(GenericFD* pGFD); - DIError Write(GenericFD* pGFD); - void SetFileName(const char* name); - - // unsigned long fSignature; - unsigned short fVersionToExtract; - unsigned short fGPBitFlag; - unsigned short fCompressionMethod; - unsigned short fLastModFileTime; - unsigned short fLastModFileDate; - unsigned long fCRC32; - unsigned long fCompressedSize; - unsigned long fUncompressedSize; - unsigned short fFileNameLength; - unsigned short fExtraFieldLength; - unsigned char* fFileName; - // extra field - - enum { - kSignature = 0x04034b50, - kLFHLen = 30, // LocalFileHdr len, excl. var fields - }; - - void Dump(void) const; - }; - - class CentralDirEntry { - public: - CentralDirEntry(void) : - fVersionMadeBy(0), - fVersionToExtract(0), - fGPBitFlag(0), - fCompressionMethod(0), - fLastModFileTime(0), - fLastModFileDate(0), - fCRC32(0), - fCompressedSize(0), - fUncompressedSize(0), - fFileNameLength(0), - fExtraFieldLength(0), - fFileCommentLength(0), - fDiskNumberStart(0), - fInternalAttrs(0), - fExternalAttrs(0), - fLocalHeaderRelOffset(0), - fFileName(NULL), - fFileComment(NULL) - {} - virtual ~CentralDirEntry(void) { - delete[] fFileName; - delete[] fFileComment; - } - - DIError Read(GenericFD* pGFD); - DIError Write(GenericFD* pGFD); - void SetFileName(const char* name); - - // unsigned long fSignature; - unsigned short fVersionMadeBy; - unsigned short fVersionToExtract; - unsigned short fGPBitFlag; - unsigned short fCompressionMethod; - unsigned short fLastModFileTime; - unsigned short fLastModFileDate; - unsigned long fCRC32; - unsigned long fCompressedSize; - unsigned long fUncompressedSize; - unsigned short fFileNameLength; - unsigned short fExtraFieldLength; - unsigned short fFileCommentLength; - unsigned short fDiskNumberStart; - unsigned short fInternalAttrs; - unsigned long fExternalAttrs; - unsigned long fLocalHeaderRelOffset; - unsigned char* fFileName; - // extra field - unsigned char* fFileComment; // alloc with new[] - - void Dump(void) const; - - enum { - kSignature = 0x02014b50, - kCDELen = 46, // CentralDirEnt len, excl. var fields - }; - }; - - class EndOfCentralDir { - public: - EndOfCentralDir(void) : - fDiskNumber(0), - fDiskWithCentralDir(0), - fNumEntries(0), - fTotalNumEntries(0), - fCentralDirSize(0), - fCentralDirOffset(0), - fCommentLen(0) - {} - virtual ~EndOfCentralDir(void) {} - - DIError ReadBuf(const unsigned char* buf, int len); - DIError Write(GenericFD* pGFD); - - // unsigned long fSignature; - unsigned short fDiskNumber; - unsigned short fDiskWithCentralDir; - unsigned short fNumEntries; - unsigned short fTotalNumEntries; - unsigned long fCentralDirSize; - unsigned long fCentralDirOffset; // offset from first disk - unsigned short fCommentLen; - // archive comment - - enum { - kSignature = 0x06054b50, - kEOCDLen = 22, // EndOfCentralDir len, excl. comment - }; - - void Dump(void) const; - }; - - enum { - kDataDescriptorSignature = 0x08074b50, - - kMaxCommentLen = 65535, // longest possible in ushort - kMaxEOCDSearch = kMaxCommentLen + EndOfCentralDir::kEOCDLen, - - kZipFssep = '/', - kDefaultVersion = 20, - kMaxUncompressedSize = kGzipMax +256, - }; - enum { - kCompressStored = 0, // no compression - //kCompressShrunk = 1, - //kCompressImploded = 6, - kCompressDeflated = 8, // standard deflate - }; - - static DIError ReadCentralDir(GenericFD* pGFD, di_off_t outerLength, - CentralDirEntry* pDirEntry); - DIError ExtractZipEntry(GenericFD* pOuterGFD, CentralDirEntry* pCDE, - unsigned char** pBuf, di_off_t* pLength); - DIError InflateGFDToBuffer(GenericFD* pGFD, unsigned long compSize, - unsigned long uncompSize, unsigned char* buf); - DIError DeflateGFDToGFD(GenericFD* pDst, GenericFD* pSrc, di_off_t length, - di_off_t* pCompLength, unsigned long* pCRC); - -private: - void SetExtension(const char* ext); - void SetStoredFileName(const char* name); - void GetMSDOSTime(unsigned short* pDate, unsigned short* pTime); - void DOSTime(time_t when, unsigned short* pDate, unsigned short* pTime); - - char* fStoredFileName; - char* fExtension; -}; - - -/* - * =========================================================================== - * Image wrappers - * =========================================================================== - */ - -/* - * Image wrapper class, representing the format of the Windows files. - * Might be "raw" data, might be data with a header, might be a complex - * or compressed format that must be extracted to a buffer. - */ -class ImageWrapper { -public: - ImageWrapper(void) {} - virtual ~ImageWrapper(void) {} - - // all sub-classes should have one of these - // static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - - // open the file and prepare to access it; fills out return values - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) = 0; - - // fill out the wrapper, using the specified parameters - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD) = 0; - - // push altered data to the wrapper GFD - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen) = 0; - - // set the storage name (used by some formats) - virtual void SetStorageName(const char* name) { - // default implementation - assert(false); - } - - // indicate that we have a "fast" flush - virtual bool HasFastFlush(void) const = 0; - - // set by "Prep" on recoverable errors, like a CRC failure, for some fmts - virtual bool IsDamaged(void) const { return false; } - - // if this wrapper format includes a file comment, return it - //virtual const char* GetComment(void) const { return NULL; } - - /* - * Some additional goodies required for accessing variable-length nibble - * tracks in TrackStar images. A default implementation is provided and - * used for everything but TrackStar. - */ - virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const - { - if (physical == DiskImg::kPhysicalFormatNib525_6656) - return kTrackLenNib525; - else if (physical == DiskImg::kPhysicalFormatNib525_6384) - return kTrackLenNb2525; - else { - assert(false); - return -1; - } - } - virtual void SetNibbleTrackLength(int track, int length) { /*do nothing*/ } - virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const - { - if (physical == DiskImg::kPhysicalFormatNib525_6656 || - physical == DiskImg::kPhysicalFormatNib525_6384) - { - /* fixed-length tracks */ - return GetNibbleTrackLength(physical, 0) * track; - } else { - assert(false); - return -1; - } - } - // TrackStar images can have more, but otherwise all nibble images have 35 - virtual int GetNibbleNumTracks(void) const - { - return kTrackCount525; - } - -private: - ImageWrapper& operator=(const ImageWrapper&); - ImageWrapper(const ImageWrapper&); -}; - - -class Wrapper2MG : public ImageWrapper { -public: - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return true; } - //virtual const char* GetComment(void) const { return NULL; } - // (need to hold TwoImgHeader in the struct, rather than as temp, or - // need to copy the comment out into Wrapper2MG storage e.g. StorageName) -}; - -class WrapperNuFX : public ImageWrapper { -public: - WrapperNuFX(void) : fpArchive(NULL), fThreadIdx(0), fStorageName(NULL), - fCompressType(kNuThreadFormatLZW2) - {} - virtual ~WrapperNuFX(void) { CloseNuFX(); delete[] fStorageName; } - - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return false; } - - void SetStorageName(const char* name) { - delete[] fStorageName; - if (name != NULL) { - fStorageName = new char[strlen(name)+1]; - strcpy(fStorageName, name); - } else - fStorageName = NULL; - } - void SetCompressType(NuThreadFormat format) { fCompressType = format; } - -private: - enum { kDefaultStorageFssep = ':' }; - static NuResult ErrMsgHandler(NuArchive* pArchive, void* vErrorMessage); - static DIError OpenNuFX(const char* pathName, NuArchive** ppArchive, - NuThreadIdx* pThreadIdx, long* pLength, bool readOnly); - DIError GetNuFXDiskImage(NuArchive* pArchive, NuThreadIdx threadIdx, - long length, char** ppData); - static char* GenTempPath(const char* path); - DIError CloseNuFX(void); - void UNIXTimeToDateTime(const time_t* pWhen, NuDateTime *pDateTime); - - NuArchive* fpArchive; - NuThreadIdx fThreadIdx; - char* fStorageName; - NuThreadFormat fCompressType; -}; - -class WrapperDiskCopy42 : public ImageWrapper { -public: - WrapperDiskCopy42(void) : fStorageName(NULL), fBadChecksum(false) - {} - virtual ~WrapperDiskCopy42(void) { delete[] fStorageName; } - - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - void SetStorageName(const char* name) { - delete[] fStorageName; - if (name != NULL) { - fStorageName = new char[strlen(name)+1]; - strcpy(fStorageName, name); - } else - fStorageName = NULL; - } - - virtual bool HasFastFlush(void) const { return false; } - virtual bool IsDamaged(void) const { return fBadChecksum; } - -private: - typedef struct DC42Header DC42Header; - static void DumpHeader(const DC42Header* pHeader); - void InitHeader(DC42Header* pHeader); - static int ReadHeader(GenericFD* pGFD, DC42Header* pHeader); - DIError WriteHeader(GenericFD* pGFD, const DC42Header* pHeader); - static DIError ComputeChecksum(GenericFD* pGFD, - unsigned long* pChecksum); - - char* fStorageName; - bool fBadChecksum; -}; - -class WrapperDDD : public ImageWrapper { -public: - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return false; } - - enum { - kMaxDDDZeroCount = 4, // 3 observed, 4 suspected - }; - -private: - class BitBuffer; - enum { - kNumTracks = 35, - kNumSectors = 16, - kSectorSize = 256, - kTrackLen = kNumSectors * kSectorSize, - }; - - static DIError CheckForRuns(GenericFD* pGFD); - static DIError Unpack(GenericFD* pGFD, GenericFD** ppNewGFD, - short* pDiskVolNum); - - static DIError UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD, - short* pDiskVolNum); - static bool UnpackTrack(BitBuffer* pBitBuffer, unsigned char* trackBuf); - static DIError PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD, - short diskVolNum); - static void PackTrack(const unsigned char* trackBuf, BitBuffer* pBitBuf); - static void ComputeFreqCounts(const unsigned char* trackBuf, - unsigned short* freqCounts); - static void ComputeFavorites(unsigned short* freqCounts, - unsigned char* favorites); - - short fDiskVolumeNum; -}; - -class WrapperSim2eHDV : public ImageWrapper { -public: - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return true; } -}; - -class WrapperTrackStar : public ImageWrapper { -public: - enum { - kTrackStarNumTracks = 40, - kFileTrackStorageLen = 6656, - kMaxTrackLen = kFileTrackStorageLen - (128+1+2), // header + footer - kCommentFieldLen = 0x2e, - }; - - WrapperTrackStar(void) : fStorageName(NULL) { - memset(&fNibbleTrackInfo, 0, sizeof(fNibbleTrackInfo)); - fNibbleTrackInfo.numTracks = -1; - } - virtual ~WrapperTrackStar(void) { delete[] fStorageName; } - - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return false; } - - virtual void SetStorageName(const char* name) - { - delete[] fStorageName; - if (name != NULL) { - fStorageName = new char[strlen(name)+1]; - strcpy(fStorageName, name); - } else - fStorageName = NULL; - } - -private: - static DIError VerifyTrack(int track, const unsigned char* trackBuf); - DIError Unpack(GenericFD* pGFD, GenericFD** ppNewGFD); - DIError UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD); - - int fImageTracks; - char* fStorageName; - - /* - * Data structure for managing nibble images with variable-length tracks. - */ - typedef struct { - int numTracks; // should be 35 or 40 - int length[kMaxNibbleTracks525]; - int offset[kMaxNibbleTracks525]; - } NibbleTrackInfo; - NibbleTrackInfo fNibbleTrackInfo; // count and lengths for variable formats - - // nibble images can have variable-length data fields - virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const - { - assert(physical == DiskImg::kPhysicalFormatNib525_Var); - assert(fNibbleTrackInfo.numTracks > 0); - - return fNibbleTrackInfo.length[track]; - } - virtual void SetNibbleTrackLength(int track, int length); -#if 0 - { - assert(track >= 0); - assert(length > 0 && length <= kMaxTrackLen); - assert(track < fNibbleTrackInfo.numTracks); - - fNibbleTrackInfo.length[track] = length; - } -#endif - virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const - { - assert(physical == DiskImg::kPhysicalFormatNib525_Var); - assert(fNibbleTrackInfo.numTracks > 0); - - return fNibbleTrackInfo.offset[track]; - } - virtual int GetNibbleNumTracks(void) const - { - return kTrackStarNumTracks; - } -}; - -class WrapperFDI : public ImageWrapper { -public: - WrapperFDI(void) {} - virtual ~WrapperFDI(void) {} - - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return false; } - - enum { - kSignatureLen = 27, - kCreatorLen = 30, - kCommentLen = 80, - }; - -private: - static const char* kFDIMagic; - - /* what type of disk is this? */ - typedef enum DiskType { - kDiskType8 = 0, - kDiskType525 = 1, - kDiskType35 = 2, - kDiskType3 = 3 - } DiskType; - - /* - * Contents of FDI header. - */ - typedef struct FDIHeader { - char signature[kSignatureLen+1]; - char creator[kCreatorLen+1]; - // CR + LF - char comment[kCommentLen+1]; - // MS-DOS EOF - unsigned short version; - unsigned short lastTrack; - unsigned char lastHead; - unsigned char type; // DiskType enum - unsigned char rotSpeed; - unsigned char flags; - unsigned char tpi; - unsigned char headWidth; - unsigned short reserved; - // track descriptors follow, at byte 152 - } FDIHeader; - - /* - * Header for pulse-index streams track. - */ - typedef struct PulseIndexHeader { - long numPulses; - long avgStreamLen; - int avgStreamCompression; - long minStreamLen; - int minStreamCompression; - long maxStreamLen; - int maxStreamCompression; - long idxStreamLen; - int idxStreamCompression; - - unsigned long* avgStream; // 4 bytes/pulse - unsigned long* minStream; // 4 bytes/pulse; optional - unsigned long* maxStream; // 4 bytes/pulse; optional - unsigned long* idxStream; // 2 bytes/pulse; optional? - } PulseIndexHeader; - - enum { - kTrackDescrOffset = 152, - kMaxHeads = 2, - kMaxHeaderBlockTracks = 180, // max 90 double-sided cylinders - kMinHeaderLen = 512, - kMinVersion = 0x0200, // v2.0 - - kMaxNibbleTracks35 = 80, // 80 double-sided tracks - kNibbleBufLen = 10240, // max seems to be a little under 10K - kBitBufferSize = kNibbleBufLen + (kNibbleBufLen / 4), - - kMaxSectors35 = 12, // max #of sectors per track - //kBytesPerSector35 = 512, // bytes per sector on 3.5" disk - - kPulseStreamDataOffset = 16, // start of header to avg stream - - kBitRate525 = 250000, // 250Kbits/sec - }; - - /* meaning of the two-bit compression format value */ - typedef enum CompressedFormat { - kCompUncompressed = 0, - kCompHuffman = 1, - } CompressedFormat; - - /* node in the Huffman tree */ - typedef struct HuffNode { - unsigned short val; - struct HuffNode* left; - struct HuffNode* right; - } HuffNode; - - /* - * Keep a copy of the header around while we work. None of the formats - * we're interested in have more than kMaxHeaderBlockTracks tracks in - * them, so we don't need anything beyond the initial 512-byte header. - */ - unsigned char fHeaderBuf[kMinHeaderLen]; - - static void UnpackHeader(const unsigned char* headerBuf, FDIHeader* hdr); - static void DumpHeader(const FDIHeader* pHdr); - - DIError Unpack525(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, - int numHeads); - DIError Unpack35(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, - int numHeads, LinearBitmap** ppBadBlockMap); - DIError PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD); - - DIError UnpackDisk525(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, - int numHeads); - DIError UnpackDisk35(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, - int numHeads, LinearBitmap* pBadBlockMap); - void GetTrackInfo(int trk, int* pType, int* pLength256); - - int BitRate35(int trk); - void FixBadNibbles(unsigned char* nibbleBuf, long nibbleLen); - bool DecodePulseTrack(const unsigned char* inputBuf, long inputLen, - int bitRate, unsigned char* nibbleBuf, long* pNibbleLen); - bool UncompressPulseStream(const unsigned char* inputBuf, long inputLen, - unsigned long* outputBuf, long numPulses, int format, int bytesPerPulse); - bool ExpandHuffman(const unsigned char* inputBuf, long inputLen, - unsigned long* outputBuf, long numPulses); - const unsigned char* HuffExtractTree(const unsigned char* inputBuf, - HuffNode* pNode, unsigned char* pBits, unsigned char* pBitMask); - const unsigned char* HuffExtractValues16(const unsigned char* inputBuf, - HuffNode* pNode); - const unsigned char* HuffExtractValues8(const unsigned char* inputBuf, - HuffNode* pNode); - void HuffFreeNodes(HuffNode* pNode); - unsigned long HuffSignExtend16(unsigned long val); - unsigned long HuffSignExtend8(unsigned long val); - bool ConvertPulseStreamsToNibbles(PulseIndexHeader* pHdr, int bitRate, - unsigned char* nibbleBuf, long* pNibbleLen); - bool ConvertPulsesToBits(const unsigned long* avgStream, - const unsigned long* minStream, const unsigned long* maxStream, - const unsigned long* idxStream, int numPulses, int maxIndex, - int indexOffset, unsigned long totalAvg, int bitRate, - unsigned char* outputBuf, int* pOutputLen); - int MyRand(void); - bool ConvertBitsToNibbles(const unsigned char* bitBuffer, int bitCount, - unsigned char* nibbleBuf, long* pNibbleLen); - - - int fImageTracks; - char* fStorageName; - - - /* - * Data structure for managing nibble images with variable-length tracks. - */ - typedef struct { - int numTracks; // expect 35 or 40 for 5.25" - int length[kMaxNibbleTracks525]; - int offset[kMaxNibbleTracks525]; - } NibbleTrackInfo; - NibbleTrackInfo fNibbleTrackInfo; // count and lengths for variable formats - - // nibble images can have variable-length data fields - virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const - { - assert(physical == DiskImg::kPhysicalFormatNib525_Var); - assert(fNibbleTrackInfo.numTracks > 0); - - return fNibbleTrackInfo.length[track]; - } - virtual void SetNibbleTrackLength(int track, int length); - virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const - { - assert(physical == DiskImg::kPhysicalFormatNib525_Var); - assert(fNibbleTrackInfo.numTracks > 0); - - return fNibbleTrackInfo.offset[track]; - } - virtual int GetNibbleNumTracks(void) const - { - return fNibbleTrackInfo.numTracks; - } -}; - - -class WrapperUnadornedNibble : public ImageWrapper { -public: - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return true; } -}; - -class WrapperUnadornedSector : public ImageWrapper { -public: - static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); - virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, - di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, - DiskImg::SectorOrder* pOrder, short* pDiskVolNum, - LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); - virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, - DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, - di_off_t* pWrappedLength, GenericFD** pDataFD); - virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, - di_off_t dataLen, di_off_t* pWrappedLen); - virtual bool HasFastFlush(void) const { return true; } -}; - - -/* - * =========================================================================== - * Non-FS DiskFSs - * =========================================================================== - */ - -/* - * A "raw" disk, i.e. no filesystem is known. Useful as a placeholder - * for applications that demand a DiskFS object even when the filesystem - * isn't known. - */ -class DISKIMG_API DiskFSUnknown : public DiskFS { -public: - DiskFSUnknown(void) : DiskFS() { - strcpy(fDiskVolumeName, "[Unknown]"); - strcpy(fDiskVolumeID, "Unknown FS"); - } - virtual ~DiskFSUnknown(void) {} - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return kDIErrNone; - } - - virtual const char* GetVolumeName(void) const { return fDiskVolumeName; } - virtual const char* GetVolumeID(void) const { return fDiskVolumeID; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - - // Use this if *something* is known about the filesystem, e.g. the - // partition type on a MacPart disk. - void SetVolumeInfo(const char* descr) { - if (strlen(descr) > kMaxVolumeName) - return; - - fDiskVolumeName[0] = '['; - strcpy(fDiskVolumeName+1, descr); - strcat(fDiskVolumeName, "]"); - strcpy(fDiskVolumeID, "Unknown FS - "); - strcat(fDiskVolumeID, descr); - } - -private: - enum { kMaxVolumeName = 64 }; - - char fDiskVolumeName[kMaxVolumeName+3]; - char fDiskVolumeID[kMaxVolumeName + 20]; -}; - - -/* - * Generic "container" DiskFS class. Contains some common functions shared - * among classes that are just containers for other filesystems. This class - * is not expected to be instantiated. - * - * TODO: create a common OpenSubVolume() function. - */ -class DISKIMG_API DiskFSContainer : public DiskFS { -public: - DiskFSContainer(void) : DiskFS() {} - virtual ~DiskFSContainer(void) {} - -protected: - virtual const char* GetDebugName(void) = 0; - virtual DIError CreatePlaceholder(long startBlock, long numBlocks, - const char* partName, const char* partType, - DiskImg** ppNewImg, DiskFS** ppNewFS); - virtual void SetVolumeUsageMap(void); -}; - -/* - * UNIDOS disk, an 800K floppy with two 400K DOS 3.3 volumes on it. - * - * The disk itself has no files; instead, it has two embedded sub-volumes. - */ -class DISKIMG_API DiskFSUNIDOS : public DiskFSContainer { -public: - DiskFSUNIDOS(void) : DiskFSContainer() {} - virtual ~DiskFSUNIDOS(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - static DIError TestWideFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return "[UNIDOS]"; } - virtual const char* GetVolumeID(void) const { return "[UNIDOS]"; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - -private: - virtual const char* GetDebugName(void) { return "UNIDOS"; } - DIError Initialize(void); - DIError OpenSubVolume(int idx); -}; - -/* - * OzDOS disk, an 800K floppy with two 400K DOS 3.3 volumes on it. They - * put the files for disk 1 in the odd sectors and the files for disk 2 - * in the even sectors (the top and bottom halves of a 512-byte block). - * - * The disk itself has no files; instead, it has two embedded sub-volumes. - * Because of the funky layout, we have to use the "sector pairing" feature - * of DiskImg to treat this like a DOS 3.3 disk. - */ -class DISKIMG_API DiskFSOzDOS : public DiskFSContainer { -public: - DiskFSOzDOS(void) : DiskFSContainer() {} - virtual ~DiskFSOzDOS(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return "[OzDOS]"; } - virtual const char* GetVolumeID(void) const { return "[OzDOS]"; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - -private: - virtual const char* GetDebugName(void) { return "OzDOS"; } - DIError Initialize(void); - DIError OpenSubVolume(int idx); -}; - -/* - * CFFA volume. A potentially very large volume with multiple partitions. - * - * This DiskFS is just a container that describes the position and sizes - * of the sub-volumes. - */ -class DISKIMG_API DiskFSCFFA : public DiskFSContainer { -public: - DiskFSCFFA(void) : DiskFSContainer() {} - virtual ~DiskFSCFFA(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return "[CFFA]"; } - virtual const char* GetVolumeID(void) const { return "[CFFA]"; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - -private: - virtual const char* GetDebugName(void) { return "CFFA"; } - - static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, - DiskImg::FSFormat* pFormatFound); - static DIError OpenSubVolume(DiskImg* pImg, long startBlock, - long numBlocks, bool scanOnly, DiskImg** ppNewImg, DiskFS** ppNewFS); - DIError Initialize(void); - DIError FindSubVolumes(void); - DIError AddVolumeSeries(int start, int count, long blocksPerVolume, - long& startBlock, long& totalBlocksLeft); - - enum { - kMinInterestingBlocks = 65536 + 1024, // less than this, ignore - kEarlyVolExpectedSize = 65536, // 32MB in 512-byte blocks - kOneGB = 1024*1024*(1024/512), // 1GB in 512-byte blocks - }; -}; - - -/* - * Macintosh-style partitioned disk image. - * - * This DiskFS is just a container that describes the position and sizes - * of the sub-volumes. - */ -class DISKIMG_API DiskFSMacPart : public DiskFSContainer { -public: - DiskFSMacPart(void) : DiskFSContainer() {} - virtual ~DiskFSMacPart(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return "[MacPartition]"; } - virtual const char* GetVolumeID(void) const { return "[MacPartition]"; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - -private: - virtual const char* GetDebugName(void) { return "MacPart"; } - - struct PartitionMap; // fwd - struct DriverDescriptorRecord; // fwd - static void UnpackDDR(const unsigned char* buf, - DriverDescriptorRecord* pDDR); - static void DumpDDR(const DriverDescriptorRecord* pDDR); - static void UnpackPartitionMap(const unsigned char* buf, - PartitionMap* pMap); - static void DumpPartitionMap(long block, const PartitionMap* pMap); - - static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); - DIError OpenSubVolume(const PartitionMap* pMap); - DIError Initialize(void); - DIError FindSubVolumes(void); - - enum { - kMinInterestingBlocks = 2048, // less than this, ignore - kDDRSignature = 0x4552, // 'ER' - kPartitionSignature = 0x504d, // 'PM' - }; -}; - - -/* - * Partitioning for Joachim Lange's MicroDrive card. - * - * This DiskFS is just a container that describes the position and sizes - * of the sub-volumes. - */ -class DISKIMG_API DiskFSMicroDrive : public DiskFSContainer { -public: - DiskFSMicroDrive(void) : DiskFSContainer() {} - virtual ~DiskFSMicroDrive(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return "[MicroDrive]"; } - virtual const char* GetVolumeID(void) const { return "[MicroDrive]"; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - -private: - virtual const char* GetDebugName(void) { return "MicroDrive"; } - - struct PartitionMap; // fwd - static void UnpackPartitionMap(const unsigned char* buf, - PartitionMap* pMap); - static void DumpPartitionMap(const PartitionMap* pMap); - - static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); - DIError OpenSubVolume(long startBlock, long numBlocks); - DIError OpenVol(int idx, long startBlock, long numBlocks); - DIError Initialize(void); - DIError FindSubVolumes(void); - - enum { - kMinInterestingBlocks = 2048, // less than this, ignore - kPartitionSignature = 0xccca, // 'JL' in little-endian high-ASCII - }; -}; - - -/* - * Partitioning for Parsons Engineering FocusDrive card. - * - * This DiskFS is just a container that describes the position and sizes - * of the sub-volumes. - */ -class DISKIMG_API DiskFSFocusDrive : public DiskFSContainer { -public: - DiskFSFocusDrive(void) : DiskFSContainer() {} - virtual ~DiskFSFocusDrive(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return "[FocusDrive]"; } - virtual const char* GetVolumeID(void) const { return "[FocusDrive]"; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - -private: - virtual const char* GetDebugName(void) { return "FocusDrive"; } - - struct PartitionMap; // fwd - static void UnpackPartitionMap(const unsigned char* buf, - const unsigned char* nameBuf, PartitionMap* pMap); - static void DumpPartitionMap(const PartitionMap* pMap); - - static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); - DIError OpenSubVolume(long startBlock, long numBlocks, - const char* name); - DIError OpenVol(int idx, long startBlock, long numBlocks, - const char* name); - DIError Initialize(void); - DIError FindSubVolumes(void); - - enum { - kMinInterestingBlocks = 2048, // less than this, ignore - }; -}; - - -/* - * =========================================================================== - * DOS 3.2/3.3 - * =========================================================================== - */ - -class A2FileDOS; - -/* - * DOS 3.2/3.3 disk. - */ -class DISKIMG_API DiskFSDOS33 : public DiskFS { -public: - DiskFSDOS33(void) : DiskFS() { - fVTOCLoaded = false; - fDiskIsGood = false; - } - virtual ~DiskFSDOS33(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(initMode); - } - virtual DIError Format(DiskImg* pDiskImg, const char* volName); - - virtual const char* GetVolumeName(void) const { return fDiskVolumeName; } - virtual const char* GetVolumeID(void) const { return fDiskVolumeID; } - virtual const char* GetBareVolumeName(void) const { - // this is fragile -- skip over the "DOS" part, return 3 digits - assert(strlen(fDiskVolumeName) > 3); - return fDiskVolumeName+3; - } - virtual bool GetReadWriteSupported(void) const { return true; } - virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const; - virtual DIError NormalizePath(const char* path, char fssep, - char* normalizedBuf, int* pNormalizedBufLen); - virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); - virtual DIError DeleteFile(A2File* pFile); - virtual DIError RenameFile(A2File* pFile, const char* newName); - virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, - long accessFlags); - virtual DIError RenameVolume(const char* newName); - - /* - * Unique to DOS 3.3 disks. - */ - int GetDiskVolumeNum(void) const { return fDiskVolumeNum; } - void SetDiskVolumeNum(int val); - - static bool IsValidFileName(const char* name); - static bool IsValidVolumeName(const char* name); - - // utility function - static void LowerASCII(unsigned char* buf, long len); - static void ReplaceFssep(char* str, char replacement); - - enum { - kMinTracks = 17, // need to put the catalog track here - kMaxTracks = 50, - kMaxCatalogSectors = 64, // two tracks on a 32-sector disk - }; - - /* a T/S pair */ - typedef struct TrackSector { - char track; - char sector; - } TrackSector; - - friend class A2FDDOS; // for Write - -private: - DIError Initialize(InitMode initMode); - DIError ReadVTOC(void); - void UpdateVolumeNum(void); - void DumpVTOC(void); - void SetSectorUsage(long track, long sector, - VolumeUsage::ChunkPurpose purpose); - void FixVolumeUsageMap(void); - DIError ReadCatalog(void); - DIError ProcessCatalogSector(int catTrack, int catSect, - const unsigned char* sctBuf); - DIError GetFileLengths(void); - DIError ComputeLength(A2FileDOS* pFile, const TrackSector* tsList, - int tsCount); - DIError TrimLastSectorUp(A2FileDOS* pFile, TrackSector lastTS); - void MarkFileUsage(A2FileDOS* pFile, TrackSector* tsList, int tsCount, - TrackSector* indexList, int indexCount); - //DIError TrimLastSectorDown(A2FileDOS* pFile, unsigned short* tsBuf, - // int maxZeroCount); - void DoNormalizePath(const char* name, char fssep, char* outBuf); - DIError MakeFileNameUnique(char* fileName); - DIError GetFreeCatalogEntry(TrackSector* pCatSect, int* pCatEntry, - unsigned char* sctBuf, A2FileDOS** ppPrevEntry); - void CreateDirEntry(unsigned char* sctBuf, int catEntry, - const char* fileName, TrackSector* pTSSect, unsigned char fileType, - int access); - void FreeTrackSectors(TrackSector* pList, int count); - - bool CheckDiskIsGood(void); - - DIError WriteDOSTracks(int sectPerTrack); - - DIError ScanVolBitmap(void); - DIError LoadVolBitmap(void); - DIError SaveVolBitmap(void); - void FreeVolBitmap(void); - DIError AllocSector(TrackSector* pTS); - DIError CreateEmptyBlockMap(bool withDOS); - bool GetSectorUseEntry(long track, int sector) const; - void SetSectorUseEntry(long track, int sector, bool inUse); - inline unsigned long GetVTOCEntry(const unsigned char* pVTOC, - long track) const; - - // Largest interesting volume is 400K (50 tracks, 32 sectors), but - // we may be looking at it in 16-sector mode, so max tracks is 100. - enum { - kMaxInterestingTracks = 100, - kSectorSize = 256, - kDefaultVolumeNum = 254, - kMaxExtensionLen = 4, // used when normalizing; ".gif" is 4 - }; - - // DOS track images, for initializing disk images - static const unsigned char gDOS33Tracks[]; - static const unsigned char gDOS32Tracks[]; - - /* some fields from the VTOC */ - int fFirstCatTrack; - int fFirstCatSector; - int fVTOCVolumeNumber; - int fVTOCNumTracks; - int fVTOCNumSectors; - - /* private data */ - int fDiskVolumeNum; // usually 254 - char fDiskVolumeName[7]; // "DOS" + num, e.g. "DOS001", "DOS254" - char fDiskVolumeID[32]; // sizeof "DOS 3.3 Volume " +3 +1 - unsigned char fVTOC[kSectorSize]; - bool fVTOCLoaded; - - /* - * There are some things we need to be careful of when reading the - * catalog track, like bad links and infinite loops. By storing a list - * of known good catalog sectors, we only have to handle that stuff once. - * The catalog doesn't grow or shrink, so this never needs to be updated. - */ - TrackSector fCatalogSectors[kMaxCatalogSectors]; - - bool fDiskIsGood; -}; - -/* - * File descriptor for an open DOS file. - */ -class DISKIMG_API A2FDDOS : public A2FileDescr { -public: - A2FDDOS(A2File* pFile) : A2FileDescr(pFile) { - fTSList = NULL; - fIndexList = NULL; - fOffset = 0; - fModified = false; - } - virtual ~A2FDDOS(void) { - delete[] fTSList; - delete[] fIndexList; - //fTSList = fIndexList = NULL; - } - - //typedef DiskFSDOS33::TrackSector TrackSector; - - friend class A2FileDOS; - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Seek(di_off_t offset, DIWhence whence); - virtual di_off_t Tell(void); - virtual DIError Close(void); - - virtual long GetSectorCount(void) const; - virtual long GetBlockCount(void) const; - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; - virtual DIError GetStorage(long blockIdx, long* pBlock) const; - -private: - typedef DiskFSDOS33::TrackSector TrackSector; - - TrackSector* fTSList; // T/S entries for data sectors - int fTSCount; - TrackSector* fIndexList; // T/S entries for T/S list sectors - int fIndexCount; - di_off_t fOffset; // current position in file - - di_off_t fOpenEOF; // how big the file currently is - long fOpenSectorsUsed; // how many sectors it occupies - bool fModified; // if modified, update stuff on Close - - void DumpTSList(void) const; -}; - -/* - * Holds DOS files. Works for DOS33, DOS32, and "wide" DOS implementations. - * - * Catalog contents are public so anyone who wants gory details of DOS 3.3 - * stuff can poke at whatever they want. Anybody else can use the virtual - * interfaces to get standardized answers for things like file type. - * - * The embedded address and length fields found in Applesoft, Integer, and - * Binary files are quietly skipped over with the fDataOffset field when - * files are read. - * - * THOUGHT: have "get filename" and "get raw filename" interfaces? There - * are no directories, so maybe we don't care about "raw pathname"?? Might - * be better to always return the "raw" value and let the caller deal with - * things like high ASCII. - */ -class DISKIMG_API A2FileDOS : public A2File { -public: - A2FileDOS(DiskFS* pDiskFS); - virtual ~A2FileDOS(void); - - // assorted constants - enum { - kMaxFileName = 30, - }; - typedef enum { - kTypeUnknown = -1, - kTypeText = 0x00, // 'T' - kTypeInteger = 0x01, // 'I' - kTypeApplesoft = 0x02, // 'A' - kTypeBinary = 0x04, // 'B' - kTypeS = 0x08, // 'S' - kTypeReloc = 0x10, // 'R' - kTypeA = 0x20, // 'A' - kTypeB = 0x40, // 'B' - - kTypeLocked = 0x80 // bitwise OR with previous values - } FileType; - - /* - * Implementations of standard interfaces. - */ - virtual const char* GetFileName(void) const { return fFileName; } - virtual const char* GetPathName(void) const { return fFileName; } - virtual char GetFssep(void) const { return '\0'; } - virtual long GetFileType(void) const; - virtual long GetAuxType(void) const { return fAuxType; } - virtual long GetAccess(void) const; - virtual time_t GetCreateWhen(void) const { return 0; } - virtual time_t GetModWhen(void) const { return 0; } - virtual di_off_t GetDataLength(void) const { return fLength; } - virtual di_off_t GetDataSparseLength(void) const { return fSparseLength; } - virtual di_off_t GetRsrcLength(void) const { return -1; } - virtual di_off_t GetRsrcSparseLength(void) const { return -1; } - - virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, - bool rsrcFork = false); - virtual void CloseDescr(A2FileDescr* pOpenFile) { - assert(pOpenFile == fpOpenFile); - delete fpOpenFile; - fpOpenFile = NULL; - } - virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } - - void Dump(void) const; - - typedef DiskFSDOS33::TrackSector TrackSector; - - /* - * Contents of directory entry. - * - * We don't hold deleted or unused entries, so fTSListTrack is always - * valid. - */ - short fTSListTrack; // (could use TrackSector here) - short fTSListSector; - unsigned short fLengthInSectors; - bool fLocked; - char fFileName[kMaxFileName+1]; // "fixed" version - FileType fFileType; - - TrackSector fCatTS; // track/sector for our catalog entry - int fCatEntryNum; // entry number within cat sector - - // these are computed or determined from the file contents - unsigned short fAuxType; // addr for bin, etc. - short fDataOffset; // for 'A'/'B'/'I' with embedded len - di_off_t fLength; // file length, in bytes - di_off_t fSparseLength; // file length, factoring sparse out - - void FixFilename(void); - - DIError LoadTSList(TrackSector** pTSList, int* pTSCount, - TrackSector** pIndexList = NULL, int* pIndexCount = NULL); - static FileType ConvertFileType(long prodosType, di_off_t fileLen); - static bool IsValidType(long prodosType); - static void MakeDOSName(char* buf, const char* name); - static void TrimTrailingSpaces(char* filename); - -private: - DIError ExtractTSPairs(const unsigned char* sctBuf, TrackSector* tsList, - int* pLastNonZero); - - A2FDDOS* fpOpenFile; -}; - - -/* - * =========================================================================== - * ProDOS - * =========================================================================== - */ - -class A2FileProDOS; - -/* - * ProDOS disk. - * - * THOUGHT: it would be undesirable for the CiderPress UI, but it would - * make things somewhat easier internally if we treated the volume dir - * like a subdirectory under which everything else sits, instead of special- - * casing it like we do. This is awkward because volume dirs have names - * under ProDOS, giving every pathname an extra component that they don't - * really need. We can never treat the volume dir purely as a subdir, - * because it can't expand beyond 51 files, but the storage_type in the - * header is sufficient to identify it as such (assuming the disk isn't - * broken). Certain operations, such as changing the file type or aux type, - * simply aren't possible on a volume dir, and deleting a volume dir doesn't - * make sense. So in some respects we simply trade one kind of special-case - * behavior for another. - */ -class DISKIMG_API DiskFSProDOS : public DiskFS { -public: - DiskFSProDOS(void) : fBitMapPointer(0), fTotalBlocks(0), fBlockUseMap(NULL) - {} - virtual ~DiskFSProDOS(void) { - if (fBlockUseMap != NULL) { - assert(false); // unexpected - delete[] fBlockUseMap; - } - } - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(initMode); - } - virtual DIError Format(DiskImg* pDiskImg, const char* volName); - virtual DIError NormalizePath(const char* path, char fssep, - char* normalizedBuf, int* pNormalizedBufLen); - virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); - virtual DIError DeleteFile(A2File* pFile); - virtual DIError RenameFile(A2File* pFile, const char* newName); - virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, - long accessFlags); - virtual DIError RenameVolume(const char* newName); - - // assorted constants - enum { - kMaxVolumeName = 15, - }; - typedef unsigned long ProDate; - - virtual const char* GetVolumeName(void) const { return fVolumeName; } - virtual const char* GetVolumeID(void) const { return fVolumeID; } - virtual const char* GetBareVolumeName(void) const { return fVolumeName; } - virtual bool GetReadWriteSupported(void) const { return true; } - virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } - virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const; - - //A2FileProDOS* GetVolDir(void) const { return fpVolDir; } - - static bool IsValidFileName(const char* name); - static bool IsValidVolumeName(const char* name); - static unsigned short GenerateLowerCaseBits(const char* upperName, - const char* lowerName, bool forAppleWorks); - static void GenerateLowerCaseName(const char* upperName, - char* lowerNameNoTerm, unsigned short lcFlags, bool fromAppleWorks); - - friend class A2FDProDOS; - -private: - struct DirHeader; - - enum { kMaxExtensionLen = 4 }; // used when normalizing; ".gif" is 4 - - DIError Initialize(InitMode initMode); - DIError LoadVolHeader(void); - void SetVolumeID(void); - void DumpVolHeader(void); - DIError ScanVolBitmap(void); - DIError LoadVolBitmap(void); - DIError SaveVolBitmap(void); - void FreeVolBitmap(void); - long AllocBlock(void); - int GetNumBitmapBlocks(void) const { - /* use fTotalBlocks rather than GetNumBlocks() */ - assert(fTotalBlocks > 0); - const int kBitsPerBlock = 512 * 8; - int numBlocks = (fTotalBlocks + kBitsPerBlock-1) / kBitsPerBlock; - return numBlocks; - } - DIError CreateEmptyBlockMap(void); - bool GetBlockUseEntry(long block) const; - void SetBlockUseEntry(long block, bool inUse); - bool ScanForExtraEntries(void) const; - - void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); - DIError GetDirHeader(const unsigned char* blkBuf, DirHeader* pHeader); - DIError RecursiveDirAdd(A2File* pParent, unsigned short dirBlock, - const char* basePath, int depth); - DIError SlurpEntries(A2File* pParent, const DirHeader* pHeader, - const unsigned char* blkBuf, bool skipFirst, int* pCount, - const char* basePath, unsigned short thisBlock, int depth); - DIError ReadExtendedInfo(A2FileProDOS* pFile); - DIError ScanFileUsage(void); - void ScanBlockList(long blockCount, unsigned short* blockList, - long indexCount, unsigned short* indexList, long* pSparseCount); - DIError ScanForSubVolumes(void); - DIError FindSubVolume(long blockStart, long blockCount, - DiskImg** ppDiskImg, DiskFS** ppDiskFS); - void MarkSubVolumeBlocks(long block, long count); - - A2File* FindFileByKeyBlock(A2File* pStart, unsigned short keyBlock); - DIError AllocInitialFileStorage(const CreateParms* pParms, - const char* upperName, unsigned short dirBlock, int dirEntrySlot, - long* pKeyBlock, int* pBlocksUsed, int* pNewEOF); - DIError WriteBootBlocks(void); - DIError DoNormalizePath(const char* path, char fssep, - char** pNormalizedPath); - void UpperCaseName(char* upperName, const char* name); - bool CheckDiskIsGood(void); - DIError AllocDirEntry(A2FileDescr* pOpenSubdir, unsigned char** ppDir, - long* pDirLen, unsigned char** ppDirEntry, unsigned short* pDirKeyBlock, - int* pDirEntrySlot, unsigned short* pDirBlock); - unsigned char* GetPrevDirEntry(unsigned char* buf, unsigned char* ptr); - DIError MakeFileNameUnique(const unsigned char* dirBuf, long dirLen, - char* fileName); - bool NameExistsInDir(const unsigned char* dirBuf, long dirLen, - const char* fileName); - - DIError FreeBlocks(long blockCount, unsigned short* blockList); - DIError RegeneratePathName(A2FileProDOS* pFile); - - /* some items from the volume header */ - char fVolumeName[kMaxVolumeName+1]; - char fVolumeID[kMaxVolumeName + 16]; // add "ProDOS /" - unsigned char fAccess; - ProDate fCreateWhen; - ProDate fModWhen; - unsigned short fBitMapPointer; - unsigned short fTotalBlocks; - //unsigned short fPrevBlock; - //unsigned short fNextBlock; - //unsigned char fVersion; - //unsigned char fMinVersion; - //unsigned char fEntryLength; - //unsigned char fEntriesPerBlock; - unsigned short fVolDirFileCount; - -// A2FileProDOS* fpVolDir; // a "fake" file entry for the volume dir - - /* - * This is a working copy of the block use map from blocks 6+. It should - * be loaded when we're about to modify files on the disk and freed - * immediately afterward. The goal is to facilitate speedy updates to the - * bitmap without creating problems if the application decides to modify - * one of the bitmap blocks directly (e.g. with the disk sector editor). - * It should never be held across calls. - */ - unsigned char* fBlockUseMap; - - /* - * Set this if the disk is "perfect". If it's not, we disallow write - * access for safety reasons. - */ - bool fDiskIsGood; - - /* set if something fixes damage so CheckDiskIsGood can't see it */ - bool fEarlyDamage; -}; - -/* - * File descriptor for an open ProDOS file. - * - * This only represents one fork. - */ -class DISKIMG_API A2FDProDOS : public A2FileDescr { -public: - A2FDProDOS(A2File* pFile) : A2FileDescr(pFile), fModified(false), - fBlockList(NULL), fOffset(0) - {} - virtual ~A2FDProDOS(void) { - delete[] fBlockList; - fBlockList = NULL; - } - - friend class A2FileProDOS; - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Seek(di_off_t offset, DIWhence whence); - virtual di_off_t Tell(void); - virtual DIError Close(void); - - virtual long GetSectorCount(void) const; - virtual long GetBlockCount(void) const; - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; - virtual DIError GetStorage(long blockIdx, long* pBlock) const; - - void DumpBlockList(void) const; - -private: - bool IsEmptyBlock(const unsigned char* blk); - DIError WriteDirectory(const void* buf, size_t len, size_t* pActual); - - /* state for open files */ - bool fModified; - long fBlockCount; - unsigned short* fBlockList; - di_off_t fOpenEOF; // current EOF - unsigned short fOpenBlocksUsed; // #of block used by open piece - int fOpenStorageType; - bool fOpenRsrcFork; // is this the resource fork? - di_off_t fOffset; // current file offset -}; - -/* - * Holds a ProDOS file. - */ -class DISKIMG_API A2FileProDOS : public A2File { -public: - A2FileProDOS(DiskFS* pDiskFS) : A2File(pDiskFS) { - fPathName = NULL; - fSparseDataEof = fSparseRsrcEof = -1; - fpOpenFile = NULL; - fParentDirBlock = 0; - fParentDirIdx = -1; - fpParent = NULL; - } - virtual ~A2FileProDOS(void) { - delete fpOpenFile; - delete[] fPathName; - } - - typedef DiskFSProDOS::ProDate ProDate; - - /* assorted constants */ - enum { - kMaxFileName = 15, - kFssep = ':', - kInvalidBlockNum = 1, // boot block, can't be in file - kMaxBlocksPerIndex = 256, - }; - /* ProDOS access permissions */ - enum { - kAccessRead = 0x01, - kAccessWrite = 0x02, - kAccessInvisible = 0x04, - kAccessBackup = 0x20, - kAccessRename = 0x40, - kAccessDelete = 0x80 - }; - /* contents of a directory entry */ - typedef struct DirEntry { - int storageType; - char fileName[kMaxFileName+1]; // shows lower case - unsigned char fileType; - unsigned short keyPointer; - unsigned short blocksUsed; - unsigned long eof; - ProDate createWhen; - unsigned char version; - unsigned char minVersion; - unsigned char access; - unsigned short auxType; - ProDate modWhen; - unsigned short headerPointer; - } DirEntry; - typedef struct ExtendedInfo { - unsigned char storageType; - unsigned short keyBlock; - unsigned short blocksUsed; - unsigned long eof; - } ExtendedInfo; - typedef enum StorageType { - kStorageDeleted = 0, /* indicates deleted file */ - kStorageSeedling = 1, /* <= 512 bytes */ - kStorageSapling = 2, /* < 128KB */ - kStorageTree = 3, /* < 16MB */ - kStoragePascalVolume = 4, /* see ProDOS technote 25 */ - kStorageExtended = 5, /* forked */ - kStorageDirectory = 13, - kStorageSubdirHeader = 14, - kStorageVolumeDirHeader = 15, - } StorageType; - - static bool IsRegularFile(int type) { - return (type == kStorageSeedling || type == kStorageSapling || - type == kStorageTree); - } - - /* - * Implementations of standard interfaces. - */ - virtual const char* GetFileName(void) const { return fDirEntry.fileName; } - virtual const char* GetPathName(void) const { return fPathName; } - virtual char GetFssep(void) const { return kFssep; } - virtual long GetFileType(void) const { return fDirEntry.fileType; } - virtual long GetAuxType(void) const { return fDirEntry.auxType; } - virtual long GetAccess(void) const { return fDirEntry.access; } - virtual time_t GetCreateWhen(void) const; - virtual time_t GetModWhen(void) const; - virtual di_off_t GetDataLength(void) const { - if (GetQuality() == kQualityDamaged) - return 0; - if (fDirEntry.storageType == kStorageExtended) - return fExtData.eof; - else - return fDirEntry.eof; - } - virtual di_off_t GetRsrcLength(void) const { - if (fDirEntry.storageType == kStorageExtended) { - if (GetQuality() == kQualityDamaged) - return 0; - else - return fExtRsrc.eof; - } else - return -1; - } - virtual di_off_t GetDataSparseLength(void) const { - if (GetQuality() == kQualityDamaged) - return 0; - else - return fSparseDataEof; - } - virtual di_off_t GetRsrcSparseLength(void) const { - if (GetQuality() == kQualityDamaged) - return 0; - else - return fSparseRsrcEof; - } - virtual bool IsDirectory(void) const { - return (fDirEntry.storageType == kStorageDirectory || - fDirEntry.storageType == kStorageVolumeDirHeader); - } - virtual bool IsVolumeDirectory(void) const { - return (fDirEntry.storageType == kStorageVolumeDirHeader); - } - - virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, - bool rsrcFork = false); - virtual void CloseDescr(A2FileDescr* pOpenFile) { - assert(pOpenFile == fpOpenFile); - delete fpOpenFile; - fpOpenFile = NULL; - } - virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } - - virtual void SetParent(A2File* pParent) { fpParent = pParent; } - virtual A2File* GetParent(void) const { return fpParent; } - - static char NameToLower(char ch); - static void InitDirEntry(DirEntry* pEntry, - const unsigned char* entryBuf); - - virtual void Dump(void) const; - - /* directory entry contents for this file */ - DirEntry fDirEntry; - - /* pointer to directory entry (update dir if file size or dates change) */ - unsigned short fParentDirBlock; // directory block - int fParentDirIdx; // index in dir block - - /* these are only valid if storageType == kStorageExtended */ - ExtendedInfo fExtData; - ExtendedInfo fExtRsrc; - - void SetPathName(const char* basePath, const char* fileName); - static time_t ConvertProDate(ProDate proDate); - static ProDate ConvertProDate(time_t unixDate); - - /* returns "true" if AppleWorks aux type is used for lower-case name */ - static bool UsesAppleWorksAuxType(unsigned char fileType) { - return (fileType >= 0x19 && fileType <= 0x1b); - } - -#if 0 - /* change fPathName; should only be used by DiskFS rename */ - void SetPathName(const char* name) { - delete[] fPathName; - if (name == NULL) { - fPathName = NULL; - } else { - fPathName = new char[strlen(name)+1]; - if (fPathName != NULL) - strcpy(fPathName, name); - } - } -#endif - - DIError LoadBlockList(int storageType, unsigned short keyBlock, - long eof, long* pBlockCount, unsigned short** pBlockList, - long* pIndexBlockCount=NULL, unsigned short** pIndexBlockList=NULL); - DIError LoadDirectoryBlockList(unsigned short keyBlock, - long eof, long* pBlockCount, unsigned short** pBlockList); - - /* fork lengths without sparseness */ - di_off_t fSparseDataEof; - di_off_t fSparseRsrcEof; - -private: - DIError LoadIndexBlock(unsigned short block, unsigned short* list, - int maxCount); - DIError ValidateBlockList(const unsigned short* list, long count); - - char* fPathName; // full pathname to file on this volume - - A2FDProDOS* fpOpenFile; // only one fork can be open at a time - A2File* fpParent; -}; - - -/* - * =========================================================================== - * Pascal - * =========================================================================== - */ - -/* - * Pascal disk. - * - * There is no allocation map or file index blocks, just a linear collection - * of files with contiguous blocks. - */ -class A2FilePascal; - -class DISKIMG_API DiskFSPascal : public DiskFS { -public: - DiskFSPascal(void) : fDirectory(NULL) {} - virtual ~DiskFSPascal(void) { - if (fDirectory != NULL) { - assert(false); // unexpected - delete[] fDirectory; - } - } - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - virtual DIError Format(DiskImg* pDiskImg, const char* volName); - - // assorted constants - enum { - kMaxVolumeName = 7, - kDirectoryEntryLen = 26, - }; - typedef unsigned short PascalDate; - - virtual const char* GetVolumeName(void) const { return fVolumeName; } - virtual const char* GetVolumeID(void) const { return fVolumeID; } - virtual const char* GetBareVolumeName(void) const { return fVolumeName; } - virtual bool GetReadWriteSupported(void) const { return true; } - virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const; - virtual DIError NormalizePath(const char* path, char fssep, - char* normalizedBuf, int* pNormalizedBufLen); - virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); - virtual DIError DeleteFile(A2File* pFile); - virtual DIError RenameFile(A2File* pFile, const char* newName); - virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, - long accessFlags); - virtual DIError RenameVolume(const char* newName); - - static bool IsValidVolumeName(const char* name); - static bool IsValidFileName(const char* name); - - unsigned short GetTotalBlocks(void) const { return fTotalBlocks; } - - friend class A2FDPascal; - -private: - DIError Initialize(void); - DIError LoadVolHeader(void); - void SetVolumeID(void); - void DumpVolHeader(void); - DIError LoadCatalog(void); - DIError SaveCatalog(void); - void FreeCatalog(void); - DIError ProcessCatalog(void); - DIError ScanFileUsage(void); - void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); - DIError WriteBootBlocks(void); - bool CheckDiskIsGood(void); - void DoNormalizePath(const char* name, char fssep, char* outBuf); - DIError MakeFileNameUnique(char* fileName); - DIError FindLargestFreeArea(int *pPrevIdx, A2FilePascal** ppPrevFile); - unsigned char* FindDirEntry(A2FilePascal* pFile); - - enum { kMaxExtensionLen = 5 }; // used when normalizing; ".code" is 4 - - /* some items from the volume header */ - unsigned short fStartBlock; // first block of dir hdr; always 2 - unsigned short fNextBlock; // i.e. first block with data - char fVolumeName[kMaxVolumeName+1]; - char fVolumeID[kMaxVolumeName + 16]; // add "Pascal ___:" - unsigned short fTotalBlocks; - unsigned short fNumFiles; - PascalDate fAccessWhen; // PascalDate last access - PascalDate fDateSetWhen; // PascalDate last date setting - unsigned short fStuff1; // - unsigned short fStuff2; // - - /* other goodies */ - bool fDiskIsGood; - bool fEarlyDamage; - - /* - * Pascal disks have one fixed-size directory. The contents aren't - * divided into blocks, which means you can't always edit an entry - * by loading a single block from disk and writing it back. Also, - * deleted entries are squeezed out, so if we delete an entry we - * have to reshuffle the entries below it. - * - * We want to keep the copy on disk synced up, so we don't hold on - * to this longer than necessary. Possibly less efficient that way; - * if it becomes a problem it's easy enough to change the behavior. - */ - unsigned char* fDirectory; -}; - -/* - * File descriptor for an open Pascal file. - */ -class DISKIMG_API A2FDPascal : public A2FileDescr { -public: - A2FDPascal(A2File* pFile) : A2FileDescr(pFile) { - fOffset = 0; - } - virtual ~A2FDPascal(void) { - /* nothing to clean up */ - } - - friend class A2FilePascal; - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Seek(di_off_t offset, DIWhence whence); - virtual di_off_t Tell(void); - virtual DIError Close(void); - - virtual long GetSectorCount(void) const; - virtual long GetBlockCount(void) const; - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; - virtual DIError GetStorage(long blockIdx, long* pBlock) const; - -private: - di_off_t fOffset; // where we are - di_off_t fOpenEOF; // how big the file currently is - long fOpenBlocksUsed; // how many blocks it occupies - bool fModified; // if modified, update dir on Close -}; - -/* - * File on a Pascal disk. - */ -class DISKIMG_API A2FilePascal : public A2File { -public: - A2FilePascal(DiskFS* pDiskFS) : A2File(pDiskFS) { - fpOpenFile = NULL; - } - virtual ~A2FilePascal(void) { - /* this comes back and calls CloseDescr */ - if (fpOpenFile != NULL) - fpOpenFile->Close(); - } - - typedef DiskFSPascal::PascalDate PascalDate; - - // assorted constants - enum { - kMaxFileName = 15, - }; - typedef enum FileType { - kTypeUntyped = 0, // NON - kTypeXdsk = 1, // BAD (bad blocks) - kTypeCode = 2, // PCD - kTypeText = 3, // PTX - kTypeInfo = 4, // ? - kTypeData = 5, // PDA - kTypeGraf = 6, // ? - kTypeFoto = 7, // FOT? (hires image) - kTypeSecurdir = 8 // ?? - } FileType; - - /* - * Implementations of standard interfaces. - */ - virtual const char* GetFileName(void) const { return fFileName; } - virtual const char* GetPathName(void) const { return fFileName; } - virtual char GetFssep(void) const { return '\0'; } - virtual long GetFileType(void) const; - virtual long GetAuxType(void) const { return 0; } - virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } - virtual time_t GetCreateWhen(void) const { return 0; } - virtual time_t GetModWhen(void) const; - virtual di_off_t GetDataLength(void) const { return fLength; } - virtual di_off_t GetDataSparseLength(void) const { return fLength; } - virtual di_off_t GetRsrcLength(void) const { return -1; } - virtual di_off_t GetRsrcSparseLength(void) const { return -1; } - - virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, - bool rsrcFork = false); - virtual void CloseDescr(A2FileDescr* pOpenFile) { - assert(pOpenFile == fpOpenFile); - delete fpOpenFile; - fpOpenFile = NULL; - } - virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } - - - virtual void Dump(void) const; - - static time_t ConvertPascalDate(PascalDate pascalDate); - static A2FilePascal::PascalDate ConvertPascalDate(time_t unixDate); - static A2FilePascal::FileType ConvertFileType(long prodosType); - - /* fields pulled out of directory block */ - unsigned short fStartBlock; - unsigned short fNextBlock; - FileType fFileType; - char fFileName[kMaxFileName+1]; - unsigned short fBytesRemaining; - PascalDate fModWhen; - - /* derived fields */ - di_off_t fLength; - - /* note to self: don't try to store a directory offset here; they shift - every time you add or delete a file */ - -private: - A2FileDescr* fpOpenFile; -}; - - -/* - * =========================================================================== - * CP/M - * =========================================================================== - */ - -/* - * CP/M disk. - * - * We really ought to be using 1K blocks here, since that's the native - * CP/M format, but there's little value in making an exception for such - * a rarely used Apple II format. - * - * There is no allocation map or file index blocks, just a single 2K - * directory filled with files that have up to 16 1K blocks each. If - * a file is longer than 16K, a second entry with the identical name - * and user number is made. These "extents" may be sparse, so it's - * necessary to use the "records" field to determine the actual file length. - */ -class A2FileCPM; -class DISKIMG_API DiskFSCPM : public DiskFS { -public: - DiskFSCPM(void) : fDiskIsGood(false) {} - virtual ~DiskFSCPM(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return "CP/M"; } - virtual const char* GetVolumeID(void) const { return "CP/M"; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - - // assorted constants - enum { - kDirectoryEntryLen = 32, - kVolDirBlock = 24, // ProDOS block where volume dir starts - kDirFileNameLen = 11, // 8+3 without the '.' - kFullDirSize = 2048, // blocks 0 and 1 - kDirEntryBlockCount = 16, // #of blocks held in dir slot - kNumDirEntries = kFullDirSize/kDirectoryEntryLen, - kExtentsInLowByte = 32, - - kDirEntryFlagContinued = 0x8000, // "flags" word - }; - // Contents of the raw 32-byte directory entry. - // - // From http://www.seasip.demon.co.uk/Cpm/format31.html - // - // UU F1 F2 F3 F4 F5 F6 F7 F8 T1 T2 T3 EX S1 S2 RC .FILENAMETYP.... - // AL AL AL AL AL AL AL AL AL AL AL AL AL AL AL AL ................ - // - // If the high bit of T1 is set, the file is read-only. If the high - // bit of T2 is set, the file is a "system" file. - // - // An entry with UU=0x20 indicates a CP/M 3.1 disk label entry. - // An entry with UU=0x21 indicates a time stamp entry (2.x or 3.x). - // - // Files larger than (1024 * 16) have multiple "extent" entries, i.e. - // entries with the same user number and file name. - typedef struct DirEntry { - unsigned char userNumber; // 0-15 or 0-31 (usually 0), e5=unused - unsigned char fileName[kDirFileNameLen+1]; - unsigned short extent; // extent (EX + S2 * 32) - unsigned char S1; // Last Record Byte Count (app-specific) - unsigned char records; // #of 128-byte records in this extent - unsigned char blocks[kDirEntryBlockCount]; - bool readOnly; - bool system; - bool badBlockList; // set if block list is damaged - } DirEntry; - - static long CPMToProDOSBlock(long cpmBlock) { - return kVolDirBlock + (cpmBlock*2); - } - -private: - DIError Initialize(void); - DIError ReadCatalog(void); - DIError ScanFileUsage(void); - void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); - void FormatName(char* dstBuf, const char* srcBuf); - DIError ComputeLength(A2FileCPM* pFile); - bool CheckDiskIsGood(void); - - // the full set of raw dir entries - DirEntry fDirEntry[kNumDirEntries]; - - bool fDiskIsGood; -}; - -/* - * File descriptor for an open CP/M file. - */ -class DISKIMG_API A2FDCPM : public A2FileDescr { -public: - A2FDCPM(A2File* pFile) : A2FileDescr(pFile) { - //fOpen = false; - fBlockList = NULL; - } - virtual ~A2FDCPM(void) { - delete fBlockList; - fBlockList = NULL; - } - - friend class A2FileCPM; - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Seek(di_off_t offset, DIWhence whence); - virtual di_off_t Tell(void); - virtual DIError Close(void); - - virtual long GetSectorCount(void) const; - virtual long GetBlockCount(void) const; - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; - virtual DIError GetStorage(long blockIdx, long* pBlock) const; - -private: - //bool fOpen; - di_off_t fOffset; - long fBlockCount; - unsigned char* fBlockList; -}; - -/* - * File on a CP/M disk. - */ -class DISKIMG_API A2FileCPM : public A2File { -public: - typedef DiskFSCPM::DirEntry DirEntry; - - A2FileCPM(DiskFS* pDiskFS, DirEntry* pDirEntry) : - A2File(pDiskFS), fpDirEntry(pDirEntry) - { - fDirIdx = -1; - fpOpenFile = NULL; - } - virtual ~A2FileCPM(void) { - delete fpOpenFile; - } - - // assorted constants - enum { - kMaxFileName = 12, // 8+3 including '.' - }; - - /* - * Implementations of standard interfaces. - */ - virtual const char* GetFileName(void) const { return fFileName; } - virtual const char* GetPathName(void) const { return fFileName; } - virtual char GetFssep(void) const { return '\0'; } - virtual long GetFileType(void) const { return 0; } - virtual long GetAuxType(void) const { return 0; } - virtual long GetAccess(void) const { - if (fReadOnly) - return DiskFS::kFileAccessLocked; - else - return DiskFS::kFileAccessUnlocked; - } - virtual time_t GetCreateWhen(void) const { return 0; } - virtual time_t GetModWhen(void) const { return 0; } - virtual di_off_t GetDataLength(void) const { return fLength; } - virtual di_off_t GetDataSparseLength(void) const { return fLength; } - virtual di_off_t GetRsrcLength(void) const { return -1; } - virtual di_off_t GetRsrcSparseLength(void) const { return -1; } - - virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, - bool rsrcFork = false); - virtual void CloseDescr(A2FileDescr* pOpenFile) { - assert(pOpenFile == fpOpenFile); - delete fpOpenFile; - fpOpenFile = NULL; - } - virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } - - virtual void Dump(void) const; - - /* fields pulled out of directory block */ - char fFileName[kMaxFileName+1]; - bool fReadOnly; - - /* derived fields */ - di_off_t fLength; - int fDirIdx; // index into fDirEntry for part #1 - - DIError GetBlockList(long* pBlockCount, unsigned char* blockBuf) const; - -private: - const DirEntry* fpDirEntry; - A2FileDescr* fpOpenFile; -}; - - -/* - * =========================================================================== - * RDOS - * =========================================================================== - */ - -/* - * RDOS disk. - * - * There is no allocation map or file index blocks, just a linear collection - * of files with contiguous sectors. Very similar to Pascal. - * - * The one interesting quirk is the "converted 13-sector disk" format, where - * only 13 of 16 sectors are actually used. The linear sector addressing - * must take that into account. - */ -class A2FileRDOS; -class DISKIMG_API DiskFSRDOS : public DiskFS { -public: - DiskFSRDOS(void) {} - virtual ~DiskFSRDOS(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - virtual const char* GetVolumeName(void) const { return fVolumeName; } - virtual const char* GetVolumeID(void) const { return fVolumeName; } - virtual const char* GetBareVolumeName(void) const { return NULL; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - - int GetOurSectPerTrack(void) const { return fOurSectPerTrack; } - -private: - static DIError TestCommon(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - FSLeniency leniency, DiskImg::FSFormat* pFormatFound); - - DIError Initialize(void); - DIError ReadCatalog(void); - DIError ScanFileUsage(void); - void SetSectorUsage(long track, long sector, - VolumeUsage::ChunkPurpose purpose); - - char fVolumeName[10]; // e.g. "RDOS 3.3" - int fOurSectPerTrack; -}; - -/* - * File descriptor for an open RDOS file. - */ -class DISKIMG_API A2FDRDOS : public A2FileDescr { -public: - A2FDRDOS(A2File* pFile) : A2FileDescr(pFile) { - fOffset = 0; - } - virtual ~A2FDRDOS(void) { - /* nothing to clean up */ - } - - friend class A2FileRDOS; - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Seek(di_off_t offset, DIWhence whence); - virtual di_off_t Tell(void); - virtual DIError Close(void); - - virtual long GetSectorCount(void) const; - virtual long GetBlockCount(void) const; - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; - virtual DIError GetStorage(long blockIdx, long* pBlock) const; - -private: - /* RDOS is unique in that it can put 13-sector disks on 16-sector tracks */ - inline int GetOurSectPerTrack(void) const { - DiskFSRDOS* pDiskFS = (DiskFSRDOS*) fpFile->GetDiskFS(); - return pDiskFS->GetOurSectPerTrack(); - } - - //bool fOpen; - di_off_t fOffset; -}; - -/* - * File on an RDOS disk. - */ -class DISKIMG_API A2FileRDOS : public A2File { -public: - A2FileRDOS(DiskFS* pDiskFS) : A2File(pDiskFS) { - //fOpen = false; - fpOpenFile = NULL; - } - virtual ~A2FileRDOS(void) { - delete fpOpenFile; - } - - // assorted constants - enum { - kMaxFileName = 24, - }; - typedef enum FileType { - kTypeUnknown = 0, - kTypeApplesoft, // 'A' - kTypeBinary, // 'B' - kTypeText, // 'T' - } FileType; - - /* - * Implementations of standard interfaces. - */ - virtual const char* GetFileName(void) const { return fFileName; } - virtual const char* GetPathName(void) const { return fFileName; } - virtual char GetFssep(void) const { return '\0'; } - virtual long GetFileType(void) const; - virtual long GetAuxType(void) const { return fLoadAddr; } - virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } - virtual time_t GetCreateWhen(void) const { return 0; } - virtual time_t GetModWhen(void) const { return 0; }; - virtual di_off_t GetDataLength(void) const { return fLength; } - virtual di_off_t GetDataSparseLength(void) const { return fLength; } - virtual di_off_t GetRsrcLength(void) const { return -1; } - virtual di_off_t GetRsrcSparseLength(void) const { return -1; } - - virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, - bool rsrcFork = false); - virtual void CloseDescr(A2FileDescr* pOpenFile) { - assert(pOpenFile == fpOpenFile); - delete fpOpenFile; - fpOpenFile = NULL; - } - virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } - - void FixFilename(void); - virtual void Dump(void) const; - - /* fields pulled out of directory block */ - char fFileName[kMaxFileName+1]; - FileType fFileType; - unsigned short fNumSectors; - unsigned short fLoadAddr; - unsigned short fLength; - unsigned short fStartSector; - -private: - void TrimTrailingSpaces(char* filename); - - A2FileDescr* fpOpenFile; -}; - - -/* - * =========================================================================== - * HFS - * =========================================================================== - */ - -/* - * HFS disk. - */ -class A2FileHFS; -class DISKIMG_API DiskFSHFS : public DiskFS { -public: - DiskFSHFS(void) { - fLocalTimeOffset = -1; - fDiskIsGood = true; -#ifndef EXCISE_GPL_CODE - fHfsVol = NULL; -#endif - } - virtual ~DiskFSHFS(void) { -#ifndef EXCISE_GPL_CODE - hfs_callback_close(fHfsVol); - fHfsVol = (hfsvol*) 0xcdaaaacd; -#endif - } - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(initMode); - } - -#ifndef EXCISE_GPL_CODE - /* these are optional, defined as no-ops in the parent class */ - virtual DIError Format(DiskImg* pDiskImg, const char* volName); - virtual DIError NormalizePath(const char* path, char fssep, - char* normalizedBuf, int* pNormalizedBufLen); - virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); - virtual DIError DeleteFile(A2File* pFile); - virtual DIError RenameFile(A2File* pFile, const char* newName); - virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, - long accessFlags); - virtual DIError RenameVolume(const char* newName); -#endif - - // assorted constants - enum { - kMaxVolumeName = 27, - kMaxExtensionLen = 4, // used when normalizing; ".gif" is 4 - }; - - /* mandatory functions */ - virtual const char* GetVolumeName(void) const { return fVolumeName; } - virtual const char* GetVolumeID(void) const { return fVolumeID; } - virtual const char* GetBareVolumeName(void) const { return fVolumeName; } - virtual bool GetReadWriteSupported(void) const { return true; } - virtual bool GetFSDamaged(void) const { return false; } - virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const; - -#ifndef EXCISE_GPL_CODE - hfsvol* GetHfsVol(void) const { return fHfsVol; } -#endif - - // utility function, used by app - static bool IsValidVolumeName(const char* name); - static bool IsValidFileName(const char* name); - -private: - enum { - // Macintosh 32-bit dates start in 1904, everybody else starts in - // 1970. Take the Mac date and adjust it 66 years plus 17 leap days. - // The annoying part is that HFS stores dates in local time, which - // means it's impossible to know absolutely when a file was modified. - // libhfs converts timestamps to the current time zone, so that a - // file written January 1st 2006 at 6pm in London will appear to have - // been written January 1st 2006 at 6pm in San Francisco if you - // happen to be sitting in California. - // - // This was fixed in HFS+, but we have to deal with it for now. The - // value below converts the date to local time in Greenwich; the - // current GMT offset and daylight saving time must be added to it. - // - // Curiously, the volume dates shown by Cmd-I on the volume on my - // Quadra are off by an hour, even though the file dates match. - kDateTimeOffset = (1970 - 1904) * 60 * 60 * 24 * 365 + - (60 * 60 * 24 * 17), - - kExpectedMinBlocks = 1440, // ignore volumes under 720K - }; - - struct MasterDirBlock; // fwd - static void UnpackMDB(const unsigned char* buf, MasterDirBlock* pMDB); - static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); - - DIError Initialize(InitMode initMode); - DIError LoadVolHeader(void); - void SetVolumeID(void); - void DumpVolHeader(void); - void SetVolumeUsageMap(void); - -#ifdef EXCISE_GPL_CODE - void CreateFakeFile(void); -#else - DIError RecursiveDirAdd(A2File* pParent, const char* basePath, int depth); - //void Sanitize(unsigned char* str); - DIError DoNormalizePath(const char* path, char fssep, - char** pNormalizedPath); - static int CompareMacFileNames(const char* str1, const char* str2); - DIError RegeneratePathName(A2FileHFS* pFile); - DIError MakeFileNameUnique(const char* pathName, char** pUniqueName); - - /* libhfs stuff */ - static unsigned long LibHFSCB(void* vThis, int op, unsigned long arg1, - void* arg2); - hfsvol* fHfsVol; -#endif - - - /* some items from the volume header */ - char fVolumeName[kMaxVolumeName+1]; - char fVolumeID[kMaxVolumeName + 8]; // add "HFS :" - unsigned long fTotalBlocks; - unsigned long fAllocationBlockSize; - unsigned long fNumAllocationBlocks; - unsigned long fCreatedDateTime; - unsigned long fModifiedDateTime; - unsigned long fNumFiles; - unsigned long fNumDirectories; - - long fLocalTimeOffset; - bool fDiskIsGood; -}; - -/* - * File descriptor for an open HFS file. - */ -class DISKIMG_API A2FDHFS : public A2FileDescr { -public: -#ifdef EXCISE_GPL_CODE - A2FDHFS(A2File* pFile, void* unused) - : A2FileDescr(pFile), fOffset(0) - {} -#else - A2FDHFS(A2File* pFile, hfsfile* pHfsFile) - : A2FileDescr(pFile), fHfsFile(pHfsFile), fModified(false) - {} -#endif - virtual ~A2FDHFS(void) { -#ifndef EXCISE_GPL_CODE - if (fHfsFile != NULL) - hfs_close(fHfsFile); -#endif - } - - friend class A2FileHFS; - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Seek(di_off_t offset, DIWhence whence); - virtual di_off_t Tell(void); - virtual DIError Close(void); - - virtual long GetSectorCount(void) const; - virtual long GetBlockCount(void) const; - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; - virtual DIError GetStorage(long blockIdx, long* pBlock) const; - -private: -#ifdef EXCISE_GPL_CODE - di_off_t fOffset; -#else - hfsfile* fHfsFile; - bool fModified; -#endif -}; - -/* - * File on an HFS disk. - */ -class DISKIMG_API A2FileHFS : public A2File { -public: - A2FileHFS(DiskFS* pDiskFS) : A2File(pDiskFS) { - fPathName = NULL; - fpOpenFile = NULL; -#ifdef EXCISE_GPL_CODE - fFakeFileBuf = NULL; -#else - //fOrigPathName = NULL; -#endif - } - virtual ~A2FileHFS(void) { - delete fpOpenFile; - delete[] fPathName; -#ifdef EXCISE_GPL_CODE - delete[] fFakeFileBuf; -#else - //delete[] fOrigPathName; -#endif - } - - /* - * Implementations of standard interfaces. - */ - virtual const char* GetFileName(void) const { return fFileName; } - virtual const char* GetPathName(void) const { return fPathName; } - virtual char GetFssep(void) const { return kFssep; } - virtual long GetFileType(void) const; - virtual long GetAuxType(void) const; - virtual long GetAccess(void) const { return fAccess; } - virtual time_t GetCreateWhen(void) const { return fCreateWhen; } - virtual time_t GetModWhen(void) const { return fModWhen; } - virtual di_off_t GetDataLength(void) const { return fDataLength; } - virtual di_off_t GetDataSparseLength(void) const { return fDataLength; } - virtual di_off_t GetRsrcLength(void) const { return fRsrcLength; } - virtual di_off_t GetRsrcSparseLength(void) const { return fRsrcLength; } - virtual bool IsDirectory(void) const { return fIsDir; } - virtual bool IsVolumeDirectory(void) const { return fIsVolumeDir; } - - virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, - bool rsrcFork = false); - virtual void CloseDescr(A2FileDescr* pOpenFile) { - assert(pOpenFile == fpOpenFile); - delete fpOpenFile; - fpOpenFile = NULL; - } - virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } - - enum { - kMaxFileName = 31, - kFssep = ':', - kPdosType = 0x70646f73, // 'pdos' - }; - - void SetPathName(const char* basePath, const char* fileName); - virtual void Dump(void) const; - -#ifdef EXCISE_GPL_CODE - void SetFakeFile(void* buf, long len) { - assert(len > 0); - if (fFakeFileBuf != NULL) - delete[] fFakeFileBuf; - fFakeFileBuf = new char[len]; - memcpy(fFakeFileBuf, buf, len); - fDataLength = len; - } - const void* GetFakeFileBuf(void) const { return fFakeFileBuf; } -#else - void InitEntry(const hfsdirent* dirEntry); - void SetOrigPathName(const char* pathName); - virtual void SetParent(A2File* pParent) { fpParent = pParent; } - virtual A2File* GetParent(void) const { return fpParent; } - char* GetLibHFSPathName(void) const; - static void ConvertTypeToHFS(long fileType, long auxType, - char* pType, char* pCreator); -#endif - - bool fIsDir; - bool fIsVolumeDir; - long fType; - long fCreator; - char fFileName[kMaxFileName+1]; - char* fPathName; - di_off_t fDataLength; - di_off_t fRsrcLength; - time_t fCreateWhen; - time_t fModWhen; - long fAccess; - -private: -#ifdef EXCISE_GPL_CODE - char* fFakeFileBuf; -#else - //char* fOrigPathName; - A2File* fpParent; -#endif - A2FileDescr* fpOpenFile; // only one fork can be open at a time -}; - - -/* - * =========================================================================== - * FAT (including FAT12, FAT16, and FAT32) - * =========================================================================== - */ - -/* - * MS-DOS FAT disk. - * - * This is currently just the minimum necessary to properly recognize - * the disk. - */ -class A2FileFAT; -class DISKIMG_API DiskFSFAT : public DiskFS { -public: - DiskFSFAT(void) {} - virtual ~DiskFSFAT(void) {} - - static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, - DiskImg::FSFormat* pFormat, FSLeniency leniency); - - virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { - SetDiskImg(pImg); - return Initialize(); - } - - // assorted constants - enum { - kMaxVolumeName = 11, - }; - - virtual const char* GetVolumeName(void) const { return fVolumeName; } - virtual const char* GetVolumeID(void) const { return fVolumeID; } - virtual const char* GetBareVolumeName(void) const { return fVolumeName; } - virtual bool GetReadWriteSupported(void) const { return false; } - virtual bool GetFSDamaged(void) const { return false; } - virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } - virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, - int* pUnitSize) const - { return kDIErrNotSupported; } - -private: - enum { - kExpectedMinBlocks = 720, // ignore volumes under 360K - }; - - struct MasterBootRecord; // fwd - struct BootSector; - static bool UnpackMBR(const unsigned char* buf, MasterBootRecord* pOut); - static bool UnpackBootSector(const unsigned char* buf, BootSector* pOut); - static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); - - DIError Initialize(void); - DIError LoadVolHeader(void); - void DumpVolHeader(void); - void SetVolumeUsageMap(void); - void CreateFakeFile(void); - - /* some items from the volume header */ - char fVolumeName[kMaxVolumeName+1]; - char fVolumeID[kMaxVolumeName + 8]; // add "FAT %s:" - unsigned long fTotalBlocks; -}; - -/* - * File descriptor for an open FAT file. - */ -class DISKIMG_API A2FDFAT : public A2FileDescr { -public: - A2FDFAT(A2File* pFile) : A2FileDescr(pFile) { - fOffset = 0; - } - virtual ~A2FDFAT(void) { - /* nothing to clean up */ - } - - friend class A2FileFAT; - - virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); - virtual DIError Seek(di_off_t offset, DIWhence whence); - virtual di_off_t Tell(void); - virtual DIError Close(void); - - virtual long GetSectorCount(void) const; - virtual long GetBlockCount(void) const; - virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; - virtual DIError GetStorage(long blockIdx, long* pBlock) const; - -private: - di_off_t fOffset; -}; - -/* - * File on a FAT disk. - */ -class DISKIMG_API A2FileFAT : public A2File { -public: - A2FileFAT(DiskFS* pDiskFS) : A2File(pDiskFS) { - fFakeFileBuf = NULL; - //fFakeFileLen = -1; - fpOpenFile = NULL; - } - virtual ~A2FileFAT(void) { - delete fpOpenFile; - delete[] fFakeFileBuf; - } - - /* - * Implementations of standard interfaces. - */ - virtual const char* GetFileName(void) const { return fFileName; } - virtual const char* GetPathName(void) const { return fFileName; } - virtual char GetFssep(void) const { return '\0'; } - virtual long GetFileType(void) const { return 0; }; - virtual long GetAuxType(void) const { return 0; } - virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } - virtual time_t GetCreateWhen(void) const { return 0; } - virtual time_t GetModWhen(void) const { return 0; } - virtual di_off_t GetDataLength(void) const { return fLength; } - virtual di_off_t GetDataSparseLength(void) const { return fLength; } - virtual di_off_t GetRsrcLength(void) const { return -1; } - virtual di_off_t GetRsrcSparseLength(void) const { return -1; } - - virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, - bool rsrcFork = false); - virtual void CloseDescr(A2FileDescr* pOpenFile) { - assert(pOpenFile == fpOpenFile); - delete fpOpenFile; - fpOpenFile = NULL; - } - virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } - - enum { kMaxFileName = 31 }; - - virtual void Dump(void) const; - - void SetFakeFile(void* buf, long len) { - assert(len > 0); - if (fFakeFileBuf != NULL) - delete[] fFakeFileBuf; - fFakeFileBuf = new char[len]; - memcpy(fFakeFileBuf, buf, len); - fLength = len; - } - const void* GetFakeFileBuf(void) const { return fFakeFileBuf; } - - char fFileName[kMaxFileName+1]; - di_off_t fLength; - -private: - char* fFakeFileBuf; - //long fFakeFileLen; - A2FileDescr* fpOpenFile; -}; - -}; // namespace DiskImgLib - -#endif /*__DISKIMGDETAIL__*/ +/* + * CiderPress + * Copyright (C) 2009 by CiderPress authors. All Rights Reserved. + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Sub-classes of the base classes defined in DiskImg.h. + * + * Most applications will not need to include this file, because the + * polymorphic interfaces do everything they need. If something needs to + * examine the actual directory structure of a file, it can do so through + * these declarations. + */ +#ifndef __DISKIMGDETAIL__ +#define __DISKIMGDETAIL__ + +#include "../prebuilt/NufxLib.h" +#define ZLIB_DLL +#include "../prebuilt/zlib.h" + +#include "DiskImg.h" + +#ifndef EXCISE_GPL_CODE +# include "libhfs/hfs.h" +#endif + +namespace DiskImgLib { + +/* + * =========================================================================== + * Outer wrappers + * =========================================================================== + */ + +/* + * Outer wrapper class, representing a compression utility or archive + * format that must be stripped away so we can get to the Apple II stuff. + * + * Outer wrappers usually have a filename embedded in them, representing + * the original name of the file. We want to use the extension from this + * name when evaluating the file contents. Usually. + */ +class OuterWrapper { +public: + OuterWrapper(void) {} + virtual ~OuterWrapper(void) {} + + // all sub-classes should have one of these + //static DIError Test(GenericFD* pGFD, long outerLength); + + // open the file and prepare to access it; fills out return values + // NOTE: pGFD must be a GFDFile. + virtual DIError Load(GenericFD* pOuterGFD, di_off_t outerLength, bool readOnly, + di_off_t* pWrapperLength, GenericFD** ppWrapperGFD) = 0; + + virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength) = 0; + + // set on recoverable errors, like a CRC failure + virtual bool IsDamaged(void) const = 0; + + // indicate that we don't have a "fast" flush + virtual bool HasFastFlush(void) const { return false; } + + virtual const char* GetExtension(void) const = 0; + +private: + OuterWrapper& operator=(const OuterWrapper&); + OuterWrapper(const OuterWrapper&); +}; + +class OuterGzip : public OuterWrapper { +public: + OuterGzip(void) { fWrapperDamaged = false; } + virtual ~OuterGzip(void) {} + + static DIError Test(GenericFD* pGFD, di_off_t outerLength); + virtual DIError Load(GenericFD* pGFD, di_off_t outerLength, bool readOnly, + di_off_t* pTotalLength, GenericFD** ppNewGFD); + virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength); + + virtual bool IsDamaged(void) const { return fWrapperDamaged; } + + virtual const char* GetExtension(void) const { return NULL; } + +private: + DIError ExtractGzipImage(gzFile gzfp, char** pBuf, di_off_t* pLength); + DIError CloseGzip(void); + + // Largest possible ProDOS volume; quite a bit to hold in RAM. Add a + // little extra for .hdv format. + enum { kMaxUncompressedSize = kGzipMax +256 }; + + bool fWrapperDamaged; +}; + +class OuterZip : public OuterWrapper { +public: + OuterZip(void) : fStoredFileName(NULL), fExtension(NULL) {} + virtual ~OuterZip(void) { + delete[] fStoredFileName; + delete[] fExtension; + } + + static DIError Test(GenericFD* pGFD, di_off_t outerLength); + virtual DIError Load(GenericFD* pGFD, di_off_t outerLength, bool readOnly, + di_off_t* pTotalLength, GenericFD** ppNewGFD); + virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength); + + virtual bool IsDamaged(void) const { return false; } + + virtual const char* GetExtension(void) const { return fExtension; } + +private: + class LocalFileHeader { + public: + LocalFileHeader(void) : + fVersionToExtract(0), + fGPBitFlag(0), + fCompressionMethod(0), + fLastModFileTime(0), + fLastModFileDate(0), + fCRC32(0), + fCompressedSize(0), + fUncompressedSize(0), + fFileNameLength(0), + fExtraFieldLength(0), + fFileName(NULL) + {} + virtual ~LocalFileHeader(void) { delete[] fFileName; } + + DIError Read(GenericFD* pGFD); + DIError Write(GenericFD* pGFD); + void SetFileName(const char* name); + + // unsigned long fSignature; + unsigned short fVersionToExtract; + unsigned short fGPBitFlag; + unsigned short fCompressionMethod; + unsigned short fLastModFileTime; + unsigned short fLastModFileDate; + unsigned long fCRC32; + unsigned long fCompressedSize; + unsigned long fUncompressedSize; + unsigned short fFileNameLength; + unsigned short fExtraFieldLength; + unsigned char* fFileName; + // extra field + + enum { + kSignature = 0x04034b50, + kLFHLen = 30, // LocalFileHdr len, excl. var fields + }; + + void Dump(void) const; + }; + + class CentralDirEntry { + public: + CentralDirEntry(void) : + fVersionMadeBy(0), + fVersionToExtract(0), + fGPBitFlag(0), + fCompressionMethod(0), + fLastModFileTime(0), + fLastModFileDate(0), + fCRC32(0), + fCompressedSize(0), + fUncompressedSize(0), + fFileNameLength(0), + fExtraFieldLength(0), + fFileCommentLength(0), + fDiskNumberStart(0), + fInternalAttrs(0), + fExternalAttrs(0), + fLocalHeaderRelOffset(0), + fFileName(NULL), + fFileComment(NULL) + {} + virtual ~CentralDirEntry(void) { + delete[] fFileName; + delete[] fFileComment; + } + + DIError Read(GenericFD* pGFD); + DIError Write(GenericFD* pGFD); + void SetFileName(const char* name); + + // unsigned long fSignature; + unsigned short fVersionMadeBy; + unsigned short fVersionToExtract; + unsigned short fGPBitFlag; + unsigned short fCompressionMethod; + unsigned short fLastModFileTime; + unsigned short fLastModFileDate; + unsigned long fCRC32; + unsigned long fCompressedSize; + unsigned long fUncompressedSize; + unsigned short fFileNameLength; + unsigned short fExtraFieldLength; + unsigned short fFileCommentLength; + unsigned short fDiskNumberStart; + unsigned short fInternalAttrs; + unsigned long fExternalAttrs; + unsigned long fLocalHeaderRelOffset; + unsigned char* fFileName; + // extra field + unsigned char* fFileComment; // alloc with new[] + + void Dump(void) const; + + enum { + kSignature = 0x02014b50, + kCDELen = 46, // CentralDirEnt len, excl. var fields + }; + }; + + class EndOfCentralDir { + public: + EndOfCentralDir(void) : + fDiskNumber(0), + fDiskWithCentralDir(0), + fNumEntries(0), + fTotalNumEntries(0), + fCentralDirSize(0), + fCentralDirOffset(0), + fCommentLen(0) + {} + virtual ~EndOfCentralDir(void) {} + + DIError ReadBuf(const unsigned char* buf, int len); + DIError Write(GenericFD* pGFD); + + // unsigned long fSignature; + unsigned short fDiskNumber; + unsigned short fDiskWithCentralDir; + unsigned short fNumEntries; + unsigned short fTotalNumEntries; + unsigned long fCentralDirSize; + unsigned long fCentralDirOffset; // offset from first disk + unsigned short fCommentLen; + // archive comment + + enum { + kSignature = 0x06054b50, + kEOCDLen = 22, // EndOfCentralDir len, excl. comment + }; + + void Dump(void) const; + }; + + enum { + kDataDescriptorSignature = 0x08074b50, + + kMaxCommentLen = 65535, // longest possible in ushort + kMaxEOCDSearch = kMaxCommentLen + EndOfCentralDir::kEOCDLen, + + kZipFssep = '/', + kDefaultVersion = 20, + kMaxUncompressedSize = kGzipMax +256, + }; + enum { + kCompressStored = 0, // no compression + //kCompressShrunk = 1, + //kCompressImploded = 6, + kCompressDeflated = 8, // standard deflate + }; + + static DIError ReadCentralDir(GenericFD* pGFD, di_off_t outerLength, + CentralDirEntry* pDirEntry); + DIError ExtractZipEntry(GenericFD* pOuterGFD, CentralDirEntry* pCDE, + unsigned char** pBuf, di_off_t* pLength); + DIError InflateGFDToBuffer(GenericFD* pGFD, unsigned long compSize, + unsigned long uncompSize, unsigned char* buf); + DIError DeflateGFDToGFD(GenericFD* pDst, GenericFD* pSrc, di_off_t length, + di_off_t* pCompLength, unsigned long* pCRC); + +private: + void SetExtension(const char* ext); + void SetStoredFileName(const char* name); + void GetMSDOSTime(unsigned short* pDate, unsigned short* pTime); + void DOSTime(time_t when, unsigned short* pDate, unsigned short* pTime); + + char* fStoredFileName; + char* fExtension; +}; + + +/* + * =========================================================================== + * Image wrappers + * =========================================================================== + */ + +/* + * Image wrapper class, representing the format of the Windows files. + * Might be "raw" data, might be data with a header, might be a complex + * or compressed format that must be extracted to a buffer. + */ +class ImageWrapper { +public: + ImageWrapper(void) {} + virtual ~ImageWrapper(void) {} + + // all sub-classes should have one of these + // static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + + // open the file and prepare to access it; fills out return values + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) = 0; + + // fill out the wrapper, using the specified parameters + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) = 0; + + // push altered data to the wrapper GFD + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) = 0; + + // set the storage name (used by some formats) + virtual void SetStorageName(const char* name) { + // default implementation + assert(false); + } + + // indicate that we have a "fast" flush + virtual bool HasFastFlush(void) const = 0; + + // set by "Prep" on recoverable errors, like a CRC failure, for some fmts + virtual bool IsDamaged(void) const { return false; } + + // if this wrapper format includes a file comment, return it + //virtual const char* GetComment(void) const { return NULL; } + + /* + * Some additional goodies required for accessing variable-length nibble + * tracks in TrackStar images. A default implementation is provided and + * used for everything but TrackStar. + */ + virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const + { + if (physical == DiskImg::kPhysicalFormatNib525_6656) + return kTrackLenNib525; + else if (physical == DiskImg::kPhysicalFormatNib525_6384) + return kTrackLenNb2525; + else { + assert(false); + return -1; + } + } + virtual void SetNibbleTrackLength(int track, int length) { /*do nothing*/ } + virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const + { + if (physical == DiskImg::kPhysicalFormatNib525_6656 || + physical == DiskImg::kPhysicalFormatNib525_6384) + { + /* fixed-length tracks */ + return GetNibbleTrackLength(physical, 0) * track; + } else { + assert(false); + return -1; + } + } + // TrackStar images can have more, but otherwise all nibble images have 35 + virtual int GetNibbleNumTracks(void) const + { + return kTrackCount525; + } + +private: + ImageWrapper& operator=(const ImageWrapper&); + ImageWrapper(const ImageWrapper&); +}; + + +class Wrapper2MG : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } + //virtual const char* GetComment(void) const { return NULL; } + // (need to hold TwoImgHeader in the struct, rather than as temp, or + // need to copy the comment out into Wrapper2MG storage e.g. StorageName) +}; + +class WrapperNuFX : public ImageWrapper { +public: + WrapperNuFX(void) : fpArchive(NULL), fThreadIdx(0), fStorageName(NULL), + fCompressType(kNuThreadFormatLZW2) + {} + virtual ~WrapperNuFX(void) { CloseNuFX(); delete[] fStorageName; } + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + void SetStorageName(const char* name) { + delete[] fStorageName; + if (name != NULL) { + fStorageName = new char[strlen(name)+1]; + strcpy(fStorageName, name); + } else + fStorageName = NULL; + } + void SetCompressType(NuThreadFormat format) { fCompressType = format; } + +private: + enum { kDefaultStorageFssep = ':' }; + static NuResult ErrMsgHandler(NuArchive* pArchive, void* vErrorMessage); + static DIError OpenNuFX(const char* pathName, NuArchive** ppArchive, + NuThreadIdx* pThreadIdx, long* pLength, bool readOnly); + DIError GetNuFXDiskImage(NuArchive* pArchive, NuThreadIdx threadIdx, + long length, char** ppData); + static char* GenTempPath(const char* path); + DIError CloseNuFX(void); + void UNIXTimeToDateTime(const time_t* pWhen, NuDateTime *pDateTime); + + NuArchive* fpArchive; + NuThreadIdx fThreadIdx; + char* fStorageName; + NuThreadFormat fCompressType; +}; + +class WrapperDiskCopy42 : public ImageWrapper { +public: + WrapperDiskCopy42(void) : fStorageName(NULL), fBadChecksum(false) + {} + virtual ~WrapperDiskCopy42(void) { delete[] fStorageName; } + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + void SetStorageName(const char* name) { + delete[] fStorageName; + if (name != NULL) { + fStorageName = new char[strlen(name)+1]; + strcpy(fStorageName, name); + } else + fStorageName = NULL; + } + + virtual bool HasFastFlush(void) const { return false; } + virtual bool IsDamaged(void) const { return fBadChecksum; } + +private: + typedef struct DC42Header DC42Header; + static void DumpHeader(const DC42Header* pHeader); + void InitHeader(DC42Header* pHeader); + static int ReadHeader(GenericFD* pGFD, DC42Header* pHeader); + DIError WriteHeader(GenericFD* pGFD, const DC42Header* pHeader); + static DIError ComputeChecksum(GenericFD* pGFD, + unsigned long* pChecksum); + + char* fStorageName; + bool fBadChecksum; +}; + +class WrapperDDD : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + enum { + kMaxDDDZeroCount = 4, // 3 observed, 4 suspected + }; + +private: + class BitBuffer; + enum { + kNumTracks = 35, + kNumSectors = 16, + kSectorSize = 256, + kTrackLen = kNumSectors * kSectorSize, + }; + + static DIError CheckForRuns(GenericFD* pGFD); + static DIError Unpack(GenericFD* pGFD, GenericFD** ppNewGFD, + short* pDiskVolNum); + + static DIError UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD, + short* pDiskVolNum); + static bool UnpackTrack(BitBuffer* pBitBuffer, unsigned char* trackBuf); + static DIError PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD, + short diskVolNum); + static void PackTrack(const unsigned char* trackBuf, BitBuffer* pBitBuf); + static void ComputeFreqCounts(const unsigned char* trackBuf, + unsigned short* freqCounts); + static void ComputeFavorites(unsigned short* freqCounts, + unsigned char* favorites); + + short fDiskVolumeNum; +}; + +class WrapperSim2eHDV : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } +}; + +class WrapperTrackStar : public ImageWrapper { +public: + enum { + kTrackStarNumTracks = 40, + kFileTrackStorageLen = 6656, + kMaxTrackLen = kFileTrackStorageLen - (128+1+2), // header + footer + kCommentFieldLen = 0x2e, + }; + + WrapperTrackStar(void) : fStorageName(NULL) { + memset(&fNibbleTrackInfo, 0, sizeof(fNibbleTrackInfo)); + fNibbleTrackInfo.numTracks = -1; + } + virtual ~WrapperTrackStar(void) { delete[] fStorageName; } + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + virtual void SetStorageName(const char* name) + { + delete[] fStorageName; + if (name != NULL) { + fStorageName = new char[strlen(name)+1]; + strcpy(fStorageName, name); + } else + fStorageName = NULL; + } + +private: + static DIError VerifyTrack(int track, const unsigned char* trackBuf); + DIError Unpack(GenericFD* pGFD, GenericFD** ppNewGFD); + DIError UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD); + + int fImageTracks; + char* fStorageName; + + /* + * Data structure for managing nibble images with variable-length tracks. + */ + typedef struct { + int numTracks; // should be 35 or 40 + int length[kMaxNibbleTracks525]; + int offset[kMaxNibbleTracks525]; + } NibbleTrackInfo; + NibbleTrackInfo fNibbleTrackInfo; // count and lengths for variable formats + + // nibble images can have variable-length data fields + virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.length[track]; + } + virtual void SetNibbleTrackLength(int track, int length); +#if 0 + { + assert(track >= 0); + assert(length > 0 && length <= kMaxTrackLen); + assert(track < fNibbleTrackInfo.numTracks); + + fNibbleTrackInfo.length[track] = length; + } +#endif + virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.offset[track]; + } + virtual int GetNibbleNumTracks(void) const + { + return kTrackStarNumTracks; + } +}; + +class WrapperFDI : public ImageWrapper { +public: + WrapperFDI(void) {} + virtual ~WrapperFDI(void) {} + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + enum { + kSignatureLen = 27, + kCreatorLen = 30, + kCommentLen = 80, + }; + +private: + static const char* kFDIMagic; + + /* what type of disk is this? */ + typedef enum DiskType { + kDiskType8 = 0, + kDiskType525 = 1, + kDiskType35 = 2, + kDiskType3 = 3 + } DiskType; + + /* + * Contents of FDI header. + */ + typedef struct FDIHeader { + char signature[kSignatureLen+1]; + char creator[kCreatorLen+1]; + // CR + LF + char comment[kCommentLen+1]; + // MS-DOS EOF + unsigned short version; + unsigned short lastTrack; + unsigned char lastHead; + unsigned char type; // DiskType enum + unsigned char rotSpeed; + unsigned char flags; + unsigned char tpi; + unsigned char headWidth; + unsigned short reserved; + // track descriptors follow, at byte 152 + } FDIHeader; + + /* + * Header for pulse-index streams track. + */ + typedef struct PulseIndexHeader { + long numPulses; + long avgStreamLen; + int avgStreamCompression; + long minStreamLen; + int minStreamCompression; + long maxStreamLen; + int maxStreamCompression; + long idxStreamLen; + int idxStreamCompression; + + unsigned long* avgStream; // 4 bytes/pulse + unsigned long* minStream; // 4 bytes/pulse; optional + unsigned long* maxStream; // 4 bytes/pulse; optional + unsigned long* idxStream; // 2 bytes/pulse; optional? + } PulseIndexHeader; + + enum { + kTrackDescrOffset = 152, + kMaxHeads = 2, + kMaxHeaderBlockTracks = 180, // max 90 double-sided cylinders + kMinHeaderLen = 512, + kMinVersion = 0x0200, // v2.0 + + kMaxNibbleTracks35 = 80, // 80 double-sided tracks + kNibbleBufLen = 10240, // max seems to be a little under 10K + kBitBufferSize = kNibbleBufLen + (kNibbleBufLen / 4), + + kMaxSectors35 = 12, // max #of sectors per track + //kBytesPerSector35 = 512, // bytes per sector on 3.5" disk + + kPulseStreamDataOffset = 16, // start of header to avg stream + + kBitRate525 = 250000, // 250Kbits/sec + }; + + /* meaning of the two-bit compression format value */ + typedef enum CompressedFormat { + kCompUncompressed = 0, + kCompHuffman = 1, + } CompressedFormat; + + /* node in the Huffman tree */ + typedef struct HuffNode { + unsigned short val; + struct HuffNode* left; + struct HuffNode* right; + } HuffNode; + + /* + * Keep a copy of the header around while we work. None of the formats + * we're interested in have more than kMaxHeaderBlockTracks tracks in + * them, so we don't need anything beyond the initial 512-byte header. + */ + unsigned char fHeaderBuf[kMinHeaderLen]; + + static void UnpackHeader(const unsigned char* headerBuf, FDIHeader* hdr); + static void DumpHeader(const FDIHeader* pHdr); + + DIError Unpack525(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, + int numHeads); + DIError Unpack35(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, + int numHeads, LinearBitmap** ppBadBlockMap); + DIError PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD); + + DIError UnpackDisk525(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, + int numHeads); + DIError UnpackDisk35(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, + int numHeads, LinearBitmap* pBadBlockMap); + void GetTrackInfo(int trk, int* pType, int* pLength256); + + int BitRate35(int trk); + void FixBadNibbles(unsigned char* nibbleBuf, long nibbleLen); + bool DecodePulseTrack(const unsigned char* inputBuf, long inputLen, + int bitRate, unsigned char* nibbleBuf, long* pNibbleLen); + bool UncompressPulseStream(const unsigned char* inputBuf, long inputLen, + unsigned long* outputBuf, long numPulses, int format, int bytesPerPulse); + bool ExpandHuffman(const unsigned char* inputBuf, long inputLen, + unsigned long* outputBuf, long numPulses); + const unsigned char* HuffExtractTree(const unsigned char* inputBuf, + HuffNode* pNode, unsigned char* pBits, unsigned char* pBitMask); + const unsigned char* HuffExtractValues16(const unsigned char* inputBuf, + HuffNode* pNode); + const unsigned char* HuffExtractValues8(const unsigned char* inputBuf, + HuffNode* pNode); + void HuffFreeNodes(HuffNode* pNode); + unsigned long HuffSignExtend16(unsigned long val); + unsigned long HuffSignExtend8(unsigned long val); + bool ConvertPulseStreamsToNibbles(PulseIndexHeader* pHdr, int bitRate, + unsigned char* nibbleBuf, long* pNibbleLen); + bool ConvertPulsesToBits(const unsigned long* avgStream, + const unsigned long* minStream, const unsigned long* maxStream, + const unsigned long* idxStream, int numPulses, int maxIndex, + int indexOffset, unsigned long totalAvg, int bitRate, + unsigned char* outputBuf, int* pOutputLen); + int MyRand(void); + bool ConvertBitsToNibbles(const unsigned char* bitBuffer, int bitCount, + unsigned char* nibbleBuf, long* pNibbleLen); + + + int fImageTracks; + char* fStorageName; + + + /* + * Data structure for managing nibble images with variable-length tracks. + */ + typedef struct { + int numTracks; // expect 35 or 40 for 5.25" + int length[kMaxNibbleTracks525]; + int offset[kMaxNibbleTracks525]; + } NibbleTrackInfo; + NibbleTrackInfo fNibbleTrackInfo; // count and lengths for variable formats + + // nibble images can have variable-length data fields + virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.length[track]; + } + virtual void SetNibbleTrackLength(int track, int length); + virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.offset[track]; + } + virtual int GetNibbleNumTracks(void) const + { + return fNibbleTrackInfo.numTracks; + } +}; + + +class WrapperUnadornedNibble : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } +}; + +class WrapperUnadornedSector : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } +}; + + +/* + * =========================================================================== + * Non-FS DiskFSs + * =========================================================================== + */ + +/* + * A "raw" disk, i.e. no filesystem is known. Useful as a placeholder + * for applications that demand a DiskFS object even when the filesystem + * isn't known. + */ +class DISKIMG_API DiskFSUnknown : public DiskFS { +public: + DiskFSUnknown(void) : DiskFS() { + strcpy(fDiskVolumeName, "[Unknown]"); + strcpy(fDiskVolumeID, "Unknown FS"); + } + virtual ~DiskFSUnknown(void) {} + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return kDIErrNone; + } + + virtual const char* GetVolumeName(void) const { return fDiskVolumeName; } + virtual const char* GetVolumeID(void) const { return fDiskVolumeID; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + + // Use this if *something* is known about the filesystem, e.g. the + // partition type on a MacPart disk. + void SetVolumeInfo(const char* descr) { + if (strlen(descr) > kMaxVolumeName) + return; + + fDiskVolumeName[0] = '['; + strcpy(fDiskVolumeName+1, descr); + strcat(fDiskVolumeName, "]"); + strcpy(fDiskVolumeID, "Unknown FS - "); + strcat(fDiskVolumeID, descr); + } + +private: + enum { kMaxVolumeName = 64 }; + + char fDiskVolumeName[kMaxVolumeName+3]; + char fDiskVolumeID[kMaxVolumeName + 20]; +}; + + +/* + * Generic "container" DiskFS class. Contains some common functions shared + * among classes that are just containers for other filesystems. This class + * is not expected to be instantiated. + * + * TODO: create a common OpenSubVolume() function. + */ +class DISKIMG_API DiskFSContainer : public DiskFS { +public: + DiskFSContainer(void) : DiskFS() {} + virtual ~DiskFSContainer(void) {} + +protected: + virtual const char* GetDebugName(void) = 0; + virtual DIError CreatePlaceholder(long startBlock, long numBlocks, + const char* partName, const char* partType, + DiskImg** ppNewImg, DiskFS** ppNewFS); + virtual void SetVolumeUsageMap(void); +}; + +/* + * UNIDOS disk, an 800K floppy with two 400K DOS 3.3 volumes on it. + * + * The disk itself has no files; instead, it has two embedded sub-volumes. + */ +class DISKIMG_API DiskFSUNIDOS : public DiskFSContainer { +public: + DiskFSUNIDOS(void) : DiskFSContainer() {} + virtual ~DiskFSUNIDOS(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + static DIError TestWideFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[UNIDOS]"; } + virtual const char* GetVolumeID(void) const { return "[UNIDOS]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "UNIDOS"; } + DIError Initialize(void); + DIError OpenSubVolume(int idx); +}; + +/* + * OzDOS disk, an 800K floppy with two 400K DOS 3.3 volumes on it. They + * put the files for disk 1 in the odd sectors and the files for disk 2 + * in the even sectors (the top and bottom halves of a 512-byte block). + * + * The disk itself has no files; instead, it has two embedded sub-volumes. + * Because of the funky layout, we have to use the "sector pairing" feature + * of DiskImg to treat this like a DOS 3.3 disk. + */ +class DISKIMG_API DiskFSOzDOS : public DiskFSContainer { +public: + DiskFSOzDOS(void) : DiskFSContainer() {} + virtual ~DiskFSOzDOS(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[OzDOS]"; } + virtual const char* GetVolumeID(void) const { return "[OzDOS]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "OzDOS"; } + DIError Initialize(void); + DIError OpenSubVolume(int idx); +}; + +/* + * CFFA volume. A potentially very large volume with multiple partitions. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSCFFA : public DiskFSContainer { +public: + DiskFSCFFA(void) : DiskFSContainer() {} + virtual ~DiskFSCFFA(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[CFFA]"; } + virtual const char* GetVolumeID(void) const { return "[CFFA]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "CFFA"; } + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, + DiskImg::FSFormat* pFormatFound); + static DIError OpenSubVolume(DiskImg* pImg, long startBlock, + long numBlocks, bool scanOnly, DiskImg** ppNewImg, DiskFS** ppNewFS); + DIError Initialize(void); + DIError FindSubVolumes(void); + DIError AddVolumeSeries(int start, int count, long blocksPerVolume, + long& startBlock, long& totalBlocksLeft); + + enum { + kMinInterestingBlocks = 65536 + 1024, // less than this, ignore + kEarlyVolExpectedSize = 65536, // 32MB in 512-byte blocks + kOneGB = 1024*1024*(1024/512), // 1GB in 512-byte blocks + }; +}; + + +/* + * Macintosh-style partitioned disk image. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSMacPart : public DiskFSContainer { +public: + DiskFSMacPart(void) : DiskFSContainer() {} + virtual ~DiskFSMacPart(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[MacPartition]"; } + virtual const char* GetVolumeID(void) const { return "[MacPartition]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "MacPart"; } + + struct PartitionMap; // fwd + struct DriverDescriptorRecord; // fwd + static void UnpackDDR(const unsigned char* buf, + DriverDescriptorRecord* pDDR); + static void DumpDDR(const DriverDescriptorRecord* pDDR); + static void UnpackPartitionMap(const unsigned char* buf, + PartitionMap* pMap); + static void DumpPartitionMap(long block, const PartitionMap* pMap); + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + DIError OpenSubVolume(const PartitionMap* pMap); + DIError Initialize(void); + DIError FindSubVolumes(void); + + enum { + kMinInterestingBlocks = 2048, // less than this, ignore + kDDRSignature = 0x4552, // 'ER' + kPartitionSignature = 0x504d, // 'PM' + }; +}; + + +/* + * Partitioning for Joachim Lange's MicroDrive card. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSMicroDrive : public DiskFSContainer { +public: + DiskFSMicroDrive(void) : DiskFSContainer() {} + virtual ~DiskFSMicroDrive(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[MicroDrive]"; } + virtual const char* GetVolumeID(void) const { return "[MicroDrive]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "MicroDrive"; } + + struct PartitionMap; // fwd + static void UnpackPartitionMap(const unsigned char* buf, + PartitionMap* pMap); + static void DumpPartitionMap(const PartitionMap* pMap); + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + DIError OpenSubVolume(long startBlock, long numBlocks); + DIError OpenVol(int idx, long startBlock, long numBlocks); + DIError Initialize(void); + DIError FindSubVolumes(void); + + enum { + kMinInterestingBlocks = 2048, // less than this, ignore + kPartitionSignature = 0xccca, // 'JL' in little-endian high-ASCII + }; +}; + + +/* + * Partitioning for Parsons Engineering FocusDrive card. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSFocusDrive : public DiskFSContainer { +public: + DiskFSFocusDrive(void) : DiskFSContainer() {} + virtual ~DiskFSFocusDrive(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[FocusDrive]"; } + virtual const char* GetVolumeID(void) const { return "[FocusDrive]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "FocusDrive"; } + + struct PartitionMap; // fwd + static void UnpackPartitionMap(const unsigned char* buf, + const unsigned char* nameBuf, PartitionMap* pMap); + static void DumpPartitionMap(const PartitionMap* pMap); + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + DIError OpenSubVolume(long startBlock, long numBlocks, + const char* name); + DIError OpenVol(int idx, long startBlock, long numBlocks, + const char* name); + DIError Initialize(void); + DIError FindSubVolumes(void); + + enum { + kMinInterestingBlocks = 2048, // less than this, ignore + }; +}; + + +/* + * =========================================================================== + * DOS 3.2/3.3 + * =========================================================================== + */ + +class A2FileDOS; + +/* + * DOS 3.2/3.3 disk. + */ +class DISKIMG_API DiskFSDOS33 : public DiskFS { +public: + DiskFSDOS33(void) : DiskFS() { + fVTOCLoaded = false; + fDiskIsGood = false; + } + virtual ~DiskFSDOS33(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(initMode); + } + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + + virtual const char* GetVolumeName(void) const { return fDiskVolumeName; } + virtual const char* GetVolumeID(void) const { return fDiskVolumeID; } + virtual const char* GetBareVolumeName(void) const { + // this is fragile -- skip over the "DOS" part, return 3 digits + assert(strlen(fDiskVolumeName) > 3); + return fDiskVolumeName+3; + } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); + + /* + * Unique to DOS 3.3 disks. + */ + int GetDiskVolumeNum(void) const { return fDiskVolumeNum; } + void SetDiskVolumeNum(int val); + + static bool IsValidFileName(const char* name); + static bool IsValidVolumeName(const char* name); + + // utility function + static void LowerASCII(unsigned char* buf, long len); + static void ReplaceFssep(char* str, char replacement); + + enum { + kMinTracks = 17, // need to put the catalog track here + kMaxTracks = 50, + kMaxCatalogSectors = 64, // two tracks on a 32-sector disk + }; + + /* a T/S pair */ + typedef struct TrackSector { + char track; + char sector; + } TrackSector; + + friend class A2FDDOS; // for Write + +private: + DIError Initialize(InitMode initMode); + DIError ReadVTOC(void); + void UpdateVolumeNum(void); + void DumpVTOC(void); + void SetSectorUsage(long track, long sector, + VolumeUsage::ChunkPurpose purpose); + void FixVolumeUsageMap(void); + DIError ReadCatalog(void); + DIError ProcessCatalogSector(int catTrack, int catSect, + const unsigned char* sctBuf); + DIError GetFileLengths(void); + DIError ComputeLength(A2FileDOS* pFile, const TrackSector* tsList, + int tsCount); + DIError TrimLastSectorUp(A2FileDOS* pFile, TrackSector lastTS); + void MarkFileUsage(A2FileDOS* pFile, TrackSector* tsList, int tsCount, + TrackSector* indexList, int indexCount); + //DIError TrimLastSectorDown(A2FileDOS* pFile, unsigned short* tsBuf, + // int maxZeroCount); + void DoNormalizePath(const char* name, char fssep, char* outBuf); + DIError MakeFileNameUnique(char* fileName); + DIError GetFreeCatalogEntry(TrackSector* pCatSect, int* pCatEntry, + unsigned char* sctBuf, A2FileDOS** ppPrevEntry); + void CreateDirEntry(unsigned char* sctBuf, int catEntry, + const char* fileName, TrackSector* pTSSect, unsigned char fileType, + int access); + void FreeTrackSectors(TrackSector* pList, int count); + + bool CheckDiskIsGood(void); + + DIError WriteDOSTracks(int sectPerTrack); + + DIError ScanVolBitmap(void); + DIError LoadVolBitmap(void); + DIError SaveVolBitmap(void); + void FreeVolBitmap(void); + DIError AllocSector(TrackSector* pTS); + DIError CreateEmptyBlockMap(bool withDOS); + bool GetSectorUseEntry(long track, int sector) const; + void SetSectorUseEntry(long track, int sector, bool inUse); + inline unsigned long GetVTOCEntry(const unsigned char* pVTOC, + long track) const; + + // Largest interesting volume is 400K (50 tracks, 32 sectors), but + // we may be looking at it in 16-sector mode, so max tracks is 100. + enum { + kMaxInterestingTracks = 100, + kSectorSize = 256, + kDefaultVolumeNum = 254, + kMaxExtensionLen = 4, // used when normalizing; ".gif" is 4 + }; + + // DOS track images, for initializing disk images + static const unsigned char gDOS33Tracks[]; + static const unsigned char gDOS32Tracks[]; + + /* some fields from the VTOC */ + int fFirstCatTrack; + int fFirstCatSector; + int fVTOCVolumeNumber; + int fVTOCNumTracks; + int fVTOCNumSectors; + + /* private data */ + int fDiskVolumeNum; // usually 254 + char fDiskVolumeName[7]; // "DOS" + num, e.g. "DOS001", "DOS254" + char fDiskVolumeID[32]; // sizeof "DOS 3.3 Volume " +3 +1 + unsigned char fVTOC[kSectorSize]; + bool fVTOCLoaded; + + /* + * There are some things we need to be careful of when reading the + * catalog track, like bad links and infinite loops. By storing a list + * of known good catalog sectors, we only have to handle that stuff once. + * The catalog doesn't grow or shrink, so this never needs to be updated. + */ + TrackSector fCatalogSectors[kMaxCatalogSectors]; + + bool fDiskIsGood; +}; + +/* + * File descriptor for an open DOS file. + */ +class DISKIMG_API A2FDDOS : public A2FileDescr { +public: + A2FDDOS(A2File* pFile) : A2FileDescr(pFile) { + fTSList = NULL; + fIndexList = NULL; + fOffset = 0; + fModified = false; + } + virtual ~A2FDDOS(void) { + delete[] fTSList; + delete[] fIndexList; + //fTSList = fIndexList = NULL; + } + + //typedef DiskFSDOS33::TrackSector TrackSector; + + friend class A2FileDOS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + typedef DiskFSDOS33::TrackSector TrackSector; + + TrackSector* fTSList; // T/S entries for data sectors + int fTSCount; + TrackSector* fIndexList; // T/S entries for T/S list sectors + int fIndexCount; + di_off_t fOffset; // current position in file + + di_off_t fOpenEOF; // how big the file currently is + long fOpenSectorsUsed; // how many sectors it occupies + bool fModified; // if modified, update stuff on Close + + void DumpTSList(void) const; +}; + +/* + * Holds DOS files. Works for DOS33, DOS32, and "wide" DOS implementations. + * + * Catalog contents are public so anyone who wants gory details of DOS 3.3 + * stuff can poke at whatever they want. Anybody else can use the virtual + * interfaces to get standardized answers for things like file type. + * + * The embedded address and length fields found in Applesoft, Integer, and + * Binary files are quietly skipped over with the fDataOffset field when + * files are read. + * + * THOUGHT: have "get filename" and "get raw filename" interfaces? There + * are no directories, so maybe we don't care about "raw pathname"?? Might + * be better to always return the "raw" value and let the caller deal with + * things like high ASCII. + */ +class DISKIMG_API A2FileDOS : public A2File { +public: + A2FileDOS(DiskFS* pDiskFS); + virtual ~A2FileDOS(void); + + // assorted constants + enum { + kMaxFileName = 30, + }; + typedef enum { + kTypeUnknown = -1, + kTypeText = 0x00, // 'T' + kTypeInteger = 0x01, // 'I' + kTypeApplesoft = 0x02, // 'A' + kTypeBinary = 0x04, // 'B' + kTypeS = 0x08, // 'S' + kTypeReloc = 0x10, // 'R' + kTypeA = 0x20, // 'A' + kTypeB = 0x40, // 'B' + + kTypeLocked = 0x80 // bitwise OR with previous values + } FileType; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const { return fAuxType; } + virtual long GetAccess(void) const; + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; } + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fSparseLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + void Dump(void) const; + + typedef DiskFSDOS33::TrackSector TrackSector; + + /* + * Contents of directory entry. + * + * We don't hold deleted or unused entries, so fTSListTrack is always + * valid. + */ + short fTSListTrack; // (could use TrackSector here) + short fTSListSector; + unsigned short fLengthInSectors; + bool fLocked; + char fFileName[kMaxFileName+1]; // "fixed" version + FileType fFileType; + + TrackSector fCatTS; // track/sector for our catalog entry + int fCatEntryNum; // entry number within cat sector + + // these are computed or determined from the file contents + unsigned short fAuxType; // addr for bin, etc. + short fDataOffset; // for 'A'/'B'/'I' with embedded len + di_off_t fLength; // file length, in bytes + di_off_t fSparseLength; // file length, factoring sparse out + + void FixFilename(void); + + DIError LoadTSList(TrackSector** pTSList, int* pTSCount, + TrackSector** pIndexList = NULL, int* pIndexCount = NULL); + static FileType ConvertFileType(long prodosType, di_off_t fileLen); + static bool IsValidType(long prodosType); + static void MakeDOSName(char* buf, const char* name); + static void TrimTrailingSpaces(char* filename); + +private: + DIError ExtractTSPairs(const unsigned char* sctBuf, TrackSector* tsList, + int* pLastNonZero); + + A2FDDOS* fpOpenFile; +}; + + +/* + * =========================================================================== + * ProDOS + * =========================================================================== + */ + +class A2FileProDOS; + +/* + * ProDOS disk. + * + * THOUGHT: it would be undesirable for the CiderPress UI, but it would + * make things somewhat easier internally if we treated the volume dir + * like a subdirectory under which everything else sits, instead of special- + * casing it like we do. This is awkward because volume dirs have names + * under ProDOS, giving every pathname an extra component that they don't + * really need. We can never treat the volume dir purely as a subdir, + * because it can't expand beyond 51 files, but the storage_type in the + * header is sufficient to identify it as such (assuming the disk isn't + * broken). Certain operations, such as changing the file type or aux type, + * simply aren't possible on a volume dir, and deleting a volume dir doesn't + * make sense. So in some respects we simply trade one kind of special-case + * behavior for another. + */ +class DISKIMG_API DiskFSProDOS : public DiskFS { +public: + DiskFSProDOS(void) : fBitMapPointer(0), fTotalBlocks(0), fBlockUseMap(NULL) + {} + virtual ~DiskFSProDOS(void) { + if (fBlockUseMap != NULL) { + assert(false); // unexpected + delete[] fBlockUseMap; + } + } + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(initMode); + } + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); + + // assorted constants + enum { + kMaxVolumeName = 15, + }; + typedef unsigned long ProDate; + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + + //A2FileProDOS* GetVolDir(void) const { return fpVolDir; } + + static bool IsValidFileName(const char* name); + static bool IsValidVolumeName(const char* name); + static unsigned short GenerateLowerCaseBits(const char* upperName, + const char* lowerName, bool forAppleWorks); + static void GenerateLowerCaseName(const char* upperName, + char* lowerNameNoTerm, unsigned short lcFlags, bool fromAppleWorks); + + friend class A2FDProDOS; + +private: + struct DirHeader; + + enum { kMaxExtensionLen = 4 }; // used when normalizing; ".gif" is 4 + + DIError Initialize(InitMode initMode); + DIError LoadVolHeader(void); + void SetVolumeID(void); + void DumpVolHeader(void); + DIError ScanVolBitmap(void); + DIError LoadVolBitmap(void); + DIError SaveVolBitmap(void); + void FreeVolBitmap(void); + long AllocBlock(void); + int GetNumBitmapBlocks(void) const { + /* use fTotalBlocks rather than GetNumBlocks() */ + assert(fTotalBlocks > 0); + const int kBitsPerBlock = 512 * 8; + int numBlocks = (fTotalBlocks + kBitsPerBlock-1) / kBitsPerBlock; + return numBlocks; + } + DIError CreateEmptyBlockMap(void); + bool GetBlockUseEntry(long block) const; + void SetBlockUseEntry(long block, bool inUse); + bool ScanForExtraEntries(void) const; + + void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); + DIError GetDirHeader(const unsigned char* blkBuf, DirHeader* pHeader); + DIError RecursiveDirAdd(A2File* pParent, unsigned short dirBlock, + const char* basePath, int depth); + DIError SlurpEntries(A2File* pParent, const DirHeader* pHeader, + const unsigned char* blkBuf, bool skipFirst, int* pCount, + const char* basePath, unsigned short thisBlock, int depth); + DIError ReadExtendedInfo(A2FileProDOS* pFile); + DIError ScanFileUsage(void); + void ScanBlockList(long blockCount, unsigned short* blockList, + long indexCount, unsigned short* indexList, long* pSparseCount); + DIError ScanForSubVolumes(void); + DIError FindSubVolume(long blockStart, long blockCount, + DiskImg** ppDiskImg, DiskFS** ppDiskFS); + void MarkSubVolumeBlocks(long block, long count); + + A2File* FindFileByKeyBlock(A2File* pStart, unsigned short keyBlock); + DIError AllocInitialFileStorage(const CreateParms* pParms, + const char* upperName, unsigned short dirBlock, int dirEntrySlot, + long* pKeyBlock, int* pBlocksUsed, int* pNewEOF); + DIError WriteBootBlocks(void); + DIError DoNormalizePath(const char* path, char fssep, + char** pNormalizedPath); + void UpperCaseName(char* upperName, const char* name); + bool CheckDiskIsGood(void); + DIError AllocDirEntry(A2FileDescr* pOpenSubdir, unsigned char** ppDir, + long* pDirLen, unsigned char** ppDirEntry, unsigned short* pDirKeyBlock, + int* pDirEntrySlot, unsigned short* pDirBlock); + unsigned char* GetPrevDirEntry(unsigned char* buf, unsigned char* ptr); + DIError MakeFileNameUnique(const unsigned char* dirBuf, long dirLen, + char* fileName); + bool NameExistsInDir(const unsigned char* dirBuf, long dirLen, + const char* fileName); + + DIError FreeBlocks(long blockCount, unsigned short* blockList); + DIError RegeneratePathName(A2FileProDOS* pFile); + + /* some items from the volume header */ + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 16]; // add "ProDOS /" + unsigned char fAccess; + ProDate fCreateWhen; + ProDate fModWhen; + unsigned short fBitMapPointer; + unsigned short fTotalBlocks; + //unsigned short fPrevBlock; + //unsigned short fNextBlock; + //unsigned char fVersion; + //unsigned char fMinVersion; + //unsigned char fEntryLength; + //unsigned char fEntriesPerBlock; + unsigned short fVolDirFileCount; + +// A2FileProDOS* fpVolDir; // a "fake" file entry for the volume dir + + /* + * This is a working copy of the block use map from blocks 6+. It should + * be loaded when we're about to modify files on the disk and freed + * immediately afterward. The goal is to facilitate speedy updates to the + * bitmap without creating problems if the application decides to modify + * one of the bitmap blocks directly (e.g. with the disk sector editor). + * It should never be held across calls. + */ + unsigned char* fBlockUseMap; + + /* + * Set this if the disk is "perfect". If it's not, we disallow write + * access for safety reasons. + */ + bool fDiskIsGood; + + /* set if something fixes damage so CheckDiskIsGood can't see it */ + bool fEarlyDamage; +}; + +/* + * File descriptor for an open ProDOS file. + * + * This only represents one fork. + */ +class DISKIMG_API A2FDProDOS : public A2FileDescr { +public: + A2FDProDOS(A2File* pFile) : A2FileDescr(pFile), fModified(false), + fBlockList(NULL), fOffset(0) + {} + virtual ~A2FDProDOS(void) { + delete[] fBlockList; + fBlockList = NULL; + } + + friend class A2FileProDOS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + + void DumpBlockList(void) const; + +private: + bool IsEmptyBlock(const unsigned char* blk); + DIError WriteDirectory(const void* buf, size_t len, size_t* pActual); + + /* state for open files */ + bool fModified; + long fBlockCount; + unsigned short* fBlockList; + di_off_t fOpenEOF; // current EOF + unsigned short fOpenBlocksUsed; // #of block used by open piece + int fOpenStorageType; + bool fOpenRsrcFork; // is this the resource fork? + di_off_t fOffset; // current file offset +}; + +/* + * Holds a ProDOS file. + */ +class DISKIMG_API A2FileProDOS : public A2File { +public: + A2FileProDOS(DiskFS* pDiskFS) : A2File(pDiskFS) { + fPathName = NULL; + fSparseDataEof = fSparseRsrcEof = -1; + fpOpenFile = NULL; + fParentDirBlock = 0; + fParentDirIdx = -1; + fpParent = NULL; + } + virtual ~A2FileProDOS(void) { + delete fpOpenFile; + delete[] fPathName; + } + + typedef DiskFSProDOS::ProDate ProDate; + + /* assorted constants */ + enum { + kMaxFileName = 15, + kFssep = ':', + kInvalidBlockNum = 1, // boot block, can't be in file + kMaxBlocksPerIndex = 256, + }; + /* ProDOS access permissions */ + enum { + kAccessRead = 0x01, + kAccessWrite = 0x02, + kAccessInvisible = 0x04, + kAccessBackup = 0x20, + kAccessRename = 0x40, + kAccessDelete = 0x80 + }; + /* contents of a directory entry */ + typedef struct DirEntry { + int storageType; + char fileName[kMaxFileName+1]; // shows lower case + unsigned char fileType; + unsigned short keyPointer; + unsigned short blocksUsed; + unsigned long eof; + ProDate createWhen; + unsigned char version; + unsigned char minVersion; + unsigned char access; + unsigned short auxType; + ProDate modWhen; + unsigned short headerPointer; + } DirEntry; + typedef struct ExtendedInfo { + unsigned char storageType; + unsigned short keyBlock; + unsigned short blocksUsed; + unsigned long eof; + } ExtendedInfo; + typedef enum StorageType { + kStorageDeleted = 0, /* indicates deleted file */ + kStorageSeedling = 1, /* <= 512 bytes */ + kStorageSapling = 2, /* < 128KB */ + kStorageTree = 3, /* < 16MB */ + kStoragePascalVolume = 4, /* see ProDOS technote 25 */ + kStorageExtended = 5, /* forked */ + kStorageDirectory = 13, + kStorageSubdirHeader = 14, + kStorageVolumeDirHeader = 15, + } StorageType; + + static bool IsRegularFile(int type) { + return (type == kStorageSeedling || type == kStorageSapling || + type == kStorageTree); + } + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fDirEntry.fileName; } + virtual const char* GetPathName(void) const { return fPathName; } + virtual char GetFssep(void) const { return kFssep; } + virtual long GetFileType(void) const { return fDirEntry.fileType; } + virtual long GetAuxType(void) const { return fDirEntry.auxType; } + virtual long GetAccess(void) const { return fDirEntry.access; } + virtual time_t GetCreateWhen(void) const; + virtual time_t GetModWhen(void) const; + virtual di_off_t GetDataLength(void) const { + if (GetQuality() == kQualityDamaged) + return 0; + if (fDirEntry.storageType == kStorageExtended) + return fExtData.eof; + else + return fDirEntry.eof; + } + virtual di_off_t GetRsrcLength(void) const { + if (fDirEntry.storageType == kStorageExtended) { + if (GetQuality() == kQualityDamaged) + return 0; + else + return fExtRsrc.eof; + } else + return -1; + } + virtual di_off_t GetDataSparseLength(void) const { + if (GetQuality() == kQualityDamaged) + return 0; + else + return fSparseDataEof; + } + virtual di_off_t GetRsrcSparseLength(void) const { + if (GetQuality() == kQualityDamaged) + return 0; + else + return fSparseRsrcEof; + } + virtual bool IsDirectory(void) const { + return (fDirEntry.storageType == kStorageDirectory || + fDirEntry.storageType == kStorageVolumeDirHeader); + } + virtual bool IsVolumeDirectory(void) const { + return (fDirEntry.storageType == kStorageVolumeDirHeader); + } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + virtual void SetParent(A2File* pParent) { fpParent = pParent; } + virtual A2File* GetParent(void) const { return fpParent; } + + static char NameToLower(char ch); + static void InitDirEntry(DirEntry* pEntry, + const unsigned char* entryBuf); + + virtual void Dump(void) const; + + /* directory entry contents for this file */ + DirEntry fDirEntry; + + /* pointer to directory entry (update dir if file size or dates change) */ + unsigned short fParentDirBlock; // directory block + int fParentDirIdx; // index in dir block + + /* these are only valid if storageType == kStorageExtended */ + ExtendedInfo fExtData; + ExtendedInfo fExtRsrc; + + void SetPathName(const char* basePath, const char* fileName); + static time_t ConvertProDate(ProDate proDate); + static ProDate ConvertProDate(time_t unixDate); + + /* returns "true" if AppleWorks aux type is used for lower-case name */ + static bool UsesAppleWorksAuxType(unsigned char fileType) { + return (fileType >= 0x19 && fileType <= 0x1b); + } + +#if 0 + /* change fPathName; should only be used by DiskFS rename */ + void SetPathName(const char* name) { + delete[] fPathName; + if (name == NULL) { + fPathName = NULL; + } else { + fPathName = new char[strlen(name)+1]; + if (fPathName != NULL) + strcpy(fPathName, name); + } + } +#endif + + DIError LoadBlockList(int storageType, unsigned short keyBlock, + long eof, long* pBlockCount, unsigned short** pBlockList, + long* pIndexBlockCount=NULL, unsigned short** pIndexBlockList=NULL); + DIError LoadDirectoryBlockList(unsigned short keyBlock, + long eof, long* pBlockCount, unsigned short** pBlockList); + + /* fork lengths without sparseness */ + di_off_t fSparseDataEof; + di_off_t fSparseRsrcEof; + +private: + DIError LoadIndexBlock(unsigned short block, unsigned short* list, + int maxCount); + DIError ValidateBlockList(const unsigned short* list, long count); + + char* fPathName; // full pathname to file on this volume + + A2FDProDOS* fpOpenFile; // only one fork can be open at a time + A2File* fpParent; +}; + + +/* + * =========================================================================== + * Pascal + * =========================================================================== + */ + +/* + * Pascal disk. + * + * There is no allocation map or file index blocks, just a linear collection + * of files with contiguous blocks. + */ +class A2FilePascal; + +class DISKIMG_API DiskFSPascal : public DiskFS { +public: + DiskFSPascal(void) : fDirectory(NULL) {} + virtual ~DiskFSPascal(void) { + if (fDirectory != NULL) { + assert(false); // unexpected + delete[] fDirectory; + } + } + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + + // assorted constants + enum { + kMaxVolumeName = 7, + kDirectoryEntryLen = 26, + }; + typedef unsigned short PascalDate; + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); + + static bool IsValidVolumeName(const char* name); + static bool IsValidFileName(const char* name); + + unsigned short GetTotalBlocks(void) const { return fTotalBlocks; } + + friend class A2FDPascal; + +private: + DIError Initialize(void); + DIError LoadVolHeader(void); + void SetVolumeID(void); + void DumpVolHeader(void); + DIError LoadCatalog(void); + DIError SaveCatalog(void); + void FreeCatalog(void); + DIError ProcessCatalog(void); + DIError ScanFileUsage(void); + void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); + DIError WriteBootBlocks(void); + bool CheckDiskIsGood(void); + void DoNormalizePath(const char* name, char fssep, char* outBuf); + DIError MakeFileNameUnique(char* fileName); + DIError FindLargestFreeArea(int *pPrevIdx, A2FilePascal** ppPrevFile); + unsigned char* FindDirEntry(A2FilePascal* pFile); + + enum { kMaxExtensionLen = 5 }; // used when normalizing; ".code" is 4 + + /* some items from the volume header */ + unsigned short fStartBlock; // first block of dir hdr; always 2 + unsigned short fNextBlock; // i.e. first block with data + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 16]; // add "Pascal ___:" + unsigned short fTotalBlocks; + unsigned short fNumFiles; + PascalDate fAccessWhen; // PascalDate last access + PascalDate fDateSetWhen; // PascalDate last date setting + unsigned short fStuff1; // + unsigned short fStuff2; // + + /* other goodies */ + bool fDiskIsGood; + bool fEarlyDamage; + + /* + * Pascal disks have one fixed-size directory. The contents aren't + * divided into blocks, which means you can't always edit an entry + * by loading a single block from disk and writing it back. Also, + * deleted entries are squeezed out, so if we delete an entry we + * have to reshuffle the entries below it. + * + * We want to keep the copy on disk synced up, so we don't hold on + * to this longer than necessary. Possibly less efficient that way; + * if it becomes a problem it's easy enough to change the behavior. + */ + unsigned char* fDirectory; +}; + +/* + * File descriptor for an open Pascal file. + */ +class DISKIMG_API A2FDPascal : public A2FileDescr { +public: + A2FDPascal(A2File* pFile) : A2FileDescr(pFile) { + fOffset = 0; + } + virtual ~A2FDPascal(void) { + /* nothing to clean up */ + } + + friend class A2FilePascal; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + di_off_t fOffset; // where we are + di_off_t fOpenEOF; // how big the file currently is + long fOpenBlocksUsed; // how many blocks it occupies + bool fModified; // if modified, update dir on Close +}; + +/* + * File on a Pascal disk. + */ +class DISKIMG_API A2FilePascal : public A2File { +public: + A2FilePascal(DiskFS* pDiskFS) : A2File(pDiskFS) { + fpOpenFile = NULL; + } + virtual ~A2FilePascal(void) { + /* this comes back and calls CloseDescr */ + if (fpOpenFile != NULL) + fpOpenFile->Close(); + } + + typedef DiskFSPascal::PascalDate PascalDate; + + // assorted constants + enum { + kMaxFileName = 15, + }; + typedef enum FileType { + kTypeUntyped = 0, // NON + kTypeXdsk = 1, // BAD (bad blocks) + kTypeCode = 2, // PCD + kTypeText = 3, // PTX + kTypeInfo = 4, // ? + kTypeData = 5, // PDA + kTypeGraf = 6, // ? + kTypeFoto = 7, // FOT? (hires image) + kTypeSecurdir = 8 // ?? + } FileType; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const { return 0; } + virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const; + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + + virtual void Dump(void) const; + + static time_t ConvertPascalDate(PascalDate pascalDate); + static A2FilePascal::PascalDate ConvertPascalDate(time_t unixDate); + static A2FilePascal::FileType ConvertFileType(long prodosType); + + /* fields pulled out of directory block */ + unsigned short fStartBlock; + unsigned short fNextBlock; + FileType fFileType; + char fFileName[kMaxFileName+1]; + unsigned short fBytesRemaining; + PascalDate fModWhen; + + /* derived fields */ + di_off_t fLength; + + /* note to self: don't try to store a directory offset here; they shift + every time you add or delete a file */ + +private: + A2FileDescr* fpOpenFile; +}; + + +/* + * =========================================================================== + * CP/M + * =========================================================================== + */ + +/* + * CP/M disk. + * + * We really ought to be using 1K blocks here, since that's the native + * CP/M format, but there's little value in making an exception for such + * a rarely used Apple II format. + * + * There is no allocation map or file index blocks, just a single 2K + * directory filled with files that have up to 16 1K blocks each. If + * a file is longer than 16K, a second entry with the identical name + * and user number is made. These "extents" may be sparse, so it's + * necessary to use the "records" field to determine the actual file length. + */ +class A2FileCPM; +class DISKIMG_API DiskFSCPM : public DiskFS { +public: + DiskFSCPM(void) : fDiskIsGood(false) {} + virtual ~DiskFSCPM(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "CP/M"; } + virtual const char* GetVolumeID(void) const { return "CP/M"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + + // assorted constants + enum { + kDirectoryEntryLen = 32, + kVolDirBlock = 24, // ProDOS block where volume dir starts + kDirFileNameLen = 11, // 8+3 without the '.' + kFullDirSize = 2048, // blocks 0 and 1 + kDirEntryBlockCount = 16, // #of blocks held in dir slot + kNumDirEntries = kFullDirSize/kDirectoryEntryLen, + kExtentsInLowByte = 32, + + kDirEntryFlagContinued = 0x8000, // "flags" word + }; + // Contents of the raw 32-byte directory entry. + // + // From http://www.seasip.demon.co.uk/Cpm/format31.html + // + // UU F1 F2 F3 F4 F5 F6 F7 F8 T1 T2 T3 EX S1 S2 RC .FILENAMETYP.... + // AL AL AL AL AL AL AL AL AL AL AL AL AL AL AL AL ................ + // + // If the high bit of T1 is set, the file is read-only. If the high + // bit of T2 is set, the file is a "system" file. + // + // An entry with UU=0x20 indicates a CP/M 3.1 disk label entry. + // An entry with UU=0x21 indicates a time stamp entry (2.x or 3.x). + // + // Files larger than (1024 * 16) have multiple "extent" entries, i.e. + // entries with the same user number and file name. + typedef struct DirEntry { + unsigned char userNumber; // 0-15 or 0-31 (usually 0), e5=unused + unsigned char fileName[kDirFileNameLen+1]; + unsigned short extent; // extent (EX + S2 * 32) + unsigned char S1; // Last Record Byte Count (app-specific) + unsigned char records; // #of 128-byte records in this extent + unsigned char blocks[kDirEntryBlockCount]; + bool readOnly; + bool system; + bool badBlockList; // set if block list is damaged + } DirEntry; + + static long CPMToProDOSBlock(long cpmBlock) { + return kVolDirBlock + (cpmBlock*2); + } + +private: + DIError Initialize(void); + DIError ReadCatalog(void); + DIError ScanFileUsage(void); + void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); + void FormatName(char* dstBuf, const char* srcBuf); + DIError ComputeLength(A2FileCPM* pFile); + bool CheckDiskIsGood(void); + + // the full set of raw dir entries + DirEntry fDirEntry[kNumDirEntries]; + + bool fDiskIsGood; +}; + +/* + * File descriptor for an open CP/M file. + */ +class DISKIMG_API A2FDCPM : public A2FileDescr { +public: + A2FDCPM(A2File* pFile) : A2FileDescr(pFile) { + //fOpen = false; + fBlockList = NULL; + } + virtual ~A2FDCPM(void) { + delete fBlockList; + fBlockList = NULL; + } + + friend class A2FileCPM; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + //bool fOpen; + di_off_t fOffset; + long fBlockCount; + unsigned char* fBlockList; +}; + +/* + * File on a CP/M disk. + */ +class DISKIMG_API A2FileCPM : public A2File { +public: + typedef DiskFSCPM::DirEntry DirEntry; + + A2FileCPM(DiskFS* pDiskFS, DirEntry* pDirEntry) : + A2File(pDiskFS), fpDirEntry(pDirEntry) + { + fDirIdx = -1; + fpOpenFile = NULL; + } + virtual ~A2FileCPM(void) { + delete fpOpenFile; + } + + // assorted constants + enum { + kMaxFileName = 12, // 8+3 including '.' + }; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const { return 0; } + virtual long GetAuxType(void) const { return 0; } + virtual long GetAccess(void) const { + if (fReadOnly) + return DiskFS::kFileAccessLocked; + else + return DiskFS::kFileAccessUnlocked; + } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; } + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + virtual void Dump(void) const; + + /* fields pulled out of directory block */ + char fFileName[kMaxFileName+1]; + bool fReadOnly; + + /* derived fields */ + di_off_t fLength; + int fDirIdx; // index into fDirEntry for part #1 + + DIError GetBlockList(long* pBlockCount, unsigned char* blockBuf) const; + +private: + const DirEntry* fpDirEntry; + A2FileDescr* fpOpenFile; +}; + + +/* + * =========================================================================== + * RDOS + * =========================================================================== + */ + +/* + * RDOS disk. + * + * There is no allocation map or file index blocks, just a linear collection + * of files with contiguous sectors. Very similar to Pascal. + * + * The one interesting quirk is the "converted 13-sector disk" format, where + * only 13 of 16 sectors are actually used. The linear sector addressing + * must take that into account. + */ +class A2FileRDOS; +class DISKIMG_API DiskFSRDOS : public DiskFS { +public: + DiskFSRDOS(void) {} + virtual ~DiskFSRDOS(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeName; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + + int GetOurSectPerTrack(void) const { return fOurSectPerTrack; } + +private: + static DIError TestCommon(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + FSLeniency leniency, DiskImg::FSFormat* pFormatFound); + + DIError Initialize(void); + DIError ReadCatalog(void); + DIError ScanFileUsage(void); + void SetSectorUsage(long track, long sector, + VolumeUsage::ChunkPurpose purpose); + + char fVolumeName[10]; // e.g. "RDOS 3.3" + int fOurSectPerTrack; +}; + +/* + * File descriptor for an open RDOS file. + */ +class DISKIMG_API A2FDRDOS : public A2FileDescr { +public: + A2FDRDOS(A2File* pFile) : A2FileDescr(pFile) { + fOffset = 0; + } + virtual ~A2FDRDOS(void) { + /* nothing to clean up */ + } + + friend class A2FileRDOS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + /* RDOS is unique in that it can put 13-sector disks on 16-sector tracks */ + inline int GetOurSectPerTrack(void) const { + DiskFSRDOS* pDiskFS = (DiskFSRDOS*) fpFile->GetDiskFS(); + return pDiskFS->GetOurSectPerTrack(); + } + + //bool fOpen; + di_off_t fOffset; +}; + +/* + * File on an RDOS disk. + */ +class DISKIMG_API A2FileRDOS : public A2File { +public: + A2FileRDOS(DiskFS* pDiskFS) : A2File(pDiskFS) { + //fOpen = false; + fpOpenFile = NULL; + } + virtual ~A2FileRDOS(void) { + delete fpOpenFile; + } + + // assorted constants + enum { + kMaxFileName = 24, + }; + typedef enum FileType { + kTypeUnknown = 0, + kTypeApplesoft, // 'A' + kTypeBinary, // 'B' + kTypeText, // 'T' + } FileType; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const { return fLoadAddr; } + virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; }; + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + void FixFilename(void); + virtual void Dump(void) const; + + /* fields pulled out of directory block */ + char fFileName[kMaxFileName+1]; + FileType fFileType; + unsigned short fNumSectors; + unsigned short fLoadAddr; + unsigned short fLength; + unsigned short fStartSector; + +private: + void TrimTrailingSpaces(char* filename); + + A2FileDescr* fpOpenFile; +}; + + +/* + * =========================================================================== + * HFS + * =========================================================================== + */ + +/* + * HFS disk. + */ +class A2FileHFS; +class DISKIMG_API DiskFSHFS : public DiskFS { +public: + DiskFSHFS(void) { + fLocalTimeOffset = -1; + fDiskIsGood = true; +#ifndef EXCISE_GPL_CODE + fHfsVol = NULL; +#endif + } + virtual ~DiskFSHFS(void) { +#ifndef EXCISE_GPL_CODE + hfs_callback_close(fHfsVol); + fHfsVol = (hfsvol*) 0xcdaaaacd; +#endif + } + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(initMode); + } + +#ifndef EXCISE_GPL_CODE + /* these are optional, defined as no-ops in the parent class */ + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); +#endif + + // assorted constants + enum { + kMaxVolumeName = 27, + kMaxExtensionLen = 4, // used when normalizing; ".gif" is 4 + }; + + /* mandatory functions */ + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return false; } + virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + +#ifndef EXCISE_GPL_CODE + hfsvol* GetHfsVol(void) const { return fHfsVol; } +#endif + + // utility function, used by app + static bool IsValidVolumeName(const char* name); + static bool IsValidFileName(const char* name); + +private: + enum { + // Macintosh 32-bit dates start in 1904, everybody else starts in + // 1970. Take the Mac date and adjust it 66 years plus 17 leap days. + // The annoying part is that HFS stores dates in local time, which + // means it's impossible to know absolutely when a file was modified. + // libhfs converts timestamps to the current time zone, so that a + // file written January 1st 2006 at 6pm in London will appear to have + // been written January 1st 2006 at 6pm in San Francisco if you + // happen to be sitting in California. + // + // This was fixed in HFS+, but we have to deal with it for now. The + // value below converts the date to local time in Greenwich; the + // current GMT offset and daylight saving time must be added to it. + // + // Curiously, the volume dates shown by Cmd-I on the volume on my + // Quadra are off by an hour, even though the file dates match. + kDateTimeOffset = (1970 - 1904) * 60 * 60 * 24 * 365 + + (60 * 60 * 24 * 17), + + kExpectedMinBlocks = 1440, // ignore volumes under 720K + }; + + struct MasterDirBlock; // fwd + static void UnpackMDB(const unsigned char* buf, MasterDirBlock* pMDB); + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + + DIError Initialize(InitMode initMode); + DIError LoadVolHeader(void); + void SetVolumeID(void); + void DumpVolHeader(void); + void SetVolumeUsageMap(void); + +#ifdef EXCISE_GPL_CODE + void CreateFakeFile(void); +#else + DIError RecursiveDirAdd(A2File* pParent, const char* basePath, int depth); + //void Sanitize(unsigned char* str); + DIError DoNormalizePath(const char* path, char fssep, + char** pNormalizedPath); + static int CompareMacFileNames(const char* str1, const char* str2); + DIError RegeneratePathName(A2FileHFS* pFile); + DIError MakeFileNameUnique(const char* pathName, char** pUniqueName); + + /* libhfs stuff */ + static unsigned long LibHFSCB(void* vThis, int op, unsigned long arg1, + void* arg2); + hfsvol* fHfsVol; +#endif + + + /* some items from the volume header */ + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 8]; // add "HFS :" + unsigned long fTotalBlocks; + unsigned long fAllocationBlockSize; + unsigned long fNumAllocationBlocks; + unsigned long fCreatedDateTime; + unsigned long fModifiedDateTime; + unsigned long fNumFiles; + unsigned long fNumDirectories; + + long fLocalTimeOffset; + bool fDiskIsGood; +}; + +/* + * File descriptor for an open HFS file. + */ +class DISKIMG_API A2FDHFS : public A2FileDescr { +public: +#ifdef EXCISE_GPL_CODE + A2FDHFS(A2File* pFile, void* unused) + : A2FileDescr(pFile), fOffset(0) + {} +#else + A2FDHFS(A2File* pFile, hfsfile* pHfsFile) + : A2FileDescr(pFile), fHfsFile(pHfsFile), fModified(false) + {} +#endif + virtual ~A2FDHFS(void) { +#ifndef EXCISE_GPL_CODE + if (fHfsFile != NULL) + hfs_close(fHfsFile); +#endif + } + + friend class A2FileHFS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: +#ifdef EXCISE_GPL_CODE + di_off_t fOffset; +#else + hfsfile* fHfsFile; + bool fModified; +#endif +}; + +/* + * File on an HFS disk. + */ +class DISKIMG_API A2FileHFS : public A2File { +public: + A2FileHFS(DiskFS* pDiskFS) : A2File(pDiskFS) { + fPathName = NULL; + fpOpenFile = NULL; +#ifdef EXCISE_GPL_CODE + fFakeFileBuf = NULL; +#else + //fOrigPathName = NULL; +#endif + } + virtual ~A2FileHFS(void) { + delete fpOpenFile; + delete[] fPathName; +#ifdef EXCISE_GPL_CODE + delete[] fFakeFileBuf; +#else + //delete[] fOrigPathName; +#endif + } + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fPathName; } + virtual char GetFssep(void) const { return kFssep; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const; + virtual long GetAccess(void) const { return fAccess; } + virtual time_t GetCreateWhen(void) const { return fCreateWhen; } + virtual time_t GetModWhen(void) const { return fModWhen; } + virtual di_off_t GetDataLength(void) const { return fDataLength; } + virtual di_off_t GetDataSparseLength(void) const { return fDataLength; } + virtual di_off_t GetRsrcLength(void) const { return fRsrcLength; } + virtual di_off_t GetRsrcSparseLength(void) const { return fRsrcLength; } + virtual bool IsDirectory(void) const { return fIsDir; } + virtual bool IsVolumeDirectory(void) const { return fIsVolumeDir; } + + virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + enum { + kMaxFileName = 31, + kFssep = ':', + kPdosType = 0x70646f73, // 'pdos' + }; + + void SetPathName(const char* basePath, const char* fileName); + virtual void Dump(void) const; + +#ifdef EXCISE_GPL_CODE + void SetFakeFile(void* buf, long len) { + assert(len > 0); + if (fFakeFileBuf != NULL) + delete[] fFakeFileBuf; + fFakeFileBuf = new char[len]; + memcpy(fFakeFileBuf, buf, len); + fDataLength = len; + } + const void* GetFakeFileBuf(void) const { return fFakeFileBuf; } +#else + void InitEntry(const hfsdirent* dirEntry); + void SetOrigPathName(const char* pathName); + virtual void SetParent(A2File* pParent) { fpParent = pParent; } + virtual A2File* GetParent(void) const { return fpParent; } + char* GetLibHFSPathName(void) const; + static void ConvertTypeToHFS(long fileType, long auxType, + char* pType, char* pCreator); +#endif + + bool fIsDir; + bool fIsVolumeDir; + long fType; + long fCreator; + char fFileName[kMaxFileName+1]; + char* fPathName; + di_off_t fDataLength; + di_off_t fRsrcLength; + time_t fCreateWhen; + time_t fModWhen; + long fAccess; + +private: +#ifdef EXCISE_GPL_CODE + char* fFakeFileBuf; +#else + //char* fOrigPathName; + A2File* fpParent; +#endif + A2FileDescr* fpOpenFile; // only one fork can be open at a time +}; + + +/* + * =========================================================================== + * Gutenberg + * =========================================================================== + */ + +class A2FileGutenberg; + +/* + * Gutenberg disk. + */ +class DISKIMG_API DiskFSGutenberg : public DiskFS { +public: + DiskFSGutenberg(void) : DiskFS() { + fVTOCLoaded = false; + fDiskIsGood = false; + } + virtual ~DiskFSGutenberg(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(initMode); + } + + virtual const char* GetVolumeName(void) const { return fDiskVolumeName; } + virtual const char* GetVolumeID(void) const { return fDiskVolumeID; } + virtual const char* GetBareVolumeName(void) const { + return fDiskVolumeName; + } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + + static bool IsValidFileName(const char* name); + static bool IsValidVolumeName(const char* name); + + // utility function + static void LowerASCII(unsigned char* buf, long len); + static void ReplaceFssep(char* str, char replacement); + + enum { + kMinTracks = 17, // need to put the catalog track here + kMaxTracks = 50, + kMaxCatalogSectors = 64, // two tracks on a 32-sector disk + }; + + /* a T/S pair */ + typedef struct TrackSector { + char track; + char sector; + } TrackSector; + + friend class A2FDGutenberg; // for Write + +private: + DIError Initialize(InitMode initMode); + DIError ReadVTOC(void); + void UpdateVolumeNum(void); + void DumpVTOC(void); + void SetSectorUsage(long track, long sector, + VolumeUsage::ChunkPurpose purpose); + void FixVolumeUsageMap(void); + DIError ReadCatalog(void); + DIError ProcessCatalogSector(int catTrack, int catSect, + const unsigned char* sctBuf); + DIError GetFileLengths(void); + DIError ComputeLength(A2FileGutenberg* pFile, const TrackSector* tsList, + int tsCount); + DIError TrimLastSectorUp(A2FileGutenberg* pFile, TrackSector lastTS); + void MarkFileUsage(A2FileGutenberg* pFile, TrackSector* tsList, int tsCount, + TrackSector* indexList, int indexCount); + DIError MakeFileNameUnique(char* fileName); + DIError GetFreeCatalogEntry(TrackSector* pCatSect, int* pCatEntry, + unsigned char* sctBuf, A2FileGutenberg** ppPrevEntry); + void CreateDirEntry(unsigned char* sctBuf, int catEntry, + const char* fileName, TrackSector* pTSSect, unsigned char fileType, + int access); + void FreeTrackSectors(TrackSector* pList, int count); + + bool CheckDiskIsGood(void); + + DIError WriteDOSTracks(int sectPerTrack); + + DIError ScanVolBitmap(void); + DIError LoadVolBitmap(void); + DIError SaveVolBitmap(void); + void FreeVolBitmap(void); + DIError AllocSector(TrackSector* pTS); + DIError CreateEmptyBlockMap(bool withDOS); + bool GetSectorUseEntry(long track, int sector) const; + void SetSectorUseEntry(long track, int sector, bool inUse); + inline unsigned long GetVTOCEntry(const unsigned char* pVTOC, + long track) const; + + // Largest interesting volume is 400K (50 tracks, 32 sectors), but + // we may be looking at it in 16-sector mode, so max tracks is 100. + enum { + kMaxInterestingTracks = 100, + kSectorSize = 256, + kDefaultVolumeNum = 254, + kMaxExtensionLen = 4, // used when normalizing; ".gif" is 4 + }; + + /* some fields from the VTOC */ + int fFirstCatTrack; + int fFirstCatSector; + int fVTOCVolumeNumber; + int fVTOCNumTracks; + int fVTOCNumSectors; + + /* private data */ + char fDiskVolumeName[10]; // + char fDiskVolumeID[11+12+1]; // sizeof "Gutenberg: " + 12 + null + unsigned char fVTOC[kSectorSize]; + bool fVTOCLoaded; + + /* + * There are some things we need to be careful of when reading the + * catalog track, like bad links and infinite loops. By storing a list + * of known good catalog sectors, we only have to handle that stuff once. + * The catalog doesn't grow or shrink, so this never needs to be updated. + */ + TrackSector fCatalogSectors[kMaxCatalogSectors]; + + bool fDiskIsGood; +}; + +/* + * File descriptor for an open Gutenberg file. + */ +class DISKIMG_API A2FDGutenberg : public A2FileDescr { +public: + A2FDGutenberg(A2File* pFile) : A2FileDescr(pFile) { + fOffset = 0; + fModified = false; + } + virtual ~A2FDGutenberg(void) { + } + + friend class A2FileGutenberg; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + typedef DiskFSGutenberg::TrackSector TrackSector; + + int fTSCount; + di_off_t fOffset; // current position in file + + di_off_t fOpenEOF; // how big the file currently is + long fOpenSectorsUsed; // how many sectors it occupies + bool fModified; // if modified, update stuff on Close + + void DumpTSList(void) const; +}; + +/* + * Holds Gutenberg files. + * + */ +class DISKIMG_API A2FileGutenberg : public A2File { +public: + A2FileGutenberg(DiskFS* pDiskFS); + virtual ~A2FileGutenberg(void); + + // assorted constants + enum { + kMaxFileName = 12, + }; + typedef enum { + kTypeText = 0x00, // 'T' + } FileType; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const { return fAuxType; } + virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; } + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fSparseLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + void Dump(void) const; + + typedef DiskFSGutenberg::TrackSector TrackSector; + + /* + * Contents of directory entry. + * + * We don't hold deleted or unused entries, so fTSListTrack is always + * valid. + */ + short fTrack; // (could use TrackSector here) + short fSector; + unsigned short fLengthInSectors; + bool fLocked; + char fFileName[kMaxFileName+1]; // "fixed" version + FileType fFileType; + + TrackSector fCatTS; // track/sector for our catalog entry + int fCatEntryNum; // entry number within cat sector + + // these are computed or determined from the file contents + unsigned short fAuxType; // addr for bin, etc. + short fDataOffset; // for 'A'/'B'/'I' with embedded len + di_off_t fLength; // file length, in bytes + di_off_t fSparseLength; // file length, factoring sparse out + + void FixFilename(void); + + static FileType ConvertFileType(long prodosType, di_off_t fileLen); + static bool IsValidType(long prodosType); + static void MakeDOSName(char* buf, const char* name); + static void TrimTrailingSpaces(char* filename); + +private: + DIError ExtractTSPairs(const unsigned char* sctBuf, TrackSector* tsList, + int* pLastNonZero); + + A2FDGutenberg* fpOpenFile; +}; + + +/* + * =========================================================================== + * FAT (including FAT12, FAT16, and FAT32) + * =========================================================================== + */ + +/* + * MS-DOS FAT disk. + * + * This is currently just the minimum necessary to properly recognize + * the disk. + */ +class A2FileFAT; +class DISKIMG_API DiskFSFAT : public DiskFS { +public: + DiskFSFAT(void) {} + virtual ~DiskFSFAT(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + // assorted constants + enum { + kMaxVolumeName = 11, + }; + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + enum { + kExpectedMinBlocks = 720, // ignore volumes under 360K + }; + + struct MasterBootRecord; // fwd + struct BootSector; + static bool UnpackMBR(const unsigned char* buf, MasterBootRecord* pOut); + static bool UnpackBootSector(const unsigned char* buf, BootSector* pOut); + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + + DIError Initialize(void); + DIError LoadVolHeader(void); + void DumpVolHeader(void); + void SetVolumeUsageMap(void); + void CreateFakeFile(void); + + /* some items from the volume header */ + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 8]; // add "FAT %s:" + unsigned long fTotalBlocks; +}; + +/* + * File descriptor for an open FAT file. + */ +class DISKIMG_API A2FDFAT : public A2FileDescr { +public: + A2FDFAT(A2File* pFile) : A2FileDescr(pFile) { + fOffset = 0; + } + virtual ~A2FDFAT(void) { + /* nothing to clean up */ + } + + friend class A2FileFAT; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + di_off_t fOffset; +}; + +/* + * File on a FAT disk. + */ +class DISKIMG_API A2FileFAT : public A2File { +public: + A2FileFAT(DiskFS* pDiskFS) : A2File(pDiskFS) { + fFakeFileBuf = NULL; + //fFakeFileLen = -1; + fpOpenFile = NULL; + } + virtual ~A2FileFAT(void) { + delete fpOpenFile; + delete[] fFakeFileBuf; + } + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const { return 0; }; + virtual long GetAuxType(void) const { return 0; } + virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; } + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + enum { kMaxFileName = 31 }; + + virtual void Dump(void) const; + + void SetFakeFile(void* buf, long len) { + assert(len > 0); + if (fFakeFileBuf != NULL) + delete[] fFakeFileBuf; + fFakeFileBuf = new char[len]; + memcpy(fFakeFileBuf, buf, len); + fLength = len; + } + const void* GetFakeFileBuf(void) const { return fFakeFileBuf; } + + char fFileName[kMaxFileName+1]; + di_off_t fLength; + +private: + char* fFakeFileBuf; + //long fFakeFileLen; + A2FileDescr* fpOpenFile; +}; + +}; // namespace DiskImgLib + +#endif /*__DISKIMGDETAIL__*/ diff --git a/diskimg/Gutenberg.cpp b/diskimg/Gutenberg.cpp new file mode 100644 index 0000000..9cb4834 --- /dev/null +++ b/diskimg/Gutenberg.cpp @@ -0,0 +1,671 @@ +/* + * 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 unsigned char* +GetCatalogEntryPtr(unsigned char* 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; + unsigned char 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) { + WMSG2(" Gutenberg detected end-of-catalog on cat (%d,%d)\n", + 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; + WMSG1(" Gutenberg directory links cause a loop (order=%d)\n", imageOrder); + goto bail; + } + + WMSG2(" Gutenberg foundGood=%d order=%d\n", 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)) + { + WMSG2(" Gutenberg test: bestCount=%d for order=%d\n", bestCount, bestOrder); + assert(bestOrder != DiskImg::kSectorOrderUnknown); + *pOrder = bestOrder; + *pFormat = DiskImg::kFormatGutenberg; + return kDIErrNone; + } + + WMSG0(" Gutenberg didn't find a valid filesystem.\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk is + * completely full and has no files on it. + */ +DIError +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\0", 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; + unsigned char 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) + { + WMSG2(" Gutenberg reading catalog sector T=%d S=%d\n", catTrack, catSect); + dierr = fpImg->ReadTrackSector(catTrack, catSect, sctBuf); + if (dierr != kDIErrNone) + goto bail; + sprintf(fDiskVolumeName, (const char *)&sctBuf[6], kMaxVolNameLen); + fDiskVolumeName[kMaxVolNameLen] = 0x00; + DiskFSGutenberg::LowerASCII((unsigned char*)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 unsigned char* sctBuf) +{ + A2FileGutenberg* pFile; + const unsigned char* 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; + unsigned char sctBuf[kSctSize]; + int tsCount = 0; + unsigned short currentTrack, currentSector; + + pFile = (A2FileGutenberg*) GetNextFile(nil); + while (pFile != nil) { + DIError dierr; + tsCount = 0; + currentTrack = pFile->fTrack; + currentSector = pFile->fSector; + + while (currentTrack < 0x80) { + tsCount ++; + dierr = fpImg->ReadTrackSector(currentTrack, currentSector, sctBuf); + if (dierr != kDIErrNone) { + WMSG1("Gutenberg failed loading track/sector for '%s'\n", + 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(unsigned char* buf, long len) +{ + while (len--) { + if (*buf & 0x80) { + if (*buf >= 0xa0) + *buf &= 0x7f; + else + *buf = (*buf & 0x7f) + 0x20; + } else + *buf = ((*buf & 0x3f) ^ 0x20) + 0x20; + + buf++; + } +} + + +/* + * =========================================================================== + * 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 = nil; +} + +/* + * 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((unsigned char*)fFileName, kMaxFileName); + TrimTrailingSpaces(fFileName); +} + +/* + * Trim the spaces off the end of a filename. + * + * Assumes the filename has already been converted to low ASCII. + */ +/*static*/ void +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 = nil; + + if (!readOnly) { + if (fpDiskFS->GetDiskImg()->GetReadOnly()) + return kDIErrAccessDenied; + if (fpDiskFS->GetFSDamaged()) + return kDIErrBadDiskImage; + } + + if (fpOpenFile != nil) { + dierr = kDIErrAlreadyOpen; + goto bail; + } + + if (rsrcFork) + return kDIErrForkNotFound; + + pOpenFile = new 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 = nil; + +bail: + delete pOpenFile; + return dierr; +} + +/* + * Dump the contents of an A2FileGutenberg. + */ +void +A2FileGutenberg::Dump(void) const +{ + WMSG1("A2FileGutenberg '%s'\n", fFileName); + WMSG2(" TS T=%-2d S=%-2d\n", fTrack, fSector); + WMSG2(" Cat T=%-2d S=%-2d\n", fCatTS.track, fCatTS.sector); + WMSG3(" type=%d lck=%d slen=%d\n", fFileType, fLocked, fLengthInSectors); + WMSG2(" auxtype=0x%04x length=%ld\n", + fAuxType, (long) fLength); +} + + +/* + * =========================================================================== + * A2FDGutenberg + * =========================================================================== + */ + +/* + * Read data from the current offset. + * + */ +DIError +A2FDGutenberg::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" Gutenberg reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + + A2FileGutenberg* pFile = (A2FileGutenberg*) fpFile; + + DIError dierr = kDIErrNone; + unsigned char 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) { + WMSG1(" Gutenberg error reading file '%s'\n", pFile->GetPathName()); + return dierr; + } + thisCount = kSctSize - bufOffset; + if (thisCount > len) + thisCount = len; + memcpy(buf, sctBuf + bufOffset, thisCount); + len -= thisCount; + buf = (char*)buf + thisCount; + 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; +} diff --git a/diskimg/diskimg.dsp b/diskimg/diskimg.dsp index 834b899..4bc54ab 100644 --- a/diskimg/diskimg.dsp +++ b/diskimg/diskimg.dsp @@ -1,294 +1,298 @@ -# Microsoft Developer Studio Project File - Name="diskimg" - Package Owner=<4> -# Microsoft Developer Studio Generated Build File, Format Version 6.00 -# ** DO NOT EDIT ** - -# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 - -CFG=diskimg - Win32 Debug -!MESSAGE This is not a valid makefile. To build this project using NMAKE, -!MESSAGE use the Export Makefile command and run -!MESSAGE -!MESSAGE NMAKE /f "diskimg.mak". -!MESSAGE -!MESSAGE You can specify a configuration when running NMAKE -!MESSAGE by defining the macro CFG on the command line. For example: -!MESSAGE -!MESSAGE NMAKE /f "diskimg.mak" CFG="diskimg - Win32 Debug" -!MESSAGE -!MESSAGE Possible choices for configuration are: -!MESSAGE -!MESSAGE "diskimg - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") -!MESSAGE "diskimg - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") -!MESSAGE - -# Begin Project -# PROP AllowPerConfigDependencies 0 -# PROP Scc_ProjName "" -# PROP Scc_LocalPath "" -CPP=cl.exe -MTL=midl.exe -RSC=rc.exe - -!IF "$(CFG)" == "diskimg - Win32 Release" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 0 -# PROP BASE Output_Dir "Release" -# PROP BASE Intermediate_Dir "Release" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 0 -# PROP Output_Dir "Release" -# PROP Intermediate_Dir "Release" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /c -# ADD CPP /nologo /MD /W3 /GX /O2 /D "WIN32" /D "NDEBUGX" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /c -# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 -# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 -# ADD BASE RSC /l 0x409 /d "NDEBUG" -# ADD RSC /l 0x409 /d "NDEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 -# ADD LINK32 ..\prebuilt\nufxlib2.lib ..\prebuilt\zdll.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 /out:"Release/diskimg4.dll" -# Begin Special Build Tool -SOURCE="$(InputPath)" -PostBuild_Desc=Copying DLL to app directory -PostBuild_Cmds=copy Release\diskimg4.dll ..\app copy Release\diskimg4.dll ..\mdc -# End Special Build Tool - -!ELSEIF "$(CFG)" == "diskimg - Win32 Debug" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 1 -# PROP BASE Output_Dir "Debug" -# PROP BASE Intermediate_Dir "Debug" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 1 -# PROP Output_Dir "Debug" -# PROP Intermediate_Dir "Debug" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /GZ /c -# ADD CPP /nologo /MDd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /GZ /c -# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 -# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 -# ADD BASE RSC /l 0x409 /d "_DEBUG" -# ADD RSC /l 0x409 /d "_DEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept -# ADD LINK32 ..\prebuilt\nufxlib2D.lib ..\prebuilt\zdll.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /out:"Debug/diskimg4.dll" /pdbtype:sept -# Begin Special Build Tool -SOURCE="$(InputPath)" -PostBuild_Desc=Copying debug DLL to app directory -PostBuild_Cmds=copy Debug\diskimg4.dll ..\app copy Debug\diskimg4.dll ..\mdc -# End Special Build Tool - -!ENDIF - -# Begin Target - -# Name "diskimg - Win32 Release" -# Name "diskimg - Win32 Debug" -# Begin Group "Source Files" - -# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" -# Begin Source File - -SOURCE=.\ASPI.cpp -# End Source File -# Begin Source File - -SOURCE=.\CFFA.cpp -# End Source File -# Begin Source File - -SOURCE=.\Container.cpp -# End Source File -# Begin Source File - -SOURCE=.\CPM.cpp -# End Source File -# Begin Source File - -SOURCE=.\DDD.cpp -# End Source File -# Begin Source File - -SOURCE=.\DiskFS.cpp -# End Source File -# Begin Source File - -SOURCE=.\DiskImg.cpp -# End Source File -# Begin Source File - -SOURCE=.\DIUtil.cpp -# End Source File -# Begin Source File - -SOURCE=.\DOS33.cpp -# End Source File -# Begin Source File - -SOURCE=.\DOSImage.cpp -# End Source File -# Begin Source File - -SOURCE=.\FAT.CPP -# End Source File -# Begin Source File - -SOURCE=.\FDI.cpp -# End Source File -# Begin Source File - -SOURCE=.\FocusDrive.cpp -# End Source File -# Begin Source File - -SOURCE=.\GenericFD.cpp -# End Source File -# Begin Source File - -SOURCE=.\Global.cpp -# End Source File -# Begin Source File - -SOURCE=.\HFS.cpp -# End Source File -# Begin Source File - -SOURCE=.\ImageWrapper.cpp -# End Source File -# Begin Source File - -SOURCE=.\MacPart.cpp -# End Source File -# Begin Source File - -SOURCE=.\MicroDrive.cpp -# End Source File -# Begin Source File - -SOURCE=.\Nibble.cpp -# End Source File -# Begin Source File - -SOURCE=.\Nibble35.cpp -# End Source File -# Begin Source File - -SOURCE=.\OuterWrapper.cpp -# End Source File -# Begin Source File - -SOURCE=.\OzDOS.cpp -# End Source File -# Begin Source File - -SOURCE=.\Pascal.cpp -# End Source File -# Begin Source File - -SOURCE=.\ProDOS.cpp -# End Source File -# Begin Source File - -SOURCE=.\RDOS.cpp -# End Source File -# Begin Source File - -SOURCE=.\SPTI.cpp -# End Source File -# Begin Source File - -SOURCE=.\StdAfx.cpp -# ADD CPP /Yc"stdafx.h" -# End Source File -# Begin Source File - -SOURCE=.\TwoImg.cpp -# End Source File -# Begin Source File - -SOURCE=.\UNIDOS.cpp -# End Source File -# Begin Source File - -SOURCE=.\VolumeUsage.cpp -# End Source File -# Begin Source File - -SOURCE=.\Win32BlockIO.cpp -# End Source File -# End Group -# Begin Group "Header Files" - -# PROP Default_Filter "h;hpp;hxx;hm;inl" -# Begin Source File - -SOURCE=.\ASPI.h -# End Source File -# Begin Source File - -SOURCE=.\CP_ntddscsi.h -# End Source File -# Begin Source File - -SOURCE=.\CP_WNASPI32.H -# End Source File -# Begin Source File - -SOURCE=.\DiskImg.h -# End Source File -# Begin Source File - -SOURCE=.\DiskImgDetail.h -# End Source File -# Begin Source File - -SOURCE=.\DiskImgPriv.h -# End Source File -# Begin Source File - -SOURCE=.\GenericFD.h -# End Source File -# Begin Source File - -SOURCE=.\SCSIDefs.h -# End Source File -# Begin Source File - -SOURCE=.\SPTI.h -# End Source File -# Begin Source File - -SOURCE=.\StdAfx.h -# End Source File -# Begin Source File - -SOURCE=.\TwoImg.h -# End Source File -# Begin Source File - -SOURCE=.\Win32BlockIO.h -# End Source File -# Begin Source File - -SOURCE=.\Win32Extra.h -# End Source File -# End Group -# Begin Group "Resource Files" - -# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" -# End Group -# End Target -# End Project +# Microsoft Developer Studio Project File - Name="diskimg" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 + +CFG=diskimg - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "diskimg.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "diskimg.mak" CFG="diskimg - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "diskimg - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE "diskimg - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "diskimg - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /c +# ADD CPP /nologo /MD /W3 /GX /O2 /D "WIN32" /D "NDEBUGX" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /c +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 +# ADD LINK32 ..\prebuilt\nufxlib2.lib ..\prebuilt\zdll.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 /out:"Release/diskimg4.dll" +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copying DLL to app directory +PostBuild_Cmds=copy Release\diskimg4.dll ..\app copy Release\diskimg4.dll ..\mdc +# End Special Build Tool + +!ELSEIF "$(CFG)" == "diskimg - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /GZ /c +# ADD CPP /nologo /MDd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /GZ /c +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept +# ADD LINK32 ..\prebuilt\nufxlib2D.lib ..\prebuilt\zdll.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /out:"Debug/diskimg4.dll" /pdbtype:sept +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copying debug DLL to app directory +PostBuild_Cmds=copy Debug\diskimg4.dll ..\app copy Debug\diskimg4.dll ..\mdc +# End Special Build Tool + +!ENDIF + +# Begin Target + +# Name "diskimg - Win32 Release" +# Name "diskimg - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Source File + +SOURCE=.\ASPI.cpp +# End Source File +# Begin Source File + +SOURCE=.\CFFA.cpp +# End Source File +# Begin Source File + +SOURCE=.\Container.cpp +# End Source File +# Begin Source File + +SOURCE=.\CPM.cpp +# End Source File +# Begin Source File + +SOURCE=.\DDD.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskFS.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskImg.cpp +# End Source File +# Begin Source File + +SOURCE=.\DIUtil.cpp +# End Source File +# Begin Source File + +SOURCE=.\DOS33.cpp +# End Source File +# Begin Source File + +SOURCE=.\DOSImage.cpp +# End Source File +# Begin Source File + +SOURCE=.\FAT.CPP +# End Source File +# Begin Source File + +SOURCE=.\FDI.cpp +# End Source File +# Begin Source File + +SOURCE=.\FocusDrive.cpp +# End Source File +# Begin Source File + +SOURCE=.\GenericFD.cpp +# End Source File +# Begin Source File + +SOURCE=.\Global.cpp +# End Source File +# Begin Source File + +SOURCE=.\Gutenberg.cpp +# End Source File +# Begin Source File + +SOURCE=.\HFS.cpp +# End Source File +# Begin Source File + +SOURCE=.\ImageWrapper.cpp +# End Source File +# Begin Source File + +SOURCE=.\MacPart.cpp +# End Source File +# Begin Source File + +SOURCE=.\MicroDrive.cpp +# End Source File +# Begin Source File + +SOURCE=.\Nibble.cpp +# End Source File +# Begin Source File + +SOURCE=.\Nibble35.cpp +# End Source File +# Begin Source File + +SOURCE=.\OuterWrapper.cpp +# End Source File +# Begin Source File + +SOURCE=.\OzDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\Pascal.cpp +# End Source File +# Begin Source File + +SOURCE=.\ProDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\RDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\SPTI.cpp +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.cpp +# ADD CPP /Yc"stdafx.h" +# End Source File +# Begin Source File + +SOURCE=.\TwoImg.cpp +# End Source File +# Begin Source File + +SOURCE=.\UNIDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\VolumeUsage.cpp +# End Source File +# Begin Source File + +SOURCE=.\Win32BlockIO.cpp +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter "h;hpp;hxx;hm;inl" +# Begin Source File + +SOURCE=.\ASPI.h +# End Source File +# Begin Source File + +SOURCE=.\CP_ntddscsi.h +# End Source File +# Begin Source File + +SOURCE=.\CP_WNASPI32.H +# End Source File +# Begin Source File + +SOURCE=.\DiskImg.h +# End Source File +# Begin Source File + +SOURCE=.\DiskImgDetail.h +# End Source File +# Begin Source File + +SOURCE=.\DiskImgPriv.h +# End Source File +# Begin Source File + +SOURCE=.\GenericFD.h +# End Source File +# Begin Source File + +SOURCE=.\SCSIDefs.h +# End Source File +# Begin Source File + +SOURCE=.\SPTI.h +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.h +# End Source File +# Begin Source File + +SOURCE=.\TwoImg.h +# End Source File +# Begin Source File + +SOURCE=.\Win32BlockIO.h +# End Source File +# Begin Source File + +SOURCE=.\Win32Extra.h +# End Source File +# End Group +# Begin Group "Resource Files" + +# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" +# End Group +# End Target +# End Project