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

IJHack / QtPass / 27478339193

13 Jun 2026 08:34PM UTC coverage: 55.569% (-0.04%) from 55.613%
27478339193

push

github

web-flow
refactor: replace executeWrapper override with beforeExecute hook (#1513) (#1526)

The Pass execution path had a three-level dispatch: the 6-arg
executeWrapper was virtual purely so ImitatePass could prepend
transactionAdd(id) before delegating back to the base. That hid the
transaction wrapping inside an override of the dispatch function itself.

Make the 6-arg executeWrapper non-virtual and introduce an explicit
virtual beforeExecute(PROCESS) hook (no-op in the base) that it calls
right before dispatching to the Executor. ImitatePass now overrides only
that hook to register the transaction, so the wrapping is visible at the
call site and there is a single executeWrapper implementation.

No behavioural change: beforeExecute runs at exactly the point
transactionAdd previously did. All tests pass (incl. imitatepass +
integration).

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

2 of 3 new or added lines in 2 files covered. (66.67%)

2 existing lines in 1 file now uncovered.

3732 of 6716 relevant lines covered (55.57%)

37.06 hits per line

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

53.81
/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 "qtpasssettings.h"
6
#include "util.h"
7
#include <QDirIterator>
8
#include <QElapsedTimer>
9
#include <QPointer>
10
#include <QRegularExpression>
11
#include <QThread>
12
#include <utility>
13

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

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

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

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

64
static auto pgit(const QString &path) -> QString {
2✔
65
  if (!QtPassSettings::getGitExecutable().startsWith("wsl ")) {
6✔
66
    return path;
67
  }
68
  QString res = "$(wslpath " + path + ")";
×
69
  return res.replace('\\', '/');
×
70
}
71

72
static auto pgpg(const QString &path) -> QString {
46✔
73
  if (!QtPassSettings::getGpgExecutable().startsWith("wsl ")) {
138✔
74
    return path;
75
  }
76
  QString res = "$(wslpath " + path + ")";
×
77
  return res.replace('\\', '/');
×
78
}
79

80
/**
81
 * @brief ImitatePass::GitInit git init wrapper
82
 */
83
void ImitatePass::GitInit() {
×
84
  executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
×
85
}
×
86

87
/**
88
 * @brief ImitatePass::GitPull git pull wrapper
89
 */
90
void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
×
91

92
/**
93
 * @brief ImitatePass::GitPull_b git pull wrapper
94
 */
95
void ImitatePass::GitPull_b() {
×
96
  Executor::executeBlocking(QtPassSettings::getGitExecutable(), {"pull"});
×
97
}
×
98

99
/**
100
 * @brief ImitatePass::GitPush git push wrapper
101
 */
102
void ImitatePass::GitPush() {
×
103
  if (QtPassSettings::isUseGit()) {
×
104
    executeGit(GIT_PUSH, {"push"});
×
105
  }
106
}
×
107

108
/**
109
 * @brief ImitatePass::Show shows content of file
110
 */
111
void ImitatePass::Show(QString file) {
10✔
112
  file = QtPassSettings::getPassStore() + file + ".gpg";
20✔
113
  QStringList args = {"-d",      "--quiet",     "--yes",   "--no-encrypt-to",
114
                      "--batch", "--use-agent", pgpg(file)};
80✔
115
  executeGpg(PASS_SHOW, args);
20✔
116
}
20✔
117

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

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

178
/**
179
 * @brief ImitatePass::gitCommit commit a file to git with an appropriate commit
180
 * message
181
 * @param file
182
 * @param msg
183
 */
184
void ImitatePass::gitCommit(const QString &file, const QString &msg) {
1✔
185
  if (file.isEmpty()) {
1✔
186
    executeGit(GIT_COMMIT, {"commit", "-m", msg});
×
187
  } else {
188
    executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
7✔
189
  }
190
}
3✔
191

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

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

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

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

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

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

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

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

416
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit() &&
×
417
      !QtPassSettings::getGitExecutable().isEmpty()) {
×
418
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
419
  }
420
  reencryptPath(path);
×
421
}
422

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

467
/**
468
 * @brief ImitatePass::removeDir delete folder recursive.
469
 * @param dirName which folder.
470
 * @return was removal successful?
471
 */
472
auto ImitatePass::removeDir(const QString &dirName) -> bool {
1✔
473
  bool result = true;
474
  QDir dir(dirName);
1✔
475

476
  if (dir.exists(dirName)) {
1✔
477
    for (const QFileInfo &info :
478
         dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
479
                               QDir::AllDirs | QDir::Files,
480
                           QDir::DirsFirst)) {
1✔
481
      if (info.isDir()) {
×
482
        result = removeDir(info.absoluteFilePath());
×
483
      } else {
484
        result = QFile::remove(info.absoluteFilePath());
×
485
      }
486

487
      if (!result) {
×
488
        return result;
489
      }
490
    }
491
    result = dir.rmdir(dirName);
1✔
492
  }
493
  return result;
494
}
1✔
495

496
/**
497
 * @brief ImitatePass::reencryptPath reencrypt all files under the chosen
498
 * directory
499
 *
500
 * This is still quite experimental..
501
 * @param dir
502
 */
503
auto ImitatePass::verifyGpgIdForDir(const QString &file,
2✔
504
                                    QStringList &gpgIdFilesVerified,
505
                                    QStringList &gpgId) -> bool {
506
  QString gpgIdPath = Pass::getGpgIdPath(file);
2✔
507
  if (gpgIdFilesVerified.contains(gpgIdPath)) {
2✔
508
    return true;
509
  }
510
  if (!verifyGpgIdFile(gpgIdPath)) {
1✔
511
    emit critical(tr("Check .gpg-id file signature!"),
×
512
                  tr("Signature for %1 is invalid.").arg(gpgIdPath));
×
513
    return false;
×
514
  }
515
  gpgIdFilesVerified.append(gpgIdPath);
516
  gpgId = getRecipientList(file);
2✔
517
  gpgId.sort();
518
  return true;
519
}
520

521
/**
522
 * @brief Extracts and returns a sorted list of valid key IDs from a GPG key
523
 * listing file.
524
 * @example
525
 * QStringList result = ImitatePass::getKeysFromFile(fileName);
526
 * std::cout << result.join(", ").toStdString() << std::endl;
527
 *
528
 * @param fileName - Path to the file used to query and parse GPG key
529
 * information.
530
 * @return QStringList - A sorted list of 16-character key IDs found in the
531
 * file.
532
 */
533
auto ImitatePass::getKeysFromFile(const QString &fileName) -> QStringList {
2✔
534
  QStringList args = {
535
      "-v",          "--no-secmem-warning", "--no-permission-warning",
536
      "--list-only", "--keyid-format=long", pgpg(fileName)};
14✔
537
  QString keys;
2✔
538
  QString err;
2✔
539
  const int result = Executor::executeBlocking(
2✔
540
      QtPassSettings::getGpgExecutable(), args, &keys, &err);
4✔
541
  if (result != 0 && keys.isEmpty() && err.isEmpty()) {
2✔
542
    return {};
×
543
  }
544
  QStringList actualKeys;
2✔
545
  keys += err;
546
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
547
  QStringList key = keys.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
2✔
548
#else
549
  QStringList key = keys.split(Util::newLinesRegex(), QString::SkipEmptyParts);
550
#endif
551
  QListIterator<QString> itr(key);
552
  while (itr.hasNext()) {
8✔
553
    QString current = itr.next();
554
    QStringList cur = current.split(" ");
12✔
555
    if (cur.length() > 4) {
6✔
556
      QString actualKey = cur.takeAt(4);
4✔
557
      if (actualKey.length() == 16) {
4✔
558
        actualKeys << actualKey;
559
      }
560
    }
561
  }
562
  actualKeys.sort();
563
  return actualKeys;
564
}
2✔
565

566
/**
567
 * @brief Re-encrypts a single encrypted file for a new set of recipients.
568
 * @example
569
 * bool result = ImitatePass::reencryptSingleFile(fileName, recipients);
570
 * std::cout << result << std::endl; // Expected output: true on success, false
571
 * on failure
572
 *
573
 * @param const QString &fileName - Path to the encrypted file to re-encrypt.
574
 * @param const QStringList &recipients - List of recipient keys to encrypt the
575
 * file to.
576
 * @return bool - True if the file was successfully decrypted, re-encrypted,
577
 * verified, and replaced; otherwise false.
578
 */
579
auto ImitatePass::reencryptSingleFile(const QString &fileName,
2✔
580
                                      const QStringList &recipients) -> bool {
581
#ifdef QT_DEBUG
582
  dbg() << "reencrypt " << fileName << " for " << recipients;
583
#endif
584
  QString local_lastDecrypt;
2✔
585
  QStringList args = {
586
      "-d",      "--quiet",     "--yes",       "--no-encrypt-to",
587
      "--batch", "--use-agent", pgpg(fileName)};
16✔
588
  int result = Executor::executeBlocking(QtPassSettings::getGpgExecutable(),
4✔
589
                                         args, &local_lastDecrypt);
590

591
  if (result != 0 || local_lastDecrypt.isEmpty()) {
2✔
592
#ifdef QT_DEBUG
593
    dbg() << "Decrypt error on re-encrypt for:" << fileName;
594
#endif
595
    return false;
596
  }
597

598
  if (local_lastDecrypt.right(1) != "\n") {
4✔
599
    local_lastDecrypt += "\n";
×
600
  }
601

602
  // Use passed recipients instead of re-reading from file
603
  if (recipients.isEmpty()) {
2✔
604
    emit critical(tr("Can not edit"),
×
605
                  tr("Could not read encryption key to use, .gpg-id "
×
606
                     "file missing or invalid."));
607
    return false;
×
608
  }
609

610
  // Encrypt to temporary file for atomic replacement
611
  QString tempPath = fileName + ".reencrypt.tmp";
2✔
612
  args = QStringList{"--yes", "--batch", "-eq", "--output", pgpg(tempPath)};
14✔
613
  for (const auto &i : recipients) {
4✔
614
    args.append("-r");
4✔
615
    args.append(i);
616
  }
617
  args.append("-");
4✔
618
  result = Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
4✔
619
                                     local_lastDecrypt);
620

621
  if (result != 0) {
2✔
622
#ifdef QT_DEBUG
623
    dbg() << "Encrypt error on re-encrypt for:" << fileName;
624
#endif
625
    QFile::remove(tempPath);
×
626
    return false;
627
  }
628

629
  // Verify encryption worked by attempting to decrypt the temp file
630
  QString verifyOutput;
2✔
631
  args = QStringList{"-d", "--quiet", "--batch", "--use-agent", pgpg(tempPath)};
14✔
632
  result = Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
4✔
633
                                     &verifyOutput);
634
  if (result != 0 || verifyOutput.isEmpty()) {
2✔
635
#ifdef QT_DEBUG
636
    dbg() << "Verification failed for:" << tempPath;
637
#endif
638
    QFile::remove(tempPath);
×
639
    return false;
640
  }
641
  // Verify content matches original decrypted content (defense in depth)
642
  if (verifyOutput.trimmed() != local_lastDecrypt.trimmed()) {
2✔
643
#ifdef QT_DEBUG
644
    dbg() << "Verification content mismatch for:" << tempPath;
645
#endif
646
    QFile::remove(tempPath);
×
647
    return false;
648
  }
649

650
  // Atomic replace with backup: rename original to .bak, rename temp to
651
  // original, then remove backup
652
  QString backupPath = fileName + ".reencrypt.bak";
2✔
653
  if (!QFile::rename(fileName, backupPath)) {
2✔
654
#ifdef QT_DEBUG
655
    dbg() << "Failed to backup original file:" << fileName;
656
#endif
657
    QFile::remove(tempPath);
×
658
    return false;
659
  }
660
  if (!QFile::rename(tempPath, fileName)) {
2✔
661
#ifdef QT_DEBUG
662
    dbg() << "Failed to rename temp file to:" << fileName;
663
#endif
664
    // Restore backup and clean up temp file
665
    QFile::rename(backupPath, fileName);
×
666
    QFile::remove(tempPath);
×
667
    emit critical(
×
668
        tr("Re-encryption failed"),
×
669
        tr("Failed to replace %1. Original has been restored.").arg(fileName));
×
670
    return false;
×
671
  }
672
  // Success - remove backup
673
  QFile::remove(backupPath);
2✔
674

675
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit()) {
2✔
676
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
677
                              {"add", pgit(fileName)});
678
    QString path =
679
        QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
×
680
    path.replace(Util::endsWithGpg(), "");
×
681
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
682
                              {"commit", pgit(fileName), "-m",
683
                               "Re-encrypt for " + path + " using QtPass."});
×
684
  }
685

686
  return true;
687
}
6✔
688

689
/**
690
 * @brief Create git backup commit before re-encryption.
691
 * @return true if backup created or not needed, false if backup failed.
692
 */
693
auto ImitatePass::createBackupCommit() -> bool {
1✔
694
  if (!QtPassSettings::isUseGit() ||
2✔
695
      QtPassSettings::getGitExecutable().isEmpty()) {
1✔
696
    return true;
697
  }
698
  emit statusMsg(tr("Creating backup commit"), 2000);
×
699
  const QString git = QtPassSettings::getGitExecutable();
×
700
  QString statusOut;
×
701
  if (Executor::executeBlocking(git, {"status", "--porcelain"}, &statusOut) !=
×
702
      0) {
703
    emit critical(
×
704
        tr("Backup commit failed"),
×
705
        tr("Could not inspect git status. Re-encryption was aborted."));
×
706
    return false;
×
707
  }
708
  if (!statusOut.trimmed().isEmpty()) {
×
709
    if (Executor::executeBlocking(git, {"add", "-A"}) != 0 ||
×
710
        Executor::executeBlocking(
×
711
            git, {"commit", "-m", "Backup before re-encryption"}) != 0) {
712
      emit critical(tr("Backup commit failed"),
×
713
                    tr("Re-encryption was aborted because a git backup could "
×
714
                       "not be created."));
715
      return false;
×
716
    }
717
  }
718
  return true;
719
}
×
720

721
/**
722
 * @brief Re-encrypts all `.gpg` files under the given directory using the
723
 *        verified GPG key configuration for each folder.
724
 *
725
 * This method optionally pulls the latest changes before starting, creates a
726
 * backup commit, verifies `.gpg-id` files per directory, and re-encrypts files
727
 * whose current recipients do not match the expected keys. It emits progress,
728
 * status, and error signals throughout the process, and optionally pushes the
729
 * updated password-store when finished.
730
 *
731
 * @param dir - Root directory to scan recursively for `.gpg` files.
732
 * @return void
733
 */
734
void ImitatePass::reencryptPath(const QString &dir) {
1✔
735
  emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
2✔
736
  emit startReencryptPath();
1✔
737
  if (QtPassSettings::isAutoPull()) {
1✔
738
    emit statusMsg(tr("Updating password-store"), 2000);
×
739
    GitPull_b();
×
740
  }
741

742
  // Create backup before re-encryption - abort if it fails
743
  if (!createBackupCommit()) {
1✔
744
    emit endReencryptPath();
×
745
    return;
×
746
  }
747

748
  QDir currentDir;
2✔
749
  QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
2✔
750
                        QDirIterator::Subdirectories);
1✔
751
  QStringList gpgIdFilesVerified;
1✔
752
  QStringList gpgId;
1✔
753
  int successCount = 0;
754
  int failCount = 0;
755
  while (gpgFiles.hasNext()) {
3✔
756
    QString fileName = gpgFiles.next();
2✔
757
    if (gpgFiles.fileInfo().path() != currentDir.path()) {
4✔
758
      if (!verifyGpgIdForDir(fileName, gpgIdFilesVerified, gpgId)) {
2✔
759
        emit endReencryptPath();
×
760
        return;
761
      }
762
      if (gpgId.isEmpty() && !gpgIdFilesVerified.isEmpty()) {
2✔
763
        emit critical(tr("GPG ID verification failed"),
×
764
                      tr("Could not verify .gpg-id for directory."));
×
765
        emit endReencryptPath();
×
766
        return;
767
      }
768
    }
769
    QStringList actualKeys = getKeysFromFile(fileName);
2✔
770
    if (actualKeys != gpgId) {
2✔
771
      if (reencryptSingleFile(fileName, gpgId)) {
2✔
772
        successCount++;
2✔
773
      } else {
774
        failCount++;
×
775
        emit critical(tr("Re-encryption failed"),
×
776
                      tr("Failed to re-encrypt %1").arg(fileName));
×
777
      }
778
    }
779
  }
780

781
  if (failCount > 0) {
1✔
782
    emit statusMsg(tr("Re-encryption completed: %1 succeeded, %2 failed")
×
783
                       .arg(successCount)
×
784
                       .arg(failCount),
×
785
                   5000);
786
  } else {
787
    emit statusMsg(
1✔
788
        tr("Re-encryption completed: %1 files re-encrypted").arg(successCount),
2✔
789
        3000);
790
  }
791

792
  if (QtPassSettings::isAutoPush()) {
1✔
793
    emit statusMsg(tr("Updating password-store"), 2000);
×
794
    GitPush();
×
795
  }
796
  emit endReencryptPath();
1✔
797
}
1✔
798

799
/**
800
 * @brief Resolves the final destination path for moving a file or directory,
801
 * applying .gpg handling for files.
802
 * @example
803
 * QString result = ImitatePass::resolveMoveDestination("/tmp/source.txt",
804
 * "/backup", false); std::cout << result.toStdString() << std::endl; //
805
 * Expected output sample: "/backup/source.txt.gpg"
806
 *
807
 * @param src - Source path to the file or directory.
808
 * @param dest - Requested destination path, which may be a file or directory.
809
 * @param force - When true, allows overwriting an existing destination file.
810
 * @return QString - Resolved destination path, or an empty QString if the
811
 * source/destination is invalid or conflicts occur.
812
 */
813
auto ImitatePass::resolveMoveDestination(const QString &src,
6✔
814
                                         const QString &dest, bool force)
815
    -> QString {
816
  QFileInfo srcFileInfo(src);
6✔
817
  QFileInfo destFileInfo(dest);
6✔
818
  QString destFile;
6✔
819
  QString srcFileBaseName = srcFileInfo.fileName();
6✔
820

821
  if (srcFileInfo.isFile()) {
6✔
822
    if (destFileInfo.isFile()) {
5✔
823
      if (!force) {
2✔
824
#ifdef QT_DEBUG
825
        dbg() << "Destination file already exists";
826
#endif
827
        return {};
828
      }
829
      destFile = dest;
1✔
830
    } else if (destFileInfo.isDir()) {
3✔
831
      destFile = QDir(dest).filePath(srcFileBaseName);
2✔
832
    } else {
833
      destFile = dest;
2✔
834
    }
835

836
    if (destFile.endsWith(".gpg", Qt::CaseInsensitive)) {
8✔
837
      destFile.chop(4);
4✔
838
    }
839
    destFile.append(".gpg");
4✔
840
  } else if (srcFileInfo.isDir()) {
1✔
841
    if (destFileInfo.isDir()) {
×
842
      destFile = QDir(dest).filePath(srcFileBaseName);
×
843
    } else if (destFileInfo.isFile()) {
×
844
#ifdef QT_DEBUG
845
      dbg() << "Destination is a file";
846
#endif
847
      return {};
848
    } else {
849
      destFile = dest;
×
850
    }
851
  } else {
852
#ifdef QT_DEBUG
853
    dbg() << "Source file does not exist";
854
#endif
855
    return {};
856
  }
857
  return destFile;
858
}
6✔
859

