/* * 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. * * Archive structure creation and manipulation. */ #include "NufxLibPriv.h" #ifdef HAVE_FCNTL_H # include #endif #ifndef O_BINARY # define O_BINARY 0 #endif /* master header identification */ static const uint8_t kNuMasterID[kNufileIDLen] = { 0x4e, 0xf5, 0x46, 0xe9, 0x6c, 0xe5 }; /* other identification; can be no longer than kNufileIDLen */ static const uint8_t kNuBinary2ID[] = { 0x0a, 0x47, 0x4c }; static const uint8_t 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?) */ #define kDefaultJunkSkipMax 1024 /* default junk scan size */ 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 != NULL); /* validate some assumptions we make throughout the code */ Assert(sizeof(int) >= 2); Assert(sizeof(void*) >= sizeof(NuArchive*)); *ppArchive = Nu_Calloc(NULL, sizeof(**ppArchive)); if (*ppArchive == NULL) 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; (*ppArchive)->valStripHighASCII = false; /* bug: this can't be set by application! */ (*ppArchive)->valJunkSkipMax = kDefaultJunkSkipMax; (*ppArchive)->valIgnoreLZW2Len = false; (*ppArchive)->valHandleBadMac = false; (*ppArchive)->messageHandlerFunc = gNuGlobalErrorMessageHandler; return kNuErrNone; } /* * Free up a NuArchive structure and its contents. */ static NuError Nu_NuArchiveFree(NuArchive* pArchive) { Assert(pArchive != NULL); 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(NULL, pArchive->archivePathnameUNI); Nu_Free(NULL, pArchive->tmpPathnameUNI); Nu_Free(NULL, pArchive->compBuf); Nu_Free(NULL, pArchive->lzwCompressState); Nu_Free(NULL, pArchive->lzwExpandState); /* mark it as deceased to prevent further use, then free it */ pArchive->structMagic = kNuArchiveStructMagic ^ 0xffffffff; Nu_Free(NULL, pArchive); return kNuErrNone; } /* * Copy a NuMasterHeader struct. */ void Nu_MasterHeaderCopy(NuArchive* pArchive, NuMasterHeader* pDstHeader, const NuMasterHeader* pSrcHeader) { Assert(pArchive != NULL); Assert(pDstHeader != NULL); Assert(pSrcHeader != NULL); *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 == NULL) return kNuErrInvalidArg; *ppMasterHeader = &pArchive->masterHeader; return kNuErrNone; } /* * Allocate the general-purpose compression buffer, if needed. */ NuError Nu_AllocCompressionBufferIFN(NuArchive* pArchive) { Assert(pArchive != NULL); if (pArchive->compBuf != NULL) return kNuErrNone; pArchive->compBuf = Nu_Malloc(pArchive, kNuGenCompBufSize); if (pArchive->compBuf == NULL) 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: * Leading junk * 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; uint8_t identBuf[kNufileIDLen]; uint32_t 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 != 0 && pArchive->headerOffset != pArchive->junkOffset) { Nu_ReportError(NU_BLOB, kNuErrNone, "Can't fix the wrapper??"); err = kNuErrInternal; goto bail; } else goto bail; } err = Nu_FSeek(fp, pArchive->junkOffset, 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, but excludes junk */ archiveLen = pArchive->newMasterHeader.mhMasterEOF + (pArchive->headerOffset - pArchive->junkOffset) - kNuBinary2BlockSize; archiveLen512 = (archiveLen + 511) / 512; err = Nu_FSeek(fp, kNuBNYFileSizeLo - kNufileIDLen, SEEK_CUR); BailError(err); Nu_WriteTwo(pArchive, fp, (uint16_t)(archiveLen512 & 0xffff)); err = Nu_FSeek(fp, kNuBNYFileSizeHi - (kNuBNYFileSizeLo+2), SEEK_CUR); BailError(err); Nu_WriteTwo(pArchive, fp, (uint16_t)(archiveLen512 >> 16)); err = Nu_FSeek(fp, kNuBNYEOFLo - (kNuBNYFileSizeHi+2), SEEK_CUR); BailError(err); Nu_WriteTwo(pArchive, fp, (uint16_t)(archiveLen & 0xffff)); Nu_WriteOne(pArchive, fp, (uint8_t)((archiveLen >> 16) & 0xff)); err = Nu_FSeek(fp, kNuBNYEOFHi - (kNuBNYEOFLo+3), SEEK_CUR); BailError(err); Nu_WriteOne(pArchive, fp, (uint8_t)(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, (uint16_t)archiveLen); err = Nu_FSeek(fp, kNuSEALength2 - (kNuSEALength1+2), SEEK_CUR); BailError(err); Nu_WriteTwo(pArchive, fp, (uint16_t)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 != 0 && pArchive->headerOffset != pArchive->junkOffset) { 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); curOffset -= pArchive->junkOffset; /* don't factor junk into account */ DBUG(("+++ BNY needs %ld bytes of padding\n", curOffset & 0x7f)); 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). * * If at first we don't succeed, we keep trying further along until we * find something we recognize. We don't want to just scan for the * NuFile ID, because that might prevent this from working properly with * SEA archives which push the NuFX start out about 12K. We also wouldn't * be able to update the BNY/SEA wrappers correctly. So, we inch our way * along until we find something we recognize or get bored. * * On exit, the stream will be positioned just past the master header. */ static NuError Nu_ReadMasterHeader(NuArchive* pArchive) { NuError err; uint16_t crc; FILE* fp; NuMasterHeader* pHeader; Boolean isBinary2 = false; Boolean isSea = false; Assert(pArchive != NULL); fp = pArchive->archiveFp; /* saves typing */ pHeader = &pArchive->masterHeader; pArchive->junkOffset = 0; retry: pArchive->headerOffset = pArchive->junkOffset; 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) { /* * Doesn't look like a NuFX archive. Scan forward and see if we * can find the start past some leading junk. MacBinary headers * and chunks of HTTP seem popular on FTP sites. */ if ((pArchive->openMode == kNuOpenRO || pArchive->openMode == kNuOpenRW) && pArchive->junkOffset < (long)pArchive->valJunkSkipMax) { pArchive->junkOffset++; DBUG(("+++ scanning from offset %ld\n", pArchive->junkOffset)); err = Nu_SeekArchive(pArchive, fp, pArchive->junkOffset, SEEK_SET); BailError(err); goto retry; } 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; } if (pArchive->junkOffset != 0) { DBUG(("+++ found apparent start of archive at offset %ld\n", pArchive->junkOffset)); } 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 Master Header version %u", pHeader->mhMasterVersion); goto bail; } /* compare the CRC */ if (!pArchive->valIgnoreCRC && crc != pHeader->mhMasterCRC) { if (!Nu_ShouldIgnoreBadCRC(pArchive, NULL, 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. * * Matthew Fischer found some archives that exhibit MasterEOF==0 * but are otherwise functional, suggesting that there might be a * version of ShrinkIt that created these without reporting an error. * One such archive was a disk image with no filename entry, suggesting * that it was created by an early version of P8 ShrinkIt. * * So, we only fail if the EOF equals 48. */ if (pHeader->mhMasterEOF == kNuMasterHeaderSize) { err = kNuErrNoRecords; Nu_ReportError(NU_BLOB, err, "Master EOF is %u, 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 != NULL); 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 = NULL; Assert(infp != NULL); Assert(ppArchive != NULL); err = Nu_NuArchiveNew(ppArchive); if (err != kNuErrNone) goto bail; pArchive = *ppArchive; pArchive->openMode = kNuOpenStreamingRO; pArchive->archiveFp = infp; pArchive->archivePathnameUNI = strdup("(stream)"); err = Nu_ReadMasterHeader(pArchive); BailError(err); bail: if (err != kNuErrNone) { if (pArchive != NULL) (void) Nu_NuArchiveFree(pArchive); *ppArchive = NULL; } return err; } /* * Open an archive in non-streaming read-only mode. */ NuError Nu_OpenRO(const UNICHAR* archivePathnameUNI, NuArchive** ppArchive) { NuError err; NuArchive* pArchive = NULL; FILE* fp = NULL; if (archivePathnameUNI == NULL || !strlen(archivePathnameUNI) || ppArchive == NULL) { return kNuErrInvalidArg; } *ppArchive = NULL; fp = fopen(archivePathnameUNI, kNuFileOpenReadOnly); if (fp == NULL) { Nu_ReportError(NU_BLOB, errno, "Unable to open '%s'", archivePathnameUNI); err = kNuErrFileOpen; goto bail; } err = Nu_NuArchiveNew(ppArchive); if (err != kNuErrNone) goto bail; pArchive = *ppArchive; pArchive->openMode = kNuOpenRO; pArchive->archiveFp = fp; fp = NULL; pArchive->archivePathnameUNI = strdup(archivePathnameUNI); err = Nu_ReadMasterHeader(pArchive); BailError(err); bail: if (err != kNuErrNone) { if (pArchive != NULL) { (void) Nu_CloseAndFree(pArchive); *ppArchive = NULL; } if (fp != NULL) 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 (so * pass a copy of the string in). * * Thought for the day: consider using Win32 SetFileAttributes() to make * temp files hidden. We will need to un-hide it before rolling it over. */ static NuError Nu_OpenTempFile(UNICHAR* fileNameUNI, FILE** pFp) { NuArchive* pArchive = NULL; /* 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(fileNameUNI); if (len > 6 && strcmp(fileNameUNI + 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(fileNameUNI); if (fd < 0) { err = errno ? errno : kNuErrFileOpen; Nu_ReportError(NU_BLOB, kNuErrNone, "mkstemp failed on '%s'", fileNameUNI); goto bail; } DBUG(("--- Fd-opening temp file '%s'\n", fileNameUNI)); *pFp = fdopen(fd, kNuFileOpenReadWriteCreat); if (*pFp == NULL) { 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(fileNameUNI); if (result == NULL) { Nu_ReportError(NU_BLOB, kNuErrNone, "mktemp failed on '%s'", fileNameUNI); err = kNuErrInternal; goto bail; } /* now open the filename as usual */ #endif } DBUG(("--- Opening temp file '%s'\n", fileNameUNI)); #if defined(HAVE_FDOPEN) { int fd; fd = open(fileNameUNI, O_RDWR|O_CREAT|O_EXCL|O_BINARY, 0600); if (fd < 0) { err = errno ? errno : kNuErrFileOpen; goto bail; } *pFp = fdopen(fd, kNuFileOpenReadWriteCreat); if (*pFp == NULL) { close(fd); err = errno ? errno : kNuErrFileOpen; goto bail; } } #else if (access(fileNameUNI, F_OK) == 0) { err = kNuErrFileExists; goto bail; } *pFp = fopen(fileNameUNI, kNuFileOpenReadWriteCreat); if (*pFp == NULL) { 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 UNICHAR* archivePathnameUNI, const UNICHAR* tmpPathnameUNI, uint32_t flags, NuArchive** ppArchive) { NuError err; FILE* fp = NULL; FILE* tmpFp = NULL; NuArchive* pArchive = NULL; char* tmpPathDup = NULL; Boolean archiveExists; Boolean newlyCreated; if (archivePathnameUNI == NULL || !strlen(archivePathnameUNI) || tmpPathnameUNI == NULL || !strlen(tmpPathnameUNI) || ppArchive == NULL || (flags & ~(kNuOpenCreat|kNuOpenExcl)) != 0) { return kNuErrInvalidArg; } archiveExists = (access(archivePathnameUNI, 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", archivePathnameUNI); goto bail; } fp = fopen(archivePathnameUNI, kNuFileOpenReadWrite); newlyCreated = false; } else { if (!(flags & kNuOpenCreat)) { err = kNuErrFileNotFound; Nu_ReportError(NU_BLOB, err, "File '%s' not found", archivePathnameUNI); goto bail; } fp = fopen(archivePathnameUNI, kNuFileOpenReadWriteCreat); newlyCreated = true; } if (fp == NULL) { if (errno == EACCES) err = kNuErrFileAccessDenied; else err = kNuErrFileOpen; Nu_ReportError(NU_BLOB, errno, "Unable to open '%s'", archivePathnameUNI); goto bail; } /* * Treat zero-length files as newly-created archives. */ if (archiveExists && !newlyCreated) { long length; err = Nu_GetFileLength(NULL, 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(tmpPathnameUNI); BailNil(tmpPathDup); err = Nu_OpenTempFile(tmpPathDup, &tmpFp); if (err != kNuErrNone) { Nu_ReportError(NU_BLOB, err, "Failed opening temp file '%s'", tmpPathnameUNI); goto bail; } err = Nu_NuArchiveNew(ppArchive); if (err != kNuErrNone) goto bail; pArchive = *ppArchive; pArchive->openMode = kNuOpenRW; pArchive->newlyCreated = newlyCreated; pArchive->archivePathnameUNI = strdup(archivePathnameUNI); pArchive->archiveFp = fp; fp = NULL; pArchive->tmpFp = tmpFp; tmpFp = NULL; pArchive->tmpPathnameUNI = tmpPathDup; tmpPathDup = NULL; if (archiveExists && !newlyCreated) { err = Nu_ReadMasterHeader(pArchive); BailError(err); } else { Nu_InitNewArchive(pArchive); } bail: if (err != kNuErrNone) { if (pArchive != NULL) { (void) Nu_CloseAndFree(pArchive); *ppArchive = NULL; } if (fp != NULL) fclose(fp); if (tmpFp != NULL) fclose(tmpFp); if (tmpPathDup != NULL) 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; uint16_t crc; Assert(pArchive != NULL); Assert(fp != NULL); Assert(pHeader != NULL); 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 != NULL) { DBUG(("--- Closing archive\n")); fclose(pArchive->archiveFp); pArchive->archiveFp = NULL; } if (pArchive->tmpFp != NULL) { DBUG(("--- Closing and removing temp file\n")); fclose(pArchive->tmpFp); pArchive->tmpFp = NULL; Assert(pArchive->tmpPathnameUNI != NULL); if (remove(pArchive->tmpPathnameUNI) != 0) { Nu_ReportError(NU_BLOB, errno, "Unable to remove temp file '%s'", pArchive->tmpPathnameUNI); /* keep going */ } } if (pArchive->newlyCreated && Nu_RecordSet_IsEmpty(&pArchive->origRecordSet)) { DBUG(("--- Newly-created archive unmodified; removing it\n")); if (remove(pArchive->archivePathnameUNI) != 0) { Nu_ReportError(NU_BLOB, errno, "Unable to remove archive file '%s'", pArchive->archivePathnameUNI); } } Nu_NuArchiveFree(pArchive); } /* * Flush pending changes to the archive, then close it. */ NuError Nu_Close(NuArchive* pArchive) { NuError err = kNuErrNone; uint32_t flushStatus; Assert(pArchive != NULL); 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 != NULL); Assert(pArchive->archiveFp == NULL); Assert(pArchive->archivePathnameUNI != NULL); return Nu_DeleteFile(pArchive->archivePathnameUNI); } /* * 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 != NULL); Assert(pArchive->archiveFp == NULL); Assert(pArchive->tmpFp == NULL); Assert(pArchive->archivePathnameUNI != NULL); Assert(pArchive->tmpPathnameUNI != NULL); return Nu_RenameFile(pArchive->tmpPathnameUNI, pArchive->archivePathnameUNI); }