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

IJHack / QtPass / 35231775420

17 Sep 2026 02:10PM UTC coverage: 75.981% (+0.2%) from 75.806%
35231775420

push

github

web-flow
ImitatePass: extract NativeGrep and GpgIdSigner, compose simpleTransaction (#1814)

* ImitatePass: extract NativeGrep and GpgIdSigner, compose simpleTransaction

Three pieces leave the 1500-line backend:

- NativeGrep owns the threaded decrypt-and-match search: the sequence
  counter, the worker threads and their shutdown, matchFile()/scanStore().
  ImitatePass::Grep() calls search() and relays finished() as finishedGrep;
  its destructor cancels the searches first so they wind down while the
  re-encryption worker is joined.
- GpgIdSigner is the .gpg-id signature logic: haveSecretKey()
  (KEY_CONSIDERED), sign() (first key only, --yes --detach-sign, WSL path
  translation) and verify() (VALIDSIG fingerprint or primary fingerprint
  against the configured keys; passes with no key configured, as pass does).
  Every gpg run goes through an Exec callback, so ImitatePass hands in its
  cancellable execBlocking() and the re-encryption worker keeps its
  interruptible verification, and ProfileInit uses the same sign() instead
  of its own copy of the gpg invocation.
- simpleTransaction is a member, not a private base.

tst_gpgidsigner drives the signer with a scripted gpg: arguments, first-key
rule, stderr reporting, both accepted fingerprints, rejection of a valid
signature by someone else, exit 0 without VALIDSIG, path cleaning, and the
default runner. Existing tests follow the renames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* GpgIdSigner: the keys are fingerprints; reflow a comment

The class comment promised that key IDs verify; gpg reports full
fingerprints in KEY_CONSIDERED and VALIDSIG, so only a 40-hex fingerprint
satisfies haveSecretKey() and verify() (pass requires the same). The
renamed comment in tst_integration ran past column 80.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

... (continued)

156 of 179 new or added lines in 5 files covered. (87.15%)

1 existing line in 1 file now uncovered.

5947 of 7827 relevant lines covered (75.98%)

75.01 hits per line

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

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

14
NativeGrep::NativeGrep(QObject *parent) : QObject(parent) {}
76 ✔
15

16
NativeGrep::~NativeGrep() {
76 ✔
17
  static constexpr int kThreadTimeoutMs = 5000;
18
  cancel();
76 ✔
19
  QElapsedTimer elapsed;
76 ✔
20
  elapsed.start();
76 ✔
21
  for (const Worker &w : std::as_const(m_workers)) {
77 ✔
22
    if (w.thread && w.thread->isRunning()) {
1 ✔
23
      const int remaining =
NEW
24
          kThreadTimeoutMs - static_cast<int>(elapsed.elapsed());
×
NEW
25
      if (remaining > 0)
×
NEW
26
        w.thread->wait(remaining);
×
27
    }
28
  }
29
}
76 ✔
30

31
/**
32
 * @brief Decrypt one .gpg file and return lines matching rx.
33
 */
34
auto NativeGrep::matchFile(const QProcessEnvironment &env,
10 ✔
35
                           const QString &gpgExe, const QString &filePath,
36
                           const QRegularExpression &rx,
37
                           const std::atomic_bool *cancel) -> QStringList {
38
  QString translatedPath = filePath;
39
  if (gpgExe.startsWith(QStringLiteral("wsl "))) {
20 ✔
NEW
40
    QString wslPath;
×
NEW
41
    const int wrc = Executor::executeBlocking(
×
NEW
42
        QStringLiteral("wsl"),
×
NEW
43
        Executor::wslExecArgs(QStringLiteral("wslpath"), {filePath}), &wslPath);
×
44
    const QString translated = wslPath.trimmed();
NEW
45
    if (wrc == 0 && !translated.isEmpty())
×
NEW
46
      translatedPath = translated;
×
47
  }
48
  QString plaintext;
10 ✔
49
  // The QProcess overload is the one that takes the cancel flag; it polls
50
  // the flag while waiting and terminates (then kills) gpg from this thread,
51
  // the one that owns the process.
52
  QProcess gpg;
10 ✔
53
  gpg.setProcessEnvironment(env);
10 ✔
54
  const int rc =
55
      Executor::executeBlocking(gpg, gpgExe,
90 ✔
56
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
57
                                 "--batch", "--use-agent", translatedPath},
58
                                QString(), &plaintext, nullptr, cancel);
10 ✔
59
  if (rc != 0 || plaintext.isEmpty())
10 ✔
60
    return {};
4 ✔
61
  QStringList matches;
6 ✔
62
  for (const QString &line : plaintext.split('\n')) {
30 ✔
63
    QString candidate = line;
64
    if (candidate.endsWith('\r'))
18 ✔
NEW
65
      candidate.chop(1);
×
66
    const QString t = candidate.trimmed();
67
    if (!t.isEmpty() && candidate.contains(rx))
18 ✔
68
      matches << t;
69
  }
70
  return matches;
71
}
20 ✔
72

