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

IJHack / QtPass / 28647196804

03 Jul 2026 08:04AM UTC coverage: 56.728% (+0.3%) from 56.397%
28647196804

push

github

web-flow
fix: correct ImitatePass git re-encryption recipients, CWD, and broken copy (#1604)

* fix: correct ImitatePass git re-encryption recipients, CWD, and broken copy

Three defects in the ImitatePass git/gpg re-encryption path:

- verifyGpgIdForDir() returned early on a .gpg-id cache hit without recomputing
  gpgId, so it kept the previously processed directory's recipient list. Files
  under a directory whose .gpg-id was already verified could then be
  re-encrypted to the WRONG recipients. Now the signature is verified once per
  .gpg-id but the recipient list is always refreshed for the current file.

- createBackupCommit() and reencryptSingleFile() ran git via
  Executor::executeBlocking(), which sets no working directory, so
  `git status`/`add`/`commit` executed in QtPass's launch directory instead of
  the store. Depending on where QtPass was started this aborted all
  re-encryption ("not a git repository") or committed an unrelated repository.
  They now pass `-C <store>`.

- ImitatePass::Copy() in git mode invoked `git cp`, which is not a git
  subcommand, so drag-and-drop copy silently produced nothing. Copy now uses a
  filesystem copy (synchronous, so the destination exists before
  re-encryption) and stages the new path with `git add`.

Adds tst_integration::imitatePass_gitCopyAndShow, which copies an entry in git
mode and verifies the destination exists and decrypts to the original content
(fails without the copy fix).

Deferred to follow-ups: Move does not re-encrypt for the destination folder's
.gpg-id (needs async ordering around `git mv`), and the actualKeys/gpgId
recipient-format comparison that defeats the "skip already-correct files"
optimization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* fix: surface Copy failure and use QFile::remove for the destination

Address review feedback on ImitatePass::Copy: QFile::copy returns false witho... (continued)

10 of 16 new or added lines in 1 file covered. (62.5%)

3811 of 6718 relevant lines covered (56.73%)

30.78 hits per line

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

57.7
/src/imitatepass.cpp
1
// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "imitatepass.h"
4
#include "executor.h"
5
#include "util.h"
6
#include <QDirIterator>
7
#include <QElapsedTimer>
8
#include <QPointer>
9
#include <QRegularExpression>
10
#include <QThread>
11
#include <utility>
12

13
#ifdef QT_DEBUG
14
#include "debughelper.h"
15
#endif
16

17
using Enums::CLIPBOARD_ALWAYS;
18
using Enums::CLIPBOARD_NEVER;
19
using Enums::CLIPBOARD_ON_DEMAND;
20
using Enums::GIT_ADD;
21
using Enums::GIT_COMMIT;
22
using Enums::GIT_COPY;
23
using Enums::GIT_INIT;
24
using Enums::GIT_MOVE;
25
using Enums::GIT_PULL;
26
using Enums::GIT_PUSH;
27
using Enums::GIT_RM;
28
using Enums::GPG_GENKEYS;
29
using Enums::INVALID;
30
using Enums::PASS_COPY;
31
using Enums::PASS_GREP;
32
using Enums::PASS_INIT;
33
using Enums::PASS_INSERT;
34
using Enums::PASS_MOVE;
35
using Enums::PASS_OTP_GENERATE;
36
using Enums::PASS_REMOVE;
37
using Enums::PASS_SHOW;
38
using Enums::PROCESS_COUNT;
39

40
/**
41
 * @brief ImitatePass::ImitatePass for situations when pass is not available
42
 * we imitate the behavior of pass https://www.passwordstore.org/
43
 */
44
ImitatePass::ImitatePass() = default;
78✔
45

46
ImitatePass::~ImitatePass() {
41✔
47
  static constexpr int kGrepThreadTimeoutMs = 5000;
48
  for (QThread *t : std::as_const(m_grepThreads))
39✔
49
    if (t && t->isRunning())
×
50
      t->requestInterruption();
×
51
  QElapsedTimer elapsed;
39✔
52
  elapsed.start();
39✔
53
  for (QThread *t : std::as_const(m_grepThreads)) {
39✔
54
    if (t && t->isRunning()) {
×
55
      const int remaining =
56
          kGrepThreadTimeoutMs - static_cast<int>(elapsed.elapsed());
×
57
      if (remaining > 0)
×
58
        t->wait(remaining);
×
59
    }
60
  }
61
}
41✔
62

63
auto ImitatePass::translatePathForWsl(const QString &path,
59✔
64
                                      const QString &exe) const -> QString {
65
  QString normalizedPath = QDir::cleanPath(path);
59✔
66
  if (!exe.startsWith(QStringLiteral("wsl ")))
118✔
67
    return normalizedPath;
68
  QString wslPath;
×
69
  const int rc = Executor::executeBlocking(
×
70
      QStringLiteral("wsl"), {QStringLiteral("wslpath"), normalizedPath},
×
71
      &wslPath);
72
  const QString translated = wslPath.trimmed();
73
  return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
×
74
}
75

76
auto ImitatePass::pgit(const QString &path) const -> QString {
12✔
77
  return translatePathForWsl(path, m_settings.gitExecutable);
12✔
78
}
79

80
auto ImitatePass::pgpg(const QString &path) const -> QString {
47✔
81
  return translatePathForWsl(path, m_settings.gpgExecutable);
47✔
82
}
83

84
/**
85
 * @brief ImitatePass::GitInit git init wrapper
86
 */
87
void ImitatePass::GitInit() {
×
88
  executeGit(GIT_INIT, {"init", pgit(m_settings.passStore)});
×
89
}
×
90

91
/**
92
 * @brief ImitatePass::GitPull git pull wrapper
93
 */
94
void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
×
95

96
/**
97
 * @brief ImitatePass::GitPull_b git pull wrapper
98
 */
99
void ImitatePass::GitPull_b() {
×
100
  Executor::executeBlocking(m_settings.gitExecutable, {"pull"});
×
101
}
×
102

103
/**
104
 * @brief ImitatePass::GitPush git push wrapper
105
 */
106
void ImitatePass::GitPush() {
×
107
  if (m_settings.useGit) {
×
108
    executeGit(GIT_PUSH, {"push"});
×
109
  }
110
}
×
111

112
/**
113
 * @brief ImitatePass::Show shows content of file
114
 */
115
void ImitatePass::Show(QString file) {
11✔
116
  file = m_settings.passStore + file + ".gpg";
11✔
117
  QStringList args = {"-d",      "--quiet",     "--yes",   "--no-encrypt-to",
118
                      "--batch", "--use-agent", pgpg(file)};
88✔
119
  executeGpg(PASS_SHOW, args);
22✔
120
}
22✔
121

122
/**
123
 * @brief ImitatePass::OtpGenerate generates an otp code
124
 */
125
void ImitatePass::OtpGenerate(QString file) {
×
126
#ifdef QT_DEBUG
127
  dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
128
               file;
129
#else
130
  Q_UNUSED(file)
131
#endif
132
}
×
133

134
/**
135
 * @brief ImitatePass::Insert create new file with encrypted content
136
 *
137
 * @param file      file to be created
138
 * @param newValue  value to be stored in file
139
 * @param overwrite whether to overwrite existing file
140
 */
141
void ImitatePass::Insert(QString file, QString newValue, bool overwrite) {
20✔
142
  file = file + ".gpg";
20✔
143
  QString gpgIdPath = Pass::getGpgIdPath(file, m_settings.passStore);
20✔
144
  if (!verifyGpgIdFile(gpgIdPath)) {
20✔
145
    emit critical(tr("Check .gpg-id file signature!"),
×
146
                  tr("Signature for %1 is invalid.").arg(gpgIdPath));
×
147
    return;
×
148
  }
149
  transactionHelper trans(this, PASS_INSERT);
20✔
150
  QStringList recipients = Pass::getRecipientList(file, m_settings.passStore);
20✔
151
  if (recipients.isEmpty()) {
20✔
152
    // Already emit critical signal to notify user of error - no need to throw
153
    emit critical(tr("Can not edit"),
×
154
                  tr("Could not read encryption key to use, .gpg-id "
×
155
                     "file missing or invalid."));
156
    return;
157
  }
158
  QStringList args = {"--batch", "--status-fd", "2",
159
                      "-eq",     "--output",    pgpg(file)};
140✔
160
  for (auto &r : recipients) {
40✔
161
    args.append("-r");
40✔
162
    args.append(r);
163
  }
164
  if (overwrite) {
20✔
165
    args.append("--yes");
2✔
166
  }
167
  args.append("-");
40✔
168
  executeGpg(PASS_INSERT, args, newValue);
20✔
169
  if (!m_settings.useWebDav && m_settings.useGit) {
20✔
170
    // Git is used when enabled - this is the standard pass workflow
171
    if (!overwrite) {
2✔
172
      executeGit(GIT_ADD, {"add", pgit(file)});
8✔
173
    }
174
    QString path = QDir(m_settings.passStore).relativeFilePath(file);
2✔
175
    path.replace(Util::endsWithGpg(), "");
2✔
176
    QString msg =
177
        QString(overwrite ? "Edit" : "Add") + " for " + path + " using QtPass.";
4✔
178
    gitCommit(file, msg);
2✔
179
  }
180
}
22✔
181

182
/**
183
 * @brief ImitatePass::gitCommit commit a file to git with an appropriate commit
184
 * message
185
 * @param file
186
 * @param msg
187
 */
188
void ImitatePass::gitCommit(const QString &file, const QString &msg) {
3✔
189
  if (file.isEmpty()) {
3✔
190
    executeGit(GIT_COMMIT, {"commit", "-m", msg});
5✔
191
  } else {
192
    executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
14✔
193
  }
194
}
8✔
195

196
/**
197
 * @brief ImitatePass::Remove custom implementation of "pass remove"
198
 */
199
void ImitatePass::Remove(QString file, bool isDir) {
1✔
200
  file = m_settings.passStore + file;
1✔
201
  transactionHelper trans(this, PASS_REMOVE);
1✔
202
  if (!isDir) {
1✔
203
    file += ".gpg";
1✔
204
  }
205
  if (m_settings.useGit) {
1✔
206
    executeGit(GIT_RM, {"rm", (isDir ? "-rf" : "-f"), pgit(file)});
×
207
    // Normalize path the same way as add/edit operations
208
    QString path = QDir(m_settings.passStore).relativeFilePath(file);
×
209
    path.replace(Util::endsWithGpg(), "");
×
210
    gitCommit(file, "Remove for " + path + " using QtPass.");
×
211
  } else {
212
    if (isDir) {
1✔
213
      QDir dir(file);
×
214
      dir.removeRecursively();
×
215
    } else {
×
216
      QFile(file).remove();
1✔
217
    }
218
  }
219
}
1✔
220

221
/**
222
 * @brief ImitatePass::Init initialize pass repository
223
 *
224
 * @param path      path in which new password-store will be created
225
 * @param users     list of users who shall be able to decrypt passwords in
226
 * path
227
 */
228
auto ImitatePass::checkSigningKeys(const QStringList &signingKeys) -> bool {
×
229
  QString out;
×
230
  QStringList args =
231
      QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys;
×
232
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args, &out);
×
233
  if (result != 0) {
×
234
#ifdef QT_DEBUG
235
    dbg() << "GPG list-secret-keys failed with code:" << result;
236
#endif
237
    return false;
238
  }
239
  for (auto &key : signingKeys) {
×
240
    if (out.contains("[GNUPG:] KEY_CONSIDERED " + key)) {
×
241
      return true;
242
    }
243
  }
244
  return false;
245
}
×
246

