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

IJHack / QtPass / 27688530046

17 Jun 2026 12:21PM UTC coverage: 57.501% (+0.02%) from 57.477%
27688530046

push

github

web-flow
refactor(pass): inject AppSettings into Pass hierarchy (PR A of #1511) (#1551)

* refactor(pass): inject AppSettings into Pass hierarchy (PR A of #1511)

- Pass::init(AppSettings) replaces no-arg init(); backends store
  m_settings and read executables/paths from it instead of calling
  QtPassSettings getters at runtime
- ImitatePass::pgit/pgpg converted from static free functions to const
  member functions; grepMatchFile (static) inlines the wslpath check
  using its gpgExe parameter
- PassBackendFactory::getPass() loads AppSettings via QtPassSettings::load()
  and normalises passStore via QtPassSettings::getPassStore(), then passes
  the snapshot to pass->init(s)
- realpass.cpp and imitatepass.cpp no longer include qtpasssettings.h
- Integration test and tst_util updated: call pass.init(s) after
  configuring QtPassSettings so m_settings is populated before updateEnv()

Two QtPassSettings calls remain in passbackendfactory.cpp (load + getPassStore);
removal of getGpgIdPath's QtPassSettings dependency is deferred to PR C.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: apply CodeRabbit auto-fixes

Fixed 2 file(s) based on 3 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* fix: address review findings on AppSettings injection

- keygendialog: pass getGpgExecutable() to getDefaultKeyTemplate() so
  gpgSupportsEd25519 uses the configured binary, not the "gpg" fallback
  (P1: silent regression on systems where gpg is GPG 1.x but user
  configured gpg2)
- QtPassSettings::load(): normalise passStore (absolutePath + trailing
  slash) so callers get a usable path without a separate getPassStore()
  override; SettingsSerializer::load() preserves raw stored value for
  round-trip fidelity (P2: duplicate override pattern removed)
- passbackendfactory: drop s.passStore = getPassStore() override; inline
  mkdir for first-run directory creation
- pass.cpp: fix clang-format violations (ColumnLimit=80 line breaks... (continued)

52 of 92 new or added lines in 6 files covered. (56.52%)

13 existing lines in 6 files now uncovered.

3990 of 6939 relevant lines covered (57.5%)

23.81 hits per line

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

53.72
/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;
76✔
45

46
ImitatePass::~ImitatePass() {
40✔
47
  static constexpr int kGrepThreadTimeoutMs = 5000;
48
  for (QThread *t : std::as_const(m_grepThreads))
38✔
49
    if (t && t->isRunning())
×
50
      t->requestInterruption();
×
51
  QElapsedTimer elapsed;
38✔
52
  elapsed.start();
38✔
53
  for (QThread *t : std::as_const(m_grepThreads)) {
38✔
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
}
40✔
62

63
auto ImitatePass::pgit(const QString &path) const -> QString {
2✔
64
  QString normalizedPath = QDir::cleanPath(path);
2✔
65
  if (!m_settings.gitExecutable.startsWith(QStringLiteral("wsl ")))
4✔
66
    return normalizedPath;
67
  QString res =
NEW
68
      QStringLiteral("$(wslpath ") + normalizedPath + QLatin1Char(')');
×
UNCOV
69
  return res.replace('\\', '/');
×
70
}
71

72
auto ImitatePass::pgpg(const QString &path) const -> QString {
37✔
73
  QString normalizedPath = QDir::cleanPath(path);
37✔
74
  if (!m_settings.gpgExecutable.startsWith(QStringLiteral("wsl ")))
74✔
75
    return normalizedPath;
76
  QString res =
NEW
77
      QStringLiteral("$(wslpath ") + normalizedPath + QLatin1Char(')');
×
UNCOV
78
  return res.replace('\\', '/');
×
79
}
80

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

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

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

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

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

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

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

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

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

218
/**
219
 * @brief ImitatePass::Init initialize pass repository
220
 *
221
 * @param path      path in which new password-store will be created
222
 * @param users     list of users who shall be able to decrypt passwords in
223
 * path
224
 */
225
auto ImitatePass::checkSigningKeys(const QStringList &signingKeys) -> bool {
×
226
  QString out;
×
227
  QStringList args =
228
      QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys;
×
NEW
229
  int result = Executor::executeBlocking(m_settings.gpgExecutable, 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});
×
NEW
314
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args);
×
315
  if (result != 0) {
×
316
#ifdef QT_DEBUG
317
    dbg() << "GPG signing failed with code:" << result;
318
#endif
319
    emit critical(tr("GPG signing failed!"),
×
320
                  tr("Failed to sign %1.").arg(gpgIdFile));
×
321
    return false;
×
322
  }
323
  if (!verifyGpgIdFile(gpgIdFile)) {
×
324
    emit critical(tr("Check .gpg-id file signature!"),
×
325
                  tr("Signature for %1 is invalid.").arg(gpgIdFile));
×
326
    return false;
×
327
  }
328
  return true;
329
}
×
330

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

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

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

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

NEW
415
  if (!m_settings.useWebDav && m_settings.useGit &&
×
416
      !m_settings.gitExecutable.isEmpty()) {
UNCOV
417
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
418
  }
419
  reencryptPath(path);
×
420
}
421

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

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

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

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

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

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

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

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

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

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

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

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

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

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

673
  if (!m_settings.useWebDav && m_settings.useGit) {
2✔
NEW
674
    Executor::executeBlocking(m_settings.gitExecutable,
×
675
                              {"add", pgit(fileName)});
NEW
676
    QString path = QDir(m_settings.passStore).relativeFilePath(fileName);
×
677
    path.replace(Util::endsWithGpg(), "");
×
NEW
678
    Executor::executeBlocking(m_settings.gitExecutable,
×
679
                              {"commit", pgit(fileName), "-m",
680
                               "Re-encrypt for " + path + " using QtPass."});
×
681
  }
682

683
  return true;
684
}
6✔
685

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

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

738
  // Create backup before re-encryption - abort if it fails
739
  if (!createBackupCommit()) {
1✔
740
    emit endReencryptPath();
×
741
    return;
×
742
  }
743

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

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

788
  if (m_settings.autoPush && m_settings.useGit) {
1✔
789
    emit statusMsg(tr("Updating password-store"), 2000);
×
790
    GitPush();
×
791
  }
792
  emit endReencryptPath();
1✔
793
}
1✔
794

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

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

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

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

NEW
878
  QString relSrc = QDir(m_settings.passStore).relativeFilePath(src);
×
879
  relSrc.replace(Util::endsWithGpg(), "");
×
NEW
880
  QString relDest = QDir(m_settings.passStore).relativeFilePath(destFile);
×
881
  relDest.replace(Util::endsWithGpg(), "");
×
882
  QString message = QString("Moved for %1 to %2 using QtPass.");
×
883
  message = message.arg(relSrc, relDest);
×
884
  gitCommit("", message);
×
885
}
×
886

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

906
#ifdef QT_DEBUG
907
  dbg() << "Move Source: " << src;
908
  dbg() << "Move Destination: " << destFile;
909
#endif
910

911
  if (m_settings.useGit) {
1✔
912
    executeMoveGit(src, destFile, force);
×
913
  } else {
914
    QDir qDir;
1✔
915
    if (force) {
1✔
916
      qDir.remove(destFile);
×
917
    }
918
    qDir.rename(src, destFile);
1✔
919
  }
1✔
920
}
921

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

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

966
/**
967
 * @brief ImitatePass::executeGpg easy wrapper for running gpg commands
968
 * @param args
969
 */
970
void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
29✔
971
                             bool readStdout, bool readStderr) {
972
  executeWrapper(id, m_settings.gpgExecutable, args, std::move(input),
29✔
973
                 readStdout, readStderr);
974
}
29✔
975

976
/**
977
 * @brief ImitatePass::executeGit easy wrapper for running git commands
978
 * @param args
979
 */
980
void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
2✔
981
                             bool readStdout, bool readStderr) {
982
  executeWrapper(id, m_settings.gitExecutable, args, std::move(input),
2✔
983
                 readStdout, readStderr);
984
}
2✔
985

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

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

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

1035
/**
1036
 * @brief Decrypt one .gpg file and return lines matching rx.
1037
 */
1038
auto ImitatePass::grepMatchFile(const QProcessEnvironment &env,
9✔
1039
                                const QString &gpgExe, const QString &filePath,
1040
                                const QRegularExpression &rx) -> QStringList {
1041
  QString translatedPath = filePath;
1042
  if (gpgExe.startsWith(QStringLiteral("wsl "))) {
18✔
NEW
1043
    translatedPath = QStringLiteral("$(wslpath ") + filePath + QLatin1Char(')');
×
NEW
1044
    translatedPath.replace('\\', '/');
×
1045
  }
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", translatedPath},
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 QProcessEnvironment &env,
5✔
1070
                                const QString &gpgExe, 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
 * The pattern is evaluated with `QRegularExpression` (**PCRE**), which differs
1095
 * from the POSIX BRE dialect of the `pass` backend — see Pass::Grep for the
1096
 * cross-backend caveat.
1097
 *
1098
 * Runs a background thread to avoid blocking the UI. Results are emitted on
1099
 * the main thread via QMetaObject::invokeMethod. A sequence counter discards
1100
 * results from superseded searches.
1101
 */
1102
void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
5✔
1103
  for (QThread *t : std::as_const(m_grepThreads))
5✔
1104
    if (t && t->isRunning())
×
1105
      t->requestInterruption();
×
1106
  // No wait() — blocking the UI thread while GPG decrypts would freeze the
1107
  // interface. Stale results are discarded via the sequence counter.
1108

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

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

1131
  const QRegularExpression rx(
1132
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
1133
                               : QRegularExpression::PatternOptions{});
10✔
1134
  if (!rx.isValid()) {
5✔
1135
    QMetaObject::invokeMethod(
1✔
1136
        this,
1137
        [this, seq]() {
1✔
1138
          if (m_grepSeq == seq)
1✔
1139
            emit finishedGrep({});
2✔
1140
        },
1✔
1141
        Qt::QueuedConnection);
1142
    return;
1143
  }
1144
  const QString gpgExe = m_settings.gpgExecutable;
1145
  const QString storeDir = m_settings.passStore;
1146
  const QProcessEnvironment env = exec.environment();
4✔
1147
  QPointer<ImitatePass> self(this);
1148

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

1161
  QThread *thread = QThread::create(
4✔
1162
      [gpgExe, storeDir, env, rx, emitResults = std::move(emitResults)]() {
8✔
1163
        std::move(emitResults)(grepScanStore(env, gpgExe, storeDir, rx));
4✔
1164
      });
4✔
1165

1166
  m_grepThreads.append(thread);
4✔
1167
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
4✔
1168
  connect(thread, &QThread::finished, this,
4✔
1169
          [this, thread]() { m_grepThreads.removeOne(thread); });
8✔
1170
  thread->start();
4✔
1171
}
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