2008-05-11 17:21:14 +00:00
|
|
|
/****************************************************************************
|
|
|
|
** Filename: unzip.cpp
|
|
|
|
** Last updated [dd/mm/yyyy]: 28/01/2007
|
|
|
|
**
|
|
|
|
** pkzip 2.0 decompression.
|
|
|
|
**
|
|
|
|
** Some of the code has been inspired by other open source projects,
|
|
|
|
** (mainly Info-Zip and Gilles Vollant's minizip).
|
|
|
|
** Compression and decompression actually uses the zlib library.
|
|
|
|
**
|
|
|
|
** Copyright (C) 2007 Angius Fabrizio. All rights reserved.
|
|
|
|
**
|
|
|
|
** This file is part of the OSDaB project (http://osdab.sourceforge.net/).
|
|
|
|
**
|
|
|
|
** This file may be distributed and/or modified under the terms of the
|
|
|
|
** GNU General Public License version 2 as published by the Free Software
|
|
|
|
** Foundation and appearing in the file LICENSE.GPL included in the
|
|
|
|
** packaging of this file.
|
|
|
|
**
|
|
|
|
** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
|
|
|
** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
|
**
|
|
|
|
** See the file LICENSE.GPL that came with this software distribution or
|
|
|
|
** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information.
|
|
|
|
**
|
|
|
|
**********************************************************************/
|
|
|
|
|
|
|
|
#include "unzip.h"
|
|
|
|
#include "unzip_p.h"
|
|
|
|
#include "zipentry_p.h"
|
|
|
|
|
|
|
|
#include <QString>
|
|
|
|
#include <QStringList>
|
|
|
|
#include <QDir>
|
|
|
|
#include <QFile>
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
|
|
|
// You can remove this #include if you replace the qDebug() statements.
|
|
|
|
#include <QtDebug>
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\class UnZip unzip.h
|
|
|
|
|
|
|
|
\brief PKZip 2.0 file decompression.
|
|
|
|
Compatibility with later versions is not ensured as they may use
|
|
|
|
unsupported compression algorithms.
|
|
|
|
Versions after 2.7 may have an incompatible header format and thus be
|
|
|
|
completely incompatible.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*! \enum UnZip::ErrorCode The result of a decompression operation.
|
|
|
|
\value UnZip::Ok No error occurred.
|
|
|
|
\value UnZip::ZlibInit Failed to init or load the zlib library.
|
|
|
|
\value UnZip::ZlibError The zlib library returned some error.
|
|
|
|
\value UnZip::OpenFailed Unable to create or open a device.
|
|
|
|
\value UnZip::PartiallyCorrupted Corrupted zip archive - some files could be extracted.
|
|
|
|
\value UnZip::Corrupted Corrupted or invalid zip archive.
|
|
|
|
\value UnZip::WrongPassword Unable to decrypt a password protected file.
|
|
|
|
\value UnZip::NoOpenArchive No archive has been opened yet.
|
|
|
|
\value UnZip::FileNotFound Unable to find the requested file in the archive.
|
|
|
|
\value UnZip::ReadFailed Reading of a file failed.
|
|
|
|
\value UnZip::WriteFailed Writing of a file failed.
|
|
|
|
\value UnZip::SeekFailed Seek failed.
|
|
|
|
\value UnZip::CreateDirFailed Could not create a directory.
|
|
|
|
\value UnZip::InvalidDevice A null device has been passed as parameter.
|
|
|
|
\value UnZip::InvalidArchive This is not a valid (or supported) ZIP archive.
|
|
|
|
\value UnZip::HeaderConsistencyError Local header record info does not match with the central directory record info. The archive may be corrupted.
|
|
|
|
|
|
|
|
\value UnZip::Skip Internal use only.
|
|
|
|
\value UnZip::SkipAll Internal use only.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*! \enum UnZip::ExtractionOptions Some options for the file extraction methods.
|
|
|
|
\value UnZip::ExtractPaths Default. Does not ignore the path of the zipped files.
|
|
|
|
\value UnZip::SkipPaths Default. Ignores the path of the zipped files and extracts them all to the same root directory.
|
|
|
|
*/
|
|
|
|
|
|
|
|
//! Local header size (excluding signature, excluding variable length fields)
|
|
|
|
#define UNZIP_LOCAL_HEADER_SIZE 26
|
|
|
|
//! Central Directory file entry size (excluding signature, excluding variable length fields)
|
|
|
|
#define UNZIP_CD_ENTRY_SIZE_NS 42
|
|
|
|
//! Data descriptor size (excluding signature)
|
|
|
|
#define UNZIP_DD_SIZE 12
|
|
|
|
//! End Of Central Directory size (including signature, excluding variable length fields)
|
|
|
|
#define UNZIP_EOCD_SIZE 22
|
|
|
|
//! Local header entry encryption header size
|
|
|
|
#define UNZIP_LOCAL_ENC_HEADER_SIZE 12
|
|
|
|
|
|
|
|
// Some offsets inside a CD record (excluding signature)
|
|
|
|
#define UNZIP_CD_OFF_VERSION 0
|
|
|
|
#define UNZIP_CD_OFF_GPFLAG 4
|
|
|
|
#define UNZIP_CD_OFF_CMETHOD 6
|
|
|
|
#define UNZIP_CD_OFF_MODT 8
|
|
|
|
#define UNZIP_CD_OFF_MODD 10
|
|
|
|
#define UNZIP_CD_OFF_CRC32 12
|
|
|
|
#define UNZIP_CD_OFF_CSIZE 16
|
|
|
|
#define UNZIP_CD_OFF_USIZE 20
|
|
|
|
#define UNZIP_CD_OFF_NAMELEN 24
|
|
|
|
#define UNZIP_CD_OFF_XLEN 26
|
|
|
|
#define UNZIP_CD_OFF_COMMLEN 28
|
|
|
|
#define UNZIP_CD_OFF_LHOFFSET 38
|
|
|
|
|
|
|
|
// Some offsets inside a local header record (excluding signature)
|
|
|
|
#define UNZIP_LH_OFF_VERSION 0
|
|
|
|
#define UNZIP_LH_OFF_GPFLAG 2
|
|
|
|
#define UNZIP_LH_OFF_CMETHOD 4
|
|
|
|
#define UNZIP_LH_OFF_MODT 6
|
|
|
|
#define UNZIP_LH_OFF_MODD 8
|
|
|
|
#define UNZIP_LH_OFF_CRC32 10
|
|
|
|
#define UNZIP_LH_OFF_CSIZE 14
|
|
|
|
#define UNZIP_LH_OFF_USIZE 18
|
|
|
|
#define UNZIP_LH_OFF_NAMELEN 22
|
|
|
|
#define UNZIP_LH_OFF_XLEN 24
|
|
|
|
|
|
|
|
// Some offsets inside a data descriptor record (excluding signature)
|
|
|
|
#define UNZIP_DD_OFF_CRC32 0
|
|
|
|
#define UNZIP_DD_OFF_CSIZE 4
|
|
|
|
#define UNZIP_DD_OFF_USIZE 8
|
|
|
|
|
|
|
|
// Some offsets inside a EOCD record
|
|
|
|
#define UNZIP_EOCD_OFF_ENTRIES 6
|
|
|
|
#define UNZIP_EOCD_OFF_CDOFF 12
|
|
|
|
#define UNZIP_EOCD_OFF_COMMLEN 16
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Max version handled by this API.
|
|
|
|
0x1B = 2.7 --> full compatibility only up to version 2.0 (0x14)
|
|
|
|
versions from 2.1 to 2.7 may use unsupported compression methods
|
|
|
|
versions after 2.7 may have an incompatible header format
|
|
|
|
*/
|
|
|
|
#define UNZIP_VERSION 0x1B
|
|
|
|
//! Full compatibility granted until this version
|
|
|
|
#define UNZIP_VERSION_STRICT 0x14
|
|
|
|
|
|
|
|
//! CRC32 routine
|
|
|
|
#define CRC32(c, b) crcTable[((int)c^b) & 0xff] ^ (c >> 8)
|
|
|
|
|
|
|
|
//! Checks if some file has been already extracted.
|
|
|
|
#define UNZIP_CHECK_FOR_VALID_DATA \
|
|
|
|
{\
|
|
|
|
if (headers != 0)\
|
|
|
|
{\
|
|
|
|
qDebug() << "Corrupted zip archive. Some files might be extracted.";\
|
|
|
|
ec = headers->size() != 0 ? UnZip::PartiallyCorrupted : UnZip::Corrupted;\
|
|
|
|
break;\
|
|
|
|
}\
|
|
|
|
else\
|
|
|
|
{\
|
|
|
|
delete device;\
|
|
|
|
device = 0;\
|
|
|
|
qDebug() << "Corrupted or invalid zip archive";\
|
|
|
|
ec = UnZip::Corrupted;\
|
|
|
|
break;\
|
|
|
|
}\
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/************************************************************************
|
|
|
|
Public interface
|
|
|
|
*************************************************************************/
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Creates a new Zip file decompressor.
|
|
|
|
*/
|
|
|
|
UnZip::UnZip()
|
|
|
|
{
|
|
|
|
d = new UnzipPrivate;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Closes any open archive and releases used resources.
|
|
|
|
*/
|
|
|
|
UnZip::~UnZip()
|
|
|
|
{
|
|
|
|
closeArchive();
|
|
|
|
delete d;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Returns true if there is an open archive.
|
|
|
|
*/
|
|
|
|
bool UnZip::isOpen() const
|
|
|
|
{
|
|
|
|
return d->device != 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Opens a zip archive and reads the files list. Closes any previously opened archive.
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::openArchive(const QString& filename)
|
|
|
|
{
|
|
|
|
QFile* file = new QFile(filename);
|
|
|
|
|
|
|
|
if (!file->exists()) {
|
|
|
|
delete file;
|
|
|
|
return UnZip::FileNotFound;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!file->open(QIODevice::ReadOnly)) {
|
|
|
|
delete file;
|
|
|
|
return UnZip::OpenFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
return openArchive(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Opens a zip archive and reads the entries list.
|
|
|
|
Closes any previously opened archive.
|
|
|
|
\warning The class takes ownership of the device so don't delete it!
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::openArchive(QIODevice* device)
|
|
|
|
{
|
|
|
|
if (device == 0)
|
|
|
|
{
|
|
|
|
qDebug() << "Invalid device.";
|
|
|
|
return UnZip::InvalidDevice;
|
|
|
|
}
|
|
|
|
|
|
|
|
return d->openArchive(device);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Closes the archive and releases all the used resources (like cached passwords).
|
|
|
|
*/
|
|
|
|
void UnZip::closeArchive()
|
|
|
|
{
|
|
|
|
d->closeArchive();
|
|
|
|
}
|
|
|
|
|
|
|
|
QString UnZip::archiveComment() const
|
|
|
|
{
|
|
|
|
if (d->device == 0)
|
|
|
|
return QString();
|
|
|
|
return d->comment;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Returns a locale translated error string for a given error code.
|
|
|
|
*/
|
|
|
|
QString UnZip::formatError(UnZip::ErrorCode c) const
|
|
|
|
{
|
|
|
|
switch (c)
|
|
|
|
{
|
|
|
|
case Ok: return QCoreApplication::translate("UnZip", "ZIP operation completed successfully."); break;
|
|
|
|
case ZlibInit: return QCoreApplication::translate("UnZip", "Failed to initialize or load zlib library."); break;
|
|
|
|
case ZlibError: return QCoreApplication::translate("UnZip", "zlib library error."); break;
|
|
|
|
case OpenFailed: return QCoreApplication::translate("UnZip", "Unable to create or open file."); break;
|
|
|
|
case PartiallyCorrupted: return QCoreApplication::translate("UnZip", "Partially corrupted archive. Some files might be extracted."); break;
|
|
|
|
case Corrupted: return QCoreApplication::translate("UnZip", "Corrupted archive."); break;
|
|
|
|
case WrongPassword: return QCoreApplication::translate("UnZip", "Wrong password."); break;
|
|
|
|
case NoOpenArchive: return QCoreApplication::translate("UnZip", "No archive has been created yet."); break;
|
|
|
|
case FileNotFound: return QCoreApplication::translate("UnZip", "File or directory does not exist."); break;
|
|
|
|
case ReadFailed: return QCoreApplication::translate("UnZip", "File read error."); break;
|
|
|
|
case WriteFailed: return QCoreApplication::translate("UnZip", "File write error."); break;
|
|
|
|
case SeekFailed: return QCoreApplication::translate("UnZip", "File seek error."); break;
|
|
|
|
case CreateDirFailed: return QCoreApplication::translate("UnZip", "Unable to create a directory."); break;
|
|
|
|
case InvalidDevice: return QCoreApplication::translate("UnZip", "Invalid device."); break;
|
|
|
|
case InvalidArchive: return QCoreApplication::translate("UnZip", "Invalid or incompatible zip archive."); break;
|
|
|
|
case HeaderConsistencyError: return QCoreApplication::translate("UnZip", "Inconsistent headers. Archive might be corrupted."); break;
|
|
|
|
default: ;
|
|
|
|
}
|
|
|
|
|
|
|
|
return QCoreApplication::translate("UnZip", "Unknown error.");
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Returns true if the archive contains a file with the given path and name.
|
|
|
|
*/
|
|
|
|
bool UnZip::contains(const QString& file) const
|
|
|
|
{
|
|
|
|
if (d->headers == 0)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return d->headers->contains(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Returns complete paths of files and directories in this archive.
|
|
|
|
*/
|
|
|
|
QStringList UnZip::fileList() const
|
|
|
|
{
|
|
|
|
return d->headers == 0 ? QStringList() : d->headers->keys();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Returns information for each (correctly parsed) entry of this archive.
|
|
|
|
*/
|
|
|
|
QList<UnZip::ZipEntry> UnZip::entryList() const
|
|
|
|
{
|
|
|
|
QList<UnZip::ZipEntry> list;
|
|
|
|
|
|
|
|
if (d->headers != 0)
|
|
|
|
{
|
|
|
|
for (QMap<QString,ZipEntryP*>::ConstIterator it = d->headers->constBegin(); it != d->headers->constEnd(); ++it)
|
|
|
|
{
|
|
|
|
const ZipEntryP* entry = it.value();
|
|
|
|
Q_ASSERT(entry != 0);
|
|
|
|
|
|
|
|
ZipEntry z;
|
|
|
|
|
|
|
|
z.filename = it.key();
|
|
|
|
if (!entry->comment.isEmpty())
|
|
|
|
z.comment = entry->comment;
|
|
|
|
z.compressedSize = entry->szComp;
|
|
|
|
z.uncompressedSize = entry->szUncomp;
|
|
|
|
z.crc32 = entry->crc;
|
|
|
|
z.lastModified = d->convertDateTime(entry->modDate, entry->modTime);
|
|
|
|
|
|
|
|
z.compression = entry->compMethod == 0 ? NoCompression : entry->compMethod == 8 ? Deflated : UnknownCompression;
|
|
|
|
z.type = z.filename.endsWith("/") ? Directory : File;
|
|
|
|
|
|
|
|
z.encrypted = entry->isEncrypted();
|
|
|
|
|
|
|
|
list.append(z);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Extracts the whole archive to a directory.
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::extractAll(const QString& dirname, ExtractionOptions options)
|
|
|
|
{
|
|
|
|
return extractAll(QDir(dirname), options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Extracts the whole archive to a directory.
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::extractAll(const QDir& dir, ExtractionOptions options)
|
|
|
|
{
|
|
|
|
// this should only happen if we didn't call openArchive() yet
|
|
|
|
if (d->device == 0)
|
|
|
|
return NoOpenArchive;
|
|
|
|
|
|
|
|
if (d->headers == 0)
|
|
|
|
return Ok;
|
|
|
|
|
|
|
|
bool end = false;
|
|
|
|
for (QMap<QString,ZipEntryP*>::Iterator itr = d->headers->begin(); itr != d->headers->end(); ++itr)
|
|
|
|
{
|
|
|
|
ZipEntryP* entry = itr.value();
|
|
|
|
Q_ASSERT(entry != 0);
|
|
|
|
|
|
|
|
if ((entry->isEncrypted()) && d->skipAllEncrypted)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
switch (d->extractFile(itr.key(), *entry, dir, options))
|
|
|
|
{
|
|
|
|
case Corrupted:
|
|
|
|
qDebug() << "Removing corrupted entry" << itr.key();
|
|
|
|
d->headers->erase(itr++);
|
|
|
|
if (itr == d->headers->end())
|
|
|
|
end = true;
|
|
|
|
break;
|
|
|
|
case CreateDirFailed:
|
|
|
|
break;
|
|
|
|
case Skip:
|
|
|
|
break;
|
|
|
|
case SkipAll:
|
|
|
|
d->skipAllEncrypted = true;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (end)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Extracts a single file to a directory.
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QString& dirname, ExtractionOptions options)
|
|
|
|
{
|
|
|
|
return extractFile(filename, QDir(dirname), options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Extracts a single file to a directory.
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QDir& dir, ExtractionOptions options)
|
|
|
|
{
|
|
|
|
QMap<QString,ZipEntryP*>::Iterator itr = d->headers->find(filename);
|
|
|
|
if (itr != d->headers->end())
|
|
|
|
{
|
|
|
|
ZipEntryP* entry = itr.value();
|
|
|
|
Q_ASSERT(entry != 0);
|
|
|
|
return d->extractFile(itr.key(), *entry, dir, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
return FileNotFound;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Extracts a single file to a directory.
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::extractFile(const QString& filename, QIODevice* dev, ExtractionOptions options)
|
|
|
|
{
|
|
|
|
if (dev == 0)
|
|
|
|
return InvalidDevice;
|
|
|
|
|
|
|
|
QMap<QString,ZipEntryP*>::Iterator itr = d->headers->find(filename);
|
|
|
|
if (itr != d->headers->end()) {
|
|
|
|
ZipEntryP* entry = itr.value();
|
|
|
|
Q_ASSERT(entry != 0);
|
|
|
|
return d->extractFile(itr.key(), *entry, dev, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
return FileNotFound;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Extracts a list of files.
|
|
|
|
Stops extraction at the first error (but continues if a file does not exist in the archive).
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QString& dirname, ExtractionOptions options)
|
|
|
|
{
|
|
|
|
QDir dir(dirname);
|
|
|
|
ErrorCode ec;
|
|
|
|
|
|
|
|
for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr)
|
|
|
|
{
|
|
|
|
ec = extractFile(*itr, dir, options);
|
|
|
|
if (ec == FileNotFound)
|
|
|
|
continue;
|
|
|
|
if (ec != Ok)
|
|
|
|
return ec;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Extracts a list of files.
|
|
|
|
Stops extraction at the first error (but continues if a file does not exist in the archive).
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QDir& dir, ExtractionOptions options)
|
|
|
|
{
|
|
|
|
ErrorCode ec;
|
|
|
|
|
|
|
|
for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr)
|
|
|
|
{
|
|
|
|
ec = extractFile(*itr, dir, options);
|
|
|
|
if (ec == FileNotFound)
|
|
|
|
continue;
|
|
|
|
if (ec != Ok)
|
|
|
|
return ec;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Remove/replace this method to add your own password retrieval routine.
|
|
|
|
*/
|
|
|
|
void UnZip::setPassword(const QString& pwd)
|
|
|
|
{
|
|
|
|
d->password = pwd;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
ZipEntry constructor - initialize data. Type is set to File.
|
|
|
|
*/
|
|
|
|
UnZip::ZipEntry::ZipEntry()
|
|
|
|
{
|
|
|
|
compressedSize = uncompressedSize = crc32 = 0;
|
|
|
|
compression = NoCompression;
|
|
|
|
type = File;
|
|
|
|
encrypted = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/************************************************************************
|
|
|
|
Private interface
|
|
|
|
*************************************************************************/
|
|
|
|
|
|
|
|
//! \internal
|
|
|
|
UnzipPrivate::UnzipPrivate()
|
|
|
|
{
|
|
|
|
skipAllEncrypted = false;
|
|
|
|
headers = 0;
|
|
|
|
device = 0;
|
|
|
|
|
|
|
|
uBuffer = (unsigned char*) buffer1;
|
|
|
|
crcTable = (quint32*) get_crc_table();
|
|
|
|
|
|
|
|
cdOffset = eocdOffset = 0;
|
|
|
|
cdEntryCount = 0;
|
|
|
|
unsupportedEntryCount = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
//! \internal Parses a Zip archive.
|
|
|
|
UnZip::ErrorCode UnzipPrivate::openArchive(QIODevice* dev)
|
|
|
|
{
|
|
|
|
Q_ASSERT(dev != 0);
|
|
|
|
|
|
|
|
if (device != 0)
|
|
|
|
closeArchive();
|
|
|
|
|
|
|
|
device = dev;
|
|
|
|
|
|
|
|
if (!(device->isOpen() || device->open(QIODevice::ReadOnly)))
|
|
|
|
{
|
|
|
|
delete device;
|
|
|
|
device = 0;
|
|
|
|
|
|
|
|
qDebug() << "Unable to open device for reading";
|
|
|
|
return UnZip::OpenFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
UnZip::ErrorCode ec;
|
|
|
|
|
|
|
|
ec = seekToCentralDirectory();
|
|
|
|
if (ec != UnZip::Ok)
|
|
|
|
{
|
|
|
|
closeArchive();
|
|
|
|
return ec;
|
|
|
|
}
|
|
|
|
|
|
|
|
//! \todo Ignore CD entry count? CD may be corrupted.
|
|
|
|
if (cdEntryCount == 0)
|
|
|
|
{
|
|
|
|
return UnZip::Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool continueParsing = true;
|
|
|
|
|
|
|
|
while (continueParsing)
|
|
|
|
{
|
|
|
|
if (device->read(buffer1, 4) != 4)
|
|
|
|
UNZIP_CHECK_FOR_VALID_DATA
|
|
|
|
|
|
|
|
if (! (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x01 && buffer1[3] == 0x02) )
|
|
|
|
break;
|
|
|
|
|
|
|
|
if ( (ec = parseCentralDirectoryRecord()) != UnZip::Ok )
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ec != UnZip::Ok)
|
|
|
|
closeArchive();
|
|
|
|
|
|
|
|
return ec;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
\internal Parses a local header record and makes some consistency check
|
|
|
|
with the information stored in the Central Directory record for this entry
|
|
|
|
that has been previously parsed.
|
|
|
|
\todo Optional consistency check (as a ExtractionOptions flag)
|
|
|
|
|
|
|
|
local file header signature 4 bytes (0x04034b50)
|
|
|
|
version needed to extract 2 bytes
|
|
|
|
general purpose bit flag 2 bytes
|
|
|
|
compression method 2 bytes
|
|
|
|
last mod file time 2 bytes
|
|
|
|
last mod file date 2 bytes
|
|
|
|
crc-32 4 bytes
|
|
|
|
compressed size 4 bytes
|
|
|
|
uncompressed size 4 bytes
|
|
|
|
file name length 2 bytes
|
|
|
|
extra field length 2 bytes
|
|
|
|
|
|
|
|
file name (variable size)
|
|
|
|
extra field (variable size)
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnzipPrivate::parseLocalHeaderRecord(const QString& path, ZipEntryP& entry)
|
|
|
|
{
|
|
|
|
if (!device->seek(entry.lhOffset))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
|
|
|
|
// Test signature
|
|
|
|
if (device->read(buffer1, 4) != 4)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
if ((buffer1[0] != 'P') || (buffer1[1] != 'K') || (buffer1[2] != 0x03) || (buffer1[3] != 0x04))
|
|
|
|
return UnZip::InvalidArchive;
|
|
|
|
|
|
|
|
if (device->read(buffer1, UNZIP_LOCAL_HEADER_SIZE) != UNZIP_LOCAL_HEADER_SIZE)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
/*
|
|
|
|
Check 3rd general purpose bit flag.
|
|
|
|
|
|
|
|
"bit 3: If this bit is set, the fields crc-32, compressed size
|
|
|
|
and uncompressed size are set to zero in the local
|
|
|
|
header. The correct values are put in the data descriptor
|
|
|
|
immediately following the compressed data."
|
|
|
|
*/
|
|
|
|
bool hasDataDescriptor = entry.hasDataDescriptor();
|
|
|
|
|
|
|
|
bool checkFailed = false;
|
|
|
|
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.compMethod != getUShort(uBuffer, UNZIP_LH_OFF_CMETHOD);
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.gpFlag[0] != uBuffer[UNZIP_LH_OFF_GPFLAG];
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.gpFlag[1] != uBuffer[UNZIP_LH_OFF_GPFLAG + 1];
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.modTime[0] != uBuffer[UNZIP_LH_OFF_MODT];
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.modTime[1] != uBuffer[UNZIP_LH_OFF_MODT + 1];
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.modDate[0] != uBuffer[UNZIP_LH_OFF_MODD];
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.modDate[1] != uBuffer[UNZIP_LH_OFF_MODD + 1];
|
|
|
|
if (!hasDataDescriptor)
|
|
|
|
{
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.crc != getULong(uBuffer, UNZIP_LH_OFF_CRC32);
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.szComp != getULong(uBuffer, UNZIP_LH_OFF_CSIZE);
|
|
|
|
if (!checkFailed)
|
|
|
|
checkFailed = entry.szUncomp != getULong(uBuffer, UNZIP_LH_OFF_USIZE);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (checkFailed)
|
|
|
|
return UnZip::HeaderConsistencyError;
|
|
|
|
|
|
|
|
// Check filename
|
|
|
|
quint16 szName = getUShort(uBuffer, UNZIP_LH_OFF_NAMELEN);
|
|
|
|
if (szName == 0)
|
|
|
|
return UnZip::HeaderConsistencyError;
|
|
|
|
|
|
|
|
if (device->read(buffer2, szName) != szName)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
QString filename = QString::fromAscii(buffer2, szName);
|
|
|
|
if (filename != path)
|
|
|
|
{
|
|
|
|
qDebug() << "Filename in local header mismatches.";
|
|
|
|
return UnZip::HeaderConsistencyError;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip extra field
|
|
|
|
quint16 szExtra = getUShort(uBuffer, UNZIP_LH_OFF_XLEN);
|
|
|
|
if (szExtra != 0)
|
|
|
|
{
|
|
|
|
if (!device->seek(device->pos() + szExtra))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
entry.dataOffset = device->pos();
|
|
|
|
|
|
|
|
if (hasDataDescriptor)
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
The data descriptor has this OPTIONAL signature: PK\7\8
|
|
|
|
We try to skip the compressed data relying on the size set in the
|
|
|
|
Central Directory record.
|
|
|
|
*/
|
|
|
|
if (!device->seek(device->pos() + entry.szComp))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
|
|
|
|
// Read 4 bytes and check if there is a data descriptor signature
|
|
|
|
if (device->read(buffer2, 4) != 4)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
bool hasSignature = buffer2[0] == 'P' && buffer2[1] == 'K' && buffer2[2] == 0x07 && buffer2[3] == 0x08;
|
|
|
|
if (hasSignature)
|
|
|
|
{
|
|
|
|
if (device->read(buffer2, UNZIP_DD_SIZE) != UNZIP_DD_SIZE)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (device->read(buffer2 + 4, UNZIP_DD_SIZE - 4) != UNZIP_DD_SIZE - 4)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
// DD: crc, compressed size, uncompressed size
|
|
|
|
if (
|
|
|
|
entry.crc != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CRC32) ||
|
|
|
|
entry.szComp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CSIZE) ||
|
|
|
|
entry.szUncomp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_USIZE)
|
|
|
|
)
|
|
|
|
return UnZip::HeaderConsistencyError;
|
|
|
|
}
|
|
|
|
|
|
|
|
return UnZip::Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \internal Attempts to find the start of the central directory record.
|
|
|
|
|
|
|
|
We seek the file back until we reach the "End Of Central Directory"
|
|
|
|
signature PK\5\6.
|
|
|
|
|
|
|
|
end of central dir signature 4 bytes (0x06054b50)
|
|
|
|
number of this disk 2 bytes
|
|
|
|
number of the disk with the
|
|
|
|
start of the central directory 2 bytes
|
|
|
|
total number of entries in the
|
|
|
|
central directory on this disk 2 bytes
|
|
|
|
total number of entries in
|
|
|
|
the central directory 2 bytes
|
|
|
|
size of the central directory 4 bytes
|
|
|
|
offset of start of central
|
|
|
|
directory with respect to
|
|
|
|
the starting disk number 4 bytes
|
|
|
|
.ZIP file comment length 2 bytes
|
|
|
|
--- SIZE UNTIL HERE: UNZIP_EOCD_SIZE ---
|
|
|
|
.ZIP file comment (variable size)
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnzipPrivate::seekToCentralDirectory()
|
|
|
|
{
|
|
|
|
qint64 length = device->size();
|
|
|
|
qint64 offset = length - UNZIP_EOCD_SIZE;
|
|
|
|
|
|
|
|
if (length < UNZIP_EOCD_SIZE)
|
|
|
|
return UnZip::InvalidArchive;
|
|
|
|
|
|
|
|
if (!device->seek( offset ))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
|
|
|
|
if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
bool eocdFound = (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x05 && buffer1[3] == 0x06);
|
|
|
|
|
|
|
|
if (eocdFound)
|
|
|
|
{
|
|
|
|
// Zip file has no comment (the only variable length field in the EOCD record)
|
|
|
|
eocdOffset = offset;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
qint64 read;
|
|
|
|
char* p = 0;
|
|
|
|
|
|
|
|
offset -= UNZIP_EOCD_SIZE;
|
|
|
|
|
|
|
|
if (offset <= 0)
|
|
|
|
return UnZip::InvalidArchive;
|
|
|
|
|
|
|
|
if (!device->seek( offset ))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
|
|
|
|
while ((read = device->read(buffer1, UNZIP_EOCD_SIZE)) >= 0)
|
|
|
|
{
|
|
|
|
if ( (p = strstr(buffer1, "PK\5\6")) != 0)
|
|
|
|
{
|
|
|
|
// Seek to the start of the EOCD record so we can read it fully
|
|
|
|
// Yes... we could simply read the missing bytes and append them to the buffer
|
|
|
|
// but this is far easier so heck it!
|
|
|
|
device->seek( offset + (p - buffer1) );
|
|
|
|
eocdFound = true;
|
|
|
|
eocdOffset = offset + (p - buffer1);
|
|
|
|
|
|
|
|
// Read EOCD record
|
|
|
|
if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
offset -= UNZIP_EOCD_SIZE;
|
|
|
|
if (offset <= 0)
|
|
|
|
return UnZip::InvalidArchive;
|
|
|
|
|
|
|
|
if (!device->seek( offset ))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!eocdFound)
|
|
|
|
return UnZip::InvalidArchive;
|
|
|
|
|
|
|
|
// Parse EOCD to locate CD offset
|
|
|
|
offset = getULong((const unsigned char*)buffer1, UNZIP_EOCD_OFF_CDOFF + 4);
|
|
|
|
|
|
|
|
cdOffset = offset;
|
|
|
|
|
|
|
|
cdEntryCount = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_ENTRIES + 4);
|
|
|
|
|
|
|
|
quint16 commentLength = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_COMMLEN + 4);
|
|
|
|
if (commentLength != 0)
|
|
|
|
{
|
|
|
|
QByteArray c = device->read(commentLength);
|
|
|
|
if (c.count() != commentLength)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
comment = c;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Seek to the start of the CD record
|
|
|
|
if (!device->seek( cdOffset ))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
|
|
|
|
return UnZip::Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Parses a central directory record.
|
|
|
|
|
|
|
|
Central Directory record structure:
|
|
|
|
|
|
|
|
[file header 1]
|
|
|
|
.
|
|
|
|
.
|
|
|
|
.
|
|
|
|
[file header n]
|
|
|
|
[digital signature] // PKZip 6.2 or later only
|
|
|
|
|
|
|
|
File header:
|
|
|
|
|
|
|
|
central file header signature 4 bytes (0x02014b50)
|
|
|
|
version made by 2 bytes
|
|
|
|
version needed to extract 2 bytes
|
|
|
|
general purpose bit flag 2 bytes
|
|
|
|
compression method 2 bytes
|
|
|
|
last mod file time 2 bytes
|
|
|
|
last mod file date 2 bytes
|
|
|
|
crc-32 4 bytes
|
|
|
|
compressed size 4 bytes
|
|
|
|
uncompressed size 4 bytes
|
|
|
|
file name length 2 bytes
|
|
|
|
extra field length 2 bytes
|
|
|
|
file comment length 2 bytes
|
|
|
|
disk number start 2 bytes
|
|
|
|
internal file attributes 2 bytes
|
|
|
|
external file attributes 4 bytes
|
|
|
|
relative offset of local header 4 bytes
|
|
|
|
|
|
|
|
file name (variable size)
|
|
|
|
extra field (variable size)
|
|
|
|
file comment (variable size)
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnzipPrivate::parseCentralDirectoryRecord()
|
|
|
|
{
|
|
|
|
// Read CD record
|
|
|
|
if (device->read(buffer1, UNZIP_CD_ENTRY_SIZE_NS) != UNZIP_CD_ENTRY_SIZE_NS)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
|
|
|
|
bool skipEntry = false;
|
|
|
|
|
|
|
|
// Get compression type so we can skip non compatible algorithms
|
|
|
|
quint16 compMethod = getUShort(uBuffer, UNZIP_CD_OFF_CMETHOD);
|
|
|
|
|
|
|
|
// Get variable size fields length so we can skip the whole record
|
|
|
|
// if necessary
|
|
|
|
quint16 szName = getUShort(uBuffer, UNZIP_CD_OFF_NAMELEN);
|
|
|
|
quint16 szExtra = getUShort(uBuffer, UNZIP_CD_OFF_XLEN);
|
|
|
|
quint16 szComment = getUShort(uBuffer, UNZIP_CD_OFF_COMMLEN);
|
|
|
|
|
|
|
|
quint32 skipLength = szName + szExtra + szComment;
|
|
|
|
|
|
|
|
UnZip::ErrorCode ec = UnZip::Ok;
|
|
|
|
|
|
|
|
if ((compMethod != 0) && (compMethod != 8))
|
|
|
|
{
|
|
|
|
qDebug() << "Unsupported compression method. Skipping file.";
|
|
|
|
skipEntry = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Header parsing may be a problem if version is bigger than UNZIP_VERSION
|
|
|
|
if (!skipEntry && buffer1[UNZIP_CD_OFF_VERSION] > UNZIP_VERSION)
|
|
|
|
{
|
|
|
|
qDebug() << "Unsupported PKZip version. Skipping file.";
|
|
|
|
skipEntry = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!skipEntry && szName == 0)
|
|
|
|
{
|
|
|
|
qDebug() << "Skipping file with no name.";
|
|
|
|
skipEntry = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!skipEntry && device->read(buffer2, szName) != szName)
|
|
|
|
{
|
|
|
|
ec = UnZip::ReadFailed;
|
|
|
|
skipEntry = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (skipEntry)
|
|
|
|
{
|
|
|
|
if (ec == UnZip::Ok)
|
|
|
|
{
|
|
|
|
if (!device->seek( device->pos() + skipLength ))
|
|
|
|
ec = UnZip::SeekFailed;
|
|
|
|
|
|
|
|
unsupportedEntryCount++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ec;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString filename = QString::fromAscii(buffer2, szName);
|
|
|
|
|
|
|
|
ZipEntryP* h = new ZipEntryP;
|
|
|
|
h->compMethod = compMethod;
|
|
|
|
|
|
|
|
h->gpFlag[0] = buffer1[UNZIP_CD_OFF_GPFLAG];
|
|
|
|
h->gpFlag[1] = buffer1[UNZIP_CD_OFF_GPFLAG + 1];
|
|
|
|
|
|
|
|
h->modTime[0] = buffer1[UNZIP_CD_OFF_MODT];
|
|
|
|
h->modTime[1] = buffer1[UNZIP_CD_OFF_MODT + 1];
|
|
|
|
|
|
|
|
h->modDate[0] = buffer1[UNZIP_CD_OFF_MODD];
|
|
|
|
h->modDate[1] = buffer1[UNZIP_CD_OFF_MODD + 1];
|
|
|
|
|
|
|
|
h->crc = getULong(uBuffer, UNZIP_CD_OFF_CRC32);
|
|
|
|
h->szComp = getULong(uBuffer, UNZIP_CD_OFF_CSIZE);
|
|
|
|
h->szUncomp = getULong(uBuffer, UNZIP_CD_OFF_USIZE);
|
|
|
|
|
|
|
|
// Skip extra field (if any)
|
|
|
|
if (szExtra != 0)
|
|
|
|
{
|
|
|
|
if (!device->seek( device->pos() + szExtra ))
|
|
|
|
{
|
|
|
|
delete h;
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read comment field (if any)
|
|
|
|
if (szComment != 0)
|
|
|
|
{
|
|
|
|
if (device->read(buffer2, szComment) != szComment)
|
|
|
|
{
|
|
|
|
delete h;
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
h->comment = QString::fromAscii(buffer2, szComment);
|
|
|
|
}
|
|
|
|
|
|
|
|
h->lhOffset = getULong(uBuffer, UNZIP_CD_OFF_LHOFFSET);
|
|
|
|
|
|
|
|
if (headers == 0)
|
|
|
|
headers = new QMap<QString, ZipEntryP*>();
|
|
|
|
headers->insert(filename, h);
|
|
|
|
|
|
|
|
return UnZip::Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
//! \internal Closes the archive and resets the internal status.
|
|
|
|
void UnzipPrivate::closeArchive()
|
|
|
|
{
|
|
|
|
if (device == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
skipAllEncrypted = false;
|
|
|
|
|
|
|
|
if (headers != 0)
|
|
|
|
{
|
|
|
|
qDeleteAll(*headers);
|
|
|
|
delete headers;
|
|
|
|
headers = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
delete device; device = 0;
|
|
|
|
|
|
|
|
cdOffset = eocdOffset = 0;
|
|
|
|
cdEntryCount = 0;
|
|
|
|
unsupportedEntryCount = 0;
|
|
|
|
|
|
|
|
comment.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
//! \internal
|
|
|
|
UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, ZipEntryP& entry, const QDir& dir, UnZip::ExtractionOptions options)
|
|
|
|
{
|
|
|
|
QString name(path);
|
|
|
|
QString dirname;
|
|
|
|
QString directory;
|
|
|
|
|
|
|
|
int pos = name.lastIndexOf('/');
|
|
|
|
|
|
|
|
// This entry is for a directory
|
|
|
|
if (pos == name.length() - 1)
|
|
|
|
{
|
|
|
|
if (options.testFlag(UnZip::SkipPaths))
|
|
|
|
return UnZip::Ok;
|
|
|
|
|
|
|
|
directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(name));
|
|
|
|
if (!createDirectory(directory))
|
|
|
|
{
|
|
|
|
qDebug() << QString("Unable to create directory: %1").arg(directory);
|
|
|
|
return UnZip::CreateDirFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
return UnZip::Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract path from entry
|
|
|
|
if (pos > 0)
|
|
|
|
{
|
|
|
|
// get directory part
|
|
|
|
dirname = name.left(pos);
|
|
|
|
if (options.testFlag(UnZip::SkipPaths))
|
|
|
|
{
|
|
|
|
directory = dir.absolutePath();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(dirname));
|
|
|
|
if (!createDirectory(directory))
|
|
|
|
{
|
|
|
|
qDebug() << QString("Unable to create directory: %1").arg(directory);
|
|
|
|
return UnZip::CreateDirFailed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
name = name.right(name.length() - pos - 1);
|
|
|
|
} else directory = dir.absolutePath();
|
|
|
|
|
|
|
|
name = QString("%1/%2").arg(directory).arg(name);
|
|
|
|
|
|
|
|
QFile outFile(name);
|
|
|
|
|
|
|
|
if (!outFile.open(QIODevice::WriteOnly))
|
|
|
|
{
|
|
|
|
qDebug() << QString("Unable to open %1 for writing").arg(name);
|
|
|
|
return UnZip::OpenFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
//! \todo Set creation/last_modified date/time
|
|
|
|
|
|
|
|
UnZip::ErrorCode ec = extractFile(path, entry, &outFile, options);
|
|
|
|
|
|
|
|
outFile.close();
|
|
|
|
|
|
|
|
if (ec != UnZip::Ok)
|
|
|
|
{
|
|
|
|
if (!outFile.remove())
|
|
|
|
qDebug() << QString("Unable to remove corrupted file: %1").arg(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ec;
|
|
|
|
}
|
|
|
|
|
|
|
|
//! \internal
|
|
|
|
UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, ZipEntryP& entry, QIODevice* dev, UnZip::ExtractionOptions options)
|
|
|
|
{
|
|
|
|
Q_UNUSED(options);
|
|
|
|
Q_ASSERT(dev != 0);
|
|
|
|
|
|
|
|
if (!entry.lhEntryChecked)
|
|
|
|
{
|
|
|
|
UnZip::ErrorCode ec = parseLocalHeaderRecord(path, entry);
|
|
|
|
entry.lhEntryChecked = true;
|
|
|
|
|
|
|
|
if (ec != UnZip::Ok)
|
|
|
|
return ec;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!device->seek(entry.dataOffset))
|
|
|
|
return UnZip::SeekFailed;
|
|
|
|
|
|
|
|
// Encryption keys
|
|
|
|
quint32 keys[3];
|
|
|
|
|
|
|
|
if (entry.isEncrypted())
|
|
|
|
{
|
|
|
|
UnZip::ErrorCode e = testPassword(keys, path, entry);
|
|
|
|
if (e != UnZip::Ok)
|
|
|
|
{
|
|
|
|
qDebug() << QString("Unable to decrypt %1").arg(path);
|
|
|
|
return e;
|
|
|
|
}//! Encryption header size
|
|
|
|
entry.szComp -= UNZIP_LOCAL_ENC_HEADER_SIZE; // remove encryption header size
|
|
|
|
}
|
|
|
|
|
|
|
|
if (entry.szComp == 0)
|
|
|
|
{
|
|
|
|
if (entry.crc != 0)
|
|
|
|
return UnZip::Corrupted;
|
|
|
|
|
|
|
|
return UnZip::Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
uInt rep = entry.szComp / UNZIP_READ_BUFFER;
|
|
|
|
uInt rem = entry.szComp % UNZIP_READ_BUFFER;
|
|
|
|
uInt cur = 0;
|
|
|
|
|
|
|
|
// extract data
|
|
|
|
qint64 read;
|
|
|
|
quint64 tot = 0;
|
|
|
|
|
|
|
|
quint32 myCRC = crc32(0L, Z_NULL, 0);
|
|
|
|
|
|
|
|
if (entry.compMethod == 0)
|
|
|
|
{
|
|
|
|
while ( (read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem)) > 0 )
|
|
|
|
{
|
|
|
|
if (entry.isEncrypted())
|
|
|
|
decryptBytes(keys, buffer1, read);
|
|
|
|
|
|
|
|
myCRC = crc32(myCRC, uBuffer, read);
|
|
|
|
|
|
|
|
if (dev->write(buffer1, read) != read)
|
|
|
|
return UnZip::WriteFailed;
|
|
|
|
|
|
|
|
cur++;
|
|
|
|
tot += read;
|
|
|
|
|
|
|
|
if (tot == entry.szComp)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (read < 0)
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
}
|
|
|
|
else if (entry.compMethod == 8)
|
|
|
|
{
|
|
|
|
/* Allocate inflate state */
|
|
|
|
z_stream zstr;
|
|
|
|
zstr.zalloc = Z_NULL;
|
|
|
|
zstr.zfree = Z_NULL;
|
|
|
|
zstr.opaque = Z_NULL;
|
|
|
|
zstr.next_in = Z_NULL;
|
|
|
|
zstr.avail_in = 0;
|
|
|
|
|
|
|
|
int zret;
|
|
|
|
|
|
|
|
// Use inflateInit2 with negative windowBits to get raw decompression
|
|
|
|
if ( (zret = inflateInit2_(&zstr, -MAX_WBITS, ZLIB_VERSION, sizeof(z_stream))) != Z_OK )
|
|
|
|
return UnZip::ZlibError;
|
|
|
|
|
|
|
|
int szDecomp;
|
|
|
|
|
|
|
|
// Decompress until deflate stream ends or end of file
|
|
|
|
do
|
|
|
|
{
|
|
|
|
read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem);
|
|
|
|
if (read == 0)
|
|
|
|
break;
|
|
|
|
if (read < 0)
|
|
|
|
{
|
|
|
|
(void)inflateEnd(&zstr);
|
|
|
|
return UnZip::ReadFailed;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (entry.isEncrypted())
|
|
|
|
decryptBytes(keys, buffer1, read);
|
|
|
|
|
|
|
|
cur++;
|
|
|
|
tot += read;
|
|
|
|
|
|
|
|
zstr.avail_in = (uInt) read;
|
|
|
|
zstr.next_in = (Bytef*) buffer1;
|
|
|
|
|
|
|
|
|
|
|
|
// Run inflate() on input until output buffer not full
|
|
|
|
do {
|
|
|
|
zstr.avail_out = UNZIP_READ_BUFFER;
|
|
|
|
zstr.next_out = (Bytef*) buffer2;;
|
|
|
|
|
|
|
|
zret = inflate(&zstr, Z_NO_FLUSH);
|
|
|
|
|
|
|
|
switch (zret) {
|
|
|
|
case Z_NEED_DICT:
|
|
|
|
case Z_DATA_ERROR:
|
|
|
|
case Z_MEM_ERROR:
|
|
|
|
inflateEnd(&zstr);
|
|
|
|
return UnZip::WriteFailed;
|
|
|
|
default:
|
|
|
|
;
|
|
|
|
}
|
|
|
|
|
|
|
|
szDecomp = UNZIP_READ_BUFFER - zstr.avail_out;
|
|
|
|
if (dev->write(buffer2, szDecomp) != szDecomp)
|
|
|
|
{
|
|
|
|
inflateEnd(&zstr);
|
|
|
|
return UnZip::ZlibError;
|
|
|
|
}
|
|
|
|
|
|
|
|
myCRC = crc32(myCRC, (const Bytef*) buffer2, szDecomp);
|
|
|
|
|
|
|
|
} while (zstr.avail_out == 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
while (zret != Z_STREAM_END);
|
|
|
|
|
|
|
|
inflateEnd(&zstr);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (myCRC != entry.crc)
|
|
|
|
return UnZip::Corrupted;
|
|
|
|
|
|
|
|
return UnZip::Ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
//! \internal Creates a new directory and all the needed parent directories.
|
|
|
|
bool UnzipPrivate::createDirectory(const QString& path)
|
|
|
|
{
|
|
|
|
QDir d(path);
|
|
|
|
if (!d.exists())
|
|
|
|
{
|
|
|
|
int sep = path.lastIndexOf("/");
|
|
|
|
if (sep <= 0) return true;
|
|
|
|
|
|
|
|
if (!createDirectory(path.left(sep)))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (!d.mkdir(path))
|
|
|
|
{
|
|
|
|
qDebug() << QString("Unable to create directory: %1").arg(path);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Reads an quint32 (4 bytes) from a byte array starting at given offset.
|
|
|
|
*/
|
|
|
|
quint32 UnzipPrivate::getULong(const unsigned char* data, quint32 offset) const
|
|
|
|
{
|
|
|
|
quint32 res = (quint32) data[offset];
|
|
|
|
res |= (((quint32)data[offset+1]) << 8);
|
|
|
|
res |= (((quint32)data[offset+2]) << 16);
|
|
|
|
res |= (((quint32)data[offset+3]) << 24);
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Reads an quint64 (8 bytes) from a byte array starting at given offset.
|
|
|
|
*/
|
|
|
|
quint64 UnzipPrivate::getULLong(const unsigned char* data, quint32 offset) const
|
|
|
|
{
|
|
|
|
quint64 res = (quint64) data[offset];
|
|
|
|
res |= (((quint64)data[offset+1]) << 8);
|
|
|
|
res |= (((quint64)data[offset+2]) << 16);
|
|
|
|
res |= (((quint64)data[offset+3]) << 24);
|
|
|
|
res |= (((quint64)data[offset+1]) << 32);
|
|
|
|
res |= (((quint64)data[offset+2]) << 40);
|
|
|
|
res |= (((quint64)data[offset+3]) << 48);
|
|
|
|
res |= (((quint64)data[offset+3]) << 56);
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Reads an quint16 (2 bytes) from a byte array starting at given offset.
|
|
|
|
*/
|
|
|
|
quint16 UnzipPrivate::getUShort(const unsigned char* data, quint32 offset) const
|
|
|
|
{
|
|
|
|
return (quint16) data[offset] | (((quint16)data[offset+1]) << 8);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Return the next byte in the pseudo-random sequence
|
|
|
|
*/
|
|
|
|
int UnzipPrivate::decryptByte(quint32 key2) const
|
|
|
|
{
|
|
|
|
quint16 temp = ((quint16)(key2) & 0xffff) | 2;
|
|
|
|
return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Update the encryption keys with the next byte of plain text
|
|
|
|
*/
|
|
|
|
void UnzipPrivate::updateKeys(quint32* keys, int c) const
|
|
|
|
{
|
|
|
|
keys[0] = CRC32(keys[0], c);
|
|
|
|
keys[1] += keys[0] & 0xff;
|
|
|
|
keys[1] = keys[1] * 134775813L + 1;
|
|
|
|
keys[2] = CRC32(keys[2], ((int)keys[1]) >> 24);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Initialize the encryption keys and the random header according to
|
|
|
|
the given password.
|
|
|
|
*/
|
|
|
|
void UnzipPrivate::initKeys(const QString& pwd, quint32* keys) const
|
|
|
|
{
|
|
|
|
keys[0] = 305419896L;
|
|
|
|
keys[1] = 591751049L;
|
|
|
|
keys[2] = 878082192L;
|
|
|
|
|
|
|
|
QByteArray pwdBytes = pwd.toAscii();
|
|
|
|
int sz = pwdBytes.size();
|
|
|
|
const char* ascii = pwdBytes.data();
|
|
|
|
|
|
|
|
for (int i=0; i<sz; ++i)
|
|
|
|
updateKeys(keys, (int)ascii[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Attempts to test a password without actually extracting a file.
|
|
|
|
The \p file parameter can be used in the user interface or for debugging purposes
|
|
|
|
as it is the name of the encrypted file for wich the password is being tested.
|
|
|
|
*/
|
|
|
|
UnZip::ErrorCode UnzipPrivate::testPassword(quint32* keys, const QString& file, const ZipEntryP& header)
|
|
|
|
{
|
|
|
|
Q_UNUSED(file);
|
|
|
|
|
|
|
|
// read encryption keys
|
|
|
|
if (device->read(buffer1, 12) != 12)
|
|
|
|
return UnZip::Corrupted;
|
|
|
|
|
|
|
|
// Replace this code if you want to i.e. call some dialog and ask the user for a password
|
|
|
|
initKeys(password, keys);
|
|
|
|
if (testKeys(header, keys))
|
|
|
|
return UnZip::Ok;
|
|
|
|
|
|
|
|
return UnZip::Skip;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Tests a set of keys on the encryption header.
|
|
|
|
*/
|
|
|
|
bool UnzipPrivate::testKeys(const ZipEntryP& header, quint32* keys)
|
|
|
|
{
|
|
|
|
char lastByte;
|
|
|
|
|
|
|
|
// decrypt encryption header
|
|
|
|
for (int i=0; i<11; ++i)
|
|
|
|
updateKeys(keys, lastByte = buffer1[i] ^ decryptByte(keys[2]));
|
|
|
|
updateKeys(keys, lastByte = buffer1[11] ^ decryptByte(keys[2]));
|
|
|
|
|
|
|
|
// if there is an extended header (bit in the gp flag) buffer[11] is a byte from the file time
|
|
|
|
// with no extended header we have to check the crc high-order byte
|
|
|
|
char c = ((header.gpFlag[0] & 0x08) == 8) ? header.modTime[1] : header.crc >> 24;
|
|
|
|
|
|
|
|
return (lastByte == c);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Decrypts an array of bytes long \p read.
|
|
|
|
*/
|
|
|
|
void UnzipPrivate::decryptBytes(quint32* keys, char* buffer, qint64 read)
|
|
|
|
{
|
|
|
|
for (int i=0; i<(int)read; ++i)
|
|
|
|
updateKeys(keys, buffer[i] ^= decryptByte(keys[2]));
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
\internal Converts date and time values from ZIP format to a QDateTime object.
|
|
|
|
*/
|
|
|
|
QDateTime UnzipPrivate::convertDateTime(const unsigned char date[2], const unsigned char time[2]) const
|
|
|
|
{
|
|
|
|
QDateTime dt;
|
|
|
|
|
|
|
|
// Usual PKZip low-byte to high-byte order
|
|
|
|
|
|
|
|
// Date: 7 bits = years from 1980, 4 bits = month, 5 bits = day
|
|
|
|
quint16 year = (date[1] >> 1) & 127;
|
|
|
|
quint16 month = ((date[1] << 3) & 14) | ((date[0] >> 5) & 7);
|
|
|
|
quint16 day = date[0] & 31;
|
|
|
|
|
|
|
|
// Time: 5 bits hour, 6 bits minutes, 5 bits seconds with a 2sec precision
|
|
|
|
quint16 hour = (time[1] >> 3) & 31;
|
|
|
|
quint16 minutes = ((time[1] << 3) & 56) | ((time[0] >> 5) & 7);
|
|
|
|
quint16 seconds = (time[0] & 31) * 2;
|
|
|
|
|
|
|
|
dt.setDate(QDate(1980 + year, month, day));
|
|
|
|
dt.setTime(QTime(hour, minutes, seconds));
|
|
|
|
return dt;
|
|
|
|
}
|