247
/**
248
 * @brief Writes the selected users' GPG key IDs to a .gpg-id file.
249
 * @details Opens the specified file for writing, stores the key ID of each
250
 * enabled user on a separate line, and warns if none of the selected users has
251
 * a secret key available.
252
 *
253
 * @param QString &gpgIdFile - Path to the .gpg-id file to be written.
254
 * @param QList<UserInfo> &users - List of users to evaluate and write to the
255
 * file.
256
 * @return void - This function does not return a value.
257
 *
258
 */
259
void ImitatePass::writeGpgIdFile(const QString &gpgIdFile,
1✔
260
                                 const QList<UserInfo> &users) {
261
  QFile gpgId(gpgIdFile);
1✔
262
  if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
1✔
263
    emit critical(tr("Cannot update"),
×
264
                  tr("Failed to open .gpg-id for writing."));
×
265
    return;
266
  }
267
  bool secret_selected = false;
268
  for (const UserInfo &user : users) {
2✔
269
    if (user.enabled) {
1✔
270
      gpgId.write((user.key_id + "\n").toUtf8());
2✔
271
      secret_selected |= user.have_secret;
1✔
272
    }
273
  }
274
  gpgId.close();
1✔
275
  // Lock the file to owner-only access. The .gpg-id leaks which keys the
