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

IJHack / QtPass / 36411484084

28 Sep 2026 10:44AM UTC coverage: 92.898% (-0.03%) from 92.924%
36411484084

push

github

web-flow
PasswordDialog: keep the entry when saving it fails (#1951)

* PasswordDialog: keep the entry when saving it fails

The dialog closed as soon as OK was pressed and started the insert on its
way out; gpg ran after exec() had returned. When that failed (a missing
or expired recipient key, the empty .gpg-id of #1944, a refused path)
the error was shown and what was typed was gone.

accept() now starts the insert itself and stays open, fields and OK
locked, "Saving…" in the status line, until the backend reports:
- finishedInsert closes the dialog as accepted;
- the new Pass::insertFailed (emitted next to processErrorExit for a
  failed PASS_INSERT, as generateGPGKeysFailed is for key generation)
  unlocks the fields, which still hold the entry, and shows the reason;
- a refusal before anything starts comes through critical() during the
  Insert() call and is handled the same way.
A new entry that is on disk after a failure was written and only a later
git step failed; the dialog then closes and the main window reports git.
An existing entry OK'd before its decrypt arrived now leaves the dialog
open instead of closing without saving.

FieldLabel also consumes Enter: confirming a field rename used to reach
the dialog's default button and save and close the whole entry.

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

* PasswordDialog: wait out a pending save; the backend says what was written

Review follow-ups:
- Cancel, Esc and the window's close button are refused while an insert
  is in flight (reject() override, Cancel disabled): a failure after the
  dialog was gone had nothing left to hand back.
- The template box and Ctrl+T are locked while saving; another template
  would rebuild the field rows under the pending insert.
- "The file is there now" was no proof that this insert wrote it: another
  writer could create the name meanwhile. insertFailed() now carries
  `writ... (continued)

72 of 77 new or added lines in 7 files covered. (93.51%)

44 existing lines in 4 files now uncovered.

9065 of 9758 relevant lines covered (92.9%)

191.64 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) {}
181✔
15

16
NativeGrep::~NativeGrep() {
181✔
17
  static constexpr int kThreadTimeoutMs = 5000;
18
  cancel();
181✔
19
  QElapsedTimer elapsed;
181✔
20
  elapsed.start();
181✔
21
  for (const Worker &w : std::as_const(m_workers)) {
181✔
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
}
181✔
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() {
372✔
172
  for (const Worker &w : std::as_const(m_workers)) {
373✔
173
    w.cancel->store(true);
174
    if (w.thread && w.thread->isRunning())
1✔
175
      w.thread->requestInterruption();
1✔
176
  }
177
}
372✔
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