diff --git a/kerfuffle/archiveentry.cpp b/kerfuffle/archiveentry.cpp index e9004b29..cd177295 100644 --- a/kerfuffle/archiveentry.cpp +++ b/kerfuffle/archiveentry.cpp @@ -1,213 +1,214 @@ /* * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archiveentry.h" namespace Kerfuffle { Archive::Entry::Entry(QObject *parent, const QString &fullPath, const QString &rootNode) : QObject(parent) , rootNode(rootNode) , compressedSizeIsSet(true) , m_parent(qobject_cast(parent)) , m_size(0) , m_compressedSize(0) , m_isDirectory(false) , m_isPasswordProtected(false) { if (!fullPath.isEmpty()) setFullPath(fullPath); } Archive::Entry::~Entry() { } void Archive::Entry::copyMetaData(const Archive::Entry *sourceEntry) { setProperty("fullPath", sourceEntry->property("fullPath")); setProperty("permissions", sourceEntry->property("permissions")); setProperty("owner", sourceEntry->property("owner")); setProperty("group", sourceEntry->property("group")); setProperty("size", sourceEntry->property("size")); setProperty("compressedSize", sourceEntry->property("compressedSize")); setProperty("link", sourceEntry->property("link")); setProperty("ratio", sourceEntry->property("ratio")); setProperty("CRC", sourceEntry->property("CRC")); + setProperty("BLAKE2", sourceEntry->property("BLAKE2")); setProperty("method", sourceEntry->property("method")); setProperty("version", sourceEntry->property("version")); setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); setProperty("isDirectory", sourceEntry->property("isDirectory")); setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); } QVector Archive::Entry::entries() { Q_ASSERT(isDir()); return m_entries; } const QVector Archive::Entry::entries() const { Q_ASSERT(isDir()); return m_entries; } void Archive::Entry::setEntryAt(int index, Entry *value) { Q_ASSERT(isDir()); Q_ASSERT(index < m_entries.count()); m_entries[index] = value; } void Archive::Entry::appendEntry(Entry *entry) { Q_ASSERT(isDir()); m_entries.append(entry); } void Archive::Entry::removeEntryAt(int index) { Q_ASSERT(isDir()); Q_ASSERT(index < m_entries.count()); m_entries.remove(index); } Archive::Entry *Archive::Entry::getParent() const { return m_parent; } void Archive::Entry::setParent(Archive::Entry *parent) { m_parent = parent; } void Archive::Entry::setFullPath(const QString &fullPath) { m_fullPath = fullPath; const QStringList pieces = m_fullPath.split(QLatin1Char('/'), QString::SkipEmptyParts); m_name = pieces.isEmpty() ? QString() : pieces.last(); } QString Archive::Entry::fullPath(PathFormat format) const { if (format == NoTrailingSlash && m_fullPath.endsWith(QLatin1Char('/'))) { return m_fullPath.left(m_fullPath.size() - 1); } else { return m_fullPath; } } QString Archive::Entry::name() const { return m_name; } void Archive::Entry::setIsDirectory(const bool isDirectory) { m_isDirectory = isDirectory; } bool Archive::Entry::isDir() const { return m_isDirectory; } int Archive::Entry::row() const { if (getParent()) { return getParent()->entries().indexOf(const_cast(this)); } return 0; } Archive::Entry *Archive::Entry::find(const QString &name) const { for (Entry *entry : qAsConst(m_entries)) { if (entry && (entry->name() == name)) { return entry; } } return nullptr; } Archive::Entry *Archive::Entry::findByPath(const QStringList &pieces, int index) const { if (index == pieces.count()) { return nullptr; } Entry *next = find(pieces.at(index)); if (index == pieces.count() - 1) { return next; } if (next && next->isDir()) { return next->findByPath(pieces, index + 1); } return nullptr; } void Archive::Entry::countChildren(uint &dirs, uint &files) const { dirs = files = 0; if (!isDir()) { return; } const auto archiveEntries = entries(); for (auto entry : archiveEntries) { if (entry->isDir()) { dirs++; } else { files++; } } } bool Archive::Entry::operator==(const Archive::Entry &right) const { return m_fullPath == right.m_fullPath; } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry) { d.nospace() << "Entry(" << entry.property("fullPath"); if (!entry.rootNode.isEmpty()) { d.nospace() << "," << entry.rootNode; } d.nospace() << ")"; return d.space(); } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry) { d.nospace() << "Entry(" << entry->property("fullPath"); if (!entry->rootNode.isEmpty()) { d.nospace() << "," << entry->rootNode; } d.nospace() << ")"; return d.space(); } } diff --git a/kerfuffle/archiveentry.h b/kerfuffle/archiveentry.h index 88e1d1d9..1d300afb 100644 --- a/kerfuffle/archiveentry.h +++ b/kerfuffle/archiveentry.h @@ -1,134 +1,136 @@ /* * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEENTRY_H #define ARCHIVEENTRY_H #include "archive_kerfuffle.h" #include namespace Kerfuffle { enum PathFormat { NoTrailingSlash, WithTrailingSlash }; class Archive::Entry : public QObject { Q_OBJECT /** * Meta data related to one entry in a compressed archive. * * When creating a plugin, information about every single entry in * an archive is contained in an ArchiveEntry, and metadata * is set with the entries in this enum. * * Please notice that not all archive formats support all the properties * below, so set those that are available. */ Q_PROPERTY(QString fullPath MEMBER m_fullPath WRITE setFullPath) Q_PROPERTY(QString name READ name) Q_PROPERTY(QString permissions MEMBER m_permissions) Q_PROPERTY(QString owner MEMBER m_owner) Q_PROPERTY(QString group MEMBER m_group) Q_PROPERTY(qulonglong size MEMBER m_size) Q_PROPERTY(qulonglong compressedSize MEMBER m_compressedSize) Q_PROPERTY(QString link MEMBER m_link) Q_PROPERTY(QString ratio MEMBER m_ratio) Q_PROPERTY(QString CRC MEMBER m_CRC) + Q_PROPERTY(QString BLAKE2 MEMBER m_BLAKE2) Q_PROPERTY(QString method MEMBER m_method) Q_PROPERTY(QString version MEMBER m_version) Q_PROPERTY(QDateTime timestamp MEMBER m_timestamp) Q_PROPERTY(bool isDirectory MEMBER m_isDirectory WRITE setIsDirectory) Q_PROPERTY(bool isPasswordProtected MEMBER m_isPasswordProtected) public: explicit Entry(QObject *parent = nullptr, const QString &fullPath = {}, const QString &rootNode = {}); ~Entry() override; void copyMetaData(const Archive::Entry *sourceEntry); QVector entries(); const QVector entries() const; void setEntryAt(int index, Entry *value); void appendEntry(Entry *entry); void removeEntryAt(int index); Entry *getParent() const; void setParent(Entry *parent); void setFullPath(const QString &fullPath); QString fullPath(PathFormat format = WithTrailingSlash) const; QString name() const; void setIsDirectory(const bool isDirectory); bool isDir() const; int row() const; Entry *find(const QString &name) const; Entry *findByPath(const QStringList & pieces, int index = 0) const; /** * Fills @p dirs and @p files with the number of directories and files * in the entry (both will be 0 if the entry is not a directory). */ void countChildren(uint &dirs, uint &files) const; bool operator==(const Archive::Entry &right) const; public: QString rootNode; bool compressedSizeIsSet; private: QVector m_entries; QString m_name; Entry *m_parent; QString m_fullPath; QString m_permissions; QString m_owner; QString m_group; qulonglong m_size; qulonglong m_compressedSize; QString m_link; QString m_ratio; QString m_CRC; + QString m_BLAKE2; QString m_method; QString m_version; QDateTime m_timestamp; bool m_isDirectory; bool m_isPasswordProtected; }; QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry); QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry); } Q_DECLARE_METATYPE(Kerfuffle::Archive::Entry*) #endif //ARCHIVEENTRY_H diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index 83641168..1c5711ad 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,915 +1,918 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2010-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archivemodel.h" #include "ark_debug.h" #include "jobs.h" #include #include #include #include #include #include #include #include using namespace Kerfuffle; // Used to speed up the loading of large archives. static Archive::Entry *s_previousMatch = nullptr; Q_GLOBAL_STATIC(QStringList, s_previousPieces) ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_dbusPathName(dbusPathName) , m_numberOfFiles(0) , m_numberOfFolders(0) , m_fileEntryListed(false) { initRootEntry(); // Mappings between column indexes and entry properties. m_propertiesMap = { { FullPath, "fullPath" }, { Size, "size" }, { CompressedSize, "compressedSize" }, { Permissions, "permissions" }, { Owner, "owner" }, { Group, "group" }, { Ratio, "ratio" }, { CRC, "CRC" }, + { BLAKE2, "BLAKE2" }, { Method, "method" }, { Version, "version" }, { Timestamp, "timestamp" }, }; } ArchiveModel::~ArchiveModel() { } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { // TODO: complete the columns. int column = m_showColumns.at(index.column()); switch (column) { case FullPath: return entry->name(); case Size: if (entry->isDir()) { uint dirs; uint files; entry->countChildren(dirs, files); return KIO::itemsSummaryString(dirs + files, files, dirs, 0, false); } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } case Ratio: // TODO: Use entry->metaData()[Ratio] when available. if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); qulonglong size = entry->property("size").toULongLong(); if (compressedSize == 0 || size == 0) { return QVariant(); } else { int ratio = int(100 * ((double)size - compressedSize) / size); return QString(QString::number(ratio) + QStringLiteral(" %")); } } case Timestamp: { const QDateTime timeStamp = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: return entry->property(m_propertiesMap[column].constData()); } } case Qt::DecorationRole: if (index.column() == 0) { const Archive::Entry *e = static_cast(index.internalPointer()); QIcon::Mode mode = (filesToMove.contains(e->fullPath())) ? QIcon::Disabled : QIcon::Normal; return m_entryIcons.value(e->fullPath(NoTrailingSlash)).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small), mode); } return QVariant(); case Qt::FontRole: { QFont f; f.setItalic(entry->property("isPasswordProtected").toBool()); return f; } default: return QVariant(); } } return QVariant(); } Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags; } return nullptr; } QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const { if (role == Qt::DisplayRole) { if (section >= m_showColumns.size()) { qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section; return QVariant(); } int columnId = m_showColumns.at(section); switch (columnId) { case FullPath: return i18nc("Name of a file inside an archive", "Name"); case Size: return i18nc("Uncompressed size of a file inside an archive", "Size"); case CompressedSize: return i18nc("Compressed size of a file inside an archive", "Compressed"); case Ratio: return i18nc("Compression rate of file", "Rate"); case Owner: return i18nc("File's owner username", "Owner"); case Group: return i18nc("File's group", "Group"); case Permissions: return i18nc("File permissions", "Mode"); case CRC: - return i18nc("CRC hash code", "CRC"); + return i18nc("CRC hash code", "CRC checksum"); + case BLAKE2: + return i18nc("BLAKE2 hash code", "BLAKE2 checksum"); case Method: return i18nc("Compression method", "Method"); case Version: // TODO: what exactly is a file version? return i18nc("File version", "Version"); case Timestamp: return i18nc("Timestamp", "Date"); default: return i18nc("Unnamed column", "??"); } } return QVariant(); } QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry.data(); Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, nullptr); if (item != nullptr) { return createIndex(row, column, const_cast(item)); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->getParent() && (item->getParent() != m_rootEntry.data())) { return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); return item; } return nullptr; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry.data(); if (parentEntry && parentEntry->isDir()) { return parentEntry->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_showColumns.size(); } Qt::DropActions ArchiveModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ArchiveModel::mimeTypes() const { QStringList types; // MIME types we accept for dragging (eg. Dolphin -> Ark). types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url"); // MIME types we accept for dropping (eg. Ark -> Dolphin). types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path"); return types; } QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const { Q_UNUSED(indexes) QMimeData *mimeData = new QMimeData; mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8()); mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8()); return mimeData; } bool ArchiveModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(action) if (!data->hasUrls()) { return false; } if (archive()->isReadOnly() || (archive()->encryptionType() != Archive::Unencrypted && archive()->password().isEmpty())) { emit messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive.")); return false; } QStringList paths; const auto urls = data->urls(); for (const QUrl &url : urls) { paths << url.toLocalFile(); } const Archive::Entry *entry = nullptr; QModelIndex droppedOnto = index(row, column, parent); if (droppedOnto.isValid()) { entry = entryForIndex(droppedOnto); if (!entry->isDir()) { entry = entry->getParent(); } } emit droppedFiles(paths, entry); return true; } // For a rationale, see bugs #194241, #241967 and #355839 QString ArchiveModel::cleanFileName(const QString& fileName) { // Skip entries with filename "/" or "//" or "." // "." is present in ISO files. QRegularExpression pattern(QStringLiteral("/+|\\.")); QRegularExpressionMatch match; if (fileName.contains(pattern, &match) && match.captured() == fileName) { qCDebug(ARK) << "Skipping entry with filename" << fileName; return QString(); } else if (fileName.startsWith(QLatin1String("./"))) { return fileName.mid(2); } return fileName; } void ArchiveModel::initRootEntry() { m_rootEntry.reset(new Archive::Entry()); m_rootEntry->setProperty("isDirectory", true); } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry, InsertBehaviour behaviour) { QStringList pieces = entry->fullPath().split(QLatin1Char('/'), QString::SkipEmptyParts); if (pieces.isEmpty()) { return nullptr; } pieces.removeLast(); // Used to speed up loading of large archives. if (s_previousMatch) { // The number of path elements must be the same for the shortcut // to work. if (s_previousPieces->count() == pieces.count()) { bool equal = true; // Check if all pieces match. for (int i = 0; i < s_previousPieces->count(); ++i) { if (s_previousPieces->at(i) != pieces.at(i)) { equal = false; break; } } // If match return it. if (equal) { return s_previousMatch; } } } Archive::Entry *parent = m_rootEntry.data(); for (const QString &piece : qAsConst(pieces)) { Archive::Entry *entry = parent->find(piece); if (!entry) { // Directory entry will be traversed later (that happens for some archive formats, 7z for instance). // We have to create one before, in order to construct tree from its children, // and then delete the existing one (see ArchiveModel::newEntry). entry = new Archive::Entry(parent); entry->setProperty("fullPath", (parent == m_rootEntry.data()) ? QString(piece + QLatin1Char('/')) : QString(parent->fullPath(WithTrailingSlash) + piece + QLatin1Char('/'))); entry->setProperty("isDirectory", true); insertEntry(entry, behaviour); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); e->copyMetaData(entry); // Maybe we have both a file and a directory of the same name. // We avoid removing previous entries unless necessary. insertEntry(e, behaviour); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { Q_ASSERT(entry); if (entry != m_rootEntry.data()) { Q_ASSERT(entry->getParent()); Q_ASSERT(entry->getParent()->isDir()); return createIndex(entry->row(), 0, entry); } return QModelIndex(); } void ArchiveModel::slotEntryRemoved(const QString & path) { const QString entryFileName(cleanFileName(path)); if (entryFileName.isEmpty()) { return; } Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts)); if (entry) { Archive::Entry *parent = entry->getParent(); QModelIndex index = indexForEntry(entry); Q_UNUSED(index); beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); m_entryIcons.remove(parent->entries().at(entry->row())->fullPath(NoTrailingSlash)); parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::slotListEntry(Archive::Entry *entry) { newEntry(entry, DoNotNotifyViews); } void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { if (receivedEntry->fullPath().isEmpty()) { qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping"; return; } // If there are no columns registered, then populate columns from entry. If the first entry // is a directory we check again for the first file entry to ensure all relevent columms are shown. if (m_showColumns.isEmpty() || !m_fileEntryListed) { QList toInsert; const auto size = receivedEntry->property("size").toULongLong(); const auto compressedSize = receivedEntry->property("compressedSize").toULongLong(); for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) { // Singlefile plugin doesn't report the uncompressed size. if (i.key() == Size && size == 0 && compressedSize > 0) { continue; } if (!receivedEntry->property(i.value().constData()).toString().isEmpty()) { if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { if (!m_showColumns.contains(i.key())) { toInsert << i.key(); } } } } if (behaviour == NotifyViews) { beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); } m_showColumns << toInsert; if (behaviour == NotifyViews) { endInsertColumns(); } m_fileEntryListed = !receivedEntry->isDir(); } // #194241: Filenames such as "./file" should be displayed as "file" // #241967: Entries called "/" should be ignored // #355839: Entries called "//" should be ignored QString entryFileName = cleanFileName(receivedEntry->fullPath()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } receivedEntry->setProperty("fullPath", entryFileName); // For some archive formats (e.g. AppImage and RPM) paths of folders do not // contain a trailing slash, so we append it. if (receivedEntry->property("isDirectory").toBool() && !receivedEntry->property("fullPath").toString().endsWith(QLatin1Char('/'))) { receivedEntry->setProperty("fullPath", QString(receivedEntry->property("fullPath").toString() + QLatin1Char('/'))); qCDebug(ARK) << "Trailing slash appended to entry:" << receivedEntry->property("fullPath"); } // Skip already created entries. Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'))); if (existing) { existing->setProperty("fullPath", entryFileName); // Multi-volume files are repeated at least in RAR archives. // In that case, we need to sum the compressed size for each volume qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong(); existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); return; } // Find parent entry, creating missing directory Archive::Entry's in the process. Archive::Entry *parent = parentFor(receivedEntry, behaviour); // Create an Archive::Entry. const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); Archive::Entry *entry = parent->find(path.last()); if (entry) { entry->copyMetaData(receivedEntry); entry->setProperty("fullPath", entryFileName); } else { receivedEntry->setParent(parent); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { std::sort(m_showColumns.begin(), m_showColumns.end()); if (!job->error()) { qCDebug(ARK) << "Showing columns: " << m_showColumns; m_archive.reset(qobject_cast(job)->archive()); beginResetModel(); endResetModel(); } emit loadingFinished(job); } void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour) { Q_ASSERT(entry); Archive::Entry *parent = entry->getParent(); Q_ASSERT(parent); if (behaviour == NotifyViews) { beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } // Save an icon for each newly added entry. QMimeDatabase db; QIcon icon; entry->isDir() ? icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)) : icon = QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)); m_entryIcons.insert(entry->fullPath(NoTrailingSlash), icon); } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } void ArchiveModel::reset() { m_archive.reset(nullptr); s_previousMatch = nullptr; s_previousPieces->clear(); initRootEntry(); // TODO: make sure if it's ok to not have calls to beginRemoveColumns here m_showColumns.clear(); beginResetModel(); endResetModel(); } void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); m_archive.reset(Archive::createEmpty(path, mimeType, parent)); } KJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); auto loadJob = Archive::load(path, mimeType, parent); connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished); connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotListEntry); connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery); emit loadingStarted(); return loadJob; } ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QVector files({file}); return extractFiles(files, destinationDir, options); } ExtractJob* ArchiveModel::extractFiles(const QVector& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { Q_ASSERT(m_archive); ExtractJob *newJob = m_archive->extractFiles(files, destinationDir, options); connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery); return newJob; } Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const { Q_ASSERT(m_archive); PreviewJob *job = m_archive->preview(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenJob *ArchiveModel::open(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenJob *job = m_archive->open(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenWithJob *job = m_archive->openWith(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } AddJob* ArchiveModel::addFiles(QVector &entries, const Archive::Entry *destination, const CompressionOptions& options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { AddJob *job = m_archive->addFiles(entries, destination, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } Kerfuffle::MoveJob *ArchiveModel::moveFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { MoveJob *job = m_archive->moveFiles(entries, destination, options); connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery); connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); return job; } return nullptr; } Kerfuffle::CopyJob *ArchiveModel::copyFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { CopyJob *job = m_archive->copyFiles(entries, destination, options); connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &CopyJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } DeleteJob* ArchiveModel::deleteFiles(QVector entries) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { DeleteJob *job = m_archive->deleteFiles(entries); connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader) { if (!m_archive) { return; } m_archive->encrypt(password, encryptHeader); } bool ArchiveModel::conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const { bool error = false; // We can't accept destination as an argument, because it can be a new entry path for renaming. const Archive::Entry *destination; { QStringList destinationParts = entries.first().split(QLatin1Char('/'), QString::SkipEmptyParts); destinationParts.removeLast(); if (destinationParts.count() > 0) { destination = m_rootEntry->findByPath(destinationParts); } else { destination = m_rootEntry.data(); } } const Archive::Entry *lastDirEntry = destination; QString skippedDirPath; for (const QString &entry : entries) { if (skippedDirPath.count() > 0 && entry.startsWith(skippedDirPath)) { continue; } else { skippedDirPath.clear(); } while (!entry.startsWith(lastDirEntry->fullPath())) { lastDirEntry = lastDirEntry->getParent(); } bool isDir = entry.right(1) == QLatin1String("/"); const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), QString::SkipEmptyParts).last()); if (archiveEntry != nullptr) { if (archiveEntry->isDir() != isDir || !allowMerging) { if (isDir) { skippedDirPath = lastDirEntry->fullPath(); } if (!error) { conflictingEntries.clear(); error = true; } conflictingEntries << archiveEntry; } else { if (isDir) { lastDirEntry = archiveEntry; } else if (!error) { conflictingEntries << archiveEntry; } } } else if (isDir) { skippedDirPath = entry; } } return error; } bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries) { QStringList tempList; for (const QString &entry : entries) { if (tempList.contains(entry)) { return true; } tempList << entry; } return false; } QMap ArchiveModel::entryMap(const QVector &entries) { QMap map; for (Archive::Entry *entry : entries) { map.insert(entry->fullPath(), entry); } return map; } const QHash ArchiveModel::entryIcons() const { return m_entryIcons; } void ArchiveModel::slotCleanupEmptyDirs() { QList queue; QList nodesToDelete; // Add root nodes. for (int i = 0; i < rowCount(); ++i) { queue.append(QPersistentModelIndex(index(i, 0))); } // Breadth-first traverse. while (!queue.isEmpty()) { QPersistentModelIndex node = queue.takeFirst(); Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { if (entry->fullPath().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } for (const QPersistentModelIndex& node : qAsConst(nodesToDelete)) { Archive::Entry *rawEntry = static_cast(node.internalPointer()); qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row(); beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row()); m_entryIcons.remove(rawEntry->getParent()->entries().at(rawEntry->row())->fullPath(NoTrailingSlash)); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } void ArchiveModel::countEntriesAndSize() { // This function is used to count the number of folders/files and // the total compressed size. This is needed for PropertiesDialog // to update the corresponding values after adding/deleting files. // When ArchiveModel has been properly fixed, this code can likely // be removed. m_numberOfFiles = 0; m_numberOfFolders = 0; m_uncompressedSize = 0; QElapsedTimer timer; timer.start(); traverseAndCountDirNode(m_rootEntry.data()); qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms"; } void ArchiveModel::traverseAndCountDirNode(Archive::Entry *dir) { const auto entries = dir->entries(); for (Archive::Entry *entry : entries) { if (entry->isDir()) { traverseAndCountDirNode(entry); m_numberOfFolders++; } else { m_numberOfFiles++; m_uncompressedSize += entry->property("size").toULongLong(); } } } qulonglong ArchiveModel::numberOfFiles() const { return m_numberOfFiles; } qulonglong ArchiveModel::numberOfFolders() const { return m_numberOfFolders; } qulonglong ArchiveModel::uncompressedSize() const { return m_uncompressedSize; } QList ArchiveModel::shownColumns() const { return m_showColumns; } QMap ArchiveModel::propertiesMap() const { return m_propertiesMap; } diff --git a/part/archivemodel.h b/part/archivemodel.h index 1aeb86f1..0142df00 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,203 +1,204 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef ARCHIVEMODEL_H #define ARCHIVEMODEL_H #include "archiveentry.h" #include #include #include using Kerfuffle::Archive; namespace Kerfuffle { class Query; } /** * Meta data related to one entry in a compressed archive. * * This is used for indexing entry properties as numbers * and for determining data displaying order in part's view. */ enum EntryMetaDataType { FullPath, /**< The entry's file name */ Size, /**< The entry's original size */ CompressedSize, /**< The compressed size for the entry */ Permissions, /**< The entry's permissions */ Owner, /**< The user the entry belongs to */ Group, /**< The user group the entry belongs to */ Ratio, /**< The compression ratio for the entry */ CRC, /**< The entry's CRC */ + BLAKE2, /**< The entry's BLAKE2 */ Method, /**< The compression method used on the entry */ Version, /**< The archiver version needed to extract the entry */ Timestamp /**< The timestamp for the current entry */ }; class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = nullptr); ~ArchiveModel() override; QVariant data(const QModelIndex &index, int role) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; //drag and drop related Qt::DropActions supportedDropActions() const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList & indexes) const override; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; void reset(); void createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent); KJob* loadArchive(const QString &path, const QString &mimeType, QObject *parent); Kerfuffle::Archive *archive() const; QList shownColumns() const; QMap propertiesMap() const; Archive::Entry *entryForIndex(const QModelIndex &index); Kerfuffle::ExtractJob* extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::ExtractJob* extractFiles(const QVector& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::PreviewJob* preview(Archive::Entry *file) const; Kerfuffle::OpenJob* open(Archive::Entry *file) const; Kerfuffle::OpenWithJob* openWith(Archive::Entry *file) const; Kerfuffle::AddJob* addFiles(QVector &entries, const Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::MoveJob* moveFiles(QVector &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::CopyJob* copyFiles(QVector &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::DeleteJob* deleteFiles(QVector entries); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encryptArchive(const QString &password, bool encryptHeader); void countEntriesAndSize(); qulonglong numberOfFiles() const; qulonglong numberOfFolders() const; qulonglong uncompressedSize() const; /** * Constructs a list of conflicting entries. * * @param conflictingEntries Reference to the empty mutable entries list, which will be constructed. * If the method returns false, this list will contain only entries which produce a critical conflict. * @param entries New entries paths list. * @param allowMerging Boolean variable indicating whether merging is permitted. * If true, existing entries won't generate an error. * * @return Boolean variable indicating whether conflicts are not critical (true for not critical, * false for critical). For example, if there are both "some/file" (not a directory) and "some/file/" (a directory) * entries for both new and existing paths, the method will return false. Also, if merging is not allowed, * this method will return false for entries with the same path and types. */ bool conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const; static bool hasDuplicatedEntries(const QStringList &entries); static QMap entryMap(const QVector &entries); const QHash entryIcons() const; QMap filesToMove; QMap filesToCopy; Q_SIGNALS: void loadingStarted(); void loadingFinished(KJob *); void extractionFinished(bool success); void error(const QString& error, const QString& details); void droppedFiles(const QStringList& files, const Archive::Entry*); void messageWidget(KMessageWidget::MessageType type, const QString& msg); private Q_SLOTS: void slotNewEntry(Archive::Entry *entry); void slotListEntry(Archive::Entry *entry); void slotLoadingFinished(KJob *job); void slotEntryRemoved(const QString & path); void slotUserQuery(Kerfuffle::Query *query); void slotCleanupEmptyDirs(); private: /** * Strips file names that start with './'. * * For more information, see bug 194241. * * @param fileName The file name that will be stripped. * * @return @p fileName without the leading './' */ QString cleanFileName(const QString& fileName); void initRootEntry(); enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); QModelIndex indexForEntry(Archive::Entry *entry); static bool compareAscending(const QModelIndex& a, const QModelIndex& b); static bool compareDescending(const QModelIndex& a, const QModelIndex& b); /** * Insert the node @p node into the model, ensuring all views are notified * of the change. */ void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); void traverseAndCountDirNode(Archive::Entry *dir); QList m_showColumns; QScopedPointer m_archive; QScopedPointer m_rootEntry; QHash m_entryIcons; QMap m_propertiesMap; QString m_dbusPathName; qulonglong m_numberOfFiles; qulonglong m_numberOfFolders; qulonglong m_uncompressedSize; // Whether a file entry has been listed. Used to ensure all relevent columns are shown, // since directories might have fewer columns than files. bool m_fileEntryListed; }; #endif // ARCHIVEMODEL_H diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index 17c32bd7..c8f56261 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,623 +1,624 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * Copyright (C) 2015-2016 Ragnar Thomsen * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "archiveentry.h" #include #include #include using namespace Kerfuffle; K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_clirar.json") CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isSolid(false) , m_isRAR5(false) , m_isLocked(false) , m_remainingIgnoreLines(1) //The first line of UNRAR output is empty. , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_rar plugin"; // Empty lines are needed for parsing output of unrar. setListEmptyLines(true); setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_unrarVersion.clear(); m_comment.clear(); m_numberOfVolumes = 0; } void CliPlugin::setupCliProperties() { qCDebug(ARK) << "Setting up parameters..."; m_cliProps->setProperty("captureProgress", true); m_cliProps->setProperty("addProgram", QStringLiteral("rar")); m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("a")})); m_cliProps->setProperty("deleteProgram", QStringLiteral("rar")); m_cliProps->setProperty("deleteSwitch", QStringLiteral("d")); m_cliProps->setProperty("extractProgram", QStringLiteral("unrar")); m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x"), QStringLiteral("-kb"), QStringLiteral("-p-")}); m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e"), QStringLiteral("-kb"), QStringLiteral("-p-")}); m_cliProps->setProperty("listProgram", QStringLiteral("unrar")); m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("vt"), QStringLiteral("-v")}); m_cliProps->setProperty("moveProgram", QStringLiteral("rar")); m_cliProps->setProperty("moveSwitch", QStringLiteral("rn")); m_cliProps->setProperty("testProgram", QStringLiteral("unrar")); m_cliProps->setProperty("testSwitch", QStringLiteral("t")); m_cliProps->setProperty("commentSwitch", QStringList{QStringLiteral("c"), QStringLiteral("-z$CommentFile")}); m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-p$Password")}); m_cliProps->setProperty("passwordSwitchHeaderEnc", QStringList{QStringLiteral("-hp$Password")}); m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-m$CompressionLevel")); m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/vnd.rar"), QStringLiteral("-ma$CompressionMethod")}, {QStringLiteral("application/x-rar"), QStringLiteral("-ma$CompressionMethod")}}); m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^All OK$")}); m_cliProps->setProperty("fileExistsFileNameRegExp", QStringList{QStringLiteral("^(.+) already exists. Overwrite it"), // unrar 3 & 4 QStringLiteral("^Would you like to replace the existing file (.+)$")}); // unrar 5 m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("Y"), //Overwrite QStringLiteral("N"), //Skip QStringLiteral("A"), //Overwrite all QStringLiteral("E"), //Autoskip QStringLiteral("Q")}); //Cancel // rar will sometimes create multi-volume archives where first volume is // called name.part1.rar and other times name.part01.rar. m_cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("part01.$Suffix"), QStringLiteral("part1.$Suffix")}); } bool CliPlugin::readListLine(const QString &line) { // Ignore number of lines corresponding to m_remainingIgnoreLines. if (m_remainingIgnoreLines > 0) { --m_remainingIgnoreLines; return true; } // Parse the title line, which contains the version of unrar. if (m_parseState == ParseStateTitle) { QRegularExpression rxVersionLine(QStringLiteral("^UNRAR (\\d+\\.\\d+)( beta \\d)? .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateComment; m_unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << m_unrarVersion << "detected"; if (m_unrarVersion.toFloat() >= 5) { m_isUnrar5 = true; qCDebug(ARK) << "Using UNRAR 5 parser"; } else { qCDebug(ARK) << "Using UNRAR 4 parser"; } } else { // If the second line doesn't contain an UNRAR title, something // is wrong. qCCritical(ARK) << "Failed to detect UNRAR output."; return false; } // Or see what version of unrar we are dealing with and call specific // handler functions. } else if (m_isUnrar5) { return handleUnrar5Line(line); } else { return handleUnrar4Line(line); } return true; } bool CliPlugin::handleUnrar5Line(const QString &line) { if (line.startsWith(QLatin1String("Cannot find volume "))) { emit error(i18n("Failed to find all archive volumes.")); return false; } switch (m_parseState) { // Parses the comment field. case ParseStateComment: // "Archive: " is printed after the comment. // FIXME: Comment itself could also contain the searched string. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } break; // Parses the header, which is whatever is between the comment field // and the entries. case ParseStateHeader: // "Details: " indicates end of header. if (line.startsWith(QLatin1String("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume"))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } if (line.contains(QLatin1String("RAR 4"))) { emit compressionMethodFound(QStringLiteral("RAR4")); } else if (line.contains(QLatin1String("RAR 5"))) { emit compressionMethodFound(QStringLiteral("RAR5")); m_isRAR5 = true; } if (line.contains(QLatin1String("lock"))) { m_isLocked = true; } } break; // Parses the entry details for each entry. case ParseStateEntryDetails: // For multi-volume archives there is a header between the entries in // each volume. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; return true; // Empty line indicates end of entry. } else if (line.trimmed().isEmpty() && !m_unrar5Details.isEmpty()) { handleUnrar5Entry(); } else { // All detail lines should contain a colon. if (!line.contains(QLatin1Char(':'))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; } // The details are on separate lines, so we store them in the QHash // m_unrar5Details. m_unrar5Details.insert(line.section(QLatin1Char(':'), 0, 0).trimmed().toLower(), line.section(QLatin1Char(':'), 1).trimmed()); } break; default: break; } return true; } void CliPlugin::handleUnrar5Entry() { Archive::Entry *e = new Archive::Entry(this); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); e->setProperty("timestamp", QDateTime::fromString(m_unrar5Details.value(QStringLiteral("mtime")), QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz"))); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e->setProperty("method", compression.mid(optionPos)); e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. e->setProperty("method", QString()); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (m_isPasswordProtected) { m_isRAR5 ? emit encryptionMethodFound(QStringLiteral("AES256")) : emit encryptionMethodFound(QStringLiteral("AES128")); } e->setProperty("fullPath", m_unrar5Details.value(QStringLiteral("name"))); e->setProperty("size", m_unrar5Details.value(QStringLiteral("size"))); e->setProperty("compressedSize", m_unrar5Details.value(QStringLiteral("packed size"))); e->setProperty("permissions", m_unrar5Details.value(QStringLiteral("attributes"))); e->setProperty("CRC", m_unrar5Details.value(QStringLiteral("crc32"))); + e->setProperty("BLAKE2", m_unrar5Details.value(QStringLiteral("blake2"))); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } bool CliPlugin::handleUnrar4Line(const QString &line) { if (line.startsWith(QLatin1String("Cannot find volume "))) { emit error(i18n("Failed to find all archive volumes.")); return false; } // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^(Solid archive|Archive|Volume) .+$")); // Three types of subHeaders can be displayed for unrar 3 and 4. // STM has 4 lines, RR has 3, and CMT has lines corresponding to // length of comment field +3. We ignore the subheaders. QRegularExpression rxSubHeader(QStringLiteral("^Data header type: (CMT|STM|RR)$")); QRegularExpressionMatch matchSubHeader; switch (m_parseState) { // Parses the comment field. case ParseStateComment: // unrar 4 outputs the following string when opening v5 RAR archives. if (line == QLatin1String("Unsupported archive format. Please update RAR to a newer version.")) { emit error(i18n("Your unrar executable is version %1, which is too old to handle this archive. Please update to a more recent version.", m_unrarVersion)); return false; } // unrar 3 reports a non-RAR archive when opening v5 RAR archives. if (line.endsWith(QLatin1String(" is not RAR archive"))) { emit error(i18n("Unrar reported a non-RAR archive. The installed unrar version (%1) is old. Try updating your unrar.", m_unrarVersion)); return false; } // If we reach this point, then we can be sure that it's not a RAR5 // archive, so assume RAR4. emit compressionMethodFound(QStringLiteral("RAR4")); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.startsWith(QLatin1String("Solid archive")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } break; // Parses the header, which is whatever is between the comment field // and the entries. case ParseStateHeader: // Horizontal line indicates end of header. if (line.startsWith(QLatin1String("--------------------"))) { m_parseState = ParseStateEntryFileName; } else if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; } else if (line == QLatin1String("Lock is present")) { m_isLocked = true; } break; // Parses the entry name, which is on the first line of each entry. case ParseStateEntryFileName: // Ignore empty lines. if (line.trimmed().isEmpty()) { return true; } matchSubHeader = rxSubHeader.match(line); if (matchSubHeader.hasMatch()) { qCDebug(ARK) << "SubHeader of type" << matchSubHeader.captured(1) << "found"; if (matchSubHeader.captured(1) == QLatin1String("STM")) { ignoreLines(4, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("CMT")) { ignoreLines(m_linesComment + 3, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("RR")) { ignoreLines(3, ParseStateEntryFileName); } return true; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QLatin1String("-----------------"))) { m_parseState = ParseStateHeader; return true; // Encrypted files are marked with an asterisk. } else if (line.startsWith(QLatin1Char('*'))) { m_isPasswordProtected = true; m_unrar4Details.append(QString(line.trimmed()).remove(0, 1)); //Remove the asterisk emit encryptionMethodFound(QStringLiteral("AES128")); // Entry names always start at the second position, so a line not // starting with a space is not an entry name. } else if (!line.startsWith(QLatin1Char(' '))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; // If we reach this, then we can assume the line is an entry name, so // save it, and move on to the rest of the entry details. } else { m_unrar4Details.append(line.trimmed()); } m_parseState = ParseStateEntryDetails; break; // Parses the remainder of the entry details for each entry. case ParseStateEntryDetails: // If the line following an entry name is empty, we did something // wrong. Q_ASSERT(!line.trimmed().isEmpty()); // If we reach a horizontal line, then the previous line was not an // entry name, so go back to header. if (line.startsWith(QLatin1String("-----------------"))) { m_parseState = ParseStateHeader; return true; } // In unrar 3 and 4 the details are on a single line, so we // pass a QStringList containing the details. We need to store // it due to symlinks (see below). m_unrar4Details.append(line.split(QLatin1Char(' '), QString::SkipEmptyParts)); // The details line contains 9 fields, so m_unrar4Details // should now contain 9 + the filename = 10 strings. If not, this is // not an archive entry. if (m_unrar4Details.size() != 10) { m_parseState = ParseStateHeader; return true; } // When unrar 3 and 4 list a symlink, they output an extra line // containing the link target. The extra line is output after // the line we ignore, so we first need to ignore one line. if (m_unrar4Details.at(6).startsWith(QLatin1Char('l'))) { ignoreLines(1, ParseStateLinkTarget); return true; } else { handleUnrar4Entry(); } // Unrar 3 & 4 show a third line for each entry, which contains // three details: Host OS, Solid, and Old. We can ignore this // line. ignoreLines(1, ParseStateEntryFileName); break; // Parses a symlink target. case ParseStateLinkTarget: m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; break; default: break; } return true; } void CliPlugin::handleUnrar4Entry() { Archive::Entry *e = new Archive::Entry(this); QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. e->setProperty("fullPath", m_unrar4Details.at(0)); e->setProperty("size", m_unrar4Details.at(1)); e->setProperty("compressedSize", m_unrar4Details.at(2)); e->setProperty("permissions", m_unrar4Details.at(6)); e->setProperty("CRC", m_unrar4Details.at(7)); e->setProperty("method", m_unrar4Details.at(8)); e->setProperty("version", m_unrar4Details.at(9)); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar4Details.at(10)); } m_unrar4Details.clear(); emit entry(e); } bool CliPlugin::readExtractLine(const QString &line) { const QRegularExpression rxCRC(QStringLiteral("CRC failed")); if (rxCRC.match(line).hasMatch()) { emit error(i18n("One or more wrong checksums")); return false; } if (line.startsWith(QLatin1String("Cannot find volume "))) { emit error(i18n("Failed to find all archive volumes.")); return false; } return true; } bool CliPlugin::hasBatchExtractionProgress() const { return true; } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } bool CliPlugin::isPasswordPrompt(const QString &line) { return line.startsWith(QLatin1String("Enter password (will not be echoed) for")); } bool CliPlugin::isWrongPasswordMsg(const QString &line) { return (line.contains(QLatin1String("password incorrect")) || line.contains(QLatin1String("wrong password"))); } bool CliPlugin::isCorruptArchiveMsg(const QString &line) { return (line == QLatin1String("Unexpected end of archive") || line.contains(QLatin1String("the file header is corrupt")) || line.endsWith(QLatin1String("checksum error"))); } bool CliPlugin::isDiskFullMsg(const QString &line) { return line.contains(QLatin1String("No space left on device")); } bool CliPlugin::isFileExistsMsg(const QString &line) { return (line == QLatin1String("[Y]es, [N]o, [A]ll, n[E]ver, [R]ename, [Q]uit ")); } bool CliPlugin::isFileExistsFileName(const QString &line) { return (line.startsWith(QLatin1String("Would you like to replace the existing file ")) || // unrar 5 line.contains(QLatin1String(" already exists. Overwrite it"))); // unrar 3 & 4 } bool CliPlugin::isLocked() const { return m_isLocked; } #include "cliplugin.moc"