276
  // store is encrypted to; while the typical ~/.password-store is 0700,
277
  // users may relocate the store onto NFS/SMB/USB where the parent dir
278
  // perms are more lax. On platforms where setPermissions is a no-op
279
  // (Windows), this is silently best-effort.
280
  QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
1✔
281
  if (!secret_selected) {
1✔
282
    emit critical(
×
283
        tr("Check selected users!"),
×
284
        tr("None of the selected keys have a secret key available.\n"
×
285
           "You will not be able to decrypt any newly added passwords!"));
286
  }
287
}
1✔
288

289
/**
290
 * @brief Signs a GPG ID file and verifies its signature.
291
 * @example
292
 * bool result = ImitatePass::signGpgIdFile(gpgIdFile, signingKeys);
293
 * std::cout << result << std::endl; // Expected output: true if signing and
294
 * verification succeed
295
 *
296
 * @param QString &gpgIdFile - Path to the .gpg-id file to be signed.
297
 * @param QStringList &signingKeys - List of signing keys; only the first key is
298
 * used.
299
 * @return bool - True if the file was signed and its signature verified
300
 * successfully; otherwise false.
301
 */
302
auto ImitatePass::signGpgIdFile(const QString &gpgIdFile,
×
303
                                const QStringList &signingKeys) -> bool {
304
  QStringList args;
×
305
  // Use only the first signing key; multiple --default-key options would
306
  // override each other and only the last one would take effect.
307
  if (!signingKeys.isEmpty()) {
×
308
#ifdef QT_DEBUG
309
    if (signingKeys.size() > 1) {
310
      dbg() << "Multiple signing keys configured; using only the first key:"
311
            << signingKeys.first();
312
    }
313
#endif
314
    args.append(QStringList{"--default-key", signingKeys.first()});
×
315
  }
316
  args.append(QStringList{"--yes", "--detach-sign", gpgIdFile});
×
317
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args);
×
318
  if (result != 0) {
×
319
#ifdef QT_DEBUG
320
    dbg() << "GPG signing failed with code:" << result;
321
#endif
322
    emit critical(tr("GPG signing failed!"),
×
323
                  tr("Failed to sign %1.").arg(gpgIdFile));
×
324
    return false;
×
325
  }
326
  if (!verifyGpgIdFile(gpgIdFile)) {
×
327
    emit critical(tr("Check .gpg-id file signature!"),
×
328
                  tr("Signature for %1 is invalid.").arg(gpgIdFile));
×
329
    return false;
×
330
  }
331
  return true;
332
}
×
333

334
/**
335
 * @brief Adds a GPG ID file and optionally its signature file to git, then
336
 * creates corresponding commit(s).
337
 * @example
338
 * void result = ImitatePass::gitAddGpgId(gpgIdFile, gpgIdSigFile, true, true);
339
 *
340
 * @param const QString &gpgIdFile - Path to the GPG ID file to add and commit.
341
 * @param const QString &gpgIdSigFile - Path to the signature file associated
342
 * with the GPG ID file.
343
 * @param bool addFile - Whether to stage and commit the GPG ID file.
344
 * @param bool addSigFile - Whether to stage and commit the signature file.
345
 * @return void - This function does not return a value.
346
 */
347
void ImitatePass::gitAddGpgId(const QString &gpgIdFile,
×
348
                              const QString &gpgIdSigFile, bool addFile,
349
                              bool addSigFile) {
350
  if (addFile) {
×
351
    executeGit(GIT_ADD, {"add", pgit(gpgIdFile)});
×
352
  }
353
  QString commitPath = gpgIdFile;
354
  commitPath.replace(Util::endsWithGpg(), "");
×
355
  gitCommit(gpgIdFile, "Added " + commitPath + " using QtPass.");
×
356
  if (!addSigFile) {
×
357
    return;
358
  }
359
  executeGit(GIT_ADD, {"add", pgit(gpgIdSigFile)});
×
360
  commitPath = gpgIdSigFile;
×
361
  commitPath.replace(QRegularExpression("\\.gpg$"), "");
×
362
  gitCommit(gpgIdSigFile, "Added " + commitPath + " using QtPass.");
×
363
}
×
364

365
/**
366
 * @brief Initializes the pass entry by writing and optionally signing the GPG
367
 * ID files.
368
 *
369
 * @example
370
 * void result = ImitatePass::Init(path, users);
371
 *
372
 * @param QString path - Base path for the pass entry where ".gpg-id" and
373
 * optional signature files are created.
374
 * @param const QList<UserInfo> &users - List of users whose keys are written
375
 * into the GPG ID file.
376
 * @return void - No return value.
377
 */
378
void ImitatePass::Init(QString path, const QList<UserInfo> &users) {
×
379
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
380
  QStringList signingKeys =
381
      m_settings.passSigningKey.split(" ", Qt::SkipEmptyParts);
×
382
#else
383
  QStringList signingKeys =
384
      m_settings.passSigningKey.split(" ", QString::SkipEmptyParts);
385
#endif
386
  QString gpgIdSigFile = path + ".gpg-id.sig";
×
387
  bool addSigFile = false;
388
  if (!signingKeys.isEmpty()) {
×
389
    if (!checkSigningKeys(signingKeys)) {
×
390
      emit critical(tr("No signing key!"),
×
391
                    tr("None of the secret signing keys is available.\n"
×
392
                       "You will not be able to change the user list!"));
393
      return;
×
394
    }
395
    QFileInfo checkFile(gpgIdSigFile);
×
396
    if (!checkFile.exists() || !checkFile.isFile()) {
×
397
      addSigFile = true;
398
    }
399
  }
×
400

401
  QString gpgIdFile = path + ".gpg-id";
×
402
  bool addFile = false;
403
  transactionHelper trans(this, PASS_INIT);
×
404
  if (m_settings.addGPGId) {
×
405
    QFileInfo checkFile(gpgIdFile);
×
406
    if (!checkFile.exists() || !checkFile.isFile()) {
×
407
      addFile = true;
408
    }
409
  }
×
410
  writeGpgIdFile(gpgIdFile, users);
×
411

412
  if (!signingKeys.isEmpty()) {
×
413
    if (!signGpgIdFile(gpgIdFile, signingKeys)) {
×
414
      return;
415
    }
416
  }
417

418
  if (!m_settings.useWebDav && m_settings.useGit &&
×
419
      !m_settings.gitExecutable.isEmpty()) {
420
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
421
  }
422
  reencryptPath(path);
×
423
}
424

