/*
 * NuLib2
 * Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved.
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the BSD License, see the file COPYING.
 *
 * Binary II support.
 *
 * The BNY format is significantly different from the NuFX format, so
 * support wasn't included in NufxLib.  Since there's no reason to
 * create BNY archives when SHK is available, I've only implemented
 * data extraction features.
 *
 * (Technically, BNY isn't an archive format.  It was a way to encapsulate
 * file attributes and file data for transmission over a modem, and was
 * meant to be added and stripped on the fly.)
 *
 * TO DO: add "junk skipping" like NufxLib has.
 */
#include "NuLib2.h"

#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#endif
#ifndef O_BINARY
# define O_BINARY   0
#endif

/* how to open a file */
#ifdef FOPEN_WANTS_B
# define kFileOpenReadOnly      "rb"
#else
# define kFileOpenReadOnly      "r"
#endif


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

/*
 * Open a file read-only.  We abstract this so we get "r" vs "rb" right.
 * (Moves this to SysUtils.c if anybody else needs it.)
 */
NuError OpenFileReadOnly(const char* filename, FILE** pFp)
{
    NuError err = kNuErrNone;

    *pFp = fopen(filename, kFileOpenReadOnly);
    if (*pFp == NULL)
        err = errno ? errno : kNuErrFileOpen;

    return err;
}

/*
 * Determine whether the current file we're examining is described by
 * the file specification given on the command line.
 *
 * If no filespec was provided, then all records are "specified".
 */
Boolean NameIsSpecified(NulibState* pState, const char* filename)
{
    char* const* pSpec;
    int i;

    if (!NState_GetFilespecCount(pState))
        return true;

    pSpec = NState_GetFilespecPointer(pState);
    for (i = NState_GetFilespecCount(pState); i > 0; i--, pSpec++) {
#ifdef NU_CASE_SENSITIVE
        if (NState_GetModRecurse(pState)) {
            if (strncmp(*pSpec, filename, strlen(*pSpec)) == 0)
                return true;
        } else {
            if (strcmp(*pSpec, filename) == 0)
                return true;
        }
#else
        if (NState_GetModRecurse(pState)) {
            if (strncasecmp(*pSpec, filename, strlen(*pSpec)) == 0)
                return true;
        } else {
            if (strcasecmp(*pSpec, filename) == 0)
                return true;
        }
#endif
    }

    return false;
}


/*
 * ===========================================================================
 *      Binary II goodies
 * ===========================================================================
 */

/*
 * State for the currently open Binary II archive.
 */
typedef struct BNYArchive {
    NulibState*     pState;
    FILE*           fp;
    Boolean         first;
} BNYArchive;


#define kBNYBlockSize       128         /* BNY files are broken into blocks */
#define kBNYMaxFileName     64
#define kBNYMaxNativeName   48
#define kBNYFlagCompressed  (1<<7)
#define kBNYFlagEncrypted   (1<<6)
#define kBNYFlagSparse      (1)

/*
 * An entry in a Binary II archive.  Each archive is essentially a stream
 * of files; only the "filesToFollow" value gives any indication that
 * something else follows this entry.
 */
typedef struct BNYEntry {
    uint16_t        access;
    uint16_t        fileType;
    uint32_t        auxType;
    uint8_t         storageType;
    uint32_t        fileSize;           /* in 512-byte blocks */
    uint16_t        prodosModDate;
    uint16_t        prodosModTime;
    NuDateTime      modWhen;            /* computed from previous two fields */
    uint16_t        prodosCreateDate;
    uint16_t        prodosCreateTime;
    NuDateTime      createWhen;         /* computed from previous two fields */
    uint32_t        eof;
    uint32_t        realEOF;            /* eof is bogus for directories */
    char            fileName[kBNYMaxFileName+1];        /* ASCII only */
    char            nativeName[kBNYMaxNativeName+1];
    uint32_t        diskSpace;          /* in 512-byte blocks */
    uint8_t         osType;             /* not exactly same as NuFileSysID */
    uint16_t        nativeFileType;
    uint8_t         phantomFlag;
    uint8_t         dataFlags;          /* advisory flags */
    uint8_t         version;
    uint8_t         filesToFollow;      /* #of files after this one */

    uint8_t         blockBuf[kBNYBlockSize];
} BNYEntry;

/*
 * Test for the magic number on a file in SQueezed format.
 */
static inline Boolean IsSqueezed(uint8_t one, uint8_t two)
{
    return (one == 0x76 && two == 0xff);
}

/*
 * Test if this entry is a directory.
 */
static inline Boolean IsDir(BNYEntry* pEntry)
{
    /*
     * NuLib and "unblu.c" compared against file type 15 (DIR), so I'm
     * going to do that too, but it would probably be better to compare
     * against storageType 0x0d.
     */
    return (pEntry->fileType == 15);
}


/*
 * Initialize a BNYArchive structure.
 */
static BNYArchive* BNYInit(NulibState* pState)
{
    BNYArchive* pBny;

    pBny = Malloc(sizeof(*pBny));
    memset(pBny, 0, sizeof(*pBny));

    pBny->pState = pState;

    return pBny;
}

/*
 * Free up a BNYArchive, disposing of anything inside it.
 */
static void BNYFree(BNYArchive* pBny)
{
    /* don't need to do this on stdin, but won't really hurt */
    if (pBny->fp != NULL)
        fclose(pBny->fp);

    Free(pBny);
}


