/* * 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. */ #include "NufxLibPriv.h" /* * =========================================================================== * Utils * =========================================================================== */ /* * Returns thread N, or NULL if the index is invalid. */ NuThread* Nu_GetThread(const NuRecord* pRecord, int idx) { if (idx >= (int)pRecord->recTotalThreads) return NULL; else return &pRecord->pThreads[idx]; } /* * 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) { uint8_t* cp; for (cp = (uint8_t*)str; *cp != '\0'; cp++) if (!(*cp & 0x80)) return; for (cp = (uint8_t*)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(uint16_t 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 != NULL); 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 != NULL); 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 != NULL); Assert(pSrcThread != NULL); 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, uint16_t* pCrc) { FILE* fp; Assert(pArchive != NULL); Assert(pThread != NULL); Assert(pCrc != NULL); 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, uint16_t* pCrc) { NuError err = kNuErrNone; NuThread* pThread; long count; Boolean needFakeData, needFakeRsrc; needFakeData = true; needFakeRsrc = (pRecord->recStorageType == kNuStorageExtended); Assert(pArchive != NULL); Assert(pRecord != NULL); Assert(pCrc != NULL); 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) { if (pThread->thThreadKind == kNuThreadKindDataFork) { needFakeData = false; } else if (pThread->thThreadKind == kNuThreadKindRsrcFork) { needFakeRsrc = false; } else if (pThread->thThreadKind == kNuThreadKindDiskImage) { /* needFakeRsrc shouldn't be set, but clear anyway */ needFakeData = needFakeRsrc = false; } } /* * 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 ((needFakeData || needFakeRsrc) && pArchive->valMaskDataless) { int firstNewThread = pRecord->recTotalThreads; if (needFakeData) { pRecord->recTotalThreads++; pRecord->fakeThreads++; } if (needFakeRsrc) { pRecord->recTotalThreads++; pRecord->fakeThreads++; } pRecord->pThreads = Nu_Realloc(pArchive, pRecord->pThreads, pRecord->recTotalThreads * sizeof(NuThread)); BailAlloc(pRecord->pThreads); pThread = pRecord->pThreads + firstNewThread; if (needFakeData) { pThread->thThreadClass = kNuThreadClassData; pThread->thThreadFormat = kNuThreadFormatUncompressed; pThread->thThreadKind = kNuThreadKindDataFork; pThread->thThreadCRC = kNuInitialThreadCRC; pThread->thThreadEOF = 0; pThread->thCompThreadEOF = 0; pThread->threadIdx = Nu_GetNextThreadIdx(pArchive); pThread->actualThreadEOF = 0; pThread->fileOffset = -99999999; pThread->used = false; pThread++; } if (needFakeRsrc) { pThread->thThreadClass = kNuThreadClassData; pThread->thThreadFormat = kNuThreadFormatUncompressed; pThread->thThreadKind = kNuThreadKindRsrcFork; 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, uint16_t* pCrc) { Assert(pArchive != NULL); Assert(pThread != NULL); Assert(fp != NULL); Assert(pCrc != NULL); Nu_WriteTwoC(pArchive, fp, pThread->thThreadClass, pCrc); Nu_WriteTwoC(pArchive, fp, (uint16_t)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, uint16_t* pCrc) { NuError err = kNuErrNone; NuThread* pThread; int idx; for (idx = 0; idx < (int)pRecord->recTotalThreads; idx++) { pThread = Nu_GetThread(pRecord, idx); Assert(pThread != NULL); 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 != NULL); Assert(pRecord != NULL); /*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 != NULL); Assert(pRecord != NULL); fp = pArchive->archiveFp; Assert(numThreads <= (long)pRecord->recTotalThreads); pThread = pRecord->pThreads; while (numThreads--) { if (pRecord->threadFilenameMOR == NULL && 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 (%u)", pThread->thCompThreadEOF); goto bail; } pRecord->threadFilenameMOR = Nu_Malloc(pArchive, pThread->thCompThreadEOF +1); BailAlloc(pRecord->threadFilenameMOR); /* note there is no CRC on a filename thread */ (void) Nu_ReadBytes(pArchive, fp, pRecord->threadFilenameMOR, 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->threadFilenameMOR[pThread->thThreadEOF] = '\0'; Nu_StripHiIfAllSet(pRecord->threadFilenameMOR); /* prefer this one over the record one, but only one should exist */ if (pRecord->filenameMOR != NULL) { DBUG(("--- HEY: got record filename and thread filename\n")); } pRecord->filenameMOR = pRecord->threadFilenameMOR; } 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->filenameMOR == NULL) { DBUG(("+++ no filename found, using default record name\n")); pRecord->filenameMOR = 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 = NULL; /* 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; UNICHAR* newPathStorageUNI = NULL; UNICHAR* recFilenameStorageUNI = NULL; const UNICHAR* newPathnameUNI; NuResult result; uint8_t newFssep; Boolean doFreeSink = false; Assert(pRecord != NULL); Assert(pThread != NULL); Assert(pDataSink != NULL); memset(&progressData, 0, sizeof(progressData)); pProgressData = NULL; /* * 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 != NULL) { 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; } } newPathnameUNI = NULL; newFssep = 0; recFilenameStorageUNI = Nu_CopyMORToUNI(pRecord->filenameMOR); 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. */ newPathnameUNI = 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 != NULL) { pathProposal.pathnameUNI = recFilenameStorageUNI; pathProposal.filenameSeparator = NuGetSepFromSysInfo(pRecord->recFileSysInfo); pathProposal.pRecord = pRecord; pathProposal.pThread = pThread; pathProposal.newPathnameUNI = NULL; pathProposal.newFilenameSeparator = '\0'; /*pathProposal.newStorage = (NuThreadID)-1;*/ pathProposal.newDataSink = NULL; 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.newPathnameUNI != NULL) { Nu_Free(pArchive, newPathStorageUNI); newPathStorageUNI = strdup(pathProposal.newPathnameUNI); newPathnameUNI = newPathStorageUNI; } else { newPathnameUNI = NULL; } if (pathProposal.newFilenameSeparator != '\0') newFssep = pathProposal.newFilenameSeparator; /* if they want to send this somewhere else, let them */ if (pathProposal.newDataSink != NULL) pDataSink = pathProposal.newDataSink; } /* at least one of these must be set */ Assert(!(newPathnameUNI == NULL && pathProposal.newDataSink == NULL)); } /* * Prepare the progress data if this is a data thread. */ if (newPathnameUNI == NULL) { /* using a data sink; get the pathname out of the record */ newPathnameUNI = recFilenameStorageUNI; newFssep = NuGetSepFromSysInfo(pRecord->recFileSysInfo); } if (pThread->thThreadClass == kNuThreadClassData) { pProgressData = &progressData; err = Nu_ProgressDataInit_Expand(pArchive, pProgressData, pRecord, newPathnameUNI, newFssep, recFilenameStorageUNI, 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 = NULL; err = Nu_OpenOutputFile(pArchive, pRecord, pThread, newPathnameUNI, newFssep, &fileFp); if (err == kNuErrRename) { /* they want to rename; the OutputPathname callback handles this */ Nu_Free(pArchive, newPathStorageUNI); newPathStorageUNI = NULL; /* reset these just to be careful */ newPathnameUNI = NULL; fileFp = NULL; goto retry_name; } else if (err != kNuErrNone) { goto bail; } Assert(fileFp != NULL); (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), newPathnameUNI); Nu_DataSinkFile_SetFP(pDataSink, NULL); BailError(err); } bail: if (err != kNuErrNone && pProgressData != NULL) { /* 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, newPathStorageUNI); Nu_Free(pArchive, recFilenameStorageUNI); 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 = NULL; UNICHAR* recFilenameStorageUNI = NULL; 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; recFilenameStorageUNI = Nu_CopyMORToUNI(pRecord->filenameMOR); err = Nu_DataSinkFile_New(true, eolConv, recFilenameStorageUNI, NuGetSepFromSysInfo(pRecord->recFileSysInfo), &pDataSink); BailError(err); err = Nu_ExtractThreadCommon(pArchive, pRecord, pThread, pDataSink); BailError(err); bail: if (pDataSink != NULL) { NuError err2 = Nu_DataSinkFree(pDataSink); if (err == kNuErrNone) err = err2; } Nu_Free(pArchive, recFilenameStorageUNI); 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 == NULL) 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 != NULL); /* 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 != NULL); if (NuGetThreadID(pThread) == threadID) { /* found a match, see if it has been deleted */ pThreadMod = Nu_ThreadMod_FindByThreadIdx(pRecord, pThread->threadIdx); if (pThreadMod != NULL && 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 != NULL) { 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 != NULL); if (pThread->thThreadClass == threadClass) { /* found a match, see if it has been deleted */ pThreadMod = Nu_ThreadMod_FindByThreadIdx(pRecord, pThread->threadIdx); if (pThreadMod != NULL && 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 != NULL) { 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 = NULL; /* 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 == NULL) { err = Nu_RecordSet_Clone(pArchive, &pArchive->copyRecordSet, &pArchive->origRecordSet); BailError(err); err = Nu_RecordSet_FindByThreadIdx(&pArchive->copyRecordSet, threadIdx, ppFoundRecord, ppFoundThread); Assert(err == kNuErrNone && *ppFoundThread != NULL); /* 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 = NULL; NuThreadFormat threadFormat; /* okay for pThreadIdx to be NULL */ if (recIdx == 0 || pDataSource == NULL) 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 != NULL); /* * 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 != NULL); /* add the thread mod to the record */ Nu_RecordAddThreadMod(pRecord, pThreadMod); if (pThreadIdx != NULL) *pThreadIdx = pThreadMod->entry.add.threadIdx; pThreadMod = NULL; /* 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 != NULL) Nu_ThreadModFree(pArchive, pThreadMod); if (err == kNuErrNone && pDataSource != NULL) { /* 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, int32_t* pMaxLen) { NuError err; NuThreadMod* pThreadMod = NULL; NuRecord* pFoundRecord; NuThread* pFoundThread; if (pDataSource == NULL) { 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 != NULL) *pMaxLen = pFoundThread->thCompThreadEOF; /* * Check to see if somebody is trying to delete this, or has already * updated it. */ if (Nu_ThreadMod_FindByThreadIdx(pFoundRecord, threadIdx) != NULL) { 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 %u bytes into %u", 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 (%u 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 != NULL); /* 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 = NULL; 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) != NULL) { 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 = NULL; /* successful, don't free */ bail: Nu_ThreadModFree(pArchive, pThreadMod); return err; }