860
/**
861
 * @brief Moves a password store item in the Git repository and commits the
862
 * change.
863
 * @example
864
 * void result = className.executeMoveGit(src, destFile, force);
865
 *
866
 * @param const QString &src - Source path of the item to move.
867
 * @param const QString &destFile - Destination path of the item after the move.
868
 * @param bool force - Whether to force the move using Git's -f option.
869
 * @return void - This method does not return a value.
870
 */
871
void ImitatePass::executeMoveGit(const QString &src, const QString &destFile,
×
872
                                 bool force) {
873
  QStringList args;
×
874
  args << "mv";
×
875
  if (force) {
×
876
    args << "-f";
×
877
  }
878
  args << pgit(src);
×
879
  args << pgit(destFile);
×
880
  executeGit(GIT_MOVE, args);
×
881

882
  QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
×
883
  relSrc.replace(Util::endsWithGpg(), "");
×
884
  QString relDest =
885
      QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
×
886
  relDest.replace(Util::endsWithGpg(), "");
×
887
  QString message = QString("Moved for %1 to %2 using QtPass.");
×
888
  message = message.arg(relSrc, relDest);
×
889
  gitCommit("", message);
×
890
}
×
891

892
/**
893
 * @brief Moves a password entry from the source path to the destination path.
894
 * @example
895
 * ImitatePass::Move(src, dest, true);
896
 *
897
 * @param const QString src - The source path or entry name to move.
898
 * @param const QString dest - The destination path or entry name.
899
 * @param const bool force - If true, overwrites an existing destination entry
900
 * when necessary.
901
 * @return void - This function does not return a value.
902
 */
903
void ImitatePass::Move(const QString src, const QString dest,
1✔
904
                       const bool force) {
905
  transactionHelper trans(this, PASS_MOVE);
1✔
906
  QString destFile = resolveMoveDestination(src, dest, force);
1✔
907
  if (destFile.isEmpty()) {
1✔
908
    return;
909
  }
910

911
#ifdef QT_DEBUG
912
  dbg() << "Move Source: " << src;
913
  dbg() << "Move Destination: " << destFile;
914
#endif
915

916
  if (QtPassSettings::isUseGit()) {
1✔
917
    executeMoveGit(src, destFile, force);
×
918
  } else {
919
    QDir qDir;
1✔
920
    if (force) {
1✔
921
      qDir.remove(destFile);
×
922
    }
923
    qDir.rename(src, destFile);
1✔
924
  }
1✔
925
}
926

927
/**
928
 * @brief Copies a file or directory from source to destination, optionally
929
 * forcing overwrite.
930
 * @example
931
 * void result = ImitatePass::Copy(src, dest, force);
932
 *
933
 * @param QString src - Source path to copy from.
934
 * @param QString dest - Destination path to copy to.
935
 * @param bool force - If true, overwrites the destination when it already
936
 * exists.
937
 * @return void - This function does not return a value.
938
 */
939
void ImitatePass::Copy(const QString src, const QString dest,
1✔
940
                       const bool force) {
941
  QFileInfo destFileInfo(dest);
1✔
942
  transactionHelper trans(this, PASS_COPY);
1✔
943
  if (QtPassSettings::isUseGit()) {
1✔
944
    QStringList args;
×
945
    args << "cp";
×
946
    if (force) {
×
947
      args << "-f";
×
948
    }
949
    args << pgit(src);
×
950
    args << pgit(dest);
×
951
    executeGit(GIT_COPY, args);
×
952

953
    QString message = QString("Copied from %1 to %2 using QtPass.");
×
954
    message = message.arg(src, dest);
×
955
    gitCommit("", message);
×
956
  } else {
957
    QDir qDir;
1✔
958
    if (force) {
1✔
959
      qDir.remove(dest);
×
960
    }
961
    QFile::copy(src, dest);
1✔
962
  }
1✔
963
  // reecrypt all files under the new folder
964
  if (destFileInfo.isDir()) {
1✔
965
    reencryptPath(destFileInfo.absoluteFilePath());
×
966
  } else if (destFileInfo.isFile()) {
1✔
967
    reencryptPath(destFileInfo.dir().path());
2✔
968
  }
969
}
1✔
970

