ciderpress/app/NufxArchive.cpp

2705 lines
83 KiB
C++

/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Bridge between NufxLib and GenericArchive.
*/
#include "stdafx.h"
#include "NufxArchive.h"
#include "ConfirmOverwriteDialog.h"
#include "RenameEntryDialog.h"
#include "RecompressOptionsDialog.h"
#include "AddClashDialog.h"
#include "Main.h"
#include "../nufxlib/NufxLib.h"
/*
* NufxLib doesn't currently allow an fssep of '\0', so we use this instead
* to indicate the absence of an fssep char. Not quite right, but it'll do
* until NufxLib gets fixed.
*/
const unsigned char kNufxNoFssep = 0xff;
/*
* ===========================================================================
* NufxEntry
* ===========================================================================
*/
/*
* Extract data from a thread into a buffer.
*
* If "*ppText" is non-nil and "*pLength" is > 0, 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 or the length is <= 0, 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, "*ppText" and "*pLength" will
* be valid but point at an error message.
*
* "which" is an anonymous GenericArchive enum.
*/
int
NufxEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength,
CString* pErrMsg) const
{
NuError nerr;
char* dataBuf = nil;
NuDataSink* pDataSink = nil;
NuThread thread;
unsigned long actualThreadEOF;
NuThreadIdx threadIdx;
bool needAlloc = true;
int result = -1;
ASSERT(IDOK != -1 && IDCANCEL != -1); // make sure return vals don't clash
if (*ppText != nil)
needAlloc = false;
FindThreadInfo(which, &thread, pErrMsg);
if (!pErrMsg->IsEmpty())
goto bail;
threadIdx = thread.threadIdx;
actualThreadEOF = thread.actualThreadEOF;
/*
* We've got the right thread. Create an appropriately-sized buffer
* and extract the data into it (WITHOUT doing EOL conversion).
*
* First check for a length of zero.
*/
if (actualThreadEOF == 0) {
WMSG0("Empty thread\n");
if (needAlloc) {
*ppText = new char[1];
**ppText = '\0';
}
*pLength = 0;
result = IDOK;
goto bail;
}
if (needAlloc) {
dataBuf = new char[actualThreadEOF];
if (dataBuf == nil) {
pErrMsg->Format(L"allocation of %ld bytes failed",
actualThreadEOF);
goto bail;
}
} else {
if (*pLength < (long) actualThreadEOF) {
pErrMsg->Format(L"buf size %ld too short (%ld)",
*pLength, actualThreadEOF);
goto bail;
}
dataBuf = *ppText;
}
nerr = NuCreateDataSinkForBuffer(true, kNuConvertOff,
(unsigned char*)dataBuf, actualThreadEOF, &pDataSink);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"unable to create buffer data sink: %hs",
NuStrError(nerr));
goto bail;
}
SET_PROGRESS_BEGIN();
nerr = NuExtractThread(fpArchive, threadIdx, pDataSink);
if (nerr != kNuErrNone) {
if (nerr == kNuErrAborted) {
result = IDCANCEL;
//::sprintf(errorBuf, "Cancelled.\n");
} else if (nerr == kNuErrBadFormat) {
pErrMsg->Format(L"The compression method used on this file is not supported "
L"by your copy of \"nufxlib.dll\". For more information, "
L"please visit us on the web at "
L"http://www.faddensoft.com/ciderpress/");
} else {
pErrMsg->Format(L"unable to extract thread %ld: %hs",
threadIdx, NuStrError(nerr));
}
goto bail;
}
if (needAlloc)
*ppText = dataBuf;
*pLength = actualThreadEOF;
result = IDOK;
bail:
if (result == IDOK) {
SET_PROGRESS_END();
ASSERT(pErrMsg->IsEmpty());
} else {
ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty());
if (needAlloc) {
delete[] dataBuf;
ASSERT(*ppText == nil);
}
}
if (pDataSink != nil)
NuFreeDataSink(pDataSink);
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
NufxEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv,
ConvertHighASCII convHA, CString* pErrMsg) const
{
NuDataSink* pDataSink = nil;
NuError nerr;
NuThread thread;
unsigned long actualThreadEOF;
NuThreadIdx threadIdx;
int result = -1;
ASSERT(outfp != nil);
//CString errMsg;
FindThreadInfo(which, &thread, pErrMsg);
if (!pErrMsg->IsEmpty())
goto bail;
threadIdx = thread.threadIdx;
actualThreadEOF = thread.actualThreadEOF;
/* we've got the right thread, see if it's empty */
if (actualThreadEOF == 0) {
WMSG0("Empty thread\n");
result = IDOK;
goto bail;
}
/* set EOL conversion flags */
NuValue nuConv;
switch (conv) {
case kConvertEOLOff: nuConv = kNuConvertOff; break;
case kConvertEOLOn: nuConv = kNuConvertOn; break;
case kConvertEOLAuto: nuConv = kNuConvertAuto; break;
default:
ASSERT(false);
pErrMsg->Format(L"internal error: bad conv flag %d", conv);
goto bail;
}
if (which == kDiskImageThread) {
/* override the above; never EOL-convert a disk image */
nuConv = kNuConvertOff;
}
switch (convHA) {
case kConvertHAOff:
nerr = NuSetValue(fpArchive, kNuValueStripHighASCII, false);
break;
case kConvertHAOn:
case kConvertHAAuto:
nerr = NuSetValue(fpArchive, kNuValueStripHighASCII, true);
break;
default:
ASSERT(false);
pErrMsg->Format(L"internal error: bad convHA flag %d", convHA);
goto bail;
}
/* make sure we convert to CRLF */
nerr = NuSetValue(fpArchive, kNuValueEOL, kNuEOLCRLF); // for Win32
if (nerr != kNuErrNone) {
pErrMsg->Format(L"failed setting EOL value: %hs", NuStrError(nerr));
goto bail;
}
/* create a data sink for "outfp" */
nerr = NuCreateDataSinkForFP(true, nuConv, outfp, &pDataSink);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"unable to create FP data sink: %hs",
NuStrError(nerr));
goto bail;
}
/* extract the thread to the file */
SET_PROGRESS_BEGIN();
nerr = NuExtractThread(fpArchive, threadIdx, pDataSink);
if (nerr != kNuErrNone) {
if (nerr == kNuErrAborted) {
/* user hit the "cancel" button */
*pErrMsg = L"cancelled";
result = IDCANCEL;
} else if (nerr == kNuErrBadFormat) {
pErrMsg->Format(L"The compression method used on this file is not supported "
L"by your copy of \"nufxlib.dll\". For more information, "
L"please visit us on the web at "
L"http://www.faddensoft.com/ciderpress/");
} else {
pErrMsg->Format(L"unable to extract thread %ld: %hs",
threadIdx, NuStrError(nerr));
}
goto bail;
}
result = IDOK;
bail:
if (result == IDOK) {
SET_PROGRESS_END();
}
if (pDataSink != nil)
NuFreeDataSink(pDataSink);
return result;
}
/*
* Find info for the thread we're about to extract.
*
* Given the NuRecordIdx stored in the object, find the thread whose
* ThreadID matches "which". Copies the NuThread structure into
* "*pThread".
*
* On failure, "pErrMsg" will have a nonzero length, and contain an error
* message describing the problem.
*/
void
NufxEntry::FindThreadInfo(int which, NuThread* pRetThread,
CString* pErrMsg) const
{
NuError nerr;
ASSERT(pErrMsg->IsEmpty());
/*
* Retrieve the record from the archive.
*/
const NuRecord* pRecord;
nerr = NuGetRecord(fpArchive, fRecordIdx, &pRecord);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"NufxLib unable to locate record %ld: %hs",
fRecordIdx, NuStrError(nerr));
goto bail;
}
/*
* Find the right thread.
*/
const NuThread* pThread;
unsigned long wantedThreadID;
switch (which) {
case kDataThread: wantedThreadID = kNuThreadIDDataFork; break;
case kRsrcThread: wantedThreadID = kNuThreadIDRsrcFork; break;
case kDiskImageThread: wantedThreadID = kNuThreadIDDiskImage; break;
case kCommentThread: wantedThreadID = kNuThreadIDComment; break;
default:
pErrMsg->Format(L"looking for bogus thread 0x%02x", which);
goto bail;
}
int i;
pThread = nil;
for (i = 0; i < (int)NuRecordGetNumThreads(pRecord); i++) {
pThread = NuGetThread(pRecord, i);
if (NuGetThreadID(pThread) == wantedThreadID)
break;
}
if (i == (int)NuRecordGetNumThreads(pRecord)) {
/* didn't find the thread we wanted */
pErrMsg->Format(L"searched %d threads but couldn't find 0x%02x",
NuRecordGetNumThreads(pRecord), which);
goto bail;
}
memcpy(pRetThread, pThread, sizeof(*pRetThread));
bail:
return;
}
//static const char* gShortFormatNames[] = {
// "unc", "squ", "lz1", "lz2", "u12", "u16", "dfl", "bzp"
//};
static const WCHAR* gFormatNames[] = {
L"Uncompr", L"Squeeze", L"LZW/1", L"LZW/2", L"LZC-12",
L"LZC-16", L"Deflate", L"Bzip2"
};
/*
* Analyze the contents of a record to determine if it's a disk, file,
* or "other". Compute the total compressed and uncompressed lengths
* of all data threads. Return the "best" format.
*
* The "best format" and "record type" stuff assume that the entire
* record contains only a disk thread or a file thread, and that any
* format is interesting so long as it isn't "no compression". In
* general these will be true, because ShrinkIt and NuLib create files
* this way.
*
* You could, of course, create a single record with a data thread and
* a disk image thread, but it's a fair bet ShrinkIt would ignore one
* or the other.
*
* NOTE: we don't currently work around the GSHK zero-length file bug.
* Such records, which have a filename thread but no data threads at all,
* will be categorized as "unknown". We could detect the situation and
* correct it, but we might as well flag it in a user-visible way.
*/
void
NufxEntry::AnalyzeRecord(const NuRecord* pRecord)
{
const NuThread* pThread;
NuThreadID threadID;
unsigned long idx;
RecordKind recordKind;
unsigned long uncompressedLen;
unsigned long compressedLen;
unsigned short format;
recordKind = kRecordKindUnknown;
uncompressedLen = compressedLen = 0;
format = kNuThreadFormatUncompressed;
for (idx = 0; idx < pRecord->recTotalThreads; idx++) {
pThread = NuGetThread(pRecord, idx);
ASSERT(pThread != nil);
threadID = NuMakeThreadID(pThread->thThreadClass,
pThread->thThreadKind);
if (pThread->thThreadClass == kNuThreadClassData) {
/* replace what's there if this might be more interesting */
if (format == kNuThreadFormatUncompressed)
format = (unsigned short) pThread->thThreadFormat;
if (threadID == kNuThreadIDRsrcFork)
recordKind = kRecordKindForkedFile;
else if (threadID == kNuThreadIDDiskImage)
recordKind = kRecordKindDisk;
else if (threadID == kNuThreadIDDataFork &&
recordKind == kRecordKindUnknown)
recordKind = kRecordKindFile;
/* sum up, so we get both forks of forked files */
//uncompressedLen += pThread->actualThreadEOF;
compressedLen += pThread->thCompThreadEOF;
}
if (threadID == kNuThreadIDDataFork) {
if (!GetHasDataFork() && !GetHasDiskImage()) {
SetHasDataFork(true);
SetDataForkLen(pThread->actualThreadEOF);
} else {
WMSG0("WARNING: ignoring second disk image / data fork\n");
}
}
if (threadID == kNuThreadIDRsrcFork) {
if (!GetHasRsrcFork()) {
SetHasRsrcFork(true);
SetRsrcForkLen(pThread->actualThreadEOF);
} else {
WMSG0("WARNING: ignoring second data fork\n");
}
}
if (threadID == kNuThreadIDDiskImage) {
if (!GetHasDiskImage() && !GetHasDataFork()) {
SetHasDiskImage(true);
SetDataForkLen(pThread->actualThreadEOF);
} else {
WMSG0("WARNING: ignoring second disk image / data fork\n");
}
}
if (threadID == kNuThreadIDComment) {
SetHasComment(true);
if (pThread->actualThreadEOF != 0)
SetHasNonEmptyComment(true);
}
}
SetRecordKind(recordKind);
//SetUncompressedLen(uncompressedLen);
SetCompressedLen(compressedLen);
if (format >= 0 && format < NELEM(gFormatNames))
SetFormatStr(gFormatNames[format]);
else
SetFormatStr(L"Unknown");
}
/*
* ===========================================================================
* NufxArchive
* ===========================================================================
*/
/*
* Perform one-time initialization of the NufxLib library.
*
* Returns with an error if the NufxLib version is off. Major version must
* match (since it indicates an interface change), minor version must be
* >= what we expect (in case we're relying on recent behavior changes).
*
* Returns 0 on success, nonzero on error.
*/
/*static*/ CString
NufxArchive::AppInit(void)
{
NuError nerr;
CString result("");
long major, minor, bug;
nerr = NuGetVersion(&major, &minor, &bug, NULL, NULL);
if (nerr != kNuErrNone) {
result = "Unable to get version number from NufxLib.";
goto bail;
}
if (major != kNuVersionMajor || minor < kNuVersionMinor) {
result.Format(L"Older or incompatible version of NufxLib DLL found.\r\r"
L"Wanted v%d.%d.x, found %ld.%ld.%ld.",
kNuVersionMajor, kNuVersionMinor,
major, minor, bug);
goto bail;
}
if (bug != kNuVersionBug) {
WMSG2("Different 'bug' version (built vX.X.%d, dll vX.X.%d)\n",
kNuVersionBug, bug);
}
/* set NufxLib's global error message handler */
NuSetGlobalErrorMessageHandler(NufxErrorMsgHandler);
bail:
return result;
}
/*
* Determine whether a particular kind of compression is supported by
* NufxLib.
*
* Returns "true" if supported, "false" if not.
*/
/*static*/ bool
NufxArchive::IsCompressionSupported(NuThreadFormat format)
{
NuFeature feature;
switch (format) {
case kNuThreadFormatUncompressed:
return true;
case kNuThreadFormatHuffmanSQ:
feature = kNuFeatureCompressSQ;
break;
case kNuThreadFormatLZW1:
case kNuThreadFormatLZW2:
feature = kNuFeatureCompressLZW;
break;
case kNuThreadFormatLZC12:
case kNuThreadFormatLZC16:
feature = kNuFeatureCompressLZC;
break;
case kNuThreadFormatDeflate:
feature = kNuFeatureCompressDeflate;
break;
case kNuThreadFormatBzip2:
feature = kNuFeatureCompressBzip2;
break;
default:
ASSERT(false);
return false;
}
NuError nerr;
nerr = NuTestFeature(feature);
if (nerr == kNuErrNone)
return true;
return false;
}
/*
* Display error messages... or not.
*/
NuResult
NufxArchive::NufxErrorMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage)
{
#if defined(_DEBUG_LOG)
const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage;
CStringA msg(pErrorMessage->message);
msg += "\n";
if (pErrorMessage->isDebug)
msg = "[D] " + msg;
fprintf(gLog, "%05u NufxLib %hs(%d) : %hs",
gPid, pErrorMessage->file, pErrorMessage->line, msg);
#elif defined(_DEBUG)
const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage;
CStringA msg(pErrorMessage->message);
msg += "\n";
if (pErrorMessage->isDebug)
msg = "[D] " + msg;
_CrtDbgReport(_CRT_WARN, pErrorMessage->file, pErrorMessage->line,
pErrorMessage->function, "%hs", msg);
#endif
return kNuOK;
}
/*
* Display our progress.
*
* "oldName" ends up on top, "newName" on bottom.
*/
/*static*/NuResult
NufxArchive::ProgressUpdater(NuArchive* pArchive, void* vpProgress)
{
const NuProgressData* pProgress = (const NuProgressData*) vpProgress;
NufxArchive* pThis;
MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd();
int status;
const char* oldName;
const char* newName;
int perc;
ASSERT(pProgress != nil);
ASSERT(pMainWin != nil);
ASSERT(pArchive != nil);
(void) NuGetExtraData(pArchive, (void**) &pThis);
ASSERT(pThis != nil);
oldName = newName = nil;
if (pProgress->operation == kNuOpAdd) {
oldName = pProgress->origPathname;
newName = pProgress->pathname;
if (pThis->fProgressAsRecompress)
oldName = "-";
} else if (pProgress->operation == kNuOpTest) {
oldName = pProgress->pathname;
} else if (pProgress->operation == kNuOpExtract) {
if (pThis->fProgressAsRecompress) {
oldName = pProgress->origPathname;
newName = "-";
}
}
perc = pProgress->percentComplete;
if (pProgress->state == kNuProgressDone)
perc = 100;
//WMSG3("Progress: %d%% '%hs' '%hs'\n", perc,
// oldName == nil ? "(nil)" : oldName,
// newName == nil ? "(nil)" : newName);
//status = pMainWin->SetProgressUpdate(perc, oldName, newName);
CString oldNameW(oldName);
CString newNameW(newName);
status = SET_PROGRESS_UPDATE2(perc, oldNameW, newNameW);
/* check to see if user hit the "cancel" button on the progress dialog */
if (pProgress->state == kNuProgressAborted) {
WMSG0("(looks like we're aborting)\n");
ASSERT(status == IDCANCEL);
}
if (status == IDCANCEL) {
WMSG0("Signaling NufxLib to abort\n");
return kNuAbort;
} else
return kNuOK;
}
/*
* Finish instantiating a NufxArchive object by opening an existing file.
*
* Returns an error string on failure, or nil on success.
*/
GenericArchive::OpenResult
NufxArchive::Open(const WCHAR* filename, bool readOnly, CString* pErrMsg)
{
NuError nerr;
CString errMsg;
ASSERT(fpArchive == nil);
CStringA filenameA(filename);
if (!readOnly) {
CString tmpname = GenDerivedTempName(filename);
WMSG2("Opening file '%ls' rw (tmp='%ls')\n", filename, (LPCWSTR) tmpname);
fIsReadOnly = false;
CStringA tmpnameA(tmpname);
nerr = NuOpenRW(filenameA, tmpnameA, 0, &fpArchive);
}
if (nerr == kNuErrFileAccessDenied || nerr == EACCES) {
WMSG0("Read-write failed with access denied, trying read-only\n");
readOnly = true;
}
if (readOnly) {
WMSG1("Opening file '%ls' ro\n", (LPCWSTR) filename);
fIsReadOnly = true;
nerr = NuOpenRO(filenameA, &fpArchive);
}
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to open '%ls': %hs", filename, NuStrError(nerr));
goto bail;
} else {
//WMSG0("FILE OPEN SUCCESS\n");
}
nerr = SetCallbacks();
if (nerr != kNuErrNone) {
errMsg = L"Callback init failed";
goto bail;
}
nerr = LoadContents();
if (nerr != kNuErrNone) {
errMsg = L"Failed reading archive contents: ";
errMsg += NuStrError(nerr);
}
SetPathName(filename);
bail:
*pErrMsg = errMsg;
if (!errMsg.IsEmpty())
return kResultFailure;
else
return kResultSuccess;
}
/*
* Finish instantiating a NufxArchive object by creating a new archive.
*
* Returns an error string on failure, or "" on success.
*/
CString
NufxArchive::New(const WCHAR* filename, const void* options)
{
NuError nerr;
CString retmsg;
ASSERT(fpArchive == nil);
ASSERT(options == nil);
CString tmpname = GenDerivedTempName(filename);
WMSG2("Creating file '%ls' (tmp='%ls')\n", filename, (LPCWSTR) tmpname);
fIsReadOnly = false;
CStringA filenameA(filename);
CStringA tmpnameA(tmpname);
nerr = NuOpenRW(filenameA, tmpnameA, kNuOpenCreat | kNuOpenExcl, &fpArchive);
if (nerr != kNuErrNone) {
retmsg.Format(L"Unable to open '%ls': %hs", filename, NuStrError(nerr));
goto bail;
} else {
WMSG0("NEW FILE SUCCESS\n");
}
nerr = SetCallbacks();
if (nerr != kNuErrNone) {
retmsg = L"Callback init failed";
goto bail;
}
SetPathName(filename);
bail:
return retmsg;
}
/*
* Set some standard callbacks and feature flags.
*/
NuError
NufxArchive::SetCallbacks(void)
{
NuError nerr;
nerr = NuSetExtraData(fpArchive, this);
if (nerr != kNuErrNone)
goto bail;
// nerr = NuSetSelectionFilter(fpArchive, SelectionFilter);
// if (nerr != kNuErrNone)
// goto bail;
// nerr = NuSetOutputPathnameFilter(fpArchive, OutputPathnameFilter);
// if (nerr != kNuErrNone)
// goto bail;
NuSetProgressUpdater(fpArchive, ProgressUpdater);
// nerr = NuSetErrorHandler(fpArchive, ErrorHandler);
// if (nerr != kNuErrNone)
// goto bail;
NuSetErrorMessageHandler(fpArchive, NufxErrorMsgHandler);
/* let NufxLib worry about buggy records without data threads */
nerr = NuSetValue(fpArchive, kNuValueMaskDataless, kNuValueTrue);
if (nerr != kNuErrNone)
goto bail;
/* set any values based on Preferences values */
PreferencesChanged();
bail:
return nerr;
}
/*
* User has updated their preferences. Take note.
*
* (This is also called the first time through.)
*/
void
NufxArchive::PreferencesChanged(void)
{
NuError nerr;
const Preferences* pPreferences = GET_PREFERENCES();
bool val;
val = pPreferences->GetPrefBool(kPrMimicShrinkIt);
nerr = NuSetValue(fpArchive, kNuValueMimicSHK, val);
if (nerr != kNuErrNone) {
WMSG2("NuSetValue(kNuValueMimicSHK, %d) failed, err=%d\n", val, nerr);
ASSERT(false);
} else {
WMSG1("Set MimicShrinkIt to %d\n", val);
}
val = pPreferences->GetPrefBool(kPrReduceSHKErrorChecks);
NuSetValue(fpArchive, kNuValueIgnoreLZW2Len, val);
NuSetValue(fpArchive, kNuValueIgnoreCRC, val);
val = pPreferences->GetPrefBool(kPrBadMacSHK);
NuSetValue(fpArchive, kNuValueHandleBadMac, val);
}
/*
* Report on what NuFX is capable of.
*/
long
NufxArchive::GetCapability(Capability cap)
{
switch (cap) {
case kCapCanTest:
return true;
break;
case kCapCanRenameFullPath:
return true;
break;
case kCapCanRecompress:
return true;
break;
case kCapCanEditComment:
return true;
break;
case kCapCanAddDisk:
return true;
break;
case kCapCanConvEOLOnAdd:
return false;
break;
case kCapCanCreateSubdir:
return false;
break;
case kCapCanRenameVolume:
return false;
break;
default:
ASSERT(false);
return -1;
break;
}
}
/*
* Load the contents of an archive into the GenericEntry/NufxEntry list.
*
* We will need to set an error handler if we want to be able to do things
* like "I found a bad CRC, did you want me to keep trying anyway?".
*/
NuError
NufxArchive::LoadContents(void)
{
long counter = 0;
NuError result;
WMSG0("NufxArchive LoadContents\n");
ASSERT(fpArchive != nil);
{
MainWindow* pMain = GET_MAIN_WINDOW();
ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog;
pWaitDlg->Create(IDD_LOADING, pMain);
pWaitDlg->CenterWindow();
pMain->PeekAndPump(); // redraw
CWaitCursor waitc;
result = NuContents(fpArchive, ContentFunc);
SET_PROGRESS_COUNTER(-1);
pWaitDlg->DestroyWindow();
//pMain->PeekAndPump(); // redraw
}
return result;
}
/*
* Reload the contents.
*/
CString
NufxArchive::Reload(void)
{
NuError nerr;
CString errMsg;
fReloadFlag = true; // tell everybody that cached data is invalid
DeleteEntries(); // a GenericArchive operation
nerr = LoadContents();
if (nerr != kNuErrNone) {
errMsg.Format(L"ERROR: unable to reload archive contents: %hs.",
NuStrError(nerr));
DeleteEntries();
fIsReadOnly = true;
}
return errMsg;
}
/*
* Reload the contents of the archive, showing an error message if the
* reload fails.
*/
NuError
NufxArchive::InternalReload(CWnd* pMsgWnd)
{
CString errMsg;
errMsg = Reload();
if (!errMsg.IsEmpty()) {
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
return kNuErrGeneric;
}
return kNuErrNone;
}
/*
* Static callback function. Used for scanning the contents of an archive.
*/
NuResult
NufxArchive::ContentFunc(NuArchive* pArchive, void* vpRecord)
{
const NuRecord* pRecord = (const NuRecord*) vpRecord;
NufxArchive* pThis;
NufxEntry* pNewEntry;
ASSERT(pArchive != nil);
ASSERT(vpRecord != nil);
NuGetExtraData(pArchive, (void**) &pThis);
pNewEntry = new NufxEntry(pArchive);
CStringW filenameW(pRecord->filename);
pNewEntry->SetPathName(filenameW);
pNewEntry->SetFssep(NuGetSepFromSysInfo(pRecord->recFileSysInfo));
pNewEntry->SetFileType(pRecord->recFileType);
pNewEntry->SetAuxType(pRecord->recExtraType);
pNewEntry->SetAccess(pRecord->recAccess);
pNewEntry->SetCreateWhen(DateTimeToSeconds(&pRecord->recCreateWhen));
pNewEntry->SetModWhen(DateTimeToSeconds(&pRecord->recModWhen));
/*
* Our files are always ProDOS format. This is especially important
* when cutting & pasting, so that the DOS high ASCII converter gets
* invoked at appropriate times.
*/
pNewEntry->SetSourceFS(DiskImg::kFormatProDOS);
pNewEntry->AnalyzeRecord(pRecord);
pNewEntry->SetRecordIdx(pRecord->recordIdx);
pThis->AddEntry(pNewEntry);
if ((pThis->GetNumEntries() % 10) == 0)
SET_PROGRESS_COUNTER(pThis->GetNumEntries());
return kNuOK;
}
/*
* Convert a NuDateTime structure to a time_t.
*/
/*static*/ time_t
NufxArchive::DateTimeToSeconds(const NuDateTime* pDateTime)
{
if (pDateTime->second == 0 &&
pDateTime->minute == 0 &&
pDateTime->hour == 0 &&
pDateTime->year == 0 &&
pDateTime->day == 0 &&
pDateTime->month == 0 &&
pDateTime->extra == 0 &&
pDateTime->weekDay == 0)
{
return kDateNone;
}
int year;
if (pDateTime->year < 40)
year = pDateTime->year + 2000;
else
year = pDateTime->year + 1900;
if (year < 1969) {
/*
* Years like 1963 are valid on an Apple II but cannot be represented
* as a time_t, which starts in 1970. (Depending on GMT offsets,
* it actually starts a few hours earlier at the end of 1969.)
*
* I'm catching this here because of an assert in the CTime
* constructor. The constructor seems to do the right thing, and the
* assert won't be present in the shipping version, but it's annoying
* during debugging.
*/
//WMSG1(" Ignoring funky year %ld\n", year);
return kDateInvalid;
}
if (pDateTime->month > 11)
return kDateInvalid;
CTime modTime(year,
pDateTime->month+1,
pDateTime->day+1,
pDateTime->hour,
pDateTime->minute,
pDateTime->second);
return (time_t) modTime.GetTime();
}
/*
* Callback from a DataSource that is done with a buffer. Use for memory
* allocated with new[].
*/
/*static*/ NuResult
NufxArchive::ArrayDeleteHandler(NuArchive* pArchive, void* ptr)
{
delete[] ptr;
return kNuOK;
}
/*
* ===========================================================================
* NufxArchive -- add files (or disks)
* ===========================================================================
*/
/*
* Process a bulk "add" request.
*
* This calls into the GenericArchive "AddFile" function, which does
* Win32-specific processing. That function calls our DoAddFile function,
* which does the NuFX stuff.
*
* Returns "true" on success, "false" on failure.
*/
bool
NufxArchive::BulkAdd(ActionProgressDialog* pActionProgress,
const AddFilesDialog* pAddOpts)
{
NuError nerr;
CString errMsg;
WCHAR curDir[MAX_PATH] = L"";
bool retVal = false;
WMSG2("Opts: '%ls' typePres=%d\n", (LPCWSTR) pAddOpts->fStoragePrefix,
pAddOpts->fTypePreservation);
WMSG3(" sub=%d strip=%d ovwr=%d\n",
pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames,
pAddOpts->fOverwriteExisting);
AddPrep(pActionProgress, pAddOpts);
pActionProgress->SetArcName(L"(Scanning files to be added...)");
pActionProgress->SetFileName(L"");
/* initialize count */
fNumAdded = 0;
const WCHAR* buf = pAddOpts->GetFileNames();
WMSG2("Selected path = '%ls' (offset=%d)\n", buf,
pAddOpts->GetFileNameOffset());
if (GetCurrentDirectory(NELEM(curDir), curDir) == 0) {
errMsg = L"Unable to get current directory.\n";
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
if (SetCurrentDirectory(buf) == false) {
errMsg.Format(L"Unable to set current directory to '%ls'.\n", buf);
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
buf += pAddOpts->GetFileNameOffset();
while (*buf != '\0') {
WMSG1(" file '%ls'\n", buf);
/* this just provides the list of files to NufxLib */
nerr = AddFile(pAddOpts, buf, &errMsg);
if (nerr != kNuErrNone) {
if (errMsg.IsEmpty())
errMsg.Format(L"Failed while adding file '%ls': %hs.",
buf, NuStrError(nerr));
if (nerr != kNuErrAborted) {
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
}
goto bail;
}
buf += wcslen(buf)+1;
}
/* actually do the work */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
if (nerr != kNuErrAborted) {
errMsg.Format(L"Unable to add files: %hs.", NuStrError(nerr));
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
}
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
goto bail;
}
if (!fNumAdded) {
errMsg = L"No files added.\n";
fpMsgWnd->MessageBox(errMsg, L"CiderPress", MB_OK | MB_ICONWARNING);
} else {
if (InternalReload(fpMsgWnd) == kNuErrNone)
retVal = true;
else
errMsg = L"Reload failed.";
}
bail:
NuAbort(fpArchive); // abort anything that didn't get flushed
if (SetCurrentDirectory(curDir) == false) {
errMsg.Format(L"Unable to reset current directory to '%ls'.\n", buf);
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
// bummer, but don't signal failure
}
AddFinish();
return retVal;
}
/*
* Add a single disk to the archive.
*/
bool
NufxArchive::AddDisk(ActionProgressDialog* pActionProgress,
const AddFilesDialog* pAddOpts)
{
NuError nerr;
CString errMsg;
const int kBlockSize = 512;
PathProposal pathProp;
PathName pathName;
DiskImg* pDiskImg;
NuDataSource* pSource = nil;
unsigned char* diskData = nil;
WCHAR curDir[MAX_PATH] = L"\\";
bool retVal = false;
CStringA storageNameA, origNameA;
WMSG2("AddDisk: '%ls' %d\n", pAddOpts->GetFileNames(),
pAddOpts->GetFileNameOffset());
WMSG2("Opts: '%ls' type=%d\n", (LPCWSTR) pAddOpts->fStoragePrefix,
pAddOpts->fTypePreservation);
WMSG3(" sub=%d strip=%d ovwr=%d\n",
pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames,
pAddOpts->fOverwriteExisting);
pDiskImg = pAddOpts->fpDiskImg;
ASSERT(pDiskImg != nil);
/* allocate storage for the entire disk */
diskData = new BYTE[pDiskImg->GetNumBlocks() * kBlockSize];
if (diskData == nil) {
errMsg.Format(L"Unable to allocate %d bytes.",
pDiskImg->GetNumBlocks() * kBlockSize);
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
/* prepare to add */
AddPrep(pActionProgress, pAddOpts);
const WCHAR* buf;
buf = pAddOpts->GetFileNames();
WMSG2("Selected path = '%ls' (offset=%d)\n", buf,
pAddOpts->GetFileNameOffset());
if (GetCurrentDirectory(NELEM(curDir), curDir) == 0) {
errMsg = L"Unable to get current directory.\n";
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
if (SetCurrentDirectory(buf) == false) {
errMsg.Format(L"Unable to set current directory to '%ls'.\n", buf);
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
buf += pAddOpts->GetFileNameOffset();
WMSG1(" file '%ls'\n", buf);
/* strip off preservation stuff, and ignore it */
pathProp.Init(buf);
pathProp.fStripDiskImageSuffix = true;
pathProp.LocalToArchive(pAddOpts);
/* fill in the necessary file details */
NuFileDetails details;
memset(&details, 0, sizeof(details));
details.threadID = kNuThreadIDDiskImage;
details.storageType = kBlockSize;
details.access = kNuAccessUnlocked;
details.extraType = pAddOpts->fpDiskImg->GetNumBlocks();
origNameA = buf;
storageNameA = pathProp.fStoredPathName;
details.origName = origNameA;
details.storageName = storageNameA;
details.fileSysID = kNuFileSysUnknown;
details.fileSysInfo = PathProposal::kDefaultStoredFssep;
time_t now, then;
pathName = buf;
now = time(nil);
then = pathName.GetModWhen();
UNIXTimeToDateTime(&now, &details.archiveWhen);
UNIXTimeToDateTime(&then, &details.modWhen);
UNIXTimeToDateTime(&then, &details.createWhen);
/* set up the progress updater */
pActionProgress->SetArcName(pathProp.fStoredPathName);
pActionProgress->SetFileName(buf);
/* read the disk now that we have progress update titles in place */
int block, numBadBlocks;
unsigned char* bufPtr;
numBadBlocks = 0;
for (block = 0, bufPtr = diskData; block < pDiskImg->GetNumBlocks(); block++)
{
DIError dierr;
dierr = pDiskImg->ReadBlock(block, bufPtr);
if (dierr != kDIErrNone)
numBadBlocks++;
bufPtr += kBlockSize;
}
if (numBadBlocks > 0) {
CString appName, msg;
appName.LoadString(IDS_MB_APP_NAME);
msg.Format(L"Skipped %ld unreadable block%ls.", numBadBlocks,
numBadBlocks == 1 ? L"" : L"s");
fpMsgWnd->MessageBox(msg, appName, MB_OK | MB_ICONWARNING);
// keep going -- just a warning
}
/* create a data source for the disk */
nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0,
diskData, 0, pAddOpts->fpDiskImg->GetNumBlocks() * kBlockSize,
nil, &pSource);
if (nerr != kNuErrNone) {
errMsg = "Unable to create NufxLib data source.";
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
/* add the record; name conflicts cause the error handler to fire */
NuRecordIdx recordIdx;
nerr = NuAddRecord(fpArchive, &details, &recordIdx);
if (nerr != kNuErrNone) {
if (nerr != kNuErrAborted) {
errMsg.Format(L"Failed adding record: %hs.", NuStrError(nerr));
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
}
goto bail;
}
/* do the compression */
nerr = NuAddThread(fpArchive, recordIdx, kNuThreadIDDiskImage,
pSource, nil);
if (nerr != kNuErrNone) {
errMsg.Format(L"Failed adding thread: %hs.", NuStrError(nerr));
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
pSource = nil; /* NufxLib owns it now */
/* actually do the work */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
if (nerr != kNuErrAborted) {
errMsg.Format(L"Unable to add disk: %hs.", NuStrError(nerr));
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
}
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
goto bail;
}
if (InternalReload(fpMsgWnd) == kNuErrNone)
retVal = true;
bail:
delete[] diskData;
NuAbort(fpArchive); // abort anything that didn't get flushed
NuFreeDataSource(pSource);
if (SetCurrentDirectory(curDir) == false) {
errMsg.Format(L"Unable to reset current directory to '%hs'.\n", buf);
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
// bummer
}
AddFinish();
return retVal;
}
/*
* Do the archive-dependent part of the file add, including things like
* adding comments. This is eventually called by AddFile() during bulk
* adds.
*/
NuError
NufxArchive::DoAddFile(const AddFilesDialog* pAddOpts,
FileDetails* pDetails)
{
NuError err;
NuRecordIdx recordIdx = 0;
NuFileDetails nuFileDetails;
retry:
nuFileDetails = *pDetails; // stuff class contents into struct
CStringA origNameA(pDetails->origName);
err = NuAddFile(fpArchive, origNameA /*pathname*/,
&nuFileDetails, false, &recordIdx);
if (err == kNuErrNone) {
fNumAdded++;
} else if (err == kNuErrSkipped) {
/* "maybe overwrite" UI causes this if user declines */
// fall through with the error
WMSG1("DoAddFile: skipped '%ls'\n", (LPCWSTR) pDetails->origName);
} else if (err == kNuErrRecordExists) {
AddClashDialog dlg;
dlg.fWindowsName = pDetails->origName;
dlg.fStorageName = pDetails->storageName;
if (dlg.DoModal() != IDOK) {
err = kNuErrAborted;
goto bail_quiet;
}
if (dlg.fDoRename) {
WMSG1("add clash: rename to '%ls'\n", (LPCWSTR) dlg.fNewName);
pDetails->storageName = dlg.fNewName;
goto retry;
} else {
WMSG0("add clash: skip");
err = kNuErrSkipped;
// fall through with error
}
}
//if (err != kNuErrNone)
// goto bail;
//bail:
if (err != kNuErrNone && err != kNuErrAborted && err != kNuErrSkipped) {
CString msg;
msg.Format(L"Unable to add file '%ls': %hs.",
(LPCWSTR) pDetails->origName, NuStrError(err));
ShowFailureMsg(fpMsgWnd, msg, IDS_FAILED);
}
bail_quiet:
return err;
}
/*
* Prepare to add files.
*/
void
NufxArchive::AddPrep(CWnd* pMsgWnd, const AddFilesDialog* pAddOpts)
{
NuError nerr;
const Preferences* pPreferences = GET_PREFERENCES();
int defaultCompression;
ASSERT(fpArchive != nil);
fpMsgWnd = pMsgWnd;
ASSERT(fpMsgWnd != nil);
fpAddOpts = pAddOpts;
ASSERT(fpAddOpts != nil);
//fBulkProgress = true;
defaultCompression = pPreferences->GetPrefLong(kPrCompressionType);
nerr = NuSetValue(fpArchive, kNuValueDataCompression,
defaultCompression + kNuCompressNone);
if (nerr != kNuErrNone) {
WMSG1("GLITCH: unable to set compression type to %d\n",
defaultCompression);
/* keep going */
}
if (pAddOpts->fOverwriteExisting)
NuSetValue(fpArchive, kNuValueHandleExisting, kNuAlwaysOverwrite);
else
NuSetValue(fpArchive, kNuValueHandleExisting, kNuMaybeOverwrite);
NuSetErrorHandler(fpArchive, BulkAddErrorHandler);
NuSetExtraData(fpArchive, this);
}
/*
* Reset some things after we finish adding files. We don't necessarily
* want these to stay in effect for other operations, e.g. extracting
* (though that is currently handled within CiderPress).
*/
void
NufxArchive::AddFinish(void)
{
NuSetErrorHandler(fpArchive, nil);
NuSetValue(fpArchive, kNuValueHandleExisting, kNuMaybeOverwrite);
fpMsgWnd = nil;
fpAddOpts = nil;
//fBulkProgress = false;
}
/*
* Error handler callback for "bulk" adds.
*/
/*static*/ NuResult
NufxArchive::BulkAddErrorHandler(NuArchive* pArchive, void* vErrorStatus)
{
const NuErrorStatus* pErrorStatus = (const NuErrorStatus*)vErrorStatus;
NufxArchive* pThis;
NuResult result;
ASSERT(pArchive != nil);
(void) NuGetExtraData(pArchive, (void**) &pThis);
ASSERT(pThis != nil);
ASSERT(pArchive == pThis->fpArchive);
/* default action is to abort the current operation */
result = kNuAbort;
/*
* When adding files, the NuAddFile and NuAddRecord calls can return
* immediate, specific results for a single add. The only reasons for
* calling here are to decide if an existing record should be replaced
* or not (without even an option to rename), or to decide what to do
* when the NuFlush call runs into a problem while adding a file.
*/
if (pErrorStatus->operation != kNuOpAdd) {
ASSERT(false);
return kNuAbort;
}
if (pErrorStatus->err == kNuErrRecordExists) {
/* if they want to update or freshen, don't hassle them */
//if (NState_GetModFreshen(pState) || NState_GetModUpdate(pState))
if (pThis->fpAddOpts->fOverwriteExisting) {
ASSERT(false); // should be handled by AddPrep()/NufxLib
result = kNuOverwrite;
} else
result = pThis->HandleReplaceExisting(pErrorStatus);
} else if (pErrorStatus->err == kNuErrFileNotFound) {
/* file was specified with NuAdd but removed during NuFlush */
result = pThis->HandleAddNotFound(pErrorStatus);
}
return result;
}
/*
* Decide whether or not to replace an existing file (during extract)
* or record (during add).
*/
NuResult
NufxArchive::HandleReplaceExisting(const NuErrorStatus* pErrorStatus)
{
NuResult result = kNuOK;
ASSERT(pErrorStatus != nil);
ASSERT(pErrorStatus->pathname != nil);
ASSERT(pErrorStatus->canOverwrite);
ASSERT(pErrorStatus->canSkip);
ASSERT(pErrorStatus->canAbort);
ASSERT(!pErrorStatus->canRename);
/* no firm policy, ask the user */
ConfirmOverwriteDialog confOvwr;
PathName path(pErrorStatus->pathname);
confOvwr.fExistingFile = pErrorStatus->pRecord->filename;
confOvwr.fExistingFileModWhen =
DateTimeToSeconds(&pErrorStatus->pRecord->recModWhen);
if (pErrorStatus->origPathname != nil) {
confOvwr.fNewFileSource = pErrorStatus->origPathname;
PathName checkPath(confOvwr.fNewFileSource);
confOvwr.fNewFileModWhen = checkPath.GetModWhen();
} else {
confOvwr.fNewFileSource = "???";
confOvwr.fNewFileModWhen = kDateNone;
}
confOvwr.fAllowRename = false;
if (confOvwr.DoModal() == IDCANCEL) {
result = kNuAbort;
goto bail;
}
if (confOvwr.fResultRename) {
ASSERT(false);
result = kNuAbort;
goto bail;
}
if (confOvwr.fResultApplyToAll) {
if (confOvwr.fResultOverwrite) {
(void) NuSetValue(fpArchive, kNuValueHandleExisting,
kNuAlwaysOverwrite);
} else {
(void) NuSetValue(fpArchive, kNuValueHandleExisting,
kNuNeverOverwrite);
}
}
if (confOvwr.fResultOverwrite)
result = kNuOverwrite;
else
result = kNuSkip;
bail:
return result;
}
/*
* A file that used to be there isn't anymore.
*
* This should be exceedingly rare.
*/
NuResult
NufxArchive::HandleAddNotFound(const NuErrorStatus* pErrorStatus)
{
CString errMsg;
errMsg.Format(L"Failed while adding '%hs': file no longer exists.",
pErrorStatus->pathname);
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
return kNuAbort;
}
/*
* ===========================================================================
* NufxArchive -- test files
* ===========================================================================
*/
/*
* Test the records represented in the selection set.
*/
bool
NufxArchive::TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
{
NuError nerr;
NufxEntry* pEntry;
CString errMsg;
bool retVal = false;
ASSERT(fpArchive != nil);
WMSG1("Testing %d entries\n", pSelSet->GetNumEntries());
SelectionEntry* pSelEntry = pSelSet->IterNext();
while (pSelEntry != nil) {
pEntry = (NufxEntry*) pSelEntry->GetEntry();
WMSG2(" Testing %ld '%ls'\n", pEntry->GetRecordIdx(),
pEntry->GetPathName());
nerr = NuTestRecord(fpArchive, pEntry->GetRecordIdx());
if (nerr != kNuErrNone) {
if (nerr == kNuErrAborted) {
CString title;
title.LoadString(IDS_MB_APP_NAME);
errMsg = "Cancelled.";
pMsgWnd->MessageBox(errMsg, title, MB_OK);
} else {
errMsg.Format(L"Failed while testing '%ls': %hs.",
pEntry->GetPathName(), NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
}
goto bail;
}
pSelEntry = pSelSet->IterNext();
}
/* show success message */
errMsg.Format(L"Tested %d file%ls, no errors found.",
pSelSet->GetNumEntries(),
pSelSet->GetNumEntries() == 1 ? L"" : L"s");
pMsgWnd->MessageBox(errMsg);
retVal = true;
bail:
return retVal;
}
/*
* ===========================================================================
* NufxArchive -- delete files
* ===========================================================================
*/
/*
* Delete the records represented in the selection set.
*/
bool
NufxArchive::DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
{
NuError nerr;
NufxEntry* pEntry;
CString errMsg;
bool retVal = false;
ASSERT(fpArchive != nil);
WMSG1("Deleting %d entries\n", pSelSet->GetNumEntries());
/* mark entries for deletion */
SelectionEntry* pSelEntry = pSelSet->IterNext();
while (pSelEntry != nil) {
pEntry = (NufxEntry*) pSelEntry->GetEntry();
WMSG2(" Deleting %ld '%ls'\n", pEntry->GetRecordIdx(),
pEntry->GetPathName());
nerr = NuDeleteRecord(fpArchive, pEntry->GetRecordIdx());
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to delete record %d: %hs.",
pEntry->GetRecordIdx(), NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
pSelEntry = pSelSet->IterNext();
}
/* actually do the delete */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to delete all files: %hs.", NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
}
if (InternalReload(fpMsgWnd) == kNuErrNone)
retVal = true;
bail:
return retVal;
}
/*
* ===========================================================================
* NufxArchive -- rename files
* ===========================================================================
*/
/*
* Rename the records represented in the selection set.
*/
bool
NufxArchive::RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
{
CString errMsg;
NuError nerr;
bool retVal = false;
ASSERT(fpArchive != nil);
WMSG1("Renaming %d entries\n", pSelSet->GetNumEntries());
/*
* 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);
WMSG1("Rename, fullpath=%d\n", renameFullPath);
/*
* 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) {
NufxEntry* pEntry = (NufxEntry*) pSelEntry->GetEntry();
WMSG1(" Renaming '%ls'\n", pEntry->GetPathName());
RenameEntryDialog renameDlg(pMsgWnd);
renameDlg.SetCanRenameFullPath(renameFullPath);
renameDlg.SetCanChangeFssep(true);
renameDlg.fOldName = pEntry->GetPathName();
renameDlg.fFssep = pEntry->GetFssep();
renameDlg.fpArchive = this;
renameDlg.fpEntry = pEntry;
int result = renameDlg.DoModal();
if (result == IDOK) {
if (renameDlg.fFssep == '\0')
renameDlg.fFssep = kNufxNoFssep;
CStringA newNameA(renameDlg.fNewName);
nerr = NuRename(fpArchive, pEntry->GetRecordIdx(),
newNameA, renameDlg.fFssep);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to rename '%ls': %hs.", pEntry->GetPathName(),
NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
break;
}
WMSG2("Rename of '%ls' to '%ls' succeeded\n",
pEntry->GetDisplayName(), (LPCWSTR) 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 '%ls'\n", pEntry->GetDisplayName());
}
pSelEntry = pSelSet->IterNext();
}
/* flush pending rename calls */
{
CWaitCursor waitc;
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to rename all files: %hs.",
NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
}
}
/* reload GenericArchive from NufxLib */
if (InternalReload(fpMsgWnd) == kNuErrNone)
retVal = true;
return retVal;
}
/*
* Verify that the a name is suitable. Called by RenameEntryDialog.
*
* Tests for context-specific syntax and checks for duplicates.
*
* Returns an empty string on success, or an error message on failure.
*/
CString
NufxArchive::TestPathName(const GenericEntry* pGenericEntry,
const CString& basePath, const CString& newName, char newFssep) const
{
CString errMsg;
ASSERT(pGenericEntry != nil);
ASSERT(basePath.IsEmpty());
/* can't start or end with fssep */
if (newName.Left(1) == newFssep || newName.Right(1) == newFssep) {
errMsg.Format(L"Names in NuFX archives may not start or end with a "
L"path separator character (%c).",
newFssep);
goto bail;
}
/* if it's a disk image, don't allow complex paths */
if (pGenericEntry->GetRecordKind() == GenericEntry::kRecordKindDisk) {
if (newName.Find(newFssep) != -1) {
errMsg.Format(L"Disk image names may not contain a path separator "
L"character (%c).",
newFssep);
goto bail;
}
}
/*
* Test for case-sensitive name collisions. Each individual path
* component must be compared
*/
GenericEntry* pEntry;
pEntry = GetEntries();
while (pEntry != nil) {
if (pEntry != pGenericEntry &&
ComparePaths(pEntry->GetPathName(), pEntry->GetFssep(),
newName, newFssep) == 0)
{
errMsg = L"An entry with that name already exists.";
}
pEntry = pEntry->GetNext();
}
bail:
return errMsg;
}
/*
* ===========================================================================
* NufxArchive -- recompress
* ===========================================================================
*/
/*
* Recompress the files in the selection set.
*
* We have to uncompress the files into memory and then recompress them.
* We don't want to flush after every file (too slow), but we can't wait
* until they're expanded (unbounded memory requirements). So we have
* to keep expanding until we reach a certain limit, then call flush to
* push the changes out.
*
* Since we're essentially making the changes in place (it's actually
* all getting routed through the temp file), we need to delete the thread
* and re-add it. This isn't quite as thorough as "launder", which
* actually reconstructs the entire record.
*/
bool
NufxArchive::RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet,
const RecompressOptionsDialog* pRecompOpts)
{
const int kMaxSizeInMemory = 2 * 1024 * 1024; // 2MB
CString errMsg;
NuError nerr;
bool retVal = false;
/* set the compression type */
nerr = NuSetValue(fpArchive, kNuValueDataCompression,
pRecompOpts->fCompressionType + kNuCompressNone);
if (nerr != kNuErrNone) {
WMSG1("GLITCH: unable to set compression type to %d\n",
pRecompOpts->fCompressionType);
/* keep going */
}
fProgressAsRecompress = true;
/*
* Loop over all items in the selection set. Because the selection
* set has one entry for each interesting thread, we don't need to
* pry the NuRecord open and play with it.
*
* We should only be here for data forks, resource forks, and disk
* images. Comments and filenames are not compressed, and so cannot
* be recompressed.
*/
SelectionEntry* pSelEntry = pSelSet->IterNext();
long sizeInMemory = 0;
bool result = true;
NufxEntry* pEntry = nil;
for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) {
pEntry = (NufxEntry*) pSelEntry->GetEntry();
/*
* Compress each thread in turn.
*/
if (pEntry->GetHasDataFork()) {
result = RecompressThread(pEntry, GenericEntry::kDataThread,
pRecompOpts, &sizeInMemory, &errMsg);
if (!result)
break;
}
if (pEntry->GetHasRsrcFork()) {
result = RecompressThread(pEntry, GenericEntry::kRsrcThread,
pRecompOpts, &sizeInMemory, &errMsg);
if (!result)
break;
}
if (pEntry->GetHasDiskImage()) {
result = RecompressThread(pEntry, GenericEntry::kDiskImageThread,
pRecompOpts, &sizeInMemory, &errMsg);
if (!result)
break;
}
/* don't do anything with comments */
/* if we're sitting on too much, push it out */
if (sizeInMemory > kMaxSizeInMemory) {
/* flush anything pending */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
if (nerr != kNuErrAborted) {
errMsg.Format(L"Unable to recompress all files: %hs.",
NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
} else {
WMSG0("Cancelled out of sub-flush/compress\n");
}
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
goto bail;
}
sizeInMemory = 0;
}
}
/* handle errors that threw us out of the while loop */
if (!result) {
ASSERT(pEntry != nil);
CString dispStr;
dispStr.Format(L"Failed while recompressing '%ls': %ls.",
pEntry->GetDisplayName(), (LPCWSTR) errMsg);
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
/* flush anything pending */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
if (nerr != kNuErrAborted) {
errMsg.Format(L"Unable to recompress all files: %hs.",
NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
} else {
WMSG0("Cancelled out of flush/compress\n");
}
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
} else {
retVal = true;
}
bail:
/* abort anything that didn't get flushed */
NuAbort(fpArchive);
/* reload to pick up changes */
(void) InternalReload(pMsgWnd);
fProgressAsRecompress = false;
return retVal;
}
/*
* Recompress one thread.
*
* Returns "true" if things went okay, "false" on a fatal failure.
*/
bool
NufxArchive::RecompressThread(NufxEntry* pEntry, int threadKind,
const RecompressOptionsDialog* pRecompOpts, long* pSizeInMemory,
CString* pErrMsg)
{
NuThread thread;
NuThreadID threadID;
NuError nerr;
NuDataSource* pSource = nil;
CString subErrMsg;
bool retVal = false;
char* buf = nil;
long len = 0;
WMSG2(" Recompressing %ld '%ls'\n", pEntry->GetRecordIdx(),
pEntry->GetDisplayName());
/* get a copy of the thread header */
pEntry->FindThreadInfo(threadKind, &thread, pErrMsg);
if (!pErrMsg->IsEmpty()) {
pErrMsg->Format(L"Unable to locate thread for %ls (type %d)",
pEntry->GetDisplayName(), threadKind);
goto bail;
}
threadID = NuGetThreadID(&thread);
/* if it's already in the target format, skip it */
if (thread.thThreadFormat == pRecompOpts->fCompressionType) {
WMSG2("Skipping (fmt=%d) '%ls'\n",
pRecompOpts->fCompressionType, pEntry->GetDisplayName());
return true;
}
/* extract the thread */
int result;
result = pEntry->ExtractThreadToBuffer(threadKind, &buf, &len, &subErrMsg);
if (result == IDCANCEL) {
WMSG0("Cancelled during extract!\n");
ASSERT(buf == nil);
goto bail; /* abort anything that was pending */
} else if (result != IDOK) {
pErrMsg->Format(L"Failed while extracting '%ls': %ls",
pEntry->GetDisplayName(), (LPCWSTR) subErrMsg);
goto bail;
}
*pSizeInMemory += len;
/* create a data source for it */
nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed,
0, (const BYTE*)buf, 0, len, ArrayDeleteHandler,
&pSource);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"Unable to create NufxLib data source (len=%d).",
len);
goto bail;
}
buf = nil; // data source owns it now
/* delete the existing thread */
//WMSG1("+++ DELETE threadIdx=%d\n", thread.threadIdx);
nerr = NuDeleteThread(fpArchive, thread.threadIdx);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"Unable to delete thread %d: %hs",
pEntry->GetRecordIdx(), NuStrError(nerr));
goto bail;
}
/* mark the new thread for addition */
//WMSG1("+++ ADD threadID=0x%08lx\n", threadID);
nerr = NuAddThread(fpArchive, pEntry->GetRecordIdx(), threadID,
pSource, nil);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"Unable to add thread type %d: %hs",
threadID, NuStrError(nerr));
goto bail;
}
pSource = nil; // now owned by nufxlib
/* at this point, we just wait for the flush in the outer loop */
retVal = true;
bail:
NuFreeDataSource(pSource);
return retVal;
}
/*
* ===========================================================================
* NufxArchive -- transfer files to another archive
* ===========================================================================
*/
/*
* Transfer the selected files out of this archive and into another.
*
* We get one entry in the selection set per record.
*
* I think this now throws kXferCancelled whenever it's supposed to. Not
* 100% sure, but it looks good.
*/
GenericArchive::XferStatus
NufxArchive::XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet,
ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts)
{
WMSG0("NufxArchive XferSelection!\n");
XferStatus retval = kXferFailed;
unsigned char* dataBuf = nil;
unsigned char* rsrcBuf = nil;
CString errMsg, dispMsg;
pXferOpts->fTarget->XferPrepare(pXferOpts);
SelectionEntry* pSelEntry = pSelSet->IterNext();
for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) {
long dataLen=-1, rsrcLen=-1;
NufxEntry* pEntry = (NufxEntry*) pSelEntry->GetEntry();
FileDetails fileDetails;
CString errMsg;
ASSERT(dataBuf == nil);
ASSERT(rsrcBuf == nil);
/* in case we start handling CRC errors better */
if (pEntry->GetDamaged()) {
WMSG1(" XFER skipping damaged entry '%ls'\n",
pEntry->GetDisplayName());
continue;
}
WMSG1(" XFER converting '%ls'\n", pEntry->GetDisplayName());
fileDetails.storageName = pEntry->GetDisplayName();
fileDetails.fileType = pEntry->GetFileType();
fileDetails.fileSysFmt = DiskImg::kFormatUnknown;
fileDetails.fileSysInfo = PathProposal::kDefaultStoredFssep;
fileDetails.access = pEntry->GetAccess();
fileDetails.extraType = pEntry->GetAuxType();
fileDetails.storageType = kNuStorageSeedling;
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;
}
/*
* Handle all relevant threads in this record. We assume it's either
* a data/rsrc pair or a disk image.
*/
if (pEntry->GetHasDataFork()) {
/*
* Found a data thread.
*/
int result;
dataBuf = nil;
dataLen = 0;
result = pEntry->ExtractThreadToBuffer(GenericEntry::kDataThread,
(char**) &dataBuf, &dataLen, &errMsg);
if (result == IDCANCEL) {
WMSG0("Cancelled during data extract!\n");
retval = kXferCancelled;
goto bail; /* abort anything that was pending */
} else if (result != IDOK) {
dispMsg.Format(L"Failed while extracting '%ls': %ls.",
pEntry->GetDisplayName(), (LPCWSTR) errMsg);
ShowFailureMsg(pMsgWnd, dispMsg, IDS_FAILED);
goto bail;
}
ASSERT(dataBuf != nil);
ASSERT(dataLen >= 0);
} else if (pEntry->GetHasDiskImage()) {
/*
* No data thread found. Look for a disk image.
*/
int result;
dataBuf = nil;
dataLen = 0;
result = pEntry->ExtractThreadToBuffer(GenericEntry::kDiskImageThread,
(char**) &dataBuf, &dataLen, &errMsg);
if (result == IDCANCEL) {
WMSG0("Cancelled during data extract!\n");
goto bail; /* abort anything that was pending */
} else if (result != IDOK) {
dispMsg.Format(L"Failed while extracting '%ls': %ls.",
pEntry->GetDisplayName(), (LPCWSTR) errMsg);
ShowFailureMsg(pMsgWnd, dispMsg, IDS_FAILED);
goto bail;
}
ASSERT(dataBuf != nil);
ASSERT(dataLen >= 0);
}
/*
* See if there's a resource fork in here (either by itself or
* with a data fork).
*/
if (pEntry->GetHasRsrcFork()) {
int result;
rsrcBuf = nil;
rsrcLen = 0;
result = pEntry->ExtractThreadToBuffer(GenericEntry::kRsrcThread,
(char**) &rsrcBuf, &rsrcLen, &errMsg);
if (result == IDCANCEL) {
WMSG0("Cancelled during rsrc extract!\n");
goto bail; /* abort anything that was pending */
} else if (result != IDOK) {
dispMsg.Format(L"Failed while extracting '%ls': %ls.",
pEntry->GetDisplayName(), (LPCWSTR) errMsg);
ShowFailureMsg(pMsgWnd, dispMsg, IDS_FAILED);
goto bail;
}
fileDetails.storageType = kNuStorageExtended;
} else {
ASSERT(rsrcBuf == nil);
}
if (dataLen < 0 && rsrcLen < 0) {
WMSG1(" XFER: WARNING: nothing worth transferring in '%ls'\n",
pEntry->GetDisplayName());
continue;
}
errMsg = pXferOpts->fTarget->XferFile(&fileDetails, &dataBuf, dataLen,
&rsrcBuf, rsrcLen);
if (!errMsg.IsEmpty()) {
WMSG0("XferFile failed!\n");
errMsg.Format(L"Failed while transferring '%ls': %ls.",
pEntry->GetDisplayName(), (LPCWSTR) errMsg);
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
ASSERT(dataBuf == nil);
ASSERT(rsrcBuf == nil);
if (pActionProgress->SetProgress(100) == IDCANCEL) {
retval = kXferCancelled;
goto bail;
}
}
retval = kXferOK;
bail:
if (retval != kXferOK)
pXferOpts->fTarget->XferAbort(pMsgWnd);
else
pXferOpts->fTarget->XferFinish(pMsgWnd);
delete[] dataBuf;
delete[] rsrcBuf;
return retval;
}
/*
* Prepare to transfer files into a NuFX archive.
*
* We set the "allow duplicates" flag because DOS 3.3 volumes can have
* files with duplicate names.
*/
void
NufxArchive::XferPrepare(const XferFileOptions* pXferOpts)
{
WMSG0(" NufxArchive::XferPrepare\n");
(void) NuSetValue(fpArchive, kNuValueAllowDuplicates, true);
}
/*
* Transfer the data and optional resource fork of a single file into the
* NuFX archive.
*
* "dataLen" and "rsrcLen" will be -1 if the corresponding fork doesn't
* exist.
*
* Returns 0 on success, -1 on failure. On success, "*pDataBuf" and
* "*pRsrcBuf" are set to nil (ownership transfers to NufxLib).
*/
CString
NufxArchive::XferFile(FileDetails* pDetails, unsigned char** pDataBuf,
long dataLen, unsigned char** pRsrcBuf, long rsrcLen)
{
NuError nerr;
const int kFileTypeTXT = 0x04;
NuDataSource* pSource = nil;
CString errMsg;
WMSG1(" NufxArchive::XferFile '%ls'\n", (LPCWSTR) pDetails->storageName);
WMSG4(" dataBuf=0x%08lx dataLen=%ld rsrcBuf=0x%08lx rsrcLen=%ld\n",
*pDataBuf, dataLen, *pRsrcBuf, rsrcLen);
ASSERT(pDataBuf != nil);
ASSERT(pRsrcBuf != nil);
/* NuFX doesn't explicitly store directories */
if (pDetails->entryKind == FileDetails::kFileKindDirectory) {
delete[] *pDataBuf;
delete[] *pRsrcBuf;
*pDataBuf = *pRsrcBuf = nil;
goto bail;
}
ASSERT(dataLen >= 0 || rsrcLen >= 0);
ASSERT(*pDataBuf != nil || *pRsrcBuf != nil);
/* add the record; we have "allow duplicates" enabled for clashes */
NuRecordIdx recordIdx;
NuFileDetails nuFileDetails;
nuFileDetails = *pDetails;
/*
* Odd bit of trivia: NufxLib refuses to accept an fssep of '\0'. It
* really wants to have one. Which is annoying, since files coming
* from DOS or Pascal don't have one. We therefore need to supply
* one, so we provide 0xff on the theory that nobody in their right
* mind would have it in an Apple II filename.
*
* Since we don't strip Pascal and ProDOS names down when we load
* the disks, it's possible the for 0xff to occur if the disk got
* damaged. For ProDOS we don't care, since it has an fssep, but
* Pascal could be at risk. DOS and RDOS are sanitized and so should
* be okay.
*
* One issue: we don't currently allow changing the fssep when renaming
* a file. We need to fix this, or else there's no way to rename a
* file into a subdirectory once it has been pasted in this way.
*/
if (NuGetSepFromSysInfo(nuFileDetails.fileSysInfo) == 0) {
nuFileDetails.fileSysInfo =
NuSetSepInSysInfo(nuFileDetails.fileSysInfo, kNufxNoFssep);
}
nerr = NuAddRecord(fpArchive, &nuFileDetails, &recordIdx);
if (nerr != kNuErrNone) {
if (nerr != kNuErrAborted) {
errMsg.Format(L"Failed adding record: %hs", NuStrError(nerr));
//ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
}
// else the add was cancelled
goto bail;
}
if (dataLen >= 0) {
ASSERT(*pDataBuf != nil);
/* strip the high ASCII from DOS and RDOS text files */
if (pDetails->entryKind != FileDetails::kFileKindDiskImage &&
pDetails->fileType == kFileTypeTXT &&
DiskImg::UsesDOSFileStructure(pDetails->fileSysFmt))
{
WMSG1(" Stripping high ASCII from '%ls'\n",
(LPCWSTR) pDetails->storageName);
unsigned char* ucp = *pDataBuf;
long len = dataLen;
while (len--)
*ucp++ &= 0x7f;
}
/* create a data source for the data fork; might be zero len */
nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0,
*pDataBuf, 0, dataLen, ArrayDeleteHandler, &pSource);
if (nerr != kNuErrNone) {
errMsg = "Unable to create NufxLib data source.";
//ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
*pDataBuf = nil; /* owned by data source */
/* add the data fork, as a disk image if appropriate */
NuThreadID targetID;
if (pDetails->entryKind == FileDetails::kFileKindDiskImage)
targetID = kNuThreadIDDiskImage;
else
targetID = kNuThreadIDDataFork;
nerr = NuAddThread(fpArchive, recordIdx, targetID, pSource, nil);
if (nerr != kNuErrNone) {
errMsg.Format(L"Failed adding thread: %hs.", NuStrError(nerr));
//ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
pSource = nil; /* NufxLib owns it now */
}
/* add the resource fork, if one was provided */
if (rsrcLen >= 0) {
ASSERT(*pRsrcBuf != nil);
nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0,
*pRsrcBuf, 0, rsrcLen, ArrayDeleteHandler, &pSource);
if (nerr != kNuErrNone) {
errMsg = L"Unable to create NufxLib data source.";
//ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
*pRsrcBuf = nil; /* owned by data source */
/* add the data fork */
nerr = NuAddThread(fpArchive, recordIdx, kNuThreadIDRsrcFork,
pSource, nil);
if (nerr != kNuErrNone) {
errMsg.Format(L"Failed adding thread: %hs.", NuStrError(nerr));
//ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
pSource = nil; /* NufxLib owns it now */
}
bail:
NuFreeDataSource(pSource);
return errMsg;
}
/*
* Abort the transfer.
*
* Since we don't do any interim flushes, we can just call NuAbort. If that
* weren't the case, we would need to delete all records and flush.
*/
void
NufxArchive::XferAbort(CWnd* pMsgWnd)
{
NuError nerr;
CString errMsg;
WMSG0(" NufxArchive::XferAbort\n");
nerr = NuAbort(fpArchive);
if (nerr != kNuErrNone) {
errMsg.Format(L"Failed while aborting procedure: %hs.", NuStrError(nerr));
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
}
}
/*
* Flush all changes to the archive.
*/
void
NufxArchive::XferFinish(CWnd* pMsgWnd)
{
NuError nerr;
CString errMsg;
WMSG0(" NufxArchive::XferFinish\n");
/* actually do the work */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
if (nerr != kNuErrAborted) {
errMsg.Format(L"Unable to add file: %hs.", NuStrError(nerr));
ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
}
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
goto bail;
}
(void) InternalReload(fpMsgWnd);
bail:
return;
}
/*
* ===========================================================================
* NufxArchive -- add/update/delete comments
* ===========================================================================
*/
/*
* Extract a comment from the archive, converting line terminators to CRLF.
*
* Returns "true" on success, "false" on failure.
*/
bool
NufxArchive::GetComment(CWnd* pMsgWnd, const GenericEntry* pGenericEntry,
CString* pStr)
{
NufxEntry* pEntry = (NufxEntry*) pGenericEntry;
CString errMsg;
const char* kNewEOL = "\r\n";
int result;
char* buf;
long len;
ASSERT(pGenericEntry->GetHasComment());
/* use standard extract function to pull comment out */
buf = nil;
len = 0;
result = pEntry->ExtractThreadToBuffer(GenericEntry::kCommentThread,
&buf, &len, &errMsg);
if (result != IDOK) {
WMSG1("Failed getting comment: %hs\n", buf);
ASSERT(buf == nil);
return false;
}
/* convert EOL and add '\0' */
CString convStr;
const char* ccp;
ccp = buf;
while (len-- && *ccp != '\0') {
if (len > 1 && *ccp == '\r' && *(ccp+1) == '\n') {
ccp++;
len--;
convStr += kNewEOL;
} else if (*ccp == '\r' || *ccp == '\n') {
convStr += kNewEOL;
} else {
convStr += *ccp;
}
ccp++;
}
*pStr = convStr;
delete[] buf;
return true;
}
/*
* Set the comment. This requires either adding a new comment or updating
* an existing one. The latter is constrained by the maximum size of the
* comment buffer.
*
* We want to update in place whenever possible because it's faster (don't
* have to rewrite the entire archive), but that really only holds for new
* archives or if we foolishly set the kNuValueModifyOrig flag.
*
* Cleanest approach is to delete the existing thread and add a new one.
* If somebody complains we can try to be smarter about it.
*
* Returns "true" on success, "false" on failure.
*/
bool
NufxArchive::SetComment(CWnd* pMsgWnd, GenericEntry* pGenericEntry,
const CString& str)
{
NuDataSource* pSource = nil;
NufxEntry* pEntry = (NufxEntry*) pGenericEntry;
NuError nerr;
bool retVal = false;
/* convert CRLF to CR */
CStringA newStr(str);
char* srcp;
char* dstp;
srcp = dstp = newStr.GetBuffer(0);
while (*srcp != '\0') {
if (*srcp == '\r' && *(srcp+1) == '\n') {
srcp++;
*dstp = '\r';
} else {
*dstp = *srcp;
}
srcp++;
dstp++;
}
*dstp = '\0';
newStr.ReleaseBuffer();
/* get the thread info */
CString errMsg;
NuThread thread;
NuThreadIdx threadIdx;
pEntry->FindThreadInfo(GenericEntry::kCommentThread, &thread, &errMsg);
threadIdx = thread.threadIdx;
if (errMsg.IsEmpty()) {
/* delete existing thread */
nerr = NuDeleteThread(fpArchive, threadIdx);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to delete thread: %hs.", NuStrError(nerr));
goto bail;
}
}
/* set a maximum pre-size value for the thread */
long maxLen;
maxLen = ((newStr.GetLength() + 99) / 100) * 100;
if (maxLen < 200)
maxLen = 200;
/* create a data source to write from */
nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed,
maxLen, (const BYTE*)(LPCSTR)newStr, 0,
newStr.GetLength(), nil, &pSource);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to create NufxLib data source (len=%d, maxLen=%d).",
newStr.GetLength(), maxLen);
goto bail;
}
/* add the new thread */
nerr = NuAddThread(fpArchive, pEntry->GetRecordIdx(),
kNuThreadIDComment, pSource, nil);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to add comment thread: %hs.",
NuStrError(nerr));
goto bail;
}
pSource = nil; // nufxlib owns it now
/* flush changes */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to flush comment changes: %hs.",
NuStrError(nerr));
goto bail;
}
/* reload GenericArchive from NufxLib */
if (InternalReload(fpMsgWnd) == kNuErrNone)
retVal = true;
bail:
NuFreeDataSource(pSource);
if (!retVal) {
WMSG1("FAILED: %ls\n", (LPCWSTR) errMsg);
NuAbort(fpArchive);
}
return retVal;
}
/*
* Remove a comment.
*
* Returns "true" on success, "false" on failure.
*/
bool
NufxArchive::DeleteComment(CWnd* pMsgWnd, GenericEntry* pGenericEntry)
{
CString errMsg;
NuError nerr;
NufxEntry* pEntry = (NufxEntry*) pGenericEntry;
NuThread thread;
NuThreadIdx threadIdx;
bool retVal = false;
pEntry->FindThreadInfo(GenericEntry::kCommentThread, &thread, &errMsg);
if (!errMsg.IsEmpty())
goto bail;
threadIdx = thread.threadIdx;
nerr = NuDeleteThread(fpArchive, threadIdx);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to delete thread: %hs.", NuStrError(nerr));
goto bail;
}
/* flush changes */
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unable to flush comment deletion: %hs.",
NuStrError(nerr));
goto bail;
}
/* reload GenericArchive from NufxLib */
if (InternalReload(pMsgWnd) == kNuErrNone)
retVal = true;
bail:
if (retVal != 0) {
WMSG1("FAILED: %ls\n", (LPCWSTR) errMsg);
NuAbort(fpArchive);
}
return retVal;
}
/*
* Set file properties via the NuSetRecordAttr call.
*
* Get the existing properties, copy the fields from FileProps over, and
* set them.
*
* [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
NufxArchive::SetProps(CWnd* pMsgWnd, GenericEntry* pEntry,
const FileProps* pProps)
{
NuError nerr;
NufxEntry* pNufxEntry = (NufxEntry*) pEntry;
const NuRecord* pRecord;
NuRecordAttr recordAttr;
WMSG3(" SET fileType=0x%02x auxType=0x%04x access=0x%02x\n",
pProps->fileType, pProps->auxType, pProps->access);
nerr = NuGetRecord(fpArchive, pNufxEntry->GetRecordIdx(), &pRecord);
if (nerr != kNuErrNone) {
WMSG2("ERROR: couldn't find recordIdx %ld: %hs\n",
pNufxEntry->GetRecordIdx(), NuStrError(nerr));
return false;
}
NuRecordCopyAttr(&recordAttr, pRecord);
recordAttr.fileType = pProps->fileType;
recordAttr.extraType = pProps->auxType;
recordAttr.access = pProps->access;
nerr = NuSetRecordAttr(fpArchive, pNufxEntry->GetRecordIdx(), &recordAttr);
if (nerr != kNuErrNone) {
WMSG2("ERROR: couldn't set recordAttr %ld: %hs\n",
pNufxEntry->GetRecordIdx(), NuStrError(nerr));
return false;
}
long statusFlags;
nerr = NuFlush(fpArchive, &statusFlags);
if (nerr != kNuErrNone) {
WMSG1("ERROR: NuFlush failed: %hs\n", NuStrError(nerr));
/* see if it got converted to read-only status */
if (statusFlags & kNuFlushReadOnly)
fIsReadOnly = true;
return false;
}
WMSG0("Props set\n");
/* do this in lieu of reloading GenericArchive */
pEntry->SetFileType(pProps->fileType);
pEntry->SetAuxType(pProps->auxType);
pEntry->SetAccess(pProps->access);
return true;
}