ciderpress/nufxlib/samples/ImgConv.c
2015-12-26 16:50:42 -08:00

643 lines
18 KiB
C

/*
* 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.
*
* 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"
#ifndef HAVE_STRCASECMP
static int strcasecmp(const char *str1, const char *str2)
{
while (*str1 && *str2 && toupper(*str1) == toupper(*str2))
str1++, str2++;
return (toupper(*str1) - toupper(*str2));
}
#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];
uint16_t headerLen;
uint16_t version;
uint32_t imageFormat;
uint32_t flags;
uint32_t numBlocks;
uint32_t dataOffset;
uint32_t dataLen;
uint32_t cmntOffset;
uint32_t cmntLen;
uint32_t creatorOffset;
uint32_t creatorLen;
uint32_t spare[4];
} ImgHeader;
/*
* Read a two-byte little-endian value.
*/
void ReadShortLE(FILE* fp, uint16_t* pBuf)
{
*pBuf = getc(fp);
*pBuf += (uint16_t) getc(fp) << 8;
}
/*
* Write a two-byte little-endian value.
*/
void WriteShortLE(FILE* fp, uint16_t val)
{
putc(val, fp);
putc(val >> 8, fp);
}
/*
* Read a four-byte little-endian value.
*/
void ReadLongLE(FILE* fp, uint32_t* pBuf)
{
*pBuf = getc(fp);
*pBuf += (uint32_t) getc(fp) << 8;
*pBuf += (uint32_t) getc(fp) << 16;
*pBuf += (uint32_t) getc(fp) << 24;
}
/*
* Write a four-byte little-endian value.
*/
void WriteLongLE(FILE* fp, uint32_t 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)
{
size_t ignored;
ignored = fread(pHeader->magic, 4, 1, fp);
ignored = fread(pHeader->creator, 4, 1, fp);
ReadShortLE(fp, &pHeader->headerLen);
ReadShortLE(fp, &pHeader->version);
ReadLongLE(fp, &pHeader->imageFormat);
ReadLongLE(fp, &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]);
(void) ignored;
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 = %u\n", pHeader->imageFormat);
printf("\tflags = 0x%08x\n", pHeader->flags);
printf("\tnumBlocks = %u\n", pHeader->numBlocks);
printf("\tdataOffset = %u\n", pHeader->dataOffset);
printf("\tdataLen = %u\n", pHeader->dataLen);
printf("\tcmntOffset = %u\n", pHeader->cmntOffset);
printf("\tcmntLen = %u\n", pHeader->cmntLen);
printf("\tcreatorOffset = %u\n", pHeader->creatorOffset);
printf("\tcreatorLen = %u\n", pHeader->creatorLen);
printf("\n");
}
/*
* ===========================================================================
* Main functions
* ===========================================================================
*/
typedef enum ArchiveKind { kKindUnknown, kKindShk, kKindImg } ArchiveKind;
/*
* This gets called when a buffer DataSource is no longer needed.
*/
NuResult FreeCallback(NuArchive* pArchive, void* args)
{
free(args);
return kNuOK;
}
/*
* This gets called when an "FP" DataSource is no longer needed.
*/
NuResult FcloseCallback(NuArchive* pArchive, void* args)
{
fclose((FILE*) args);
return kNuOK;
}
/*
* 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.
*
* This supplies an FcloseCallback so that we can exercise that feature
* of NufxLib. We could just as easily not set it and call fclose()
* ourselves, because the structure of this program is pretty simple.
*/
NuError CreateProdosSource(const ImgHeader* pHeader, FILE* fp,
NuDataSource** ppDataSource)
{
return NuCreateDataSourceForFP(kNuThreadFormatUncompressed, 0, fp,
pHeader->dataOffset, pHeader->dataLen, FcloseCallback,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 = NULL;
long offset;
if (pHeader->dataLen % 4096) {
fprintf(stderr,
"ERROR: image size must be multiple of 4096 (%u 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 == NULL) {
fprintf(stderr, "ERROR: malloc(%u) 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 < (long) pHeader->dataLen; offset += 4096) {
size_t ignored;
ignored = fread(diskBuffer + offset + 0x0000, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0e00, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0d00, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0c00, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0b00, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0a00, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0900, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0800, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0700, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0600, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0500, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0400, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0300, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0200, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0100, 256, 1, fp);
ignored = fread(diskBuffer + offset + 0x0f00, 256, 1, fp);
(void) ignored;
}
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, 0,
(const uint8_t*) diskBuffer, 0, pHeader->dataLen,
FreeCallback, ppDataSource);
if (err == kNuErrNone)
diskBuffer = NULL;
bail:
if (diskBuffer != NULL)
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 = NULL;
NuDataSource* pDataSource = NULL;
NuRecordIdx recordIdx;
NuFileDetails fileDetails;
ImgHeader header;
FILE* fp = NULL;
uint32_t flushStatus;
char* storageName = NULL;
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 != NULL)
*cp = '\0';
cp = strrchr(storageName, kLocalFssep);
if (cp != NULL && *(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.storageNameMOR = 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);
fp = NULL;
break;
case kImageFormatProDOS:
err = CreateProdosSource(&header, fp, &pDataSource);
fp = NULL;
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,
NULL);
if (err != kNuErrNone) {
fprintf(stderr, "ERROR: unable to create thread (err=%d)\n", err);
goto bail;
}
pDataSource = NULL; /* 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%04x)\n",
err, flushStatus);
goto bail;
}
err = NuClose(pArchive);
if (err != kNuErrNone) {
fprintf(stderr, "ERROR: close failed (err=%d)\n", err);
goto bail;
}
pArchive = NULL;
bail:
if (pArchive != NULL) {
(void)NuAbort(pArchive);
(void)NuClose(pArchive);
}
NuFreeDataSource(pDataSource);
if (storageName != NULL)
free(storageName);
if (fp != NULL)
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 = NULL;
NuDataSink* pDataSink = NULL;
NuRecordIdx recordIdx;
const NuRecord* pRecord;
const NuThread* pThread = NULL;
ImgHeader header;
FILE* fp = NULL;
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 != NULL)
NuClose(pArchive);
NuFreeDataSink(pDataSink);
if (fp != NULL)
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 == NULL)
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);
}