/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//
// Access to entries in a Zip archive.
//

#define LOG_TAG "zip"

#include "ZipEntry.h"
#include <utils/Log.h>

#include <stdio.h>
#include <string.h>
#include <assert.h>

using namespace android;

/*
 * Initialize a new ZipEntry structure from a FILE* positioned at a
 * CentralDirectoryEntry.
 *
 * On exit, the file pointer will be at the start of the next CDE or
 * at the EOCD.
 */
status_t ZipEntry::initFromCDE(FILE* fp)
{
    status_t result;
    long posn;
    bool hasDD;

    //LOGV("initFromCDE ---\n");

    /* read the CDE */
    result = mCDE.read(fp);
    if (result != NO_ERROR) {
        LOGD("mCDE.read failed\n");
        return result;
    }

    //mCDE.dump();

    /* using the info in the CDE, go load up the LFH */
    posn = ftell(fp);
    if (fseek(fp, mCDE.mLocalHeaderRelOffset, SEEK_SET) != 0) {
        LOGD("local header seek failed (%ld)\n",
            mCDE.mLocalHeaderRelOffset);
        return UNKNOWN_ERROR;
    }

    result = mLFH.read(fp);
    if (result != NO_ERROR) {
        LOGD("mLFH.read failed\n");
        return result;
    }

    if (fseek(fp, posn, SEEK_SET) != 0)
        return UNKNOWN_ERROR;

    //mLFH.dump();

    /*
     * We *might* need to read the Data Descriptor at this point and
     * integrate it into the LFH.  If this bit is set, the CRC-32,
     * compressed size, and uncompressed size will be zero.  In practice
     * these seem to be rare.
     */
    hasDD = (mLFH.mGPBitFlag & kUsesDataDescr) != 0;
    if (hasDD) {
        // do something clever
        //LOGD("+++ has data descriptor\n");
    }

    /*
     * Sanity-check the LFH.  Note that this will fail if the "kUsesDataDescr"
     * flag is set, because the LFH is incomplete.  (Not a problem, since we
     * prefer the CDE values.)
     */
    if (!hasDD && !compareHeaders()) {
        LOGW("WARNING: header mismatch\n");
        // keep going?
    }

    /*
     * If the mVersionToExtract is greater than 20, we may have an
     * issue unpacking the record -- could be encrypted, compressed
     * with something we don't support, or use Zip64 extensions.  We
     * can defer worrying about that to when we're extracting data.
     */

    return NO_ERROR;
}

/*
 * Initialize a new entry.  Pass in the file name and an optional comment.
 *
 * Initializes the CDE and the LFH.
 */
void ZipEntry::initNew(const char* fileName, const char* comment)
{
    assert(fileName != NULL && *fileName != '\0');  // name required

    /* most fields are properly initialized by constructor */
    mCDE.mVersionMadeBy = kDefaultMadeBy;
    mCDE.mVersionToExtract = kDefaultVersion;
    mCDE.mCompressionMethod = kCompressStored;
    mCDE.mFileNameLength = strlen(fileName);
    if (comment != NULL)
        mCDE.mFileCommentLength = strlen(comment);
    mCDE.mExternalAttrs = 0x81b60020;   // matches what WinZip does

    if (mCDE.mFileNameLength > 0) {
        mCDE.mFileName = new unsigned char[mCDE.mFileNameLength+1];
        strcpy((char*) mCDE.mFileName, fileName);
    }
    if (mCDE.mFileCommentLength > 0) {
        /* TODO: stop assuming null-terminated ASCII here? */
        mCDE.mFileComment = new unsigned char[mCDE.mFileCommentLength+1];
        strcpy((char*) mCDE.mFileComment, comment);
    }

    copyCDEtoLFH();
}

/*
 * Initialize a new entry, starting with the ZipEntry from a different
 * archive.
 *
 * Initializes the CDE and the LFH.
 */
