mirror of
https://github.com/fadden/nulib2.git
synced 2024-12-29 15:29:16 +00:00
588 lines
18 KiB
C
588 lines
18 KiB
C
/*
|
|
* NuFX archive manipulation library
|
|
* Copyright (C) 2000 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.
|
|
*
|
|
* Run an archive through the laundry. The net result is a duplicate
|
|
* archive that matches the original in most respects. Extracting the
|
|
* files from the duplicate will yield the same results as if they were
|
|
* extracted from the original, but the duplicate archive may differ
|
|
* in subtle ways (e.g. filename threads may be added, data may be
|
|
* recompressed).
|
|
*
|
|
* This demonstrates copying threads around, both with and without
|
|
* recompressing, between two archives that are open simultaneously. This
|
|
* also tests NufxLib's thread ordering and verifies that you can abort
|
|
* frequently with no adverse effects.
|
|
*
|
|
* NOTE: depending on the options you select, you have to have enough
|
|
* memory to hold the entire uncompressed contents of the original archive.
|
|
* The memory requirements are reduced if you use the "copy only" flag, and
|
|
* are virtually eliminated if you use "frequent flush".
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include "NufxLib.h"
|
|
#include "Common.h"
|
|
|
|
|
|
#define kTempFile "tmp-laundry"
|
|
|
|
#define kFlagCopyOnly (1)
|
|
#define kFlagReverseThreads (1 << 1)
|
|
#define kFlagFrequentFlush (1 << 2)
|
|
#define kFlagFrequentAbort (1 << 3) /* implies FrequentFlush */
|
|
#define kFlagUseTmp (1 << 4)
|
|
|
|
|
|
/*
|
|
* Globals.
|
|
*/
|
|
char gSentRecordWarning = false;
|
|
|
|
|
|
|
|
/*
|
|
* Copy a thread, expanding and recompressing it.
|
|
*
|
|
* This assumes the library is configured for compression (it defaults
|
|
* to LZW/2, so this is a reasonable assumption).
|
|
*/
|
|
NuError
|
|
CopyThreadRecompressed(NuArchive* pInArchive, NuArchive* pOutArchive,
|
|
long flags, const NuThread* pThread, long newRecordIdx)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuDataSource* pDataSource = nil;
|
|
NuDataSink* pDataSink = nil;
|
|
uchar* buffer = nil;
|
|
|
|
/*
|
|
* Allocate a buffer large enough to hold all the uncompressed data, and
|
|
* wrap a data sink around it.
|
|
*
|
|
* If the thread is zero bytes long, we can skip this part.
|
|
*/
|
|
if (pThread->actualThreadEOF) {
|
|
buffer = malloc(pThread->actualThreadEOF);
|
|
if (buffer == nil) {
|
|
err = kNuErrMalloc;
|
|
goto bail;
|
|
}
|
|
err = NuCreateDataSinkForBuffer(true, kNuConvertOff, buffer,
|
|
pThread->actualThreadEOF, &pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create data sink (err=%d)\n",
|
|
err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Expand the data. For a pre-sized thread, this grabs only the
|
|
* interesting part of the buffer.
|
|
*/
|
|
err = NuExtractThread(pInArchive, pThread->threadIdx, pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to extract thread %ld (err=%d)\n",
|
|
pThread->threadIdx, err);
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The expanded data is in the buffer, now create a data source that
|
|
* describes it.
|
|
*
|
|
* This is complicated by the existence of pre-sized threads, which
|
|
* require us to set "otherLen".
|
|
*
|
|
* We always use "actualThreadEOF" because "thThreadEOF" is broken
|
|
* for disk archives created by certain versions of ShrinkIt.
|
|
*
|
|
* It's okay to pass in a nil value for "buffer", so long as the
|
|
* amount of data in the buffer is also zero. The library will do
|
|
* the right thing.
|
|
*/
|
|
if (NuIsPresizedThreadID(NuGetThreadID(pThread))) {
|
|
err = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, true,
|
|
pThread->thCompThreadEOF, buffer, 0,
|
|
pThread->actualThreadEOF, &pDataSource);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr,
|
|
"ERROR: unable to create pre-sized data source (err=%d)\n",err);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
err = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, true,
|
|
0, buffer, 0,
|
|
pThread->actualThreadEOF, &pDataSource);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr,
|
|
"ERROR: unable to create data source (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
}
|
|
buffer = nil; /* doClose was set, so it's owned by the data source */
|
|
|
|
/*
|
|
* Schedule the data for addition to the record.
|
|
*/
|
|
err = NuAddThread(pOutArchive, newRecordIdx, NuGetThreadID(pThread),
|
|
pDataSource, nil);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to add thread (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
pDataSource = nil; /* library owns it now */
|
|
|
|
bail:
|
|
if (pDataSource != nil)
|
|
NuFreeDataSource(pDataSource);
|
|
if (pDataSink != nil)
|
|
NuFreeDataSink(pDataSink);
|
|
if (buffer != nil)
|
|
free(buffer);
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Copy a thread from one archive to another without disturbing the
|
|
* compression.
|
|
*
|
|
* There is a much more efficient way to do this: create an FP
|
|
* data source using an offset within the archive file itself.
|
|
* Since pInArchive->archiveFp isn't exposed, we can't use that,
|
|
* but under most operating systems you aren't prevented from
|
|
* opening the same file twice in read-only mode. The file offset
|
|
* in pThread tells us where the data is.
|
|
*
|
|
* The method used below is less memory-efficient but more portable.
|
|
*
|
|
* This always extracts based on the compThreadEOF, which is
|
|
* reliable but extracts a little more than we need on pre-sized
|
|
* threads (filenames, comments).
|
|
*/
|
|
NuError
|
|
CopyThreadUncompressed(NuArchive* pInArchive, NuArchive* pOutArchive,
|
|
long flags, const NuThread* pThread, long newRecordIdx)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuDataSource* pDataSource = nil;
|
|
NuDataSink* pDataSink = nil;
|
|
uchar* buffer = nil;
|
|
|
|
/*
|
|
* If we have some data files that were left uncompressed, perhaps
|
|
* because of GSHK's "don't compress anything smaller than 512 bytes"
|
|
* rule, NufxLib will try to compress them. We disable this
|
|
* behavior by disabling compression. That way, stuff that is
|
|
* already compressed will remain that way, and stuff that isn't
|
|
* compressed won't be. (We really only need to do this once, at
|
|
* the start of the program, but it's illustrative to do it here.)
|
|
*/
|
|
err = NuSetValue(pOutArchive, kNuValueDataCompression, kNuCompressNone);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to set compression (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Allocate a buffer large enough to hold all the compressed data, and
|
|
* wrap a data sink around it.
|
|
*/
|
|
buffer = malloc(pThread->thCompThreadEOF);
|
|
if (buffer == nil) {
|
|
err = kNuErrMalloc;
|
|
goto bail;
|
|
}
|
|
err = NuCreateDataSinkForBuffer(false, kNuConvertOff, buffer,
|
|
pThread->thCompThreadEOF, &pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create data sink (err=%d)\n",
|
|
err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Get the compressed data. For a pre-sized thread, this grabs the
|
|
* entire contents of the buffer, including the padding.
|
|
*/
|
|
err = NuExtractThread(pInArchive, pThread->threadIdx, pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to extract thread %ld (err=%d)\n",
|
|
pThread->threadIdx, err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* The (perhaps compressed) data is in the buffer, now create a data
|
|
* source that describes it.
|
|
*
|
|
* This is complicated by the existence of pre-sized threads. There
|
|
* are two possibilities:
|
|
* 1. We have a certain amount of non-pre-sized data (thCompThreadEOF)
|
|
* that will expand out to a certain length (actualThreadEOF).
|
|
* 2. We have a certain amount of pre-sized data (actualThreadEOF)
|
|
* that will fit within a buffer (thCompThreadEOF).
|
|
* As you can see, the arguments need to be reversed for pre-sized
|
|
* threads.
|
|
*
|
|
* We always use "actualThreadEOF" because "thThreadEOF" is broken
|
|
* for disk archives created by certain versions of ShrinkIt.
|
|
*/
|
|
if (NuIsPresizedThreadID(NuGetThreadID(pThread))) {
|
|
err = NuCreateDataSourceForBuffer(pThread->thThreadFormat, true,
|
|
pThread->thCompThreadEOF, buffer, 0,
|
|
pThread->actualThreadEOF, &pDataSource);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr,
|
|
"ERROR: unable to create pre-sized data source (err=%d)\n",err);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
err = NuCreateDataSourceForBuffer(pThread->thThreadFormat, true,
|
|
pThread->actualThreadEOF, buffer, 0,
|
|
pThread->thCompThreadEOF, &pDataSource);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr,
|
|
"ERROR: unable to create data source (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
}
|
|
buffer = nil; /* doClose was set, so it's owned by the data source */
|
|
|
|
/* yes, this is a kluge... sigh */
|
|
err = NuDataSourceSetRawCrc(pDataSource, pThread->thThreadCRC);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: can't set source CRC (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Schedule the data for addition to the record.
|
|
*
|
|
* Note that NuAddThread makes a copy of the data source, and clears
|
|
* "doClose" on our copy, so we are free to dispose of pDataSource.
|
|
*/
|
|
err = NuAddThread(pOutArchive, newRecordIdx, NuGetThreadID(pThread),
|
|
pDataSource, nil);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to add thread (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
pDataSource = nil; /* library owns it now */
|
|
|
|
bail:
|
|
if (pDataSource != nil)
|
|
NuFreeDataSource(pDataSource);
|
|
if (pDataSink != nil)
|
|
NuFreeDataSink(pDataSink);
|
|
if (buffer != nil)
|
|
free(buffer);
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Copy a thread from one archive to another.
|
|
*
|
|
* Depending on "flags", this will either copy it raw or uncompress and
|
|
* recompress.
|
|
*/
|
|
NuError
|
|
CopyThread(NuArchive* pInArchive, NuArchive* pOutArchive, long flags,
|
|
const NuThread* pThread, long newRecordIdx)
|
|
{
|
|
if (flags & kFlagCopyOnly) {
|
|
return CopyThreadUncompressed(pInArchive, pOutArchive, flags, pThread,
|
|
newRecordIdx);
|
|
} else {
|
|
return CopyThreadRecompressed(pInArchive, pOutArchive, flags, pThread,
|
|
newRecordIdx);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Copy a record from the input to the output.
|
|
*
|
|
* This runs through the list of threads and copies each one individually.
|
|
* It will copy them in the original order or in reverse order (the latter
|
|
* of which will not usually have any effect since NufxLib imposes a
|
|
* specific thread ordering on most common types) depending on "flags".
|
|
*/
|
|
NuError
|
|
CopyRecord(NuArchive* pInArchive, NuArchive* pOutArchive, long flags,
|
|
NuRecordIdx recordIdx)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
const NuRecord* pRecord;
|
|
const NuThread* pThread;
|
|
NuFileDetails fileDetails;
|
|
NuRecordIdx newRecordIdx;
|
|
long numThreads;
|
|
int idx;
|
|
|
|
/*
|
|
* Grab the original record and see how many threads it has.
|
|
*/
|
|
err = NuGetRecord(pInArchive, recordIdx, &pRecord);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to get recordIdx %ld\n", recordIdx);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Pre-v3 records didn't put CRCs in the thread headers. If we just
|
|
* copy the thread over without reprocessing the data, we won't compute
|
|
* a CRC for the thread, and we will get CRC failures.
|
|
*/
|
|
if (!gSentRecordWarning && (flags & kFlagCopyOnly) &&
|
|
pRecord->recVersionNumber < 3)
|
|
{
|
|
printf("WARNING: pre-v3 records that aren't recompressed may exhibit CRC failures\n");
|
|
gSentRecordWarning = true;
|
|
}
|
|
|
|
numThreads = NuRecordGetNumThreads(pRecord);
|
|
if (!numThreads) {
|
|
fprintf(stderr, "WARNING: recordIdx=%ld was empty\n", recordIdx);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Create a new record that looks just like the original.
|
|
*/
|
|
memset(&fileDetails, 0, sizeof(fileDetails));
|
|
fileDetails.storageName = pRecord->filename;
|
|
fileDetails.fileSysID = pRecord->recFileSysID;
|
|
fileDetails.fileSysInfo = pRecord->recFileSysInfo;
|
|
fileDetails.access = pRecord->recAccess;
|
|
fileDetails.fileType = pRecord->recFileType;
|
|
fileDetails.extraType = pRecord->recExtraType;
|
|
fileDetails.storageType = pRecord->recStorageType;
|
|
fileDetails.createWhen = pRecord->recCreateWhen;
|
|
fileDetails.modWhen = pRecord->recModWhen;
|
|
fileDetails.archiveWhen = pRecord->recArchiveWhen;
|
|
|
|
err = NuAddRecord(pOutArchive, &fileDetails, &newRecordIdx);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: NuAddRecord failed (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Copy the threads.
|
|
*/
|
|
if (flags & kFlagReverseThreads) {
|
|
for (idx = numThreads-1; idx >= 0; idx--) {
|
|
pThread = NuGetThread(pRecord, idx);
|
|
assert(pThread != nil);
|
|
|
|
err = CopyThread(pInArchive, pOutArchive, flags, pThread,
|
|
newRecordIdx);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
}
|
|
} else {
|
|
for (idx = 0; idx < numThreads; idx++) {
|
|
pThread = NuGetThread(pRecord, idx);
|
|
assert(pThread != nil);
|
|
|
|
err = CopyThread(pInArchive, pOutArchive, flags, pThread,
|
|
newRecordIdx);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Launder an archive from inFile to outFile.
|
|
*
|
|
* Returns 0 on success, nonzero on failure.
|
|
*/
|
|
int
|
|
LaunderArchive(const char* inFile, const char* outFile, long flags)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuArchive* pInArchive = nil;
|
|
NuArchive* pOutArchive = nil;
|
|
const NuMasterHeader* pMasterHeader;
|
|
NuRecordIdx recordIdx;
|
|
long idx, flushStatus;
|
|
|
|
err = NuOpenRO(inFile, &pInArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't open input archive '%s' (err=%d)\n",
|
|
inFile, err);
|
|
goto bail;
|
|
}
|
|
err = NuOpenRW(outFile, kTempFile, kNuOpenCreat|kNuOpenExcl, &pOutArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't open output archive '%s' (err=%d)\n",
|
|
outFile, err);
|
|
goto bail;
|
|
}
|
|
|
|
/* allow duplicates, in case the original archive has them */
|
|
err = NuSetValue(pOutArchive, kNuValueAllowDuplicates, true);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't allow duplicates (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
if (flags & kFlagUseTmp) {
|
|
err = NuSetValue(pOutArchive, kNuValueModifyOrig, false);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr,
|
|
"ERROR: couldn't disable modify orig (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
err = NuGetMasterHeader(pInArchive, &pMasterHeader);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't get master header (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Iterate through the set of records.
|
|
*/
|
|
for (idx = 0; idx < (int)pMasterHeader->mhTotalRecords; idx++) {
|
|
err = NuGetRecordIdxByPosition(pInArchive, idx, &recordIdx);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't get record #%ld (err=%d)\n",
|
|
idx, err);
|
|
goto bail;
|
|
}
|
|
|
|
err = CopyRecord(pInArchive, pOutArchive, flags, recordIdx);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
|
|
/*
|
|
* If "frequent abort" is set, abort what we just did and redo it.
|
|
*/
|
|
if (flags & kFlagFrequentAbort) {
|
|
printf("(abort)\n");
|
|
err = NuAbort(pOutArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: abort failed (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
err = CopyRecord(pInArchive, pOutArchive, flags, recordIdx);
|
|
if (err != kNuErrNone)
|
|
goto bail;
|
|
|
|
}
|
|
|
|
/*
|
|
* If "frequent abort" or "frequent flush" is set, flush after
|
|
* each record is copied.
|
|
*/
|
|
if ((flags & kFlagFrequentAbort) || (flags & kFlagFrequentFlush)) {
|
|
printf("(flush)\n");
|
|
err = NuFlush(pOutArchive, &flushStatus);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr,
|
|
"ERROR: flush failed (err=%d, status=0x%04lx)\n",
|
|
err, flushStatus);
|
|
goto bail;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* first and only flush if frequent-flushing wasn't enabled */
|
|
err = NuFlush(pOutArchive, &flushStatus);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: flush failed (err=%d, status=0x%04lx)\n",
|
|
err, flushStatus);
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
if (pInArchive != nil)
|
|
NuClose(pInArchive);
|
|
if (pOutArchive != nil) {
|
|
if (err != kNuErrNone)
|
|
NuAbort(pOutArchive);
|
|
NuClose(pOutArchive); /* flush pending changes and close */
|
|
}
|
|
return (err != kNuErrNone);
|
|
}
|
|
|
|
|
|
/*
|
|
* Print usage info.
|
|
*/
|
|
void
|
|
Usage(const char* argv0)
|
|
{
|
|
fprintf(stderr, "Usage: %s [-crfat] infile.shk outfile.shk\n", argv0);
|
|
}
|
|
|
|
/*
|
|
* Grab the name of an archive to read.
|
|
*/
|
|
int
|
|
main(int argc, char** argv)
|
|
{
|
|
long major, minor, bug;
|
|
const char* pBuildDate;
|
|
long flags = 0;
|
|
char* cp = nil;
|
|
int cc;
|
|
|
|
(void) NuGetVersion(&major, &minor, &bug, &pBuildDate, nil);
|
|
printf("Using NuFX lib %ld.%ld.%ld built on or after %s\n",
|
|
major, minor, bug, pBuildDate);
|
|
|
|
if (argc < 3 || argc > 4) {
|
|
Usage(argv[0]);
|
|
exit(2);
|
|
}
|
|
|
|
if (argc == 4) {
|
|
cp = argv[1];
|
|
if (*cp++ != '-') {
|
|
Usage(argv[0]);
|
|
exit(2);
|
|
}
|
|
|
|
while (*cp != '\0') {
|
|
switch (*cp) {
|
|
case 'c': flags |= kFlagCopyOnly; break;
|
|
case 'r': flags |= kFlagReverseThreads; break;
|
|
case 'f': flags |= kFlagFrequentFlush; break;
|
|
case 'a': flags |= kFlagFrequentAbort; break;
|
|
case 't': flags |= kFlagUseTmp; break;
|
|
default:
|
|
Usage(argv[0]);
|
|
exit(2);
|
|
}
|
|
cp++;
|
|
}
|
|
|
|
argv++;
|
|
}
|
|
|
|
cc = LaunderArchive(argv[1], argv[2], flags);
|
|
|
|
if (cc == 0)
|
|
printf("Success!\n");
|
|
else
|
|
printf("Failed.\n");
|
|
exit(cc != 0);
|
|
}
|
|
|