mirror of
https://github.com/fadden/nulib2.git
synced 2024-12-29 00:31:43 +00:00
3841a233b2
threads by giving them an empty data fork (and, if it's an extended file, an empty resource fork). This allows applications to simply ignore the bogus records and let NufxLib do the dirty work. Fixed dataless thread handling for the original (non-masked) case. We weren't creating empty resource forks when required, so a dataless record with a forked-file storage type wouldn't be recreated properly if extracted and then added. Added version numbers to the public header, so applications can test for "compiled" version vs "linked" version.
1159 lines
34 KiB
C
1159 lines
34 KiB
C
/*
|
|
* NuFX archive manipulation library
|
|
* Copyright (C) 2000-2002 by Andy McFadden, All Rights Reserved.
|
|
* This is free software; you can redistribute it and/or modify it under the
|
|
* terms of the GNU Library General Public License, see the file COPYING-LIB.
|
|
*
|
|
* Archive structure creation and manipulation.
|
|
*/
|
|
#include "NufxLibPriv.h"
|
|
|
|
#ifdef HAVE_FCNTL_H
|
|
# include <fcntl.h>
|
|
#endif
|
|
#ifndef O_BINARY
|
|
# define O_BINARY 0
|
|
#endif
|
|
|
|
/* master header identification */
|
|
static const uchar kNuMasterID[kNufileIDLen] =
|
|
{ 0x4e, 0xf5, 0x46, 0xe9, 0x6c, 0xe5 };
|
|
|
|
/* other identification; can be no longer than kNufileIDLen */
|
|
static const uchar kNuBinary2ID[] =
|
|
{ 0x0a, 0x47, 0x4c };
|
|
static const uchar kNuSHKSEAID[] =
|
|
{ 0xa2, 0x2e, 0x00 };
|
|
|
|
/*
|
|
* Offsets to some interesting places in the wrappers.
|
|
*/
|
|
#define kNuBNYFileSizeLo 8 /* file size in 512-byte blocks (2B) */
|
|
#define kNuBNYFileSizeHi 114 /* ... (2B) */
|
|
#define kNuBNYEOFLo 20 /* file size in bytes (3B) */
|
|
#define kNuBNYEOFHi 116 /* ... (1B) */
|
|
#define kNuBNYDiskSpace 117 /* total space req'd; equiv FileSize (4B) */
|
|
#define kNuBNYFilesToFollow 127 /* (1B) #of files in rest of BNY file */
|
|
#define kNuSEAFunkySize 11938 /* length of archive + 68 (4B?) */
|
|
#define kNuSEAFunkyAdjust 68 /* ... adjustment to "FunkySize" */
|
|
#define kNuSEALength1 11946 /* length of archive (4B?) */
|
|
#define kNuSEALength2 12001 /* length of archive (4B?) */
|
|
|
|
|
|
static void Nu_CloseAndFree(NuArchive* pArchive);
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Archive and MasterHeader utility functions
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Allocate and initialize a new NuArchive structure.
|
|
*/
|
|
static NuError
|
|
Nu_NuArchiveNew(NuArchive** ppArchive)
|
|
{
|
|
Assert(ppArchive != nil);
|
|
|
|
/* validate some assumptions we make throughout the code */
|
|
Assert(sizeof(int) >= 2);
|
|
Assert(sizeof(ushort) >= 2);
|
|
Assert(sizeof(ulong) >= 4);
|
|
Assert(sizeof(void*) >= sizeof(NuArchive*));
|
|
|
|
*ppArchive = Nu_Calloc(nil, sizeof(**ppArchive));
|
|
if (*ppArchive == nil)
|
|
return kNuErrMalloc;
|
|
|
|
(*ppArchive)->structMagic = kNuArchiveStructMagic;
|
|
|
|
(*ppArchive)->recordIdxSeed = 1000; /* could be a random number */
|
|
(*ppArchive)->nextRecordIdx = (*ppArchive)->recordIdxSeed;
|
|
|
|
/*
|
|
* Initialize assorted values to defaults. We don't try to do any
|
|
* system-specific values here; it's up to the application to decide
|
|
* what is most appropriate for the current system.
|
|
*/
|
|
(*ppArchive)->valIgnoreCRC = false;
|
|
#ifdef ENABLE_LZW
|
|
(*ppArchive)->valDataCompression = kNuCompressLZW2;
|
|
#else
|
|
(*ppArchive)->valDataCompression = kNuCompressNone;
|
|
#endif
|
|
(*ppArchive)->valDiscardWrapper = false;
|
|
(*ppArchive)->valEOL = kNuEOLLF; /* non-UNIX apps must override */
|
|
(*ppArchive)->valConvertExtractedEOL = kNuConvertOff;
|
|
(*ppArchive)->valOnlyUpdateOlder = false;
|
|
(*ppArchive)->valAllowDuplicates = false;
|
|
(*ppArchive)->valHandleExisting = kNuMaybeOverwrite;
|
|
(*ppArchive)->valModifyOrig = false;
|
|
(*ppArchive)->valMimicSHK = false;
|
|
(*ppArchive)->valMaskDataless = false;
|
|
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* Free up a NuArchive structure and its contents.
|
|
*/
|
|
static NuError
|
|
Nu_NuArchiveFree(NuArchive* pArchive)
|
|
{
|
|
Assert(pArchive != nil);
|
|
Assert(pArchive->structMagic == kNuArchiveStructMagic);
|
|
|
|
(void) Nu_RecordSet_FreeAllRecords(pArchive, &pArchive->origRecordSet);
|
|
pArchive->haveToc = false;
|
|
(void) Nu_RecordSet_FreeAllRecords(pArchive, &pArchive->copyRecordSet);
|
|
(void) Nu_RecordSet_FreeAllRecords(pArchive, &pArchive->newRecordSet);
|
|
|
|
Nu_Free(nil, pArchive->archivePathname);
|
|
Nu_Free(nil, pArchive->tmpPathname);
|
|
Nu_Free(nil, pArchive->compBuf);
|
|
Nu_Free(nil, pArchive->lzwCompressState);
|
|
Nu_Free(nil, pArchive->lzwExpandState);
|
|
|
|
/* mark it as deceased to prevent further use, then free it */
|
|
pArchive->structMagic = kNuArchiveStructMagic ^ 0xffffffff;
|
|
Nu_Free(nil, pArchive);
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Copy a NuMasterHeader struct.
|
|
*/
|
|
void
|
|
Nu_MasterHeaderCopy(NuArchive* pArchive, NuMasterHeader* pDstHeader,
|
|
const NuMasterHeader* pSrcHeader)
|
|
{
|
|
Assert(pArchive != nil);
|
|
Assert(pDstHeader != nil);
|
|
Assert(pSrcHeader != nil);
|
|
|
|
*pDstHeader = *pSrcHeader;
|
|
}
|
|
|
|
/*
|
|
* Get a pointer to the archive master header (this is an API call).
|
|
*/
|
|
NuError
|
|
Nu_GetMasterHeader(NuArchive* pArchive, const NuMasterHeader** ppMasterHeader)
|
|
{
|
|
if (ppMasterHeader == nil)
|
|
return kNuErrInvalidArg;
|
|
|
|
*ppMasterHeader = &pArchive->masterHeader;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Allocate the general-purpose compression buffer, if needed.
|
|
*/
|
|
NuError
|
|
Nu_AllocCompressionBufferIFN(NuArchive* pArchive)
|
|
{
|
|
Assert(pArchive != nil);
|
|
|
|
if (pArchive->compBuf != nil)
|
|
return kNuErrNone;
|
|
|
|
pArchive->compBuf = Nu_Malloc(pArchive, kNuGenCompBufSize);
|
|
if (pArchive->compBuf == nil)
|
|
return kNuErrMalloc;
|
|
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Return a unique value.
|
|
*/
|
|
NuRecordIdx
|
|
Nu_GetNextRecordIdx(NuArchive* pArchive)
|
|
{
|
|
return pArchive->nextRecordIdx++;
|
|
}
|
|
|
|
/*
|
|
* Return a unique value.
|
|
*/
|
|
NuThreadIdx
|
|
Nu_GetNextThreadIdx(NuArchive* pArchive)
|
|
{
|
|
return pArchive->nextRecordIdx++; /* just use the record counter */
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Wrapper (SEA, BXY, BSE) functions
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Copy the wrapper from the archive file to the temp file.
|
|
*/
|
|
NuError
|
|
Nu_CopyWrapperToTemp(NuArchive* pArchive)
|
|
{
|
|
NuError err;
|
|
|
|
Assert(pArchive->headerOffset); /* no wrapper to copy?? */
|
|
|
|
err = Nu_FSeek(pArchive->archiveFp, 0, SEEK_SET);
|
|
BailError(err);
|
|
err = Nu_FSeek(pArchive->tmpFp, 0, SEEK_SET);
|
|
BailError(err);
|
|
err = Nu_CopyFileSection(pArchive, pArchive->tmpFp,
|
|
pArchive->archiveFp, pArchive->headerOffset);
|
|
BailError(err);
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Fix up the wrapper. The SEA and BXY headers have some fields
|
|
* set according to file length and archive attributes.
|
|
*
|
|
* Pass in the file pointer that will be written to. Wrappers are
|
|
* assumed to start at offset 0.
|
|
*
|
|
* Wrappers must appear in this order:
|
|
* Binary II
|
|
* ShrinkIt SEA (Self-Extracting Archive)
|
|
*
|
|
* If they didn't, we wouldn't be this far.
|
|
*
|
|
* I have a Binary II specification, but don't have one for SEA, so I'm
|
|
* making educated guesses based on the differences between archives. I'd
|
|
* guess some of the SEA weirdness stems from some far-sighted support
|
|
* for multiple archives within a single SEA wrapper.
|
|
*/
|
|
NuError
|
|
Nu_UpdateWrapper(NuArchive* pArchive, FILE* fp)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
Boolean hasBinary2, hasSea;
|
|
uchar identBuf[kNufileIDLen];
|
|
ulong archiveLen, archiveLen512;
|
|
|
|
Assert(pArchive->newMasterHeader.isValid); /* need new crc and len */
|
|
|
|
hasBinary2 = hasSea = false;
|
|
|
|
switch (pArchive->archiveType) {
|
|
case kNuArchiveNuFX:
|
|
goto bail;
|
|
case kNuArchiveNuFXInBNY:
|
|
hasBinary2 = true;
|
|
break;
|
|
case kNuArchiveNuFXSelfEx:
|
|
hasSea = true;
|
|
break;
|
|
case kNuArchiveNuFXSelfExInBNY:
|
|
hasBinary2 = hasSea = true;
|
|
break;
|
|
default:
|
|
if (pArchive->headerOffset) {
|
|
Nu_ReportError(NU_BLOB, kNuErrNone, "Can't fix the wrapper??");
|
|
err = kNuErrInternal;
|
|
goto bail;
|
|
} else
|
|
goto bail;
|
|
}
|
|
|
|
err = Nu_FSeek(fp, 0, SEEK_SET);
|
|
BailError(err);
|
|
|
|
if (hasBinary2) {
|
|
/* sanity check - make sure it's Binary II */
|
|
Nu_ReadBytes(pArchive, fp, identBuf, kNufileIDLen);
|
|
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed reading BNY wrapper");
|
|
goto bail;
|
|
}
|
|
if (memcmp(identBuf, kNuBinary2ID, sizeof(kNuBinary2ID)) != 0) {
|
|
err = kNuErrInternal;
|
|
Nu_ReportError(NU_BLOB, kNuErrNone,"Didn't find Binary II wrapper");
|
|
goto bail;
|
|
}
|
|
|
|
/* archiveLen includes the SEA wrapper, if any */
|
|
archiveLen = pArchive->newMasterHeader.mhMasterEOF +
|
|
(pArchive->headerOffset - kNuBinary2BlockSize);
|
|
archiveLen512 = (archiveLen + 511) / 512;
|
|
|
|
err = Nu_FSeek(fp, kNuBNYFileSizeLo - kNufileIDLen, SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteTwo(pArchive, fp, (ushort)(archiveLen512 & 0xffff));
|
|
|
|
err = Nu_FSeek(fp, kNuBNYFileSizeHi - (kNuBNYFileSizeLo+2), SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteTwo(pArchive, fp, (ushort)(archiveLen512 >> 16));
|
|
|
|
err = Nu_FSeek(fp, kNuBNYEOFLo - (kNuBNYFileSizeHi+2), SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteTwo(pArchive, fp, (ushort)(archiveLen & 0xffff));
|
|
Nu_WriteOne(pArchive, fp, (uchar)((archiveLen >> 16) & 0xff));
|
|
|
|
err = Nu_FSeek(fp, kNuBNYEOFHi - (kNuBNYEOFLo+3), SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteOne(pArchive, fp, (uchar)(archiveLen >> 24));
|
|
|
|
err = Nu_FSeek(fp, kNuBNYDiskSpace - (kNuBNYEOFHi+1), SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteFour(pArchive, fp, archiveLen512);
|
|
|
|
/* probably ought to update "modified when" date/time field */
|
|
|
|
/* seek just past end of BNY wrapper */
|
|
err = Nu_FSeek(fp, kNuBinary2BlockSize - (kNuBNYDiskSpace+4), SEEK_CUR);
|
|
BailError(err);
|
|
|
|
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed updating Binary II wrapper");
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
if (hasSea) {
|
|
/* sanity check - make sure it's SEA */
|
|
Nu_ReadBytes(pArchive, fp, identBuf, kNufileIDLen);
|
|
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed reading SEA wrapper");
|
|
goto bail;
|
|
}
|
|
if (memcmp(identBuf, kNuSHKSEAID, sizeof(kNuSHKSEAID)) != 0) {
|
|
err = kNuErrInternal;
|
|
Nu_ReportError(NU_BLOB, kNuErrNone, "Didn't find SEA wrapper");
|
|
goto bail;
|
|
}
|
|
|
|
archiveLen = pArchive->newMasterHeader.mhMasterEOF;
|
|
|
|
err = Nu_FSeek(fp, kNuSEAFunkySize - kNufileIDLen, SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteFour(pArchive, fp, archiveLen + kNuSEAFunkyAdjust);
|
|
|
|
err = Nu_FSeek(fp, kNuSEALength1 - (kNuSEAFunkySize+4), SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteTwo(pArchive, fp, (ushort)archiveLen);
|
|
|
|
err = Nu_FSeek(fp, kNuSEALength2 - (kNuSEALength1+2), SEEK_CUR);
|
|
BailError(err);
|
|
Nu_WriteTwo(pArchive, fp, (ushort)archiveLen);
|
|
|
|
/* seek past end of SEA wrapper */
|
|
err = Nu_FSeek(fp, kNuSEAOffset - (kNuSEALength2+2), SEEK_CUR);
|
|
BailError(err);
|
|
|
|
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed updating SEA wrapper");
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
bail:
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Adjust wrapper-induced padding on the archive.
|
|
*
|
|
* GS/ShrinkIt v1.1 does some peculiar things with SEA (Self-Extracting
|
|
* Archive) files. For no apparent reason, it always adds one extra 00
|
|
* byte to the end. When you combine SEA and BXY to make BSE, it will
|
|
* leave that extra byte inside the BXY 128-byte padding area, UNLESS
|
|
* the archive itself happens to be exactly 128 bytes, in which case
|
|
* it throws the pad byte onto the end -- resulting in an archive that
|
|
* isn't an exact multiple of 128.
|
|
*
|
|
* I've chosen to emulate the 1-byte padding "feature" of GSHK, but I'm
|
|
* not going to try to emulate the quirky behavior described above.
|
|
*
|
|
* The SEA pad byte is added first, and then the 128-byte BXY padding
|
|
* is considered. In the odd case described above, the file would be
|
|
* 127 bytes larger with nufxlib than it is with GSHK. This shouldn't
|
|
* require additional disk space to be used, assuming a filesystem block
|
|
* size of at least 128 bytes.
|
|
*/
|
|
NuError
|
|
Nu_AdjustWrapperPadding(NuArchive* pArchive, FILE* fp)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
Boolean hasBinary2, hasSea;
|
|
|
|
hasBinary2 = hasSea = false;
|
|
|
|
switch (pArchive->archiveType) {
|
|
case kNuArchiveNuFX:
|
|
goto bail;
|
|
case kNuArchiveNuFXInBNY:
|
|
hasBinary2 = true;
|
|
break;
|
|
case kNuArchiveNuFXSelfEx:
|
|
hasSea = true;
|
|
break;
|
|
case kNuArchiveNuFXSelfExInBNY:
|
|
hasBinary2 = hasSea = true;
|
|
break;
|
|
default:
|
|
if (pArchive->headerOffset) {
|
|
Nu_ReportError(NU_BLOB, kNuErrNone, "Can't check the padding??");
|
|
err = kNuErrInternal;
|
|
goto bail;
|
|
} else
|
|
goto bail;
|
|
}
|
|
|
|
err = Nu_FSeek(fp, 0, SEEK_END);
|
|
BailError(err);
|
|
|
|
if (hasSea && pArchive->valMimicSHK) {
|
|
/* throw on a single pad byte, for no apparent reason whatsoever */
|
|
Nu_WriteOne(pArchive, fp, 0);
|
|
}
|
|
|
|
if (hasBinary2) {
|
|
/* pad out to the next 128-byte boundary */
|
|
long curOffset;
|
|
|
|
err = Nu_FTell(fp, &curOffset);
|
|
BailError(err);
|
|
|
|
if (curOffset & 0x7f) {
|
|
int i;
|
|
|
|
for (i = kNuBinary2BlockSize - (curOffset & 0x7f); i > 0; i--)
|
|
Nu_WriteOne(pArchive, fp, 0);
|
|
}
|
|
}
|
|
|
|
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed updating wrapper padding");
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Open an archive
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Read the master header from the archive file.
|
|
*
|
|
* This also handles skipping the first 128 bytes of a .BXY file and the
|
|
* front part of a self-extracting GSHK archive.
|
|
*
|
|
* We try to provide helpful messages about things that aren't archives,
|
|
* but try to stay silent about files that are other types of archives.
|
|
* That way, if the application is trying a series of libraries to find
|
|
* one that will accept the file, we don't generate spurious complaints.
|
|
*
|
|
* Since there's a fair possibility that whoever is opening this file is
|
|
* also interested in related formats, we try to return a meaningful error
|
|
* code for stuff we recognize (especially Binary II).
|
|
*
|
|
* On exit, the stream will be positioned just past the master header.
|
|
*/
|
|
static NuError
|
|
Nu_ReadMasterHeader(NuArchive* pArchive)
|
|
{
|
|
NuError err;
|
|
ushort crc;
|
|
FILE* fp;
|
|
NuMasterHeader* pHeader;
|
|
Boolean isBinary2 = false;
|
|
Boolean isSea = false;
|
|
|
|
Assert(pArchive != nil);
|
|
|
|
fp = pArchive->archiveFp; /* saves typing */
|
|
pHeader = &pArchive->masterHeader;
|
|
|
|
pArchive->headerOffset = 0;
|
|
Nu_ReadBytes(pArchive, fp, pHeader->mhNufileID, kNufileIDLen);
|
|
/* may have read fewer than kNufileIDLen; that's okay */
|
|
|
|
if (memcmp(pHeader->mhNufileID, kNuBinary2ID, sizeof(kNuBinary2ID)) == 0)
|
|
{
|
|
int count;
|
|
|
|
/* looks like a Binary II archive, might be BXY or BSE; seek forward */
|
|
err = Nu_SeekArchive(pArchive, fp, kNuBNYFilesToFollow - kNufileIDLen,
|
|
SEEK_CUR);
|
|
if (err != kNuErrNone) {
|
|
err = kNuErrNotNuFX;
|
|
/* probably too short to be BNY, so go ahead and whine */
|
|
Nu_ReportError(NU_BLOB, kNuErrNone,
|
|
"Looks like a truncated Binary II archive?");
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Check "files to follow", so we can be sure this isn't a BNY that
|
|
* just happened to have a .SHK as the first file. If it is, then
|
|
* any updates to the archive will trash the rest of the BNY files.
|
|
*/
|
|
count = Nu_ReadOne(pArchive, fp);
|
|
if (count != 0) {
|
|
err = kNuErrIsBinary2;
|
|
/*Nu_ReportError(NU_BLOB, kNuErrNone,
|
|
"This is a Binary II archive with %d files in it", count+1);*/
|
|
DBUG(("This is a Binary II archive with %d files in it\n",count+1));
|
|
goto bail;
|
|
}
|
|
|
|
/* that was last item in BNY header, no need to seek */
|
|
Assert(kNuBNYFilesToFollow == kNuBinary2BlockSize -1);
|
|
|
|
isBinary2 = true;
|
|
pArchive->headerOffset += kNuBinary2BlockSize;
|
|
Nu_ReadBytes(pArchive, fp, pHeader->mhNufileID, kNufileIDLen);
|
|
}
|
|
if (memcmp(pHeader->mhNufileID, kNuSHKSEAID, sizeof(kNuSHKSEAID)) == 0)
|
|
{
|
|
/* might be GSHK self-extracting; seek forward */
|
|
err = Nu_SeekArchive(pArchive, fp, kNuSEAOffset - kNufileIDLen,
|
|
SEEK_CUR);
|
|
if (err != kNuErrNone) {
|
|
err = kNuErrNotNuFX;
|
|
Nu_ReportError(NU_BLOB, kNuErrNone,
|
|
"Looks like GS executable, not NuFX");
|
|
goto bail;
|
|
}
|
|
|
|
isSea = true;
|
|
pArchive->headerOffset += kNuSEAOffset;
|
|
Nu_ReadBytes(pArchive, fp, pHeader->mhNufileID, kNufileIDLen);
|
|
}
|
|
|
|
if (memcmp(kNuMasterID, pHeader->mhNufileID, kNufileIDLen) != 0) {
|
|
err = kNuErrNotNuFX;
|
|
|
|
if (isBinary2) {
|
|
err = kNuErrIsBinary2;
|
|
/*Nu_ReportError(NU_BLOB, kNuErrNone,
|
|
"Looks like Binary II, not NuFX");*/
|
|
DBUG(("Looks like Binary II, not NuFX\n"));
|
|
} else if (isSea)
|
|
Nu_ReportError(NU_BLOB, kNuErrNone,
|
|
"Looks like GS executable, not NuFX");
|
|
else if (Nu_HeaderIOFailed(pArchive, fp) != kNuErrNone)
|
|
Nu_ReportError(NU_BLOB, kNuErrNone,
|
|
"Couldn't read enough data, not NuFX?");
|
|
else
|
|
Nu_ReportError(NU_BLOB, kNuErrNone,
|
|
"Not a NuFX archive? Got 0x%02x%02x%02x%02x%02x%02x...",
|
|
pHeader->mhNufileID[0], pHeader->mhNufileID[1],
|
|
pHeader->mhNufileID[2], pHeader->mhNufileID[3],
|
|
pHeader->mhNufileID[4], pHeader->mhNufileID[5]);
|
|
goto bail;
|
|
}
|
|
|
|
crc = 0;
|
|
pHeader->mhMasterCRC = Nu_ReadTwo(pArchive, fp);
|
|
pHeader->mhTotalRecords = Nu_ReadFourC(pArchive, fp, &crc);
|
|
pHeader->mhArchiveCreateWhen = Nu_ReadDateTimeC(pArchive, fp, &crc);
|
|
pHeader->mhArchiveModWhen = Nu_ReadDateTimeC(pArchive, fp, &crc);
|
|
pHeader->mhMasterVersion = Nu_ReadTwoC(pArchive, fp, &crc);
|
|
Nu_ReadBytesC(pArchive, fp, pHeader->mhReserved1,
|
|
kNufileMasterReserved1Len, &crc);
|
|
pHeader->mhMasterEOF = Nu_ReadFourC(pArchive, fp, &crc);
|
|
Nu_ReadBytesC(pArchive, fp, pHeader->mhReserved2,
|
|
kNufileMasterReserved2Len, &crc);
|
|
|
|
/* check for errors in any of the above reads */
|
|
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed reading master header");
|
|
goto bail;
|
|
}
|
|
if (pHeader->mhMasterVersion > kNuMaxMHVersion) {
|
|
err = kNuErrBadMHVersion;
|
|
Nu_ReportError(NU_BLOB, err, "Bad MH version %u",
|
|
pHeader->mhMasterVersion);
|
|
goto bail;
|
|
}
|
|
|
|
/* compare the CRC */
|
|
if (!pArchive->valIgnoreCRC && crc != pHeader->mhMasterCRC) {
|
|
if (!Nu_ShouldIgnoreBadCRC(pArchive, nil, kNuErrBadMHCRC)) {
|
|
err = kNuErrBadMHCRC;
|
|
Nu_ReportError(NU_BLOB, err, "Stored MH CRC=0x%04x, calc=0x%04x",
|
|
pHeader->mhMasterCRC, crc);
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Check for an unusual condition. GS/ShrinkIt appears to update
|
|
* the archive structure in the disk file periodically as it writes,
|
|
* so it's possible to get an apparently complete archive (with
|
|
* correct CRCs in the master and record headers!) that is actually
|
|
* only partially written. I did this by accident when archiving a
|
|
* 3.5" disk across a slow AppleTalk network. The only obvious
|
|
* indication of brain-damage, until you try to unpack the archive,
|
|
* seems to be a bogus MasterEOF==48.
|
|
*/
|
|
if (pHeader->mhMasterEOF <= kNuMasterHeaderSize) {
|
|
err = kNuErrNoRecords;
|
|
Nu_ReportError(NU_BLOB, err,
|
|
"Master EOF is %ld, archive is probably truncated",
|
|
pHeader->mhMasterEOF);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Set up a few things in the archive structure on our way out.
|
|
*/
|
|
if (isBinary2) {
|
|
if (isSea)
|
|
pArchive->archiveType = kNuArchiveNuFXSelfExInBNY;
|
|
else
|
|
pArchive->archiveType = kNuArchiveNuFXInBNY;
|
|
} else {
|
|
if (isSea)
|
|
pArchive->archiveType = kNuArchiveNuFXSelfEx;
|
|
else
|
|
pArchive->archiveType = kNuArchiveNuFX;
|
|
}
|
|
|
|
if (isSea || isBinary2) {
|
|
DBUG(("--- Archive isSea=%d isBinary2=%d type=%d\n",
|
|
isSea, isBinary2, pArchive->archiveType));
|
|
}
|
|
|
|
/*pArchive->origNumRecords = pHeader->mhTotalRecords;*/
|
|
pArchive->currentOffset = pArchive->headerOffset + kNuMasterHeaderSize;
|
|
|
|
/*DBUG(("--- GOT: records=%ld, vers=%d, EOF=%ld, type=%d, hdrOffset=%ld\n",
|
|
pHeader->mhTotalRecords, pHeader->mhMasterVersion,
|
|
pHeader->mhMasterEOF, pArchive->archiveType, pArchive->headerOffset));*/
|
|
|
|
pHeader->isValid = true;
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Prepare the NuArchive and NuMasterHeader structures for use with a
|
|
* newly-created archive.
|
|
*/
|
|
static void
|
|
Nu_InitNewArchive(NuArchive* pArchive)
|
|
{
|
|
NuMasterHeader* pHeader;
|
|
|
|
Assert(pArchive != nil);
|
|
|
|
pHeader = &pArchive->masterHeader;
|
|
|
|
memcpy(pHeader->mhNufileID, kNuMasterID, kNufileIDLen);
|
|
/*pHeader->mhMasterCRC*/
|
|
pHeader->mhTotalRecords = 0;
|
|
Nu_SetCurrentDateTime(&pHeader->mhArchiveCreateWhen);
|
|
/*pHeader->mhArchiveModWhen*/
|
|
pHeader->mhMasterVersion = kNuOurMHVersion;
|
|
/*pHeader->mhReserved1*/
|
|
pHeader->mhMasterEOF = kNuMasterHeaderSize;
|
|
/*pHeader->mhReserved2*/
|
|
|
|
pHeader->isValid = true;
|
|
|
|
/* no need to use a temp file for a newly-created archive */
|
|
pArchive->valModifyOrig = true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Open an archive in streaming read-only mode.
|
|
*/
|
|
NuError
|
|
Nu_StreamOpenRO(FILE* infp, NuArchive** ppArchive)
|
|
{
|
|
NuError err;
|
|
NuArchive* pArchive = nil;
|
|
|
|
Assert(infp != nil);
|
|
Assert(ppArchive != nil);
|
|
|
|
err = Nu_NuArchiveNew(ppArchive);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
pArchive = *ppArchive;
|
|
|
|
pArchive->openMode = kNuOpenStreamingRO;
|
|
pArchive->archiveFp = infp;
|
|
pArchive->archivePathname = strdup("(stream)");
|
|
|
|
err = Nu_ReadMasterHeader(pArchive);
|
|
BailError(err);
|
|
|
|
bail:
|
|
if (err != kNuErrNone) {
|
|
if (pArchive != nil)
|
|
(void) Nu_NuArchiveFree(pArchive);
|
|
*ppArchive = nil;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Open an archive in non-streaming read-only mode.
|
|
*/
|
|
NuError
|
|
Nu_OpenRO(const char* archivePathname, NuArchive** ppArchive)
|
|
{
|
|
NuError err;
|
|
NuArchive* pArchive = nil;
|
|
FILE* fp = nil;
|
|
|
|
if (archivePathname == nil || !strlen(archivePathname) || ppArchive == nil)
|
|
return kNuErrInvalidArg;
|
|
|
|
*ppArchive = nil;
|
|
|
|
fp = fopen(archivePathname, kNuFileOpenReadOnly);
|
|
if (fp == nil) {
|
|
Nu_ReportError(NU_BLOB, errno, "Unable to open '%s'", archivePathname);
|
|
err = kNuErrFileOpen;
|
|
goto bail;
|
|
}
|
|
|
|
err = Nu_NuArchiveNew(ppArchive);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
pArchive = *ppArchive;
|
|
|
|
pArchive->openMode = kNuOpenRO;
|
|
pArchive->archiveFp = fp;
|
|
fp = nil;
|
|
pArchive->archivePathname = strdup(archivePathname);
|
|
|
|
err = Nu_ReadMasterHeader(pArchive);
|
|
BailError(err);
|
|
|
|
bail:
|
|
if (err != kNuErrNone) {
|
|
if (pArchive != nil) {
|
|
(void) Nu_CloseAndFree(pArchive);
|
|
*ppArchive = nil;
|
|
}
|
|
if (fp != nil)
|
|
fclose(fp);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Open a temp file. If "fileName" contains six Xs ("XXXXXX"), it will
|
|
* be treated as a mktemp-style template, and modified before use.
|
|
*/
|
|
static NuError
|
|
Nu_OpenTempFile(char* fileName, FILE** pFp)
|
|
{
|
|
NuArchive* pArchive = nil; /* dummy for NU_BLOB */
|
|
NuError err = kNuErrNone;
|
|
int len;
|
|
|
|
/*
|
|
* If this is a mktemp-style template, use mktemp or mkstemp to fill in
|
|
* the blanks.
|
|
*
|
|
* BUG: not all implementations of mktemp actually generate a unique
|
|
* name. We probably need to do probing here. Some BSD variants like
|
|
* to complain about mktemp, since it's generally a bad way to do
|
|
* things.
|
|
*/
|
|
len = strlen(fileName);
|
|
if (len > 6 && strcmp(fileName + len - 6, "XXXXXX") == 0) {
|
|
#if defined(HAVE_MKSTEMP) && defined(HAVE_FDOPEN)
|
|
int fd;
|
|
|
|
DBUG(("+++ Using mkstemp\n"));
|
|
|
|
/* this modifies the template *and* opens the file */
|
|
fd = mkstemp(fileName);
|
|
if (fd < 0) {
|
|
err = errno ? errno : kNuErrFileOpen;
|
|
Nu_ReportError(NU_BLOB, kNuErrNone, "mkstemp failed on '%s'",
|
|
fileName);
|
|
goto bail;
|
|
}
|
|
|
|
DBUG(("--- Fd-opening temp file '%s'\n", fileName));
|
|
*pFp = fdopen(fd, kNuFileOpenReadWriteCreat);
|
|
if (*pFp == nil) {
|
|
close(fd);
|
|
err = errno ? errno : kNuErrFileOpen;
|
|
goto bail;
|
|
}
|
|
|
|
/* file is open, we're done */
|
|
goto bail;
|
|
|
|
#else
|
|
char* result;
|
|
|
|
DBUG(("+++ Using mktemp\n"));
|
|
result = mktemp(fileName);
|
|
if (result == nil) {
|
|
Nu_ReportError(NU_BLOB, kNuErrNone, "mktemp failed on '%s'",
|
|
fileName);
|
|
err = kNuErrInternal;
|
|
goto bail;
|
|
}
|
|
|
|
/* now open the filename as usual */
|
|
#endif
|
|
}
|
|
|
|
DBUG(("--- Opening temp file '%s'\n", fileName));
|
|
|
|
#if defined(HAVE_FDOPEN)
|
|
{
|
|
int fd;
|
|
|
|
fd = open(fileName, O_RDWR|O_CREAT|O_EXCL|O_BINARY, 0600);
|
|
if (fd < 0) {
|
|
err = errno ? errno : kNuErrFileOpen;
|
|
goto bail;
|
|
}
|
|
|
|
*pFp = fdopen(fd, kNuFileOpenReadWriteCreat);
|
|
if (*pFp == nil) {
|
|
close(fd);
|
|
err = errno ? errno : kNuErrFileOpen;
|
|
goto bail;
|
|
}
|
|
}
|
|
#else
|
|
/* (not sure how portable "access" is... I think it's POSIX) */
|
|
if (access(fileName, F_OK) == 0) {
|
|
err = kNuErrFileExists;
|
|
goto bail;
|
|
}
|
|
|
|
*pFp = fopen(fileName, kNuFileOpenReadWriteCreat);
|
|
if (*pFp == nil) {
|
|
err = errno ? errno : kNuErrFileOpen;
|
|
goto bail;
|
|
}
|
|
#endif
|
|
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Open an archive in read-write mode, optionally creating it if it doesn't
|
|
* exist.
|
|
*/
|
|
NuError
|
|
Nu_OpenRW(const char* archivePathname, const char* tmpPathname, ulong flags,
|
|
NuArchive** ppArchive)
|
|
{
|
|
NuError err;
|
|
FILE* fp = nil;
|
|
FILE* tmpFp = nil;
|
|
NuArchive* pArchive = nil;
|
|
char* tmpPathDup = nil;
|
|
Boolean archiveExists;
|
|
Boolean newlyCreated;
|
|
|
|
if (archivePathname == nil || !strlen(archivePathname) ||
|
|
tmpPathname == nil || !strlen(tmpPathname) || ppArchive == nil ||
|
|
(flags & ~(kNuOpenCreat|kNuOpenExcl)) != 0)
|
|
{
|
|
return kNuErrInvalidArg;
|
|
}
|
|
|
|
archiveExists = (access(archivePathname, F_OK) == 0);
|
|
|
|
/*
|
|
* Open or create archive file.
|
|
*/
|
|
if (archiveExists) {
|
|
if ((flags & kNuOpenCreat) && (flags & kNuOpenExcl)) {
|
|
err = kNuErrFileExists;
|
|
Nu_ReportError(NU_BLOB, err, "File '%s' exists", archivePathname);
|
|
goto bail;
|
|
}
|
|
fp = fopen(archivePathname, kNuFileOpenReadWrite);
|
|
newlyCreated = false;
|
|
} else {
|
|
if (!(flags & kNuOpenCreat)) {
|
|
err = kNuErrFileNotFound;
|
|
Nu_ReportError(NU_BLOB, err, "File '%s' not found",archivePathname);
|
|
goto bail;
|
|
}
|
|
fp = fopen(archivePathname, kNuFileOpenReadWriteCreat);
|
|
newlyCreated = true;
|
|
}
|
|
|
|
if (fp == nil) {
|
|
Nu_ReportError(NU_BLOB, errno, "Unable to open '%s'", archivePathname);
|
|
err = kNuErrFileOpen;
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Treat zero-length files as newly-created archives.
|
|
*/
|
|
if (archiveExists && !newlyCreated) {
|
|
long length;
|
|
|
|
err = Nu_GetFileLength(nil, fp, &length);
|
|
BailError(err);
|
|
|
|
if (!length) {
|
|
DBUG(("--- treating zero-length file as newly created archive\n"));
|
|
newlyCreated = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Create a temp file. We don't need one for a newly-created archive,
|
|
* at least not right away. It's possible the caller could add some
|
|
* files, flush the changes, and then want to delete them without
|
|
* closing and reopening the archive.
|
|
*
|
|
* So, create a temp file whether we think we need one or not. Won't
|
|
* do any harm, and might save us some troubles later.
|
|
*/
|
|
tmpPathDup = strdup(tmpPathname);
|
|
BailNil(tmpPathDup);
|
|
err = Nu_OpenTempFile(tmpPathDup, &tmpFp);
|
|
if (err != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed opening temp file '%s'",
|
|
tmpPathname);
|
|
goto bail;
|
|
}
|
|
|
|
err = Nu_NuArchiveNew(ppArchive);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
pArchive = *ppArchive;
|
|
|
|
pArchive->openMode = kNuOpenRW;
|
|
pArchive->newlyCreated = newlyCreated;
|
|
pArchive->archivePathname = strdup(archivePathname);
|
|
pArchive->archiveFp = fp;
|
|
fp = nil;
|
|
pArchive->tmpFp = tmpFp;
|
|
tmpFp = nil;
|
|
pArchive->tmpPathname = tmpPathDup;
|
|
tmpPathDup = nil;
|
|
|
|
if (archiveExists && !newlyCreated) {
|
|
err = Nu_ReadMasterHeader(pArchive);
|
|
BailError(err);
|
|
} else {
|
|
Nu_InitNewArchive(pArchive);
|
|
}
|
|
|
|
bail:
|
|
if (err != kNuErrNone) {
|
|
if (pArchive != nil) {
|
|
(void) Nu_CloseAndFree(pArchive);
|
|
*ppArchive = nil;
|
|
}
|
|
if (fp != nil)
|
|
fclose(fp);
|
|
if (tmpFp != nil)
|
|
fclose(tmpFp);
|
|
if (tmpPathDup != nil)
|
|
Nu_Free(pArchive, tmpPathDup);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Update an archive
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Write the NuFX master header at the current offset.
|
|
*/
|
|
NuError
|
|
Nu_WriteMasterHeader(NuArchive* pArchive, FILE* fp,
|
|
NuMasterHeader* pHeader)
|
|
{
|
|
NuError err;
|
|
long crcOffset;
|
|
ushort crc;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(fp != nil);
|
|
Assert(pHeader != nil);
|
|
Assert(pHeader->isValid);
|
|
Assert(pHeader->mhMasterVersion == kNuOurMHVersion);
|
|
|
|
crc = 0;
|
|
|
|
Nu_WriteBytes(pArchive, fp, pHeader->mhNufileID, kNufileIDLen);
|
|
err = Nu_FTell(fp, &crcOffset);
|
|
BailError(err);
|
|
Nu_WriteTwo(pArchive, fp, 0);
|
|
Nu_WriteFourC(pArchive, fp, pHeader->mhTotalRecords, &crc);
|
|
Nu_WriteDateTimeC(pArchive, fp, pHeader->mhArchiveCreateWhen, &crc);
|
|
Nu_WriteDateTimeC(pArchive, fp, pHeader->mhArchiveModWhen, &crc);
|
|
Nu_WriteTwoC(pArchive, fp, pHeader->mhMasterVersion, &crc);
|
|
Nu_WriteBytesC(pArchive, fp, pHeader->mhReserved1,
|
|
kNufileMasterReserved1Len, &crc);
|
|
Nu_WriteFourC(pArchive, fp, pHeader->mhMasterEOF, &crc);
|
|
Nu_WriteBytesC(pArchive, fp, pHeader->mhReserved2,
|
|
kNufileMasterReserved2Len, &crc);
|
|
|
|
/* go back and write the CRC (sadly, the seek will flush the stdio buf) */
|
|
pHeader->mhMasterCRC = crc;
|
|
err = Nu_FSeek(fp, crcOffset, SEEK_SET);
|
|
BailError(err);
|
|
Nu_WriteTwo(pArchive, fp, pHeader->mhMasterCRC);
|
|
|
|
/* check for errors in any of the above writes */
|
|
if ((err = Nu_HeaderIOFailed(pArchive, fp)) != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err, "Failed writing master header");
|
|
goto bail;
|
|
}
|
|
|
|
DBUG(("--- Master header written successfully at %ld (crc=0x%04x)\n",
|
|
crcOffset - kNufileIDLen, crc));
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Close an archive
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Close all open files, and free the memory associated with the structure.
|
|
*
|
|
* If it's a brand-new archive, and we didn't add anything to it, then we
|
|
* want to remove the stub archive file.
|
|
*/
|
|
static void
|
|
Nu_CloseAndFree(NuArchive* pArchive)
|
|
{
|
|
if (pArchive->archiveFp != nil) {
|
|
DBUG(("--- Closing archive\n"));
|
|
fclose(pArchive->archiveFp);
|
|
pArchive->archiveFp = nil;
|
|
}
|
|
|
|
if (pArchive->tmpFp != nil) {
|
|
DBUG(("--- Closing and removing temp file\n"));
|
|
fclose(pArchive->tmpFp);
|
|
pArchive->tmpFp = nil;
|
|
Assert(pArchive->tmpPathname != nil);
|
|
if (remove(pArchive->tmpPathname) != 0) {
|
|
Nu_ReportError(NU_BLOB, errno, "Unable to remove temp file '%s'",
|
|
pArchive->tmpPathname);
|
|
/* keep going */
|
|
}
|
|
}
|
|
|
|
if (pArchive->newlyCreated && Nu_RecordSet_IsEmpty(&pArchive->origRecordSet))
|
|
{
|
|
DBUG(("--- Newly-created archive unmodified; removing it\n"));
|
|
if (remove(pArchive->archivePathname) != 0) {
|
|
Nu_ReportError(NU_BLOB, errno, "Unable to remove archive file '%s'",
|
|
pArchive->archivePathname);
|
|
}
|
|
}
|
|
|
|
Nu_NuArchiveFree(pArchive);
|
|
}
|
|
|
|
/*
|
|
* Flush pending changes to the archive, then close it.
|
|
*/
|
|
NuError
|
|
Nu_Close(NuArchive* pArchive)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
long flushStatus;
|
|
|
|
Assert(pArchive != nil);
|
|
|
|
if (!Nu_IsReadOnly(pArchive))
|
|
err = Nu_Flush(pArchive, &flushStatus);
|
|
if (err == kNuErrNone)
|
|
Nu_CloseAndFree(pArchive);
|
|
else {
|
|
DBUG(("--- Close NuFlush status was 0x%4lx\n", flushStatus));
|
|
}
|
|
|
|
if (err != kNuErrNone) {
|
|
DBUG(("--- Nu_Close returning error %d\n", err));
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Delete and replace an archive
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Delete the archive file, which should already have been closed.
|
|
*/
|
|
NuError
|
|
Nu_DeleteArchiveFile(NuArchive* pArchive)
|
|
{
|
|
Assert(pArchive != nil);
|
|
Assert(pArchive->archiveFp == nil);
|
|
Assert(pArchive->archivePathname != nil);
|
|
|
|
return Nu_DeleteFile(pArchive->archivePathname);
|
|
}
|
|
|
|
/*
|
|
* Rename the temp file on top of the original archive. The temp file
|
|
* should be closed, and the archive file should be deleted.
|
|
*/
|
|
NuError
|
|
Nu_RenameTempToArchive(NuArchive* pArchive)
|
|
{
|
|
Assert(pArchive != nil);
|
|
Assert(pArchive->archiveFp == nil);
|
|
Assert(pArchive->tmpFp == nil);
|
|
Assert(pArchive->archivePathname != nil);
|
|
Assert(pArchive->tmpPathname != nil);
|
|
|
|
return Nu_RenameFile(pArchive->tmpPathname, pArchive->archivePathname);
|
|
}
|
|
|