nulib2/nufxlib-0/samples/Launder.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);
}