/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Implementation of GenericArchive and GenericEntry.
 *
 * These serve as abstract base classes for archive-specific classes.
 */
#include "stdafx.h"
#include "GenericArchive.h"
#include "FileNameConv.h"
#include "ContentList.h"
#include "Main.h"
#include <sys/stat.h>
#include <errno.h>

/*
 * For systems (e.g. Visual C++ 6.0) that don't have these standard values.
 */
#ifndef S_IRUSR
# define S_IRUSR    0400
# define S_IWUSR    0200
# define S_IXUSR    0100
# define S_IRWXU    (S_IRUSR|S_IWUSR|S_IXUSR)
# define S_IRGRP    (S_IRUSR >> 3)
# define S_IWGRP    (S_IWUSR >> 3)
# define S_IXGRP    (S_IXUSR >> 3)
# define S_IRWXG    (S_IRWXU >> 3)
# define S_IROTH    (S_IRGRP >> 3)
# define S_IWOTH    (S_IWGRP >> 3)
# define S_IXOTH    (S_IXGRP >> 3)
# define S_IRWXO    (S_IRWXG >> 3)
#endif
#ifndef S_ISREG
# define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#endif


/*
 * ===========================================================================
 *		GenericEntry
 * ===========================================================================
 */

/*
 * Initialize all data members.
 */
GenericEntry::GenericEntry(void)
{
	fPathName = nil;
	fFileName = nil;
	fFssep = '\0';
	fSubVolName = nil;
	fDisplayName = nil;
	fFileType = 0;
	fAuxType = 0;
	fAccess = 0;
	fModWhen = kDateNone;
	fCreateWhen = kDateNone;
	fRecordKind = kRecordKindUnknown;
	fFormatStr = "Unknown";
	fCompressedLen = 0;
	//fUncompressedLen = 0;
	fDataForkLen = fRsrcForkLen = 0;

	fSourceFS = DiskImg::kFormatUnknown;

	fHasDataFork = false;
	fHasRsrcFork = false;
	fHasDiskImage = false;
	fHasComment = false;
	fHasNonEmptyComment = false;

	fIndex = -1;
	fpPrev = nil;
	fpNext = nil;

	fDamaged = fSuspicious = false;
}

/*
 * Throw out anything we allocated.
 */
GenericEntry::~GenericEntry(void)
{
	delete[] fPathName;
	delete[] fSubVolName;
	delete[] fDisplayName;
}

/*
 * Pathname getters and setters.
 */
void
GenericEntry::SetPathName(const char* path)
{
	ASSERT(path != nil && strlen(path) > 0);
	if (fPathName != nil)
		delete fPathName;
	fPathName = new char[strlen(path)+1];
	strcpy(fPathName, path);
	// nuke the derived fields
	fFileName = nil;
	fFileNameExtension = nil;
	delete[] fDisplayName;
	fDisplayName = nil;

	/*
	 * Warning: to be 100% pedantically correct here, we should NOT do this
	 * if the fssep char is '_'.  However, that may not have been set by
	 * the time we got here, so to do this "correctly" we'd need to delay
	 * the underscorage until the first GetPathName call.
	 */
	const Preferences* pPreferences = GET_PREFERENCES();
	if (pPreferences->GetPrefBool(kPrSpacesToUnder))
		SpacesToUnderscores(fPathName);
}
const char*
GenericEntry::GetFileName(void)
{
	ASSERT(fPathName != nil);
	if (fFileName == nil)
		fFileName = FilenameOnly(fPathName, fFssep);
	return fFileName;
}
const char*
GenericEntry::GetFileNameExtension(void)
{
	ASSERT(fPathName != nil);
	if (fFileNameExtension == nil)
		fFileNameExtension = FindExtension(fPathName, fFssep);
	return fFileNameExtension;
}
void
GenericEntry::SetSubVolName(const char* name)
{
	delete[] fSubVolName;
	fSubVolName = nil;
	if (name != nil) {
		fSubVolName = new char[strlen(name)+1];
		strcpy(fSubVolName, name);
	}
}
const char*
GenericEntry::GetDisplayName(void) const
{
	ASSERT(fPathName != nil);
	if (fDisplayName != nil)
		return fDisplayName;

	GenericEntry* pThis = const_cast<GenericEntry*>(this);

	int len = strlen(fPathName) +1;
	if (fSubVolName != nil)
		len += strlen(fSubVolName) +1;
	pThis->fDisplayName = new char[len];
	if (fSubVolName != nil) {
		char xtra[2] = { DiskFS::kDIFssep, '\0' };
		strcpy(pThis->fDisplayName, fSubVolName);
		strcat(pThis->fDisplayName, xtra);
	} else
		pThis->fDisplayName[0] = '\0';
	strcat(pThis->fDisplayName, fPathName);
	return pThis->fDisplayName;
}

/*
 * Get a string for this entry's filetype.
 */
const char*
GenericEntry::GetFileTypeString(void) const
{
	return PathProposal::FileTypeString(fFileType);
}

/*
 * Convert spaces to underscores.
 */
/*static*/ void
GenericEntry::SpacesToUnderscores(char* buf)
{
	while (*buf != '\0') {
		if (*buf == ' ')
			*buf = '_';
		buf++;
	}
}