status_t ZipEntry::initFromExternal(const ZipFile* pZipFile,
    const ZipEntry* pEntry)
{
    /*
     * Copy everything in the CDE over, then fix up the hairy bits.
     */
    memcpy(&mCDE, &pEntry->mCDE, sizeof(mCDE));

    if (mCDE.mFileNameLength > 0) {
        mCDE.mFileName = new unsigned char[mCDE.mFileNameLength+1];
        if (mCDE.mFileName == NULL)
            return NO_MEMORY;
        strcpy((char*) mCDE.mFileName, (char*)pEntry->mCDE.mFileName);
    }
    if (mCDE.mFileCommentLength > 0) {
        mCDE.mFileComment = new unsigned char[mCDE.mFileCommentLength+1];
        if (mCDE.mFileComment == NULL)
            return NO_MEMORY;
        strcpy((char*) mCDE.mFileComment, (char*)pEntry->mCDE.mFileComment);
    }
    if (mCDE.mExtraFieldLength > 0) {
        /* we null-terminate this, though it may not be a string */
        mCDE.mExtraField = new unsigned char[mCDE.mExtraFieldLength+1];
        if (mCDE.mExtraField == NULL)
            return NO_MEMORY;
        memcpy(mCDE.mExtraField, pEntry->mCDE.mExtraField,
            mCDE.mExtraFieldLength+1);
    }

    /* construct the LFH from the CDE */
    copyCDEtoLFH();

    /*
     * The LFH "extra" field is independent of the CDE "extra", so we
     * handle it here.
     */
    assert(mLFH.mExtraField == NULL);
    mLFH.mExtraFieldLength = pEntry->mLFH.mExtraFieldLength;
    if (mLFH.mExtraFieldLength > 0) {
        mLFH.mExtraField = new unsigned char[mLFH.mExtraFieldLength+1];
        if (mLFH.mExtraField == NULL)
            return NO_MEMORY;
        memcpy(mLFH.mExtraField, pEntry->mLFH.mExtraField,
            mLFH.mExtraFieldLength+1);
    }

    return NO_ERROR;
}

/*
 * Insert pad bytes in the LFH by tweaking the "extra" field.  This will
 * potentially confuse something that put "extra" data in here earlier,
 * but I can't find an actual problem.
 */
status_t ZipEntry::addPadding(int padding)
{
    if (padding <= 0)
        return INVALID_OPERATION;

    //LOGI("HEY: adding %d pad bytes to existing %d in %s\n",
    //    padding, mLFH.mExtraFieldLength, mCDE.mFileName);

    if (mLFH.mExtraFieldLength > 0) {
        /* extend existing field */
        unsigned char* newExtra;

        newExtra = new unsigned char[mLFH.mExtraFieldLength + padding];
        if (newExtra == NULL)
            return NO_MEMORY;
        memset(newExtra + mLFH.mExtraFieldLength, 0, padding);
        memcpy(newExtra, mLFH.mExtraField, mLFH.mExtraFieldLength);

        delete[] mLFH.mExtraField;
        mLFH.mExtraField = newExtra;
        mLFH.mExtraFieldLength += padding;
    } else {
        /* create new field */
        mLFH.mExtraField = new unsigned char[padding];
        memset(mLFH.mExtraField, 0, padding);
        mLFH.mExtraFieldLength = padding;
    }

    return NO_ERROR;
}

/*
 * Set the fields in the LFH equal to the corresponding fields in the CDE.
 *
 * This does not touch the LFH "extra" field.
 */
void ZipEntry::copyCDEtoLFH(void)
{
    mLFH.mVersionToExtract  = mCDE.mVersionToExtract;
    mLFH.mGPBitFlag         = mCDE.mGPBitFlag;
    mLFH.mCompressionMethod = mCDE.mCompressionMethod;
    mLFH.mLastModFileTime   = mCDE.mLastModFileTime;
    mLFH.mLastModFileDate   = mCDE.mLastModFileDate;
    mLFH.mCRC32             = mCDE.mCRC32;
    mLFH.mCompressedSize    = mCDE.mCompressedSize;
    mLFH.mUncompressedSize  = mCDE.mUncompressedSize;
    mLFH.mFileNameLength    = mCDE.mFileNameLength;
    // the "extra field" is independent

    delete[] mLFH.mFileName;
    if (mLFH.mFileNameLength > 0) {
        mLFH.mFileName = new unsigned char[mLFH.mFileNameLength+1];
        strcpy((char*) mLFH.mFileName, (const char*) mCDE.mFileName);
    } else {
        mLFH.mFileName = NULL;
    }
}

