/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Code for handling disk image "wrappers", things like DiskCopy, 2MG, and
 * SHK that surround a disk image.
 *
 * Returning with kDIErrBadChecksum from Test or Prep is taken as a sign
 * that, while we have correctly identified the wrapper format, the contents
 * of the file are corrupt, and the user needs to be told.
 *
 * Some formats, such as 2MG, include a DOS volume number.  This is useful
 * because DOS actually embeds the volume number in sector headers; the value
 * stored in the VTOC is ignored by certain things (notably some games with
 * trivial copy-protection).  This value needs to be preserved.  It's
 * unclear how useful this will actually be; mostly we just want to preserve
 * it when translating from one format to another.
 *
 * If a library (such as NufxLib) needs to read an actual file, it can
 * (usually) pry the name out of the GFD.
 *
 * In general, it should be possible to write to any "wrapped" file that we
 * can read from.  For things like NuFX and DDD, this means we need to be
 * able to re-compress the image file when we're done with it.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"
#include "TwoImg.h"


/*
 * ===========================================================================
 *		2MG (a/k/a 2IMG)
 * ===========================================================================
 */

/*
 * Test to see if this is a 2MG file.
 *
 * The easiest way to do that is to open up the header and see if
 * it looks valid.
 */
/*static*/ DIError
Wrapper2MG::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
	TwoImgHeader header;

	WMSG0("Testing for 2MG\n");

	// HEY: should test for wrappedLength > 2GB; if so, skip

	pGFD->Rewind();
	if (header.ReadHeader(pGFD, (long) wrappedLength) != 0)
		return kDIErrGeneric;

	WMSG0("Looks like valid 2MG\n");
	return kDIErrNone;
}

/*
 * Get the header (again) and use it to locate the data.
 */
DIError
Wrapper2MG::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
	TwoImgHeader header;
    long offset;

	WMSG0("Prepping for 2MG\n");
	pGFD->Rewind();
	if (header.ReadHeader(pGFD, (long) wrappedLength) != 0)
		return kDIErrGeneric;

	offset = header.fDataOffset;

    if (header.fFlags & TwoImgHeader::kDOSVolumeSet)
        *pDiskVolNum = header.GetDOSVolumeNum();

	*pLength = header.fDataLen;
	*pPhysical = DiskImg::kPhysicalFormatSectors;
	if (header.fImageFormat == TwoImgHeader::kImageFormatDOS)
		*pOrder = DiskImg::kSectorOrderDOS;
	else if (header.fImageFormat == TwoImgHeader::kImageFormatProDOS)
		*pOrder = DiskImg::kSectorOrderProDOS;
	else if (header.fImageFormat == TwoImgHeader::kImageFormatNibble) {
		*pOrder = DiskImg::kSectorOrderPhysical;
        if (*pLength == kTrackCount525 * kTrackLenNib525) {
            WMSG0("  Prepping for 6656-byte 2MG-NIB\n");
            *pPhysical = DiskImg::kPhysicalFormatNib525_6656;
        } else if (*pLength == kTrackCount525 * kTrackLenNb2525) {
            WMSG0("  Prepping for 6384-byte 2MG-NB2\n");
            *pPhysical = DiskImg::kPhysicalFormatNib525_6384;
        } else {
            WMSG1("  NIB 2MG with length=%ld rejected\n", (long) *pLength);
            return kDIErrOddLength;
        }
    }

    *ppNewGFD = new GFDGFD;
    return ((GFDGFD*)*ppNewGFD)->Open(pGFD, offset, readOnly);
}

/*
 * Initialize fields for a new file.
 */
DIError
Wrapper2MG::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    TwoImgHeader header;
    int cc;

    switch (physical) {
    case DiskImg::kPhysicalFormatNib525_6656:
        if (length != kTrackLenNib525 * kTrackCount525) {
            WMSG1("Invalid 2MG nibble length %ld\n", (long) length);
            return kDIErrInvalidArg;
        }
        header.InitHeader(TwoImgHeader::kImageFormatNibble, (long) length,
            8 * kTrackCount525);	// 8 blocks per track
        break;
    case DiskImg::kPhysicalFormatSectors:
        if ((length % 512) != 0) {
            WMSG1("Invalid 2MG length %ld\n", (long) length);
            return kDIErrInvalidArg;
        }
        if (order == DiskImg::kSectorOrderProDOS)
            cc = header.InitHeader(TwoImgHeader::kImageFormatProDOS,
				(long) length, (long) length / 512);
        else if (order == DiskImg::kSectorOrderDOS)
            cc = header.InitHeader(TwoImgHeader::kImageFormatDOS,
				(long) length, (long) length / 512);
        else {
            WMSG1("Invalid 2MG sector order %d\n", order);
            return kDIErrInvalidArg;
        }
		if (cc != 0) {
			WMSG1("TwoImg InitHeader failed (len=%ld)\n", (long) length);
			return kDIErrInvalidArg;
		}
        break;
    default:
        WMSG1("Invalid 2MG physical %d\n", physical);
        return kDIErrInvalidArg;
    }

    if (dosVolumeNum != DiskImg::kVolumeNumNotSet)
        header.SetDOSVolumeNum(dosVolumeNum);

    cc = header.WriteHeader(pWrapperGFD);
    if (cc != 0) {
        WMSG1("ERROR: 2MG header write failed (cc=%d)\n", cc);
        return kDIErrGeneric;
    }

	long footerLen = header.fCmtLen + header.fCreatorLen;
	if (footerLen > 0) {
		// This is currently impossible, which is good because the Seek call
		// will fail if pWrapperGFD is a buffer.
		assert(false);
		pWrapperGFD->Seek(header.fDataOffset + length, kSeekSet);
		header.WriteFooter(pWrapperGFD);
	}

    long offset = header.fDataOffset;


    *pWrappedLength = length + offset + footerLen;
    *pDataFD = new GFDGFD;
    return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, offset, false);
}

/*
 * We only use GFDGFD, so there's nothing to do here.
 *
 * If we want to support changing the comment field in an open image, we'd
 * need to handle making the file longer or shorter here.  Right now we
 * just ignore everything that comes before or after the start of the data.
 * Since there's no checksum, none of the header fields change, so we
 * don't even deal with that.
 */
DIError
Wrapper2MG::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    return kDIErrNone;
}


/*
 * ===========================================================================
 *		SHK (ShrinkIt NuFX), also .SDK and .BXY
 * ===========================================================================
 */

/*
 * NOTE: this doesn't override the global error message callback because
 * we expect it to be set by the application.
 */

/*
 * Display error messages... or not.
 */
/*static*/ NuResult
WrapperNuFX::ErrMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage)
{
	const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage;

    if (pErrorMessage->isDebug) {
		Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line,
            "[D] %s\n", pErrorMessage->message);
    } else {
        Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line,
            "%s\n", pErrorMessage->message);
    }

	return kNuOK;
}

/*
 * Open a NuFX archive, and verify that it holds exactly one disk archive.
 *
 * On success, the NuArchive pointer and thread idx are set, and 0 is
 * returned.  Returns -1 on failure.
 */
/*static*/ DIError
WrapperNuFX::OpenNuFX(const char* pathName, NuArchive** ppArchive,
	NuThreadIdx* pThreadIdx, long* pLength, bool readOnly)
{
	NuError nerr = kNuErrNone;
	NuArchive* pArchive = nil;
	NuRecordIdx recordIdx;
	NuAttr attr;
	const NuRecord* pRecord;
	const NuThread* pThread = nil;
	int idx;

	WMSG1("Opening file '%s' to test for NuFX\n", pathName);

	/*
	 * Open the archive.
	 */
	if (readOnly) {
		nerr = NuOpenRO(pathName, &pArchive);
		if (nerr != kNuErrNone) {
			WMSG1(" NuFX unable to open archive (err=%d)\n", nerr);
			goto bail;
		}
	} else {
		char* tmpPath;

		tmpPath = GenTempPath(pathName);
		if (tmpPath == nil) {
			nerr = kNuErrInternal;
			goto bail;
		}

		nerr = NuOpenRW(pathName, tmpPath, 0, &pArchive);
		if (nerr != kNuErrNone) {
			WMSG1(" NuFX OpenRW failed (nerr=%d)\n", nerr);
			nerr = kNuErrGeneric;
			delete[] tmpPath;
			goto bail;
		}
		delete[] tmpPath;
	}

    NuSetErrorMessageHandler(pArchive, ErrMsgHandler);

	nerr = NuGetAttr(pArchive, kNuAttrNumRecords, &attr);
	if (nerr != kNuErrNone) {
		WMSG1(" NuFX unable to get record count (err=%d)\n", nerr);
		goto bail;
	}
	if (attr != 1) {
		WMSG1(" NuFX archive has %ld entries, not disk-only\n", attr);
		nerr = kNuErrGeneric;
		if (attr > 1)
			goto file_archive;
		else
			goto bail;		// shouldn't get zero-count archives, but...
	}

	/* get the first record */
	nerr = NuGetRecordIdxByPosition(pArchive, 0, &recordIdx);
	if (nerr != kNuErrNone) {
		WMSG1(" NuFX unable to get first recordIdx (err=%d)\n", nerr);
		goto bail;
	}
	nerr = NuGetRecord(pArchive, recordIdx, &pRecord);
	if (nerr != kNuErrNone) {
		WMSG1(" NuFX unable to get first record (err=%d)\n", nerr);
		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)) {
		WMSG0(" NuFX no disk image found in first record\n");
		nerr = kNuErrGeneric;
		goto file_archive;
	}
	assert(pThread != nil);
	*pThreadIdx = pThread->threadIdx;

	/*
	 * Don't allow zero-length disks.
	 */
	*pLength = pThread->actualThreadEOF;
	if (!*pLength) {
		WMSG1(" NuFX length of disk image is bad (%ld)\n", *pLength);
		nerr = kNuErrGeneric;
		goto bail;
	}

	/*
	 * Success!
	 */
	assert(nerr == kNuErrNone);
	*ppArchive = pArchive;
	pArchive = nil;