425
/**
426
 * @brief ImitatePass::verifyGpgIdFile verify detached gpgid file signature.
427
 * @param file which gpgid file.
428
 * @return was verification successful?
429
 */
430
auto ImitatePass::verifyGpgIdFile(const QString &file) -> bool {
22✔
431
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
432
  QStringList signingKeys =
433
      m_settings.passSigningKey.split(" ", Qt::SkipEmptyParts);
44✔
434
#else
435
  QStringList signingKeys =
436
      m_settings.passSigningKey.split(" ", QString::SkipEmptyParts);
437
#endif
438
  if (signingKeys.isEmpty()) {
22✔
439
    return true;
440
  }
441
  QString out;
×
442
  QStringList args =
443
      QStringList{"--verify", "--status-fd=1", pgpg(file) + ".sig", pgpg(file)};
×
444
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args, &out);
×
445
  if (result != 0) {
×
446
#ifdef QT_DEBUG
447
    dbg() << "GPG verify failed with code:" << result;
448
#endif
449
    return false;
450
  }
451
  QRegularExpression re(
452
      R"(^\[GNUPG:\] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\r?$)",
453
      QRegularExpression::MultilineOption);
×
454
  QRegularExpressionMatch m = re.match(out);
×
455
  if (!m.hasMatch()) {
×
456
    return false;
457
  }
458
  QStringList fingerprints = m.capturedTexts();
×
459
  fingerprints.removeFirst();
×
460
  for (auto &key : signingKeys) {
×
461
    if (fingerprints.contains(key)) {
×
462
      return true;
×
463
    }
464
  }
465
  return false;
×
466
}
×
467

468
/**
469
 * @brief ImitatePass::reencryptPath reencrypt all files under the chosen
470
 * directory
471
 *
472
 * This is still quite experimental..
473
 * @param dir
474
 */
475
auto ImitatePass::verifyGpgIdForDir(const QString &file,
2✔
476
                                    QStringList &gpgIdFilesVerified,
477
                                    QStringList &gpgId) -> bool {
478
  QString gpgIdPath = Pass::getGpgIdPath(file, m_settings.passStore);
2✔
479
  // Verify each .gpg-id signature only once, but always refresh the recipient
480
  // list for the current file: a cache hit means the signature was already
481
  // checked, not that gpgId still holds this directory's recipients — it may
482
  // carry a different directory's list, which would re-encrypt to wrong keys.
483
  if (!gpgIdFilesVerified.contains(gpgIdPath)) {
2✔
484
    if (!verifyGpgIdFile(gpgIdPath)) {
2✔
NEW
485
      emit critical(tr("Check .gpg-id file signature!"),
×
NEW
486
                    tr("Signature for %1 is invalid.").arg(gpgIdPath));
×
NEW
487
      return false;
×
488
    }
489
    gpgIdFilesVerified.append(gpgIdPath);
490
  }
491
  gpgId = getRecipientList(file, m_settings.passStore);
4✔
492
  gpgId.sort();
493
  return true;
494
}
495

496
/**
497
 * @brief Extracts and returns a sorted list of valid key IDs from a GPG key
498
 * listing file.
499
 * @example
500
 * QStringList result = ImitatePass::getKeysFromFile(fileName);
501
 * std::cout << result.join(", ").toStdString() << std::endl;
502
 *
503
 * @param fileName - Path to the file used to query and parse GPG key
504
 * information.
505
 * @return QStringList - A sorted list of 16-character key IDs found in the
506
 * file.
507
 */
508
auto ImitatePass::getKeysFromFile(const QString &fileName) -> QStringList {
4✔
509
  QStringList args = {
510
      "-v",          "--no-secmem-warning", "--no-permission-warning",
511
      "--list-only", "--keyid-format=long", pgpg(fileName)};
28✔
512
  QString keys;
4✔
513
  QString err;
4✔
514
  const int result =
515
      Executor::executeBlocking(m_settings.gpgExecutable, args, &keys, &err);
4✔
516
  if (result != 0) {
4✔
517
    return {};
×
518
  }
519
  QStringList actualKeys;
4✔
520
  keys += err;
521
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
522
  QStringList key = keys.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
4✔
523
#else
524
  QStringList key = keys.split(Util::newLinesRegex(), QString::SkipEmptyParts);
525
#endif
526
  QListIterator<QString> itr(key);
527
  while (itr.hasNext()) {
16✔
528
    QString current = itr.next();
529
    QStringList cur = current.split(" ");
24✔
530
    if (cur.length() > 4) {
12✔
531
      QString actualKey = cur.takeAt(4);
8✔
532
      if (actualKey.length() == 16) {
8✔
533
        actualKeys << actualKey;
534
      }
535
    }
536
  }
537
  actualKeys.sort();
538
  return actualKeys;
539
}
4✔
540

541
/**
542
 * @brief Re-encrypts a single encrypted file for a new set of recipients.
543
 * @example
544
 * bool result = ImitatePass::reencryptSingleFile(fileName, recipients);
545
 * std::cout << result << std::endl; // Expected output: true on success, false
546
 * on failure
547
 *
548
 * @param const QString &fileName - Path to the encrypted file to re-encrypt.
549
 * @param const QStringList &recipients - List of recipient keys to encrypt the
550
 * file to.
551
 * @return bool - True if the file was successfully decrypted, re-encrypted,
552
 * verified, and replaced; otherwise false.
553
 */
554
auto ImitatePass::reencryptSingleFile(const QString &fileName,
4✔
555
                                      const QStringList &recipients) -> bool {
556
#ifdef QT_DEBUG
557
  dbg() << "reencrypt " << fileName << " for " << recipients;
558
#endif
559
  QString local_lastDecrypt;
4✔
560
  QStringList args = {
561
      "-d",      "--quiet",     "--yes",       "--no-encrypt-to",
562
      "--batch", "--use-agent", pgpg(fileName)};
32✔
563
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args,
4✔
564
                                         &local_lastDecrypt);
