//-----------------------------------------------------------------------------
// Copyright (c) 2013 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
#include "io/zip/fileHeader.h"
#include "io/zip/centralDir.h"
#include "io/zip/compressor.h"
#include "io/fileStream.h"
#include "collection/simpleHashTable.h"
#include "collection/vector.h"
#ifndef _ZIPARCHIVE_H_
#define _ZIPARCHIVE_H_
/// @addtogroup zip_group Zip Code
// @{
/// Password to use when opening encrypted zip files. Change this to whatever the password is for your zips.
#define DEFAULT_ZIP_PASSWORD "changeme"
class ZipTestWrite;
class ZipTestRead;
class ZipTestMisc;
namespace Zip
{
/// @addtogroup zip_group Zip Code
// @{
// Forward Refs
class ZipTempStream;
// [tom, 10/18/2006] This will be split up into a separate interface for allowing
// the resource manager to handle any kind of archive relatively easily.
// [tom, 2/9/2007] At least, it was designed so that it could be. It may not
// actually happen for a very long time, if ever ;-)
//////////////////////////////////////////////////////////////////////////
/// @brief Class for accessing Zip files
///
/// ZipArchive provides two interfaces for reading or writing zip files.
/// The first is a stream based interface that should be familiar to anyone
/// who's ever written code, and the other is an archiver interface that
/// should be familiar to anyone who's ever used a standard Zip application.
///
/// The two interfaces are not mutually exclusive so you can use both if
/// you wish. For example, you may want to use the stream interface to add
/// a configuration file to a zip without having to create a temporary file
/// and then add a number of existing files using the addFile() method.
///
/// Both interfaces have their advantages and disadvantages, which will
/// be discussed below.
///
///
Accessing a Zip file
///
/// Before you can access any files in the zip, you first need to open the
/// archive. This is the same regardless of which interface you use, and there
/// are two ways to accomplish it.
///
/// Opening from a file on the file system
///
/// The simplest method of opening a zip file is to use openArchive(const char *, AccessMode)
/// to open a file that is on the disk.
///
/// When opening a zip file on the file system, the filename is automatically set.
///
/// Opening a file from a stream
///
/// A more advanced way to open the zip file is from an arbitrary stream. The
/// only requirements are that the stream supports seeking and was opened with
/// the correct access mode. Use the openArchive(Stream *, AccessMode) method to
/// do this.
///
/// Opening zip files from arbitrary streams is a very powerful feature and
/// opens many interesting doors. For example, combined with some small changes
/// to the resource manager and startup code, it was possible to implement a
/// VFS that allows the entire game to run from a single executable with no
/// external files.
///
/// Note that the filename is not automatically set when you open the zip file
/// from a stream. The filename is used in error reporting and by the resource
/// manager, so you may wish to set it to something meaningful.
///
/// Regardless of which method you use to open the file, the #AccessMode controls
/// what you can do with it. If you open the archive as #ReadWrite, you can both
/// write to and read from files in the zip. However, it is not possible to open
/// files in the zip as #ReadWrite.
///
/// Closing the zip file
///
/// When you are done with the zip file, call closeArchive() to free any resources
/// and rebuild the zip file if it was open for #Write.
///
/// Example
///
/// @code
/// Zip::ZipArchive za;
/// if(za.openArchive("filename.zip", ZipArchive::Read))
/// {
/// // ... do stuff ...
/// za.closeArchive();
/// }
/// @endcode
///
/// Archiver Interface
///
/// The archiver style interface allows you to add, extract and delete files in
/// the zip in a way similar to that of an standard archiver application.
///
/// While the archiver interface is simple to use, it is blocking and thus
/// difficult to use asynchronously. If you require zip file support and
/// responsive UI then you should consider using the stream interface instead.
///
/// See the following method documentation for more information:
///
///
/// - addFile()
///
- extractFile()
///
- deleteFile()
///
///
/// Example
///
/// @code
/// Zip::ZipArchive za;
/// if(za.openArchive("filename.zip", ZipArchive::ReadWrite))
/// {
/// // Extract a file
/// za.extractFile("test.txt", "test.txt");
/// // Add a file
/// za.addFile("test.txt", "test.txt");
/// // Delete a file
/// za.deleteFile("test.txt");
///
/// za.closeArchive();
/// }
/// @endcode
///
/// Stream Interface
///
/// The stream based interface allows you to access files within the zip
/// in a similar way to accessing the file system through the ResourceManager.
///
/// There are a few small caveats to the stream interface:
///
/// - When writing files, the whole file must be written sequentially. You
/// cannot seek in the stream.
///
- It may or may not be possible to seek in streams opened for read.
/// Files that were not compressed in the zip file support seeking with
/// no penalty. In all cases where the file is compressed, if seeking is
/// supported by the decompression and/or decryption filter then it
/// carries with it an extreme performance penalty and should be avoided.
/// All currently available decompression filters (Deflate and BZip2) and
/// decryption filters (Zip 2.0 and AES) support seeking, but have to reset
/// their state and re-decompress/decrypt the entire file up to the point
/// you are seeking to. An extreme example would be that if you had a
/// 20MB file and were currently at the end of the file, seeking back 1 byte
/// of the file would cause the entire file to be decompressed again. This
/// would be a blocking operation that would lock Torque up for an appreciable
/// chunk of time.
///
- Files can only be open as #Read or #Write, but not #ReadWrite
///
- Only one file can be open for read at a time, but multiple files can
/// be open for write at a time. - [tom, 2/9/2007] Check this
///
///
/// See the following method documentation for more information:
///
///
/// - openFile()
///
- closeFile()
///
///
/// CRC Checking
///
/// Unlike the archiver interface, there is no automatic CRC checking when
/// reading from files using the stream interface. If you will only be
/// reading files sequentially, see the documentation for ZipStatFilter
/// for a useful trick to get easy CRC checking.
///
/// Example
///
/// @code
/// Zip::ZipArchive za;
/// if(za.openArchive("filename.zip", ZipArchive::Write))
/// {
/// // Write to the file
/// Stream *stream;
/// if(stream = za.openFile("test.txt", ZipArchive::Write))
/// {
/// stream->writeLine((U8 *)"Hello, Zipped World!");
/// za.closeFile(stream);
/// }
///
/// za.closeArchive();
/// }
/// @endcode
///
/// Compressed Files
///
/// The zip code included with stock Torque supports "stored" (uncompressed) files
/// and deflate compressed files. The code is easily extensible to support any
/// compression format that the Zip file format supports.
///
/// In addition to the deflate and stored formats, BZip2 is supported but not
/// included with stock Torque. BZip2 support will be released as a resource in
/// the future.
///
/// Encrypted Files
///
/// Preliminary support for Encrypted/Passworded files is included in TGB Pro only.
/// Currently, only Zip 2.0 encryption is supported by the stock code. AES support
/// exists and may be released as a resource in the future.
///
/// To set the password used for zips, you need to modify the #DEFAULT_ZIP_PASSWORD
/// define in core/zip/zipArchive.h. This password will be used for all zips that
/// require a password. The default password is changeme. This may be used by
/// TGB Binary users to test encrypted zips with their game. Shipping with the
/// default password is not recommended for obvious reasons.
///
/// The intended use of encrypted zips is for preventing casual copying of your
/// game's assets. Zip 2.0 encryption has known weaknesses that allow an attacker
/// to decrypt the contents of the zip. AES encryption is significantly more secure,
/// but as the password must be stored in the executable it will not stop a
/// determined attacker.
///
/// A script accessible mechanism for setting the password does not currently exist.
/// To use encrypted mod zips, if the password was in script then the password
/// would be clearly visible to anyone that cared to poke around in your scripts.
///
/// Encrypted zip support will be improved in a future version. For now, a more
/// secure method of storing the password is left as an exercise for the reader.
///
/// Accessing Zip files from script
///
/// ZipArchive is a C++ class and thus cannot be used from script. However,
/// a wrapper is provided to allow script access to zips. See the documentation
/// on ZipObject for more information.
///
/// More Examples
///
/// More in depth example code than that featured here can be found in the
/// unit tests for the zip code (in the core/zip/unitTests directory)
/// and the script code for the packaging utility.
///
//////////////////////////////////////////////////////////////////////////
class ZipArchive
{
public:
/// Access modes for zip files and files within the zip
enum AccessMode
{
Read = FileStream::Read, //!< Open a zip or file in a zip for reading
Write = FileStream::Write, //!< Open a zip or file in a zip for writing
ReadWrite = FileStream::ReadWrite //!< Open a zip file for reading and writing. Note: Not valid for files in zips.
};
protected:
struct ZipEntry
{
ZipEntry *mParent;
StringTableEntry mName;
bool mIsDirectory;
CentralDir mCD;
SimpleHashTable mChildren;
ZipEntry()
{
mName = "";
mIsDirectory = false;
}
};
Stream *mStream;
FileStream *mDiskStream;
AccessMode mMode;
EndOfCentralDir mEOCD;
// mRoot forms a tree of entries for fast queries given a file path
// mEntries allows easy iteration of the entire file list
ZipEntry *mRoot;
VectorPtr mEntries;
const char *mFilename;
VectorPtr mTempFiles;
bool readCentralDirectory();
void insertEntry(ZipEntry *ze);
void removeEntry(ZipEntry *ze);
ZipEntry *findZipEntry(const char *filename);
Stream *createNewFile(const char *filename, Compressor *method);
Stream *createNewFile(const char *filename, const char *method)
{
return createNewFile(filename, Compressor::findCompressor(method));
}
Stream *createNewFile(const char *filename, S32 method)
{
return createNewFile(filename, Compressor::findCompressor(method));
}
void updateFile(ZipTempStream *stream);
bool rebuildZip();
bool copyFileToNewZip(CentralDir *cdir, Stream *newZipStream);
bool writeDirtyFileToNewZip(ZipTempStream *fileStream, Stream *zipStream);
U32 localTimeToDOSTime(const Platform::LocalTime &t);
U32 currentTimeToDOSTime();
public:
ZipArchive();
virtual ~ZipArchive();
/// @name Miscellaneous Methods
// @{
//////////////////////////////////////////////////////////////////////////
/// @brief Set the filename of the zip file.
///
/// The zip filename is used by the resource manager and for error reporting.
///
/// Note: The filename is set automatically when you open the file.
///
/// @param filename Filename of the zip file
//////////////////////////////////////////////////////////////////////////
void setFilename(const char *filename);
//////////////////////////////////////////////////////////////////////////
/// @brief Get the filename of the zip file.
///
/// @returns Filename of the zip file, or NULL if none set
//////////////////////////////////////////////////////////////////////////
const char *getFilename() { return mFilename; }
//////////////////////////////////////////////////////////////////////////
/// @brief Determine if the Zip code is in verbose mode
///
/// Verbose mode causes the Zip code to provide diagnostic error messages
/// when things go wrong. It can be enabled or disabled through script by
/// setting the $Pref::Zip::Verbose variable to true to enable it or false
/// to disable it.
///
/// Verbose mode is mostly useful when tracking down issues with opening
/// a zip file without having to resort to using a debugger.
///
/// @returns The value of $Pref::Zip::Verbose
/// @see ZipArchive::setVerbose()
//////////////////////////////////////////////////////////////////////////
bool isVerbose();
//////////////////////////////////////////////////////////////////////////
/// @brief Turn verbose mode on or off.
///
/// This sets the $Pref::Zip::Verbose variable.
///
/// See isVerbose() for a discussion on verbose mode.
///
/// @param verbose True to enable verbose mode, false to disable
/// @see ZipArchive::isVerbose()
//////////////////////////////////////////////////////////////////////////
void setVerbose(bool verbose);
// @}
/// @name Archive Access Methods
// @{
//////////////////////////////////////////////////////////////////////////
/// @brief Open a zip archive from a file
///
/// The archive must be closed with closeArchive() when you are done with it.
///
/// @param filename Filename of zip file to open
/// @param mode Access mode. May be Read, Write or ReadWrite
/// @return true for success, false for failure
/// @see ZipArchive::openArchive(Stream *, AccessMode), ZipArchive::closeArchive()
//////////////////////////////////////////////////////////////////////////
virtual bool openArchive(const char *filename, AccessMode mode = Read);
//////////////////////////////////////////////////////////////////////////
/// @brief Open a zip archive from a stream
///
/// The stream must support seeking and must support the specified access
/// mode. For example, if the stream is opened for Read you cannot specify
/// Write to openArchive(). However, if the stream is open for ReadWrite
/// then you can specify any one of Read, Write or ReadWrite as the mode
/// argument to openArchive().
///
/// The archive must be closed with closeArchive() when you are done with it.
///
/// @param stream Pointer to stream to open the zip archive from
/// @param mode Access mode. May be Read, Write or ReadWrite
/// @return true for success, false for failure
/// @see ZipArchive::openArchive(const char *, AccessMode), ZipArchive::closeArchive()
//////////////////////////////////////////////////////////////////////////
// CodeReview [tom, 2/9/2007] I just thought, if we open a stream directly
// for write then rebuilding the zip file probably won't work. This needs to
// be checked and either fixed or worked around.
virtual bool openArchive(Stream *stream, AccessMode mode = Read);
//////////////////////////////////////////////////////////////////////////
/// @brief Close the zip archive and free any resources
///
/// @see ZipArchive::openArchive(Stream *, AccessMode), ZipArchive::openArchive(const char *, AccessMode)
//////////////////////////////////////////////////////////////////////////
virtual void closeArchive();
// @}
/// @name Stream Based File Access Methods
// @{
//////////////////////////////////////////////////////////////////////////
/// @brief Open a file within the zip file
///
/// The access mode can only be Read or Write. It is not possible to open
/// a file within the zip as ReadWrite.
///
/// The returned stream must be freed with closeFile(). Do not delete it
/// directly.
///
/// In verbose mode, openFile() will display additional error information
/// in the console when it fails.
///
/// @param filename Filename of the file in the zip
/// @param mode Access mode. May be Read or Write
/// @return Pointer to stream or NULL for failure
/// @see ZipArchive::closeFile(), ZipArchive::isVerbose()
//////////////////////////////////////////////////////////////////////////
virtual Stream *openFile(const char *filename, AccessMode mode = Read);
//////////////////////////////////////////////////////////////////////////
/// @brief Close a file opened through openFile()
///
/// @param stream Stream to close
/// @see ZipArchive::openFile(const char *, AccessMode)
//////////////////////////////////////////////////////////////////////////
virtual void closeFile(Stream *stream);
//////////////////////////////////////////////////////////////////////////
/// @brief Open a file within the zip file for read
///
/// This method exists mainly for the integration with the resource manager.
/// Unless there is good reason to use this method, it is better to use the
/// openFile() method instead.
///
/// @param fileCD Pointer to central directory of the file to open
/// @return Pointer to stream or NULL for failure
/// @see ZipArchive::openFile(const char *, AccessMode), ZipArchive::closeFile()
//////////////////////////////////////////////////////////////////////////
Stream *openFileForRead(const CentralDir *fileCD);
// @}
/// @name Archiver Style File Access Methods
// @{
//////////////////////////////////////////////////////////////////////////
/// @brief Add a file to the zip
///
/// If replace is false and the file already exists in the zip, this function
/// will fail and return false. If replace is true, the existing file will be
/// overwritten.
///
/// @param filename Filename on the local file system to add
/// @param pathInZip The path and filename in the zip file to give this file
/// @param replace true to replace existing files, false otherwise
/// @return true for success, false for failure
/// @see ZipArchive::extractFile(), ZipArchive::deleteFile(), ZipArchive::isVerbose()
//////////////////////////////////////////////////////////////////////////
bool addFile(const char *filename, const char *pathInZip, bool replace = true);
//////////////////////////////////////////////////////////////////////////
/// @brief Extract a file from the zip
///
/// The file will be created through the resource manager and so must be
/// in a location that is writable.
///
/// The file will be CRC checked during extraction and extractFile() will
/// return false if the CRC check failed. The CRC check is just an advisory,
/// the output file will still exist if the CRC check failed. It is up to
/// the caller to decide what to do in the event of a CRC failure.
///
/// You can optionally pass a pointer to a bool to determine if a CRC check
/// failed. If extractFile() returns false and crcFail is false then the failure
/// was not CRC related. If crcFail is true and extractFile() returns false,
/// then the CRC check failed but the file otherwise extracted OK. You can
/// take your chances as to whether the data is valid or not, if you wish.
///
/// In verbose mode, extractFile() will display an error in the console when
/// a file fails the CRC check.
///
/// @param pathInZip The path and filename in the zip file to extract
/// @param filename Filename on the local file system to extract to
/// @param crcFail Pointer to a boolean that receives the result of the CRC check
/// @return true for success, false for failure
/// @see ZipArchive::addFile(), ZipArchive::deleteFile(), ZipArchive::isVerbose()
//////////////////////////////////////////////////////////////////////////
bool extractFile(const char *pathInZip, const char *filename, bool *crcFail = NULL);
//////////////////////////////////////////////////////////////////////////
/// @brief Delete a file from the zip
///
/// Flags a file as deleted so it is removed when the zip file is rebuilt.
///
/// @param filename Filename in the zip to delete
/// @return true for success, false for failure
/// @see ZipArchive::addFile(), ZipArchive::extractFile(), ZipArchive::isVerbose()
//////////////////////////////////////////////////////////////////////////
bool deleteFile(const char *filename);
// @}
/// @name Enumeration Methods
// @{
//////////////////////////////////////////////////////////////////////////
/// @brief Get number of entries in the central directory
///
/// @see ZipArchive::findFileInfo(const char *)
//////////////////////////////////////////////////////////////////////////
U32 numEntries() const { return mEntries.size(); }
//////////////////////////////////////////////////////////////////////////
/// Get a central directory entry
//////////////////////////////////////////////////////////////////////////
const CentralDir & operator[](const U32 idx) const { return mEntries[idx]->mCD; }
//////////////////////////////////////////////////////////////////////////
/// @brief Find a file in the zip
///
/// @param filename Path and filename to find
/// @return Pointer to the central directory entry
//////////////////////////////////////////////////////////////////////////
CentralDir *findFileInfo(const char *filename);
// @}
};
// @}
} // end namespace Zip
// @}
#endif // _ZIPARCHIVE_H_