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