bail:
	if (pArchive != nil)
		NuClose(pArchive);
	if (nerr == kNuErrNone)
		return kDIErrNone;
	else if (nerr == kNuErrBadMHCRC || nerr == kNuErrBadRHCRC)
		return kDIErrBadChecksum;
	else
		return kDIErrGeneric;

file_archive:
	if (pArchive != nil)
		NuClose(pArchive);
	return kDIErrFileArchive;
}

/*
 * Load a disk image into memory.
 *
 * Allocates a buffer with the specified length and loads the desired
 * thread into it.
 *
 * In an LZW-I compressed thread, the third byte of the compressed thread
 * data is the disk volume number that P8 ShrinkIt would use when formatting
 * the disk.  In an LZW-II compressed thread, it's the first byte of the
 * compressed data.  Uncompressed disk images simply don't have the disk
 * volume number in them.  Until NufxLib provides a simple way to access
 * this bit of loveliness, we're going to pretend it's not there.
 *
 * Returns 0 on success, -1 on error.
 */
DIError
WrapperNuFX::GetNuFXDiskImage(NuArchive* pArchive, NuThreadIdx threadIdx,
	long length, char** ppData)
{
	NuError err;
	NuDataSink* pDataSink = nil;
	unsigned char* buf = nil;

	assert(length > 0);
	buf = new unsigned char[length];
	if (buf == nil)
		return kDIErrMalloc;

	/*
	 * Create a buffer and expand the disk image into it.
	 */
    err = NuCreateDataSinkForBuffer(true, kNuConvertOff, buf, length,
			&pDataSink);
	if (err != kNuErrNone) {
		WMSG1(" NuFX: unable to create data sink (err=%d)\n", err);
		goto bail;
	}

	err = NuExtractThread(pArchive, threadIdx, pDataSink);
	if (err != kNuErrNone) {
		WMSG1(" NuFX: unable to extract thread (err=%d)\n", err);
		goto bail;
	}

	//err = kNuErrBadThreadCRC; goto bail;	// debug test only

	*ppData = (char*)buf;

bail:
	NuFreeDataSink(pDataSink);
	if (err != kNuErrNone) {
		WMSG1(" NuFX GetNuFXDiskImage returning after nuerr=%d\n", err);
		delete buf;
	}
	if (err == kNuErrNone)
		return kDIErrNone;
	else if (err == kNuErrBadDataCRC || err == kNuErrBadThreadCRC)
		return kDIErrBadChecksum;
    else if (err == kNuErrBadData)
        return kDIErrBadCompressedData;
	else if (err == kNuErrBadFormat)
		return kDIErrUnsupportedCompression;
	else
		return kDIErrGeneric;
}

/*
 * Test to see if this is a single-record NuFX archive with a disk archive
 * in it.
 */
/*static*/ DIError
WrapperNuFX::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
	DIError dierr;
	NuArchive* pArchive = nil;
	NuThreadIdx threadIdx;
	long length;
    const char* imagePath;

    imagePath = pGFD->GetPathName();
	if (imagePath == nil) {
		WMSG0("Can't test NuFX on non-file\n");
		return kDIErrNotSupported;
	}
	WMSG0("Testing for NuFX\n");
	dierr = OpenNuFX(imagePath, &pArchive, &threadIdx, &length, true);
	if (dierr != kDIErrNone)
		return dierr;

	/* success; throw away state in case they don't like us anyway */
	assert(pArchive != nil);
	NuClose(pArchive);
	
	return kDIErrNone;
}

/*
 * Open the archive, extract the disk image into a memory buffer.
 */
DIError
WrapperNuFX::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
	DIError dierr = kDIErrNone;
	NuThreadIdx threadIdx;
	GFDBuffer* pNewGFD = nil;
	char* buf = nil;
	long length = -1;
    const char* imagePath;

    imagePath = pGFD->GetPathName();
	if (imagePath == nil) {
		assert(false);		// should've been caught in Test
		return kDIErrNotSupported;
	}
	pGFD->Close();		// don't hold the file open
	dierr = OpenNuFX(imagePath, &fpArchive, &threadIdx, &length, readOnly);
	if (dierr != kDIErrNone)
		goto bail;

	dierr = GetNuFXDiskImage(fpArchive, threadIdx, length, &buf);
	if (dierr != kDIErrNone)
		goto bail;

	pNewGFD = new GFDBuffer;
	dierr = pNewGFD->Open(buf, length, true, false, readOnly);
	if (dierr != kDIErrNone)
		goto bail;
	buf = nil;		// now owned by pNewGFD;

	/*
	 * Success!
	 */
	assert(dierr == kDIErrNone);
	*ppNewGFD = pNewGFD;
	*pLength = length;
	*pPhysical = DiskImg::kPhysicalFormatSectors;
	*pOrder = DiskImg::kSectorOrderProDOS;

    WMSG1(" NuFX is ready, threadIdx=%ld\n", threadIdx);
    fThreadIdx = threadIdx;

bail:
	if (dierr != kDIErrNone) {
		NuClose(fpArchive);
        fpArchive = nil;
		delete pNewGFD;
		delete buf;
	}
	return dierr;
}

/*
 * Given a filename, create a suitable temp pathname.
 *
 * This is really the wrong place to be doing this -- the application
 * should get to deal with this -- but it's not the end of the world
 * if we handle it here.  Add to wish list: fix NufxLib so that the
 * temp file can be a memory buffer.
 *
 * Returns a string allocated with new[].
 */
/*static*/ char*
WrapperNuFX::GenTempPath(const char* path)
{
    static const char* kTmpTemplate = "DItmp_XXXXXX";
    char* tmpPath;

    assert(path != nil);
    assert(strlen(path) > 0);

    tmpPath = new char[strlen(path) + 32];
    if (tmpPath == nil)
        return nil;

    strcpy(tmpPath, path);

    /* back up to the first thing that looks like it's an fssep */
    char* cp;
    cp = tmpPath + strlen(tmpPath);
    while (--cp >= tmpPath) {
        if (*cp == '/' || *cp == '\\' || *cp == ':')
            break;
    }

    /* we either fell off the back end or found an fssep; advance */
    cp++;

    strcpy(cp, kTmpTemplate);

    WMSG2("  NuFX GenTempPath '%s' -> '%s'\n", path, tmpPath);

    return tmpPath;
}

/*
 * Initialize fields for a new file.
 *
 * "pWrapperGFD" will be fairly useless after this, because we're
 * recreating the underlying file.  (If it doesn't have an underlying
 * file, then we're hosed.)
 */
DIError
WrapperNuFX::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    assert(physical == DiskImg::kPhysicalFormatSectors);
    assert(order == DiskImg::kSectorOrderProDOS);

    DIError dierr = kDIErrNone;
    NuArchive* pArchive;
    const char* imagePath;
    char* tmpPath = nil;
    unsigned char* buf = nil;
    NuError nerr;

    /*
     * Create the NuFX archive, stomping on the existing file.  (This
     * makes pWrapperGFD invalid, but such is life with NufxLib.)
     */
    imagePath = pWrapperGFD->GetPathName();
	if (imagePath == nil) {
		assert(false);		// must not have an outer wrapper
		dierr = kDIErrNotSupported;
        goto bail;
	}
	pWrapperGFD->Close();		// don't hold the file open
    tmpPath = GenTempPath(imagePath);
    if (tmpPath == nil) {
        dierr = kDIErrInternal;
        goto bail;
    }

    nerr = NuOpenRW(imagePath, tmpPath, kNuOpenCreat, &pArchive);
    if (nerr != kNuErrNone) {
        WMSG1(" NuFX OpenRW failed (nerr=%d)\n", nerr);
        dierr = kDIErrGeneric;
        goto bail;
    }

    /*
     * Create a blank chunk of memory for the image.
     */
	assert(length > 0);
	buf = new unsigned char[(int) length];
	if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

    GFDBuffer* pNewGFD;
	pNewGFD = new GFDBuffer;
	dierr = pNewGFD->Open(buf, length, true, false, false);
	if (dierr != kDIErrNone) {
        delete pNewGFD;
		goto bail;
    }
    *pDataFD = pNewGFD;
	buf = nil;		    // now owned by pNewGFD;

    /*
     * Success!  Set misc stuff.
     */
    fThreadIdx = 0;     // don't have one to overwrite
    fpArchive = pArchive;

bail:
    delete[] tmpPath;
    delete[] buf;
    return dierr;
}

/*
 * Close the NuFX archive.
 */
DIError
WrapperNuFX::CloseNuFX(void)
{
	NuError nerr;

    /* throw away any un-flushed changes so that "close" can't fail */
    (void) NuAbort(fpArchive);

	nerr = NuClose(fpArchive);
	if (nerr != kNuErrNone) {
		WMSG0("WARNING: NuClose failed\n");
		return kDIErrGeneric;
	}
	return kDIErrNone;
}

/*
 * Write the data using the default compression method.
 *
 * Doesn't touch "pWrapperGFD" or "pWrappedLen".  Could probably update
 * "pWrappedLen", but that's really only useful if we have a gzip Outer
 * that wants to know how much data we have.  Because we don't write to
 * pWrapperGFD, we can't have a gzip wrapper, so there's no point in
 * updating it.
 */
