diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h index 5296e403..adb3f41e 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,274 +1,274 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" #include "kerfuffle_export.h" #include "archiveentry.h" #include #include #include #include namespace Kerfuffle { class Query; class KERFUFFLE_EXPORT ReadOnlyArchiveInterface: public QObject { Q_OBJECT public: explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList &args); ~ReadOnlyArchiveInterface() override; /** * Returns the filename of the archive currently being handled. */ QString filename() const; /** * Returns the comment of the archive. */ QString comment() const; /** * @return The password of the archive, if any. */ QString password() const; bool isMultiVolume() const; int numberOfVolumes() const; /** * Returns whether the file can only be read. * * @return @c true The file cannot be written. * @return @c false The file can be read and written. */ virtual bool isReadOnly() const; virtual bool open(); /** * List archive contents. * This runs the process of reading archive contents. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool list() = 0; virtual bool testArchive() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); /** * Extracts the given @p files to the given @p destinationDirectory. * If @p files is empty, the whole archive will be extracted. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the extraction succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; /** * @return Whether the plugins do NOT run the functions in their own thread. * @see setWaitForFinishedSignal() */ bool waitForFinishedSignal(); /** * Returns count of required finish signals for a job to be finished. * * These two methods are used by move and copy jobs, which in some plugins implementations have to call * several processes sequentially. For instance, moving entries in zip archive is only possible if * extracting the entries, deleting them, recreating destination folder structure and adding them back again. */ virtual int moveRequiredSignals() const; virtual int copyRequiredSignals() const; /** * Returns the list of filenames retrieved from the list of entries. */ static QStringList entryFullPaths(const QVector &entries, PathFormat format = WithTrailingSlash); /** * Returns the list of the entries, excluding their children. * * This method relies on entries paths so doesn't require parents to be set. */ static QVector entriesWithoutChildren(const QVector &entries); /** * Returns the string list of entry paths, which will be a result of adding/moving/copying entries. * * @param entries The entries which will be added/moved/copied. * @param destination Destination path within the archive to which entries have to be added. For renaming an entry * the path has to contain a new filename too. * @param entriesWithoutChildren Entries count, excluding their children. For AddJob or CopyJob 0 MUST be passed. * * @return For entries * some/dir/ * some/dir/entry * some/dir/some/entry * some/another/entry * and destination * some/destination * will return * some/destination/dir/ * some/destination/dir/entry * some/destination/dir/some/enty * some/destination/entry */ static QStringList entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren); /** - * @return true if the interface has killed the job or if it will stop it as soon as possible. - * Otherwise returns false if the interface is not able to kill the operation. + * @return true if the interface has killed the job. + * Otherwise returns false if the interface is not able to instantly kill the operation. */ virtual bool doKill(); bool isHeaderEncryptionEnabled() const; virtual QString multiVolumeName() const; void setMultiVolume(bool value); uint numberOfEntries() const; QMimeType mimetype() const; /** * @return Whether the interface supports progress reporting for BatchExtractJobs. */ virtual bool hasBatchExtractionProgress() const; /** * @return Whether the archive is locked (RAR feature). */ virtual bool isLocked() const; Q_SIGNALS: /** * Emitted when the user cancels the operation. Examples: * - the user cancels the password dialog * - the user cancels the overwrite dialog */ void cancelled(); void error(const QString &message, const QString &details = QString()); void entry(Archive::Entry *archiveEntry); void progress(double progress); void info(const QString &info); void finished(bool result); void testSuccess(); void compressionMethodFound(const QString &method); void encryptionMethodFound(const QString &method); /** * Emitted when @p query needs to be executed on the GUI thread. */ void userQuery(Kerfuffle::Query *query); protected: /** * Setting this option to true will NOT run the functions in their own thread. * Instead it will be necessary to call finished(bool) when the operation is actually finished. */ void setWaitForFinishedSignal(bool value); void setCorrupt(bool isCorrupt); bool isCorrupt() const; QString m_comment; int m_numberOfVolumes; uint m_numberOfEntries; KPluginMetaData m_metaData; private: QString m_filename; QMimeType m_mimetype; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; bool m_isMultiVolume; private Q_SLOTS: void onEntry(Archive::Entry *archiveEntry); }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: enum OperationMode { NoOperation, List, Extract, Add, Move, Copy, Delete, Comment, Test }; explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList &args); ~ReadWriteArchiveInterface() override; bool isReadOnly() const override; /** * Adds the given @p files under the given @p destination. * If @p destination is null, the files will be added under the root of the archive. * @param options The compression options that must be respected. * @param numberOfEntriesToAdd The total number of entries the will be added. * @return Whether the operation succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd = 0) = 0; virtual bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool deleteFiles(const QVector &files) = 0; virtual bool addComment(const QString &comment) = 0; Q_SIGNALS: void entryRemoved(const QString &path); protected: OperationMode m_operationMode = NoOperation; private Q_SLOTS: void onEntryRemoved(const QString &path); }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 422a5a24..abdf5432 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,857 +1,853 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jobs.h" #include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include #include #include #include namespace Kerfuffle { class Job::Private : public QThread { Q_OBJECT public: Private(Job *job, QObject *parent = nullptr) : QThread(parent) , q(job) { } void run() override; private: Job *q; }; void Job::Private::run() { q->doWork(); } Job::Job(Archive *archive, ReadOnlyArchiveInterface *interface) : KJob() , m_archive(archive) , m_archiveInterface(interface) , d(new Private(this)) { setCapabilities(KJob::Killable); } Job::Job(Archive *archive) : Job(archive, nullptr) {} Job::Job(ReadOnlyArchiveInterface *interface) : Job(nullptr, interface) {} Job::~Job() { if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { // Use the archive interface. if (archive()) { return archive()->interface(); } // Use the interface passed to this job (e.g. JSONArchiveInterface in jobstest.cpp). return m_archiveInterface; } Archive *Job::archive() const { return m_archive; } QString Job::errorString() const { if (!errorText().isEmpty()) { return errorText(); } if (archive()) { if (archive()->error() == NoPlugin) { return i18n("No suitable plugin found. Ark does not seem to support this file type."); } if (archive()->error() == FailedPlugin) { return i18n("Failed to load a suitable plugin. Make sure any executables needed to handle the archive type are installed."); } } return QString(); } void Job::start() { jobTimer.start(); // We have an archive but it's not valid, nothing to do. if (archive() && !archive()->isValid()) { QTimer::singleShot(0, this, [=]() { onFinished(false); }); return; } if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); auto readWriteInterface = qobject_cast(archiveInterface()); if (readWriteInterface) { connect(readWriteInterface, &ReadWriteArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); } } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(Archive::Entry *entry) { emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; if (archive() && !archive()->isValid()) { setError(KJob::UserDefinedError); } if (!d->isInterruptionRequested()) { emitResult(); } } void Job::onUserQuery(Query *query) { if (archiveInterface()->waitForFinishedSignal()) { qCWarning(ARK) << "Plugins run from the main thread should call directly query->execute()"; } emit userQuery(query); } bool Job::doKill() { - const bool canKillJob = archiveInterface()->doKill(); - - if (!d->isRunning()) { - return canKillJob; + const bool killed = archiveInterface()->doKill(); + if (killed) { + return true; } - d->requestInterruption(); - - if (!canKillJob) { - qCDebug(ARK) << "Forcing thread exit in one second..."; + if (d->isRunning()) { + qCDebug(ARK) << "Requesting graceful thread interruption, will abort in one second otherwise."; + d->requestInterruption(); d->wait(1000); - return true; } - d->wait(); - return canKillJob; + return true; } LoadJob::LoadJob(Archive *archive, ReadOnlyArchiveInterface *interface) : Job(archive, interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "Created job instance"; connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry); } LoadJob::LoadJob(Archive *archive) : LoadJob(archive, nullptr) {} LoadJob::LoadJob(ReadOnlyArchiveInterface *interface) : LoadJob(nullptr, interface) {} void LoadJob::doWork() { emit description(this, i18n("Loading archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { // onFinished() needs to be called after onNewEntry(), because the former reads members set in the latter. // So we need to put it in the event queue, just like the single-thread case does by emitting finished(). QTimer::singleShot(0, this, [=]() { onFinished(ret); }); } } void LoadJob::onFinished(bool result) { if (archive() && result) { archive()->setProperty("unpackedSize", extractedFilesSize()); archive()->setProperty("isSingleFolder", isSingleFolderArchive()); const auto name = subfolderName().isEmpty() ? archive()->completeBaseName() : subfolderName(); archive()->setProperty("subfolderName", name); if (isPasswordProtected()) { archive()->setProperty("encryptionType", archive()->password().isEmpty() ? Archive::Encrypted : Archive::HeaderEncrypted); } } Job::onFinished(result); } qlonglong LoadJob::extractedFilesSize() const { return m_extractedFilesSize; } bool LoadJob::isPasswordProtected() const { return m_isPasswordProtected; } bool LoadJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void LoadJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. const QString fullPath = entry->fullPath().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString LoadJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } BatchExtractJob::BatchExtractJob(LoadJob *loadJob, const QString &destination, bool autoSubfolder, bool preservePaths) : Job(loadJob->archive()) , m_loadJob(loadJob) , m_destination(destination) , m_autoSubfolder(autoSubfolder) , m_preservePaths(preservePaths) { qCDebug(ARK) << "Created job instance"; } void BatchExtractJob::doWork() { connect(m_loadJob, &KJob::result, this, &BatchExtractJob::slotLoadingFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &BatchExtractJob::onCancelled); if (archiveInterface()->hasBatchExtractionProgress()) { // progress() will be actually emitted by the LoadJob, but the archiveInterface() is the same. connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); } // Forward LoadJob's signals. connect(m_loadJob, &Kerfuffle::Job::newEntry, this, &BatchExtractJob::newEntry); connect(m_loadJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); m_loadJob->start(); } bool BatchExtractJob::doKill() { if (m_step == Loading) { return m_loadJob->kill(); } return m_extractJob->kill(); } void BatchExtractJob::slotLoadingProgress(double progress) { // Progress from LoadJob counts only for 50% of the BatchExtractJob's duration. m_lastPercentage = static_cast(50.0*progress); setPercent(m_lastPercentage); } void BatchExtractJob::slotExtractProgress(double progress) { // The 2nd 50% of the BatchExtractJob's duration comes from the ExtractJob. setPercent(m_lastPercentage + static_cast(50.0*progress)); } void BatchExtractJob::slotLoadingFinished(KJob *job) { if (job->error()) { // Forward errors as well. onError(job->errorString(), QString()); onFinished(false); return; } // Now we can start extraction. setupDestination(); Kerfuffle::ExtractionOptions options; options.setPreservePaths(m_preservePaths); m_extractJob = archive()->extractFiles({}, m_destination, options); if (m_extractJob) { connect(m_extractJob, &KJob::result, this, &BatchExtractJob::emitResult); connect(m_extractJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); if (archiveInterface()->hasBatchExtractionProgress()) { // The LoadJob is done, change slot and start setting the percentage from m_lastPercentage on. disconnect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotExtractProgress); } m_step = Extracting; m_extractJob->start(); } else { emitResult(); } } void BatchExtractJob::setupDestination() { const bool isSingleFolderRPM = (archive()->isSingleFolder() && (archive()->mimeType().name() == QLatin1String("application/x-rpm"))); if (m_autoSubfolder && (!archive()->isSingleFolder() || isSingleFolderRPM)) { const QDir d(m_destination); QString subfolderName = archive()->subfolderName(); // Special case for single folder RPM archives. // We don't want the autodetected folder to have a meaningless "usr" name. if (isSingleFolderRPM && subfolderName == QStringLiteral("usr")) { qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name"; subfolderName = QFileInfo(archive()->fileName()).completeBaseName(); } if (d.exists(subfolderName)) { subfolderName = KIO::suggestName(QUrl::fromUserInput(m_destination, QString(), QUrl::AssumeLocalFile), subfolderName); } d.mkdir(subfolderName); m_destination += QLatin1Char( '/' ) + subfolderName; } } CreateJob::CreateJob(Archive *archive, const QVector &entries, const CompressionOptions &options) : Job(archive) , m_entries(entries) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void CreateJob::enableEncryption(const QString &password, bool encryptHeader) { archive()->encrypt(password, encryptHeader); } void CreateJob::setMultiVolume(bool isMultiVolume) { archive()->setMultiVolume(isMultiVolume); } void CreateJob::doWork() { m_addJob = archive()->addFiles(m_entries, nullptr, m_options); if (m_addJob) { connect(m_addJob, &KJob::result, this, &CreateJob::emitResult); // Forward description signal from AddJob, we need to change the first argument ('this' needs to be a CreateJob). connect(m_addJob, &KJob::description, this, [=](KJob *, const QString &title, const QPair &field1, const QPair &) { emit description(this, title, field1); }); connect(m_addJob, QOverload::of(&KJob::percent), this, [=](KJob*, unsigned long percent) { emitPercent(percent, 100); }); m_addJob->start(); } else { emitResult(); } } bool CreateJob::doKill() { return m_addJob && m_addJob->kill(); } ExtractJob::ExtractJob(const QVector &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void ExtractJob::doWork() { QString desc; if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); } emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()), qMakePair(i18nc("extraction folder", "Destination"), m_destinationDir)); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files." << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { m_tmpExtractDir = new QTemporaryDir(); } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; if (m_passwordProtectedHint) { options.setEncryptedArchiveHint(true); } return options; } QTemporaryDir *TempExtractJob::tempDir() const { return m_tmpExtractDir; } void TempExtractJob::doWork() { // pass 1 to i18np on purpose so this translation may properly be reused. emit description(this, i18np("Extracting one file", "Extracting %1 files", 1)); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_entry; bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString TempExtractJob::extractionDir() const { return m_tmpExtractDir->path(); } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } AddJob::AddJob(const QVector &entries, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void AddJob::doWork() { // Set current dir. const QString globalWorkDir = m_options.globalWorkDir(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // Count total number of entries to be added. uint totalCount = 0; QElapsedTimer timer; timer.start(); for (const Archive::Entry* entry : qAsConst(m_entries)) { totalCount++; if (QFileInfo(entry->fullPath()).isDir()) { QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); totalCount++; } } } qCDebug(ARK) << "Going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms"; const QString desc = i18np("Compressing a file", "Compressing %1 files", totalCount); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); // The file paths must be relative to GlobalWorkDir. for (Archive::Entry *entry : qAsConst(m_entries)) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically const QString &fullPath = entry->fullPath(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options, totalCount); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } MoveJob::MoveJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void MoveJob::doWork() { qCDebug(ARK) << "Going to move" << m_entries.count() << "file(s)"; QString desc = i18np("Moving a file", "Moving %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void MoveJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) { Job::onFinished(result); } } CopyJob::CopyJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void CopyJob::doWork() { qCDebug(ARK) << "Going to copy" << m_entries.count() << "file(s)"; QString desc = i18np("Copying a file", "Copying %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void CopyJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) { Job::onFinished(result); } } DeleteJob::DeleteJob(const QVector &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { QString desc = i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "Job started"; emit description(this, i18n("Testing archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle #include "jobs.moc" diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index e8a38950..6a1df53b 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,577 +1,577 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libarchiveplugin.h" #include "ark_debug.h" #include "queries.h" #include #include #include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList &args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); connect(this, &ReadOnlyArchiveInterface::error, this, &LibarchivePlugin::slotRestoreWorkingDir); connect(this, &ReadOnlyArchiveInterface::cancelled, this, &LibarchivePlugin::slotRestoreWorkingDir); } LibarchivePlugin::~LibarchivePlugin() { for (const auto e : qAsConst(m_emittedEntries)) { // Entries might be passed to pending slots, so we just schedule their deletion. e->deleteLater(); } } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; if (!initializeReader()) { return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(m_archiveReader.data(), 0); QString compMethod = convertCompressionName(QString::fromUtf8(archive_filter_name(m_archiveReader.data(), 0))); if (!compMethod.isEmpty()) { emit compressionMethodFound(compMethod); } m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; m_numberOfEntries = 0; auto compressedArchiveSize = QFileInfo(filename()).size(); struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!QThread::currentThread()->isInterruptionRequested() && (result = archive_read_next_header(m_archiveReader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(m_archiveReader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); emit progress(float(archive_filter_bytes(m_archiveReader.data(), -1))/float(compressedArchiveSize)); m_cachedArchiveEntryCount++; archive_read_data_skip(m_archiveReader.data()); } if (result != ARCHIVE_EOF) { qCWarning(ARK) << "Could not read until the end of the archive:" << QLatin1String(archive_error_string(m_archiveReader.data())); return false; } return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions &options, uint numberOfEntriesToAdd) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) Q_UNUSED(numberOfEntriesToAdd) return false; } bool LibarchivePlugin::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QVector &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString &comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } bool LibarchivePlugin::hasBatchExtractionProgress() const { return true; } bool LibarchivePlugin::doKill() { - return true; + return false; } bool LibarchivePlugin::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { if (!initializeReader()) { return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int totalEntriesCount = 0; const bool extractAll = files.isEmpty(); if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalEntriesCount = m_cachedArchiveEntryCount; } else { totalEntriesCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalEntriesCount << "entries"; qCDebug(ARK) << "Changing current directory to " << destinationDirectory; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(destinationDirectory); // Initialize variables. const bool preservePaths = options.preservePaths(); const bool removeRootNode = options.isDragAndDropEnabled(); bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int extractedEntriesCount = 0; int progressEntryCount = 0; struct archive_entry *entry; QString fileBeingRenamed; // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's empty. const QStringList fullPaths = entryFullPaths(files); QStringList remainingFiles = entryFullPaths(files); // Iterate through all entries in archive. while (!QThread::currentThread()->isInterruptionRequested() && (archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index = -1; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(m_archiveReader.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // Some archive types e.g. AppImage prepend all entries with "./" so remove this part. if (entryName.startsWith(QLatin1String("./"))) { entryName.remove(0, 2); } // Static libraries (*.a) contain the two entries "/" and "//". // We just skip these to allow extracting this archive type. if (entryName == QLatin1String("/") || entryName == QLatin1String("//")) { archive_read_data_skip(m_archiveReader.data()); continue; } // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } // Should the entry be extracted? if (extractAll || remainingFiles.contains(entryName) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = fullPaths.indexOf(entryName); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) { const QString &rootNode = files.at(index)->rootNode; if (!rootNode.isEmpty()) { const QString truncatedFilename(entryName.remove(entryName.indexOf(rootNode), rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(m_archiveReader.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, m_archiveReader.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(i18nc("@info", "Fatal error, extraction aborted.")); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++progressEntryCount; emit progress(float(progressEntryCount) / totalEntriesCount); } extractedEntriesCount++; remainingFiles.removeOne(entryName); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(m_archiveReader.data()); } } qCDebug(ARK) << "Extracted" << extractedEntriesCount << "entries"; slotRestoreWorkingDir(); return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::initializeReader() { m_archiveReader.reset(archive_read_new()); if (!(m_archiveReader.data())) { emit error(i18n("The archive reader could not be initialized.")); return false; } if (archive_read_support_filter_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(m_archiveReader.data(), QFile::encodeName(filename()).constData(), 10240) != ARCHIVE_OK) { qCWarning(ARK) << "Could not open the archive:" << archive_error_string(m_archiveReader.data()); emit error(i18nc("@info", "Archive corrupted or insufficient permissions.")); return false; } return true; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { auto e = new Archive::Entry(); #ifdef Q_OS_WIN e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry)))); #else e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e->setProperty("group", group); } e->compressedSizeIsSet = false; e->setProperty("size", (qlonglong)archive_entry_size(aentry)); e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } auto time = static_cast(archive_entry_mtime(aentry)); e->setProperty("timestamp", QDateTime::fromTime_t(time)); emit entry(e); m_emittedEntries << e; } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } auto readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0 && !QThread::currentThread()->isInterruptionRequested()) { archive_write_data(dest, buff, static_cast(readBytes)); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; auto readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0 && !QThread::currentThread()->isInterruptionRequested()) { archive_write_data(dest, buff, static_cast(readBytes)); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } void LibarchivePlugin::slotRestoreWorkingDir() { if (m_oldWorkingDir.isEmpty()) { return; } if (!QDir::setCurrent(m_oldWorkingDir)) { qCWarning(ARK) << "Failed to restore old working directory:" << m_oldWorkingDir; } else { m_oldWorkingDir.clear(); } } QString LibarchivePlugin::convertCompressionName(const QString &method) { if (method == QLatin1String("gzip")) { return QStringLiteral("GZip"); } else if (method == QLatin1String("bzip2")) { return QStringLiteral("BZip2"); } else if (method == QLatin1String("xz")) { return QStringLiteral("XZ"); } else if (method == QLatin1String("compress (.Z)")) { return QStringLiteral("Compress"); } else if (method == QLatin1String("lrzip")) { return QStringLiteral("LRZip"); } else if (method == QLatin1String("lzip")) { return QStringLiteral("LZip"); } else if (method == QLatin1String("lz4")) { return QStringLiteral("LZ4"); } else if (method == QLatin1String("lzop")) { return QStringLiteral("lzop"); } else if (method == QLatin1String("lzma")) { return QStringLiteral("LZMA"); } else if (method == QLatin1String("zstd")) { return QStringLiteral("Zstandard"); } return QString(); } diff --git a/plugins/libzipplugin/libzipplugin.cpp b/plugins/libzipplugin/libzipplugin.cpp index 6209ec9a..020493fd 100644 --- a/plugins/libzipplugin/libzipplugin.cpp +++ b/plugins/libzipplugin/libzipplugin.cpp @@ -1,986 +1,961 @@ /* * Copyright (c) 2017 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libzipplugin.h" #include "ark_debug.h" #include "queries.h" #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_CLASS_WITH_JSON(LibzipPlugin, "kerfuffle_libzip.json") void LibzipPlugin::progressCallback(zip_t *, double progress, void *that) { static_cast(that)->emitProgress(progress); } LibzipPlugin::LibzipPlugin(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) , m_overwriteAll(false) , m_skipAll(false) , m_listAfterAdd(false) { qCDebug(ARK) << "Initializing libzip plugin"; } LibzipPlugin::~LibzipPlugin() { for (const auto e : qAsConst(m_emittedEntries)) { // Entries might be passed to pending slots, so we just schedule their deletion. e->deleteLater(); } } bool LibzipPlugin::list() { qCDebug(ARK) << "Listing archive contents for:" << QFile::encodeName(filename()); - setOperationMode(List); m_numberOfEntries = 0; int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), ZIP_RDONLY, &errcode); zip_error_init_with_code(&err, errcode); if (!archive) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } // Fetch archive comment. m_comment = QString::fromLocal8Bit(zip_get_archive_comment(archive, nullptr, ZIP_FL_ENC_RAW)); // Get number of archive entries. const auto nofEntries = zip_get_num_entries(archive, 0); qCDebug(ARK) << "Found entries:" << nofEntries; // Loop through all archive entries. for (int i = 0; i < nofEntries; i++) { if (QThread::currentThread()->isInterruptionRequested()) { break; } emitEntryForIndex(archive, i); if (m_listAfterAdd) { // Start at 50%. emit progress(0.5 + (0.5 * float(i + 1) / nofEntries)); } else { emit progress(float(i + 1) / nofEntries); } } zip_close(archive); m_listAfterAdd = false; return true; } bool LibzipPlugin::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd) { Q_UNUSED(numberOfEntriesToAdd) - setOperationMode(Add); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), ZIP_CREATE, &errcode); zip_error_init_with_code(&err, errcode); if (!archive) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } uint i = 0; for (const Archive::Entry* e : files) { if (QThread::currentThread()->isInterruptionRequested()) { break; } // If entry is a directory, traverse and add all its files and subfolders. if (QFileInfo(e->fullPath()).isDir()) { if (!writeEntry(archive, e->fullPath(), destination, options, true)) { return false; } QDirIterator it(e->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (!QThread::currentThread()->isInterruptionRequested() && it.hasNext()) { const QString path = it.next(); if (QFileInfo(path).isDir()) { if (!writeEntry(archive, path, destination, options, true)) { return false; } } else { if (!writeEntry(archive, path, destination, options)) { return false; } } i++; } } else { if (!writeEntry(archive, e->fullPath(), destination, options)) { return false; } } i++; } qCDebug(ARK) << "Added" << i << "entries"; // Register the callback function to get progress feedback. zip_register_progress_callback_with_state(archive, 0.001, progressCallback, nullptr, this); qCDebug(ARK) << "Writing entries to disk..."; if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } // We list the entire archive after adding files to ensure entry // properties are up-to-date. m_listAfterAdd = true; list(); return true; } void LibzipPlugin::emitProgress(double percentage) { // Go from 0 to 50%. The second half is the subsequent listing. emit progress(0.5 * percentage); } bool LibzipPlugin::writeEntry(zip_t *archive, const QString &file, const Archive::Entry* destination, const CompressionOptions& options, bool isDir) { Q_ASSERT(archive); QByteArray destFile; if (destination) { destFile = QString(destination->fullPath() + file).toUtf8(); } else { destFile = file.toUtf8(); } qlonglong index; if (isDir) { index = zip_dir_add(archive, destFile.constData(), ZIP_FL_ENC_GUESS); if (index == -1) { // If directory already exists in archive, we get an error. qCWarning(ARK) << "Failed to add dir " << file << ":" << zip_strerror(archive); return true; } } else { zip_source_t *src = zip_source_file(archive, QFile::encodeName(file).constData(), 0, -1); Q_ASSERT(src); index = zip_file_add(archive, destFile.constData(), src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE); if (index == -1) { zip_source_free(src); qCCritical(ARK) << "Could not add entry" << file << ":" << zip_strerror(archive); emit error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } } #ifndef Q_OS_WIN // Set permissions. QT_STATBUF result; if (QT_STAT(QFile::encodeName(file).constData(), &result) != 0) { qCWarning(ARK) << "Failed to read permissions for:" << file; } else { zip_uint32_t attributes = result.st_mode << 16; if (zip_file_set_external_attributes(archive, index, ZIP_FL_UNCHANGED, ZIP_OPSYS_UNIX, attributes) != 0) { qCWarning(ARK) << "Failed to set external attributes for:" << file; } } #endif if (!password().isEmpty()) { Q_ASSERT(!options.encryptionMethod().isEmpty()); if (options.encryptionMethod() == QLatin1String("AES128")) { zip_file_set_encryption(archive, index, ZIP_EM_AES_128, password().toUtf8().constData()); } else if (options.encryptionMethod() == QLatin1String("AES192")) { zip_file_set_encryption(archive, index, ZIP_EM_AES_192, password().toUtf8().constData()); } else if (options.encryptionMethod() == QLatin1String("AES256")) { zip_file_set_encryption(archive, index, ZIP_EM_AES_256, password().toUtf8().constData()); } } // Set compression level and method. zip_int32_t compMethod = ZIP_CM_DEFAULT; if (!options.compressionMethod().isEmpty()) { if (options.compressionMethod() == QLatin1String("Deflate")) { compMethod = ZIP_CM_DEFLATE; } else if (options.compressionMethod() == QLatin1String("BZip2")) { compMethod = ZIP_CM_BZIP2; } else if (options.compressionMethod() == QLatin1String("Store")) { compMethod = ZIP_CM_STORE; } } const int compLevel = options.isCompressionLevelSet() ? options.compressionLevel() : 6; if (zip_set_file_compression(archive, index, compMethod, compLevel) != 0) { qCCritical(ARK) << "Could not set compression options for" << file << ":" << zip_strerror(archive); emit error(xi18n("Failed to set compression options for entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } return true; } bool LibzipPlugin::emitEntryForIndex(zip_t *archive, qlonglong index) { Q_ASSERT(archive); zip_stat_t statBuffer; if (zip_stat_index(archive, index, ZIP_FL_ENC_GUESS, &statBuffer)) { qCCritical(ARK) << "Failed to read stat for index" << index; return false; } auto e = new Archive::Entry(); if (statBuffer.valid & ZIP_STAT_NAME) { e->setFullPath(QString::fromUtf8(statBuffer.name)); } if (e->fullPath(PathFormat::WithTrailingSlash).endsWith(QDir::separator())) { e->setProperty("isDirectory", true); } if (statBuffer.valid & ZIP_STAT_MTIME) { e->setProperty("timestamp", QDateTime::fromTime_t(statBuffer.mtime)); } if (statBuffer.valid & ZIP_STAT_SIZE) { e->setProperty("size", (qulonglong)statBuffer.size); } if (statBuffer.valid & ZIP_STAT_COMP_SIZE) { e->setProperty("compressedSize", (qlonglong)statBuffer.comp_size); } if (statBuffer.valid & ZIP_STAT_CRC) { if (!e->isDir()) { e->setProperty("CRC", QString::number((qulonglong)statBuffer.crc, 16).toUpper()); } } if (statBuffer.valid & ZIP_STAT_COMP_METHOD) { switch(statBuffer.comp_method) { case ZIP_CM_STORE: e->setProperty("method", QStringLiteral("Store")); emit compressionMethodFound(QStringLiteral("Store")); break; case ZIP_CM_DEFLATE: e->setProperty("method", QStringLiteral("Deflate")); emit compressionMethodFound(QStringLiteral("Deflate")); break; case ZIP_CM_DEFLATE64: e->setProperty("method", QStringLiteral("Deflate64")); emit compressionMethodFound(QStringLiteral("Deflate64")); break; case ZIP_CM_BZIP2: e->setProperty("method", QStringLiteral("BZip2")); emit compressionMethodFound(QStringLiteral("BZip2")); break; case ZIP_CM_LZMA: e->setProperty("method", QStringLiteral("LZMA")); emit compressionMethodFound(QStringLiteral("LZMA")); break; case ZIP_CM_XZ: e->setProperty("method", QStringLiteral("XZ")); emit compressionMethodFound(QStringLiteral("XZ")); break; } } if (statBuffer.valid & ZIP_STAT_ENCRYPTION_METHOD) { if (statBuffer.encryption_method != ZIP_EM_NONE) { e->setProperty("isPasswordProtected", true); switch(statBuffer.encryption_method) { case ZIP_EM_TRAD_PKWARE: emit encryptionMethodFound(QStringLiteral("ZipCrypto")); break; case ZIP_EM_AES_128: emit encryptionMethodFound(QStringLiteral("AES128")); break; case ZIP_EM_AES_192: emit encryptionMethodFound(QStringLiteral("AES192")); break; case ZIP_EM_AES_256: emit encryptionMethodFound(QStringLiteral("AES256")); break; } } } // Read external attributes, which contains the file permissions. zip_uint8_t opsys; zip_uint32_t attributes; if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) { qCCritical(ARK) << "Could not read external attributes for entry:" << QString::fromUtf8(statBuffer.name); emit error(xi18n("Failed to read metadata for entry: %1", QString::fromUtf8(statBuffer.name))); return false; } // Set permissions. switch (opsys) { case ZIP_OPSYS_UNIX: // Unix permissions are stored in the leftmost 16 bits of the external file attribute. e->setProperty("permissions", permissionsToString(attributes >> 16)); break; default: // TODO: non-UNIX. break; } emit entry(e); m_emittedEntries << e; return true; } bool LibzipPlugin::deleteFiles(const QVector &files) { - setOperationMode(Delete); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } qulonglong i = 0; for (const Archive::Entry* e : files) { if (QThread::currentThread()->isInterruptionRequested()) { break; } const qlonglong index = zip_name_locate(archive, e->fullPath().toUtf8().constData(), ZIP_FL_ENC_GUESS); if (index == -1) { qCCritical(ARK) << "Could not find entry to delete:" << e->fullPath(); emit error(xi18n("Failed to delete entry: %1", e->fullPath())); return false; } if (zip_delete(archive, index) == -1) { qCCritical(ARK) << "Could not delete entry" << e->fullPath() << ":" << zip_strerror(archive); emit error(xi18n("Failed to delete entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } emit entryRemoved(e->fullPath()); emit progress(float(++i) / files.size()); } qCDebug(ARK) << "Deleted" << i << "entries"; if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } return true; } bool LibzipPlugin::addComment(const QString& comment) { - setOperationMode(Comment); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } // Set archive comment. if (zip_set_archive_comment(archive, comment.toUtf8().constData(), comment.length())) { qCCritical(ARK) << "Failed to set comment:" << zip_strerror(archive); return false; } if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } return true; } bool LibzipPlugin::testArchive() { qCDebug(ARK) << "Testing archive"; - setOperationMode(Test); int errcode = 0; zip_error_t err; // Open archive performing extra consistency checks. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), ZIP_CHECKCONS, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive:" << zip_error_strerror(&err); return false; } // Check CRC-32 for each archive entry. const int nofEntries = zip_get_num_entries(archive, 0); for (int i = 0; i < nofEntries; i++) { if (QThread::currentThread()->isInterruptionRequested()) { return false; } // Get statistic for entry. Used to get entry size. zip_stat_t statBuffer; if (zip_stat_index(archive, i, 0, &statBuffer) != 0) { qCCritical(ARK) << "Failed to read stat for" << statBuffer.name; return false; } zip_file *zipFile = zip_fopen_index(archive, i, 0); std::unique_ptr buf(new uchar[statBuffer.size]); const int len = zip_fread(zipFile, buf.get(), statBuffer.size); if (len == -1 || uint(len) != statBuffer.size) { qCCritical(ARK) << "Failed to read data for" << statBuffer.name; return false; } if (statBuffer.crc != crc32(0, &buf.get()[0], len)) { qCCritical(ARK) << "CRC check failed for" << statBuffer.name; return false; } emit progress(float(i) / nofEntries); } zip_close(archive); emit testSuccess(); return true; } bool LibzipPlugin::doKill() { - QMutexLocker mutexLocker(&m_mutex); - switch (m_operationMode) { - case Add: - case Copy: - case Delete: - case Move: - return false; - default: - break; - } - - return true; + return false; } bool LibzipPlugin::extractFiles(const QVector &files, const QString& destinationDirectory, const ExtractionOptions& options) { qCDebug(ARK) << "Extracting files to:" << destinationDirectory; - setOperationMode(Extract); const bool extractAll = files.isEmpty(); const bool removeRootNode = options.isDragAndDropEnabled(); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), ZIP_RDONLY, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } // Set password if known. if (!password().isEmpty()) { qCDebug(ARK) << "Password already known. Setting..."; zip_set_default_password(archive, password().toUtf8().constData()); } // Get number of archive entries. const qlonglong nofEntries = extractAll ? zip_get_num_entries(archive, 0) : files.size(); // Extract entries. m_overwriteAll = false; // Whether to overwrite all files m_skipAll = false; // Whether to skip all files if (extractAll) { // We extract all entries. for (qlonglong i = 0; i < nofEntries; i++) { if (QThread::currentThread()->isInterruptionRequested()) { break; } if (!extractEntry(archive, QDir::fromNativeSeparators(QString::fromUtf8(zip_get_name(archive, i, ZIP_FL_ENC_GUESS))), QString(), destinationDirectory, options.preservePaths(), removeRootNode)) { qCDebug(ARK) << "Extraction failed"; return false; } emit progress(float(i + 1) / nofEntries); } } else { // We extract only the entries in files. qulonglong i = 0; for (const Archive::Entry* e : files) { if (QThread::currentThread()->isInterruptionRequested()) { break; } if (!extractEntry(archive, e->fullPath(), e->rootNode, destinationDirectory, options.preservePaths(), removeRootNode)) { qCDebug(ARK) << "Extraction failed"; return false; } emit progress(float(++i) / nofEntries); } } zip_close(archive); return true; } bool LibzipPlugin::extractEntry(zip_t *archive, const QString &entry, const QString &rootNode, const QString &destDir, bool preservePaths, bool removeRootNode) { const bool isDirectory = entry.endsWith(QDir::separator()); // Add trailing slash to destDir if not present. QString destDirCorrected(destDir); if (!destDir.endsWith(QDir::separator())) { destDirCorrected.append(QDir::separator()); } // Remove rootnode if supplied and set destination path. QString destination; if (preservePaths) { if (!removeRootNode || rootNode.isEmpty()) { destination = destDirCorrected + entry; } else { QString truncatedEntry = entry; truncatedEntry.remove(0, rootNode.size()); destination = destDirCorrected + truncatedEntry; } } else { if (isDirectory) { qCDebug(ARK) << "Skipping directory:" << entry; return true; } destination = destDirCorrected + QFileInfo(entry).fileName(); } // Store parent mtime. QString parentDir; if (isDirectory) { QDir pDir = QFileInfo(destination).dir(); pDir.cdUp(); parentDir = pDir.path(); } else { parentDir = QFileInfo(destination).path(); } // For top-level items, don't restore parent dir mtime. const bool restoreParentMtime = (parentDir + QDir::separator() != destDirCorrected); time_t parent_mtime; if (restoreParentMtime) { parent_mtime = QFileInfo(parentDir).lastModified().toMSecsSinceEpoch() / 1000; } // Create parent directories for files. For directories create them. if (!QDir().mkpath(QFileInfo(destination).path())) { qCDebug(ARK) << "Failed to create directory:" << QFileInfo(destination).path(); emit error(xi18n("Failed to create directory: %1", QFileInfo(destination).path())); return false; } // Get statistic for entry. Used to get entry size and mtime. zip_stat_t statBuffer; if (zip_stat(archive, entry.toUtf8().constData(), 0, &statBuffer) != 0) { if (isDirectory && zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOENT) { qCWarning(ARK) << "Skipping folder without entry:" << entry; return true; } qCCritical(ARK) << "Failed to read stat for entry" << entry; return false; } if (!isDirectory) { // Handle existing destination files. QString renamedEntry = entry; while (!m_overwriteAll && QFileInfo::exists(destination)) { if (m_skipAll) { return true; } else { Kerfuffle::OverwriteQuery query(renamedEntry); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } else if (query.responseSkip()) { return true; } else if (query.responseAutoSkip()) { m_skipAll = true; return true; } else if (query.responseRename()) { const QString newName(query.newFilename()); destination = QFileInfo(destination).path() + QDir::separator() + QFileInfo(newName).fileName(); renamedEntry = QFileInfo(entry).path() + QDir::separator() + QFileInfo(newName).fileName(); } else if (query.responseOverwriteAll()) { m_overwriteAll = true; break; } else if (query.responseOverwrite()) { break; } } } // Handle password-protected files. zip_file *zipFile = nullptr; bool firstTry = true; while (!zipFile) { zipFile = zip_fopen(archive, entry.toUtf8().constData(), 0); if (zipFile) { break; } else if (zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOPASSWD || zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_WRONGPASSWD) { Kerfuffle::PasswordNeededQuery query(filename(), !firstTry); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); if (zip_set_default_password(archive, password().toUtf8().constData())) { qCDebug(ARK) << "Failed to set password for:" << entry; } firstTry = false; } else { qCCritical(ARK) << "Failed to open file:" << zip_strerror(archive); emit error(xi18n("Failed to open '%1':%2", entry, QString::fromUtf8(zip_strerror(archive)))); return false; } } QFile file(destination); if (!file.open(QIODevice::WriteOnly)) { qCCritical(ARK) << "Failed to open file for writing"; emit error(xi18n("Failed to open file for writing: %1", destination)); return false; } QDataStream out(&file); // Write archive entry to file. We use a read/write buffer of 1000 chars. qulonglong sum = 0; char buf[1000]; while (sum != statBuffer.size) { const auto readBytes = zip_fread(zipFile, buf, 1000); if (readBytes < 0) { qCCritical(ARK) << "Failed to read data"; emit error(xi18n("Failed to read data for entry: %1", entry)); return false; } if (out.writeRawData(buf, readBytes) != readBytes) { qCCritical(ARK) << "Failed to write data"; emit error(xi18n("Failed to write data for entry: %1", entry)); return false; } sum += readBytes; } const auto index = zip_name_locate(archive, entry.toUtf8().constData(), ZIP_FL_ENC_GUESS); if (index == -1) { qCCritical(ARK) << "Could not locate entry:" << entry; emit error(xi18n("Failed to locate entry: %1", entry)); return false; } zip_uint8_t opsys; zip_uint32_t attributes; if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) { qCCritical(ARK) << "Could not read external attributes for entry:" << entry; emit error(xi18n("Failed to read metadata for entry: %1", entry)); return false; } // Inspired by fuse-zip source code: fuse-zip/lib/fileNode.cpp switch (opsys) { case ZIP_OPSYS_UNIX: // Unix permissions are stored in the leftmost 16 bits of the external file attribute. file.setPermissions(KIO::convertPermissions(attributes >> 16)); break; default: // TODO: non-UNIX. break; } file.close(); } // Set mtime for entry. utimbuf times; times.modtime = statBuffer.mtime; if (utime(destination.toUtf8().constData(), ×) != 0) { qCWarning(ARK) << "Failed to restore mtime:" << destination; } if (restoreParentMtime) { // Restore mtime for parent dir. times.modtime = parent_mtime; if (utime(parentDir.toUtf8().constData(), ×) != 0) { qCWarning(ARK) << "Failed to restore mtime for parent dir of:" << destination; } } return true; } bool LibzipPlugin::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options) - setOperationMode(Move); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } QStringList filePaths = entryFullPaths(files); filePaths.sort(); const QStringList destPaths = entryPathsFromDestination(filePaths, destination, entriesWithoutChildren(files).count()); int i; for (i = 0; i < filePaths.size(); ++i) { const int index = zip_name_locate(archive, filePaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS); if (index == -1) { qCCritical(ARK) << "Could not find entry to move:" << filePaths.at(i); emit error(xi18n("Failed to move entry: %1", filePaths.at(i))); return false; } if (zip_file_rename(archive, index, destPaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS) == -1) { qCCritical(ARK) << "Could not move entry:" << filePaths.at(i); emit error(xi18n("Failed to move entry: %1", filePaths.at(i))); return false; } emit entryRemoved(filePaths.at(i)); emitEntryForIndex(archive, index); emit progress(i/filePaths.count()); } if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } qCDebug(ARK) << "Moved" << i << "entries"; return true; } bool LibzipPlugin::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options) - setOperationMode(Copy); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()).constData(), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } const QStringList filePaths = entryFullPaths(files); const QStringList destPaths = entryPathsFromDestination(filePaths, destination, 0); int i; for (i = 0; i < filePaths.size(); ++i) { QString dest = destPaths.at(i); if (dest.endsWith(QDir::separator())) { if (zip_dir_add(archive, dest.toUtf8().constData(), ZIP_FL_ENC_GUESS) == -1) { // If directory already exists in archive, we get an error. qCWarning(ARK) << "Failed to add dir " << dest << ":" << zip_strerror(archive); continue; } } const int srcIndex = zip_name_locate(archive, filePaths.at(i).toUtf8().constData(), ZIP_FL_ENC_GUESS); if (srcIndex == -1) { qCCritical(ARK) << "Could not find entry to copy:" << filePaths.at(i); emit error(xi18n("Failed to copy entry: %1", filePaths.at(i))); return false; } zip_source_t *src = zip_source_zip(archive, archive, srcIndex, 0, 0, -1); if (!src) { qCCritical(ARK) << "Failed to create source for:" << filePaths.at(i); return false; } const int destIndex = zip_file_add(archive, dest.toUtf8().constData(), src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE); if (destIndex == -1) { zip_source_free(src); qCCritical(ARK) << "Could not add entry" << dest << ":" << zip_strerror(archive); emit error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } // Get permissions from source entry. zip_uint8_t opsys; zip_uint32_t attributes; if (zip_file_get_external_attributes(archive, srcIndex, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) { qCCritical(ARK) << "Failed to read external attributes for source:" << filePaths.at(i); emit error(xi18n("Failed to read metadata for entry: %1", filePaths.at(i))); return false; } // Set permissions on dest entry. if (zip_file_set_external_attributes(archive, destIndex, ZIP_FL_UNCHANGED, opsys, attributes) != 0) { qCCritical(ARK) << "Failed to set external attributes for destination:" << dest; emit error(xi18n("Failed to set metadata for entry: %1", dest)); return false; } } // Register the callback function to get progress feedback. zip_register_progress_callback_with_state(archive, 0.001, progressCallback, nullptr, this); if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } // List the archive to update the model. m_listAfterAdd = true; list(); qCDebug(ARK) << "Copied" << i << "entries"; return true; } QString LibzipPlugin::permissionsToString(const mode_t &perm) { QString modeval; if ((perm & S_IFMT) == S_IFDIR) { modeval.append(QLatin1Char('d')); } else if ((perm & S_IFMT) == S_IFLNK) { modeval.append(QLatin1Char('l')); } else { modeval.append(QLatin1Char('-')); } modeval.append((perm & S_IRUSR) ? QLatin1Char('r') : QLatin1Char('-')); modeval.append((perm & S_IWUSR) ? QLatin1Char('w') : QLatin1Char('-')); if ((perm & S_ISUID) && (perm & S_IXUSR)) { modeval.append(QLatin1Char('s')); } else if ((perm & S_ISUID)) { modeval.append(QLatin1Char('S')); } else if ((perm & S_IXUSR)) { modeval.append(QLatin1Char('x')); } else { modeval.append(QLatin1Char('-')); } modeval.append((perm & S_IRGRP) ? QLatin1Char('r') : QLatin1Char('-')); modeval.append((perm & S_IWGRP) ? QLatin1Char('w') : QLatin1Char('-')); if ((perm & S_ISGID) && (perm & S_IXGRP)) { modeval.append(QLatin1Char('s')); } else if ((perm & S_ISGID)) { modeval.append(QLatin1Char('S')); } else if ((perm & S_IXGRP)) { modeval.append(QLatin1Char('x')); } else { modeval.append(QLatin1Char('-')); } modeval.append((perm & S_IROTH) ? QLatin1Char('r') : QLatin1Char('-')); modeval.append((perm & S_IWOTH) ? QLatin1Char('w') : QLatin1Char('-')); if ((perm & S_ISVTX) && (perm & S_IXOTH)) { modeval.append(QLatin1Char('t')); } else if ((perm & S_ISVTX)) { modeval.append(QLatin1Char('T')); } else if ((perm & S_IXOTH)) { modeval.append(QLatin1Char('x')); } else { modeval.append(QLatin1Char('-')); } return modeval; } -void LibzipPlugin::setOperationMode(ReadWriteArchiveInterface::OperationMode operationMode) -{ - QMutexLocker mutexLocker(&m_mutex); - m_operationMode = operationMode; -} - #include "libzipplugin.moc" diff --git a/plugins/libzipplugin/libzipplugin.h b/plugins/libzipplugin/libzipplugin.h index 827513ee..3bd2331e 100644 --- a/plugins/libzipplugin/libzipplugin.h +++ b/plugins/libzipplugin/libzipplugin.h @@ -1,72 +1,70 @@ /* * Copyright (c) 2017 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LIBZIPPLUGIN_H #define LIBZIPPLUGIN_H #include "archiveinterface.h" #include #include using namespace Kerfuffle; class LibzipPlugin : public ReadWriteArchiveInterface { Q_OBJECT public: explicit LibzipPlugin(QObject *parent, const QVariantList& args); ~LibzipPlugin() override; bool list() override; bool doKill() override; bool extractFiles(const QVector &files, const QString& destinationDirectory, const ExtractionOptions& options) override; bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd = 0) override; bool deleteFiles(const QVector &files) override; bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) override; bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) override; bool addComment(const QString& comment) override; bool testArchive() override; private: bool extractEntry(zip_t *archive, const QString &entry, const QString &rootNode, const QString &destDir, bool preservePaths, bool removeRootNode); bool writeEntry(zip_t *archive, const QString &entry, const Archive::Entry* destination, const CompressionOptions& options, bool isDir = false); bool emitEntryForIndex(zip_t *archive, qlonglong index); void emitProgress(double percentage); QString permissionsToString(const mode_t &perm); - void setOperationMode(OperationMode operationMode); static void progressCallback(zip_t *, double progress, void *that); QVector m_emittedEntries; bool m_overwriteAll; bool m_skipAll; bool m_listAfterAdd; - QMutex m_mutex; }; #endif // LIBZIPPLUGIN_H