mirror of
https://github.com/fadden/nulib2.git
synced 2024-12-28 09:29:16 +00:00
e2088e64d3
NufxLib has historically made no effort to distinguish between the character set used for filenames on the local disk, and for filenames stored within the archive. Now all Unicode filename strings use the UNICHAR type and have "UNI" in the name, and all Mac OS Roman strings have "MOR" in the name. (The naming convention makes it obvious when you're assigning the wrong thing; on Linux both formats are char*, so the compiler won't tell you if you get it wrong.) The distinction is necessary because filesystems generally support Unicode these days, but on Windows you need to use a separate set of wide-character file I/O functions. (On Linux it all works with "narrow" strings, and the UTF-8 encoding is interpreted by applications.) The character set used for NuFX archive filenames is MOR, matching what GS/OS + HFS supported, and we want to be able to convert back and forth between MOR and a Unicode representation. This change updates the various character types and string names, adds conversion functions, and updates NuLib2 for proper execution on Linux. It does not include the (probably extensive) changes required for Windows UTF-16 support. Instead, the conversion functions are no-ops, which should result in NuLib2 for Windows continuing to behave in the same slightly broken way. This adds "test-names", which exercises Unicode filenames a bit. It will not pass on Win32. Also, tweaked the Linux makefiles to have explicit dependencies, rather than empty space and an expectation that "makedepend" exists. Also, minor source code cleanups. While this probably doesn't affect binary compatibility -- it's mainly a matter of naming and string interpretation -- there's enough going on that it should be considered an API revision, so this updates the version to 3.0.0.
905 lines
26 KiB
C
905 lines
26 KiB
C
/*
|
|
* NuFX archive manipulation library
|
|
* Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved.
|
|
* This is free software; you can redistribute it and/or modify it under the
|
|
* terms of the BSD License, see the file COPYING-LIB.
|
|
*
|
|
* Implementation of NuFunnel, NuStraw and ProgressUpdater.
|
|
*/
|
|
#include "NufxLibPriv.h"
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Progress updater
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Initialize the fields in a ProgressData structure, prior to compressing
|
|
* data into a record.
|
|
*
|
|
* The same structure will be used when expanding all threads in a given
|
|
* record.
|
|
*/
|
|
NuError Nu_ProgressDataInit_Compress(NuArchive* pArchive,
|
|
NuProgressData* pProgressData, const NuRecord* pRecord,
|
|
const UNICHAR* origPathnameUNI, const UNICHAR* pathnameUNI)
|
|
{
|
|
const char* cp;
|
|
|
|
Assert(pProgressData != NULL);
|
|
Assert(pArchive != NULL);
|
|
Assert(pRecord != NULL);
|
|
Assert(origPathnameUNI != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
|
|
pProgressData->pRecord = pRecord;
|
|
|
|
pProgressData->origPathnameUNI = origPathnameUNI;
|
|
pProgressData->pathnameUNI = pathnameUNI;
|
|
cp = strrchr(pathnameUNI, NuGetSepFromSysInfo(pRecord->recFileSysInfo));
|
|
if (cp == NULL || *(cp+1) == '\0')
|
|
pProgressData->filenameUNI = pProgressData->pathnameUNI;
|
|
else
|
|
pProgressData->filenameUNI = cp+1;
|
|
|
|
pProgressData->operation = kNuOpAdd;
|
|
pProgressData->state = kNuProgressPreparing;
|
|
pProgressData->uncompressedLength = 0;
|
|
pProgressData->uncompressedProgress = 0;
|
|
|
|
pProgressData->compress.threadFormat = (NuThreadFormat)-1;
|
|
|
|
/* ya know... if this is NULL, none of the above matters much */
|
|
pProgressData->progressFunc = pArchive->progressUpdaterFunc;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Initialize the fields in a ProgressData structure, prior to expanding
|
|
* data from a record.
|
|
*
|
|
* The same structure will be used when expanding all threads in a given
|
|
* record.
|
|
*/
|
|
NuError Nu_ProgressDataInit_Expand(NuArchive* pArchive,
|
|
NuProgressData* pProgressData, const NuRecord* pRecord,
|
|
const UNICHAR* newPathnameUNI, UNICHAR newFssep,
|
|
const UNICHAR* origPathnameUNI, NuValue convertEOL)
|
|
{
|
|
const NuThread* pThreadIter;
|
|
const char* cp;
|
|
int i;
|
|
|
|
Assert(pProgressData != NULL);
|
|
Assert(pArchive != NULL);
|
|
Assert(pRecord != NULL);
|
|
Assert(newPathnameUNI != NULL);
|
|
Assert(origPathnameUNI != NULL);
|
|
Assert(newFssep != 0);
|
|
|
|
pProgressData->pRecord = pRecord;
|
|
pProgressData->expand.pThread = NULL;
|
|
|
|
pProgressData->origPathnameUNI = origPathnameUNI;
|
|
pProgressData->pathnameUNI = newPathnameUNI;
|
|
cp = strrchr(newPathnameUNI, newFssep);
|
|
if (cp == NULL || *(cp+1) == '\0')
|
|
pProgressData->filenameUNI = newPathnameUNI;
|
|
else
|
|
pProgressData->filenameUNI = cp+1;
|
|
|
|
pProgressData->expand.convertEOL = convertEOL;
|
|
|
|
/* total up the data threads */
|
|
pProgressData->expand.totalCompressedLength = 0;
|
|
pProgressData->expand.totalUncompressedLength = 0;
|
|
|
|
for (i = 0; i < (int)pRecord->recTotalThreads; i++) {
|
|
pThreadIter = Nu_GetThread(pRecord, i);
|
|
if (pThreadIter->thThreadClass != kNuThreadClassData)
|
|
continue;
|
|
pProgressData->expand.totalCompressedLength += pThreadIter->thCompThreadEOF;
|
|
pProgressData->expand.totalUncompressedLength += pThreadIter->actualThreadEOF;
|
|
}
|
|
|
|
pProgressData->operation = kNuOpExtract;
|
|
if (pArchive->testMode)
|
|
pProgressData->operation = kNuOpTest;
|
|
pProgressData->state = kNuProgressPreparing;
|
|
pProgressData->uncompressedLength = 0;
|
|
pProgressData->uncompressedProgress = 0;
|
|
|
|
/* ya know... if this is NULL, none of the above matters much */
|
|
pProgressData->progressFunc = pArchive->progressUpdaterFunc;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Do the setup on a ProgressData prior to compressing a thread.
|
|
*/
|
|
NuError Nu_ProgressDataCompressPrep(NuArchive* pArchive, NuStraw* pStraw,
|
|
NuThreadFormat threadFormat, uint32_t sourceLen)
|
|
{
|
|
NuProgressData* pProgressData;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pStraw != NULL);
|
|
Assert(sourceLen < 32767*65536);
|
|
|
|
pProgressData = pStraw->pProgress;
|
|
if (pProgressData == NULL)
|
|
return kNuErrNone;
|
|
|
|
pProgressData->uncompressedLength = sourceLen;
|
|
pProgressData->compress.threadFormat = threadFormat;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* Do the setup on a ProgressData prior to expanding a thread.
|
|
*
|
|
* "pThread" is the thread being expanded.
|
|
*/
|
|
NuError Nu_ProgressDataExpandPrep(NuArchive* pArchive, NuFunnel* pFunnel,
|
|
const NuThread* pThread)
|
|
{
|
|
NuProgressData* pProgressData;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pFunnel != NULL);
|
|
Assert(pThread != NULL);
|
|
|
|
pProgressData = pFunnel->pProgress;
|
|
if (pProgressData == NULL)
|
|
return kNuErrNone;
|
|
|
|
/*pProgressData->compressedLength = pThread->thCompThreadEOF;*/
|
|
pProgressData->uncompressedLength = pThread->actualThreadEOF;
|
|
pProgressData->expand.pThread = pThread;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* Compute a completion percentage.
|
|
*/
|
|
static int Nu_ComputePercent(uint32_t total, uint32_t progress)
|
|
{
|
|
uint32_t perc;
|
|
|
|
if (!total)
|
|
return 0;
|
|
|
|
if (total < 21474836) {
|
|
perc = (progress * 100 + 50) / total;
|
|
if (perc > 100)
|
|
perc = 100;
|
|
} else {
|
|
perc = progress / (total / 100);
|
|
if (perc > 100)
|
|
perc = 100;
|
|
}
|
|
|
|
return (int) perc;
|
|
}
|
|
|
|
/*
|
|
* Send the initial progress message, before the output file is opened
|
|
* (when extracting) or the input file is opened (when adding).
|
|
*/
|
|
NuError Nu_SendInitialProgress(NuArchive* pArchive, NuProgressData* pProgress)
|
|
{
|
|
NuResult result;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pProgress != NULL);
|
|
|
|
if (pProgress->progressFunc == NULL)
|
|
return kNuErrNone;
|
|
|
|
pProgress->percentComplete = Nu_ComputePercent(
|
|
pProgress->uncompressedLength, pProgress->uncompressedProgress);
|
|
|
|
result = (*pProgress->progressFunc)(pArchive, (NuProgressData*) pProgress);
|
|
|
|
if (result == kNuSkip)
|
|
return kNuErrSkipped; /* [dunno how well this works] */
|
|
if (result == kNuAbort)
|
|
return kNuErrAborted;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* NuFunnel object
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Allocate and initialize a Funnel.
|
|
*/
|
|
NuError Nu_FunnelNew(NuArchive* pArchive, NuDataSink* pDataSink,
|
|
NuValue convertEOL, NuValue convertEOLTo, NuProgressData* pProgress,
|
|
NuFunnel** ppFunnel)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuFunnel* pFunnel = NULL;
|
|
|
|
Assert(ppFunnel != NULL);
|
|
Assert(pDataSink != NULL);
|
|
Assert(convertEOL == kNuConvertOff ||
|
|
convertEOL == kNuConvertOn ||
|
|
convertEOL == kNuConvertAuto);
|
|
|
|
pFunnel = Nu_Calloc(pArchive, sizeof(*pFunnel));
|
|
BailAlloc(pFunnel);
|
|
pFunnel->buffer = Nu_Malloc(pArchive, kNuFunnelBufSize);
|
|
BailAlloc(pFunnel->buffer);
|
|
|
|
pFunnel->pDataSink = pDataSink;
|
|
pFunnel->convertEOL = convertEOL;
|
|
pFunnel->convertEOLTo = convertEOLTo;
|
|
pFunnel->convertEOLFrom = kNuEOLUnknown;
|
|
pFunnel->pProgress = pProgress;
|
|
|
|
pFunnel->checkStripHighASCII = (pArchive->valStripHighASCII != 0);
|
|
pFunnel->doStripHighASCII = false; /* determined on first write */
|
|
|
|
pFunnel->isFirstWrite = true;
|
|
|
|
bail:
|
|
if (err != kNuErrNone)
|
|
Nu_FunnelFree(pArchive, pFunnel);
|
|
else
|
|
*ppFunnel = pFunnel;
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Free a Funnel.
|
|
*
|
|
* The data should already have been written; it's not the duty of a
|
|
* "free" function to flush data out.
|
|
*/
|
|
NuError Nu_FunnelFree(NuArchive* pArchive, NuFunnel* pFunnel)
|
|
{
|
|
if (pFunnel == NULL)
|
|
return kNuErrNone;
|
|
|
|
#ifdef DEBUG_MSGS
|
|
if (pFunnel->bufCount)
|
|
Nu_ReportError(NU_BLOB_DEBUG, kNuErrNone,
|
|
"freeing non-empty funnel");
|
|
#endif
|
|
|
|
Nu_Free(pArchive, pFunnel->buffer);
|
|
Nu_Free(pArchive, pFunnel);
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
#if 0
|
|
/*
|
|
* Set the maximum amount of output we're willing to push through the
|
|
* funnel. Attempts to write more than this many bytes will fail. This
|
|
* allows us to bail out as soon as it's apparent that compression is
|
|
* failing and is actually resulting in a larger file.
|
|
*/
|
|
void Nu_FunnelSetMaxOutput(NuFunnel* pFunnel, uint32_t maxBytes)
|
|
{
|
|
Assert(pFunnel != NULL);
|
|
Assert(maxBytes > 0);
|
|
|
|
pFunnel->outMax = maxBytes;
|
|
if (pFunnel->outCount >= pFunnel->outMax)
|
|
pFunnel->outMaxExceeded = true;
|
|
else
|
|
pFunnel->outMaxExceeded = false;
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
* Check to see if this is a high-ASCII file. To qualify, EVERY
|
|
* character must have its high bit set, except for spaces (0x20).
|
|
* (The exception is courtesy Glen Bredon's "Merlin".)
|
|
*/
|
|
static Boolean Nu_CheckHighASCII(const NuFunnel* pFunnel, const uint8_t* buffer,
|
|
uint32_t count)
|
|
{
|
|
Boolean isHighASCII;
|
|
|
|
Assert(buffer != NULL);
|
|
Assert(count != 0);
|
|
Assert(pFunnel->checkStripHighASCII);
|
|
|
|
isHighASCII = true;
|
|
while (count--) {
|
|
if ((*buffer & 0x80) == 0 && *buffer != 0x20) {
|
|
isHighASCII = false;
|
|
break;
|
|
}
|
|
|
|
buffer++;
|
|
}
|
|
|
|
return isHighASCII;
|
|
}
|
|
|
|
/*
|
|
* 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 gNuIsBinary[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 kNuMinConvThreshold 40 /* min of 40 chars for auto-detect */
|
|
/*
|
|
* 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 enable a "high-ASCII" stripper if requested. This is
|
|
* only enabled when EOL conversions are enabled.
|
|
*
|
|
* Returns kConvEOLOff or kConvEOLOn, and sets pFunnel->doStripHighASCII
|
|
* if pFunnel->CheckStripHighASCII is set.
|
|
*/
|
|
static NuValue Nu_DetermineConversion(NuFunnel* pFunnel, const uint8_t* buffer,
|
|
uint32_t count)
|
|
{
|
|
uint32_t bufCount, numBinary, numLF, numCR;
|
|
Boolean isHighASCII;
|
|
uint8_t val;
|
|
|
|
if (count < kNuMinConvThreshold)
|
|
return kNuConvertOff;
|
|
|
|
/*
|
|
* 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 (pFunnel->checkStripHighASCII) {
|
|
isHighASCII = Nu_CheckHighASCII(pFunnel, buffer, count);
|
|
DBUG(("+++ determined isHighASCII=%d\n", isHighASCII));
|
|
} else {
|
|
isHighASCII = false;
|
|
DBUG(("+++ not even checking isHighASCII\n"));
|
|
}
|
|
|
|
bufCount = count;
|
|
numBinary = numLF = numCR = 0;
|
|
while (bufCount--) {
|
|
val = *buffer++;
|
|
if (isHighASCII)
|
|
val &= 0x7f;
|
|
if (gNuIsBinary[val])
|
|
numBinary++;
|
|
if (val == kNuCharLF)
|
|
numLF++;
|
|
if (val == kNuCharCR)
|
|
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 kNuConvertOff;
|
|
} else if (numBinary > (count / 100) * kNuMaxUpperASCII)
|
|
return kNuConvertOff;
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
if (isHighASCII) {
|
|
pFunnel->doStripHighASCII = true;
|
|
} else {
|
|
if (numLF && !numCR)
|
|
pFunnel->convertEOLFrom = kNuEOLLF;
|
|
else if (!numLF && numCR)
|
|
pFunnel->convertEOLFrom = kNuEOLCR;
|
|
else if (numLF && numLF == numCR)
|
|
pFunnel->convertEOLFrom = kNuEOLCRLF;
|
|
else
|
|
pFunnel->convertEOLFrom = kNuEOLUnknown;
|
|
}
|
|
|
|
return kNuConvertOn;
|
|
}
|
|
|
|
/*
|
|
* Write a block of data to the appropriate output device. Test for
|
|
* excessive data, and raise "outMaxExceeded" if we overrun.
|
|
*
|
|
* This is either a Funnel function or a DataSink function, depending on
|
|
* your perspective.
|
|
*/
|
|
static inline void Nu_FunnelPutBlock(NuFunnel* pFunnel, const uint8_t* buf,
|
|
uint32_t len)
|
|
{
|
|
Assert(pFunnel != NULL);
|
|
Assert(pFunnel->pDataSink != NULL);
|
|
Assert(buf != NULL);
|
|
Assert(len > 0);
|
|
|
|
#if 0
|
|
if (pFunnel->outMax) {
|
|
if (pFunnel->outMaxExceeded)
|
|
return;
|
|
if (pFunnel->outCount + len > pFunnel->outMax) {
|
|
pFunnel->outMaxExceeded = true;
|
|
return;
|
|
}
|
|
}
|
|
pFunnel->outCount += len;
|
|
#endif
|
|
|
|
Nu_DataSinkPutBlock(pFunnel->pDataSink, buf, len);
|
|
}
|
|
|
|
|
|
/*
|
|
* Output the EOL marker requested for this system.
|
|
*/
|
|
static inline void Nu_PutEOL(NuFunnel* pFunnel)
|
|
{
|
|
uint8_t ch;
|
|
|
|
if (pFunnel->convertEOLTo == kNuEOLCR) {
|
|
ch = kNuCharCR;
|
|
Nu_FunnelPutBlock(pFunnel, &ch, 1);
|
|
} else if (pFunnel->convertEOLTo == kNuEOLLF) {
|
|
ch = kNuCharLF;
|
|
Nu_FunnelPutBlock(pFunnel, &ch, 1);
|
|
} else if (pFunnel->convertEOLTo == kNuEOLCRLF) {
|
|
ch = kNuCharCR;
|
|
Nu_FunnelPutBlock(pFunnel, &ch, 1);
|
|
ch = kNuCharLF;
|
|
Nu_FunnelPutBlock(pFunnel, &ch, 1);
|
|
} else {
|
|
Assert(0);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Write a buffer of data, using the EOL conversion associated with the
|
|
* funnel (if any).
|
|
*
|
|
* When converting to the system's EOL convention, we take anything
|
|
* that looks like an EOL mark and convert it. Doesn't matter if it's
|
|
* CR, LF, or CRLF; all three get converted to whatever the system uses.
|
|
*/
|
|
static NuError Nu_FunnelWriteConvert(NuFunnel* pFunnel, const uint8_t* buffer,
|
|
uint32_t count)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
uint32_t progressCount = count;
|
|
|
|
/*if (pFunnel->outMaxExceeded)
|
|
return kNuErrOutMax;*/
|
|
|
|
if (pFunnel->isFirstWrite) {
|
|
/*
|
|
* This is the first write/flush we've done on this Funnel.
|
|
* Check the data we have buffered to decide whether or not
|
|
* we want to do text conversions.
|
|
*/
|
|
if (pFunnel->convertEOL == kNuConvertAuto) {
|
|
pFunnel->convertEOL = Nu_DetermineConversion(pFunnel, buffer,count);
|
|
DBUG(("+++ DetermineConversion --> %ld / %ld (%d)\n",
|
|
pFunnel->convertEOL, pFunnel->convertEOLFrom,
|
|
pFunnel->doStripHighASCII));
|
|
|
|
if (pFunnel->convertEOLFrom == pFunnel->convertEOLTo) {
|
|
DBUG(("+++ Switching redundant converter off\n"));
|
|
pFunnel->convertEOL = kNuConvertOff;
|
|
}
|
|
/* put it where the progress meter can see it */
|
|
if (pFunnel->pProgress != NULL)
|
|
pFunnel->pProgress->expand.convertEOL = pFunnel->convertEOL;
|
|
} else if (pFunnel->convertEOL == kNuConvertOn) {
|
|
if (pFunnel->checkStripHighASCII) {
|
|
/* assume this part of the buffer is representative */
|
|
pFunnel->doStripHighASCII = Nu_CheckHighASCII(pFunnel,
|
|
buffer, count);
|
|
} else {
|
|
Assert(!pFunnel->doStripHighASCII);
|
|
}
|
|
DBUG(("+++ Converter is on, convHighASCII=%d\n",
|
|
pFunnel->doStripHighASCII));
|
|
}
|
|
}
|
|
Assert(pFunnel->convertEOL != kNuConvertAuto); /* on or off now */
|
|
pFunnel->isFirstWrite = false;
|
|
|
|
if (pFunnel->convertEOL == kNuConvertOff) {
|
|
/* write it straight */
|
|
Nu_FunnelPutBlock(pFunnel, buffer, count);
|
|
} else {
|
|
/* do the EOL conversion and optional high-bit stripping */
|
|
Boolean lastCR = pFunnel->lastCR; /* make local copy */
|
|
uint8_t uch;
|
|
int mask;
|
|
|
|
if (pFunnel->doStripHighASCII)
|
|
mask = 0x7f;
|
|
else
|
|
mask = 0xff;
|
|
|
|
/*
|
|
* We could get a significant speed improvement here by writing
|
|
* non-EOL chars as a larger block instead of single bytes.
|
|
*/
|
|
while (count--) {
|
|
uch = (*buffer) & mask;
|
|
|
|
if (uch == kNuCharCR) {
|
|
Nu_PutEOL(pFunnel);
|
|
lastCR = true;
|
|
} else if (uch == kNuCharLF) {
|
|
if (!lastCR)
|
|
Nu_PutEOL(pFunnel);
|
|
lastCR = false;
|
|
} else {
|
|
Nu_FunnelPutBlock(pFunnel, &uch, 1);
|
|
lastCR = false;
|
|
}
|
|
buffer++;
|
|
}
|
|
pFunnel->lastCR = lastCR; /* save copy */
|
|
|
|
}
|
|
|
|
/*if (pFunnel->outMaxExceeded)
|
|
err = kNuErrOutMax;*/
|
|
|
|
err = Nu_DataSinkGetError(pFunnel->pDataSink);
|
|
|
|
/* update progress counter with pre-LFCR count */
|
|
if (err == kNuErrNone && pFunnel->pProgress != NULL)
|
|
pFunnel->pProgress->uncompressedProgress += progressCount;
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Flush any data currently in the funnel.
|
|
*/
|
|
NuError Nu_FunnelFlush(NuArchive* pArchive, NuFunnel* pFunnel)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
if (!pFunnel->bufCount)
|
|
goto bail;
|
|
|
|
err = Nu_FunnelWriteConvert(pFunnel, pFunnel->buffer, pFunnel->bufCount);
|
|
BailError(err);
|
|
|
|
pFunnel->bufCount = 0;
|
|
err = Nu_FunnelSendProgressUpdate(pArchive, pFunnel);
|
|
/* fall through with error */
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Write a bunch of bytes into a funnel. They will be held in the buffer
|
|
* if they fit, or flushed out the bottom if not.
|
|
*/
|
|
NuError Nu_FunnelWrite(NuArchive* pArchive, NuFunnel* pFunnel,
|
|
const uint8_t* buffer, uint32_t count)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
/*pFunnel->inCount += count;*/
|
|
|
|
/*
|
|
* If it will fit into the buffer, just copy it in.
|
|
*/
|
|
if (pFunnel->bufCount + count < kNuFunnelBufSize) {
|
|
if (count == 1) /* minor optimization */
|
|
*(pFunnel->buffer + pFunnel->bufCount) = *buffer;
|
|
else
|
|
memcpy(pFunnel->buffer + pFunnel->bufCount, buffer, count);
|
|
pFunnel->bufCount += count;
|
|
goto bail;
|
|
} else {
|
|
/*
|
|
* Won't fit. We have to flush what we have, and we can either
|
|
* blow out what we were just given or put it at the start of
|
|
* the buffer.
|
|
*/
|
|
if (pFunnel->bufCount) {
|
|
err = Nu_FunnelFlush(pArchive, pFunnel);
|
|
BailError(err);
|
|
} else {
|
|
err = Nu_FunnelSendProgressUpdate(pArchive, pFunnel);
|
|
BailError(err);
|
|
}
|
|
|
|
Assert(pFunnel->bufCount == 0);
|
|
|
|
if (count >= kNuFunnelBufSize / 4) {
|
|
/* it's more than 25% of the buffer, just write it now */
|
|
err = Nu_FunnelWriteConvert(pFunnel, buffer, count);
|
|
BailError(err);
|
|
} else {
|
|
memcpy(pFunnel->buffer, buffer, count);
|
|
pFunnel->bufCount = count;
|
|
}
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set the Funnel's progress state.
|
|
*/
|
|
NuError Nu_FunnelSetProgressState(NuFunnel* pFunnel, NuProgressState state)
|
|
{
|
|
Assert(pFunnel != NULL);
|
|
|
|
if (pFunnel->pProgress == NULL)
|
|
return kNuErrNone;
|
|
|
|
pFunnel->pProgress->state = state;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Send a progress update to the application, if they're interested.
|
|
*/
|
|
NuError Nu_FunnelSendProgressUpdate(NuArchive* pArchive, NuFunnel* pFunnel)
|
|
{
|
|
NuProgressData* pProgress;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pFunnel != NULL);
|
|
|
|
pProgress = pFunnel->pProgress;
|
|
if (pProgress == NULL)
|
|
return kNuErrNone; /* no progress meter attached */
|
|
|
|
/* don't continue if they're not accepting progress messages */
|
|
if (pProgress->progressFunc == NULL)
|
|
return kNuErrNone;
|
|
|
|
/* other than the choice of arguments, it's pretty much the same story */
|
|
return Nu_SendInitialProgress(pArchive, pProgress);
|
|
}
|
|
|
|
|
|
/*
|
|
* Pull the "doExpand" parameter out of the data source.
|
|
*/
|
|
Boolean Nu_FunnelGetDoExpand(NuFunnel* pFunnel)
|
|
{
|
|
Assert(pFunnel != NULL);
|
|
Assert(pFunnel->pDataSink != NULL);
|
|
|
|
return Nu_DataSinkGetDoExpand(pFunnel->pDataSink);
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* NuStraw object
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Allocate and initialize a Straw.
|
|
*/
|
|
NuError Nu_StrawNew(NuArchive* pArchive, NuDataSource* pDataSource,
|
|
NuProgressData* pProgress, NuStraw** ppStraw)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuStraw* pStraw = NULL;
|
|
|
|
Assert(ppStraw != NULL);
|
|
Assert(pDataSource != NULL);
|
|
|
|
pStraw = Nu_Calloc(pArchive, sizeof(*pStraw));
|
|
BailAlloc(pStraw);
|
|
pStraw->pDataSource = pDataSource;
|
|
pStraw->pProgress = pProgress;
|
|
pStraw->lastProgress = 0;
|
|
pStraw->lastDisplayed = 0;
|
|
|
|
bail:
|
|
if (err != kNuErrNone)
|
|
Nu_StrawFree(pArchive, pStraw);
|
|
else
|
|
*ppStraw = pStraw;
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Free a Straw.
|
|
*/
|
|
NuError Nu_StrawFree(NuArchive* pArchive, NuStraw* pStraw)
|
|
{
|
|
if (pStraw == NULL)
|
|
return kNuErrNone;
|
|
|
|
/* we don't own the data source or progress meter */
|
|
Nu_Free(pArchive, pStraw);
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set the Straw's progress state.
|
|
*/
|
|
NuError Nu_StrawSetProgressState(NuStraw* pStraw, NuProgressState state)
|
|
{
|
|
Assert(pStraw != NULL);
|
|
Assert(pStraw->pProgress != NULL);
|
|
|
|
pStraw->pProgress->state = state;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* Send a progress update to the application, if they're interested.
|
|
*/
|
|
NuError Nu_StrawSendProgressUpdate(NuArchive* pArchive, NuStraw* pStraw)
|
|
{
|
|
NuProgressData* pProgress;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pStraw != NULL);
|
|
|
|
pProgress = pStraw->pProgress;
|
|
if (pProgress == NULL)
|
|
return kNuErrNone; /* no progress meter attached */
|
|
|
|
/* don't continue if they're not accepting progress messages */
|
|
if (pProgress->progressFunc == NULL)
|
|
return kNuErrNone;
|
|
|
|
/* other than the choice of arguments, it's pretty much the same story */
|
|
return Nu_SendInitialProgress(pArchive, pProgress);
|
|
}
|
|
|
|
|
|
/*
|
|
* Read data from a straw.
|
|
*/
|
|
NuError Nu_StrawRead(NuArchive* pArchive, NuStraw* pStraw, uint8_t* buffer,
|
|
long len)
|
|
{
|
|
NuError err;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pStraw != NULL);
|
|
Assert(buffer != NULL);
|
|
Assert(len > 0);
|
|
|
|
/*
|
|
* No buffering going on, so this is straightforward.
|
|
*/
|
|
|
|
err = Nu_DataSourceGetBlock(pStraw->pDataSource, buffer, len);
|
|
BailError(err);
|
|
|
|
/*
|
|
* Progress updating for adding is a little more complicated than
|
|
* for extracting. When extracting, the funnel controls the size
|
|
* of the output buffer, and only pushes an update when the output
|
|
* buffer fills. Here, we don't know how much will be asked for at
|
|
* a time, so we have to pace the updates or we risk flooding the
|
|
* application.
|
|
*
|
|
* We also have another problem: we want to indicate how much data
|
|
* has been processed, not how much data is *about* to be processed.
|
|
* So we have to set the percentage based on how much was requested
|
|
* on the previous call. (This assumes that whatever they asked for
|
|
* last time has already been fully processed.)
|
|
*/
|
|
if (pStraw->pProgress != NULL) {
|
|
pStraw->pProgress->uncompressedProgress = pStraw->lastProgress;
|
|
pStraw->lastProgress += len;
|
|
|
|
if (!pStraw->pProgress->uncompressedProgress ||
|
|
(pStraw->pProgress->uncompressedProgress - pStraw->lastDisplayed
|
|
> (kNuFunnelBufSize * 3 / 4)))
|
|
{
|
|
err = Nu_StrawSendProgressUpdate(pArchive, pStraw);
|
|
pStraw->lastDisplayed = pStraw->pProgress->uncompressedProgress;
|
|
BailError(err);
|
|
}
|
|
|
|
}
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Rewind a straw. This rewinds the underlying data source, and resets
|
|
* some progress counters.
|
|
*/
|
|
NuError Nu_StrawRewind(NuArchive* pArchive, NuStraw* pStraw)
|
|
{
|
|
Assert(pStraw != NULL);
|
|
Assert(pStraw->pDataSource != NULL);
|
|
|
|
pStraw->lastProgress = 0;
|
|
pStraw->lastDisplayed = 0;
|
|
|
|
return Nu_DataSourceRewind(pStraw->pDataSource);
|
|
}
|
|
|