565

566
  if (result != 0 || local_lastDecrypt.isEmpty()) {
4✔
567
#ifdef QT_DEBUG
568
    dbg() << "Decrypt error on re-encrypt for:" << fileName;
569
#endif
570
    return false;
571
  }
572

573
  if (local_lastDecrypt.right(1) != "\n") {
8✔
574
    local_lastDecrypt += "\n";
×
575
  }
576

577
  // Use passed recipients instead of re-reading from file
578
  if (recipients.isEmpty()) {
4✔
579
    emit critical(tr("Can not edit"),
×
580
                  tr("Could not read encryption key to use, .gpg-id "
×
581
                     "file missing or invalid."));
582
    return false;
×
583
  }
584

585
  // Encrypt to temporary file for atomic replacement
586
  QString tempPath = fileName + ".reencrypt.tmp";
4✔
587
  args = QStringList{"--yes", "--batch", "-eq", "--output", pgpg(tempPath)};
28✔
588
  for (const auto &i : recipients) {
8✔
589
    args.append("-r");
8✔
590
    args.append(i);
591
  }
592
  args.append("-");
4✔
593
  result = Executor::executeBlocking(m_settings.gpgExecutable, args,
4✔
594
                                     local_lastDecrypt);
595

596
  if (result != 0) {
4✔
597
#ifdef QT_DEBUG
598
    dbg() << "Encrypt error on re-encrypt for:" << fileName;
599
#endif
600
    QFile::remove(tempPath);
×
601
    return false;
602
  }
603

604
  // Verify encryption worked by attempting to decrypt the temp file
605
  QString verifyOutput;
4✔
606
  args = QStringList{"-d", "--quiet", "--batch", "--use-agent", pgpg(tempPath)};
28✔
607
  result =
608
      Executor::executeBlocking(m_settings.gpgExecutable, args, &verifyOutput);
4✔
609
  if (result != 0 || verifyOutput.isEmpty()) {
4✔
610
#ifdef QT_DEBUG
611
    dbg() << "Verification failed for:" << tempPath;
612
#endif
613
    QFile::remove(tempPath);
×
614
    return false;
615
  }
616
  // Verify content matches original decrypted content (defense in depth)
617
  if (verifyOutput.trimmed() != local_lastDecrypt.trimmed()) {
4✔
618
#ifdef QT_DEBUG
619
    dbg() << "Verification content mismatch for:" << tempPath;
620
#endif
621
    QFile::remove(tempPath);
×
622
    return false;
623
  }
624

625
  // Atomic replace with backup: rename original to .bak, rename temp to
626
  // original, then remove backup
627
  QString backupPath = fileName + ".reencrypt.bak";
4✔
628
  if (!QFile::rename(fileName, backupPath)) {
4✔
629
#ifdef QT_DEBUG
630
    dbg() << "Failed to backup original file:" << fileName;
631
#endif
632
    QFile::remove(tempPath);
×
633
    return false;
634
  }
635
  if (!QFile::rename(tempPath, fileName)) {
4✔
636
#ifdef QT_DEBUG
637
    dbg() << "Failed to rename temp file to:" << fileName;
638
#endif
639
    // Restore backup and clean up temp file
640
    QFile::rename(backupPath, fileName);
×
641
    QFile::remove(tempPath);
×
642
    emit critical(
×
643
        tr("Re-encryption failed"),
×
644
        tr("Failed to replace %1. Original has been restored.").arg(fileName));
×
645
    return false;
×
646
  }
647
  // Success - remove backup
648
  QFile::remove(backupPath);
4✔
649

650
  if (!m_settings.useWebDav && m_settings.useGit) {
4✔
651
    // -C the store so git runs there rather than in QtPass's launch directory
652
    // (executeBlocking sets no working directory).
653
    const QString store = pgit(m_settings.passStore);
2✔
654
    Executor::executeBlocking(m_settings.gitExecutable,
12✔
655
                              {"-C", store, "add", pgit(fileName)});
656
    QString path = QDir(m_settings.passStore).relativeFilePath(fileName);
2✔
657
    path.replace(Util::endsWithGpg(), "");
4✔
658
    Executor::executeBlocking(m_settings.gitExecutable,
18✔
659
                              {"-C", store, "commit", pgit(fileName), "-m",
660
                               "Re-encrypt for " + path + " using QtPass."});
4✔
661
  }
662

663
  return true;
664
}
22✔
665

666
/**
667
 * @brief Create git backup commit before re-encryption.
668
 * @return true if backup created or not needed, false if backup failed.
669
 */
670
auto ImitatePass::createBackupCommit() -> bool {
2✔
671
  if (!m_settings.useGit || m_settings.gitExecutable.isEmpty()) {
2✔
672
    return true;
673
  }
674
  emit statusMsg(tr("Creating backup commit"), 2000);
2✔
675
  const QString git = m_settings.gitExecutable;
676
  // Run git in the password store: executeBlocking does not set a working
677
  // directory, so without -C these commands would run in QtPass's launch
678
  // directory and either fail or operate on an unrelated repository.
679
  const QString store = pgit(m_settings.passStore);
1✔
680
  QString statusOut;
1✔
681
  if (Executor::executeBlocking(git, {"-C", store, "status", "--porcelain"},
6✔
682
                                &statusOut) != 0) {
683
    emit critical(
×
684
        tr("Backup commit failed"),
×
685
        tr("Could not inspect git status. Re-encryption was aborted."));
×
686
    return false;
×
687
  }
688
  if (!statusOut.trimmed().isEmpty()) {
1✔
689
    if (Executor::executeBlocking(git, {"-C", store, "add", "-A"}) != 0 ||
7✔
690
        Executor::executeBlocking(git, {"-C", store, "commit", "-m",
9✔
691
                                        "Backup before re-encryption"}) != 0) {
692
      emit critical(tr("Backup commit failed"),
×
693
                    tr("Re-encryption was aborted because a git backup could "
×
694
                       "not be created."));
695
      return false;
×
696
    }
697
  }
698
  return true;
699
}
6✔
700

