/*
 * 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;
}