• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 35646821641

21 Sep 2026 07:45PM UTC coverage: 79.024% (+0.009%) from 79.015%
35646821641

push

github

web-flow
Types named like types (#1885)

Six type names began with a lower-case letter and stood out from the rest:
dragAndDropInfoPasswordStore (now StoreDragItem, and its MIME type
application/vnd.qtpass.store-item; the drag never leaves the process),
simpleTransaction, transactionHelper, execQueueItem, Enums::clipBoardType
and PasswordConfiguration::characterSet. The AppSettings field, the
settings constant and the settings key clipBoardType keep their names:
those are the stored form. No behaviour change.


Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

31 of 32 new or added lines in 9 files covered. (96.88%)

7478 of 9463 relevant lines covered (79.02%)

80.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.76
/src/storemodel.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "storemodel.h"
4
#include "pass.h"
5
#include "pathvalidator.h"
6
#include "qtpasslogging.h"
7
#include "util.h"
8
#include <QApplication>
9
#include <QDebug>
10
#include <QFileSystemModel>
11
#include <QMessageBox>
12
#include <QMimeData>
13
#include <QRegularExpression>
14
#include <QtGlobal>
15

16
auto operator<<(QDataStream &out, const StoreDragItem &item) -> QDataStream & {
16 ✔
17
  out << static_cast<quint8>(item.kind) << item.path;
16 ✔
18
  return out;
16 ✔
19
}
20

21
auto operator>>(QDataStream &in, StoreDragItem &item) -> QDataStream & {
23 ✔
22
  quint8 k;
23
  in >> k >> item.path;
23 ✔
24
  switch (k) {
23 ✔
25
  case static_cast<quint8>(StoreDragItem::ItemKind::Directory):
5 ✔
26
    item.kind = StoreDragItem::ItemKind::Directory;
5 ✔
27
    break;
5 ✔
28
  case static_cast<quint8>(StoreDragItem::ItemKind::File):
17 ✔
29
    item.kind = StoreDragItem::ItemKind::File;
17 ✔
30
    break;
17 ✔
31
  default:
1 ✔
32
    item.kind = StoreDragItem::ItemKind::Unknown;
1 ✔
33
    break;
1 ✔
34
  }
35
  return in;
23 ✔
36
}
37

38
/**
39
 * @brief StoreModel::StoreModel
40
 * SubClass of QSortFilterProxyModel via
41
 * http://www.qtcentre.org/threads/46471-QTreeView-Filter
42
 */
43
StoreModel::StoreModel() { fs = nullptr; }
92 ✔
44

45
void StoreModel::setPass(Pass *pass) { m_pass = pass; }
54 ✔
46

47
/**
48
 * @brief StoreModel::filterAcceptsRow should row be shown, wrapper for
49
 * StoreModel::showThis method.
50
 * @param sourceRow
51
 * @param sourceParent
52
 * @return
53
 */
54
auto StoreModel::filterAcceptsRow(int sourceRow,
693 ✔
55
                                  const QModelIndex &sourceParent) const
56
    -> bool {
57
  QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
693 ✔
58
  return showThis(index);
693 ✔
59
}
60

61
/**
62
 * @brief StoreModel::showThis should a row be shown, based on our search
63
 * criteria.
64
 * @param index
65
 * @return
66
 */
67
auto StoreModel::showThis(const QModelIndex &index) const -> bool {
1,222 ✔
68
  bool retVal = false;
69
  if (fs == nullptr) {
1,222 ✔
70
    return retVal;
71
  }
72
  // Gives you the info for number of children with a parent
73
  if (sourceModel()->rowCount(index) > 0) {
1,167 ✔
74
    for (int nChild = 0; nChild < sourceModel()->rowCount(index); ++nChild) {
552 ✔
75
      QModelIndex childIndex = sourceModel()->index(nChild, 0, index);
528 ✔
76
      if (!childIndex.isValid()) {
77
        break;
78
      }
79
      retVal = showThis(childIndex);
528 ✔
80
      if (retVal) {
528 ✔
81
        break;
82
      }
83
    }
84
  } else {
85
    QModelIndex useIndex = sourceModel()->index(index.row(), 0, index.parent());
653 ✔
86
    QString path = fs->filePath(useIndex);
653 ✔
87
    path = QDir(store).relativeFilePath(path);
1,306 ✔
88
    if (path.startsWith(".git")) {
1,306 ✔
89
      return false;
90
    }
91
    path.replace(Util::endsWithGpg(), "");
652 ✔
92
    retVal = path.contains(filterRegularExpression());
652 ✔
93
  }
94
  return retVal;
95
}
96

97
/**
98
 * @brief StoreModel::setModelAndStore update the source model and store.
99
 * @param sourceModel
100
 * @param passStore
101
 */
102
void StoreModel::setModelAndStore(QFileSystemModel *sourceModel,
86 ✔
103
                                  const QString &passStore) {
104
  setSourceModel(sourceModel);
86 ✔
105
  fs = sourceModel;
86 ✔
106
  store = passStore;
86 ✔
107
}
86 ✔
108

109
auto StoreModel::rootIndexFor(const QString &path) -> QModelIndex {
68 ✔
110
  if (fs == nullptr) {
68 ✔
111
    return {};
112
  }
113
  return mapFromSource(fs->setRootPath(QDir::cleanPath(path)));
134 ✔
114
}
115

116
void StoreModel::setStore(const QString &passStore) {
4 ✔
117
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
118
  // beginFilterChange() is available since Qt 6.9, but the Direction-scoped
119
  // endFilterChange(QSortFilterProxyModel::Direction) overload is only
120
  // available since Qt 6.10, which is the preferred API for scoped and more
121
  // efficient filter updates.
122
  beginFilterChange();
123
  store = passStore;
124
  endFilterChange(QSortFilterProxyModel::Direction::Rows);
125
#else
126
  // Direction-scoped filter changes are unavailable before Qt 6.10, so older
127
  // Qt versions must manually invalidate filters. We update the store and
128
  // manually invalidate the filter as a compatibility path.
129
  store = passStore;
4 ✔
130
  invalidateFilter();
4 ✔
131
#endif
132
}
4 ✔
133

134
/**
135
 * @brief StoreModel::data don't show the .gpg at the end of a file.
136
 * @param index
137
 * @param role
138
 * @return
139
 */
140
auto StoreModel::data(const QModelIndex &index, int role) const -> QVariant {
1,844 ✔
141
  if (!index.isValid()) {
142
    return {};
143
  }
144

145
  QVariant initial_value;
146
  initial_value = QSortFilterProxyModel::data(index, role);
1,843 ✔
147

148
  if (role == Qt::DisplayRole) {
1,843 ✔
149
    QString name = initial_value.toString();
249 ✔
150
    name.replace(Util::endsWithGpg(), "");
249 ✔
151
    initial_value.setValue(name);
249 ✔
152
  }
153

154
  return initial_value;
1,843 ✔
155
}
1,843 ✔
156

157
/**
158
 * @brief StoreModel::supportedDropActions enable drop.
159
 * @return
160
 */
161
auto StoreModel::supportedDropActions() const -> Qt::DropActions {
1 ✔
162
  return Qt::CopyAction | Qt::MoveAction;
1 ✔
163
}
164

165
/**
166
 * @brief StoreModel::supportedDragActions enable drag.
167
 * @return
168
 */
169
auto StoreModel::supportedDragActions() const -> Qt::DropActions {
1 ✔
170
  return Qt::CopyAction | Qt::MoveAction;
1 ✔
171
}
172

173
/**
174
 * @brief StoreModel::flags
175
 * @param index
176
 * @return
177
 */
178
auto StoreModel::flags(const QModelIndex &index) const -> Qt::ItemFlags {
649 ✔
179
  Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index);
649 ✔
180

181
  if (index.isValid()) {
182
    return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
183
  }
184
  return Qt::ItemIsDropEnabled | defaultFlags;
1 ✔
185
}
186

