File indexing completed on 2024-05-12 05:50:23

0001 /*
0002     SPDX-FileCopyrightText: 2017 Ragnar Thomsen <rthomsen6@gmail.com>
0003     SPDX-FileCopyrightText: 2023 Ilya Pominov <ipominov@astralinux.ru>
0004 
0005     SPDX-License-Identifier: BSD-2-Clause
0006 */
0007 
0008 #include "libzipplugin.h"
0009 #include "../config.h"
0010 #include "ark_debug.h"
0011 #include "queries.h"
0012 
0013 #include <KIO/Global>
0014 #include <KLocalizedString>
0015 #include <KPluginFactory>
0016 
0017 #include <QDataStream>
0018 #include <QDateTime>
0019 #include <QDir>
0020 #include <QDirIterator>
0021 #include <QFile>
0022 #include <QFileInfo>
0023 #include <QThread>
0024 #include <qplatformdefs.h>
0025 
0026 #include <zlib.h>
0027 
0028 #include <memory>
0029 
0030 #if !HAVE_CHRONO_CAST
0031 #include <utime.h>
0032 #endif
0033 
0034 K_PLUGIN_CLASS_WITH_JSON(LibzipPlugin, "kerfuffle_libzip.json")
0035 
0036 template<auto fn>
0037 using deleter_from_fn = std::integral_constant<decltype(fn), fn>;
0038 template<typename T, auto fn>
0039 using ark_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
0040 
0041 class ZipSource
0042 {
0043 public:
0044     ZipSource(const QString &fileName)
0045     {
0046         const auto &file = m_files.emplace_back(std::make_unique<QFile>(fileName));
0047         m_length = static_cast<zip_uint64_t>(file->size());
0048         m_multiVolumeName = fileName;
0049         zip_error_init(&m_error);
0050 
0051         // Multi-volume zip's are named name.zip.001, try find other parts
0052         if (fileName.endsWith(QStringLiteral(".zip.001"), Qt::CaseInsensitive)) {
0053             m_multiVolumeName.resize(m_multiVolumeName.size() - 4);
0054             auto fNameSize = fileName.size();
0055             for (int i = 2; i <= 999; ++i) {
0056                 auto partFileName = fileName;
0057                 partFileName.replace(fNameSize - 3, 3, QStringLiteral("%1").arg(i, 3, 10, QLatin1Char('0')));
0058                 if (!QFileInfo::exists(partFileName)) {
0059                     break;
0060                 }
0061                 const auto &part = m_files.emplace_back(std::make_unique<QFile>(partFileName));
0062                 m_length += static_cast<zip_uint64_t>(part->size());
0063             }
0064         }
0065     }
0066 
0067     int numberOfVolumes() const
0068     {
0069         return static_cast<int>(m_files.size());
0070     }
0071 
0072     QString multiVolumeName() const
0073     {
0074         return m_multiVolumeName;
0075     }
0076 
0077     zip_int64_t stat(zip_stat_t *info)
0078     {
0079         zip_stat_init(info);
0080         if (!info) {
0081             zip_error_set(&m_error, ZIP_ER_ZLIB, 0);
0082             return -1;
0083         }
0084 
0085         info->size = m_length;
0086         info->valid = ZIP_STAT_SIZE;
0087         return sizeof(struct zip_stat);
0088     }
0089 
0090     zip_int64_t seek(void *data, zip_uint64_t len)
0091     {
0092         zip_int64_t newOffset = zip_source_seek_compute_offset(m_offset, m_length, data, len, &m_error);
0093         if (newOffset < 0) {
0094             zip_error_set(&m_error, ZIP_ER_SEEK, 0);
0095             return -1;
0096         }
0097 
0098         m_offset = static_cast<zip_uint64_t>(newOffset);
0099         return 0;
0100     }
0101 
0102     zip_int64_t read(void *data, zip_uint64_t len)
0103     {
0104         if (len == 0 || m_offset >= m_length) {
0105             return 0;
0106         }
0107 
0108         zip_int64_t ret = 0;
0109         zip_uint64_t offset = m_offset;
0110         for (auto &file : m_files) {
0111             const auto length = static_cast<zip_uint64_t>(file->size());
0112             if (offset >= length) {
0113                 offset -= length;
0114                 continue;
0115             }
0116 
0117             if (!file->isOpen() && !file->open(QIODevice::ReadOnly)) {
0118                 qCDebug(ARK) << "ZipSource error: Can't open" << file->fileName();
0119                 break;
0120             }
0121 
0122             const auto available = (length - offset) < len ? length - offset : len;
0123             if (!file->seek(static_cast<qint64>(offset))) {
0124                 qCDebug(ARK) << "ZipSource error: Can't seek to" << available << "in file" << file->fileName();
0125                 break;
0126             }
0127 
0128             const auto readed = file->read(reinterpret_cast<char *>(data), static_cast<qint64>(available));
0129             if (static_cast<zip_uint64_t>(readed) != available) {
0130                 qCDebug(ARK) << "ZipSource error: Read" << readed << "bytes instead" << available << "in file" << file->fileName();
0131                 break;
0132             }
0133 
0134             ret += static_cast<zip_int64_t>(available);
0135             m_offset += available;
0136             offset = 0;
0137             len -= available;
0138             data = reinterpret_cast<char *>(data) + available;
0139             if (len == 0) {
0140                 return ret;
0141             }
0142         }
0143 
0144         zip_error_set(&m_error, ZIP_ER_READ, 0);
0145         return -1;
0146     }
0147 
0148     // Commands should return -1 on error. ZIP_SOURCE_ERROR will be called to retrieve the error code.
0149     // On success, commands return 0, unless specified otherwise in the description above.
0150     // See https://libzip.org/documentation/zip_source_function_create.html for more details.
0151     static zip_int64_t callbackFn(void *userdata, void *data, zip_uint64_t len, zip_source_cmd_t cmd)
0152     {
0153         auto source = reinterpret_cast<ZipSource *>(userdata);
0154         switch (cmd) {
0155         case ZIP_SOURCE_OPEN:
0156             return 0;
0157         case ZIP_SOURCE_READ:
0158             return source->read(data, len);
0159         case ZIP_SOURCE_CLOSE:
0160             return 0;
0161         case ZIP_SOURCE_STAT:
0162             return source->stat(reinterpret_cast<zip_stat_t *>(data));
0163         case ZIP_SOURCE_ERROR:
0164             return zip_error_to_data(&source->m_error, data, len);
0165         case ZIP_SOURCE_FREE:
0166             return 0;
0167         case ZIP_SOURCE_SEEK:
0168             return source->seek(data, len);
0169         case ZIP_SOURCE_TELL:
0170             return static_cast<zip_int64_t>(source->m_offset);
0171         case ZIP_SOURCE_SUPPORTS:
0172             return ZIP_SOURCE_SUPPORTS_SEEKABLE;
0173         default:
0174             zip_error_set(&source->m_error, ZIP_ER_INVAL, 0);
0175             break;
0176         }
0177         return -1;
0178     }
0179 
0180     static ark_unique_ptr<zip_t, zip_discard> create(LibzipPlugin *plugin, ZipSource &zipSource, int zipOpenFlags)
0181     {
0182         zip_error_t err;
0183         zip_error_init(&err);
0184         ark_unique_ptr<zip_t, zip_discard> archive;
0185         if (plugin->isMultiVolume()) {
0186             auto source = zip_source_function_create(&ZipSource::callbackFn, &zipSource, nullptr);
0187             archive.reset(zip_open_from_source(source, zipOpenFlags, &err));
0188             if (!archive) {
0189                 zip_source_free(source);
0190             }
0191         } else {
0192             int errcode = 0;
0193             archive.reset(zip_open(QFile::encodeName(plugin->filename()).constData(), zipOpenFlags, &errcode));
0194             zip_error_init_with_code(&err, errcode);
0195         }
0196         if (!archive) {
0197             qCCritical(ARK) << "Failed to open archive. Code:" << zip_error_code_zip(&err);
0198             Q_EMIT plugin->error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
0199         }
0200         return archive;
0201     }
0202 
0203 private:
0204     std::vector<std::unique_ptr<QFile>> m_files;
0205     QString m_multiVolumeName;
0206     zip_error_t m_error;
0207     zip_uint64_t m_length = 0;
0208     zip_uint64_t m_offset = 0;
0209 };
0210 
0211 void LibzipPlugin::progressCallback(zip_t *, double progress, void *that)
0212 {
0213     static_cast<LibzipPlugin *>(that)->emitProgress(progress);
0214 }
0215 
0216 int LibzipPlugin::cancelCallback(zip_t *, void * /* unused that*/)
0217 {
0218     return QThread::currentThread()->isInterruptionRequested();
0219 }
0220 
0221 LibzipPlugin::LibzipPlugin(QObject *parent, const QVariantList &args)
0222     : ReadWriteArchiveInterface(parent, args)
0223     , m_overwriteAll(false)
0224     , m_skipAll(false)
0225     , m_listAfterAdd(false)
0226     , m_backslashedZip(false)
0227     , m_zipSource(std::make_unique<ZipSource>(filename()))
0228 {
0229     qCDebug(ARK) << "Initializing libzip plugin";
0230 
0231     if (m_zipSource->numberOfVolumes() > 1) {
0232         m_numberOfVolumes = m_zipSource->numberOfVolumes();
0233         setMultiVolume(true);
0234         m_multiVolumeName = m_zipSource->multiVolumeName();
0235     }
0236 }
0237 
0238 LibzipPlugin::~LibzipPlugin()
0239 {
0240     for (const auto e : std::as_const(m_emittedEntries)) {
0241         // Entries might be passed to pending slots, so we just schedule their deletion.
0242         e->deleteLater();
0243     }
0244 }
0245 
0246 bool LibzipPlugin::list()
0247 {
0248     qCDebug(ARK) << "Listing archive contents for:" << QFile::encodeName(filename());
0249     m_numberOfEntries = 0;
0250 
0251     // Open archive.
0252     auto archive = ZipSource::create(this, *m_zipSource, ZIP_RDONLY);
0253     if (!archive) {
0254         return false;
0255     }
0256 
0257     // Fetch archive comment.
0258     m_comment = QString::fromUtf8(zip_get_archive_comment(archive.get(), nullptr, ZIP_FL_ENC_GUESS));
0259 
0260     // Get number of archive entries.
0261     const auto nofEntries = zip_get_num_entries(archive.get(), 0);
0262     qCDebug(ARK) << "Found entries:" << nofEntries;
0263 
0264     // Loop through all archive entries.
0265     for (int i = 0; i < nofEntries; i++) {
0266         if (QThread::currentThread()->isInterruptionRequested()) {
0267             break;
0268         }
0269 
0270         emitEntryForIndex(archive.get(), i);
0271         if (m_listAfterAdd) {
0272             // Start at 50%.
0273             Q_EMIT progress(0.5 + (0.5 * float(i + 1) / nofEntries));
0274         } else {
0275             Q_EMIT progress(float(i + 1) / nofEntries);
0276         }
0277     }
0278 
0279     m_listAfterAdd = false;
0280     return true;
0281 }
0282 
0283 bool LibzipPlugin::addFiles(const QVector<Archive::Entry *> &files,
0284                             const Archive::Entry *destination,
0285                             const CompressionOptions &options,
0286                             uint numberOfEntriesToAdd)
0287 {
0288     Q_UNUSED(numberOfEntriesToAdd)
0289     int errcode = 0;
0290     zip_error_t err;
0291 
0292     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
0293     ark_unique_ptr<zip_t, zip_discard> archive{zip_open(QFile::encodeName(filename()).constData(), ZIP_CREATE, &errcode)};
0294     zip_error_init_with_code(&err, errcode);
0295     if (!archive) {
0296         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
0297         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
0298         return false;
0299     }
0300 
0301     uint i = 0;
0302     for (const Archive::Entry *e : files) {
0303         if (QThread::currentThread()->isInterruptionRequested()) {
0304             break;
0305         }
0306 
0307         // If entry is a directory, traverse and add all its files and subfolders.
0308         if (QFileInfo(e->fullPath()).isDir()) {
0309             if (!writeEntry(archive.get(), e->fullPath(), destination, options, true)) {
0310                 return false;
0311             }
0312 
0313             QDirIterator it(e->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
0314 
0315             while (!QThread::currentThread()->isInterruptionRequested() && it.hasNext()) {
0316                 const QString path = it.next();
0317 
0318                 if (QFileInfo(path).isDir()) {
0319                     if (!writeEntry(archive.get(), path, destination, options, true)) {
0320                         return false;
0321                     }
0322                 } else {
0323                     if (!writeEntry(archive.get(), path, destination, options)) {
0324                         return false;
0325                     }
0326                 }
0327                 i++;
0328             }
0329         } else {
0330             if (!writeEntry(archive.get(), e->fullPath(), destination, options)) {
0331                 return false;
0332             }
0333         }
0334         i++;
0335     }
0336     qCDebug(ARK) << "Writing " << i << "entries to disk...";
0337 
0338     // Register the callback function to get progress feedback and cancelation.
0339     zip_register_progress_callback_with_state(archive.get(), 0.001, progressCallback, nullptr, this);
0340 #if LIBZIP_CANCELATION
0341     zip_register_cancel_callback_with_state(archive.get(), cancelCallback, nullptr, this);
0342 #endif
0343 
0344     // Write and close archive manually.
0345     zip_close(archive.get());
0346     // Release unique pointer as it set to NULL via zip_close.
0347     archive.release();
0348     if (errcode > 0) {
0349         qCCritical(ARK) << "Failed to write archive";
0350         Q_EMIT error(xi18n("Failed to write archive."));
0351         return false;
0352     }
0353 
0354     if (QThread::currentThread()->isInterruptionRequested()) {
0355         return false;
0356     }
0357 
0358     // We list the entire archive after adding files to ensure entry
0359     // properties are up-to-date.
0360     m_listAfterAdd = true;
0361     list();
0362 
0363     return true;
0364 }
0365 
0366 void LibzipPlugin::emitProgress(double percentage)
0367 {
0368     // Go from 0 to 50%. The second half is the subsequent listing.
0369     Q_EMIT progress(0.5 * percentage);
0370 }
0371 
0372 bool LibzipPlugin::writeEntry(zip_t *archive, const QString &file, const Archive::Entry *destination, const CompressionOptions &options, bool isDir)
0373 {
0374     Q_ASSERT(archive);
0375 
0376     QByteArray destFile;
0377     if (destination) {
0378         destFile = fromUnixSeparator(QString(destination->fullPath() + file)).toUtf8();
0379     } else {
0380         destFile = fromUnixSeparator(file).toUtf8();
0381     }
0382 
0383     qlonglong index;
0384     if (isDir) {
0385         index = zip_dir_add(archive, destFile.constData(), ZIP_FL_ENC_GUESS);
0386         if (index == -1) {
0387             // If directory already exists in archive, we get an error.
0388             qCWarning(ARK) << "Failed to add dir " << file << ":" << zip_strerror(archive);
0389             return true;
0390         }
0391     } else {
0392         zip_source_t *src = zip_source_file(archive, QFile::encodeName(file).constData(), 0, -1);
0393         Q_ASSERT(src);
0394 
0395         index = zip_file_add(archive, destFile.constData(), src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE);
0396         if (index == -1) {
0397             zip_source_free(src);
0398             qCCritical(ARK) << "Could not add entry" << file << ":" << zip_strerror(archive);
0399             Q_EMIT error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive))));
0400             return false;
0401         }
0402     }
0403 
0404 #ifndef Q_OS_WIN
0405     // Set permissions.
0406     QT_STATBUF result;
0407     if (QT_STAT(QFile::encodeName(file).constData(), &result) != 0) {
0408         qCWarning(ARK) << "Failed to read permissions for:" << file;
0409     } else {
0410         zip_uint32_t attributes = result.st_mode << 16;
0411         if (zip_file_set_external_attributes(archive, index, ZIP_FL_UNCHANGED, ZIP_OPSYS_UNIX, attributes) != 0) {
0412             qCWarning(ARK) << "Failed to set external attributes for:" << file;
0413         }
0414     }
0415 #endif
0416 
0417     if (!password().isEmpty()) {
0418         Q_ASSERT(!options.encryptionMethod().isEmpty());
0419         if (options.encryptionMethod() == QLatin1String("AES128")) {
0420             zip_file_set_encryption(archive, index, ZIP_EM_AES_128, password().toUtf8().constData());
0421         } else if (options.encryptionMethod() == QLatin1String("AES192")) {
0422             zip_file_set_encryption(archive, index, ZIP_EM_AES_192, password().toUtf8().constData());
0423         } else if (options.encryptionMethod() == QLatin1String("AES256")) {
0424             zip_file_set_encryption(archive, index, ZIP_EM_AES_256, password().toUtf8().constData());
0425         }
0426     }
0427 
0428     // Set compression level and method.
0429     zip_int32_t compMethod = ZIP_CM_DEFAULT;
0430     if (!options.compressionMethod().isEmpty()) {
0431         if (options.compressionMethod() == QLatin1String("Deflate")) {
0432             compMethod = ZIP_CM_DEFLATE;
0433         } else if (options.compressionMethod() == QLatin1String("BZip2")) {
0434             compMethod = ZIP_CM_BZIP2;
0435 #ifdef ZIP_CM_ZSTD
0436         } else if (options.compressionMethod() == QLatin1String("Zstd")) {
0437             compMethod = ZIP_CM_ZSTD;
0438 #endif
0439 #ifdef ZIP_CM_LZMA
0440         } else if (options.compressionMethod() == QLatin1String("LZMA")) {
0441             compMethod = ZIP_CM_LZMA;
0442 #endif
0443 #ifdef ZIP_CM_XZ
0444         } else if (options.compressionMethod() == QLatin1String("XZ")) {
0445             compMethod = ZIP_CM_XZ;
0446 #endif
0447         } else if (options.compressionMethod() == QLatin1String("Store")) {
0448             compMethod = ZIP_CM_STORE;
0449         }
0450     }
0451     const int compLevel = options.isCompressionLevelSet() ? options.compressionLevel() : 6;
0452     if (zip_set_file_compression(archive, index, compMethod, compLevel) != 0) {
0453         qCCritical(ARK) << "Could not set compression options for" << file << ":" << zip_strerror(archive);
0454         Q_EMIT error(xi18n("Failed to set compression options for entry: %1", QString::fromUtf8(zip_strerror(archive))));
0455         return false;
0456     }
0457 
0458     return true;
0459 }
0460 
0461 bool LibzipPlugin::emitEntryForIndex(zip_t *archive, qlonglong index)
0462 {
0463     Q_ASSERT(archive);
0464 
0465     zip_stat_t statBuffer;
0466     if (zip_stat_index(archive, index, ZIP_FL_ENC_GUESS, &statBuffer)) {
0467         qCCritical(ARK) << "Failed to read stat for index" << index;
0468         return false;
0469     }
0470 
0471     auto e = new Archive::Entry();
0472     auto name = toUnixSeparator(QString::fromUtf8(statBuffer.name));
0473 
0474     if (statBuffer.valid & ZIP_STAT_NAME) {
0475         e->setFullPath(name);
0476     }
0477 
0478     if (e->fullPath(PathFormat::WithTrailingSlash).endsWith(QDir::separator())) {
0479         e->setProperty("isDirectory", true);
0480     }
0481 
0482     if (statBuffer.valid & ZIP_STAT_MTIME) {
0483         e->setProperty("timestamp", QDateTime::fromSecsSinceEpoch(statBuffer.mtime));
0484     }
0485     if (statBuffer.valid & ZIP_STAT_SIZE) {
0486         e->setProperty("size", (qulonglong)statBuffer.size);
0487     }
0488     if (statBuffer.valid & ZIP_STAT_COMP_SIZE) {
0489         e->setProperty("compressedSize", (qlonglong)statBuffer.comp_size);
0490     }
0491     if (statBuffer.valid & ZIP_STAT_CRC) {
0492         if (!e->isDir()) {
0493             e->setProperty("CRC", QStringLiteral("%1").arg((qulonglong)statBuffer.crc, /*fieldWidth*/ 8, /*base*/ 16, QLatin1Char('0')).toUpper());
0494         }
0495     }
0496     if (statBuffer.valid & ZIP_STAT_COMP_METHOD) {
0497         switch (statBuffer.comp_method) {
0498         case ZIP_CM_STORE:
0499             e->setProperty("method", QStringLiteral("Store"));
0500             Q_EMIT compressionMethodFound(QStringLiteral("Store"));
0501             break;
0502         case ZIP_CM_DEFLATE:
0503             e->setProperty("method", QStringLiteral("Deflate"));
0504             Q_EMIT compressionMethodFound(QStringLiteral("Deflate"));
0505             break;
0506         case ZIP_CM_DEFLATE64:
0507             e->setProperty("method", QStringLiteral("Deflate64"));
0508             Q_EMIT compressionMethodFound(QStringLiteral("Deflate64"));
0509             break;
0510         case ZIP_CM_BZIP2:
0511             e->setProperty("method", QStringLiteral("BZip2"));
0512             Q_EMIT compressionMethodFound(QStringLiteral("BZip2"));
0513             break;
0514 #ifdef ZIP_CM_ZSTD
0515         case ZIP_CM_ZSTD:
0516             e->setProperty("method", QStringLiteral("Zstd"));
0517             Q_EMIT compressionMethodFound(QStringLiteral("Zstd"));
0518             break;
0519 #endif
0520 #ifdef ZIP_CM_LZMA
0521         case ZIP_CM_LZMA:
0522             e->setProperty("method", QStringLiteral("LZMA"));
0523             Q_EMIT compressionMethodFound(QStringLiteral("LZMA"));
0524             break;
0525 #endif
0526 #ifdef ZIP_CM_XZ
0527         case ZIP_CM_XZ:
0528             e->setProperty("method", QStringLiteral("XZ"));
0529             Q_EMIT compressionMethodFound(QStringLiteral("XZ"));
0530             break;
0531 #endif
0532         }
0533     }
0534     if (statBuffer.valid & ZIP_STAT_ENCRYPTION_METHOD) {
0535         if (statBuffer.encryption_method != ZIP_EM_NONE) {
0536             e->setProperty("isPasswordProtected", true);
0537             switch (statBuffer.encryption_method) {
0538             case ZIP_EM_TRAD_PKWARE:
0539                 Q_EMIT encryptionMethodFound(QStringLiteral("ZipCrypto"));
0540                 break;
0541             case ZIP_EM_AES_128:
0542                 Q_EMIT encryptionMethodFound(QStringLiteral("AES128"));
0543                 break;
0544             case ZIP_EM_AES_192:
0545                 Q_EMIT encryptionMethodFound(QStringLiteral("AES192"));
0546                 break;
0547             case ZIP_EM_AES_256:
0548                 Q_EMIT encryptionMethodFound(QStringLiteral("AES256"));
0549                 break;
0550             }
0551         }
0552     }
0553 
0554     // Read external attributes, which contains the file permissions.
0555     zip_uint8_t opsys;
0556     zip_uint32_t attributes;
0557     if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) {
0558         qCCritical(ARK) << "Could not read external attributes for entry:" << name;
0559         Q_EMIT error(xi18n("Failed to read metadata for entry: %1", name));
0560         return false;
0561     }
0562 
0563     // Set permissions.
0564     switch (opsys) {
0565     case ZIP_OPSYS_UNIX:
0566         // Unix permissions are stored in the leftmost 16 bits of the external file attribute.
0567         e->setProperty("permissions", permissionsToString(attributes >> 16));
0568         break;
0569     default: // TODO: non-UNIX.
0570         break;
0571     }
0572 
0573     Q_EMIT entry(e);
0574     m_emittedEntries << e;
0575 
0576     return true;
0577 }
0578 
0579 bool LibzipPlugin::deleteFiles(const QVector<Archive::Entry *> &files)
0580 {
0581     int errcode = 0;
0582     zip_error_t err;
0583 
0584     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
0585     ark_unique_ptr<zip_t, zip_discard> archive{zip_open(QFile::encodeName(filename()).constData(), 0, &errcode)};
0586     zip_error_init_with_code(&err, errcode);
0587     if (archive.get() == nullptr) {
0588         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
0589         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
0590         return false;
0591     }
0592 
0593     qulonglong i = 0;
0594     for (const Archive::Entry *e : files) {
0595         if (QThread::currentThread()->isInterruptionRequested()) {
0596             break;
0597         }
0598 
0599         const qlonglong index = zip_name_locate(archive.get(), fromUnixSeparator(e->fullPath()).toUtf8().constData(), ZIP_FL_ENC_GUESS);
0600         if (index == -1) {
0601             qCCritical(ARK) << "Could not find entry to delete:" << e->fullPath();
0602             Q_EMIT error(xi18n("Failed to delete entry: %1", e->fullPath()));
0603             return false;
0604         }
0605         if (zip_delete(archive.get(), index) == -1) {
0606             qCCritical(ARK) << "Could not delete entry" << e->fullPath() << ":" << zip_strerror(archive.get());
0607             Q_EMIT error(xi18n("Failed to delete entry: %1", QString::fromUtf8(zip_strerror(archive.get()))));
0608             return false;
0609         }
0610         Q_EMIT entryRemoved(e->fullPath());
0611         Q_EMIT progress(float(++i) / files.size());
0612     }
0613     qCDebug(ARK) << "Deleted" << i << "entries";
0614 
0615     // Write and close archive manually.
0616     zip_close(archive.get());
0617     // Release unique pointer as it set to NULL via zip_close.
0618     archive.release();
0619     if (errcode > 0) {
0620         qCCritical(ARK) << "Failed to write archive";
0621         Q_EMIT error(xi18n("Failed to write archive."));
0622         return false;
0623     }
0624     return true;
0625 }
0626 
0627 bool LibzipPlugin::addComment(const QString &comment)
0628 {
0629     int errcode = 0;
0630     zip_error_t err;
0631 
0632     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
0633     ark_unique_ptr<zip_t, zip_discard> archive{zip_open(QFile::encodeName(filename()).constData(), 0, &errcode)};
0634     zip_error_init_with_code(&err, errcode);
0635     if (archive.get() == nullptr) {
0636         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
0637         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
0638         return false;
0639     }
0640 
0641     // Set archive comment.
0642     if (zip_set_archive_comment(archive.get(), comment.toUtf8().constData(), comment.length())) {
0643         qCCritical(ARK) << "Failed to set comment:" << zip_strerror(archive.get());
0644         return false;
0645     }
0646 
0647     // Write comment to archive.
0648     zip_close(archive.get());
0649     // Release unique pointer as it set to NULL via zip_close.
0650     archive.release();
0651     if (errcode > 0) {
0652         qCCritical(ARK) << "Failed to write archive";
0653         Q_EMIT error(xi18n("Failed to write archive."));
0654         return false;
0655     }
0656     return true;
0657 }
0658 
0659 bool LibzipPlugin::testArchive()
0660 {
0661     qCDebug(ARK) << "Testing archive";
0662 
0663     // Open archive performing extra consistency checks, free memory using zip_discard as no write oprations needed.
0664     auto archive = ZipSource::create(this, *m_zipSource, ZIP_RDONLY | ZIP_CHECKCONS);
0665     if (!archive) {
0666         return false;
0667     }
0668 
0669     // Check CRC-32 for each archive entry.
0670     const int nofEntries = zip_get_num_entries(archive.get(), 0);
0671     for (int i = 0; i < nofEntries; i++) {
0672         if (QThread::currentThread()->isInterruptionRequested()) {
0673             return false;
0674         }
0675 
0676         // Get statistic for entry. Used to get entry size.
0677         zip_stat_t statBuffer;
0678         int stat_index = zip_stat_index(archive.get(), i, 0, &statBuffer);
0679         auto name = toUnixSeparator(QString::fromUtf8(statBuffer.name));
0680         if (stat_index != 0) {
0681             qCCritical(ARK) << "Failed to read stat for" << name;
0682             return false;
0683         }
0684 
0685         ark_unique_ptr<zip_file, zip_fclose> zipFile{zip_fopen_index(archive.get(), i, 0)};
0686         std::unique_ptr<uchar[]> buf(new uchar[statBuffer.size]);
0687         const int len = zip_fread(zipFile.get(), buf.get(), statBuffer.size);
0688         if (len == -1 || uint(len) != statBuffer.size) {
0689             qCCritical(ARK) << "Failed to read data for" << name;
0690             return false;
0691         }
0692         if (statBuffer.crc != crc32(0, &buf.get()[0], len)) {
0693             qCCritical(ARK) << "CRC check failed for" << name;
0694             return false;
0695         }
0696 
0697         Q_EMIT progress(float(i) / nofEntries);
0698     }
0699 
0700     Q_EMIT testSuccess();
0701     return true;
0702 }
0703 
0704 bool LibzipPlugin::doKill()
0705 {
0706     return false;
0707 }
0708 
0709 bool LibzipPlugin::extractFiles(const QVector<Archive::Entry *> &files, const QString &destinationDirectory, const ExtractionOptions &options)
0710 {
0711     qCDebug(ARK) << "Extracting files to:" << destinationDirectory;
0712     const bool extractAll = files.isEmpty();
0713     const bool removeRootNode = options.isDragAndDropEnabled();
0714 
0715     // Open archive, free memory using zip_discard as no write oprations needed.
0716     auto archive = ZipSource::create(this, *m_zipSource, ZIP_RDONLY);
0717     if (!archive) {
0718         return false;
0719     }
0720 
0721     // Set password if known.
0722     if (!password().isEmpty()) {
0723         qCDebug(ARK) << "Password already known. Setting...";
0724         zip_set_default_password(archive.get(), password().toUtf8().constData());
0725     }
0726 
0727     // Get number of archive entries.
0728     const qlonglong nofEntries = extractAll ? zip_get_num_entries(archive.get(), 0) : files.size();
0729 
0730     // Extract entries.
0731     m_overwriteAll = false; // Whether to overwrite all files
0732     m_skipAll = false; // Whether to skip all files
0733     if (extractAll) {
0734         // We extract all entries.
0735         for (qlonglong i = 0; i < nofEntries; i++) {
0736             if (QThread::currentThread()->isInterruptionRequested()) {
0737                 break;
0738             }
0739             if (!extractEntry(archive.get(),
0740                               toUnixSeparator(QString::fromUtf8(zip_get_name(archive.get(), i, ZIP_FL_ENC_GUESS))),
0741                               QString(),
0742                               destinationDirectory,
0743                               options.preservePaths(),
0744                               removeRootNode)) {
0745                 qCDebug(ARK) << "Extraction failed";
0746                 return false;
0747             }
0748             Q_EMIT progress(float(i + 1) / nofEntries);
0749         }
0750     } else {
0751         // We extract only the entries in files.
0752         qulonglong i = 0;
0753         for (const Archive::Entry *e : files) {
0754             if (QThread::currentThread()->isInterruptionRequested()) {
0755                 break;
0756             }
0757             if (!extractEntry(archive.get(), e->fullPath(), e->rootNode, destinationDirectory, options.preservePaths(), removeRootNode)) {
0758                 qCDebug(ARK) << "Extraction failed";
0759                 return false;
0760             }
0761             Q_EMIT progress(float(++i) / nofEntries);
0762         }
0763     }
0764 
0765     return true;
0766 }
0767 
0768 bool LibzipPlugin::extractEntry(zip_t *archive, const QString &entry, const QString &rootNode, const QString &destDir, bool preservePaths, bool removeRootNode)
0769 {
0770     const bool isDirectory = entry.endsWith(QDir::separator());
0771 
0772     // Add trailing slash to destDir if not present.
0773     QString destDirCorrected(destDir);
0774     if (!destDir.endsWith(QDir::separator())) {
0775         destDirCorrected.append(QDir::separator());
0776     }
0777 
0778     // Remove rootnode if supplied and set destination path.
0779     QString destination;
0780     if (preservePaths) {
0781         if (!removeRootNode || rootNode.isEmpty()) {
0782             destination = destDirCorrected + entry;
0783         } else {
0784             QString truncatedEntry = entry;
0785             truncatedEntry.remove(0, rootNode.size());
0786             destination = destDirCorrected + truncatedEntry;
0787         }
0788     } else {
0789         if (isDirectory) {
0790             qCDebug(ARK) << "Skipping directory:" << entry;
0791             return true;
0792         }
0793         destination = destDirCorrected + QFileInfo(entry).fileName();
0794     }
0795 
0796     // Store parent mtime.
0797     QString parentDir;
0798     if (isDirectory) {
0799         QDir pDir = QFileInfo(destination).dir();
0800         pDir.cdUp();
0801         parentDir = pDir.path();
0802     } else {
0803         parentDir = QFileInfo(destination).path();
0804     }
0805     // For top-level items, don't restore parent dir mtime.
0806     const bool restoreParentMtime = (parentDir + QDir::separator() != destDirCorrected);
0807 
0808     std::filesystem::file_time_type parent_mtime;
0809     std::error_code error_code;
0810     if (restoreParentMtime) {
0811         parent_mtime = std::filesystem::last_write_time(QFileInfo(parentDir).filesystemAbsoluteFilePath(), error_code);
0812         if (error_code) {
0813             qCWarning(ARK) << "Failed to read parent modtime" << error_code.message();
0814         }
0815     }
0816 
0817     // Create parent directories for files. For directories create them.
0818     if (!QDir().mkpath(QFileInfo(destination).path())) {
0819         qCDebug(ARK) << "Failed to create directory:" << QFileInfo(destination).path();
0820         Q_EMIT error(xi18n("Failed to create directory: %1", QFileInfo(destination).path()));
0821         return false;
0822     }
0823 
0824     // Get statistic for entry. Used to get entry size and mtime.
0825     zip_stat_t statBuffer;
0826     if (zip_stat(archive, fromUnixSeparator(entry).toUtf8().constData(), 0, &statBuffer) != 0) {
0827         if (isDirectory && zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOENT) {
0828             qCWarning(ARK) << "Skipping folder without entry:" << entry;
0829             return true;
0830         }
0831         qCCritical(ARK) << "Failed to read stat for entry" << entry;
0832         return false;
0833     }
0834 
0835     if (!isDirectory) {
0836         // Handle existing destination files.
0837         QString renamedEntry = entry;
0838         while (!m_overwriteAll && QFileInfo::exists(destination)) {
0839             if (m_skipAll) {
0840                 return true;
0841             } else {
0842                 Kerfuffle::OverwriteQuery query(renamedEntry);
0843                 Q_EMIT userQuery(&query);
0844                 query.waitForResponse();
0845 
0846                 if (query.responseCancelled()) {
0847                     Q_EMIT cancelled();
0848                     return false;
0849                 } else if (query.responseSkip()) {
0850                     return true;
0851                 } else if (query.responseAutoSkip()) {
0852                     m_skipAll = true;
0853                     return true;
0854                 } else if (query.responseRename()) {
0855                     const QString newName(query.newFilename());
0856                     destination = QFileInfo(destination).path() + QDir::separator() + QFileInfo(newName).fileName();
0857                     renamedEntry = QFileInfo(entry).path() + QDir::separator() + QFileInfo(newName).fileName();
0858                 } else if (query.responseOverwriteAll()) {
0859                     m_overwriteAll = true;
0860                     break;
0861                 } else if (query.responseOverwrite()) {
0862                     break;
0863                 }
0864             }
0865         }
0866 
0867         // Handle password-protected files.
0868         ark_unique_ptr<zip_file, zip_fclose> zipFile{nullptr};
0869         bool firstTry = true;
0870         while (!zipFile) {
0871             zipFile.reset(zip_fopen(archive, fromUnixSeparator(entry).toUtf8().constData(), 0));
0872             if (zipFile) {
0873                 break;
0874             } else if (zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOPASSWD || zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_WRONGPASSWD) {
0875                 Kerfuffle::PasswordNeededQuery query(filename(), !firstTry);
0876                 Q_EMIT userQuery(&query);
0877                 query.waitForResponse();
0878 
0879                 if (query.responseCancelled()) {
0880                     Q_EMIT cancelled();
0881                     return false;
0882                 }
0883                 setPassword(query.password());
0884 
0885                 if (zip_set_default_password(archive, password().toUtf8().constData())) {
0886                     qCDebug(ARK) << "Failed to set password for:" << entry;
0887                 }
0888                 firstTry = false;
0889             } else {
0890                 qCCritical(ARK) << "Failed to open file:" << zip_strerror(archive);
0891                 Q_EMIT error(xi18n("Failed to open '%1':<nl/>%2", entry, QString::fromUtf8(zip_strerror(archive))));
0892                 return false;
0893             }
0894         }
0895 
0896         QFile file(destination);
0897         if (!file.open(QIODevice::WriteOnly)) {
0898             qCCritical(ARK) << "Failed to open file for writing";
0899             Q_EMIT error(xi18n("Failed to open file for writing: %1", destination));
0900             return false;
0901         }
0902 
0903         QDataStream out(&file);
0904 
0905         // Write archive entry to file. We use a read/write buffer of 1000 chars.
0906         qulonglong sum = 0;
0907         char buf[1000];
0908         while (sum != statBuffer.size) {
0909             const auto readBytes = zip_fread(zipFile.get(), buf, 1000);
0910             if (readBytes < 0) {
0911                 qCCritical(ARK) << "Failed to read data";
0912                 Q_EMIT error(xi18n("Failed to read data for entry: %1", entry));
0913                 return false;
0914             }
0915             if (out.writeRawData(buf, readBytes) != readBytes) {
0916                 qCCritical(ARK) << "Failed to write data";
0917                 Q_EMIT error(xi18n("Failed to write data for entry: %1", entry));
0918                 return false;
0919             }
0920 
0921             sum += readBytes;
0922         }
0923 
0924         const auto index = zip_name_locate(archive, fromUnixSeparator(entry).toUtf8().constData(), ZIP_FL_ENC_GUESS);
0925         if (index == -1) {
0926             qCCritical(ARK) << "Could not locate entry:" << entry;
0927             Q_EMIT error(xi18n("Failed to locate entry: %1", entry));
0928             return false;
0929         }
0930 
0931         zip_uint8_t opsys;
0932         zip_uint32_t attributes;
0933         if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) {
0934             qCCritical(ARK) << "Could not read external attributes for entry:" << entry;
0935             Q_EMIT error(xi18n("Failed to read metadata for entry: %1", entry));
0936             return false;
0937         }
0938 
0939         // Inspired by fuse-zip source code: fuse-zip/lib/fileNode.cpp
0940         switch (opsys) {
0941         case ZIP_OPSYS_UNIX:
0942             if (attributes != 0) {
0943                 // Unix permissions are stored in the leftmost 16 bits of the external file attribute.
0944                 file.setPermissions(KIO::convertPermissions(attributes >> 16));
0945             }
0946             break;
0947         default: // TODO: non-UNIX.
0948             break;
0949         }
0950 
0951         file.close();
0952     }
0953 
0954     // Set mtime for entry (also access time otherwise it's "uninitilized")
0955 #if HAVE_CHRONO_CAST
0956     const auto time = std::chrono::clock_cast<std::chrono::file_clock>(std::chrono::system_clock::from_time_t(statBuffer.mtime));
0957     std::filesystem::last_write_time(QFileInfo(destination).filesystemAbsoluteFilePath(), time, error_code);
0958     if (error_code) {
0959         qCWarning(ARK) << "Failed to restore mtime:" << destination << error_code.message();
0960     }
0961 #else
0962     utimbuf times;
0963     times.actime = statBuffer.mtime;
0964     times.modtime = statBuffer.mtime;
0965     if (utime(destination.toUtf8().constData(), &times) != 0) {
0966         qCWarning(ARK) << "Failed to restore mtime:" << destination;
0967     }
0968 #endif
0969 
0970     Q_ASSERT([&] {
0971         const auto mtime = QDateTime::fromSecsSinceEpoch(statBuffer.mtime);
0972         const auto actualMtime = QFileInfo(destination).fileTime(QFile::FileModificationTime);
0973         if (mtime != actualMtime) {
0974             qDebug() << "Target mtime:" << mtime << "Actual mtime:" << actualMtime;
0975             return false;
0976         }
0977         return true;
0978     }());
0979 
0980     if (restoreParentMtime) {
0981         // Restore mtime for parent dir.
0982         std::filesystem::last_write_time(QFileInfo(parentDir).filesystemAbsoluteFilePath(), parent_mtime, error_code);
0983         if (error_code) {
0984             qCWarning(ARK) << "Failed to restore mtime for parent dir of:" << destination << error_code.message();
0985         }
0986     }
0987     return true;
0988 }
0989 
0990 bool LibzipPlugin::moveFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options)
0991 {
0992     Q_UNUSED(options)
0993     int errcode = 0;
0994     zip_error_t err;
0995 
0996     // Open archive.
0997     ark_unique_ptr<zip_t, zip_close> archive{zip_open(QFile::encodeName(filename()).constData(), 0, &errcode)};
0998     zip_error_init_with_code(&err, errcode);
0999     if (archive.get() == nullptr) {
1000         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
1001         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
1002         return false;
1003     }
1004 
1005     QStringList filePaths = entryFullPaths(files);
1006     filePaths.sort();
1007     const QStringList destPaths = entryPathsFromDestination(filePaths, destination, entriesWithoutChildren(files).count());
1008 
1009     int i;
1010     for (i = 0; i < filePaths.size(); ++i) {
1011         const int index = zip_name_locate(archive.get(), filePaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS);
1012         if (index == -1) {
1013             qCCritical(ARK) << "Could not find entry to move:" << filePaths.at(i);
1014             Q_EMIT error(xi18n("Failed to move entry: %1", filePaths.at(i)));
1015             return false;
1016         }
1017 
1018         if (zip_file_rename(archive.get(), index, destPaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS) == -1) {
1019             qCCritical(ARK) << "Could not move entry:" << filePaths.at(i);
1020             Q_EMIT error(xi18n("Failed to move entry: %1", filePaths.at(i)));
1021             return false;
1022         }
1023 
1024         Q_EMIT entryRemoved(filePaths.at(i));
1025         emitEntryForIndex(archive.get(), index);
1026         Q_EMIT progress(i / filePaths.count());
1027     }
1028 
1029     // Write and close archive manually.
1030     zip_close(archive.get());
1031     // Release unique pointer as it set to NULL via zip_close.
1032     archive.release();
1033     if (errcode > 0) {
1034         qCCritical(ARK) << "Failed to write archive";
1035         Q_EMIT error(xi18n("Failed to write archive."));
1036         return false;
1037     }
1038 
1039     qCDebug(ARK) << "Moved" << i << "entries";
1040 
1041     return true;
1042 }
1043 
1044 bool LibzipPlugin::copyFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options)
1045 {
1046     Q_UNUSED(options)
1047     int errcode = 0;
1048     zip_error_t err;
1049 
1050     // Open archive and don't write changes in unique_ptr destructor but instead call zip_close manually when needed.
1051     ark_unique_ptr<zip_t, zip_discard> archive{zip_open(QFile::encodeName(filename()).constData(), 0, &errcode)};
1052     zip_error_init_with_code(&err, errcode);
1053     if (archive.get() == nullptr) {
1054         qCCritical(ARK) << "Failed to open archive. Code:" << errcode;
1055         Q_EMIT error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err))));
1056         return false;
1057     }
1058 
1059     const QStringList filePaths = entryFullPaths(files);
1060     const QStringList destPaths = entryPathsFromDestination(filePaths, destination, 0);
1061 
1062     int i;
1063     for (i = 0; i < filePaths.size(); ++i) {
1064         QString dest = destPaths.at(i);
1065 
1066         if (dest.endsWith(QDir::separator())) {
1067             if (zip_dir_add(archive.get(), dest.toUtf8().constData(), ZIP_FL_ENC_GUESS) == -1) {
1068                 // If directory already exists in archive, we get an error.
1069                 qCWarning(ARK) << "Failed to add dir " << dest << ":" << zip_strerror(archive.get());
1070                 continue;
1071             }
1072         }
1073 
1074         const int srcIndex = zip_name_locate(archive.get(), filePaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS);
1075         if (srcIndex == -1) {
1076             qCCritical(ARK) << "Could not find entry to copy:" << filePaths.at(i);
1077             Q_EMIT error(xi18n("Failed to copy entry: %1", filePaths.at(i)));
1078             return false;
1079         }
1080 
1081         zip_source_t *src = zip_source_zip(archive.get(), archive.get(), srcIndex, 0, 0, -1);
1082         if (!src) {
1083             qCCritical(ARK) << "Failed to create source for:" << filePaths.at(i);
1084             return false;
1085         }
1086 
1087         const int destIndex = zip_file_add(archive.get(), dest.toUtf8().constData(), src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE);
1088         if (destIndex == -1) {
1089             zip_source_free(src);
1090             qCCritical(ARK) << "Could not add entry" << dest << ":" << zip_strerror(archive.get());
1091             Q_EMIT error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive.get()))));
1092             return false;
1093         }
1094 
1095         // Get permissions from source entry.
1096         zip_uint8_t opsys;
1097         zip_uint32_t attributes;
1098         if (zip_file_get_external_attributes(archive.get(), srcIndex, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) {
1099             qCCritical(ARK) << "Failed to read external attributes for source:" << filePaths.at(i);
1100             Q_EMIT error(xi18n("Failed to read metadata for entry: %1", filePaths.at(i)));
1101             return false;
1102         }
1103 
1104         // Set permissions on dest entry.
1105         if (zip_file_set_external_attributes(archive.get(), destIndex, ZIP_FL_UNCHANGED, opsys, attributes) != 0) {
1106             qCCritical(ARK) << "Failed to set external attributes for destination:" << dest;
1107             Q_EMIT error(xi18n("Failed to set metadata for entry: %1", dest));
1108             return false;
1109         }
1110     }
1111 
1112     // Register the callback function to get progress feedback and cancelation.
1113     zip_register_progress_callback_with_state(archive.get(), 0.001, progressCallback, nullptr, this);
1114 #if LIBZIP_CANCELATION
1115     zip_register_cancel_callback_with_state(archive.get(), cancelCallback, nullptr, this);
1116 #endif
1117 
1118     // Write and close archive manually before using list() function.
1119     zip_close(archive.get());
1120     // Release unique pointer as it set to NULL via zip_close.
1121     archive.release();
1122     if (errcode > 0) {
1123         qCCritical(ARK) << "Failed to write archive";
1124         Q_EMIT error(xi18n("Failed to write archive."));
1125         return false;
1126     }
1127 
1128     if (QThread::currentThread()->isInterruptionRequested()) {
1129         return false;
1130     }
1131 
1132     // List the archive to update the model.
1133     m_listAfterAdd = true;
1134     list();
1135 
1136     qCDebug(ARK) << "Copied" << i << "entries";
1137 
1138     return true;
1139 }
1140 
1141 QString LibzipPlugin::fromUnixSeparator(const QString &path)
1142 {
1143     if (!m_backslashedZip) {
1144         return path;
1145     }
1146     return QString(path).replace(QLatin1Char('/'), QLatin1Char('\\'));
1147 }
1148 
1149 QString LibzipPlugin::toUnixSeparator(const QString &path)
1150 {
1151     // Even though the two contains may look similar they are not, the first is the \ char
1152     // that needs to be escaped, the second is the string with two \ that doesn't need escaping
1153     // so they look similar but they aren't
1154     if (path.contains(QLatin1Char('\\')) && !path.contains(QLatin1String("\\"))) {
1155         m_backslashedZip = true;
1156         return QString(path).replace(QLatin1Char('\\'), QLatin1Char('/'));
1157     }
1158     return path;
1159 }
1160 
1161 bool LibzipPlugin::hasBatchExtractionProgress() const
1162 {
1163     return true;
1164 }
1165 
1166 bool LibzipPlugin::isReadOnly() const
1167 {
1168     return isMultiVolume() || ReadWriteArchiveInterface::isReadOnly();
1169 }
1170 
1171 QString LibzipPlugin::multiVolumeName() const
1172 {
1173     return m_multiVolumeName.isEmpty() ? filename() : m_multiVolumeName;
1174 }
1175 
1176 #include "libzipplugin.moc"
1177 #include "moc_libzipplugin.cpp"