1 /*
2     SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net>
3     SPDX-FileCopyrightText: 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
4     SPDX-FileCopyrightText: 2010-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
5     SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "archivemodel.h"
11 #include "ark_debug.h"
12 #include "jobs.h"
13 #include "util.h"
14 #include "qstringtokenizer.h"
15 
16 #include <KIO/Global>
17 #include <KLocalizedString>
18 
19 #include <QApplication>
20 #include <QDBusConnection>
21 #include <QMimeData>
22 #include <QRegularExpression>
23 #include <QStyle>
24 #include <QUrl>
25 
26 using namespace Kerfuffle;
27 
28 // Used to speed up the loading of large archives.
29 static Archive::Entry *s_previousMatch = nullptr;
Q_GLOBAL_STATIC(QString,s_previousPath)30 Q_GLOBAL_STATIC(QString, s_previousPath)
31 
32 ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent)
33     : QAbstractItemModel(parent)
34     , m_dbusPathName(dbusPathName)
35     , m_numberOfFiles(0)
36     , m_numberOfFolders(0)
37     , m_fileEntryListed(false)
38 {
39     initRootEntry();
40 
41     // Mappings between column indexes and entry properties.
42     m_propertiesMap = {
43         { FullPath, "fullPath" },
44         { Size, "size" },
45         { CompressedSize, "compressedSize" },
46         { Permissions, "permissions" },
47         { Owner, "owner" },
48         { Group, "group" },
49         { Ratio, "ratio" },
50         { CRC, "CRC" },
51         { BLAKE2, "BLAKE2" },
52         { Method, "method" },
53         { Version, "version" },
54         { Timestamp, "timestamp" },
55     };
56 }
57 
~ArchiveModel()58 ArchiveModel::~ArchiveModel()
59 {
60 }
61 
data(const QModelIndex & index,int role) const62 QVariant ArchiveModel::data(const QModelIndex &index, int role) const
63 {
64     if (index.isValid()) {
65         Archive::Entry *entry = static_cast<Archive::Entry*>(index.internalPointer());
66         switch (role) {
67         case Qt::DisplayRole: {
68             // TODO: complete the columns.
69             int column = m_showColumns.at(index.column());
70             switch (column) {
71             case FullPath:
72                 return entry->name();
73             case Size:
74                 if (entry->isDir()) {
75                     uint dirs;
76                     uint files;
77                     entry->countChildren(dirs, files);
78                     return KIO::itemsSummaryString(dirs + files, files, dirs, 0, false);
79                 } else if (!entry->property("link").toString().isEmpty()) {
80                     return QVariant();
81                 } else {
82                     return KIO::convertSize(entry->property("size").toULongLong());
83                 }
84             case CompressedSize:
85                 if (entry->isDir() || !entry->property("link").toString().isEmpty()) {
86                     return QVariant();
87                 } else {
88                     qulonglong compressedSize = entry->property("compressedSize").toULongLong();
89                     if (compressedSize != 0) {
90                         return KIO::convertSize(compressedSize);
91                     } else {
92                         return QVariant();
93                     }
94                 }
95             case Ratio: // TODO: Use entry->metaData()[Ratio] when available.
96                 if (entry->isDir() || !entry->property("link").toString().isEmpty()) {
97                     return QVariant();
98                 } else {
99                     qulonglong compressedSize = entry->property("compressedSize").toULongLong();
100                     qulonglong size = entry->property("size").toULongLong();
101                     if (compressedSize == 0 || size == 0) {
102                         return QVariant();
103                     } else {
104                         int ratio = int(100 * ((double)size - compressedSize) / size);
105                         return QString(QString::number(ratio) + QStringLiteral(" %"));
106                     }
107                 }
108 
109             case Timestamp: {
110                 const QDateTime timeStamp = entry->property("timestamp").toDateTime();
111                 return QLocale().toString(timeStamp, QLocale::ShortFormat);
112             }
113 
114             default:
115                 return entry->property(m_propertiesMap[column].constData());
116             }
117         }
118         case Qt::DecorationRole:
119             if (index.column() == 0) {
120                 Archive::Entry *e = static_cast<Archive::Entry*>(index.internalPointer());
121                 QIcon::Mode mode = (filesToMove.contains(e->fullPath())) ? QIcon::Disabled : QIcon::Normal;
122                 return e->icon().pixmap(QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize), mode);
123             }
124             return QVariant();
125         case Qt::FontRole: {
126             QFont f;
127             f.setItalic(entry->property("isPasswordProtected").toBool());
128             return f;
129         }
130         default:
131             return QVariant();
132         }
133     }
134     return QVariant();
135 }
136 
flags(const QModelIndex & index) const137 Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const
138 {
139     Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);
140 
141     if (index.isValid()) {
142         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags;
143     }
144 
145     return Qt::NoItemFlags;
146 }
147 
headerData(int section,Qt::Orientation,int role) const148 QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const
149 {
150     if (role == Qt::DisplayRole) {
151         if (section >= m_showColumns.size()) {
152             qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size()
153             << " and section = " << section;
154             return QVariant();
155         }
156 
157         int columnId = m_showColumns.at(section);
158 
159         switch (columnId) {
160         case FullPath:
161             return i18nc("Name of a file inside an archive", "Name");
162         case Size:
163             return i18nc("Uncompressed size of a file inside an archive", "Size");
164         case CompressedSize:
165             return i18nc("Compressed size of a file inside an archive", "Compressed");
166         case Ratio:
167             return i18nc("Compression rate of file", "Rate");
168         case Owner:
169             return i18nc("File's owner username", "Owner");
170         case Group:
171             return i18nc("File's group", "Group");
172         case Permissions:
173             return i18nc("File permissions", "Mode");
174         case CRC:
175             return i18nc("CRC hash code", "CRC checksum");
176         case BLAKE2:
177             return i18nc("BLAKE2 hash code", "BLAKE2 checksum");
178         case Method:
179             return i18nc("Compression method", "Method");
180         case Version:
181             // TODO: what exactly is a file version?
182             return i18nc("File version", "Version");
183         case Timestamp:
184             return i18nc("Timestamp", "Date");
185         default:
186             return i18nc("Unnamed column", "??");
187         }
188     }
189     return QVariant();
190 }
191 
index(int row,int column,const QModelIndex & parent) const192 QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const
193 {
194     if (hasIndex(row, column, parent)) {
195         const Archive::Entry *parentEntry = parent.isValid()
196                                             ? static_cast<Archive::Entry*>(parent.internalPointer())
197                                             : m_rootEntry.data();
198 
199         Q_ASSERT(parentEntry->isDir());
200 
201         const Archive::Entry *item = parentEntry->entries().value(row, nullptr);
202         if (item != nullptr) {
203             return createIndex(row, column, const_cast<Archive::Entry*>(item));
204         }
205     }
206 
207     return QModelIndex();
208 }
209 
parent(const QModelIndex & index) const210 QModelIndex ArchiveModel::parent(const QModelIndex &index) const
211 {
212     if (index.isValid()) {
213         Archive::Entry *item = static_cast<Archive::Entry*>(index.internalPointer());
214         Q_ASSERT(item);
215         if (item->getParent() && (item->getParent() != m_rootEntry.data())) {
216             return createIndex(item->getParent()->row(), 0, item->getParent());
217         }
218     }
219     return QModelIndex();
220 }
221 
entryForIndex(const QModelIndex & index)222 Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index)
223 {
224     if (index.isValid()) {
225         Archive::Entry *item = static_cast<Archive::Entry*>(index.internalPointer());
226         Q_ASSERT(item);
227         return item;
228     }
229     return nullptr;
230 }
231 
rowCount(const QModelIndex & parent) const232 int ArchiveModel::rowCount(const QModelIndex &parent) const
233 {
234     if (parent.column() <= 0) {
235         const Archive::Entry *parentEntry = parent.isValid()
236                                             ? static_cast<Archive::Entry*>(parent.internalPointer())
237                                             : m_rootEntry.data();
238 
239         if (parentEntry && parentEntry->isDir()) {
240             return parentEntry->entries().count();
241         }
242     }
243     return 0;
244 }
245 
columnCount(const QModelIndex & parent) const246 int ArchiveModel::columnCount(const QModelIndex &parent) const
247 {
248     Q_UNUSED(parent)
249     return m_showColumns.size();
250 }
251 
supportedDropActions() const252 Qt::DropActions ArchiveModel::supportedDropActions() const
253 {
254     return Qt::CopyAction | Qt::MoveAction;
255 }
256 
mimeTypes() const257 QStringList ArchiveModel::mimeTypes() const
258 {
259     QStringList types;
260 
261     // MIME types we accept for dragging (eg. Dolphin -> Ark).
262     types << QStringLiteral("text/uri-list")
263           << QStringLiteral("text/plain")
264           << QStringLiteral("text/x-moz-url");
265 
266     // MIME types we accept for dropping (eg. Ark -> Dolphin).
267     types << QStringLiteral("application/x-kde-ark-dndextract-service")
268           << QStringLiteral("application/x-kde-ark-dndextract-path");
269 
270     return types;
271 }
272 
mimeData(const QModelIndexList & indexes) const273 QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const
274 {
275     Q_UNUSED(indexes)
276 
277     QMimeData *mimeData = new QMimeData;
278     mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"),
279                       QDBusConnection::sessionBus().baseService().toUtf8());
280     mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"),
281                       m_dbusPathName.toUtf8());
282 
283     return mimeData;
284 }
285 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)286 bool ArchiveModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
287 {
288     Q_UNUSED(action)
289 
290     if (!data->hasUrls()) {
291         return false;
292     }
293 
294     if (archive()->isReadOnly() ||
295         (archive()->encryptionType() != Archive::Unencrypted &&
296          archive()->password().isEmpty())) {
297         Q_EMIT messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive."));
298         return false;
299     }
300 
301     QStringList paths;
302     const auto urls = data->urls();
303     for (const QUrl &url : urls) {
304         if (!url.isLocalFile()) {
305             Q_EMIT messageWidget(KMessageWidget::Error, i18n("You can only add local files to an archive."));
306             return false;
307         }
308         paths << url.toLocalFile();
309     }
310 
311     const Archive::Entry *entry = nullptr;
312     QModelIndex droppedOnto = index(row, column, parent);
313     if (droppedOnto.isValid()) {
314         entry = entryForIndex(droppedOnto);
315         if (!entry->isDir()) {
316             entry = entry->getParent();
317         }
318     }
319 
320     Q_EMIT droppedFiles(paths, entry);
321 
322     return true;
323 }
324 
325 // For a rationale, see bugs #194241, #241967 and #355839
cleanFileName(const QString & fileName)326 QString ArchiveModel::cleanFileName(const QString& fileName)
327 {
328     // Skip entries with filename "/" or "//" or "."
329     // "." is present in ISO files.
330     static QRegularExpression pattern(QStringLiteral("/+|\\."));
331     QRegularExpressionMatch match;
332     if (fileName.contains(pattern, &match) && match.captured() == fileName) {
333         qCDebug(ARK) << "Skipping entry with filename" << fileName;
334         return QString();
335     } else if (fileName.startsWith(QLatin1String("./"))) {
336         return fileName.mid(2);
337     }
338 
339     return fileName;
340 }
341 
initRootEntry()342 void ArchiveModel::initRootEntry()
343 {
344     m_rootEntry.reset(new Archive::Entry());
345     m_rootEntry->setProperty("isDirectory", true);
346 }
347 
parentFor(const Archive::Entry * entry,InsertBehaviour behaviour)348 Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry, InsertBehaviour behaviour)
349 {
350     QString fullPath = entry->fullPath();
351 
352     if (fullPath.endsWith(QLatin1Char('/'))) {
353         fullPath = fullPath.chopped(1);
354     }
355 
356     // Used to speed up loading of large archives.
357     const int index = fullPath.lastIndexOf(QLatin1Char('/'));
358     const QString folderPath = index != -1 ? fullPath.left(index) : QString();
359 
360     if (s_previousMatch && *s_previousPath == folderPath) {
361         return s_previousMatch;
362     }
363 
364     Archive::Entry *parent = m_rootEntry.data();
365 
366     const auto pieces = QStringTokenizer{folderPath, QLatin1Char('/'), Qt::SkipEmptyParts};
367 
368     for (const auto piece : pieces) {
369         Archive::Entry *entry = parent->find(piece);
370         if (!entry) {
371             // Directory entry will be traversed later (that happens for some archive formats, 7z for instance).
372             // We have to create one before, in order to construct tree from its children,
373             // and then delete the existing one (see ArchiveModel::newEntry).
374             entry = new Archive::Entry(parent);
375 
376             entry->setProperty("fullPath", (parent == m_rootEntry.data())
377                                            ? QString(piece + QLatin1Char('/'))
378                                            : QString(parent->fullPath(WithTrailingSlash) + piece + QLatin1Char('/')));
379             entry->setProperty("isDirectory", true);
380             insertEntry(entry, behaviour);
381         }
382         if (!entry->isDir()) {
383             Archive::Entry *e = new Archive::Entry(parent);
384             e->copyMetaData(entry);
385             // Maybe we have both a file and a directory of the same name.
386             // We avoid removing previous entries unless necessary.
387             insertEntry(e, behaviour);
388         }
389         parent = entry;
390     }
391 
392     s_previousMatch = parent;
393     *s_previousPath = folderPath;
394 
395     return parent;
396 }
397 
indexForEntry(Archive::Entry * entry)398 QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry)
399 {
400     Q_ASSERT(entry);
401     if (entry != m_rootEntry.data()) {
402         Q_ASSERT(entry->getParent());
403         Q_ASSERT(entry->getParent()->isDir());
404         return createIndex(entry->row(), 0, entry);
405     }
406     return QModelIndex();
407 }
408 
slotEntryRemoved(const QString & path)409 void ArchiveModel::slotEntryRemoved(const QString & path)
410 {
411     const QString entryFileName(cleanFileName(path));
412     if (entryFileName.isEmpty()) {
413         return;
414     }
415 
416     Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'), Qt::SkipEmptyParts));
417     if (entry) {
418         Archive::Entry *parent = entry->getParent();
419         QModelIndex index = indexForEntry(entry);
420         Q_UNUSED(index);
421 
422         beginRemoveRows(indexForEntry(parent), entry->row(), entry->row());
423         parent->removeEntryAt(entry->row());
424         endRemoveRows();
425     }
426 }
427 
slotUserQuery(Kerfuffle::Query * query)428 void ArchiveModel::slotUserQuery(Kerfuffle::Query *query)
429 {
430     query->execute();
431 }
432 
slotNewEntry(Archive::Entry * entry)433 void ArchiveModel::slotNewEntry(Archive::Entry *entry)
434 {
435     newEntry(entry, NotifyViews);
436 }
437 
slotListEntry(Archive::Entry * entry)438 void ArchiveModel::slotListEntry(Archive::Entry *entry)
439 {
440     newEntry(entry, DoNotNotifyViews);
441 }
442 
newEntry(Archive::Entry * receivedEntry,InsertBehaviour behaviour)443 void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour)
444 {
445     if (receivedEntry->fullPath().isEmpty()) {
446         qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping";
447         return;
448     }
449 
450     // If there are no columns registered, then populate columns from entry. If the first entry
451     // is a directory we check again for the first file entry to ensure all relevant columms are shown.
452     if (m_showColumns.isEmpty() || !m_fileEntryListed) {
453         QList<int> toInsert;
454 
455         const auto size = receivedEntry->property("size").toULongLong();
456         const auto compressedSize = receivedEntry->property("compressedSize").toULongLong();
457         for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) {
458             // Singlefile plugin doesn't report the uncompressed size.
459             if (i.key() == Size && size == 0 && compressedSize > 0) {
460                 continue;
461             }
462             if (!receivedEntry->property(i.value().constData()).toString().isEmpty()) {
463                 if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) {
464                     if (!m_showColumns.contains(i.key())) {
465                         toInsert << i.key();
466                     }
467                 }
468             }
469         }
470         if (behaviour == NotifyViews) {
471             beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1);
472         }
473         m_showColumns << toInsert;
474         if (behaviour == NotifyViews) {
475             endInsertColumns();
476         }
477 
478         m_fileEntryListed = !receivedEntry->isDir();
479     }
480 
481     // #194241: Filenames such as "./file" should be displayed as "file"
482     // #241967: Entries called "/" should be ignored
483     // #355839: Entries called "//" should be ignored
484     QString entryFileName = cleanFileName(receivedEntry->fullPath());
485     if (entryFileName.isEmpty()) { // The entry contains only "." or "./"
486         return;
487     }
488     receivedEntry->setProperty("fullPath", entryFileName);
489 
490     // For some archive formats (e.g. AppImage and RPM) paths of folders do not
491     // contain a trailing slash, so we append it.
492     if (receivedEntry->property("isDirectory").toBool() &&
493         !receivedEntry->property("fullPath").toString().endsWith(QLatin1Char('/'))) {
494         receivedEntry->setProperty("fullPath", QString(receivedEntry->property("fullPath").toString() + QLatin1Char('/')));
495         qCDebug(ARK) << "Trailing slash appended to entry:" << receivedEntry->property("fullPath");
496     }
497 
498     // Skip already created entries.
499     Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/')));
500     if (existing) {
501         existing->setProperty("fullPath", entryFileName);
502         // Multi-volume files are repeated at least in RAR archives.
503         // In that case, we need to sum the compressed size for each volume
504         qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong();
505         existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong());
506         return;
507     }
508 
509     // Find parent entry, creating missing directory Archive::Entry's in the process.
510     Archive::Entry *parent = parentFor(receivedEntry, behaviour);
511 
512     // Create an Archive::Entry.
513     Archive::Entry *entry = parent->find(Kerfuffle::Util::lastPathSegment(entryFileName));
514     if (entry) {
515         entry->copyMetaData(receivedEntry);
516         entry->setProperty("fullPath", entryFileName);
517     } else {
518         receivedEntry->setParent(parent);
519         insertEntry(receivedEntry, behaviour);
520     }
521 }
522 
slotLoadingFinished(KJob * job)523 void ArchiveModel::slotLoadingFinished(KJob *job)
524 {
525     std::sort(m_showColumns.begin(), m_showColumns.end());
526 
527     if (!job->error()) {
528 
529         qCDebug(ARK) << "Showing columns: " << m_showColumns;
530 
531         m_archive.reset(qobject_cast<LoadJob*>(job)->archive());
532 
533         beginResetModel();
534         endResetModel();
535     }
536 
537     Q_EMIT loadingFinished(job);
538 }
539 
insertEntry(Archive::Entry * entry,InsertBehaviour behaviour)540 void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour)
541 {
542     Q_ASSERT(entry);
543     Archive::Entry *parent = entry->getParent();
544     Q_ASSERT(parent);
545     if (behaviour == NotifyViews) {
546         beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count());
547     }
548     parent->appendEntry(entry);
549     if (behaviour == NotifyViews) {
550         endInsertRows();
551     }
552 }
553 
archive() const554 Kerfuffle::Archive* ArchiveModel::archive() const
555 {
556     return m_archive.data();
557 }
558 
reset()559 void ArchiveModel::reset()
560 {
561     m_archive.reset(nullptr);
562     s_previousMatch = nullptr;
563     s_previousPath->clear();
564     initRootEntry();
565 
566     // TODO: make sure if it's ok to not have calls to beginRemoveColumns here
567     m_showColumns.clear();
568     beginResetModel();
569     endResetModel();
570 }
571 
createEmptyArchive(const QString & path,const QString & mimeType,QObject * parent)572 void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent)
573 {
574     reset();
575     m_archive.reset(Archive::createEmpty(path, mimeType, parent));
576 }
577 
loadArchive(const QString & path,const QString & mimeType,QObject * parent)578 KJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent)
579 {
580     reset();
581 
582     auto loadJob = Archive::load(path, mimeType, parent);
583     connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished);
584     connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotListEntry);
585     connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
586 
587     Q_EMIT loadingStarted();
588 
589     return loadJob;
590 }
591 
extractFile(Archive::Entry * file,const QString & destinationDir,Kerfuffle::ExtractionOptions options) const592 ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, Kerfuffle::ExtractionOptions options) const
593 {
594     QVector<Archive::Entry*> files({file});
595     return extractFiles(files, destinationDir, options);
596 }
597 
extractFiles(const QVector<Archive::Entry * > & files,const QString & destinationDir,Kerfuffle::ExtractionOptions options) const598 ExtractJob* ArchiveModel::extractFiles(const QVector<Archive::Entry*>& files, const QString& destinationDir, Kerfuffle::ExtractionOptions options) const
599 {
600     Q_ASSERT(m_archive);
601     ExtractJob *newJob = m_archive->extractFiles(files, destinationDir, options);
602     connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery);
603     return newJob;
604 }
605 
preview(Archive::Entry * file) const606 Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const
607 {
608     Q_ASSERT(m_archive);
609     PreviewJob *job = m_archive->preview(file);
610     connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
611     return job;
612 }
613 
open(Archive::Entry * file) const614 OpenJob *ArchiveModel::open(Archive::Entry *file) const
615 {
616     Q_ASSERT(m_archive);
617     OpenJob *job = m_archive->open(file);
618     connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
619     return job;
620 }
621 
openWith(Archive::Entry * file) const622 OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const
623 {
624     Q_ASSERT(m_archive);
625     OpenWithJob *job = m_archive->openWith(file);
626     connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
627     return job;
628 }
629 
addFiles(QVector<Archive::Entry * > & entries,const Archive::Entry * destination,const CompressionOptions & options)630 AddJob* ArchiveModel::addFiles(QVector<Archive::Entry*> &entries, const Archive::Entry *destination, const CompressionOptions& options)
631 {
632     if (!m_archive) {
633         return nullptr;
634     }
635 
636     if (!m_archive->isReadOnly()) {
637         AddJob *job = m_archive->addFiles(entries, destination, options);
638         connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry);
639         connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery);
640 
641 
642         return job;
643     }
644     return nullptr;
645 }
646 
moveFiles(QVector<Archive::Entry * > & entries,Archive::Entry * destination,const CompressionOptions & options)647 Kerfuffle::MoveJob *ArchiveModel::moveFiles(QVector<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions &options)
648 {
649     if (!m_archive) {
650         return nullptr;
651     }
652 
653     if (!m_archive->isReadOnly()) {
654         MoveJob *job = m_archive->moveFiles(entries, destination, options);
655         connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry);
656         connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery);
657         connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved);
658         connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs);
659 
660 
661         return job;
662     }
663     return nullptr;
664 }
copyFiles(QVector<Archive::Entry * > & entries,Archive::Entry * destination,const CompressionOptions & options)665 Kerfuffle::CopyJob *ArchiveModel::copyFiles(QVector<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions &options)
666 {
667     if (!m_archive) {
668         return nullptr;
669     }
670 
671     if (!m_archive->isReadOnly()) {
672         CopyJob *job = m_archive->copyFiles(entries, destination, options);
673         connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry);
674         connect(job, &CopyJob::userQuery, this, &ArchiveModel::slotUserQuery);
675 
676 
677         return job;
678     }
679     return nullptr;
680 }
681 
deleteFiles(QVector<Archive::Entry * > entries)682 DeleteJob* ArchiveModel::deleteFiles(QVector<Archive::Entry*> entries)
683 {
684     Q_ASSERT(m_archive);
685     if (!m_archive->isReadOnly()) {
686         DeleteJob *job = m_archive->deleteFiles(entries);
687         connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved);
688 
689         connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs);
690 
691         connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery);
692         return job;
693     }
694     return nullptr;
695 }
696 
encryptArchive(const QString & password,bool encryptHeader)697 void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader)
698 {
699     if (!m_archive) {
700         return;
701     }
702 
703     m_archive->encrypt(password, encryptHeader);
704 }
705 
conflictingEntries(QList<const Archive::Entry * > & conflictingEntries,const QStringList & entries,bool allowMerging) const706 bool ArchiveModel::conflictingEntries(QList<const Archive::Entry*> &conflictingEntries, const QStringList &entries, bool allowMerging) const
707 {
708     bool error = false;
709 
710     // We can't accept destination as an argument, because it can be a new entry path for renaming.
711     const Archive::Entry *destination;
712     {
713         QStringList destinationParts = entries.first().split(QLatin1Char('/'), Qt::SkipEmptyParts);
714         destinationParts.removeLast();
715         if (destinationParts.count() > 0) {
716             destination = m_rootEntry->findByPath(destinationParts);
717         } else {
718             destination = m_rootEntry.data();
719         }
720     }
721     const Archive::Entry *lastDirEntry = destination;
722     QString skippedDirPath;
723 
724     for (const QString &entry : entries) {
725         if (skippedDirPath.count() > 0 && entry.startsWith(skippedDirPath)) {
726             continue;
727         } else {
728             skippedDirPath.clear();
729         }
730 
731         while (!entry.startsWith(lastDirEntry->fullPath())) {
732             lastDirEntry = lastDirEntry->getParent();
733         }
734 
735         bool isDir = entry.right(1) == QLatin1String("/");
736         const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), Qt::SkipEmptyParts).last());
737 
738         if (archiveEntry != nullptr) {
739             if (archiveEntry->isDir() != isDir || !allowMerging) {
740                 if (isDir) {
741                     skippedDirPath = lastDirEntry->fullPath();
742                 }
743 
744                 if (!error) {
745                     conflictingEntries.clear();
746                     error = true;
747                 }
748                 conflictingEntries << archiveEntry;
749             } else {
750                 if (isDir) {
751                     lastDirEntry = archiveEntry;
752                 }
753                 else if (!error) {
754                     conflictingEntries << archiveEntry;
755                 }
756             }
757         } else if (isDir) {
758             skippedDirPath = entry;
759         }
760     }
761 
762     return error;
763 }
764 
hasDuplicatedEntries(const QStringList & entries)765 bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries)
766 {
767     QStringList tempList;
768     for (const QString &entry : entries) {
769         if (tempList.contains(entry)) {
770             return true;
771         }
772         tempList << entry;
773     }
774     return false;
775 }
776 
entryMap(const QVector<Archive::Entry * > & entries)777 QMap<QString, Archive::Entry*> ArchiveModel::entryMap(const QVector<Archive::Entry*> &entries)
778 {
779     QMap<QString, Archive::Entry*> map;
780     for (Archive::Entry *entry : entries) {
781         map.insert(entry->fullPath(), entry);
782     }
783     return map;
784 }
785 
slotCleanupEmptyDirs()786 void ArchiveModel::slotCleanupEmptyDirs()
787 {
788     QList<QPersistentModelIndex> queue;
789     QList<QPersistentModelIndex> nodesToDelete;
790 
791     // Add root nodes.
792     for (int i = 0; i < rowCount(); ++i) {
793         queue.append(QPersistentModelIndex(index(i, 0)));
794     }
795 
796     // Breadth-first traverse.
797     while (!queue.isEmpty()) {
798         QPersistentModelIndex node = queue.takeFirst();
799         Archive::Entry *entry = entryForIndex(node);
800 
801         if (!hasChildren(node)) {
802             if (entry->fullPath().isEmpty()) {
803                 nodesToDelete << node;
804             }
805         } else {
806             for (int i = 0; i < rowCount(node); ++i) {
807                 queue.append(QPersistentModelIndex(index(i, 0, node)));
808             }
809         }
810     }
811 
812     for (const QPersistentModelIndex& node : std::as_const(nodesToDelete)) {
813         Archive::Entry *rawEntry = static_cast<Archive::Entry*>(node.internalPointer());
814         qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row();
815         beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row());
816         rawEntry->getParent()->removeEntryAt(rawEntry->row());
817         endRemoveRows();
818     }
819 }
820 
countEntriesAndSize()821 void ArchiveModel::countEntriesAndSize()
822 {
823     // This function is used to count the number of folders/files and
824     // the total compressed size. This is needed for PropertiesDialog
825     // to update the corresponding values after adding/deleting files.
826 
827     // When ArchiveModel has been properly fixed, this code can likely
828     // be removed.
829 
830     m_numberOfFiles = 0;
831     m_numberOfFolders = 0;
832     m_uncompressedSize = 0;
833 
834     QElapsedTimer timer;
835     timer.start();
836 
837     traverseAndCountDirNode(m_rootEntry.data());
838 
839     qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms";
840 }
841 
traverseAndCountDirNode(Archive::Entry * dir)842 void ArchiveModel::traverseAndCountDirNode(Archive::Entry *dir)
843 {
844     const auto entries = dir->entries();
845     for (Archive::Entry *entry : entries) {
846         if (entry->isDir()) {
847             traverseAndCountDirNode(entry);
848             m_numberOfFolders++;
849         } else {
850             m_numberOfFiles++;
851             m_uncompressedSize += entry->property("size").toULongLong();
852         }
853     }
854 }
855 
numberOfFiles() const856 qulonglong ArchiveModel::numberOfFiles() const
857 {
858     return m_numberOfFiles;
859 }
860 
numberOfFolders() const861 qulonglong ArchiveModel::numberOfFolders() const
862 {
863     return m_numberOfFolders;
864 }
865 
uncompressedSize() const866 qulonglong ArchiveModel::uncompressedSize() const
867 {
868     return m_uncompressedSize;
869 }
870 
shownColumns() const871 QList<int> ArchiveModel::shownColumns() const
872 {
873     return m_showColumns;
874 }
875 
propertiesMap() const876 QMap<int, QByteArray> ArchiveModel::propertiesMap() const
877 {
878     return m_propertiesMap;
879 }
880