1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qzipreader_p.h"
5#include "qzipwriter_p.h"
6
7#include <qdatetime.h>
8#include <qendian.h>
9#include <qdebug.h>
10#include <qdir.h>
11
12#include <memory>
13
14#include <zlib.h>
15
16// Zip standard version for archives handled by this API
17// (actually, the only basic support of this version is implemented but it is enough for now)
18#define ZIP_VERSION 20
19
20#if 0
21#define ZDEBUG qDebug
22#else
23#define ZDEBUG if (0) qDebug
24#endif
25
26QT_BEGIN_NAMESPACE
27
28static inline uint readUInt(const uchar *data)
29{
30 return (data[0]) + (data[1]<<8) + (data[2]<<16) + (data[3]<<24);
31}
32
33static inline ushort readUShort(const uchar *data)
34{
35 return (data[0]) + (data[1]<<8);
36}
37
38static inline void writeUInt(uchar *data, uint i)
39{
40 data[0] = i & 0xff;
41 data[1] = (i>>8) & 0xff;
42 data[2] = (i>>16) & 0xff;
43 data[3] = (i>>24) & 0xff;
44}
45
46static inline void writeUShort(uchar *data, ushort i)
47{
48 data[0] = i & 0xff;
49 data[1] = (i>>8) & 0xff;
50}
51
52static inline void copyUInt(uchar *dest, const uchar *src)
53{
54 dest[0] = src[0];
55 dest[1] = src[1];
56 dest[2] = src[2];
57 dest[3] = src[3];
58}
59
60static inline void copyUShort(uchar *dest, const uchar *src)
61{
62 dest[0] = src[0];
63 dest[1] = src[1];
64}
65
66static void writeMSDosDate(uchar *dest, const QDateTime& dt)
67{
68 if (dt.isValid()) {
69 quint16 time =
70 (dt.time().hour() << 11) // 5 bit hour
71 | (dt.time().minute() << 5) // 6 bit minute
72 | (dt.time().second() >> 1); // 5 bit double seconds
73
74 dest[0] = time & 0xff;
75 dest[1] = time >> 8;
76
77 quint16 date =
78 ((dt.date().year() - 1980) << 9) // 7 bit year 1980-based
79 | (dt.date().month() << 5) // 4 bit month
80 | (dt.date().day()); // 5 bit day
81
82 dest[2] = char(date);
83 dest[3] = char(date >> 8);
84 } else {
85 dest[0] = 0;
86 dest[1] = 0;
87 dest[2] = 0;
88 dest[3] = 0;
89 }
90}
91
92static int inflate(Bytef *dest, ulong *destLen, const Bytef *source, ulong sourceLen)
93{
94 z_stream stream;
95 int err;
96
97 stream.next_in = const_cast<Bytef*>(source);
98 stream.avail_in = (uInt)sourceLen;
99 if ((uLong)stream.avail_in != sourceLen)
100 return Z_BUF_ERROR;
101
102 stream.next_out = dest;
103 stream.avail_out = (uInt)*destLen;
104 if ((uLong)stream.avail_out != *destLen)
105 return Z_BUF_ERROR;
106
107 stream.zalloc = (alloc_func)nullptr;
108 stream.zfree = (free_func)nullptr;
109
110 err = inflateInit2(&stream, -MAX_WBITS);
111 if (err != Z_OK)
112 return err;
113
114 err = inflate(strm: &stream, Z_FINISH);
115 if (err != Z_STREAM_END) {
116 inflateEnd(strm: &stream);
117 if (err == Z_NEED_DICT || (err == Z_BUF_ERROR && stream.avail_in == 0))
118 return Z_DATA_ERROR;
119 return err;
120 }
121 *destLen = stream.total_out;
122
123 err = inflateEnd(strm: &stream);
124 return err;
125}
126
127static int deflate (Bytef *dest, ulong *destLen, const Bytef *source, ulong sourceLen)
128{
129 z_stream stream;
130 int err;
131
132 stream.next_in = const_cast<Bytef*>(source);
133 stream.avail_in = (uInt)sourceLen;
134 stream.next_out = dest;
135 stream.avail_out = (uInt)*destLen;
136 if ((uLong)stream.avail_out != *destLen) return Z_BUF_ERROR;
137
138 stream.zalloc = (alloc_func)nullptr;
139 stream.zfree = (free_func)nullptr;
140 stream.opaque = (voidpf)nullptr;
141
142 err = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY);
143 if (err != Z_OK) return err;
144
145 err = deflate(strm: &stream, Z_FINISH);
146 if (err != Z_STREAM_END) {
147 deflateEnd(strm: &stream);
148 return err == Z_OK ? Z_BUF_ERROR : err;
149 }
150 *destLen = stream.total_out;
151
152 err = deflateEnd(strm: &stream);
153 return err;
154}
155
156
157namespace WindowsFileAttributes {
158enum {
159 Dir = 0x10, // FILE_ATTRIBUTE_DIRECTORY
160 File = 0x80, // FILE_ATTRIBUTE_NORMAL
161 TypeMask = 0x90,
162
163 ReadOnly = 0x01, // FILE_ATTRIBUTE_READONLY
164 PermMask = 0x01
165};
166}
167
168namespace UnixFileAttributes {
169enum {
170 Dir = 0040000, // __S_IFDIR
171 File = 0100000, // __S_IFREG
172 SymLink = 0120000, // __S_IFLNK
173 TypeMask = 0170000, // __S_IFMT
174
175 ReadUser = 0400, // __S_IRUSR
176 WriteUser = 0200, // __S_IWUSR
177 ExeUser = 0100, // __S_IXUSR
178 ReadGroup = 0040, // __S_IRGRP
179 WriteGroup = 0020, // __S_IWGRP
180 ExeGroup = 0010, // __S_IXGRP
181 ReadOther = 0004, // __S_IROTH
182 WriteOther = 0002, // __S_IWOTH
183 ExeOther = 0001, // __S_IXOTH
184 PermMask = 0777
185};
186}
187
188static QFile::Permissions modeToPermissions(quint32 mode)
189{
190 QFile::Permissions ret;
191 if (mode & UnixFileAttributes::ReadUser)
192 ret |= QFile::ReadOwner | QFile::ReadUser;
193 if (mode & UnixFileAttributes::WriteUser)
194 ret |= QFile::WriteOwner | QFile::WriteUser;
195 if (mode & UnixFileAttributes::ExeUser)
196 ret |= QFile::ExeOwner | QFile::ExeUser;
197 if (mode & UnixFileAttributes::ReadGroup)
198 ret |= QFile::ReadGroup;
199 if (mode & UnixFileAttributes::WriteGroup)
200 ret |= QFile::WriteGroup;
201 if (mode & UnixFileAttributes::ExeGroup)
202 ret |= QFile::ExeGroup;
203 if (mode & UnixFileAttributes::ReadOther)
204 ret |= QFile::ReadOther;
205 if (mode & UnixFileAttributes::WriteOther)
206 ret |= QFile::WriteOther;
207 if (mode & UnixFileAttributes::ExeOther)
208 ret |= QFile::ExeOther;
209 return ret;
210}
211
212static quint32 permissionsToMode(QFile::Permissions perms)
213{
214 quint32 mode = 0;
215 if (perms & (QFile::ReadOwner | QFile::ReadUser))
216 mode |= UnixFileAttributes::ReadUser;
217 if (perms & (QFile::WriteOwner | QFile::WriteUser))
218 mode |= UnixFileAttributes::WriteUser;
219 if (perms & (QFile::ExeOwner | QFile::ExeUser))
220 mode |= UnixFileAttributes::WriteUser;
221 if (perms & QFile::ReadGroup)
222 mode |= UnixFileAttributes::ReadGroup;
223 if (perms & QFile::WriteGroup)
224 mode |= UnixFileAttributes::WriteGroup;
225 if (perms & QFile::ExeGroup)
226 mode |= UnixFileAttributes::ExeGroup;
227 if (perms & QFile::ReadOther)
228 mode |= UnixFileAttributes::ReadOther;
229 if (perms & QFile::WriteOther)
230 mode |= UnixFileAttributes::WriteOther;
231 if (perms & QFile::ExeOther)
232 mode |= UnixFileAttributes::ExeOther;
233 return mode;
234}
235
236static QDateTime readMSDosDate(const uchar *src)
237{
238 uint dosDate = readUInt(data: src);
239 quint64 uDate;
240 uDate = (quint64)(dosDate >> 16);
241 uint tm_mday = (uDate & 0x1f);
242 uint tm_mon = ((uDate & 0x1E0) >> 5);
243 uint tm_year = (((uDate & 0x0FE00) >> 9) + 1980);
244 uint tm_hour = ((dosDate & 0xF800) >> 11);
245 uint tm_min = ((dosDate & 0x7E0) >> 5);
246 uint tm_sec = ((dosDate & 0x1f) << 1);
247
248 return QDateTime(QDate(tm_year, tm_mon, tm_mday), QTime(tm_hour, tm_min, tm_sec));
249}
250
251// for details, see http://www.pkware.com/documents/casestudies/APPNOTE.TXT
252
253enum HostOS {
254 HostFAT = 0,
255 HostAMIGA = 1,
256 HostVMS = 2, // VAX/VMS
257 HostUnix = 3,
258 HostVM_CMS = 4,
259 HostAtari = 5, // what if it's a minix filesystem? [cjh]
260 HostHPFS = 6, // filesystem used by OS/2 (and NT 3.x)
261 HostMac = 7,
262 HostZ_System = 8,
263 HostCPM = 9,
264 HostTOPS20 = 10, // pkzip 2.50 NTFS
265 HostNTFS = 11, // filesystem used by Windows NT
266 HostQDOS = 12, // SMS/QDOS
267 HostAcorn = 13, // Archimedes Acorn RISC OS
268 HostVFAT = 14, // filesystem used by Windows 95, NT
269 HostMVS = 15,
270 HostBeOS = 16, // hybrid POSIX/database filesystem
271 HostTandem = 17,
272 HostOS400 = 18,
273 HostOSX = 19
274};
275Q_DECLARE_TYPEINFO(HostOS, Q_PRIMITIVE_TYPE);
276
277enum GeneralPurposeFlag {
278 Encrypted = 0x01,
279 AlgTune1 = 0x02,
280 AlgTune2 = 0x04,
281 HasDataDescriptor = 0x08,
282 PatchedData = 0x20,
283 StrongEncrypted = 0x40,
284 Utf8Names = 0x0800,
285 CentralDirectoryEncrypted = 0x2000
286};
287Q_DECLARE_TYPEINFO(GeneralPurposeFlag, Q_PRIMITIVE_TYPE);
288
289enum CompressionMethod {
290 CompressionMethodStored = 0,
291 CompressionMethodShrunk = 1,
292 CompressionMethodReduced1 = 2,
293 CompressionMethodReduced2 = 3,
294 CompressionMethodReduced3 = 4,
295 CompressionMethodReduced4 = 5,
296 CompressionMethodImploded = 6,
297 CompressionMethodReservedTokenizing = 7, // reserved for tokenizing
298 CompressionMethodDeflated = 8,
299 CompressionMethodDeflated64 = 9,
300 CompressionMethodPKImploding = 10,
301
302 CompressionMethodBZip2 = 12,
303
304 CompressionMethodLZMA = 14,
305
306 CompressionMethodTerse = 18,
307 CompressionMethodLz77 = 19,
308
309 CompressionMethodJpeg = 96,
310 CompressionMethodWavPack = 97,
311 CompressionMethodPPMd = 98,
312 CompressionMethodWzAES = 99
313};
314Q_DECLARE_TYPEINFO(CompressionMethod, Q_PRIMITIVE_TYPE);
315
316struct LocalFileHeader
317{
318 uchar signature[4]; // 0x04034b50
319 uchar version_needed[2];
320 uchar general_purpose_bits[2];
321 uchar compression_method[2];
322 uchar last_mod_file[4];
323 uchar crc_32[4];
324 uchar compressed_size[4];
325 uchar uncompressed_size[4];
326 uchar file_name_length[2];
327 uchar extra_field_length[2];
328};
329Q_DECLARE_TYPEINFO(LocalFileHeader, Q_PRIMITIVE_TYPE);
330
331struct DataDescriptor
332{
333 uchar crc_32[4];
334 uchar compressed_size[4];
335 uchar uncompressed_size[4];
336};
337Q_DECLARE_TYPEINFO(DataDescriptor, Q_PRIMITIVE_TYPE);
338
339struct CentralFileHeader
340{
341 uchar signature[4]; // 0x02014b50
342 uchar version_made[2];
343 uchar version_needed[2];
344 uchar general_purpose_bits[2];
345 uchar compression_method[2];
346 uchar last_mod_file[4];
347 uchar crc_32[4];
348 uchar compressed_size[4];
349 uchar uncompressed_size[4];
350 uchar file_name_length[2];
351 uchar extra_field_length[2];
352 uchar file_comment_length[2];
353 uchar disk_start[2];
354 uchar internal_file_attributes[2];
355 uchar external_file_attributes[4];
356 uchar offset_local_header[4];
357};
358Q_DECLARE_TYPEINFO(CentralFileHeader, Q_PRIMITIVE_TYPE);
359
360struct EndOfDirectory
361{
362 uchar signature[4]; // 0x06054b50
363 uchar this_disk[2];
364 uchar start_of_directory_disk[2];
365 uchar num_dir_entries_this_disk[2];
366 uchar num_dir_entries[2];
367 uchar directory_size[4];
368 uchar dir_start_offset[4];
369 uchar comment_length[2];
370};
371Q_DECLARE_TYPEINFO(EndOfDirectory, Q_PRIMITIVE_TYPE);
372
373struct FileHeader
374{
375 CentralFileHeader h;
376 QByteArray file_name;
377 QByteArray extra_field;
378 QByteArray file_comment;
379};
380Q_DECLARE_TYPEINFO(FileHeader, Q_RELOCATABLE_TYPE);
381
382class QZipPrivate
383{
384public:
385 QZipPrivate(QIODevice *device, bool ownDev)
386 : device(device), ownDevice(ownDev), dirtyFileTree(true), start_of_directory(0)
387 {
388 }
389
390 ~QZipPrivate()
391 {
392 if (ownDevice)
393 delete device;
394 }
395
396 QZipReader::FileInfo fillFileInfo(int index) const;
397
398 QIODevice *device;
399 bool ownDevice;
400 bool dirtyFileTree;
401 QList<FileHeader> fileHeaders;
402 QByteArray comment;
403 uint start_of_directory;
404};
405
406QZipReader::FileInfo QZipPrivate::fillFileInfo(int index) const
407{
408 QZipReader::FileInfo fileInfo;
409 FileHeader header = fileHeaders.at(i: index);
410 quint32 mode = readUInt(data: header.h.external_file_attributes);
411 const HostOS hostOS = HostOS(readUShort(data: header.h.version_made) >> 8);
412 switch (hostOS) {
413 case HostUnix:
414 mode = (mode >> 16) & 0xffff;
415 switch (mode & UnixFileAttributes::TypeMask) {
416 case UnixFileAttributes::SymLink:
417 fileInfo.isSymLink = true;
418 break;
419 case UnixFileAttributes::Dir:
420 fileInfo.isDir = true;
421 break;
422 case UnixFileAttributes::File:
423 default: // ### just for the case; should we warn?
424 fileInfo.isFile = true;
425 break;
426 }
427 fileInfo.permissions = modeToPermissions(mode);
428 break;
429 case HostFAT:
430 case HostNTFS:
431 case HostHPFS:
432 case HostVFAT:
433 switch (mode & WindowsFileAttributes::TypeMask) {
434 case WindowsFileAttributes::Dir:
435 fileInfo.isDir = true;
436 break;
437 case WindowsFileAttributes::File:
438 default:
439 fileInfo.isFile = true;
440 break;
441 }
442 fileInfo.permissions |= QFile::ReadOwner | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther;
443 if ((mode & WindowsFileAttributes::ReadOnly) == 0)
444 fileInfo.permissions |= QFile::WriteOwner | QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther;
445 if (fileInfo.isDir)
446 fileInfo.permissions |= QFile::ExeOwner | QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther;
447 break;
448 default:
449 qWarning(msg: "QZip: Zip entry format at %d is not supported.", index);
450 return fileInfo; // we don't support anything else
451 }
452
453 ushort general_purpose_bits = readUShort(data: header.h.general_purpose_bits);
454 // if bit 11 is set, the filename and comment fields must be encoded using UTF-8
455 const bool inUtf8 = (general_purpose_bits & Utf8Names) != 0;
456 fileInfo.filePath = inUtf8 ? QString::fromUtf8(ba: header.file_name) : QString::fromLocal8Bit(ba: header.file_name);
457 fileInfo.crc = readUInt(data: header.h.crc_32);
458 fileInfo.size = readUInt(data: header.h.uncompressed_size);
459 fileInfo.lastModified = readMSDosDate(src: header.h.last_mod_file);
460
461 // fix the file path, if broken (convert separators, eat leading and trailing ones)
462 fileInfo.filePath = QDir::fromNativeSeparators(pathName: fileInfo.filePath);
463 QStringView filePathRef(fileInfo.filePath);
464 while (filePathRef.startsWith(c: u'.') || filePathRef.startsWith(c: u'/'))
465 filePathRef = filePathRef.mid(pos: 1);
466 while (filePathRef.endsWith(c: u'/'))
467 filePathRef.chop(n: 1);
468
469 fileInfo.filePath = filePathRef.toString();
470 return fileInfo;
471}
472
473class QZipReaderPrivate : public QZipPrivate
474{
475public:
476 QZipReaderPrivate(QIODevice *device, bool ownDev)
477 : QZipPrivate(device, ownDev), status(QZipReader::NoError)
478 {
479 }
480
481 void scanFiles();
482
483 QZipReader::Status status;
484};
485
486class QZipWriterPrivate : public QZipPrivate
487{
488public:
489 QZipWriterPrivate(QIODevice *device, bool ownDev)
490 : QZipPrivate(device, ownDev),
491 status(QZipWriter::NoError),
492 permissions(QFile::ReadOwner | QFile::WriteOwner),
493 compressionPolicy(QZipWriter::AlwaysCompress)
494 {
495 }
496
497 QZipWriter::Status status;
498 QFile::Permissions permissions;
499 QZipWriter::CompressionPolicy compressionPolicy;
500
501 enum EntryType { Directory, File, Symlink };
502
503 void addEntry(EntryType type, const QString &fileName, const QByteArray &contents);
504};
505
506static LocalFileHeader toLocalHeader(const CentralFileHeader &ch)
507{
508 LocalFileHeader h;
509 writeUInt(data: h.signature, i: 0x04034b50);
510 copyUShort(dest: h.version_needed, src: ch.version_needed);
511 copyUShort(dest: h.general_purpose_bits, src: ch.general_purpose_bits);
512 copyUShort(dest: h.compression_method, src: ch.compression_method);
513 copyUInt(dest: h.last_mod_file, src: ch.last_mod_file);
514 copyUInt(dest: h.crc_32, src: ch.crc_32);
515 copyUInt(dest: h.compressed_size, src: ch.compressed_size);
516 copyUInt(dest: h.uncompressed_size, src: ch.uncompressed_size);
517 copyUShort(dest: h.file_name_length, src: ch.file_name_length);
518 copyUShort(dest: h.extra_field_length, src: ch.extra_field_length);
519 return h;
520}
521
522void QZipReaderPrivate::scanFiles()
523{
524 if (!dirtyFileTree)
525 return;
526
527 if (! (device->isOpen() || device->open(mode: QIODevice::ReadOnly))) {
528 status = QZipReader::FileOpenError;
529 return;
530 }
531
532 if ((device->openMode() & QIODevice::ReadOnly) == 0) { // only read the index from readable files.
533 status = QZipReader::FileReadError;
534 return;
535 }
536
537 dirtyFileTree = false;
538 uchar tmp[4];
539 device->read(data: (char *)tmp, maxlen: 4);
540 if (readUInt(data: tmp) != 0x04034b50) {
541 qWarning(msg: "QZip: not a zip file!");
542 return;
543 }
544
545 // find EndOfDirectory header
546 int i = 0;
547 int start_of_directory = -1;
548 int num_dir_entries = 0;
549 EndOfDirectory eod;
550 while (start_of_directory == -1) {
551 const int pos = device->size() - int(sizeof(EndOfDirectory)) - i;
552 if (pos < 0 || i > 65535) {
553 qWarning(msg: "QZip: EndOfDirectory not found");
554 return;
555 }
556
557 device->seek(pos);
558 device->read(data: (char *)&eod, maxlen: sizeof(EndOfDirectory));
559 if (readUInt(data: eod.signature) == 0x06054b50)
560 break;
561 ++i;
562 }
563
564 // have the eod
565 start_of_directory = readUInt(data: eod.dir_start_offset);
566 num_dir_entries = readUShort(data: eod.num_dir_entries);
567 ZDEBUG(msg: "start_of_directory at %d, num_dir_entries=%d", start_of_directory, num_dir_entries);
568 int comment_length = readUShort(data: eod.comment_length);
569 if (comment_length != i)
570 qWarning(msg: "QZip: failed to parse zip file.");
571 comment = device->read(maxlen: qMin(a: comment_length, b: i));
572
573
574 device->seek(pos: start_of_directory);
575 for (i = 0; i < num_dir_entries; ++i) {
576 FileHeader header;
577 int read = device->read(data: (char *) &header.h, maxlen: sizeof(CentralFileHeader));
578 if (read < (int)sizeof(CentralFileHeader)) {
579 qWarning(msg: "QZip: Failed to read complete header, index may be incomplete");
580 break;
581 }
582 if (readUInt(data: header.h.signature) != 0x02014b50) {
583 qWarning(msg: "QZip: invalid header signature, index may be incomplete");
584 break;
585 }
586
587 int l = readUShort(data: header.h.file_name_length);
588 header.file_name = device->read(maxlen: l);
589 if (header.file_name.size() != l) {
590 qWarning(msg: "QZip: Failed to read filename from zip index, index may be incomplete");
591 break;
592 }
593 l = readUShort(data: header.h.extra_field_length);
594 header.extra_field = device->read(maxlen: l);
595 if (header.extra_field.size() != l) {
596 qWarning(msg: "QZip: Failed to read extra field in zip file, skipping file, index may be incomplete");
597 break;
598 }
599 l = readUShort(data: header.h.file_comment_length);
600 header.file_comment = device->read(maxlen: l);
601 if (header.file_comment.size() != l) {
602 qWarning(msg: "QZip: Failed to read read file comment, index may be incomplete");
603 break;
604 }
605
606 ZDEBUG(msg: "found file '%s'", header.file_name.data());
607 fileHeaders.append(t: header);
608 }
609}
610
611void QZipWriterPrivate::addEntry(EntryType type, const QString &fileName, const QByteArray &contents/*, QFile::Permissions permissions, QZip::Method m*/)
612{
613#ifndef NDEBUG
614 static const char *const entryTypes[] = {
615 "directory",
616 "file ",
617 "symlink " };
618 ZDEBUG() << "adding" << entryTypes[type] <<":" << fileName.toUtf8().data() << (type == 2 ? QByteArray(" -> " + contents).constData() : "");
619#endif
620
621 if (! (device->isOpen() || device->open(mode: QIODevice::WriteOnly))) {
622 status = QZipWriter::FileOpenError;
623 return;
624 }
625 device->seek(pos: start_of_directory);
626
627 // don't compress small files
628 QZipWriter::CompressionPolicy compression = compressionPolicy;
629 if (compressionPolicy == QZipWriter::AutoCompress) {
630 if (contents.size() < 64)
631 compression = QZipWriter::NeverCompress;
632 else
633 compression = QZipWriter::AlwaysCompress;
634 }
635
636 FileHeader header;
637 memset(s: &header.h, c: 0, n: sizeof(CentralFileHeader));
638 writeUInt(data: header.h.signature, i: 0x02014b50);
639
640 writeUShort(data: header.h.version_needed, ZIP_VERSION);
641 writeUInt(data: header.h.uncompressed_size, i: contents.size());
642 writeMSDosDate(dest: header.h.last_mod_file, dt: QDateTime::currentDateTime());
643 QByteArray data = contents;
644 if (compression == QZipWriter::AlwaysCompress) {
645 writeUShort(data: header.h.compression_method, i: CompressionMethodDeflated);
646
647 ulong len = contents.size();
648 // shamelessly copied form zlib
649 len += (len >> 12) + (len >> 14) + 11;
650 int res;
651 do {
652 data.resize(size: len);
653 res = deflate(dest: (uchar*)data.data(), destLen: &len, source: (const uchar*)contents.constData(), sourceLen: contents.size());
654
655 switch (res) {
656 case Z_OK:
657 data.resize(size: len);
658 break;
659 case Z_MEM_ERROR:
660 qWarning(msg: "QZip: Z_MEM_ERROR: Not enough memory to compress file, skipping");
661 data.resize(size: 0);
662 break;
663 case Z_BUF_ERROR:
664 len *= 2;
665 break;
666 }
667 } while (res == Z_BUF_ERROR);
668 }
669// TODO add a check if data.length() > contents.length(). Then try to store the original and revert the compression method to be uncompressed
670 writeUInt(data: header.h.compressed_size, i: data.size());
671 uint crc_32 = ::crc32(crc: 0, buf: nullptr, len: 0);
672 crc_32 = ::crc32(crc: crc_32, buf: (const uchar *)contents.constData(), len: contents.size());
673 writeUInt(data: header.h.crc_32, i: crc_32);
674
675 // if bit 11 is set, the filename and comment fields must be encoded using UTF-8
676 ushort general_purpose_bits = Utf8Names; // always use utf-8
677 writeUShort(data: header.h.general_purpose_bits, i: general_purpose_bits);
678
679 const bool inUtf8 = (general_purpose_bits & Utf8Names) != 0;
680 header.file_name = inUtf8 ? fileName.toUtf8() : fileName.toLocal8Bit();
681 if (header.file_name.size() > 0xffff) {
682 qWarning(msg: "QZip: Filename is too long, chopping it to 65535 bytes");
683 header.file_name = header.file_name.left(len: 0xffff); // ### don't break the utf-8 sequence, if any
684 }
685 if (header.file_comment.size() + header.file_name.size() > 0xffff) {
686 qWarning(msg: "QZip: File comment is too long, chopping it to 65535 bytes");
687 header.file_comment.truncate(pos: 0xffff - header.file_name.size()); // ### don't break the utf-8 sequence, if any
688 }
689 writeUShort(data: header.h.file_name_length, i: header.file_name.size());
690 //h.extra_field_length[2];
691
692 writeUShort(data: header.h.version_made, i: HostUnix << 8);
693 //uchar internal_file_attributes[2];
694 //uchar external_file_attributes[4];
695 quint32 mode = permissionsToMode(perms: permissions);
696 switch (type) {
697 case Symlink:
698 mode |= UnixFileAttributes::SymLink;
699 break;
700 case Directory:
701 mode |= UnixFileAttributes::Dir;
702 break;
703 case File:
704 mode |= UnixFileAttributes::File;
705 break;
706 default:
707 Q_UNREACHABLE();
708 break;
709 }
710 writeUInt(data: header.h.external_file_attributes, i: mode << 16);
711 writeUInt(data: header.h.offset_local_header, i: start_of_directory);
712
713
714 fileHeaders.append(t: header);
715
716 LocalFileHeader h = toLocalHeader(ch: header.h);
717 device->write(data: (const char *)&h, len: sizeof(LocalFileHeader));
718 device->write(data: header.file_name);
719 device->write(data);
720 start_of_directory = device->pos();
721 dirtyFileTree = true;
722}
723
724////////////////////////////// Reader
725
726/*!
727 \class QZipReader::FileInfo
728 \internal
729 Represents one entry in the zip table of contents.
730*/
731
732/*!
733 \variable FileInfo::filePath
734 The full filepath inside the archive.
735*/
736
737/*!
738 \variable FileInfo::isDir
739 A boolean type indicating if the entry is a directory.
740*/
741
742/*!
743 \variable FileInfo::isFile
744 A boolean type, if it is one this entry is a file.
745*/
746
747/*!
748 \variable FileInfo::isSymLink
749 A boolean type, if it is one this entry is symbolic link.
750*/
751
752/*!
753 \variable FileInfo::permissions
754 A list of flags for the permissions of this entry.
755*/
756
757/*!
758 \variable FileInfo::crc
759 The calculated checksum as a crc type.
760*/
761
762/*!
763 \variable FileInfo::size
764 The total size of the unpacked content.
765*/
766
767/*!
768 \class QZipReader
769 \internal
770 \since 4.5
771
772 \brief the QZipReader class provides a way to inspect the contents of a zip
773 archive and extract individual files from it.
774
775 QZipReader can be used to read a zip archive either from a file or from any
776 device. An in-memory QBuffer for instance. The reader can be used to read
777 which files are in the archive using fileInfoList() and entryInfoAt() but
778 also to extract individual files using fileData() or even to extract all
779 files in the archive using extractAll()
780*/
781
782/*!
783 Create a new zip archive that operates on the \a fileName. The file will be
784 opened with the \a mode.
785*/
786QZipReader::QZipReader(const QString &archive, QIODevice::OpenMode mode)
787{
788 auto f = std::make_unique<QFile>(args: archive);
789 const bool result = f->open(flags: mode);
790 QZipReader::Status status;
791 const QFileDevice::FileError error = f->error();
792 if (result && error == QFile::NoError) {
793 status = NoError;
794 } else {
795 if (error == QFile::ReadError)
796 status = FileReadError;
797 else if (error == QFile::OpenError)
798 status = FileOpenError;
799 else if (error == QFile::PermissionsError)
800 status = FilePermissionsError;
801 else
802 status = FileError;
803 }
804
805 d = new QZipReaderPrivate(f.get(), /*ownDevice=*/true);
806 Q_UNUSED(f.release());
807 d->status = status;
808}
809
810/*!
811 Create a new zip archive that operates on the archive found in \a device.
812 You have to open the device previous to calling the constructor and only a
813 device that is readable will be scanned for zip filecontent.
814 */
815QZipReader::QZipReader(QIODevice *device)
816 : d(new QZipReaderPrivate(device, /*ownDevice=*/false))
817{
818 Q_ASSERT(device);
819}
820
821/*!
822 Destructor
823*/
824QZipReader::~QZipReader()
825{
826 close();
827 delete d;
828}
829
830/*!
831 Returns device used for reading zip archive.
832*/
833QIODevice* QZipReader::device() const
834{
835 return d->device;
836}
837
838/*!
839 Returns \c true if the user can read the file; otherwise returns \c false.
840*/
841bool QZipReader::isReadable() const
842{
843 return d->device->isReadable();
844}
845
846/*!
847 Returns \c true if the file exists; otherwise returns \c false.
848*/
849bool QZipReader::exists() const
850{
851 QFile *f = qobject_cast<QFile*> (object: d->device);
852 if (f == nullptr)
853 return true;
854 return f->exists();
855}
856
857/*!
858 Returns the list of files the archive contains.
859*/
860QList<QZipReader::FileInfo> QZipReader::fileInfoList() const
861{
862 d->scanFiles();
863 QList<FileInfo> files;
864 const int numFileHeaders = d->fileHeaders.size();
865 files.reserve(asize: numFileHeaders);
866 for (int i = 0; i < numFileHeaders; ++i)
867 files.append(t: d->fillFileInfo(index: i));
868 return files;
869
870}
871
872/*!
873 Return the number of items in the zip archive.
874*/
875int QZipReader::count() const
876{
877 d->scanFiles();
878 return d->fileHeaders.size();
879}
880
881/*!
882 Returns a FileInfo of an entry in the zipfile.
883 The \a index is the index into the directory listing of the zipfile.
884 Returns an invalid FileInfo if \a index is out of boundaries.
885
886 \sa fileInfoList()
887*/
888QZipReader::FileInfo QZipReader::entryInfoAt(int index) const
889{
890 d->scanFiles();
891 if (index >= 0 && index < d->fileHeaders.size())
892 return d->fillFileInfo(index);
893 return QZipReader::FileInfo();
894}
895
896/*!
897 Fetch the file contents from the zip archive and return the uncompressed bytes.
898*/
899QByteArray QZipReader::fileData(const QString &fileName) const
900{
901 d->scanFiles();
902 int i;
903 for (i = 0; i < d->fileHeaders.size(); ++i) {
904 if (QString::fromLocal8Bit(ba: d->fileHeaders.at(i).file_name) == fileName)
905 break;
906 }
907 if (i == d->fileHeaders.size())
908 return QByteArray();
909
910 FileHeader header = d->fileHeaders.at(i);
911
912 ushort version_needed = readUShort(data: header.h.version_needed);
913 if (version_needed > ZIP_VERSION) {
914 qWarning(msg: "QZip: .ZIP specification version %d implementationis needed to extract the data.", version_needed);
915 return QByteArray();
916 }
917
918 ushort general_purpose_bits = readUShort(data: header.h.general_purpose_bits);
919 int compressed_size = readUInt(data: header.h.compressed_size);
920 int uncompressed_size = readUInt(data: header.h.uncompressed_size);
921 int start = readUInt(data: header.h.offset_local_header);
922 //qDebug("uncompressing file %d: local header at %d", i, start);
923
924 d->device->seek(pos: start);
925 LocalFileHeader lh;
926 d->device->read(data: (char *)&lh, maxlen: sizeof(LocalFileHeader));
927 uint skip = readUShort(data: lh.file_name_length) + readUShort(data: lh.extra_field_length);
928 d->device->seek(pos: d->device->pos() + skip);
929
930 int compression_method = readUShort(data: lh.compression_method);
931 //qDebug("file=%s: compressed_size=%d, uncompressed_size=%d", fileName.toLocal8Bit().data(), compressed_size, uncompressed_size);
932
933 if ((general_purpose_bits & Encrypted) != 0) {
934 qWarning(msg: "QZip: Unsupported encryption method is needed to extract the data.");
935 return QByteArray();
936 }
937
938 //qDebug("file at %lld", d->device->pos());
939 QByteArray compressed = d->device->read(maxlen: compressed_size);
940 if (compression_method == CompressionMethodStored) {
941 // no compression
942 compressed.truncate(pos: uncompressed_size);
943 return compressed;
944 } else if (compression_method == CompressionMethodDeflated) {
945 // Deflate
946 //qDebug("compressed=%d", compressed.size());
947 compressed.truncate(pos: compressed_size);
948 QByteArray baunzip;
949 ulong len = qMax(a: uncompressed_size, b: 1);
950 int res;
951 do {
952 baunzip.resize(size: len);
953 res = inflate(dest: (uchar*)baunzip.data(), destLen: &len,
954 source: (const uchar*)compressed.constData(), sourceLen: compressed_size);
955
956 switch (res) {
957 case Z_OK:
958 if ((int)len != baunzip.size())
959 baunzip.resize(size: len);
960 break;
961 case Z_MEM_ERROR:
962 qWarning(msg: "QZip: Z_MEM_ERROR: Not enough memory");
963 break;
964 case Z_BUF_ERROR:
965 len *= 2;
966 break;
967 case Z_DATA_ERROR:
968 qWarning(msg: "QZip: Z_DATA_ERROR: Input data is corrupted");
969 break;
970 }
971 } while (res == Z_BUF_ERROR);
972 return baunzip;
973 }
974
975 qWarning(msg: "QZip: Unsupported compression method %d is needed to extract the data.", compression_method);
976 return QByteArray();
977}
978
979/*!
980 Extracts the full contents of the zip file into \a destinationDir on
981 the local filesystem.
982 In case writing or linking a file fails, the extraction will be aborted.
983*/
984bool QZipReader::extractAll(const QString &destinationDir) const
985{
986 QDir baseDir(destinationDir);
987
988 // create directories first
989 const QList<FileInfo> allFiles = fileInfoList();
990 bool foundDirs = false;
991 bool hasDirs = false;
992 for (const FileInfo &fi : allFiles) {
993 const QString absPath = destinationDir + QDir::separator() + fi.filePath;
994 if (fi.isDir) {
995 foundDirs = true;
996 if (!baseDir.mkpath(dirPath: fi.filePath))
997 return false;
998 if (!QFile::setPermissions(filename: absPath, permissionSpec: fi.permissions))
999 return false;
1000 } else if (!hasDirs && fi.filePath.contains(s: u"/")) {
1001 // filePath does not have leading or trailing '/', so if we find
1002 // one, than the file path contains directories.
1003 hasDirs = true;
1004 }
1005 }
1006
1007 // Some zip archives can be broken in the sense that they do not report
1008 // separate entries for directories, only for files. In this case we
1009 // need to recreate directory structure based on the file paths.
1010 if (hasDirs && !foundDirs) {
1011 for (const FileInfo &fi : allFiles) {
1012 const auto dirPath = fi.filePath.left(n: fi.filePath.lastIndexOf(s: u"/"));
1013 if (!baseDir.mkpath(dirPath))
1014 return false;
1015 // We will leave the directory permissions default in this case,
1016 // because setting dir permissions based on file is incorrect
1017 }
1018 }
1019
1020 // set up symlinks
1021 for (const FileInfo &fi : allFiles) {
1022 const QString absPath = destinationDir + QDir::separator() + fi.filePath;
1023 if (fi.isSymLink) {
1024 QString destination = QFile::decodeName(localFileName: fileData(fileName: fi.filePath));
1025 if (destination.isEmpty())
1026 return false;
1027 QFileInfo linkFi(absPath);
1028 if (!QFile::exists(fileName: linkFi.absolutePath()))
1029 QDir::root().mkpath(dirPath: linkFi.absolutePath());
1030 if (!QFile::link(fileName: destination, newName: absPath))
1031 return false;
1032 /* cannot change permission of links
1033 if (!QFile::setPermissions(absPath, fi.permissions))
1034 return false;
1035 */
1036 }
1037 }
1038
1039 for (const FileInfo &fi : allFiles) {
1040 const QString absPath = destinationDir + QDir::separator() + fi.filePath;
1041 if (fi.isFile) {
1042 QFile f(absPath);
1043 if (!f.open(flags: QIODevice::WriteOnly))
1044 return false;
1045 f.write(data: fileData(fileName: fi.filePath));
1046 f.setPermissions(fi.permissions);
1047 f.close();
1048 }
1049 }
1050
1051 return true;
1052}
1053
1054/*!
1055 \enum QZipReader::Status
1056
1057 The following status values are possible:
1058
1059 \value NoError No error occurred.
1060 \value FileReadError An error occurred when reading from the file.
1061 \value FileOpenError The file could not be opened.
1062 \value FilePermissionsError The file could not be accessed.
1063 \value FileError Another file error occurred.
1064*/
1065
1066/*!
1067 Returns a status code indicating the first error that was met by QZipReader,
1068 or QZipReader::NoError if no error occurred.
1069*/
1070QZipReader::Status QZipReader::status() const
1071{
1072 return d->status;
1073}
1074
1075/*!
1076 Close the zip file.
1077*/
1078void QZipReader::close()
1079{
1080 d->device->close();
1081}
1082
1083////////////////////////////// Writer
1084
1085/*!
1086 \class QZipWriter
1087 \internal
1088 \since 4.5
1089
1090 \brief the QZipWriter class provides a way to create a new zip archive.
1091
1092 QZipWriter can be used to create a zip archive containing any number of files
1093 and directories. The files in the archive will be compressed in a way that is
1094 compatible with common zip reader applications.
1095*/
1096
1097
1098/*!
1099 Create a new zip archive that operates on the \a archive filename. The file will
1100 be opened with the \a mode.
1101 \sa isValid()
1102*/
1103QZipWriter::QZipWriter(const QString &fileName, QIODevice::OpenMode mode)
1104{
1105 auto f = std::make_unique<QFile>(args: fileName);
1106 QZipWriter::Status status;
1107 if (f->open(flags: mode) && f->error() == QFile::NoError)
1108 status = QZipWriter::NoError;
1109 else {
1110 if (f->error() == QFile::WriteError)
1111 status = QZipWriter::FileWriteError;
1112 else if (f->error() == QFile::OpenError)
1113 status = QZipWriter::FileOpenError;
1114 else if (f->error() == QFile::PermissionsError)
1115 status = QZipWriter::FilePermissionsError;
1116 else
1117 status = QZipWriter::FileError;
1118 }
1119
1120 d = new QZipWriterPrivate(f.get(), /*ownDevice=*/true);
1121 Q_UNUSED(f.release());
1122 d->status = status;
1123}
1124
1125/*!
1126 Create a new zip archive that operates on the archive found in \a device.
1127 You have to open the device previous to calling the constructor and
1128 only a device that is readable will be scanned for zip filecontent.
1129 */
1130QZipWriter::QZipWriter(QIODevice *device)
1131 : d(new QZipWriterPrivate(device, /*ownDevice=*/false))
1132{
1133 Q_ASSERT(device);
1134}
1135
1136QZipWriter::~QZipWriter()
1137{
1138 close();
1139 delete d;
1140}
1141
1142/*!
1143 Returns device used for writing zip archive.
1144*/
1145QIODevice* QZipWriter::device() const
1146{
1147 return d->device;
1148}
1149
1150/*!
1151 Returns \c true if the user can write to the archive; otherwise returns \c false.
1152*/
1153bool QZipWriter::isWritable() const
1154{
1155 return d->device->isWritable();
1156}
1157
1158/*!
1159 Returns \c true if the file exists; otherwise returns \c false.
1160*/
1161bool QZipWriter::exists() const
1162{
1163 QFile *f = qobject_cast<QFile*> (object: d->device);
1164 if (f == nullptr)
1165 return true;
1166 return f->exists();
1167}
1168
1169/*!
1170 \enum QZipWriter::Status
1171
1172 The following status values are possible:
1173
1174 \value NoError No error occurred.
1175 \value FileWriteError An error occurred when writing to the device.
1176 \value FileOpenError The file could not be opened.
1177 \value FilePermissionsError The file could not be accessed.
1178 \value FileError Another file error occurred.
1179*/
1180
1181/*!
1182 Returns a status code indicating the first error that was met by QZipWriter,
1183 or QZipWriter::NoError if no error occurred.
1184*/
1185QZipWriter::Status QZipWriter::status() const
1186{
1187 return d->status;
1188}
1189
1190/*!
1191 \enum QZipWriter::CompressionPolicy
1192
1193 \value AlwaysCompress A file that is added is compressed.
1194 \value NeverCompress A file that is added will be stored without changes.
1195 \value AutoCompress A file that is added will be compressed only if that will give a smaller file.
1196*/
1197
1198/*!
1199 Sets the policy for compressing newly added files to the new \a policy.
1200
1201 \note the default policy is AlwaysCompress
1202
1203 \sa compressionPolicy()
1204 \sa addFile()
1205*/
1206void QZipWriter::setCompressionPolicy(CompressionPolicy policy)
1207{
1208 d->compressionPolicy = policy;
1209}
1210
1211/*!
1212 Returns the currently set compression policy.
1213 \sa setCompressionPolicy()
1214 \sa addFile()
1215*/
1216QZipWriter::CompressionPolicy QZipWriter::compressionPolicy() const
1217{
1218 return d->compressionPolicy;
1219}
1220
1221/*!
1222 Sets the permissions that will be used for newly added files.
1223
1224 \note the default permissions are QFile::ReadOwner | QFile::WriteOwner.
1225
1226 \sa creationPermissions()
1227 \sa addFile()
1228*/
1229void QZipWriter::setCreationPermissions(QFile::Permissions permissions)
1230{
1231 d->permissions = permissions;
1232}
1233
1234/*!
1235 Returns the currently set creation permissions.
1236
1237 \sa setCreationPermissions()
1238 \sa addFile()
1239*/
1240QFile::Permissions QZipWriter::creationPermissions() const
1241{
1242 return d->permissions;
1243}
1244
1245/*!
1246 Add a file to the archive with \a data as the file contents.
1247 The file will be stored in the archive using the \a fileName which
1248 includes the full path in the archive.
1249
1250 The new file will get the file permissions based on the current
1251 creationPermissions and it will be compressed using the zip compression
1252 based on the current compression policy.
1253
1254 \sa setCreationPermissions()
1255 \sa setCompressionPolicy()
1256*/
1257void QZipWriter::addFile(const QString &fileName, const QByteArray &data)
1258{
1259 d->addEntry(type: QZipWriterPrivate::File, fileName: QDir::fromNativeSeparators(pathName: fileName), contents: data);
1260}
1261
1262/*!
1263 Add a file to the archive with \a device as the source of the contents.
1264 The contents returned from QIODevice::readAll() will be used as the
1265 filedata.
1266 The file will be stored in the archive using the \a fileName which
1267 includes the full path in the archive.
1268*/
1269void QZipWriter::addFile(const QString &fileName, QIODevice *device)
1270{
1271 Q_ASSERT(device);
1272 QIODevice::OpenMode mode = device->openMode();
1273 bool opened = false;
1274 if ((mode & QIODevice::ReadOnly) == 0) {
1275 opened = true;
1276 if (! device->open(mode: QIODevice::ReadOnly)) {
1277 d->status = FileOpenError;
1278 return;
1279 }
1280 }
1281 d->addEntry(type: QZipWriterPrivate::File, fileName: QDir::fromNativeSeparators(pathName: fileName), contents: device->readAll());
1282 if (opened)
1283 device->close();
1284}
1285
1286/*!
1287 Create a new directory in the archive with the specified \a dirName and
1288 the \a permissions;
1289*/
1290void QZipWriter::addDirectory(const QString &dirName)
1291{
1292 QString name(QDir::fromNativeSeparators(pathName: dirName));
1293 // separator is mandatory
1294 if (!name.endsWith(c: u'/'))
1295 name.append(c: u'/');
1296 d->addEntry(type: QZipWriterPrivate::Directory, fileName: name, contents: QByteArray());
1297}
1298
1299/*!
1300 Create a new symbolic link in the archive with the specified \a dirName
1301 and the \a permissions;
1302 A symbolic link contains the destination (relative) path and name.
1303*/
1304void QZipWriter::addSymLink(const QString &fileName, const QString &destination)
1305{
1306 d->addEntry(type: QZipWriterPrivate::Symlink, fileName: QDir::fromNativeSeparators(pathName: fileName), contents: QFile::encodeName(fileName: destination));
1307}
1308
1309/*!
1310 Closes the zip file.
1311*/
1312void QZipWriter::close()
1313{
1314 if (!(d->device->openMode() & QIODevice::WriteOnly)) {
1315 d->device->close();
1316 return;
1317 }
1318
1319 //qDebug("QZip::close writing directory, %d entries", d->fileHeaders.size());
1320 d->device->seek(pos: d->start_of_directory);
1321 // write new directory
1322 for (int i = 0; i < d->fileHeaders.size(); ++i) {
1323 const FileHeader &header = d->fileHeaders.at(i);
1324 d->device->write(data: (const char *)&header.h, len: sizeof(CentralFileHeader));
1325 d->device->write(data: header.file_name);
1326 d->device->write(data: header.extra_field);
1327 d->device->write(data: header.file_comment);
1328 }
1329 int dir_size = d->device->pos() - d->start_of_directory;
1330 // write end of directory
1331 EndOfDirectory eod;
1332 memset(s: &eod, c: 0, n: sizeof(EndOfDirectory));
1333 writeUInt(data: eod.signature, i: 0x06054b50);
1334 //uchar this_disk[2];
1335 //uchar start_of_directory_disk[2];
1336 writeUShort(data: eod.num_dir_entries_this_disk, i: d->fileHeaders.size());
1337 writeUShort(data: eod.num_dir_entries, i: d->fileHeaders.size());
1338 writeUInt(data: eod.directory_size, i: dir_size);
1339 writeUInt(data: eod.dir_start_offset, i: d->start_of_directory);
1340 writeUShort(data: eod.comment_length, i: d->comment.size());
1341
1342 d->device->write(data: (const char *)&eod, len: sizeof(EndOfDirectory));
1343 d->device->write(data: d->comment);
1344 d->device->close();
1345}
1346
1347QT_END_NAMESPACE
1348

source code of qtbase/src/corelib/io/qzip.cpp