DIError
WrapperNuFX::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    NuError nerr = kNuErrNone;
    NuFileDetails fileDetails;
    NuRecordIdx recordIdx;
    NuThreadIdx threadIdx;
    NuDataSource* pDataSource = nil;

	if (fThreadIdx != 0) {
		/*
		 * Mark the old record for deletion.
		 */
		nerr = NuGetRecordIdxByPosition(fpArchive, 0, &recordIdx);
		if (nerr != kNuErrNone) {
			WMSG1(" NuFX unable to get first recordIdx (err=%d)\n", nerr);
			goto bail;
		}
		nerr = NuDeleteRecord(fpArchive, recordIdx);
		if (nerr != kNuErrNone) {
			WMSG1(" NuFX unable to delete first record (err=%d)\n", nerr);
			goto bail;
		}
	}

    assert((dataLen % 512) == 0);

	nerr = NuSetValue(fpArchive, kNuValueDataCompression,
				fCompressType + kNuCompressNone);
	if (nerr != kNuErrNone) {
		WMSG1("WARNING: unable to set compression to format %d\n",
			fCompressType);
		nerr = kNuErrNone;
	} else {
		WMSG2(" NuFX set compression to %d/%d\n", fCompressType,
			fCompressType + kNuCompressNone);
	}

    /*
     * Fill out the fileDetails record appropriately.
     */
    memset(&fileDetails, 0, sizeof(fileDetails));
    fileDetails.threadID = kNuThreadIDDiskImage;
    if (fStorageName != nil)
        fileDetails.storageName = fStorageName;
    else
        fileDetails.storageName = "NEW.DISK";
    fileDetails.fileSysID = kNuFileSysUnknown;
    fileDetails.fileSysInfo = kDefaultStorageFssep;
    fileDetails.storageType = 512;
    fileDetails.extraType = (long) (dataLen / 512);
    fileDetails.access = kNuAccessUnlocked;

    time_t now;
    now = time(nil);
    UNIXTimeToDateTime(&now, &fileDetails.archiveWhen);
    UNIXTimeToDateTime(&now, &fileDetails.modWhen);
    UNIXTimeToDateTime(&now, &fileDetails.createWhen);

    /*
     * Create the new record.
     */
    nerr = NuAddRecord(fpArchive, &fileDetails, &recordIdx);
    if (nerr != kNuErrNone) {
        WMSG1(" NuFX AddRecord failed (nerr=%d)\n", nerr);
        goto bail;
    }

    /*
     * Create a data source for the thread.
     *
     * We need to get the memory buffer from pDataGFD, which we do in
     * a somewhat unwholesome manner.  However, there's no other way to
     * feed the data into NufxLib.
     */
    nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0,
            (const unsigned char*) ((GFDBuffer*) pDataGFD)->GetBuffer(),
            0, (long) dataLen, nil, &pDataSource);
    if (nerr != kNuErrNone) {
        WMSG1(" NuFX unable to create NufxLib data source (nerr=%d)", nerr);
        goto bail;
    }

    /*
     * Add the thread.
     */
    nerr = NuAddThread(fpArchive, recordIdx, kNuThreadIDDiskImage,
            pDataSource, &threadIdx);
    if (nerr != kNuErrNone) {
        WMSG1(" NuFX AddThread failed (nerr=%d)\n", nerr);
        goto bail;
    }
    pDataSource = nil;      // now owned by NufxLib
    WMSG2(" NuFX added thread %ld in record %ld, flushing changes\n",
        threadIdx, recordIdx);

    /*
     * Flush changes (does the actual compression).
     */
    long status;
    nerr = NuFlush(fpArchive, &status);
    if (nerr != kNuErrNone) {
        WMSG2(" NuFX flush failed (nerr=%d, status=%ld)\n", nerr, status);
        goto bail;
    }

	/* update the threadID */
	fThreadIdx = threadIdx;

bail:
    NuFreeDataSource(pDataSource);
    if (nerr != kNuErrNone)
        return kDIErrGeneric;
    return kDIErrNone;
}

/*
 * Common NuFX utility function.  This ought to be in NufxLib.
 */
void
WrapperNuFX::UNIXTimeToDateTime(const time_t* pWhen, NuDateTime *pDateTime)
{
    struct tm* ptm;

    assert(pWhen != nil);
    assert(pDateTime != nil);

    ptm = localtime(pWhen);
    pDateTime->second = ptm->tm_sec;
    pDateTime->minute = ptm->tm_min;
    pDateTime->hour = ptm->tm_hour;
    pDateTime->day = ptm->tm_mday -1;
    pDateTime->month = ptm->tm_mon;
    pDateTime->year = ptm->tm_year;
    pDateTime->extra = 0;
    pDateTime->weekDay = ptm->tm_wday +1;
}


/*
 * ===========================================================================
 *		DDD (DDD 2.1, DDD Pro)
 * ===========================================================================
 */

/*
 * There really isn't a way to test if the file is a DDD archive, except
 * to try to unpack it.  One thing we can do fairly quickly is look for
 * runs of repeated bytes, which will be impossible in a DDD file because
 * we compress runs of repeated bytes with RLE.
 */
/*static*/ DIError
WrapperDDD::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
    DIError dierr;
    GenericFD* pNewGFD = nil;
	WMSG0("Testing for DDD\n");

    pGFD->Rewind();

    dierr = CheckForRuns(pGFD);
    if (dierr != kDIErrNone)
        return dierr;

    dierr = Unpack(pGFD, &pNewGFD, nil);
    delete pNewGFD;
    return dierr;
}

/*
 * Load a bunch of data and check it for repeated byte sequences that
 * would be removed by RLE.  Runs of 4 bytes or longer should have been
 * stripped out.  DDD adds a couple of zeroes onto the end, so to avoid
 * special cases we assume that a run of 5 is okay, and only flunk the
 * data when it gets to 6.
 *
 * One big exception: the "favorites" table isn't run-length encoded,
 * and if the track is nothing but zeroes the entire thing will be
 * filled with 0xff.  So we allow runs of 0xff bytes.
 *
 * PROBLEM: some sequences, such as repeated d5aa, can turn into what looks
 * like a run of bytes in the output.  We can't assume that arbitrary
 * sequences of bytes won't be repeated.  It does appear that we can assume
 * that 00 bytes won't be repeated, so we can still scan for a series of
 * zeroes and reject the image if found (which should clear us for all
 * uncompressed formats and any compressed format with a padded file header).
 *
 * The goal is to detect uncompressed data sources.  The test for DDD
 * should come after other compressed data formats.
 *
 * For speed we crank the data in 8K at a time and don't correctly handle
 * the boundaries.  We do, however, need to avoid scanning the last 256
 * bytes of the file, because DOS DDD just fills it with junk, and it's
 * possible that junk might have runs in it.
 */
/*static*/ DIError
WrapperDDD::CheckForRuns(GenericFD* pGFD)
{
	DIError dierr = kDIErrNone;
    int kRunThreshold = 5;
    unsigned char buf[8192];
    size_t bufCount;
    int runLen;
	di_off_t fileLen;
    int i;

	dierr = pGFD->Seek(0, kSeekEnd);
	if (dierr != kDIErrNone)
		goto bail;
	fileLen = pGFD->Tell();
	pGFD->Rewind();

	fileLen -= 256;		// could be extra data from DOS DDD

    while (fileLen) {
		bufCount = (size_t) fileLen;
		if (bufCount > sizeof(buf))
			bufCount = sizeof(buf);
		fileLen -= bufCount;

        dierr = pGFD->Read(buf, bufCount);
        if (dierr != kDIErrNone)
            goto bail;
        //WMSG1(" DDD READ %d bytes\n", bufCount);
        if (dierr != kDIErrNone) {
            WMSG1(" DDD CheckForRuns read failed (err=%d)\n", dierr);
            return dierr;
        }

        runLen = 0;
        for (i = 1; i < (int) bufCount; i++) {
            if (buf[i] == 0 && buf[i] == buf[i-1]) {
                runLen++;
                if (runLen == kRunThreshold && buf[i] != 0xff) {
                    WMSG2(" DDD found run of >= %d of 0x%02x, bailing\n",
                        runLen+1, buf[i]);
                    return kDIErrGeneric;
                }
            } else {
                runLen = 0;
            }
        }
    }

    WMSG0(" DDD CheckForRuns scan complete, no long runs found\n");

bail:
    return dierr;
}

/*
 * Prepping is much the same as testing, but we fill in a few more details.
 */
DIError
WrapperDDD::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
    DIError dierr;
	WMSG0("Prepping for DDD\n");

    assert(*ppNewGFD == nil);

    dierr = Unpack(pGFD, ppNewGFD, pDiskVolNum);
    if (dierr != kDIErrNone)
        return dierr;

	*pLength = kNumTracks * kTrackLen;
	*pPhysical = DiskImg::kPhysicalFormatSectors;
	*pOrder = DiskImg::kSectorOrderDOS;

    return dierr;
}

/*
 * Unpack a compressed disk image from "pGFD" to a new memory buffer
 * created in "*ppNewGFD".
 */
/*static*/ DIError
WrapperDDD::Unpack(GenericFD* pGFD, GenericFD** ppNewGFD, short* pDiskVolNum)
{
    DIError dierr;
	GFDBuffer* pNewGFD = nil;
    unsigned char* buf = nil;
    short diskVolNum;

	pGFD->Rewind();

    buf = new unsigned char[kNumTracks * kTrackLen];
    if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

	pNewGFD = new GFDBuffer;
    if (pNewGFD == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }
	dierr = pNewGFD->Open(buf, kNumTracks * kTrackLen, true, false, false);
	if (dierr != kDIErrNone)
		goto bail;
	buf = nil;		// now owned by pNewGFD;

    dierr = UnpackDisk(pGFD, pNewGFD, &diskVolNum);
    if (dierr != kDIErrNone)
        goto bail;

    if (pDiskVolNum != nil)
        *pDiskVolNum = diskVolNum;
    *ppNewGFD = pNewGFD;
    pNewGFD = nil;  // now owned by caller

bail:
    delete[] buf;
    delete pNewGFD;
    return dierr;
}

/*
 * Initialize stuff for a new file.  There's no file header or other
 * goodies, so we leave "pWrapperGFD" and "pWrappedLength" alone.
 */