701
/**
702
 * @brief Re-encrypts all `.gpg` files under the given directory using the
703
 *        verified GPG key configuration for each folder.
704
 *
705
 * This method optionally pulls the latest changes before starting, creates a
706
 * backup commit, verifies `.gpg-id` files per directory, and re-encrypts files
707
 * whose current recipients do not match the expected keys. It emits progress,
708
 * status, and error signals throughout the process, and optionally pushes the
709
 * updated password-store when finished.
710
 *
711
 * @param dir - Root directory to scan recursively for `.gpg` files.
712
 * @return void
713
 */
714
void ImitatePass::reencryptPath(const QString &dir) {
2✔
715
  emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
4✔
716
  emit startReencryptPath();
2✔
717
  if (m_settings.autoPull && m_settings.useGit) {
2✔
718
    emit statusMsg(tr("Updating password-store"), 2000);
×
719
    GitPull_b();
×
720
  }
721

722
  // Create backup before re-encryption - abort if it fails
723
  if (!createBackupCommit()) {
2✔
724
    emit endReencryptPath();
×
725
    return;
×
726
  }
727

728
  QDir currentDir;
4✔
729
  QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
4✔
730
                        QDirIterator::Subdirectories);
2✔
731
  QStringList gpgIdFilesVerified;
2✔
732
  QStringList gpgId;
2✔
733
  int successCount = 0;
734
  int failCount = 0;
735
  while (gpgFiles.hasNext()) {
6✔
736
    QString fileName = gpgFiles.next();
4✔
737
    if (gpgFiles.fileInfo().path() != currentDir.path()) {
8✔
738
      if (!verifyGpgIdForDir(fileName, gpgIdFilesVerified, gpgId)) {
2✔
739
        emit endReencryptPath();
×
740
        return;
741
      }
742
      if (gpgId.isEmpty() && !gpgIdFilesVerified.isEmpty()) {
2✔
743
        emit critical(tr("GPG ID verification failed"),
×
744
                      tr("Could not verify .gpg-id for directory."));
×
745
        emit endReencryptPath();
×
746
        return;
747
      }
748
      currentDir = QDir(gpgFiles.fileInfo().path());
4✔
749
    }
750
    QStringList actualKeys = getKeysFromFile(fileName);
4✔
751
    if (actualKeys != gpgId) {
4✔
752
      if (reencryptSingleFile(fileName, gpgId)) {
4✔
753
        successCount++;
4✔
754
      } else {
755
        failCount++;
×
756
        emit critical(tr("Re-encryption failed"),
×
757
                      tr("Failed to re-encrypt %1").arg(fileName));
×
758
      }
759
    }
760
  }
761

762
  if (failCount > 0) {
2✔
763
    emit statusMsg(tr("Re-encryption completed: %1 succeeded, %2 failed")
×
764
                       .arg(successCount)
×
765
                       .arg(failCount),
×
766
                   5000);
767
  } else {
768
    emit statusMsg(
2✔
769
        tr("Re-encryption completed: %1 files re-encrypted").arg(successCount),
4✔
770
        3000);
771
  }
772

773
  if (m_settings.autoPush && m_settings.useGit) {
2✔
774
    emit statusMsg(tr("Updating password-store"), 2000);
×
775
    GitPush();
×
776
  }
777
  emit endReencryptPath();
2✔
778
}
2✔
779

780
/**
781
 * @brief Resolves the final destination path for moving a file or directory,
782
 * applying .gpg handling for files.
783
 * @example
784
 * QString result = ImitatePass::resolveMoveDestination("/tmp/source.txt",
785
 * "/backup", false); std::cout << result.toStdString() << std::endl; //
786
 * Expected output sample: "/backup/source.txt.gpg"
787
 *
788
 * @param src - Source path to the file or directory.
789
 * @param dest - Requested destination path, which may be a file or directory.
790
 * @param force - When true, allows overwriting an existing destination file.
791
 * @return QString - Resolved destination path, or an empty QString if the
792
 * source/destination is invalid or conflicts occur.
793
 */
794
auto ImitatePass::resolveMoveDestination(const QString &src,
6✔
795
                                         const QString &dest, bool force)
796
    -> QString {
797
  QFileInfo srcFileInfo(src);
6✔
798
  QFileInfo destFileInfo(dest);
6✔
799
  QString destFile;
6✔
800
  QString srcFileBaseName = srcFileInfo.fileName();
6✔
801

802
  if (srcFileInfo.isFile()) {
6✔
803
    if (destFileInfo.isFile()) {
5✔
804
      if (!force) {
2✔
805
#ifdef QT_DEBUG
806
        dbg() << "Destination file already exists";
807
#endif
808
        return {};
809
      }
810
      destFile = dest;
1✔
811
    } else if (destFileInfo.isDir()) {
3✔
812
      destFile = QDir(dest).filePath(srcFileBaseName);
2✔
813
    } else {
814
      destFile = dest;
2✔
815
    }
816

817
    if (destFile.endsWith(".gpg", Qt::CaseInsensitive)) {
8✔
818
      destFile.chop(4);
4✔
819
    }
820
    destFile.append(".gpg");
4✔
821
  } else if (srcFileInfo.isDir()) {
1✔
822
    if (destFileInfo.isDir()) {
×
823
      destFile = QDir(dest).filePath(srcFileBaseName);
×
824
    } else if (destFileInfo.isFile()) {
×
825
#ifdef QT_DEBUG
826
      dbg() << "Destination is a file";
827
#endif
828
      return {};
829
    } else {
830
      destFile = dest;
×
831
    }
832
  } else {
833
#ifdef QT_DEBUG
834
    dbg() << "Source file does not exist";
835
#endif
836
    return {};
837
  }
838
  return destFile;
839
}
6✔
840

841
/**
842
 * @brief Moves a password store item in the Git repository and commits the
843
 * change.
844
 * @example
845
 * void result = className.executeMoveGit(src, destFile, force);
846
 *
847
 * @param const QString &src - Source path of the item to move.
848
 * @param const QString &destFile - Destination path of the item after the move.
849
 * @param bool force - Whether to force the move using Git's -f option.
850
 * @return void - This method does not return a value.
851
 */
852
void ImitatePass::executeMoveGit(const QString &src, const QString &destFile,
×
853
                                 bool force) {
854
  QStringList args;
×
855
  args << "mv";
×
856
  if (force) {
×
857
    args << "-f";
×
858
  }
859
  args << pgit(src);
×
860
  args << pgit(destFile);
×
861
  executeGit(GIT_MOVE, args);
×
862

863
  QString relSrc = QDir(m_settings.passStore).relativeFilePath(src);
×
864
  relSrc.replace(Util::endsWithGpg(), "");
×
865
  QString relDest = QDir(m_settings.passStore).relativeFilePath(destFile);
×
866
  relDest.replace(Util::endsWithGpg(), "");
×
867
  QString message = QString("Moved for %1 to %2 using QtPass.");
×
868
  message = message.arg(relSrc, relDest);
×
869
  gitCommit("", message);
×
870
}
×
871

872
/**
873
 * @brief Moves a password entry from the source path to the destination path.
874
 * @example
875
 * ImitatePass::Move(src, dest, true);
876
 *
877
 * @param const QString src - The source path or entry name to move.
878
 * @param const QString dest - The destination path or entry name.
879
 * @param const bool force - If true, overwrites an existing destination entry
880
 * when necessary.
881
 * @return void - This function does not return a value.
882
 */
883
void ImitatePass::Move(const QString src, const QString dest,
1✔
884
                       const bool force) {
885
  transactionHelper trans(this, PASS_MOVE);
1✔
886
  QString destFile = resolveMoveDestination(src, dest, force);
1✔
887
  if (destFile.isEmpty()) {
1✔
888
    return;
889
  }
890

891
#ifdef QT_DEBUG
892
  dbg() << "Move Source: " << src;
893
  dbg() << "Move Destination: " << destFile;
894
#endif
895

896
  if (m_settings.useGit) {
1✔
897
    executeMoveGit(src, destFile, force);
×
898
  } else {
899
    QDir qDir;
1✔
900
    if (force) {
1✔
901
      qDir.remove(destFile);
×
902
    }
903
    qDir.rename(src, destFile);
1✔
904
  }
1✔
905
}
906

907
/**
908
 * @brief Copies a file or directory from source to destination, optionally
909
 * forcing overwrite.
910
 * @example
911
 * void result = ImitatePass::Copy(src, dest, force);
912
 *
913
 * @param QString src - Source path to copy from.
914
 * @param QString dest - Destination path to copy to.
915
 * @param bool force - If true, overwrites the destination when it already
916
 * exists.
917
 * @return void - This function does not return a value.
918
 */
919
void ImitatePass::Copy(const QString src, const QString dest,
2✔
920
                       const bool force) {
921
  QFileInfo destFileInfo(dest);
2✔
922
  transactionHelper trans(this, PASS_COPY);
2✔
923
  if (force) {
2✔
NEW
924
    QFile::remove(dest);
×
925
  }
926
  // git has no "cp" subcommand, so copy on the filesystem in both modes and,
927
  // when using git, stage the new path afterwards. QFile::copy is synchronous,
928
  // so the destination exists before the re-encryption below runs. It fails
929
  // (without overwriting) when dest already exists, so surface that instead of
930
  // committing a copy that never happened.
931
  if (!QFile::copy(src, dest)) {
2✔
NEW
932
    emit critical(tr("Copy failed"),
×
NEW
933
                  tr("Could not copy %1 to %2.").arg(src, dest));
×
934
    return;
935
  }
936
  if (m_settings.useGit) {
2✔
937
    executeGit(GIT_COPY, {"add", pgit(dest)});
4✔
938
    QString message = QString("Copied from %1 to %2 using QtPass.");
1✔
939
    message = message.arg(src, dest);
1✔
940
    gitCommit("", message);
2✔
941
  }
942
  // reecrypt all files under the new folder
943
  if (destFileInfo.isDir()) {
2✔
944
    reencryptPath(destFileInfo.absoluteFilePath());
×
945
  } else if (destFileInfo.isFile()) {
2✔
946
    reencryptPath(destFileInfo.dir().path());
4✔
947
  }
948
}
3✔
949

950
/**
951
 * @brief ImitatePass::executeGpg easy wrapper for running gpg commands
952
 * @param args
953
 */
954
void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
31✔
955
                             bool readStdout, bool readStderr) {
956
  executeWrapper(id, m_settings.gpgExecutable, args, std::move(input),
31✔
957
                 readStdout, readStderr);
958
}
31✔
959

960
/**
961
 * @brief ImitatePass::executeGit easy wrapper for running git commands
962
 * @param args
963
 */
964
void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
6✔
965
                             bool readStdout, bool readStderr) {
966
  executeWrapper(id, m_settings.gitExecutable, args, std::move(input),
6✔
967
                 readStdout, readStderr);
968
}
6✔
969

970
/**
971
 * @brief ImitatePass::finished this function is overloaded to ensure
972
 *                              identical behaviour to RealPass ie. only PASS_*
973
 *                              processes are visible inside Pass::finish, so
974
 *                              that interface-wise it all looks the same
975
 * @param id
976
 * @param exitCode
977
 * @param out
978
 * @param err
979
 */
980
void ImitatePass::finished(int id, int exitCode, const QString &out,
37✔
981
                           const QString &err) {
982
#ifdef QT_DEBUG
983
  dbg() << "Imitate Pass";
984
#endif
985
  PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
37✔
986
  m_transactionOutput.append(out);
37✔
987

988
  if (exitCode == 0) {
37✔
989
    if (pid == INVALID) {
36✔
990
      return;
991
    }
992
  } else {
993
    while (pid == INVALID) {
1✔
994
      id = exec.cancelNext();
×
995
      if (id == -1) {
×
996
        //  this is probably irrecoverable and shall not happen
997
#ifdef QT_DEBUG
998
        dbg() << "No such transaction!";
999
#endif
1000
        return;
1001
      }
1002
      pid = transactionIsOver(static_cast<PROCESS>(id));
×
1003
    }
1004
  }
1005
  Pass::finished(pid, exitCode, m_transactionOutput, err);
32✔
1006
  m_transactionOutput.clear();
32✔
1007
}
1008

1009
/**
1010
 * @brief Register a transaction before each wrapped execution.
1011
 *
1012
 * Native mode treats every git/gpg invocation as a transaction; the base
1013
 * Pass::executeWrapper calls this hook just before dispatching.
1014
 * @param id Process identifier of the command about to run.
1015
 */
1016
void ImitatePass::beforeExecute(PROCESS id) { transactionAdd(id); }
37✔
1017

1018
/**
1019
 * @brief Decrypt one .gpg file and return lines matching rx.
1020
 */