/*
 * (Pulled from NufxLib Funnel.c.)
 *
 * Check to see if this is a high-ASCII file.  To qualify, EVERY
 * character must have its high bit set, except for spaces (0x20,
 * courtesy Glen Bredon's "Merlin") and nulls (0x00, because of random-
 * access text files).
 *
 * The test for 0x00 is actually useless in many circumstances because the
 * NULLs will cause the text file auto-detector to flunk the file.  It will,
 * however, allow the user to select "convert ALL files" and still have the
 * stripping enabled.
 */
/*static*/ bool
GenericEntry::CheckHighASCII(const unsigned char* buffer,
	unsigned long count)
{
	bool isHighASCII;

	ASSERT(buffer != nil);
	ASSERT(count != 0);

	isHighASCII = true;
	while (count--) {
		if ((*buffer & 0x80) == 0 && *buffer != 0x20 && *buffer != 0x00) {
			WMSG1("Flunking CheckHighASCII on 0x%02x\n", *buffer);
			isHighASCII = false;
			break;
		}
		
		buffer++;
	}

	return isHighASCII;
}

/*
 * (Pulled from NufxLib Funnel.c.)
 *
 * Table determining what's a binary character and what isn't.  It would
 * possibly be more compact to generate this from a simple description,
 * but I'm hoping static/const data will end up in the code segment and
 * save space on the heap.
 *
 * This corresponds to less-316's ISO-latin1 "8bcccbcc18b95.33b.".  This
 * may be too loose by itself; we may want to require that the lower-ASCII
 * values appear in higher proportions than the upper-ASCII values.
 * Otherwise we run the risk of converting a binary file with specific
 * properties.  (Note that "upper-ASCII" refers to umlauts and other
 * accented characters, not DOS 3.3 "high ASCII".)
 *
 * The auto-detect mechanism will never be perfect though, so there's not
 * much point in tweaking it to death.
 */
