mirror of
https://github.com/fadden/ciderpress.git
synced 2024-12-22 05:29:43 +00:00
3158 lines
84 KiB
C++
3158 lines
84 KiB
C++
/*
|
|
* 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 = "<blank filename>";
|
|
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);
|
|
}
|