/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Code for handling "outer wrappers" like ZIP and gzip.
 *
 * TODO: for safety, these should compress into a temp file and then rename
 * the temp file over the original.  The current implementation just
 * truncates the open file descriptor or reopens the original file.  Both
 * risk data loss if the program or system crashes while the data is being
 * written to disk.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"
#define DEF_MEM_LEVEL 8		// normally in zutil.h


/*
 * ===========================================================================
 *		OuterGzip
 * ===========================================================================
 */

/*
 * Test to see if this is a gzip file.
 *
 * This test is pretty weak, so we shouldn't even be looking at this
 * unless the file ends in ".gz".  A better test would scan the entire
 * header.
 *
 * Would be nice to just gzopen the file, but unfortunately that tries
 * to be "helpful" and reads the file whether it's in gz format or not.
 * Some days I could do with a little less "help".
 */
/*static*/ DIError
OuterGzip::Test(GenericFD* pGFD, di_off_t outerLength)
{
    const int kGzipMagic = 0x8b1f;  // 0x1f 0x8b
    unsigned short magic, magicBuf;
    const char* imagePath;

    WMSG0("Testing for gzip\n");

    /* don't need this here, but we will later on */
    imagePath = pGFD->GetPathName();
    if (imagePath == nil) {
		WMSG0("Can't test gzip on non-file\n");
		return kDIErrNotSupported;
	}

    pGFD->Rewind();

    if (pGFD->Read(&magicBuf, 2) != kDIErrNone)
		return kDIErrGeneric;
    magic = GetShortLE((unsigned char*) &magicBuf);

    if (magic == kGzipMagic)
        return kDIErrNone;
    else
        return kDIErrGeneric;
}


/*
 * The gzip file format has a length embedded in the footer, but
 * unfortunately there is no interface to access it.  So, we have
 * to keep reading until we run out of data, extending the buffer
 * to accommodate the new data each time.  (We could also just read
 * the footer directly, but that requires that there are no garbage
 * bytes at the end of the file, which is a real concern on some FTP sites.)
 *
 * Start out by trying sizes that we think will work (140K, 800K),
 * then grow quickly.
 *
 * The largest possible ProDOS image is 32MB, but it's possible to
 * have an HFS volume or a partitioned image larger than that.  We currently
 * cap the limit to avoid nasty behavior when encountering really
 * large .gz files.  This isn't great -- we ought to support extracting
 * to a temp file, or allowing the caller to specify what the largest
 * size they can handle is.
 */
DIError
OuterGzip::ExtractGzipImage(gzFile gzfp, char** pBuf, di_off_t* pLength)
{
	DIError dierr = kDIErrNone;
    const int kMinEmpty = 256 * 1024;
    const int kStartSize = 141 * 1024;
    const int kNextSize1 = 801 * 1024;
    const int kNextSize2 = 1024 * 1024;
    const int kMaxIncr = 4096 * 1024;
	const int kAbsoluteMax = kMaxUncompressedSize;
    char* buf = nil;
    char* newBuf = nil;
    long curSize, maxSize;

    assert(gzfp != nil);
    assert(pBuf != nil);
    assert(pLength != nil);

    curSize = 0;
    maxSize = kStartSize;

    buf = new char[maxSize];
    if (buf == nil) {
        dierr = kDIErrMalloc;
        goto bail;
    }

    while (1) {
        long len;

		/*
		 * Try to fill the buffer.
		 *
		 * It appears that zlib v1.1.4 was more tolerant of certain kinds
		 * of broken archives than v1.2.1.  Both give you a pile of data
		 * on the first read, with no error reported, but the next read
		 * attempt returns with z_err=-3 (Z_DATA_ERROR) and z_eof set.  I'm
		 * not sure exactly what the flaw is, but I'm guessing something
		 * got lopped off the end of the archives.  gzip v1.3.3 won't touch
		 * them either.
		 *
		 * It would be easy enough to access them, if they were accessible.
		 * Unfortunately the implementation is buried.  Instead, we do
		 * a quick test against known unadorned floppy disk sizes to see
		 * if we can salvage the contents.  (Our read attempts are all
		 * slightly *over* the standard disk sizes, so if it comes back right
		 * on one we're *probably* okay.)
		 */
        len = gzread(gzfp, buf + curSize, maxSize - curSize);
        if (len < 0) {
            WMSG1("  ExGZ Call to gzread failed, errno=%d\n", errno);
			if (curSize == 140*1024 || curSize == 800*1024) {
				WMSG0("WARNING: accepting damaged gzip file\n");
				fWrapperDamaged = true;
				break;		// sleazy, but currently necessary
			}
            dierr = kDIErrReadFailed;
            goto bail;
        } else if (len == 0) {
            /* EOF reached */
            break;
        } else if (len < (maxSize - curSize)) {
            /* we've probably reached the end, but we can't be sure,
               so let's go around again */
            WMSG2("  ExGZ gzread(%ld) returned %ld, letting it ride\n",
                maxSize - curSize, len);
            curSize += len;
        } else {
            /* update buffer, and grow it if it's not big enough */
            curSize += len;
            WMSG2("  max=%ld cur=%ld\n", maxSize, curSize);
            if (maxSize - curSize < kMinEmpty) {
                /* not enough room, grow it */

                if (maxSize == kStartSize)
                    maxSize = kNextSize1;
                else if (maxSize == kNextSize1)
                    maxSize = kNextSize2;
                else {
                    if (maxSize < kMaxIncr)
                        maxSize = maxSize * 2;
                    else
                        maxSize += kMaxIncr;
                }

                newBuf = new char[maxSize];
                if (newBuf == nil) {
                    WMSG1("  ExGZ failed buffer alloc (%ld)\n",
                        maxSize);
                    dierr = kDIErrMalloc;
                    goto bail;
                }

                memcpy(newBuf, buf, curSize);
                delete[] buf;
                buf = newBuf;
                newBuf = nil;

                WMSG1("  ExGZ grew buffer to %ld\n", maxSize);
            } else {
                /* don't need to grow buffer yet */
                WMSG3("  ExGZ read %ld bytes, cur=%ld max=%ld\n",
                    len, curSize, maxSize);
            }
        }
        assert(curSize < maxSize);

		if (curSize > kAbsoluteMax) {
			WMSG0("  ExGZ excessive size, probably not a disk image\n");
			dierr = kDIErrTooBig;	// close enough
			goto bail;
		}
    }

    if (curSize + (1024*1024) < maxSize) {
        /* shrink it down so it fits */
        WMSG2("  Down-sizing buffer from %ld to %ld\n", maxSize, curSize);
        newBuf = new char[curSize];
        if (newBuf == nil)
            goto bail;
        memcpy(newBuf, buf, curSize);
        delete[] buf;
        buf = newBuf;
        newBuf = nil;
    }

    *pBuf = buf;
    *pLength = curSize;
    WMSG1("  ExGZ final size = %ld\n", curSize);

    buf = nil;

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

/*
 * Open the archive, and extract the disk image into a memory buffer.
 */
DIError
OuterGzip::Load(GenericFD* pOuterGFD, di_off_t outerLength, bool readOnly,
    di_off_t* pWrapperLength, GenericFD** ppWrapperGFD)
{
	DIError dierr = kDIErrNone;
	GFDBuffer* pNewGFD = nil;
	char* buf = nil;
	di_off_t length = -1;
    const char* imagePath;
	gzFile gzfp = nil;

    imagePath = pOuterGFD->GetPathName();
	if (imagePath == nil) {
		assert(false);		// should've been caught in Test
		return kDIErrNotSupported;
	}

    gzfp = gzopen(imagePath, "rb");        // use "readOnly" here
    if (gzfp == nil) { // DON'T retry RO -- should be done at higher level?
        WMSG1("gzopen failed, errno=%d\n", errno);
		dierr = kDIErrGeneric;
		goto bail;
	}

	dierr = ExtractGzipImage(gzfp, &buf, &length);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Everything is going well.  Now we substitute a memory-based GenericFD
	 * for the existing GenericFD.
     */
	pNewGFD = new GFDBuffer;
	dierr = pNewGFD->Open(buf, length, true, false, readOnly);
	if (dierr != kDIErrNone)
		goto bail;
	buf = nil;		// now owned by pNewGFD;

	/*
	 * Success!
	 */
	assert(dierr == kDIErrNone);
	*ppWrapperGFD = pNewGFD;
    pNewGFD = nil;

	*pWrapperLength = length;

bail:
	if (dierr != kDIErrNone) {
		delete pNewGFD;
	}
    if (gzfp != nil)
        gzclose(gzfp);
	return dierr;
}

/*
 * Save the contents of "pWrapperGFD" to the file pointed to by
 * "pOuterGFD".
 *
 * "pOuterGFD" isn't disturbed (same as Load).  All we want is to get the
 * filename and then do everything through gzio.
 */
DIError
OuterGzip::Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD,
    di_off_t wrapperLength)
{
	DIError dierr = kDIErrNone;
    const char* imagePath;
	gzFile gzfp = nil;

    WMSG1(" GZ save (wrapperLen=%ld)\n", (long) wrapperLength);
    assert(wrapperLength > 0);

    /*
     * Reopen the file.
     */
    imagePath = pOuterGFD->GetPathName();
	if (imagePath == nil) {
		assert(false);		// should've been caught long ago
		return kDIErrNotSupported;
	}

    gzfp = gzopen(imagePath, "wb");
    if (gzfp == nil) {
        WMSG1("gzopen for write failed, errno=%d\n", errno);
		dierr = kDIErrGeneric;
		goto bail;
	}

    char buf[16384];
    size_t actual;
    long written, totalWritten;

    pWrapperGFD->Rewind();

    totalWritten = 0;
    while (wrapperLength > 0) {
        dierr = pWrapperGFD->Read(buf, sizeof(buf), &actual);
        if (dierr == kDIErrEOF) {
            dierr = kDIErrNone;
            break;
        }
        if (dierr != kDIErrNone) {
            WMSG1("Error reading source GFD during gzip save (err=%d)\n",dierr);
            goto bail;
        }
        assert(actual > 0);

        written = gzwrite(gzfp, buf, actual);
        if (written == 0) {
            WMSG1("Failed writing %d bytes to gzio\n", actual);
            dierr = kDIErrGeneric;
            goto bail;
        }

        totalWritten += written;
        wrapperLength -= actual;
    }
    assert(wrapperLength == 0);     // not expecting any slop

    WMSG1(" GZ wrote %ld bytes\n", totalWritten);

	/*
	 * Success!
	 */
	assert(dierr == kDIErrNone);

bail:
    if (gzfp != nil)
        gzclose(gzfp);
	return dierr;
}


/*
 * ===========================================================================
 *		OuterZip
 * ===========================================================================
 */

/*
 * Test to see if this is a ZIP archive.
 */
/*static*/ DIError
OuterZip::Test(GenericFD* pGFD, di_off_t outerLength)
{
	DIError dierr = kDIErrNone;
	CentralDirEntry cde;

    WMSG0("Testing for zip\n");
	dierr = ReadCentralDir(pGFD, outerLength, &cde);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Make sure it's a compression method we support.
	 */
	if (cde.fCompressionMethod != kCompressStored &&
		cde.fCompressionMethod != kCompressDeflated)
	{
		WMSG1(" ZIP compression method %d not supported\n",
			cde.fCompressionMethod);
		dierr = kDIErrGeneric;
		goto bail;
	}

	/*
	 * Limit the size to something reasonable.
	 */
	if (cde.fUncompressedSize < 512 ||
		cde.fUncompressedSize > kMaxUncompressedSize)
	{
		WMSG1(" ZIP uncompressed size %lu is outside range\n",
			cde.fUncompressedSize);
		dierr = kDIErrGeneric;
		goto bail;
	}

	assert(dierr == kDIErrNone);

bail:
	return dierr;
}

/*
 * Open the archive, and extract the disk image into a memory buffer.
 */
DIError
OuterZip::Load(GenericFD* pOuterGFD, di_off_t outerLength, bool readOnly,
    di_off_t* pWrapperLength, GenericFD** ppWrapperGFD)
{
	DIError dierr = kDIErrNone;
	GFDBuffer* pNewGFD = nil;
	CentralDirEntry cde;
	unsigned char* buf = nil;
	di_off_t length = -1;
	const char* pExt;

	dierr = ReadCentralDir(pOuterGFD, outerLength, &cde);
	if (dierr != kDIErrNone)
		goto bail;

	if (cde.fFileNameLength > 0) {
		pExt = FindExtension((const char*) cde.fFileName, kZipFssep);
		if (pExt != nil) {
			assert(*pExt == '.');
			SetExtension(pExt+1);

			WMSG1("OuterZip using extension '%s'\n", GetExtension());
		}

		SetStoredFileName((const char*) cde.fFileName);
	}

	dierr = ExtractZipEntry(pOuterGFD, &cde, &buf, &length);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Everything is going well.  Now we substitute a memory-based GenericFD
	 * for the existing GenericFD.
     */
	pNewGFD = new GFDBuffer;
	dierr = pNewGFD->Open(buf, length, true, false, readOnly);
	if (dierr != kDIErrNone)
		goto bail;
	buf = nil;		// now owned by pNewGFD;

	/*
	 * Success!
	 */
	assert(dierr == kDIErrNone);
	*ppWrapperGFD = pNewGFD;
    pNewGFD = nil;

	*pWrapperLength = length;

bail:
	if (dierr != kDIErrNone) {
		delete pNewGFD;
	}
	return dierr;
}

/*
 * Save the contents of "pWrapperGFD" to the file pointed to by
 * "pOuterGFD".
 */
DIError
OuterZip::Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD,
    di_off_t wrapperLength)
{
	DIError dierr = kDIErrNone;
	LocalFileHeader lfh;
	CentralDirEntry cde;
	EndOfCentralDir eocd;
	di_off_t lfhOffset;

    WMSG1(" ZIP save (wrapperLen=%ld)\n", (long) wrapperLength);
    assert(wrapperLength > 0);

	dierr = pOuterGFD->Rewind();
	if (dierr != kDIErrNone)
		goto bail;
	dierr = pOuterGFD->Truncate();
	if (dierr != kDIErrNone)
		goto bail;

	dierr = pWrapperGFD->Rewind();
	if (dierr != kDIErrNone)
		goto bail;

	lfhOffset = pOuterGFD->Tell();		// always 0 with only one file

    /*
     * Don't store an empty filename.  Some applications, e.g. Info-ZIP's
     * "unzip", get confused.  Ideally the DiskImg image creation code
     * will have set the actual filename, with an extension that matches
     * the file contents.
     */
    if (fStoredFileName == nil || fStoredFileName[0] == '\0')
        SetStoredFileName("disk");

	/*
	 * Write the ZIP local file header.  We don't have file lengths or
	 * CRCs yet, so we have to go back and fill those in later.
	 */
	lfh.fVersionToExtract = kDefaultVersion;
#if NO_ZIP_COMPRESS
	lfh.fGPBitFlag = 0;
	lfh.fCompressionMethod = 0;
#else
	lfh.fGPBitFlag = 0x0002;		// indicates maximum compression used
	lfh.fCompressionMethod = 8;		//  when compressionMethod == deflate
#endif
	GetMSDOSTime(&lfh.fLastModFileDate, &lfh.fLastModFileTime);
	lfh.SetFileName(fStoredFileName);
	dierr = lfh.Write(pOuterGFD);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Write the compressed data.
	 */
	unsigned long crc;
	di_off_t compressedLen;
	if (lfh.fCompressionMethod == kCompressDeflated) {
		dierr = DeflateGFDToGFD(pOuterGFD, pWrapperGFD, wrapperLength,
					&compressedLen, &crc);
		if (dierr != kDIErrNone)
			goto bail;
	} else if (lfh.fCompressionMethod == kCompressStored) {
		dierr = GenericFD::CopyFile(pOuterGFD, pWrapperGFD, wrapperLength,
					&crc);
		if (dierr != kDIErrNone)
			goto bail;
		compressedLen = wrapperLength;
	} else {
		assert(false);
		dierr = kDIErrInternal;
		goto bail;
	}

	/*
	 * Go back and take care of the local file header stuff.
	 *
	 * It's not supposed to be necessary, but some utilities (WinZip,
	 * Info-ZIP) get bent out of shape if these aren't set and the data
	 * is compressed.  They seem okay with it when the file isn't
	 * compressed.  I don't understand this behavior, but writing the
	 * local file header is easy enough.
	 */
	lfh.fCRC32 = crc;
	lfh.fCompressedSize = (unsigned long) compressedLen;
	lfh.fUncompressedSize = (unsigned long) wrapperLength;

	di_off_t curPos;
	curPos = pOuterGFD->Tell();
	dierr = pOuterGFD->Seek(lfhOffset, kSeekSet);
	if (dierr != kDIErrNone)
		goto bail;
	dierr = lfh.Write(pOuterGFD);
	if (dierr != kDIErrNone)
		goto bail;
	dierr = pOuterGFD->Seek(curPos, kSeekSet);
	if (dierr != kDIErrNone)
		goto bail;

	di_off_t cdeStart, cdeFinish;
	cdeStart = pOuterGFD->Tell();

	/*
	 * Write the central dir entry.  This is largely just a copy of the
	 * data in the local file header (and in fact some utilities will
	 * get rather bent out of shape if the two don't match exactly).
	 */
	cde.fVersionMadeBy = kDefaultVersion;
	cde.fVersionToExtract = lfh.fVersionToExtract;
	cde.fGPBitFlag = lfh.fGPBitFlag;
	cde.fCompressionMethod = lfh.fCompressionMethod;
	cde.fLastModFileDate = lfh.fLastModFileDate;
	cde.fLastModFileTime = lfh.fLastModFileTime;
	cde.fCRC32 = lfh.fCRC32;
	cde.fCompressedSize = lfh.fCompressedSize;
	cde.fUncompressedSize = lfh.fUncompressedSize;
	assert(lfh.fExtraFieldLength == 0 && cde.fExtraFieldLength == 0);
	cde.fExternalAttrs = 0x81b60020;	// matches what WinZip does
	cde.fLocalHeaderRelOffset = (unsigned long) lfhOffset;
	cde.SetFileName(fStoredFileName);
	dierr = cde.Write(pOuterGFD);
	if (dierr != kDIErrNone)
		goto bail;

	cdeFinish = pOuterGFD->Tell();

	/*
	 * Write the end-of-central-dir stuff.
	 */
	eocd.fNumEntries = 1;
	eocd.fTotalNumEntries = 1;
	eocd.fCentralDirSize = (unsigned long) (cdeFinish - cdeStart);
	eocd.fCentralDirOffset = (unsigned long) cdeStart;
	assert(eocd.fCentralDirSize >= EndOfCentralDir::kEOCDLen);
	dierr = eocd.Write(pOuterGFD);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Success!
	 */
	assert(dierr == kDIErrNone);

bail:
	return dierr;
}


/*
 * Track the name of the file stored in the ZIP archive.
 */
void
OuterZip::SetStoredFileName(const char* name)
{
	delete[] fStoredFileName;
	fStoredFileName = StrcpyNew(name);
}


/*
 * Find the central directory and read the contents.
 *
 * We currently only support archives with a single entry.
 *
 * The fun thing about ZIP archives is that they may or may not be
 * readable from start to end.  In some cases, notably for archives
 * that were written to stdout, the only length information is in the
 * central directory at the end of the file.
 *
 * Of course, the central directory can be followed by a variable-length
 * comment field, so we have to scan through it backwards.  The comment
 * is at most 64K, plus we have 18 bytes for the end-of-central-dir stuff
 * itself, plus apparently sometimes people throw random junk on the end
 * just for the fun of it.
 *
 * This is all a little wobbly.  If the wrong value ends up in the EOCD
 * area, we're hosed.  This appears to be the way that the Info-ZIP guys
 * do it though, so we're in pretty good company if this fails.
 */
/*static*/ DIError
OuterZip::ReadCentralDir(GenericFD* pGFD, di_off_t outerLength,
	CentralDirEntry* pDirEntry)
{
	DIError dierr = kDIErrNone;
	EndOfCentralDir eocd;
	unsigned char* buf = nil;
	di_off_t seekStart;
	long readAmount;
	int i;

	/* too small to be a ZIP archive? */
	if (outerLength < EndOfCentralDir::kEOCDLen + 4)
		return kDIErrGeneric;

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

	if (outerLength > kMaxEOCDSearch) {
		seekStart = outerLength - kMaxEOCDSearch;
		readAmount = kMaxEOCDSearch;
	} else {
		seekStart = 0;
		readAmount = (long) outerLength;
	}
	dierr = pGFD->Seek(seekStart, kSeekSet);
	if (dierr != kDIErrNone)
		goto bail;

	/* read the last part of the file into the buffer */
	dierr = pGFD->Read(buf, readAmount);
	if (dierr != kDIErrNone)
		goto bail;

	/* find the end-of-central-dir magic */
	for (i = readAmount - 4; i >= 0; i--) {
		if (buf[i] == 0x50 &&
			GetLongLE(&buf[i]) == EndOfCentralDir::kSignature)
		{
			WMSG1("+++ Found EOCD at buf+%d\n", i);
			break;
		}
	}
	if (i < 0) {
		WMSG0("+++ EOCD not found, not ZIP\n");
		dierr = kDIErrGeneric;
		goto bail;
	}

	/* extract eocd values */
	dierr = eocd.ReadBuf(buf + i, readAmount - i);
	if (dierr != kDIErrNone)
		goto bail;
	eocd.Dump();

	if (eocd.fDiskNumber != 0 || eocd.fDiskWithCentralDir != 0 ||
		eocd.fNumEntries != 1 || eocd.fTotalNumEntries != 1)
	{
		WMSG0(" Probable ZIP archive has more than one member\n");
		dierr = kDIErrFileArchive;
		goto bail;
	}

	/*
	 * So far so good.  "fCentralDirSize" is the size in bytes of the
	 * central directory, so we can just seek back that far to find it.
	 * We can also seek forward fCentralDirOffset bytes from the
	 * start of the file.
	 *
	 * We're not guaranteed to have the rest of the central dir in the
	 * buffer, nor are we guaranteed that the central dir will have any
	 * sort of convenient size.  We need to skip to the start of it and
	 * read the header, then the other goodies.
	 *
	 * The only thing we really need right now is the file comment, which
	 * we're hoping to preserve.
	 */
	dierr = pGFD->Seek(eocd.fCentralDirOffset, kSeekSet);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Read the central dir entry.
	 */
	dierr = pDirEntry->Read(pGFD);
	if (dierr != kDIErrNone)
		goto bail;

	pDirEntry->Dump();

	{
		unsigned char checkBuf[4];
		dierr = pGFD->Read(checkBuf, 4);
		if (dierr != kDIErrNone)
			goto bail;
		if (GetLongLE(checkBuf) != EndOfCentralDir::kSignature) {
			WMSG0("CDE read check failed\n");
			assert(false);
			dierr = kDIErrGeneric;
			goto bail;
		}
		WMSG0("+++ CDE read check passed\n");
	}

bail:
	delete[] buf;
	return dierr;
}


/*
 * The central directory tells us where to find the local header.  We
 * have to skip over that to get to the start of the data.s
 */
DIError
OuterZip::ExtractZipEntry(GenericFD* pOuterGFD, CentralDirEntry* pCDE,
	unsigned char** pBuf, di_off_t* pLength)
{
	DIError dierr = kDIErrNone;
	LocalFileHeader lfh;
	unsigned char* buf = nil;

	/* seek to the start of the local header */
	dierr = pOuterGFD->Seek(pCDE->fLocalHeaderRelOffset, kSeekSet);
	if (dierr != kDIErrNone)
		goto bail;

	/*
	 * Read the local file header, mainly as a way to get past it.  There
	 * are legitimate reasons why the size fields and filename might be
	 * empty, so we really don't want to depend on any data in the LFH.
	 * We just need to find where the data starts.
	 */
	dierr = lfh.Read(pOuterGFD);
	if (dierr != kDIErrNone)
		goto bail;
	lfh.Dump();

	/* we should now be pointing at the data */
	WMSG1("File offset is 0x%08lx\n", (long) pOuterGFD->Tell());

	buf = new unsigned char[pCDE->fUncompressedSize];
	if (buf == nil) {
		/* a very real possibility */
		WMSG1(" ZIP unable to allocate buffer of %lu bytes\n",
			pCDE->fUncompressedSize);
		dierr = kDIErrMalloc;
		goto bail;
	}

	/* unpack or copy the data */
	if (pCDE->fCompressionMethod == kCompressDeflated) {
		dierr = InflateGFDToBuffer(pOuterGFD, pCDE->fCompressedSize,
					pCDE->fUncompressedSize, buf);
		if (dierr != kDIErrNone)
			goto bail;
	} else if (pCDE->fCompressionMethod == kCompressStored) {
		dierr = pOuterGFD->Read(buf, pCDE->fUncompressedSize);
		if (dierr != kDIErrNone)
			goto bail;
	} else {
		assert(false);
		dierr = kDIErrInternal;
		goto bail;
	}

	/* check the CRC32 */
	unsigned long crc;
	crc = crc32(0L, Z_NULL, 0);
	crc = crc32(crc, buf, pCDE->fUncompressedSize);

	if (crc == pCDE->fCRC32) {
		WMSG0("+++ ZIP CRCs match\n");
	} else {
		WMSG2("ZIP CRC mismatch: inflated crc32=0x%08lx, stored=0x%08lx\n",
			crc, pCDE->fCRC32);
		dierr = kDIErrBadChecksum;
		goto bail;
	}

    *pBuf = buf;
    *pLength = pCDE->fUncompressedSize;

    buf = nil;

bail:
    delete[] buf;
    return dierr;
}

/*
 * Uncompress data from "pOuterGFD" to "buf".
 *
 * "buf" must be able to hold "uncompSize" bytes.
 */
DIError
OuterZip::InflateGFDToBuffer(GenericFD* pGFD, unsigned long compSize,
	unsigned long uncompSize, unsigned char* buf)
{
    DIError dierr = kDIErrNone;
	const unsigned long kReadBufSize = 65536;
	unsigned char* readBuf = nil;
    z_stream zstream;
    int zerr;
    unsigned long compRemaining;

	readBuf = new unsigned char[kReadBufSize];
	if (readBuf == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}
    compRemaining = compSize;

    /*
     * Initialize the zlib stream.
     */
	memset(&zstream, 0, sizeof(zstream));
    zstream.zalloc = Z_NULL;
    zstream.zfree = Z_NULL;
    zstream.opaque = Z_NULL;
    zstream.next_in = nil;
    zstream.avail_in = 0;
    zstream.next_out = buf;
    zstream.avail_out = uncompSize;
    zstream.data_type = Z_UNKNOWN;

	/*
	 * Use the undocumented "negative window bits" feature to tell zlib
	 * that there's no zlib header waiting for it.
	 */
    zerr = inflateInit2(&zstream, -MAX_WBITS);
    if (zerr != Z_OK) {
        dierr = kDIErrInternal;
        if (zerr == Z_VERSION_ERROR) {
            WMSG1("Installed zlib is not compatible with linked version (%s)\n",
                ZLIB_VERSION);
        } else {
            WMSG1("Call to inflateInit2 failed (zerr=%d)\n", zerr);
        }
        goto bail;
    }

    /*
     * Loop while we have data.
     */
    do {
        unsigned long getSize;

        /* read as much as we can */
        if (zstream.avail_in == 0) {
            getSize = (compRemaining > kReadBufSize) ?
                        kReadBufSize : compRemaining;
            WMSG2("+++ reading %ld bytes (%ld left)\n", getSize,
                compRemaining);

			dierr = pGFD->Read(readBuf, getSize);
			if (dierr != kDIErrNone) {
                WMSG0("inflate read failed\n");
                goto z_bail;
            }

            compRemaining -= getSize;

            zstream.next_in = readBuf;
            zstream.avail_in = getSize;
        }

        /* uncompress the data */
        zerr = inflate(&zstream, Z_NO_FLUSH);
        if (zerr != Z_OK && zerr != Z_STREAM_END) {
            dierr = kDIErrInternal;
            WMSG1("zlib inflate call failed (zerr=%d)\n", zerr);
            goto z_bail;
        }

		/* output buffer holds all, so no need to write the output */
    } while (zerr == Z_OK);

    assert(zerr == Z_STREAM_END);       /* other errors should've been caught */

    if (zstream.total_out != uncompSize) {
        dierr = kDIErrBadCompressedData;
        WMSG2("Size mismatch on inflated file (%ld vs %ld)\n",
            zstream.total_out, uncompSize);
        goto z_bail;
    }

z_bail:
    inflateEnd(&zstream);        /* free up any allocated structures */

bail:
	delete[] readBuf;
    return dierr;
}


/*
 * Get the current date/time, in MS-DOS format.
 */
void
OuterZip::GetMSDOSTime(unsigned short* pDate, unsigned short* pTime)
{
#if 0
	/* this gets gmtime; we want localtime */
	SYSTEMTIME sysTime;
	FILETIME fileTime;
	::GetSystemTime(&sysTime);
	::SystemTimeToFileTime(&sysTime, &fileTime);
	::FileTimeToDosDateTime(&fileTime, pDate, pTime);
	//WMSG3("+++ Windows date: %04x %04x %d\n", *pDate, *pTime,
	//	(*pTime >> 11) & 0x1f);
#endif

	time_t now = time(nil);
	DOSTime(now, pDate, pTime);
	//WMSG3("+++ Our date    : %04x %04x %d\n", *pDate, *pTime,
	//	(*pTime >> 11) & 0x1f);
}

/*
 * Convert a time_t to MS-DOS date and time values.
 */
void
OuterZip::DOSTime(time_t when, unsigned short* pDate, unsigned short* pTime)
{
	time_t even;

	*pDate = *pTime = 0;

	struct tm* ptm;

	/* round up to an even number of seconds */
	even = (time_t)(((unsigned long)(when) + 1) & (~1));

	/* expand */
	ptm = localtime(&even);

	int year;
	year = ptm->tm_year;
	if (year < 80)
		year = 80;

	*pDate = (year - 80) << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday;
	*pTime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1;
}


/*
 * Compress "length" bytes of data from "pSrc" to "pDst".
 */
DIError
OuterZip::DeflateGFDToGFD(GenericFD* pDst, GenericFD* pSrc, di_off_t srcLen,
	di_off_t* pCompLength, unsigned long* pCRC)
{
	DIError dierr = kDIErrNone;
	const unsigned long kBufSize = 32768;
	unsigned char* inBuf = nil;
	unsigned char* outBuf = nil;
	z_stream zstream;
	unsigned long crc;
	int zerr;

	/*
	 * Create an input buffer and an output buffer.
	 */
	inBuf = new unsigned char[kBufSize];
	outBuf = new unsigned char[kBufSize];
	if (inBuf == nil || outBuf == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	/*
	 * Initialize the zlib stream.
	 */
	memset(&zstream, 0, sizeof(zstream));
	zstream.zalloc = Z_NULL;
	zstream.zfree = Z_NULL;
	zstream.opaque = Z_NULL;
	zstream.next_in = nil;
	zstream.avail_in = 0;
	zstream.next_out = outBuf;
	zstream.avail_out = kBufSize;
	zstream.data_type = Z_UNKNOWN;

	zerr = deflateInit2(&zstream, Z_BEST_COMPRESSION,
		Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY);
	if (zerr != Z_OK) {
		dierr = kDIErrInternal;
		if (zerr == Z_VERSION_ERROR) {
			WMSG1("Installed zlib is not compatible with linked version (%s)\n",
				ZLIB_VERSION);
		} else {
			WMSG1("Call to deflateInit2 failed (zerr=%d)\n", zerr);
		}
		goto bail;
	}

 	crc = crc32(0L, Z_NULL, 0);

	/*
	 * Loop while we have data.
	 */
	do {
		long getSize;
		int flush;

		/* only read if the input is empty */
		if (zstream.avail_in == 0 && srcLen) {
			getSize = (srcLen > kBufSize) ? kBufSize : (long) srcLen;
			WMSG1("+++ reading %ld bytes\n", getSize);

			dierr = pSrc->Read(inBuf, getSize);
			if (dierr != kDIErrNone) {
				WMSG0("deflate read failed\n");
				goto z_bail;
			}

			srcLen -= getSize;

			crc = crc32(crc, inBuf, getSize);

			zstream.next_in = inBuf;
			zstream.avail_in = getSize;
		}

		if (srcLen == 0)
			flush = Z_FINISH;       /* tell zlib that we're done */
		else
			flush = Z_NO_FLUSH;     /* more to come! */

		zerr = deflate(&zstream, flush);
		if (zerr != Z_OK && zerr != Z_STREAM_END) {
			WMSG1("zlib deflate call failed (zerr=%d)\n", zerr);
			dierr = kDIErrInternal;
			goto z_bail;
		}

		/* write when we're full or when we're done */
		if (zstream.avail_out == 0 ||
			(zerr == Z_STREAM_END && zstream.avail_out != kBufSize))
		{
			WMSG1("+++ writing %d bytes\n", zstream.next_out - outBuf);
			dierr = pDst->Write(outBuf, zstream.next_out - outBuf);
			if (dierr != kDIErrNone) {
				WMSG0("write failed in deflate\n");
				goto z_bail;
			}

			zstream.next_out = outBuf;
			zstream.avail_out = kBufSize;
		}
	} while (zerr == Z_OK);

	assert(zerr == Z_STREAM_END);       /* other errors should've been caught */

	*pCompLength = zstream.total_out;
	*pCRC = crc;

z_bail:
	deflateEnd(&zstream);        /* free up any allocated structures */

bail:
	delete[] inBuf;
	delete[] outBuf;

	return dierr;
}

/*
 * Set the "fExtension" field.
 */
void
OuterZip::SetExtension(const char* ext)
{
	delete[] fExtension;
	fExtension = StrcpyNew(ext);
}


/*
 * ===================================
 *		OuterZip::LocalFileHeader
 * ===================================
 */

/*
 * Read a local file header.
 *
 * On entry, "pGFD" points to the signature at the start of the header.
 * On exit, "pGFD" points to the start of data.
 */
DIError
OuterZip::LocalFileHeader::Read(GenericFD* pGFD)
{
	DIError dierr = kDIErrNone;
	unsigned char buf[kLFHLen];

	dierr = pGFD->Read(buf, kLFHLen);
	if (dierr != kDIErrNone)
		goto bail;

	if (GetLongLE(&buf[0x00]) != kSignature) {
		WMSG0(" ZIP: whoops: didn't find expected signature\n");
		assert(false);
		return kDIErrGeneric;
	}

	fVersionToExtract = GetShortLE(&buf[0x04]);
	fGPBitFlag = GetShortLE(&buf[0x06]);
	fCompressionMethod = GetShortLE(&buf[0x08]);
	fLastModFileTime = GetShortLE(&buf[0x0a]);
	fLastModFileDate = GetShortLE(&buf[0x0c]);
	fCRC32 = GetLongLE(&buf[0x0e]);
	fCompressedSize = GetLongLE(&buf[0x12]);
	fUncompressedSize = GetLongLE(&buf[0x16]);
	fFileNameLength = GetShortLE(&buf[0x1a]);
	fExtraFieldLength = GetShortLE(&buf[0x1c]);

	/* grab filename */
	if (fFileNameLength != 0) {
		assert(fFileName == nil);
		fFileName = new unsigned char[fFileNameLength+1];
		if (fFileName == nil) {
			dierr = kDIErrMalloc;
			goto bail;
		} else {
			dierr = pGFD->Read(fFileName, fFileNameLength);
			fFileName[fFileNameLength] = '\0';
		}
		if (dierr != kDIErrNone)
			goto bail;
	}

	dierr = pGFD->Seek(fExtraFieldLength, kSeekCur);
	if (dierr != kDIErrNone)
		goto bail;

bail:
	return dierr;
}

/*
 * Write a local file header.
 */
DIError
OuterZip::LocalFileHeader::Write(GenericFD* pGFD)
{
	DIError dierr = kDIErrNone;
	unsigned char buf[kLFHLen];

	PutLongLE(&buf[0x00], kSignature);
	PutShortLE(&buf[0x04], fVersionToExtract);
	PutShortLE(&buf[0x06], fGPBitFlag);
	PutShortLE(&buf[0x08], fCompressionMethod);
	PutShortLE(&buf[0x0a], fLastModFileTime);
	PutShortLE(&buf[0x0c], fLastModFileDate);
	PutLongLE(&buf[0x0e], fCRC32);
	PutLongLE(&buf[0x12], fCompressedSize);
	PutLongLE(&buf[0x16], fUncompressedSize);
	PutShortLE(&buf[0x1a], fFileNameLength);
	PutShortLE(&buf[0x1c], fExtraFieldLength);

	dierr = pGFD->Write(buf, kLFHLen);
	if (dierr != kDIErrNone)
		goto bail;

	/* write filename */
	if (fFileNameLength != 0) {
		dierr = pGFD->Write(fFileName, fFileNameLength);
		if (dierr != kDIErrNone)
			goto bail;
	}
	assert(fExtraFieldLength == 0);

bail:
	return dierr;
}

/*
 * Change the filename field.
 */
void
OuterZip::LocalFileHeader::SetFileName(const char* name)
{
	delete[] fFileName;
	fFileName = nil;
	fFileNameLength = 0;

	if (name != nil) {
		fFileNameLength = strlen(name);
		fFileName = new unsigned char[fFileNameLength+1];
		if (fFileName == nil) {
			WMSG1("Malloc failure in SetFileName %u\n", fFileNameLength);
			fFileName = nil;
			fFileNameLength = 0;
		} else {
			memcpy(fFileName, name, fFileNameLength);
			fFileName[fFileNameLength] = '\0';
			WMSG1("+++ OuterZip LFH filename set to '%s'\n", fFileName);
		}
	}
}

/*
 * Dump the contents of a LocalFileHeader object.
 */
void
OuterZip::LocalFileHeader::Dump(void) const
{
	WMSG0(" LocalFileHeader contents:\n");
	WMSG3("  versToExt=%u gpBits=0x%04x compression=%u\n",
		fVersionToExtract, fGPBitFlag, fCompressionMethod);
	WMSG3("  modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n",
		fLastModFileTime, fLastModFileDate, fCRC32);
	WMSG2("  compressedSize=%lu uncompressedSize=%lu\n",
		fCompressedSize, fUncompressedSize);
	WMSG2("  filenameLen=%u extraLen=%u\n",
		fFileNameLength, fExtraFieldLength);
}


/*
 * ===================================
 *		OuterZip::CentralDirEntry
 * ===================================
 */

/*
 * Read the central dir entry that appears next in the file.
 *
 * On entry, "pGFD" should be positioned on the signature bytes for the
 * entry.  On exit, "pGFD" will point at the signature word for the next
 * entry or for the EOCD.
 */
DIError
OuterZip::CentralDirEntry::Read(GenericFD* pGFD)
{
	DIError dierr = kDIErrNone;
	unsigned char buf[kCDELen];

	dierr = pGFD->Read(buf, kCDELen);
	if (dierr != kDIErrNone)
		goto bail;

	if (GetLongLE(&buf[0x00]) != kSignature) {
		WMSG0(" ZIP: whoops: didn't find expected signature\n");
		assert(false);
		return kDIErrGeneric;
	}

	fVersionMadeBy = GetShortLE(&buf[0x04]);
	fVersionToExtract = GetShortLE(&buf[0x06]);
	fGPBitFlag = GetShortLE(&buf[0x08]);
	fCompressionMethod = GetShortLE(&buf[0x0a]);
	fLastModFileTime = GetShortLE(&buf[0x0c]);
	fLastModFileDate = GetShortLE(&buf[0x0e]);
	fCRC32 = GetLongLE(&buf[0x10]);
	fCompressedSize = GetLongLE(&buf[0x14]);
	fUncompressedSize = GetLongLE(&buf[0x18]);
	fFileNameLength = GetShortLE(&buf[0x1c]);
	fExtraFieldLength = GetShortLE(&buf[0x1e]);
	fFileCommentLength = GetShortLE(&buf[0x20]);
	fDiskNumberStart = GetShortLE(&buf[0x22]);
	fInternalAttrs = GetShortLE(&buf[0x24]);
	fExternalAttrs = GetLongLE(&buf[0x26]);
	fLocalHeaderRelOffset = GetLongLE(&buf[0x2a]);

	/* grab filename */
	if (fFileNameLength != 0) {
		assert(fFileName == nil);
		fFileName = new unsigned char[fFileNameLength+1];
		if (fFileName == nil) {
			dierr = kDIErrMalloc;
			goto bail;
		} else {
			dierr = pGFD->Read(fFileName, fFileNameLength);
			fFileName[fFileNameLength] = '\0';
		}
		if (dierr != kDIErrNone)
			goto bail;
	}

	/* skip over "extra field" */
	dierr = pGFD->Seek(fExtraFieldLength, kSeekCur);
	if (dierr != kDIErrNone)
		goto bail;

	/* grab comment, if any */
	if (fFileCommentLength != 0) {
		assert(fFileComment == nil);
		fFileComment = new unsigned char[fFileCommentLength+1];
		if (fFileComment == nil) {
			dierr = kDIErrMalloc;
			goto bail;
		} else {
			dierr = pGFD->Read(fFileComment, fFileCommentLength);
			fFileComment[fFileCommentLength] = '\0';
		}
		if (dierr != kDIErrNone)
			goto bail;
	}

bail:
	return dierr;
}

/*
 * Write a central dir entry.
 */
DIError
OuterZip::CentralDirEntry::Write(GenericFD* pGFD)
{
	DIError dierr = kDIErrNone;
	unsigned char buf[kCDELen];

	PutLongLE(&buf[0x00], kSignature);
	PutShortLE(&buf[0x04], fVersionMadeBy);
	PutShortLE(&buf[0x06], fVersionToExtract);
	PutShortLE(&buf[0x08], fGPBitFlag);
	PutShortLE(&buf[0x0a], fCompressionMethod);
	PutShortLE(&buf[0x0c], fLastModFileTime);
	PutShortLE(&buf[0x0e], fLastModFileDate);
	PutLongLE(&buf[0x10], fCRC32);
	PutLongLE(&buf[0x14], fCompressedSize);
	PutLongLE(&buf[0x18], fUncompressedSize);
	PutShortLE(&buf[0x1c], fFileNameLength);
	PutShortLE(&buf[0x1e], fExtraFieldLength);
	PutShortLE(&buf[0x20], fFileCommentLength);
	PutShortLE(&buf[0x22], fDiskNumberStart);
	PutShortLE(&buf[0x24], fInternalAttrs);
	PutLongLE(&buf[0x26], fExternalAttrs);
	PutLongLE(&buf[0x2a], fLocalHeaderRelOffset);

	dierr = pGFD->Write(buf, kCDELen);
	if (dierr != kDIErrNone)
		goto bail;

	/* write filename */
	if (fFileNameLength != 0) {
		dierr = pGFD->Write(fFileName, fFileNameLength);
		if (dierr != kDIErrNone)
			goto bail;
	}
	assert(fExtraFieldLength == 0);
	assert(fFileCommentLength == 0);

bail:
	return dierr;
}

/*
 * Change the filename field.
 */
void
OuterZip::CentralDirEntry::SetFileName(const char* name)
{
	delete[] fFileName;
	fFileName = nil;
	fFileNameLength = 0;

	if (name != nil) {
		fFileNameLength = strlen(name);
		fFileName = new unsigned char[fFileNameLength+1];
		if (fFileName == nil) {
			WMSG1("Malloc failure in SetFileName %u\n", fFileNameLength);
			fFileName = nil;
			fFileNameLength = 0;
		} else {
			memcpy(fFileName, name, fFileNameLength);
			fFileName[fFileNameLength] = '\0';
			WMSG1("+++ OuterZip CDE filename set to '%s'\n", fFileName);
		}
	}
}


/*
 * Dump the contents of a CentralDirEntry object.
 */
void
OuterZip::CentralDirEntry::Dump(void) const
{
	WMSG0(" CentralDirEntry contents:\n");
	WMSG4("  versMadeBy=%u versToExt=%u gpBits=0x%04x compression=%u\n",
		fVersionMadeBy, fVersionToExtract, fGPBitFlag, fCompressionMethod);
	WMSG3("  modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n",
		fLastModFileTime, fLastModFileDate, fCRC32);
	WMSG2("  compressedSize=%lu uncompressedSize=%lu\n",
		fCompressedSize, fUncompressedSize);
	WMSG3("  filenameLen=%u extraLen=%u commentLen=%u\n",
		fFileNameLength, fExtraFieldLength, fFileCommentLength);
	WMSG4("  diskNumStart=%u intAttr=0x%04x extAttr=0x%08lx relOffset=%lu\n",
		fDiskNumberStart, fInternalAttrs, fExternalAttrs,
		fLocalHeaderRelOffset);

	if (fFileName != nil) {
		WMSG1("  filename: '%s'\n", fFileName);
	}
	if (fFileComment != nil) {
		WMSG1("  comment: '%s'\n", fFileComment);
	}
}


/*
 * ===================================
 *		OuterZip::EndOfCentralDir
 * ===================================
 */

/*
 * Read the end-of-central-dir fields.
 *
 * "buf" should be positioned at the EOCD signature.
 */
DIError
OuterZip::EndOfCentralDir::ReadBuf(const unsigned char* buf, int len)
{
	if (len < kEOCDLen) {
		/* looks like ZIP file got truncated */
		WMSG2(" Zip EOCD: expected >= %d bytes, found %d\n",
			kEOCDLen, len);
		return kDIErrBadArchiveStruct;
	}

	if (GetLongLE(&buf[0x00]) != kSignature)
		return kDIErrInternal;

	fDiskNumber = GetShortLE(&buf[0x04]);
	fDiskWithCentralDir = GetShortLE(&buf[0x06]);
	fNumEntries = GetShortLE(&buf[0x08]);
	fTotalNumEntries = GetShortLE(&buf[0x0a]);
	fCentralDirSize = GetLongLE(&buf[0x0c]);
	fCentralDirOffset = GetLongLE(&buf[0x10]);
	fCommentLen = GetShortLE(&buf[0x14]);

	return kDIErrNone;
}

/*
 * Write an end-of-central-directory section.
 */
DIError
OuterZip::EndOfCentralDir::Write(GenericFD* pGFD)
{
	DIError dierr = kDIErrNone;
	unsigned char buf[kEOCDLen];

	PutLongLE(&buf[0x00], kSignature);
	PutShortLE(&buf[0x04], fDiskNumber);
	PutShortLE(&buf[0x06], fDiskWithCentralDir);
	PutShortLE(&buf[0x08], fNumEntries);
	PutShortLE(&buf[0x0a], fTotalNumEntries);
	PutLongLE(&buf[0x0c], fCentralDirSize);
	PutLongLE(&buf[0x10], fCentralDirOffset);
	PutShortLE(&buf[0x14], fCommentLen);

	dierr = pGFD->Write(buf, kEOCDLen);
	if (dierr != kDIErrNone)
		goto bail;

	assert(fCommentLen == 0);

bail:
	return dierr;
}
/*
 * Dump the contents of an EndOfCentralDir object.
 */
void
OuterZip::EndOfCentralDir::Dump(void) const
{
	WMSG0(" EndOfCentralDir contents:\n");
	WMSG4("  diskNum=%u diskWCD=%u numEnt=%u totalNumEnt=%u\n",
		fDiskNumber, fDiskWithCentralDir, fNumEntries, fTotalNumEntries);
	WMSG3("  centDirSize=%lu centDirOff=%lu commentLen=%u\n",
		fCentralDirSize, fCentralDirOffset, fCommentLen);
}