static const char gIsBinary[256] = {
	1, 1, 1, 1, 1, 1, 1, 1,  0, 0, 0, 1, 0, 0, 1, 1,	/* ^@-^O */
	1, 1, 1, 1, 1, 1, 1, 1,  1, 1, 1, 1, 1, 1, 1, 1,	/* ^P-^_ */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/*	 - / */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* 0 - ? */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* @ - O */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* P - _ */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* ` - o */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 1,	/* p - DEL */
	1, 1, 1, 1, 1, 1, 1, 1,  1, 1, 1, 1, 1, 1, 1, 1,	/* 0x80 */
	1, 1, 1, 1, 1, 1, 1, 1,  1, 1, 1, 1, 1, 1, 1, 1,	/* 0x90 */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* 0xa0 */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* 0xb0 */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* 0xc0 */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* 0xd0 */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* 0xe0 */
	0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,	/* 0xf0 */
};

#define kNuMaxUpperASCII	1		/* max #of binary chars per 100 bytes */
#define kMinConvThreshold	40		/* min of 40 chars for auto-detect */
#define kCharLF				'\n'
#define kCharCR				'\r'
/*
 * Decide, based on the contents of the buffer, whether we should do an
 * EOL conversion on the data.
 *
 * We need to decide if we are looking at text data, and if so, what kind
 * of line terminator is in use.
 *
 * If we don't have enough data to make a determination, don't mess with it.
 * (Thought for the day: add a "bias" flag, based on the NuRecord fileType,
 * that causes us to handle borderline or sub-min-threshold cases more
 * reasonably.  If it's of type TXT, it's probably text.)
 *
 * We try to figure out whether it's CR, LF, or CRLF, so that we can
 * skip the CPU-intensive conversion process if it isn't necessary.
 *
 * We will also investigate enabling a "high-ASCII" stripper if requested.
 * This is only enabled when EOL conversions are enabled.  Set "*pConvHA"
 * to on/off/auto before calling.  If it's initially set to "off", no
 * attempt to evaluate high ASCII will be made.  If "on" or "auto", the
 * buffer will be scanned, and if the input appears to be high ASCII then
 * it will be stripped *before* the EOL determination is made.
 *
 * Returns kConvEOLOff or kConvEOLOn.
 */
/*static*/ GenericEntry::ConvertEOL
GenericEntry::DetermineConversion(const unsigned char* buffer, long count,
	EOLType* pSourceType, ConvertHighASCII* pConvHA)
{
	ConvertHighASCII wantConvHA = *pConvHA;
	long bufCount, numBinary, numLF, numCR;
	bool isHighASCII;
	unsigned char val;

	*pSourceType = kEOLUnknown;
	*pConvHA = kConvertHAOff;

	if (count < kMinConvThreshold)
		return kConvertEOLOff;

    /*
     * Check to see if the buffer is all high-ASCII characters.  If it is,
     * we want to strip characters before we test them below.
	 *
	 * If high ASCII conversion is disabled, assume that any high-ASCII
	 * characters are not meant to be line terminators, i.e. 0x8d != 0x0d.
     */
	if (wantConvHA == kConvertHAOn || wantConvHA == kConvertHAAuto) {
        isHighASCII = CheckHighASCII(buffer, count);
		WMSG1(" +++ Determined isHighASCII=%d\n", isHighASCII);
	} else {
		isHighASCII = false;
		WMSG0(" +++ Not even checking isHighASCII\n");
	}

	bufCount = count;
	numBinary = numLF = numCR = 0;
	while (bufCount--) {
		val = *buffer++;
		if (isHighASCII)
			val &= 0x7f;
		if (gIsBinary[val])
			numBinary++;
		if (val == kCharLF)
			numLF++;
		if (val == kCharCR)
			numCR++;
	}

	/* if #found is > #allowed, it's a binary file */
	if (count < 100) {
		/* use simplified check on files between kNuMinConvThreshold and 100 */
		if (numBinary > kNuMaxUpperASCII)
			return kConvertEOLOff;
	} else if (numBinary > (count / 100) * kNuMaxUpperASCII)
		return kConvertEOLOff;

	/*
	 * If our "convert to" setting is the same as what we're converting
	 * from, we can turn off the converter and speed things up.
	 *
	 * These are simplistic, but this is intended as an optimization.  We
	 * will blow it if the input has lots of CRs and LFs scattered about,
	 * and they just happen to be in equal amounts, but it's not clear
	 * to me that an automatic EOL conversion makes sense on that sort
	 * of file anyway.
	 *
	 * None of this applies if we also need to do a high-ASCII conversion,
	 * because we can't bypass the processing.
	 */
	if (isHighASCII) {
		*pConvHA = kConvertHAOn;
	} else {
		if (numLF && !numCR)
			*pSourceType = kEOLLF;
		else if (!numLF && numCR)
			*pSourceType = kEOLCR;
		else if (numLF && numLF == numCR)
			*pSourceType = kEOLCRLF;
		else
			*pSourceType = kEOLUnknown;
	}

	return kConvertEOLOn;
}

/*
 * Output CRLF.
 */
static inline void
PutEOL(FILE* fp)
{
	putc(kCharCR, fp);
	putc(kCharLF, fp);
}

/*
 * Write data to a file, possibly converting EOL markers to Windows CRLF
 * and stripping high ASCII.
 *
 * If "*pConv" is kConvertEOLAuto, this will try to auto-detect whether
 * the input is a text file or not by scanning the input buffer.
 *
 * Ditto for "*pConvHA".
 *
 * "fp" is the output file, "buf" is the input, "len" is the buffer length.
 * "*pLastCR" should initially be "false", and carried across invocations.
 *
 * Returns 0 on success, or an errno value on error.
 */
/*static*/ int
GenericEntry::WriteConvert(FILE* fp, const char* buf, size_t len,
	ConvertEOL* pConv, ConvertHighASCII* pConvHA, bool* pLastCR)
{
	int err = 0;

	WMSG2("+++ WriteConvert conv=%d convHA=%d\n", *pConv, *pConvHA);

	if (len == 0) {
		WMSG0("WriteConvert asked to write 0 bytes; returning\n");
		return err;
	}

	/* if we're in "auto" mode, scan the input for EOL and high ASCII */
	if (*pConv == kConvertEOLAuto) {
		EOLType sourceType;
		*pConv = DetermineConversion((unsigned char*)buf, len, &sourceType,
					pConvHA);
		if (*pConv == kConvertEOLOn && sourceType == kEOLCRLF) {
			WMSG0(" Auto-detected text conversion from CRLF; disabling\n");
			*pConv = kConvertEOLOff;
		}
		WMSG2(" Auto-detected EOL conv=%d ha=%d\n", *pConv, *pConvHA);
	} else if (*pConvHA == kConvertHAAuto) {
		if (*pConv == kConvertEOLOn) {
			/* definitely converting EOL, test for high ASCII */
			if (CheckHighASCII((unsigned char*)buf, len))
				*pConvHA = kConvertHAOn;
			else
				*pConvHA = kConvertHAOff;
		} else {
			/* not converting EOL, don't convert high ASCII */
			*pConvHA = kConvertHAOff;
		}
	}
	WMSG2("+++  After auto, conv=%d convHA=%d\n", *pConv, *pConvHA);
	ASSERT(*pConv == kConvertEOLOn || *pConv == kConvertEOLOff);
	ASSERT(*pConvHA == kConvertHAOn || *pConvHA == kConvertHAOff);

	/* write the output */
	if (*pConv == kConvertEOLOff) {
		if (fwrite(buf, len, 1, fp) != 1) {
			err = errno;
			WMSG1("WriteConvert failed, err=%d\n", errno);
		}
	} else {
		ASSERT(*pConv == kConvertEOLOn);
		bool lastCR = *pLastCR;
		unsigned char uch;
		int mask;

		if (*pConvHA == kConvertHAOn)
			mask = 0x7f;
		else
			mask = 0xff;

		while (len--) {
			uch = (*buf) & mask;

			if (uch == kCharCR) {
				PutEOL(fp);
				lastCR = true;
			} else if (uch == kCharLF) {
				if (!lastCR)
					PutEOL(fp);
				lastCR = false;
			} else {
				putc(uch, fp);
				lastCR = false;
			}
			buf++;
		}
		*pLastCR = lastCR;
	}

	return err;
}


/*
 * ===========================================================================
 *		GenericArchive
 * ===========================================================================
 */

/*
 * Add a new entry to the end of the list.
 */
void
GenericArchive::AddEntry(GenericEntry* pEntry)
{
	if (fEntryHead == nil) {
		ASSERT(fEntryTail == nil);
		fEntryHead = pEntry;
		fEntryTail = pEntry;
		ASSERT(pEntry->GetPrev() == nil);
		ASSERT(pEntry->GetNext() == nil);
	} else {
		ASSERT(fEntryTail != nil);
		ASSERT(pEntry->GetPrev() == nil);
		pEntry->SetPrev(fEntryTail);
		ASSERT(fEntryTail->GetNext() == nil);
		fEntryTail->SetNext(pEntry);
		fEntryTail = pEntry;
	}

	fNumEntries++;

	//if (fEntryIndex != nil) {
	//	WMSG0("Resetting fEntryIndex\n");
	//	delete [] fEntryIndex;
	//	fEntryIndex = nil;
	//}
}

/*
 * Delete the "entries" list.
 */
void
GenericArchive::DeleteEntries(void)
{
	GenericEntry* pEntry;
	GenericEntry* pNext;

	WMSG1("Deleting %d archive entries\n", fNumEntries);

	pEntry = GetEntries();
	while (pEntry != nil) {
		pNext = pEntry->GetNext();
		delete pEntry;
		pEntry = pNext;
	}

	//delete [] fEntryIndex;
	fNumEntries = 0;
	fEntryHead = fEntryTail = nil;
}

#if 0
/*
 * Create an index for fast access.
 */
void
GenericArchive::CreateIndex(void)
{
	GenericEntry* pEntry;
	int num;

	WMSG1("Creating entry index (%d entries)\n", fNumEntries);

	ASSERT(fNumEntries != 0);

	fEntryIndex = new GenericEntry*[fNumEntries];
	if (fEntryIndex == nil)
		return;

	pEntry = GetEntries();
	num = 0;
	while (pEntry != nil) {
		fEntryIndex[num] = pEntry;
		pEntry = pEntry->GetNext();
		num++;
	}
}
#endif

/*
 * Generate a temp name from a file name.
 *
 * The key is to come up with the name of a temp file in the same directory
 * (or at least on the same disk volume) so that the temp file can be
 * renamed on top of the original.
 *
 * Windows _mktemp does appear to test for the existence of the file, which
 * is good.  It doesn't actually open the file, which creates a small window
 * in which bad things could happen, but it should be okay.
 */
/*static*/ CString
GenericArchive::GenDerivedTempName(const char* filename)
{
	static const char* kTmpTemplate = "CPtmp_XXXXXX";
	CString mangle(filename);
	int idx, len;

	ASSERT(filename != nil);

	len = mangle.GetLength();
	ASSERT(len > 0);
	idx = mangle.ReverseFind('\\');
	if (idx < 0) {
		/* generally shouldn't happen -- we're using full paths */
		return kTmpTemplate;
	} else {
		mangle.Delete(idx+1, len-(idx+1));	/* delete out to the end */
		mangle += kTmpTemplate;
	}
	WMSG2("GenDerived: passed '%s' returned '%s'\n", filename, mangle);

	return mangle;
}


/*
 * Do a strcasecmp-like comparison, taking equivalent fssep chars into
 * account.
 *
 * The tricky part is with files like "foo:bar" ':' -- "foo:bar" '/'.  The
 * names appear to match, but the fssep chars are different, so they don't.
 * If we just return (char1 - char2), though, we'll be returning 0 because
 * the ASCII values match even if the character *meanings* don't.
 *
 * This assumes that the fssep char is not affected by tolower().
 *
 * [This may not sort correctly...haven't verified that I'm returning the
 * right thing for ascending ASCII sort.]
 */
/*static*/ int
GenericArchive::ComparePaths(const CString& name1, char fssep1,
	const CString& name2, char fssep2)
{
	const char* cp1 = name1;
	const char* cp2 = name2;

	while (*cp1 != '\0' && *cp2 != '\0') {
		if (*cp1 == fssep1) {
			if (*cp2 != fssep2) {
				/* one fssep, one not, no match */
				if (*cp1 == *cp2)
					return 1;
				else
					return *cp1 - *cp2;
			} else {
				/* both are fssep, it's a match even if ASCII is different */
			}
		} else if (*cp2 == fssep2) {
			/* one fssep, one not */
			if (*cp1 == *cp2)
				return -1;
			else
				return *cp1 - *cp2;
		} else if (tolower(*cp1) != tolower(*cp2)) {
			/* mismatch */
			return tolower(*cp1) - tolower(*cp2);
		}

		cp1++;
		cp2++;
	}

	return *cp1 - *cp2;
}


/*
 * ===========================================================================
 *		GenericArchive -- "add files" stuff
 * ===========================================================================
 */

/*
 * This comes straight out of NuLib2, and uses NufxLib data structures.  While
 * it may seem strange to use these structures for non-NuFX archives, they are
 * convenient and hold at least as much information as any other format needs.
 */

typedef bool Boolean;

/*
 * Convert from time in seconds to Apple IIgs DateTime format.
 */
/*static*/ void
GenericArchive::UNIXTimeToDateTime(const time_t* pWhen, NuDateTime* pDateTime)
{
    struct tm* ptm;

    ASSERT(pWhen != nil);
    ASSERT(pDateTime != nil);

    ptm = localtime(pWhen);
	if (ptm == nil) {
		ASSERT(*pWhen == kDateNone || *pWhen == kDateInvalid);
		memset(pDateTime, 0, sizeof(*pDateTime));
		return;
	}
    pDateTime->second = ptm->tm_sec;
    pDateTime->minute = ptm->tm_min;
    pDateTime->hour = ptm->tm_hour;
    pDateTime->day = ptm->tm_mday -1;
    pDateTime->month = ptm->tm_mon;
    pDateTime->year = ptm->tm_year;
    pDateTime->extra = 0;
    pDateTime->weekDay = ptm->tm_wday +1;
}


/*
 * Set the contents of a NuFileDetails structure, based on the pathname
 * and characteristics of the file.
 *
 * For efficiency and simplicity, the pathname fields are set to CStrings in
 * the GenericArchive object instead of newly-allocated storage.
 */
NuError
GenericArchive::GetFileDetails(const AddFilesDialog* pAddOpts,
	const char* pathname, struct stat* psb, FileDetails* pDetails)
{
	//char* livePathStr;
	time_t now;

	ASSERT(pAddOpts != nil);
	ASSERT(pathname != nil);
	ASSERT(pDetails != nil);

	/* init to defaults */
	//pDetails->threadID = kNuThreadIDDataFork;
	pDetails->entryKind = FileDetails::kFileKindDataFork;
	//pDetails->fileSysID = kNuFileSysUnknown;
	pDetails->fileSysFmt = DiskImg::kFormatUnknown;
	pDetails->fileSysInfo = PathProposal::kDefaultStoredFssep;
	pDetails->fileType = 0;
	pDetails->extraType = 0;
	pDetails->storageType = kNuStorageUnknown;	/* let NufxLib worry about it */
	if (psb->st_mode & S_IWUSR)
		pDetails->access = kNuAccessUnlocked;
	else
		pDetails->access = kNuAccessLocked;

#if 0
	/* if this is a disk image, fill in disk-specific fields */
	if (NState_GetModAddAsDisk(pState)) {
		if ((psb->st_size & 0x1ff) != 0) {
			/* reject anything whose size isn't a multiple of 512 bytes */
			printf("NOT storing odd-sized (%ld) file as disk image: %s\n",
				(long)psb->st_size, livePathStr);
		} else {
			/* set fields; note the "preserve" stuff can override this */
			pDetails->threadID = kNuThreadIDDiskImage;
			pDetails->storageType = 512;
			pDetails->extraType = psb->st_size / 512;
		}
	}
#endif

	now = time(nil);
	UNIXTimeToDateTime(&now, &pDetails->archiveWhen);
	UNIXTimeToDateTime(&psb->st_mtime, &pDetails->modWhen);
	UNIXTimeToDateTime(&psb->st_ctime, &pDetails->createWhen);

	/* get adjusted filename, along with any preserved type info */
	PathProposal pathProp;
	pathProp.Init(pathname);
	pathProp.LocalToArchive(pAddOpts);

	/* set up the local and archived pathnames */
	pDetails->storageName = "";
	if (!pAddOpts->fStoragePrefix.IsEmpty()) {
		pDetails->storageName += pAddOpts->fStoragePrefix;
		pDetails->storageName += pathProp.fStoredFssep;
	}
	pDetails->storageName += pathProp.fStoredPathName;

	/*
	 * Fill in the NuFileDetails struct.
	 *
	 * We use GetBuffer to get the string to ensure that the CString object
	 * doesn't do anything while we're not looking.  The string won't be
	 * modified, and it won't be around for very long, so it's not strictly
	 * necessary to do this.  It is, however, the correct approach.
	 */
	pDetails->origName = pathname;
	pDetails->fileSysInfo = pathProp.fStoredFssep;
	pDetails->fileType = pathProp.fFileType;
	pDetails->extraType = pathProp.fAuxType;
	switch (pathProp.fThreadKind) {
	case GenericEntry::kDataThread:
		//pDetails->threadID = kNuThreadIDDataFork;
		pDetails->entryKind = FileDetails::kFileKindDataFork;
		break;
	case GenericEntry::kRsrcThread:
		//pDetails->threadID = kNuThreadIDRsrcFork;
		pDetails->entryKind = FileDetails::kFileKindRsrcFork;
		break;
	case GenericEntry::kDiskImageThread:
		//pDetails->threadID = kNuThreadIDDiskImage;
		pDetails->entryKind = FileDetails::kFileKindDiskImage;
		break;
	default:
		ASSERT(false);
		// was initialized to default earlier
		break;
	}

/*bail:*/
	return kNuErrNone;
}

/*
 * Directory structure and functions, based on zDIR in Info-Zip sources.
 */
typedef struct Win32dirent {
	char	d_attr;
	char	d_name[MAX_PATH];
	int 	d_first;
	HANDLE	d_hFindFile;
} Win32dirent;

static const char* kWildMatchAll = "*.*";

/*
 * Prepare a directory for reading.
 *
 * Allocates a Win32dirent struct that must be freed by the caller.
 */
Win32dirent*
GenericArchive::OpenDir(const char* name)
{
	Win32dirent* dir = nil;
	char* tmpStr = nil;
	char* cp;
	WIN32_FIND_DATA fnd;

	dir = (Win32dirent*) malloc(sizeof(*dir));
	tmpStr = (char*) malloc(strlen(name) + (2 + sizeof(kWildMatchAll)));
	if (dir == nil || tmpStr == nil)
		goto failed;

	strcpy(tmpStr, name);
	cp = tmpStr + strlen(tmpStr);

	/* don't end in a colon (e.g. "C:") */
	if ((cp - tmpStr) > 0 && strrchr(tmpStr, ':') == (cp - 1))
		*cp++ = '.';
	/* must end in a slash */
	if ((cp - tmpStr) > 0 &&
			strrchr(tmpStr, PathProposal::kLocalFssep) != (cp - 1))
		*cp++ = PathProposal::kLocalFssep;

	strcpy(cp, kWildMatchAll);

	dir->d_hFindFile = FindFirstFile(tmpStr, &fnd);
	if (dir->d_hFindFile == INVALID_HANDLE_VALUE)
		goto failed;

	strcpy(dir->d_name, fnd.cFileName);
	dir->d_attr = (unsigned char) fnd.dwFileAttributes;
	dir->d_first = 1;

bail:
	free(tmpStr);
	return dir;

failed:
	free(dir);
	dir = nil;
	goto bail;
}

/*
 * Get an entry from an open directory.
 *
 * Returns a nil pointer after the last entry has been read.
 */
Win32dirent*
GenericArchive::ReadDir(Win32dirent* dir)
{
	if (dir->d_first)
		dir->d_first = 0;
	else {
		WIN32_FIND_DATA fnd;

		if (!FindNextFile(dir->d_hFindFile, &fnd))
			return nil;
		strcpy(dir->d_name, fnd.cFileName);
		dir->d_attr = (unsigned char) fnd.dwFileAttributes;
	}

	return dir;
}

/*
 * Close a directory.
 */
void
GenericArchive::CloseDir(Win32dirent* dir)
{
	if (dir == nil)
		return;

	FindClose(dir->d_hFindFile);
	free(dir);
}


/* might as well blend in with the UNIX version */
#define DIR_NAME_LEN(dirent)	((int)strlen((dirent)->d_name))

//static NuError Win32AddFile(NulibState* pState, NuArchive* pArchive,
//	const char* pathname);


/*
 * Win32 recursive directory descent.  Scan the contents of a directory.
 * If a subdirectory is found, follow it; otherwise, call Win32AddFile to
 * add the file.
 */
NuError
GenericArchive::Win32AddDirectory(const AddFilesDialog* pAddOpts,
	const char* dirName, CString* pErrMsg)
{
	NuError err = kNuErrNone;
	Win32dirent* dirp = nil;
	Win32dirent* entry;
	char nbuf[MAX_PATH];	/* malloc might be better; this soaks stack */
	char fssep;
	int len;

	ASSERT(pAddOpts != nil);
	ASSERT(dirName != nil);

	WMSG1("+++ DESCEND: '%s'\n", dirName);

	dirp = OpenDir(dirName);
	if (dirp == nil) {
		if (errno == ENOTDIR)
			err = kNuErrNotDir;
		else
			err = errno ? (NuError)errno : kNuErrOpenDir;
		
		pErrMsg->Format("Failed on '%s': %s.", dirName, NuStrError(err));
		goto bail;
	}

	fssep = PathProposal::kLocalFssep;

	/* could use readdir_r, but we don't care about reentrancy here */
	while ((entry = ReadDir(dirp)) != nil) {
		/* skip the dotsies */
		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
			continue;

		len = strlen(dirName);
		if (len + DIR_NAME_LEN(entry) +2 > MAX_PATH) {
			err = kNuErrInternal;
			WMSG4("ERROR: Filename exceeds %d bytes: %s%c%s",
				MAX_PATH, dirName, fssep, entry->d_name);
			goto bail;
		}

		/* form the new name, inserting an fssep if needed */
		strcpy(nbuf, dirName);
		if (dirName[len-1] != fssep)
			nbuf[len++] = fssep;
		strcpy(nbuf+len, entry->d_name);

		err = Win32AddFile(pAddOpts, nbuf, pErrMsg);
		if (err != kNuErrNone)
			goto bail;
	}

bail:
	if (dirp != nil)
		(void)CloseDir(dirp);
	return err;
}

/*
 * Add a file to the list we're adding to the archive.  If it's a directory,
 * and the recursive descent feature is enabled, call Win32AddDirectory to
 * add the contents of the dir.
 *
 * Returns with an error if the file doesn't exist or isn't readable.
 */
NuError
GenericArchive::Win32AddFile(const AddFilesDialog* pAddOpts,
	const char* pathname, CString* pErrMsg)
{
	NuError err = kNuErrNone;
	Boolean exists, isDir, isReadable;
	FileDetails details;
	struct stat sb;

	ASSERT(pAddOpts != nil);
	ASSERT(pathname != nil);

	PathName checkPath(pathname);
	int ierr = checkPath.CheckFileStatus(&sb, &exists, &isReadable, &isDir);
	if (ierr != 0) {
		err = kNuErrGeneric;
		pErrMsg->Format("Unexpected error while examining '%s': %s.", pathname,
			NuStrError((NuError) ierr));
		goto bail;
	}

	if (!exists) {
		err = kNuErrFileNotFound;
		pErrMsg->Format("Couldn't find '%s'", pathname);
		goto bail;
	}
	if (!isReadable) {
		err = kNuErrFileNotReadable;
		pErrMsg->Format("File '%s' isn't readable.", pathname);
		goto bail;
	}
	if (isDir) {
		if (pAddOpts->fIncludeSubfolders)
			err = Win32AddDirectory(pAddOpts, pathname, pErrMsg);
		goto bail;
	}

	/*
	 * We've found a file that we want to add.	We need to decide what
	 * filetype and auxtype it has, and whether or not it's actually the
	 * resource fork of another file.
	 */
	WMSG1("+++ ADD '%s'\n", pathname);

	/*
	 * Fill out the "details" structure.  The class has an automatic
	 * conversion to NuFileDetails, but it relies on the CString storage
	 * in the FileDetails, so be careful how you use it.
	 */
	err = GetFileDetails(pAddOpts, pathname, &sb, &details);
	if (err != kNuErrNone)
		goto bail;

	assert(strcmp(pathname, details.origName) == 0);
	err = DoAddFile(pAddOpts, &details);
	if (err == kNuErrSkipped)	// ignore "skipped" result
		err = kNuErrNone;
	if (err != kNuErrNone)
		goto bail;

bail:
	if (err != kNuErrNone && pErrMsg->IsEmpty()) {
		pErrMsg->Format("Unable to add file '%s': %s.",
			pathname, NuStrError(err));
	}
	return err;
}

/*
 * External entry point; just calls the system-specific version.
 *
 * [ I figure the GS/OS version will want to pass a copy of the file
 *	 info from the GSOSAddDirectory function back into GSOSAddFile, so we'd
 *	 want to call it from here with a nil pointer indicating that we
 *	 don't yet have the file info.	That way we can get the file info
 *	 from the directory read call and won't have to check it again in
 *	 GSOSAddFile. ]
 */
NuError
GenericArchive::AddFile(const AddFilesDialog* pAddOpts, const char* pathname,
	CString* pErrMsg)
{
	*pErrMsg = "";
	return Win32AddFile(pAddOpts, pathname, pErrMsg);
}

/*
 * ===========================================================================
 *		GenericArchive::FileDetails
 * ===========================================================================
 */

/*
 * Constructor.
 */
GenericArchive::FileDetails::FileDetails(void)
{
	//threadID = 0;
	entryKind = kFileKindUnknown;
	fileSysFmt = DiskImg::kFormatUnknown;
	fileSysInfo = storageType = 0;
	access = fileType = extraType = 0;
	memset(&createWhen, 0, sizeof(createWhen));
	memset(&modWhen, 0, sizeof(modWhen));
	memset(&archiveWhen, 0, sizeof(archiveWhen));
}

/*
 * Automatic cast conversion to NuFileDetails.
 *
 * Note the NuFileDetails will have a string pointing into our storage.
 * This is not a good thing, but it's tough to work around.
 */
GenericArchive::FileDetails::operator const NuFileDetails() const
{
	NuFileDetails details;

	//details.threadID = threadID;
	switch (entryKind) {
	case kFileKindDataFork:
		details.threadID = kNuThreadIDDataFork;
		break;
	case kFileKindBothForks:	// not exactly supported, doesn't really matter
	case kFileKindRsrcFork:
		details.threadID = kNuThreadIDRsrcFork;
		break;
	case kFileKindDiskImage:
		details.threadID = kNuThreadIDDiskImage;
		break;
	case kFileKindDirectory:
	default:
		WMSG1("Invalid entryKind (%d) for NuFileDetails conversion\n",
			entryKind);
		ASSERT(false);
		details.threadID = 0;		// that makes it an old-style comment?!
		break;
	}

	details.origName = origName;		// CString to char*
	details.storageName = storageName;	// CString to char*
	//details.fileSysID = fileSysID;
	details.fileSysInfo = fileSysInfo;
	details.access = access;
	details.fileType = fileType;
	details.extraType = extraType;
	details.storageType = storageType;
	details.createWhen = createWhen;
	details.modWhen = modWhen;
	details.archiveWhen = archiveWhen;

	switch (fileSysFmt) {
	case DiskImg::kFormatProDOS:
	case DiskImg::kFormatDOS33:
	case DiskImg::kFormatDOS32:
	case DiskImg::kFormatPascal:
	case DiskImg::kFormatMacHFS:
	case DiskImg::kFormatMacMFS:
	case DiskImg::kFormatLisa:
	case DiskImg::kFormatCPM:
	//kFormatCharFST
	case DiskImg::kFormatMSDOS:
	//kFormatHighSierra
	case DiskImg::kFormatISO9660:
		/* these map directly */
		details.fileSysID = (enum NuFileSysID) fileSysFmt;
		break;

	case DiskImg::kFormatRDOS33:
	case DiskImg::kFormatRDOS32:
	case DiskImg::kFormatRDOS3:
		/* these look like DOS33, e.g. text is high-ASCII */
		details.fileSysID = kNuFileSysDOS33;
		break;

	default:
		details.fileSysID = kNuFileSysUnknown;
		break;
	}


	// Return stack copy, which copies into compiler temporary with our
	// copy constructor.
	return details;
}

/*
 * Copy the contents of our object to a new object.
 *
 * Useful for operator= and copy construction.
 */
/*static*/ void
GenericArchive::FileDetails::CopyFields(FileDetails* pDst,
	const FileDetails* pSrc)
{
	//pDst->threadID = pSrc->threadID;
	pDst->entryKind = pSrc->entryKind;
	pDst->origName = pSrc->origName;
	pDst->storageName = pSrc->storageName;
	pDst->fileSysFmt = pSrc->fileSysFmt;
	pDst->fileSysInfo = pSrc->fileSysInfo;
	pDst->access = pSrc->access;
	pDst->fileType = pSrc->fileType;
	pDst->extraType = pSrc->extraType;
	pDst->storageType = pSrc->storageType;
	pDst->createWhen = pSrc->createWhen;
	pDst->modWhen = pSrc->modWhen;
	pDst->archiveWhen = pSrc->archiveWhen;
}


/*
 * ===========================================================================
 *		SelectionSet
 * ===========================================================================
 */

/*
 * Create a selection set from the selected items in a ContentList.
 *
 * This grabs the items in the order in which they appear in the display
 * (at least under Win2K), which is a good thing.  It appears that, if you
 * just grab indices 0..N, you will get them in order.
 */
void
SelectionSet::CreateFromSelection(ContentList* pContentList, int threadMask)
{
	WMSG1("CreateFromSelection (threadMask=0x%02x)\n", threadMask);

	POSITION posn;
	posn = pContentList->GetFirstSelectedItemPosition();
	ASSERT(posn != nil);
	if (posn == nil)
		return;
	while (posn != nil) {
		int num = pContentList->GetNextSelectedItem(/*ref*/ posn);
		GenericEntry* pEntry = (GenericEntry*) pContentList->GetItemData(num);

		AddToSet(pEntry, threadMask);
	}
}

/*
 * Like CreateFromSelection, but includes the entire list.
 */
void
SelectionSet::CreateFromAll(ContentList* pContentList, int threadMask)
{
	WMSG1("CreateFromAll (threadMask=0x%02x)\n", threadMask);

	int count = pContentList->GetItemCount();
	for (int idx = 0; idx < count; idx++) {
		GenericEntry* pEntry = (GenericEntry*) pContentList->GetItemData(idx);

		AddToSet(pEntry, threadMask);
	}
}

/*
 * Add a GenericEntry to the set, but only if we can find a thread that
 * matches the flags in "threadMask".
 */
void
SelectionSet::AddToSet(GenericEntry* pEntry, int threadMask)
{
	SelectionEntry* pSelEntry;

	//WMSG1("  Sel '%s'\n", pEntry->GetPathName());

	if (!(threadMask & GenericEntry::kAllowVolumeDir) &&
		 pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir)
	{
		/* only include volume dir if specifically requested */
		//WMSG1(" Excluding volume dir '%s' from set\n", pEntry->GetPathName());
		return;
	}

	if (!(threadMask & GenericEntry::kAllowDirectory) &&
		pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory)
	{
		/* only include directories if specifically requested */
		//WMSG1(" Excluding folder '%s' from set\n", pEntry->GetPathName());
		return;
	}

	if (!(threadMask & GenericEntry::kAllowDamaged) && pEntry->GetDamaged())
	{
		/* only include "damaged" files if specifically requested */
		return;
	}

	bool doAdd = false;

	if (threadMask & GenericEntry::kAnyThread)
		doAdd = true;

	if ((threadMask & GenericEntry::kCommentThread) && pEntry->GetHasComment())
		doAdd = true;
	if ((threadMask & GenericEntry::kDataThread) && pEntry->GetHasDataFork())
		doAdd = true;
	if ((threadMask & GenericEntry::kRsrcThread) && pEntry->GetHasRsrcFork())
		doAdd = true;
	if ((threadMask & GenericEntry::kDiskImageThread) && pEntry->GetHasDiskImage())
		doAdd = true;

	if (doAdd) {
		pSelEntry = new SelectionEntry(pEntry);
		AddEntry(pSelEntry);
	}
}

/*
 * Add a new entry to the end of the list.
 */
void
SelectionSet::AddEntry(SelectionEntry* pEntry)
{
	if (fEntryHead == nil) {
		ASSERT(fEntryTail == nil);
		fEntryHead = pEntry;
		fEntryTail = pEntry;
		ASSERT(pEntry->GetPrev() == nil);
		ASSERT(pEntry->GetNext() == nil);
	} else {
		ASSERT(fEntryTail != nil);
		ASSERT(pEntry->GetPrev() == nil);
		pEntry->SetPrev(fEntryTail);
		ASSERT(fEntryTail->GetNext() == nil);
		fEntryTail->SetNext(pEntry);
		fEntryTail = pEntry;
	}

	fNumEntries++;
}

/*
 * Delete the "entries" list.
 */
void
SelectionSet::DeleteEntries(void)
{
	SelectionEntry* pEntry;
	SelectionEntry* pNext;

	WMSG0("Deleting selection entries\n");

	pEntry = GetEntries();
	while (pEntry != nil) {
		pNext = pEntry->GetNext();
		delete pEntry;
		pEntry = pNext;
	}
}

/*
 * Count the #of entries whose display name matches the prefix string.
 */
int
SelectionSet::CountMatchingPrefix(const char* prefix)
{
	SelectionEntry* pEntry;
	int count = 0;
	int len = strlen(prefix);
	ASSERT(len > 0);

	pEntry = GetEntries();
	while (pEntry != nil) {
		GenericEntry* pGeneric = pEntry->GetEntry();

		if (strncasecmp(prefix, pGeneric->GetDisplayName(), len) == 0)
			count++;
		pEntry = pEntry->GetNext();
	}

	return count;
}

/*
 * Dump the contents of a selection set.
 */
void
SelectionSet::Dump(void)
{
	const SelectionEntry* pEntry;

	WMSG1("SelectionSet: %d entries\n", fNumEntries);

	pEntry = fEntryHead;
	while (pEntry != nil) {
		WMSG1("  : name='%s'\n", pEntry->GetEntry()->GetPathName());
		pEntry = pEntry->GetNext();
	}
}
