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

IJHack / QtPass / 35766441897

22 Sep 2026 06:19PM UTC coverage: 92.968% (-0.01%) from 92.98%
35766441897

push

github

web-flow
Hide the password dialog before it is destroyed; a reload forgets what was typed (#1907)

* Hide the dialog before it is destroyed; a reload forgets what was typed

From the adversarial review of this branch.

The destructor disconnected the line edits, which left the other way into a
slot of the destroyed dialog open: FieldLabel's rename editor commits through
editingFinished() -> finishEdit() -> renamed() -> renameField(), and ASAN
shows the use-after-free when a shown dialog with an open editor is deleted.
The destructor hides the dialog instead, while the object is whole, so every
focus-out slot runs on it and QDialog's own hide is a no-op.

setPassword() reuses the template line edits and refills them, so the mark
that says "the user typed in this field" had to go with the old value: a
second decrypt landing for the same entry (the tree view issues one, the
dialog another) otherwise turned a loaded backup code into an otpauth URI.

validateOtpField() and normalizeOtpField() act on the line hookOtpField()
wired up rather than re-resolving otpLineEdit(): the signals that reach them
are that line's, and which field is the OTP one is settled when the fields
change, not by what the user types into them.

Three tests (each red on the previous commit, the reload one also under the
mutation that drops the mark reset), and the rename in the first test is now
committed by leaving the editor: Return also reaches the dialog's default
button, which accepted the dialog and left the rest of that test running on a
hidden, already-saved one.

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

* tests: wait for the row before editing its name

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

---------

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

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

1 existing line in 1 file now uncovered.

8792 of 9457 relevant lines covered (92.97%)

149.62 hits per line

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

86.42
/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) {}
178 ✔
15

16
NativeGrep::~NativeGrep() {
178 ✔
17
  static constexpr int kThreadTimeoutMs = 5000;
18
  cancel();
178 ✔
19
  QElapsedTimer elapsed;
178 ✔
20
  elapsed.start();
178 ✔
21
  for (const Worker &w : std::as_const(m_workers)) {
178 ✔
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
}
178 ✔
30

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

65
/**
66
 * @brief Walk the store, decrypt every .gpg file, collect matches.
67
 */
68
auto NativeGrep::scanStore(const QProcessEnvironment &env,
10 ✔
69
                           const QString &gpgExe, const QString &storeDir,
70
                           const QRegularExpression &rx,
71
                           const std::atomic_bool *cancel)
72
    -> QList<QPair<QString, QStringList>> {
73
  QList<QPair<QString, QStringList>> results;
10 ✔
74
  // Regular files only: a link or junction is not an entry, and searching
75
  // through one would decrypt files outside the store.
76
  const QStringList files =
77
      Util::regularFilesUnder(storeDir, QStringList() << "*.gpg");
30 ✔
78
  for (const QString &filePath : files) {
74 ✔
79
    if (QThread::currentThread()->isInterruptionRequested() ||
64 ✔
80
        (cancel != nullptr && cancel->load()))
64 ✔
81
      return {};
×
82
    const QStringList matches = matchFile(env, gpgExe, filePath, rx, cancel);
64 ✔
83
    if (!matches.isEmpty()) {
64 ✔
84
      QString entry = QDir(storeDir).relativeFilePath(filePath);
61 ✔
85
      if (entry.endsWith(QLatin1String(".gpg")))
61 ✔
86
        entry.chop(4);
61 ✔
87
      results.append({entry, matches});
61 ✔
88
    }
89
  }
90
  return results;
91
}
92

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

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

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

128
  const QRegularExpression rx(
129
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
130
                               : QRegularExpression::PatternOptions{});
20 ✔
131
  if (!rx.isValid()) {
10 ✔
132
    QMetaObject::invokeMethod(
1 ✔
133
        this,
134
        [this, seq]() {
1 ✔
135
          if (m_seq == seq)
1 ✔
136
            emit finished({});
2 ✔
137
        },
1 ✔
138
        Qt::QueuedConnection);
139
    return;
140
  }
141
  QPointer<NativeGrep> self(this);
142

143
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
18 ✔
144
    if (!self)
9 ✔
145
      return;
146
    QMetaObject::invokeMethod(
9 ✔
147
        self,
148
        [self, seq, results = std::move(results)]() {
27 ✔
149
          if (self && self->m_seq == seq)
18 ✔
150
            emit self->finished(results);
9 ✔
151
        },
9 ✔
152
        Qt::QueuedConnection);
153
  };
9 ✔
154

155
  auto cancelFlag = std::make_shared<std::atomic_bool>(false);
9 ✔
156
  QThread *thread = QThread::create([gpgExe, storeDir, env, rx, cancelFlag,
18 ✔
157
                                     emitResults = std::move(emitResults)]() {
158
    std::move(emitResults)(
9 ✔
159
        scanStore(env, gpgExe, storeDir, rx, cancelFlag.get()));
9 ✔
160
  });
9 ✔
161

162
  m_workers.append({thread, cancelFlag});
18 ✔
163
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
9 ✔
164
  connect(thread, &QThread::finished, this, [this, thread]() {
9 ✔
165
    m_workers.removeIf(
9 ✔
166
        [thread](const Worker &w) { return w.thread == thread; });
9 ✔
167
  });
168
  thread->start();
9 ✔
169
}
19 ✔
170

171
void NativeGrep::cancel() {
366 ✔
172
  for (const Worker &w : std::as_const(m_workers)) {
367 ✔
173
    w.cancel->store(true);
174
    if (w.thread && w.thread->isRunning())
1 ✔
175
      w.thread->requestInterruption();
1 ✔
176
  }
177
}
366 ✔
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