/* * NuFX archive manipulation library * Copyright (C) 2000-2003 by Andy McFadden, All Rights Reserved. * This is free software; you can redistribute it and/or modify it under the * terms of the GNU Library General Public 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 */ 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->actualThreadEOF = 0; pThread->threadIdx = Nu_GetNextThreadIdx(pArchive); pThread->fileOffset = -99999999; if (needRsrc) { pThread++; pThread->thThreadClass = kNuThreadClassData; pThread->thThreadFormat = kNuThreadFormatUncompressed; pThread->thThreadKind = 0x0002; /* rsrc fork */ pThread->thThreadCRC = kNuInitialThreadCRC; pThread->thThreadEOF = 0; pThread->thCompThreadEOF = 0; pThread->actualThreadEOF = 0; pThread->threadIdx = Nu_GetNextThreadIdx(pArchive); pThread->fileOffset = -99999999; } } 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. */ 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); } 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) 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) { /* 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; /* 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; }