/*
 * Open a Binary II archive read-only.  Might be a file on disk or a
 * stream on stdin.
 */
static NuError BNYOpenReadOnly(BNYArchive* pBny)
{
    NuError err = kNuErrNone;
    NulibState* pState;
    
    Assert(pBny != NULL);
    Assert(pBny->pState != NULL);
    Assert(pBny->fp == NULL);

    pState = pBny->pState;

    if (IsFilenameStdin(NState_GetArchiveFilename(pState))) {
        pBny->fp = stdin;
        /*
         * Since the archive is on stdin, we can't ask the user questions.
         * On a UNIX system we could open /dev/tty, but that's not portable,
         * and I don't think archives on stdin are going to be popular
         * enough to make this worth doing.
         */
        NState_SetInputUnavailable(pState, true);
    } else {
        err = OpenFileReadOnly(NState_GetArchiveFilename(pState), &pBny->fp);
        if (err != kNuErrNone) {
            ReportError(err, "unable to open '%s'",
                NState_GetArchiveFilename(pState));
            goto bail;
        }
    }

bail:
    return err;
}


/*
 * Wrapper for fread().  Note the arguments resemble read(2) rather
 * than fread(3S).
 */
static NuError BNYRead(BNYArchive* pBny, void* buf, size_t nbyte)
{
    size_t result;

    Assert(pBny != NULL);
    Assert(buf != NULL);
    Assert(nbyte > 0);
    Assert(pBny->fp != NULL);

    errno = 0;
    result = fread(buf, 1, nbyte, pBny->fp);
    if (result != nbyte)
        return errno ? errno : kNuErrFileRead;
    return kNuErrNone;
}

/*
 * Seek within an archive.  Because we need to handle streaming archives,
 * and don't need to special-case anything, we only allow relative
 * forward seeks.
 */
static NuError BNYSeek(BNYArchive* pBny, long offset)
{
    Assert(pBny != NULL);
    Assert(pBny->fp != NULL);
    Assert(pBny->pState != NULL);
    Assert(offset > 0);

    /*DBUG(("--- seeking forward %ld bytes\n", offset));*/

    if (IsFilenameStdin(NState_GetArchiveFilename(pBny->pState))) {
        /* OPT: might be faster to fread a chunk at a time */
        while (offset--)
            (void) getc(pBny->fp);

        if (ferror(pBny->fp) || feof(pBny->fp))
            return kNuErrFileSeek;
    } else {
        if (fseek(pBny->fp, offset, SEEK_CUR) < 0)
            return kNuErrFileSeek;
    }
    
    return kNuErrNone;
}


/*
 * Convert from ProDOS compact date format to the expanded DateTime format.
 */
static void BNYConvertDateTime(uint16_t prodosDate, uint16_t prodosTime,
    NuDateTime* pWhen)
{
    pWhen->second = 0;
    pWhen->minute = prodosTime & 0x3f;
    pWhen->hour = (prodosTime >> 8) & 0x1f;
    pWhen->day = (prodosDate & 0x1f) -1;
    pWhen->month = ((prodosDate >> 5) & 0x0f) -1;
    pWhen->year = (prodosDate >> 9) & 0x7f;
    if (pWhen->year < 40)
        pWhen->year += 100;     /* P8 uses 0-39 for 2000-2039 */
    pWhen->extra = 0;
    pWhen->weekDay = 0;
}

/*
 * Decode a Binary II header.
 *
 * See the File Type Note for $e0/8000 to decipher the buffer offsets
 * and meanings.
 */
static NuError BNYDecodeHeader(BNYArchive* pBny, BNYEntry* pEntry)
{
    NuError err = kNuErrNone;
    uint8_t* raw;
    int len;

    Assert(pBny != NULL);
    Assert(pEntry != NULL);

    raw = pEntry->blockBuf;

    if (raw[0] != 0x0a || raw[1] != 0x47 || raw[2] != 0x4c || raw[18] != 0x02) {
        err = kNuErrBadData;
        ReportError(err, "this doesn't look like a Binary II header");
        goto bail;
    }

    pEntry->access = raw[3] | raw[111] << 8;
    pEntry->fileType = raw[4] | raw[112] << 8;
    pEntry->auxType = raw[5] | raw[6] << 8 | raw[109] << 16 | raw[110] << 24;
    pEntry->storageType = raw[7];
    pEntry->fileSize = raw[8] | raw[9] << 8;
    pEntry->prodosModDate = raw[10] | raw[11] << 8;
    pEntry->prodosModTime = raw[12] | raw[13] << 8;
    BNYConvertDateTime(pEntry->prodosModDate, pEntry->prodosModTime,
        &pEntry->modWhen);
    pEntry->prodosCreateDate = raw[14] | raw[15] << 8;
    pEntry->prodosCreateTime = raw[16] | raw[17] << 8;
    BNYConvertDateTime(pEntry->prodosCreateDate, pEntry->prodosCreateTime,
        &pEntry->createWhen);
    pEntry->eof = raw[20] | raw[21] << 8 | raw[22] << 16 | raw[116] << 24;
    len = raw[23];
    if (len > kBNYMaxFileName) {
        err = kNuErrBadData;
        ReportError(err, "invalid filename length %d", len);
        goto bail;
    }
    memcpy(pEntry->fileName, &raw[24], len);
    pEntry->fileName[len] = '\0';

    pEntry->nativeName[0] = '\0';
    if (len <= 15 && raw[39] != 0) {
        len = raw[39];
        if (len > kBNYMaxNativeName) {
            err = kNuErrBadData;
            ReportError(err, "invalid filename length %d", len);
            goto bail;
        }
        memcpy(pEntry->nativeName, &raw[40], len);
        pEntry->nativeName[len] = '\0';
    }

    pEntry->diskSpace = raw[117] | raw[118] << 8 | raw[119] << 16 |
                        raw[120] << 24;

    pEntry->osType = raw[121];
    pEntry->nativeFileType = raw[122] | raw[123] << 8;
    pEntry->phantomFlag = raw[124];
    pEntry->dataFlags = raw[125];
    pEntry->version = raw[126];
    pEntry->filesToFollow = raw[127];

    /* directories are given an EOF but don't actually have any content */
    if (IsDir(pEntry))
        pEntry->realEOF = 0;
    else
        pEntry->realEOF = pEntry->eof;

bail:
    return err;
}


/*
 * Normalize the pathname by running it through the usual NuLib2
 * function.  The trick here is that the function usually takes a
 * NuPathnameProposal, which we don't happen to have handy.  Rather
 * than generalize the NuLib2 code, we just create a fake proposal,
 * which is a bit dicey but shouldn't break too easily.
 *
 * This takes care of -e, -ee, and -j.
 *
 * We return the new path, which is stored in NulibState's temporary
 * filename buffer.
 */
const char* BNYNormalizePath(BNYArchive* pBny, BNYEntry* pEntry)
{
    NuPathnameProposal pathProposal;
    NuRecord fakeRecord;
    NuThread fakeThread;

    /* make uninitialized data obvious */
    memset(&fakeRecord, 0xa1, sizeof(fakeRecord));
    memset(&fakeThread, 0xa5, sizeof(fakeThread));

    pathProposal.pathnameUNI = pEntry->fileName;
    pathProposal.filenameSeparator = '/';   /* BNY always uses ProDOS conv */
    pathProposal.pRecord = &fakeRecord;
    pathProposal.pThread = &fakeThread;

    pathProposal.newPathnameUNI = NULL;
    pathProposal.newFilenameSeparator = '\0';
    pathProposal.newDataSink = NULL;

    /* need the filetype and auxtype for -e/-ee */
    fakeRecord.recFileType = pEntry->fileType;
    fakeRecord.recExtraType = pEntry->auxType;

    /* need the components of a ThreadID */
    fakeThread.thThreadClass = kNuThreadClassData;
    fakeThread.thThreadKind = 0x0000;       /* data fork */

    return NormalizePath(pBny->pState, &pathProposal);
}


/*
 * Copy all data from the Binary II file to "outfp", reading in 128-byte
 * blocks.  We need to read multiples of 128 bytes so that we're ready to
 * read the next entry without additional seeking.
 *
 * Some programs (like ProTERM) apply a 128-byte header without padding
 * out the file length, so we may come up short at the end of the file.
 *
 * Uses pEntry->blockBuf, which already has the first 128 bytes in it.
 */
static NuError BNYCopyBlocks(BNYArchive* pBny, BNYEntry* pEntry, FILE* outfp)
{
    NuError err = kNuErrNone;
    long bytesLeft;

    Assert(pEntry->realEOF > 0);

    bytesLeft = pEntry->realEOF;
    while (bytesLeft > 0) {
        long toWrite;

        toWrite = bytesLeft;
        if (toWrite > kBNYBlockSize)
            toWrite = kBNYBlockSize;

        if (outfp != NULL) {
            if (fwrite(pEntry->blockBuf, toWrite, 1, outfp) != 1) {
                err = errno ? errno : kNuErrFileWrite;
                ReportError(err, "BNY write failed");
                goto bail;
            }
        }

        bytesLeft -= toWrite;

        if (bytesLeft) {
            size_t actual;

            errno = 0;
            actual = fread(pEntry->blockBuf, 1, kBNYBlockSize, pBny->fp);
            if (actual != kBNYBlockSize) {
                /* failure or partial read */
                if (actual != bytesLeft) {
                    err = errno ? errno : kNuErrFileRead;
                    ReportError(err, "BNY read failed");
                    goto bail;
                } else {
                    DBUG(("+++ partial read %d/%d\n", actual, kBNYBlockSize));
                }
            }
        }
    }

bail:
    return err;
}


/*
 * ===========================================================================
 *      Unsqueeze
 * ===========================================================================
 */

/*
 * This was ripped fairly directly from Squeeze.c in NufxLib.  Because
 * there's relatively little code, and providing direct access to the
 * compression functions is a little unwieldy, I've cut & pasted the
 * necessary pieces here.
 */
#define FULL_SQ_HEADER
#define kSqBufferSize 8192          /* must be enough to hold full SQ header */

#define kNuSQMagic      0xff76      /* magic value for file header */
#define kNuSQRLEDelim   0x90        /* RLE delimiter */
#define kNuSQEOFToken   256         /* distinguished stop symbol */
#define kNuSQNumVals    257         /* 256 symbols + stop */


/*
 * State during uncompression.
 */
typedef struct USQState {
    uint32_t        dataInBuffer;
    uint8_t*        dataPtr;
    int             bitPosn;
    int             bits;

    /*
     * Decoding tree; first "nodeCount" values are populated.  Positive
     * values are indicies to another node in the tree, negative values
     * are literals (+1 because "negative zero" doesn't work well).
     */
    int             nodeCount;
    struct {
        short       child[2];       /* left/right kids, must be signed 16-bit */
    } decTree[kNuSQNumVals-1];
} USQState;


/*
 * Decode the next symbol from the Huffman stream.
 */
static NuError USQDecodeHuffSymbol(USQState* pUsqState, int* pVal)
{
    short val = 0;
    int bits, bitPosn;

    bits = pUsqState->bits;     /* local copy */
    bitPosn = pUsqState->bitPosn;

    do {
        if (++bitPosn > 7) {
            /* grab the next byte and use that */
            bits = *pUsqState->dataPtr++;
            bitPosn = 0;
            if (!pUsqState->dataInBuffer--)
                return kNuErrBufferUnderrun;

            val = pUsqState->decTree[val].child[1 & bits];
        } else {
            /* still got bits; shift right and use it */
            val = pUsqState->decTree[val].child[1 & (bits >>= 1)];
        }
    } while (val >= 0);

    /* val is negative literal; add one to make it zero-based then negate it */
    *pVal = -(val + 1);

    pUsqState->bits = bits;
    pUsqState->bitPosn = bitPosn;

    return kNuErrNone;
}


/*
 * Read two bytes of signed data out of the buffer.
 */
static inline NuError USQReadShort(USQState* pUsqState, short* pShort)
{
    if (pUsqState->dataInBuffer < 2)
        return kNuErrBufferUnderrun;

    *pShort = *pUsqState->dataPtr++;
    *pShort |= (*pUsqState->dataPtr++) << 8;
    pUsqState->dataInBuffer -= 2;

    return kNuErrNone;
}

/*
 * Expand "SQ" format.
 *
 * Because we have a stop symbol, knowing the uncompressed length of
 * the file is not essential.
 */
static NuError BNYUnSqueeze(BNYArchive* pBny, BNYEntry* pEntry, FILE* outfp)
{
    NuError err = kNuErrNone;
    USQState usqState;
    uint32_t compRemaining, getSize;
#ifdef FULL_SQ_HEADER
    uint16_t magic, fileChecksum, checksum;
#endif
    short nodeCount;
    int i, inrep;
    uint8_t* tmpBuf = NULL;
    uint8_t lastc = 0;

    tmpBuf = Malloc(kSqBufferSize);
    if (tmpBuf == NULL) {
        err = kNuErrMalloc;
        goto bail;
    }

    usqState.dataInBuffer = 0;
    usqState.dataPtr = tmpBuf;
    usqState.bits = 0;

    compRemaining = pEntry->realEOF;
#ifdef FULL_SQ_HEADER
    if (compRemaining < 8)
#else
    if (compRemaining < 3)
#endif
    {
        err = kNuErrBadData;
        ReportError(err, "too short to be valid SQ data");
        goto bail;
    }

    /*
     * Round up to the nearest 128-byte boundary.  We need to read
     * everything out of the file in case this is a streaming archive.
     * Because the compressed data has an embedded stop symbol, it's okay
     * to "overrun" the expansion code.
     */
    compRemaining =
        ((compRemaining + kBNYBlockSize-1) / kBNYBlockSize) * kBNYBlockSize;

    /* want to grab up to kSqBufferSize bytes */
    if (compRemaining > kSqBufferSize)
        getSize = kSqBufferSize;
    else
        getSize = compRemaining;

    /* copy the <= 128 bytes we already have into the general buffer */
    memcpy(usqState.dataPtr, pEntry->blockBuf, kBNYBlockSize);
    if (getSize > kBNYBlockSize) {
        getSize -= kBNYBlockSize;
        compRemaining -= kBNYBlockSize;
        usqState.dataInBuffer += kBNYBlockSize;
    } else {
        Assert(compRemaining <= kBNYBlockSize);
        usqState.dataInBuffer = getSize;
        getSize = 0;
        compRemaining = 0;
    }

    /* temporarily advance dataPtr past the block we copied in */
    usqState.dataPtr += kBNYBlockSize;

    /*
     * Grab a big chunk.  "compRemaining" is the amount of compressed
     * data left in the file, usqState.dataInBuffer is the amount of
     * compressed data left in the buffer.
     *
     * We always want to read 128-byte blocks.
     */
    if (getSize) {
        Assert(getSize <= kSqBufferSize);
        err = BNYRead(pBny, usqState.dataPtr, getSize);
        if (err != kNuErrNone) {
            ReportError(err,
                "failed reading compressed data (%u bytes)", getSize);
            goto bail;
        }
        usqState.dataInBuffer += getSize;
        if (getSize > compRemaining)
            compRemaining = 0;
        else
            compRemaining -= getSize;
    }

    /* reset dataPtr */
    usqState.dataPtr = tmpBuf;

    /*
     * Read the header.  We assume that the header will fit in the
     * compression buffer ( sq allowed 300+ for the filename, plus
     * 257*2 for the tree, plus misc).
     */
    Assert(kSqBufferSize > 1200);
#ifdef FULL_SQ_HEADER
    err = USQReadShort(&usqState, (short*) &magic);
    if (err != kNuErrNone)
        goto bail;
    if (magic != kNuSQMagic) {
        err = kNuErrBadData;
        ReportError(err, "bad magic number in SQ block");
        goto bail;
    }

    err = USQReadShort(&usqState, (short*) &fileChecksum);
    if (err != kNuErrNone)
        goto bail;

    checksum = 0;

    /* skip over the filename */
    while (*usqState.dataPtr++ != '\0')
        usqState.dataInBuffer--;
    usqState.dataInBuffer--;
#endif

    err = USQReadShort(&usqState, &nodeCount);
    if (err != kNuErrNone)
        goto bail;
    if (nodeCount < 0 || nodeCount >= kNuSQNumVals) {
        err = kNuErrBadData;
        ReportError(err, "invalid decode tree in SQ (%d nodes)", nodeCount);
        goto bail;
    }
    usqState.nodeCount = nodeCount;

    /* initialize for possibly empty tree (only happens on an empty file) */
    usqState.decTree[0].child[0] = -(kNuSQEOFToken+1);
    usqState.decTree[0].child[1] = -(kNuSQEOFToken+1);

    /* read the nodes, ignoring "read errors" until we're done */
    for (i = 0; i < nodeCount; i++) {
        err = USQReadShort(&usqState, &usqState.decTree[i].child[0]);
        err = USQReadShort(&usqState, &usqState.decTree[i].child[1]);
    }
    if (err != kNuErrNone) {
        err = kNuErrBadData;
        ReportError(err, "SQ data looks truncated at tree");
        goto bail;
    }

    usqState.bitPosn = 99;      /* force an immediate read */

    /*
     * Start pulling data out of the file.  We have to Huffman-decode
     * the input, and then feed that into an RLE expander.
     *
     * A completely lopsided (and broken) Huffman tree could require
     * 256 tree descents, so we want to try to ensure we have at least 256
     * bits in the buffer.  Otherwise, we could get a false buffer underrun
     * indication back from DecodeHuffSymbol.
     *
     * The SQ sources actually guarantee that a code will fit entirely
     * in 16 bits, but there's no reason not to use the larger value.
     */
    inrep = false;
    while (1) {
        int val;

        if (usqState.dataInBuffer < 65 && compRemaining) {
            /*
             * Less than 256 bits, but there's more in the file.
             *
             * First thing we do is slide the old data to the start of
             * the buffer.
             */
            if (usqState.dataInBuffer) {
                Assert(tmpBuf != usqState.dataPtr);
                memmove(tmpBuf, usqState.dataPtr, usqState.dataInBuffer);
            }
            usqState.dataPtr = tmpBuf;

            /*
             * Next we read as much as we can.
             */
            if (kSqBufferSize - usqState.dataInBuffer < compRemaining)
                getSize = kSqBufferSize - usqState.dataInBuffer;
            else
                getSize = compRemaining;

            Assert(getSize <= kSqBufferSize);
            err = BNYRead(pBny, usqState.dataPtr + usqState.dataInBuffer,
                    getSize);
            if (err != kNuErrNone) {
                ReportError(err,
                    "failed reading compressed data (%u bytes)", getSize);
                goto bail;
            }
            usqState.dataInBuffer += getSize;
            if (getSize > compRemaining)
                compRemaining = 0;
            else
                compRemaining -= getSize;

            Assert(compRemaining < 32767*65536);
            Assert(usqState.dataInBuffer <= kSqBufferSize);
        }

        err = USQDecodeHuffSymbol(&usqState, &val);
        if (err != kNuErrNone) {
            ReportError(err, "failed decoding huff symbol");
            goto bail;
        }

        if (val == kNuSQEOFToken)
            break;

        /*
         * Feed the symbol into the RLE decoder.
         */
        if (inrep) {
            /*
             * Last char was RLE delim, handle this specially.  We use
             * --val instead of val-- because we already emitted the
             * first occurrence of the char (right before the RLE delim).
             */
            if (val == 0) {
                /* special case -- just an escaped RLE delim */
                lastc = kNuSQRLEDelim;
                val = 2;
            }
            while (--val) {
                /*if (pCrc != NULL)
                    *pCrc = Nu_CalcCRC16(*pCrc, &lastc, 1);*/
                if (outfp != NULL)
                    putc(lastc, outfp);
                #ifdef FULL_SQ_HEADER
                checksum += lastc;
                #endif
            }
            inrep = false;
        } else {
            /* last char was ordinary */
            if (val == kNuSQRLEDelim) {
                /* set a flag and catch the count the next time around */
                inrep = true;
            } else {
                lastc = val;
                /*if (pCrc != NULL)
                    *pCrc = Nu_CalcCRC16(*pCrc, &lastc, 1);*/
                if (outfp != NULL)
                    putc(lastc, outfp);
                #ifdef FULL_SQ_HEADER
                checksum += lastc;
                #endif
            }
        }

    }

    if (inrep) {
        err = kNuErrBadData;
        ReportError(err, "got stop symbol when run length expected");
        goto bail;
    }

    #ifdef FULL_SQ_HEADER
    /* verify the checksum stored in the SQ file */
    if (checksum != fileChecksum) {
        err = kNuErrBadDataCRC;
        ReportError(err, "expected 0x%04x, got 0x%04x (SQ)",
            fileChecksum, checksum);
        goto bail;
    } else {
        DBUG(("--- SQ checksums match (0x%04x)\n", checksum));
    }
    #endif

    /*
     * Gobble up any unused bytes in the last 128-byte block.  There
     * shouldn't be more than that left over.
     */
    if (compRemaining > kSqBufferSize) {
        err = kNuErrBadData;
        ReportError(err, "wow: found %u bytes left over", compRemaining);
        goto bail;
    }
    if (compRemaining) {
        DBUG(("+++ slurping up last %ld bytes\n", compRemaining));
        err = BNYRead(pBny, tmpBuf, compRemaining);
        if (err != kNuErrNone) {
            ReportError(err, "failed reading leftovers");
            goto bail;
        }
    }

bail:
    if (outfp != NULL)
        fflush(outfp);
    Free(tmpBuf);
    return err;
}


/*
 * ===========================================================================
 *      Iterators
 * ===========================================================================
 */

typedef NuError (*BNYIteratorFunc)(BNYArchive* pBny, BNYEntry* pEntry,
                                    Boolean* consumedFlag);

/*
 * Iterate through a Binary II archive, calling "func" to perform
 * operations on the file.
 */
static NuError BNYIterate(NulibState* pState, BNYIteratorFunc func)
{
    NuError err = kNuErrNone;
    BNYArchive* pBny = NULL;
    BNYEntry entry;
    Boolean consumed;
    int first = true;
    int toFollow;

    Assert(pState != NULL);
    Assert(func != NULL);

    NState_SetMatchCount(pState, 0);

    pBny = BNYInit(pState);
    if (pBny == NULL) {
        err = kNuErrMalloc;
        goto bail;
    }

    err = BNYOpenReadOnly(pBny);
    if (err != kNuErrNone)
        goto bail;

    toFollow = 1;       /* assume 1 file in archive */
    pBny->first = true;
    while (toFollow) {
        err = BNYRead(pBny, entry.blockBuf, sizeof(entry.blockBuf));
        if (err != kNuErrNone) {
            ReportError(err, "failed while reading header in '%s'",
                NState_GetArchiveFilename(pState));
            goto bail;
        }

        err = BNYDecodeHeader(pBny, &entry);
        if (err != kNuErrNone) {
            if (first)
                ReportError(err, "not a Binary II archive?");
            goto bail;
        }

        /*
         * If the file has one or more blocks, read the first block now.
         * This will allow the various functions to evaluate the file
         * contents for SQueeze compression.
         */
        if (entry.realEOF != 0) {
            err = BNYRead(pBny, entry.blockBuf, sizeof(entry.blockBuf));
            if (err != kNuErrNone) {
                ReportError(err, "failed while reading '%s'",
                    NState_GetArchiveFilename(pState));
                goto bail;
            }
        }

        /*
         * Invoke the appropriate function if this file was requested.
         */
        consumed = false;
        if (NameIsSpecified(pBny->pState, entry.fileName)) {
            NState_IncMatchCount(pState);

            err = (*func)(pBny, &entry, &consumed);
            if (err != kNuErrNone)
                goto bail;

            pBny->first = false;
        }

        /*
         * If they didn't "consume" the entire BNY entry, we need to
         * do it for them.  We've already read the first block (if it
         * existed), so we don't need to eat that one again.
         */
        if (!consumed) {
            int nblocks = (entry.realEOF + kBNYBlockSize-1) / kBNYBlockSize;

            if (nblocks > 1) {
                err = BNYSeek(pBny, (nblocks-1) * kBNYBlockSize);
                if (err != kNuErrNone) {
                    ReportError(err, "failed while seeking forward");
                    goto bail;
                }
            }
        }

        if (!first) {
            if (entry.filesToFollow != toFollow -1) {
                ReportError(kNuErrNone,
                    "WARNING: filesToFollow %d, expected %d\n",
                    entry.filesToFollow, toFollow -1);
            }
        }
        toFollow = entry.filesToFollow;

        first = false;
    }

    if (!NState_GetMatchCount(pState))
        printf("%s: no records match\n", gProgName);

bail:
    if (pBny != NULL)
        BNYFree(pBny);
    if (err != kNuErrNone) {
        DBUG(("--- Iterator returning failure %d\n", err));
    }
    return err;
}

/*
 * Get a quick table of contents.
 */
static NuError BNYListShort(BNYArchive* pBny, BNYEntry* pEntry,
    Boolean* pConsumedFlag)
{
    NuError err = kNuErrNone;

    printf("%s\n", pEntry->fileName);

    return err;
}

/*
 * Get a verbose listing of contents.
 */
static NuError BNYListVerbose(BNYArchive* pBny, BNYEntry* pEntry,
    Boolean* pConsumedFlag)
{
    NuError err = kNuErrNone;
    Boolean isSqueezed, isReadOnly;
    NulibState* pState;
    char date1[kDateOutputLen];
    int len;

    pState = pBny->pState;

    if (pBny->first) {
        const char* ccp;

        if (IsFilenameStdin(NState_GetArchiveFilename(pState)))
            ccp = "<stdin>";
        else
            ccp = FilenameOnly(pState, NState_GetArchiveFilename(pState));

        printf("%-59.59s Files:%5u\n\n",
            ccp, pEntry->filesToFollow+1);

        printf(" Name                        Type Auxtyp Modified"
               "         Fmat   Length\n");
        printf("-------------------------------------------------"
               "----------------------\n");
    }

    isSqueezed = false;
    if (pEntry->realEOF && IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1]))
        isSqueezed = true;

    len = strlen(pEntry->fileName);
    isReadOnly = pEntry->access == 0x21L || pEntry->access == 0x01L;
    if (len <= 27) {
        printf("%c%-27.27s ",
            isReadOnly ? '+' : ' ', pEntry->fileName);
    } else {
        printf("%c..%-25.25s ",
            isReadOnly ? '+' : ' ', pEntry->fileName + len - 25);
    }
    printf("%s  $%04X  ",
        GetFileTypeString(pEntry->fileType), pEntry->auxType);

    printf("%s  ", FormatDateShort(&pEntry->modWhen, date1));
    if (isSqueezed)
        printf("squ  ");
    else
        printf("unc  ");

    printf("%8u", pEntry->realEOF);

    printf("\n");

    if (!pEntry->filesToFollow) {
        /* last entry, print footer */
        printf("-------------------------------------------------"
               "----------------------\n");
    }

    return err;
}

/*
 * Get a verbose table of contents.
 */
static NuError BNYListDebug(BNYArchive* pBny, BNYEntry* pEntry,
    Boolean* pConsumedFlag)
{
    NuError err = kNuErrNone;

    printf("File name: '%s'  Native name: '%s'  BNY Version %d\n",
        pEntry->fileName, pEntry->nativeName,
        pEntry->version);
    printf("  Phantom: %s  DataFlags: 0x%02x (%s%s%s)\n",
        pEntry->phantomFlag ? "YES" : "no",
        pEntry->dataFlags,
        pEntry->dataFlags & kBNYFlagCompressed ? "compr" : "",
        pEntry->dataFlags & kBNYFlagEncrypted ? "encry" : "",
        pEntry->dataFlags & kBNYFlagSparse ? "sparse" : "");
    printf("  Modified %d/%02d/%02d %02d:%02d  Created %d/%02d/%02d %02d:%02d\n",
        pEntry->modWhen.year+1900, pEntry->modWhen.month,
        pEntry->modWhen.day, pEntry->modWhen.hour,
        pEntry->modWhen.minute,
        pEntry->createWhen.year+1900, pEntry->createWhen.month,
        pEntry->createWhen.day, pEntry->createWhen.hour,
        pEntry->createWhen.minute);
    printf("  FileType: 0x%04x  AuxType: 0x%08x  StorageType: 0x%02x\n",
        pEntry->fileType, pEntry->auxType, pEntry->storageType);
    printf("  EOF: %u  FileSize: %u blocks  DiskSpace: %u blocks\n",
        pEntry->eof, pEntry->fileSize, pEntry->diskSpace);
    printf("  Access: 0x%04x  OSType: %d  NativeFileType: 0x%04x\n",
        pEntry->access, pEntry->osType, pEntry->nativeFileType);
    if (pEntry->realEOF) {
        printf("  *File begins 0x%02x 0x%02x%s\n",
            pEntry->blockBuf[0], pEntry->blockBuf[1],
            IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1]) ?
                " (probably SQueezed)" : "");
    }
    printf("  FilesToFollow: %d\n", pEntry->filesToFollow);

    return err;
}

/* quick enum to simplify our lives a bit */
typedef enum { kBNYExtNormal, kBNYExtPipe, kBNYExtTest } ExtMode;

/*
 * Handle "extraction" of a directory.
 */
static NuError BNYExtractDirectory(BNYArchive* pBny, BNYEntry* pEntry,
    ExtMode extMode)
{
    NuError err = kNuErrNone;
    const char* newName;
    Boolean isDir;
    const char* actionStr = "HOSED";

    if (extMode == kBNYExtTest) {
        actionStr = "skipping ";
    } else if (NState_GetModJunkPaths(pBny->pState)) {
        actionStr = "skipping  ";
    } else {
        /*
         * Using the normalized name of a directory is a problem
         * when "-e" is set.  Since Binary II officially only
         * allows ProDOS names, and everything under the sun that
         * supports "long" filenames can handle 15 chars of
         * [A-Z][a-z][0-9][.], I'm going to skip the normalization
         * step.
         *
         * The "right" way to handle this is to ignore the directory
         * entries and just create any directories for the fully
         * normalized pathname generated below.
         */
        /*newName = BNYNormalizePath(pBny, pEntry);*/
        newName = pEntry->fileName;
        if (newName == NULL)
            goto bail;

        err = TestFileExistence(newName, &isDir);
        if (err == kNuErrNone) {
            if (isDir) {
                actionStr = "skipping  ";
            } else {
                err = kNuErrFileExists;
                ReportError(err,
                    "unable to create directory '%s'", newName);
                goto bail;
            }
        } else if (err == kNuErrFileNotFound) {
            err = Mkdir(newName);

            if (err == kNuErrNone) {
                actionStr = "creating  ";
            } else {
                ReportError(err, "failed creating directory '%s'", newName);
            }
        }
    }

    if (!NState_GetSuppressOutput(pBny->pState)) {
        printf("\rDONE %s  %s  (directory)\n", actionStr, pEntry->fileName);
    }

bail:
    return err;
}


/*
 * Handle "extract", "extract to pipe", and "test".
 */
