1 /*
2     SPDX-FileCopyrightText: 2017 Ragnar Thomsen <rthomsen6@gmail.com>
3 
4     SPDX-License-Identifier: BSD-2-Clause
5 */
6 
7 #include "libzipplugin.h"
8 #include "../config.h"
9 #include "ark_debug.h"
10 #include "queries.h"
11 
12 #include <KIO/Global>
13 #include <KLocalizedString>
14 #include <KPluginFactory>
15 
16 #include <QDataStream>
17 #include <QDateTime>
18 #include <QDir>
19 #include <QDirIterator>
20 #include <QFile>
21 #include <qplatformdefs.h>
22 #include <QThread>
23 
24 #include <utime.h>
25 #include <zlib.h>
26 #include <memory>
27 
28 K_PLUGIN_CLASS_WITH_JSON(LibzipPlugin, "kerfuffle_libzip.json")
29 
30 template <auto fn>
31 using deleter_from_fn = std::integral_constant<decltype(fn), fn>;
32 template <typename T, auto fn>
33 using ark_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
34 
progressCallback(zip_t *,double progress,void * that)35 void LibzipPlugin::progressCallback(zip_t *, double progress, void *that)
36 {
37     static_cast<LibzipPlugin *>(that)->emitProgress(progress);
38 }
39 
cancelCallback(zip_t *,void *)40 int LibzipPlugin::cancelCallback(zip_t *, void * /* unused that*/)
41 {
42     return QThread::currentThread()->isInterruptionRequested();
43 }
44 
LibzipPlugin(QObject * parent,const QVariantList & args)45 LibzipPlugin::LibzipPlugin(QObject *parent, const QVariantList & args)
46     : ReadWriteArchiveInterface(parent, args)
47     , m_overwriteAll(false)
48     , m_skipAll(false)
49     , m_listAfterAdd(false)
50     , m_backslashedZip(false)
51 {
52     qCDebug(ARK) << "Initializing libzip plugin";
53 }
54 
~LibzipPlugin()55 LibzipPlugin::~LibzipPlugin()
56 {
57     for (const auto e : std::as_const(m_emittedEntries)) {
58         // Entries might be passed to pending slots, so we just schedule their deletion.
59         e->deleteLater();
60     }
61 }
62 
list()63 bool LibzipPlugin::list()
64 {
65     qCDebug(ARK) << "Listing archive contents for:" << QFile::encodeName(filename());
66     m_numberOfEntries = 0;
67 
68     int errcode = 0;
69     zip_error_t err;
70 
71     // Open archive.
72     ark_unique_ptr<zip_t, zip_discard> archive { zip_open(QFile::encodeName(filename()).constData(), ZIP_RDONLY, &errcode) };
73     zip_error_init_with_code(&err, errcode);
74     if (!archive) {
75         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
76         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
77         return false;
78     }
79 
80     // Fetch archive comment.
81     m_comment = QString::fromLocal8Bit(zip_get_archive_comment(archive.get(), nullptr, ZIP_FL_ENC_RAW));
82 
83     // Get number of archive entries.
84     const auto nofEntries = zip_get_num_entries(archive.get(), 0);
85     qCDebug(ARK) << "Found entries:" << nofEntries;
86 
87     // Loop through all archive entries.
88     for (int i = 0; i < nofEntries; i++) {
89 
90         if (QThread::currentThread()->isInterruptionRequested()) {
91             break;
92         }
93 
94         emitEntryForIndex(archive.get(), i);
95         if (m_listAfterAdd) {
96             // Start at 50%.
97             Q_EMIT progress(0.5 + (0.5 * float(i + 1) / nofEntries));
98         } else {
99             Q_EMIT progress(float(i + 1) / nofEntries);
100         }
101     }
102 
103     m_listAfterAdd = false;
104     return true;
105 }
106 
addFiles(const QVector<Archive::Entry * > & files,const Archive::Entry * destination,const CompressionOptions & options,uint numberOfEntriesToAdd)107 bool LibzipPlugin::addFiles(const QVector<Archive::Entry*> &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd)
108 {
109     Q_UNUSED(numberOfEntriesToAdd)
110     int errcode = 0;
111     zip_error_t err;
112 
113     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
114     ark_unique_ptr<zip_t, zip_discard> archive { zip_open(QFile::encodeName(filename()).constData(), ZIP_CREATE, &errcode) };
115     zip_error_init_with_code(&err, errcode);
116     if (!archive) {
117         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
118         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
119         return false;
120     }
121 
122     uint i = 0;
123     for (const Archive::Entry* e : files) {
124 
125         if (QThread::currentThread()->isInterruptionRequested()) {
126             break;
127         }
128 
129         // If entry is a directory, traverse and add all its files and subfolders.
130         if (QFileInfo(e->fullPath()).isDir()) {
131 
132             if (!writeEntry(archive.get(), e->fullPath(), destination, options, true)) {
133                 return false;
134             }
135 
136             QDirIterator it(e->fullPath(),
137                             QDir::AllEntries | QDir::Readable |
138                             QDir::Hidden | QDir::NoDotAndDotDot,
139                             QDirIterator::Subdirectories);
140 
141             while (!QThread::currentThread()->isInterruptionRequested() && it.hasNext()) {
142                 const QString path = it.next();
143 
144                 if (QFileInfo(path).isDir()) {
145                     if (!writeEntry(archive.get(), path, destination, options, true)) {
146                         return false;
147                     }
148                 } else {
149                     if (!writeEntry(archive.get(), path, destination, options)) {
150                         return false;
151                     }
152                 }
153                 i++;
154             }
155         } else {
156             if (!writeEntry(archive.get(), e->fullPath(), destination, options)) {
157                 return false;
158             }
159         }
160         i++;
161     }
162     qCDebug(ARK) << "Writing " << i << "entries to disk...";
163 
164     // Register the callback function to get progress feedback and cancelation.
165     zip_register_progress_callback_with_state(archive.get(), 0.001, progressCallback, nullptr, this);
166 #ifdef LIBZIP_CANCELATION
167     zip_register_cancel_callback_with_state(archive.get(), cancelCallback, nullptr, this);
168 #endif
169 
170     // Write and close archive manually.
171     zip_close(archive.get());
172     // Release unique pointer as it set to NULL via zip_close.
173     archive.release();
174     if (errcode > 0) {
175         qCCritical(ARK) << "Failed to write archive";
176         Q_EMIT error(xi18n("Failed to write archive."));
177         return false;
178     }
179 
180     if (QThread::currentThread()->isInterruptionRequested()) {
181         return false;
182     }
183 
184     // We list the entire archive after adding files to ensure entry
185     // properties are up-to-date.
186     m_listAfterAdd = true;
187     list();
188 
189     return true;
190 }
191 
emitProgress(double percentage)192 void LibzipPlugin::emitProgress(double percentage)
193 {
194     // Go from 0 to 50%. The second half is the subsequent listing.
195     Q_EMIT progress(0.5 * percentage);
196 }
197 
writeEntry(zip_t * archive,const QString & file,const Archive::Entry * destination,const CompressionOptions & options,bool isDir)198 bool LibzipPlugin::writeEntry(zip_t *archive, const QString &file, const Archive::Entry* destination, const CompressionOptions& options, bool isDir)
199 {
200     Q_ASSERT(archive);
201 
202     QByteArray destFile;
203     if (destination) {
204         destFile = fromUnixSeparator(QString(destination->fullPath() + file)).toUtf8();
205     } else {
206         destFile = fromUnixSeparator(file).toUtf8();
207     }
208 
209     qlonglong index;
210     if (isDir) {
211         index = zip_dir_add(archive, destFile.constData(), ZIP_FL_ENC_GUESS);
212         if (index == -1) {
213             // If directory already exists in archive, we get an error.
214             qCWarning(ARK) << "Failed to add dir " << file << ":" << zip_strerror(archive);
215             return true;
216         }
217     } else {
218         zip_source_t *src = zip_source_file(archive, QFile::encodeName(file).constData(), 0, -1);
219         Q_ASSERT(src);
220 
221         index = zip_file_add(archive, destFile.constData(), src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE);
222         if (index == -1) {
223             zip_source_free(src);
224             qCCritical(ARK) << "Could not add entry" << file << ":" << zip_strerror(archive);
225             Q_EMIT error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive))));
226             return false;
227         }
228     }
229 
230 #ifndef Q_OS_WIN
231     // Set permissions.
232     QT_STATBUF result;
233     if (QT_STAT(QFile::encodeName(file).constData(), &result) != 0) {
234         qCWarning(ARK) << "Failed to read permissions for:" << file;
235     } else {
236         zip_uint32_t attributes = result.st_mode << 16;
237         if (zip_file_set_external_attributes(archive, index, ZIP_FL_UNCHANGED, ZIP_OPSYS_UNIX, attributes) != 0) {
238             qCWarning(ARK) << "Failed to set external attributes for:" << file;
239         }
240     }
241 #endif
242 
243     if (!password().isEmpty()) {
244         Q_ASSERT(!options.encryptionMethod().isEmpty());
245         if (options.encryptionMethod() == QLatin1String("AES128")) {
246             zip_file_set_encryption(archive, index, ZIP_EM_AES_128, password().toUtf8().constData());
247         } else if (options.encryptionMethod() == QLatin1String("AES192")) {
248             zip_file_set_encryption(archive, index, ZIP_EM_AES_192, password().toUtf8().constData());
249         } else if (options.encryptionMethod() == QLatin1String("AES256")) {
250             zip_file_set_encryption(archive, index, ZIP_EM_AES_256, password().toUtf8().constData());
251         }
252     }
253 
254     // Set compression level and method.
255     zip_int32_t compMethod = ZIP_CM_DEFAULT;
256     if (!options.compressionMethod().isEmpty()) {
257         if (options.compressionMethod() == QLatin1String("Deflate")) {
258             compMethod = ZIP_CM_DEFLATE;
259         } else if (options.compressionMethod() == QLatin1String("BZip2")) {
260             compMethod = ZIP_CM_BZIP2;
261 #ifdef ZIP_CM_ZSTD
262         } else if (options.compressionMethod() == QLatin1String("Zstd")) {
263             compMethod = ZIP_CM_ZSTD;
264 #endif
265 #ifdef ZIP_CM_LZMA
266         } else if (options.compressionMethod() == QLatin1String("LZMA")) {
267             compMethod = ZIP_CM_LZMA;
268 #endif
269 #ifdef ZIP_CM_XZ
270         } else if (options.compressionMethod() == QLatin1String("XZ")) {
271             compMethod = ZIP_CM_XZ;
272 #endif
273         } else if (options.compressionMethod() == QLatin1String("Store")) {
274             compMethod = ZIP_CM_STORE;
275         }
276     }
277     const int compLevel = options.isCompressionLevelSet() ? options.compressionLevel() : 6;
278     if (zip_set_file_compression(archive, index, compMethod, compLevel) != 0) {
279         qCCritical(ARK) << "Could not set compression options for" << file << ":" << zip_strerror(archive);
280         Q_EMIT error(xi18n("Failed to set compression options for entry: %1", QString::fromUtf8(zip_strerror(archive))));
281         return false;
282     }
283 
284     return true;
285 }
286 
emitEntryForIndex(zip_t * archive,qlonglong index)287 bool LibzipPlugin::emitEntryForIndex(zip_t *archive, qlonglong index)
288 {
289     Q_ASSERT(archive);
290 
291     zip_stat_t statBuffer;
292     if (zip_stat_index(archive, index, ZIP_FL_ENC_GUESS, &statBuffer)) {
293         qCCritical(ARK) << "Failed to read stat for index" << index;
294         return false;
295     }
296 
297     auto e = new Archive::Entry();
298     auto name = toUnixSeparator(QString::fromUtf8(statBuffer.name));
299 
300     if (statBuffer.valid & ZIP_STAT_NAME) {
301         e->setFullPath(name);
302     }
303 
304     if (e->fullPath(PathFormat::WithTrailingSlash).endsWith(QDir::separator())) {
305         e->setProperty("isDirectory", true);
306     }
307 
308     if (statBuffer.valid & ZIP_STAT_MTIME) {
309         e->setProperty("timestamp", QDateTime::fromSecsSinceEpoch(statBuffer.mtime));
310     }
311     if (statBuffer.valid & ZIP_STAT_SIZE) {
312         e->setProperty("size", (qulonglong)statBuffer.size);
313     }
314     if (statBuffer.valid & ZIP_STAT_COMP_SIZE) {
315         e->setProperty("compressedSize", (qlonglong)statBuffer.comp_size);
316     }
317     if (statBuffer.valid & ZIP_STAT_CRC) {
318         if (!e->isDir()) {
319             e->setProperty("CRC", QString::number((qulonglong)statBuffer.crc, 16).toUpper());
320         }
321     }
322     if (statBuffer.valid & ZIP_STAT_COMP_METHOD) {
323         switch(statBuffer.comp_method) {
324             case ZIP_CM_STORE:
325                 e->setProperty("method", QStringLiteral("Store"));
326                 Q_EMIT compressionMethodFound(QStringLiteral("Store"));
327                 break;
328             case ZIP_CM_DEFLATE:
329                 e->setProperty("method", QStringLiteral("Deflate"));
330                 Q_EMIT compressionMethodFound(QStringLiteral("Deflate"));
331                 break;
332             case ZIP_CM_DEFLATE64:
333                 e->setProperty("method", QStringLiteral("Deflate64"));
334                 Q_EMIT compressionMethodFound(QStringLiteral("Deflate64"));
335                 break;
336             case ZIP_CM_BZIP2:
337                 e->setProperty("method", QStringLiteral("BZip2"));
338                 Q_EMIT compressionMethodFound(QStringLiteral("BZip2"));
339                 break;
340 #ifdef ZIP_CM_ZSTD
341             case ZIP_CM_ZSTD:
342                 e->setProperty("method", QStringLiteral("Zstd"));
343                 Q_EMIT compressionMethodFound(QStringLiteral("Zstd"));
344                 break;
345 #endif
346 #ifdef ZIP_CM_LZMA
347             case ZIP_CM_LZMA:
348                 e->setProperty("method", QStringLiteral("LZMA"));
349                 Q_EMIT compressionMethodFound(QStringLiteral("LZMA"));
350                 break;
351 #endif
352 #ifdef ZIP_CM_XZ
353             case ZIP_CM_XZ:
354                 e->setProperty("method", QStringLiteral("XZ"));
355                 Q_EMIT compressionMethodFound(QStringLiteral("XZ"));
356                 break;
357 #endif
358         }
359     }
360     if (statBuffer.valid & ZIP_STAT_ENCRYPTION_METHOD) {
361         if (statBuffer.encryption_method != ZIP_EM_NONE) {
362             e->setProperty("isPasswordProtected", true);
363             switch(statBuffer.encryption_method) {
364                 case ZIP_EM_TRAD_PKWARE:
365                     Q_EMIT encryptionMethodFound(QStringLiteral("ZipCrypto"));
366                     break;
367                 case ZIP_EM_AES_128:
368                     Q_EMIT encryptionMethodFound(QStringLiteral("AES128"));
369                     break;
370                 case ZIP_EM_AES_192:
371                     Q_EMIT encryptionMethodFound(QStringLiteral("AES192"));
372                     break;
373                 case ZIP_EM_AES_256:
374                     Q_EMIT encryptionMethodFound(QStringLiteral("AES256"));
375                     break;
376             }
377         }
378     }
379 
380     // Read external attributes, which contains the file permissions.
381     zip_uint8_t opsys;
382     zip_uint32_t attributes;
383     if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) {
384         qCCritical(ARK) << "Could not read external attributes for entry:" << name;
385         Q_EMIT error(xi18n("Failed to read metadata for entry: %1", name));
386         return false;
387     }
388 
389     // Set permissions.
390     switch (opsys) {
391     case ZIP_OPSYS_UNIX:
392         // Unix permissions are stored in the leftmost 16 bits of the external file attribute.
393         e->setProperty("permissions", permissionsToString(attributes >> 16));
394         break;
395     default:    // TODO: non-UNIX.
396         break;
397     }
398 
399     Q_EMIT entry(e);
400     m_emittedEntries << e;
401 
402     return true;
403 }
404 
deleteFiles(const QVector<Archive::Entry * > & files)405 bool LibzipPlugin::deleteFiles(const QVector<Archive::Entry*> &files)
406 {
407     int errcode = 0;
408     zip_error_t err;
409 
410     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
411     ark_unique_ptr<zip_t, zip_discard> archive { zip_open(QFile::encodeName(filename()).constData(), 0, &errcode) };
412     zip_error_init_with_code(&err, errcode);
413     if (archive.get() == nullptr) {
414         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
415         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
416         return false;
417     }
418 
419     qulonglong i = 0;
420     for (const Archive::Entry* e : files) {
421 
422         if (QThread::currentThread()->isInterruptionRequested()) {
423             break;
424         }
425 
426         const qlonglong index = zip_name_locate(archive.get(), fromUnixSeparator(e->fullPath()).toUtf8().constData(), ZIP_FL_ENC_GUESS);
427         if (index == -1) {
428             qCCritical(ARK) << "Could not find entry to delete:" << e->fullPath();
429             Q_EMIT error(xi18n("Failed to delete entry: %1", e->fullPath()));
430             return false;
431         }
432         if (zip_delete(archive.get(), index) == -1) {
433             qCCritical(ARK) << "Could not delete entry" << e->fullPath() << ":" << zip_strerror(archive.get());
434             Q_EMIT error(xi18n("Failed to delete entry: %1", QString::fromUtf8(zip_strerror(archive.get()))));
435             return false;
436         }
437         Q_EMIT entryRemoved(e->fullPath());
438         Q_EMIT progress(float(++i) / files.size());
439     }
440     qCDebug(ARK) << "Deleted" << i << "entries";
441 
442     // Write and close archive manually.
443     zip_close(archive.get());
444     // Release unique pointer as it set to NULL via zip_close.
445     archive.release();
446     if (errcode > 0) {
447         qCCritical(ARK) << "Failed to write archive";
448         Q_EMIT error(xi18n("Failed to write archive."));
449         return false;
450     }
451     return true;
452 }
453 
addComment(const QString & comment)454 bool LibzipPlugin::addComment(const QString& comment)
455 {
456     int errcode = 0;
457     zip_error_t err;
458 
459     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
460     ark_unique_ptr<zip_t, zip_discard> archive { zip_open(QFile::encodeName(filename()).constData(), 0, &errcode) };
461     zip_error_init_with_code(&err, errcode);
462     if (archive.get() == nullptr) {
463         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
464         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
465         return false;
466     }
467 
468     // Set archive comment.
469     if (zip_set_archive_comment(archive.get(), comment.toUtf8().constData(), comment.length())) {
470         qCCritical(ARK) << "Failed to set comment:" << zip_strerror(archive.get());
471         return false;
472     }
473 
474     // Write comment to archive.
475     zip_close(archive.get());
476     // Release unique pointer as it set to NULL via zip_close.
477     archive.release();
478     if (errcode > 0) {
479         qCCritical(ARK) << "Failed to write archive";
480         Q_EMIT error(xi18n("Failed to write archive."));
481         return false;
482     }
483     return true;
484 }
485 
testArchive()486 bool LibzipPlugin::testArchive()
487 {
488     qCDebug(ARK) << "Testing archive";
489     int errcode = 0;
490     zip_error_t err;
491 
492     // Open archive performing extra consistency checks, free memory using zip_discard as no write oprations needed.
493     ark_unique_ptr<zip_t, zip_discard> archive { zip_open(QFile::encodeName(filename()).constData(), ZIP_CHECKCONS, &errcode) };
494     zip_error_init_with_code(&err, errcode);
495     if (archive == nullptr) {
496         qCCritical(ARK) << "Failed to open archive:" << zip_error_strerror(&err);
497         return false;
498     }
499 
500     // Check CRC-32 for each archive entry.
501     const int nofEntries = zip_get_num_entries(archive.get(), 0);
502     for (int i = 0; i < nofEntries; i++) {
503 
504         if (QThread::currentThread()->isInterruptionRequested()) {
505             return false;
506         }
507 
508         // Get statistic for entry. Used to get entry size.
509         zip_stat_t statBuffer;
510         int stat_index = zip_stat_index(archive.get(), i, 0, &statBuffer);
511         auto name = toUnixSeparator(QString::fromUtf8(statBuffer.name));
512         if (stat_index != 0) {
513             qCCritical(ARK) << "Failed to read stat for" << name;
514             return false;
515         }
516 
517         ark_unique_ptr<zip_file, zip_fclose> zipFile { zip_fopen_index(archive.get(), i, 0) };
518         std::unique_ptr<uchar[]> buf(new uchar[statBuffer.size]);
519         const int len = zip_fread(zipFile.get(), buf.get(), statBuffer.size);
520         if (len == -1 || uint(len) != statBuffer.size) {
521             qCCritical(ARK) << "Failed to read data for" << name;
522             return false;
523         }
524         if (statBuffer.crc != crc32(0, &buf.get()[0], len)) {
525             qCCritical(ARK) << "CRC check failed for" << name;
526             return false;
527         }
528 
529         Q_EMIT progress(float(i) / nofEntries);
530     }
531 
532     Q_EMIT testSuccess();
533     return true;
534 }
535 
doKill()536 bool LibzipPlugin::doKill()
537 {
538     return false;
539 }
540 
extractFiles(const QVector<Archive::Entry * > & files,const QString & destinationDirectory,const ExtractionOptions & options)541 bool LibzipPlugin::extractFiles(const QVector<Archive::Entry*> &files, const QString& destinationDirectory, const ExtractionOptions& options)
542 {
543     qCDebug(ARK) << "Extracting files to:" << destinationDirectory;
544     const bool extractAll = files.isEmpty();
545     const bool removeRootNode = options.isDragAndDropEnabled();
546 
547     int errcode = 0;
548     zip_error_t err;
549 
550     // Open archive, free memory using zip_discard as no write oprations needed.
551     ark_unique_ptr<zip_t, zip_discard> archive { zip_open(QFile::encodeName(filename()).constData(), ZIP_RDONLY, &errcode) };
552     zip_error_init_with_code(&err, errcode);
553     if (archive == nullptr) {
554         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
555         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
556         return false;
557     }
558 
559     // Set password if known.
560     if (!password().isEmpty()) {
561         qCDebug(ARK) << "Password already known. Setting...";
562         zip_set_default_password(archive.get(), password().toUtf8().constData());
563     }
564 
565     // Get number of archive entries.
566     const qlonglong nofEntries = extractAll ? zip_get_num_entries(archive.get(), 0) : files.size();
567 
568     // Extract entries.
569     m_overwriteAll = false; // Whether to overwrite all files
570     m_skipAll = false; // Whether to skip all files
571     if (extractAll) {
572         // We extract all entries.
573         for (qlonglong i = 0; i < nofEntries; i++) {
574             if (QThread::currentThread()->isInterruptionRequested()) {
575                 break;
576             }
577             if (!extractEntry(archive.get(),
578                               toUnixSeparator(QString::fromUtf8(zip_get_name(archive.get(), i, ZIP_FL_ENC_GUESS))),
579                               QString(),
580                               destinationDirectory,
581                               options.preservePaths(),
582                               removeRootNode)) {
583                 qCDebug(ARK) << "Extraction failed";
584                 return false;
585             }
586             Q_EMIT progress(float(i + 1) / nofEntries);
587         }
588     } else {
589         // We extract only the entries in files.
590         qulonglong i = 0;
591         for (const Archive::Entry* e : files) {
592             if (QThread::currentThread()->isInterruptionRequested()) {
593                 break;
594             }
595             if (!extractEntry(archive.get(),
596                               e->fullPath(),
597                               e->rootNode,
598                               destinationDirectory,
599                               options.preservePaths(),
600                               removeRootNode)) {
601                 qCDebug(ARK) << "Extraction failed";
602                 return false;
603             }
604             Q_EMIT progress(float(++i) / nofEntries);
605         }
606     }
607 
608     return true;
609 }
610 
extractEntry(zip_t * archive,const QString & entry,const QString & rootNode,const QString & destDir,bool preservePaths,bool removeRootNode)611 bool LibzipPlugin::extractEntry(zip_t *archive, const QString &entry, const QString &rootNode, const QString &destDir, bool preservePaths, bool removeRootNode)
612 {
613     const bool isDirectory = entry.endsWith(QDir::separator());
614 
615     // Add trailing slash to destDir if not present.
616     QString destDirCorrected(destDir);
617     if (!destDir.endsWith(QDir::separator())) {
618         destDirCorrected.append(QDir::separator());
619     }
620 
621     // Remove rootnode if supplied and set destination path.
622     QString destination;
623     if (preservePaths) {
624         if (!removeRootNode || rootNode.isEmpty()) {
625             destination = destDirCorrected + entry;
626         } else {
627             QString truncatedEntry = entry;
628             truncatedEntry.remove(0, rootNode.size());
629             destination = destDirCorrected + truncatedEntry;
630         }
631     } else {
632         if (isDirectory) {
633             qCDebug(ARK) << "Skipping directory:" << entry;
634             return true;
635         }
636         destination = destDirCorrected + QFileInfo(entry).fileName();
637     }
638 
639     // Store parent mtime.
640     QString parentDir;
641     if (isDirectory) {
642         QDir pDir = QFileInfo(destination).dir();
643         pDir.cdUp();
644         parentDir = pDir.path();
645     } else {
646         parentDir = QFileInfo(destination).path();
647     }
648     // For top-level items, don't restore parent dir mtime.
649     const bool restoreParentMtime = (parentDir + QDir::separator() != destDirCorrected);
650 
651     time_t parent_mtime;
652     if (restoreParentMtime) {
653         parent_mtime = QFileInfo(parentDir).lastModified().toMSecsSinceEpoch() / 1000;
654     }
655 
656     // Create parent directories for files. For directories create them.
657     if (!QDir().mkpath(QFileInfo(destination).path())) {
658         qCDebug(ARK) << "Failed to create directory:" << QFileInfo(destination).path();
659         Q_EMIT error(xi18n("Failed to create directory: %1", QFileInfo(destination).path()));
660         return false;
661     }
662 
663     // Get statistic for entry. Used to get entry size and mtime.
664     zip_stat_t statBuffer;
665     if (zip_stat(archive, fromUnixSeparator(entry).toUtf8().constData(), 0, &statBuffer) != 0) {
666         if (isDirectory && zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOENT) {
667             qCWarning(ARK) << "Skipping folder without entry:" << entry;
668             return true;
669         }
670         qCCritical(ARK) << "Failed to read stat for entry" << entry;
671         return false;
672     }
673 
674     if (!isDirectory) {
675 
676         // Handle existing destination files.
677         QString renamedEntry = entry;
678         while (!m_overwriteAll && QFileInfo::exists(destination)) {
679             if (m_skipAll) {
680                 return true;
681             } else {
682                 Kerfuffle::OverwriteQuery query(renamedEntry);
683                 Q_EMIT userQuery(&query);
684                 query.waitForResponse();
685 
686                 if (query.responseCancelled()) {
687                     Q_EMIT cancelled();
688                     return false;
689                 } else if (query.responseSkip()) {
690                     return true;
691                 } else if (query.responseAutoSkip()) {
692                     m_skipAll = true;
693                     return true;
694                 } else if (query.responseRename()) {
695                     const QString newName(query.newFilename());
696                     destination = QFileInfo(destination).path() + QDir::separator() + QFileInfo(newName).fileName();
697                     renamedEntry = QFileInfo(entry).path() + QDir::separator() + QFileInfo(newName).fileName();
698                 } else if (query.responseOverwriteAll()) {
699                     m_overwriteAll = true;
700                     break;
701                 } else if (query.responseOverwrite()) {
702                     break;
703                 }
704             }
705         }
706 
707         // Handle password-protected files.
708         ark_unique_ptr<zip_file, zip_fclose> zipFile { nullptr };
709         bool firstTry = true;
710         while (!zipFile) {
711             zipFile.reset(zip_fopen(archive, fromUnixSeparator(entry).toUtf8().constData(), 0));
712             if (zipFile) {
713                 break;
714             } else if (zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOPASSWD ||
715                        zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_WRONGPASSWD) {
716                 Kerfuffle::PasswordNeededQuery query(filename(), !firstTry);
717                 Q_EMIT userQuery(&query);
718                 query.waitForResponse();
719 
720                 if (query.responseCancelled()) {
721                     Q_EMIT cancelled();
722                     return false;
723                 }
724                 setPassword(query.password());
725 
726                 if (zip_set_default_password(archive, password().toUtf8().constData())) {
727                     qCDebug(ARK) << "Failed to set password for:" << entry;
728                 }
729                 firstTry = false;
730             } else {
731                 qCCritical(ARK) << "Failed to open file:" << zip_strerror(archive);
732                 Q_EMIT error(xi18n("Failed to open '%1':<nl/>%2", entry, QString::fromUtf8(zip_strerror(archive))));
733                 return false;
734             }
735         }
736 
737         QFile file(destination);
738         if (!file.open(QIODevice::WriteOnly)) {
739             qCCritical(ARK) << "Failed to open file for writing";
740             Q_EMIT error(xi18n("Failed to open file for writing: %1", destination));
741             return false;
742         }
743 
744         QDataStream out(&file);
745 
746         // Write archive entry to file. We use a read/write buffer of 1000 chars.
747         qulonglong sum = 0;
748         char buf[1000];
749         while (sum != statBuffer.size) {
750             const auto readBytes = zip_fread(zipFile.get(), buf, 1000);
751             if (readBytes < 0) {
752                 qCCritical(ARK) << "Failed to read data";
753                 Q_EMIT error(xi18n("Failed to read data for entry: %1", entry));
754                 return false;
755             }
756             if (out.writeRawData(buf, readBytes) != readBytes) {
757                 qCCritical(ARK) << "Failed to write data";
758                 Q_EMIT error(xi18n("Failed to write data for entry: %1", entry));
759                 return false;
760             }
761 
762             sum += readBytes;
763         }
764 
765         const auto index = zip_name_locate(archive, fromUnixSeparator(entry).toUtf8().constData(), ZIP_FL_ENC_GUESS);
766         if (index == -1) {
767             qCCritical(ARK) << "Could not locate entry:" << entry;
768             Q_EMIT error(xi18n("Failed to locate entry: %1", entry));
769             return false;
770         }
771 
772         zip_uint8_t opsys;
773         zip_uint32_t attributes;
774         if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) {
775             qCCritical(ARK) << "Could not read external attributes for entry:" << entry;
776             Q_EMIT error(xi18n("Failed to read metadata for entry: %1", entry));
777             return false;
778         }
779 
780         // Inspired by fuse-zip source code: fuse-zip/lib/fileNode.cpp
781         switch (opsys) {
782         case ZIP_OPSYS_UNIX:
783             // Unix permissions are stored in the leftmost 16 bits of the external file attribute.
784             file.setPermissions(KIO::convertPermissions(attributes >> 16));
785             break;
786         default:    // TODO: non-UNIX.
787             break;
788         }
789 
790         file.close();
791     }
792 
793     // Set mtime for entry (also access time otherwise it's "uninitilized")
794     utimbuf times;
795     times.actime = statBuffer.mtime;
796     times.modtime = statBuffer.mtime;
797     if (utime(destination.toUtf8().constData(), &times) != 0) {
798         qCWarning(ARK) << "Failed to restore mtime:" << destination;
799     }
800 
801     if (restoreParentMtime) {
802         // Restore mtime for parent dir.
803         times.actime = parent_mtime;
804         times.modtime = parent_mtime;
805         if (utime(parentDir.toUtf8().constData(), &times) != 0) {
806             qCWarning(ARK) << "Failed to restore mtime for parent dir of:" << destination;
807         }
808     }
809 
810     return true;
811 }
812 
moveFiles(const QVector<Archive::Entry * > & files,Archive::Entry * destination,const CompressionOptions & options)813 bool LibzipPlugin::moveFiles(const QVector<Archive::Entry*> &files, Archive::Entry *destination, const CompressionOptions &options)
814 {
815     Q_UNUSED(options)
816     int errcode = 0;
817     zip_error_t err;
818 
819     // Open archive.
820     ark_unique_ptr<zip_t, zip_close> archive { zip_open(QFile::encodeName(filename()).constData(), 0, &errcode) };
821     zip_error_init_with_code(&err, errcode);
822     if (archive.get() == nullptr) {
823         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
824         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
825         return false;
826     }
827 
828     QStringList filePaths = entryFullPaths(files);
829     filePaths.sort();
830     const QStringList destPaths = entryPathsFromDestination(filePaths, destination, entriesWithoutChildren(files).count());
831 
832     int i;
833     for (i = 0; i < filePaths.size(); ++i) {
834 
835         const int index = zip_name_locate(archive.get(), filePaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS);
836         if (index == -1) {
837             qCCritical(ARK) << "Could not find entry to move:" << filePaths.at(i);
838             Q_EMIT error(xi18n("Failed to move entry: %1", filePaths.at(i)));
839             return false;
840         }
841 
842         if (zip_file_rename(archive.get(), index, destPaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS) == -1) {
843             qCCritical(ARK) << "Could not move entry:" << filePaths.at(i);
844             Q_EMIT error(xi18n("Failed to move entry: %1", filePaths.at(i)));
845             return false;
846         }
847 
848         Q_EMIT entryRemoved(filePaths.at(i));
849         emitEntryForIndex(archive.get(), index);
850         Q_EMIT progress(i/filePaths.count());
851     }
852 
853     // Write and close archive manually.
854     zip_close(archive.get());
855     // Release unique pointer as it set to NULL via zip_close.
856     archive.release();
857     if (errcode > 0) {
858         qCCritical(ARK) << "Failed to write archive";
859         Q_EMIT error(xi18n("Failed to write archive."));
860         return false;
861     }
862 
863     qCDebug(ARK) << "Moved" << i << "entries";
864 
865     return true;
866 }
867 
copyFiles(const QVector<Archive::Entry * > & files,Archive::Entry * destination,const CompressionOptions & options)868 bool LibzipPlugin::copyFiles(const QVector<Archive::Entry*> &files, Archive::Entry *destination, const CompressionOptions &options)
869 {
870     Q_UNUSED(options)
871     int errcode = 0;
872     zip_error_t err;
873 
874     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
875     ark_unique_ptr<zip_t, zip_discard> archive { zip_open(QFile::encodeName(filename()).constData(), 0, &errcode) };
876     zip_error_init_with_code(&err, errcode);
877     if (archive.get() == nullptr) {
878         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
879         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
880         return false;
881     }
882 
883     const QStringList filePaths = entryFullPaths(files);
884     const QStringList destPaths = entryPathsFromDestination(filePaths, destination, 0);
885 
886     int i;
887     for (i = 0; i < filePaths.size(); ++i) {
888 
889         QString dest = destPaths.at(i);
890 
891         if (dest.endsWith(QDir::separator())) {
892             if (zip_dir_add(archive.get(), dest.toUtf8().constData(), ZIP_FL_ENC_GUESS) == -1) {
893                 // If directory already exists in archive, we get an error.
894                 qCWarning(ARK) << "Failed to add dir " << dest << ":" << zip_strerror(archive.get());
895                 continue;
896             }
897         }
898 
899         const int srcIndex = zip_name_locate(archive.get(), filePaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS);
900         if (srcIndex == -1) {
901             qCCritical(ARK) << "Could not find entry to copy:" << filePaths.at(i);
902             Q_EMIT error(xi18n("Failed to copy entry: %1", filePaths.at(i)));
903             return false;
904         }
905 
906         zip_source_t *src = zip_source_zip(archive.get(), archive.get(), srcIndex, 0, 0, -1);
907         if (!src) {
908             qCCritical(ARK) << "Failed to create source for:" << filePaths.at(i);
909             return false;
910         }
911 
912         const int destIndex = zip_file_add(archive.get(), dest.toUtf8().constData(), src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE);
913         if (destIndex == -1) {
914             zip_source_free(src);
915             qCCritical(ARK) << "Could not add entry" << dest << ":" << zip_strerror(archive.get());
916             Q_EMIT error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive.get()))));
917             return false;
918         }
919 
920         // Get permissions from source entry.
921         zip_uint8_t opsys;
922         zip_uint32_t attributes;
923         if (zip_file_get_external_attributes(archive.get(), srcIndex, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) {
924             qCCritical(ARK) << "Failed to read external attributes for source:" << filePaths.at(i);
925             Q_EMIT error(xi18n("Failed to read metadata for entry: %1", filePaths.at(i)));
926             return false;
927         }
928 
929         // Set permissions on dest entry.
930         if (zip_file_set_external_attributes(archive.get(), destIndex, ZIP_FL_UNCHANGED, opsys, attributes) != 0) {
931             qCCritical(ARK) << "Failed to set external attributes for destination:" << dest;
932             Q_EMIT error(xi18n("Failed to set metadata for entry: %1", dest));
933             return false;
934         }
935     }
936 
937     // Register the callback function to get progress feedback and cancelation.
938     zip_register_progress_callback_with_state(archive.get(), 0.001, progressCallback, nullptr, this);
939 #ifdef LIBZIP_CANCELATION
940     zip_register_cancel_callback_with_state(archive.get(), cancelCallback, nullptr, this);
941 #endif
942 
943     // Write and close archive manually before using list() function.
944     zip_close(archive.get());
945     // Release unique pointer as it set to NULL via zip_close.
946     archive.release();
947     if (errcode > 0) {
948         qCCritical(ARK) << "Failed to write archive";
949         Q_EMIT error(xi18n("Failed to write archive."));
950         return false;
951     }
952 
953     if (QThread::currentThread()->isInterruptionRequested()) {
954         return false;
955     }
956 
957     // List the archive to update the model.
958     m_listAfterAdd = true;
959     list();
960 
961     qCDebug(ARK) << "Copied" << i << "entries";
962 
963     return true;
964 }
965 
permissionsToString(mode_t perm)966 QString LibzipPlugin::permissionsToString(mode_t perm)
967 {
968     QString modeval;
969     if ((perm & S_IFMT) == S_IFDIR) {
970         modeval.append(QLatin1Char('d'));
971     } else if ((perm & S_IFMT) == S_IFLNK) {
972         modeval.append(QLatin1Char('l'));
973     } else {
974         modeval.append(QLatin1Char('-'));
975     }
976     modeval.append((perm & S_IRUSR) ? QLatin1Char('r') : QLatin1Char('-'));
977     modeval.append((perm & S_IWUSR) ? QLatin1Char('w') : QLatin1Char('-'));
978     if ((perm & S_ISUID) && (perm & S_IXUSR)) {
979         modeval.append(QLatin1Char('s'));
980     } else if ((perm & S_ISUID)) {
981         modeval.append(QLatin1Char('S'));
982     } else if ((perm & S_IXUSR)) {
983         modeval.append(QLatin1Char('x'));
984     } else {
985         modeval.append(QLatin1Char('-'));
986     }
987     modeval.append((perm & S_IRGRP) ? QLatin1Char('r') : QLatin1Char('-'));
988     modeval.append((perm & S_IWGRP) ? QLatin1Char('w') : QLatin1Char('-'));
989     if ((perm & S_ISGID) && (perm & S_IXGRP)) {
990         modeval.append(QLatin1Char('s'));
991     } else if ((perm & S_ISGID)) {
992         modeval.append(QLatin1Char('S'));
993     } else if ((perm & S_IXGRP)) {
994         modeval.append(QLatin1Char('x'));
995     } else {
996         modeval.append(QLatin1Char('-'));
997     }
998     modeval.append((perm & S_IROTH) ? QLatin1Char('r') : QLatin1Char('-'));
999     modeval.append((perm & S_IWOTH) ? QLatin1Char('w') : QLatin1Char('-'));
1000     if ((perm & S_ISVTX) && (perm & S_IXOTH)) {
1001         modeval.append(QLatin1Char('t'));
1002     } else if ((perm & S_ISVTX)) {
1003         modeval.append(QLatin1Char('T'));
1004     } else if ((perm & S_IXOTH)) {
1005         modeval.append(QLatin1Char('x'));
1006     } else {
1007         modeval.append(QLatin1Char('-'));
1008     }
1009     return modeval;
1010 }
1011 
fromUnixSeparator(const QString & path)1012 QString LibzipPlugin::fromUnixSeparator(const QString& path)
1013 {
1014     if (!m_backslashedZip) {
1015         return path;
1016     }
1017     return QString(path).replace(QLatin1Char('/'), QLatin1Char('\\'));
1018 }
1019 
toUnixSeparator(const QString & path)1020 QString LibzipPlugin::toUnixSeparator(const QString& path)
1021 {
1022     // Even though the two contains may look similar they are not, the first is the \ char
1023     // that needs to be escaped, the second is the string with two \ that doesn't need escaping
1024     // so they look similar but they aren't
1025     if (path.contains(QLatin1Char('\\')) && !path.contains(QLatin1String("\\"))) {
1026         m_backslashedZip = true;
1027         return QString(path).replace(QLatin1Char('\\'), QLatin1Char('/'));
1028     }
1029     return path;
1030 }
1031 
hasBatchExtractionProgress() const1032 bool LibzipPlugin::hasBatchExtractionProgress() const
1033 {
1034     return true;
1035 }
1036 
1037 #include "libzipplugin.moc"
1038