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

IJHack / QtPass / 27477250065

13 Jun 2026 07:49PM UTC coverage: 55.57%. Remained the same
27477250065

push

github

web-flow
refactor: extract isPathInStore into PathValidator module (#1514) (#1524)

* refactor: extract isPathInStore into PathValidator module (#1514)

Move Util::isPathInStore into a dedicated PathValidator class
(src/pathvalidator.{h,cpp}), continuing the Util grab-bag split.
The store-boundary check is a self-contained, security-sensitive
concern with no dependency on the rest of Util, so it earns its own
focused module.

- Verbatim move of the impl (walk to nearest existing ancestor,
  canonicalise, re-append leaf, compare against canonical store root).
- Update callers: StoreModel drag-drop (2 sites), MainWindow
  confirmPathInStore (1 site).
- Retarget the isPathInStore* cases in tst_util to PathValidator.
- Wire pathvalidator.cpp/.h into src/src.pro.

#1514 stays open for the remaining Template I/O slice
(readTemplates/writeTemplates/getFolderTemplate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* l10n: fill empty unfinished translations flagged on review

Fill the empty <translation type="unfinished"> entries surfaced by the
qmake6 .ts refresh, keeping type="unfinished" so Weblate can refine.

- 10 locales (af, ar, bg, bn, ca, cs, de_DE, de_LU, el, en_GB): best-effort
  translations for the two new MainWindow strings "That name would resolve
  outside the password store..." and "Open %1 in browser", placeholders
  preserved. en_GB only lacked the latter.
- en_US: fill all 106 empty entries with the source text, converting British
  spellings (behaviour->behavior, etc.) and preserving %1/%2 placeholders.

All 11 files validated with lrelease6 (no errors/warnings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

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

24 of 26 new or added lines in 3 files covered. (92.31%)

9 existing lines in 1 file now uncovered.

3731 of 6714 relevant lines covered (55.57%)

36.9 hits per line

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

85.33
/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 "qtpasssettings.h"
5

6
#include "pathvalidator.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<<(
11✔
17
    QDataStream &out,
18
    const dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore)
19
    -> QDataStream & {
20
  out << static_cast<quint8>(dragAndDropInfoPasswordStore.kind)
11✔
21
      << dragAndDropInfoPasswordStore.path;
11✔
22
  return out;
11✔
23
}
24

25
auto operator>>(QDataStream &in,
14✔
26
                dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore)
27
    -> QDataStream & {
28
  quint8 k;
29
  in >> k >> dragAndDropInfoPasswordStore.path;
14✔
30
  switch (k) {
14✔
31
  case static_cast<quint8>(dragAndDropInfoPasswordStore::ItemKind::Directory):
3✔
32
    dragAndDropInfoPasswordStore.kind =
3✔
33
        dragAndDropInfoPasswordStore::ItemKind::Directory;
34
    break;
3✔
35
  case static_cast<quint8>(dragAndDropInfoPasswordStore::ItemKind::File):
10✔
36
    dragAndDropInfoPasswordStore.kind =
10✔
37
        dragAndDropInfoPasswordStore::ItemKind::File;
38
    break;
10✔
39
  default:
1✔
40
    dragAndDropInfoPasswordStore.kind =
1✔
41
        dragAndDropInfoPasswordStore::ItemKind::Unknown;
42
    break;
1✔
43
  }
44
  return in;
14✔
45
}
46

47
/**
48
 * @brief StoreModel::StoreModel
49
 * SubClass of QSortFilterProxyModel via
50
 * http://www.qtcentre.org/threads/46471-QTreeView-Filter
51
 */
52
StoreModel::StoreModel() { fs = nullptr; }
45✔
53

54
/**
55
 * @brief StoreModel::filterAcceptsRow should row be shown, wrapper for
56
 * StoreModel::showThis method.
57
 * @param sourceRow
58
 * @param sourceParent
59
 * @return
60
 */
61
auto StoreModel::filterAcceptsRow(int sourceRow,
115✔
62
                                  const QModelIndex &sourceParent) const
63
    -> bool {
64
  QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
115✔
65
  return showThis(index);
115✔
66
}
67

68
/**
69
 * @brief StoreModel::showThis should a row be shown, based on our search
70
 * criteria.
71
 * @param index
72
 * @return
73
 */
74
auto StoreModel::showThis(const QModelIndex &index) const -> bool {
248✔
75
  bool retVal = false;
76
  if (fs == nullptr) {
248✔
77
    return retVal;
78
  }
79
  // Gives you the info for number of children with a parent
80
  if (sourceModel()->rowCount(index) > 0) {
247✔
81
    for (int nChild = 0; nChild < sourceModel()->rowCount(index); ++nChild) {
132✔
82
      QModelIndex childIndex = sourceModel()->index(nChild, 0, index);
132✔
83
      if (!childIndex.isValid()) {
84
        break;
85
      }
86
      retVal = showThis(childIndex);
132✔
87
      if (retVal) {
132✔
88
        break;
89
      }
90
    }
91
  } else {
92
    QModelIndex useIndex = sourceModel()->index(index.row(), 0, index.parent());
115✔
93
    QString path = fs->filePath(useIndex);
115✔
94
    path = QDir(store).relativeFilePath(path);
230✔
95
    if (path.startsWith(".git")) {
230✔
96
      return false;
97
    }
98
    path.replace(Util::endsWithGpg(), "");
114✔
99
    retVal = path.contains(filterRegularExpression());
114✔
100
  }
101
  return retVal;
102
}
103

104
/**
105
 * @brief StoreModel::setModelAndStore update the source model and store.
106
 * @param sourceModel
107
 * @param passStore
108
 */
109
void StoreModel::setModelAndStore(QFileSystemModel *sourceModel,
40✔
110
                                  const QString &passStore) {
111
  setSourceModel(sourceModel);
40✔
112
  fs = sourceModel;
40✔
113
  store = passStore;
40✔
114
}
40✔
115

116
void StoreModel::setStore(const QString &passStore) {
1✔
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;
1✔
130
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
131
  QSortFilterProxyModel::invalidateFilter();
1✔
132
#else
133
  invalidateFilter();
134
#endif
135
#endif
136
}
1✔
137

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

149
  QVariant initial_value;
150
  initial_value = QSortFilterProxyModel::data(index, role);
2✔
151

152
  if (role == Qt::DisplayRole) {
2✔
153
    QString name = initial_value.toString();
1✔
154
    name.replace(Util::endsWithGpg(), "");
1✔
155
    initial_value.setValue(name);
1✔
156
  }
157

158
  return initial_value;
2✔
159
}
2✔
160

161
/**
162
 * @brief StoreModel::supportedDropActions enable drop.
163
 * @return
164
 */
165
auto StoreModel::supportedDropActions() const -> Qt::DropActions {
1✔
166
  return Qt::CopyAction | Qt::MoveAction;
1✔
167
}
168

169
/**
170
 * @brief StoreModel::supportedDragActions enable drag.
171
 * @return
172
 */
173
auto StoreModel::supportedDragActions() const -> Qt::DropActions {
1✔
174
  return Qt::CopyAction | Qt::MoveAction;
1✔
175
}
176

177
/**
178
 * @brief StoreModel::flags
179
 * @param index
180
 * @return
181
 */
182
auto StoreModel::flags(const QModelIndex &index) const -> Qt::ItemFlags {
2✔
183
  Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index);
2✔
184

185
  if (index.isValid()) {
186
    return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
187
  }
188
  return Qt::ItemIsDropEnabled | defaultFlags;
1✔
189
}
190

191
/**
192
 * @brief StoreModel::mimeTypes
193
 * @return
194
 */
195
auto StoreModel::mimeTypes() const -> QStringList {
1✔
196
  QStringList types;
1✔
197
  types << "application/vnd.qtpass.dragAndDropInfoPasswordStore";
1✔
198
  return types;
1✔
199
}
200

201
/**
202
 * @brief StoreModel::mimeData
203
 * @param indexes
204
 * @return
205
 */
206
auto StoreModel::mimeData(const QModelIndexList &indexes) const -> QMimeData * {
1✔
207
  dragAndDropInfoPasswordStore info;
1✔
208

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

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

226
  auto *mimeData = new QMimeData();
1✔
227
  mimeData->setData("application/vnd.qtpass.dragAndDropInfoPasswordStore",
2✔
228
                    encodedData);
229
  return mimeData;
1✔
230
}
231

232
/**
233
 * @brief StoreModel::canDropMimeData
234
 * @param data
235
 * @param action
236
 * @param row
237
 * @param column
238
 * @param parent
239
 * @return
240
 */
241
auto StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action,
11✔
242
                                 int row, int column,
243
                                 const QModelIndex &parent) const -> bool {
244
#ifdef QT_DEBUG
245
  qDebug() << action << row;
246
#else
247
  Q_UNUSED(action)
248
  Q_UNUSED(row)
249
#endif
250

251
  if (data == nullptr ||
21✔
252
      !data->hasFormat("application/vnd.qtpass.dragAndDropInfoPasswordStore")) {
21✔
253
    return false;
254
  }
255

256
  QByteArray encodedData =
257
      data->data("application/vnd.qtpass.dragAndDropInfoPasswordStore");
18✔
258
  if (encodedData.isEmpty()) {
9✔
259
    return false;
260
  }
261
  QDataStream stream(&encodedData, QIODevice::ReadOnly);
8✔
262
  dragAndDropInfoPasswordStore info;
8✔
263
  stream >> info;
8✔
264
  if (stream.status() != QDataStream::Ok) {
8✔
265
    return false;
266
  }
267

268
  QModelIndex useIndex =
269
      this->index(parent.row(), parent.column(), parent.parent());
8✔
270

271
  if (column > 0) {
8✔
272
    return false;
273
  }
274

275
  using IK = dragAndDropInfoPasswordStore::ItemKind;
276
  // you can drop a folder on a folder
277
  if (fs->fileInfo(mapToSource(useIndex)).isDir() &&
14✔
278
      info.kind == IK::Directory) {
5✔
279
    return true;
280
  }
281
  // you can drop a file on a folder
282
  if (fs->fileInfo(mapToSource(useIndex)).isDir() && info.kind == IK::File) {
8✔
283
    return true;
284
  }
285
  // you can drop a file on a file
286
  if (fs->fileInfo(mapToSource(useIndex)).isFile() && info.kind == IK::File) {
3✔
287
    return true;
288
  }
289

290
  return false;
291
}
8✔
292

