mirror of
https://github.com/fadden/ciderpress.git
synced 2024-12-22 20:29:51 +00:00
bb24f51ccb
We weren't doing a MOR-to-UNI conversion on the sub-volume name, so HFS volumes with non-ASCII characters didn't look right. This also relocates the character-conversion code to a new source file. It's currently part of the reformat lib, though it arguably belongs in util (but that would introduce a new dependency between reformat and util).
1192 lines
36 KiB
C++
1192 lines
36 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 "NufxArchive.h"
|
|
#include "FileNameConv.h"
|
|
#include "ContentList.h"
|
|
#include "../reformat/ReformatBase.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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
GenericEntry::GenericEntry(void)
|
|
: fFssep('\0'),
|
|
fFileType(0),
|
|
fAuxType(0),
|
|
fAccess(0),
|
|
fCreateWhen(kDateNone),
|
|
fModWhen(kDateNone),
|
|
fRecordKind(kRecordKindUnknown),
|
|
fFormatStr(L"Unknown"),
|
|
fDataForkLen(0),
|
|
fRsrcForkLen(0),
|
|
fCompressedLen(0),
|
|
fSourceFS(DiskImg::kFormatUnknown),
|
|
fHasDataFork(false),
|
|
fHasRsrcFork(false),
|
|
fHasDiskImage(false),
|
|
fHasComment(false),
|
|
fHasNonEmptyComment(false),
|
|
fDamaged(false),
|
|
fSuspicious(false),
|
|
fIndex(-1),
|
|
fpPrev(NULL),
|
|
fpNext(NULL)
|
|
{
|
|
}
|
|
|
|
GenericEntry::~GenericEntry(void) {}
|
|
|
|
void GenericEntry::SetPathNameMOR(const char* path)
|
|
{
|
|
ASSERT(path != NULL && strlen(path) > 0);
|
|
fPathNameMOR = path;
|
|
// nuke the derived fields
|
|
fFileName = L"";
|
|
fFileNameExtension = L"";
|
|
fDisplayName = L"";
|
|
|
|
/*
|
|
* Generate the Unicode representation from the Mac OS Roman source.
|
|
* For now, we just treat the input as CP-1252.
|
|
*
|
|
* TODO(Unicode)
|
|
*/
|
|
fPathNameUNI = fPathNameMOR;
|
|
|
|
/*
|
|
* 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(&fPathNameMOR);
|
|
}
|
|
}
|
|
|
|
const CString& GenericEntry::GetFileName(void)
|
|
{
|
|
ASSERT(!fPathNameMOR.IsEmpty());
|
|
if (fFileName.IsEmpty()) {
|
|
fFileName = PathName::FilenameOnly(fPathNameUNI, fFssep);
|
|
}
|
|
return fFileName;
|
|
}
|
|
|
|
const CString& GenericEntry::GetFileNameExtension(void)
|
|
{
|
|
ASSERT(!fPathNameMOR.IsEmpty());
|
|
if (fFileNameExtension.IsEmpty()) {
|
|
fFileNameExtension = PathName::FindExtension(fPathNameUNI, fFssep);
|
|
}
|
|
return fFileNameExtension;
|
|
}
|
|
|
|
const CStringA& GenericEntry::GetFileNameExtensionMOR(void)
|
|
{
|
|
ASSERT(!fPathNameMOR.IsEmpty());
|
|
if (fFileNameExtensionMOR.IsEmpty()) {
|
|
CString str = PathName::FindExtension(fPathNameUNI, fFssep);
|
|
// TODO(Unicode): either get the extension from the MOR filename,
|
|
// or convert this properly from Unicode to MOR (not CP-1252).
|
|
fFileNameExtensionMOR = str;
|
|
}
|
|
return fFileNameExtensionMOR;
|
|
}
|
|
|
|
void GenericEntry::SetSubVolName(const WCHAR* name)
|
|
{
|
|
fSubVolName = name;
|
|
}
|
|
|
|
const CString& GenericEntry::GetDisplayName(void) const
|
|
{
|
|
ASSERT(!fPathNameMOR.IsEmpty());
|
|
if (!fDisplayName.IsEmpty()) {
|
|
return fDisplayName;
|
|
}
|
|
|
|
if (!fSubVolName.IsEmpty()) {
|
|
fDisplayName = fSubVolName + (WCHAR) DiskFS::kDIFssep;
|
|
}
|
|
fDisplayName += Charset::ConvertMORToUNI(fPathNameMOR);
|
|
return fDisplayName;
|
|
}
|
|
|
|
const WCHAR* GenericEntry::GetFileTypeString(void) const
|
|
{
|
|
return PathProposal::FileTypeString(fFileType);
|
|
}
|
|
|
|
/*static*/ void GenericEntry::SpacesToUnderscores(CStringA* pStr)
|
|
{
|
|
pStr->Replace(' ', '_');
|
|
}
|
|
|
|
/*static*/ bool GenericEntry::CheckHighASCII(const uint8_t* buffer,
|
|
size_t count)
|
|
{
|
|
/*
|
|
* (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.
|
|
*/
|
|
bool isHighASCII;
|
|
|
|
ASSERT(buffer != NULL);
|
|
ASSERT(count != 0);
|
|
|
|
isHighASCII = true;
|
|
while (count--) {
|
|
if ((*buffer & 0x80) == 0 && *buffer != 0x20 && *buffer != 0x00) {
|
|
LOGI("Flunking CheckHighASCII on 0x%02x", *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 */
|
|
};
|
|
|
|
static const int kNuMaxUpperASCII = 1; /* max #of binary chars per 100 bytes */
|
|
static const int kMinConvThreshold = 40; /* min of 40 chars for auto-detect */
|
|
static const char kCharLF = '\n';
|
|
static const char kCharCR = '\r';
|
|
|
|
/*static*/ GenericEntry::ConvertEOL GenericEntry::DetermineConversion(
|
|
const uint8_t* buffer, size_t count,
|
|
EOLType* pSourceType, ConvertHighASCII* pConvHA)
|
|
{
|
|
/*
|
|
* 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.
|
|
*/
|
|
ConvertHighASCII wantConvHA = *pConvHA;
|
|
size_t bufCount, numBinary, numLF, numCR;
|
|
bool isHighASCII;
|
|
uint8_t 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);
|
|
LOGI(" +++ Determined isHighASCII=%d", isHighASCII);
|
|
} else {
|
|
isHighASCII = false;
|
|
LOGI(" +++ Not even checking isHighASCII");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/*static*/ int GenericEntry::WriteConvert(FILE* fp, const char* buf, size_t len,
|
|
ConvertEOL* pConv, ConvertHighASCII* pConvHA, bool* pLastCR)
|
|
{
|
|
int err = 0;
|
|
|
|
LOGD("+++ WriteConvert conv=%d convHA=%d", *pConv, *pConvHA);
|
|
|
|
if (len == 0) {
|
|
LOGI("WriteConvert asked to write 0 bytes; returning");
|
|
return err;
|
|
}
|
|
|
|
/* if we're in "auto" mode, scan the input for EOL and high ASCII */
|
|
if (*pConv == kConvertEOLAuto) {
|
|
EOLType sourceType;
|
|
*pConv = DetermineConversion((uint8_t*)buf, len, &sourceType,
|
|
pConvHA);
|
|
if (*pConv == kConvertEOLOn && sourceType == kEOLCRLF) {
|
|
LOGI(" Auto-detected text conversion from CRLF; disabling");
|
|
*pConv = kConvertEOLOff;
|
|
}
|
|
LOGI(" Auto-detected EOL conv=%d ha=%d", *pConv, *pConvHA);
|
|
} else if (*pConvHA == kConvertHAAuto) {
|
|
if (*pConv == kConvertEOLOn) {
|
|
/* definitely converting EOL, test for high ASCII */
|
|
if (CheckHighASCII((uint8_t*)buf, len))
|
|
*pConvHA = kConvertHAOn;
|
|
else
|
|
*pConvHA = kConvertHAOff;
|
|
} else {
|
|
/* not converting EOL, don't convert high ASCII */
|
|
*pConvHA = kConvertHAOff;
|
|
}
|
|
}
|
|
LOGD("+++ After auto, conv=%d convHA=%d", *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;
|
|
LOGE("WriteConvert failed, err=%d", errno);
|
|
}
|
|
} else {
|
|
ASSERT(*pConv == kConvertEOLOn);
|
|
bool lastCR = *pLastCR;
|
|
uint8_t 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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
void GenericArchive::AddEntry(GenericEntry* pEntry)
|
|
{
|
|
if (fEntryHead == NULL) {
|
|
ASSERT(fEntryTail == NULL);
|
|
fEntryHead = pEntry;
|
|
fEntryTail = pEntry;
|
|
ASSERT(pEntry->GetPrev() == NULL);
|
|
ASSERT(pEntry->GetNext() == NULL);
|
|
} else {
|
|
ASSERT(fEntryTail != NULL);
|
|
ASSERT(pEntry->GetPrev() == NULL);
|
|
pEntry->SetPrev(fEntryTail);
|
|
ASSERT(fEntryTail->GetNext() == NULL);
|
|
fEntryTail->SetNext(pEntry);
|
|
fEntryTail = pEntry;
|
|
}
|
|
|
|
fNumEntries++;
|
|
|
|
//if (fEntryIndex != NULL) {
|
|
// LOGI("Resetting fEntryIndex");
|
|
// delete [] fEntryIndex;
|
|
// fEntryIndex = NULL;
|
|
//}
|
|
}
|
|
|
|
void GenericArchive::DeleteEntries(void)
|
|
{
|
|
GenericEntry* pEntry;
|
|
GenericEntry* pNext;
|
|
|
|
LOGI("Deleting %d archive entries", fNumEntries);
|
|
|
|
pEntry = GetEntries();
|
|
while (pEntry != NULL) {
|
|
pNext = pEntry->GetNext();
|
|
delete pEntry;
|
|
pEntry = pNext;
|
|
}
|
|
|
|
//delete [] fEntryIndex;
|
|
fNumEntries = 0;
|
|
fEntryHead = fEntryTail = NULL;
|
|
}
|
|
|
|
#if 0
|
|
/*
|
|
* Create an index for fast access.
|
|
*/
|
|
void
|
|
GenericArchive::CreateIndex(void)
|
|
{
|
|
GenericEntry* pEntry;
|
|
int num;
|
|
|
|
LOGI("Creating entry index (%d entries)", fNumEntries);
|
|
|
|
ASSERT(fNumEntries != 0);
|
|
|
|
fEntryIndex = new GenericEntry*[fNumEntries];
|
|
if (fEntryIndex == NULL)
|
|
return;
|
|
|
|
pEntry = GetEntries();
|
|
num = 0;
|
|
while (pEntry != NULL) {
|
|
fEntryIndex[num] = pEntry;
|
|
pEntry = pEntry->GetNext();
|
|
num++;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/*static*/ CString GenericArchive::GenDerivedTempName(const WCHAR* filename)
|
|
{
|
|
/*
|
|
* 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 const WCHAR kTmpTemplate[] = L"CPtmp_XXXXXX";
|
|
CString mangle(filename);
|
|
int idx, len;
|
|
|
|
ASSERT(filename != NULL);
|
|
|
|
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;
|
|
}
|
|
LOGD("GenDerived: passed '%ls' returned '%ls'", filename, (LPCWSTR) mangle);
|
|
|
|
return mangle;
|
|
}
|
|
|
|
/*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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Much of this was adapted from NuLib2.
|
|
*/
|
|
|
|
/*static*/ void GenericArchive::UNIXTimeToDateTime(const time_t* pWhen,
|
|
NuDateTime* pDateTime)
|
|
{
|
|
struct tm* ptm;
|
|
|
|
ASSERT(pWhen != NULL);
|
|
ASSERT(pDateTime != NULL);
|
|
|
|
ptm = localtime(pWhen);
|
|
if (ptm == NULL) {
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* 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"*.*";
|
|
|
|
Win32dirent* GenericArchive::OpenDir(const WCHAR* name)
|
|
{
|
|
Win32dirent* dir = NULL;
|
|
WCHAR* tmpStr = NULL;
|
|
WCHAR* cp;
|
|
WIN32_FIND_DATA fnd;
|
|
|
|
dir = (Win32dirent*) malloc(sizeof(*dir));
|
|
tmpStr = (WCHAR*) malloc((wcslen(name) + 2 + wcslen(kWildMatchAll)) * sizeof(WCHAR));
|
|
if (dir == NULL || tmpStr == NULL)
|
|
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 = NULL;
|
|
goto bail;
|
|
}
|
|
|
|
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 NULL;
|
|
wcscpy(dir->d_name, fnd.cFileName);
|
|
dir->d_attr = (unsigned char) fnd.dwFileAttributes;
|
|
}
|
|
|
|
return dir;
|
|
}
|
|
|
|
void GenericArchive::CloseDir(Win32dirent* dir)
|
|
{
|
|
if (dir == NULL)
|
|
return;
|
|
|
|
FindClose(dir->d_hFindFile);
|
|
free(dir);
|
|
}
|
|
|
|
NuError GenericArchive::Win32AddDirectory(const AddFilesDialog* pAddOpts,
|
|
const WCHAR* dirName, CString* pErrMsg)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
Win32dirent* dirp = NULL;
|
|
Win32dirent* entry;
|
|
WCHAR nbuf[MAX_PATH]; /* malloc might be better; this soaks stack */
|
|
char fssep;
|
|
int len;
|
|
|
|
ASSERT(pAddOpts != NULL);
|
|
ASSERT(dirName != NULL);
|
|
|
|
LOGI("+++ DESCEND: '%ls'", dirName);
|
|
|
|
dirp = OpenDir(dirName);
|
|
if (dirp == NULL) {
|
|
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)) != NULL) {
|
|
/* 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;
|
|
LOGE("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 != NULL)
|
|
(void)CloseDir(dirp);
|
|
return err;
|
|
}
|
|
|
|
NuError GenericArchive::Win32AddFile(const AddFilesDialog* pAddOpts,
|
|
const WCHAR* pathname, CString* pErrMsg)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
bool exists, isDir, isReadable;
|
|
LocalFileDetails details;
|
|
struct _stat sb;
|
|
|
|
ASSERT(pAddOpts != NULL);
|
|
ASSERT(pathname != NULL);
|
|
|
|
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.
|
|
*/
|
|
LOGD("+++ ADD '%ls'", pathname);
|
|
|
|
/*
|
|
* Fill out the "details" structure.
|
|
*/
|
|
err = details.SetFields(pAddOpts, pathname, &sb);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
|
|
assert(wcscmp(pathname, details.GetLocalPathName()) == 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;
|
|
}
|
|
|
|
NuError GenericArchive::AddFile(const AddFilesDialog* pAddOpts,
|
|
const WCHAR* pathname, CString* pErrMsg)
|
|
{
|
|
*pErrMsg = L"";
|
|
return Win32AddFile(pAddOpts, pathname, pErrMsg);
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* GenericArchive::FileDetails
|
|
* ===========================================================================
|
|
*/
|
|
|
|
GenericArchive::LocalFileDetails::LocalFileDetails(void)
|
|
: fEntryKind(kFileKindUnknown),
|
|
fFileSysFmt(DiskImg::kFormatUnknown),
|
|
fFssep('\0'),
|
|
fFileType(0),
|
|
fExtraType(0),
|
|
fAccess(0),
|
|
fStorageType(0)
|
|
{
|
|
memset(&fCreateWhen, 0, sizeof(fCreateWhen));
|
|
memset(&fModWhen, 0, sizeof(fModWhen));
|
|
memset(&fArchiveWhen, 0, sizeof(fArchiveWhen));
|
|
|
|
// set these for debugging
|
|
memset(&fNuFileDetails, 0xcc, sizeof(fNuFileDetails));
|
|
memset(&fCreateParms, 0xcc, sizeof(&fCreateParms));
|
|
fStoragePathNameMOR = "!INIT!";
|
|
}
|
|
|
|
NuError GenericArchive::LocalFileDetails::SetFields(const AddFilesDialog* pAddOpts,
|
|
const WCHAR* pathname, struct _stat* psb)
|
|
{
|
|
time_t now;
|
|
|
|
ASSERT(pAddOpts != NULL);
|
|
ASSERT(pathname != NULL);
|
|
|
|
/* get adjusted filename, along with any preserved type info */
|
|
PathProposal pathProp;
|
|
pathProp.Init(pathname);
|
|
pathProp.LocalToArchive(pAddOpts);
|
|
|
|
/* set up the local and archived pathnames */
|
|
fLocalPathName = pathname;
|
|
fStrippedLocalPathName = L"";
|
|
if (!pAddOpts->fStoragePrefix.IsEmpty()) {
|
|
fStrippedLocalPathName += pAddOpts->fStoragePrefix;
|
|
fStrippedLocalPathName += pathProp.fStoredFssep;
|
|
}
|
|
fStrippedLocalPathName += pathProp.fStoredPathName;
|
|
GenerateStoragePathName();
|
|
|
|
fFileSysFmt = DiskImg::kFormatUnknown;
|
|
fStorageType = kNuStorageUnknown; /* let NufxLib et.al. worry about it */
|
|
if (psb->st_mode & S_IWUSR)
|
|
fAccess = kNuAccessUnlocked;
|
|
else
|
|
fAccess = kNuAccessLocked;
|
|
fEntryKind = LocalFileDetails::kFileKindDataFork;
|
|
fFssep = pathProp.fStoredFssep;
|
|
fFileType = pathProp.fFileType;
|
|
fExtraType = pathProp.fAuxType;
|
|
|
|
#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(NULL);
|
|
UNIXTimeToDateTime(&now, &fArchiveWhen);
|
|
UNIXTimeToDateTime(&psb->st_mtime, &fModWhen);
|
|
UNIXTimeToDateTime(&psb->st_ctime, &fCreateWhen);
|
|
|
|
switch (pathProp.fThreadKind) {
|
|
case GenericEntry::kDataThread:
|
|
//pDetails->threadID = kNuThreadIDDataFork;
|
|
fEntryKind = LocalFileDetails::kFileKindDataFork;
|
|
break;
|
|
case GenericEntry::kRsrcThread:
|
|
//pDetails->threadID = kNuThreadIDRsrcFork;
|
|
fEntryKind = LocalFileDetails::kFileKindRsrcFork;
|
|
break;
|
|
case GenericEntry::kDiskImageThread:
|
|
//pDetails->threadID = kNuThreadIDDiskImage;
|
|
fEntryKind = LocalFileDetails::kFileKindDiskImage;
|
|
break;
|
|
default:
|
|
ASSERT(false);
|
|
// was initialized to default earlier
|
|
break;
|
|
}
|
|
|
|
/*bail:*/
|
|
return kNuErrNone;
|
|
}
|
|
|
|
const NuFileDetails& GenericArchive::LocalFileDetails::GetNuFileDetails()
|
|
{
|
|
//details.threadID = threadID;
|
|
switch (fEntryKind) {
|
|
case kFileKindDataFork:
|
|
fNuFileDetails.threadID = kNuThreadIDDataFork;
|
|
break;
|
|
case kFileKindBothForks: // not exactly supported, doesn't really matter
|
|
case kFileKindRsrcFork:
|
|
fNuFileDetails.threadID = kNuThreadIDRsrcFork;
|
|
break;
|
|
case kFileKindDiskImage:
|
|
fNuFileDetails.threadID = kNuThreadIDDiskImage;
|
|
break;
|
|
case kFileKindDirectory:
|
|
default:
|
|
LOGW("Invalid entryKind (%d) for NuFileDetails conversion", fEntryKind);
|
|
ASSERT(false);
|
|
fNuFileDetails.threadID = 0; // that makes it an old-style comment?!
|
|
break;
|
|
}
|
|
|
|
fNuFileDetails.origName = (LPCWSTR) fLocalPathName;
|
|
fNuFileDetails.storageNameMOR = (LPCSTR) fStoragePathNameMOR;
|
|
fNuFileDetails.fileSysInfo = fFssep;
|
|
fNuFileDetails.access = fAccess;
|
|
fNuFileDetails.fileType = fFileType;
|
|
fNuFileDetails.extraType = fExtraType;
|
|
fNuFileDetails.storageType = fStorageType;
|
|
fNuFileDetails.createWhen = fCreateWhen;
|
|
fNuFileDetails.modWhen = fModWhen;
|
|
fNuFileDetails.archiveWhen = fArchiveWhen;
|
|
|
|
switch (fFileSysFmt) {
|
|
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 */
|
|
fNuFileDetails.fileSysID = (enum NuFileSysID) fFileSysFmt;
|
|
break;
|
|
|
|
case DiskImg::kFormatRDOS33:
|
|
case DiskImg::kFormatRDOS32:
|
|
case DiskImg::kFormatRDOS3:
|
|
/* these look like DOS33, e.g. text is high-ASCII */
|
|
fNuFileDetails.fileSysID = kNuFileSysDOS33;
|
|
break;
|
|
|
|
default:
|
|
fNuFileDetails.fileSysID = kNuFileSysUnknown;
|
|
break;
|
|
}
|
|
|
|
return fNuFileDetails;
|
|
}
|
|
|
|
const DiskFS::CreateParms& GenericArchive::LocalFileDetails::GetCreateParms()
|
|
{
|
|
fCreateParms.pathName = (LPCSTR) fStoragePathNameMOR;
|
|
fCreateParms.fssep = fFssep;
|
|
fCreateParms.storageType = fStorageType;
|
|
fCreateParms.fileType = fFileType;
|
|
fCreateParms.auxType = fExtraType;
|
|
fCreateParms.access = fAccess;
|
|
fCreateParms.createWhen = NufxArchive::DateTimeToSeconds(&fCreateWhen);
|
|
fCreateParms.modWhen = NufxArchive::DateTimeToSeconds(&fModWhen);
|
|
return fCreateParms;
|
|
}
|
|
|
|
void GenericArchive::LocalFileDetails::GenerateStoragePathName()
|
|
{
|
|
// TODO(Unicode): generate MOR name from Unicode, instead of just
|
|
// doing a generic CP-1252 conversion. We need to do this on both
|
|
// sides though, so until we can extract MOR->Unicode we don't
|
|
// want to add Unicode->MOR. For this all to work well we need NufxLib
|
|
// and DiskImgLib to be able to handle UTF-16 filenames.
|
|
fStoragePathNameMOR = fStrippedLocalPathName;
|
|
}
|
|
|
|
|
|
/*static*/ void GenericArchive::LocalFileDetails::CopyFields(LocalFileDetails* pDst,
|
|
const LocalFileDetails* pSrc)
|
|
{
|
|
// don't copy fNuFileDetails, fCreateParms
|
|
pDst->fEntryKind = pSrc->fEntryKind;
|
|
pDst->fLocalPathName = pSrc->fLocalPathName;
|
|
pDst->fStrippedLocalPathName = pSrc->fStrippedLocalPathName;
|
|
pDst->fStoragePathNameMOR = pSrc->fStoragePathNameMOR;
|
|
pDst->fFileSysFmt = pSrc->fFileSysFmt;
|
|
pDst->fFssep = pSrc->fFssep;
|
|
pDst->fAccess = pSrc->fAccess;
|
|
pDst->fFileType = pSrc->fFileType;
|
|
pDst->fExtraType = pSrc->fExtraType;
|
|
pDst->fStorageType = pSrc->fStorageType;
|
|
pDst->fCreateWhen = pSrc->fCreateWhen;
|
|
pDst->fModWhen = pSrc->fModWhen;
|
|
pDst->fArchiveWhen = pSrc->fArchiveWhen;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* SelectionSet
|
|
* ===========================================================================
|
|
*/
|
|
|
|
void SelectionSet::CreateFromSelection(ContentList* pContentList, int threadMask)
|
|
{
|
|
/*
|
|
* 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.
|
|
*/
|
|
LOGD("CreateFromSelection (threadMask=0x%02x)", threadMask);
|
|
|
|
POSITION posn;
|
|
posn = pContentList->GetFirstSelectedItemPosition();
|
|
ASSERT(posn != NULL);
|
|
if (posn == NULL)
|
|
return;
|
|
while (posn != NULL) {
|
|
int num = pContentList->GetNextSelectedItem(/*ref*/ posn);
|
|
GenericEntry* pEntry = (GenericEntry*) pContentList->GetItemData(num);
|
|
|
|
AddToSet(pEntry, threadMask);
|
|
}
|
|
}
|
|
|
|
void SelectionSet::CreateFromAll(ContentList* pContentList, int threadMask)
|
|
{
|
|
LOGD("CreateFromAll (threadMask=0x%02x)", threadMask);
|
|
|
|
int count = pContentList->GetItemCount();
|
|
for (int idx = 0; idx < count; idx++) {
|
|
GenericEntry* pEntry = (GenericEntry*) pContentList->GetItemData(idx);
|
|
|
|
AddToSet(pEntry, threadMask);
|
|
}
|
|
}
|
|
|
|
void SelectionSet::AddToSet(GenericEntry* pEntry, int threadMask)
|
|
{
|
|
SelectionEntry* pSelEntry;
|
|
|
|
LOGV(" Sel '%ls'", (LPCWSTR) pEntry->GetPathNameUNI());
|
|
|
|
if (!(threadMask & GenericEntry::kAllowVolumeDir) &&
|
|
pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir)
|
|
{
|
|
/* only include volume dir if specifically requested */
|
|
//LOGI(" Excluding volume dir '%ls' from set", pEntry->GetPathName());
|
|
return;
|
|
}
|
|
|
|
if (!(threadMask & GenericEntry::kAllowDirectory) &&
|
|
pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory)
|
|
{
|
|
/* only include directories if specifically requested */
|
|
//LOGI(" Excluding folder '%ls' from set", 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);
|
|
}
|
|
}
|
|
|
|
void SelectionSet::AddEntry(SelectionEntry* pEntry)
|
|
{
|
|
if (fEntryHead == NULL) {
|
|
ASSERT(fEntryTail == NULL);
|
|
fEntryHead = pEntry;
|
|
fEntryTail = pEntry;
|
|
ASSERT(pEntry->GetPrev() == NULL);
|
|
ASSERT(pEntry->GetNext() == NULL);
|
|
} else {
|
|
ASSERT(fEntryTail != NULL);
|
|
ASSERT(pEntry->GetPrev() == NULL);
|
|
pEntry->SetPrev(fEntryTail);
|
|
ASSERT(fEntryTail->GetNext() == NULL);
|
|
fEntryTail->SetNext(pEntry);
|
|
fEntryTail = pEntry;
|
|
}
|
|
|
|
fNumEntries++;
|
|
}
|
|
|
|
void SelectionSet::DeleteEntries(void)
|
|
{
|
|
SelectionEntry* pEntry;
|
|
SelectionEntry* pNext;
|
|
|
|
LOGD("Deleting selection entries");
|
|
|
|
pEntry = GetEntries();
|
|
while (pEntry != NULL) {
|
|
pNext = pEntry->GetNext();
|
|
delete pEntry;
|
|
pEntry = pNext;
|
|
}
|
|
}
|
|
|
|
int SelectionSet::CountMatchingPrefix(const WCHAR* prefix)
|
|
{
|
|
SelectionEntry* pEntry;
|
|
int count = 0;
|
|
int len = wcslen(prefix);
|
|
ASSERT(len > 0);
|
|
|
|
pEntry = GetEntries();
|
|
while (pEntry != NULL) {
|
|
GenericEntry* pGeneric = pEntry->GetEntry();
|
|
|
|
if (wcsnicmp(prefix, pGeneric->GetDisplayName(), len) == 0)
|
|
count++;
|
|
pEntry = pEntry->GetNext();
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void SelectionSet::Dump(void)
|
|
{
|
|
const SelectionEntry* pEntry;
|
|
|
|
LOGI("SelectionSet: %d entries", fNumEntries);
|
|
|
|
pEntry = fEntryHead;
|
|
while (pEntry != NULL) {
|
|
LOGI(" : name='%ls'", (LPCWSTR) pEntry->GetEntry()->GetPathNameUNI());
|
|
pEntry = pEntry->GetNext();
|
|
}
|
|
}
|