diff options
Diffstat (limited to 'src/libs/installer')
28 files changed, 3134 insertions, 226 deletions
diff --git a/src/libs/installer/abstractarchive.cpp b/src/libs/installer/abstractarchive.cpp new file mode 100644 index 000000000..dd9b8e625 --- /dev/null +++ b/src/libs/installer/abstractarchive.cpp @@ -0,0 +1,221 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#include "abstractarchive.h" + +namespace QInstaller { + +/*! + \inmodule QtInstallerFramework + \class QInstaller::ArchiveEntry + \brief The ArchiveEntry struct represents an entry in an archive file, + which can be for example a file or a directory. +*/ + +/*! + \inmodule QtInstallerFramework + \class QInstaller::AbstractArchive + \brief The AbstractArchive class is the base class for classes representing + different archive files. It cannot be instantiated on its own but + defines the API and provides common functionality when subclassed. +*/ + +/*! + \enum AbstractArchive::CompressionLevel + This enum holds the possible values for archive compression level. + + \value Non + \value Fastest + \value Fast + \value Normal + \value Maximum + \value Ultra +*/ + +/*! + \fn QInstaller::AbstractArchive::currentEntryChanged(const QString &filename) + + Current entry changed to \a filename. Subclasses should emit this signal whenever + the entry to process is changed. +*/ + +/*! + \fn QInstaller::AbstractArchive::completedChanged(quint64 completed, quint64 total) + + The ratio of \a completed entries from \a total changed. Subclasses should emit + this whenever the progress changes. +*/ + +/*! + \fn QInstaller::AbstractArchive::cancel() + + Cancels current operation. A subclass should implement this slot. +*/ + +/*! + \fn QInstaller::AbstractArchive::close() + + Closes the archive. A subclass should implement this method. +*/ + +/*! + \fn QInstaller::AbstractArchive::create(const QStringList &data) + + Creates an archive from \a data. Returns \c true on success; + \c false otherwise. A subclass should implement this method. +*/ + +/*! + \fn QInstaller::AbstractArchive::extract(const QString &dirPath) + + Extracts the archive to \a dirPath. Returns \c true on success; + \c false otherwise. A subclass should implement this method. +*/ + +/*! + \fn QInstaller::AbstractArchive::extract(const QString &dirPath, const quint64 totalFiles) + + Extracts the contents of an archive to \a dirPath with precalculated + count of \a totalFiles. Returns \c true on success; \c false otherwise. + A subclass should implement this method. +*/ + +/*! + \fn QInstaller::AbstractArchive::isSupported() + + Returns \c true if the archive is supported; \c false otherwise. + A subclass should implement this method. +*/ + +/*! + \fn QInstaller::AbstractArchive::list() + + Returns a list of entries in this archive. A subclass should implement this method. +*/ + +/*! + \fn QInstaller::AbstractArchive::open(QIODevice::OpenMode mode) + + Opens the file device for an archive in \a mode. Returns \c true on success; + \c false otherwise. A subclass should implement this method. +*/ + +/*! + \fn QInstaller::AbstractArchive::setFilename(const QString &filename) + + Sets the \a filename for the archive. A subclass should implement this method. +*/ + +/*! + Constructs a new archive object with \a parent as parent. Cannot be + called directly but instead from subclass constructors. +*/ +AbstractArchive::AbstractArchive(QObject *parent) + : QObject(parent) + , m_compressionLevel(CompressionLevel::Normal) +{ +} + +/*! + Virtual destructor for \c AbstractArchive. +*/ +AbstractArchive::~AbstractArchive() +{ +} + +/*! + Returns a human-readable description of the last error that occurred. +*/ +QString AbstractArchive::errorString() const +{ + return m_error; +} + +/*! + Sets the compression level for new archives to \a level. +*/ +void AbstractArchive::setCompressionLevel(const CompressionLevel level) +{ + m_compressionLevel = level; +} + +/*! + Sets a human-readable description of the current \a error. +*/ +void AbstractArchive::setErrorString(const QString &error) +{ + m_error = error; +} + +/*! + Returns the current compression level. +*/ +AbstractArchive::CompressionLevel AbstractArchive::compressionLevel() const +{ + return m_compressionLevel; +} + +/*! + Reads an \a entry from the specified \a istream. Returns a reference to \a istream. +*/ +QDataStream &operator>>(QDataStream &istream, ArchiveEntry &entry) +{ + istream >> entry.path >> entry.utcTime >> entry.isDirectory + >> entry.uncompressedSize >> entry.permissions_mode >> entry.permissions_enum; + + return istream; +} + +/*! + Writes an \a entry to the specified \a ostream. Returns a reference to \a ostream. +*/ +QDataStream &operator<<(QDataStream &ostream, const ArchiveEntry &entry) +{ + ostream << entry.path << entry.utcTime << entry.isDirectory + << entry.uncompressedSize << entry.permissions_mode << entry.permissions_enum; + + return ostream; +} + +/*! + Returns \c true if left-hand-side entry \a lhs is equal to right-hand-size entry \a rhs. +*/ +bool operator==(const ArchiveEntry &lhs, const ArchiveEntry &rhs) +{ + return lhs.path == rhs.path + && lhs.utcTime == rhs.utcTime + && lhs.isDirectory == rhs.isDirectory + && lhs.compressedSize == rhs.compressedSize + && lhs.uncompressedSize == rhs.uncompressedSize + && lhs.permissions_mode == rhs.permissions_mode + && (lhs.permissions_enum == rhs.permissions_enum // ignore invalid permissions + || lhs.permissions_enum == static_cast<QFile::Permissions>(-1) + || rhs.permissions_enum == static_cast<QFile::Permissions>(-1)); +} + +} // namespace QInstaller diff --git a/src/libs/installer/abstractarchive.h b/src/libs/installer/abstractarchive.h new file mode 100644 index 000000000..77cc35b61 --- /dev/null +++ b/src/libs/installer/abstractarchive.h @@ -0,0 +1,120 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#ifndef ABSTRACTARCHIVE_H +#define ABSTRACTARCHIVE_H + +#include "installer_global.h" + +#include <QFile> +#include <QDateTime> +#include <QDataStream> +#include <QPoint> + +#ifdef Q_OS_WIN +typedef int mode_t; +#endif + +namespace QInstaller { + +struct INSTALLER_EXPORT ArchiveEntry +{ + ArchiveEntry() + : isDirectory(false) + , compressedSize(0) + , uncompressedSize(0) + , permissions_mode(0) + , permissions_enum(0) + {} + + QString path; + QDateTime utcTime; + QPoint archiveIndex; + bool isDirectory; + quint64 compressedSize; + quint64 uncompressedSize; + mode_t permissions_mode; + QFile::Permissions permissions_enum; +}; + +class INSTALLER_EXPORT AbstractArchive : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(AbstractArchive) + +public: + enum CompressionLevel { + Non = 0, + Fastest = 1, + Fast = 3, + Normal = 5, + Maximum = 7, + Ultra = 9 + }; + Q_ENUM(CompressionLevel) + + explicit AbstractArchive(QObject *parent = nullptr); + virtual ~AbstractArchive() = 0; + + virtual bool open(QIODevice::OpenMode mode) = 0; + virtual void close() = 0; + virtual void setFilename(const QString &filename) = 0; + + virtual QString errorString() const; + + virtual bool extract(const QString &dirPath) = 0; + virtual bool extract(const QString &dirPath, const quint64 totalFiles) = 0; + virtual bool create(const QStringList &data) = 0; + virtual QVector<ArchiveEntry> list() = 0; + virtual bool isSupported() = 0; + + virtual void setCompressionLevel(const CompressionLevel level); + +Q_SIGNALS: + void currentEntryChanged(const QString &filename); + void completedChanged(const quint64 completed, const quint64 total); + +public Q_SLOTS: + virtual void cancel() = 0; + +protected: + void setErrorString(const QString &error); + CompressionLevel compressionLevel() const; + +private: + QString m_error; + CompressionLevel m_compressionLevel; +}; + +INSTALLER_EXPORT QDataStream &operator>>(QDataStream &istream, ArchiveEntry &entry); +INSTALLER_EXPORT QDataStream &operator<<(QDataStream &ostream, const ArchiveEntry &entry); +INSTALLER_EXPORT bool operator==(const ArchiveEntry &lhs, const ArchiveEntry &rhs); + +} // namespace QInstaller + +#endif // ABSTRACTARCHIVE_H diff --git a/src/libs/installer/archivefactory.cpp b/src/libs/installer/archivefactory.cpp new file mode 100644 index 000000000..a946f9416 --- /dev/null +++ b/src/libs/installer/archivefactory.cpp @@ -0,0 +1,147 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#include "archivefactory.h" +#include "lib7zarchive.h" +#ifdef IFW_LIBARCHIVE +#include "libarchivewrapper.h" +#endif + +#include <QFileInfo> + +using namespace QInstaller; + +/*! + \inmodule QtInstallerFramework + \class QInstaller::ArchiveFactory + \brief The ArchiveFactory class is used to create archive objects + based on the suffix of a given filename. + + This class acts as a factory for \c QInstaller::AbstractArchive. You can + register one or more archive handlers with this factory and create + registered objects based on the file suffix. + + This class follows the singleton design pattern. Only one instance + of this class can be created and its reference can be fetched from + the \c {instance()} method. + + The following archive handlers are registered by default: + \list + \li Lib7z + \li LibArchive + \endlist +*/ + +/*! + \fn void QInstaller::ArchiveFactory::registerArchive(const QString &name, const QStringList &types) + + Registers a new archive handler with the factory based on \a name and list + of supported file suffix \a types. +*/ + + +/*! + Returns the only instance of this class. +*/ +ArchiveFactory &ArchiveFactory::instance() +{ + static ArchiveFactory instance; + return instance; +} + +/*! + Constructs and returns a pointer to an archive object with \a filename and \a parent. + If the archive type referenced by \a filename is not registered, a null pointer is + returned instead. +*/ +AbstractArchive *ArchiveFactory::create(const QString &filename, QObject *parent) const +{ + const QString suffix = QFileInfo(filename).completeSuffix(); + QString name; + for (auto &types : m_supportedTypesHash) { + QStringList::const_iterator it; + for (it = types.constBegin(); it != types.constEnd(); ++it) { + if (suffix.endsWith(*it, Qt::CaseInsensitive)) { + name = m_supportedTypesHash.key(types); + break; + } + } + } + if (name.isEmpty()) + return nullptr; + + AbstractArchive *archive = GenericFactory<AbstractArchive, QString, QString, QObject *> + ::create(name, filename, parent); + + return archive; +} + +/*! + Returns a list of supported archive types. +*/ +QStringList ArchiveFactory::supportedTypes() +{ + QStringList types; + QHash<QString, QStringList> *const typesHash = &instance().m_supportedTypesHash; + for (auto &value : *typesHash) + types.append(value); + + return types; +} + +/*! + Returns \c true if the archive type from \a filename is registered with + an archive handler. +*/ +bool ArchiveFactory::isSupportedType(const QString &filename) +{ + const QString suffix = QFileInfo(filename).completeSuffix(); + QHash<QString, QStringList> *const typesHash = &instance().m_supportedTypesHash; + for (auto &types : *typesHash) { + QStringList::const_iterator it; + for (it = types.constBegin(); it != types.constEnd(); ++it) { + if (suffix.endsWith(*it, Qt::CaseInsensitive)) + return true; + } + } + return false; +} + +/*! + Private constructor for ArchiveFactory. Registers default archive handlers. +*/ +ArchiveFactory::ArchiveFactory() +{ + registerArchive<Lib7zArchive>(QLatin1String("Lib7z"), QStringList() + << QLatin1String("7z")); +#ifdef IFW_LIBARCHIVE + registerArchive<LibArchiveWrapper>(QLatin1String("LibArchive"), QStringList() + << QLatin1String("tar.gz") << QLatin1String("tar.bz2") + << QLatin1String("tar.xz") << QLatin1String("zip") ); +#endif +} diff --git a/src/libs/installer/archivefactory.h b/src/libs/installer/archivefactory.h new file mode 100644 index 000000000..6f545eefa --- /dev/null +++ b/src/libs/installer/archivefactory.h @@ -0,0 +1,69 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#ifndef ARCHIVEFACTORY_H +#define ARCHIVEFACTORY_H + +#include "installer_global.h" +#include "genericfactory.h" +#include "abstractarchive.h" + +namespace QInstaller { + +class INSTALLER_EXPORT ArchiveFactory + : public GenericFactory<AbstractArchive, QString, QString, QObject *> +{ + Q_DISABLE_COPY(ArchiveFactory) + +public: + static ArchiveFactory &instance(); + + template <typename T> + void registerArchive(const QString &name, const QStringList &types) + { + if (containsProduct(name)) + m_supportedTypesHash.remove(name); + + registerProduct<T>(name); + m_supportedTypesHash.insert(name, types); + } + AbstractArchive *create(const QString &filename, QObject *parent = nullptr) const; + + static QStringList supportedTypes(); + static bool isSupportedType(const QString &filename); + +private: + ArchiveFactory(); + +private: + QHash<QString, QStringList> m_supportedTypesHash; +}; + +} // namespace QInstaller + +#endif // ARCHIVEFACTORY_H diff --git a/src/libs/installer/component.cpp b/src/libs/installer/component.cpp index 6e3856502..1a83291c1 100644 --- a/src/libs/installer/component.cpp +++ b/src/libs/installer/component.cpp @@ -31,7 +31,7 @@ #include "errors.h" #include "fileutils.h" #include "globals.h" -#include "lib7z_facade.h" +#include "archivefactory.h" #include "messageboxhandler.h" #include "packagemanagercore.h" #include "remoteclient.h" @@ -865,7 +865,8 @@ void Component::createOperationsForArchive(const QString &archive) return; } - const bool isZip = Lib7z::isSupportedArchive(archive); + QScopedPointer<AbstractArchive> archiveFile(ArchiveFactory::instance().create(archive)); + const bool isZip = (archiveFile && archiveFile->open(QIODevice::ReadOnly) && archiveFile->isSupported()); if (isZip) { // component.xml can override this value diff --git a/src/libs/installer/createlocalrepositoryoperation.cpp b/src/libs/installer/createlocalrepositoryoperation.cpp index e05c34b91..a40838178 100644 --- a/src/libs/installer/createlocalrepositoryoperation.cpp +++ b/src/libs/installer/createlocalrepositoryoperation.cpp @@ -1,6 +1,6 @@ /************************************************************************** ** -** Copyright (C) 2020 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -34,8 +34,7 @@ #include "fileio.h" #include "fileutils.h" #include "copydirectoryoperation.h" -#include "lib7z_create.h" -#include "lib7z_facade.h" +#include "lib7zarchive.h" #include "packagemanagercore.h" #include "productkeycheck.h" #include "constants.h" @@ -124,8 +123,12 @@ static QString createArchive(const QString repoPath, const QString &sourceDir, c const QString fileName = QString::fromLatin1("/%1meta.7z").arg(version); QFile archive(repoPath + fileName); - QInstaller::openForWrite(&archive); - Lib7z::createArchive(&archive, QStringList() << sourceDir); + + Lib7zArchive archiveFile(archive.fileName()); + if (!(archiveFile.open(QIODevice::WriteOnly) && archiveFile.create(QStringList() << sourceDir))) { + throw Error(CreateLocalRepositoryOperation::tr("Cannot create archive \"%1\": %2") + .arg(QDir::toNativeSeparators(archive.fileName()), archiveFile.errorString())); + } removeFiles(sourceDir, helper); // cleanup the files we compressed if (!archive.rename(sourceDir + fileName)) { throw Error(CreateLocalRepositoryOperation::tr("Cannot move file \"%1\" to \"%2\": %3") @@ -356,10 +359,6 @@ bool CreateLocalRepositoryOperation::performOperation() } } catch (...) {} setValue(QLatin1String("local-repo"), repoPath); - } catch (const Lib7z::SevenZipException &e) { - setError(UserDefinedError); - setErrorString(e.message()); - return false; } catch (const QInstaller::Error &e) { setError(UserDefinedError); setErrorString(e.message()); diff --git a/src/libs/installer/directoryguard.cpp b/src/libs/installer/directoryguard.cpp new file mode 100644 index 000000000..9c97130a4 --- /dev/null +++ b/src/libs/installer/directoryguard.cpp @@ -0,0 +1,112 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#include "directoryguard.h" + +#include "globals.h" +#include "errors.h" + +#include <QCoreApplication> +#include <QDir> + +namespace QInstaller { + +/*! + \inmodule QtInstallerFramework + \class QInstaller::DirectoryGuard + \brief RAII class to create a directory and delete it on destruction unless released. +*/ + +/*! + Constructs a new guard object for \a path. +*/ +DirectoryGuard::DirectoryGuard(const QString &path) + : m_path(path) + , m_created(false) + , m_released(false) +{ + m_path.replace(QLatin1Char('\\'), QLatin1Char('/')); +} + +/*! + Destroys the directory guard instance and removes the + guarded directory unless released. +*/ +DirectoryGuard::~DirectoryGuard() +{ + if (!m_created || m_released) + return; + QDir dir(m_path); + if (!dir.rmdir(m_path)) + qCWarning(lcInstallerInstallLog) << "Cannot delete directory" << m_path; +} + +/*! + Tries to create the directory structure. + Returns a list of every directory created. +*/ +QStringList DirectoryGuard::tryCreate() +{ + if (m_path.isEmpty()) + return QStringList(); + + const QFileInfo fi(m_path); + if (fi.exists() && fi.isDir()) + return QStringList(); + if (fi.exists() && !fi.isDir()) { + throw Error(QCoreApplication::translate("DirectoryGuard", + "Path \"%1\" exists but is not a directory.").arg(QDir::toNativeSeparators(m_path))); + } + QStringList created; + + QDir toCreate(m_path); + while (!toCreate.exists()) { + QString p = toCreate.absolutePath(); + created.push_front(p); + p = p.section(QLatin1Char('/'), 0, -2); + toCreate = QDir(p); + } + + QDir dir(m_path); + m_created = dir.mkpath(m_path); + if (!m_created) { + throw Error(QCoreApplication::translate("DirectoryGuard", + "Cannot create directory \"%1\".").arg(QDir::toNativeSeparators(m_path))); + } + return created; +} + +/*! + Marks the directory as released. +*/ +void DirectoryGuard::release() +{ + m_released = true; +} + +} // namespace QInstaller diff --git a/src/libs/installer/directoryguard.h b/src/libs/installer/directoryguard.h new file mode 100644 index 000000000..c18402557 --- /dev/null +++ b/src/libs/installer/directoryguard.h @@ -0,0 +1,55 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#ifndef DIRECTORYGUARD_H +#define DIRECTORYGUARD_H + +#include "installer_global.h" + +#include <QString> + +namespace QInstaller { + +class INSTALLER_EXPORT DirectoryGuard +{ +public: + explicit DirectoryGuard(const QString &path); + ~DirectoryGuard(); + + QStringList tryCreate(); + void release(); + +private: + QString m_path; + bool m_created; + bool m_released; +}; + +} // namespace QInstaller + +#endif // DIRECTORYGUARD_H diff --git a/src/libs/installer/extractarchiveoperation.cpp b/src/libs/installer/extractarchiveoperation.cpp index c2d541929..b2bda24a2 100644 --- a/src/libs/installer/extractarchiveoperation.cpp +++ b/src/libs/installer/extractarchiveoperation.cpp @@ -88,27 +88,28 @@ bool ExtractArchiveOperation::performOperation() connect(&callback, &Callback::progressChanged, this, &ExtractArchiveOperation::progressChanged); - if (PackageManagerCore *core = packageManager()) { - connect(core, &PackageManagerCore::statusChanged, &callback, &Callback::statusChanged); - } - - Runnable *runnable = new Runnable(archivePath, targetDir, &callback); - connect(runnable, &Runnable::finished, &receiver, &Receiver::runnableFinished, + Worker *worker = new Worker(archivePath, targetDir, &callback); + connect(worker, &Worker::finished, &receiver, &Receiver::workerFinished, Qt::QueuedConnection); + if (PackageManagerCore *core = packageManager()) + connect(core, &PackageManagerCore::statusChanged, worker, &Worker::onStatusChanged); + QFileInfo fileInfo(archivePath); emit outputTextChanged(tr("Extracting \"%1\"").arg(fileInfo.fileName())); + { + QEventLoop loop; + QThread workerThread; + worker->moveToThread(&workerThread); - QEventLoop loop; - connect(&receiver, &Receiver::finished, &loop, &QEventLoop::quit); - if (QThreadPool::globalInstance()->tryStart(runnable)) { + connect(&workerThread, &QThread::started, worker, &Worker::run); + connect(&receiver, &Receiver::finished, &workerThread, &QThread::quit); + connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); + connect(&workerThread, &QThread::finished, &loop, &QEventLoop::quit); + + workerThread.start(); loop.exec(); - } else { - // HACK: In case there is no availabe thread we should call it directly. - runnable->run(); - receiver.runnableFinished(true, QString()); } - // Write all file names which belongs to a package to a separate file and only the separate // filename to a .dat file. There can be enormous amount of files in a package, which makes // the dat file very slow to read and write. The .dat file is read into memory in startup, diff --git a/src/libs/installer/extractarchiveoperation.h b/src/libs/installer/extractarchiveoperation.h index fa05d403a..7fc008887 100644 --- a/src/libs/installer/extractarchiveoperation.h +++ b/src/libs/installer/extractarchiveoperation.h @@ -1,6 +1,6 @@ /************************************************************************** ** -** Copyright (C) 2017 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -63,7 +63,7 @@ private: private: class Callback; - class Runnable; + class Worker; class Receiver; }; diff --git a/src/libs/installer/extractarchiveoperation_p.h b/src/libs/installer/extractarchiveoperation_p.h index 9cc07246f..706187eb7 100644 --- a/src/libs/installer/extractarchiveoperation_p.h +++ b/src/libs/installer/extractarchiveoperation_p.h @@ -1,6 +1,6 @@ /************************************************************************** ** -** Copyright (C) 2017 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -31,8 +31,7 @@ #include "extractarchiveoperation.h" #include "fileutils.h" -#include "lib7z_extract.h" -#include "lib7z_facade.h" +#include "archivefactory.h" #include "packagemanagercore.h" #include <QRunnable> @@ -85,7 +84,7 @@ private: typedef QPair<QString, QString> Backup; typedef QVector<Backup> BackupFiles; -class ExtractArchiveOperation::Callback : public QObject, public Lib7z::ExtractCallback +class ExtractArchiveOperation::Callback : public QObject { Q_OBJECT Q_DISABLE_COPY(Callback) @@ -93,36 +92,14 @@ class ExtractArchiveOperation::Callback : public QObject, public Lib7z::ExtractC public: Callback() = default; - BackupFiles backupFiles() const { + BackupFiles backupFiles() const + { return m_backupFiles; } - QStringList extractedFiles() const { - return m_extractedFiles; - } - -public slots: - void statusChanged(QInstaller::PackageManagerCore::Status status) + QStringList extractedFiles() const { - switch(status) { - case PackageManagerCore::Canceled: - m_state = E_ABORT; - break; - case PackageManagerCore::Failure: - m_state = E_FAIL; - break; - default: // ignore all other status values - break; - } - } - -signals: - void progressChanged(double progress); - -private: - void setCurrentFile(const QString &filename) Q_DECL_OVERRIDE - { - m_extractedFiles.prepend(QDir::toNativeSeparators(filename)); + return m_extractedFiles; } static QString generateBackupName(const QString &fn) @@ -135,7 +112,7 @@ private: return res; } - bool prepareForFile(const QString &filename) Q_DECL_OVERRIDE + bool prepareForFile(const QString &filename) { if (!QFile::exists(filename)) return true; @@ -151,59 +128,110 @@ private: return true; } - HRESULT setCompleted(quint64 completed, quint64 total) Q_DECL_OVERRIDE +Q_SIGNALS: + void progressChanged(double progress); + +public Q_SLOTS: + void onCurrentEntryChanged(const QString &filename) + { + m_extractedFiles.prepend(QDir::toNativeSeparators(filename)); + } + + void onCompletedChanged(quint64 completed, quint64 total) { emit progressChanged(double(completed) / total); - return m_state; } private: - HRESULT m_state = S_OK; BackupFiles m_backupFiles; QStringList m_extractedFiles; }; -class ExtractArchiveOperation::Runnable : public QObject, public QRunnable +class ExtractArchiveOperation::Worker : public QObject { Q_OBJECT - Q_DISABLE_COPY(Runnable) + Q_DISABLE_COPY(Worker) public: - Runnable(const QString &archivePath, const QString &targetDir, - ExtractArchiveOperation::Callback *callback) + Worker(const QString &archivePath, const QString &targetDir, Callback *callback) : m_archivePath(archivePath) , m_targetDir(targetDir) + , m_canceled(false) , m_callback(callback) {} +Q_SIGNALS: + void finished(bool success, const QString &errorString); + +public Q_SLOTS: void run() { - QFile archive(m_archivePath); - if (!archive.open(QIODevice::ReadOnly)) { - emit finished(false, tr("Cannot open archive \"%1\" for reading: %2").arg(m_archivePath, - archive.errorString())); + m_canceled = false; + m_archive.reset(ArchiveFactory::instance().create(m_archivePath)); + if (!m_archive) { + emit finished(false, tr("Could not create handler object for archive \"%1\": \"%2\".") + .arg(m_archivePath, QLatin1String(Q_FUNC_INFO))); return; } - try { - Lib7z::extractArchive(&archive, m_targetDir, m_callback); - emit finished(true, QString()); - } catch (const Lib7z::SevenZipException& e) { + connect(m_archive.get(), &AbstractArchive::currentEntryChanged, m_callback, &Callback::onCurrentEntryChanged); + connect(m_archive.get(), &AbstractArchive::completedChanged, m_callback, &Callback::onCompletedChanged); + + if (!(m_archive->open(QIODevice::ReadOnly) && m_archive->isSupported())) { + emit finished(false, tr("Cannot open archive \"%1\" for reading: %2").arg(m_archivePath, + m_archive->errorString())); + return; + } + const QVector<ArchiveEntry> entries = m_archive->list(); + if (entries.isEmpty()) { + emit finished(false, tr("Error while reading contents of archive \"%1\": %2").arg(m_archivePath, + m_archive->errorString())); + return; + } + for (auto &entry : entries) { + QString completeFilePath = m_targetDir + QDir::separator() + entry.path; + if (!entry.isDirectory && !m_callback->prepareForFile(completeFilePath)) { + emit finished(false, tr("Cannot prepare for file \"%1\"").arg(completeFilePath)); + return; + } + } + if (m_canceled) { + // For large archives the reading takes some time, and the user might have + // canceled before we start the actual extracting. + emit finished(false, tr("Extract for archive \"%1\" canceled.").arg(m_archivePath)); + } else if (!m_archive->extract(m_targetDir, entries.size())) { emit finished(false, tr("Error while extracting archive \"%1\": %2").arg(m_archivePath, - e.message())); - } catch (...) { - emit finished(false, tr("Unknown exception caught while extracting \"%1\".") - .arg(m_archivePath)); + m_archive->errorString())); + } else { + emit finished(true, QString()); } } -signals: - void finished(bool success, const QString &errorString); + void onStatusChanged(PackageManagerCore::Status status) + { + if (!m_archive) + return; + + switch (status) { + case PackageManagerCore::Canceled: + m_canceled = true; + m_archive->cancel(); + break; + case PackageManagerCore::Failure: + m_canceled = true; + m_archive->cancel(); + break; + default: // ignore all other status values + break; + } + } private: QString m_archivePath; QString m_targetDir; - ExtractArchiveOperation::Callback *m_callback; + QScopedPointer<AbstractArchive> m_archive; + bool m_canceled; + Callback *m_callback; }; class ExtractArchiveOperation::Receiver : public QObject @@ -223,7 +251,7 @@ public: } public slots: - void runnableFinished(bool ok, const QString &msg) + void workerFinished(bool ok, const QString &msg) { m_success = ok; m_errorString = msg; diff --git a/src/libs/installer/installer.pro b/src/libs/installer/installer.pro index c28a840bd..4721bb089 100644 --- a/src/libs/installer/installer.pro +++ b/src/libs/installer/installer.pro @@ -138,10 +138,18 @@ HEADERS += packagemanagercore.h \ repositorycategory.h \ componentselectionpage_p.h \ commandlineparser.h \ - commandlineparser_p.h + commandlineparser_p.h \ + abstractarchive.h \ + directoryguard.h \ + lib7zarchive.h \ + archivefactory.h SOURCES += packagemanagercore.cpp \ + abstractarchive.cpp \ + archivefactory.cpp \ aspectratiolabel.cpp \ + directoryguard.cpp \ + lib7zarchive.cpp \ loggingutils.cpp \ packagemanagercore_p.cpp \ packagemanagergui.cpp \ @@ -229,8 +237,19 @@ unix { else: SOURCES += adminauthorization_x11.cpp } +CONFIG(libarchive) { + HEADERS += libarchivearchive.h \ + libarchivewrapper.h \ + libarchivewrapper_p.h + + SOURCES += libarchivearchive.cpp \ + libarchivewrapper.cpp \ + libarchivewrapper_p.cpp + + LIBS += -llibarchive +} + LIBS += -l7z -CONFIG(libarchive): LIBS += -llibarchive win32 { SOURCES += adminauthorization_win.cpp sysinfo_win.cpp diff --git a/src/libs/installer/lib7z_create.h b/src/libs/installer/lib7z_create.h index bc61db7ab..72fc56c81 100644 --- a/src/libs/installer/lib7z_create.h +++ b/src/libs/installer/lib7z_create.h @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2017 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -30,6 +30,7 @@ #define LIB7Z_CREATE_H #include "installer_global.h" +#include "abstractarchive.h" #include <Common/MyCom.h> #include <7zip/UI/Common/Update.h> @@ -46,14 +47,7 @@ namespace Lib7z Yes }; - enum struct Compression { - Non = 0, - Fastest = 1, - Fast = 3, - Normal = 5, - Maximum = 7, - Ultra = 9 - }; + typedef QInstaller::AbstractArchive::CompressionLevel Compression; class INSTALLER_EXPORT UpdateCallback : public IUpdateCallbackUI2, public CMyUnknownImp { diff --git a/src/libs/installer/lib7z_facade.cpp b/src/libs/installer/lib7z_facade.cpp index 2f33c95e3..17176d7b0 100644 --- a/src/libs/installer/lib7z_facade.cpp +++ b/src/libs/installer/lib7z_facade.cpp @@ -1,6 +1,6 @@ /************************************************************************** ** -** Copyright (C) 2017 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -36,6 +36,7 @@ #include "lib7z_list.h" #include "lib7z_guid.h" #include "globals.h" +#include "directoryguard.h" #ifndef Q_OS_WIN # include "StdAfx.h" @@ -294,73 +295,6 @@ QString errorMessageFrom7zResult(const LONG &extractResult) return errorMessage; } -/* - RAII class to create a directory (tryCreate()) and delete it on destruction unless released. -*/ -struct DirectoryGuard -{ - explicit DirectoryGuard(const QString &path) - : m_path(path) - , m_created(false) - , m_released(false) - { - m_path.replace(QLatin1Char('\\'), QLatin1Char('/')); - } - - ~DirectoryGuard() - { - if (!m_created || m_released) - return; - QDir dir(m_path); - if (!dir.rmdir(m_path)) - qCWarning(QInstaller::lcInstallerInstallLog) << "Cannot delete directory " << m_path; - } - - /* - Tries to create the directory structure. - Returns a list of every directory created. - */ - QStringList tryCreate() - { - if (m_path.isEmpty()) - return QStringList(); - - const QFileInfo fi(m_path); - if (fi.exists() && fi.isDir()) - return QStringList(); - if (fi.exists() && !fi.isDir()) { - throw SevenZipException(QCoreApplication::translate("DirectoryGuard", - "Path \"%1\" exists but is not a directory.").arg(QDir::toNativeSeparators(m_path))); - } - QStringList created; - - QDir toCreate(m_path); - while (!toCreate.exists()) { - QString p = toCreate.absolutePath(); - created.push_front(p); - p = p.section(QLatin1Char('/'), 0, -2); - toCreate = QDir(p); - } - - QDir dir(m_path); - m_created = dir.mkpath(m_path); - if (!m_created) { - throw SevenZipException(QCoreApplication::translate("DirectoryGuard", - "Cannot create directory \"%1\".").arg(QDir::toNativeSeparators(m_path))); - } - return created; - } - - void release() - { - m_released = true; - } - - QString m_path; - bool m_created; - bool m_released; -}; - static UString QString2UString(const QString &str) { return str.toStdWString().c_str(); @@ -554,18 +488,6 @@ private: QPointer<QIODevice> m_device; }; -bool operator==(const File &lhs, const File &rhs) -{ - return lhs.path == rhs.path - && lhs.utcTime == rhs.utcTime - && lhs.isDirectory == rhs.isDirectory - && lhs.compressedSize == rhs.compressedSize - && lhs.uncompressedSize == rhs.uncompressedSize - && (lhs.permissions == rhs.permissions - || lhs.permissions == static_cast<QFile::Permissions>(-1) - || rhs.permissions == static_cast<QFile::Permissions>(-1)); -} - /*! Returns a list of files belonging to an \a archive. */ @@ -586,6 +508,8 @@ QVector<File> listArchive(QFileDevice *archive) op.types = &types; // Empty, because we use a stream. CIntVector excluded; + excluded.Add(codecs.FindFormatForExtension( + QString2UString(QLatin1String("xz")))); // handled by libarchive op.excludedFormats = &excluded; const CMyComPtr<IInStream> stream = new QIODeviceInStream(archive); @@ -620,7 +544,7 @@ QVector<File> listArchive(QFileDevice *archive) f.archiveIndex.setY(item); f.path = UString2QString(s).replace(QLatin1Char('\\'), QLatin1Char('/')); Archive_IsItem_Folder(arch, item, f.isDirectory); - f.permissions = getPermissions(arch, item, nullptr); + f.permissions_enum = getPermissions(arch, item, nullptr); getDateTimeProperty(arch, item, kpidMTime, &(f.utcTime)); f.uncompressedSize = getUInt64Property(arch, item, kpidSize, 0); f.compressedSize = getUInt64Property(arch, item, kpidPackSize, 0); @@ -692,7 +616,7 @@ STDMETHODIMP ExtractCallback::GetStream(UInt32 index, ISequentialOutStream **out const QFileInfo fi(QString::fromLatin1("%1/%2").arg(targetDir, UString2QString(s))); - DirectoryGuard guard(fi.absolutePath()); + QInstaller::DirectoryGuard guard(fi.absolutePath()); const QStringList directories = guard.tryCreate(); bool isDir = false; @@ -846,19 +770,6 @@ STDMETHODIMP ExtractCallback::SetOperationResult(Int32 /*resultEOperationResult* */ /*! - \enum Lib7z::Compression - - This enum specifies the compression ratio of an archive: - - \value Non - \value Fastest - \value Fast - \value Normal - \value Maximum - \value Ultra -*/ - -/*! \namespace Lib7z \inmodule QtInstallerFramework \brief The Lib7z namespace contains miscellaneous identifiers used throughout the Lib7z library. @@ -1176,7 +1087,7 @@ void extractArchive(QFileDevice *archive, const QString &directory, ExtractCallb localCallback = callback; } - DirectoryGuard outDir(QFileInfo(directory).absolutePath()); + QInstaller::DirectoryGuard outDir(QFileInfo(directory).absolutePath()); try { outDir.tryCreate(); @@ -1191,6 +1102,8 @@ void extractArchive(QFileDevice *archive, const QString &directory, ExtractCallb op.types = &types; // Empty, because we use a stream. CIntVector excluded; + excluded.Add(codecs.FindFormatForExtension( + QString2UString(QLatin1String("xz")))); // handled by libarchive op.excludedFormats = &excluded; const CMyComPtr<IInStream> stream = new QIODeviceInStream(archive); @@ -1248,6 +1161,8 @@ bool isSupportedArchive(QFileDevice *archive) op.types = &types; // Empty, because we use a stream. CIntVector excluded; + excluded.Add(codecs.FindFormatForExtension( + QString2UString(QLatin1String("xz")))); // handled by libarchive op.excludedFormats = &excluded; const CMyComPtr<IInStream> stream = new QIODeviceInStream(archive); diff --git a/src/libs/installer/lib7z_list.h b/src/libs/installer/lib7z_list.h index 965a8b5ea..e09c73746 100644 --- a/src/libs/installer/lib7z_list.h +++ b/src/libs/installer/lib7z_list.h @@ -1,6 +1,6 @@ /************************************************************************** ** -** Copyright (C) 2015 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -40,6 +40,7 @@ #define LIB7Z_LIST_H #include "installer_global.h" +#include "abstractarchive.h" #include <QDateTime> #include <QFile> @@ -47,19 +48,7 @@ namespace Lib7z { - struct INSTALLER_EXPORT File - { - public: - QString path; - QDateTime utcTime; - QPoint archiveIndex; - bool isDirectory = false; - quint64 compressedSize = 0; - quint64 uncompressedSize = 0; - QFile::Permissions permissions = 0; - }; - - INSTALLER_EXPORT bool operator==(const File &lhs, const File &rhs); + typedef QInstaller::ArchiveEntry File; QVector<File> INSTALLER_EXPORT listArchive(QFileDevice *archive); diff --git a/src/libs/installer/lib7zarchive.cpp b/src/libs/installer/lib7zarchive.cpp new file mode 100644 index 000000000..d7b0c0dc9 --- /dev/null +++ b/src/libs/installer/lib7zarchive.cpp @@ -0,0 +1,242 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#include "lib7zarchive.h" + +#include "errors.h" +#include "lib7z_facade.h" +#include "lib7z_create.h" +#include "lib7z_list.h" + +#include <QCoreApplication> + +namespace QInstaller { + +/*! + \inmodule QtInstallerFramework + \class QInstaller::Lib7zArchive + \brief The Lib7zArchive class represents an archive file + handled with the LZMA software development kit. +*/ + +/*! + \inmodule QtInstallerFramework + \class QInstaller::Lib7zArchive::ExtractCallbackWrapper + \internal +*/ + +/*! + Constructs an archive object representing an archive file + specified by \a filename with \a parent as parent object. +*/ +Lib7zArchive::Lib7zArchive(const QString &filename, QObject *parent) + : AbstractArchive(parent) + , m_extractCallback(new ExtractCallbackWrapper()) +{ + Lib7zArchive::setFilename(filename); + listenExtractCallback(); +} + +/*! + Constructs an archive object with the given \a parent. +*/ +Lib7zArchive::Lib7zArchive(QObject *parent) + : AbstractArchive(parent) + , m_extractCallback(new ExtractCallbackWrapper()) +{ + listenExtractCallback(); +} + +/*! + Destroys the instance and releases resources. +*/ +Lib7zArchive::~Lib7zArchive() +{ + delete m_extractCallback; +} + +/*! + \reimp + + Opens the underlying file device using \a mode. Returns \c true if + succesfull; otherwise \c false. +*/ +bool Lib7zArchive::open(QIODevice::OpenMode mode) +{ + if (!m_file.open(mode)) { + setErrorString(m_file.errorString()); + return false; + } + return true; +} + +/*! + \reimp + + Closes the underlying file device. +*/ +void Lib7zArchive::close() +{ + m_file.close(); +} + +/*! + \reimp + + Sets the \a filename of the underlying file device. +*/ +void Lib7zArchive::setFilename(const QString &filename) +{ + m_file.setFileName(filename); +} + +/*! + \reimp + + Extracts the contents of this archive to \a dirPath. + Returns \c true on success; \c false otherwise. +*/ +bool Lib7zArchive::extract(const QString &dirPath) +{ + m_extractCallback->setState(S_OK); + try { + Lib7z::extractArchive(&m_file, dirPath, m_extractCallback); + } catch (const Lib7z::SevenZipException &e) { + setErrorString(e.message()); + return false; + } + return true; +} + +/*! + \reimp + + Extracts the contents of this archive to \a dirPath. The \a totalFiles + parameter is unused. Returns \c true on success; \c false otherwise. +*/ +bool Lib7zArchive::extract(const QString &dirPath, const quint64 totalFiles) +{ + Q_UNUSED(totalFiles) + return extract(dirPath); +} + +/*! + \reimp + + Packages the given \a data into the archive and creates the file on disk. +*/ +bool Lib7zArchive::create(const QStringList &data) +{ + try { + // No support for callback yet. + Lib7z::createArchive(&m_file, data, compressionLevel()); + } catch (const Lib7z::SevenZipException &e) { + setErrorString(e.message()); + return false; + } + return true; +} + +/*! + \reimp + + Returns the contents of this archive as an array of \c ArchiveEntry objects. + On failure, returns an empty array. +*/ +QVector<ArchiveEntry> Lib7zArchive::list() +{ + try { + return Lib7z::listArchive(&m_file); + } catch (const Lib7z::SevenZipException &e) { + setErrorString(e.message()); + return QVector<ArchiveEntry>(); + } +} + +/*! + \reimp + + Returns \c true if the current archive is of supported format; + \c false otherwise. +*/ +bool Lib7zArchive::isSupported() +{ + try { + return Lib7z::isSupportedArchive(&m_file); + } catch (const Lib7z::SevenZipException &e) { + setErrorString(e.message()); + return false; + } +} + +/*! + \reimp + + Cancels the extract operation in progress. +*/ +void Lib7zArchive::cancel() +{ + m_extractCallback->setState(E_ABORT); +} + +/*! + \internal +*/ +void Lib7zArchive::listenExtractCallback() +{ + connect(m_extractCallback, &ExtractCallbackWrapper::currentEntryChanged, + this, &Lib7zArchive::currentEntryChanged); + connect(m_extractCallback, &ExtractCallbackWrapper::completedChanged, + this, &Lib7zArchive::completedChanged); +} + + +Lib7zArchive::ExtractCallbackWrapper::ExtractCallbackWrapper() + : m_state(S_OK) +{ +} + +void Lib7zArchive::ExtractCallbackWrapper::setState(HRESULT state) +{ + m_state = state; +} + +void Lib7zArchive::ExtractCallbackWrapper::setCurrentFile(const QString &filename) +{ + emit currentEntryChanged(filename); +} + +HRESULT Lib7zArchive::ExtractCallbackWrapper::setCompleted(quint64 completed, quint64 total) +{ + qApp->processEvents(); + + emit completedChanged(completed, total); + return m_state; +} + +} // namespace QInstaller diff --git a/src/libs/installer/lib7zarchive.h b/src/libs/installer/lib7zarchive.h new file mode 100644 index 000000000..45f352aeb --- /dev/null +++ b/src/libs/installer/lib7zarchive.h @@ -0,0 +1,95 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#ifndef LIB7ZARCHIVE_H +#define LIB7ZARCHIVE_H + +#include "installer_global.h" +#include "abstractarchive.h" +#include "lib7z_extract.h" + +namespace QInstaller { + +class INSTALLER_EXPORT Lib7zArchive : public AbstractArchive +{ + Q_OBJECT + Q_DISABLE_COPY(Lib7zArchive) + +public: + Lib7zArchive(const QString &filename, QObject *parent = nullptr); + explicit Lib7zArchive(QObject *parent = nullptr); + ~Lib7zArchive(); + + bool open(QIODevice::OpenMode mode) Q_DECL_OVERRIDE; + void close() Q_DECL_OVERRIDE; + void setFilename(const QString &filename) Q_DECL_OVERRIDE; + + bool extract(const QString &dirPath) Q_DECL_OVERRIDE; + bool extract(const QString &dirPath, const quint64 totalFiles) Q_DECL_OVERRIDE; + bool create(const QStringList &data) Q_DECL_OVERRIDE; + QVector<ArchiveEntry> list() Q_DECL_OVERRIDE; + bool isSupported() Q_DECL_OVERRIDE; + +public Q_SLOTS: + void cancel() Q_DECL_OVERRIDE; + +private: + void listenExtractCallback(); + + class ExtractCallbackWrapper; + +private: + QFile m_file; + ExtractCallbackWrapper *const m_extractCallback; +}; + +class Lib7zArchive::ExtractCallbackWrapper : public QObject, public Lib7z::ExtractCallback +{ + Q_OBJECT + Q_DISABLE_COPY(ExtractCallbackWrapper) + +public: + ExtractCallbackWrapper(); + + void setState(HRESULT state); + +Q_SIGNALS: + void currentEntryChanged(const QString &filename); + void completedChanged(quint64 completed, quint64 total); + +private: + void setCurrentFile(const QString &filename) Q_DECL_OVERRIDE; + HRESULT setCompleted(quint64 completed, quint64 total) Q_DECL_OVERRIDE; + +private: + HRESULT m_state; +}; + +} // namespace QInstaller + +#endif // LIB7ZARCHIVE_H diff --git a/src/libs/installer/libarchivearchive.cpp b/src/libs/installer/libarchivearchive.cpp new file mode 100644 index 000000000..ad5609490 --- /dev/null +++ b/src/libs/installer/libarchivearchive.cpp @@ -0,0 +1,819 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#include "libarchivearchive.h" + +#include "directoryguard.h" +#include "errors.h" +#include "globals.h" + +#include <QApplication> +#include <QFileInfo> +#include <QDir> +#include <QTimer> + +namespace QInstaller { + +/*! + \inmodule QtInstallerFramework + \class QInstaller::ScopedPointerReaderDeleter + \internal +*/ + +/*! + \inmodule QtInstallerFramework + \class QInstaller::ScopedPointerWriterDeleter + \internal +*/ + +/*! + \inmodule QtInstallerFramework + \class QInstaller::ScopedPointerEntryDeleter + \internal +*/ + +/*! + \inmodule QtInstallerFramework + \class QInstaller::ExtractWorker + \internal +*/ + +ExtractWorker::Status ExtractWorker::status() const +{ + return m_status; +} + +void ExtractWorker::extract(const QString &dirPath, const quint64 totalFiles) +{ + m_status = Unfinished; + quint64 completed = 0; + + if (!totalFiles) { + m_status = Failure; + emit finished(QLatin1String("The file count for current archive is null!")); + return; + } + + QScopedPointer<archive, ScopedPointerReaderDeleter> reader(archive_read_new()); + QScopedPointer<archive, ScopedPointerWriterDeleter> writer(archive_write_disk_new()); + archive_entry *entry = nullptr; + + LibArchiveArchive::configureReader(reader.get()); + LibArchiveArchive::configureDiskWriter(writer.get()); + + DirectoryGuard targetDir(QFileInfo(dirPath).absolutePath()); + try { + const QStringList createdDirs = targetDir.tryCreate(); + // Make sure that all leading directories created get removed as well + foreach (const QString &directory, createdDirs) + emit currentEntryChanged(directory); + + int status = archive_read_open(reader.get(), this, nullptr, readCallback, nullptr); + if (status != ARCHIVE_OK) { + m_status = Failure; + emit finished(QLatin1String(archive_error_string(reader.get()))); + return; + } + + forever { + if (m_status == Canceled) { + emit finished(QLatin1String("Extract canceled.")); + return; + } + status = archive_read_next_header(reader.get(), &entry); + if (status == ARCHIVE_EOF) + break; + if (status != ARCHIVE_OK) { + m_status = Failure; + emit finished(QLatin1String(archive_error_string(reader.get()))); + return; + } + const char *current = archive_entry_pathname(entry); + const QString outputPath = dirPath + QDir::separator() + QString::fromLocal8Bit(current); + archive_entry_set_pathname(entry, outputPath.toLocal8Bit()); + + emit currentEntryChanged(outputPath); + if (!writeEntry(reader.get(), writer.get(), entry)) + return; + + ++completed; + emit completedChanged(completed, totalFiles); + + qApp->processEvents(); + } + } catch (const Error &e) { + m_status = Failure; + emit finished(e.message()); + return; + } + targetDir.release(); + m_status = Success; + emit finished(); +} + +void ExtractWorker::addDataBlock(const QByteArray buffer) +{ + m_buffer.append(buffer); + emit dataReadyForRead(); +} + +void ExtractWorker::cancel() +{ + m_status = Canceled; +} + +ssize_t ExtractWorker::readCallback(archive *reader, void *caller, const void **buff) +{ + Q_UNUSED(reader) + + ExtractWorker *obj; + if (!(obj = static_cast<ExtractWorker *>(caller))) + return ARCHIVE_FATAL; + + QByteArray *buffer = &obj->m_buffer; + if (!buffer->isEmpty()) + buffer->clear(); + + emit obj->dataBlockRequested(); + + // It's a bit bad that we have to wait here, but libarchive doesn't + // provide an event based reading method. + { + QEventLoop loop; + QTimer::singleShot(30000, &loop, &QEventLoop::quit); + connect(obj, &ExtractWorker::dataReadyForRead, &loop, &QEventLoop::quit); + connect(obj, &ExtractWorker::dataAtEnd, &loop, &QEventLoop::quit); + loop.exec(); + } + + if (!(*buff = static_cast<const void *>(buffer->constData()))) + return ARCHIVE_FATAL; + + return buffer->size(); +} + +bool ExtractWorker::writeEntry(archive *reader, archive *writer, archive_entry *entry) +{ + int status; + const void *buff; + size_t size; + int64_t offset; + + status = archive_write_header(writer, entry); + if (status != ARCHIVE_OK) { + emit finished(QLatin1String(archive_error_string(writer))); + return false; + } + + forever { + status = archive_read_data_block(reader, &buff, &size, &offset); + if (status == ARCHIVE_EOF) + return true; + if (status != ARCHIVE_OK) { + m_status = Failure; + emit finished(QLatin1String(archive_error_string(reader))); + return false; + } + status = archive_write_data_block(writer, buff, size, offset); + if (status != ARCHIVE_OK) { + m_status = Failure; + emit finished(QLatin1String(archive_error_string(writer))); + return false; + } + } +} + +/*! + \inmodule QtInstallerFramework + \class QInstaller::LibArchiveArchive + \brief The LibArchiveArchive class represents an archive file + handled with libarchive archive and compression library. + + In addition to extracting data from the underlying file device, + the class supports a non-blocking mode of extracting from an external + data source. When using this mode, the calling client must pass the data + to be read in chunks of arbitrary size, and inform the object when there + is no more data to read. +*/ + +/*! + \inmodule QtInstallerFramework + \class QInstaller::LibArchiveArchive::ArchiveData + \brief Bundles a file device and associated read buffer for access + as client data in libarchive callbacks. +*/ + +/*! + \fn QInstaller::LibArchiveArchive::dataBlockRequested() + + Emitted when the worker object requires more data to continue extracting. +*/ + +/*! + \fn QInstaller::LibArchiveArchive::workerFinished() + + Emitted when the worker object finished extracting an archive. +*/ + +/*! + \fn QInstaller::LibArchiveArchive::workerAboutToExtract(const QString &dirPath, const quint64 totalFiles) + + Emitted when the worker object is about to extract \a totalFiles + from an archive to \a dirPath. +*/ + +/*! + \fn QInstaller::LibArchiveArchive::workerAboutToAddDataBlock(const QByteArray buffer) + + Emitted when the worker object is about to add and read the data block in \a buffer. +*/ + +/*! + \fn QInstaller::LibArchiveArchive::workerAboutToSetDataAtEnd() + + Emitted when the worker object is about to set data-at-end, meaning there + will be no further read requests for the calling client. +*/ + +/*! + \fn QInstaller::LibArchiveArchive::workerAboutToCancel() + + Emitted when the worker object is about to cancel extracting. +*/ + +/*! + Constructs an archive object representing an archive file + specified by \a filename with \a parent as the parent object. +*/ +LibArchiveArchive::LibArchiveArchive(const QString &filename, QObject *parent) + : AbstractArchive(parent) + , m_data(new ArchiveData()) + , m_cancelScheduled(false) +{ + LibArchiveArchive::setFilename(filename); + initExtractWorker(); +} + +/*! + Constructs an archive object with the given \a parent. +*/ +LibArchiveArchive::LibArchiveArchive(QObject *parent) + : AbstractArchive(parent) + , m_data(new ArchiveData()) + , m_cancelScheduled(false) +{ + initExtractWorker(); +} + +/*! + Destroys the instance and releases resources. +*/ +LibArchiveArchive::~LibArchiveArchive() +{ + m_workerThread.quit(); + m_workerThread.wait(); + + delete m_data; +} + +/*! + \reimp + + Opens the underlying file device using \a mode. Returns \c true if + succesfull; otherwise \c false. +*/ +bool LibArchiveArchive::open(QIODevice::OpenMode mode) +{ + if (!m_data->file.open(mode)) { + setErrorString(m_data->file.errorString()); + return false; + } + return true; +} + +/*! + \reimp + + Closes the underlying file device. +*/ +void LibArchiveArchive::close() +{ + m_data->file.close(); +} + +/*! + \reimp + + Sets the \a filename of the underlying file device. +*/ +void LibArchiveArchive::setFilename(const QString &filename) +{ + m_data->file.setFileName(filename); +} + +/*! + \reimp + + Extracts the contents of this archive to \a dirPath. + Returns \c true on success; \c false otherwise. +*/ +bool LibArchiveArchive::extract(const QString &dirPath) +{ + return extract(dirPath, totalFiles()); +} + +/*! + \reimp + + Extracts the contents of this archive to \a dirPath with + precalculated count of \a totalFiles. Returns \c true on + success; \c false otherwise. +*/ +bool LibArchiveArchive::extract(const QString &dirPath, const quint64 totalFiles) +{ + m_cancelScheduled = false; + quint64 completed = 0; + if (!totalFiles) { + setErrorString(QLatin1String("The file count for current archive is null!")); + return false; + } + + QScopedPointer<archive, ScopedPointerReaderDeleter> reader(archive_read_new()); + QScopedPointer<archive, ScopedPointerWriterDeleter> writer(archive_write_disk_new()); + archive_entry *entry = nullptr; + + configureReader(reader.get()); + configureDiskWriter(writer.get()); + + DirectoryGuard targetDir(QFileInfo(dirPath).absolutePath()); + try { + const QStringList createdDirs = targetDir.tryCreate(); + // Make sure that all leading directories created get removed as well + foreach (const QString &directory, createdDirs) + emit currentEntryChanged(directory); + + int status = archive_read_open(reader.get(), m_data, nullptr, readCallback, nullptr); + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + forever { + if (m_cancelScheduled) + throw Error(QLatin1String("Extract canceled.")); + + status = archive_read_next_header(reader.get(), &entry); + if (status == ARCHIVE_EOF) + break; + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + const char *current = archive_entry_pathname(entry); + const QString outputPath = dirPath + QDir::separator() + QString::fromLocal8Bit(current); + archive_entry_set_pathname(entry, outputPath.toLocal8Bit()); + + emit currentEntryChanged(outputPath); + if (!writeEntry(reader.get(), writer.get(), entry)) + throw Error(errorString()); // appropriate error string set in writeEntry() + + ++completed; + emit completedChanged(completed, totalFiles); + + qApp->processEvents(); + } + } catch (const Error &e) { + setErrorString(e.message()); + m_data->file.seek(0); + return false; + } + targetDir.release(); + m_data->file.seek(0); + return true; +} + +/*! + \reimp + + Packages the given \a data into the archive and creates the file on disk. +*/ +bool LibArchiveArchive::create(const QStringList &data) +{ + QScopedPointer<archive, ScopedPointerWriterDeleter> writer(archive_write_new()); + configureWriter(writer.get()); + + try { + int status; + if ((status = archive_write_open_filename(writer.get(), m_data->file.fileName().toLocal8Bit()))) + throw Error(QLatin1String(archive_error_string(writer.get()))); + + for (auto &dataEntry : data) { + QScopedPointer<archive, ScopedPointerReaderDeleter> reader(archive_read_disk_new()); + configureDiskReader(reader.get()); + + if ((status = archive_read_disk_open(reader.get(), dataEntry.toLocal8Bit()))) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + QDir basePath = QFileInfo(dataEntry).dir(); + forever { + QScopedPointer<archive_entry, ScopedPointerEntryDeleter> entry(archive_entry_new()); + status = archive_read_next_header2(reader.get(), entry.get()); + if (status == ARCHIVE_EOF) + break; + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + const QFileInfo fileOrDir(pathWithoutNamespace(QLatin1String(archive_entry_sourcepath(entry.get())))); + // Set new path name in archive, otherwise we add all directories from absolute path + const QString newPath = basePath.relativeFilePath(fileOrDir.filePath()); + archive_entry_set_pathname(entry.get(), newPath.toLocal8Bit()); + + archive_read_disk_descend(reader.get()); + status = archive_write_header(writer.get(), entry.get()); + if (status < ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(writer.get()))); + + if (fileOrDir.isDir()) + continue; // nothing to copy + + QFile file(pathWithoutNamespace(QLatin1String(archive_entry_sourcepath(entry.get())))); + if (!file.open(QIODevice::ReadOnly)) + throw Error(file.errorString()); + + QByteArray buffer; + constexpr qint64 blockSize = 4 * 1024; + buffer.resize(blockSize); + + ssize_t bytesRead = readData(&file, buffer.data(), blockSize); + while (bytesRead > 0) { + archive_write_data(writer.get(), buffer.constData(), blockSize); + bytesRead = readData(&file, buffer.data(), blockSize); + } + file.close(); + } + } + } catch (const Error &e) { + setErrorString(e.message()); + return false; + } + return true; +} + +/*! + \reimp + + Returns the contents of this archive as an array of \c ArchiveEntry objects. + On failure, returns an empty array. +*/ +QVector<ArchiveEntry> LibArchiveArchive::list() +{ + QScopedPointer<archive, ScopedPointerReaderDeleter> reader(archive_read_new()); + archive_entry *entry = nullptr; + + configureReader(reader.get()); + + QVector<ArchiveEntry> entries; + try { + int status = archive_read_open(reader.get(), m_data, nullptr, readCallback, nullptr); + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + forever { + status = archive_read_next_header(reader.get(), &entry); + if (status == ARCHIVE_EOF) + break; + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + ArchiveEntry archiveEntry; + archiveEntry.path = QLatin1String(archive_entry_pathname(entry)); + archiveEntry.utcTime = QDateTime::fromTime_t(archive_entry_mtime(entry)); + archiveEntry.isDirectory = (archive_entry_filetype(entry) == AE_IFDIR); + archiveEntry.uncompressedSize = archive_entry_size(entry); + archiveEntry.permissions_mode = archive_entry_perm(entry); + + entries.append(archiveEntry); + } + } catch (const Error &e) { + setErrorString(e.message()); + m_data->file.seek(0); + return QVector<ArchiveEntry>(); + } + m_data->file.seek(0); + return entries; +} + +/*! + \reimp + + Returns \c true if the current archive is of a supported format; + \c false otherwise. +*/ +bool LibArchiveArchive::isSupported() +{ + QScopedPointer<archive, ScopedPointerReaderDeleter> reader(archive_read_new()); + configureReader(reader.get()); + + try { + const int status = archive_read_open(reader.get(), m_data, nullptr, readCallback, nullptr); + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + } catch (const Error &e) { + setErrorString(e.message()); + m_data->file.seek(0); + return false; + } + m_data->file.seek(0); + return true; +} + +/*! + Requests to extract the archive to \a dirPath with \a totalFiles + in a separate thread with a worker object. +*/ +void LibArchiveArchive::workerExtract(const QString &dirPath, const quint64 totalFiles) +{ + emit workerAboutToExtract(dirPath, totalFiles); +} + +/*! + Adds data to be read by the worker object in \a buffer. +*/ +void LibArchiveArchive::workerAddDataBlock(const QByteArray buffer) +{ + emit workerAboutToAddDataBlock(buffer); +} + +/*! + Signals the worker object that the client data is at end. +*/ +void LibArchiveArchive::workerSetDataAtEnd() +{ + emit workerAboutToSetDataAtEnd(); +} + +/*! + Cancels the extract in progress for the worker object. +*/ +void LibArchiveArchive::workerCancel() +{ + emit workerAboutToCancel(); +} + +/*! + Returns the status of the worker object. +*/ +ExtractWorker::Status LibArchiveArchive::workerStatus() const +{ + return m_worker.status(); +} + +/*! + \reimp + + Cancels the extract in progress. +*/ +void LibArchiveArchive::cancel() +{ + m_cancelScheduled = true; +} + +/*! + \internal +*/ +void LibArchiveArchive::onWorkerFinished(const QString &errorString) +{ + setErrorString(errorString); + emit workerFinished(); +} + +/*! + \internal +*/ +void LibArchiveArchive::configureReader(archive *archive) +{ + archive_read_support_filter_bzip2(archive); + archive_read_support_filter_gzip(archive); + archive_read_support_filter_xz(archive); + + archive_read_support_format_tar(archive); + archive_read_support_format_zip(archive); +} + +/*! + \internal +*/ +void LibArchiveArchive::configureWriter(archive *archive) +{ + if (QFileInfo(m_data->file.fileName()).suffix() == QLatin1String("zip")) { + archive_write_set_format_zip(archive); + } else { + archive_write_set_format_pax_restricted(archive); + archive_write_set_format_filter_by_ext(archive, m_data->file.fileName().toLatin1()); + } + const QByteArray options = "compression-level=" + QString::number(compressionLevel()).toLatin1(); + if (archive_write_set_options(archive, options.constData())) { // not fatal + qCWarning(QInstaller::lcInstallerInstallLog) << "Could not set options" << options + << "for archive" << m_data->file.fileName() << ":" << archive_error_string(archive); + } +} + +/*! + \internal +*/ +void LibArchiveArchive::configureDiskReader(archive *archive) +{ + archive_read_disk_set_standard_lookup(archive); +} + +/*! + \internal +*/ +void LibArchiveArchive::configureDiskWriter(archive *archive) +{ + constexpr int flags = ARCHIVE_EXTRACT_TIME + | ARCHIVE_EXTRACT_PERM + | ARCHIVE_EXTRACT_ACL + | ARCHIVE_EXTRACT_FFLAGS; + + archive_write_disk_set_options(archive, flags); + archive_write_disk_set_standard_lookup(archive); +} + +/*! + \internal +*/ +void LibArchiveArchive::initExtractWorker() +{ + m_worker.moveToThread(&m_workerThread); + + connect(this, &LibArchiveArchive::workerAboutToExtract, &m_worker, &ExtractWorker::extract); + connect(this, &LibArchiveArchive::workerAboutToAddDataBlock, &m_worker, &ExtractWorker::addDataBlock); + connect(this, &LibArchiveArchive::workerAboutToSetDataAtEnd, &m_worker, &ExtractWorker::dataAtEnd); + connect(this, &LibArchiveArchive::workerAboutToCancel, &m_worker, &ExtractWorker::cancel); + + connect(&m_worker, &ExtractWorker::dataBlockRequested, this, &LibArchiveArchive::dataBlockRequested); + connect(&m_worker, &ExtractWorker::finished, this, &LibArchiveArchive::onWorkerFinished); + + connect(&m_worker, &ExtractWorker::currentEntryChanged, this, &LibArchiveArchive::currentEntryChanged); + connect(&m_worker, &ExtractWorker::completedChanged, this, &LibArchiveArchive::completedChanged); + + m_workerThread.start(); +} + +/*! + Writes the current \a entry header, then pulls data from the archive \a reader + and writes it to the \a writer handle. +*/ +bool LibArchiveArchive::writeEntry(archive *reader, archive *writer, archive_entry *entry) +{ + int status; + const void *buff; + size_t size; + int64_t offset; + + status = archive_write_header(writer, entry); + if (status != ARCHIVE_OK) { + setErrorString(QLatin1String(archive_error_string(writer))); + return false; + } + + forever { + status = archive_read_data_block(reader, &buff, &size, &offset); + if (status == ARCHIVE_EOF) + return true; + if (status != ARCHIVE_OK) { + setErrorString(QLatin1String(archive_error_string(reader))); + return false; + } + status = archive_write_data_block(writer, buff, size, offset); + if (status != ARCHIVE_OK) { + setErrorString(QLatin1String(archive_error_string(writer))); + return false; + } + } +} + +/*! + \internal + + Reads \a data from the current position of \a file. The maximum bytes to + read are specified by \a maxSize. Returns the amount of bytes read. +*/ +qint64 LibArchiveArchive::readData(QFile *file, char *data, qint64 maxSize) +{ + if (!file->isOpen() || file->isSequential()) + return ARCHIVE_FATAL; + + if (file->atEnd() && file->seek(0)) + return ARCHIVE_OK; + + const qint64 bytesRead = file->read(data, maxSize); + if (bytesRead == -1) + return ARCHIVE_FATAL; + + return bytesRead; +} + +/*! + \internal + + Called by libarchive when new data is needed. Reads data from the file device + in \a archiveData into the buffer referenced by \a buff. Returns the number of bytes read. +*/ +ssize_t LibArchiveArchive::readCallback(archive *reader, void *archiveData, const void **buff) +{ + Q_UNUSED(reader) + constexpr qint64 blockSize = 1024 * 1024; // 1MB + + ArchiveData *data; + if (!(data = static_cast<ArchiveData *>(archiveData))) + return ARCHIVE_FATAL; + + if (!data->buffer.isEmpty()) + data->buffer.clear(); + + if (data->buffer.size() != blockSize) + data->buffer.resize(blockSize); + + if (!(*buff = static_cast<const void *>(data->buffer.constData()))) + return ARCHIVE_FATAL; + + // Doesn't matter if the buffer size exceeds the actual data read, + // the return value indicates the length of relevant bytes. + return readData(&data->file, data->buffer.data(), data->buffer.size()); +} + +/*! + Returns the \a path to a file or directory, without the Win32 namespace prefix. + On Unix platforms, the \a path is returned unaltered. +*/ +QString LibArchiveArchive::pathWithoutNamespace(const QString &path) +{ + QString aPath = path; +#ifdef Q_OS_WIN + if (aPath.size() > 4 && aPath.at(0) == QLatin1Char('\\') + && aPath.at(2) == QLatin1Char('?') && aPath.at(3) == QLatin1Char('\\')) { + aPath = aPath.mid(4); + } +#endif + return aPath; +} + +/*! + Returns the number of files in this archive. +*/ +quint64 LibArchiveArchive::totalFiles() +{ + QScopedPointer<archive, ScopedPointerReaderDeleter> reader(archive_read_new()); + archive_entry *entry = nullptr; + quint64 files = 0; + + configureReader(reader.get()); + + try { + int status = archive_read_open(reader.get(), m_data, nullptr, readCallback, nullptr); + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + forever { + status = archive_read_next_header(reader.get(), &entry); + if (status == ARCHIVE_EOF) + break; + if (status != ARCHIVE_OK) + throw Error(QLatin1String(archive_error_string(reader.get()))); + + ++files; + } + } catch (const Error &e) { + setErrorString(e.message()); + m_data->file.seek(0); + return 0; + } + m_data->file.seek(0); + return files; +} + +} // namespace QInstaller diff --git a/src/libs/installer/libarchivearchive.h b/src/libs/installer/libarchivearchive.h new file mode 100644 index 000000000..e0281e655 --- /dev/null +++ b/src/libs/installer/libarchivearchive.h @@ -0,0 +1,189 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#ifndef LIBARCHIVEARCHIVE_H +#define LIBARCHIVEARCHIVE_H + +#include "installer_global.h" +#include "abstractarchive.h" + +#include <archive.h> +#include <archive_entry.h> + +#include <QThread> + +#if defined(_MSC_VER) +#include <BaseTsd.h> +typedef SSIZE_T ssize_t; +#endif + +namespace QInstaller { + +class ExtractWorker : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(ExtractWorker) + +public: + enum Status { + Success = 0, + Failure = 1, + Canceled = 2, + Unfinished = 3 + }; + + ExtractWorker() = default; + + Status status() const; + +public Q_SLOTS: + void extract(const QString &dirPath, const quint64 totalFiles); + void addDataBlock(const QByteArray buffer); + void cancel(); + +Q_SIGNALS: + void dataBlockRequested(); + void dataAtEnd(); + void dataReadyForRead(); + void finished(const QString &errorString = QString()); + + void currentEntryChanged(const QString &filename); + void completedChanged(quint64 completed, quint64 total); + +private: + static ssize_t readCallback(archive *reader, void *caller, const void **buff); + bool writeEntry(archive *reader, archive *writer, archive_entry *entry); + +private: + QByteArray m_buffer; + Status m_status; +}; + +class INSTALLER_EXPORT LibArchiveArchive : public AbstractArchive +{ + Q_OBJECT + Q_DISABLE_COPY(LibArchiveArchive) + +public: + LibArchiveArchive(const QString &filename, QObject *parent = nullptr); + explicit LibArchiveArchive(QObject *parent = nullptr); + ~LibArchiveArchive(); + + bool open(QIODevice::OpenMode mode) Q_DECL_OVERRIDE; + void close() Q_DECL_OVERRIDE; + void setFilename(const QString &filename) Q_DECL_OVERRIDE; + + bool extract(const QString &dirPath) Q_DECL_OVERRIDE; + bool extract(const QString &dirPath, const quint64 totalFiles) Q_DECL_OVERRIDE; + bool create(const QStringList &data) Q_DECL_OVERRIDE; + QVector<ArchiveEntry> list() Q_DECL_OVERRIDE; + bool isSupported() Q_DECL_OVERRIDE; + + void workerExtract(const QString &dirPath, const quint64 totalFiles); + void workerAddDataBlock(const QByteArray buffer); + void workerSetDataAtEnd(); + void workerCancel(); + ExtractWorker::Status workerStatus() const; + +Q_SIGNALS: + void dataBlockRequested(); + void workerFinished(); + + void workerAboutToExtract(const QString &dirPath, const quint64 totalFiles); + void workerAboutToAddDataBlock(const QByteArray buffer); + void workerAboutToSetDataAtEnd(); + void workerAboutToCancel(); + +public Q_SLOTS: + void cancel() Q_DECL_OVERRIDE; + +private Q_SLOTS: + void onWorkerFinished(const QString &errorString); + +private: + static void configureReader(archive *archive); + void configureWriter(archive *archive); + static void configureDiskReader(archive *archive); + static void configureDiskWriter(archive *archive); + + void initExtractWorker(); + + bool writeEntry(archive *reader, archive *writer, archive_entry *entry); + + static qint64 readData(QFile *file, char *data, qint64 maxSize); + static ssize_t readCallback(archive *reader, void *archiveData, const void **buff); + + static QString pathWithoutNamespace(const QString &path); + + quint64 totalFiles(); + +private: + friend class ExtractWorker; + friend class LibArchiveWrapperPrivate; + + struct ArchiveData + { + QFile file; + QByteArray buffer; + }; + +private: + ArchiveData *m_data; + ExtractWorker m_worker; + QThread m_workerThread; + + bool m_cancelScheduled; +}; + +struct ScopedPointerReaderDeleter +{ + static inline void cleanup(archive *p) + { + archive_read_free(p); + } +}; + +struct ScopedPointerWriterDeleter +{ + static inline void cleanup(archive *p) + { + archive_write_free(p); + } +}; + +struct ScopedPointerEntryDeleter +{ + static inline void cleanup(archive_entry *p) + { + archive_entry_free(p); + } +}; + +} // namespace QInstaller + +#endif // LIBARCHIVEARCHIVE_H diff --git a/src/libs/installer/libarchivewrapper.cpp b/src/libs/installer/libarchivewrapper.cpp new file mode 100644 index 000000000..c259678ca --- /dev/null +++ b/src/libs/installer/libarchivewrapper.cpp @@ -0,0 +1,204 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#include "libarchivewrapper.h" + +namespace QInstaller { + +/*! + \inmodule QtInstallerFramework + \class QInstaller::LibArchiveWrapper + \brief The LibArchiveWrapper class provides an interface for interacting + with archives handled using the libarchive archive and compression library. + + The invoked archive operations are performed in a normal user mode, or in an + on-demand elevated user mode through the remote client-server protocol of the framework. + + This class is not thread-safe; extra care should be taken especially when + elevated mode is active to not call its functions from any thread other than + where the object was created. +*/ + +/*! + \fn QInstaller::LibArchiveWrapper::currentEntryChanged(const QString &filename) + + Current entry changed to \a filename. Emitted when the entry to process is changed. +*/ + +/*! + \fn QInstaller::LibArchiveWrapper::completedChanged(quint64 completed, quint64 total) + + The ratio of \a completed entries from \a total changed. + Emitted when the progress changes. +*/ + +/*! + Constructs an archive object representing an archive file + specified by \a filename with \a parent as the parent object. +*/ +LibArchiveWrapper::LibArchiveWrapper(const QString &filename, QObject *parent) + : AbstractArchive(parent) + , d(new LibArchiveWrapperPrivate(filename)) +{ + connect(d, &LibArchiveWrapperPrivate::currentEntryChanged, + this, &LibArchiveWrapper::currentEntryChanged); + connect(d, &LibArchiveWrapperPrivate::completedChanged, + this, &LibArchiveWrapper::completedChanged); +} + +/*! + Constructs an archive object with the given \a parent. +*/ +LibArchiveWrapper::LibArchiveWrapper(QObject *parent) + : AbstractArchive(parent) + , d(new LibArchiveWrapperPrivate()) +{ + connect(d, &LibArchiveWrapperPrivate::currentEntryChanged, + this, &LibArchiveWrapper::currentEntryChanged); + connect(d, &LibArchiveWrapperPrivate::completedChanged, + this, &LibArchiveWrapper::completedChanged); +} + +/*! + Destroys the instance. +*/ +LibArchiveWrapper::~LibArchiveWrapper() +{ + delete d; +} + +/*! + Opens the file device using \a mode. + Returns \c true if succesfull; otherwise \c false. +*/ +bool LibArchiveWrapper::open(QIODevice::OpenMode mode) +{ + return d->open(mode); +} + +/*! + Closes the file device. +*/ +void LibArchiveWrapper::close() +{ + d->close(); +} + +/*! + Sets the \a filename for the archive. + + If the remote connection is active, the same method is called by the server. +*/ +void LibArchiveWrapper::setFilename(const QString &filename) +{ + d->setFilename(filename); +} + +/*! + Returns a human-readable description of the last error that occurred. + + If the remote connection is active, the method is called by the server instead. +*/ +QString LibArchiveWrapper::errorString() const +{ + return d->errorString(); +} + +/*! + Extracts the contents of this archive to \a dirPath. Returns \c true + on success; \c false otherwise. + + If the remote connection is active, the method is called by the server instead, + with the client starting a new event loop waiting for the extraction to finish. +*/ +bool LibArchiveWrapper::extract(const QString &dirPath) +{ + return d->extract(dirPath); +} + +/*! + Extracts the contents of this archive to \a dirPath with + precalculated count of \a totalFiles. Returns \c true + on success; \c false otherwise. + + If the remote connection is active, the method is called by the server instead, + with the client starting a new event loop waiting for the extraction to finish. +*/ +bool LibArchiveWrapper::extract(const QString &dirPath, const quint64 totalFiles) +{ + return d->extract(dirPath, totalFiles); +} + +/*! + Packages the given \a data into the archive and creates the file on disk. + Returns \c true on success; \c false otherwise. + + If the remote connection is active, the method is called by the server instead. +*/ +bool LibArchiveWrapper::create(const QStringList &data) +{ + return d->create(data); +} + +/*! + Returns the contents of this archive as an array of \c ArchiveEntry objects. + On failure, returns an empty array. +*/ +QVector<ArchiveEntry> LibArchiveWrapper::list() +{ + return d->list(); +} + +/*! + Returns \c true if the current archive is of a supported format; + \c false otherwise. +*/ +bool LibArchiveWrapper::isSupported() +{ + return d->isSupported(); +} + +/*! + Sets the compression level for new archives to \a level. +*/ +void LibArchiveWrapper::setCompressionLevel(const CompressionLevel level) +{ + d->setCompressionLevel(level); +} + +/*! + Cancels the extract operation in progress. + + If the remote connection is active, the method is called by the server instead. +*/ +void LibArchiveWrapper::cancel() +{ + d->cancel(); +} + +} // namespace QInstaller diff --git a/src/libs/installer/libarchivewrapper.h b/src/libs/installer/libarchivewrapper.h new file mode 100644 index 000000000..c638d10dc --- /dev/null +++ b/src/libs/installer/libarchivewrapper.h @@ -0,0 +1,71 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#ifndef LIBARCHIVEWRAPPER_H +#define LIBARCHIVEWRAPPER_H + +#include "installer_global.h" +#include "abstractarchive.h" +#include "libarchivewrapper_p.h" + +namespace QInstaller { + +class INSTALLER_EXPORT LibArchiveWrapper : public AbstractArchive +{ + Q_OBJECT + Q_DISABLE_COPY(LibArchiveWrapper) + +public: + LibArchiveWrapper(const QString &filename, QObject *parent = nullptr); + explicit LibArchiveWrapper(QObject *parent = nullptr); + ~LibArchiveWrapper(); + + bool open(QIODevice::OpenMode mode) Q_DECL_OVERRIDE; + void close() Q_DECL_OVERRIDE; + void setFilename(const QString &filename) Q_DECL_OVERRIDE; + + QString errorString() const Q_DECL_OVERRIDE; + + bool extract(const QString &dirPath) Q_DECL_OVERRIDE; + bool extract(const QString &dirPath, const quint64 totalFiles) Q_DECL_OVERRIDE; + bool create(const QStringList &data) Q_DECL_OVERRIDE; + QVector<ArchiveEntry> list() Q_DECL_OVERRIDE; + bool isSupported() Q_DECL_OVERRIDE; + + void setCompressionLevel(const AbstractArchive::CompressionLevel level) Q_DECL_OVERRIDE; + +public Q_SLOTS: + void cancel() Q_DECL_OVERRIDE; + +private: + LibArchiveWrapperPrivate *const d; +}; + +} // namespace QInstaller + +#endif // LIBARCHIVEWRAPPER_H diff --git a/src/libs/installer/libarchivewrapper_p.cpp b/src/libs/installer/libarchivewrapper_p.cpp new file mode 100644 index 000000000..5509812cf --- /dev/null +++ b/src/libs/installer/libarchivewrapper_p.cpp @@ -0,0 +1,356 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#include "libarchivewrapper_p.h" + +#include "globals.h" + +#include <QFileInfo> + +namespace QInstaller { + +/*! + \inmodule QtInstallerFramework + \class QInstaller::LibArchiveWrapperPrivate + \internal +*/ + +/*! + \fn QInstaller::ArchiveWrapper::dataBlockRequested() + + Emitted when the server process has requested another data block. +*/ + +/*! + \fn QInstaller::ArchiveWrapper::remoteWorkerFinished() + + Emitted when the server process has finished extracting an archive. +*/ + +/*! + Constructs an archive object representing an archive file + specified by \a filename. +*/ +LibArchiveWrapperPrivate::LibArchiveWrapperPrivate(const QString &filename) + : RemoteObject(QLatin1String(Protocol::AbstractArchive)) +{ + init(); + LibArchiveWrapperPrivate::setFilename(filename); +} + +/*! + Constructs the object. +*/ +LibArchiveWrapperPrivate::LibArchiveWrapperPrivate() + : RemoteObject(QLatin1String(Protocol::AbstractArchive)) +{ + init(); +} + +/*! + Destroys the instance. +*/ +LibArchiveWrapperPrivate::~LibArchiveWrapperPrivate() +{ + m_timer.stop(); +} + +/*! + Opens the file device using \a mode. + Returns \c true if succesfull; otherwise \c false. +*/ +bool LibArchiveWrapperPrivate::open(QIODevice::OpenMode mode) +{ + return m_archive.open(mode); +} + +/*! + Closes the file device. +*/ +void LibArchiveWrapperPrivate::close() +{ + m_archive.close(); +} + +/*! + Sets the \a filename for the archive. + + If the remote connection is active, the same method is called by the server. +*/ +void LibArchiveWrapperPrivate::setFilename(const QString &filename) +{ + if (connectToServer()) { + m_lock.lockForWrite(); + callRemoteMethod(QLatin1String(Protocol::AbstractArchiveSetFilename), filename, dummy); + m_lock.unlock(); + } + m_archive.setFilename(filename); +} + +/*! + Returns a human-readable description of the last error that occurred. + + If the remote connection is active, the method is called by the server instead. +*/ +QString LibArchiveWrapperPrivate::errorString() const +{ + if ((const_cast<LibArchiveWrapperPrivate *>(this))->connectToServer()) { + m_lock.lockForWrite(); + const QString errorString + = callRemoteMethod<QString>(QLatin1String(Protocol::AbstractArchiveErrorString)); + m_lock.unlock(); + return errorString; + } + return m_archive.errorString(); +} + +/*! + Extracts the contents of this archive to \a dirPath with + an optional precalculated count of \a totalFiles. Returns \c true + on success; \c false otherwise. + + If the remote connection is active, the method is called by the server instead, + with the client starting a new event loop waiting for the extraction to finish. +*/ +bool LibArchiveWrapperPrivate::extract(const QString &dirPath, const quint64 totalFiles) +{ + if (connectToServer()) { + m_lock.lockForWrite(); + callRemoteMethod(QLatin1String(Protocol::AbstractArchiveExtract), dirPath, totalFiles); + m_lock.unlock(); + { + QEventLoop loop; + connect(this, &LibArchiveWrapperPrivate::remoteWorkerFinished, &loop, &QEventLoop::quit); + loop.exec(); + } + return (workerStatus() == ExtractWorker::Success); + } + return m_archive.extract(dirPath, totalFiles); +} + +/*! + Packages the given \a data into the archive and creates the file on disk. + Returns \c true on success; \c false otherwise. + + If the remote connection is active, the method is called by the server instead. +*/ +bool LibArchiveWrapperPrivate::create(const QStringList &data) +{ + if (connectToServer()) { + m_lock.lockForWrite(); + const bool success + = callRemoteMethod<bool>(QLatin1String(Protocol::AbstractArchiveCreate), data); + m_lock.unlock(); + return success; + } + return m_archive.create(data); +} + +/*! + Returns the contents of this archive as an array of \c ArchiveEntry objects. + On failure, returns an empty array. +*/ +QVector<ArchiveEntry> LibArchiveWrapperPrivate::list() +{ + return m_archive.list(); +} + +/*! + Returns \c true if the current archive is of a supported format; + \c false otherwise. +*/ +bool LibArchiveWrapperPrivate::isSupported() +{ + return m_archive.isSupported(); +} + +/*! + Sets the compression level for new archives to \a level. + + If the remote connection is active, the method is called by the server instead. +*/ +void LibArchiveWrapperPrivate::setCompressionLevel(const AbstractArchive::CompressionLevel level) +{ + if (connectToServer()) { + m_lock.lockForWrite(); + callRemoteMethod(QLatin1String(Protocol::AbstractArchiveSetCompressionLevel), level, dummy); + m_lock.unlock(); + return; + } + m_archive.setCompressionLevel(level); +} + +/*! + Cancels the extract operation in progress. + + If the remote connection is active, the method is called by the server instead. +*/ +void LibArchiveWrapperPrivate::cancel() +{ + if (connectToServer()) { + m_lock.lockForWrite(); + callRemoteMethod(QLatin1String(Protocol::AbstractArchiveCancel)); + m_lock.unlock(); + return; + } + m_archive.cancel(); +} + +/*! + Calls a remote method to get the associated queued signals from the server. + Signals are then processed and emitted client-side. +*/ +void LibArchiveWrapperPrivate::processSignals() +{ + if (!isConnectedToServer()) + return; + + if (!m_lock.tryLockForRead()) + return; + + QList<QVariant> receivedSignals = + callRemoteMethod<QList<QVariant>>(QString::fromLatin1(Protocol::GetAbstractArchiveSignals)); + + m_lock.unlock(); + while (!receivedSignals.isEmpty()) { + const QString name = receivedSignals.takeFirst().toString(); + if (name == QLatin1String(Protocol::AbstractArchiveSignalCurrentEntryChanged)) { + emit currentEntryChanged(receivedSignals.takeFirst().toString()); + } else if (name == QLatin1String(Protocol::AbstractArchiveSignalCompletedChanged)) { + const quint64 completed = receivedSignals.takeFirst().value<quint64>(); + const quint64 total = receivedSignals.takeFirst().value<quint64>(); + emit completedChanged(completed, total); + } else if (name == QLatin1String(Protocol::AbstractArchiveSignalDataBlockRequested)) { + emit dataBlockRequested(); + } else if (name == QLatin1String(Protocol::AbstractArchiveSignalWorkerFinished)) { + emit remoteWorkerFinished(); + } + } +} + +/*! + Reads a block of data from the current position of the underlying file device. +*/ +void LibArchiveWrapperPrivate::onDataBlockRequested() +{ + constexpr quint64 blockSize = 10 * 1024 * 1024; // 10MB + + QFile *const file = &m_archive.m_data->file; + if (!file->isOpen() || file->isSequential()) { + qCWarning(QInstaller::lcInstallerInstallLog) << file->errorString(); + setClientDataAtEnd(); + return; + } + if (file->atEnd() && file->seek(0)) { + setClientDataAtEnd(); + return; + } + + QByteArray *const buff = &m_archive.m_data->buffer; + if (!buff->isEmpty()) + buff->clear(); + + if (buff->size() != blockSize) + buff->resize(blockSize); + + const qint64 bytesRead = file->read(buff->data(), blockSize); + if (bytesRead == -1) { + qCWarning(QInstaller::lcInstallerInstallLog) << file->errorString(); + setClientDataAtEnd(); + return; + } + // The read callback in ExtractWorker class expects the buffer size to + // match the number of bytes read. Some formats will fail if the buffer + // is larger than the actual data. + if (buff->size() != bytesRead) + buff->resize(bytesRead); + + addDataBlock(*buff); +} + +/*! + Starts the timer to process server-side signals and connects handler + signals for the matching signals of the wrapper object. +*/ +void LibArchiveWrapperPrivate::init() +{ + m_timer.start(250); + QObject::connect(&m_timer, &QTimer::timeout, + this, &LibArchiveWrapperPrivate::processSignals); + + QObject::connect(&m_archive, &LibArchiveArchive::currentEntryChanged, + this, &LibArchiveWrapperPrivate::currentEntryChanged); + QObject::connect(&m_archive, &LibArchiveArchive::completedChanged, + this, &LibArchiveWrapperPrivate::completedChanged); + + QObject::connect(this, &LibArchiveWrapperPrivate::dataBlockRequested, + this, &LibArchiveWrapperPrivate::onDataBlockRequested); +} + +/*! + Calls a remote method to add a \a buffer for reading. +*/ +void LibArchiveWrapperPrivate::addDataBlock(const QByteArray &buffer) +{ + if (connectToServer()) { + m_lock.lockForWrite(); + callRemoteMethod(QLatin1String(Protocol::AbstractArchiveAddDataBlock), buffer, dummy); + m_lock.unlock(); + } +} + +/*! + Calls a remote method to inform that the client has finished + reading the current file. +*/ +void LibArchiveWrapperPrivate::setClientDataAtEnd() +{ + if (connectToServer()) { + m_lock.lockForWrite(); + callRemoteMethod(QLatin1String(Protocol::AbstractArchiveSetClientDataAtEnd)); + m_lock.unlock(); + } +} + +/*! + Calls a remote method to retrieve and return the status of + the extract worker on a server process. +*/ +ExtractWorker::Status LibArchiveWrapperPrivate::workerStatus() const +{ + ExtractWorker::Status status = ExtractWorker::Unfinished; + if ((const_cast<LibArchiveWrapperPrivate *>(this))->connectToServer()) { + m_lock.lockForWrite(); + status = static_cast<ExtractWorker::Status>( + callRemoteMethod<qint32>(QLatin1String(Protocol::AbstractArchiveWorkerStatus))); + m_lock.unlock(); + } + return status; +} + +} // namespace QInstaller diff --git a/src/libs/installer/libarchivewrapper_p.h b/src/libs/installer/libarchivewrapper_p.h new file mode 100644 index 000000000..ea8409da0 --- /dev/null +++ b/src/libs/installer/libarchivewrapper_p.h @@ -0,0 +1,94 @@ +/************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +**************************************************************************/ + +#ifndef LIBARCHIVEWRAPPER_P_H +#define LIBARCHIVEWRAPPER_P_H + +#include "installer_global.h" +#include "remoteobject.h" +#include "libarchivearchive.h" +#include "lib7zarchive.h" + +#include <QTimer> +#include <QReadWriteLock> + +namespace QInstaller { + +class INSTALLER_EXPORT LibArchiveWrapperPrivate : public RemoteObject +{ + Q_OBJECT + Q_DISABLE_COPY(LibArchiveWrapperPrivate) + +public: + explicit LibArchiveWrapperPrivate(const QString &filename); + LibArchiveWrapperPrivate(); + ~LibArchiveWrapperPrivate(); + + bool open(QIODevice::OpenMode mode); + void close(); + void setFilename(const QString &filename); + + QString errorString() const; + + bool extract(const QString &dirPath, const quint64 totalFiles = 0); + bool create(const QStringList &data); + QVector<ArchiveEntry> list(); + bool isSupported(); + + void setCompressionLevel(const AbstractArchive::CompressionLevel level); + +Q_SIGNALS: + void currentEntryChanged(const QString &filename); + void completedChanged(const quint64 completed, const quint64 total); + void dataBlockRequested(); + void remoteWorkerFinished(); + +public Q_SLOTS: + void cancel(); + +private Q_SLOTS: + void processSignals(); + void onDataBlockRequested(); + +private: + void init(); + + void addDataBlock(const QByteArray &buffer); + void setClientDataAtEnd(); + ExtractWorker::Status workerStatus() const; + +private: + QTimer m_timer; + mutable QReadWriteLock m_lock; + + LibArchiveArchive m_archive; +}; + +} // namespace QInstaller + +#endif // LIBARCHIVEWRAPPER_P_H diff --git a/src/libs/installer/metadatajob_p.h b/src/libs/installer/metadatajob_p.h index 9160f4cc9..99b48e626 100644 --- a/src/libs/installer/metadatajob_p.h +++ b/src/libs/installer/metadatajob_p.h @@ -1,6 +1,6 @@ /************************************************************************** ** -** Copyright (C) 2017 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -29,8 +29,7 @@ #ifndef METADATAJOB_P_H #define METADATAJOB_P_H -#include "lib7z_extract.h" -#include "lib7z_facade.h" +#include "lib7zarchive.h" #include "metadatajob.h" #include <QDir> @@ -76,21 +75,15 @@ public: return; // ignore already canceled } - QFile archive(m_archive); - if (archive.open(QIODevice::ReadOnly)) { - try { - Lib7z::extractArchive(&archive, m_targetDir); - } catch (const Lib7z::SevenZipException& e) { - fi.reportException(UnzipArchiveException(MetadataJob::tr("Error while extracting " - "archive \"%1\": %2").arg(QDir::toNativeSeparators(m_archive), e.message()))); - } catch (...) { - fi.reportException(UnzipArchiveException(MetadataJob::tr("Unknown exception " - "caught while extracting archive \"%1\".").arg(QDir::toNativeSeparators(m_archive)))); - } - } else { + Lib7zArchive archive(m_archive); + if (!archive.open(QIODevice::ReadOnly)) { fi.reportException(UnzipArchiveException(MetadataJob::tr("Cannot open file \"%1\" for " "reading: %2").arg(QDir::toNativeSeparators(m_archive), archive.errorString()))); } + if (!archive.extract(m_targetDir)) { + fi.reportException(UnzipArchiveException(MetadataJob::tr("Error while extracting " + "archive \"%1\": %2").arg(QDir::toNativeSeparators(m_archive), archive.errorString()))); + } fi.reportFinished(); } diff --git a/src/libs/installer/protocol.h b/src/libs/installer/protocol.h index 9ead7943c..c7eb9a308 100644 --- a/src/libs/installer/protocol.h +++ b/src/libs/installer/protocol.h @@ -1,6 +1,6 @@ /************************************************************************** ** -** Copyright (C) 2017 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Installer Framework. @@ -163,6 +163,29 @@ const char QAbstractFileEngineSyncToDisk[] = "QAbstractFileEngine::syncToDisk"; const char QAbstractFileEngineRenameOverwrite[] = "QAbstractFileEngine::renameOverwrite"; const char QAbstractFileEngineFileTime[] = "QAbstractFileEngine::fileTime"; + +// LibArchiveWrapper +const char AbstractArchive[] = "AbstractArchive"; +const char AbstractArchiveOpen[] = "AbstractArchive::open"; +const char AbstractArchiveClose[] = "AbstractArchive::close"; +const char AbstractArchiveSetFilename[] = "AbstractArchive::setFilename"; +const char AbstractArchiveErrorString[] = "AbstractArchive::errorString"; +const char AbstractArchiveExtract[] = "AbstractArchive::extract"; +const char AbstractArchiveCreate[] = "AbstractArchive::create"; +const char AbstractArchiveList[] = "AbstractArchive::list"; +const char AbstractArchiveIsSupported[] = "AbstractArchive::isSupported"; +const char AbstractArchiveSetCompressionLevel[] = "AbstractArchive::setCompressionLevel"; +const char AbstractArchiveAddDataBlock[] = "AbstractArchive::addDataBlock"; +const char AbstractArchiveSetClientDataAtEnd[] = "AbstractArchive::setClientDataAtEnd"; +const char AbstractArchiveWorkerStatus[] = "AbstractArchive::workerStatus"; +const char AbstractArchiveCancel[] = "AbstractArchive::cancel"; + +const char GetAbstractArchiveSignals[] = "GetAbstractArchiveSignals"; +const char AbstractArchiveSignalCurrentEntryChanged[] = "AbstractArchive::currentEntryChanged"; +const char AbstractArchiveSignalCompletedChanged[] = "AbstractArchive::completedChanged"; +const char AbstractArchiveSignalDataBlockRequested[] = "AbstractArchive::dataBlockRequested"; +const char AbstractArchiveSignalWorkerFinished[] = "AbstractArchive::workerFinished"; + } // namespace Protocol void INSTALLER_EXPORT sendPacket(QIODevice *device, const QByteArray &command, const QByteArray &data); diff --git a/src/libs/installer/remoteserverconnection.cpp b/src/libs/installer/remoteserverconnection.cpp index a6b66c081..5d72f834d 100644 --- a/src/libs/installer/remoteserverconnection.cpp +++ b/src/libs/installer/remoteserverconnection.cpp @@ -34,6 +34,9 @@ #include "utils.h" #include "permissionsettings.h" #include "globals.h" +#ifdef IFW_LIBARCHIVE +#include "libarchivearchive.h" +#endif #include <QCoreApplication> #include <QDataStream> @@ -59,8 +62,10 @@ RemoteServerConnection::RemoteServerConnection(qintptr socketDescriptor, const Q , m_socketDescriptor(socketDescriptor) , m_process(nullptr) , m_engine(nullptr) + , m_archive(nullptr) , m_authorizationKey(key) - , m_signalReceiver(nullptr) + , m_processSignalReceiver(nullptr) + , m_archiveSignalReceiver(nullptr) { setObjectName(QString::fromLatin1("RemoteServerConnection(%1)").arg(socketDescriptor)); } @@ -89,6 +94,7 @@ void RemoteServerConnection::run() if (!receivePacket(&socket, &cmd, &data)) { socket.waitForReadyRead(250); + qApp->processEvents(); continue; } @@ -142,11 +148,20 @@ void RemoteServerConnection::run() if (m_process) m_process->deleteLater(); m_process = new QProcess; - m_signalReceiver = new QProcessSignalReceiver(m_process); + m_processSignalReceiver = new QProcessSignalReceiver(m_process); } else if (type == QLatin1String(Protocol::QAbstractFileEngine)) { if (m_engine) delete m_engine; m_engine = new QFSFileEngine; + } else if (type == QLatin1String(Protocol::AbstractArchive)) { +#ifdef IFW_LIBARCHIVE + if (m_archive) + m_archive->deleteLater(); + m_archive = new LibArchiveArchive; + m_archiveSignalReceiver = new AbstractArchiveSignalReceiver(static_cast<LibArchiveArchive *>(m_archive)); +#else + Q_ASSERT_X(false, Q_FUNC_INFO, "No compatible archive handler exists for protocol."); +#endif } continue; } @@ -157,24 +172,44 @@ void RemoteServerConnection::run() if (type == QLatin1String(Protocol::QSettings)) { settings.reset(); } else if (type == QLatin1String(Protocol::QProcess)) { - m_signalReceiver->m_receivedSignals.clear(); + m_processSignalReceiver->m_receivedSignals.clear(); m_process->deleteLater(); m_process = nullptr; } else if (type == QLatin1String(Protocol::QAbstractFileEngine)) { delete m_engine; m_engine = nullptr; + } else if (type == QLatin1String(Protocol::AbstractArchive)) { +#ifdef IFW_LIBARCHIVE + m_archiveSignalReceiver->m_receivedSignals.clear(); + m_archive->deleteLater(); + m_archive = nullptr; +#else + Q_ASSERT_X(false, Q_FUNC_INFO, "No compatible archive handler exists for protocol."); +#endif } return; } if (command == QLatin1String(Protocol::GetQProcessSignals)) { - if (m_signalReceiver) { - QMutexLocker _(&m_signalReceiver->m_lock); - sendData(&socket, m_signalReceiver->m_receivedSignals); + if (m_processSignalReceiver) { + QMutexLocker _(&m_processSignalReceiver->m_lock); + sendData(&socket, m_processSignalReceiver->m_receivedSignals); socket.flush(); - m_signalReceiver->m_receivedSignals.clear(); + m_processSignalReceiver->m_receivedSignals.clear(); } continue; + } else if (command == QLatin1String(Protocol::GetAbstractArchiveSignals)) { +#ifdef IFW_LIBARCHIVE + if (m_archiveSignalReceiver) { + QMutexLocker _(&m_archiveSignalReceiver->m_lock); + sendData(&socket, m_archiveSignalReceiver->m_receivedSignals); + socket.flush(); + m_archiveSignalReceiver->m_receivedSignals.clear(); + } + continue; +#else + Q_ASSERT_X(false, Q_FUNC_INFO, "No compatible archive handler exists for protocol."); +#endif } if (command.startsWith(QLatin1String(Protocol::QProcess))) { @@ -183,6 +218,8 @@ void RemoteServerConnection::run() handleQSettings(&socket, command, stream, settings.data()); } else if (command.startsWith(QLatin1String(Protocol::QAbstractFileEngine))) { handleQFSFileEngine(&socket, command, stream); + } else if (command.startsWith(QLatin1String(Protocol::AbstractArchive))) { + handleArchive(&socket, command, stream); } else { qCDebug(QInstaller::lcServer) << "Unknown command:" << command; } @@ -519,4 +556,56 @@ void RemoteServerConnection::handleQFSFileEngine(QIODevice *socket, const QStrin } } +void RemoteServerConnection::handleArchive(QIODevice *socket, const QString &command, QDataStream &data) +{ +#ifdef IFW_LIBARCHIVE + LibArchiveArchive *archive = static_cast<LibArchiveArchive *>(m_archive); + if (command == QLatin1String(Protocol::AbstractArchiveOpen)) { + qint32 openMode; + data >> openMode; + sendData(socket, archive->open(static_cast<QIODevice::OpenMode>(openMode))); + } else if (command == QLatin1String(Protocol::AbstractArchiveClose)) { + archive->close(); + } else if (command == QLatin1String(Protocol::AbstractArchiveSetFilename)) { + QString fileName; + data >> fileName; + archive->setFilename(fileName); + } else if (command == QLatin1String(Protocol::AbstractArchiveErrorString)) { + sendData(socket, archive->errorString()); + } else if (command == QLatin1String(Protocol::AbstractArchiveExtract)) { + QString dirPath; + quint64 total; + data >> dirPath; + data >> total; + archive->workerExtract(dirPath, total); + } else if (command == QLatin1String(Protocol::AbstractArchiveCreate)) { + QStringList entries; + data >> entries; + sendData(socket, archive->create(entries)); + } else if (command == QLatin1String(Protocol::AbstractArchiveList)) { + sendData(socket, archive->list()); + } else if (command == QLatin1String(Protocol::AbstractArchiveIsSupported)) { + sendData(socket, archive->isSupported()); + } else if (command == QLatin1String(Protocol::AbstractArchiveSetCompressionLevel)) { + qint32 level; + data >> level; + archive->setCompressionLevel(static_cast<AbstractArchive::CompressionLevel>(level)); + } else if (command == QLatin1String(Protocol::AbstractArchiveAddDataBlock)) { + QByteArray buff; + data >> buff; + archive->workerAddDataBlock(buff); + } else if (command == QLatin1String(Protocol::AbstractArchiveSetClientDataAtEnd)) { + archive->workerSetDataAtEnd(); + } else if (command == QLatin1String(Protocol::AbstractArchiveWorkerStatus)) { + sendData(socket, static_cast<qint32>(archive->workerStatus())); + } else if (command == QLatin1String(Protocol::AbstractArchiveCancel)) { + archive->workerCancel(); + } else if (!command.isEmpty()) { + qCDebug(QInstaller::lcServer) << "Unknown AbstractArchive command:" << command; + } +#else + Q_ASSERT_X(false, Q_FUNC_INFO, "No compatible archive handler exists for protocol."); +#endif +} + } // namespace QInstaller diff --git a/src/libs/installer/remoteserverconnection.h b/src/libs/installer/remoteserverconnection.h index 07cfb98c0..ccb8e153d 100644 --- a/src/libs/installer/remoteserverconnection.h +++ b/src/libs/installer/remoteserverconnection.h @@ -29,6 +29,8 @@ #ifndef REMOTESERVERCONNECTION_H #define REMOTESERVERCONNECTION_H +#include "abstractarchive.h" + #include <QPointer> #include <QThread> @@ -44,6 +46,7 @@ namespace QInstaller { class PermissionSettings; class QProcessSignalReceiver; +class AbstractArchiveSignalReceiver; class RemoteServerConnection : public QThread { @@ -66,14 +69,17 @@ private: void handleQSettings(QIODevice *device, const QString &command, QDataStream &data, PermissionSettings *settings); void handleQFSFileEngine(QIODevice *device, const QString &command, QDataStream &data); + void handleArchive(QIODevice *device, const QString &command, QDataStream &data); private: qintptr m_socketDescriptor; QProcess *m_process; QFSFileEngine *m_engine; + AbstractArchive *m_archive; QString m_authorizationKey; - QProcessSignalReceiver *m_signalReceiver; + QProcessSignalReceiver *m_processSignalReceiver; + AbstractArchiveSignalReceiver *m_archiveSignalReceiver; }; } // namespace QInstaller diff --git a/src/libs/installer/remoteserverconnection_p.h b/src/libs/installer/remoteserverconnection_p.h index dad5f4133..dc6d794b6 100644 --- a/src/libs/installer/remoteserverconnection_p.h +++ b/src/libs/installer/remoteserverconnection_p.h @@ -30,6 +30,9 @@ #define REMOTESERVERCONNECTION_P_H #include "protocol.h" +#ifdef IFW_LIBARCHIVE +#include "libarchivearchive.h" +#endif #include <QMutex> #include <QProcess> @@ -124,6 +127,60 @@ private: QVariantList m_receivedSignals; }; +#ifdef IFW_LIBARCHIVE +class AbstractArchiveSignalReceiver : public QObject +{ + Q_OBJECT + friend class RemoteServerConnection; + +private: + explicit AbstractArchiveSignalReceiver(LibArchiveArchive *archive) + : QObject(archive) + { + connect(archive, &LibArchiveArchive::currentEntryChanged, + this, &AbstractArchiveSignalReceiver::onCurrentEntryChanged); + connect(archive, &LibArchiveArchive::completedChanged, + this, &AbstractArchiveSignalReceiver::onCompletedChanged); + connect(archive, &LibArchiveArchive::dataBlockRequested, + this, &AbstractArchiveSignalReceiver::onDataBlockRequested); + connect(archive, &LibArchiveArchive::workerFinished, + this, &AbstractArchiveSignalReceiver::onWorkerFinished); + } + +private Q_SLOTS: + void onCurrentEntryChanged(const QString &filename) + { + QMutexLocker _(&m_lock); + m_receivedSignals.append(QLatin1String(Protocol::AbstractArchiveSignalCurrentEntryChanged)); + m_receivedSignals.append(filename); + } + + void onCompletedChanged(quint64 completed, quint64 total) + { + QMutexLocker _(&m_lock); + m_receivedSignals.append(QLatin1String(Protocol::AbstractArchiveSignalCompletedChanged)); + m_receivedSignals.append(completed); + m_receivedSignals.append(total); + } + + void onDataBlockRequested() + { + QMutexLocker _(&m_lock); + m_receivedSignals.append(QLatin1String(Protocol::AbstractArchiveSignalDataBlockRequested)); + } + + void onWorkerFinished() + { + QMutexLocker _(&m_lock); + m_receivedSignals.append(QLatin1String(Protocol::AbstractArchiveSignalWorkerFinished)); + } + +private: + QMutex m_lock; + QVariantList m_receivedSignals; +}; +#endif + } // namespace QInstaller #endif // REMOTESERVERCONNECTION_P_H |