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

IJHack / QtPass / 35467906150

19 Sep 2026 08:36PM UTC coverage: 77.868% (-0.3%) from 78.167%
35467906150

Pull #1851

github

web-flow
Merge 27c744ca6 into d67d8e6cc
Pull Request #1851: Store walks visit real directories only; junctions and links are not entries

74 of 117 new or added lines in 6 files covered. (63.25%)

97 existing lines in 6 files now uncovered.

6843 of 8788 relevant lines covered (77.87%)

74.55 hits per line

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

80.46
/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 "util.h"
6
#include <QDir>
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) {}
90✔
15

16
NativeGrep::~NativeGrep() {
90✔
17
  static constexpr int kThreadTimeoutMs = 5000;
18
  cancel();
90✔
19
  QElapsedTimer elapsed;
90✔
20
  elapsed.start();
90✔
21
  for (const Worker &w : std::as_const(m_workers)) {
90✔
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
}
90✔
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✔
UNCOV
40
    QString wslPath;
×
UNCOV
41
    const int wrc = Executor::executeBlocking(
×
UNCOV
42
        QStringLiteral("wsl"),
×
UNCOV
43
        Executor::wslExecArgs(QStringLiteral("wslpath"), {filePath}), &wslPath);
×
44
    const QString translated = wslPath.trimmed();
UNCOV
45
    if (wrc == 0 && !translated.isEmpty())
×
UNCOV
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✔
UNCOV
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
  // Regular files only: a link or junction is not an entry, and searching
83
  // through one would decrypt files outside the store.
84
  const QStringList files =
85
      Util::regularFilesUnder(storeDir, QStringList() << "*.gpg");
18✔
86
  for (const QString &filePath : files) {
15✔
87
    if (QThread::currentThread()->isInterruptionRequested() ||
9✔
88
        (cancel != nullptr && cancel->load()))
9✔
UNCOV
89
      return {};
×
90
    const QStringList matches = matchFile(env, gpgExe, filePath, rx, cancel);
9✔
91
    if (!matches.isEmpty()) {
9✔
92
      QString entry = QDir(storeDir).relativeFilePath(filePath);
6✔
93
      if (entry.endsWith(QLatin1String(".gpg")))
6✔
94
        entry.chop(4);
6✔
95
      results.append({entry, matches});
6✔
96
    }
97
  }
98
  return results;
99
}
100

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

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

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

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

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

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

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

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