971
/**
972
 * @brief ImitatePass::executeGpg easy wrapper for running gpg commands
973
 * @param args
974
 */
975
void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
29✔
976
                             bool readStdout, bool readStderr) {
977
  executeWrapper(id, QtPassSettings::getGpgExecutable(), args, std::move(input),
58✔
978
                 readStdout, readStderr);
979
}
29✔
980

981
/**
982
 * @brief ImitatePass::executeGit easy wrapper for running git commands
983
 * @param args
984
 */
985
void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
2✔
986
                             bool readStdout, bool readStderr) {
987
  executeWrapper(id, QtPassSettings::getGitExecutable(), args, std::move(input),
4✔
988
                 readStdout, readStderr);
989
}
2✔
990

991
/**
992
 * @brief ImitatePass::finished this function is overloaded to ensure
993
 *                              identical behaviour to RealPass ie. only PASS_*
994
 *                              processes are visible inside Pass::finish, so
995
 *                              that interface-wise it all looks the same
996
 * @param id
997
 * @param exitCode
998
 * @param out
999
 * @param err
1000
 */
1001
void ImitatePass::finished(int id, int exitCode, const QString &out,
31✔
1002
                           const QString &err) {
1003
#ifdef QT_DEBUG
1004
  dbg() << "Imitate Pass";
1005
#endif
1006
  static QString transactionOutput;
31✔
1007
  PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
31✔
1008
  transactionOutput.append(out);
31✔
1009

1010
  if (exitCode == 0) {
31✔
1011
    if (pid == INVALID) {
31✔
1012
      return;
1013
    }
1014
  } else {
1015
    while (pid == INVALID) {
×
1016
      id = exec.cancelNext();
×
1017
      if (id == -1) {
×
1018
        //  this is probably irrecoverable and shall not happen
1019
#ifdef QT_DEBUG
1020
        dbg() << "No such transaction!";
1021
#endif
1022
        return;
1023
      }
1024
      pid = transactionIsOver(static_cast<PROCESS>(id));
×
1025
    }
1026
  }
1027
  Pass::finished(pid, exitCode, transactionOutput, err);
29✔
1028
  transactionOutput.clear();
29✔
1029
}
1030

1031
/**
1032
 * @brief Register a transaction before each wrapped execution.
1033
 *
1034
 * Native mode treats every git/gpg invocation as a transaction; the base
1035
 * Pass::executeWrapper calls this hook just before dispatching.
1036
 * @param id Process identifier of the command about to run.
1037
 */
1038
void ImitatePass::beforeExecute(PROCESS id) { transactionAdd(id); }
31✔
1039

1040
/**
1041
 * @brief Decrypt one .gpg file and return lines matching rx.
1042
 */
1043
auto ImitatePass::grepMatchFile(const QStringList &env, const QString &gpgExe,
9✔
1044
                                const QString &filePath,
1045
                                const QRegularExpression &rx) -> QStringList {
1046
  QString plaintext;
9✔
1047
  const int rc =
1048
      Executor::executeBlocking(env, gpgExe,
81✔
1049
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
1050
                                 "--batch", "--use-agent", pgpg(filePath)},
1051
                                &plaintext);
1052
  if (rc != 0 || plaintext.isEmpty())
9✔
1053
    return {};
3✔
1054
  QStringList matches;
6✔
1055
  for (const QString &line : plaintext.split('\n')) {
30✔
1056
    QString candidate = line;
1057
    if (candidate.endsWith('\r'))
18✔
1058
      candidate.chop(1);
×
1059
    const QString t = candidate.trimmed();
1060
    if (!t.isEmpty() && candidate.contains(rx))
18✔
1061
      matches << t;
1062
  }
