ciderpress/nufxlib/Record.c
Andy McFadden 84706d7ea4 Update NufxLib snapshot to v3.0.0d2
This integrates the latest NufxLib sources, and updates CiderPress
to work with the API changes.
2015-01-04 11:29:51 -08:00

2826 lines
87 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.
*
* Record-level operations.
*/
#include "NufxLibPriv.h"
/*
* Local constants.
*/
static const uint8_t kNufxID[kNufxIDLen] = { 0x4e, 0xf5, 0x46, 0xd8 };
/*
* ===========================================================================
* Simple NuRecord stuff
* ===========================================================================
*/
/*
* Initialize the contents of a NuRecord. The goal here is to init the
* things that a Nu_FreeRecordContents call will check, so that we don't
* end up trying to free garbage. No need to memset() the whole thing.
*/
static NuError Nu_InitRecordContents(NuArchive* pArchive, NuRecord* pRecord)
{
Assert(pRecord != NULL);
DebugFill(pRecord, sizeof(*pRecord));
pRecord->recOptionList = NULL;
pRecord->extraBytes = NULL;
pRecord->recFilenameMOR = NULL;
pRecord->threadFilenameMOR = NULL;
pRecord->newFilenameMOR = NULL;
pRecord->pThreads = NULL;
pRecord->pNext = NULL;
pRecord->pThreadMods = NULL;
pRecord->dirtyHeader = false;
pRecord->dropRecFilename = false;
pRecord->isBadMac = false;
return kNuErrNone;
}
/*
* Allocate and initialize a new NuRecord struct.
*/
static NuError Nu_RecordNew(NuArchive* pArchive, NuRecord** ppRecord)
{
Assert(ppRecord != NULL);
*ppRecord = Nu_Malloc(pArchive, sizeof(**ppRecord));
if (*ppRecord == NULL)
return kNuErrMalloc;
return Nu_InitRecordContents(pArchive, *ppRecord);
}
/*
* Free anything allocated within a record. Doesn't try to free the record
* itself.
*/
static NuError Nu_FreeRecordContents(NuArchive* pArchive, NuRecord* pRecord)
{
Assert(pRecord != NULL);
Nu_Free(pArchive, pRecord->recOptionList);
Nu_Free(pArchive, pRecord->extraBytes);
Nu_Free(pArchive, pRecord->recFilenameMOR);
Nu_Free(pArchive, pRecord->threadFilenameMOR);
Nu_Free(pArchive, pRecord->newFilenameMOR);
Nu_Free(pArchive, pRecord->pThreads);
/* don't Free(pRecord->pNext)! */
Nu_FreeThreadMods(pArchive, pRecord);
(void) Nu_InitRecordContents(pArchive, pRecord); /* mark as freed */
return kNuErrNone;
}
/*
* Free up a NuRecord struct.
*/
static NuError Nu_RecordFree(NuArchive* pArchive, NuRecord* pRecord)
{
if (pRecord == NULL)
return kNuErrNone;
(void) Nu_FreeRecordContents(pArchive, pRecord);
Nu_Free(pArchive, pRecord);
return kNuErrNone;
}
/*
* Copy a field comprised of a buffer and a length from one structure to
* another. It is assumed that the length value has already been copied.
*/
static NuError CopySizedField(NuArchive* pArchive, void* vppDst,
const void* vpSrc, uint32_t len)
{
NuError err = kNuErrNone;
uint8_t** ppDst = vppDst;
const uint8_t* pSrc = vpSrc;
Assert(ppDst != NULL);
if (len) {
Assert(pSrc != NULL);
*ppDst = Nu_Malloc(pArchive, len);
BailAlloc(*ppDst);
memcpy(*ppDst, pSrc, len);
} else {
Assert(pSrc == NULL);
*ppDst = NULL;
}
bail:
return err;
}
/*
* Make a copy of a record.
*/
static NuError Nu_RecordCopy(NuArchive* pArchive, NuRecord** ppDst,
const NuRecord* pSrc)
{
NuError err;
NuRecord* pDst;
err = Nu_RecordNew(pArchive, ppDst);
BailError(err);
/* copy all the static fields, then copy or blank the "hairy" parts */
pDst = *ppDst;
memcpy(pDst, pSrc, sizeof(*pSrc));
CopySizedField(pArchive, &pDst->recOptionList, pSrc->recOptionList,
pSrc->recOptionSize);
CopySizedField(pArchive, &pDst->extraBytes, pSrc->extraBytes,
pSrc->extraCount);
CopySizedField(pArchive, &pDst->recFilenameMOR, pSrc->recFilenameMOR,
pSrc->recFilenameLength == 0 ? 0 : pSrc->recFilenameLength+1);
CopySizedField(pArchive, &pDst->threadFilenameMOR, pSrc->threadFilenameMOR,
pSrc->threadFilenameMOR == NULL ? 0 : strlen(pSrc->threadFilenameMOR) +1);
CopySizedField(pArchive, &pDst->newFilenameMOR, pSrc->newFilenameMOR,
pSrc->newFilenameMOR == NULL ? 0 : strlen(pSrc->newFilenameMOR) +1);
CopySizedField(pArchive, &pDst->pThreads, pSrc->pThreads,
pSrc->recTotalThreads * sizeof(*pDst->pThreads));
/* now figure out what the filename is supposed to point at */
if (pSrc->filenameMOR == pSrc->threadFilenameMOR)
pDst->filenameMOR = pDst->threadFilenameMOR;
else if (pSrc->filenameMOR == pSrc->recFilenameMOR)
pDst->filenameMOR = pDst->recFilenameMOR;
else if (pSrc->filenameMOR == pSrc->newFilenameMOR)
pDst->filenameMOR = pDst->newFilenameMOR;
else
pDst->filenameMOR = pSrc->filenameMOR; /* probably static kDefault value */
pDst->pNext = NULL;
/* these only hold for copy from orig... may need to remove */
Assert(pSrc->pThreadMods == NULL);
Assert(!pSrc->dirtyHeader);
bail:
return err;
}
/*
* Add a ThreadMod to the list in the NuRecord.
*
* In general, the order is not significant. However, if we're adding
* a bunch of "add" threadMods for control threads to a record, their
* order might be important. So, we want to add the threadMod to the
* end of the list.
*
* I'm expecting these lists to be short, so walking down them is
* acceptable. We could do simple optimizations, like only preserving
* ordering for "add" threadMods, but even that seems silly.
*/
void Nu_RecordAddThreadMod(NuRecord* pRecord, NuThreadMod* pThreadMod)
{
NuThreadMod* pScanThreadMod;
Assert(pRecord != NULL);
Assert(pThreadMod != NULL);
if (pRecord->pThreadMods == NULL) {
pRecord->pThreadMods = pThreadMod;
} else {
pScanThreadMod = pRecord->pThreadMods;
while (pScanThreadMod->pNext != NULL)
pScanThreadMod = pScanThreadMod->pNext;
pScanThreadMod->pNext = pThreadMod;
}
pThreadMod->pNext = NULL;
}
/*
* Decide if a record is empty. An empty record is one that will have no
* threads after all adds and deletes are processed.
*
* You can't delete something you just added or has been updated, and you
* can't update something that has been deleted, so any "add" or "update"
* items indicate that the thread isn't empty.
*
* You can't delete a thread more than once, or delete a thread that
* doesn't exist, so all we need to do is count up the number of current
* threads, subtract the number of deletes, and return "true" if the net
* result is zero.
*/
Boolean Nu_RecordIsEmpty(NuArchive* pArchive, const NuRecord* pRecord)
{
const NuThreadMod* pThreadMod;
int numThreads;
Assert(pRecord != NULL);
numThreads = pRecord->recTotalThreads;
pThreadMod = pRecord->pThreadMods;
while (pThreadMod != NULL) {
switch (pThreadMod->entry.kind) {
case kNuThreadModAdd:
case kNuThreadModUpdate:
return false;
case kNuThreadModDelete:
numThreads--;
break;
case kNuThreadModUnknown:
default:
Assert(0);
return false;
}
pThreadMod = pThreadMod->pNext;
}
if (numThreads > 0)
return false;
else if (numThreads == 0)
return true;
else {
Assert(0);
Nu_ReportError(NU_BLOB, kNuErrInternal,
"Thread counting failed (%d)", numThreads);
return false;
}
}
/*
* ===========================================================================
* NuRecordSet functions
* ===========================================================================
*/
/*
* Trivial getters and setters
*/
Boolean Nu_RecordSet_GetLoaded(const NuRecordSet* pRecordSet)
{
Assert(pRecordSet != NULL);
return pRecordSet->loaded;
}
void Nu_RecordSet_SetLoaded(NuRecordSet* pRecordSet, Boolean val)
{
pRecordSet->loaded = val;
}
uint32_t Nu_RecordSet_GetNumRecords(const NuRecordSet* pRecordSet)
{
return pRecordSet->numRecords;
}
void Nu_RecordSet_SetNumRecords(NuRecordSet* pRecordSet, uint32_t val)
{
pRecordSet->numRecords = val;
}
void Nu_RecordSet_IncNumRecords(NuRecordSet* pRecordSet)
{
pRecordSet->numRecords++;
}
NuRecord* Nu_RecordSet_GetListHead(const NuRecordSet* pRecordSet)
{
return pRecordSet->nuRecordHead;
}
NuRecord** Nu_RecordSet_GetListHeadPtr(NuRecordSet* pRecordSet)
{
return &pRecordSet->nuRecordHead;
}
NuRecord* Nu_RecordSet_GetListTail(const NuRecordSet* pRecordSet)
{
return pRecordSet->nuRecordTail;
}
/*
* Returns "true" if the record set has no records or hasn't ever been
* used.
*/
Boolean Nu_RecordSet_IsEmpty(const NuRecordSet* pRecordSet)
{
if (!pRecordSet->loaded || pRecordSet->numRecords == 0)
return true;
return false;
}
/*
* Free the list of records, and reset the record sets to initial state.
*/
NuError Nu_RecordSet_FreeAllRecords(NuArchive* pArchive,
NuRecordSet* pRecordSet)
{
NuError err = kNuErrNone;
NuRecord* pRecord;
NuRecord* pNextRecord;
if (!pRecordSet->loaded) {
Assert(pRecordSet->nuRecordHead == NULL);
Assert(pRecordSet->nuRecordTail == NULL);
Assert(pRecordSet->numRecords == 0);
return kNuErrNone;
}
DBUG(("+++ FreeAllRecords\n"));
pRecord = pRecordSet->nuRecordHead;
while (pRecord != NULL) {
pNextRecord = pRecord->pNext;
err = Nu_RecordFree(pArchive, pRecord);
BailError(err); /* don't really expect this to fail */
pRecord = pNextRecord;
}
pRecordSet->nuRecordHead = pRecordSet->nuRecordTail = NULL;
pRecordSet->numRecords = 0;
pRecordSet->loaded = false;
bail:
return err;
}
/*
* Add a new record to the end of the list.
*/
static NuError Nu_RecordSet_AddRecord(NuRecordSet* pRecordSet,
NuRecord* pRecord)
{
Assert(pRecordSet != NULL);
Assert(pRecord != NULL);
/* if one is NULL, both must be NULL */
Assert(pRecordSet->nuRecordHead == NULL || pRecordSet->nuRecordTail != NULL);
Assert(pRecordSet->nuRecordTail == NULL || pRecordSet->nuRecordHead != NULL);
if (pRecordSet->nuRecordHead == NULL) {
/* empty list */
pRecordSet->nuRecordHead = pRecordSet->nuRecordTail = pRecord;
pRecordSet->loaded = true;
Assert(!pRecordSet->numRecords);
} else {
pRecord->pNext = NULL;
pRecordSet->nuRecordTail->pNext = pRecord;
pRecordSet->nuRecordTail = pRecord;
}
pRecordSet->numRecords++;
return kNuErrNone;
}
/*
* Delete a record from the record set. Pass in a pointer to the pointer
* to the record (usually either the head pointer or another record's
* "pNext" pointer).
*
* (Should have a "heavy assert" mode where we verify that "ppRecord"
* actually has something to do with pRecordSet.)
*/
NuError Nu_RecordSet_DeleteRecordPtr(NuArchive* pArchive,
NuRecordSet* pRecordSet, NuRecord** ppRecord)
{
NuError err;
NuRecord* pRecord;
Assert(pRecordSet != NULL);
Assert(ppRecord != NULL);
Assert(*ppRecord != NULL);
/* save a copy of the record we're freeing */
pRecord = *ppRecord;
/* update the pHead or pNext pointer */
*ppRecord = (*ppRecord)->pNext;
pRecordSet->numRecords--;
/* if we're deleting the tail, we have to find the "new" last entry */
if (pRecord == pRecordSet->nuRecordTail) {
if (pRecordSet->nuRecordHead == NULL) {
/* this was the last entry; we're done */
pRecordSet->nuRecordTail = NULL;
} else {
/* walk through the list... delete bottom-up will be slow! */
pRecordSet->nuRecordTail = pRecordSet->nuRecordHead;
while (pRecordSet->nuRecordTail->pNext != NULL)
pRecordSet->nuRecordTail = pRecordSet->nuRecordTail->pNext;
}
}
if (pRecordSet->numRecords)
Assert(pRecordSet->nuRecordHead!=NULL && pRecordSet->nuRecordTail!=NULL);
else
Assert(pRecordSet->nuRecordHead==NULL && pRecordSet->nuRecordTail==NULL);
err = Nu_RecordFree(pArchive, pRecord);
return err;
}
/*
* Delete a record from the record set.
*/
NuError Nu_RecordSet_DeleteRecord(NuArchive* pArchive, NuRecordSet* pRecordSet,
NuRecord* pRecord)
{
NuError err;
NuRecord** ppRecord;
ppRecord = Nu_RecordSet_GetListHeadPtr(pRecordSet);
Assert(ppRecord != NULL);
Assert(*ppRecord != NULL);
/* look for the record, so we can update his neighbors */
/* (this also ensures that the record really is in the set we think it is)*/
while (*ppRecord) {
if (*ppRecord == pRecord) {
err = Nu_RecordSet_DeleteRecordPtr(pArchive, pRecordSet, ppRecord);
BailError(err);
goto bail;
}
ppRecord = &((*ppRecord)->pNext);
}
DBUG(("--- Nu_RecordSet_DeleteRecord failed\n"));
err = kNuErrNotFound;
bail:
return err;
}
/*
* Make a clone of a record set. This is used to create the "copy" record
* set out of the "orig" set.
*/
NuError Nu_RecordSet_Clone(NuArchive* pArchive, NuRecordSet* pDstSet,
const NuRecordSet* pSrcSet)
{
NuError err = kNuErrNone;
const NuRecord* pSrcRecord;
NuRecord* pDstRecord;
Assert(pDstSet != NULL);
Assert(pSrcSet != NULL);
Assert(Nu_RecordSet_GetLoaded(pDstSet) == false);
Assert(Nu_RecordSet_GetLoaded(pSrcSet) == true);
DBUG(("--- Cloning record set\n"));
Nu_RecordSet_SetLoaded(pDstSet, true);
/* copy each record over */
pSrcRecord = pSrcSet->nuRecordHead;
while (pSrcRecord != NULL) {
err = Nu_RecordCopy(pArchive, &pDstRecord, pSrcRecord);
BailError(err);
err = Nu_RecordSet_AddRecord(pDstSet, pDstRecord);
BailError(err);
pSrcRecord = pSrcRecord->pNext;
}
Assert(pDstSet->numRecords == pSrcSet->numRecords);
bail:
if (err != kNuErrNone) {
Nu_RecordSet_FreeAllRecords(pArchive, pDstSet);
}
return err;
}
/*
* Move all of the records from one record set to another. The records
* from "pSrcSet" are appended to "pDstSet".
*
* On completion, "pSrcSet" will be empty and "unloaded".
*/
NuError Nu_RecordSet_MoveAllRecords(NuArchive* pArchive, NuRecordSet* pDstSet,
NuRecordSet* pSrcSet)
{
NuError err = kNuErrNone;
Assert(pDstSet != NULL);
Assert(pSrcSet != NULL);
/* move records over */
if (Nu_RecordSet_GetNumRecords(pSrcSet)) {
Assert(pSrcSet->loaded);
Assert(pSrcSet->nuRecordHead != NULL);
Assert(pSrcSet->nuRecordTail != NULL);
if (pDstSet->nuRecordHead == NULL) {
/* empty dst list */
Assert(pDstSet->nuRecordTail == NULL);
pDstSet->nuRecordHead = pSrcSet->nuRecordHead;
pDstSet->nuRecordTail = pSrcSet->nuRecordTail;
pDstSet->numRecords = pSrcSet->numRecords;
pDstSet->loaded = true;
} else {
/* append to dst list */
Assert(pDstSet->loaded);
Assert(pDstSet->nuRecordTail != NULL);
pDstSet->nuRecordTail->pNext = pSrcSet->nuRecordHead;
pDstSet->nuRecordTail = pSrcSet->nuRecordTail;
pDstSet->numRecords += pSrcSet->numRecords;
}
} else {
/* no records in src set */
Assert(pSrcSet->nuRecordHead == NULL);
Assert(pSrcSet->nuRecordTail == NULL);
if (pSrcSet->loaded)
pDstSet->loaded = true;
}
/* nuke all pointers in original list */
pSrcSet->nuRecordHead = pSrcSet->nuRecordTail = NULL;
pSrcSet->numRecords = 0;
pSrcSet->loaded = false;
return err;
}
/*
* Find a record in the list by index.
*/
NuError Nu_RecordSet_FindByIdx(const NuRecordSet* pRecordSet,
NuRecordIdx recIdx, NuRecord** ppRecord)
{
NuRecord* pRecord;
pRecord = pRecordSet->nuRecordHead;
while (pRecord != NULL) {
if (pRecord->recordIdx == recIdx) {
*ppRecord = pRecord;
return kNuErrNone;
}
pRecord = pRecord->pNext;
}
return kNuErrRecIdxNotFound;
}
/*
* Search for a specific thread in all records in the specified record set.
*/
NuError Nu_RecordSet_FindByThreadIdx(NuRecordSet* pRecordSet,
NuThreadIdx threadIdx, NuRecord** ppRecord, NuThread** ppThread)
{
NuError err = kNuErrThreadIdxNotFound;
NuRecord* pRecord;
pRecord = Nu_RecordSet_GetListHead(pRecordSet);
while (pRecord != NULL) {
err = Nu_FindThreadByIdx(pRecord, threadIdx, ppThread);
if (err == kNuErrNone) {
*ppRecord = pRecord;
break;
}
pRecord = pRecord->pNext;
}
Assert(err != kNuErrNone || (*ppRecord != NULL && *ppThread != NULL));
return err;
}
/*
* Compare two record filenames. This comes into play when looking for
* conflicts while adding records to an archive.
*
* Interesting issues:
* - some filesystems are case-sensitive, some aren't
* - the fssep may be different ('/', ':') for otherwise equivalent names
* - system-dependent conversions could resolve two different names to
* the same thing
*
* Some of these are out of our control. For now, I'm just doing a
* case-insensitive comparison, since the most interesting case for us is
* when the person is adding a data fork and a resource fork from the
* same file during the same operation.
*
* [ Could run both names through the pathname conversion callback first?
* Might be expensive. ]
*
* Returns an integer greater than, equal to, or less than 0, if the
* string pointed to by name1 is greater than, equal to, or less than
* the string pointed to by s2, respectively (i.e. same as strcmp).
*/
static int Nu_CompareRecordNames(const char* name1MOR, const char* name2MOR)
{
#ifdef NU_CASE_SENSITIVE
return strcmp(name1MOR, name2MOR);
#else
return strcasecmp(name1MOR, name2MOR);
#endif
}
/*
* Find a record in the list by storageName.
*/
static NuError Nu_RecordSet_FindByName(const NuRecordSet* pRecordSet,
const char* nameMOR, NuRecord** ppRecord)
{
NuRecord* pRecord;
Assert(pRecordSet != NULL);
Assert(pRecordSet->loaded);
Assert(nameMOR != NULL);
Assert(ppRecord != NULL);
pRecord = pRecordSet->nuRecordHead;
while (pRecord != NULL) {
if (Nu_CompareRecordNames(pRecord->filenameMOR, nameMOR) == 0) {
*ppRecord = pRecord;
return kNuErrNone;
}
pRecord = pRecord->pNext;
}
return kNuErrRecNameNotFound;
}
/*
* Find a record in the list by storageName, starting from the end and
* searching backwards.
*
* Since we don't actually have a "prev" pointer in the record, we end
* up scanning the entire list and keeping the last match. If this
* causes a notable reduction in efficiency we'll have to fix this.
*/
static NuError Nu_RecordSet_ReverseFindByName(const NuRecordSet* pRecordSet,
const char* nameMOR, NuRecord** ppRecord)
{
NuRecord* pRecord;
NuRecord* pFoundRecord = NULL;
Assert(pRecordSet != NULL);
Assert(pRecordSet->loaded);
Assert(nameMOR != NULL);
Assert(ppRecord != NULL);
pRecord = pRecordSet->nuRecordHead;
while (pRecord != NULL) {
if (Nu_CompareRecordNames(pRecord->filenameMOR, nameMOR) == 0)
pFoundRecord = pRecord;
pRecord = pRecord->pNext;
}
if (pFoundRecord != NULL) {
*ppRecord = pFoundRecord;
return kNuErrNone;
}
return kNuErrRecNameNotFound;
}
/*
* We have a copy of the record in the "copy" set, but we've decided
* (perhaps because the user elected to Skip a failed add) that we'd
* rather have the original.
*
* Delete the record from the "copy" set, clone the "orig" record, and
* insert the "orig" record into the same spot in the "copy" set.
*
* "ppNewRecord" will get a pointer to the newly-created clone.
*/
NuError Nu_RecordSet_ReplaceRecord(NuArchive* pArchive, NuRecordSet* pBadSet,
NuRecord* pBadRecord, NuRecordSet* pGoodSet, NuRecord** ppNewRecord)
{
NuError err;
NuRecord* pGoodRecord;
NuRecord* pSiblingRecord;
NuRecord* pNewRecord = NULL;
Assert(pArchive != NULL);
Assert(pBadSet != NULL);
Assert(pBadRecord != NULL);
Assert(pGoodSet != NULL);
Assert(ppNewRecord != NULL);
/*
* Find a record in "pGoodSet" that has the same record index as
* the "bad" record.
*/
err = Nu_RecordSet_FindByIdx(pGoodSet, pBadRecord->recordIdx,
&pGoodRecord);
BailError(err);
/*
* Clone the original.
*/
err = Nu_RecordCopy(pArchive, &pNewRecord, pGoodRecord);
BailError(err);
/*
* Insert the new one into the "bad" record set, in the exact same
* position.
*/
pNewRecord->pNext = pBadRecord->pNext;
if (pBadSet->nuRecordTail == pBadRecord)
pBadSet->nuRecordTail = pNewRecord;
if (pBadSet->nuRecordHead == pBadRecord)
pBadSet->nuRecordHead = pNewRecord;
else {
/* find the record that points to pBadRecord */
pSiblingRecord = pBadSet->nuRecordHead;
while (pSiblingRecord->pNext != pBadRecord && pSiblingRecord != NULL)
pSiblingRecord = pSiblingRecord->pNext;
if (pSiblingRecord == NULL) {
/* looks like "pBadRecord" wasn't part of "pBadSet" after all */
Assert(0);
err = kNuErrInternal;
goto bail;
}
pSiblingRecord->pNext = pNewRecord;
}
err = Nu_RecordFree(pArchive, pBadRecord);
BailError(err);
*ppNewRecord = pNewRecord;
pNewRecord = NULL; /* don't free */
bail:
if (pNewRecord != NULL)
Nu_RecordFree(pArchive, pNewRecord);
return err;
}
/*
* ===========================================================================
* Assorted utility functions
* ===========================================================================
*/
/*
* Ask the user if it's okay to ignore a bad CRC. If we can't ask the
* user, return "false".
*/
Boolean Nu_ShouldIgnoreBadCRC(NuArchive* pArchive, const NuRecord* pRecord,
NuError err)
{
NuErrorStatus errorStatus;
NuResult result;
Boolean retval = false;
UNICHAR* pathnameUNI = NULL;
Assert(pArchive->valIgnoreCRC == false);
if (pArchive->errorHandlerFunc != NULL) {
errorStatus.operation = kNuOpTest; /* mostly accurate */
errorStatus.err = err;
errorStatus.sysErr = 0;
errorStatus.message = NULL;
errorStatus.pRecord = pRecord;
errorStatus.pathnameUNI = NULL;
errorStatus.origPathname = NULL;
errorStatus.filenameSeparator = 0;
if (pRecord != NULL) {
pathnameUNI = Nu_CopyMORToUNI(pRecord->filenameMOR);
errorStatus.pathnameUNI = pathnameUNI;
errorStatus.filenameSeparator =
NuGetSepFromSysInfo(pRecord->recFileSysInfo);
}
/*errorStatus.origArchiveTouched = false;*/
errorStatus.canAbort = true;
errorStatus.canRetry = false;
errorStatus.canIgnore = true;
errorStatus.canSkip = false;
errorStatus.canRename = false;
errorStatus.canOverwrite = false;
result = (*pArchive->errorHandlerFunc)(pArchive, &errorStatus);
switch (result) {
case kNuAbort:
goto bail;
case kNuIgnore:
retval = true;
goto bail;
case kNuSkip:
case kNuOverwrite:
case kNuRetry:
case kNuRename:
default:
Nu_ReportError(NU_BLOB, kNuErrSyntax,
"Wasn't expecting result %d here", result);
break;
}
}
bail:
Nu_Free(pArchive, pathnameUNI);
return retval;
}
/*
* Read the next NuFX record from the current offset in the archive stream.
* This includes the record header and the thread header blocks.
*
* Pass in a NuRecord structure that will hold the data we read.
*/
static NuError Nu_ReadRecordHeader(NuArchive* pArchive, NuRecord* pRecord)
{
NuError err = kNuErrNone;
uint16_t crc;
FILE* fp;
int bytesRead;
Assert(pArchive != NULL);
Assert(pRecord != NULL);
Assert(pRecord->pThreads == NULL);
Assert(pRecord->pNext == NULL);
fp = pArchive->archiveFp;
pRecord->recordIdx = Nu_GetNextRecordIdx(pArchive);
/* points to whichever filename storage we like best */
pRecord->filenameMOR = NULL;
pRecord->fileOffset = pArchive->currentOffset;
(void) Nu_ReadBytes(pArchive, fp, pRecord->recNufxID, kNufxIDLen);
if (memcmp(kNufxID, pRecord->recNufxID, kNufxIDLen) != 0) {
err = kNuErrRecHdrNotFound;
Nu_ReportError(NU_BLOB, kNuErrNone,
"Couldn't find start of next record");
goto bail;
}
/*
* Read the static fields.
*/
crc = 0;
pRecord->recHeaderCRC = Nu_ReadTwo(pArchive, fp);
pRecord->recAttribCount = Nu_ReadTwoC(pArchive, fp, &crc);
pRecord->recVersionNumber = Nu_ReadTwoC(pArchive, fp, &crc);
pRecord->recTotalThreads = Nu_ReadFourC(pArchive, fp, &crc);
pRecord->recFileSysID = Nu_ReadTwoC(pArchive, fp, &crc);
pRecord->recFileSysInfo = Nu_ReadTwoC(pArchive, fp, &crc);
pRecord->recAccess = Nu_ReadFourC(pArchive, fp, &crc);
pRecord->recFileType = Nu_ReadFourC(pArchive, fp, &crc);
pRecord->recExtraType = Nu_ReadFourC(pArchive, fp, &crc);
pRecord->recStorageType = Nu_ReadTwoC(pArchive, fp, &crc);
pRecord->recCreateWhen = Nu_ReadDateTimeC(pArchive, fp, &crc);
pRecord->recModWhen = Nu_ReadDateTimeC(pArchive, fp, &crc);
pRecord->recArchiveWhen = Nu_ReadDateTimeC(pArchive, fp, &crc);
bytesRead = 56; /* 4-byte 'NuFX' plus the above */
/*
* Do some sanity checks before we continue.
*/
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
Nu_ReportError(NU_BLOB, err, "Failed reading record header");
goto bail;
}
if (pRecord->recAttribCount > kNuReasonableAttribCount) {
err = kNuErrBadRecord;
Nu_ReportError(NU_BLOB, err, "Attrib count is huge (%u)",
pRecord->recAttribCount);
goto bail;
}
if (pRecord->recVersionNumber > kNuMaxRecordVersion) {
err = kNuErrBadRecord;
Nu_ReportError(NU_BLOB, err, "Unrecognized record version number (%u)",
pRecord->recVersionNumber);
goto bail;
}
if (pRecord->recTotalThreads > kNuReasonableTotalThreads) {
err = kNuErrBadRecord;
Nu_ReportError(NU_BLOB, err, "Unreasonable number of threads (%u)",
pRecord->recTotalThreads);
goto bail;
}
/*
* Read the option list, if present.
*/
if (pRecord->recVersionNumber > 0) {
pRecord->recOptionSize = Nu_ReadTwoC(pArchive, fp, &crc);
bytesRead += 2;
/*
* It appears GS/ShrinkIt is creating bad option lists, claiming
* 36 bytes of data when there's only room for 18. Since we don't
* really pay attention to the option list
*/
if (pRecord->recOptionSize + bytesRead > pRecord->recAttribCount -2) {
DBUG(("--- truncating option list from %d to %d\n",
pRecord->recOptionSize,
pRecord->recAttribCount -2 - bytesRead));
if (pRecord->recAttribCount -2 > bytesRead)
pRecord->recOptionSize = pRecord->recAttribCount -2 - bytesRead;
else
pRecord->recOptionSize = 0;
}
/* this is the older test, which rejected funky archives */
if (pRecord->recOptionSize + bytesRead > pRecord->recAttribCount -2) {
/* option size exceeds the total attribute area */
err = kNuErrBadRecord;
Nu_ReportError(NU_BLOB, kNuErrBadRecord,
"Option size (%u) exceeds attribs (%u,%u-2)",
pRecord->recOptionSize, bytesRead,
pRecord->recAttribCount);
goto bail;
}
if (pRecord->recOptionSize) {
pRecord->recOptionList = Nu_Malloc(pArchive,pRecord->recOptionSize);
BailAlloc(pRecord->recOptionList);
(void) Nu_ReadBytesC(pArchive, fp, pRecord->recOptionList,
pRecord->recOptionSize, &crc);
bytesRead += pRecord->recOptionSize;
}
} else {
pRecord->recOptionSize = 0;
pRecord->recOptionList = NULL;
}
/* last two bytes are the filename len; all else is "extra" */
pRecord->extraCount = (pRecord->recAttribCount -2) - bytesRead;
Assert(pRecord->extraCount >= 0);
/*
* Some programs (for example, NuLib) may leave extra junk in here. This
* is allowed by the archive spec. We may want to preserve it, so we
* allocate space for it and read it if it exists.
*/
if (pRecord->extraCount) {
pRecord->extraBytes = Nu_Malloc(pArchive, pRecord->extraCount);
BailAlloc(pRecord->extraBytes);
(void) Nu_ReadBytesC(pArchive, fp, pRecord->extraBytes,
pRecord->extraCount, &crc);
bytesRead += pRecord->extraCount;
}
/*
* Read the in-record filename if one exists (likely in v0 records only).
*/
pRecord->recFilenameLength = Nu_ReadTwoC(pArchive, fp, &crc);
bytesRead += 2;
if (pRecord->recFilenameLength > kNuReasonableFilenameLen) {
err = kNuErrBadRecord;
Nu_ReportError(NU_BLOB, kNuErrBadRecord, "Filename length is huge (%u)",
pRecord->recFilenameLength);
goto bail;
}
if (pRecord->recFilenameLength) {
pRecord->recFilenameMOR =
Nu_Malloc(pArchive, pRecord->recFilenameLength +1);
BailAlloc(pRecord->recFilenameMOR);
(void) Nu_ReadBytesC(pArchive, fp, pRecord->recFilenameMOR,
pRecord->recFilenameLength, &crc);
pRecord->recFilenameMOR[pRecord->recFilenameLength] = '\0';
bytesRead += pRecord->recFilenameLength;
Nu_StripHiIfAllSet(pRecord->recFilenameMOR);
/* use the in-header one */
pRecord->filenameMOR = pRecord->recFilenameMOR;
}
/*
* Read the threads records. The data is included in the record header
* CRC, so we have to pass that in too.
*/
pRecord->fakeThreads = 0;
err = Nu_ReadThreadHeaders(pArchive, pRecord, &crc);
BailError(err);
/*
* After all is said and done, did we read the file without errors,
* and does the CRC match?
*/
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
Nu_ReportError(NU_BLOB, err, "Failed reading late record header");
goto bail;
}
if (!pArchive->valIgnoreCRC && crc != pRecord->recHeaderCRC) {
if (!Nu_ShouldIgnoreBadCRC(pArchive, pRecord, kNuErrBadRHCRC)) {
err = kNuErrBadRHCRC;
Nu_ReportError(NU_BLOB, err, "Stored RH CRC=0x%04x, calc=0x%04x",
pRecord->recHeaderCRC, crc);
Nu_ReportError(NU_BLOB_DEBUG, kNuErrNone,
"--- Problematic record is id=%u", pRecord->recordIdx);
goto bail;
}
}
/*
* Init or compute misc record fields.
*/
/* adjust "currentOffset" for the entire record header */
pArchive->currentOffset += bytesRead;
pArchive->currentOffset +=
(pRecord->recTotalThreads - pRecord->fakeThreads) * kNuThreadHeaderSize;
pRecord->recHeaderLength =
bytesRead + pRecord->recTotalThreads * kNuThreadHeaderSize;
pRecord->recHeaderLength -= pRecord->fakeThreads * kNuThreadHeaderSize;
err = Nu_ComputeThreadData(pArchive, pRecord);
BailError(err);
/* check for "bad Mac" archives */
if (pArchive->valHandleBadMac) {
if (pRecord->recFileSysInfo == '?' &&
pRecord->recFileSysID == kNuFileSysMacMFS)
{
DBUG(("--- using 'bad mac' handling\n"));
pRecord->isBadMac = true;
pRecord->recFileSysInfo = ':';
}
}
bail:
if (err != kNuErrNone)
(void)Nu_FreeRecordContents(pArchive, pRecord);
return err;
}
/*
* Update the record's storageType if it looks like it needs it, based on
* the current set of threads.
*
* The rules we follow (stopping at the first match) are:
* - If there's a disk thread, leave it alone. Disk block size issues
* should already have been resolved. If we end up copying the same
* bogus block size we were given initially, that's fine.
* - If there's a resource fork, set the storageType to 5.
* - If there's a data fork, set the storageType to 1-3.
* - If there are no data-class threads at all, set the storageType to zero.
*
* This assumes that all updates have already been processed, i.e. there's
* no lingering add or delete threadMods. This only examines the thread
* array.
*
* NOTE: for data files (types 1, 2, and 3), the actual value may not match
* up what ProDOS would use, because this doesn't test for sparseness.
*/
static void Nu_UpdateStorageType(NuArchive* pArchive, NuRecord* pRecord)
{
NuError err;
NuThread* pThread;
err = Nu_FindThreadByID(pRecord, kNuThreadIDDiskImage, &pThread);
if (err == kNuErrNone)
goto bail;
err = Nu_FindThreadByID(pRecord, kNuThreadIDRsrcFork, &pThread);
if (err == kNuErrNone) {
DBUG(("--- setting storageType to %d (was %d)\n", kNuStorageExtended,
pRecord->recStorageType));
pRecord->recStorageType = kNuStorageExtended;
goto bail;
}
err = Nu_FindThreadByID(pRecord, kNuThreadIDDataFork, &pThread);
if (err == kNuErrNone) {
int newType;
if (pThread->actualThreadEOF <= 512)
newType = kNuStorageSeedling;
else if (pThread->actualThreadEOF < 131072)
newType = kNuStorageSapling;
else
newType = kNuStorageTree;
DBUG(("--- setting storageType to %d (was %d)\n", newType,
pRecord->recStorageType));
pRecord->recStorageType = newType;
goto bail;
}
DBUG(("--- no stuff here, setting storageType to %d (was %d)\n",
kNuStorageUnknown, pRecord->recStorageType));
pRecord->recStorageType = kNuStorageUnknown;
bail:
return;
}
/*
* Write the record header to the current offset of the specified file.
* This includes writing all of the thread headers.
*
* We don't "promote" records to newer versions, because that might
* require expanding and CRCing data threads. Instead, we write the
* record in a manner appropriate for the version.
*
* As a side effect, this may update the storageType to something appropriate.
*
* The position of the file pointer on exit is undefined. The position
* past the end of the record will be stored in pArchive->currentOffset.
*/
NuError Nu_WriteRecordHeader(NuArchive* pArchive, NuRecord* pRecord, FILE* fp)
{
NuError err = kNuErrNone;
uint16_t crc;
long crcOffset;
int bytesWritten;
Assert(pArchive != NULL);
Assert(pRecord != NULL);
Assert(fp != NULL);
/*
* Before we get started, let's make sure the storageType makes sense
* for this record.
*/
Nu_UpdateStorageType(pArchive, pRecord);
DBUG(("--- Writing record header (v=%d)\n", pRecord->recVersionNumber));
(void) Nu_WriteBytes(pArchive, fp, pRecord->recNufxID, kNufxIDLen);
err = Nu_FTell(fp, &crcOffset);
BailError(err);
/*
* Write the static fields.
*/
crc = 0;
Nu_WriteTwo(pArchive, fp, 0); /* crc -- come back later */
Nu_WriteTwoC(pArchive, fp, pRecord->recAttribCount, &crc);
Nu_WriteTwoC(pArchive, fp, pRecord->recVersionNumber, &crc);
Nu_WriteFourC(pArchive, fp, pRecord->recTotalThreads, &crc);
Nu_WriteTwoC(pArchive, fp, (uint16_t)pRecord->recFileSysID, &crc);
Nu_WriteTwoC(pArchive, fp, pRecord->recFileSysInfo, &crc);
Nu_WriteFourC(pArchive, fp, pRecord->recAccess, &crc);
Nu_WriteFourC(pArchive, fp, pRecord->recFileType, &crc);
Nu_WriteFourC(pArchive, fp, pRecord->recExtraType, &crc);
Nu_WriteTwoC(pArchive, fp, pRecord->recStorageType, &crc);
Nu_WriteDateTimeC(pArchive, fp, pRecord->recCreateWhen, &crc);
Nu_WriteDateTimeC(pArchive, fp, pRecord->recModWhen, &crc);
Nu_WriteDateTimeC(pArchive, fp, pRecord->recArchiveWhen, &crc);
bytesWritten = 56; /* 4-byte 'NuFX' plus the above */
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
Nu_ReportError(NU_BLOB, err, "Failed writing record header");
goto bail;
}
/*
* Write the option list, if present.
*/
if (pRecord->recVersionNumber > 0) {
Nu_WriteTwoC(pArchive, fp, pRecord->recOptionSize, &crc);
bytesWritten += 2;
if (pRecord->recOptionSize) {
Nu_WriteBytesC(pArchive, fp, pRecord->recOptionList,
pRecord->recOptionSize, &crc);
bytesWritten += pRecord->recOptionSize;
}
}
/*
* Preserve whatever miscellaneous junk was left in here by the last guy.
* We don't know what this is or why it's here, but who knows, maybe
* it's important.
*
* Besides, if we don't, we'll have to go back and fix the attrib count.
*/
if (pRecord->extraCount) {
Nu_WriteBytesC(pArchive, fp, pRecord->extraBytes, pRecord->extraCount,
&crc);
bytesWritten += pRecord->extraCount;
}
/*
* If the record has a filename in the header, write it, unless
* recent changes have inspired us to drop the name from the header.
*
* Records that begin with no filename will have a default one
* stuffed in, so it's possible for pRecord->filename to be set
* already even if there wasn't one in the record. (In such cases,
* we don't write a name.)
*/
if (pRecord->recFilenameLength && !pRecord->dropRecFilename) {
Nu_WriteTwoC(pArchive, fp, pRecord->recFilenameLength, &crc);
bytesWritten += 2;
Nu_WriteBytesC(pArchive, fp, pRecord->recFilenameMOR,
pRecord->recFilenameLength, &crc);
} else {
Nu_WriteTwoC(pArchive, fp, 0, &crc);
bytesWritten += 2;
}
/* make sure we are where we thought we would be */
if (bytesWritten != pRecord->recAttribCount) {
err = kNuErrInternal;
Nu_ReportError(NU_BLOB, kNuErrNone,
"Didn't write what was expected (%d vs %d)",
bytesWritten, pRecord->recAttribCount);
goto bail;
}
/* write the thread headers, and zero out "fake" thread count */
err = Nu_WriteThreadHeaders(pArchive, pRecord, fp, &crc);
BailError(err);
/* get the current file offset, for some computations later */
err = Nu_FTell(fp, &pArchive->currentOffset);
BailError(err);
/* go back and fill in the CRC */
pRecord->recHeaderCRC = crc;
err = Nu_FSeek(fp, crcOffset, SEEK_SET);
BailError(err);
Nu_WriteTwo(pArchive, fp, pRecord->recHeaderCRC);
/*
* All okay?
*/
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
Nu_ReportError(NU_BLOB, err, "Failed writing late record header");
goto bail;
}
/*
* Update values for misc record fields.
*/
Assert(pRecord->fakeThreads == 0);
pRecord->recHeaderLength =
bytesWritten + pRecord->recTotalThreads * kNuThreadHeaderSize;
pRecord->recHeaderLength -= pRecord->fakeThreads * kNuThreadHeaderSize;
err = Nu_ComputeThreadData(pArchive, pRecord);
BailError(err);
bail:
return err;
}
/*
* Prepare for a "walk" through the records. This is useful for the
* "read the TOC as you go" method of archive use.
*/
static NuError Nu_RecordWalkPrepare(NuArchive* pArchive, NuRecord** ppRecord)
{
NuError err = kNuErrNone;
Assert(pArchive != NULL);
Assert(ppRecord != NULL);
DBUG(("--- walk prep\n"));
*ppRecord = NULL;
if (!pArchive->haveToc) {
/* might have tried and aborted earlier, rewind to start of records */
err = Nu_RewindArchive(pArchive);
BailError(err);
}
bail:
return err;
}
/*
* Get the next record from the "orig" set in the archive.
*
* On entry, pArchive->archiveFp must point at the start of the next
* record. On exit, it will point past the end of the record (headers and
* all data) that we just read.
*
* If we have the TOC, we just pull it out of the structure. If we don't,
* we read it from the archive file, and add it to the TOC being
* constructed.
*/
static NuError Nu_RecordWalkGetNext(NuArchive* pArchive, NuRecord** ppRecord)
{
NuError err = kNuErrNone;
Assert(pArchive != NULL);
Assert(ppRecord != NULL);
/*DBUG(("--- walk toc=%d\n", pArchive->haveToc));*/
if (pArchive->haveToc) {
if (*ppRecord == NULL)
*ppRecord = Nu_RecordSet_GetListHead(&pArchive->origRecordSet);
else
*ppRecord = (*ppRecord)->pNext;
} else {
*ppRecord = NULL; /* so we don't try to free it on exit */
/* allocate and fill in a new record */
err = Nu_RecordNew(pArchive, ppRecord);
BailError(err);
/* read data from archive file */
err = Nu_ReadRecordHeader(pArchive, *ppRecord);
BailError(err);
err = Nu_ScanThreads(pArchive, *ppRecord, (*ppRecord)->recTotalThreads);
BailError(err);
DBUG(("--- Found record '%s'\n", (*ppRecord)->filenameMOR));
/* add to list */
err = Nu_RecordSet_AddRecord(&pArchive->origRecordSet, *ppRecord);
BailError(err);
}
bail:
if (err != kNuErrNone && !pArchive->haveToc) {
/* on failure, free whatever we allocated */
Nu_RecordFree(pArchive, *ppRecord);
*ppRecord = NULL;
}
return err;
}
/*
* Finish off a successful record walk by noting that we now have a
* full table of contents. On an unsuccessful walk, blow away the TOC
* if we don't have all of it.
*/
static NuError Nu_RecordWalkFinish(NuArchive* pArchive, NuError walkErr)
{
if (pArchive->haveToc)
return kNuErrNone;
if (walkErr == kNuErrNone) {
pArchive->haveToc = true;
/* mark as loaded, even if there weren't any entries (e.g. new arc) */
Nu_RecordSet_SetLoaded(&pArchive->origRecordSet, true);
return kNuErrNone;
} else {
pArchive->haveToc = false; /* redundant */
return Nu_RecordSet_FreeAllRecords(pArchive, &pArchive->origRecordSet);
}
}
/*
* If we don't have the complete record listing from the archive in
* the "orig" record set, go get it.
*
* Uses the "record walk" functions, because they're there.
*/
NuError Nu_GetTOCIfNeeded(NuArchive* pArchive)
{
NuError err = kNuErrNone;
NuRecord* pRecord;
uint32_t count;
Assert(pArchive != NULL);
if (pArchive->haveToc)
goto bail;
DBUG(("--- GetTOCIfNeeded\n"));
err = Nu_RecordWalkPrepare(pArchive, &pRecord);
BailError(err);
count = pArchive->masterHeader.mhTotalRecords;
while (count--) {
err = Nu_RecordWalkGetNext(pArchive, &pRecord);
BailError(err);
}
bail:
(void) Nu_RecordWalkFinish(pArchive, err);
return err;
}
/*
* ===========================================================================
* Streaming read-only operations
* ===========================================================================
*/
/*
* Run through the entire archive, pulling out the header bits, skipping
* over the data bits, and calling "contentFunc" for each record.
*/
NuError Nu_StreamContents(NuArchive* pArchive, NuCallback contentFunc)
{
NuError err = kNuErrNone;
NuRecord tmpRecord;
NuResult result;
uint32_t count;
if (contentFunc == NULL) {
err = kNuErrInvalidArg;
goto bail;
}
Nu_InitRecordContents(pArchive, &tmpRecord);
count = pArchive->masterHeader.mhTotalRecords;
while (count--) {
err = Nu_ReadRecordHeader(pArchive, &tmpRecord);
BailError(err);
err = Nu_ScanThreads(pArchive, &tmpRecord, tmpRecord.recTotalThreads);
BailError(err);
/*Nu_DebugDumpRecord(&tmpRecord);
printf("\n");*/
/* let them display the contents */
result = (*contentFunc)(pArchive, &tmpRecord);
if (result == kNuAbort) {
err = kNuErrAborted;
goto bail;
}
/* dispose of the entry */
(void) Nu_FreeRecordContents(pArchive, &tmpRecord);
(void) Nu_InitRecordContents(pArchive, &tmpRecord);
}
bail:
(void) Nu_FreeRecordContents(pArchive, &tmpRecord);
return err;
}
/*
* If we're trying to be compatible with ShrinkIt, and we tried to extract
* a record that had nothing in it but comments and filenames, then we need
* to create a zero-byte data file.
*
* GS/ShrinkIt v1.1 has a bug that causes it to store zero-byte data files
* (and, for that matter, zero-byte resource forks) without a thread header.
* It isn't able to extract them. This isn't so much a compatibility
* thing as it is a bug-workaround thing.
*
* The record's storage type should tell us if it was an extended file or
* a plain file. Not really important when extracting, but if we want
* to recreate the original we need to re-add the resource fork so
* NufxLib knows to make it an extended file.
*/
static NuError Nu_FakeZeroExtract(NuArchive* pArchive, NuRecord* pRecord,
int threadKind)
{
NuError err;
NuThread fakeThread;
Assert(pRecord != NULL);
DBUG(("--- found empty record, creating zero-byte file (kind=0x%04x)\n",
threadKind));
fakeThread.thThreadClass = kNuThreadClassData;
fakeThread.thThreadFormat = kNuThreadFormatUncompressed;
fakeThread.thThreadKind = threadKind;
fakeThread.thThreadCRC = kNuInitialThreadCRC;
fakeThread.thThreadEOF = 0;
fakeThread.thCompThreadEOF = 0;
fakeThread.threadIdx = (NuThreadIdx)-1; /* shouldn't matter */
fakeThread.actualThreadEOF = 0;
fakeThread.fileOffset = 0; /* shouldn't matter */
fakeThread.used = false;
err = Nu_ExtractThreadBulk(pArchive, pRecord, &fakeThread);
if (err == kNuErrSkipped)
err = Nu_SkipThread(pArchive, pRecord, &fakeThread);
return err;
}
/*
* Run through the entire archive, extracting the contents.
*/
NuError Nu_StreamExtract(NuArchive* pArchive)
{
NuError err = kNuErrNone;
NuRecord tmpRecord;
Boolean hasInterestingThread;
uint32_t count;
long idx;
/* reset this just to be safe */
pArchive->lastDirCreatedUNI = NULL;
Nu_InitRecordContents(pArchive, &tmpRecord);
count = pArchive->masterHeader.mhTotalRecords;
while (count--) {
/*
* Read the record header (which includes the thread header blocks).
*/
err = Nu_ReadRecordHeader(pArchive, &tmpRecord);
BailError(err);
/*
* We may need to pull the filename out of a thread, but we don't
* want to blow past any data while we do it. There's no really
* good way to deal with this, so we just assume that all NuFX
* applications are nice and put the filename thread first.
*/
for (idx = 0; idx < (long)tmpRecord.recTotalThreads; idx++) {
const NuThread* pThread = Nu_GetThread(&tmpRecord, idx);
if (NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind)
== kNuThreadIDFilename)
{
break;
}
}
/* if we have fn, read it; either way, leave idx pointing at next */
if (idx < (long)tmpRecord.recTotalThreads) {
idx++; /* want count, not index */
err = Nu_ScanThreads(pArchive, &tmpRecord, idx);
BailError(err);
} else
idx = 0;
if (tmpRecord.filenameMOR == NULL) {
Nu_ReportError(NU_BLOB, kNuErrNone,
"Couldn't find filename in record");
err = kNuErrBadRecord;
goto bail;
}
/*Nu_DebugDumpRecord(&tmpRecord);
printf("\n");*/
hasInterestingThread = false;
/* extract all relevant (remaining) threads */
pArchive->lastFileCreatedUNI = NULL;
for ( ; idx < (long)tmpRecord.recTotalThreads; idx++) {
const NuThread* pThread = Nu_GetThread(&tmpRecord, idx);
if (pThread->thThreadClass == kNuThreadClassData) {
hasInterestingThread = true;
err = Nu_ExtractThreadBulk(pArchive, &tmpRecord, pThread);
if (err == kNuErrSkipped) {
err = Nu_SkipThread(pArchive, &tmpRecord, pThread);
BailError(err);
} else if (err != kNuErrNone)
goto bail;
} else {
DBUG(("IGNORING 0x%08lx from '%s'\n",
NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind),
tmpRecord.filename));
if (NuGetThreadID(pThread) != kNuThreadIDComment &&
NuGetThreadID(pThread) != kNuThreadIDFilename)
{
hasInterestingThread = true;
}
err = Nu_SkipThread(pArchive, &tmpRecord, pThread);
BailError(err);
}
}
/*
* If we're trying to be compatible with ShrinkIt, and the record
* had nothing in it but comments and filenames, then we need to
* create a zero-byte data file (and possibly a resource fork).
*
* See notes in previous instance, above.
*/
if (/*pArchive->valMaskDataless &&*/ !hasInterestingThread) {
err = Nu_FakeZeroExtract(pArchive, &tmpRecord, 0x0000);
BailError(err);
if (tmpRecord.recStorageType == kNuStorageExtended) {
err = Nu_FakeZeroExtract(pArchive, &tmpRecord, 0x0002);
BailError(err);
}
}
/* dispose of the entry */
(void) Nu_FreeRecordContents(pArchive, &tmpRecord);
(void) Nu_InitRecordContents(pArchive, &tmpRecord);
}
bail:
(void) Nu_FreeRecordContents(pArchive, &tmpRecord);
return err;
}
/*
* Test the contents of an archive. Works just like extraction, but we
* don't store anything.
*/
NuError Nu_StreamTest(NuArchive* pArchive)
{
NuError err;
pArchive->testMode = true;
err = Nu_StreamExtract(pArchive);
pArchive->testMode = false;
return err;
}
/*
* ===========================================================================
* Non-streaming read-only operations
* ===========================================================================
*/
/*
* Shove the archive table of contents through the callback function.
*
* This only walks through the "orig" list, so it does not reflect the
* results of un-flushed changes.
*/
NuError Nu_Contents(NuArchive* pArchive, NuCallback contentFunc)
{
NuError err = kNuErrNone;
NuRecord* pRecord;
NuResult result;
uint32_t count;
if (contentFunc == NULL) {
err = kNuErrInvalidArg;
goto bail;
}
err = Nu_RecordWalkPrepare(pArchive, &pRecord);
BailError(err);
count = pArchive->masterHeader.mhTotalRecords;
while (count--) {
err = Nu_RecordWalkGetNext(pArchive, &pRecord);
BailError(err);
Assert(pRecord->filenameMOR != NULL);
result = (*contentFunc)(pArchive, pRecord);
if (result == kNuAbort) {
err = kNuErrAborted;
goto bail;
}
}
bail:
(void) Nu_RecordWalkFinish(pArchive, err);
return err;
}
/*
* Extract all interesting threads from a record, given a NuRecord pointer
* into the archive data structure.
*
* This assumes random access, so it can't be used in streaming mode.
*/
static NuError Nu_ExtractRecordByPtr(NuArchive* pArchive, NuRecord* pRecord)
{
NuError err = kNuErrNone;
Boolean hasInterestingThread;
uint32_t idx;
Assert(!Nu_IsStreaming(pArchive)); /* we don't skip things we don't read */
Assert(pRecord != NULL);
/* extract all relevant threads */
hasInterestingThread = false;
pArchive->lastFileCreatedUNI = NULL;
for (idx = 0; idx < pRecord->recTotalThreads; idx++) {
const NuThread* pThread = Nu_GetThread(pRecord, idx);
if (pThread->thThreadClass == kNuThreadClassData) {
hasInterestingThread = true;
err = Nu_ExtractThreadBulk(pArchive, pRecord, pThread);
if (err == kNuErrSkipped) {
err = Nu_SkipThread(pArchive, pRecord, pThread);
BailError(err);
} else if (err != kNuErrNone)
goto bail;
} else {
if (NuGetThreadID(pThread) != kNuThreadIDComment &&
NuGetThreadID(pThread) != kNuThreadIDFilename)
{
hasInterestingThread = true;
}
DBUG(("IGNORING 0x%08lx from '%s'\n",
NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind),
pRecord->filenameMOR));
}
}
/*
* If we're trying to be compatible with ShrinkIt, and the record
* had nothing in it but comments and filenames, then we need to
* create a zero-byte file.
*
* (GSHK handles empty data and resource forks by not storing a
* thread at all. It doesn't correctly deal with them when extracting
* though, so it appears this behavior wasn't entirely expected.)
*
* If it's a forked file, we also need to create an empty rsrc file.
*
* If valMaskDataless is enabled, this won't fire, because we "forge"
* appropriate threads.
*
* Note there's another one of these below, in Nu_StreamExtract.
*/
if (/*pArchive->valMaskDataless &&*/ !hasInterestingThread) {
err = Nu_FakeZeroExtract(pArchive, pRecord, 0x0000 /*data*/);
BailError(err);
if (pRecord->recStorageType == kNuStorageExtended) {
err = Nu_FakeZeroExtract(pArchive, pRecord, 0x0002 /*rsrc*/);
BailError(err);
}
}
bail:
return err;
}
/*
* Extract a big buncha files.
*/
NuError Nu_Extract(NuArchive* pArchive)
{
NuError err;
NuRecord* pRecord = NULL;
uint32_t count;
long offset;
/* reset this just to be safe */
pArchive->lastDirCreatedUNI = NULL;
err = Nu_RecordWalkPrepare(pArchive, &pRecord);
BailError(err);
count = pArchive->masterHeader.mhTotalRecords;
while (count--) {
/* read the record and threads if we don't have them yet */
err = Nu_RecordWalkGetNext(pArchive, &pRecord);
BailError(err);
if (!pArchive->haveToc) {
/* remember where the end of the record is */
err = Nu_FTell(pArchive->archiveFp, &offset);
BailError(err);
}
/* extract one or more threads */
err = Nu_ExtractRecordByPtr(pArchive, pRecord);
BailError(err);
if (!pArchive->haveToc) {
/* line us back up so RecordWalkGetNext can read the record hdr */
err = Nu_FSeek(pArchive->archiveFp, offset, SEEK_SET);
BailError(err);
}
}
bail:
(void) Nu_RecordWalkFinish(pArchive, err);
return err;
}
/*
* Extract a single record.
*/
NuError Nu_ExtractRecord(NuArchive* pArchive, NuRecordIdx recIdx)
{
NuError err;
NuRecord* pRecord;
if (Nu_IsStreaming(pArchive))
return kNuErrUsage;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/* find the correct record by index */
err = Nu_RecordSet_FindByIdx(&pArchive->origRecordSet, recIdx, &pRecord);
BailError(err);
Assert(pRecord != NULL);
/* extract whatever looks promising */
err = Nu_ExtractRecordByPtr(pArchive, pRecord);
BailError(err);
bail:
return err;
}
/*
* Test the contents of an archive. Works just like extraction, but we
* don't store anything.
*/
NuError Nu_Test(NuArchive* pArchive)
{
NuError err;
pArchive->testMode = true;
err = Nu_Extract(pArchive);
pArchive->testMode = false;
return err;
}
/*
* Test a single record.
*/
NuError Nu_TestRecord(NuArchive* pArchive, NuRecordIdx recIdx)
{
NuError err;
NuRecord* pRecord;
if (Nu_IsStreaming(pArchive))
return kNuErrUsage;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/* find the correct record by index */
err = Nu_RecordSet_FindByIdx(&pArchive->origRecordSet, recIdx, &pRecord);
BailError(err);
Assert(pRecord != NULL);
/* extract whatever looks promising */
pArchive->testMode = true;
err = Nu_ExtractRecordByPtr(pArchive, pRecord);
pArchive->testMode = false;
BailError(err);
bail:
return err;
}
/*
* Return a pointer to a NuRecord.
*
* This pulls the record out of the "orig" set, so it will work even
* for records that have been deleted. It will not reflect changes
* made by previous "write" calls, not even SetRecordAttr.
*/
NuError Nu_GetRecord(NuArchive* pArchive, NuRecordIdx recordIdx,
const NuRecord** ppRecord)
{
NuError err;
if (recordIdx == 0 || ppRecord == NULL)
return kNuErrInvalidArg;
if (Nu_IsStreaming(pArchive))
return kNuErrUsage;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
err = Nu_RecordSet_FindByIdx(&pArchive->origRecordSet, recordIdx,
(NuRecord**)ppRecord);
if (err == kNuErrNone) {
Assert(*ppRecord != NULL);
}
/* fall through with error */
bail:
return err;
}
/*
* Find the recordIdx of a record by storage name.
*/
NuError Nu_GetRecordIdxByName(NuArchive* pArchive, const char* nameMOR,
NuRecordIdx* pRecordIdx)
{
NuError err;
NuRecord* pRecord = NULL;
if (pRecordIdx == NULL)
return kNuErrInvalidArg;
if (Nu_IsStreaming(pArchive))
return kNuErrUsage;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
err = Nu_RecordSet_FindByName(&pArchive->origRecordSet, nameMOR, &pRecord);
if (err == kNuErrNone) {
Assert(pRecord != NULL);
*pRecordIdx = pRecord->recordIdx;
}
/* fall through with error */
bail:
return err;
}
/*
* Find the recordIdx of a record by zero-based position.
*/
NuError Nu_GetRecordIdxByPosition(NuArchive* pArchive, uint32_t position,
NuRecordIdx* pRecordIdx)
{
NuError err;
const NuRecord* pRecord;
if (pRecordIdx == NULL)
return kNuErrInvalidArg;
if (Nu_IsStreaming(pArchive))
return kNuErrUsage;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
if (position >= Nu_RecordSet_GetNumRecords(&pArchive->origRecordSet)) {
err = kNuErrRecordNotFound;
goto bail;
}
pRecord = Nu_RecordSet_GetListHead(&pArchive->origRecordSet);
while (position--) {
Assert(pRecord->pNext != NULL);
pRecord = pRecord->pNext;
}
*pRecordIdx = pRecord->recordIdx;
bail:
return err;
}
/*
* ===========================================================================
* Read/write record operations (add, delete)
* ===========================================================================
*/
/*
* Find an existing record 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 goal is to always return something from the "copy" set, which we
* could do easily by just creating the "copy" set and then searching in
* it. However, we don't want to create the "copy" set if we don't have
* to, so we search "orig" if "copy" doesn't exist yet.
*
* The record returned will always be from the "copy" set. An error result
* is returned if the record isn't found.
*/
NuError Nu_FindRecordForWriteByIdx(NuArchive* pArchive, NuRecordIdx recIdx,
NuRecord** ppFoundRecord)
{
NuError err;
Assert(pArchive != NULL);
Assert(ppFoundRecord != NULL);
if (Nu_RecordSet_GetLoaded(&pArchive->copyRecordSet)) {
err = Nu_RecordSet_FindByIdx(&pArchive->copyRecordSet, recIdx,
ppFoundRecord);
} else {
Assert(Nu_RecordSet_GetLoaded(&pArchive->origRecordSet));
err = Nu_RecordSet_FindByIdx(&pArchive->origRecordSet, recIdx,
ppFoundRecord);
*ppFoundRecord = NULL; /* can't delete from here */
}
BailErrorQuiet(err);
/*
* The record exists. If we were looking in the "orig" set, we have
* to create a "copy" set and return it from there.
*/
if (*ppFoundRecord == NULL) {
err = Nu_RecordSet_Clone(pArchive, &pArchive->copyRecordSet,
&pArchive->origRecordSet);
BailError(err);
err = Nu_RecordSet_FindByIdx(&pArchive->copyRecordSet, recIdx,
ppFoundRecord);
Assert(err == kNuErrNone && *ppFoundRecord != NULL); /* must succeed */
BailError(err);
}
bail:
return err;
}
/*
* Deal with the situation where we're trying to add a record with the
* same name as an existing record. The existing record can't be in the
* "new" list (that's handled differently) and can't already have been
* deleted.
*
* This will either delete the existing record or return with an error.
*
* If we decide to delete the record, and the "orig" record set was
* passed in, then the record will be deleted from the "copy" set (which
* will be created only if necessary).
*/
static NuError Nu_HandleAddDuplicateRecord(NuArchive* pArchive,
NuRecordSet* pRecordSet, NuRecord* pRecord,
const NuFileDetails* pFileDetails)
{
NuError err = kNuErrNone;
NuErrorStatus errorStatus;
NuResult result;
Assert(pRecordSet == &pArchive->origRecordSet ||
pRecordSet == &pArchive->copyRecordSet);
Assert(pRecord != NULL);
Assert(pFileDetails != NULL);
Assert(pArchive->valAllowDuplicates == false);
/*
* If "only update older" is set, check the dates. Reject the
* request if the archived file isn't older than the new file. This
* tells the application that the request was rejected, but it's
* okay for them to move on to the next file.
*/
if (pArchive->valOnlyUpdateOlder) {
if (!Nu_IsOlder(&pRecord->recModWhen, &pFileDetails->modWhen))
return kNuErrNotNewer;
}
/*
* The file exists when it shouldn't. Decide what to do, based
* on the options configured by the application.
*
* If they "might" allow overwrites, and they have an error-handling
* callback defined, call that to find out what they want to do
* here. Options include skipping or overwriting the record.
*
* We don't currently allow renaming of records, though I suppose we
* could.
*/
switch (pArchive->valHandleExisting) {
case kNuMaybeOverwrite:
if (pArchive->errorHandlerFunc != NULL) {
errorStatus.operation = kNuOpAdd;
errorStatus.err = kNuErrRecordExists;
errorStatus.sysErr = 0;
errorStatus.message = NULL;
errorStatus.pRecord = pRecord;
UNICHAR* pathnameUNI =
Nu_CopyMORToUNI(pFileDetails->storageNameMOR);
errorStatus.pathnameUNI = pathnameUNI;
errorStatus.origPathname = pFileDetails->origName;
errorStatus.filenameSeparator =
NuGetSepFromSysInfo(pFileDetails->fileSysInfo);
/*errorStatus.origArchiveTouched = false;*/
errorStatus.canAbort = true;
errorStatus.canRetry = false;
errorStatus.canIgnore = false;
errorStatus.canSkip = true;
errorStatus.canRename = false;
errorStatus.canOverwrite = true;
result = (*pArchive->errorHandlerFunc)(pArchive, &errorStatus);
Nu_Free(pArchive, pathnameUNI);
switch (result) {
case kNuAbort:
err = kNuErrAborted;
goto bail;
case kNuSkip:
err = kNuErrSkipped;
goto bail;
case kNuOverwrite:
break; /* fall back into main code */
case kNuRetry:
case kNuRename:
case kNuIgnore:
default:
err = kNuErrSyntax;
Nu_ReportError(NU_BLOB, err,
"Wasn't expecting result %d here", result);
goto bail;
}
} else {
/* no error handler, treat like NeverOverwrite */
err = kNuErrSkipped;
goto bail;
}
break;
case kNuNeverOverwrite:
err = kNuErrSkipped;
goto bail;
case kNuMustOverwrite:
case kNuAlwaysOverwrite:
/* fall through to record deletion */
break;
default:
Assert(0);
err = kNuErrInternal;
goto bail;
}
err = kNuErrNone;
/*
* We're going to overwrite the existing record. To do this, we have
* to start by deleting it from the "copy" list.
*
* If the copy set doesn't yet exist, we have to create it and find
* the record in the new set.
*/
if (pRecordSet == &pArchive->origRecordSet) {
Assert(!Nu_RecordSet_GetLoaded(&pArchive->copyRecordSet));
err = Nu_RecordSet_Clone(pArchive, &pArchive->copyRecordSet,
&pArchive->origRecordSet);
BailError(err);
err = Nu_RecordSet_FindByIdx(&pArchive->copyRecordSet,
pRecord->recordIdx, &pRecord);
Assert(err == kNuErrNone && pRecord != NULL); /* must succeed */
BailError(err);
}
DBUG(("+++ deleting record %ld\n", pRecord->recordIdx));
err = Nu_RecordSet_DeleteRecord(pArchive,&pArchive->copyRecordSet, pRecord);
BailError(err);
bail:
return err;
}
/*
* Create a new record, filling in most of the blanks from "pFileDetails".
*
* The filename in pFileDetails->storageName will be remembered. If no
* filename thread is added to this record before the next Flush call, a
* filename thread will be generated from this name.
*
* This always creates a "version 3" record, regardless of what else is
* in the archive. The filename is always in a thread.
*
* On success, the NuRecordIdx of the newly-created record will be placed
* in "*pRecordIdx", and the NuThreadIdx of the filename thread will be
* placed in "*pThreadIdx". If "*ppNewRecord" is non-NULL, it gets a pointer
* to the newly-created record (this isn't part of the external interface).
*/
NuError Nu_AddRecord(NuArchive* pArchive, const NuFileDetails* pFileDetails,
NuRecordIdx* pRecordIdx, NuRecord** ppNewRecord)
{
NuError err;
NuRecord* pNewRecord = NULL;
if (pFileDetails == NULL || pFileDetails->storageNameMOR == NULL ||
pFileDetails->storageNameMOR[0] == '\0' ||
NuGetSepFromSysInfo(pFileDetails->fileSysInfo) == 0)
/* pRecordIdx may be NULL */
/* ppNewRecord may be NULL */
{
err = kNuErrInvalidArg;
goto bail;
}
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/* NuFX spec forbids leading fssep chars */
if (pFileDetails->storageNameMOR[0] ==
NuGetSepFromSysInfo(pFileDetails->fileSysInfo))
{
err = kNuErrLeadingFssep;
goto bail;
}
/*
* If requested, look for an existing record. Look in the "copy"
* list if we have it (so we don't complain if they've already deleted
* the record), or in the "orig" list if we don't. Look in the "new"
* list to see if it clashes with something we've just added.
*
* If this is a brand-new archive, there won't be an "orig" list
* either.
*/
if (!pArchive->valAllowDuplicates) {
NuRecordSet* pRecordSet;
NuRecord* pFoundRecord;
pRecordSet = &pArchive->copyRecordSet;
if (!Nu_RecordSet_GetLoaded(pRecordSet))
pRecordSet = &pArchive->origRecordSet;
Assert(Nu_RecordSet_GetLoaded(pRecordSet));
err = Nu_RecordSet_FindByName(pRecordSet, pFileDetails->storageNameMOR,
&pFoundRecord);
if (err == kNuErrNone) {
/* handle the existing record */
DBUG(("--- Duplicate record found (%06ld) '%s'\n",
pFoundRecord->recordIdx, pFoundRecord->filenameMOR));
err = Nu_HandleAddDuplicateRecord(pArchive, pRecordSet,
pFoundRecord, pFileDetails);
if (err != kNuErrNone) {
/* for whatever reason, we're not replacing it */
DBUG(("--- Returning err=%d\n", err));
goto bail;
}
} else {
/* if we *must* replace an existing file, we fail now */
if (pArchive->valHandleExisting == kNuMustOverwrite) {
DBUG(("+++ can't freshen nonexistent '%s'\n",
pFileDetails->storageName));
err = kNuErrDuplicateNotFound;
goto bail;
}
}
if (Nu_RecordSet_GetLoaded(&pArchive->newRecordSet)) {
err = Nu_RecordSet_FindByName(&pArchive->newRecordSet,
pFileDetails->storageNameMOR, &pFoundRecord);
if (err == kNuErrNone) {
/* we can't delete from the "new" list, so return an error */
err = kNuErrRecordExists;
goto bail;
}
}
/* clear "err" so we can continue */
err = kNuErrNone;
}
/*
* Prepare the new record structure.
*/
err = Nu_RecordNew(pArchive, &pNewRecord);
BailError(err);
(void) Nu_InitRecordContents(pArchive, pNewRecord);
memcpy(pNewRecord->recNufxID, kNufxID, kNufxIDLen);
/*pNewRecord->recHeaderCRC*/
/*pNewRecord->recAttribCount*/
pNewRecord->recVersionNumber = kNuOurRecordVersion;
pNewRecord->recTotalThreads = 0;
pNewRecord->recFileSysID = pFileDetails->fileSysID;
pNewRecord->recFileSysInfo = pFileDetails->fileSysInfo;
pNewRecord->recAccess = pFileDetails->access;
pNewRecord->recFileType = pFileDetails->fileType;
pNewRecord->recExtraType = pFileDetails->extraType;
pNewRecord->recStorageType = pFileDetails->storageType;
pNewRecord->recCreateWhen = pFileDetails->createWhen;
pNewRecord->recModWhen = pFileDetails->modWhen;
pNewRecord->recArchiveWhen = pFileDetails->archiveWhen;
pNewRecord->recOptionSize = 0;
pNewRecord->extraCount = 0;
pNewRecord->recFilenameLength = 0;
pNewRecord->recordIdx = Nu_GetNextRecordIdx(pArchive);
pNewRecord->threadFilenameMOR = NULL;
pNewRecord->newFilenameMOR = strdup(pFileDetails->storageNameMOR);
pNewRecord->filenameMOR = pNewRecord->newFilenameMOR;
pNewRecord->recHeaderLength = -1;
pNewRecord->totalCompLength = 0;
pNewRecord->fakeThreads = 0;
pNewRecord->fileOffset = -1;
/*
* Add it to the "new" record set.
*/
err = Nu_RecordSet_AddRecord(&pArchive->newRecordSet, pNewRecord);
BailError(err);
/* return values */
if (pRecordIdx != NULL)
*pRecordIdx = pNewRecord->recordIdx;
if (ppNewRecord != NULL)
*ppNewRecord = pNewRecord;
bail:
return err;
}
/*
* Add a new "add file" thread mod to the specified record.
*
* The caller should have already verified that there isn't another
* "add file" thread mod with the same ThreadID.
*/
static NuError Nu_AddFileThreadMod(NuArchive* pArchive, NuRecord* pRecord,
const UNICHAR* pathnameUNI, const NuFileDetails* pFileDetails,
Boolean fromRsrcFork)
{
NuError err;
NuThreadFormat threadFormat;
NuDataSource* pDataSource = NULL;
NuThreadMod* pThreadMod = NULL;
Assert(pArchive != NULL);
Assert(pRecord != NULL);
Assert(pathnameUNI != NULL);
Assert(pFileDetails != NULL);
Assert(fromRsrcFork == true || fromRsrcFork == false);
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
/* decide if this should be compressed; we know source isn't */
if (Nu_IsCompressibleThreadID(pFileDetails->threadID))
threadFormat = Nu_ConvertCompressValToFormat(pArchive,
pArchive->valDataCompression);
else
threadFormat = kNuThreadFormatUncompressed;
/* create a data source for this file, which is assumed uncompressed */
err = Nu_DataSourceFile_New(kNuThreadFormatUncompressed, 0,
pathnameUNI, fromRsrcFork, &pDataSource);
BailError(err);
/* create a new ThreadMod */
err = Nu_ThreadModAdd_New(pArchive, pFileDetails->threadID, threadFormat,
pDataSource, &pThreadMod);
BailError(err);
Assert(pThreadMod != NULL);
/*pDataSource = NULL;*/ /* ThreadModAdd_New makes a copy */
/* add the thread mod to the record */
Nu_RecordAddThreadMod(pRecord, pThreadMod);
pThreadMod = NULL; /* don't free on exit */
bail:
if (pDataSource != NULL)
Nu_DataSourceFree(pDataSource);
if (pThreadMod != NULL)
Nu_ThreadModFree(pArchive, pThreadMod);
return err;
}
/*
* Make note of a file to add. This goes beyond AddRecord and AddThread
* calls by searching the list of newly-added files for matching pairs
* of data and rsrc forks. This is independent of the "overwrite existing
* files" feature. The comparison is made based on storageName.
*
* "fromRsrcFork" tells us how to open the source file, not what type
* of thread the file should be stored as.
*
* If "pRecordIdx" is non-NULL, it will receive the newly assigned recordID.
*/
NuError Nu_AddFile(NuArchive* pArchive, const UNICHAR* pathnameUNI,
const NuFileDetails* pFileDetails, Boolean fromRsrcFork,
NuRecordIdx* pRecordIdx)
{
NuError err = kNuErrNone;
NuRecordIdx recordIdx = 0;
NuRecord* pRecord;
if (pathnameUNI == NULL || pFileDetails == NULL ||
!(fromRsrcFork == true || fromRsrcFork == false))
{
return kNuErrInvalidArg;
}
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
if (pFileDetails->storageNameMOR == NULL) {
err = kNuErrInvalidArg;
Nu_ReportError(NU_BLOB, err, "Must specify storageName");
goto bail;
}
if (pFileDetails->storageNameMOR[0] ==
NuGetSepFromSysInfo(pFileDetails->fileSysInfo))
{
err = kNuErrLeadingFssep;
goto bail;
}
DBUG(("+++ ADDING '%s' (%s) 0x%02lx 0x%04lx threadID=0x%08lx\n",
pathnameUNI, pFileDetails->storageName, pFileDetails->fileType,
pFileDetails->extraType, pFileDetails->threadID));
/*
* See if there's another record among the "new additions" with the
* same storageName and compatible threads.
*
* If found, add a new thread in that record. If an incompatibility
* exists (same fork already present, disk image is there, etc), either
* create a new record or return with an error.
*
* We want to search from the *end* of the "new" list, so that if
* duplicates are allowed we find the entry most likely to be paired
* up with the fork currently being added.
*/
if (Nu_RecordSet_GetLoaded(&pArchive->newRecordSet)) {
NuRecord* pNewRecord;
err = Nu_RecordSet_ReverseFindByName(&pArchive->newRecordSet,
pFileDetails->storageNameMOR, &pNewRecord);
if (err == kNuErrNone) {
/* is it okay to add it here? */
err = Nu_OkayToAddThread(pArchive, pNewRecord,
pFileDetails->threadID);
if (err == kNuErrNone) {
/* okay to add it to this record */
DBUG((" attaching to existing record %06ld\n",
pNewRecord->recordIdx));
err = Nu_AddFileThreadMod(pArchive, pNewRecord, pathnameUNI,
pFileDetails, fromRsrcFork);
BailError(err);
recordIdx = pNewRecord->recordIdx;
goto bail; /* we're done! */
}
err = kNuErrNone; /* go a little farther */
/*
* We found a brand-new record with the same name, but we
* can't add this fork to that record. We can't delete the
* item from the "new" list, so we can ignore HandleExisting.
* If we don't allow duplicates, return an error; if we do,
* then just continue with the normal processing path.
*/
if (!pArchive->valAllowDuplicates) {
DBUG(("+++ found matching record in new list, no dups\n"));
err = kNuErrRecordExists;
goto bail;
}
} else if (err == kNuErrRecNameNotFound) {
/* no match in "new" list, fall through to normal processing */
err = kNuErrNone;
} else {
/* general failure */
goto bail;
}
}
/*
* Wasn't found, invoke Nu_AddRecord. This will search through the
* existing records, using the "allow duplicates" flag to cope with
* any matches it finds. On success, we should have a brand-new record
* to play with.
*/
err = Nu_AddRecord(pArchive, pFileDetails, &recordIdx, &pRecord);
BailError(err);
DBUG(("--- Added new record %06ld\n", recordIdx));
/*
* Got the record, now add a data file thread.
*/
err = Nu_AddFileThreadMod(pArchive, pRecord, pathnameUNI, pFileDetails,
fromRsrcFork);
BailError(err);
bail:
if (err == kNuErrNone && pRecordIdx != NULL)
*pRecordIdx = recordIdx;
return err;
}
/*
* Rename a record. There are three situations:
*
* (1) Record has the filename in a thread, and the field has enough
* room to hold the new name. For this case we add an "update" threadMod
* with the new data.
* (2) Record has the filename in a thread, and there is not enough room
* to hold the new name. Here, we add a "delete" threadMod for the
* existing filename, and add an "add" threadMod for the new.
* (3) Record stores the filename in the header. We zero out the filename
* and add a filename thread.
*
* We don't actually check to see if the filename is changing. If you
* want to rename something to the same thing, go right ahead. (This
* provides a way for applications to "filter" records that have filenames
* in the headers instead of a thread.)
*
* BUG: we shouldn't allow a disk image to be renamed to have a complex
* path name (e.g. "dir1:dir2:foo"). However, we may not be able to catch
* that here depending on pending operations.
*
* We might also want to screen out trailing fssep chars, though the NuFX
* spec doesn't say they're illegal.
*/
NuError Nu_Rename(NuArchive* pArchive, NuRecordIdx recIdx,
const char* pathnameMOR, char fssepMOR)
{
NuError err;
NuRecord* pRecord;
NuThread* pFilenameThread;
const NuThreadMod* pThreadMod;
NuThreadMod* pNewThreadMod = NULL;
NuDataSource* pDataSource = NULL;
long requiredCapacity, existingCapacity, newCapacity;
Boolean doDelete, doAdd, doUpdate;
if (recIdx == 0 || pathnameMOR == NULL || pathnameMOR[0] == '\0' ||
fssepMOR == '\0')
{
return kNuErrInvalidArg;
}
if (pathnameMOR[0] == fssepMOR) {
err = kNuErrLeadingFssep;
Nu_ReportError(NU_BLOB, err, "rename path");
goto bail;
}
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/* find the record in the "copy" set */
err = Nu_FindRecordForWriteByIdx(pArchive, recIdx, &pRecord);
BailError(err);
Assert(pRecord != NULL);
/* look for a filename thread */
err = Nu_FindThreadByID(pRecord, kNuThreadIDFilename, &pFilenameThread);
if (err != kNuErrNone)
pFilenameThread = NULL;
else if (err == kNuErrNone && pRecord->pThreadMods) {
/* found a thread, check to see if it has been deleted (or modifed) */
Assert(pFilenameThread != NULL);
pThreadMod = Nu_ThreadMod_FindByThreadIdx(pRecord,
pFilenameThread->threadIdx);
if (pThreadMod != NULL) {
DBUG(("--- tried to modify threadIdx %ld, which has already been\n",
pFilenameThread->threadIdx));
err = kNuErrModThreadChange;
goto bail;
}
}
/*
* Looks like we're okay so far. Figure out what to do.
*/
doDelete = doAdd = doUpdate = false;
newCapacity = existingCapacity = 0;
requiredCapacity = strlen(pathnameMOR);
if (pFilenameThread != NULL) {
existingCapacity = pFilenameThread->thCompThreadEOF;
if (existingCapacity >= requiredCapacity) {
doUpdate = true;
newCapacity = existingCapacity;
} else {
doDelete = doAdd = true;
/* make sure they have a few bytes of leeway */
/*newCapacity = (requiredCapacity + kNuDefaultFilenameThreadSize) &
(~(kNuDefaultFilenameThreadSize-1));*/
newCapacity = requiredCapacity + 8;
}
} else {
doAdd = true;
/*newCapacity = (requiredCapacity + kNuDefaultFilenameThreadSize) &
(~(kNuDefaultFilenameThreadSize-1));*/
newCapacity = requiredCapacity + 8;
}
Assert(doAdd || doDelete || doUpdate);
Assert(doDelete == false || doAdd == true);
/* create a data source for the filename, if needed */
if (doAdd || doUpdate) {
Assert(newCapacity);
err = Nu_DataSourceBuffer_New(kNuThreadFormatUncompressed,
newCapacity, (const uint8_t*)strdup(pathnameMOR), 0,
requiredCapacity /*(strlen)*/, Nu_InternalFreeCallback,
&pDataSource);
BailError(err);
}
if (doDelete) {
err = Nu_ThreadModDelete_New(pArchive, pFilenameThread->threadIdx,
kNuThreadIDFilename, &pNewThreadMod);
BailError(err);
Nu_RecordAddThreadMod(pRecord, pNewThreadMod);
pNewThreadMod = NULL; /* successful, don't free */
}
if (doAdd) {
err = Nu_ThreadModAdd_New(pArchive, kNuThreadIDFilename,
kNuThreadFormatUncompressed, pDataSource, &pNewThreadMod);
BailError(err);
/*pDataSource = NULL;*/ /* ThreadModAdd_New makes a copy */
Nu_RecordAddThreadMod(pRecord, pNewThreadMod);
pNewThreadMod = NULL; /* successful, don't free */
}
if (doUpdate) {
err = Nu_ThreadModUpdate_New(pArchive, pFilenameThread->threadIdx,
pDataSource, &pNewThreadMod);
BailError(err);
/*pDataSource = NULL;*/ /* ThreadModAdd_New makes a copy */
Nu_RecordAddThreadMod(pRecord, pNewThreadMod);
pNewThreadMod = NULL; /* successful, don't free */
}
DBUG(("--- renaming '%s' to '%s' with delete=%d add=%d update=%d\n",
pRecord->filenameMOR, pathnameMOR, doDelete, doAdd, doUpdate));
/*
* Update the fssep, if necessary. (This is slightly silly -- we
* have to rewrite the record header anyway since we're changing
* threads around.)
*/
if (NuGetSepFromSysInfo(pRecord->recFileSysInfo) != fssepMOR) {
DBUG(("--- and updating the fssep\n"));
pRecord->recFileSysInfo = NuSetSepInSysInfo(pRecord->recFileSysInfo,
fssepMOR);
pRecord->dirtyHeader = true;
}
/* if we had a header filename, mark it for oblivion */
if (pFilenameThread == NULL) {
DBUG(("+++ rename gonna drop the filename\n"));
pRecord->dropRecFilename = true;
}
bail:
Nu_ThreadModFree(pArchive, pNewThreadMod);
Nu_DataSourceFree(pDataSource);
return err;
}
/*
* Update a record's attributes with the contents of pRecordAttr.
*/
NuError Nu_SetRecordAttr(NuArchive* pArchive, NuRecordIdx recordIdx,
const NuRecordAttr* pRecordAttr)
{
NuError err;
NuRecord* pRecord;
if (pRecordAttr == NULL)
return kNuErrInvalidArg;
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/* pull the record out of the "copy" set */
err = Nu_FindRecordForWriteByIdx(pArchive, recordIdx, &pRecord);
BailError(err);
Assert(pRecord != NULL);
pRecord->recFileSysID = pRecordAttr->fileSysID;
/*pRecord->recFileSysInfo = pRecordAttr->fileSysInfo;*/
pRecord->recAccess = pRecordAttr->access;
pRecord->recFileType = pRecordAttr->fileType;
pRecord->recExtraType = pRecordAttr->extraType;
pRecord->recCreateWhen = pRecordAttr->createWhen;
pRecord->recModWhen = pRecordAttr->modWhen;
pRecord->recArchiveWhen = pRecordAttr->archiveWhen;
pRecord->dirtyHeader = true;
bail:
return err;
}
/*
* Bulk-delete several records, using the selection filter callback.
*/
NuError Nu_Delete(NuArchive* pArchive)
{
NuError err;
NuSelectionProposal selProposal;
NuRecord* pNextRecord;
NuRecord* pRecord;
NuResult result;
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
/*
* If we don't yet have a copy set, make one.
*/
if (!Nu_RecordSet_GetLoaded(&pArchive->copyRecordSet)) {
err = Nu_RecordSet_Clone(pArchive, &pArchive->copyRecordSet,
&pArchive->origRecordSet);
BailError(err);
}
/*
* Run through the copy set. This is different from most other
* operations, which run through the "orig" set. However, since
* we're not interested in allowing the user to delete things that
* have already been deleted, we might as well use this set.
*/
pNextRecord = Nu_RecordSet_GetListHead(&pArchive->copyRecordSet);
while (pNextRecord != NULL) {
pRecord = pNextRecord;
pNextRecord = pRecord->pNext;
/*
* Deletion of modified records (thread adds, deletes, or updates)
* isn't allowed. There's no point in showing the record to the
* user.
*/
if (pRecord->pThreadMods != NULL) {
DBUG(("+++ Skipping delete on a modified record\n"));
continue;
}
/*
* If a selection filter is defined, allow the user the opportunity
* to select which files will be deleted, or abort the entire
* operation.
*/
if (pArchive->selectionFilterFunc != NULL) {
selProposal.pRecord = pRecord;
selProposal.pThread = pRecord->pThreads; /* doesn't matter */
result = (*pArchive->selectionFilterFunc)(pArchive, &selProposal);
if (result == kNuSkip)
continue;
if (result == kNuAbort) {
err = kNuErrAborted;
goto bail;
}
}
/*
* Do we want to allow this? (Same test as for DeleteRecord.)
*/
if (pRecord->pThreadMods != NULL || pRecord->dirtyHeader) {
DBUG(("--- Tried to delete a modified record\n"));
err = kNuErrModRecChange;
goto bail;
}
err = Nu_RecordSet_DeleteRecord(pArchive, &pArchive->copyRecordSet,
pRecord);
BailError(err);
}
bail:
return err;
}
/*
* Delete an entire record.
*/
NuError Nu_DeleteRecord(NuArchive* pArchive, NuRecordIdx recIdx)
{
NuError err;
NuRecord* pRecord;
if (Nu_IsReadOnly(pArchive))
return kNuErrArchiveRO;
err = Nu_GetTOCIfNeeded(pArchive);
BailError(err);
err = Nu_FindRecordForWriteByIdx(pArchive, recIdx, &pRecord);
BailError(err);
/*
* Deletion of modified records (thread adds, deletes, or updates) isn't
* allowed. It probably wouldn't be hard to handle, but it's pointless.
* Preventing the action maintains our general semantics of disallowing
* conflicting actions on the same object.
*
* We also block it if the header is dirty (e.g. they changed the
* record's filetype). This isn't necessary for correct operation,
* but again it maintains the semantics.
*/
if (pRecord->pThreadMods != NULL || pRecord->dirtyHeader) {
DBUG(("--- Tried to delete a modified record\n"));
err = kNuErrModRecChange;
goto bail;
}
err = Nu_RecordSet_DeleteRecord(pArchive,&pArchive->copyRecordSet, pRecord);
BailError(err);
bail:
return err;
}