File indexing completed on 2024-05-05 05:50:39

0001 /*
0002     SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net>
0003     SPDX-FileCopyrightText: 2008 Harald Hvaal <haraldhv@stud.ntnu.no>
0004     SPDX-FileCopyrightText: 2009-2011 Raphael Kubo da Costa <rakuco@FreeBSD.org>
0005     SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
0006 
0007     SPDX-License-Identifier: BSD-2-Clause
0008 */
0009 
0010 #include "archive_kerfuffle.h"
0011 #include "archiveinterface.h"
0012 #include "ark_debug.h"
0013 #include "jobs.h"
0014 #include "mimetypes.h"
0015 #include "pluginmanager.h"
0016 
0017 #include <KPluginFactory>
0018 
0019 #include <QMimeDatabase>
0020 #include <QRegularExpression>
0021 
0022 namespace Kerfuffle
0023 {
0024 Archive *Archive::create(const QString &fileName, QObject *parent)
0025 {
0026     return create(fileName, QString(), parent);
0027 }
0028 
0029 Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent)
0030 {
0031     qCDebug(ARK) << "Going to create archive" << fileName;
0032 
0033     PluginManager pluginManager;
0034     const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType);
0035 
0036     qCDebug(ARK) << "Looking for plugins that handle the mimetype: " << mimeType.name();
0037     QVector<Plugin *> offers = pluginManager.preferredPluginsFor(mimeType);
0038     if (offers.isEmpty()) {
0039         if (fixedMimeType.isEmpty()) {
0040             const QMimeType extensionMimeType = determineMimeType(fileName, PreferExtensionMime);
0041             offers = pluginManager.preferredPluginsFor(extensionMimeType);
0042         }
0043         if (offers.isEmpty()) {
0044             qCCritical(ARK) << "Could not find a plugin to handle" << fileName;
0045             return new Archive(NoPlugin, parent);
0046         }
0047     }
0048 
0049     Archive *archive = nullptr;
0050     for (Plugin *plugin : std::as_const(offers)) {
0051         archive = create(fileName, plugin, parent);
0052         // Use the first valid plugin, according to the priority sorting.
0053         if (archive->isValid()) {
0054             return archive;
0055         }
0056     }
0057 
0058     qCCritical(ARK) << "Failed to find a usable plugin for" << fileName;
0059     return archive;
0060 }
0061 
0062 Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent)
0063 {
0064     Q_ASSERT(plugin);
0065 
0066     qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId();
0067 
0068     const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath()), QVariant().fromValue(plugin->metaData())};
0069     ReadOnlyArchiveInterface *iface = KPluginFactory::instantiatePlugin<ReadOnlyArchiveInterface>(plugin->metaData(), nullptr, args).plugin;
0070     if (!iface) {
0071         qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId();
0072         return new Archive(FailedPlugin, parent);
0073     }
0074 
0075     if (!plugin->isValid()) {
0076         qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed.";
0077         return new Archive(FailedPlugin, parent);
0078     }
0079 
0080     qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId();
0081     return new Archive(iface, !plugin->isReadWrite(), parent);
0082 }
0083 
0084 BatchExtractJob *Archive::batchExtract(const QString &fileName, const QString &destination, bool autoSubfolder, bool preservePaths, QObject *parent)
0085 {
0086     auto loadJob = load(fileName, parent);
0087     auto batchJob = new BatchExtractJob(loadJob, destination, autoSubfolder, preservePaths);
0088 
0089     return batchJob;
0090 }
0091 
0092 CreateJob *
0093 Archive::create(const QString &fileName, const QString &mimeType, const QVector<Archive::Entry *> &entries, const CompressionOptions &options, QObject *parent)
0094 {
0095     auto archive = create(fileName, mimeType, parent);
0096     auto createJob = new CreateJob(archive, entries, options);
0097 
0098     return createJob;
0099 }
0100 
0101 Archive *Archive::createEmpty(const QString &fileName, const QString &mimeType, QObject *parent)
0102 {
0103     auto archive = create(fileName, mimeType, parent);
0104     Q_ASSERT(archive->isEmpty());
0105 
0106     return archive;
0107 }
0108 
0109 LoadJob *Archive::load(const QString &fileName, QObject *parent)
0110 {
0111     return load(fileName, QString(), parent);
0112 }
0113 
0114 LoadJob *Archive::load(const QString &fileName, const QString &mimeType, QObject *parent)
0115 {
0116     auto archive = create(fileName, mimeType, parent);
0117     auto loadJob = new LoadJob(archive);
0118 
0119     return loadJob;
0120 }
0121 
0122 LoadJob *Archive::load(const QString &fileName, Plugin *plugin, QObject *parent)
0123 {
0124     auto archive = create(fileName, plugin, parent);
0125     auto loadJob = new LoadJob(archive);
0126 
0127     return loadJob;
0128 }
0129 
0130 Archive::Archive(ArchiveError errorCode, QObject *parent)
0131     : QObject(parent)
0132     , m_iface(nullptr)
0133     , m_error(errorCode)
0134 {
0135     qCDebug(ARK) << "Created archive instance with error";
0136 }
0137 
0138 Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent)
0139     : QObject(parent)
0140     , m_iface(archiveInterface)
0141     , m_isReadOnly(isReadOnly)
0142     , m_isSingleFolder(false)
0143     , m_isMultiVolume(false)
0144     , m_extractedFilesSize(0)
0145     , m_error(NoError)
0146     , m_encryptionType(Unencrypted)
0147 {
0148     qCDebug(ARK) << "Created archive instance";
0149 
0150     Q_ASSERT(m_iface);
0151     m_iface->setParent(this);
0152 
0153     connect(m_iface, &ReadOnlyArchiveInterface::compressionMethodFound, this, &Archive::onCompressionMethodFound);
0154     connect(m_iface, &ReadOnlyArchiveInterface::encryptionMethodFound, this, &Archive::onEncryptionMethodFound);
0155 
0156     m_userMetaData = std::make_optional<MetadataBackup>(fileName());
0157 }
0158 
0159 void Archive::onCompressionMethodFound(const QString &method)
0160 {
0161     QStringList methods = property("compressionMethods").toStringList();
0162 
0163     if (!methods.contains(method) && method != QLatin1String("Store")) {
0164         methods.append(method);
0165     }
0166     methods.sort();
0167 
0168     setProperty("compressionMethods", methods);
0169 }
0170 
0171 void Archive::onEncryptionMethodFound(const QString &method)
0172 {
0173     QStringList methods = property("encryptionMethods").toStringList();
0174 
0175     if (!methods.contains(method)) {
0176         methods.append(method);
0177     }
0178     methods.sort();
0179 
0180     setProperty("encryptionMethods", methods);
0181 }
0182 
0183 Archive::~Archive()
0184 {
0185 }
0186 
0187 QString Archive::completeBaseName() const
0188 {
0189     const QString suffix = QFileInfo(fileName()).suffix();
0190     QString base = QFileInfo(fileName()).completeBaseName();
0191 
0192     // Special case for compressed tar archives.
0193     if (base.right(4).toUpper() == QLatin1String(".TAR")) {
0194         base.chop(4);
0195 
0196         // Multi-volume 7z's are named name.7z.001.
0197     } else if (base.right(3).toUpper() == QLatin1String(".7Z")) {
0198         base.chop(3);
0199 
0200         // Multi-volume zip's are named name.zip.001.
0201     } else if (base.right(4).toUpper() == QLatin1String(".ZIP")) {
0202         base.chop(4);
0203 
0204         // For multivolume rar's we want to remove the ".partNNN" suffix.
0205     } else if (suffix.toUpper() == QLatin1String("RAR")) {
0206         base.remove(QRegularExpression(QStringLiteral("\\.part[0-9]{1,3}$")));
0207     }
0208 
0209     return base;
0210 }
0211 
0212 QString Archive::fileName() const
0213 {
0214     return isValid() ? m_iface->filename() : QString();
0215 }
0216 
0217 QString Archive::comment() const
0218 {
0219     return isValid() ? m_iface->comment() : QString();
0220 }
0221 
0222 CommentJob *Archive::addComment(const QString &comment)
0223 {
0224     if (!isValid()) {
0225         return nullptr;
0226     }
0227 
0228     qCDebug(ARK) << "Going to add comment:" << comment;
0229     Q_ASSERT(!isReadOnly());
0230     CommentJob *job = new CommentJob(comment, static_cast<ReadWriteArchiveInterface *>(m_iface));
0231     return job;
0232 }
0233 
0234 TestJob *Archive::testArchive()
0235 {
0236     if (!isValid()) {
0237         return nullptr;
0238     }
0239 
0240     qCDebug(ARK) << "Going to test archive";
0241 
0242     TestJob *job = new TestJob(m_iface);
0243     return job;
0244 }
0245 
0246 QMimeType Archive::mimeType()
0247 {
0248     if (!isValid()) {
0249         return QMimeType();
0250     }
0251 
0252     if (!m_mimeType.isValid()) {
0253         m_mimeType = determineMimeType(fileName());
0254     }
0255 
0256     return m_mimeType;
0257 }
0258 
0259 bool Archive::isEmpty() const
0260 {
0261     return (numberOfEntries() == 0);
0262 }
0263 
0264 bool Archive::isReadOnly() const
0265 {
0266     return isValid() ? (m_iface->isReadOnly() || m_isReadOnly || (isMultiVolume() && (numberOfEntries() > 0))) : false;
0267 }
0268 
0269 bool Archive::isSingleFile() const
0270 {
0271     // If the only entry is a folder, isSingleFolder() is true.
0272     return numberOfEntries() == 1 && !isSingleFolder();
0273 }
0274 
0275 bool Archive::isSingleFolder() const
0276 {
0277     if (!isValid()) {
0278         return false;
0279     }
0280 
0281     return m_isSingleFolder;
0282 }
0283 
0284 bool Archive::hasComment() const
0285 {
0286     return isValid() ? !comment().isEmpty() : false;
0287 }
0288 
0289 bool Archive::isMultiVolume() const
0290 {
0291     if (!isValid()) {
0292         return false;
0293     }
0294 
0295     return m_iface->isMultiVolume();
0296 }
0297 
0298 void Archive::setMultiVolume(bool value)
0299 {
0300     m_iface->setMultiVolume(value);
0301 }
0302 
0303 int Archive::numberOfVolumes() const
0304 {
0305     return m_iface->numberOfVolumes();
0306 }
0307 
0308 Archive::EncryptionType Archive::encryptionType() const
0309 {
0310     if (!isValid()) {
0311         return Unencrypted;
0312     }
0313 
0314     return m_encryptionType;
0315 }
0316 
0317 QString Archive::password() const
0318 {
0319     return m_iface->password();
0320 }
0321 
0322 uint Archive::numberOfEntries() const
0323 {
0324     if (!isValid()) {
0325         return 0;
0326     }
0327 
0328     return m_iface->numberOfEntries();
0329 }
0330 
0331 qulonglong Archive::unpackedSize() const
0332 {
0333     if (!isValid()) {
0334         return 0;
0335     }
0336 
0337     return m_extractedFilesSize;
0338 }
0339 
0340 qulonglong Archive::packedSize() const
0341 {
0342     return isValid() ? static_cast<qulonglong>(QFileInfo(fileName()).size()) : 0;
0343 }
0344 
0345 QString Archive::subfolderName() const
0346 {
0347     if (!isValid()) {
0348         return QString();
0349     }
0350 
0351     return m_subfolderName;
0352 }
0353 
0354 bool Archive::isValid() const
0355 {
0356     return m_iface && (m_error == NoError);
0357 }
0358 
0359 ArchiveError Archive::error() const
0360 {
0361     return m_error;
0362 }
0363 
0364 DeleteJob *Archive::deleteFiles(QVector<Archive::Entry *> &entries)
0365 {
0366     if (!isValid()) {
0367         return nullptr;
0368     }
0369 
0370     qCDebug(ARK) << "Going to delete" << entries.size() << "entries";
0371 
0372     if (m_iface->isReadOnly()) {
0373         return nullptr;
0374     }
0375 
0376     DeleteJob *newJob = new DeleteJob(entries, static_cast<ReadWriteArchiveInterface *>(m_iface));
0377     connect(newJob, &DeleteJob::result, this, [this]() {
0378         restoreUserMetadata();
0379     });
0380 
0381     return newJob;
0382 }
0383 
0384 AddJob *Archive::addFiles(const QVector<Archive::Entry *> &files, const Archive::Entry *destination, const CompressionOptions &options)
0385 {
0386     if (!isValid()) {
0387         return nullptr;
0388     }
0389 
0390     CompressionOptions newOptions = options;
0391     if (encryptionType() != Unencrypted) {
0392         newOptions.setEncryptedArchiveHint(true);
0393     }
0394 
0395     qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions;
0396     Q_ASSERT(!m_iface->isReadOnly());
0397 
0398     AddJob *newJob = new AddJob(files, destination, newOptions, static_cast<ReadWriteArchiveInterface *>(m_iface));
0399     connect(newJob, &AddJob::result, this, &Archive::onAddFinished);
0400     return newJob;
0401 }
0402 
0403 MoveJob *Archive::moveFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options)
0404 {
0405     if (!isValid()) {
0406         return nullptr;
0407     }
0408 
0409     CompressionOptions newOptions = options;
0410     if (encryptionType() != Unencrypted) {
0411         newOptions.setEncryptedArchiveHint(true);
0412     }
0413 
0414     qCDebug(ARK) << "Going to move files" << files << "to destination" << destination << "with options" << newOptions;
0415     Q_ASSERT(!m_iface->isReadOnly());
0416 
0417     MoveJob *newJob = new MoveJob(files, destination, newOptions, static_cast<ReadWriteArchiveInterface *>(m_iface));
0418     connect(newJob, &MoveJob::result, this, [this]() {
0419         restoreUserMetadata();
0420     });
0421 
0422     return newJob;
0423 }
0424 
0425 CopyJob *Archive::copyFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options)
0426 {
0427     if (!isValid()) {
0428         return nullptr;
0429     }
0430 
0431     CompressionOptions newOptions = options;
0432     if (encryptionType() != Unencrypted) {
0433         newOptions.setEncryptedArchiveHint(true);
0434     }
0435 
0436     qCDebug(ARK) << "Going to copy files" << files << "with options" << newOptions;
0437     Q_ASSERT(!m_iface->isReadOnly());
0438 
0439     CopyJob *newJob = new CopyJob(files, destination, newOptions, static_cast<ReadWriteArchiveInterface *>(m_iface));
0440     connect(newJob, &CopyJob::result, this, [this]() {
0441         restoreUserMetadata();
0442     });
0443 
0444     return newJob;
0445 }
0446 
0447 ExtractJob *Archive::extractFiles(const QVector<Archive::Entry *> &files, const QString &destinationDir, ExtractionOptions options)
0448 {
0449     if (!isValid()) {
0450         return nullptr;
0451     }
0452 
0453     ExtractionOptions newOptions = options;
0454     if (encryptionType() != Unencrypted) {
0455         newOptions.setEncryptedArchiveHint(true);
0456     }
0457 
0458     ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface);
0459     return newJob;
0460 }
0461 
0462 PreviewJob *Archive::preview(Archive::Entry *entry)
0463 {
0464     if (!isValid()) {
0465         return nullptr;
0466     }
0467 
0468     PreviewJob *job = new PreviewJob(entry, (encryptionType() != Unencrypted), m_iface);
0469     return job;
0470 }
0471 
0472 OpenJob *Archive::open(Archive::Entry *entry)
0473 {
0474     if (!isValid()) {
0475         return nullptr;
0476     }
0477 
0478     OpenJob *job = new OpenJob(entry, (encryptionType() != Unencrypted), m_iface);
0479     return job;
0480 }
0481 
0482 OpenWithJob *Archive::openWith(Archive::Entry *entry)
0483 {
0484     if (!isValid()) {
0485         return nullptr;
0486     }
0487 
0488     OpenWithJob *job = new OpenWithJob(entry, (encryptionType() != Unencrypted), m_iface);
0489     return job;
0490 }
0491 
0492 void Archive::encrypt(const QString &password, bool encryptHeader)
0493 {
0494     if (!isValid()) {
0495         return;
0496     }
0497 
0498     m_iface->setPassword(password);
0499     m_iface->setHeaderEncryptionEnabled(encryptHeader);
0500     m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted;
0501 
0502     restoreUserMetadata();
0503 }
0504 
0505 void Archive::onAddFinished(KJob *job)
0506 {
0507     // if the archive was previously a single folder archive and an add job
0508     // has successfully finished, then it is no longer a single folder
0509     // archive (for the current implementation, which does not allow adding
0510     // folders/files other places than the root.
0511     // TODO: handle the case of creating a new file and singlefolderarchive
0512     // then.
0513     if (m_isSingleFolder && !job->error()) {
0514         m_isSingleFolder = false;
0515     }
0516 
0517     restoreUserMetadata();
0518 }
0519 
0520 void Archive::onUserQuery(Query *query)
0521 {
0522     query->execute();
0523 }
0524 
0525 QString Archive::multiVolumeName() const
0526 {
0527     return m_iface->multiVolumeName();
0528 }
0529 
0530 ReadOnlyArchiveInterface *Archive::interface()
0531 {
0532     return m_iface;
0533 }
0534 
0535 bool Archive::hasMultipleTopLevelEntries() const
0536 {
0537     return !isSingleFile() && !isSingleFolder();
0538 }
0539 
0540 void Archive::restoreUserMetadata()
0541 {
0542     if (!m_userMetaData.has_value()) {
0543         return;
0544     }
0545 
0546     m_userMetaData->restore(fileName());
0547 }
0548 
0549 } // namespace Kerfuffle
0550 
0551 #include "moc_archive_kerfuffle.cpp"