QtPass Developer documentation
QtPass test coverage
Current view: top level - src/storemodel.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 92.8 % 152 141
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 21 21

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements StoreModel and the StoreDragItem stream operators; the
       6              :  *        drop validation lives in executeDropAction().
       7              :  */
       8              : #include "storemodel.h"
       9              : #include "pass.h"
      10              : #include "pathvalidator.h"
      11              : #include "plainmessagebox.h"
      12              : #include "qtpasslogging.h"
      13              : #include "util.h"
      14              : #include <QApplication>
      15              : #include <QDebug>
      16              : #include <QFileSystemModel>
      17              : #include <QMessageBox>
      18              : #include <QMimeData>
      19              : #include <QRegularExpression>
      20              : #include <QtGlobal>
      21              : 
      22           16 : auto operator<<(QDataStream &out, const StoreDragItem &item) -> QDataStream & {
      23           16 :   out << static_cast<quint8>(item.kind) << item.path;
      24           16 :   return out;
      25              : }
      26              : 
      27           23 : auto operator>>(QDataStream &in, StoreDragItem &item) -> QDataStream & {
      28              :   quint8 k;
      29           23 :   in >> k >> item.path;
      30           23 :   switch (k) {
      31            5 :   case static_cast<quint8>(StoreDragItem::ItemKind::Directory):
      32            5 :     item.kind = StoreDragItem::ItemKind::Directory;
      33            5 :     break;
      34           17 :   case static_cast<quint8>(StoreDragItem::ItemKind::File):
      35           17 :     item.kind = StoreDragItem::ItemKind::File;
      36           17 :     break;
      37            1 :   default:
      38            1 :     item.kind = StoreDragItem::ItemKind::Unknown;
      39            1 :     break;
      40              :   }
      41           23 :   return in;
      42              : }
      43              : 
      44              : // Filter approach via http://www.qtcentre.org/threads/46471-QTreeView-Filter
      45          174 : StoreModel::StoreModel() { fs = nullptr; }
      46              : 
      47          137 : void StoreModel::setPass(Pass *pass) { m_pass = pass; }
      48              : 
      49         4642 : auto StoreModel::filterAcceptsRow(int sourceRow,
      50              :                                   const QModelIndex &sourceParent) const
      51              :     -> bool {
      52         4642 :   QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
      53         4642 :   return showThis(index);
      54              : }
      55              : 
      56         7029 : auto StoreModel::showThis(const QModelIndex &index) const -> bool {
      57              :   bool retVal = false;
      58         7029 :   if (fs == nullptr) {
      59              :     return retVal;
      60              :   }
      61         6892 :   if (sourceModel()->rowCount(index) > 0) {
      62         2480 :     for (int nChild = 0; nChild < sourceModel()->rowCount(index); ++nChild) {
      63         2386 :       QModelIndex childIndex = sourceModel()->index(nChild, 0, index);
      64              :       if (!childIndex.isValid()) {
      65              :         break;
      66              :       }
      67         2386 :       retVal = showThis(childIndex);
      68         2386 :       if (retVal) {
      69              :         break;
      70              :       }
      71              :     }
      72              :   } else {
      73         5379 :     QModelIndex useIndex = sourceModel()->index(index.row(), 0, index.parent());
      74         5379 :     QString path = fs->filePath(useIndex);
      75        10758 :     path = QDir(store).relativeFilePath(path);
      76        10758 :     if (path.startsWith(".git")) {
      77              :       return false;
      78              :     }
      79         5378 :     path.replace(Util::endsWithGpg(), "");
      80         5378 :     retVal = path.contains(filterRegularExpression());
      81              :   }
      82              :   return retVal;
      83              : }
      84              : 
      85          168 : void StoreModel::setModelAndStore(QFileSystemModel *sourceModel,
      86              :                                   const QString &passStore) {
      87          168 :   setSourceModel(sourceModel);
      88          168 :   fs = sourceModel;
      89          168 :   store = passStore;
      90          168 : }
      91              : 
      92          196 : auto StoreModel::rootIndexFor(const QString &path) -> QModelIndex {
      93          196 :   if (fs == nullptr) {
      94            1 :     return {};
      95              :   }
      96          390 :   return mapFromSource(fs->setRootPath(QDir::cleanPath(path)));
      97              : }
      98              : 
      99           10 : void StoreModel::setStore(const QString &passStore) {
     100              : #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
     101              :   // The Direction-scoped endFilterChange() overload needs Qt 6.10.
     102              :   beginFilterChange();
     103              :   store = passStore;
     104              :   endFilterChange(QSortFilterProxyModel::Direction::Rows);
     105              : #else
     106           10 :   store = passStore;
     107           10 :   invalidateFilter();
     108              : #endif
     109           10 : }
     110              : 
     111        15800 : auto StoreModel::data(const QModelIndex &index, int role) const -> QVariant {
     112              :   if (!index.isValid()) {
     113              :     return {};
     114              :   }
     115              : 
     116              :   QVariant initial_value;
     117        15799 :   initial_value = QSortFilterProxyModel::data(index, role);
     118              : 
     119        15799 :   if (role == Qt::DisplayRole) {
     120         2076 :     QString name = initial_value.toString();
     121         2076 :     name.replace(Util::endsWithGpg(), "");
     122         2076 :     initial_value.setValue(name);
     123              :   }
     124              : 
     125              :   return initial_value;
     126        15799 : }
     127              : 
     128            1 : auto StoreModel::supportedDropActions() const -> Qt::DropActions {
     129            1 :   return Qt::CopyAction | Qt::MoveAction;
     130              : }
     131              : 
     132            1 : auto StoreModel::supportedDragActions() const -> Qt::DropActions {
     133            1 :   return Qt::CopyAction | Qt::MoveAction;
     134              : }
     135              : 
     136         7047 : auto StoreModel::flags(const QModelIndex &index) const -> Qt::ItemFlags {
     137         7047 :   Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index);
     138              : 
     139              :   if (index.isValid()) {
     140              :     return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
     141              :   }
     142            1 :   return Qt::ItemIsDropEnabled | defaultFlags;
     143              : }
     144              : 
     145            1 : auto StoreModel::mimeTypes() const -> QStringList {
     146            1 :   QStringList types;
     147            0 :   types << kStoreDragMimeType;
     148            1 :   return types;
     149              : }
     150              : 
     151            1 : auto StoreModel::mimeData(const QModelIndexList &indexes) const -> QMimeData * {
     152            1 :   StoreDragItem info;
     153              : 
     154            1 :   if (indexes.isEmpty())
     155              :     return nullptr;
     156            1 :   QByteArray encodedData;
     157              :   // only use the first, otherwise we should enable multiselection
     158            1 :   QModelIndex index = indexes.at(0);
     159              :   if (index.isValid()) {
     160            1 :     QModelIndex useIndex = mapToSource(index);
     161            1 :     const QFileInfo fileInfo = fs->fileInfo(useIndex);
     162              : 
     163            1 :     if (fileInfo.isDir()) {
     164            0 :       info.kind = StoreDragItem::ItemKind::Directory;
     165            1 :     } else if (fileInfo.isFile()) {
     166            1 :       info.kind = StoreDragItem::ItemKind::File;
     167              :     }
     168            1 :     info.path = fileInfo.absoluteFilePath();
     169            1 :     QDataStream stream(&encodedData, QIODevice::WriteOnly);
     170            1 :     stream << info;
     171            1 :   }
     172              : 
     173            1 :   auto *mimeData = new QMimeData();
     174            2 :   mimeData->setData(kStoreDragMimeType, encodedData);
     175              :   return mimeData;
     176              : }
     177              : 
     178           16 : auto StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action,
     179              :                                  int row, int column,
     180              :                                  const QModelIndex &parent) const -> bool {
     181           16 :   qCDebug(lcQtPass) << "canDropMimeData" << action << row;
     182              : 
     183           16 :   const auto parsed = parseDropData(data);
     184           16 :   if (!parsed) {
     185              :     return false;
     186              :   }
     187              :   const StoreDragItem &info = *parsed;
     188              : 
     189              :   QModelIndex useIndex =
     190           13 :       this->index(parent.row(), parent.column(), parent.parent());
     191              : 
     192           13 :   if (column > 0) {
     193              :     return false;
     194              :   }
     195              : 
     196              :   using IK = StoreDragItem::ItemKind;
     197              :   // you can drop a folder on a folder
     198           24 :   if (fs->fileInfo(mapToSource(useIndex)).isDir() &&
     199           10 :       info.kind == IK::Directory) {
     200              :     return true;
     201              :   }
     202              :   // you can drop a file on a folder
     203           12 :   if (fs->fileInfo(mapToSource(useIndex)).isDir() && info.kind == IK::File) {
     204              :     return true;
     205              :   }
     206              :   // you can drop a file on a file
     207            3 :   if (fs->fileInfo(mapToSource(useIndex)).isFile() && info.kind == IK::File) {
     208              :     return true;
     209              :   }
     210              : 
     211              :   return false;
     212              : }
     213              : 
     214            8 : auto StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
     215              :                               int row, int column, const QModelIndex &parent)
     216              :     -> bool {
     217            8 :   if (!canDropMimeData(data, action, row, column, parent)) {
     218              :     return false;
     219              :   }
     220              : 
     221            8 :   if (action == Qt::IgnoreAction) {
     222              :     return true;
     223              :   }
     224              : 
     225            7 :   if (action != Qt::MoveAction && action != Qt::CopyAction) {
     226              :     return false;
     227              :   }
     228              : 
     229            7 :   const auto info = parseDropData(data);
     230            7 :   if (!info) {
     231              :     return false;
     232              :   }
     233              : 
     234            7 :   return executeDropAction(*info, action, parent);
     235              : }
     236              : 
     237           23 : auto StoreModel::parseDropData(const QMimeData *data)
     238              :     -> std::optional<StoreDragItem> {
     239           45 :   if (data == nullptr || !data->hasFormat(kStoreDragMimeType)) {
     240            2 :     return std::nullopt;
     241              :   }
     242           42 :   QByteArray encodedData = data->data(kStoreDragMimeType);
     243           21 :   if (encodedData.isEmpty()) {
     244            1 :     return std::nullopt;
     245              :   }
     246           20 :   QDataStream stream(&encodedData, QIODevice::ReadOnly);
     247           20 :   StoreDragItem info;
     248           20 :   stream >> info;
     249           20 :   if (stream.status() != QDataStream::Ok) {
     250            0 :     return std::nullopt;
     251              :   }
     252              :   return info;
     253           20 : }
     254              : 
     255            7 : auto StoreModel::executeDropAction(const StoreDragItem &info,
     256              :                                    Qt::DropAction action,
     257              :                                    const QModelIndex &parent) -> bool {
     258              :   QModelIndex destIndex =
     259            7 :       this->index(parent.row(), parent.column(), parent.parent());
     260            7 :   QFileInfo destFileinfo = fs->fileInfo(mapToSource(destIndex));
     261            7 :   QFileInfo srcFileInfo = QFileInfo(info.path);
     262              : 
     263            7 :   QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath());
     264            7 :   QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath());
     265              : 
     266              :   // Drop data could be crafted: both endpoints must resolve inside the store
     267              :   // after symlink resolution (e.g. no in-store symlink pointing at /etc).
     268           11 :   if (!PathValidator::isPathInStore(store, cleanedSrc) ||
     269            4 :       !PathValidator::isPathInStore(store, cleanedDest)) {
     270            6 :     qCWarning(lcQtPass)
     271            3 :         << "executeDropAction: rejecting drop that escapes the store"
     272            3 :         << "(src=" << cleanedSrc << "dest=" << cleanedDest << ")";
     273            3 :     return false;
     274              :   }
     275              : 
     276            4 :   switch (info.kind) {
     277            1 :   case StoreDragItem::ItemKind::Directory: {
     278              :     // Dropping a folder onto a folder: move/copy it *into* the target.
     279            1 :     if (!destFileinfo.isDir()) {
     280              :       return false;
     281              :     }
     282              :     const QString destDir =
     283            2 :         QDir::cleanPath(QDir(cleanedDest).filePath(srcFileInfo.fileName()));
     284            1 :     return performDrop(cleanedSrc, destDir, action, false);
     285              :   }
     286            3 :   case StoreDragItem::ItemKind::File:
     287              :     // File onto a folder drops into it (no clash); file onto an existing
     288              :     // file asks before overwriting.
     289            3 :     if (destFileinfo.isDir()) {
     290            3 :       return performDrop(cleanedSrc, cleanedDest, action, false);
     291              :     }
     292            0 :     return performDrop(
     293              :         cleanedSrc, cleanedDest, action,
     294            0 :         PlainMessageBox::askYes(
     295            0 :             qobject_cast<QWidget *>(QObject::parent()), tr("Force overwrite?"),
     296            0 :             tr("overwrite %1 with %2?").arg(cleanedDest, cleanedSrc)));
     297            0 :   default:
     298            0 :     qCWarning(lcQtPass)
     299            0 :         << "executeDropAction: unexpected ItemKind, ignoring drop";
     300            0 :     return false;
     301              :   }
     302            7 : }
     303              : 
     304            4 : auto StoreModel::performDrop(const QString &cleanedSrc,
     305              :                              const QString &cleanedDest, Qt::DropAction action,
     306              :                              bool force) -> bool {
     307            4 :   if (!m_pass) {
     308              :     return false;
     309              :   }
     310            3 :   if (action == Qt::MoveAction) {
     311            6 :     m_pass->Move(cleanedSrc, cleanedDest, force);
     312            1 :   } else if (action == Qt::CopyAction) {
     313            3 :     m_pass->Copy(cleanedSrc, cleanedDest, force);
     314              :   }
     315              :   return true;
     316              : }
     317              : 
     318         8946 : auto StoreModel::lessThan(const QModelIndex &source_left,
     319              :                           const QModelIndex &source_right) const -> bool {
     320              : /* matches logic in QFileSystemModelSorter::compareNodes() */
     321              : #ifndef Q_OS_MAC
     322         8946 :   if (fs && (source_left.column() == 0 || source_left.column() == 1)) {
     323         8946 :     bool leftD = fs->isDir(source_left);
     324         8946 :     bool rightD = fs->isDir(source_right);
     325              : 
     326         8946 :     if (leftD ^ rightD) {
     327              :       return leftD;
     328              :     }
     329              :   }
     330              : #endif
     331              : 
     332         7217 :   return QSortFilterProxyModel::lessThan(source_left, source_right);
     333              : }
        

Generated by: LCOV version 2.0-1