1063
  return matches;
1064
}
9✔
1065

1066
/**
1067
 * @brief Walk the store, decrypt every .gpg file, collect matches.
1068
 */
1069
auto ImitatePass::grepScanStore(const QStringList &env, const QString &gpgExe,
5✔
1070
                                const QString &storeDir,
1071
                                const QRegularExpression &rx)
1072
    -> QList<QPair<QString, QStringList>> {
1073
  QList<QPair<QString, QStringList>> results;
5✔
1074
  QDirIterator it(storeDir, QStringList() << "*.gpg", QDir::Files,
10✔
1075
                  QDirIterator::Subdirectories);
5✔
1076
  while (it.hasNext()) {
13✔
1077
    if (QThread::currentThread()->isInterruptionRequested())
8✔
1078
      return {};
×
1079
    const QString filePath = it.next();
8✔
1080
    const QStringList matches = grepMatchFile(env, gpgExe, filePath, rx);
8✔
1081
    if (!matches.isEmpty()) {
8✔
1082
      QString entry = QDir(storeDir).relativeFilePath(filePath);
6✔
1083
      if (entry.endsWith(QLatin1String(".gpg")))
6✔
1084
        entry.chop(4);
6✔
1085
      results.append({entry, matches});
6✔
1086
    }
1087
  }
1088
  return results;
1089
}
5✔
1090

1091
/**
1092
 * @brief Search all password content by GPG-decrypting each .gpg file.
1093
 *
1094
 * Runs a background thread to avoid blocking the UI. Results are emitted on
1095
 * the main thread via QMetaObject::invokeMethod. A sequence counter discards
1096
 * results from superseded searches.
1097
 */
1098
void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
5✔
1099
  for (QThread *t : std::as_const(m_grepThreads))
5✔
1100
    if (t && t->isRunning())
×
1101
      t->requestInterruption();
×
1102
  // No wait() — blocking the UI thread while GPG decrypts would freeze the
1103
  // interface. Stale results are discarded via the sequence counter.
1104

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

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

1127
  const QRegularExpression rx(
1128
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
1129
                               : QRegularExpression::PatternOptions{});
10✔
1130
  if (!rx.isValid()) {
5✔
1131
    QMetaObject::invokeMethod(
1✔
1132
        this,
1133
        [this, seq]() {
1✔
1134
          if (m_grepSeq == seq)
1✔
1135
            emit finishedGrep({});
2✔
1136
        },
1✔
1137
        Qt::QueuedConnection);
1138
    return;
1139
  }
1140
  const QString gpgExe = QtPassSettings::getGpgExecutable();
8✔
1141
  const QString storeDir = QtPassSettings::getPassStore();
8✔
1142
  const QStringList env = exec.environment();
4✔
1143
  QPointer<ImitatePass> self(this);
1144

1145
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
8✔
1146
    if (!self)
4✔
1147
      return;
1148
    QMetaObject::invokeMethod(
4✔
1149
        self,
1150
        [self, seq, results = std::move(results)]() {
12✔
1151
          if (self && self->m_grepSeq == seq)
8✔
1152
            emit self->finishedGrep(results);
4✔
1153
        },
4✔
1154
        Qt::QueuedConnection);
1155
  };
4✔
1156

1157
  QThread *thread = QThread::create(
4✔
1158
      [gpgExe, storeDir, env, rx, emitResults = std::move(emitResults)]() {
8✔
1159
        std::move(emitResults)(grepScanStore(env, gpgExe, storeDir, rx));
4✔
1160
      });
4✔
1161

1162
  m_grepThreads.append(thread);
4✔
1163
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
4✔
1164
  connect(thread, &QThread::finished, this,
4✔
1165
          [this, thread]() { m_grepThreads.removeOne(thread); });
8✔
1166
  thread->start();
4✔
1167
}
5✔
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