187
/**
188
 * @brief StoreModel::mimeTypes
189
 * @return
190
 */
191
auto StoreModel::mimeTypes() const -> QStringList {
1 ✔
192
  QStringList types;
1 ✔
193
  types << kStoreDragMimeType;
×
194
  return types;
1 ✔
195
}
196

197
/**
198
 * @brief StoreModel::mimeData
199
 * @param indexes
200
 * @return
201
 */
202
auto StoreModel::mimeData(const QModelIndexList &indexes) const -> QMimeData * {
1 ✔
203
  StoreDragItem info;
1 ✔
204

205
  if (indexes.isEmpty())
1 ✔
206
    return nullptr;
207
  QByteArray encodedData;
1 ✔
208
  // only use the first, otherwise we should enable multiselection
209
  QModelIndex index = indexes.at(0);
1 ✔
210
  if (index.isValid()) {
211
    QModelIndex useIndex = mapToSource(index);
1 ✔
212
    const QFileInfo fileInfo = fs->fileInfo(useIndex);
1 ✔
213

214
    if (fileInfo.isDir()) {
1 ✔
NEW
215
      info.kind = StoreDragItem::ItemKind::Directory;
×
216
    } else if (fileInfo.isFile()) {
1 ✔
217
      info.kind = StoreDragItem::ItemKind::File;
1 ✔
218
    }
219
    info.path = fileInfo.absoluteFilePath();
2 ✔
220
    QDataStream stream(&encodedData, QIODevice::WriteOnly);
1 ✔
221
    stream << info;
1 ✔
222
  }
1 ✔
223

224
  auto *mimeData = new QMimeData();
1 ✔
225
  mimeData->setData(kStoreDragMimeType, encodedData);
2 ✔
226
  return mimeData;
227
}
228

229
/**
230
 * @brief StoreModel::canDropMimeData
231
 * @param data
232
 * @param action
233
 * @param row
234
 * @param column
235
 * @param parent
236
 * @return
237
 */
238
auto StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action,
16 ✔
239
                                 int row, int column,
240
                                 const QModelIndex &parent) const -> bool {
241
  qCDebug(lcQtPass) << "canDropMimeData" << action << row;
16 ✔
242

243
  const auto parsed = parseDropData(data);
16 ✔
244
  if (!parsed) {
16 ✔
245
    return false;
246
  }
247
  const StoreDragItem &info = *parsed;
248

249
  QModelIndex useIndex =
250
      this->index(parent.row(), parent.column(), parent.parent());
13 ✔
251

252
  if (column > 0) {
13 ✔
253
    return false;
254
  }
255

256
  using IK = StoreDragItem::ItemKind;
257
  // you can drop a folder on a folder
258
  if (fs->fileInfo(mapToSource(useIndex)).isDir() &&
24 ✔
259
      info.kind == IK::Directory) {
10 ✔
260
    return true;
261
  }
262
  // you can drop a file on a folder
263
  if (fs->fileInfo(mapToSource(useIndex)).isDir() && info.kind == IK::File) {
12 ✔
264
    return true;
265
  }
266
  // you can drop a file on a file
267
  if (fs->fileInfo(mapToSource(useIndex)).isFile() && info.kind == IK::File) {
3 ✔
268
    return true;
269
  }
270

271
  return false;
272
}
273

274
/**
275
 * @brief StoreModel::dropMimeData
276
 * @param data
277
 * @param action
278
 * @param row
279
 * @param column
280
 * @param parent
281
 * @return
282
 */
283
auto StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
8 ✔
284
                              int row, int column, const QModelIndex &parent)
285
    -> bool {
286
  if (!canDropMimeData(data, action, row, column, parent)) {
8 ✔
287
    return false;
288
  }
289

290
  if (action == Qt::IgnoreAction) {
8 ✔
291
    return true;
292
  }
293

294
  if (action != Qt::MoveAction && action != Qt::CopyAction) {
7 ✔
295
    return false;
296
  }
297

298
  const auto info = parseDropData(data);
7 ✔
299
  if (!info) {
7 ✔
300
    return false;
301
  }
302

303
  return executeDropAction(*info, action, parent);
7 ✔
304
}
305

306
auto StoreModel::parseDropData(const QMimeData *data)
23 ✔
307
    -> std::optional<StoreDragItem> {
308
  if (data == nullptr || !data->hasFormat(kStoreDragMimeType)) {
45 ✔
309
    return std::nullopt;
2 ✔
310
  }
311
  QByteArray encodedData = data->data(kStoreDragMimeType);
42 ✔
312
  if (encodedData.isEmpty()) {
21 ✔
313
    return std::nullopt;
1 ✔
314
  }
315
  QDataStream stream(&encodedData, QIODevice::ReadOnly);
20 ✔
316
  StoreDragItem info;
20 ✔
317
  stream >> info;
20 ✔
318
  if (stream.status() != QDataStream::Ok) {
20 ✔
319
    return std::nullopt;
×
320
  }
321
  return info;
322
}
20 ✔
323

