ciderpress/app/GenericArchive.cpp

1365 lines
40 KiB
C++

/*
* 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 = L"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 WCHAR* path)
{
ASSERT(path != nil && wcslen(path) > 0);
if (fPathName != nil)
delete fPathName;
fPathName = wcsdup(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 WCHAR*
GenericEntry::GetFileName(void)
{
ASSERT(fPathName != nil);
if (fFileName == nil)
fFileName = PathName::FilenameOnly(fPathName, fFssep);
return fFileName;
}
const WCHAR*
GenericEntry::GetFileNameExtension(void)
{
ASSERT(fPathName != nil);
if (fFileNameExtension == nil)
fFileNameExtension = PathName::FindExtension(fPathName, fFssep);
return fFileNameExtension;
}
CStringA
GenericEntry::GetFileNameExtensionA(void)
{
return GetFileNameExtension();
}
void
GenericEntry::SetSubVolName(const WCHAR* name)
{
delete[] fSubVolName;
fSubVolName = nil;
if (name != nil) {
fSubVolName = wcsdup(name);
}
}
const WCHAR*
GenericEntry::GetDisplayName(void) const
{
ASSERT(fPathName != nil);
if (fDisplayName != nil)
return fDisplayName;
// TODO: hmm...
GenericEntry* pThis = const_cast<GenericEntry*>(this);
int len = wcslen(fPathName) +1;
if (fSubVolName != nil)
len += wcslen(fSubVolName) +1;
pThis->fDisplayName = new WCHAR[len];
if (fSubVolName != nil) {
WCHAR xtra[2] = { DiskFS::kDIFssep, '\0' };
wcscpy(pThis->fDisplayName, fSubVolName);
wcscat(pThis->fDisplayName, xtra);
} else {
pThis->fDisplayName[0] = '\0';
}
wcscat(pThis->fDisplayName, fPathName);
return pThis->fDisplayName;
}
/*
* Get a string for this entry's filetype.
*/
const WCHAR*
GenericEntry::GetFileTypeString(void) const
{
return PathProposal::FileTypeString(fFileType);
}
/*
* Convert spaces to underscores.
*/
/*static*/ void
GenericEntry::SpacesToUnderscores(WCHAR* 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 WCHAR* filename)
{
static const WCHAR kTmpTemplate[] = L"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 '%ls' returned '%ls'\n", filename, (LPCWSTR) 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 WCHAR* cp1 = name1;
const WCHAR* 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 WCHAR* 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: %ls\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;
WCHAR d_name[MAX_PATH];
int d_first;
HANDLE d_hFindFile;
} Win32dirent;
static const WCHAR kWildMatchAll[] = L"*.*";
/*
* Prepare a directory for reading.
*
* Allocates a Win32dirent struct that must be freed by the caller.
*/
Win32dirent*
GenericArchive::OpenDir(const WCHAR* name)
{
Win32dirent* dir = nil;
WCHAR* tmpStr = nil;
WCHAR* cp;
WIN32_FIND_DATA fnd;
dir = (Win32dirent*) malloc(sizeof(*dir));
tmpStr = (WCHAR*) malloc((wcslen(name) + 2 + wcslen(kWildMatchAll)) * sizeof(WCHAR));
if (dir == nil || tmpStr == nil)
goto failed;
wcscpy(tmpStr, name);
cp = tmpStr + wcslen(tmpStr);
/* don't end in a colon (e.g. "C:") */
if ((cp - tmpStr) > 0 && wcsrchr(tmpStr, ':') == (cp - 1))
*cp++ = '.';
/* must end in a slash */
if ((cp - tmpStr) > 0 &&
wcsrchr(tmpStr, PathProposal::kLocalFssep) != (cp - 1))
*cp++ = PathProposal::kLocalFssep;
wcscpy(cp, kWildMatchAll);
dir->d_hFindFile = FindFirstFile(tmpStr, &fnd);
if (dir->d_hFindFile == INVALID_HANDLE_VALUE)
goto failed;
wcscpy(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;
wcscpy(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);
}
/*
* 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 WCHAR* dirName, CString* pErrMsg)
{
NuError err = kNuErrNone;
Win32dirent* dirp = nil;
Win32dirent* entry;
WCHAR nbuf[MAX_PATH]; /* malloc might be better; this soaks stack */
char fssep;
int len;
ASSERT(pAddOpts != nil);
ASSERT(dirName != nil);
WMSG1("+++ DESCEND: '%ls'\n", dirName);
dirp = OpenDir(dirName);
if (dirp == nil) {
if (errno == ENOTDIR)
err = kNuErrNotDir;
else
err = errno ? (NuError)errno : kNuErrOpenDir;
pErrMsg->Format(L"Failed on '%ls': %hs.", 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 (wcscmp(entry->d_name, L".") == 0 ||
wcscmp(entry->d_name, L"..") == 0)
{
continue;
}
len = wcslen(dirName);
if (len + wcslen(entry->d_name) +2 > MAX_PATH) {
err = kNuErrInternal;
WMSG4("ERROR: Filename exceeds %d bytes: %ls%c%ls",
MAX_PATH, dirName, fssep, entry->d_name);
goto bail;
}
/* form the new name, inserting an fssep if needed */
wcscpy(nbuf, dirName);
if (dirName[len-1] != fssep)
nbuf[len++] = fssep;
wcscpy(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 WCHAR* 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(L"Unexpected error while examining '%ls': %hs.",
pathname, NuStrError((NuError) ierr));
goto bail;
}
if (!exists) {
err = kNuErrFileNotFound;
pErrMsg->Format(L"Couldn't find '%ls'", pathname);
goto bail;
}
if (!isReadable) {
err = kNuErrFileNotReadable;
pErrMsg->Format(L"File '%ls' 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 '%ls'\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(wcscmp(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(L"Unable to add file '%ls': %hs.",
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 WCHAR* 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;
}
// TODO(xyzzy): need narrow-string versions of origName and storageName
// (probably need to re-think this automatic-cast-conversion stuff)
details.origName = "XYZZY-GenericArchive1"; // origName;
details.storageName = "XYZZY-GenericArchive2"; // storageName;
//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 '%ls'\n", pEntry->GetPathName());
if (!(threadMask & GenericEntry::kAllowVolumeDir) &&
pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir)
{
/* only include volume dir if specifically requested */
//WMSG1(" Excluding volume dir '%ls' from set\n", pEntry->GetPathName());
return;
}
if (!(threadMask & GenericEntry::kAllowDirectory) &&
pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory)
{
/* only include directories if specifically requested */
//WMSG1(" Excluding folder '%ls' 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 WCHAR* prefix)
{
SelectionEntry* pEntry;
int count = 0;
int len = wcslen(prefix);
ASSERT(len > 0);
pEntry = GetEntries();
while (pEntry != nil) {
GenericEntry* pGeneric = pEntry->GetEntry();
if (wcsnicmp(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='%ls'\n", pEntry->GetEntry()->GetPathName());
pEntry = pEntry->GetNext();
}
}