293
/**
294
 * @brief StoreModel::dropMimeData
295
 * @param data
296
 * @param action
297
 * @param row
298
 * @param column
299
 * @param parent
300
 * @return
301
 */
302
auto StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
3✔
303
                              int row, int column, const QModelIndex &parent)
304
    -> bool {
305
  if (!canDropMimeData(data, action, row, column, parent)) {
3✔
306
    return false;
307
  }
308

309
  if (action == Qt::IgnoreAction) {
3✔
310
    return true;
311
  }
312

313
  if (action != Qt::MoveAction && action != Qt::CopyAction) {
3✔
314
    return false;
315
  }
316

317
  dragAndDropInfoPasswordStore info;
3✔
318
  if (!parseDropData(data, &info)) {
3✔
319
    return false;
320
  }
321

322
  return executeDropAction(info, action, parent);
3✔
323
}
324

325
auto StoreModel::parseDropData(const QMimeData *data,
3✔
326
                               dragAndDropInfoPasswordStore *outInfo) -> bool {
327
  QByteArray encodedData =
328
      data->data("application/vnd.qtpass.dragAndDropInfoPasswordStore");
6✔
329
  if (encodedData.isEmpty()) {
3✔
330
    return false;
331
  }
332

333
  QDataStream stream(&encodedData, QIODevice::ReadOnly);
3✔
334
  dragAndDropInfoPasswordStore info;
3✔
335
  stream >> info;
3✔
336
  if (stream.status() != QDataStream::Ok) {
3✔
337
    return false;
338
  }
339

340
  *outInfo = info;
341
  return true;
3✔
342
}
3✔
343