73
/**
74
 * @brief Walk the store, decrypt every .gpg file, collect matches.
75
 */
76
auto NativeGrep::scanStore(const QProcessEnvironment &env,
6 ✔
77
                           const QString &gpgExe, const QString &storeDir,
78
                           const QRegularExpression &rx,
79
                           const std::atomic_bool *cancel)
80
    -> QList<QPair<QString, QStringList>> {
81
  QList<QPair<QString, QStringList>> results;
6 ✔
82
  QDirIterator it(storeDir, QStringList() << "*.gpg", QDir::Files,
12 ✔
83
                  QDirIterator::Subdirectories);
6 ✔
84
  while (it.hasNext()) {
15 ✔
85
    if (QThread::currentThread()->isInterruptionRequested() ||
9 ✔
86
        (cancel != nullptr && cancel->load()))
9 ✔
NEW
87
      return {};
×
88
    const QString filePath = it.next();
9 ✔
89
    const QStringList matches = matchFile(env, gpgExe, filePath, rx, cancel);
9 ✔
90
    if (!matches.isEmpty()) {
9 ✔
91
      QString entry = QDir(storeDir).relativeFilePath(filePath);
6 ✔
92
      if (entry.endsWith(QLatin1String(".gpg")))
6 ✔
93
        entry.chop(4);
6 ✔
94
      results.append({entry, matches});
6 ✔
95
    }
96
  }
97
  return results;
98
}
6 ✔
99

100
/**
101
 * @brief Start a search on a worker thread.
102
 *
103
 * Results are emitted on the owner's thread via QMetaObject::invokeMethod. A
104
 * sequence counter discards results from superseded searches; the previous
105
 * search is asked to stop but not waited for, since blocking the UI thread
106
 * while gpg decrypts would freeze the interface.
107
 */
108
void NativeGrep::search(const QString &pattern, bool caseInsensitive,
6 ✔
109
                        const QString &gpgExe, const QString &storeDir,
110
                        const QProcessEnvironment &env) {
111
  cancel();
6 ✔
112

113
  // Advance the sequence before any early return so in-flight workers from the
114
  // previous query fail the seq check and cannot publish stale results.
115
  const int seq = ++m_seq;
6 ✔
116

117
  // Use trimmed() rather than isEmpty(): a whitespace-only string is a valid
118
  // regex that matches every non-empty line, which is almost never intentional
119
  // and would decrypt the entire store.
120
  //
121
  // Both early returns post finished() via Qt::QueuedConnection so that the
122
  // signal is always delivered asynchronously after search() returns, matching
123
  // the contract of the threaded path.
124
  if (pattern.trimmed().isEmpty()) {
6 ✔
NEW
125
    QMetaObject::invokeMethod(
×
126
        this,
NEW
127
        [this, seq]() {
×
NEW
128
          if (m_seq == seq)
×
NEW
129
            emit finished({});
×
NEW
130
        },
×
131
        Qt::QueuedConnection);
132
    return;
1 ✔
133
  }
134

135
  const QRegularExpression rx(
136
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
137
                               : QRegularExpression::PatternOptions{});
12 ✔
138
  if (!rx.isValid()) {
6 ✔
139
    QMetaObject::invokeMethod(
1 ✔
140
        this,
141
        [this, seq]() {
1 ✔
142
          if (m_seq == seq)
1 ✔
143
            emit finished({});
2 ✔
144
        },
1 ✔
145
        Qt::QueuedConnection);
146
    return;
147
  }
148
  QPointer<NativeGrep> self(this);
149

150
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
10 ✔
151
    if (!self)
5 ✔
152
      return;
153
    QMetaObject::invokeMethod(
5 ✔
154
        self,
155
        [self, seq, results = std::move(results)]() {
15 ✔
156
          if (self && self->m_seq == seq)
10 ✔
157
            emit self->finished(results);
5 ✔
158
        },
5 ✔
159
        Qt::QueuedConnection);
160
  };
5 ✔
161

162
  auto cancelFlag = std::make_shared<std::atomic_bool>(false);
5 ✔
163
  QThread *thread = QThread::create([gpgExe, storeDir, env, rx, cancelFlag,
10 ✔
164
                                     emitResults = std::move(emitResults)]() {
165
    std::move(emitResults)(
5 ✔
166
        scanStore(env, gpgExe, storeDir, rx, cancelFlag.get()));
5 ✔
167
  });
5 ✔
168

169
  m_workers.append({thread, cancelFlag});
10 ✔
170
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
5 ✔
171
  connect(thread, &QThread::finished, this, [this, thread]() {
5 ✔
172
    m_workers.removeIf(
4 ✔
173
        [thread](const Worker &w) { return w.thread == thread; });
4 ✔
174
  });
175
  thread->start();
5 ✔
176
}
11 ✔
177

178
void NativeGrep::cancel() {
158 ✔
179
  for (const Worker &w : std::as_const(m_workers)) {
160 ✔
180
    w.cancel->store(true);
181
    if (w.thread && w.thread->isRunning())
2 ✔
182
      w.thread->requestInterruption();
1 ✔
183
  }
184
}
158 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc