1 /****************************************************************************
\r
2 ** Filename: unzip.cpp
\r
3 ** Last updated [dd/mm/yyyy]: 07/09/2008
\r
5 ** pkzip 2.0 decompression.
\r
7 ** Some of the code has been inspired by other open source projects,
\r
8 ** (mainly Info-Zip and Gilles Vollant's minizip).
\r
9 ** Compression and decompression actually uses the zlib library.
\r
11 ** Copyright (C) 2007-2008 Angius Fabrizio. All rights reserved.
\r
13 ** This file is part of the OSDaB project (http://osdab.sourceforge.net/).
\r
15 ** This file may be distributed and/or modified under the terms of the
\r
16 ** GNU General Public License version 2 as published by the Free Software
\r
17 ** Foundation and appearing in the file LICENSE.GPL included in the
\r
18 ** packaging of this file.
\r
20 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
\r
21 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
\r
23 ** See the file LICENSE.GPL that came with this software distribution or
\r
24 ** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information.
\r
26 **********************************************************************/
\r
29 #include "unzip_p.h"
\r
30 #include "zipentry_p.h"
\r
33 #include <QStringList>
\r
36 #include <QCoreApplication>
\r
38 // You can remove this #include if you replace the qDebug() statements.
\r
42 \class UnZip unzip.h
\r
44 \brief PKZip 2.0 file decompression.
\r
45 Compatibility with later versions is not ensured as they may use
\r
46 unsupported compression algorithms.
\r
47 Versions after 2.7 may have an incompatible header format and thus be
\r
48 completely incompatible.
\r
51 /*! \enum UnZip::ErrorCode The result of a decompression operation.
\r
52 \value UnZip::Ok No error occurred.
\r
53 \value UnZip::ZlibInit Failed to init or load the zlib library.
\r
54 \value UnZip::ZlibError The zlib library returned some error.
\r
55 \value UnZip::OpenFailed Unable to create or open a device.
\r
56 \value UnZip::PartiallyCorrupted Corrupted zip archive - some files could be extracted.
\r
57 \value UnZip::Corrupted Corrupted or invalid zip archive.
\r
58 \value UnZip::WrongPassword Unable to decrypt a password protected file.
\r
59 \value UnZip::NoOpenArchive No archive has been opened yet.
\r
60 \value UnZip::FileNotFound Unable to find the requested file in the archive.
\r
61 \value UnZip::ReadFailed Reading of a file failed.
\r
62 \value UnZip::WriteFailed Writing of a file failed.
\r
63 \value UnZip::SeekFailed Seek failed.
\r
64 \value UnZip::CreateDirFailed Could not create a directory.
\r
65 \value UnZip::InvalidDevice A null device has been passed as parameter.
\r
66 \value UnZip::InvalidArchive This is not a valid (or supported) ZIP archive.
\r
67 \value UnZip::HeaderConsistencyError Local header record info does not match with the central directory record info. The archive may be corrupted.
\r
69 \value UnZip::Skip Internal use only.
\r
70 \value UnZip::SkipAll Internal use only.
\r
73 /*! \enum UnZip::ExtractionOptions Some options for the file extraction methods.
\r
74 \value UnZip::ExtractPaths Default. Does not ignore the path of the zipped files.
\r
75 \value UnZip::SkipPaths Default. Ignores the path of the zipped files and extracts them all to the same root directory.
\r
78 //! Local header size (excluding signature, excluding variable length fields)
\r
79 #define UNZIP_LOCAL_HEADER_SIZE 26
\r
80 //! Central Directory file entry size (excluding signature, excluding variable length fields)
\r
81 #define UNZIP_CD_ENTRY_SIZE_NS 42
\r
82 //! Data descriptor size (excluding signature)
\r
83 #define UNZIP_DD_SIZE 12
\r
84 //! End Of Central Directory size (including signature, excluding variable length fields)
\r
85 #define UNZIP_EOCD_SIZE 22
\r
86 //! Local header entry encryption header size
\r
87 #define UNZIP_LOCAL_ENC_HEADER_SIZE 12
\r
89 // Some offsets inside a CD record (excluding signature)
\r
90 #define UNZIP_CD_OFF_VERSION 0
\r
91 #define UNZIP_CD_OFF_GPFLAG 4
\r
92 #define UNZIP_CD_OFF_CMETHOD 6
\r
93 #define UNZIP_CD_OFF_MODT 8
\r
94 #define UNZIP_CD_OFF_MODD 10
\r
95 #define UNZIP_CD_OFF_CRC32 12
\r
96 #define UNZIP_CD_OFF_CSIZE 16
\r
97 #define UNZIP_CD_OFF_USIZE 20
\r
98 #define UNZIP_CD_OFF_NAMELEN 24
\r
99 #define UNZIP_CD_OFF_XLEN 26
\r
100 #define UNZIP_CD_OFF_COMMLEN 28
\r
101 #define UNZIP_CD_OFF_LHOFFSET 38
\r
103 // Some offsets inside a local header record (excluding signature)
\r
104 #define UNZIP_LH_OFF_VERSION 0
\r
105 #define UNZIP_LH_OFF_GPFLAG 2
\r
106 #define UNZIP_LH_OFF_CMETHOD 4
\r
107 #define UNZIP_LH_OFF_MODT 6
\r
108 #define UNZIP_LH_OFF_MODD 8
\r
109 #define UNZIP_LH_OFF_CRC32 10
\r
110 #define UNZIP_LH_OFF_CSIZE 14
\r
111 #define UNZIP_LH_OFF_USIZE 18
\r
112 #define UNZIP_LH_OFF_NAMELEN 22
\r
113 #define UNZIP_LH_OFF_XLEN 24
\r
115 // Some offsets inside a data descriptor record (excluding signature)
\r
116 #define UNZIP_DD_OFF_CRC32 0
\r
117 #define UNZIP_DD_OFF_CSIZE 4
\r
118 #define UNZIP_DD_OFF_USIZE 8
\r
120 // Some offsets inside a EOCD record
\r
121 #define UNZIP_EOCD_OFF_ENTRIES 6
\r
122 #define UNZIP_EOCD_OFF_CDOFF 12
\r
123 #define UNZIP_EOCD_OFF_COMMLEN 16
\r
126 Max version handled by this API.
\r
127 0x1B = 2.7 --> full compatibility only up to version 2.0 (0x14)
\r
128 versions from 2.1 to 2.7 may use unsupported compression methods
\r
129 versions after 2.7 may have an incompatible header format
\r
131 // TODO: FIX THIS! Need support for the latest versions
\r
132 #define UNZIP_VERSION 0x1B
\r
133 //! Full compatibility granted until this version
\r
134 #define UNZIP_VERSION_STRICT 0x14
\r
137 #define CRC32(c, b) crcTable[((int)c^b) & 0xff] ^ (c >> 8)
\r
139 //! Checks if some file has been already extracted.
\r
140 #define UNZIP_CHECK_FOR_VALID_DATA \
\r
144 qDebug() << "Corrupted zip archive. Some files might be extracted.";\
\r
145 ec = headers->size() != 0 ? UnZip::PartiallyCorrupted : UnZip::Corrupted;\
\r
152 qDebug() << "Corrupted or invalid zip archive";\
\r
153 ec = UnZip::Corrupted;\
\r
159 /************************************************************************
\r
161 *************************************************************************/
\r
164 Creates a new Zip file decompressor.
\r
168 d = new UnzipPrivate;
\r
172 Closes any open archive and releases used resources.
\r
181 Returns true if there is an open archive.
\r
183 bool UnZip::isOpen() const
\r
185 return d->device != 0;
\r
189 Opens a zip archive and reads the files list. Closes any previously opened archive.
\r
191 UnZip::ErrorCode UnZip::openArchive(const QString& filename)
\r
193 QFile* file = new QFile(filename);
\r
195 if (!file->exists()) {
\r
197 return UnZip::FileNotFound;
\r
200 if (!file->open(QIODevice::ReadOnly)) {
\r
202 return UnZip::OpenFailed;
\r
205 return openArchive(file);
\r
209 Opens a zip archive and reads the entries list.
\r
210 Closes any previously opened archive.
\r
211 \warning The class takes ownership of the device so don't delete it!
\r
213 UnZip::ErrorCode UnZip::openArchive(QIODevice* device)
\r
217 qDebug() << "Invalid device.";
\r
218 return UnZip::InvalidDevice;
\r
221 return d->openArchive(device);
\r
225 Closes the archive and releases all the used resources (like cached passwords).
\r
227 void UnZip::closeArchive()
\r
232 QString UnZip::archiveComment() const
\r
234 if (d->device == 0)
\r
240 Returns a locale translated error string for a given error code.
\r
242 QString UnZip::formatError(UnZip::ErrorCode c) const
\r
246 case Ok: return QCoreApplication::translate("UnZip", "ZIP operation completed successfully."); break;
\r
247 case ZlibInit: return QCoreApplication::translate("UnZip", "Failed to initialize or load zlib library."); break;
\r
248 case ZlibError: return QCoreApplication::translate("UnZip", "zlib library error."); break;
\r
249 case OpenFailed: return QCoreApplication::translate("UnZip", "Unable to create or open file."); break;
\r
250 case PartiallyCorrupted: return QCoreApplication::translate("UnZip", "Partially corrupted archive. Some files might be extracted."); break;
\r
251 case Corrupted: return QCoreApplication::translate("UnZip", "Corrupted archive."); break;
\r
252 case WrongPassword: return QCoreApplication::translate("UnZip", "Wrong password."); break;
\r
253 case NoOpenArchive: return QCoreApplication::translate("UnZip", "No archive has been created yet."); break;
\r
254 case FileNotFound: return QCoreApplication::translate("UnZip", "File or directory does not exist."); break;
\r
255 case ReadFailed: return QCoreApplication::translate("UnZip", "File read error."); break;
\r
256 case WriteFailed: return QCoreApplication::translate("UnZip", "File write error."); break;
\r
257 case SeekFailed: return QCoreApplication::translate("UnZip", "File seek error."); break;
\r
258 case CreateDirFailed: return QCoreApplication::translate("UnZip", "Unable to create a directory."); break;
\r
259 case InvalidDevice: return QCoreApplication::translate("UnZip", "Invalid device."); break;
\r
260 case InvalidArchive: return QCoreApplication::translate("UnZip", "Invalid or incompatible zip archive."); break;
\r
261 case HeaderConsistencyError: return QCoreApplication::translate("UnZip", "Inconsistent headers. Archive might be corrupted."); break;
\r
265 return QCoreApplication::translate("UnZip", "Unknown error.");
\r
269 Returns true if the archive contains a file with the given path and name.
\r
271 bool UnZip::contains(const QString& file) const
\r
273 if (d->headers == 0)
\r
276 return d->headers->contains(file);
\r
280 Returns complete paths of files and directories in this archive.
\r
282 QStringList UnZip::fileList() const
\r
284 return d->headers == 0 ? QStringList() : d->headers->keys();
\r
288 Returns information for each (correctly parsed) entry of this archive.
\r
290 QList<UnZip::ZipEntry> UnZip::entryList() const
\r
292 QList<UnZip::ZipEntry> list;
\r
294 if (d->headers != 0)
\r
296 for (QMap<QString,ZipEntryP*>::ConstIterator it = d->headers->constBegin(); it != d->headers->constEnd(); ++it)
\r
298 const ZipEntryP* entry = it.value();
\r
299 Q_ASSERT(entry != 0);
\r
303 z.filename = it.key();
\r
304 if (!entry->comment.isEmpty())
\r
305 z.comment = entry->comment;
\r
306 z.compressedSize = entry->szComp;
\r
307 z.uncompressedSize = entry->szUncomp;
\r
308 z.crc32 = entry->crc;
\r
309 z.lastModified = d->convertDateTime(entry->modDate, entry->modTime);
\r
311 z.compression = entry->compMethod == 0 ? NoCompression : entry->compMethod == 8 ? Deflated : UnknownCompression;
\r
312 z.type = z.filename.endsWith("/") ? Directory : File;
\r
314 z.encrypted = entry->isEncrypted();
\r
324 Extracts the whole archive to a directory.
\r
326 UnZip::ErrorCode UnZip::extractAll(const QString& dirname, ExtractionOptions options)
\r
328 return extractAll(QDir(dirname), options);
\r
332 Extracts the whole archive to a directory.
\r
334 UnZip::ErrorCode UnZip::extractAll(const QDir& dir, ExtractionOptions options)
\r
336 // this should only happen if we didn't call openArchive() yet
\r
337 if (d->device == 0)
\r
338 return NoOpenArchive;
\r
340 if (d->headers == 0)
\r
344 for (QMap<QString,ZipEntryP*>::Iterator itr = d->headers->begin(); itr != d->headers->end(); ++itr)
\r
346 ZipEntryP* entry = itr.value();
\r
347 Q_ASSERT(entry != 0);
\r
349 if ((entry->isEncrypted()) && d->skipAllEncrypted)
\r
352 switch (d->extractFile(itr.key(), *entry, dir, options))
\r
355 qDebug() << "Removing corrupted entry" << itr.key();
\r
356 d->headers->erase(itr++);
\r
357 if (itr == d->headers->end())
\r
360 case CreateDirFailed:
\r
365 d->skipAllEncrypted = true;
\r
379 Extracts a single file to a directory.
\r
381 UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QString& dirname, ExtractionOptions options)
\r
383 return extractFile(filename, QDir(dirname), options);
\r
387 Extracts a single file to a directory.
\r
389 UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QDir& dir, ExtractionOptions options)
\r
391 QMap<QString,ZipEntryP*>::Iterator itr = d->headers->find(filename);
\r
392 if (itr != d->headers->end())
\r
394 ZipEntryP* entry = itr.value();
\r
395 Q_ASSERT(entry != 0);
\r
396 return d->extractFile(itr.key(), *entry, dir, options);
\r
399 return FileNotFound;
\r
403 Extracts a single file to a directory.
\r
405 UnZip::ErrorCode UnZip::extractFile(const QString& filename, QIODevice* dev, ExtractionOptions options)
\r
408 return InvalidDevice;
\r
410 QMap<QString,ZipEntryP*>::Iterator itr = d->headers->find(filename);
\r
411 if (itr != d->headers->end()) {
\r
412 ZipEntryP* entry = itr.value();
\r
413 Q_ASSERT(entry != 0);
\r
414 return d->extractFile(itr.key(), *entry, dev, options);
\r
417 return FileNotFound;
\r
421 Extracts a list of files.
\r
422 Stops extraction at the first error (but continues if a file does not exist in the archive).
\r
424 UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QString& dirname, ExtractionOptions options)
\r
429 for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr)
\r
431 ec = extractFile(*itr, dir, options);
\r
432 if (ec == FileNotFound)
\r
442 Extracts a list of files.
\r
443 Stops extraction at the first error (but continues if a file does not exist in the archive).
\r
445 UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QDir& dir, ExtractionOptions options)
\r
449 for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr)
\r
451 ec = extractFile(*itr, dir, options);
\r
452 if (ec == FileNotFound)
\r
462 Remove/replace this method to add your own password retrieval routine.
\r
464 void UnZip::setPassword(const QString& pwd)
\r
470 ZipEntry constructor - initialize data. Type is set to File.
\r
472 UnZip::ZipEntry::ZipEntry()
\r
474 compressedSize = uncompressedSize = crc32 = 0;
\r
475 compression = NoCompression;
\r
481 /************************************************************************
\r
483 *************************************************************************/
\r
486 UnzipPrivate::UnzipPrivate()
\r
488 skipAllEncrypted = false;
\r
492 uBuffer = (unsigned char*) buffer1;
\r
493 crcTable = (quint32*) get_crc_table();
\r
495 cdOffset = eocdOffset = 0;
\r
497 unsupportedEntryCount = 0;
\r
500 //! \internal Parses a Zip archive.
\r
501 UnZip::ErrorCode UnzipPrivate::openArchive(QIODevice* dev)
\r
503 Q_ASSERT(dev != 0);
\r
510 if (!(device->isOpen() || device->open(QIODevice::ReadOnly)))
\r
515 qDebug() << "Unable to open device for reading";
\r
516 return UnZip::OpenFailed;
\r
519 UnZip::ErrorCode ec;
\r
521 ec = seekToCentralDirectory();
\r
522 if (ec != UnZip::Ok)
\r
528 //! \todo Ignore CD entry count? CD may be corrupted.
\r
529 if (cdEntryCount == 0)
\r
534 bool continueParsing = true;
\r
536 while (continueParsing)
\r
538 if (device->read(buffer1, 4) != 4)
\r
539 UNZIP_CHECK_FOR_VALID_DATA
\r
541 if (! (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x01 && buffer1[3] == 0x02) )
\r
544 if ( (ec = parseCentralDirectoryRecord()) != UnZip::Ok )
\r
548 if (ec != UnZip::Ok)
\r
555 \internal Parses a local header record and makes some consistency check
\r
556 with the information stored in the Central Directory record for this entry
\r
557 that has been previously parsed.
\r
558 \todo Optional consistency check (as a ExtractionOptions flag)
\r
560 local file header signature 4 bytes (0x04034b50)
\r
561 version needed to extract 2 bytes
\r
562 general purpose bit flag 2 bytes
\r
563 compression method 2 bytes
\r
564 last mod file time 2 bytes
\r
565 last mod file date 2 bytes
\r
567 compressed size 4 bytes
\r
568 uncompressed size 4 bytes
\r
569 file name length 2 bytes
\r
570 extra field length 2 bytes
\r
572 file name (variable size)
\r
573 extra field (variable size)
\r
575 UnZip::ErrorCode UnzipPrivate::parseLocalHeaderRecord(const QString& path, ZipEntryP& entry)
\r
577 if (!device->seek(entry.lhOffset))
\r
578 return UnZip::SeekFailed;
\r
581 if (device->read(buffer1, 4) != 4)
\r
582 return UnZip::ReadFailed;
\r
584 if ((buffer1[0] != 'P') || (buffer1[1] != 'K') || (buffer1[2] != 0x03) || (buffer1[3] != 0x04))
\r
585 return UnZip::InvalidArchive;
\r
587 if (device->read(buffer1, UNZIP_LOCAL_HEADER_SIZE) != UNZIP_LOCAL_HEADER_SIZE)
\r
588 return UnZip::ReadFailed;
\r
591 Check 3rd general purpose bit flag.
\r
593 "bit 3: If this bit is set, the fields crc-32, compressed size
\r
594 and uncompressed size are set to zero in the local
\r
595 header. The correct values are put in the data descriptor
\r
596 immediately following the compressed data."
\r
598 bool hasDataDescriptor = entry.hasDataDescriptor();
\r
600 bool checkFailed = false;
\r
603 checkFailed = entry.compMethod != getUShort(uBuffer, UNZIP_LH_OFF_CMETHOD);
\r
605 checkFailed = entry.gpFlag[0] != uBuffer[UNZIP_LH_OFF_GPFLAG];
\r
607 checkFailed = entry.gpFlag[1] != uBuffer[UNZIP_LH_OFF_GPFLAG + 1];
\r
609 checkFailed = entry.modTime[0] != uBuffer[UNZIP_LH_OFF_MODT];
\r
611 checkFailed = entry.modTime[1] != uBuffer[UNZIP_LH_OFF_MODT + 1];
\r
613 checkFailed = entry.modDate[0] != uBuffer[UNZIP_LH_OFF_MODD];
\r
615 checkFailed = entry.modDate[1] != uBuffer[UNZIP_LH_OFF_MODD + 1];
\r
616 if (!hasDataDescriptor)
\r
619 checkFailed = entry.crc != getULong(uBuffer, UNZIP_LH_OFF_CRC32);
\r
621 checkFailed = entry.szComp != getULong(uBuffer, UNZIP_LH_OFF_CSIZE);
\r
623 checkFailed = entry.szUncomp != getULong(uBuffer, UNZIP_LH_OFF_USIZE);
\r
627 return UnZip::HeaderConsistencyError;
\r
630 quint16 szName = getUShort(uBuffer, UNZIP_LH_OFF_NAMELEN);
\r
632 return UnZip::HeaderConsistencyError;
\r
634 if (device->read(buffer2, szName) != szName)
\r
635 return UnZip::ReadFailed;
\r
637 QString filename = QString::fromAscii(buffer2, szName);
\r
638 if (filename != path)
\r
640 qDebug() << "Filename in local header mismatches.";
\r
641 return UnZip::HeaderConsistencyError;
\r
644 // Skip extra field
\r
645 quint16 szExtra = getUShort(uBuffer, UNZIP_LH_OFF_XLEN);
\r
648 if (!device->seek(device->pos() + szExtra))
\r
649 return UnZip::SeekFailed;
\r
652 entry.dataOffset = device->pos();
\r
654 if (hasDataDescriptor)
\r
657 The data descriptor has this OPTIONAL signature: PK\7\8
\r
658 We try to skip the compressed data relying on the size set in the
\r
659 Central Directory record.
\r
661 if (!device->seek(device->pos() + entry.szComp))
\r
662 return UnZip::SeekFailed;
\r
664 // Read 4 bytes and check if there is a data descriptor signature
\r
665 if (device->read(buffer2, 4) != 4)
\r
666 return UnZip::ReadFailed;
\r
668 bool hasSignature = buffer2[0] == 'P' && buffer2[1] == 'K' && buffer2[2] == 0x07 && buffer2[3] == 0x08;
\r
671 if (device->read(buffer2, UNZIP_DD_SIZE) != UNZIP_DD_SIZE)
\r
672 return UnZip::ReadFailed;
\r
676 if (device->read(buffer2 + 4, UNZIP_DD_SIZE - 4) != UNZIP_DD_SIZE - 4)
\r
677 return UnZip::ReadFailed;
\r
680 // DD: crc, compressed size, uncompressed size
\r
682 entry.crc != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CRC32) ||
\r
683 entry.szComp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CSIZE) ||
\r
684 entry.szUncomp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_USIZE)
\r
686 return UnZip::HeaderConsistencyError;
\r
692 /*! \internal Attempts to find the start of the central directory record.
\r
694 We seek the file back until we reach the "End Of Central Directory"
\r
697 end of central dir signature 4 bytes (0x06054b50)
\r
698 number of this disk 2 bytes
\r
699 number of the disk with the
\r
700 start of the central directory 2 bytes
\r
701 total number of entries in the
\r
702 central directory on this disk 2 bytes
\r
703 total number of entries in
\r
704 the central directory 2 bytes
\r
705 size of the central directory 4 bytes
\r
706 offset of start of central
\r
707 directory with respect to
\r
708 the starting disk number 4 bytes
\r
709 .ZIP file comment length 2 bytes
\r
710 --- SIZE UNTIL HERE: UNZIP_EOCD_SIZE ---
\r
711 .ZIP file comment (variable size)
\r
713 UnZip::ErrorCode UnzipPrivate::seekToCentralDirectory()
\r
715 qint64 length = device->size();
\r
716 qint64 offset = length - UNZIP_EOCD_SIZE;
\r
718 if (length < UNZIP_EOCD_SIZE)
\r
719 return UnZip::InvalidArchive;
\r
721 if (!device->seek( offset ))
\r
722 return UnZip::SeekFailed;
\r
724 if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE)
\r
725 return UnZip::ReadFailed;
\r
727 bool eocdFound = (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x05 && buffer1[3] == 0x06);
\r
731 // Zip file has no comment (the only variable length field in the EOCD record)
\r
732 eocdOffset = offset;
\r
739 offset -= UNZIP_EOCD_SIZE;
\r
742 return UnZip::InvalidArchive;
\r
744 if (!device->seek( offset ))
\r
745 return UnZip::SeekFailed;
\r
747 while ((read = device->read(buffer1, UNZIP_EOCD_SIZE)) >= 0)
\r
749 if ( (p = strstr(buffer1, "PK\5\6")) != 0)
\r
751 // Seek to the start of the EOCD record so we can read it fully
\r
752 // Yes... we could simply read the missing bytes and append them to the buffer
\r
753 // but this is far easier so heck it!
\r
754 device->seek( offset + (p - buffer1) );
\r
756 eocdOffset = offset + (p - buffer1);
\r
758 // Read EOCD record
\r
759 if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE)
\r
760 return UnZip::ReadFailed;
\r
765 // TODO: This is very slow and only a temporary bug fix. Need some pattern matching algorithm here.
\r
766 offset -= 1 /*UNZIP_EOCD_SIZE*/;
\r
768 return UnZip::InvalidArchive;
\r
770 if (!device->seek( offset ))
\r
771 return UnZip::SeekFailed;
\r
776 return UnZip::InvalidArchive;
\r
778 // Parse EOCD to locate CD offset
\r
779 offset = getULong((const unsigned char*)buffer1, UNZIP_EOCD_OFF_CDOFF + 4);
\r
783 cdEntryCount = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_ENTRIES + 4);
\r
785 quint16 commentLength = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_COMMLEN + 4);
\r
786 if (commentLength != 0)
\r
788 QByteArray c = device->read(commentLength);
\r
789 if (c.count() != commentLength)
\r
790 return UnZip::ReadFailed;
\r
795 // Seek to the start of the CD record
\r
796 if (!device->seek( cdOffset ))
\r
797 return UnZip::SeekFailed;
\r
803 \internal Parses a central directory record.
\r
805 Central Directory record structure:
\r
812 [digital signature] // PKZip 6.2 or later only
\r
816 central file header signature 4 bytes (0x02014b50)
\r
817 version made by 2 bytes
\r
818 version needed to extract 2 bytes
\r
819 general purpose bit flag 2 bytes
\r
820 compression method 2 bytes
\r
821 last mod file time 2 bytes
\r
822 last mod file date 2 bytes
\r
824 compressed size 4 bytes
\r
825 uncompressed size 4 bytes
\r
826 file name length 2 bytes
\r
827 extra field length 2 bytes
\r
828 file comment length 2 bytes
\r
829 disk number start 2 bytes
\r
830 internal file attributes 2 bytes
\r
831 external file attributes 4 bytes
\r
832 relative offset of local header 4 bytes
\r
834 file name (variable size)
\r
835 extra field (variable size)
\r
836 file comment (variable size)
\r
838 UnZip::ErrorCode UnzipPrivate::parseCentralDirectoryRecord()
\r
841 if (device->read(buffer1, UNZIP_CD_ENTRY_SIZE_NS) != UNZIP_CD_ENTRY_SIZE_NS)
\r
842 return UnZip::ReadFailed;
\r
844 bool skipEntry = false;
\r
846 // Get compression type so we can skip non compatible algorithms
\r
847 quint16 compMethod = getUShort(uBuffer, UNZIP_CD_OFF_CMETHOD);
\r
849 // Get variable size fields length so we can skip the whole record
\r
851 quint16 szName = getUShort(uBuffer, UNZIP_CD_OFF_NAMELEN);
\r
852 quint16 szExtra = getUShort(uBuffer, UNZIP_CD_OFF_XLEN);
\r
853 quint16 szComment = getUShort(uBuffer, UNZIP_CD_OFF_COMMLEN);
\r
855 quint32 skipLength = szName + szExtra + szComment;
\r
857 UnZip::ErrorCode ec = UnZip::Ok;
\r
859 if ((compMethod != 0) && (compMethod != 8))
\r
861 qDebug() << "Unsupported compression method. Skipping file.";
\r
865 // Header parsing may be a problem if version is bigger than UNZIP_VERSION
\r
866 if (!skipEntry && buffer1[UNZIP_CD_OFF_VERSION] > UNZIP_VERSION)
\r
868 qDebug() << "Unsupported PKZip version. Skipping file.";
\r
872 if (!skipEntry && szName == 0)
\r
874 qDebug() << "Skipping file with no name.";
\r
878 if (!skipEntry && device->read(buffer2, szName) != szName)
\r
880 ec = UnZip::ReadFailed;
\r
886 if (ec == UnZip::Ok)
\r
888 if (!device->seek( device->pos() + skipLength ))
\r
889 ec = UnZip::SeekFailed;
\r
891 unsupportedEntryCount++;
\r
897 QString filename = QString::fromAscii(buffer2, szName);
\r
899 ZipEntryP* h = new ZipEntryP;
\r
900 h->compMethod = compMethod;
\r
902 h->gpFlag[0] = buffer1[UNZIP_CD_OFF_GPFLAG];
\r
903 h->gpFlag[1] = buffer1[UNZIP_CD_OFF_GPFLAG + 1];
\r
905 h->modTime[0] = buffer1[UNZIP_CD_OFF_MODT];
\r
906 h->modTime[1] = buffer1[UNZIP_CD_OFF_MODT + 1];
\r
908 h->modDate[0] = buffer1[UNZIP_CD_OFF_MODD];
\r
909 h->modDate[1] = buffer1[UNZIP_CD_OFF_MODD + 1];
\r
911 h->crc = getULong(uBuffer, UNZIP_CD_OFF_CRC32);
\r
912 h->szComp = getULong(uBuffer, UNZIP_CD_OFF_CSIZE);
\r
913 h->szUncomp = getULong(uBuffer, UNZIP_CD_OFF_USIZE);
\r
915 // Skip extra field (if any)
\r
918 if (!device->seek( device->pos() + szExtra ))
\r
921 return UnZip::SeekFailed;
\r
925 // Read comment field (if any)
\r
926 if (szComment != 0)
\r
928 if (device->read(buffer2, szComment) != szComment)
\r
931 return UnZip::ReadFailed;
\r
934 h->comment = QString::fromAscii(buffer2, szComment);
\r
937 h->lhOffset = getULong(uBuffer, UNZIP_CD_OFF_LHOFFSET);
\r
940 headers = new QMap<QString, ZipEntryP*>();
\r
941 headers->insert(filename, h);
\r
946 //! \internal Closes the archive and resets the internal status.
\r
947 void UnzipPrivate::closeArchive()
\r
952 skipAllEncrypted = false;
\r
956 qDeleteAll(*headers);
\r
961 delete device; device = 0;
\r
963 cdOffset = eocdOffset = 0;
\r
965 unsupportedEntryCount = 0;
\r
971 UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, ZipEntryP& entry, const QDir& dir, UnZip::ExtractionOptions options)
\r
973 QString name(path);
\r
977 int pos = name.lastIndexOf('/');
\r
979 // This entry is for a directory
\r
980 if (pos == name.length() - 1)
\r
982 if (options.testFlag(UnZip::SkipPaths))
\r
985 directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(name));
\r
986 if (!createDirectory(directory))
\r
988 qDebug() << QString("Unable to create directory: %1").arg(directory);
\r
989 return UnZip::CreateDirFailed;
\r
995 // Extract path from entry
\r
998 // get directory part
\r
999 dirname = name.left(pos);
\r
1000 if (options.testFlag(UnZip::SkipPaths))
\r
1002 directory = dir.absolutePath();
\r
1006 directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(dirname));
\r
1007 if (!createDirectory(directory))
\r
1009 qDebug() << QString("Unable to create directory: %1").arg(directory);
\r
1010 return UnZip::CreateDirFailed;
\r
1013 name = name.right(name.length() - pos - 1);
\r
1014 } else directory = dir.absolutePath();
\r
1016 name = QString("%1/%2").arg(directory).arg(name);
\r
1018 QFile outFile(name);
\r
1020 if (!outFile.open(QIODevice::WriteOnly))
\r
1022 qDebug() << QString("Unable to open %1 for writing").arg(name);
\r
1023 return UnZip::OpenFailed;
\r
1026 //! \todo Set creation/last_modified date/time
\r
1028 UnZip::ErrorCode ec = extractFile(path, entry, &outFile, options);
\r
1032 if (ec != UnZip::Ok)
\r
1034 if (!outFile.remove())
\r
1035 qDebug() << QString("Unable to remove corrupted file: %1").arg(name);
\r
1042 UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, ZipEntryP& entry, QIODevice* dev, UnZip::ExtractionOptions options)
\r
1044 Q_UNUSED(options);
\r
1045 Q_ASSERT(dev != 0);
\r
1047 if (!entry.lhEntryChecked)
\r
1049 UnZip::ErrorCode ec = parseLocalHeaderRecord(path, entry);
\r
1050 entry.lhEntryChecked = true;
\r
1052 if (ec != UnZip::Ok)
\r
1056 if (!device->seek(entry.dataOffset))
\r
1057 return UnZip::SeekFailed;
\r
1059 // Encryption keys
\r
1062 if (entry.isEncrypted())
\r
1064 UnZip::ErrorCode e = testPassword(keys, path, entry);
\r
1065 if (e != UnZip::Ok)
\r
1067 qDebug() << QString("Unable to decrypt %1").arg(path);
\r
1069 }//! Encryption header size
\r
1070 entry.szComp -= UNZIP_LOCAL_ENC_HEADER_SIZE; // remove encryption header size
\r
1073 if (entry.szComp == 0)
\r
1075 if (entry.crc != 0)
\r
1076 return UnZip::Corrupted;
\r
1081 uInt rep = entry.szComp / UNZIP_READ_BUFFER;
\r
1082 uInt rem = entry.szComp % UNZIP_READ_BUFFER;
\r
1089 quint32 myCRC = crc32(0L, Z_NULL, 0);
\r
1091 if (entry.compMethod == 0)
\r
1093 while ( (read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem)) > 0 )
\r
1095 if (entry.isEncrypted())
\r
1096 decryptBytes(keys, buffer1, read);
\r
1098 myCRC = crc32(myCRC, uBuffer, read);
\r
1100 if (dev->write(buffer1, read) != read)
\r
1101 return UnZip::WriteFailed;
\r
1106 if (tot == entry.szComp)
\r
1111 return UnZip::ReadFailed;
\r
1113 else if (entry.compMethod == 8)
\r
1115 /* Allocate inflate state */
\r
1117 zstr.zalloc = Z_NULL;
\r
1118 zstr.zfree = Z_NULL;
\r
1119 zstr.opaque = Z_NULL;
\r
1120 zstr.next_in = Z_NULL;
\r
1121 zstr.avail_in = 0;
\r
1125 // Use inflateInit2 with negative windowBits to get raw decompression
\r
1126 if ( (zret = inflateInit2_(&zstr, -MAX_WBITS, ZLIB_VERSION, sizeof(z_stream))) != Z_OK )
\r
1127 return UnZip::ZlibError;
\r
1131 // Decompress until deflate stream ends or end of file
\r
1134 read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem);
\r
1139 (void)inflateEnd(&zstr);
\r
1140 return UnZip::ReadFailed;
\r
1143 if (entry.isEncrypted())
\r
1144 decryptBytes(keys, buffer1, read);
\r
1149 zstr.avail_in = (uInt) read;
\r
1150 zstr.next_in = (Bytef*) buffer1;
\r
1153 // Run inflate() on input until output buffer not full
\r
1155 zstr.avail_out = UNZIP_READ_BUFFER;
\r
1156 zstr.next_out = (Bytef*) buffer2;;
\r
1158 zret = inflate(&zstr, Z_NO_FLUSH);
\r
1162 case Z_DATA_ERROR:
\r
1164 inflateEnd(&zstr);
\r
1165 return UnZip::WriteFailed;
\r
1170 szDecomp = UNZIP_READ_BUFFER - zstr.avail_out;
\r
1171 if (dev->write(buffer2, szDecomp) != szDecomp)
\r
1173 inflateEnd(&zstr);
\r
1174 return UnZip::ZlibError;
\r
1177 myCRC = crc32(myCRC, (const Bytef*) buffer2, szDecomp);
\r
1179 } while (zstr.avail_out == 0);
\r
1182 while (zret != Z_STREAM_END);
\r
1184 inflateEnd(&zstr);
\r
1187 if (myCRC != entry.crc)
\r
1188 return UnZip::Corrupted;
\r
1193 //! \internal Creates a new directory and all the needed parent directories.
\r
1194 bool UnzipPrivate::createDirectory(const QString& path)
\r
1199 int sep = path.lastIndexOf("/");
\r
1200 if (sep <= 0) return true;
\r
1202 if (!createDirectory(path.left(sep)))
\r
1205 if (!d.mkdir(path))
\r
1207 qDebug() << QString("Unable to create directory: %1").arg(path);
\r
1216 \internal Reads an quint32 (4 bytes) from a byte array starting at given offset.
\r
1218 quint32 UnzipPrivate::getULong(const unsigned char* data, quint32 offset) const
\r
1220 quint32 res = (quint32) data[offset];
\r
1221 res |= (((quint32)data[offset+1]) << 8);
\r
1222 res |= (((quint32)data[offset+2]) << 16);
\r
1223 res |= (((quint32)data[offset+3]) << 24);
\r
1229 \internal Reads an quint64 (8 bytes) from a byte array starting at given offset.
\r
1231 quint64 UnzipPrivate::getULLong(const unsigned char* data, quint32 offset) const
\r
1233 quint64 res = (quint64) data[offset];
\r
1234 res |= (((quint64)data[offset+1]) << 8);
\r
1235 res |= (((quint64)data[offset+2]) << 16);
\r
1236 res |= (((quint64)data[offset+3]) << 24);
\r
1237 res |= (((quint64)data[offset+1]) << 32);
\r
1238 res |= (((quint64)data[offset+2]) << 40);
\r
1239 res |= (((quint64)data[offset+3]) << 48);
\r
1240 res |= (((quint64)data[offset+3]) << 56);
\r
1246 \internal Reads an quint16 (2 bytes) from a byte array starting at given offset.
\r
1248 quint16 UnzipPrivate::getUShort(const unsigned char* data, quint32 offset) const
\r
1250 return (quint16) data[offset] | (((quint16)data[offset+1]) << 8);
\r
1254 \internal Return the next byte in the pseudo-random sequence
\r
1256 int UnzipPrivate::decryptByte(quint32 key2) const
\r
1258 quint16 temp = ((quint16)(key2) & 0xffff) | 2;
\r
1259 return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);
\r
1263 \internal Update the encryption keys with the next byte of plain text
\r
1265 void UnzipPrivate::updateKeys(quint32* keys, int c) const
\r
1267 keys[0] = CRC32(keys[0], c);
\r
1268 keys[1] += keys[0] & 0xff;
\r
1269 keys[1] = keys[1] * 134775813L + 1;
\r
1270 keys[2] = CRC32(keys[2], ((int)keys[1]) >> 24);
\r
1274 \internal Initialize the encryption keys and the random header according to
\r
1275 the given password.
\r
1277 void UnzipPrivate::initKeys(const QString& pwd, quint32* keys) const
\r
1279 keys[0] = 305419896L;
\r
1280 keys[1] = 591751049L;
\r
1281 keys[2] = 878082192L;
\r
1283 QByteArray pwdBytes = pwd.toAscii();
\r
1284 int sz = pwdBytes.size();
\r
1285 const char* ascii = pwdBytes.data();
\r
1287 for (int i=0; i<sz; ++i)
\r
1288 updateKeys(keys, (int)ascii[i]);
\r
1292 \internal Attempts to test a password without actually extracting a file.
\r
1293 The \p file parameter can be used in the user interface or for debugging purposes
\r
1294 as it is the name of the encrypted file for wich the password is being tested.
\r
1296 UnZip::ErrorCode UnzipPrivate::testPassword(quint32* keys, const QString& file, const ZipEntryP& header)
\r
1300 // read encryption keys
\r
1301 if (device->read(buffer1, 12) != 12)
\r
1302 return UnZip::Corrupted;
\r
1304 // Replace this code if you want to i.e. call some dialog and ask the user for a password
\r
1305 initKeys(password, keys);
\r
1306 if (testKeys(header, keys))
\r
1309 return UnZip::Skip;
\r
1313 \internal Tests a set of keys on the encryption header.
\r
1315 bool UnzipPrivate::testKeys(const ZipEntryP& header, quint32* keys)
\r
1319 // decrypt encryption header
\r
1320 for (int i=0; i<11; ++i)
\r
1321 updateKeys(keys, lastByte = buffer1[i] ^ decryptByte(keys[2]));
\r
1322 updateKeys(keys, lastByte = buffer1[11] ^ decryptByte(keys[2]));
\r
1324 // if there is an extended header (bit in the gp flag) buffer[11] is a byte from the file time
\r
1325 // with no extended header we have to check the crc high-order byte
\r
1326 char c = ((header.gpFlag[0] & 0x08) == 8) ? header.modTime[1] : header.crc >> 24;
\r
1328 return (lastByte == c);
\r
1332 \internal Decrypts an array of bytes long \p read.
\r
1334 void UnzipPrivate::decryptBytes(quint32* keys, char* buffer, qint64 read)
\r
1336 for (int i=0; i<(int)read; ++i)
\r
1337 updateKeys(keys, buffer[i] ^= decryptByte(keys[2]));
\r
1341 \internal Converts date and time values from ZIP format to a QDateTime object.
\r
1343 QDateTime UnzipPrivate::convertDateTime(const unsigned char date[2], const unsigned char time[2]) const
\r
1347 // Usual PKZip low-byte to high-byte order
\r
1349 // Date: 7 bits = years from 1980, 4 bits = month, 5 bits = day
\r
1350 quint16 year = (date[1] >> 1) & 127;
\r
1351 quint16 month = ((date[1] << 3) & 14) | ((date[0] >> 5) & 7);
\r
1352 quint16 day = date[0] & 31;
\r
1354 // Time: 5 bits hour, 6 bits minutes, 5 bits seconds with a 2sec precision
\r
1355 quint16 hour = (time[1] >> 3) & 31;
\r
1356 quint16 minutes = ((time[1] << 3) & 56) | ((time[0] >> 5) & 7);
\r
1357 quint16 seconds = (time[0] & 31) * 2;
\r
1359 dt.setDate(QDate(1980 + year, month, day));
\r
1360 dt.setTime(QTime(hour, minutes, seconds));
\r