static NuError BNYExtract(BNYArchive* pBny, BNYEntry* pEntry,
    Boolean* pConsumedFlag)
{
    NuError err = kNuErrNone;
    NulibState* pState;
    ExtMode extMode;
    const char* actionStr = "HOSED";
    FILE* outfp = NULL;
    Boolean eolConv;

    pState = pBny->pState;

    switch (NState_GetCommand(pState)) {
    case kCommandExtract:           extMode = kBNYExtNormal;    break;
    case kCommandExtractToPipe:     extMode = kBNYExtPipe;      break;
    case kCommandTest:              extMode = kBNYExtTest;      break;
    default:
        err = kNuErrInternal;
        ReportError(err, "unexpected command %d in BNYExtract",
            NState_GetCommand(pState));
        Assert(0);
        goto bail;
    }

    /*eolConv = NState_GetModConvertAll(pState);*/
    eolConv = false;        /* maybe someday */

    /*
     * Binary II requires that all directories be listed explicitly.
     * If we see one, create it.
     */
    if (IsDir(pEntry)) {
        err = BNYExtractDirectory(pBny, pEntry, extMode);
        goto bail;
    }

    /*
     * Open the file, taking various command line flags into account.
     * If we're writing to a pipe, just use that.  If we're in test
     * mode, the output goes nowhere.
     */
    if (extMode == kBNYExtPipe) {
        outfp = stdout;
    } else if (extMode == kBNYExtNormal) {
        /*
         * Normalize the filename.  If the file is squeezed, and it ends
         * with ".QQ", strip off the .QQ part.  (The SQ format actually
         * includes a filename within, but it could be confusing to use
         * that instead of the name in the BNY archive, so I've decided to
         * ignore it.)
         */
        const char* newName;
        Boolean isDir;
        char* ext;

        /* if we find .qq on a squeezed file, just stomp the copy in pEntry */
        if (strlen(pEntry->fileName) > 3) {
            ext = pEntry->fileName + strlen(pEntry->fileName) -3;
            if (IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1]) &&
                strcasecmp(ext, ".qq") == 0)
            {
                *ext = '\0';
            }
        }

        newName = BNYNormalizePath(pBny, pEntry);
        if (newName == NULL)
            goto bail;

        err = TestFileExistence(newName, &isDir);
        if (err == kNuErrNone) {
            /* file exists, what to do? */
            if (isDir) {
                err = kNuErrFileExists;
                ReportError(err, "unable to replace directory '%s'", newName);
                goto bail;
            }

            if (!NState_GetModOverwriteExisting(pState)) {
                err = kNuErrFileExists;
                ReportError(err, "unable to overwrite '%s' (try '-s'?)",
                    newName);
                goto bail;
            }
        } else if (err != kNuErrFileNotFound) {
            ReportError(err, "stat failed on '%s'", newName);
            goto bail;
        } else {
            Assert(err == kNuErrFileNotFound);
            err = kNuErrNone;
        }

        /* open it, overwriting anything present */
        outfp = fopen(newName, "w");
        if (outfp == NULL) {
            err = kNuErrFileOpen;
            goto bail;
        }
    } else {
        /* outfp == NULL means we're in test mode */
        Assert(outfp == NULL);
    }

    /*
     * Show initial progress update message ("extracting" or "expanding",
     * depending on whether or not the file was squeezed).
     */
    if (IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1]))
        actionStr = "expanding ";
    else
        actionStr = "extracting";
    if (extMode == kBNYExtTest)
        actionStr = "verifying";
    if (!NState_GetSuppressOutput(pState)) {
        printf("\r  0%% %s%c %s", actionStr, eolConv ? '+' : ' ',
            pEntry->fileName);
    }

    /*
     * Extract the file.  Send the output, perhaps with EOL conversion,
     * to the output file.
     *
     * Thought for the day: assuming a file is Squeezed based only on
     * the magic number is bogus.  If we get certain classes of failures,
     * and this isn't a streaming archive, we should back up and just
     * extract it as a plain file.
     *
     * If the file is empty, don't do anything.
     */
    if (pEntry->realEOF) {
        if (IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1]))
            err = BNYUnSqueeze(pBny, pEntry, outfp);
        else
            err = BNYCopyBlocks(pBny, pEntry, outfp);

        if (err != kNuErrNone)
            goto bail;
    }

    /*
     * Show final progress update.
     */
    if (!NState_GetSuppressOutput(pState)) {
        printf("\rDONE\n");
    }

    *pConsumedFlag = true;

bail:
    if (outfp != NULL && outfp != stdout) {
#if defined(MAC_LIKE)
        if (SetFinderInfo(fileno(outfp), pEntry->fileType,
                 pEntry->auxType) != kNuErrNone)
        {
            fprintf(stderr, "WARNING: unable to set finder info\n");
        }
#endif
        fclose(outfp);
    }
    return err;
}


/*
 * ===========================================================================
 *      Entry points from NuLib2
 * ===========================================================================
 */

NuError BNYDoExtract(NulibState* pState)
{
    if (NState_GetModConvertText(pState) ||
        NState_GetModConvertAll(pState))
    {
        fprintf(stderr,
            "%s: Binary II extraction doesn't support '-l' or '-ll'\n",
            gProgName); 
        return kNuErrSyntax;
    }
    if (NState_GetModUpdate(pState) ||
        NState_GetModFreshen(pState))
    {
        fprintf(stderr,
            "%s: Binary II extraction doesn't support '-f' or '-u'\n",
            gProgName);
        return kNuErrSyntax;
    }
    if (NState_GetModComments(pState))
    {
        fprintf(stderr,
            "%s: Binary II extraction doesn't support '-c'\n",
            gProgName);
        return kNuErrSyntax;
    }

    if (NState_GetCommand(pState) == kCommandExtractToPipe)
        NState_SetSuppressOutput(pState, true);
    return BNYIterate(pState, BNYExtract);
}

NuError BNYDoTest(NulibState* pState)
{
    return BNYIterate(pState, BNYExtract);
}

NuError BNYDoListShort(NulibState* pState)
{
    return BNYIterate(pState, BNYListShort);
}

NuError BNYDoListVerbose(NulibState* pState)
{
    return BNYIterate(pState, BNYListVerbose);
}

NuError BNYDoListDebug(NulibState* pState)
{
    return BNYIterate(pState, BNYListDebug);
}