324
auto StoreModel::executeDropAction(const StoreDragItem &info,
7 ✔
325
                                   Qt::DropAction action,
326
                                   const QModelIndex &parent) -> bool {
327
  QModelIndex destIndex =
328
      this->index(parent.row(), parent.column(), parent.parent());
7 ✔
329
  QFileInfo destFileinfo = fs->fileInfo(mapToSource(destIndex));
7 ✔
330
  QFileInfo srcFileInfo = QFileInfo(info.path);
7 ✔
331

332
  QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath());
7 ✔
333
  QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath());
7 ✔
334

335
  // Both endpoints must resolve inside the password store after symlink
336
  // resolution. Drop data is encoded by the dragged item but could be
337
  // crafted; canonical-path checks stop drops that would move/copy outside
338
  // the store or follow a symlink out (e.g. a symlink within the store
339
  // pointing at /etc).
340
  if (!PathValidator::isPathInStore(store, cleanedSrc) ||
11 ✔
341
      !PathValidator::isPathInStore(store, cleanedDest)) {
4 ✔
342
    qCWarning(lcQtPass)
6 ✔
343
        << "executeDropAction: rejecting drop that escapes the store"
3 ✔
344
        << "(src=" << cleanedSrc << "dest=" << cleanedDest << ")";
3 ✔
345
    return false;
3 ✔
346
  }
347

348
  switch (info.kind) {
4 ✔
349
  case StoreDragItem::ItemKind::Directory: {
1 ✔
350
    // Dropping a folder onto a folder: move/copy it *into* the target.
351
    if (!destFileinfo.isDir()) {
1 ✔
352
      return false;
353
    }
354
    const QString destDir =
355
        QDir::cleanPath(QDir(cleanedDest).filePath(srcFileInfo.fileName()));
2 ✔
356
    return performDrop(cleanedSrc, destDir, action, false);
1 ✔
357
  }
358
  case StoreDragItem::ItemKind::File:
3 ✔
359
    // File onto a folder drops into it (no clash); file onto an existing
360
    // file asks before overwriting.
361
    if (destFileinfo.isDir()) {
3 ✔
362
      return performDrop(cleanedSrc, cleanedDest, action, false);
3 ✔
363
    }
364
    return performDrop(
×
365
        cleanedSrc, cleanedDest, action,
366
        QMessageBox::question(
×
367
            qobject_cast<QWidget *>(QObject::parent()), tr("Force overwrite?"),
×
368
            tr("overwrite %1 with %2?").arg(cleanedDest, cleanedSrc),
×
369
            QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes);
370
  default:
×
371
    qCWarning(lcQtPass)
×
372
        << "executeDropAction: unexpected ItemKind, ignoring drop";
×
373
    return false;
×
374
  }
375
}
7 ✔
376

377
auto StoreModel::performDrop(const QString &cleanedSrc,
4 ✔
378
                             const QString &cleanedDest, Qt::DropAction action,
379
                             bool force) -> bool {
380
  if (!m_pass) {
4 ✔
381
    return false;
382
  }
383
  if (action == Qt::MoveAction) {
3 ✔
384
    m_pass->Move(cleanedSrc, cleanedDest, force);
6 ✔
385
  } else if (action == Qt::CopyAction) {
1 ✔
386
    m_pass->Copy(cleanedSrc, cleanedDest, force);
3 ✔
387
  }
388
  return true;
389
}
390

391
/**
392
 * @brief StoreModel::lessThan
393
 * @param source_left
394
 * @param source_right
395
 * @return
396
 */
397
auto StoreModel::lessThan(const QModelIndex &source_left,
466 ✔
398
                          const QModelIndex &source_right) const -> bool {
399
/* matches logic in QFileSystemModelSorter::compareNodes() */
400
#ifndef Q_OS_MAC
401
  if (fs && (source_left.column() == 0 || source_left.column() == 1)) {
466 ✔
402
    bool leftD = fs->isDir(source_left);
466 ✔
403
    bool rightD = fs->isDir(source_right);
466 ✔
404

405
    if (leftD ^ rightD) {
466 ✔
406
      return leftD;
407
    }
408
  }
409
#endif
410

411
  return QSortFilterProxyModel::lessThan(source_left, source_right);
456 ✔
412
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc