/*
 * 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.
 */

//
// General-purpose Zip archive access.  This class allows both reading and
// writing to Zip archives, including deletion of existing entries.
//
#ifndef __LIBS_ZIPFILE_H
#define __LIBS_ZIPFILE_H

#include <utils/Vector.h>
#include <utils/Errors.h>
#include <stdio.h>

#include "ZipEntry.h"

namespace android {

/*
 * Manipulate a Zip archive.
 *
 * Some changes will not be visible in the until until "flush" is called.
 *
 * The correct way to update a file archive is to make all changes to a
 * copy of the archive in a temporary file, and then unlink/rename over
 * the original after everything completes.  Because we're only interested
 * in using this for packaging, we don't worry about such things.  Crashing
 * after making changes and before flush() completes could leave us with
 * an unusable Zip archive.
 */
class ZipFile {
public:
    ZipFile(void)
      : mZipFp(NULL), mReadOnly(false), mNeedCDRewrite(false)
      {}
    ~ZipFile(void) {
        if (!mReadOnly)
            flush();
        if (mZipFp != NULL)
            fclose(mZipFp);
        discardEntries();
    }

    /*
     * Open a new or existing archive.
     */
    typedef enum {
        kOpenReadOnly   = 0x01,
        kOpenReadWrite  = 0x02,
        kOpenCreate     = 0x04,     // create if it doesn't exist
        kOpenTruncate   = 0x08,     // if it exists, empty it
    };
    status_t open(const char* zipFileName, int flags);

    /*
     * Add a file to the end of the archive.  Specify whether you want the
     * library to try to store it compressed.
     *
     * If "storageName" is specified, the archive will use that instead
     * of "fileName".
     *
     * If there is already an entry with the same name, the call fails.
     * Existing entries with the same name must be removed first.
     *
     * If "ppEntry" is non-NULL, a pointer to the new entry will be returned.
     */
    status_t add(const char* fileName, int compressionMethod,
        ZipEntry** ppEntry)
    {
        return add(fileName, fileName, compressionMethod, ppEntry);
    }
    status_t add(const char* fileName, const char* storageName,
        int compressionMethod, ZipEntry** ppEntry)
    {
        return addCommon(fileName, NULL, 0, storageName,
                         ZipEntry::kCompressStored,
                         compressionMethod, ppEntry);
    }

    /*
     * Add a file that is already compressed with gzip.
     *
     * If "ppEntry" is non-NULL, a pointer to the new entry will be returned.
     */
    status_t addGzip(const char* fileName, const char* storageName,
        ZipEntry** ppEntry)
    {
        return addCommon(fileName, NULL, 0, storageName,
                         ZipEntry::kCompressDeflated,
                         ZipEntry::kCompressDeflated, ppEntry);
    }

    /*
     * Add a file from an in-memory data buffer.
     *
     * If "ppEntry" is non-NULL, a pointer to the new entry will be returned.
     */
    status_t add(const void* data, size_t size, const char* storageName,
        int compressionMethod, ZipEntry** ppEntry)
    {
        return addCommon(NULL, data, size, storageName,
                         ZipEntry::kCompressStored,
                         compressionMethod, ppEntry);
    }

    /*
     * Add an entry by copying it from another zip file.  If "padding" is
     * nonzero, the specified number of bytes will be added to the "extra"
     * field in the header.
     *
     * If "ppEntry" is non-NULL, a pointer to the new entry will be returned.
     */
    status_t add(const ZipFile* pSourceZip, const ZipEntry* pSourceEntry,
        int padding, ZipEntry** ppEntry);

    /*
     * Mark an entry as having been removed.  It is not actually deleted
     * from the archive or our internal data structures until flush() is
     * called.
     */
    status_t remove(ZipEntry* pEntry);

    /*
     * Flush changes.  If mNeedCDRewrite is set, this writes the central dir.
     */
    status_t flush(void);

