mirror of
https://github.com/fadden/nulib2.git
synced 2024-11-19 14:33:47 +00:00
e2088e64d3
NufxLib has historically made no effort to distinguish between the character set used for filenames on the local disk, and for filenames stored within the archive. Now all Unicode filename strings use the UNICHAR type and have "UNI" in the name, and all Mac OS Roman strings have "MOR" in the name. (The naming convention makes it obvious when you're assigning the wrong thing; on Linux both formats are char*, so the compiler won't tell you if you get it wrong.) The distinction is necessary because filesystems generally support Unicode these days, but on Windows you need to use a separate set of wide-character file I/O functions. (On Linux it all works with "narrow" strings, and the UTF-8 encoding is interpreted by applications.) The character set used for NuFX archive filenames is MOR, matching what GS/OS + HFS supported, and we want to be able to convert back and forth between MOR and a Unicode representation. This change updates the various character types and string names, adds conversion functions, and updates NuLib2 for proper execution on Linux. It does not include the (probably extensive) changes required for Windows UTF-16 support. Instead, the conversion functions are no-ops, which should result in NuLib2 for Windows continuing to behave in the same slightly broken way. This adds "test-names", which exercises Unicode filenames a bit. It will not pass on Win32. Also, tweaked the Linux makefiles to have explicit dependencies, rather than empty space and an expectation that "makedepend" exists. Also, minor source code cleanups. While this probably doesn't affect binary compatibility -- it's mainly a matter of naming and string interpretation -- there's enough going on that it should be considered an API revision, so this updates the version to 3.0.0.
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;
|
|
}
|
|
|