/* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org> * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ #include "zipfile.hh" #include <QtEndian> #include <QByteArray> #include <QFileInfo> namespace ZipFile { #pragma pack( push, 1 ) /// End-of-central-directory record, as is struct EndOfCdirRecord { quint32 signature; quint16 numDisk, numDiskCd, totalEntriesDisk, totalEntries; quint32 size, offset; quint16 commentLength; } #ifndef _MSC_VER __attribute__((packed)) #endif ; struct CentralFileHeaderRecord { quint32 signature; quint16 verMadeBy, verNeeded, gpBits, compressionMethod, fileTime, fileDate; quint32 crc32, compressedSize, uncompressedSize; quint16 fileNameLength, extraFieldLength, fileCommentLength, diskNumberStart, intFileAttrs; quint32 externalFileAttrs, offsetOfLocalHeader; } #ifndef _MSC_VER __attribute__((packed)) #endif ; struct LocalFileHeaderRecord { quint32 signature; quint16 verNeeded, gpBits, compressionMethod, fileTime, fileDate; quint32 crc32, compressedSize, uncompressedSize; quint16 fileNameLength, extraFieldLength; } #ifndef _MSC_VER __attribute__((packed)) #endif ; #pragma pack( pop ) static quint32 const endOfCdirRecordSignatureValue = qToLittleEndian( 0x06054b50 ); static quint32 const centralFileHeaderSignature = qToLittleEndian( 0x02014b50 ); static quint32 const localFileHeaderSignature = qToLittleEndian( 0x04034b50 ); static CompressionMethod getCompressionMethod( quint16 compressionMethod ) { switch( qFromLittleEndian( compressionMethod ) ) { case 0: return Uncompressed; case 8: return Deflated; default: return Unsupported; } } bool positionAtCentralDir( SplitZipFile & zip ) { // Find the end-of-central-directory record int maxEofBufferSize = 65535 + sizeof( EndOfCdirRecord ); if ( zip.size() > maxEofBufferSize ) zip.seek( zip.size() - maxEofBufferSize ); else if ( (size_t) zip.size() < sizeof( EndOfCdirRecord ) ) return false; else zip.seek( 0 ); QByteArray eocBuffer = zip.read( maxEofBufferSize ); if ( eocBuffer.size() < (int)sizeof( EndOfCdirRecord ) ) return false; int lastIndex = eocBuffer.size() - sizeof( EndOfCdirRecord ); QByteArray endOfCdirRecordSignature( (char const *)&endOfCdirRecordSignatureValue, sizeof( endOfCdirRecordSignatureValue ) ); EndOfCdirRecord endOfCdirRecord; quint32 cdir_offset; for( ; ; --lastIndex ) { lastIndex = eocBuffer.lastIndexOf( endOfCdirRecordSignature, lastIndex ); if ( lastIndex == -1 ) return false; /// We need to copy it due to possible alignment issues on ARM etc memcpy( &endOfCdirRecord, eocBuffer.data() + lastIndex, sizeof( endOfCdirRecord ) ); /// Sanitize the record by checking the offset cdir_offset = zip.calcAbsoluteOffset( qFromLittleEndian( endOfCdirRecord.offset ), qFromLittleEndian( endOfCdirRecord.numDiskCd ) ); if ( !zip.seek( cdir_offset ) ) continue; quint32 signature; if ( zip.read( (char *)&signature, sizeof( signature ) ) != sizeof( signature ) ) continue; if ( signature == centralFileHeaderSignature ) break; } // Found cdir -- position the file on the first header return zip.seek( cdir_offset ); } bool readNextEntry( SplitZipFile & zip, CentralDirEntry & entry ) { CentralFileHeaderRecord record; if ( zip.read( (char *)&record, sizeof( record ) ) != sizeof( record ) ) return false; if ( record.signature != centralFileHeaderSignature ) return false; // Read file name int fileNameLength = qFromLittleEndian( record.fileNameLength ); entry.fileName = zip.read( fileNameLength ); if ( entry.fileName.size() != fileNameLength ) return false; // Skip extra fields if ( !zip.seek( ( zip.pos() + qFromLittleEndian( record.extraFieldLength ) ) + qFromLittleEndian( record.fileCommentLength ) ) ) return false; entry.localHeaderOffset = zip.calcAbsoluteOffset( qFromLittleEndian( record.offsetOfLocalHeader ), qFromLittleEndian( record.diskNumberStart ) ); entry.compressedSize = qFromLittleEndian( record.compressedSize ); entry.uncompressedSize = qFromLittleEndian( record.uncompressedSize ); entry.compressionMethod = getCompressionMethod( record.compressionMethod ); entry.fileNameInUTF8 = ( qFromLittleEndian( record.gpBits ) & 0x800 ) != 0; return true; } bool readLocalHeader( SplitZipFile & zip, LocalFileHeader & entry ) { LocalFileHeaderRecord record; if ( zip.read( (char *)&record, sizeof( record ) ) != sizeof( record ) ) return false; if ( record.signature != localFileHeaderSignature ) return false; // Read file name int fileNameLength = qFromLittleEndian( record.fileNameLength ); entry.fileName = zip.read( fileNameLength ); if ( entry.fileName.size() != fileNameLength ) return false; // Skip extra field if ( !zip.seek( zip.pos() + qFromLittleEndian( record.extraFieldLength ) ) ) return false; entry.compressedSize = qFromLittleEndian( record.compressedSize ); entry.uncompressedSize = qFromLittleEndian( record.uncompressedSize ); entry.compressionMethod = getCompressionMethod( record.compressionMethod ); return true; } SplitZipFile::SplitZipFile( const QString & name ) { setFileName( name ); } void SplitZipFile::setFileName( const QString & name ) { { QString lname = name.toLower(); if( lname.endsWith( ".zips" ) ) { appendFile( name ); return; } if( !lname.endsWith( ".zip" ) ) return; } if( QFileInfo( name ).isFile() ) { for( int i = 1; i < 100; i++ ) { QString name2 = name.left( name.size() - 2 ) + QString( "%1" ).arg( i, 2, 10, QChar( '0' ) ); if( QFileInfo( name2 ).isFile() ) appendFile( name2 ); else break; } appendFile( name ); } else { for( int i = 1; i < 1000; i++ ) { QString name2 = name + QString( ".%1" ).arg( i, 3, 10, QChar( '0' ) ); if( QFileInfo( name2 ).isFile() ) appendFile( name2 ); else break; } } } QDateTime SplitZipFile::lastModified() const { unsigned long ts = 0; for( QVector< QFile * >::const_iterator i = files.begin(); i != files.end(); ++i ) { unsigned long t = QFileInfo( (*i)->fileName() ).lastModified().toTime_t(); if( t > ts ) ts = t; } return QDateTime::fromTime_t( ts ); } qint64 SplitZipFile::calcAbsoluteOffset( qint64 offset, quint16 partNo ) { if( partNo >= offsets.size() ) return 0; return offsets.at( partNo ) + offset; } }