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

IJHack / QtPass / 40584149370

27 Oct 2026 09:07PM UTC coverage: 92.461% (-0.01%) from 92.471%
40584149370

push

github

web-flow
New profile: count encrypted entries in sub-folders too (#2072)

ProfileInit tells the user when the folder a new profile is made in
already holds encrypted entries (they are not re-encrypted). It counted
the top level only, so a store taken over whole, whose entries sit in
folders, read as "1 file" or as none at all. Util::regularFilesUnder()
walks the folders (not through links, .git not walked).

From CodeRabbit's merge-risk summary on #1782 ("existing nested entries
may not be reported"); the rest of that summary was fixed since.


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

Signed-off-by: Anne Jan Brouwer <brouwer@annejan.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

9431 of 10200 relevant lines covered (92.46%)

304.64 hits per line

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

89.8
/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 "pass.h"
6
#include "util.h"
7
#include <QDir>
8
#include <QElapsedTimer>
9
#include <QPointer>
10
#include <QProcess>
11
#include <QRegularExpression>
12
#include <QStringTokenizer>
13
#include <QThread>
14
#include <utility>
15

16
NativeGrep::NativeGrep(QObject *parent) : QObject(parent) {}
191 ✔
17

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

33
/**
34
 * @brief Decrypt one .gpg file and return lines matching rx.
35
 */
36
auto NativeGrep::matchFile(const QProcessEnvironment &env,
409 ✔
37
                           const QString &gpgExe, const QString &filePath,
38
                           const QRegularExpression &rx,
39
                           const std::atomic_bool *cancel) -> QStringList {
40
  const QString translatedPath =
41
      Executor::translatePathForWsl(filePath, gpgExe);
409 ✔
42
  QString plaintext;
409 ✔
43
  // The QProcess overload is the one that takes the cancel flag; it polls
44
  // the flag while waiting and terminates (then kills) gpg from this thread,
45
  // the one that owns the process.
46
  QProcess gpg;
409 ✔
47
  gpg.setProcessEnvironment(env);
409 ✔
48
  const int rc = Executor::executeBlocking(
409 ✔
49
      gpg, gpgExe,
50
      QStringList{"-d", "--quiet", "--yes", "--no-encrypt-to", "--batch",
3,272 ✔
51
                  "--use-agent"} +
2,454 ✔
52
          Pass::maxOutputArgs() + QStringList{translatedPath},
1,636 ✔
53
      QString(), &plaintext, nullptr, cancel);
409 ✔
54
  if (rc != 0 || plaintext.isEmpty())
409 ✔
55
    return {};
4 ✔
56
  // Line by line over the text, never a list of every line (a compressed
57
  // entry can expand into tens of millions), and a bounded number taken.
58
  QStringList matches;
405 ✔
59
  for (QStringView line : QStringView(plaintext).tokenize(u'\n')) {
60,942 ✔
60
    if (line.endsWith(u'\r'))
61
      line.chop(1);
62
    const QStringView t = line.trimmed();
63
    if (t.isEmpty() || !rx.matchView(line).hasMatch())
90,910 ✔
64
      continue;
65
    if (matches.size() == kMaxMatchesPerEntry) {
30,272 ✔
66
      matches << QString(); // isMoreMarker(): not a match, not counted
101 ✔
67
      break;
68
    }
69
    matches << t.toString();
30,171 ✔
70
  }
71
  return matches;
72
}
818 ✔
73

74
/**
75
 * @brief Walk the store, decrypt every .gpg file, collect matches.
76
 */
77
auto NativeGrep::scanStore(const QProcessEnvironment &env,
13 ✔
78
                           const QString &gpgExe, const QString &storeDir,
79
                           const QRegularExpression &rx,
80
                           const std::atomic_bool *cancel)
81
    -> QList<QPair<QString, QStringList>> {
82
  QList<QPair<QString, QStringList>> results;
13 ✔
83
  int total = 0; // matching lines, the markers not counted
84
  // Regular files only: a link or junction is not an entry, and searching
85
  // through one would decrypt files outside the store.
86
  const QStringList files =
87
      Util::regularFilesUnder(storeDir, QStringList() << "*.gpg");
39 ✔
88
  for (const QString &filePath : files) {
420 ✔
89
    if (QThread::currentThread()->isInterruptionRequested() ||
409 ✔
90
        (cancel != nullptr && cancel->load()))
64 ✔
91
      return {};
×
92
    if (total == kMaxMatches) {
409 ✔
93
      // The budget is spent and entries remain: the search stopped here.
94
      results.append({QString(), {}});
2 ✔
95
      break;
2 ✔
96
    }
97
    QStringList matches = matchFile(env, gpgExe, filePath, rx, cancel);
407 ✔
98
    if (matches.isEmpty())
407 ✔
99
      continue;
100
    const bool more = isMoreMarker(matches.constLast());
101
    if (more)
404 ✔
102
      matches.removeLast();
100 ✔
103
    // The store-wide budget: what still fits, and a marker when that cut
104
    // the entry short.
105
    const int room = kMaxMatches - total;
404 ✔
106
    if (matches.size() > room) {
404 ✔
107
      matches = matches.mid(0, room);
2 ✔
108
      matches << QString();
1 ✔
109
    } else if (more) {
403 ✔
110
      matches << QString();
100 ✔
111
    }
112
    total += static_cast<int>(matches.size()) -
404 ✔
113
             (isMoreMarker(matches.constLast()) ? 1 : 0);
404 ✔
114
    QString entry = QDir(storeDir).relativeFilePath(filePath);
404 ✔
115
    if (entry.endsWith(QLatin1String(".gpg")))
404 ✔
116
      entry.chop(4);
404 ✔
117
    results.append({entry, matches});
404 ✔
118
  }
119
  return results;
120
}
121

122
/**
123
 * @brief Start a search on a worker thread.
124
 *
125
 * Results are emitted on the owner's thread via QMetaObject::invokeMethod. A
126
 * sequence counter discards results from superseded searches; the previous
127
 * search is asked to stop but not waited for, since blocking the UI thread
128
 * while gpg decrypts would freeze the interface.
129
 */
130
void NativeGrep::search(const QString &pattern, bool caseInsensitive,
10 ✔
131
                        const QString &gpgExe, const QString &storeDir,
132
                        const QProcessEnvironment &env) {
133
  cancel();
10 ✔
134

135
  // Advance the sequence before any early return so in-flight workers from the
136
  // previous query fail the seq check and cannot publish stale results.
137
  const int seq = ++m_seq;
10 ✔
138

139
  // Use trimmed() rather than isEmpty(): a whitespace-only string is a valid
140
  // regex that matches every non-empty line, which is almost never intentional
141
  // and would decrypt the entire store.
142
  //
143
  // Both early returns post finished() via Qt::QueuedConnection so that the
144
  // signal is always delivered asynchronously after search() returns, matching
145
  // the contract of the threaded path.
146
  if (pattern.trimmed().isEmpty()) {
10 ✔
147
    QMetaObject::invokeMethod(
×
148
        this,
149
        [this, seq]() {
×
150
          if (m_seq == seq)
×
151
            emit finished({});
×
152
        },
×
153
        Qt::QueuedConnection);
154
    return;
1 ✔
155
  }
156

157
  const QRegularExpression rx(
158
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
159
                               : QRegularExpression::PatternOptions{});
20 ✔
160
  if (!rx.isValid()) {
10 ✔
161
    QMetaObject::invokeMethod(
1 ✔
162
        this,
163
        [this, seq]() {
1 ✔
164
          if (m_seq == seq)
1 ✔
165
            emit finished({});
2 ✔
166
        },
1 ✔
167
        Qt::QueuedConnection);
168
    return;
169
  }
170
  QPointer<NativeGrep> self(this);
171

172
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
18 ✔
173
    if (!self)
9 ✔
174
      return;
175
    QMetaObject::invokeMethod(
9 ✔
176
        self,
177
        [self, seq, results = std::move(results)]() {
27 ✔
178
          if (self && self->m_seq == seq)
18 ✔
179
            emit self->finished(results);
9 ✔
180
        },
9 ✔
181
        Qt::QueuedConnection);
182
  };
9 ✔
183

184
  auto cancelFlag = std::make_shared<std::atomic_bool>(false);
9 ✔
185
  QThread *thread = QThread::create([gpgExe, storeDir, env, rx, cancelFlag,
18 ✔
186
                                     emitResults = std::move(emitResults)]() {
187
    std::move(emitResults)(
9 ✔
188
        scanStore(env, gpgExe, storeDir, rx, cancelFlag.get()));
9 ✔
189
  });
9 ✔
190

191
  m_workers.append({thread, cancelFlag});
18 ✔
192
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
9 ✔
193
  connect(thread, &QThread::finished, this, [this, thread]() {
9 ✔
194
    m_workers.removeIf(
9 ✔
195
        [thread](const Worker &w) { return w.thread == thread; });
9 ✔
196
  });
197
  thread->start();
9 ✔
198
}
19 ✔
199

200
void NativeGrep::cancel() {
392 ✔
201
  for (const Worker &w : std::as_const(m_workers)) {
393 ✔
202
    w.cancel->store(true);
203
    if (w.thread && w.thread->isRunning())
1 ✔
204
      w.thread->requestInterruption();
1 ✔
205
  }
206
}
392 ✔
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