nulib2/nufxlib/Thread.c

1371 lines
43 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.
*
* Thread-level operations.
*/
#define __Thread_c__ 1
#include "NufxLibPriv.h"
/*
* ===========================================================================
* Utils
* ===========================================================================
*/
/*
* ShrinkIt v3.0.0 had a bug where the filename thread would get created
* with the high bits set. We want to undo that without stomping on
* filenames that just happen to have a fancy character in them. If all
* of the high bits are set, assume it's a "defective" name and clear
* them all. If some aren't set, assume it's just a fancy filename.
*
* This high-bit-ism was also done for disk archives by most older versions
* of ShrinkIt.
*/
void
Nu_StripHiIfAllSet(char* str)
{
uchar* cp;
for (cp = (uchar*)str; *cp != '\0'; cp++)
if (!(*cp & 0x80))
return;
for (cp = (uchar*)str; *cp != '\0'; cp++)
*cp &= 0x7f;
}
/*
* Decide if a thread is pre-sized (i.e. has a fixed maximum size with a
* lesser amount of uncompressed data within) based on the threadID.
*/
Boolean
Nu_IsPresizedThreadID(NuThreadID threadID)
{
if (threadID == kNuThreadIDFilename || threadID == kNuThreadIDComment)
return true;
else
return false;
}
/*
* Return an indication of whether the type of thread specified by ThreadID
* should ever be compressed. Right now, that's only data-class threads.
*/
Boolean
Nu_IsCompressibleThreadID(NuThreadID threadID)
{
if (NuThreadIDGetClass(threadID) == kNuThreadClassData)
return true;
else
return false;
}
/*
* Decide if the thread has a CRC, based on the record version and the
* threadID.
*/
Boolean
Nu_ThreadHasCRC(long recordVersion, NuThreadID threadID)
{
return recordVersion >= 3 &&
NuThreadIDGetClass(threadID) == kNuThreadClassData;
}
/*
* Search through a given NuRecord for the specified thread.
*/
NuError
Nu_FindThreadByIdx(const NuRecord* pRecord, NuThreadIdx thread,
NuThread** ppThread)
{
NuThread* pThread;
int idx;
for (idx = 0; idx < (int)pRecord->recTotalThreads; idx++) {
pThread = Nu_GetThread(pRecord, idx);
Assert(pThread != nil);
if (pThread->threadIdx == thread) {
*ppThread = pThread;
return kNuErrNone;
}
}
return kNuErrThreadIdxNotFound;
}
/*
* Search through a given NuRecord for the first thread with a matching
* threadID.
*/
NuError
Nu_FindThreadByID(const NuRecord* pRecord, NuThreadID threadID,
NuThread** ppThread)
{
NuThread* pThread;
int idx;
for (idx = 0; idx < (int)pRecord->recTotalThreads; idx++) {
pThread = Nu_GetThread(pRecord, idx);
Assert(pThread != nil);
if (NuGetThreadID(pThread) == threadID) {
*ppThread = pThread;
return kNuErrNone;
}
}
return kNuErrThreadIDNotFound;
}
/*
* Copy the contents of a NuThread.
*/
void
Nu_CopyThreadContents(NuThread* pDstThread, const NuThread* pSrcThread)
{
Assert(pDstThread != nil);
Assert(pSrcThread != nil);
memcpy(pDstThread, pSrcThread, sizeof(*pDstThread));
}
/*
* ===========================================================================
* Reading threads from the archive
* ===========================================================================
*/
/*
* Read a single thread header from the archive.
*/
static NuError
Nu_ReadThreadHeader(NuArchive* pArchive, NuThread* pThread, ushort* pCrc)
{
FILE* fp;
Assert(pArchive != nil);
Assert(pThread != nil);
Assert(pCrc != nil);
fp = pArchive->archiveFp;
pThread->thThreadClass = Nu_ReadTwoC(pArchive, fp, pCrc);
pThread->thThreadFormat = Nu_ReadTwoC(pArchive, fp, pCrc);
pThread->thThreadKind = Nu_ReadTwoC(pArchive, fp, pCrc);
pThread->thThreadCRC = Nu_ReadTwoC(pArchive, fp, pCrc);
pThread->thThreadEOF = Nu_ReadFourC(pArchive, fp, pCrc);
pThread->thCompThreadEOF = Nu_ReadFourC(pArchive, fp, pCrc);
pThread->threadIdx = Nu_GetNextThreadIdx(pArchive);
pThread->actualThreadEOF = 0; /* fix me later */
pThread->fileOffset = -1; /* mark as invalid */
pThread->used = 0xcfcf; /* init to invalid value */
return Nu_HeaderIOFailed(pArchive, fp);
}
/*
* Read the threads from the current archive file position.
*
* The storage for the threads is allocated here, in one block. We could
* have used a linked list like NuLib, but that doesn't really provide any
* benefit for us, and adds complexity.
*/
NuError
Nu_ReadThreadHeaders(NuArchive* pArchive, NuRecord* pRecord, ushort* pCrc)
{
NuError err = kNuErrNone;
NuThread* pThread;
long count;
Boolean hasData = false;
Assert(pArchive != nil);
Assert(pRecord != nil);
Assert(pCrc != nil);
if (!pRecord->recTotalThreads) {
/* not sure if this is reasonable, but we can handle it */
DBUG(("--- WEIRD: no threads in the record?\n"));
goto bail;
}
pRecord->pThreads = Nu_Malloc(pArchive,
pRecord->recTotalThreads * sizeof(NuThread));
BailAlloc(pRecord->pThreads);
count = pRecord->recTotalThreads;
pThread = pRecord->pThreads;
while (count--) {
err = Nu_ReadThreadHeader(pArchive, pThread, pCrc);
BailError(err);
if (pThread->thThreadClass == kNuThreadClassData)
hasData = true;
/*
* Some versions of ShrinkIt write an invalid thThreadEOF for disks,
* so we have to figure out what it's supposed to be.
*/
if (NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind) ==
kNuThreadIDDiskImage)
{
if (pRecord->recStorageType <= 13) {
/* supposed to be block size, but SHK v3.0.1 stored it wrong */
pThread->actualThreadEOF = pRecord->recExtraType * 512;
} else if (pRecord->recStorageType == 256 &&
pRecord->recExtraType == 280 &&
pRecord->recFileSysID == kNuFileSysDOS33)
{
/*
* Fix for less-common ShrinkIt problem: looks like an old
* version of GS/ShrinkIt used 256 as the block size when
* compressing DOS 3.3 images from 5.25" disks. If that
* appears to be the case here, crank up the block size.
*/
DBUG(("--- no such thing as a 70K disk image!\n"));
pThread->actualThreadEOF = pRecord->recExtraType * 512;
} else {
pThread->actualThreadEOF =
pRecord->recExtraType * pRecord->recStorageType;
}
} else {
pThread->actualThreadEOF = pThread->thThreadEOF;
}
pThread->used = false;
pThread++;
}
/*
* If "mask threadless" is set, create "fake" threads with empty
* data and resource forks as needed.
*/
if (!hasData && pArchive->valMaskDataless) {
Boolean needRsrc = (pRecord->recStorageType == kNuStorageExtended);
int firstNewThread = pRecord->recTotalThreads;
pRecord->recTotalThreads++;
pRecord->fakeThreads++;
if (needRsrc) {
pRecord->recTotalThreads++;
pRecord->fakeThreads++;
}
pRecord->pThreads = Nu_Realloc(pArchive, pRecord->pThreads,
pRecord->recTotalThreads * sizeof(NuThread));
BailAlloc(pRecord->pThreads);
pThread = pRecord->pThreads + firstNewThread;
pThread->thThreadClass = kNuThreadClassData;
pThread->thThreadFormat = kNuThreadFormatUncompressed;
pThread->thThreadKind = 0x0000; /* data fork */
pThread->thThreadCRC = kNuInitialThreadCRC;
pThread->thThreadEOF = 0;
pThread->thCompThreadEOF = 0;
pThread->threadIdx = Nu_GetNextThreadIdx(pArchive);
pThread->actualThreadEOF = 0;
pThread->fileOffset = -99999999;
pThread->used = false;
if (needRsrc) {
pThread++;
pThread->thThreadClass = kNuThreadClassData;
pThread->thThreadFormat = kNuThreadFormatUncompressed;
pThread->thThreadKind = 0x0002; /* rsrc fork */
pThread->thThreadCRC = kNuInitialThreadCRC;
pThread->thThreadEOF = 0;
pThread->thCompThreadEOF = 0;
pThread->threadIdx = Nu_GetNextThreadIdx(pArchive);
pThread->actualThreadEOF = 0;
pThread->fileOffset = -99999999;
pThread->used = false;
}
}
bail:
return err;
}
/*
* Write a single thread header to the archive.
*/
static NuError
Nu_WriteThreadHeader(NuArchive* pArchive, const NuThread* pThread, FILE* fp,
ushort* pCrc)
{
Assert(pArchive != nil);
Assert(pThread != nil);
Assert(fp != nil);
Assert(pCrc != nil);
Nu_WriteTwoC(pArchive, fp, pThread->thThreadClass, pCrc);
Nu_WriteTwoC(pArchive, fp, (ushort)pThread->thThreadFormat, pCrc);
Nu_WriteTwoC(pArchive, fp, pThread->thThreadKind, pCrc);
Nu_WriteTwoC(pArchive, fp, pThread->thThreadCRC, pCrc);
Nu_WriteFourC(pArchive, fp, pThread->thThreadEOF, pCrc);
Nu_WriteFourC(pArchive, fp, pThread->thCompThreadEOF, pCrc);
return Nu_HeaderIOFailed(pArchive, fp);
}
/*
* Write the thread headers for the record at the current file position.
*
* Note this doesn't care whether a thread was "fake" or not. In
* effect, we promote all threads to "real" status. We update the
* "fake" count in pRecord accordingly.
*/
NuError
Nu_WriteThreadHeaders(NuArchive* pArchive, NuRecord* pRecord, FILE* fp,
ushort* pCrc)
{
NuError err = kNuErrNone;
NuThread* pThread;
int idx;
for (idx = 0; idx < (int)pRecord->recTotalThreads; idx++) {
pThread = Nu_GetThread(pRecord, idx);
Assert(pThread != nil);
err = Nu_WriteThreadHeader(pArchive, pThread, fp, pCrc);
BailError(err);
}
if (pRecord->fakeThreads != 0) {
DBUG(("+++ promoting %ld fake threads to real\n",pRecord->fakeThreads));
pRecord->fakeThreads = 0;
}
bail:
return err;
}
/*
* Compute miscellaneous thread information, like total size and file
* offsets. Some values (like file offsets) will not be useful for
* streaming archives.
*
* Requires that the pArchive->currentOffset be set to the offset
* immediately after the last of the thread headers.
*/
NuError
Nu_ComputeThreadData(NuArchive* pArchive, NuRecord* pRecord)
{
NuThread* pThread;
long fileOffset, count;
Assert(pArchive != nil);
Assert(pRecord != nil);
/*pRecord->totalLength = 0;*/
pRecord->totalCompLength = 0;
fileOffset = pArchive->currentOffset;
count = pRecord->recTotalThreads;
pThread = pRecord->pThreads;
while (count--) {
pThread->fileOffset = fileOffset;
/*pRecord->totalLength += pThread->thThreadEOF;*/
pRecord->totalCompLength += pThread->thCompThreadEOF;
fileOffset += pThread->thCompThreadEOF;
pThread++;
}
return kNuErrNone;
}
/*
* Skip past some or all of the thread data in the archive. For file
* archives, we scan all the threads, but for streaming archives we only
* want to scan up to the filename thread. (If the filename thread comes
* after one of the data threads, we have a problem!)
*
* The tricky part here is that we don't want to skip over a filename
* thread. We actually want to read it in, so that we have something to
* show to the application. (Someday I'll get AndyN for putting me
* through this...)
*/
NuError
Nu_ScanThreads(NuArchive* pArchive, NuRecord* pRecord, long numThreads)
{
NuError err = kNuErrNone;
NuThread* pThread;
FILE* fp;
Assert(pArchive != nil);
Assert(pRecord != nil);
fp = pArchive->archiveFp;
Assert(numThreads <= (long)pRecord->recTotalThreads);
pThread = pRecord->pThreads;
while (numThreads--) {
if (pRecord->threadFilename == nil &&
NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind) ==
kNuThreadIDFilename)
{
/* it's the first filename thread, read the whole thing */
if (pThread->thCompThreadEOF > kNuReasonableFilenameLen) {
err = kNuErrBadRecord;
Nu_ReportError(NU_BLOB, err, "Bad thread filename len (%lu)",
pThread->thCompThreadEOF);
goto bail;
}
pRecord->threadFilename = Nu_Malloc(pArchive,
pThread->thCompThreadEOF +1);
BailAlloc(pRecord->threadFilename);
/* note there is no CRC on a filename thread */
(void) Nu_ReadBytes(pArchive, fp, pRecord->threadFilename,
pThread->thCompThreadEOF);
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
Nu_ReportError(NU_BLOB, err, "Failed reading filename thread");
goto bail;
}
/* null-terminate on the actual len, not the buffer len */
pRecord->threadFilename[pThread->thThreadEOF] = '\0';
Nu_StripHiIfAllSet(pRecord->threadFilename);
/* prefer this one over the record one, but only one should exist */
if (pRecord->filename != nil) {
DBUG(("--- HEY: got record filename and thread filename\n"));
}
pRecord->filename = pRecord->threadFilename;
} else {
/* not a filename (or not first filename), skip past it */
err = Nu_SeekArchive(pArchive, pArchive->archiveFp,
pThread->thCompThreadEOF, SEEK_CUR);
BailError(err);
}
pThread++;
}
/*
* Should've had one by now. Supposedly, older versions of ShrinkIt
* wouldn't prompt for a disk image name on DOS 3.3 volumes, so you'd
* end up with a disk image that had no name attached. This will tend
* to confuse things, so we go ahead and give it a name.
*/
if (pRecord->filename == nil) {
DBUG(("+++ no filename found, using default record name\n"));
pRecord->filename = kNuDefaultRecordName;
}
pArchive->currentOffset += pRecord->totalCompLength;
if (!Nu_IsStreaming(pArchive)) {
Assert(pArchive->currentOffset == ftell(pArchive->archiveFp));
}
bail:
return err;
}
/*
* Skip the thread. This only has meaning for streaming archives, and
* assumes that the file pointer is set to the start of the thread's data
* already.
*/
NuError
Nu_SkipThread(NuArchive* pArchive, const NuRecord* pRecord,
const NuThread* pThread)
{
NuError err;
if (!Nu_IsStreaming(pArchive)) /* for debugging */
return kNuErrNone; /* for debugging */
Assert(Nu_IsStreaming(pArchive));
err = Nu_SeekArchive(pArchive, pArchive->archiveFp,
pThread->thCompThreadEOF, SEEK_CUR);
return err;
}
/*
* ===========================================================================
* Extract
* ===========================================================================
*/
/*
* Extract the thread to the specified file pointer.
*
* If the archive is a stream, the stream must be positioned at the
* start of pThread's data. If not, it will be seeked first.
*/
static NuError
Nu_ExtractThreadToDataSink(NuArchive* pArchive, const NuRecord* pRecord,
const NuThread* pThread, NuProgressData* pProgress, NuDataSink* pDataSink)
{
NuError err;
NuFunnel* pFunnel = nil;
/* if it's not a stream, seek to the appropriate spot in the file */
if (!Nu_IsStreaming(pArchive)) {
err = Nu_SeekArchive(pArchive, pArchive->archiveFp,
pThread->fileOffset, SEEK_SET);
if (err != kNuErrNone) {
Nu_ReportError(NU_BLOB, err, "Unable to seek input to %ld",
pThread->fileOffset);
goto bail;
}
}
/*
* Set up an output funnel to write to.
*/
err = Nu_FunnelNew(pArchive, pDataSink, Nu_DataSinkGetConvertEOL(pDataSink),
pArchive->valEOL, pProgress, &pFunnel);
BailError(err);
/*
* Write it.
*/
err = Nu_ExpandStream(pArchive, pRecord, pThread, pArchive->archiveFp,
pFunnel);
if (err != kNuErrNone) {
if (err != kNuErrSkipped && err != kNuErrAborted)
Nu_ReportError(NU_BLOB, err, "ExpandStream failed");
goto bail;
}
bail:
(void) Nu_FunnelFree(pArchive, pFunnel);
return err;
}
/*
* Extract the specified thread to "pDataSink". If the sink is to a file,
* this will take care of opening (and, if appropriate, creating) the file.
*
* If we're operating on a streaming archive, the file pointer must be
* positioned at the start of the thread's data. If not, it will be
* seeked appropriately.
*
* This calls the "should we extract" and "what pathname should we use"
* filters for every thread, which means we can reject specific kinds
* of forks and/or give them different names. This is a good thing.
*/
static NuError
Nu_ExtractThreadCommon(NuArchive* pArchive, const NuRecord* pRecord,
const NuThread* pThread, NuDataSink* pDataSink)
{
NuError err = kNuErrNone;
NuSelectionProposal selProposal;
NuPathnameProposal pathProposal;
NuProgressData progressData;
NuProgressData* pProgressData;
NuDataSink* pOrigDataSink;
char* newPathStorage = nil;
const char* newPathname;
NuResult result;
uchar newFssep;
Boolean doFreeSink = false;
Assert(pRecord != nil);
Assert(pThread != nil);
Assert(pDataSink != nil);
memset(&progressData, 0, sizeof(progressData));
pProgressData = nil;
/*
* If we're just trying to verify the archive contents, create a
* data sink that goes nowhere at all.
*/
if (pArchive->testMode) {
err = Nu_DataSinkVoid_New(
Nu_DataSinkGetDoExpand(pDataSink),
Nu_DataSinkGetConvertEOL(pDataSink),
&pDataSink);
BailError(err);
doFreeSink = true;
}
pOrigDataSink = pDataSink; /* save a copy for the "retry" loop */
/*
* Decide if we want to extract this thread. This is mostly for
* use by the "bulk" extract, not the per-thread extract, but it
* still applies if they so desire.
*/
if (pArchive->selectionFilterFunc != nil) {
selProposal.pRecord = pRecord;
selProposal.pThread = pThread;
result = (*pArchive->selectionFilterFunc)(pArchive, &selProposal);
if (result == kNuSkip)
return Nu_SkipThread(pArchive, pRecord, pThread);
if (result == kNuAbort) {
err = kNuErrAborted;
goto bail;
}
}
newPathname = nil;
newFssep = 0;
retry_name:
if (Nu_DataSinkGetType(pDataSink) == kNuDataSinkToFile) {
/*
* We're extracting. Figure out the name of the file to write it to.
* If they want to use the sleazy FILE* back door, create a new
* data sink and use that instead.
*
* Start by resetting everything to defaults, in case this isn't
* our first time through the "rename" loop.
*/
newPathname = Nu_DataSinkFile_GetPathname(pDataSink);
newFssep = Nu_DataSinkFile_GetFssep(pDataSink);
pDataSink = pOrigDataSink;
/* if they don't have a pathname func defined, we just use default */
if (pArchive->outputPathnameFunc != nil) {
pathProposal.pathname = pRecord->filename;
pathProposal.filenameSeparator =
NuGetSepFromSysInfo(pRecord->recFileSysInfo);
pathProposal.pRecord = pRecord;
pathProposal.pThread = pThread;
pathProposal.newPathname = nil;
pathProposal.newFilenameSeparator = '\0';
/*pathProposal.newStorage = (NuThreadID)-1;*/
pathProposal.newDataSink = nil;
result = (*pArchive->outputPathnameFunc)(pArchive, &pathProposal);
if (result == kNuSkip)
return Nu_SkipThread(pArchive, pRecord, pThread);
if (result == kNuAbort) {
err = kNuErrAborted;
goto bail;
}
/* we don't own this string, so make a copy */
if (pathProposal.newPathname != nil) {
newPathStorage = strdup(pathProposal.newPathname);
newPathname = newPathStorage;
} else
newPathname = nil;
if (pathProposal.newFilenameSeparator != '\0')
newFssep = pathProposal.newFilenameSeparator;
/* if they want to send this somewhere else, let them */
if (pathProposal.newDataSink != nil)
pDataSink = pathProposal.newDataSink;
}
/* at least one of these must be set */
Assert(!(newPathname == nil && pathProposal.newDataSink == nil));
}
/*
* Prepare the progress data if this is a data thread.
*/
if (newPathname == nil) {
/* using a data sink; get the pathname out of the record */
newPathname = pRecord->filename;
newFssep = NuGetSepFromSysInfo(pRecord->recFileSysInfo);
}
if (pThread->thThreadClass == kNuThreadClassData) {
pProgressData = &progressData;
err = Nu_ProgressDataInit_Expand(pArchive, pProgressData, pRecord,
newPathname, newFssep, Nu_DataSinkGetConvertEOL(pOrigDataSink));
BailError(err);
/* send initial progress so they see the right name if "open" fails */
pProgressData->state = kNuProgressOpening;
err = Nu_SendInitialProgress(pArchive, pProgressData);
BailError(err);
}
if (Nu_DataSinkGetType(pDataSink) == kNuDataSinkToFile) {
/*
* We're extracting to a file. Open it, creating it if necessary and
* allowed.
*/
FILE* fileFp = nil;
err = Nu_OpenOutputFile(pArchive, pRecord, pThread, newPathname,
newFssep, &fileFp);
if (err == kNuErrRename) {
/* they want to rename; the OutputPathname callback handles this */
Nu_Free(pArchive, newPathStorage);
newPathStorage = nil;
/* reset these just to be careful */
newPathname = nil;
fileFp = nil;
goto retry_name;
} else if (err != kNuErrNone) {
goto bail;
}
Assert(fileFp != nil);
(void) Nu_DataSinkFile_SetFP(pDataSink, fileFp);
DBUG(("+++ EXTRACTING 0x%08lx from '%s' at offset %0ld to '%s'\n",
NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind),
pRecord->filename, pThread->fileOffset, newPathname));
} else {
DBUG(("+++ EXTRACTING 0x%08lx from '%s' at offset %0ld to sink\n",
NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind),
pRecord->filename, pThread->fileOffset));
}
/* extract to the file */
err = Nu_ExtractThreadToDataSink(pArchive, pRecord, pThread,
pProgressData, pDataSink);
BailError(err);
if (Nu_DataSinkGetType(pDataSink) == kNuDataSinkToFile) {
/*
* Close the file, adjusting the modification date and access
* permissions as appropriate.
*/
err = Nu_CloseOutputFile(pArchive, pRecord,
Nu_DataSinkFile_GetFP(pDataSink), newPathname);
Nu_DataSinkFile_SetFP(pDataSink, nil);
BailError(err);
}
bail:
if (err != kNuErrNone && pProgressData != nil) {
/* send a final progress message, indicating failure */
if (err == kNuErrSkipped)
pProgressData->state = kNuProgressSkipped;
else if (err == kNuErrAborted)
pProgressData->state = kNuProgressAborted;
else
pProgressData->state = kNuProgressFailed;
(void) Nu_SendInitialProgress(pArchive, pProgressData);
}
/* if this was an ordinary file, and it's still open, close it */
if (Nu_DataSinkGetType(pDataSink) == kNuDataSinkToFile)
Nu_DataSinkFile_Close(pDataSink);
Nu_Free(pArchive, newPathStorage);
if (doFreeSink)
Nu_DataSinkFree(pDataSink);
return err;
}
/*
* Extract a thread from the archive as part of a "bulk" extract operation.
*
* Streaming archives must be properly positioned.
*/
NuError
Nu_ExtractThreadBulk(NuArchive* pArchive, const NuRecord* pRecord,
const NuThread* pThread)
{
NuError err;
NuDataSink* pDataSink = nil;
NuValue eolConv;
/*
* Create a file data sink for the file. We use whatever EOL conversion
* is set as the default for the entire archive. (If you want to
* specify your own EOL conversion for each individual file, you will
* need to extract them individually, creating a data sink for each.)
*
* One exception: we turn EOL conversion off for disk image threads.
* It's *very* unlikely this would be desirable, and could be a problem
* if the user is extracting a collection of disks and files.
*/
eolConv = pArchive->valConvertExtractedEOL;
if (NuGetThreadID(pThread) == kNuThreadIDDiskImage)
eolConv = kNuConvertOff;
err = Nu_DataSinkFile_New(true, eolConv, pRecord->filename,
NuGetSepFromSysInfo(pRecord->recFileSysInfo), &pDataSink);
BailError(err);
err = Nu_ExtractThreadCommon(pArchive, pRecord, pThread, pDataSink);
BailError(err);
bail:
if (pDataSink != nil) {
NuError err2 = Nu_DataSinkFree(pDataSink);
if (err == kNuErrNone)
err = err2;
}
return err;
}
/*
* Extract a thread, given the IDs and a data sink.
*/
NuError
Nu_ExtractThread(NuArchive* pArchive, NuThreadIdx threadIdx,
NuDataSink* pDataSink)
{
NuError err;
NuRecord* pRecord;
NuThread* pThread;
if (Nu_IsStreaming(pArchive))
return kNuErrUsage;
if (threadIdx == 0 || pDataSink == nil)
return kNuErrInvalidArg;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/* find the correct record and thread by index */
err = Nu_RecordSet_FindByThreadIdx(&pArchive->origRecordSet, threadIdx,
&pRecord, &pThread);
BailError(err);
Assert(pRecord != nil);
/* extract away */
err = Nu_ExtractThreadCommon(pArchive, pRecord, pThread, pDataSink);
BailError(err);
bail:
return err;
}
/*
* ===========================================================================
* Add/update/delete
* ===========================================================================
*/
/*
* Verify that a conflicting thread with the specified threadID does not
* exist in this record, now or in the future.
*
* The set of interesting threads is equal to the current threads, minus
* any that have been deleted, plus any that have been added already.
*
* If a matching threadID is found, this returns an error.
*/
static NuError
Nu_FindNoFutureThread(NuArchive* pArchive, const NuRecord* pRecord,
NuThreadID threadID)
{
NuError err = kNuErrNone;
const NuThread* pThread;
const NuThreadMod* pThreadMod;
int idx;
/*
* Start by scanning the existing threads (if any).
*/
for (idx = 0; idx < (int)pRecord->recTotalThreads; idx++) {
pThread = Nu_GetThread(pRecord, idx);
Assert(pThread != nil);
if (NuGetThreadID(pThread) == threadID) {
/* found a match, see if it has been deleted */
pThreadMod = Nu_ThreadMod_FindByThreadIdx(pRecord,
pThread->threadIdx);
if (pThreadMod != nil &&
pThreadMod->entry.kind == kNuThreadModDelete)
{
/* it's deleted, ignore it */
continue;
}
DBUG(("--- found existing thread matching 0x%08lx\n", threadID));
err = kNuErrThreadAdd;
goto bail;
}
}
/*
* Now look for "add" threadMods with a matching threadID.
*/
pThreadMod = pRecord->pThreadMods;
while (pThreadMod != nil) {
if (pThreadMod->entry.kind == kNuThreadModAdd &&
pThreadMod->entry.add.threadID == threadID)
{
DBUG(("--- found 'add' threadMod matching 0x%08lx\n", threadID));
err = kNuErrThreadAdd;
goto bail;
}
pThreadMod = pThreadMod->pNext;
}
bail:
return err;
}
/*
* Like Nu_FindNoFutureThread, but tests against a whole class.
*/
static NuError
Nu_FindNoFutureThreadClass(NuArchive* pArchive, const NuRecord* pRecord,
long threadClass)
{
NuError err = kNuErrNone;
const NuThread* pThread;
const NuThreadMod* pThreadMod;
int idx;
/*
* Start by scanning the existing threads (if any).
*/
for (idx = 0; idx < (int)pRecord->recTotalThreads; idx++) {
pThread = Nu_GetThread(pRecord, idx);
Assert(pThread != nil);
if (pThread->thThreadClass == threadClass) {
/* found a match, see if it has been deleted */
pThreadMod = Nu_ThreadMod_FindByThreadIdx(pRecord,
pThread->threadIdx);
if (pThreadMod != nil &&
pThreadMod->entry.kind == kNuThreadModDelete)
{
/* it's deleted, ignore it */
continue;
}
DBUG(("--- Found existing thread matching 0x%04lx\n", threadClass));
err = kNuErrThreadAdd;
goto bail;
}
}
/*
* Now look for "add" threadMods with a matching threadClass.
*/
pThreadMod = pRecord->pThreadMods;
while (pThreadMod != nil) {
if (pThreadMod->entry.kind == kNuThreadModAdd &&
NuThreadIDGetClass(pThreadMod->entry.add.threadID) == threadClass)
{
DBUG(("--- Found 'add' threadMod matching 0x%04lx\n", threadClass));
err = kNuErrThreadAdd;
goto bail;
}
pThreadMod = pThreadMod->pNext;
}
bail:
return err;
}
/*
* Find an existing thread somewhere in the archive. If the "copy" set
* exists it will be searched. If not, the "orig" set is searched, and
* if an entry is found a "copy" set will be created.
*
* The record and thread returned will always be from the "copy" set. An
* error result is returned if the record and thread aren't found.
*/
static NuError
Nu_FindThreadForWriteByIdx(NuArchive* pArchive, NuThreadIdx threadIdx,
NuRecord** ppFoundRecord, NuThread** ppFoundThread)
{
NuError err;
if (Nu_RecordSet_GetLoaded(&pArchive->copyRecordSet)) {
err = Nu_RecordSet_FindByThreadIdx(&pArchive->copyRecordSet, threadIdx,
ppFoundRecord, ppFoundThread);
} else {
Assert(Nu_RecordSet_GetLoaded(&pArchive->origRecordSet));
err = Nu_RecordSet_FindByThreadIdx(&pArchive->origRecordSet, threadIdx,
ppFoundRecord, ppFoundThread);
*ppFoundThread = nil; /* can't delete from here, wipe ptr */
}
BailError(err);
/*
* The thread exists. If we were looking in the "orig" set, we have
* to create a "copy" set, and delete it from that.
*/
if (*ppFoundThread == nil) {
err = Nu_RecordSet_Clone(pArchive, &pArchive->copyRecordSet,
&pArchive->origRecordSet);
BailError(err);
err = Nu_RecordSet_FindByThreadIdx(&pArchive->copyRecordSet, threadIdx,
ppFoundRecord, ppFoundThread);
Assert(err == kNuErrNone && *ppFoundThread != nil); /* must succeed */
BailError(err);
}
bail:
return err;
}
/*
* Determine if it's okay to add a thread of the type specified by
* "threadID" into "pRecord".
*
* Returns with an error (kNuErrThreadAdd) if it's not okay.
*/
NuError
Nu_OkayToAddThread(NuArchive* pArchive, const NuRecord* pRecord,
NuThreadID threadID)
{
NuError err = kNuErrNone;
/*
* Check for class conflicts (can't mix data and control threads).
*/
if (NuThreadIDGetClass(threadID) == kNuThreadClassData) {
err = Nu_FindNoFutureThreadClass(pArchive, pRecord,
kNuThreadClassControl);
BailError(err);
} else if (NuThreadIDGetClass(threadID) == kNuThreadClassControl) {
err = Nu_FindNoFutureThreadClass(pArchive, pRecord,
kNuThreadClassData);
BailError(err);
}
/*
* Check for specific type conflicts.
*/
if (threadID == kNuThreadIDDataFork) {
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDDataFork);
BailError(err);
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDDiskImage);
BailError(err);
} else if (threadID == kNuThreadIDRsrcFork) {
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDRsrcFork);
BailError(err);
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDDiskImage);
BailError(err);
} else if (threadID == kNuThreadIDDiskImage) {
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDDataFork);
BailError(err);
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDRsrcFork);
BailError(err);
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDDiskImage);
BailError(err);
} else if (threadID == kNuThreadIDFilename) {
err = Nu_FindNoFutureThread(pArchive, pRecord, kNuThreadIDFilename);
BailError(err);
}
bail:
return err;
}
/*
* Add a new thread to a record.
*
* In some cases, you aren't allowed to add a thread whose type matches
* one that already exists. This applies to data threads and filenames,
* but not to comments, control threads, or IIgs icons. You also can't
* add a disk image thread when there are data-class threads, or vice-versa.
*
* This is the first and last place we do this sort of checking. If
* an illegal situation gets past this function, it will either get
* caught with a fatal assert or (if NDEBUG is defined) not at all.
*
* On success, the NuThreadIdx of the newly-created record will be placed
* in "*pThreadIdx", and "pDataSource" will be owned by NufxLib.
*/
NuError
Nu_AddThread(NuArchive* pArchive, NuRecordIdx recIdx, NuThreadID threadID,
NuDataSource* pDataSource, NuThreadIdx* pThreadIdx)
{
NuError err;
NuRecord* pRecord;
NuThreadMod* pThreadMod = nil;
NuThreadFormat threadFormat;
/* okay for pThreadIdx to be nil */
if (recIdx == 0 || pDataSource == nil)
return kNuErrInvalidArg;
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/*
* Find the record. If it doesn't exist in the copy set, check to
* see if it's in the "new" set.
*/
err = Nu_FindRecordForWriteByIdx(pArchive, recIdx, &pRecord);
if (err == kNuErrRecIdxNotFound &&
Nu_RecordSet_GetLoaded(&pArchive->newRecordSet))
{
err = Nu_RecordSet_FindByIdx(&pArchive->newRecordSet, recIdx, &pRecord);
}
BailError(err);
Assert(pRecord != nil);
/*
* Do some tests, looking for specific types of threads that conflict
* with what we're trying to add.
*/
err = Nu_OkayToAddThread(pArchive, pRecord, threadID);
BailError(err);
/*
* Decide if we want to compress the data from this source. If the
* data is already compressed (as indicated by the data source) or
* this type of thread isn't compressible (e.g. it's a filename), then
* we don't compress it. Otherwise, we use whatever compression mode
* is currently configured.
*/
if (Nu_DataSourceGetThreadFormat(pDataSource) == kNuThreadFormatUncompressed &&
Nu_IsCompressibleThreadID(threadID))
{
threadFormat = Nu_ConvertCompressValToFormat(pArchive,
pArchive->valDataCompression);
} else {
threadFormat = kNuThreadFormatUncompressed;
}
DBUG(("--- using threadFormat = %d\n", threadFormat));
/* create a new ThreadMod (which makes a copy of the data source) */
err = Nu_ThreadModAdd_New(pArchive, threadID, threadFormat, pDataSource,
&pThreadMod);
BailError(err);
Assert(pThreadMod != nil);
/* add the thread mod to the record */
Nu_RecordAddThreadMod(pRecord, pThreadMod);
if (pThreadIdx != nil)
*pThreadIdx = pThreadMod->entry.add.threadIdx;
pThreadMod = nil; /* successful, don't free */
/*
* If we've got a header filename and we're adding a filename thread,
* we don't want to write the record header name when we reconstruct
* the record.
*/
if (threadID == kNuThreadIDFilename && pRecord->recFilenameLength) {
DBUG(("+++ gonna drop the filename\n"));
pRecord->dropRecFilename = true;
}
bail:
if (pThreadMod != nil)
Nu_ThreadModFree(pArchive, pThreadMod);
if (err == kNuErrNone && pDataSource != nil) {
/* on success, we have ownership of the data source. ThreadMod
made its own copy, so get rid of this one */
Nu_DataSourceFree(pDataSource);
}
return err;
}
/*
* Update the contents of a pre-sized thread, such as a filename or
* comment thread.
*
* The data from the source must fit within the limits of the existing
* thread. The source data is never compressed, and must not come from
* a compressed source.
*
* You aren't allowed to update threads that have been deleted. Updating
* newly-added threads isn't possible, since they aren't really threads yet.
*/
NuError
Nu_UpdatePresizedThread(NuArchive* pArchive, NuThreadIdx threadIdx,
NuDataSource* pDataSource, long* pMaxLen)
{
NuError err;
NuThreadMod* pThreadMod = nil;
NuRecord* pFoundRecord;
NuThread* pFoundThread;
if (pDataSource == nil) {
err = kNuErrInvalidArg;
goto bail;
}
/* presized threads always contain uncompressed data */
if (Nu_DataSourceGetThreadFormat(pDataSource) !=
kNuThreadFormatUncompressed)
{
err = kNuErrBadFormat;
Nu_ReportError(NU_BLOB, err,
"presized threads can't hold compressed data");
goto bail;
}
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/*
* Find the thread in the "copy" set. (If there isn't a copy set,
* make one.)
*/
err = Nu_FindThreadForWriteByIdx(pArchive, threadIdx, &pFoundRecord,
&pFoundThread);
BailError(err);
if (!Nu_IsPresizedThreadID(NuGetThreadID(pFoundThread)) ||
!(pFoundThread->thCompThreadEOF >= pFoundThread->thThreadEOF))
{
err = kNuErrNotPreSized;
Nu_ReportError(NU_BLOB, err, "invalid thread for update");
goto bail;
}
if (pMaxLen != nil)
*pMaxLen = pFoundThread->thCompThreadEOF;
/*
* Check to see if somebody is trying to delete this, or has already
* updated it.
*/
if (Nu_ThreadMod_FindByThreadIdx(pFoundRecord, threadIdx) != nil) {
DBUG(("--- Tried to modify a deleted or modified thread\n"));
err = kNuErrModThreadChange;
goto bail;
}
/*
* Verify that "otherLen" in the data source is less than or equal
* to our len, if we can. If the data source is a file on disk,
* we're not really supposed to look at it until we flush. We
* could sneak a peek right now, which would prevent us from aborting
* the entire operation when it turns out the file won't fit, but
* that violates our semantics (and besides, the application really
* should've done that already).
*
* If the data source is from a file, we just assume it'll fit and
* let the chips fall where they may later on.
*/
if (Nu_DataSourceGetType(pDataSource) != kNuDataSourceFromFile) {
if (pFoundThread->thCompThreadEOF <
Nu_DataSourceGetOtherLen(pDataSource))
{
err = kNuErrPreSizeOverflow;
Nu_ReportError(NU_BLOB, err, "can't put %ld bytes into %ld",
Nu_DataSourceGetOtherLen(pDataSource),
pFoundThread->thCompThreadEOF);
goto bail;
}
/* check for zero-length and excessively long filenames */
if (NuGetThreadID(pFoundThread) == kNuThreadIDFilename &&
(Nu_DataSourceGetOtherLen(pDataSource) == 0 ||
Nu_DataSourceGetOtherLen(pDataSource) > kNuReasonableFilenameLen))
{
err = kNuErrInvalidFilename;
Nu_ReportError(NU_BLOB, err, "invalid filename (%ld bytes)",
Nu_DataSourceGetOtherLen(pDataSource));
goto bail;
}
}
/*
* Looks like it'll fit, and it's the right kind of data. Create
* an "update" threadMod. Note this copies the data source.
*/
Assert(pFoundThread->thThreadFormat == kNuThreadFormatUncompressed);
err = Nu_ThreadModUpdate_New(pArchive, threadIdx, pDataSource, &pThreadMod);
BailError(err);
Assert(pThreadMod != nil);
/* add the thread mod to the record */
Nu_RecordAddThreadMod(pFoundRecord, pThreadMod);
/*
* NOTE: changes to filename threads will be picked up later and
* incorporated into the record's threadFilename. We don't worry
* about the record header filename, because we might be doing an
* update-in-place and that prevents us from removing the filename
* (doing so would change the size of the archive). No need to
* do any filename-specific changes here.
*/
bail:
return err;
}
/*
* Delete an individual thread.
*
* You aren't allowed to delete threads that have been updated. Deleting
* newly-added threads isn't possible, since they aren't really threads yet.
*
* Don't worry about deleting filename threads here; we take care of that
* later on. Besides, it's sort of handy to hang on to the filename for
* as long as possible.
*/
NuError
Nu_DeleteThread(NuArchive* pArchive, NuThreadIdx threadIdx)
{
NuError err;
NuThreadMod* pThreadMod = nil;
NuRecord* pFoundRecord;
NuThread* pFoundThread;
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/*
* Find the thread in the "copy" set. (If there isn't a copy set,
* make one.)
*/
err = Nu_FindThreadForWriteByIdx(pArchive, threadIdx, &pFoundRecord,
&pFoundThread);
BailError(err);
/*
* Deletion of modified threads (updates or previous deletes) isn't
* allowed. Deletion of threads from deleted records can't happen,
* because deleted records are completely removed from the "copy" set.
*/
if (Nu_ThreadMod_FindByThreadIdx(pFoundRecord, threadIdx) != nil) {
DBUG(("--- Tried to delete a deleted or modified thread\n"));
err = kNuErrModThreadChange;
goto bail;
}
/*
* Looks good. Add a new "delete" ThreadMod to the list.
*/
err = Nu_ThreadModDelete_New(pArchive, threadIdx,
NuGetThreadID(pFoundThread), &pThreadMod);
BailError(err);
Nu_RecordAddThreadMod(pFoundRecord, pThreadMod);
pThreadMod = nil; /* successful, don't free */
bail:
Nu_ThreadModFree(pArchive, pThreadMod);
return err;
}