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

IJHack / QtPass / 35463508362

19 Sep 2026 07:10PM UTC coverage: 77.948% (+0.003%) from 77.945%
35463508362

push

github

web-flow
Terminate option parsing before every path handed to git or gpg (#1847)

git add/rm/mv/commit and gpg's positional inputs (decrypt, list-only,
verify, detach-sign) get a "--" in front of the path, as the .gpg-id
commit and the ls-files check already did. The paths QtPass builds are
absolute, so nothing was exploitable; the rule is now visible in one
glance instead of proven per call site.


Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

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

11 of 13 new or added lines in 2 files covered. (84.62%)

2 existing lines in 2 files now uncovered.

6783 of 8702 relevant lines covered (77.95%)

74.55 hits per line

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

81.11
/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) {}
86 ✔
15

16
NativeGrep::~NativeGrep() {
86 ✔
17
  static constexpr int kThreadTimeoutMs = 5000;
18
  cancel();
86 ✔
19
  QElapsedTimer elapsed;
86 ✔
20
  elapsed.start();
86 ✔
21
  for (const Worker &w : std::as_const(m_workers)) {
86 ✔
UNCOV
22
    if (w.thread && w.thread->isRunning()) {
×
23
      const int remaining =
24
          kThreadTimeoutMs - static_cast<int>(elapsed.elapsed());
×
25
      if (remaining > 0)
×
26
        w.thread->wait(remaining);
×
27
    }
28
  }
29
}
86 ✔
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 ✔
40
    QString wslPath;
×
41
    const int wrc = Executor::executeBlocking(
×
42
        QStringLiteral("wsl"),
×
43
        Executor::wslExecArgs(QStringLiteral("wslpath"), {filePath}), &wslPath);
×
44
    const QString translated = wslPath.trimmed();
45
    if (wrc == 0 && !translated.isEmpty())
×
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 ✔
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 ✔
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 ✔
125
    QMetaObject::invokeMethod(
×
126
        this,
127
        [this, seq]() {
×
128
          if (m_seq == seq)
×
129
            emit finished({});
×
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(
5 ✔
173
        [thread](const Worker &w) { return w.thread == thread; });
5 ✔
174
  });
175
  thread->start();
5 ✔
176
}
11 ✔
177

178
void NativeGrep::cancel() {
178 ✔
179
  for (const Worker &w : std::as_const(m_workers)) {
179 ✔
180
    w.cancel->store(true);
181
    if (w.thread && w.thread->isRunning())
1 ✔
182
      w.thread->requestInterruption();
1 ✔
183
  }
184
}
178 ✔
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