344
auto StoreModel::executeDropAction(const dragAndDropInfoPasswordStore &info,
3✔
345
                                   Qt::DropAction action,
346
                                   const QModelIndex &parent) -> bool {
347
  QModelIndex destIndex =
348
      this->index(parent.row(), parent.column(), parent.parent());
3✔
349
  QFileInfo destFileinfo = fs->fileInfo(mapToSource(destIndex));
3✔
350
  QFileInfo srcFileInfo = QFileInfo(info.path);
3✔
351

352
  QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath());
3✔
353
  QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath());
6✔
354

355
  // Both endpoints must resolve inside the password store after symlink
356
  // resolution. Drop data is encoded by the dragged item but could be
357
  // crafted; canonical-path checks stop drops that would move/copy outside
358
  // the store or follow a symlink out (e.g. a symlink within the store
359
  // pointing at /etc).
360
  const QString storeRoot = QtPassSettings::getPassStore();
6✔
361
  if (!PathValidator::isPathInStore(storeRoot, cleanedSrc) ||
3✔
NEW
362
      !PathValidator::isPathInStore(storeRoot, cleanedDest)) {
×
363
    qWarning() << "executeDropAction: rejecting drop that escapes the store"
6✔
364
               << "(src=" << cleanedSrc << "dest=" << cleanedDest << ")";
3✔
365
    return false;
3✔
366
  }