/*
 * Set some information about a file after we add it.
 */
void ZipEntry::setDataInfo(long uncompLen, long compLen, unsigned long crc32,
    int compressionMethod)
{
    mCDE.mCompressionMethod = compressionMethod;
    mCDE.mCRC32 = crc32;
    mCDE.mCompressedSize = compLen;
    mCDE.mUncompressedSize = uncompLen;
    mCDE.mCompressionMethod = compressionMethod;
    if (compressionMethod == kCompressDeflated) {
        mCDE.mGPBitFlag |= 0x0002;      // indicates maximum compression used
    }
    copyCDEtoLFH();
}

/*
 * See if the data in mCDE and mLFH match up.  This is mostly useful for
 * debugging these classes, but it can be used to identify damaged
 * archives.
 *
 * Returns "false" if they differ.
 */
bool ZipEntry::compareHeaders(void) const
{
    if (mCDE.mVersionToExtract != mLFH.mVersionToExtract) {
        LOGV("cmp: VersionToExtract\n");
        return false;
    }
    if (mCDE.mGPBitFlag != mLFH.mGPBitFlag) {
        LOGV("cmp: GPBitFlag\n");
        return false;
    }
    if (mCDE.mCompressionMethod != mLFH.mCompressionMethod) {
        LOGV("cmp: CompressionMethod\n");
        return false;
    }
    if (mCDE.mLastModFileTime != mLFH.mLastModFileTime) {
        LOGV("cmp: LastModFileTime\n");
        return false;
    }
    if (mCDE.mLastModFileDate != mLFH.mLastModFileDate) {
        LOGV("cmp: LastModFileDate\n");
        return false;
    }
    if (mCDE.mCRC32 != mLFH.mCRC32) {
        LOGV("cmp: CRC32\n");
        return false;
    }
    if (mCDE.mCompressedSize != mLFH.mCompressedSize) {
        LOGV("cmp: CompressedSize\n");
        return false;
    }
    if (mCDE.mUncompressedSize != mLFH.mUncompressedSize) {
        LOGV("cmp: UncompressedSize\n");
        return false;
    }
    if (mCDE.mFileNameLength != mLFH.mFileNameLength) {
        LOGV("cmp: FileNameLength\n");
        return false;
    }
#if 0       // this seems to be used for padding, not real data
    if (mCDE.mExtraFieldLength != mLFH.mExtraFieldLength) {
        LOGV("cmp: ExtraFieldLength\n");
        return false;
    }
#endif
    if (mCDE.mFileName != NULL) {
        if (strcmp((char*) mCDE.mFileName, (char*) mLFH.mFileName) != 0) {
            LOGV("cmp: FileName\n");
            return false;
        }
    }

    return true;
}


/*
 * Convert the DOS date/time stamp into a UNIX time stamp.
 */
time_t ZipEntry::getModWhen(void) const
{
    struct tm parts;

    parts.tm_sec = (mCDE.mLastModFileTime & 0x001f) << 1;
    parts.tm_min = (mCDE.mLastModFileTime & 0x07e0) >> 5;
    parts.tm_hour = (mCDE.mLastModFileTime & 0xf800) >> 11;
    parts.tm_mday = (mCDE.mLastModFileDate & 0x001f);
    parts.tm_mon = ((mCDE.mLastModFileDate & 0x01e0) >> 5) -1;
    parts.tm_year = ((mCDE.mLastModFileDate & 0xfe00) >> 9) + 80;
    parts.tm_wday = parts.tm_yday = 0;
    parts.tm_isdst = -1;        // DST info "not available"

    return mktime(&parts);
}

