mirror of
https://github.com/fadden/ciderpress.git
synced 2024-11-26 02:49:20 +00:00
84706d7ea4
This integrates the latest NufxLib sources, and updates CiderPress to work with the API changes.
2826 lines
87 KiB
C
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;
|
|
}
|
|
|