367

368
  switch (info.kind) {
×
369
  case dragAndDropInfoPasswordStore::ItemKind::Directory: {
×
370
    // Dropping a folder onto a folder: move/copy it *into* the target.
371
    if (!destFileinfo.isDir()) {
×
372
      return false;
373
    }
374
    const QString destDir =
375
        QDir::cleanPath(QDir(cleanedDest).filePath(srcFileInfo.fileName()));
×
376
    return performDrop(cleanedSrc, destDir, action, false);
×
377
  }
378
  case dragAndDropInfoPasswordStore::ItemKind::File:
×
379
    // File onto a folder drops into it (no clash); file onto an existing
380
    // file asks before overwriting.
381
    if (destFileinfo.isDir()) {
×
382
      return performDrop(cleanedSrc, cleanedDest, action, false);
×
383
    }
384
    return performDrop(
×
385
        cleanedSrc, cleanedDest, action,
386
        QMessageBox::question(
×
387
            qobject_cast<QWidget *>(QObject::parent()), tr("Force overwrite?"),
×
388
            tr("overwrite %1 with %2?").arg(cleanedDest, cleanedSrc),
×
389
            QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes);
390
  default:
391
    qWarning() << "executeDropAction: unexpected ItemKind, ignoring drop";
×
392
    return false;
×
393
  }
394
}
3✔
395

396
auto StoreModel::performDrop(const QString &cleanedSrc,
×
397
                             const QString &cleanedDest, Qt::DropAction action,
398
                             bool force) -> bool {
399
  if (action == Qt::MoveAction) {
×
400
    QtPassSettings::getPass()->Move(cleanedSrc, cleanedDest, force);
×
401
  } else if (action == Qt::CopyAction) {
×
402
    QtPassSettings::getPass()->Copy(cleanedSrc, cleanedDest, force);
×
403
  }
404
  return true;
×
405
}
406

407
/**
408
 * @brief StoreModel::lessThan
409
 * @param source_left
410
 * @param source_right
411
 * @return
412
 */
413
auto StoreModel::lessThan(const QModelIndex &source_left,
2✔
414
                          const QModelIndex &source_right) const -> bool {
415
/* matches logic in QFileSystemModelSorter::compareNodes() */
416
#ifndef Q_OS_MAC
417
  if (fs && (source_left.column() == 0 || source_left.column() == 1)) {
2✔
418
    bool leftD = fs->isDir(source_left);
2✔
419
    bool rightD = fs->isDir(source_right);
2✔
420

421
    if (leftD ^ rightD) {
2✔
422
      return leftD;
423
    }
424
  }
425
#endif
426

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

© 2026 Coveralls, Inc