/*
 * CiderPress
 * Copyright (C) 2009 by CiderPress authors.  All Rights Reserved.
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Implementation of the DiskImg class.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"
#include "TwoImg.h"


/*
 * ===========================================================================
 *      DiskImg
 * ===========================================================================
 */

/*
 * Standard NibbleDescr profiles.
 *
 * These will be tried in the order in which they appear here.
 *
 * IMPORTANT: if you add or remove an entry, update the StdNibbleDescr enum
 * in DiskImg.h.
 *
 * Formats that allow the data checksum to be ignored should NOT be written.
 * It's possible that the DOS on the disk is ignoring the checksums, but
 * it's more likely that they're using a non-standard seed, and the newly-
 * written sectors will have the wrong checksum value.
 *
 * Non-standard headers are usually okay, because we don't rewrite the
 * headers, just the sector contents.
 */
/*static*/ const DiskImg::NibbleDescr DiskImg::kStdNibbleDescrs[] = {
    {
        "DOS 3.3 Standard",
        16,
        { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb },
        0x00,   // checksum seed
        true,   // verify checksum
        true,   // verify track
        2,      // epilog verify count
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,   // checksum seed
        true,   // verify checksum
        2,      // epilog verify count
        kNibbleEnc62,
        kNibbleSpecialNone,
    },
    {
        "DOS 3.3 Patched",
        16,
        { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb },
        0x00,   // checksum seed
        false,  // verify checksum
        false,  // verify track
        0,      // epilog verify count
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,   // checksum seed
        true,   // verify checksum
        0,      // epilog verify count
        kNibbleEnc62,
        kNibbleSpecialNone,
    },
    {
        "DOS 3.3 Ignore Checksum",
        16,
        { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb },
        0x00,   // checksum seed
        false,  // verify checksum
        false,  // verify track
        0,      // epilog verify count
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,   // checksum seed
        false,  // verify checksum
        0,      // epilog verify count
        kNibbleEnc62,
        kNibbleSpecialNone,
    },
    {
        "DOS 3.2 Standard",
        13,
        { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        true,
        2,
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        2,
        kNibbleEnc53,
        kNibbleSpecialNone,
    },
    {
        "DOS 3.2 Patched",
        13,
        { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb },
        0x00,
        false,
        false,
        0,
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        0,
        kNibbleEnc53,
        kNibbleSpecialNone,
    },
    {
        "Muse DOS 3.2",     // standard DOS 3.2 with doubled sectors
        13,
        { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        true,
        2,
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        2,
        kNibbleEnc53,
        kNibbleSpecialMuse,
    },
    {
        "RDOS 3.3",         // SSI 16-sector RDOS, with altered headers
        16,
        { 0xd4, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        true,
        0,      // epilog verify count
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        2,
        kNibbleEnc62,
        kNibbleSpecialSkipFirstAddrByte,
        /* odd tracks use d4aa96, even tracks use d5aa96 */
    },
    {
        "RDOS 3.2",         // SSI 13-sector RDOS, with altered headers
        13,
        { 0xd4, 0xaa, 0xb7 }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        true,
        2,
        { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb },
        0x00,
        true,
        2,
        kNibbleEnc53,
        kNibbleSpecialNone,
    },
    {
        "Custom",   // reserve space for empty slot
        0,
    },
};
/*static*/ const DiskImg::NibbleDescr*
DiskImg::GetStdNibbleDescr(StdNibbleDescr idx)
{
    if ((int)idx < 0 || (int)idx >= (int) NELEM(kStdNibbleDescrs))
        return NULL;
    return &kStdNibbleDescrs[(int)idx];
}


/*
 * Initialize the members during construction.
 */
DiskImg::DiskImg(void)
{
    assert(Global::GetAppInitCalled());

    fOuterFormat = kOuterFormatUnknown;
    fFileFormat = kFileFormatUnknown;
    fPhysical = kPhysicalFormatUnknown;
    fpNibbleDescr = NULL;
    fOrder = kSectorOrderUnknown;
    fFormat = kFormatUnknown;

    fFileSysOrder = kSectorOrderUnknown;
    fSectorPairing = false;
    fSectorPairOffset = -1;

    fpOuterGFD = NULL;
    fpWrapperGFD = NULL;
    fpDataGFD = NULL;
    fpOuterWrapper = NULL;
    fpImageWrapper = NULL;
    fpParentImg = NULL;
    fDOSVolumeNum = kVolumeNumNotSet;
    fOuterLength = -1;
    fWrappedLength = -1;
    fLength = -1;
    fExpandable = false;
    fReadOnly = true;
    fDirty = false;

    fHasSectors = false;
    fHasBlocks = false;
    fHasNibbles = false;

    fNumTracks = -1;
    fNumSectPerTrack = -1;
    fNumBlocks = -1;

    fpScanProgressCallback = NULL;

    /*
     * Create a working copy of the nibble descr table.  We want to leave
     * open the possibility of applications editing or discarding entries,
     * so we work off of a copy.
     *
     * Ideally we'd allow these to be set per-track, so that certain odd
     * formats could be handled transparently (e.g. Muse tweaked DOS 3.2)
     * for formatting as well as reading.
     */
    assert(kStdNibbleDescrs[kNibbleDescrCustom].numSectors == 0);
    assert(kNibbleDescrCustom == NELEM(kStdNibbleDescrs)-1);
    fpNibbleDescrTable = new NibbleDescr[NELEM(kStdNibbleDescrs)];
    fNumNibbleDescrEntries = NELEM(kStdNibbleDescrs);
    memcpy(fpNibbleDescrTable, kStdNibbleDescrs, sizeof(kStdNibbleDescrs));

    fNibbleTrackBuf = NULL;
    fNibbleTrackLoaded = -1;

    fNuFXCompressType = kNuThreadFormatLZW2;

    fNotes = NULL;
    fpBadBlockMap = NULL;
    fDiskFSRefCnt = 0;
}

/*
 * Throw away local storage.
 */
DiskImg::~DiskImg(void)
{
    if (fpDataGFD != NULL) {
        LOGI("~DiskImg closing GenericFD(s)");
    }
    (void) CloseImage();
    delete[] fpNibbleDescrTable;
    delete[] fNibbleTrackBuf;
    delete[] fNotes;
    delete fpBadBlockMap;

    /* normally these will be closed, but perhaps not if something failed */
    if (fpOuterGFD != NULL)
        delete fpOuterGFD;
    if (fpWrapperGFD != NULL)
        delete fpWrapperGFD;
    if (fpDataGFD != NULL)
        delete fpDataGFD;
    if (fpOuterWrapper != NULL)
        delete fpOuterWrapper;
    if (fpImageWrapper != NULL)
        delete fpImageWrapper;

    fDiskFSRefCnt = 100;    // flag as freed
}


/*
 * Set the nibble descr pointer.
 */
void DiskImg::SetNibbleDescr(int idx)
{
    assert(idx >= 0 && idx < kNibbleDescrMAX);
    fpNibbleDescr = &fpNibbleDescrTable[idx];
}

/*
 * Set up a custom nibble descriptor.
 */
void DiskImg::SetCustomNibbleDescr(const NibbleDescr* pDescr)
{
    if (pDescr == NULL) {
        fpNibbleDescr = NULL;
    } else {
        assert(fpNibbleDescrTable != NULL);
        //LOGI("Overwriting entry %d with new value (special=%d)",
        //  kNibbleDescrCustom, pDescr->special);
        fpNibbleDescrTable[kNibbleDescrCustom] = *pDescr;
        fpNibbleDescr = &fpNibbleDescrTable[kNibbleDescrCustom];
    }
}


/*
 * Open a volume or a file on disk.
 *
 * For Windows, we need to handle logical/physical volumes specially.  If
 * the filename matches the appropriate pattern, use a different GFD.
 */
DIError DiskImg::OpenImage(const char* pathName, char fssep, bool readOnly)
{
    DIError dierr = kDIErrNone;
    bool isWinDevice = false;

    if (fpDataGFD != NULL) {
        LOGI(" DI already open!");
        return kDIErrAlreadyOpen;
    }
    LOGI(" DI OpenImage '%s' '%.1s' ro=%d", pathName, &fssep, readOnly);

    fReadOnly = readOnly;

#ifdef _WIN32
    if ((fssep == '\0' || fssep == '\\') &&
        pathName[0] >= 'A' && pathName[0] <= 'Z' &&
        pathName[1] == ':' && pathName[2] == '\\' &&
        pathName[3] == '\0')
    {
        isWinDevice = true;     // logical volume ("A:\")
    }
    if ((fssep == '\0' || fssep == '\\') &&
        isdigit(pathName[0]) && isdigit(pathName[1]) &&
        pathName[2] == ':' && pathName[3] == '\\' &&
        pathName[4] == '\0')
    {
        isWinDevice = true;     // physical volume ("80:\")
    }
    if ((fssep == '\0' || fssep == '\\') &&
        strncmp(pathName, kASPIDev, strlen(kASPIDev)) == 0 &&
        pathName[strlen(pathName)-1] == '\\')
    {
        isWinDevice = true;     // ASPI volume ("ASPI:x:y:z\")
    }
#endif

    if (isWinDevice) {
#ifdef _WIN32
        GFDWinVolume* pGFDWinVolume = new GFDWinVolume;

        dierr = pGFDWinVolume->Open(pathName, fReadOnly);
        if (dierr != kDIErrNone) {
            delete pGFDWinVolume;
            goto bail;
        }

        fpWrapperGFD = pGFDWinVolume;
        // Use a unique extension to skip some of the probing.
        dierr = AnalyzeImageFile("CPDevice.cp-win-vol", '\0');
        if (dierr != kDIErrNone)
            goto bail;
#endif
    } else {
        GFDFile* pGFDFile = new GFDFile;

        dierr = pGFDFile->Open(pathName, fReadOnly);
        if (dierr != kDIErrNone) {
            delete pGFDFile;
            goto bail;
        }

        //fImageFileName = new char[strlen(pathName) + 1];
        //strcpy(fImageFileName, pathName);

        fpWrapperGFD = pGFDFile;
        pGFDFile = NULL;

        dierr = AnalyzeImageFile(pathName, fssep);
        if (dierr != kDIErrNone)
            goto bail;
    }


    assert(fpDataGFD != NULL);

bail:
    return dierr;
}

DIError DiskImg::OpenImageFromBufferRO(const uint8_t* buffer, long length) {
    return OpenImageFromBuffer(const_cast<uint8_t*>(buffer), length, true);
}

DIError DiskImg::OpenImageFromBufferRW(uint8_t* buffer, long length) {
    return OpenImageFromBuffer(buffer, length, false);
}

/*
 * Open from a buffer, which could point to unadorned ready-to-go content
 * or to a preloaded image file.
 */
DIError DiskImg::OpenImageFromBuffer(uint8_t* buffer, long length, bool readOnly)
{
    if (fpDataGFD != NULL) {
        LOGW(" DI already open!");
        return kDIErrAlreadyOpen;
    }
    LOGI(" DI OpenImage %08lx %ld ro=%d", (long) buffer, length, readOnly);

    DIError dierr;
    GFDBuffer* pGFDBuffer;

    fReadOnly = readOnly;
    pGFDBuffer = new GFDBuffer;

    dierr = pGFDBuffer->Open(buffer, length, false, false, readOnly);
    if (dierr != kDIErrNone) {
        delete pGFDBuffer;
        return dierr;
    }

    fpWrapperGFD = pGFDBuffer;
    pGFDBuffer = NULL;

    dierr = AnalyzeImageFile("", '\0');
    if (dierr != kDIErrNone)
        return dierr;

    assert(fpDataGFD != NULL);
    return kDIErrNone;
}

/*
 * Open a range of blocks from an already-open disk image.  This is only
 * useful for things like UNIDOS volumes, which don't have an associated
 * file in the image and are linear.
 *
 * The "read only" flag is inherited from the parent.
 *
 * For embedded images with visible file structure, we should be using
 * an EmbeddedFD instead.  [Note these were never implemented.]
 *
 * NOTE: there is an implicit ProDOS block ordering imposed on the parent
 * image.  It turns out that all of our current embedded parents use
 * ProDOS-ordered blocks, so it works out okay, but the "linear" requirement
 * above goes beyond just having contiguous blocks.
 */
DIError DiskImg::OpenImage(DiskImg* pParent, long firstBlock, long numBlocks)
{
    LOGI(" DI OpenImage parent=0x%08lx %ld %ld", (long) pParent, firstBlock,
        numBlocks);
    if (fpDataGFD != NULL) {
        LOGI(" DW already open!");
        return kDIErrAlreadyOpen;
    }

    if (pParent == NULL || firstBlock < 0 || numBlocks <= 0 ||
        firstBlock + numBlocks > pParent->GetNumBlocks())
    {
        assert(false);
        return kDIErrInvalidArg;
    }

    fReadOnly = pParent->GetReadOnly();     // very important

    DIError dierr;
    GFDGFD* pGFDGFD;

    pGFDGFD = new GFDGFD;
    dierr = pGFDGFD->Open(pParent->fpDataGFD, firstBlock * kBlockSize, fReadOnly);
    if (dierr != kDIErrNone) {
        delete pGFDGFD;
        return dierr;
    }

    fpDataGFD = pGFDGFD;
    assert(fpWrapperGFD == NULL);

    /*
     * This replaces the call to "analyze image file" because we know we
     * already have an open file with specific characteristics.
     */
    //fOffset = pParent->fOffset + kBlockSize * firstBlock;
    fLength = numBlocks * kBlockSize;
    fOuterLength = fWrappedLength = fLength;
    fFileFormat = kFileFormatUnadorned;
    fPhysical = pParent->fPhysical;
    fOrder = pParent->fOrder;

    fpParentImg = pParent;

    return dierr;
}

DIError DiskImg::OpenImage(DiskImg* pParent, long firstTrack, long firstSector,
    long numSectors)
{
    LOGI(" DI OpenImage parent=0x%08lx %ld %ld %ld", (long) pParent,
        firstTrack, firstSector, numSectors);
    if (fpDataGFD != NULL) {
        LOGW(" DI already open!");
        return kDIErrAlreadyOpen;
    }

    if (pParent == NULL)
        return kDIErrInvalidArg;

    int prntSectPerTrack = pParent->GetNumSectPerTrack();
    int lastTrack = firstTrack +
                        (numSectors + prntSectPerTrack-1) / prntSectPerTrack;
    if (firstTrack < 0 || numSectors <= 0 ||
        lastTrack > pParent->GetNumTracks())
    {
        return kDIErrInvalidArg;
    }

    fReadOnly = pParent->GetReadOnly();     // very important

    DIError dierr;
    GFDGFD* pGFDGFD;

    pGFDGFD = new GFDGFD;
    dierr = pGFDGFD->Open(pParent->fpDataGFD,
                kSectorSize * firstTrack * prntSectPerTrack, fReadOnly);
    if (dierr != kDIErrNone) {
        delete pGFDGFD;
        return dierr;
    }
    
    fpDataGFD = pGFDGFD;
    assert(fpWrapperGFD == NULL);

    /*
     * This replaces the call to "analyze image file" because we know we
     * already have an open file with specific characteristics.
     */
    assert(firstSector == 0);   // else fOffset calculation breaks
    //fOffset = pParent->fOffset + kSectorSize * firstTrack * prntSectPerTrack;
    fLength = numSectors * kSectorSize;
    fOuterLength = fWrappedLength = fLength;
    fFileFormat = kFileFormatUnadorned;
    fPhysical = pParent->fPhysical;
    fOrder = pParent->fOrder;

    fpParentImg = pParent;

    return dierr;
}


/*
 * Enable sector pairing.  Useful for OzDOS.
 */
void DiskImg::SetPairedSectors(bool enable, int idx)
{
    fSectorPairing = enable;
    fSectorPairOffset = idx;

    if (enable) {
        assert(idx == 0 || idx == 1);
    }
}

/*
 * Close the image, freeing resources.
 *
 * If we write to a child DiskImg, it's responsible for setting the "dirty"
 * flag in its parent (and so on up the chain).  That's necessary so that,
 * when we close the file, changes made to a child DiskImg cause the parent
 * to do any necessary recompression.
 *
 * [ This is getting called even when image creation failed with an error.
 * This is probably the correct behavior, but we may want to be aborting the
 * image creation instead of completing it.  That's a higher-level decision
 * though. ++ATM 20040506 ]
 */
DIError DiskImg::CloseImage(void)
{
    DIError dierr;

    LOGI("CloseImage 0x%p", this);

    /* check for DiskFS objects that still point to us */
    if (fDiskFSRefCnt != 0) {
        LOGE("ERROR: CloseImage: fDiskFSRefCnt=%d", fDiskFSRefCnt);
        assert(false); //DebugBreak();
    }

    /*
     * Flush any changes.
     */
    dierr = FlushImage(kFlushAll);
    if (dierr != kDIErrNone)
        return dierr;

    /*
     * Clean up.  Close GFD, OrigGFD, and OuterGFD.  Delete ImageWrapper
     * and OuterWrapper.
     *
     * In some cases we will have the file open more than once (e.g. a
     * NuFX archive, which must be opened on disk).
     */
    if (fpDataGFD != NULL) {
        fpDataGFD->Close();
        delete fpDataGFD;
        fpDataGFD = NULL;
    }
    if (fpWrapperGFD != NULL) {
        fpWrapperGFD->Close();
        delete fpWrapperGFD;
        fpWrapperGFD = NULL;
    }
    if (fpOuterGFD != NULL) {
        fpOuterGFD->Close();
        delete fpOuterGFD;
        fpOuterGFD = NULL;
    }
    delete fpImageWrapper;
    fpImageWrapper = NULL;
    delete fpOuterWrapper;
    fpOuterWrapper = NULL;

    return dierr;
}


/*
 * Flush data to disk.
 *
 * The only time this really needs to do anything on a disk image file is
 * when we have compressed data (NuFX, DDD, .gz, .zip).  The uncompressed
 * wrappers either don't do anything ("unadorned") or just update some
 * header fields (DiskCopy42).
 *
 * If "mode" is kFlushFastOnly, we only flush the formats that don't really
 * need flushing.  This is part of a scheme to keep the disk contents in a
 * reasonable state on the off chance we crash with a modified file open.
 * It also helps the user understand when changes are being made immediately
 * vs. when they're written to memory and compressed later.  We could just
 * refuse to raise the "dirty" flag when modifying "simple" file formats,
 * but that would change the meaning of the flag from "something has been
 * changed" to "what's in the file and what's in memory differ".  I want it
 * to be a "dirty" flag.
 */
DIError DiskImg::FlushImage(FlushMode mode)
{
    DIError dierr = kDIErrNone;

    LOGI(" DI FlushImage (dirty=%d mode=%d)", fDirty, mode);
    if (!fDirty)
        return kDIErrNone;
    if (fpDataGFD == NULL) {
        /*
         * This can happen if we tried to create a disk image but failed, e.g.
         * couldn't create the output file because of access denied on the
         * directory.  There's no data, therefore nothing to flush, but the
         * "dirty" flag is set because CreateImageCommon sets it almost
         * immediately.
         */
        LOGI("  (disk must've failed during creation)");
        fDirty = false;
        return kDIErrNone;
    }

    if (mode == kFlushFastOnly &&
        ((fpImageWrapper != NULL && !fpImageWrapper->HasFastFlush()) ||
         (fpOuterWrapper != NULL && !fpOuterWrapper->HasFastFlush()) ))
    {
        LOGI("DI fast flush requested, but one or both wrappers are slow");
        return kDIErrNone;
    }

    /*
     * Step 1: make sure any local caches have been flushed.
     */
    /* (none) */

    /*
     * Step 2: push changes from fpDataGFD to fpWrapperGFD.  This will
     * cause ImageWrapper to rebuild itself (SHK, DDD, whatever).  In
     * some cases this amounts to copying the data on top of itself,
     * which we can avoid easily.
     *
     * Embedded volumes don't have wrappers; when you write to an
     * embedded volume, it passes straight through to the parent.
     *
     * (Note to self: formats like NuFX that write to a temp file and then
     * rename over the old will close fpWrapperGFD and just access it
     * directly.  This is bad, because it doesn't allow them to have an
     * "outer" format, but it's the way life is.  The point is that it's
     * okay for fpWrapperGFD to be non-NULL but represent a closed file,
     * so long as the "Flush" function has it figured out.)
     */
    if (fpWrapperGFD != NULL) {
        LOGI(" DI flushing data changes to wrapper (fLen=%ld fWrapLen=%ld)",
            (long) fLength, (long) fWrappedLength);
        dierr = fpImageWrapper->Flush(fpWrapperGFD, fpDataGFD, fLength,
                    &fWrappedLength);
        if (dierr != kDIErrNone) {
            LOGI(" ERROR: wrapper flush failed (err=%d)", dierr);
            return dierr;
        }
        /* flush the GFD in case it's a Win32 volume with block caching */
        dierr = fpWrapperGFD->Flush();
    } else {
        assert(fpParentImg != NULL);
    }

    /*
     * Step 3: if we have an fpOuterGFD, rebuild the file with the data
     * in fpWrapperGFD.
     */
    if (fpOuterWrapper != NULL) {
        LOGI(" DI saving wrapper to outer, fWrapLen=%ld",
            (long) fWrappedLength);
        assert(fpOuterGFD != NULL);
        dierr = fpOuterWrapper->Save(fpOuterGFD, fpWrapperGFD,
                    fWrappedLength);
        if (dierr != kDIErrNone) {
            LOGI(" ERROR: outer save failed (err=%d)", dierr);
            return dierr;
        }
    }

    fDirty = false;
    return kDIErrNone;
}


/*
 * Given the filename extension and a GFD, figure out what's inside.
 *
 * The filename extension should give us some idea what to expect:
 *  SHK, SDK, BXY - ShrinkIt compressed disk image
 *  GZ - gzip-compressed file (with something else inside)
 *  ZIP - ZIP archive with a single disk image inside
 *  DDD - DDD, DDD Pro, or DDD5.0 compressed image
 *  DSK - DiskCopy 4.2 or DO/PO
 *  DC - DiskCopy 4.2 (or 6?)
 *  DC6 - DiskCopy 6 (usually just raw sectors)
 *  DO, PO, D13, RAW? - DOS-order or ProDOS-order uncompressed
 *  IMG - Copy ][+ image (unadorned, physical sector order)
 *  HDV - virtual hard drive image
 *  NIB, RAW? - nibblized image
 *  (no extension) uncompressed
 *  cp-win-vol - our "magic" extension to indicate a Windows logical volume
 *
 * We can also examine the file length to see if it's a standard size
 * (140K, 800K) and look for magic values in the header.
 *
 * If we can access the contents directly from disk, we do so.  It's
 * possibly more efficient to load the whole thing into memory, but if
 * we have that much memory then the OS should cache it for us.  (I have
 * some 20MB disk images from my hard drive that shouldn't be loaded
 * in their entirety.  Certainly don't want to load a 512MB CFFA image.)
 *
 * On input, the following fields must be set:
 *  fpWrapperGFD - GenericFD for the file pointed to by "pathname" (or for a
 *            memory buffer if this is a sub-volume)
 *
 * On success, the following fields will be set:
 *  fWrappedLength, fOuterLength - set appropriately
 *  fpDataGFD - GFD for the raw data, possibly just a GFDGFD with an offset
 *  fLength - length of unadorned data in the file, or the length of
 *            data stored in fBuffer (test for fBuffer!=NULL)
 *  fFileFormat - set to the overall file format, mostly interesting
 *            for identification of the file "wrapper"
 *  fPhysicalFormat - set to the type of data this holds
 *  (maybe) fOrder - set when the file format or extension dictates, e.g.
 *             2MG or *.po; not always reliable
 *  (maybe) fDOSVolumeNum - set to DOS volume number from wrapper
 *
 * This may set fReadOnly if one of the wrappers looks okay but is reporting
 * a bad checksum.
 */
DIError DiskImg::AnalyzeImageFile(const char* pathName, char fssep)
{
    DIError dierr = kDIErrNone;
    FileFormat probableFormat;
    bool reliableExt;
    const char* ext = FindExtension(pathName, fssep);
    char* extBuf = NULL;     // uses malloc/free
    bool needExtFromOuter = false;

    if (ext != NULL) {
        assert(*ext == '.');
        ext++;
    } else
        ext = "";

    LOGI(" DI AnalyzeImageFile '%s' '%c' ext='%s'",
        pathName, fssep, ext);

    /* sanity check: nobody should have configured these yet */
    assert(fOuterFormat == kOuterFormatUnknown);
    assert(fFileFormat == kFileFormatUnknown);
    assert(fOrder == kSectorOrderUnknown);
    assert(fFormat == kFormatUnknown);
    fLength = -1;
    dierr = fpWrapperGFD->Seek(0, kSeekEnd);
    if (dierr != kDIErrNone) {
        LOGW("  DI Couldn't seek to end of wrapperGFD");
        goto bail;
    }
    fWrappedLength = fOuterLength = fpWrapperGFD->Tell();

    /* quick test for zero-length files */
    if (fWrappedLength == 0)
        return kDIErrUnrecognizedFileFmt;

    /*
     * Start by checking for a zip/gzip "wrapper wrapper".  We want to strip
     * that away before we do anything else.  Because web sites tend to
     * gzip everything in sight whether it needs it or not, we treat this
     * as a special case and assume that anything could be inside.
     *
     * Some cases are difficult to handle, e.g. ".SDK", since NufxLib
     * doesn't let us open an archive that is sitting in memory.
     *
     * We could also handle disk images stored as ordinary files stored
     * inside SHK.  Not much point in handling multiple files down at
     * this level though.
     */
    if (strcasecmp(ext, "gz") == 0 &&
        OuterGzip::Test(fpWrapperGFD, fOuterLength) == kDIErrNone)
    {
        LOGI("  DI found gz outer wrapper");

        fpOuterWrapper = new OuterGzip();
        if (fpOuterWrapper == NULL) {
            dierr = kDIErrMalloc;
            goto bail;
        }
        fOuterFormat = kOuterFormatGzip;

        /* drop the ".gz" and get down to the next extension */
        ext = "";
        extBuf = strdup(pathName);
        if (extBuf != NULL) {
            char* localExt;

            localExt = (char*) FindExtension(extBuf, fssep);
            if (localExt != NULL)
                *localExt = '\0';
            localExt = (char*) FindExtension(extBuf, fssep);
            if (localExt != NULL) {
                ext = localExt;
                assert(*ext == '.');
                ext++;
            }
        }
        LOGI("  DI after gz, ext='%s'", ext == NULL ? "(NULL)" : ext);

    } else if (strcasecmp(ext, "zip") == 0) {
        dierr = OuterZip::Test(fpWrapperGFD, fOuterLength);
        if (dierr != kDIErrNone)
            goto bail;

        LOGI("  DI found ZIP outer wrapper");

        fpOuterWrapper = new OuterZip();
        if (fpOuterWrapper == NULL) {
            dierr = kDIErrMalloc;
            goto bail;
        }
        fOuterFormat = kOuterFormatZip;

        needExtFromOuter = true;

    } else {
        fOuterFormat = kOuterFormatNone;
    }

    /* finish up outer wrapper stuff */
    if (fOuterFormat != kOuterFormatNone) {
        GenericFD* pNewGFD = NULL;
        dierr = fpOuterWrapper->Load(fpWrapperGFD, fOuterLength, fReadOnly,
                    &fWrappedLength, &pNewGFD);
        if (dierr != kDIErrNone) {
            LOGI("  DW outer prep failed");
            /* extensions are "reliable", so failure is unavoidable */
            goto bail;
        }

        /* Load() sets this */
        if (fpOuterWrapper->IsDamaged()) {
            AddNote(kNoteWarning, "The zip/gzip wrapper appears to be damaged.");
            fReadOnly = true;
        }

        /* shift GFDs */
        fpOuterGFD = fpWrapperGFD;
        fpWrapperGFD = pNewGFD;

        if (needExtFromOuter) {
            ext = fpOuterWrapper->GetExtension();
            if (ext == NULL)
                ext = "";
        }
    }

    /*
     * Try to figure out what format the file is in.
     *
     * First pass, try only what the filename says it is.  This way, if
     * two file formats look alike, we have a good chance of getting it
     * right.
     *
     * The "Test" functions have the complete file at their disposal.  The
     * file's length is stored in "fWrappedLength" for convenience.
     */
    reliableExt = false;
    probableFormat = kFileFormatUnknown;
    if (strcasecmp(ext, "2mg") == 0 || strcasecmp(ext, "2img") == 0) {
        reliableExt = true;
        if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            probableFormat = kFileFormat2MG;
    } else if (strcasecmp(ext, "shk") == 0 || strcasecmp(ext, "sdk") == 0 ||
        strcasecmp(ext, "bxy") == 0)
    {
        DIError dierr2;
        reliableExt = true;
        dierr2 = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength);
        if (dierr2 == kDIErrNone)
            probableFormat = kFileFormatNuFX;
        else if (dierr2 == kDIErrFileArchive) {
            LOGI(" AnalyzeImageFile thinks it found a NuFX file archive");
            dierr = dierr2;
            goto bail;
        }
    } else if (strcasecmp(ext, "hdv") == 0) {
        /* usually just a "raw" disk, but check for Sim //e */
        if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            probableFormat = kFileFormatSim2eHDV;

        /* ProDOS .hdv volumes can expand */
        fExpandable = true;
    } else if (strcasecmp(ext, "dsk") == 0 || strcasecmp(ext, "dc") == 0) {
        /* might be DiskCopy */
        if (WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            probableFormat = kFileFormatDiskCopy42;
    } else if (strcasecmp(ext, "ddd") == 0) {
        /* do this after compressed formats but before unadorned */
        reliableExt = true;
        if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            probableFormat = kFileFormatDDD;
    } else if (strcasecmp(ext, "app") == 0) {
        reliableExt = true;
        if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            probableFormat = kFileFormatTrackStar;
    } else if (strcasecmp(ext, "fdi") == 0) {
        reliableExt = true;
        if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            probableFormat = kFileFormatFDI;
    } else if (strcasecmp(ext, "img") == 0) {
        if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
        {
            probableFormat = kFileFormatUnadorned;
            fPhysical = kPhysicalFormatSectors;
            fOrder = kSectorOrderPhysical;
        }
    } else if (strcasecmp(ext, "nib") == 0 || strcasecmp(ext, "raw") == 0) {
        if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
        {
            probableFormat = kFileFormatUnadorned;
            fPhysical = kPhysicalFormatNib525_6656;
            /* figure out NibbleFormat later */
        }
    } else if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "po") == 0 ||
        strcasecmp(ext, "d13") == 0 || strcasecmp(ext, "dc6") == 0)
    {
        if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
        {
            probableFormat = kFileFormatUnadorned;
            fPhysical = kPhysicalFormatSectors;
            if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "d13") == 0)
                fOrder = kSectorOrderDOS;
            else
                fOrder = kSectorOrderProDOS;    // po, dc6
            LOGI("  DI guessing order is %d by extension", fOrder);
        }
    } else if (strcasecmp(ext, "cp-win-vol") == 0) {
        /* this is a Windows logical volume */
        reliableExt = true;
        probableFormat = kFileFormatUnadorned;
        fPhysical = kPhysicalFormatSectors;
        fOrder = kSectorOrderProDOS;
    } else {
        /* no match on the filename extension; start guessing */
    }

    if (probableFormat != kFileFormatUnknown) {
        /*
         * Found a match.  Use "probableFormat" to open the file.
         */
        LOGI(" DI scored hit on extension '%s'", ext);
    } else {
        /*
         * Didn't work.  If the file extension was marked "reliable", then
         * either we have the wrong extension on the file, or the contents
         * are damaged.
         *
         * If the extension isn't reliable, or simply absent, then we have
         * to probe through the formats we know and just hope for the best.
         *
         * If the "test" function returns with a checksum failure, we take
         * it to mean that the format was positively identified, but the
         * data inside is corrupted.  This results in an immediate return
         * with the checksum failure noted.  Only a few wrapper formats
         * have checksums embedded.  (The "test" functions should only
         * be looking at header checksums.)
         */
        if (reliableExt) {
            LOGI(" DI file extension '%s' did not match contents", ext);
            dierr = kDIErrBadFileFormat;
            goto bail;
        } else {
            LOGI(" DI extension '%s' not useful, probing formats", ext);
            dierr = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength);
            if (dierr == kDIErrNone) {
                probableFormat = kFileFormatNuFX;
                goto gotit;
            } else if (dierr == kDIErrFileArchive)
                goto bail;      // we know it's NuFX, we know we can't use it
            else if (dierr == kDIErrBadChecksum)
                goto bail;      // right file type, bad data

            dierr = WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength);
            if (dierr == kDIErrNone) {
                probableFormat = kFileFormatDiskCopy42;
                goto gotit;
            } else if (dierr == kDIErrBadChecksum)
                goto bail;      // right file type, bad data

            if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) {
                probableFormat = kFileFormat2MG;
            } else if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) {
                probableFormat = kFileFormatDDD;
            } else if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            {
                probableFormat = kFileFormatSim2eHDV;
            } else if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            {
                probableFormat = kFileFormatTrackStar;
            } else if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone)
            {
                probableFormat = kFileFormatFDI;
            } else if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) {
                probableFormat = kFileFormatUnadorned;
                fPhysical = kPhysicalFormatNib525_6656;  // placeholder
            } else if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) {
                probableFormat = kFileFormatUnadorned;
                fPhysical = kPhysicalFormatSectors;
            }
gotit: ;
        }
    }

    /*
     * Either we recognize it or we don't.  Finish opening the file by
     * setting up "fLength" and "fPhysical" values, extracting data
     * into a memory buffer if necessary.  fpDataGFD is set up by the
     * "prep" function.
     *
     * If we're lucky, this will also configure "fOrder" for us, which is
     * important when we can't recognize the filesystem format (for correct
     * operation of disk tools).
     */
    switch (probableFormat) {
    case kFileFormat2MG:
        fpImageWrapper = new Wrapper2MG();
        break;
    case kFileFormatDiskCopy42:
        fpImageWrapper = new WrapperDiskCopy42();
        break;
    case kFileFormatSim2eHDV:
        fpImageWrapper = new WrapperSim2eHDV();
        break;
    case kFileFormatTrackStar:
        fpImageWrapper = new WrapperTrackStar();
        break;
    case kFileFormatFDI:
        fpImageWrapper = new WrapperFDI();
        fReadOnly = true;       // writing to FDI not yet supported
        break;
    case kFileFormatNuFX:
        fpImageWrapper = new WrapperNuFX();
        ((WrapperNuFX*)fpImageWrapper)->SetCompressType(
                                        (NuThreadFormat) fNuFXCompressType);
        break;
    case kFileFormatDDD:
        fpImageWrapper = new WrapperDDD();
        break;
    case kFileFormatUnadorned:
        if (IsSectorFormat(fPhysical))
            fpImageWrapper = new WrapperUnadornedSector();
        else if (IsNibbleFormat(fPhysical))
            fpImageWrapper = new WrapperUnadornedNibble();
        else {
            assert(false);
        }
        break;
    default:
        LOGI(" DI couldn't figure out the file format");
        dierr = kDIErrUnrecognizedFileFmt;
        break;
    }
    if (fpImageWrapper != NULL) {
        assert(fpDataGFD == NULL);
        dierr = fpImageWrapper->Prep(fpWrapperGFD, fWrappedLength, fReadOnly,
                    &fLength, &fPhysical, &fOrder, &fDOSVolumeNum,
                    &fpBadBlockMap, &fpDataGFD);
    } else {
        /* could be a mem alloc failure that didn't set dierr */
        if (dierr == kDIErrNone)
            dierr = kDIErrGeneric;
    }

    if (dierr != kDIErrNone) {
        LOGI(" DI wrapper prep failed (err=%d)", dierr);
        goto bail;
    }

    /* check for non-fatal checksum failures, e.g. DiskCopy42 */
    if (fpImageWrapper->IsDamaged()) {
        AddNote(kNoteWarning, "File checksum didn't match.");
        fReadOnly = true;
    }

    fFileFormat = probableFormat;

    assert(fLength >= 0);
    assert(fpDataGFD != NULL);
    assert(fOuterFormat != kOuterFormatUnknown);
    assert(fFileFormat != kFileFormatUnknown);
    assert(fPhysical != kPhysicalFormatUnknown);

bail:
    free(extBuf);
    return dierr;
}


/*
 * Try to figure out what we're looking at.
 *
 * Returns an error if we don't think this is even a disk image.  If we
 * just can't figure it out, we return success but with the format value
 * set to "unknown".  This gives the caller a chance to use "override"
 * to help us find our way.
 *
 * On entry:
 *  fpDataGFD, fLength, and fFileFormat are defined
 *  fSectorPairing is specified
 *  fOrder has a semi-reliable guess at sector ordering
 * On exit:
 *  fOrder and fFormat are set to the best of our ability
 *  fNumTracks, fNumSectPerTrack, and fNumBlocks are set
 *  fHasSectors, fHasTracks, and fHasNibbles are set
 *  fFileSysOrder is set
 *  fpNibbleDescr will be set for nibble images
 */
DIError DiskImg::AnalyzeImage(void)
{
    assert(fLength >= 0);
    assert(fpDataGFD != NULL);
    assert(fFileFormat != kFileFormatUnknown);
    assert(fPhysical != kPhysicalFormatUnknown);
    assert(fFormat == kFormatUnknown);
    assert(fFileSysOrder == kSectorOrderUnknown);
    assert(fNumTracks == -1);
    assert(fNumSectPerTrack == -1);
    assert(fNumBlocks == -1);
    if (fpDataGFD == NULL)
        return kDIErrInternal;

    /*
     * Figure out how many tracks and sectors the image has.
     *
     * For an odd-sized ProDOS image, there will be no tracks and sectors.
     */
    if (IsSectorFormat(fPhysical)) {
        if (!fLength) {
            LOGI(" DI zero-length disk images not allowed");
            return kDIErrOddLength;
        }

        if (fLength == kD13Length) {
            /* 13-sector .d13 image */
            fHasSectors = true;
            fNumSectPerTrack = 13;
            fNumTracks = kTrackCount525;
            assert(!fHasBlocks);
        } else if (fLength % (16 * kSectorSize) == 0) {
            /* looks like a collection of 16-sector tracks */
            fHasSectors = true;

            fNumSectPerTrack = 16;
            fNumTracks = (int) (fLength / (fNumSectPerTrack * kSectorSize));

            /* sector pairing effectively cuts #of tracks in half */
            if (fSectorPairing) {
                if ((fNumTracks & 0x01) != 0) {
                    LOGI(" DI error: bad attempt at sector pairing");
                    assert(false);
                    fSectorPairing = false;
                }
            }

            if (fSectorPairing)
                fNumTracks /= 2;
        } else {
            if (fSectorPairing) {
                LOGI("GLITCH: sector pairing enabled, but fLength=%ld",
                    (long) fLength);
                return kDIErrOddLength;
            }

            assert(fNumTracks == -1);
            assert(fNumSectPerTrack == -1);
            assert((fLength % kBlockSize) == 0);

            fHasBlocks = true;
            fNumBlocks = (long) (fLength / kBlockSize);
        }
    } else if (IsNibbleFormat(fPhysical)) {
        fHasNibbles = fHasSectors = true;

        /*
         * Figure out if it's 13-sector or 16-sector (or garbage).  We
         * have to make an assessment of the entire disk so we can declare
         * it to be 13-sector or 16-sector, which is useful for DiskFS
         * which will want to scan for DOS VTOCs and other goodies.  We
         * also want to provide a default NibbleDescr.
         *
         * Failing that, we still allow it to be opened for raw track access.
         *
         * This also sets fNumTracks, which could be more than 35 if we're
         * working with a TrackStar or FDI image.
         */
        DIError dierr;
        dierr = AnalyzeNibbleData();    // sets nibbleDescr and DOS vol num
        if (dierr == kDIErrNone) {
            assert(fpNibbleDescr != NULL);
            fNumSectPerTrack = fpNibbleDescr->numSectors;
            fOrder = kSectorOrderPhysical;

            if (!fReadOnly && !fpNibbleDescr->dataVerifyChecksum) {
                LOGI("DI nibbleDescr does not verify data checksum, disabling writes");
                AddNote(kNoteInfo,
                    "Sectors use non-standard data checksums; writing disabled.");
                fReadOnly = true;
            }
        } else {
            //assert(fpNibbleDescr == NULL);
            fNumSectPerTrack = -1;
            fOrder = kSectorOrderPhysical;
            fHasSectors = false;
        }
    } else {
        LOGI("Unsupported physical %d", fPhysical);
        assert(false);
        return kDIErrGeneric;
    }

    /*
     * Compute the number of blocks.  For a 13-sector disk, block access
     * is not possible.
     *
     * For nibble formats, we have to base the block count on the number
     * of sectors rather than the file length.
     */
    if (fHasSectors) {
        assert(fNumSectPerTrack > 0);
        if ((fNumSectPerTrack & 0x01) == 0) {
            /* not a 13-sector disk, so define blocks in terms of sectors */
            /* (effects of sector pairing are already taken into account) */
            fHasBlocks = true;
            fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2;
        }
    } else if (fHasBlocks) {
        if ((fLength % kBlockSize) == 0) {
            /* not sector-oriented, so define blocks based on length */
            fHasBlocks = true;
            fNumBlocks = (long) (fLength / kBlockSize);

             if (fSectorPairing) {
                if ((fNumBlocks & 0x01) != 0) {
                    LOGI(" DI error: bad attempt at sector pairing (blk)");
                    assert(false);
                    fSectorPairing = false;
                } else
                   fNumBlocks /= 2;
            }

        } else {
            assert(false);
            return kDIErrGeneric;
        }
    } else if (fHasNibbles) {
        assert(fNumBlocks == -1);
    } else {
        LOGI(" DI none of fHasSectors/fHasBlocks/fHasNibbles are set");
        assert(false);
        return kDIErrInternal;
    }

    /*
     * We've got the track/sector/block layout sorted out; now figure out
     * what kind of filesystem we're dealing with.
     */
    AnalyzeImageFS();

    LOGI(" DI AnalyzeImage tracks=%ld sectors=%d blocks=%ld fileSysOrder=%d",
        fNumTracks, fNumSectPerTrack, fNumBlocks, fFileSysOrder);
    LOGI("    hasBlocks=%d hasSectors=%d hasNibbles=%d",
        fHasBlocks, fHasSectors, fHasNibbles);

    return kDIErrNone;
}

/*
 * Try to figure out what filesystem exists on this disk image.
 *
 * We want to test for DOS before ProDOS, because sometimes they overlap (e.g.
 * 800K ProDOS disk with five 160K DOS volumes on it).
 *
 * Sets fFormat, fOrder, and fFileSysOrder.
 */
void DiskImg::AnalyzeImageFS(void)
{
    /*
     * In some circumstances it would be useful to have a set describing
     * what filesystems we might expect to find, e.g. we're not likely to
     * encounter RDOS embedded in a CF card.
     */
    if (DiskFSMacPart::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatMacPart);
        LOGI(" DI found MacPart, order=%d", fOrder);
    } else if (DiskFSMicroDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatMicroDrive);
        LOGI(" DI found MicroDrive, order=%d", fOrder);
    } else if (DiskFSFocusDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatFocusDrive);
        LOGI(" DI found FocusDrive, order=%d", fOrder);
    } else if (DiskFSCFFA::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        // The CFFA format doesn't have a partition map, but we do insist
        // on finding multiple volumes.  It needs to come after MicroDrive,
        // because a disk formatted for CFFA then subsequently partitioned
        // for MicroDrive will still look like valid CFFA unless you zero
        // out the blocks.
        assert(fFormat == kFormatCFFA4 || fFormat == kFormatCFFA8);
        LOGI(" DI found CFFA, order=%d", fOrder);
    } else if (DiskFSFAT::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        // This is really just a trap to catch CFFA cards that were formatted
        // for ProDOS and then re-formatted for MSDOS.  As such it needs to
        // come before the ProDOS test.  It only works on larger volumes,
        // and can be overridden, so it's pretty safe.
        assert(fFormat == kFormatMSDOS);
        LOGI(" DI found MSDOS, order=%d", fOrder);
    } else if (DiskFSDOS33::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatDOS32 || fFormat == kFormatDOS33);
        LOGI(" DI found DOS3.x, order=%d", fOrder);
        if (fNumSectPerTrack == 13)
            fFormat = kFormatDOS32;
    } else if (DiskFSUNIDOS::TestWideFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        // Should only succeed on 400K embedded chunks.
        assert(fFormat == kFormatDOS33);
        fNumSectPerTrack = 32;
        fNumTracks /= 2;
        LOGI(" DI found 'wide' DOS3.3, order=%d", fOrder);
    } else if (DiskFSUNIDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatUNIDOS);
        fNumSectPerTrack = 32;
        fNumTracks /= 2;
        LOGI(" DI found UNIDOS, order=%d", fOrder);
    } else if (DiskFSOzDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatOzDOS);
        fNumSectPerTrack = 32;
        fNumTracks /= 2;
        LOGI(" DI found OzDOS, order=%d", fOrder);
    } else if (DiskFSProDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatProDOS);
        LOGI(" DI found ProDOS, order=%d", fOrder);
    } else if (DiskFSPascal::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatPascal);
        LOGI(" DI found Pascal, order=%d", fOrder);
    } else if (DiskFSCPM::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatCPM);
        LOGI(" DI found CP/M, order=%d", fOrder);
    } else if (DiskFSRDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatRDOS33 ||
               fFormat == kFormatRDOS32 ||
               fFormat == kFormatRDOS3);
        LOGI(" DI found RDOS 3.3, order=%d", fOrder);
    } else if (DiskFSHFS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatMacHFS);
        LOGI(" DI found HFS, order=%d", fOrder);
    } else if (DiskFSGutenberg::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone)
    {
        assert(fFormat == kFormatGutenberg);
        LOGI(" DI found Gutenberg, order=%d", fOrder);
    } else {
        fFormat = kFormatUnknown;
        LOGI(" DI no recognizeable filesystem found (fOrder=%d)",
            fOrder);
    }

    fFileSysOrder = CalcFSSectorOrder();
}


/*
 * Override the format determined by the analyzer.
 *
 * If they insist on the presence of a valid filesystem, check to make sure
 * that filesystem actually exists.
 *
 * Note that this does not allow overriding the file structure, which must
 * be clearly identifiable to be at all useful.  If the file has no "wrapper"
 * structure, the "unadorned" format should be specified, and the contents
 * identified by the PhysicalFormat.
 */
DIError DiskImg::OverrideFormat(PhysicalFormat physical, FSFormat format,
    SectorOrder order)
{
    DIError dierr = kDIErrNone;
    SectorOrder newOrder;
    FSFormat newFormat;

    LOGI(" DI override: physical=%d format=%d order=%d",
        physical, format, order);

    if (!IsSectorFormat(physical) && !IsNibbleFormat(physical))
        return kDIErrUnsupportedPhysicalFmt;

    /* don't allow forcing physical format change */
    if (physical != fPhysical)
        return kDIErrInvalidArg;

    /* optimization */
    if (physical == fPhysical && format == fFormat && order == fOrder) {
        LOGI("  DI override matches existing, ignoring");
        return kDIErrNone;
    }

    newOrder = order;
    newFormat = format;

    switch (format) {
    case kFormatDOS33:
    case kFormatDOS32:
        dierr = DiskFSDOS33::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        // Go ahead and allow the override even if the DOS version is wrong.
        // So long as the sector count is correct, it's okay.
        break;
    case kFormatProDOS:
        dierr = DiskFSProDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatPascal:
        dierr = DiskFSPascal::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatMacHFS:
        dierr = DiskFSHFS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatUNIDOS:
        dierr = DiskFSUNIDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatOzDOS:
        dierr = DiskFSOzDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatCFFA4:
    case kFormatCFFA8:
        dierr = DiskFSCFFA::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        // So long as it's CFFA, we allow the user to force it to be 4-mode
        // or 8-mode.  Don't require newFormat==format.
        break;
    case kFormatMacPart:
        dierr = DiskFSMacPart::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatMicroDrive:
        dierr = DiskFSMicroDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatFocusDrive:
        dierr = DiskFSFocusDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatCPM:
        dierr = DiskFSCPM::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatMSDOS:
        dierr = DiskFSFAT::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        break;
    case kFormatRDOS33:
    case kFormatRDOS32:
    case kFormatRDOS3:
        dierr = DiskFSRDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery);
        if (newFormat != format)
            dierr = kDIErrFilesystemNotFound;   // found RDOS, but wrong flavor
        break;
    case kFormatGenericPhysicalOrd:
    case kFormatGenericProDOSOrd:
    case kFormatGenericDOSOrd:
    case kFormatGenericCPMOrd:
        /* no discussion possible, since there's no FS to validate */
        newFormat = format;
        newOrder = order;
        break;
    case kFormatUnknown:
        /* only valid in rare situations, e.g. CFFA CreatePlaceholder */
        newFormat = format;
        newOrder = order;
        break;
    default:
        dierr = kDIErrUnsupportedFSFmt;
        break;
    }

    if (dierr != kDIErrNone) {
        LOGI(" DI override failed");
        goto bail;
    }

    /*
     * We passed in "order" to TestFS.  If it came back with something
     * different, it means that it didn't like the new order value even
     * when "leniency" was granted.
     */
    if (newOrder != order) {
        dierr = kDIErrBadOrdering;
        goto bail;
    }

    fFormat = format;
    fOrder = newOrder;
    fFileSysOrder = CalcFSSectorOrder();

    LOGI(" DI override accepted");

bail:
    return dierr;
}

/*
 * Figure out the sector ordering for this filesystem, so we can decide
 * how the sectors need to be re-arranged when we're reading them.
 *
 * If the value returned by this function matches fOrder, then no swapping
 * will be done.
 *
 * NOTE: this table is redundant with some knowledge embedded in the
 * individual "TestFS" functions.
 */
DiskImg::SectorOrder DiskImg::CalcFSSectorOrder(void) const
{
    /* in the absence of information, just leave it alone */
    if (fFormat == kFormatUnknown || fOrder == kSectorOrderUnknown) {
        LOGI(" DI WARNING: FindSectorOrder but format not known");
        return fOrder;
    }

    assert(fOrder == kSectorOrderPhysical || fOrder == kSectorOrderCPM ||
        fOrder == kSectorOrderProDOS || fOrder == kSectorOrderDOS);

    switch (fFormat) {
    case kFormatGenericPhysicalOrd:
    case kFormatRDOS32:
    case kFormatRDOS3:
        return kSectorOrderPhysical;

    case kFormatGenericDOSOrd:
    case kFormatDOS33:
    case kFormatDOS32:
    case kFormatUNIDOS:
    case kFormatOzDOS:
    case kFormatGutenberg:
        return kSectorOrderDOS;

    case kFormatGenericCPMOrd:
    case kFormatCPM:
        return kSectorOrderCPM;

    case kFormatGenericProDOSOrd:
    case kFormatProDOS:
    case kFormatRDOS33:
    case kFormatPascal:
    case kFormatMacHFS:
    case kFormatMacMFS:
    case kFormatLisa:
    case kFormatMSDOS:
    case kFormatISO9660:
    case kFormatCFFA4:
    case kFormatCFFA8:
    case kFormatMacPart:
    case kFormatMicroDrive:
    case kFormatFocusDrive:
        return kSectorOrderProDOS;

    default:
        assert(false);
        return fOrder;
    }
}

/*
 * Based on the disk format, figure out if we should prefer blocks or
 * sectors when examining disk contents.
 */
bool DiskImg::ShowAsBlocks(void) const
{
    if (!fHasBlocks)
        return false;

    /* in the absence of information, assume sectors */
    if (fFormat == kFormatUnknown) {
        if (fOrder == kSectorOrderProDOS)
            return true;
        else
            return false;
    }

    switch (fFormat) {
    case kFormatGenericPhysicalOrd:
    case kFormatGenericDOSOrd:
    case kFormatDOS33:
    case kFormatDOS32:
    case kFormatRDOS3:
    case kFormatRDOS33:
    case kFormatUNIDOS:
    case kFormatOzDOS:
    case kFormatGutenberg:
        return false;

    case kFormatGenericProDOSOrd:
    case kFormatGenericCPMOrd:
    case kFormatProDOS:
    case kFormatPascal:
    case kFormatMacHFS:
    case kFormatMacMFS:
    case kFormatLisa:
    case kFormatCPM:
    case kFormatMSDOS:
    case kFormatISO9660:
    case kFormatCFFA4:
    case kFormatCFFA8:
    case kFormatMacPart:
    case kFormatMicroDrive:
    case kFormatFocusDrive:
        return true;

    default:
        assert(false);
        return false;
    }
}


/*
 * Format an image with the requested fileystem format.  This only works if
 * the matching DiskFS supports formatting of disks.
 */
DIError DiskImg::FormatImage(FSFormat format, const char* volName)
{
    DIError dierr = kDIErrNone;
    DiskFS* pDiskFS = NULL;
    FSFormat savedFormat;

    LOGI(" DI FormatImage '%s'", volName);

    /*
     * Open a temporary DiskFS for the requested format.  We do this via the
     * standard OpenAppropriate call, so we temporarily switch our format
     * out.  (We will eventually replace it, but we want to make sure that
     * local error handling works correctly, so we restore it for now.)
     */
    savedFormat = fFormat;
    fFormat = format;
    pDiskFS = OpenAppropriateDiskFS(false);
    fFormat = savedFormat;

    if (pDiskFS == NULL) {
        dierr = kDIErrUnsupportedFSFmt;
        goto bail;
    }

    dierr = pDiskFS->Format(this, volName);
    if (dierr != kDIErrNone)
        goto bail;

    LOGI("DI format successful");
    fFormat = format;

bail:
    delete pDiskFS;
    return dierr;
}

/*
 * Clear an image to zeros, usually done as a prelude to a higher-level format.
 *
 * BUG: this should also handle the track/sector case.
 *
 * HEY: this is awfully slow on large disks... should have some sort of
 * optimized path that just writes to the GFD or something.  Maybe even just
 * a "ZeroBlock" instead of "WriteBlock" so we can memset instead of memcpy?
 */
DIError DiskImg::ZeroImage(void)
{
    DIError dierr = kDIErrNone;
    uint8_t blkBuf[kBlockSize];
    long block;

    LOGI(" DI ZeroImage (%ld blocks)", GetNumBlocks());
    memset(blkBuf, 0, sizeof(blkBuf));

    for (block = 0; block < GetNumBlocks(); block++) {
        dierr = WriteBlock(block, blkBuf);
        if (dierr != kDIErrNone)
            break;
    }

    return dierr;
}


/*
 * Set the "scan progress" function.
 *
 * We want to use the same function for our sub-volumes too.
 */
void DiskImg::SetScanProgressCallback(ScanProgressCallback func, void* cookie)
{
    if (fpParentImg != NULL) {
        /* unexpected, but perfectly okay */
        DebugBreak();
    }

    fpScanProgressCallback = func;
    fScanProgressCookie = cookie;
    fScanCount = 0;
    fScanMsg[0] = '\0';
    fScanLastMsgWhen = time(NULL);
}

/*
 * Update the progress.  Call with a string at the start of a volume, then
 * call with a NULL pointer every time we add a file.
 */
bool DiskImg::UpdateScanProgress(const char* newStr)
{
    ScanProgressCallback func = fpScanProgressCallback;
    DiskImg* pImg = this;
    bool result = true;

    /* search up the tree to find a progress updater */
    while (func == NULL) {
        pImg = pImg->fpParentImg;
        if (pImg == NULL)
            return result;      // none defined, bail out
        func = pImg->fpScanProgressCallback;
    }

    time_t now = time(NULL);

    if (newStr == NULL) {
        fScanCount++;
        //if ((fScanCount % 100) == 0)
        if (fScanLastMsgWhen != now) {
            result = (*func)(fScanProgressCookie,
                        fScanMsg, fScanCount);
            fScanLastMsgWhen = now;
        }
    } else {
        fScanCount = 0;
        strncpy(fScanMsg, newStr, sizeof(fScanMsg));
        fScanMsg[sizeof(fScanMsg)-1] = '\0';
        result = (*func)(fScanProgressCookie, fScanMsg,
                    fScanCount);
        fScanLastMsgWhen = now;
    }

    return result;
}


/*
 * ==========================================================================
 *      Block/track/sector I/O
 * ==========================================================================
 */

/*
 * Handle sector order conversions.
 */
DIError DiskImg::CalcSectorAndOffset(long track, int sector, SectorOrder imageOrder,
    SectorOrder fsOrder, di_off_t* pOffset, int* pNewSector)
{
    if (!fHasSectors)
        return kDIErrUnsupportedAccess;

    /*
     * Sector order conversions.  No table is needed for Copy ][+ format,
     * which is equivalent to "physical".
     */
    static const int raw2dos[16] = {
        0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15
    };
    static const int dos2raw[16] = {
        0, 13, 11, 9, 7, 5, 3, 1, 14, 12, 10, 8, 6, 4, 2, 15
    };
    static const int raw2prodos[16] = {
        0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15
    };
    static const int prodos2raw[16] = {
        0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15
    };
    static const int raw2cpm[16] = {
        0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5
    };
    static const int cpm2raw[16] = {
        0, 3, 6, 9, 12, 15, 2, 5, 8, 11, 14, 1, 4, 7, 10, 13
    };

    if (track < 0 || track >= fNumTracks) {
        LOGI(" DI read invalid track %ld", track);
        return kDIErrInvalidTrack;
    }
    if (sector < 0 || sector >= fNumSectPerTrack) {
        LOGI(" DI read invalid sector %d", sector);
        return kDIErrInvalidSector;
    }

    di_off_t offset;
    int newSector = -1;

    /*
     * 16-sector disks write sectors in ascending order and then remap
     * them with a translation table.
     */
    if (fNumSectPerTrack == 16 || fNumSectPerTrack == 32) {
        if (fSectorPairing) {
            assert(fSectorPairOffset == 0 || fSectorPairOffset == 1);
            // this pushes "track" beyond fNumTracks
            track *= 2;
            if (sector >= 16) {
                track++;
                sector -= 16;
            }
            offset = track * fNumSectPerTrack * kSectorSize;

            sector = sector * 2 + fSectorPairOffset;
            if (sector >= 16) {
                offset += 16*kSectorSize;
                sector -= 16;
            }
        } else {
            offset = track * fNumSectPerTrack * kSectorSize;
            if (sector >= 16) {
                offset += 16*kSectorSize;
                sector -= 16;
            }
        }
        assert(sector >= 0 && sector < 16);

        /* convert request to "raw" sector number */
        switch (fsOrder) {
        case kSectorOrderProDOS:
            newSector = prodos2raw[sector];
            break;
        case kSectorOrderDOS:
            newSector = dos2raw[sector];
            break;
        case kSectorOrderCPM:
            newSector = cpm2raw[sector];
            break;
        case kSectorOrderPhysical:  // used for Copy ][+
            newSector = sector;
            break;
        case kSectorOrderUnknown:
            // should never happen; fall through to "default"
        default:
            assert(false);
            newSector = sector;
            break;
        }

        /* convert "raw" request to the image's ordering */
        switch (imageOrder) {
        case kSectorOrderProDOS:
            newSector = raw2prodos[newSector];
            break;
        case kSectorOrderDOS:
            newSector = raw2dos[newSector];
            break;
        case kSectorOrderCPM:
            newSector = raw2cpm[newSector];
            break;
        case kSectorOrderPhysical:
            //newSector = newSector;
            break;
        case kSectorOrderUnknown:
            // should never happen; fall through to "default"
        default:
            assert(false);
            //newSector = newSector;
            break;
        }

        if (imageOrder == fsOrder) {
            assert(sector == newSector);
        }

        offset += newSector * kSectorSize;
    } else if (fNumSectPerTrack == 13) {
        /* sector skew has no meaning, so assume no translation */
        offset = track * fNumSectPerTrack * kSectorSize;
        newSector = sector;
        offset += newSector * kSectorSize;
        if (imageOrder != fsOrder) {
            /* translation expected */
            LOGI("NOTE: CalcSectorAndOffset for nspt=13 with img=%d fs=%d",
                imageOrder, fsOrder);
        }
    } else {
        assert(false);      // should not be here

        /* try to do something reasonable */
        assert(imageOrder == fsOrder);
        offset = (di_off_t)track * fNumSectPerTrack * kSectorSize;
        offset += sector * kSectorSize;
    }

    *pOffset = offset;
    *pNewSector = newSector;
    return kDIErrNone;
}

/*
 * Determine whether an image uses a linear mapping.  This allows us to
 * optimize block reads & writes, very useful when dealing with logical
 * volumes under Windows (which also use 512-byte blocks).
 *
 * The "imageOrder" argument usually comes from fOrder, and "fsOrder"
 * comes from "fFileSysOrder".
 */
inline bool DiskImg::IsLinearBlocks(SectorOrder imageOrder, SectorOrder fsOrder)
{
    /*
     * Any time fOrder==fFileSysOrder, we know that we have a linear
     * mapping.  This holds true for reading ProDOS blocks from a ".po"
     * file or reading DOS sectors from a ".do" file.
     */
    return (IsSectorFormat(fPhysical) && fHasBlocks &&
            imageOrder == fsOrder);
}

/*
 * Read the specified track and sector, adjusting for sector ordering as
 * appropriate.
 *
 * Copies 256 bytes into "*buf".
 *
 * Returns 0 on success, nonzero on failure.
 */
DIError DiskImg::ReadTrackSectorSwapped(long track, int sector, void* buf,
    SectorOrder imageOrder, SectorOrder fsOrder)
{
    DIError dierr;
    di_off_t offset;
    int newSector = -1;

    if (buf == NULL)
        return kDIErrInvalidArg;

#if 0   // Pre-d13
    if (fNumSectPerTrack == 13) {
        /* no sector skewing possible for 13-sector disks */
        assert(fHasNibbles);

        return ReadNibbleSector(track, sector, buf, fpNibbleDescr);
    }
#endif

    dierr = CalcSectorAndOffset(track, sector, imageOrder, fsOrder,
                &offset, &newSector);
    if (dierr != kDIErrNone)
        return dierr;

    if (IsSectorFormat(fPhysical)) {
        assert(offset+kSectorSize <= fLength);

        //LOGI("  DI t=%d s=%d", track,
        //  (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize);

        dierr = CopyBytesOut(buf, offset, kSectorSize);
    } else if (IsNibbleFormat(fPhysical)) {
        if (imageOrder != kSectorOrderPhysical) {
            LOGI("  NOTE: nibble imageOrder is %d (expected %d)",
                imageOrder, kSectorOrderPhysical);
        }
        dierr = ReadNibbleSector(track, newSector, buf, fpNibbleDescr);
    } else {
        assert(false);
        dierr = kDIErrInternal;
    }

    return dierr;
}

/*
 * Write the specified track and sector, adjusting for sector ordering as
 * appropriate.
 *
 * Copies 256 bytes out of "buf".
 *
 * Returns 0 on success, nonzero on failure.
 */
DIError DiskImg::WriteTrackSector(long track, int sector, const void* buf)
{
    DIError dierr;
    di_off_t offset;
    int newSector = -1;

    if (buf == NULL)
        return kDIErrInvalidArg;
    if (fReadOnly)
        return kDIErrAccessDenied;

#if 0   // Pre-d13
    if (fNumSectPerTrack == 13) {
        /* no sector skewing possible for 13-sector disks */
        assert(fHasNibbles);

        return WriteNibbleSector(track, sector, buf, fpNibbleDescr);
    }
#endif

    dierr = CalcSectorAndOffset(track, sector, fOrder, fFileSysOrder,
                &offset, &newSector);
    if (dierr != kDIErrNone)
        return dierr;

    if (IsSectorFormat(fPhysical)) {
        assert(offset+kSectorSize <= fLength);

        //LOGI("  DI t=%d s=%d", track,
        //  (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize);

        dierr = CopyBytesIn(buf, offset, kSectorSize);
    } else if (IsNibbleFormat(fPhysical)) {
        if (fOrder != kSectorOrderPhysical) {
            LOGI("  NOTE: nibble fOrder is %d (expected %d)",
                fOrder, kSectorOrderPhysical);
        }
        dierr = WriteNibbleSector(track, newSector, buf, fpNibbleDescr);
    } else {
        assert(false);
        dierr = kDIErrInternal;
    }

    return dierr;
}

/*
 * Read a 512-byte block.
 *
 * Copies 512 bytes into "*buf".
 */
DIError DiskImg::ReadBlockSwapped(long block, void* buf, SectorOrder imageOrder,
    SectorOrder fsOrder)
{
    if (!fHasBlocks)
        return kDIErrUnsupportedAccess;
    if (block < 0 || block >= fNumBlocks)
        return kDIErrInvalidBlock;
    if (buf == NULL)
        return kDIErrInvalidArg;

    DIError dierr;
    long track, blkInTrk;

    /* if we have a bad block map, check it */
    if (CheckForBadBlocks(block, 1)) {
        dierr = kDIErrReadFailed;
        goto bail;
    }

    if (fHasSectors && !IsLinearBlocks(imageOrder, fsOrder)) {
        /* run it through the t/s call so we handle DOS ordering */
        track = block / (fNumSectPerTrack/2);
        blkInTrk = block - (track * (fNumSectPerTrack/2));
        dierr = ReadTrackSectorSwapped(track, blkInTrk*2, buf,
                    imageOrder, fsOrder);
        if (dierr != kDIErrNone)
            return dierr;
        dierr = ReadTrackSectorSwapped(track, blkInTrk*2+1,
                    (char*)buf+kSectorSize, imageOrder, fsOrder);
    } else if (fHasBlocks) {
        /* no sectors, so no swapping; must be linear blocks */
        if (imageOrder != fsOrder) {
            LOGI(" DI NOTE: ReadBlockSwapped on non-sector (%d/%d)",
                imageOrder, fsOrder);
        }
        dierr = CopyBytesOut(buf, (di_off_t) block * kBlockSize, kBlockSize);
    } else {
        assert(false);
        dierr = kDIErrInternal;
    }

bail:
    return dierr;
}

/*
 * Read multiple blocks.
 *
 * IMPORTANT: this returns immediately when a read fails.  The buffer will
 * probably not contain data from all readable sectors.  The application is
 * expected to retry the blocks individually.
 */
DIError DiskImg::ReadBlocks(long startBlock, int numBlocks, void* buf)
{
    DIError dierr = kDIErrNone;

    assert(fHasBlocks);
    assert(startBlock >= 0);
    assert(numBlocks > 0);
    assert(buf != NULL);

    if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) {
        assert(false);
        return kDIErrInvalidArg;
    }

    /* if we have a bad block map, check it */
    if (CheckForBadBlocks(startBlock, numBlocks)) {
        dierr = kDIErrReadFailed;
        goto bail;
    }

    if (!IsLinearBlocks(fOrder, fFileSysOrder)) {
        /*
         * This isn't a collection of linear blocks, so we need to read it one
         * block at a time with sector swapping.  This almost certainly means
         * that we're not reading from physical media, so performance shouldn't
         * be an issue.
         */
        if (startBlock == 0) {
            LOGI(" ReadBlocks: nonlinear, not trying");
        }
        while (numBlocks--) {
            dierr = ReadBlock(startBlock, buf);
            if (dierr != kDIErrNone)
                goto bail;
            startBlock++;
            buf = (uint8_t*)buf + kBlockSize;
        }
    } else {
        if (startBlock == 0) {
            LOGI(" ReadBlocks: doing big linear reads");
        }
        dierr = CopyBytesOut(buf,
                    (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize);
    }

bail:
    return dierr;
}

/*
 * Check to see if any blocks in a range of blocks show up in the bad
 * block map.  This is primarily useful for 3.5" disk images converted
 * from nibble images, because we convert them directly to "cooked"
 * 512-byte blocks.
 *
 * Returns "true" if we found bad blocks, "false" if not.
 */
bool DiskImg::CheckForBadBlocks(long startBlock, int numBlocks)
{
    int i;

    if (fpBadBlockMap == NULL)
        return false;

    for (i = startBlock; i < startBlock+numBlocks; i++) {
        if (fpBadBlockMap->IsSet(i))
            return true;
    }
    return false;
}

/*
 * Write a block of data to a DiskImg.
 *
 * Returns immediately when a block write fails.  Does not try to write all
 * blocks before returning failure.
 */
DIError DiskImg::WriteBlock(long block, const void* buf)
{
    if (!fHasBlocks)
        return kDIErrUnsupportedAccess;
    if (block < 0 || block >= fNumBlocks)
        return kDIErrInvalidBlock;
    if (buf == NULL)
        return kDIErrInvalidArg;
    if (fReadOnly)
        return kDIErrAccessDenied;

    DIError dierr;
    long track, blkInTrk;

    if (fHasSectors && !IsLinearBlocks(fOrder, fFileSysOrder)) {
        /* run it through the t/s call so we handle DOS ordering */
        track = block / (fNumSectPerTrack/2);
        blkInTrk = block - (track * (fNumSectPerTrack/2));
        dierr = WriteTrackSector(track, blkInTrk*2, buf);
        if (dierr != kDIErrNone)
            return dierr;
        dierr = WriteTrackSector(track, blkInTrk*2+1, (char*)buf+kSectorSize);
    } else if (fHasBlocks) {
        /* no sectors, so no swapping; must be linear blocks */
        if (fOrder != fFileSysOrder) {
            LOGI(" DI NOTE: WriteBlock on non-sector (%d/%d)",
                fOrder, fFileSysOrder);
        }
        dierr = CopyBytesIn(buf, (di_off_t)block * kBlockSize, kBlockSize);
    } else {
        assert(false);
        dierr = kDIErrInternal;
    }
    return dierr;
}

/*
 * Write multiple blocks.
 */
DIError DiskImg::WriteBlocks(long startBlock, int numBlocks, const void* buf)
{
    DIError dierr = kDIErrNone;

    assert(fHasBlocks);
    assert(startBlock >= 0);
    assert(numBlocks > 0);
    assert(buf != NULL);

    if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) {
        assert(false);
        return kDIErrInvalidArg;
    }

    if (!IsLinearBlocks(fOrder, fFileSysOrder)) {
        /*
         * This isn't a collection of linear blocks, so we need to write it
         * one block at a time with sector swapping.  This almost certainly
         * means that we're not reading from physical media, so performance
         * shouldn't be an issue.
         */
        if (startBlock == 0) {
            LOGI(" WriteBlocks: nonlinear, not trying");
        }
        while (numBlocks--) {
            dierr = WriteBlock(startBlock, buf);
            if (dierr != kDIErrNone)
                goto bail;
            startBlock++;
            buf = (uint8_t*)buf + kBlockSize;
        }
    } else {
        if (startBlock == 0) {
            LOGI(" WriteBlocks: doing big linear writes");
        }
        dierr = CopyBytesIn(buf,
                    (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize);
    }

bail:
    return dierr;
}


/*
 * Copy a chunk of bytes out of the disk image.
 *
 * (This is the lowest-level read routine in this class.)
 */
DIError DiskImg::CopyBytesOut(void* buf, di_off_t offset, int size) const
{
    DIError dierr;

    dierr = fpDataGFD->Seek(offset, kSeekSet);
    if (dierr != kDIErrNone) {
        LOGI(" DI seek off=%ld failed (err=%d)", (long) offset, dierr);
        return dierr;
    }

    dierr = fpDataGFD->Read(buf, size);
    if (dierr != kDIErrNone) {
        LOGI(" DI read off=%ld size=%d failed (err=%d)",
            (long) offset, size, dierr);
        return dierr;
    }

    return kDIErrNone;
}

/*
 * Copy a chunk of bytes into the disk image.
 *
 * Sets the "dirty" flag.
 *
 * (This is the lowest-level write routine in DiskImg.)
 */
DIError DiskImg::CopyBytesIn(const void* buf, di_off_t offset, int size)
{
    DIError dierr;

    if (fReadOnly) {
        DebugBreak();
        return kDIErrAccessDenied;
    }
    assert(fpDataGFD != NULL);   // somebody closed the image?

    dierr = fpDataGFD->Seek(offset, kSeekSet);
    if (dierr != kDIErrNone) {
        LOGI(" DI seek off=%ld failed (err=%d)", (long) offset, dierr);
        return dierr;
    }

    dierr = fpDataGFD->Write(buf, size);
    if (dierr != kDIErrNone) {
        LOGI(" DI write off=%ld size=%d failed (err=%d)",
            (long) offset, size, dierr);
        return dierr;
    }

    /* set the dirty flag here and everywhere above */
    DiskImg* pImg = this;
    while (pImg != NULL) {
        pImg->fDirty = true;
        pImg = pImg->fpParentImg;
    }

    return kDIErrNone;
}


/*
 * ===========================================================================
 *      Image creation
 * ===========================================================================
 */

/*
 * Create a disk image with the specified parameters.
 *
 * "storageName" and "pNibbleDescr" may be NULL.
 */
DIError DiskImg::CreateImage(const char* pathName, const char* storageName,
    OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical,
    const NibbleDescr* pNibbleDescr, SectorOrder order,
    FSFormat format, long numBlocks, bool skipFormat)
{
    assert(fpDataGFD == NULL);       // should not be open already!

    if (numBlocks <= 0) {
        LOGI("ERROR: bad numBlocks %ld", numBlocks);
        assert(false);
        return kDIErrInvalidCreateReq;
    }

    fOuterFormat = outerFormat;
    fFileFormat = fileFormat;
    fPhysical = physical;
    SetCustomNibbleDescr(pNibbleDescr);
    fOrder = order;
    fFormat = format;

    fNumBlocks = numBlocks;
    fHasBlocks = true;

    return CreateImageCommon(pathName, storageName, skipFormat);
}

DIError DiskImg::CreateImage(const char* pathName, const char* storageName,
    OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical,
    const NibbleDescr* pNibbleDescr, SectorOrder order,
    FSFormat format, long numTracks, long numSectPerTrack, bool skipFormat)
{
    assert(fpDataGFD == NULL);       // should not be open already!

    if (numTracks <= 0 || numSectPerTrack == 0) {
        LOGI("ERROR: bad tracks/sectors %ld/%ld", numTracks, numSectPerTrack);
        assert(false);
        return kDIErrInvalidCreateReq;
    }

    fOuterFormat = outerFormat;
    fFileFormat = fileFormat;
    fPhysical = physical;
    SetCustomNibbleDescr(pNibbleDescr);
    fOrder = order;
    fFormat = format;

    fNumTracks = numTracks;
    fNumSectPerTrack = numSectPerTrack;
    fHasSectors = true;
    if (numSectPerTrack < 0) {
        /* nibble image with non-standard formatting */
        if (!IsNibbleFormat(fPhysical)) {
            LOGI("Whoa: expected nibble format here");
            assert(false);
            return kDIErrInvalidCreateReq;
        }
        LOGI("Sector image w/o sectors, switching to nibble mode");
        fHasNibbles = true;
        fHasSectors = false;
        fpNibbleDescr = NULL;
    }

    return CreateImageCommon(pathName, storageName, skipFormat);
}

/*
 * Do the actual disk image creation.
 */
DIError DiskImg::CreateImageCommon(const char* pathName, const char* storageName,
    bool skipFormat)
{
    DIError dierr;

    /*
     * Step 1: figure out fHasBlocks/fHasSectors/fHasNibbles and any
     * other misc fields.
     *
     * If the disk is a nibble image expected to have a particular
     * volume number, it should have already been set by the application.
     */
    if (fHasBlocks) {
        if ((fNumBlocks % 8) == 0) {
            fHasSectors = true;
            fNumSectPerTrack = 16;
            fNumTracks = fNumBlocks / 8;
        } else {
            LOGI("NOTE: sector access to new image not possible");
        }
    } else if (fHasSectors) {
        if ((fNumSectPerTrack & 0x01) == 0) {
            fHasBlocks = true;
            fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2;
        } else {
            LOGI("NOTE: block access to new image not possible");
        }
    }
    if (fHasSectors && fPhysical != kPhysicalFormatSectors)
        fHasNibbles = true;
    assert(fHasBlocks || fHasSectors || fHasNibbles);

    fFileSysOrder = CalcFSSectorOrder();
    fReadOnly = false;
    fDirty = true;

    /*
     * Step 2: check for invalid arguments and bad combinations.
     */
    dierr = ValidateCreateFormat();
    if (dierr != kDIErrNone) {
        LOGE("ERROR: CIC arg validation failed, bailing");
        goto bail;
    }

    /*
     * Step 3: create the destination file.  Put this into fpWrapperGFD
     * or fpOuterGFD.
     *
     * The file must not already exist.
     *
     * THOUGHT: should allow creation of an in-memory disk image.  This won't
     * work for NuFX, but will work for pretty much everything else.
     */
    LOGI(" CIC: creating '%s'", pathName);
    int fd;
    fd = open(pathName, O_CREAT | O_EXCL, 0644);
    if (fd < 0) {
        dierr = (DIError) errno;
        LOGE("ERROR: unable to create file '%s' (errno=%d)",
            pathName, dierr);
        goto bail;
    }
    close(fd);

    GFDFile* pGFDFile;
    pGFDFile = new GFDFile;

    dierr = pGFDFile->Open(pathName, false);
    if (dierr != kDIErrNone) {
        delete pGFDFile;
        goto bail;
    }

    if (fOuterFormat == kOuterFormatNone)
        fpWrapperGFD = pGFDFile;
    else
        fpOuterGFD = pGFDFile;
    pGFDFile = NULL;

    /*
     * Step 4: if we have an outer GFD and therefore don't currently have
     * an fpWrapperGFD, create an expandable memory buffer to use.
     *
     * We want to take a guess at how big the image will be, so compute
     * fLength now.
     *
     * Create an OuterWrapper as needed.
     */
    if (IsSectorFormat(fPhysical)) {
        if (fHasBlocks)
            fLength = (di_off_t) GetNumBlocks() * kBlockSize;
        else
            fLength = (di_off_t) GetNumTracks() * GetNumSectPerTrack() * kSectorSize;
    } else {
        assert(IsNibbleFormat(fPhysical));
        fLength = GetNumTracks() * GetNibbleTrackAllocLength();
    }
    assert(fLength > 0);

    if (fpWrapperGFD == NULL) {
        /* shift GFDs and create a new memory GFD, pre-sized */
        GFDBuffer* pGFDBuffer = new GFDBuffer;

        /* use fLength as a starting point for buffer size; this may expand */
        dierr = pGFDBuffer->Open(NULL, fLength, true, true, false);
        if (dierr != kDIErrNone) {
            delete pGFDBuffer;
            goto bail;
        }

        fpWrapperGFD = pGFDBuffer;
        pGFDBuffer = NULL;
    }

    /* create an fpOuterWrapper struct */
    switch (fOuterFormat) {
    case kOuterFormatNone:
        break;
    case kOuterFormatGzip:
        fpOuterWrapper = new OuterGzip;
        if (fpOuterWrapper == NULL) {
            dierr = kDIErrMalloc;
            goto bail;
        }
        break;
    case kOuterFormatZip:
        fpOuterWrapper = new OuterZip;
        if (fpOuterWrapper == NULL) {
            dierr = kDIErrMalloc;
            goto bail;
        }
        break;
    default:
        assert(false);
        dierr = kDIErrInternal;
        goto bail;
    }

    /*
     * Step 5: tell the ImageWrapper to write itself into the GFD, passing
     * in the blank memory buffer.
     *
     *  - Unadorned formats copy from memory buffer to fpWrapperGFD on disk.
     *    (With gz, fpWrapperGFD is actually a memory buffer.)  fpDataGFD
     *    becomes an offset into the file.
     *  - 2MG writes header into GFD and follows it with all data; DC42
     *    and Sim2e do similar things.
     *  - NuFX reopens pathName as SHK file (fpWrapperGFD must point to a
     *    file) and accesses the archive through an fpArchive.  fpDataGFD
     *    is created as a memory buffer and the blank image is copied in.
     *  - DDD leaves fpWrapperGFD alone and copies the blank image into a
     *    new buffer for fpDataGFD.
     *
     * Sets fWrappedLength when possible, determined from fPhysical and
     * either fNumBlocks or fNumTracks.  Creates fpDataGFD, often as a
     * GFDGFD offset into fpWrapperGFD.
     */
    switch (fFileFormat) {
    case kFileFormat2MG:
        fpImageWrapper = new Wrapper2MG();
        break;
    case kFileFormatDiskCopy42:
        fpImageWrapper = new WrapperDiskCopy42();
        fpImageWrapper->SetStorageName(storageName);
        break;
    case kFileFormatSim2eHDV:
        fpImageWrapper = new WrapperSim2eHDV();
        break;
    case kFileFormatTrackStar:
        fpImageWrapper = new WrapperTrackStar();
        fpImageWrapper->SetStorageName(storageName);
        break;
    case kFileFormatFDI:
        fpImageWrapper = new WrapperFDI();
        break;
    case kFileFormatNuFX:
        fpImageWrapper = new WrapperNuFX();
        fpImageWrapper->SetStorageName(storageName);
        ((WrapperNuFX*)fpImageWrapper)->SetCompressType(
                                        (NuThreadFormat) fNuFXCompressType);
        break;
    case kFileFormatDDD:
        fpImageWrapper = new WrapperDDD();
        break;
    case kFileFormatUnadorned:
        if (IsSectorFormat(fPhysical))
            fpImageWrapper = new WrapperUnadornedSector();
        else if (IsNibbleFormat(fPhysical))
            fpImageWrapper = new WrapperUnadornedNibble();
        else {
            assert(false);
        }
        break;
    default:
        assert(fpImageWrapper == NULL);
        break;
    }

    if (fpImageWrapper == NULL) {
        LOGW(" DI couldn't figure out the file format");
        dierr = kDIErrUnrecognizedFileFmt;
        goto bail;
    }

    /* create the wrapper, write the header, and create fpDataGFD */
    assert(fpDataGFD == NULL);
    dierr = fpImageWrapper->Create(fLength, fPhysical, fOrder,
                fDOSVolumeNum, fpWrapperGFD, &fWrappedLength, &fpDataGFD);
    if (dierr != kDIErrNone) {
        LOGE("ImageWrapper Create failed, err=%d", dierr);
        goto bail;
    }
    assert(fpDataGFD != NULL);

    /*
     * Step 6: "format" fpDataGFD.
     *
     * Note we don't specify an ordering to the "create blank" functions.
     * Either it's sectors, in which case it's all zeroes, or it's nibbles,
     * in which case it's always in physical order.
     *
     * If we're formatting for nibbles, and the application hasn't specified
     * a disk volume number, use the default (254).
     */
    if (fPhysical == kPhysicalFormatSectors)
        dierr = FormatSectors(fpDataGFD, skipFormat);   // zero out the image
    else {
        assert(!skipFormat);        // don't skip low-level nibble formatting!
        if (fDOSVolumeNum == kVolumeNumNotSet) {
            fDOSVolumeNum = kDefaultNibbleVolumeNum;
            LOGD("    Using default nibble volume num");
        }

        dierr = FormatNibbles(fpDataGFD);   // write basic nibble stuff
    }


    /*
     * We're done!
     *
     * Quick sanity check...
     */
    if (fOuterFormat != kOuterFormatNone) {
        assert(fpOuterGFD != NULL);
        assert(fpWrapperGFD != NULL);
        assert(fpDataGFD != NULL);
    }

bail:
    return dierr;
}

/*
 * Check that the requested format is one we can create.
 *
 * We don't allow .SDK.GZ or 6384-byte nibble 2MG.  2MG sector images
 * must be in DOS or ProDOS order.
 *
 * Only "generic" FS formats may be used.  The application may choose
 * to call AnalyzeImage later on to set the actual FS once data has
 * been written.
 */
