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(), ×) != 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(), ×) != 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