1021
auto ImitatePass::grepMatchFile(const QProcessEnvironment &env,
9✔
1022
                                const QString &gpgExe, const QString &filePath,
1023
                                const QRegularExpression &rx) -> QStringList {
1024
  QString translatedPath = filePath;
1025
  if (gpgExe.startsWith(QStringLiteral("wsl "))) {
18✔
1026
    QString wslPath;
×
1027
    const int wrc = Executor::executeBlocking(
×
1028
        QStringLiteral("wsl"), {QStringLiteral("wslpath"), filePath}, &wslPath);
×
1029
    const QString translated = wslPath.trimmed();
1030
    if (wrc == 0 && !translated.isEmpty())
×
1031
      translatedPath = translated;
×
1032
  }
1033
  QString plaintext;
9✔
1034
  const int rc =
1035
      Executor::executeBlocking(env, gpgExe,
81✔
1036
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
1037
                                 "--batch", "--use-agent", translatedPath},
1038
                                &plaintext);
1039
  if (rc != 0 || plaintext.isEmpty())
9✔
1040
    return {};
3✔
1041
  QStringList matches;
6✔
1042
  for (const QString &line : plaintext.split('\n')) {
30✔
1043
    QString candidate = line;
1044
    if (candidate.endsWith('\r'))
18✔
1045
      candidate.chop(1);
×
1046
    const QString t = candidate.trimmed();
1047
    if (!t.isEmpty() && candidate.contains(rx))
18✔
1048
      matches << t;
1049
  }
1050
  return matches;
1051
}
9✔
1052

1053
/**
1054
 * @brief Walk the store, decrypt every .gpg file, collect matches.
1055
 */
1056
auto ImitatePass::grepScanStore(const QProcessEnvironment &env,
5✔
1057
                                const QString &gpgExe, const QString &storeDir,
1058
                                const QRegularExpression &rx)
1059
    -> QList<QPair<QString, QStringList>> {
1060
  QList<QPair<QString, QStringList>> results;
5✔
1061
  QDirIterator it(storeDir, QStringList() << "*.gpg", QDir::Files,
10✔
1062
                  QDirIterator::Subdirectories);
5✔
1063
  while (it.hasNext()) {
13✔
1064
    if (QThread::currentThread()->isInterruptionRequested())
8✔
1065
      return {};
×
1066
    const QString filePath = it.next();
8✔
1067
    const QStringList matches = grepMatchFile(env, gpgExe, filePath, rx);
8✔
1068
    if (!matches.isEmpty()) {
8✔
1069
      QString entry = QDir(storeDir).relativeFilePath(filePath);
6✔
1070
      if (entry.endsWith(QLatin1String(".gpg")))
6✔
1071
        entry.chop(4);
6✔
1072
      results.append({entry, matches});
6✔
1073
    }
1074
  }
1075
  return results;
1076
}
5✔
1077

1078
/**
1079
 * @brief Search all password content by GPG-decrypting each .gpg file.
1080
 *
1081
 * The pattern is evaluated with `QRegularExpression` (**PCRE**), which differs
1082
 * from the POSIX BRE dialect of the `pass` backend — see Pass::Grep for the
1083
 * cross-backend caveat.
1084
 *
1085
 * Runs a background thread to avoid blocking the UI. Results are emitted on
1086
 * the main thread via QMetaObject::invokeMethod. A sequence counter discards
1087
 * results from superseded searches.
1088
 */
1089
void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
5✔
1090
  for (QThread *t : std::as_const(m_grepThreads))
5✔
1091
    if (t && t->isRunning())
×
1092
      t->requestInterruption();
×
1093
  // No wait() — blocking the UI thread while GPG decrypts would freeze the
1094
  // interface. Stale results are discarded via the sequence counter.
1095

1096
  // Advance the sequence before any early return so in-flight workers from the
1097
  // previous query fail the seq check and cannot publish stale results.
1098
  const int seq = ++m_grepSeq;
5✔
1099

1100
  // Use trimmed() rather than isEmpty(): a whitespace-only string is a valid
1101
  // regex that matches every non-empty line, which is almost never intentional
1102
  // and would decrypt the entire store.
1103
  //
1104
  // Both early returns post finishedGrep via Qt::QueuedConnection so that the
1105
  // signal is always delivered asynchronously after Grep() returns, matching
1106
  // the contract of the threaded path.
1107
  if (pattern.trimmed().isEmpty()) {
5✔
1108
    QMetaObject::invokeMethod(
×
1109
        this,
1110
        [this, seq]() {
×
1111
          if (m_grepSeq == seq)
×
1112
            emit finishedGrep({});
×
1113
        },
×
1114
        Qt::QueuedConnection);
1115
    return;
1✔
1116
  }
1117

1118
  const QRegularExpression rx(
1119
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
1120
                               : QRegularExpression::PatternOptions{});
10✔
1121
  if (!rx.isValid()) {
5✔
1122
    QMetaObject::invokeMethod(
1✔
1123
        this,
1124
        [this, seq]() {
1✔
1125
          if (m_grepSeq == seq)
1✔
1126
            emit finishedGrep({});
2✔
1127
        },
1✔
1128
        Qt::QueuedConnection);
1129
    return;
1130
  }
1131
  const QString gpgExe = m_settings.gpgExecutable;
1132
  const QString storeDir = m_settings.passStore;
1133
  const QProcessEnvironment env = exec.environment();
4✔
1134
  QPointer<ImitatePass> self(this);
1135

1136
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
8✔
1137
    if (!self)
4✔
1138
      return;
1139
    QMetaObject::invokeMethod(
4✔
1140
        self,
1141
        [self, seq, results = std::move(results)]() {
12✔
1142
          if (self && self->m_grepSeq == seq)
8✔
1143
            emit self->finishedGrep(results);
4✔
1144
        },
4✔
1145
        Qt::QueuedConnection);
1146
  };
4✔
1147

1148
  QThread *thread = QThread::create(
4✔
1149
      [gpgExe, storeDir, env, rx, emitResults = std::move(emitResults)]() {
8✔
1150
        std::move(emitResults)(grepScanStore(env, gpgExe, storeDir, rx));
4✔
1151
      });
4✔
1152

1153
  m_grepThreads.append(thread);
4✔
1154
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
4✔
1155
  connect(thread, &QThread::finished, this,
4✔
1156
          [this, thread]() { m_grepThreads.removeOne(thread); });
8✔
1157
  thread->start();
4✔
1158
}
9✔
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