DIError DiskImg::ValidateCreateFormat(void) const
{
    /*
     * Check for invalid arguments.
     */
    if (fHasBlocks && fNumBlocks >= 4194304) {  // 2GB or larger?
        if (fFileFormat != kFileFormatUnadorned) {
            LOGW("CreateImage: images >= 2GB can only be unadorned");
            return kDIErrInvalidCreateReq;
        }
    }
    if (fOuterFormat == kOuterFormatUnknown ||
        fFileFormat == kFileFormatUnknown ||
        fPhysical == kPhysicalFormatUnknown ||
        fOrder == kSectorOrderUnknown ||
        fFormat == kFormatUnknown)
    {
        LOGW("CreateImage: ambiguous format");
        return kDIErrInvalidCreateReq;
    }
    if (fOuterFormat != kOuterFormatNone &&
        fOuterFormat != kOuterFormatGzip &&
        fOuterFormat != kOuterFormatZip)
    {
        LOGW("CreateImage: unsupported outer format %d", fOuterFormat);
        return kDIErrInvalidCreateReq;
    }
    if (fFileFormat != kFileFormatUnadorned &&
        fFileFormat != kFileFormat2MG &&
        fFileFormat != kFileFormatDiskCopy42 &&
        fFileFormat != kFileFormatSim2eHDV &&
        fFileFormat != kFileFormatTrackStar &&
        fFileFormat != kFileFormatFDI &&
        fFileFormat != kFileFormatNuFX &&
        fFileFormat != kFileFormatDDD)
    {
        LOGW("CreateImage: unsupported file format %d", fFileFormat);
        return kDIErrInvalidCreateReq;
    }
    if (fFormat != kFormatGenericPhysicalOrd &&
        fFormat != kFormatGenericProDOSOrd &&
        fFormat != kFormatGenericDOSOrd &&
        fFormat != kFormatGenericCPMOrd)
    {
        LOGW("CreateImage: may only use 'generic' formats");
        return kDIErrInvalidCreateReq;
    }

    /*
     * Check for invalid combinations.
     */
    if (fPhysical != kPhysicalFormatSectors) {
        if (fOrder != kSectorOrderPhysical) {
            LOGW("CreateImage: nibble images are always 'physical' order");
            return kDIErrInvalidCreateReq;
        }

        if (GetHasSectors() == false && GetHasNibbles() == false) {
            LOGW("CreateImage: must set hasSectors(%d) or hasNibbles(%d)",
                GetHasSectors(), GetHasNibbles());
            return kDIErrInvalidCreateReq;
        }

        if (fpNibbleDescr == NULL && GetNumSectPerTrack() > 0) {
            LOGW("CreateImage: must provide NibbleDescr for non-sector");
            return kDIErrInvalidCreateReq;
        }

        if (fpNibbleDescr != NULL &&
            fpNibbleDescr->numSectors != GetNumSectPerTrack())
        {
            LOGW("CreateImage: ?? nd->numSectors=%d, GetNumSectPerTrack=%d",
                fpNibbleDescr->numSectors, GetNumSectPerTrack());
            return kDIErrInvalidCreateReq;
        }

        if (fpNibbleDescr != NULL && (
            (fpNibbleDescr->numSectors == 13 &&
             fpNibbleDescr->encoding != kNibbleEnc53) ||
            (fpNibbleDescr->numSectors == 16 &&
             fpNibbleDescr->encoding != kNibbleEnc62))
            )
        {
            LOGW("CreateImage: sector count/encoding mismatch");
            return kDIErrInvalidCreateReq;
        }

        if (GetNumTracks() != kTrackCount525 &&
            !(GetNumTracks() == 40 && fFileFormat == kFileFormatTrackStar))
        {
            LOGW("CreateImage: unexpected track count %ld", GetNumTracks());
            return kDIErrInvalidCreateReq;
        }
    }
    if (fFileFormat == kFileFormat2MG) {
        if (fPhysical != kPhysicalFormatSectors &&
            fPhysical != kPhysicalFormatNib525_6656)
        {
            LOGW("CreateImage: 2MG can't handle physical %d", fPhysical);
            return kDIErrInvalidCreateReq;
        }

        if (fPhysical == kPhysicalFormatSectors &&
                (fOrder != kSectorOrderProDOS &&
                fOrder != kSectorOrderDOS))
        {
            LOGW("CreateImage: 2MG requires DOS or ProDOS ordering");
            return kDIErrInvalidCreateReq;
        }
    }
    if (fFileFormat == kFileFormatNuFX) {
        if (fOuterFormat != kOuterFormatNone) {
            LOGW("CreateImage: can't mix NuFX and outer wrapper");
            return kDIErrInvalidCreateReq;
        }
        if (fPhysical != kPhysicalFormatSectors) {
            LOGW("CreateImage: NuFX physical must be sectors");
            return kDIErrInvalidCreateReq;
        }
        if (fOrder != kSectorOrderProDOS) {
            LOGW("CreateImage: NuFX is always ProDOS-order");
            return kDIErrInvalidCreateReq;
        }
    }
    if (fFileFormat == kFileFormatDiskCopy42) {
        if (fPhysical != kPhysicalFormatSectors) {
            LOGW("CreateImage: DC42 physical must be sectors");
            return kDIErrInvalidCreateReq;
        }
        if ((GetHasBlocks() && GetNumBlocks() != 1600) ||
            (GetHasSectors() &&
                (GetNumTracks() != 200 || GetNumSectPerTrack() != 16)))
        {
            LOGW("CreateImage: DC42 only for 800K disks");
            return kDIErrInvalidCreateReq;
        }
        if (fOrder != kSectorOrderProDOS &&
            fOrder != kSectorOrderDOS)      // used for UNIDOS disks??
        {
            LOGW("CreateImage: DC42 is always ProDOS or DOS");
            return kDIErrInvalidCreateReq;
        }
    }
    if (fFileFormat == kFileFormatSim2eHDV) {
        if (fPhysical != kPhysicalFormatSectors) {
            LOGW("CreateImage: Sim2eHDV physical must be sectors");
            return kDIErrInvalidCreateReq;
        }
        if (fOrder != kSectorOrderProDOS) {
            LOGW("CreateImage: Sim2eHDV is always ProDOS-order");
            return kDIErrInvalidCreateReq;
        }
    }
    if (fFileFormat == kFileFormatTrackStar) {
        if (fPhysical != kPhysicalFormatNib525_Var) {
            LOGW("CreateImage: TrackStar physical must be var-nibbles");
            return kDIErrInvalidCreateReq;
        }
    }
    if (fFileFormat == kFileFormatFDI) {
        if (fPhysical != kPhysicalFormatNib525_Var) {
            LOGW("CreateImage: FDI physical must be var-nibbles");
            return kDIErrInvalidCreateReq;
        }
    }
    if (fFileFormat == kFileFormatDDD) {
        if (fPhysical != kPhysicalFormatSectors) {
            LOGW("CreateImage: DDD physical must be sectors");
            return kDIErrInvalidCreateReq;
        }
        if (fOrder != kSectorOrderDOS) {
            LOGW("CreateImage: DDD is always DOS-order");
            return kDIErrInvalidCreateReq;
        }
        if (!GetHasSectors() || GetNumTracks() != 35 ||
            GetNumSectPerTrack() != 16)
        {
            LOGW("CreateImage: DDD is only for 16-sector 35-track disks");
            return kDIErrInvalidCreateReq;
        }
    }

    return kDIErrNone;
}

/*
 * Create a blank image for physical=="sectors".
 *
 * fLength must be a multiple of 256.
 *
 * If "quickFormat" is set, only the very last sector is written (to set
 * the EOF on the file).
 */
DIError DiskImg::FormatSectors(GenericFD* pGFD, bool quickFormat) const
{
    DIError dierr = kDIErrNone;
    char sctBuf[kSectorSize];
    di_off_t length;

    assert(fLength > 0 && (fLength & 0xff) == 0);

    //if (!(fLength & 0x01))
    //  return FormatBlocks(pGFD);

    memset(sctBuf, 0, sizeof(sctBuf));
    pGFD->Rewind();

    if (quickFormat) {
        dierr = pGFD->Seek(fLength - sizeof(sctBuf), kSeekSet);
        if (dierr != kDIErrNone) {
            LOGI(" FormatSectors: GFD seek %ld failed (err=%d)",
                (long) fLength - sizeof(sctBuf), dierr);
            goto bail;
        }
        dierr = pGFD->Write(sctBuf, sizeof(sctBuf), NULL);
        if (dierr != kDIErrNone) {
            LOGI(" FormatSectors: GFD quick write failed (err=%d)", dierr);
            goto bail;
        }
    } else {
        for (length = fLength ; length > 0; length -= sizeof(sctBuf)) {
            dierr = pGFD->Write(sctBuf, sizeof(sctBuf), NULL);
            if (dierr != kDIErrNone) {
                LOGI(" FormatSectors: GFD write failed (err=%d)", dierr);
                goto bail;
            }
        }
        assert(length == 0);
    }


bail:
    return dierr;
}

#if 0   // didn't help
/*
 * Create a blank image for physical=="sectors".  This is called from
 * FormatSectors when it looks like we're formatting entire blocks.
 */
DIError
DiskImg::FormatBlocks(GenericFD* pGFD) const
{
    DIError dierr;
    char blkBuf[kBlockSize];
    long length;
    time_t start, end;

    assert(fLength > 0 && (fLength & 0x1ff) == 0);

    start = time(NULL);

    memset(blkBuf, 0, sizeof(blkBuf));
    pGFD->Rewind();

    for (length = fLength ; length > 0; length -= sizeof(blkBuf)) {
        dierr = pGFD->Write(blkBuf, sizeof(blkBuf), NULL);
        if (dierr != kDIErrNone) {
            LOGI(" FormatBlocks: GFD write failed (err=%d)", dierr);
            return dierr;
        }
    }
    assert(length == 0);

    end = time(NULL);
    LOGI("FormatBlocks complete, time=%ld", end - start);

    return kDIErrNone;
}
#endif


/*
 * ===========================================================================
 *      Utility functions
 * ===========================================================================
 */

/*
 * Add a note to this disk image.
 *
 * This is how we communicate cautions and warnings to the user.  Use
 * linefeeds ('\n') to indicate line breaks.
 *
 * The maximum length of a single note is set by the size of "buf".
 */
void DiskImg::AddNote(NoteType type, const char* fmt, ...)
{
    char buf[512];
    char* cp = buf;
    int maxLen = sizeof(buf);
    va_list args;
    int len;

    /*
     * Prepend a string that highlights the note.
     */
    switch (type) {
    case kNoteWarning:
        strcpy(cp, "- WARNING: ");
        break;
    default:
        strcpy(cp, "- ");
        break;
    }
    len = strlen(cp);
    cp += len;
    maxLen -= len;

    /*
     * Add the note.
     */
    va_start(args, fmt);
#if defined(HAVE_VSNPRINTF)
    (void) vsnprintf(cp, maxLen, fmt, args);
#elif defined(HAVE__VSNPRINTF)
    (void) _vsnprintf(cp, maxLen, fmt, args);
#else
# error "hosed"
#endif
    va_end(args);

    buf[sizeof(buf)-2] = '\0';      // leave room for additional '\n'
    len = strlen(buf);
    if (len > 0 && buf[len-1] != '\n') {
        buf[len] = '\n';
        buf[len+1] = '\0';
        len++;
    }

    LOGD("+++ adding note '%s'", buf);

    if (fNotes == NULL) {
        fNotes = new char[len +1];
        if (fNotes == NULL) {
            LOGW("Unable to create notes[%d]", len+1);
            assert(false);
            return;
        }
        strcpy(fNotes, buf);
    } else {
        int existingLen = strlen(fNotes);
        char* newNotes = new char[existingLen + len +1];
        if (newNotes == NULL) {
            LOGW("Unable to create newNotes[%d]", existingLen+len+1);
            assert(false);
            return;
        }
        strcpy(newNotes, fNotes);
        strcpy(newNotes + existingLen, buf);
        delete[] fNotes;
        fNotes = newNotes;
    }
}

/*
 * Return a string with the notes in it.
 */
const char* DiskImg::GetNotes(void) const
{
    if (fNotes == NULL)
        return "";
    else
        return fNotes;
}


/*
 * Get length and offset of tracks in a nibble image.  This is necessary
 * because of formats with variable-length tracks (e.g. TrackStar).
 */
int DiskImg::GetNibbleTrackLength(long track) const
{
    assert(fpImageWrapper != NULL);
    return fpImageWrapper->GetNibbleTrackLength(fPhysical, track);
}

int DiskImg::GetNibbleTrackOffset(long track) const
{
    assert(fpImageWrapper != NULL);
    return fpImageWrapper->GetNibbleTrackOffset(fPhysical, track);
}


/*
 * Return a new object with the appropriate DiskFS sub-class.
 *
 * If the image hasn't been analyzed, or was analyzed to no avail, "NULL"
 * is returned unless "allowUnknown" is set to "true".  In that case, a
 * DiskFSUnknown is returned.
 *
 * This doesn't inspire the DiskFS to do any processing, just creates the
 * new object.
 */
DiskFS* DiskImg::OpenAppropriateDiskFS(bool allowUnknown)
{
    DiskFS* pDiskFS = NULL;

    /*
     * Create an appropriate DiskFS object.
     */
    switch (GetFSFormat()) {
    case DiskImg::kFormatDOS33:
    case DiskImg::kFormatDOS32:
        pDiskFS = new DiskFSDOS33();
        break;
    case DiskImg::kFormatProDOS:
        pDiskFS = new DiskFSProDOS();
        break;
    case DiskImg::kFormatPascal:
        pDiskFS = new DiskFSPascal();
        break;
    case DiskImg::kFormatMacHFS:
        pDiskFS = new DiskFSHFS();
        break;
    case DiskImg::kFormatUNIDOS:
        pDiskFS = new DiskFSUNIDOS();
        break;
    case DiskImg::kFormatOzDOS:
        pDiskFS = new DiskFSOzDOS();
        break;
    case DiskImg::kFormatCFFA4:
    case DiskImg::kFormatCFFA8:
        pDiskFS = new DiskFSCFFA();
        break;
    case DiskImg::kFormatMacPart:
        pDiskFS = new DiskFSMacPart();
        break;
    case DiskImg::kFormatMicroDrive:
        pDiskFS = new DiskFSMicroDrive();
        break;
    case DiskImg::kFormatFocusDrive:
        pDiskFS = new DiskFSFocusDrive();
        break;
    case DiskImg::kFormatCPM:
        pDiskFS = new DiskFSCPM();
        break;
    case DiskImg::kFormatMSDOS:
        pDiskFS = new DiskFSFAT();
        break;
    case DiskImg::kFormatRDOS33:
    case DiskImg::kFormatRDOS32:
    case DiskImg::kFormatRDOS3:
        pDiskFS = new DiskFSRDOS();
        break;
    case DiskImg::kFormatGutenberg:
        pDiskFS = new DiskFSGutenberg();
        break;

    default:
        LOGI("WARNING: unhandled DiskFS case %d", GetFSFormat());
        assert(false);
        /* fall through */
    case DiskImg::kFormatGenericPhysicalOrd:
    case DiskImg::kFormatGenericProDOSOrd:
    case DiskImg::kFormatGenericDOSOrd:
    case DiskImg::kFormatGenericCPMOrd:
    case DiskImg::kFormatUnknown:
        if (allowUnknown) {
            pDiskFS = new DiskFSUnknown();
            break;
        }
    }

    return pDiskFS;
}


/*
 * Fill an array with SectorOrder values.  The ordering specified by "first"
 * will come first.  Unused entries will be set to "unknown" and should be
 * ignored.
 *
 * "orderArray" must have kSectorOrderMax elements.
 */
/*static*/ void DiskImg::GetSectorOrderArray(SectorOrder* orderArray,
    SectorOrder first)
{
    // init array
    for (int i = 0; i < kSectorOrderMax; i++)
        orderArray[i] = (SectorOrder) i;

    // pull the best-guess ordering to the front
    assert(orderArray[0] == kSectorOrderUnknown);

    orderArray[0] = first;
    orderArray[(int) first] = kSectorOrderUnknown;

    // don't bother checking CP/M sector order
    orderArray[kSectorOrderCPM] = kSectorOrderUnknown;
}


/*
 * Return a short string describing "format".
 *
 * These are semi-duplicated in ImageFormatDialog.cpp in CiderPress.
 */
/*static*/ const char* DiskImg::ToStringCommon(int format,
    const ToStringLookup* pTable, int tableSize)
{
    for (int i = 0; i < tableSize; i++) {
        if (pTable[i].format == format)
            return pTable[i].str;
    }

    assert(false);
    return "(unknown)";
}

/*static*/ const char* DiskImg::ToString(OuterFormat format)
{
    static const ToStringLookup kOuterFormats[] = {
        { DiskImg::kOuterFormatUnknown,         "Unknown format" },
        { DiskImg::kOuterFormatNone,            "(none)" },
        { DiskImg::kOuterFormatCompress,        "UNIX compress" },
        { DiskImg::kOuterFormatGzip,            "gzip" },
        { DiskImg::kOuterFormatBzip2,           "bzip2" },
        { DiskImg::kOuterFormatZip,             "Zip archive" },
    };

    return ToStringCommon(format, kOuterFormats, NELEM(kOuterFormats));
}

/*static*/ const char* DiskImg::ToString(FileFormat format)
{
    static const ToStringLookup kFileFormats[] = {
        { DiskImg::kFileFormatUnknown,          "Unknown format" },
        { DiskImg::kFileFormatUnadorned,        "Unadorned raw data" },
        { DiskImg::kFileFormat2MG,              "2MG" },
        { DiskImg::kFileFormatNuFX,             "NuFX (ShrinkIt)" },
        { DiskImg::kFileFormatDiskCopy42,       "DiskCopy 4.2" },
        { DiskImg::kFileFormatDiskCopy60,       "DiskCopy 6.0" },
        { DiskImg::kFileFormatDavex,            "Davex volume image" },
        { DiskImg::kFileFormatSim2eHDV,         "Sim //e HDV" },
        { DiskImg::kFileFormatTrackStar,        "TrackStar image" },
        { DiskImg::kFileFormatFDI,              "FDI image" },
        { DiskImg::kFileFormatDDD,              "DDD" },
        { DiskImg::kFileFormatDDDDeluxe,        "DDDDeluxe" },
    };

    return ToStringCommon(format, kFileFormats, NELEM(kFileFormats));
};

/*static*/ const char* DiskImg::ToString(PhysicalFormat format)
{
    static const ToStringLookup kPhysicalFormats[] = {
        { DiskImg::kPhysicalFormatUnknown,      "Unknown format" },
        { DiskImg::kPhysicalFormatSectors,      "Sectors" },
        { DiskImg::kPhysicalFormatNib525_6656,  "Raw nibbles (6656-byte)" },
        { DiskImg::kPhysicalFormatNib525_6384,  "Raw nibbles (6384-byte)" },
        { DiskImg::kPhysicalFormatNib525_Var,   "Raw nibbles (variable len)" },
    };

    return ToStringCommon(format, kPhysicalFormats, NELEM(kPhysicalFormats));
};

/*static*/ const char* DiskImg::ToString(SectorOrder format)
{
    static const ToStringLookup kSectorOrders[] = {
        { DiskImg::kSectorOrderUnknown,         "Unknown ordering" },
        { DiskImg::kSectorOrderProDOS,          "ProDOS block ordering" },
        { DiskImg::kSectorOrderDOS,             "DOS sector ordering" },
        { DiskImg::kSectorOrderCPM,             "CP/M block ordering" },
        { DiskImg::kSectorOrderPhysical,        "Physical sector ordering" },
    };

    return ToStringCommon(format, kSectorOrders, NELEM(kSectorOrders));
};

/*static*/ const char* DiskImg::ToString(FSFormat format)
{
    static const ToStringLookup kFSFormats[] = {
        { DiskImg::kFormatUnknown,              "Unknown" },
        { DiskImg::kFormatProDOS,               "ProDOS" },
        { DiskImg::kFormatDOS33,                "DOS 3.3" },
        { DiskImg::kFormatDOS32,                "DOS 3.2" },
        { DiskImg::kFormatPascal,               "Pascal" },
        { DiskImg::kFormatMacHFS,               "HFS" },
        { DiskImg::kFormatMacMFS,               "MFS" },
        { DiskImg::kFormatLisa,                 "Lisa" },
        { DiskImg::kFormatCPM,                  "CP/M" },
        { DiskImg::kFormatMSDOS,                "MS-DOS FAT" },
        { DiskImg::kFormatISO9660,              "ISO-9660" },
        { DiskImg::kFormatRDOS33,               "RDOS 3.3 (16-sector)" },
        { DiskImg::kFormatRDOS32,               "RDOS 3.2 (13-sector)" },
        { DiskImg::kFormatRDOS3,                "RDOS 3 (cracked 13-sector)" },
        { DiskImg::kFormatGenericDOSOrd,        "Generic DOS sectors" },
        { DiskImg::kFormatGenericProDOSOrd,     "Generic ProDOS blocks" },
        { DiskImg::kFormatGenericPhysicalOrd,   "Generic raw sectors" },
        { DiskImg::kFormatGenericCPMOrd,        "Generic CP/M blocks" },
        { DiskImg::kFormatUNIDOS,               "UNIDOS (400K DOS x2)" },
        { DiskImg::kFormatOzDOS,                "OzDOS (400K DOS x2)" },
        { DiskImg::kFormatCFFA4,                "CFFA (4 or 6 partitions)" },
        { DiskImg::kFormatCFFA8,                "CFFA (8 partitions)" },
        { DiskImg::kFormatMacPart,              "Macintosh partitioned disk" },
        { DiskImg::kFormatMicroDrive,           "MicroDrive partitioned disk" },
        { DiskImg::kFormatFocusDrive,           "FocusDrive partitioned disk" },
    };

    return ToStringCommon(format, kFSFormats, NELEM(kFSFormats));
};


/*
 * strerror() equivalent for DiskImg errors.
 */
const char* DiskImgLib::DIStrError(DIError dierr)
{
    if (dierr > 0) {
        const char* msg;
        msg = strerror(dierr);
        if (msg != NULL)
            return msg;
    }

    /*
     * BUG: this should be set up as per-thread storage in an MT environment.
     * I would be more inclined to worry about this if I was expecting
     * to hit "default:".  So long as valid values are passed in, and the
     * switch statement is kept up to date, we should never have cause
     * to return this.
     *
     * An easier solution, should this present a problem for someone, would
     * be to have the function return NULL or "unknown error" when the
     * error value isn't recognized.  I'd recommend leaving it as-is for
     * debug builds, though, as it's helpful to know *which* error is not
     * recognized.
     */
    static char defaultMsg[32];

    switch (dierr) {
    case kDIErrNone:
        return "(no error)";

    case kDIErrAccessDenied:
        return "access denied";
    case kDIErrVWAccessForbidden:
        return "for safety, write access to this volume is forbidden";
    case kDIErrSharingViolation:
        return "file is already open and cannot be shared";
    case kDIErrNoExclusiveAccess:
        return "couldn't get exclusive access";
    case kDIErrWriteProtected:
        return "write protected";
    case kDIErrCDROMNotSupported:
        return "access to CD-ROM drives is not supported";
    case kDIErrASPIFailure:
        return "an ASPI request failed";
    case kDIErrSPTIFailure:
        return "an SPTI request failed";
    case kDIErrSCSIFailure:
        return "a SCSI request failed";
    case kDIErrDeviceNotReady:
        return "device not ready";

    case kDIErrFileNotFound:
        return "file not found";
    case kDIErrForkNotFound:
        return "fork not found";
    case kDIErrAlreadyOpen:
        return "an image is already open";
    case kDIErrFileOpen:
        return "file is open";
    case kDIErrNotReady:
        return "object not ready";
    case kDIErrFileExists:
        return "file already exists";
    case kDIErrDirectoryExists:
        return "directory already exists";

    case kDIErrEOF:
        return "end of file reached";
    case kDIErrReadFailed:
        return "read failed";
    case kDIErrWriteFailed:
        return "write failed";
    case kDIErrDataUnderrun:
        return "tried to read past end of file";
    case kDIErrDataOverrun:
        return "tried to write past end of file";
    case kDIErrGenericIO:
        return "I/O error";

    case kDIErrOddLength:
        return "image size is wrong";
    case kDIErrUnrecognizedFileFmt:
        return "not a recognized disk image format";
    case kDIErrBadFileFormat:
        return "image file contents aren't in expected format";
    case kDIErrUnsupportedFileFmt:
        return "file format not supported";
    case kDIErrUnsupportedPhysicalFmt:
        return "physical format not supported";
    case kDIErrUnsupportedFSFmt:
        return "filesystem type not supported";
    case kDIErrBadOrdering:
        return "bad sector ordering";
    case kDIErrFilesystemNotFound:
        return "specified filesystem not found";
    case kDIErrUnsupportedAccess:
        return "the method of access used isn't supported for this image";
    case kDIErrUnsupportedImageFeature:
        return "image file uses features that CiderPress doesn't support";

    case kDIErrInvalidTrack:
        return "invalid track number";
    case kDIErrInvalidSector:
        return "invalid sector number";
    case kDIErrInvalidBlock:
        return "invalid block number";
    case kDIErrInvalidIndex:
        return "invalid index number";

    case kDIErrDirectoryLoop:
        return "disk directory structure has an infinite loop";
    case kDIErrFileLoop:
        return "file structure has an infinite loop";
    case kDIErrBadDiskImage:
        return "the filesystem on this image appears damaged";
    case kDIErrBadFile:
        return "file structure appears damaged";
    case kDIErrBadDirectory:
        return "a directory appears damaged";
    case kDIErrBadPartition:
        return "bad partition";

    case kDIErrFileArchive:
        return "this looks like a file archive, not a disk archive";
    case kDIErrUnsupportedCompression:
        return "compression method not supported";
    case kDIErrBadChecksum:
        return "checksum doesn't match, data may be corrupted";
    case kDIErrBadCompressedData:
        return "the compressed data is corrupted";
    case kDIErrBadArchiveStruct:
        return "archive may be damaged";

    case kDIErrBadNibbleSectors:
        return "couldn't read sectors from this image";
    case kDIErrSectorUnreadable:
        return "sector not readable";
    case kDIErrInvalidDiskByte:
        return "found invalid nibble image disk byte";
    case kDIErrBadRawData:
        return "couldn't convert raw data to nibble data";

    case kDIErrInvalidFileName:
        return "invalid file name";
    case kDIErrDiskFull:
        return "disk full";
    case kDIErrVolumeDirFull:
        return "volume directory is full";
    case kDIErrInvalidCreateReq:
        return "invalid disk image create request";
    case kDIErrTooBig:
        return "size is larger than we can handle";

    case kDIErrGeneric:
        return "DiskImg generic error";
    case kDIErrInternal:
        return "DiskImg internal error";
    case kDIErrMalloc:
        return "memory allocation failure";
    case kDIErrInvalidArg:
        return "invalid argument";
    case kDIErrNotSupported:
        return "feature not supported";
    case kDIErrCancelled:
        return "cancelled by user";

    case kDIErrNufxLibInitFailed:
        return "NufxLib initialization failed";

    default:
        sprintf(defaultMsg, "(error=%d)", dierr);
        return defaultMsg;
    }
}

/*
 * High ASCII conversion table, from Technical Note PT515,
 * "Apple File Exchange Q&As".  The table is available in a hopelessly
 * blurry PDF or a pair of GIFs created with small fonts, but I think I
 * have mostly captured it.
 */
/*static*/ const uint8_t DiskImg::kMacHighASCII[128+1] =
    "AACENOUaaaaaaceeeeiiiinooooouuuu"      // 0x80 - 0x9f
    "tocL$oPBrct'.=AO%+<>YudsPpSaoOao"      // 0xa0 - 0xbf
    "?!-vf=d<>. AAOOo--\"\"''/oyY/o<> f"    // 0xc0 - 0xdf
    "|*,,%AEAEEIIIIOOaOUUUi^~-,**,\"? ";    // 0xe0 - 0xff


/*
 * Hack for Win32 systems.  See Win32BlockIO.cpp for commentary.
 */
bool DiskImgLib::gAllowWritePhys0 = false;
/*static*/ void DiskImg::SetAllowWritePhys0(bool val) {
    DiskImgLib::gAllowWritePhys0 = val;
}