DIError
WrapperDDD::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    assert(length == kNumTracks * kTrackLen);
    assert(physical == DiskImg::kPhysicalFormatSectors);
    assert(order == DiskImg::kSectorOrderDOS);

    DIError dierr;
	unsigned char* buf = nil;

    /*
     * Create a blank chunk of memory for the image.
     */
    buf = new unsigned char[(int) length];
	if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

    GFDBuffer* pNewGFD;
	pNewGFD = new GFDBuffer;
	dierr = pNewGFD->Open(buf, length, true, false, false);
	if (dierr != kDIErrNone) {
        delete pNewGFD;
		goto bail;
    }
    *pDataFD = pNewGFD;
	buf = nil;		    // now owned by pNewGFD;

    // can't set *pWrappedLength yet

    if (dosVolumeNum != DiskImg::kVolumeNumNotSet)
        fDiskVolumeNum = dosVolumeNum;
    else
        fDiskVolumeNum = kDefaultNibbleVolumeNum;

bail:
    delete[] buf;
    return dierr;
}

/*
 * Compress the disk image.
 */
DIError
WrapperDDD::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    DIError dierr;

    assert(dataLen == kNumTracks * kTrackLen);

    pDataGFD->Rewind();

    dierr = PackDisk(pDataGFD, pWrapperGFD, fDiskVolumeNum);
    if (dierr != kDIErrNone)
        return dierr;

    *pWrappedLen = pWrapperGFD->Tell();
    WMSG2("  DDD compressed from %d to %ld\n",
        kNumTracks * kTrackLen, (long) *pWrappedLen);

    return kDIErrNone;
}


/*
 * ===========================================================================
 *		DiskCopy (primarily a Mac format)
 * ===========================================================================
 */

/*
 * DiskCopy 4.2 header, from FTN $e0/0005.
 *
 * All values are BIG-endian.
 */
const int kDC42NameLen = 64;
const int kDC42ChecksumOffset = 72;		// where the checksum lives
const int kDC42DataOffset = 84;			// header is always this long
const int kDC42PrivateMagic = 0x100;
const int kDC42FakeTagLen = 19200;		// add a "fake" tag to match Mac

typedef struct DiskImgLib::DC42Header {
	char			diskName[kDC42NameLen+1];	// from pascal string
	long			dataSize;			// in bytes
	long			tagSize;
	unsigned long	dataChecksum;
	unsigned long	tagChecksum;
	unsigned char	diskFormat;			// should be 1 for 800K
	unsigned char	formatByte;			// should be $24, sometimes $22
	unsigned short	privateWord;		// must be 0x0100
	// userData begins at +84
	// tagData follows user data
} DC42Header;

/*
 * Dump the contents of a DC42Header.
 */
/*static*/ void
WrapperDiskCopy42::DumpHeader(const DC42Header* pHeader)
{
	WMSG0("--- header contents:\n");
	WMSG1("\tdiskName      = '%s'\n", pHeader->diskName);
	WMSG2("\tdataSize      = %ld (%ldK)\n", pHeader->dataSize,
		pHeader->dataSize / 1024);
	WMSG1("\ttagSize       = %ld\n", pHeader->tagSize);
	WMSG1("\tdataChecksum  = 0x%08lx\n", pHeader->dataChecksum);
	WMSG1("\ttagChecksum   = 0x%08lx\n", pHeader->tagChecksum);
	WMSG1("\tdiskFormat    = %d\n", pHeader->diskFormat);
	WMSG1("\tformatByte    = 0x%02x\n", pHeader->formatByte);
	WMSG1("\tprivateWord   = 0x%04x\n", pHeader->privateWord);
}

/*
 * Init a DC42 header for an 800K ProDOS disk.
 */
void
WrapperDiskCopy42::InitHeader(DC42Header* pHeader)
{
    memset(pHeader, 0, sizeof(*pHeader));
	if (fStorageName == NULL || strlen(fStorageName) == 0)
		strcpy(pHeader->diskName, "-not a Macintosh disk");
	else
		strcpy(pHeader->diskName, fStorageName);
    pHeader->dataSize = 819200;
    pHeader->tagSize = kDC42FakeTagLen;		// emulate Mac behavior
	pHeader->dataChecksum = 0xffffffff;		// fixed during Flush
    pHeader->tagChecksum = 0x00000000;		// 19200 zeroes
    pHeader->diskFormat = 1;
    pHeader->formatByte = 0x24;
    pHeader->privateWord = kDC42PrivateMagic;
}

/*
 * Read the header from a DC42 file and verify it.
 *
 * Returns 0 on success, -1 on error or invalid header.
 */
/*static*/ int
WrapperDiskCopy42::ReadHeader(GenericFD* pGFD, DC42Header* pHeader)
{
	unsigned char hdrBuf[kDC42DataOffset];

    if (pGFD->Read(hdrBuf, kDC42DataOffset) != kDIErrNone)
		return -1;

	// test the Pascal length byte
	if (hdrBuf[0] >= kDC42NameLen)
		return -1;

	memcpy(pHeader->diskName, &hdrBuf[1], hdrBuf[0]);
	pHeader->diskName[hdrBuf[0]] = '\0';

	pHeader->dataSize = GetLongBE(&hdrBuf[64]);
	pHeader->tagSize = GetLongBE(&hdrBuf[68]);
	pHeader->dataChecksum = GetLongBE(&hdrBuf[72]);
	pHeader->tagChecksum = GetLongBE(&hdrBuf[76]);
	pHeader->diskFormat = hdrBuf[80];
	pHeader->formatByte = hdrBuf[81];
	pHeader->privateWord = GetShortBE(&hdrBuf[82]);

	if (pHeader->dataSize != 800 * 1024 ||
		pHeader->diskFormat != 1 ||
		(pHeader->formatByte != 0x22 && pHeader->formatByte != 0x24) ||
		pHeader->privateWord != kDC42PrivateMagic)
	{
		return -1;
	}

	return 0;
}

/*
 * Write the header for a DC42 file.
 */
DIError
WrapperDiskCopy42::WriteHeader(GenericFD* pGFD, const DC42Header* pHeader)
{
	unsigned char hdrBuf[kDC42DataOffset];

    pGFD->Rewind();

    memset(hdrBuf, 0, sizeof(hdrBuf));
	/*
	 * Disks created on a Mac include the null byte in the count; not sure
	 * if this applies to volume labels or just the "not a Macintosh disk"
	 * magic string.  To be safe, we only increment it if it starts with '-'.
	 * (Need access to a Macintosh to test this.)
	 */
    hdrBuf[0] = strlen(pHeader->diskName);
	if (pHeader->diskName[0] == '-' && hdrBuf[0] < (kDC42NameLen-1))
		hdrBuf[0]++;
	memcpy(&hdrBuf[1], pHeader->diskName, hdrBuf[0]);

	PutLongBE(&hdrBuf[64], pHeader->dataSize);
	PutLongBE(&hdrBuf[68], pHeader->tagSize);
	PutLongBE(&hdrBuf[72], pHeader->dataChecksum);
	PutLongBE(&hdrBuf[76], pHeader->tagChecksum);
	hdrBuf[80] = pHeader->diskFormat;
	hdrBuf[81] = pHeader->formatByte;
	PutShortBE(&hdrBuf[82], pHeader->privateWord);

    return pGFD->Write(hdrBuf, kDC42DataOffset);
}

/*
 * Check to see if this is a DiskCopy 4.2 image.
 *
 * The format doesn't really have a magic number, but if we're stringent
 * about our interpretation of some of the header fields (e.g. we only
 * recognize 800K disks) we should be okay.
 */
/*static*/ DIError
WrapperDiskCopy42::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
	DC42Header header;

	WMSG0("Testing for DiskCopy\n");

	if (wrappedLength < 800 * 1024 + kDC42DataOffset)
		return kDIErrGeneric;

	pGFD->Rewind();
	if (ReadHeader(pGFD, &header) != 0)
		return kDIErrGeneric;

	DumpHeader(&header);

	return kDIErrNone;
}

/*
 * Compute the funky DiskCopy checksum.
 *
 * Position "pGFD" at the start of data.
 */
/*static*/ DIError
WrapperDiskCopy42::ComputeChecksum(GenericFD* pGFD,
    unsigned long* pChecksum)
{
	DIError dierr = kDIErrNone;
	unsigned char buf[512];
	long dataRem = 800 * 1024 /*pHeader->dataSize*/;
	unsigned long checksum;

	assert(dataRem % sizeof(buf) == 0);
	assert((sizeof(buf) & 0x01) == 0);	// we take it two bytes at a time

	checksum = 0;
	while (dataRem) {
		int i;

		dierr = pGFD->Read(buf, sizeof(buf));
		if (dierr != kDIErrNone) {
            WMSG2(" DC42 read failed, dataRem=%ld (err=%d)\n", dataRem, dierr);
			return dierr;
        }

		for (i = 0; i < (int) sizeof(buf); i += 2) {
			unsigned short val = GetShortBE(buf+i);

			checksum += val;
			if (checksum & 0x01)
				checksum = checksum >> 1 | 0x80000000;
			else
				checksum = checksum >> 1;
		}

		dataRem -= sizeof(buf);
	}

    *pChecksum = checksum;

	return dierr;
}

/*
 * Prepare a DiskCopy image for use.
 */
DIError
WrapperDiskCopy42::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
    DIError dierr;
	DC42Header header;

	WMSG0("Prepping for DiskCopy 4.2\n");
	pGFD->Rewind();
	if (ReadHeader(pGFD, &header) != 0)
		return kDIErrGeneric;

	/*
	 * Verify checksum.  File should already be seeked to appropriate place.
	 */
    unsigned long checksum;
	dierr = ComputeChecksum(pGFD, &checksum);
    if (dierr != kDIErrNone)
		return dierr;

	if (checksum != header.dataChecksum) {
		WMSG2(" DC42 checksum mismatch (got 0x%08lx, expected 0x%08lx)\n",
			checksum, header.dataChecksum);
		fBadChecksum = true;
		//return kDIErrBadChecksum;
	} else {
		WMSG0(" DC42 checksum matches!\n");
	}


    /* looks good! */
	*pLength = header.dataSize;
	*pPhysical = DiskImg::kPhysicalFormatSectors;
	*pOrder = DiskImg::kSectorOrderProDOS;

    *ppNewGFD = new GFDGFD;
    return ((GFDGFD*)*ppNewGFD)->Open(pGFD, kDC42DataOffset, readOnly);

}

/*
 * Initialize fields for a new file.
 */
DIError
WrapperDiskCopy42::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    DIError dierr;
    DC42Header header;

    assert(length == 800 * 1024);
    assert(physical == DiskImg::kPhysicalFormatSectors);
    //assert(order == DiskImg::kSectorOrderProDOS);

    InitHeader(&header);        // set all but checksum

    dierr = WriteHeader(pWrapperGFD, &header);
    if (dierr != kDIErrNone) {
        WMSG1("ERROR: 2MG header write failed (err=%d)\n", dierr);
        return dierr;
    }

    *pWrappedLength = length + kDC42DataOffset;
    *pDataFD = new GFDGFD;
    return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, kDC42DataOffset, false);
}

/*
 * We only use GFDGFD, so there's no data to write.  However, we do need
 * to update the checksum, and append our "fake" tag section.
 */
DIError
WrapperDiskCopy42::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    DIError dierr;
    unsigned long checksum;

    /* compute the data checksum */
    dierr = pWrapperGFD->Seek(kDC42DataOffset, kSeekSet);
    if (dierr != kDIErrNone)
        goto bail;

    dierr = ComputeChecksum(pWrapperGFD, &checksum);
    if (dierr != kDIErrNone) {
        WMSG1(" DC42 failed while computing checksum (err=%d)\n", dierr);
        goto bail;
    }

    /* write it into the wrapper */
    dierr = pWrapperGFD->Seek(kDC42ChecksumOffset, kSeekSet);
    if (dierr != kDIErrNone)
        goto bail;

    dierr = WriteLongBE(pWrapperGFD, checksum);
    if (dierr != kDIErrNone)
        goto bail;

	/* add the tag bytes */
	dierr = pWrapperGFD->Seek(kDC42DataOffset + 800*1024, kSeekSet);
	char* tmpBuf;
	tmpBuf = new char[kDC42FakeTagLen];
	if (tmpBuf == nil)
		return kDIErrMalloc;
	memset(tmpBuf, 0, kDC42FakeTagLen);
	dierr = pWrapperGFD->Write(tmpBuf, kDC42FakeTagLen, nil);
	delete[] tmpBuf;
	if (dierr != kDIErrNone)
		goto bail;

bail:
    return dierr;
}


/*
 * ===========================================================================
 *		Sim2eHDV (Sim2e virtual hard-drive images)
 * ===========================================================================
 */

/*
//	mkhdv.c
//
//	Create a Hard Disk Volume File (.HDV) for simIIe
static	int mkhdv(FILE *op, uint blocks)
{
	byte	sector[512];
	byte	data[15];
	uint	i;

	memset(data, 0, sizeof(data));
	memcpy(data, "SIMSYSTEM_HDV", 13);
	data[13] = (blocks & 0xff);
	data[14] = (blocks & 0xff00) >> 8;
	fwrite(data, 1, sizeof(data), op);

	memset(sector, 0, sizeof(sector));
	for (i = 0; i < blocks; i++)
		fwrite(sector, 1, sizeof(sector), op);
	return 0;
}
*/

const int kSim2eHeaderLen = 15;
static const char* kSim2eID = "SIMSYSTEM_HDV";

/*
 * Test for a virtual hard-drive image.  This is either a "raw" unadorned
 * image, or one with a 15-byte "SimIIe" header on it.
 */
DIError
WrapperSim2eHDV::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
	char buf[kSim2eHeaderLen];

    WMSG0("Testing for Sim2e HDV\n");

	if (wrappedLength < 512 ||
		((wrappedLength - kSim2eHeaderLen) % 4096) != 0)
	{
		return kDIErrGeneric;
	}

    pGFD->Rewind();

    if (pGFD->Read(buf, sizeof(buf)) != kDIErrNone)
		return kDIErrGeneric;

    if (strncmp(buf, kSim2eID, strlen(kSim2eID)) == 0)
        return kDIErrNone;
    else
        return kDIErrGeneric;
}

/*
 * These are always ProDOS volumes.
 */
DIError
WrapperSim2eHDV::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
	*pLength = wrappedLength - kSim2eHeaderLen;
	*pPhysical = DiskImg::kPhysicalFormatSectors;
	*pOrder = DiskImg::kSectorOrderProDOS;

    *ppNewGFD = new GFDGFD;
    return ((GFDGFD*)*ppNewGFD)->Open(pGFD, kSim2eHeaderLen, readOnly);
}

/*
 * Initialize fields for a new file.
 */
DIError
WrapperSim2eHDV::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    unsigned char header[kSim2eHeaderLen];
    long blocks = (long) (length / 512);

    assert(physical == DiskImg::kPhysicalFormatSectors);
    assert(order == DiskImg::kSectorOrderProDOS);

    if (blocks < 4 || blocks > 65536) {
        WMSG1("  Sim2e invalid blocks %ld\n", blocks);
        return kDIErrInvalidArg;
    }
    if (blocks == 65536)        // 32MB volumes are actually 31.9
        blocks = 65535;

    memcpy(header, kSim2eID, strlen(kSim2eID));
    header[13] = (unsigned char) blocks;
    header[14] = (unsigned char) ((blocks & 0xff00) >> 8);
    DIError dierr = pWrapperGFD->Write(header, kSim2eHeaderLen);
    if (dierr != kDIErrNone) {
        WMSG1(" Sim2eHDV header write failed (err=%d)\n", dierr);
        return dierr;
    }

    *pWrappedLength = length + kSim2eHeaderLen;

    *pDataFD = new GFDGFD;
    return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, kSim2eHeaderLen, false);
}

/*
 * We only use GFDGFD, so there's nothing to do here.
 */
DIError
WrapperSim2eHDV::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    return kDIErrNone;
}


/*
 * ===========================================================================
 *		TrackStar .app images
 * ===========================================================================
 */

/*
 * File format:
 *	$0000  track 0 data
 *	$1a00  track 1 data
 *	$3400  track 2 data
 *	 ...
 *	$3f600 track 39 data
 *
 * Each track consists of:
 *	$0000  Text description of disk contents (same on every track), in low
 *	       ASCII, padded out with spaces ($20)
 *	$002e  Start of zeroed-out header field
 *	$0080  $00 (indicates end of data when reading from end??)
 *	$0081  Raw nibble data (hi bit set), written backwards
 *	$19fe  Start offset of track data
 *
 * Take the start offset, add 128, and walk backward until you find a
 * value with the high bit clear.  If the start offset is zero, start
 * scanning from $19fd backward.  (This approach courtesty Gerald Ryckman.)
 *
 * My take: the "offset" actually indicates the length of data, and the
 * $00 is there to simplify somebody's algorithm.  If the offset is zero
 * it means the track couldn't be analyzed successfully, so a raw dump has
 * been provided.  Tracks 35-39 on most Apple II disks have zero length,
 * but occasionally one analyzes "successfully" with some horribly truncated
 * length.
 *
 * I'm going to assert that byte $81 be zero and that nothing else has the
 * high bit clear until you hit the end of valid data.
 *
 * Because the nibbles are stored in reverse order, it's easiest to unpack
 * the tracks to local buffers, then re-pack them when saving the file.
 */

/*
 * Test to see if this is a TrackStar 5.25" disk image.
 *
 * While the image format supports variable-length nibble tracks, it uses
 * fixed-length fields to store them.  Each track is stored in 6656 bytes,
 * but has a 129-byte header and a 2-byte footer (max of 6525).
 *
 * Images may be 40-track (5.25") or 80-track (5.25" disk with half-track
 * stepping).  The latter is useful in some circumstances for handling
 * copy-protected disks.  We don't have a half-track interface, so we just
 * ignore the odd-numbered tracks.
 *
 * There is currently no way for the API to set the number of tracks.
 */
/*static*/ DIError
WrapperTrackStar::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
    DIError dierr = kDIErrNone;
	WMSG0("Testing for TrackStar\n");
	int numTracks;

	/* check the length */
	if (wrappedLength == 6656*40)
		numTracks = 40;
	else if (wrappedLength == 6656*80)
		numTracks = 80;
	else
		return kDIErrGeneric;

	WMSG1("  Checking for %d-track image\n", numTracks);

	/* verify each track */
	unsigned char trackBuf[kFileTrackStorageLen];
    pGFD->Rewind();
	for (int trk = 0; trk < numTracks; trk++) {
		dierr = pGFD->Read(trackBuf, sizeof(trackBuf));
		if (dierr != kDIErrNone)
			goto bail;
		dierr = VerifyTrack(trk, trackBuf);
		if (dierr != kDIErrNone)
			goto bail;
	}
	WMSG0("  TrackStar tracks verified\n");

bail:
    return dierr;
}

/*
 * Check the format.
 */
/*static*/ DIError
WrapperTrackStar::VerifyTrack(int track, const unsigned char* trackBuf)
{
	unsigned int dataLen;

	if (trackBuf[0x80] != 0) {
		WMSG1("   TrackStar track=%d found nonzero at 129\n", track);
		return kDIErrGeneric;
	}

	dataLen = GetShortLE(trackBuf + 0x19fe);
	if (dataLen > kMaxTrackLen) {
		WMSG3("   TrackStar track=%d len=%d exceeds max (%d)\n",
			track, dataLen, kMaxTrackLen);
		return kDIErrGeneric;
	}
	if (dataLen == 0)
		dataLen = kMaxTrackLen;

	unsigned int i;
	for (i = 0; i < dataLen; i++) {
		if ((trackBuf[0x81 + i] & 0x80) == 0) {
			WMSG3("   TrackStar track=%d found invalid data 0x%02x at %d\n",
				track, trackBuf[0x81+i], i);
			return kDIErrGeneric;
		}
	}

	if (track == 0) {
		WMSG1("   TrackStar msg='%s'\n", trackBuf);
	}

	return kDIErrNone;
}

/*
 * Fill in some details.
 */
DIError
WrapperTrackStar::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
	WMSG0("Prepping for TrackStar\n");
	DIError dierr = kDIErrNone;

	if (wrappedLength == kFileTrackStorageLen * 40)
		fImageTracks = 40;
	else if (wrappedLength == kFileTrackStorageLen * 80)
		fImageTracks = 80;
	else
		return kDIErrInternal;

    dierr = Unpack(pGFD, ppNewGFD);
    if (dierr != kDIErrNone)
        return dierr;

	*pLength = kTrackStarNumTracks * kTrackAllocSize;
	*pPhysical = DiskImg::kPhysicalFormatNib525_Var;
	*pOrder = DiskImg::kSectorOrderPhysical;

    return dierr;
}

/*
 * Unpack reverse-order nibbles from "pGFD" to a new memory buffer
 * created in "*ppNewGFD".
 */
DIError
WrapperTrackStar::Unpack(GenericFD* pGFD, GenericFD** ppNewGFD)
{
    DIError dierr;
	GFDBuffer* pNewGFD = nil;
    unsigned char* buf = nil;

	pGFD->Rewind();

    buf = new unsigned char[kTrackStarNumTracks * kTrackAllocSize];
    if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

	pNewGFD = new GFDBuffer;
    if (pNewGFD == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }
	dierr = pNewGFD->Open(buf, kTrackStarNumTracks * kTrackAllocSize,
		true, false, false);
	if (dierr != kDIErrNone)
		goto bail;
	buf = nil;		// now owned by pNewGFD;

    dierr = UnpackDisk(pGFD, pNewGFD);
    if (dierr != kDIErrNone)
        goto bail;

    *ppNewGFD = pNewGFD;
    pNewGFD = nil;  // now owned by caller

bail:
    delete[] buf;
    delete pNewGFD;
    return dierr;
}

/*
 * Unpack a TrackStar image.  This is mainly just copying bytes around.  The
 * nibble code is perfectly happy with odd-sized tracks.  However, we want
 * to be able to find a particular track without having to do a lookup.  So,
 * we just block out 40 sets of 6656-byte tracks.
 *
 * The resultant image will always have 40 tracks.  On an 80-track image
 * we skip the odd ones.
 *
 * The bytes are stored in reverse order, so we need to unpack them to a
 * separate buffer.
 *
 * This fills out "fNibbleTrackInfo".
 */
DIError
WrapperTrackStar::UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD)
{
	DIError dierr = kDIErrNone;
	unsigned char inBuf[kFileTrackStorageLen];
	unsigned char outBuf[kTrackAllocSize];
	int i, trk;

	assert(kTrackStarNumTracks <= kMaxNibbleTracks525);

    pGFD->Rewind();
	pNewGFD->Rewind();

	/* we don't currently support half-tracks */
	fNibbleTrackInfo.numTracks = kTrackStarNumTracks;
	for (trk = 0; trk < kTrackStarNumTracks; trk++) {
		unsigned int dataLen;

		fNibbleTrackInfo.offset[trk] = trk * kTrackAllocSize;

		/* these were verified earlier, so assume data is okay */
		dierr = pGFD->Read(inBuf, sizeof(inBuf));
		if (dierr != kDIErrNone)
			goto bail;

		dataLen = GetShortLE(inBuf + 0x19fe);
		if (dataLen == 0)
			dataLen = kMaxTrackLen;
		assert(dataLen <= kMaxTrackLen);
		assert(dataLen <= sizeof(outBuf));

		fNibbleTrackInfo.length[trk] = dataLen;

		memset(outBuf, 0x11, sizeof(outBuf));
		for (i = 0; i < (int) dataLen; i++)
			outBuf[i] = inBuf[128+dataLen-i];

		pNewGFD->Write(outBuf, sizeof(outBuf));

		if (fImageTracks == 2*kTrackStarNumTracks) {
			/* skip the odd-numbered tracks */
			dierr = pGFD->Read(inBuf, sizeof(inBuf));
			if (dierr != kDIErrNone)
				goto bail;
		}
	}

bail:
	return dierr;
}


/*
 * Initialize stuff for a new file.  There's no file header or other
 * goodies, so we leave "pWrapperGFD" and "pWrappedLength" alone.
 */
DIError
WrapperTrackStar::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    assert(length == kTrackLenTrackStar525 * kTrackCount525 ||
		   length == kTrackLenTrackStar525 * kTrackStarNumTracks);
    assert(physical == DiskImg::kPhysicalFormatNib525_Var);
    assert(order == DiskImg::kSectorOrderPhysical);

    DIError dierr;
	unsigned char* buf = nil;
	int numTracks = (int) (length / kTrackLenTrackStar525);
	int i;

	/*
	 * Set up the track offset and length table.  We use the maximum
	 * data length (kTrackLenTrackStar525) for each.  The nibble write
	 * routine will alter the length field as appropriate.
	 */
	fNibbleTrackInfo.numTracks = numTracks;
	assert(fNibbleTrackInfo.numTracks <= kMaxNibbleTracks525);
	for (i = 0; i < numTracks; i++) {
		fNibbleTrackInfo.offset[i] = kTrackLenTrackStar525 * i;
		fNibbleTrackInfo.length[i] = kTrackLenTrackStar525;
	}

    /*
     * Create a blank chunk of memory for the image.
     */
    buf = new unsigned char[(int) length];
	if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

    GFDBuffer* pNewGFD;
	pNewGFD = new GFDBuffer;
	dierr = pNewGFD->Open(buf, length, true, false, false);
	if (dierr != kDIErrNone) {
        delete pNewGFD;
		goto bail;
    }
    *pDataFD = pNewGFD;
	buf = nil;		    // now owned by pNewGFD;

    // can't set *pWrappedLength yet

bail:
    delete[] buf;
    return dierr;
}

/*
 * Write the stored data into TrackStar format.
 *
 * The source data is in "pDataGFD" in a layout described by fNibbleTrackInfo.
 * We need to create the new file in "pWrapperGFD".
 */
DIError
WrapperTrackStar::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    DIError dierr = kDIErrNone;

    assert(dataLen == kTrackLenTrackStar525 * kTrackCount525 ||
		   dataLen == kTrackLenTrackStar525 * kTrackStarNumTracks);
	assert(kTrackLenTrackStar525 <= kMaxTrackLen);

    pDataGFD->Rewind();

	unsigned char writeBuf[kFileTrackStorageLen];
	unsigned char dataBuf[kTrackLenTrackStar525];
	int track, trackLen;

	for (track = 0; track < kTrackStarNumTracks; track++) {
		if (track < fNibbleTrackInfo.numTracks) {
			dierr = pDataGFD->Read(dataBuf, kTrackLenTrackStar525);
			if (dierr != kDIErrNone)
				goto bail;
			trackLen = fNibbleTrackInfo.length[track];
			assert(fNibbleTrackInfo.offset[track] == kTrackLenTrackStar525 * track);
		} else {
			WMSG1("   TrackStar faking track %d\n", track);
			memset(dataBuf, 0xff, sizeof(dataBuf));
			trackLen = kMaxTrackLen;
		}

		memset(writeBuf, 0x80, sizeof(writeBuf));	// not strictly necessary
		memset(writeBuf, 0x20, kCommentFieldLen);
		memset(writeBuf+kCommentFieldLen, 0x00, 0x81-kCommentFieldLen);

		const char* comment;
		if (fStorageName != NULL && *fStorageName != '\0')
			comment = fStorageName;
		else
			comment = "(created by CiderPress)";
		if (strlen(comment) > kCommentFieldLen)
			memcpy(writeBuf, comment, kCommentFieldLen);
		else
			memcpy(writeBuf, comment, strlen(comment));

		int i;
		for (i = 0; i < trackLen; i++) {
			// If we write a value here with the high bit clear, we will
			// reject the file when we try to open it.  So, we force the
			// high bit on here, on the assumption that the nibble data
			// we've been handled is otherwise good.
			//writeBuf[0x81+i] = dataBuf[trackLen - i -1];
			writeBuf[0x81+i] = dataBuf[trackLen - i -1] | 0x80;
		}

		if (trackLen == kMaxTrackLen)
			PutShortLE(writeBuf + 0x19fe, 0);
		else
			PutShortLE(writeBuf + 0x19fe, (unsigned short) trackLen);

		dierr = pWrapperGFD->Write(writeBuf, sizeof(writeBuf));
		if (dierr != kDIErrNone)
			goto bail;
	}

    *pWrappedLen = pWrapperGFD->Tell();
	assert(*pWrappedLen == kFileTrackStorageLen * kTrackStarNumTracks);

bail:
    return dierr;
}

void
WrapperTrackStar::SetNibbleTrackLength(int track, int length)
{
	assert(track >= 0);
	assert(length > 0 && length <= kMaxTrackLen);
	assert(track < fNibbleTrackInfo.numTracks);

	WMSG2("  TrackStar: set length of track %d to %d\n", track, length);
	fNibbleTrackInfo.length[track] = length;
}


/*
 * ===========================================================================
 *		FDI (Formatted Disk Image) format
 * ===========================================================================
 */

/*
 * The format is described in detail in documents on the "disk2fdi" web site.
 *
 * FDI is currently unique in that it can (and often does) store nibble
 * images of 3.5" disks.  Rather than add an understanding of nibblized
 * 3.5" disks to DiskImg, I've chosen to present it as a simple 800K
 * ProDOS disk image.  The only flaw in the scheme is that we have to
 * keep track of the bad blocks in a parallel data structure.
 */

/*static*/ const char* WrapperFDI::kFDIMagic = "Formatted Disk Image file\r\n";

/*
 * Test to see if this is an FDI disk image.
 */
/*static*/ DIError
WrapperFDI::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
    DIError dierr = kDIErrNone;
	unsigned char headerBuf[kMinHeaderLen];
	FDIHeader hdr;

	WMSG0("Testing for FDI\n");

    pGFD->Rewind();
	dierr = pGFD->Read(headerBuf, sizeof(headerBuf));
	if (dierr != kDIErrNone)
		goto bail;

	UnpackHeader(headerBuf, &hdr);
	if (strcmp(hdr.signature, kFDIMagic) != 0) {
		WMSG0("FDI: FDI signature not found\n");
		return kDIErrGeneric;
	}
	if (hdr.version < kMinVersion) {
		WMSG1("FDI: bad version 0x%.04x\n", hdr.version);
		return kDIErrGeneric;
	}

bail:
    return dierr;
}

/*
 * Unpack a 512-byte buffer with the FDI header into its components.
 */
/*static*/ void
WrapperFDI::UnpackHeader(const unsigned char* headerBuf, FDIHeader* pHdr)
{
	memcpy(pHdr->signature, &headerBuf[0], kSignatureLen);
	pHdr->signature[kSignatureLen] = '\0';
	memcpy(pHdr->creator, &headerBuf[27], kCreatorLen);
	pHdr->creator[kCreatorLen] = '\0';
	memcpy(pHdr->comment, &headerBuf[59], kCommentLen);
	pHdr->comment[kCommentLen] = '\0';

	pHdr->version = GetShortBE(&headerBuf[140]);
	pHdr->lastTrack = GetShortBE(&headerBuf[142]);
	pHdr->lastHead = headerBuf[144];
	pHdr->type = headerBuf[145];
	pHdr->rotSpeed = headerBuf[146];
	pHdr->flags = headerBuf[147];
	pHdr->tpi = headerBuf[148];
	pHdr->headWidth = headerBuf[149];
	pHdr->reserved = GetShortBE(&headerBuf[150]);
}

/*
 * Dump the contents of an FDI header.
 */
/*static*/ void
WrapperFDI::DumpHeader(const FDIHeader* pHdr)
{
	static const char* kTypes[] = {
		"8\"", "5.25\"", "3.5\"", "3\""
	};
	static const char* kTPI[] = {
		"48", "67", "96", "100", "135", "192"
	};

	WMSG0(" FDI header contents:\n");
	WMSG1("  signature: '%s'\n", pHdr->signature);
	WMSG1("  creator  : '%s'\n", pHdr->creator);
	WMSG1("  comment  : '%s'\n", pHdr->comment);
	WMSG2("  version  : %d.%d\n", pHdr->version >> 8, pHdr->version & 0xff);
	WMSG1("  lastTrack: %d\n", pHdr->lastTrack);
	WMSG1("  lastHead : %d\n", pHdr->lastHead);
	WMSG2("  type     : %d (%s)\n", pHdr->type,
		(/*pHdr->type >= 0 &&*/ pHdr->type < NELEM(kTypes)) ?
		kTypes[pHdr->type] : "???");
	WMSG1("  rotSpeed : %drpm\n", pHdr->rotSpeed + 128);
	WMSG1("  flags    : 0x%02x\n", pHdr->flags);
	WMSG2("  tpi      : %d (%s)\n", pHdr->tpi,
		(/*pHdr->tpi >= 0 &&*/ pHdr->tpi < NELEM(kTPI)) ?
		kTPI[pHdr->tpi] : "???");
	WMSG2("  headWidth: %d (%s)\n", pHdr->headWidth,
		(/*pHdr->headWidth >= 0 &&*/ pHdr->headWidth < NELEM(kTPI)) ?
		kTPI[pHdr->headWidth] : "???");
	WMSG1("  reserved : %d\n", pHdr->reserved);
}

/*
 * Unpack the disk to heap storage.
 */
DIError
WrapperFDI::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
	WMSG0("Prepping for FDI\n");
	DIError dierr = kDIErrNone;
	FDIHeader hdr;

    pGFD->Rewind();
	dierr = pGFD->Read(fHeaderBuf, sizeof(fHeaderBuf));
	if (dierr != kDIErrNone)
		goto bail;

	UnpackHeader(fHeaderBuf, &hdr);
	if (strcmp(hdr.signature, kFDIMagic) != 0)
		return kDIErrGeneric;
	DumpHeader(&hdr);

	/*
	 * There are two formats that we're interested in, 3.5" and 5.25".  They
	 * are handled differently within CiderPress, so we split here.
	 *
	 * Sometimes disk2fdi finds extra tracks.  No Apple II hardware ever
	 * went past 40 on 5.25" disks, but we'll humor the software and allow
	 * images with up to 50.  Ditto for 3.5" disks, which should always
	 * have 80 double-sided tracks.
	 */
	if (hdr.type == kDiskType525) {
		WMSG0("FDI: decoding 5.25\" disk\n");
		if (hdr.lastHead != 0 || hdr.lastTrack >= kMaxNibbleTracks525 + 10) {
			WMSG2("FDI: bad params head=%d ltrack=%d\n",
				hdr.lastHead, hdr.lastTrack);
			dierr = kDIErrUnsupportedImageFeature;
			goto bail;
		}
		if (hdr.lastTrack >= kMaxNibbleTracks525) {
			WMSG2("FDI: reducing hdr.lastTrack from %d to %d\n",
				hdr.lastTrack, kMaxNibbleTracks525-1);
			hdr.lastTrack = kMaxNibbleTracks525-1;
		}

		/*
		 * Unpack to a series of variable-length nibble tracks.  The data
		 * goes into ppNewGFD, and a table of track info goes into
		 * fNibbleTrackInfo.
		 */
		dierr = Unpack525(pGFD, ppNewGFD, hdr.lastTrack+1, hdr.lastHead+1);
		if (dierr != kDIErrNone)
			return dierr;

		*pLength = kMaxNibbleTracks525 * kTrackAllocSize;
		*pPhysical = DiskImg::kPhysicalFormatNib525_Var;
		*pOrder = DiskImg::kSectorOrderPhysical;
	} else if (hdr.type == kDiskType35) {
		WMSG0("FDI: decoding 3.5\" disk\n");
		if (hdr.lastHead != 1 || hdr.lastTrack >= kMaxNibbleTracks35 + 10) {
			WMSG2("FDI: bad params head=%d ltrack=%d\n",
				hdr.lastHead, hdr.lastTrack);
			dierr = kDIErrUnsupportedImageFeature;
			goto bail;
		}
		if (hdr.lastTrack >= kMaxNibbleTracks35) {
			WMSG2("FDI: reducing hdr.lastTrack from %d to %d\n",
				hdr.lastTrack, kMaxNibbleTracks35-1);
			hdr.lastTrack = kMaxNibbleTracks35-1;
		}

		/*
		 * Unpack to 800K of 512-byte ProDOS-order blocks, with a
		 * "bad block" map.
		 */
		dierr = Unpack35(pGFD, ppNewGFD, hdr.lastTrack+1, hdr.lastHead+1,
					ppBadBlockMap);
		if (dierr != kDIErrNone)
			return dierr;

		*pLength = 800 * 1024;
		*pPhysical = DiskImg::kPhysicalFormatSectors;
		*pOrder = DiskImg::kSectorOrderProDOS;
	} else {
		WMSG0("FDI: unsupported disk type\n");
		dierr = kDIErrUnsupportedImageFeature;
		goto bail;
	}

bail:
    return dierr;
}

/*
 * Unpack pulse timing values to nibbles.
 */
DIError
WrapperFDI::Unpack525(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls,
	int numHeads)
{
    DIError dierr = kDIErrNone;
	GFDBuffer* pNewGFD = nil;
    unsigned char* buf = nil;
	int numTracks;

	numTracks = numCyls * numHeads;
	if (numTracks < kMaxNibbleTracks525)
		numTracks = kMaxNibbleTracks525;

	pGFD->Rewind();

    buf = new unsigned char[numTracks * kTrackAllocSize];
    if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

	pNewGFD = new GFDBuffer;
    if (pNewGFD == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }
	dierr = pNewGFD->Open(buf, numTracks * kTrackAllocSize,
		true, false, false);
	if (dierr != kDIErrNone)
		goto bail;
	buf = nil;		// now owned by pNewGFD;

    dierr = UnpackDisk525(pGFD, pNewGFD, numCyls, numHeads);
    if (dierr != kDIErrNone)
        goto bail;

    *ppNewGFD = pNewGFD;
    pNewGFD = nil;  // now owned by caller

bail:
    delete[] buf;
    delete pNewGFD;
    return dierr;
}

/*
 * Unpack pulse timing values to fully-decoded blocks.
 */