/*
 * Set the CDE/LFH timestamp from UNIX time.
 */
void ZipEntry::setModWhen(time_t when)
{
#ifdef HAVE_LOCALTIME_R
    struct tm tmResult;
#endif
    time_t even;
    unsigned short zdate, ztime;

    struct tm* ptm;

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

    /* expand */
#ifdef HAVE_LOCALTIME_R
    ptm = localtime_r(&even, &tmResult);
#else
    ptm = localtime(&even);
#endif

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

    zdate = (year - 80) << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday;
    ztime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1;

    mCDE.mLastModFileTime = mLFH.mLastModFileTime = ztime;
    mCDE.mLastModFileDate = mLFH.mLastModFileDate = zdate;
}


/*
 * ===========================================================================
 *      ZipEntry::LocalFileHeader
 * ===========================================================================
 */

/*
 * Read a local file header.
 *
 * On entry, "fp" points to the signature at the start of the header.
 * On exit, "fp" points to the start of data.
 */
status_t ZipEntry::LocalFileHeader::read(FILE* fp)
{
    status_t result = NO_ERROR;
    unsigned char buf[kLFHLen];

    assert(mFileName == NULL);
    assert(mExtraField == NULL);

    if (fread(buf, 1, kLFHLen, fp) != kLFHLen) {
        result = UNKNOWN_ERROR;
        goto bail;
    }

    if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) {
        LOGD("whoops: didn't find expected signature\n");
        result = UNKNOWN_ERROR;
        goto bail;
    }

    mVersionToExtract = ZipEntry::getShortLE(&buf[0x04]);
    mGPBitFlag = ZipEntry::getShortLE(&buf[0x06]);
    mCompressionMethod = ZipEntry::getShortLE(&buf[0x08]);
    mLastModFileTime = ZipEntry::getShortLE(&buf[0x0a]);
    mLastModFileDate = ZipEntry::getShortLE(&buf[0x0c]);
    mCRC32 = ZipEntry::getLongLE(&buf[0x0e]);
    mCompressedSize = ZipEntry::getLongLE(&buf[0x12]);
    mUncompressedSize = ZipEntry::getLongLE(&buf[0x16]);
    mFileNameLength = ZipEntry::getShortLE(&buf[0x1a]);
    mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1c]);

    // TODO: validate sizes

    /* grab filename */
    if (mFileNameLength != 0) {
        mFileName = new unsigned char[mFileNameLength+1];
        if (mFileName == NULL) {
            result = NO_MEMORY;
            goto bail;
        }
        if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) {
            result = UNKNOWN_ERROR;
            goto bail;
        }
        mFileName[mFileNameLength] = '\0';
    }

    /* grab extra field */
    if (mExtraFieldLength != 0) {
        mExtraField = new unsigned char[mExtraFieldLength+1];
        if (mExtraField == NULL) {
            result = NO_MEMORY;
            goto bail;
        }
        if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) {
            result = UNKNOWN_ERROR;
            goto bail;
        }
        mExtraField[mExtraFieldLength] = '\0';
    }

bail:
    return result;
}

/*
 * Write a local file header.
 */
status_t ZipEntry::LocalFileHeader::write(FILE* fp)
{
    unsigned char buf[kLFHLen];

    ZipEntry::putLongLE(&buf[0x00], kSignature);
    ZipEntry::putShortLE(&buf[0x04], mVersionToExtract);
    ZipEntry::putShortLE(&buf[0x06], mGPBitFlag);
    ZipEntry::putShortLE(&buf[0x08], mCompressionMethod);
    ZipEntry::putShortLE(&buf[0x0a], mLastModFileTime);
    ZipEntry::putShortLE(&buf[0x0c], mLastModFileDate);
    ZipEntry::putLongLE(&buf[0x0e], mCRC32);
    ZipEntry::putLongLE(&buf[0x12], mCompressedSize);
    ZipEntry::putLongLE(&buf[0x16], mUncompressedSize);
    ZipEntry::putShortLE(&buf[0x1a], mFileNameLength);
    ZipEntry::putShortLE(&buf[0x1c], mExtraFieldLength);

    if (fwrite(buf, 1, kLFHLen, fp) != kLFHLen)
        return UNKNOWN_ERROR;

    /* write filename */
    if (mFileNameLength != 0) {
        if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength)
            return UNKNOWN_ERROR;
    }

    /* write "extra field" */
    if (mExtraFieldLength != 0) {
        if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength)
            return UNKNOWN_ERROR;
    }

    return NO_ERROR;
}


/*
 * Dump the contents of a LocalFileHeader object.
 */
void ZipEntry::LocalFileHeader::dump(void) const
{
    LOGD(" LocalFileHeader contents:\n");
    LOGD("  versToExt=%u gpBits=0x%04x compression=%u\n",
        mVersionToExtract, mGPBitFlag, mCompressionMethod);
    LOGD("  modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n",
        mLastModFileTime, mLastModFileDate, mCRC32);
    LOGD("  compressedSize=%lu uncompressedSize=%lu\n",
        mCompressedSize, mUncompressedSize);
    LOGD("  filenameLen=%u extraLen=%u\n",
        mFileNameLength, mExtraFieldLength);
    if (mFileName != NULL)
        LOGD("  filename: '%s'\n", mFileName);
}


/*
 * ===========================================================================
 *      ZipEntry::CentralDirEntry
 * ===========================================================================
 */

/*
 * Read the central dir entry that appears next in the file.
 *
 * On entry, "fp" should be positioned on the signature bytes for the
 * entry.  On exit, "fp" will point at the signature word for the next
 * entry or for the EOCD.
 */
status_t ZipEntry::CentralDirEntry::read(FILE* fp)
{
    status_t result = NO_ERROR;
    unsigned char buf[kCDELen];

    /* no re-use */
    assert(mFileName == NULL);
    assert(mExtraField == NULL);
    assert(mFileComment == NULL);

    if (fread(buf, 1, kCDELen, fp) != kCDELen) {
        result = UNKNOWN_ERROR;
        goto bail;
    }

    if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) {
        LOGD("Whoops: didn't find expected signature\n");
        result = UNKNOWN_ERROR;
        goto bail;
    }

    mVersionMadeBy = ZipEntry::getShortLE(&buf[0x04]);
    mVersionToExtract = ZipEntry::getShortLE(&buf[0x06]);
    mGPBitFlag = ZipEntry::getShortLE(&buf[0x08]);
    mCompressionMethod = ZipEntry::getShortLE(&buf[0x0a]);
    mLastModFileTime = ZipEntry::getShortLE(&buf[0x0c]);
    mLastModFileDate = ZipEntry::getShortLE(&buf[0x0e]);
    mCRC32 = ZipEntry::getLongLE(&buf[0x10]);
    mCompressedSize = ZipEntry::getLongLE(&buf[0x14]);
    mUncompressedSize = ZipEntry::getLongLE(&buf[0x18]);
    mFileNameLength = ZipEntry::getShortLE(&buf[0x1c]);
    mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1e]);
    mFileCommentLength = ZipEntry::getShortLE(&buf[0x20]);
    mDiskNumberStart = ZipEntry::getShortLE(&buf[0x22]);
    mInternalAttrs = ZipEntry::getShortLE(&buf[0x24]);
    mExternalAttrs = ZipEntry::getLongLE(&buf[0x26]);
    mLocalHeaderRelOffset = ZipEntry::getLongLE(&buf[0x2a]);

    // TODO: validate sizes and offsets

    /* grab filename */
    if (mFileNameLength != 0) {
        mFileName = new unsigned char[mFileNameLength+1];
        if (mFileName == NULL) {
            result = NO_MEMORY;
            goto bail;
        }
        if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) {
            result = UNKNOWN_ERROR;
            goto bail;
        }
        mFileName[mFileNameLength] = '\0';
    }

    /* read "extra field" */
    if (mExtraFieldLength != 0) {
        mExtraField = new unsigned char[mExtraFieldLength+1];
        if (mExtraField == NULL) {
            result = NO_MEMORY;
            goto bail;
        }
        if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) {
            result = UNKNOWN_ERROR;
            goto bail;
        }
        mExtraField[mExtraFieldLength] = '\0';
    }


    /* grab comment, if any */
    if (mFileCommentLength != 0) {
        mFileComment = new unsigned char[mFileCommentLength+1];
        if (mFileComment == NULL) {
            result = NO_MEMORY;
            goto bail;
        }
        if (fread(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength)
        {
            result = UNKNOWN_ERROR;
            goto bail;
        }
        mFileComment[mFileCommentLength] = '\0';
    }

bail:
    return result;
}

/*
 * Write a central dir entry.
 */
status_t ZipEntry::CentralDirEntry::write(FILE* fp)
{
    unsigned char buf[kCDELen];

    ZipEntry::putLongLE(&buf[0x00], kSignature);
    ZipEntry::putShortLE(&buf[0x04], mVersionMadeBy);
    ZipEntry::putShortLE(&buf[0x06], mVersionToExtract);
    ZipEntry::putShortLE(&buf[0x08], mGPBitFlag);
    ZipEntry::putShortLE(&buf[0x0a], mCompressionMethod);
    ZipEntry::putShortLE(&buf[0x0c], mLastModFileTime);
    ZipEntry::putShortLE(&buf[0x0e], mLastModFileDate);
    ZipEntry::putLongLE(&buf[0x10], mCRC32);
    ZipEntry::putLongLE(&buf[0x14], mCompressedSize);
    ZipEntry::putLongLE(&buf[0x18], mUncompressedSize);
    ZipEntry::putShortLE(&buf[0x1c], mFileNameLength);
    ZipEntry::putShortLE(&buf[0x1e], mExtraFieldLength);
    ZipEntry::putShortLE(&buf[0x20], mFileCommentLength);
    ZipEntry::putShortLE(&buf[0x22], mDiskNumberStart);
    ZipEntry::putShortLE(&buf[0x24], mInternalAttrs);
    ZipEntry::putLongLE(&buf[0x26], mExternalAttrs);
    ZipEntry::putLongLE(&buf[0x2a], mLocalHeaderRelOffset);

    if (fwrite(buf, 1, kCDELen, fp) != kCDELen)
        return UNKNOWN_ERROR;

    /* write filename */
    if (mFileNameLength != 0) {
        if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength)
            return UNKNOWN_ERROR;
    }

    /* write "extra field" */
    if (mExtraFieldLength != 0) {
        if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength)
            return UNKNOWN_ERROR;
    }

    /* write comment */
    if (mFileCommentLength != 0) {
        if (fwrite(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength)
            return UNKNOWN_ERROR;
    }

    return NO_ERROR;
}

/*
 * Dump the contents of a CentralDirEntry object.
 */
void ZipEntry::CentralDirEntry::dump(void) const
{
    LOGD(" CentralDirEntry contents:\n");
    LOGD("  versMadeBy=%u versToExt=%u gpBits=0x%04x compression=%u\n",
        mVersionMadeBy, mVersionToExtract, mGPBitFlag, mCompressionMethod);
    LOGD("  modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n",
        mLastModFileTime, mLastModFileDate, mCRC32);
    LOGD("  compressedSize=%lu uncompressedSize=%lu\n",
        mCompressedSize, mUncompressedSize);
    LOGD("  filenameLen=%u extraLen=%u commentLen=%u\n",
        mFileNameLength, mExtraFieldLength, mFileCommentLength);
    LOGD("  diskNumStart=%u intAttr=0x%04x extAttr=0x%08lx relOffset=%lu\n",
        mDiskNumberStart, mInternalAttrs, mExternalAttrs,
        mLocalHeaderRelOffset);

    if (mFileName != NULL)
        LOGD("  filename: '%s'\n", mFileName);
    if (mFileComment != NULL)
        LOGD("  comment: '%s'\n", mFileComment);
}