mirror of
https://github.com/fadden/nulib2.git
synced 2024-11-19 14:33:47 +00:00
625 lines
17 KiB
C
625 lines
17 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.
|
|
*
|
|
* 2IMG <-> SHK converter. This is a practical example of using the
|
|
* NufxLib Thread functions to add and extract data in the middle of a file.
|
|
*
|
|
* Conversions from 2IMG files do not work for raw nibble images.
|
|
* Conversions from SHK archives only work if the disk image is in the
|
|
* first record in the archive. (This is easy to fix, but I'm trying to
|
|
* keep it simple.)
|
|
*/
|
|
#include "NufxLib.h"
|
|
#include "Common.h"
|
|
|
|
/* we can get this out of NufxLib if the OS doesn't have it */
|
|
#ifndef HAVE_STRCASECMP
|
|
#define strcasecmp Nu_strcasecmp
|
|
int Nu_strcasecmp(const char *s1, const char *s2);
|
|
#endif
|
|
|
|
|
|
#define kTempFile "imgconv.tmp"
|
|
#define kLocalFssep PATH_SEP
|
|
#define false 0
|
|
#define true (!false)
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* 2IMG stuff
|
|
* ===========================================================================
|
|
*/
|
|
|
|
#define kImgMagic "2IMG"
|
|
#define kMyCreator "NFXL"
|
|
|
|
#define kImageFormatDOS 0
|
|
#define kImageFormatProDOS 1
|
|
#define kImageFormatNibble 2
|
|
|
|
/*
|
|
* 2IMG header definition (http://www.magnet.ch/emutech/Tech/).
|
|
*/
|
|
typedef struct ImgHeader {
|
|
char magic[4];
|
|
char creator[4];
|
|
short headerLen;
|
|
short version;
|
|
long imageFormat;
|
|
unsigned long flags;
|
|
long numBlocks;
|
|
long dataOffset;
|
|
long dataLen;
|
|
long cmntOffset;
|
|
long cmntLen;
|
|
long creatorOffset;
|
|
long creatorLen;
|
|
long spare[4];
|
|
} ImgHeader;
|
|
|
|
/*
|
|
* Read a two-byte little-endian value.
|
|
*/
|
|
void
|
|
ReadShortLE(FILE* fp, short* pBuf)
|
|
{
|
|
*pBuf = getc(fp);
|
|
*pBuf += (short) getc(fp) << 8;
|
|
}
|
|
|
|
/*
|
|
* Write a two-byte little-endian value.
|
|
*/
|
|
void
|
|
WriteShortLE(FILE* fp, unsigned short val)
|
|
{
|
|
putc(val, fp);
|
|
putc(val >> 8, fp);
|
|
}
|
|
|
|
/*
|
|
* Read a four-byte little-endian value.
|
|
*/
|
|
void
|
|
ReadLongLE(FILE* fp, long* pBuf)
|
|
{
|
|
*pBuf = getc(fp);
|
|
*pBuf += (long) getc(fp) << 8;
|
|
*pBuf += (long) getc(fp) << 16;
|
|
*pBuf += (long) getc(fp) << 24;
|
|
}
|
|
|
|
/*
|
|
* Write a four-byte little-endian value.
|
|
*/
|
|
void
|
|
WriteLongLE(FILE* fp, unsigned long val)
|
|
{
|
|
putc(val, fp);
|
|
putc(val >> 8, fp);
|
|
putc(val >> 16, fp);
|
|
putc(val >> 24, fp);
|
|
}
|
|
|
|
/*
|
|
* Read the header from a 2IMG file.
|
|
*/
|
|
int
|
|
ReadImgHeader(FILE* fp, ImgHeader* pHeader)
|
|
{
|
|
fread(pHeader->magic, 4, 1, fp);
|
|
fread(pHeader->creator, 4, 1, fp);
|
|
ReadShortLE(fp, &pHeader->headerLen);
|
|
ReadShortLE(fp, &pHeader->version);
|
|
ReadLongLE(fp, &pHeader->imageFormat);
|
|
ReadLongLE(fp, (long*)&pHeader->flags);
|
|
ReadLongLE(fp, &pHeader->numBlocks);
|
|
ReadLongLE(fp, &pHeader->dataOffset);
|
|
ReadLongLE(fp, &pHeader->dataLen);
|
|
ReadLongLE(fp, &pHeader->cmntOffset);
|
|
ReadLongLE(fp, &pHeader->cmntLen);
|
|
ReadLongLE(fp, &pHeader->creatorOffset);
|
|
ReadLongLE(fp, &pHeader->creatorLen);
|
|
ReadLongLE(fp, &pHeader->spare[0]);
|
|
ReadLongLE(fp, &pHeader->spare[1]);
|
|
ReadLongLE(fp, &pHeader->spare[2]);
|
|
ReadLongLE(fp, &pHeader->spare[3]);
|
|
|
|
if (feof(fp) || ferror(fp))
|
|
return -1;
|
|
|
|
if (strncmp(pHeader->magic, kImgMagic, 4) != 0) {
|
|
fprintf(stderr, "ERROR: bad magic number on 2IMG file\n");
|
|
return -1;
|
|
}
|
|
|
|
if (pHeader->version > 1) {
|
|
fprintf(stderr, "WARNING: might not be able to handle version=%d\n",
|
|
pHeader->version);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Write the header to a 2IMG file.
|
|
*/
|
|
int
|
|
WriteImgHeader(FILE* fp, ImgHeader* pHeader)
|
|
{
|
|
fwrite(pHeader->magic, 4, 1, fp);
|
|
fwrite(pHeader->creator, 4, 1, fp);
|
|
WriteShortLE(fp, pHeader->headerLen);
|
|
WriteShortLE(fp, pHeader->version);
|
|
WriteLongLE(fp, pHeader->imageFormat);
|
|
WriteLongLE(fp, pHeader->flags);
|
|
WriteLongLE(fp, pHeader->numBlocks);
|
|
WriteLongLE(fp, pHeader->dataOffset);
|
|
WriteLongLE(fp, pHeader->dataLen);
|
|
WriteLongLE(fp, pHeader->cmntOffset);
|
|
WriteLongLE(fp, pHeader->cmntLen);
|
|
WriteLongLE(fp, pHeader->creatorOffset);
|
|
WriteLongLE(fp, pHeader->creatorLen);
|
|
WriteLongLE(fp, pHeader->spare[0]);
|
|
WriteLongLE(fp, pHeader->spare[1]);
|
|
WriteLongLE(fp, pHeader->spare[2]);
|
|
WriteLongLE(fp, pHeader->spare[3]);
|
|
|
|
if (ferror(fp))
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* Dump the contents of an ImgHeader.
|
|
*/
|
|
void
|
|
DumpImgHeader(ImgHeader* pHeader)
|
|
{
|
|
printf("--- header contents:\n");
|
|
printf("\tmagic = '%.4s'\n", pHeader->magic);
|
|
printf("\tcreator = '%.4s'\n", pHeader->creator);
|
|
printf("\theaderLen = %d\n", pHeader->headerLen);
|
|
printf("\tversion = %d\n", pHeader->version);
|
|
printf("\timageFormat = %ld\n", pHeader->imageFormat);
|
|
printf("\tflags = 0x%08lx\n", pHeader->flags);
|
|
printf("\tnumBlocks = %ld\n", pHeader->numBlocks);
|
|
printf("\tdataOffset = %ld\n", pHeader->dataOffset);
|
|
printf("\tdataLen = %ld\n", pHeader->dataLen);
|
|
printf("\tcmntOffset = %ld\n", pHeader->cmntOffset);
|
|
printf("\tcmntLen = %ld\n", pHeader->cmntLen);
|
|
printf("\tcreatorOffset = %ld\n", pHeader->creatorOffset);
|
|
printf("\tcreatorLen = %ld\n", pHeader->creatorLen);
|
|
printf("\n");
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Main functions
|
|
* ===========================================================================
|
|
*/
|
|
|
|
typedef enum ArchiveKind { kKindUnknown, kKindShk, kKindImg } ArchiveKind;
|
|
|
|
/*
|
|
* Create a data source for a ProDOS-ordered image. Since this is already
|
|
* in the correct format, we just point at the correct offset in the 2MG file.
|
|
*/
|
|
NuError
|
|
CreateProdosSource(const ImgHeader* pHeader, FILE* fp,
|
|
NuDataSource** ppDataSource)
|
|
{
|
|
return NuCreateDataSourceForFP(kNuThreadFormatUncompressed, false, 0, fp,
|
|
pHeader->dataOffset, pHeader->dataLen, ppDataSource);
|
|
}
|
|
|
|
/*
|
|
* Create a data source for a DOS-ordered image. This is a little harder,
|
|
* since we have to reorder the blocks into ProDOS ordering for ShrinkIt.
|
|
*/
|
|
NuError
|
|
CreateDosSource(const ImgHeader* pHeader, FILE* fp,
|
|
NuDataSource** ppDataSource)
|
|
{
|
|
NuError err;
|
|
char* diskBuffer = nil;
|
|
long offset;
|
|
|
|
if (pHeader->dataLen % 4096) {
|
|
fprintf(stderr,
|
|
"ERROR: image size must be multiple of 4096 (%ld isn't)\n",
|
|
pHeader->dataLen);
|
|
err = kNuErrGeneric;
|
|
goto bail;
|
|
}
|
|
|
|
if (fseek(fp, pHeader->dataOffset, SEEK_SET) < 0) {
|
|
err = errno;
|
|
perror("fseek failed");
|
|
goto bail;
|
|
}
|
|
|
|
diskBuffer = malloc(pHeader->dataLen);
|
|
if (diskBuffer == nil) {
|
|
fprintf(stderr, "ERROR: malloc(%ld) failed\n", pHeader->dataLen);
|
|
err = kNuErrMalloc;
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Run through the image, reordering each track. This is a
|
|
* reversible transformation, i.e. if you do this twice you're back
|
|
* to ProDOS ordering.
|
|
*/
|
|
for (offset = 0; offset < pHeader->dataLen; offset += 4096) {
|
|
fread(diskBuffer + offset + 0x0000, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0e00, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0d00, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0c00, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0b00, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0a00, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0900, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0800, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0700, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0600, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0500, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0400, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0300, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0200, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0100, 256, 1, fp);
|
|
fread(diskBuffer + offset + 0x0f00, 256, 1, fp);
|
|
}
|
|
if (feof(fp) || ferror(fp)) {
|
|
err = errno ? errno : -1;
|
|
fprintf(stderr, "ERROR: failed while reading source file\n");
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Create a data source for the buffer. We set the "doClose" flag to
|
|
* "true", so NufxLib will free the buffer for us.
|
|
*/
|
|
err = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, true, 0,
|
|
(const unsigned char*) diskBuffer, 0, pHeader->dataLen,
|
|
ppDataSource);
|
|
if (err == kNuErrNone)
|
|
diskBuffer = nil;
|
|
|
|
bail:
|
|
if (diskBuffer != nil)
|
|
free(diskBuffer);
|
|
return err;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Convert a 2IMG file into a new SHK archive.
|
|
*
|
|
* This requires opening up the 2IMG file, verifying that it's okay, and
|
|
* then creating a new disk image record and thread.
|
|
*/
|
|
int
|
|
ConvertFromImgToShk(const char* srcName, const char* dstName)
|
|
{
|
|
NuError err;
|
|
NuArchive* pArchive = nil;
|
|
NuDataSource* pDataSource = nil;
|
|
NuRecordIdx recordIdx;
|
|
NuFileDetails fileDetails;
|
|
ImgHeader header;
|
|
FILE* fp = nil;
|
|
long flushStatus;
|
|
char* storageName = nil;
|
|
char* cp;
|
|
|
|
printf("Converting 2IMG file '%s' to ShrinkIt archive '%s'\n\n",
|
|
srcName, dstName);
|
|
|
|
err = kNuErrGeneric;
|
|
|
|
fp = fopen(srcName, kNuFileOpenReadOnly);
|
|
if (fp == NULL) {
|
|
perror("fopen failed");
|
|
goto bail;
|
|
}
|
|
|
|
if (ReadImgHeader(fp, &header) < 0) {
|
|
fprintf(stderr, "ERROR: header read failed\n");
|
|
goto bail;
|
|
}
|
|
|
|
DumpImgHeader(&header);
|
|
|
|
if (header.imageFormat != kImageFormatDOS &&
|
|
header.imageFormat != kImageFormatProDOS)
|
|
{
|
|
fprintf(stderr, "ERROR: can only handle DOS and ProDOS images\n");
|
|
goto bail;
|
|
}
|
|
|
|
if (header.numBlocks > 1600)
|
|
printf("WARNING: that's a big honking image!\n");
|
|
|
|
/*
|
|
* Open a new archive read-write. This refuses to overwrite an
|
|
* existing file.
|
|
*/
|
|
(void) unlink(kTempFile);
|
|
err = NuOpenRW(dstName, kTempFile, kNuOpenCreat|kNuOpenExcl, &pArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create archive (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/* create the name that will be stored in the archive */
|
|
storageName = strdup(dstName);
|
|
cp = strrchr(storageName, '.');
|
|
if (cp != nil)
|
|
*cp = '\0';
|
|
cp = strrchr(storageName, kLocalFssep);
|
|
if (cp != nil && *(cp+1) != '\0')
|
|
cp++;
|
|
else
|
|
cp = storageName;
|
|
|
|
/*
|
|
* We can't say "add file", because NufxLib doesn't know what a 2MG
|
|
* archive is. However, we can point a DataSource at the data in
|
|
* the file, and construct the record manually.
|
|
*/
|
|
|
|
/* set up the contents of the NuFX Record */
|
|
memset(&fileDetails, 0, sizeof(fileDetails));
|
|
fileDetails.storageName = cp;
|
|
fileDetails.fileSysID = kNuFileSysUnknown; /* DOS? ProDOS? */
|
|
fileDetails.fileSysInfo = kLocalFssep;
|
|
fileDetails.access = kNuAccessUnlocked;
|
|
fileDetails.extraType = header.numBlocks;
|
|
fileDetails.storageType = 512;
|
|
/* FIX - ought to set the file dates */
|
|
|
|
/* add a new record */
|
|
err = NuAddRecord(pArchive, &fileDetails, &recordIdx);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create record (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Create a data source for the 2IMG file. We do this differently
|
|
* for DOS and ProDOS, because we have to rearrange the sector
|
|
* ordering for DOS-ordered images (ShrinkIt always uses ProDOS order).
|
|
*/
|
|
switch (header.imageFormat) {
|
|
case kImageFormatDOS:
|
|
err = CreateDosSource(&header, fp, &pDataSource);
|
|
break;
|
|
case kImageFormatProDOS:
|
|
err = CreateProdosSource(&header, fp, &pDataSource);
|
|
break;
|
|
default:
|
|
fprintf(stderr, "How the heck did I get here?");
|
|
err = kNuErrInternal;
|
|
goto bail;
|
|
}
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create data source (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/* add a disk image thread */
|
|
err = NuAddThread(pArchive, recordIdx, kNuThreadIDDiskImage, pDataSource,
|
|
nil);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create thread (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
pDataSource = nil; /* library owns it now */
|
|
|
|
/* nothing happens until we Flush */
|
|
err = NuFlush(pArchive, &flushStatus);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: flush failed (err=%d, status=0x%04lx)\n",
|
|
err, flushStatus);
|
|
goto bail;
|
|
}
|
|
err = NuClose(pArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: close failed (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
pArchive = nil;
|
|
|
|
bail:
|
|
if (pArchive != nil) {
|
|
(void)NuAbort(pArchive);
|
|
(void)NuClose(pArchive);
|
|
}
|
|
NuFreeDataSource(pDataSource);
|
|
if (storageName != nil)
|
|
free(storageName);
|
|
if (fp != nil)
|
|
fclose(fp);
|
|
return (err == kNuErrNone) ? 0 : -1;
|
|
}
|
|
|
|
|
|
/*
|
|
* Convert an SHK archive into a 2IMG file.
|
|
*
|
|
* This takes a simple-minded approach and assumes that the first record
|
|
* in the archive has the disk image in it. If it doesn't, we give up.
|
|
*/
|
|
int
|
|
ConvertFromShkToImg(const char* srcName, const char* dstName)
|
|
{
|
|
NuError err;
|
|
NuArchive* pArchive = nil;
|
|
NuDataSink* pDataSink = nil;
|
|
NuRecordIdx recordIdx;
|
|
const NuRecord* pRecord;
|
|
const NuThread* pThread = nil;
|
|
ImgHeader header;
|
|
FILE* fp = nil;
|
|
int idx;
|
|
|
|
printf("Converting ShrinkIt archive '%s' to 2IMG file '%s'\n\n",
|
|
srcName, dstName);
|
|
|
|
/*
|
|
* Open the archive.
|
|
*/
|
|
err = NuOpenRO(srcName, &pArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to open archive (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/* get the first record */
|
|
err = NuGetRecordIdxByPosition(pArchive, 0, &recordIdx);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to get first recordIdx (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
err = NuGetRecord(pArchive, recordIdx, &pRecord);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to get first record (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
/* find a disk image thread */
|
|
for (idx = 0; idx < (int)NuRecordGetNumThreads(pRecord); idx++) {
|
|
pThread = NuGetThread(pRecord, idx);
|
|
|
|
if (NuGetThreadID(pThread) == kNuThreadIDDiskImage)
|
|
break;
|
|
}
|
|
if (idx == (int)NuRecordGetNumThreads(pRecord)) {
|
|
fprintf(stderr, "ERROR: no disk image found in first record\n");
|
|
err = -1;
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Looks good. Open the 2IMG file, and create the header.
|
|
*/
|
|
if (access(dstName, F_OK) == 0) {
|
|
fprintf(stderr, "ERROR: output file already exists\n");
|
|
err = -1;
|
|
goto bail;
|
|
}
|
|
|
|
fp = fopen(dstName, kNuFileOpenWriteTrunc);
|
|
if (fp == NULL) {
|
|
perror("fopen failed");
|
|
goto bail;
|
|
}
|
|
|
|
/* set up the 2MG header, based on the NuFX Record */
|
|
memset(&header, 0, sizeof(header));
|
|
memcpy(header.magic, kImgMagic, sizeof(header.magic));
|
|
memcpy(header.creator, kMyCreator, sizeof(header.creator));
|
|
header.headerLen = 64;
|
|
header.version = 1;
|
|
header.imageFormat = kImageFormatProDOS; /* always ProDOS-order */
|
|
header.numBlocks = pRecord->recExtraType;
|
|
header.dataOffset = 64;
|
|
/* old versions of ShrinkIt blew the threadEOF, so use NufxLib's "actual" */
|
|
header.dataLen = pThread->actualThreadEOF;
|
|
DumpImgHeader(&header);
|
|
if (WriteImgHeader(fp, &header) < 0) {
|
|
fprintf(stderr, "ERROR: header write failed\n");
|
|
err = -1;
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* We want to expand the disk image thread into "fp" at the current
|
|
* offset.
|
|
*/
|
|
err = NuCreateDataSinkForFP(true, kNuConvertOff, fp, &pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create data sink (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
err = NuExtractThread(pArchive, pThread->threadIdx, pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to extract thread (err=%d)\n", err);
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
if (pArchive != nil)
|
|
NuClose(pArchive);
|
|
NuFreeDataSink(pDataSink);
|
|
if (fp != nil)
|
|
fclose(fp);
|
|
return (err == kNuErrNone) ? 0 : -1;
|
|
}
|
|
|
|
|
|
/*
|
|
* Figure out what kind of archive this is by looking at the filename.
|
|
*/
|
|
ArchiveKind
|
|
DetermineKind(const char* filename)
|
|
{
|
|
const char* dot;
|
|
|
|
dot = strrchr(filename, '.');
|
|
if (dot == nil)
|
|
return kKindUnknown;
|
|
|
|
if (strcasecmp(dot, ".shk") == 0 || strcasecmp(dot, ".sdk") == 0)
|
|
return kKindShk;
|
|
else if (strcasecmp(dot, ".2mg") == 0)
|
|
return kKindImg;
|
|
|
|
return kKindUnknown;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Figure out what we want to do.
|
|
*/
|
|
int
|
|
main(int argc, char** argv)
|
|
{
|
|
ArchiveKind kind;
|
|
int cc;
|
|
|
|
if (argc != 3) {
|
|
fprintf(stderr, "Usage: %s (input.2mg|input.shk) output\n", argv[0]);
|
|
exit(2);
|
|
}
|
|
|
|
kind = DetermineKind(argv[1]);
|
|
if (kind == kKindUnknown) {
|
|
fprintf(stderr, "ERROR: input name must end in '.shk' or '.2mg'\n");
|
|
exit(2);
|
|
}
|
|
|
|
if (kind == kKindShk)
|
|
cc = ConvertFromShkToImg(argv[1], argv[2]);
|
|
else
|
|
cc = ConvertFromImgToShk(argv[1], argv[2]);
|
|
|
|
if (cc)
|
|
fprintf(stderr, "Failed\n");
|
|
else
|
|
printf("Done!\n");
|
|
|
|
exit(cc != 0);
|
|
}
|
|
|