DIError
WrapperFDI::Unpack35(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls,
	int numHeads, LinearBitmap** ppBadBlockMap)
{
    DIError dierr = kDIErrNone;
	GFDBuffer* pNewGFD = nil;
    unsigned char* buf = nil;

	pGFD->Rewind();

    buf = new unsigned char[800 * 1024];
    if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

	pNewGFD = new GFDBuffer;
    if (pNewGFD == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }
	dierr = pNewGFD->Open(buf, 800 * 1024, true, false, false);
	if (dierr != kDIErrNone)
		goto bail;
	buf = nil;		// now owned by pNewGFD;

	*ppBadBlockMap = new LinearBitmap(1600);
	if (*ppBadBlockMap == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

    dierr = UnpackDisk35(pGFD, pNewGFD, numCyls, numHeads, *ppBadBlockMap);
    if (dierr != kDIErrNone)
        goto bail;

    *ppNewGFD = pNewGFD;
    pNewGFD = nil;  // now owned by caller

bail:
    delete[] buf;
    delete pNewGFD;
    return dierr;
}

/*
 * Initialize stuff for a new file.  There's no file header or other
 * goodies, so we leave "pWrapperGFD" and "pWrappedLength" alone.
 */
DIError
WrapperFDI::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    DIError dierr = kDIErrGeneric;		// not yet
#if 0
	unsigned char* buf = nil;
	int numTracks = (int) (length / kTrackLenTrackStar525);
	int i;

    assert(length == kTrackLenTrackStar525 * kTrackCount525 ||
		   length == kTrackLenTrackStar525 * kTrackStarNumTracks);
    assert(physical == DiskImg::kPhysicalFormatNib525_Var);
    assert(order == DiskImg::kSectorOrderPhysical);

	/*
	 * Set up the track offset and length table.  We use the maximum
	 * data length (kTrackLenTrackStar525) for each.  The nibble write
	 * routine will alter the length field as appropriate.
	 */
	fNibbleTrackInfo.numTracks = numTracks;
	assert(fNibbleTrackInfo.numTracks <= kMaxNibbleTracks);
	for (i = 0; i < numTracks; i++) {
		fNibbleTrackInfo.offset[i] = kTrackLenTrackStar525 * i;
		fNibbleTrackInfo.length[i] = kTrackLenTrackStar525;
	}

    /*
     * Create a blank chunk of memory for the image.
     */
    buf = new unsigned char[(int) length];
	if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

    GFDBuffer* pNewGFD;
	pNewGFD = new GFDBuffer;
	dierr = pNewGFD->Open(buf, length, true, false, false);
	if (dierr != kDIErrNone) {
        delete pNewGFD;
		goto bail;
    }
    *pDataFD = pNewGFD;
	buf = nil;		    // now owned by pNewGFD;

    // can't set *pWrappedLength yet

bail:
    delete[] buf;
#endif
    return dierr;
}

/*
 * Write the stored data into FDI format.
 *
 * The source data is in "pDataGFD" in a layout described by fNibbleTrackInfo.
 * We need to create the new file in "pWrapperGFD".
 */
DIError
WrapperFDI::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    DIError dierr = kDIErrGeneric;		// not yet

#if 0
    assert(dataLen == kTrackLenTrackStar525 * kTrackCount525 ||
		   dataLen == kTrackLenTrackStar525 * kTrackStarNumTracks);
	assert(kTrackLenTrackStar525 <= kMaxTrackLen);

    pDataGFD->Rewind();

	unsigned char writeBuf[kFileTrackStorageLen];
	unsigned char dataBuf[kTrackLenTrackStar525];
	int track, trackLen;

	for (track = 0; track < kTrackStarNumTracks; track++) {
		if (track < fNibbleTrackInfo.numTracks) {
			dierr = pDataGFD->Read(dataBuf, kTrackLenTrackStar525);
			if (dierr != kDIErrNone)
				goto bail;
			trackLen = fNibbleTrackInfo.length[track];
			assert(fNibbleTrackInfo.offset[track] == kTrackLenTrackStar525 * track);
		} else {
			WMSG1("   TrackStar faking track %d\n", track);
			memset(dataBuf, 0xff, sizeof(dataBuf));
			trackLen = kMaxTrackLen;
		}

		memset(writeBuf, 0x80, sizeof(writeBuf));	// not strictly necessary
		memset(writeBuf, 0x20, kCommentFieldLen);
		memset(writeBuf+kCommentFieldLen, 0x00, 0x81-kCommentFieldLen);

		const char* comment;
		if (fStorageName != NULL && *fStorageName != '\0')
			comment = fStorageName;
		else
			comment = "(created by CiderPress)";
		if (strlen(comment) > kCommentFieldLen)
			memcpy(writeBuf, comment, kCommentFieldLen);
		else
			memcpy(writeBuf, comment, strlen(comment));

		int i;
		for (i = 0; i < trackLen; i++)
			writeBuf[0x81+i] = dataBuf[trackLen - i -1];

		if (trackLen == kMaxTrackLen)
			PutShortLE(writeBuf + 0x19fe, 0);
		else
			PutShortLE(writeBuf + 0x19fe, (unsigned short) trackLen);

		dierr = pWrapperGFD->Write(writeBuf, sizeof(writeBuf));
		if (dierr != kDIErrNone)
			goto bail;
	}

    *pWrappedLen = pWrapperGFD->Tell();
	assert(*pWrappedLen == kFileTrackStorageLen * kTrackStarNumTracks);

bail:
#endif
    return dierr;
}

void
WrapperFDI::SetNibbleTrackLength(int track, int length)
{
	assert(false);		// not yet
#if 0
	assert(track >= 0);
	assert(length > 0 && length <= kMaxTrackLen);
	assert(track < fNibbleTrackInfo.numTracks);

	WMSG2("  FDI: set length of track %d to %d\n", track, length);
	fNibbleTrackInfo.length[track] = length;
#endif
}


/*
 * ===========================================================================
 *		Unadorned nibble format
 * ===========================================================================
 */

/*
 * See if this is unadorned nibble format.
 */
/*static*/ DIError
WrapperUnadornedNibble::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
	WMSG0("Testing for unadorned nibble\n");

    /* test length */
    if (wrappedLength != kTrackCount525 * kTrackLenNib525 &&
        wrappedLength != kTrackCount525 * kTrackLenNb2525)
    {
        return kDIErrGeneric;
    }

    /* quick scan for invalid data */
    const int kScanSize = 512;
    unsigned char buf[kScanSize];

    pGFD->Rewind();
    if (pGFD->Read(buf, sizeof(buf)) != kDIErrNone)
		return kDIErrGeneric;

    /*
     * Make sure this is a nibble image and not just a ProDOS volume that
     * happened to get the right number of blocks.  The primary test is
     * for < 0x80 since there's no way that can be valid, even on a track
     * full of junk.
     */
    for (int i = 0; i < kScanSize; i++) {
        if (buf[i] < 0x80) {
            WMSG2("  Disqualifying len=%ld from nibble, byte=0x%02x\n",
                (long) wrappedLength, buf[i]);
            return kDIErrGeneric;
        } else if (buf[i] < 0x96) {
            WMSG1("  Warning: funky byte 0x%02x in file\n", buf[i]);
        }
    }

    return kDIErrNone;
}

/*
 * Prepare unadorned nibble for use.  Not much to do here.
 */
DIError
WrapperUnadornedNibble::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
	WMSG0("Prep for unadorned nibble\n");

    if (wrappedLength == kTrackCount525 * kTrackLenNib525) {
        WMSG0("  Prepping for 6656-byte NIB\n");
        *pPhysical = DiskImg::kPhysicalFormatNib525_6656;
    } else if (wrappedLength == kTrackCount525 * kTrackLenNb2525) {
        WMSG0("  Prepping for 6384-byte NB2\n");
        *pPhysical = DiskImg::kPhysicalFormatNib525_6384;
    } else {
		WMSG1("  Unexpected wrappedLength %ld for unadorned nibble\n",
			(long) wrappedLength);
		assert(false);
	}

    *pLength = wrappedLength;
    *pOrder = DiskImg::kSectorOrderPhysical;

    *ppNewGFD = new GFDGFD;
    return ((GFDGFD*)*ppNewGFD)->Open(pGFD, 0, readOnly);
}

/*
 * Initialize fields for a new file.
 */
DIError
WrapperUnadornedNibble::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    WMSG0("Create unadorned nibble\n");

    *pWrappedLength = length;
    *pDataFD = new GFDGFD;
    return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, 0, false);
}

/*
 * We only use GFDGFD, so there's nothing to do here.
 */
DIError
WrapperUnadornedNibble::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    return kDIErrNone;
}


/*
 * ===========================================================================
 *		Unadorned sectors
 * ===========================================================================
 */

/*
 * See if this is unadorned sector format.  The only way we can really tell
 * is by looking at the file size.
 *
 * The only requirement is that it be a multiple of 512 bytes.  This holds
 * for all ProDOS volumes and all floppy disk images.  We also need to test
 * for 13-sector ".d13" images.
 *
 * It also holds for 35-track 6656-byte unadorned nibble images, so we need
 * to test for them first.
 */
/*static*/ DIError
WrapperUnadornedSector::Test(GenericFD* pGFD, di_off_t wrappedLength)
{
	WMSG2("Testing for unadorned sector (wrappedLength=%ld/%lu)\n",
		(long) (wrappedLength >> 32), (unsigned long) wrappedLength);
    if (wrappedLength >= 1536 && (wrappedLength % 512) == 0)
		return kDIErrNone;
	if (wrappedLength == kD13Length)		// 13-sector image?
		return kDIErrNone;

	return kDIErrGeneric;
}

/*
 * Prepare unadorned sector for use.  Not much to do here.
 */
DIError
WrapperUnadornedSector::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly,
    di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical,
	DiskImg::SectorOrder* pOrder, short* pDiskVolNum,
	LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD)
{
	WMSG0("Prepping for unadorned sector\n");
	assert(wrappedLength > 0);
	*pLength = wrappedLength;
	*pPhysical = DiskImg::kPhysicalFormatSectors;
    //*pOrder = undetermined

    *ppNewGFD = new GFDGFD;
    return ((GFDGFD*)*ppNewGFD)->Open(pGFD, 0, readOnly);
}

/*
 * Initialize fields for a new file.
 */
DIError
WrapperUnadornedSector::Create(di_off_t length, DiskImg::PhysicalFormat physical,
    DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD,
    di_off_t* pWrappedLength, GenericFD** pDataFD)
{
    WMSG0("Create unadorned sector\n");

    *pWrappedLength = length;
    *pDataFD = new GFDGFD;
    return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, 0, false);
}

/*
 * We only use GFDGFD, so there's nothing to do here.
 */
DIError
WrapperUnadornedSector::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD,
    di_off_t dataLen, di_off_t* pWrappedLen)
{
    return kDIErrNone;
}
