/*
 * 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 "../prebuilt/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("allocation of %ld bytes failed",
				actualThreadEOF);
			goto bail;
		}
	} else {
		if (*pLength < (long) actualThreadEOF) {
			pErrMsg->Format("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("unable to create buffer data sink: %s",
			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("The compression method used on this file is not supported "
							"by your copy of \"nufxlib2.dll\".  For more information, "
							"please visit us on the web at "
							"http://www.faddensoft.com/ciderpress/");
		} else {
			pErrMsg->Format("unable to extract thread %ld: %s",
				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("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("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("failed setting EOL value: %s", NuStrError(nerr));
		goto bail;
	}

	/* create a data sink for "outfp" */
	nerr = NuCreateDataSinkForFP(true, nuConv, outfp, &pDataSink);
	if (nerr != kNuErrNone) {
		pErrMsg->Format("unable to create FP data sink: %s",
			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 = _T("cancelled");
			result = IDCANCEL;
		} else if (nerr == kNuErrBadFormat) {
			pErrMsg->Format("The compression method used on this file is not supported "
							"by your copy of \"nufxlib2.dll\".  For more information, "
							"please visit us on the web at "
							"http://www.faddensoft.com/ciderpress/");
		} else {
			pErrMsg->Format("unable to extract thread %ld: %s",
				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("NufxLib unable to locate record %ld: %s",
			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("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("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 char* gFormatNames[] = {
	"Uncompr", "Squeeze", "LZW/1", "LZW/2", "LZC-12",
	"LZC-16", "Deflate", "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("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("Older or incompatible version of NufxLib DLL found.\r\r"
						"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;
	CString msg(pErrorMessage->message);
	
	msg += "\n";

	if (pErrorMessage->isDebug)
		msg = "[D] " + msg;
	fprintf(gLog, "%05u NufxLib %s(%d) : %s",
		gPid, pErrorMessage->file, pErrorMessage->line, msg);

#elif defined(_DEBUG)
	const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage;
	CString msg(pErrorMessage->message);
	
	msg += "\n";

	if (pErrorMessage->isDebug)
		msg = "[D] " + msg;
	_CrtDbgReport(_CRT_WARN, pErrorMessage->file, pErrorMessage->line,
		pErrorMessage->function, 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%% '%s' '%s'\n", perc,
	//	oldName == nil ? "(nil)" : oldName,
	//	newName == nil ? "(nil)" : newName);

	//status = pMainWin->SetProgressUpdate(perc, oldName, newName);
	status = SET_PROGRESS_UPDATE2(perc, oldName, newName);

	/* 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 char* filename, bool readOnly, CString* pErrMsg)
{
	NuError nerr;
	CString errMsg;

	ASSERT(fpArchive == nil);

	if (!readOnly) {
		CString tmpname = GenDerivedTempName(filename);
		WMSG2("Opening file '%s' rw (tmp='%s')\n", filename, tmpname);
		fIsReadOnly = false;
		nerr = NuOpenRW(filename, tmpname, 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 '%s' ro\n", filename);
		fIsReadOnly = true;
		nerr = NuOpenRO(filename, &fpArchive);
	}
	if (nerr != kNuErrNone) {
		errMsg = "Unable to open '";
		errMsg += filename;
		errMsg += "': ";
		errMsg += NuStrError(nerr);
		goto bail;
	} else {
		//WMSG0("FILE OPEN SUCCESS\n");
	}

	nerr = SetCallbacks();
	if (nerr != kNuErrNone) {
		errMsg = "Callback init failed";
		goto bail;
	}

	nerr = LoadContents();
	if (nerr != kNuErrNone) {
		errMsg = "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 char* filename, const void* options)
{
	NuError nerr;
	CString retmsg("");

	ASSERT(fpArchive == nil);
	ASSERT(options == nil);

	CString tmpname = GenDerivedTempName(filename);
	WMSG2("Creating file '%s' (tmp='%s')\n", filename, tmpname);
	fIsReadOnly = false;
	nerr = NuOpenRW(filename, tmpname, kNuOpenCreat | kNuOpenExcl, &fpArchive);
	if (nerr != kNuErrNone) {
		retmsg = "Unable to open '";
		retmsg += filename;
		retmsg += "': ";
		retmsg += NuStrError(nerr);
		goto bail;
	} else {
		WMSG0("NEW FILE SUCCESS\n");
	}


	nerr = SetCallbacks();
	if (nerr != kNuErrNone) {
		retmsg = "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("ERROR: unable to reload archive contents: %s.",
			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);

	pNewEntry->SetPathName(pRecord->filename);
	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;
	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);

	AddPrep(pActionProgress, pAddOpts);

	pActionProgress->SetArcName("(Scanning files to be added...)");
	pActionProgress->SetFileName("");

	/* initialize count */
	fNumAdded = 0;

	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(fpMsgWnd, errMsg, IDS_FAILED);
		goto bail;
	}
	if (SetCurrentDirectory(buf) == false) {
		errMsg.Format("Unable to set current directory to '%s'.\n", buf);
		ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
		goto bail;
	}

	buf += pAddOpts->GetFileNameOffset();
	while (*buf != '\0') {
		WMSG1("  file '%s'\n", buf);

		/* this just provides the list of files to NufxLib */
		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(fpMsgWnd, errMsg, IDS_FAILED);
			}
			goto bail;
		}

		buf += strlen(buf)+1;
	}

	/* actually do the work */
	long statusFlags;
	nerr = NuFlush(fpArchive, &statusFlags);
	if (nerr != kNuErrNone) {
		if (nerr != kNuErrAborted) {
			errMsg.Format("Unable to add files: %s.", 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 = "No files added.\n";
		fpMsgWnd->MessageBox(errMsg, "CiderPress", MB_OK | MB_ICONWARNING);
	} else {
		if (InternalReload(fpMsgWnd) == kNuErrNone)
			retVal = true;
		else
			errMsg = "Reload failed.";
	}

bail:
	NuAbort(fpArchive);		// abort anything that didn't get flushed
	if (SetCurrentDirectory(curDir) == false) {
		errMsg.Format("Unable to reset current directory to '%s'.\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;
	char curDir[MAX_PATH] = "\\";
	bool retVal = false;

	WMSG2("AddDisk: '%s' %d\n", pAddOpts->GetFileNames(),
		pAddOpts->GetFileNameOffset());
	WMSG2("Opts: '%s' type=%d\n",
		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 disk */
	diskData = new unsigned char[pDiskImg->GetNumBlocks() * kBlockSize];
	if (diskData == nil) {
		errMsg.Format("Unable to allocate %d bytes.",
			pDiskImg->GetNumBlocks() * kBlockSize);
		ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
		goto bail;
	}

	/* prepare to add */
	AddPrep(pActionProgress, pAddOpts);

	const char* buf;
	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(fpMsgWnd, errMsg, IDS_FAILED);
		goto bail;
	}
	if (SetCurrentDirectory(buf) == false) {
		errMsg.Format("Unable to set current directory to '%s'.\n", buf);
		ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
		goto bail;
	}

	buf += pAddOpts->GetFileNameOffset();
	WMSG1("  file '%s'\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();
	details.origName = buf;
	details.storageName = pathProp.fStoredPathName;
	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(details.storageName);
	pActionProgress->SetFileName(details.origName);

	/* 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("Skipped %ld unreadable block%s.", numBadBlocks,
			numBadBlocks == 1 ? "" : "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("Failed adding record: %s.", NuStrError(nerr));
			ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED);
		}
		goto bail;
	}

	/* do the compression */
	nerr = NuAddThread(fpArchive, recordIdx, kNuThreadIDDiskImage,
			pSource, nil);
	if (nerr != kNuErrNone) {
		errMsg.Format("Failed adding thread: %s.", 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("Unable to add disk: %s.", 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("Unable to reset current directory to '%s'.\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
	err = NuAddFile(fpArchive, pDetails->origName /*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 '%s'\n", 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 '%s'\n", (const char*) 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("Unable to add file '%s': %s.", 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("Failed while adding '%s': 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 '%s'\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("Failed while testing '%s': %s.",
					pEntry->GetPathName(), NuStrError(nerr));
				ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
			}
			goto bail;
		}

		pSelEntry = pSelSet->IterNext();
	}

	/* show success message */
	errMsg.Format("Tested %d file%s, no errors found.",
		pSelSet->GetNumEntries(),
		pSelSet->GetNumEntries() == 1 ? "" : "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 '%s'\n", pEntry->GetRecordIdx(),
			pEntry->GetPathName());
		nerr = NuDeleteRecord(fpArchive, pEntry->GetRecordIdx());
		if (nerr != kNuErrNone) {
			errMsg.Format("Unable to delete record %d: %s.",
				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("Unable to delete all files: %s.", 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 '%s'\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;
			nerr = NuRename(fpArchive, pEntry->GetRecordIdx(),
					renameDlg.fNewName, renameDlg.fFssep);
			if (nerr != kNuErrNone) {
				errMsg.Format("Unable to rename '%s': %s.", pEntry->GetPathName(),
					NuStrError(nerr));
				ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
				break;
			}
			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();
	}

	/* flush pending rename calls */
	{
		CWaitCursor waitc;

		long statusFlags;
		nerr = NuFlush(fpArchive, &statusFlags);
		if (nerr != kNuErrNone) {
			errMsg.Format("Unable to rename all files: %s.",
				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("Names in NuFX archives may not start or end with a "
				"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("Disk image names may not contain a path separator "
					"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.Format("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("Unable to recompress all files: %s.",
						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("Failed while recompressing '%s': %s.",
			pEntry->GetDisplayName(), 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("Unable to recompress all files: %s.",
				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 '%s'\n", pEntry->GetRecordIdx(),
		pEntry->GetDisplayName());

	/* get a copy of the thread header */
	pEntry->FindThreadInfo(threadKind, &thread, pErrMsg);
	if (!pErrMsg->IsEmpty()) {
		pErrMsg->Format("Unable to locate thread for %s (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) '%s'\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("Failed while extracting '%s': %s",
			pEntry->GetDisplayName(), subErrMsg);
		goto bail;
	}
	*pSizeInMemory += len;

	/* create a data source for it */
	nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed,
			0, (const unsigned char*)buf, 0, len, ArrayDeleteHandler,
			&pSource);
	if (nerr != kNuErrNone) {
		pErrMsg->Format("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("Unable to delete thread %d: %s",
			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("Unable to add thread type %d: %s",
			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 '%s'\n",
				pEntry->GetDisplayName());
			continue;
		}

		WMSG1(" XFER converting '%s'\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("Failed while extracting '%s': %s.",
					pEntry->GetDisplayName(), 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("Failed while extracting '%s': %s.",
					pEntry->GetDisplayName(), 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("Failed while extracting '%s': %s.",
					pEntry->GetDisplayName(), 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 '%s'\n",
				pEntry->GetDisplayName());
			continue;
		}

		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;
		}
	}

	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 '%s'\n", 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("Failed adding record: %s", 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 '%s'\n", 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("Failed adding thread: %s.", 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 = "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("Failed adding thread: %s.", 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("Failed while aborting procedure: %s.", 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("Unable to add file: %s.", 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: %s\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 */
	CString 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("Unable to delete thread: %s.", 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 unsigned char*)(const char*)newStr, 0,
			newStr.GetLength(), nil, &pSource);
	if (nerr != kNuErrNone) {
		errMsg.Format("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("Unable to add comment thread: %s.",
			NuStrError(nerr));
		goto bail;
	}
	pSource = nil;	// nufxlib owns it now

	/* flush changes */
	long statusFlags;
	nerr = NuFlush(fpArchive, &statusFlags);
	if (nerr != kNuErrNone) {
		errMsg.Format("Unable to flush comment changes: %s.",
			NuStrError(nerr));
		goto bail;
	}

	/* reload GenericArchive from NufxLib */
	if (InternalReload(fpMsgWnd) == kNuErrNone)
		retVal = true;

bail:
	NuFreeDataSource(pSource);
	if (!retVal) {
		WMSG1("FAILED: %s\n", (LPCTSTR) 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("Unable to delete thread: %s.", NuStrError(nerr));
		goto bail;
	}

	/* flush changes */
	long statusFlags;
	nerr = NuFlush(fpArchive, &statusFlags);
	if (nerr != kNuErrNone) {
		errMsg.Format("Unable to flush comment deletion: %s.",
			NuStrError(nerr));
		goto bail;
	}

	/* reload GenericArchive from NufxLib */
	if (InternalReload(pMsgWnd) == kNuErrNone)
		retVal = true;

bail:
	if (retVal != 0) {
		WMSG1("FAILED: %s\n", (LPCTSTR) 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: %s\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: %s\n",
			pNufxEntry->GetRecordIdx(), NuStrError(nerr));
		return false;
	}

	long statusFlags;
	nerr = NuFlush(fpArchive, &statusFlags);
	if (nerr != kNuErrNone) {
		WMSG1("ERROR: NuFlush failed: %s\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;
}