    /*
     * Expand the data into the buffer provided.  The buffer must hold
     * at least <uncompressed len> bytes.  Variation expands directly
     * to a file.
     *
     * Returns "false" if an error was encountered in the compressed data.
     */
    //bool uncompress(const ZipEntry* pEntry, void* buf) const;
    //bool uncompress(const ZipEntry* pEntry, FILE* fp) const;
    void* uncompress(const ZipEntry* pEntry);

    /*
     * Get an entry, by name.  Returns NULL if not found.
     *
     * Does not return entries pending deletion.
     */
    ZipEntry* getEntryByName(const char* fileName) const;

    /*
     * Get the Nth entry in the archive.
     *
     * This will return an entry that is pending deletion.
     */
    int getNumEntries(void) const { return mEntries.size(); }
    ZipEntry* getEntryByIndex(int idx) const;

private:
    /* these are private and not defined */
    ZipFile(const ZipFile& src);
    ZipFile& operator=(const ZipFile& src);

    class EndOfCentralDir {
    public:
        EndOfCentralDir(void) :
            mDiskNumber(0),
            mDiskWithCentralDir(0),
            mNumEntries(0),
            mTotalNumEntries(0),
            mCentralDirSize(0),
            mCentralDirOffset(0),
            mCommentLen(0),
            mComment(NULL)
            {}
        virtual ~EndOfCentralDir(void) {
            delete[] mComment;
        }

        status_t readBuf(const unsigned char* buf, int len);
        status_t write(FILE* fp);

        //unsigned long   mSignature;
        unsigned short  mDiskNumber;
        unsigned short  mDiskWithCentralDir;
        unsigned short  mNumEntries;
        unsigned short  mTotalNumEntries;
        unsigned long   mCentralDirSize;
        unsigned long   mCentralDirOffset;      // offset from first disk
        unsigned short  mCommentLen;
        unsigned char*  mComment;

        enum {
            kSignature      = 0x06054b50,
            kEOCDLen        = 22,       // EndOfCentralDir len, excl. comment

            kMaxCommentLen  = 65535,    // longest possible in ushort
            kMaxEOCDSearch  = kMaxCommentLen + EndOfCentralDir::kEOCDLen,

        };

        void dump(void) const;
    };


    /* read all entries in the central dir */
    status_t readCentralDir(void);

    /* crunch deleted entries out */
    status_t crunchArchive(void);

    /* clean up mEntries */
    void discardEntries(void);

    /* common handler for all "add" functions */
    status_t addCommon(const char* fileName, const void* data, size_t size,
        const char* storageName, int sourceType, int compressionMethod,
        ZipEntry** ppEntry);

    /* copy all of "srcFp" into "dstFp" */
    status_t copyFpToFp(FILE* dstFp, FILE* srcFp, unsigned long* pCRC32);
    /* copy all of "data" into "dstFp" */
    status_t copyDataToFp(FILE* dstFp,
        const void* data, size_t size, unsigned long* pCRC32);
    /* copy some of "srcFp" into "dstFp" */
    status_t copyPartialFpToFp(FILE* dstFp, FILE* srcFp, long length,
        unsigned long* pCRC32);
    /* like memmove(), but on parts of a single file */
    status_t filemove(FILE* fp, off_t dest, off_t src, size_t n);
    /* compress all of "srcFp" into "dstFp", using Deflate */
    status_t compressFpToFp(FILE* dstFp, FILE* srcFp,
        const void* data, size_t size, unsigned long* pCRC32);

    /* get modification date from a file descriptor */
    time_t getModTime(int fd);

    /*
     * We use stdio FILE*, which gives us buffering but makes dealing
     * with files >2GB awkward.  Until we support Zip64, we're fine.
     */
    FILE*           mZipFp;             // Zip file pointer

    /* one of these per file */
    EndOfCentralDir mEOCD;

    /* did we open this read-only? */
    bool            mReadOnly;

    /* set this when we trash the central dir */
    bool            mNeedCDRewrite;

    /*
     * One ZipEntry per entry in the zip file.  I'm using pointers instead
     * of objects because it's easier than making operator= work for the
     * classes and sub-classes.
     */
    Vector<ZipEntry*>   mEntries;
};

}; // namespace android

#endif // __LIBS_ZIPFILE_H
