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

IJHack / QtPass / 35231775420

17 Sep 2026 02:10PM UTC coverage: 75.981% (+0.2%) from 75.806%
35231775420

push

github

web-flow
ImitatePass: extract NativeGrep and GpgIdSigner, compose simpleTransaction (#1814)

* ImitatePass: extract NativeGrep and GpgIdSigner, compose simpleTransaction

Three pieces leave the 1500-line backend:

- NativeGrep owns the threaded decrypt-and-match search: the sequence
  counter, the worker threads and their shutdown, matchFile()/scanStore().
  ImitatePass::Grep() calls search() and relays finished() as finishedGrep;
  its destructor cancels the searches first so they wind down while the
  re-encryption worker is joined.
- GpgIdSigner is the .gpg-id signature logic: haveSecretKey()
  (KEY_CONSIDERED), sign() (first key only, --yes --detach-sign, WSL path
  translation) and verify() (VALIDSIG fingerprint or primary fingerprint
  against the configured keys; passes with no key configured, as pass does).
  Every gpg run goes through an Exec callback, so ImitatePass hands in its
  cancellable execBlocking() and the re-encryption worker keeps its
  interruptible verification, and ProfileInit uses the same sign() instead
  of its own copy of the gpg invocation.
- simpleTransaction is a member, not a private base.

tst_gpgidsigner drives the signer with a scripted gpg: arguments, first-key
rule, stderr reporting, both accepted fingerprints, rejection of a valid
signature by someone else, exit 0 without VALIDSIG, path cleaning, and the
default runner. Existing tests follow the renames.

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

* GpgIdSigner: the keys are fingerprints; reflow a comment

The class comment promised that key IDs verify; gpg reports full
fingerprints in KEY_CONSIDERED and VALIDSIG, so only a 40-hex fingerprint
satisfies haveSecretKey() and verify() (pass requires the same). The
renamed comment in tst_integration ran past column 80.

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

... (continued)

156 of 179 new or added lines in 5 files covered. (87.15%)

1 existing line in 1 file now uncovered.

5947 of 7827 relevant lines covered (75.98%)

75.01 hits per line

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

76.39
/src/profileinit.cpp
1
// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "profileinit.h"
4
#include "appsettings.h"
5
#include "executor.h"
6
#include "gpgidsigner.h"
7
#include "userinfo.h"
8
#include <QDir>
9
#include <QFile>
10
#include <QFileInfo>
11
#include <QProcess>
12

13
auto ProfileInit::needsInit(const QString &path) -> bool {
7 ✔
14
  if (path.isEmpty()) {
7 ✔
15
    return false;
16
  }
17
  QDir dir(path);
6 ✔
18
  if (!dir.exists()) {
6 ✔
19
    return false;
20
  }
21
  return !dir.exists(".gpg-id");
5 ✔
22
}
6 ✔
23

24
auto ProfileInit::initialise(const QString &dir, const QList<UserInfo> &users,
5 ✔
25
                             const AppSettings &s, bool useGit, QString *note)
26
    -> bool {
27
  QString scratch;
5 ✔
28
  QString &out = note != nullptr ? *note : scratch;
5 ✔
29
  out.clear();
5 ✔
30

31
  const QDir folder(dir);
5 ✔
32
  if (!folder.exists() && !QDir().mkpath(folder.absolutePath())) {
9 ✔
33
    out = tr("Could not create %1.").arg(folder.absolutePath());
×
34
    return false;
×
35
  }
36
  const QString gpgIdFile = folder.filePath(QStringLiteral(".gpg-id"));
10 ✔
37
  if (!writeGpgId(gpgIdFile, users, &out)) {
5 ✔
38
    return false;
39
  }
40
  QString sigFile;
4 ✔
41
  if (!s.passSigningKey.trimmed().isEmpty()) {
8 ✔
42
    if (!signGpgId(gpgIdFile, s, &out)) {
×
43
      return false;
44
    }
45
    sigFile = gpgIdFile + QStringLiteral(".sig");
×
46
  }
47
  if (useGit &&
5 ✔
48
      !commitGpgId(folder.absolutePath(), gpgIdFile, sigFile, s, &out)) {
5 ✔
49
    return false;
50
  }
51

52
  const QStringList existing = folder.entryList(
4 ✔
53
      {QStringLiteral("*.gpg")}, QDir::Files | QDir::NoDotAndDotDot);
12 ✔
54
  if (!existing.isEmpty()) {
4 ✔
55
    out = tr("%1 already contains %n encrypted file(s); they were not "
1 ✔
56
             "re-encrypted. Switch to the profile and open Users to do that.",
57
             nullptr, static_cast<int>(existing.size()))
58
              .arg(folder.absolutePath());
2 ✔
59
  }
60
  return true;
61
}
5 ✔
62

63
auto ProfileInit::writeGpgId(const QString &gpgIdFile,
5 ✔
64
                             const QList<UserInfo> &users, QString *note)
65
    -> bool {
66
  QStringList ids;
5 ✔
67
  for (const UserInfo &user : users) {
15 ✔
68
    if (user.enabled) {
10 ✔
69
      ids << user.key_id;
4 ✔
70
    }
71
  }
72
  if (ids.isEmpty()) {
5 ✔
73
    *note = tr("No recipient selected; %1 was not written.").arg(gpgIdFile);
2 ✔
74
    return false;
1 ✔
75
  }
76
  QFile file(gpgIdFile);
4 ✔
77
  if (!file.open(QIODevice::WriteOnly | QIODevice::Text) ||
8 ✔
78
      file.write((ids.join(QLatin1Char('\n')) + QLatin1Char('\n')).toUtf8()) <
24 ✔
79
          0) {
80
    *note = tr("Could not write %1: %2").arg(gpgIdFile, file.errorString());
×
81
    return false;
×
82
  }
83
  file.close();
4 ✔
84
  // Same as ImitatePass::writeGpgIdFile: the recipient list leaks which keys
85
  // the store is encrypted to, so keep it owner-only where that means
86
  // anything.
87
  QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
4 ✔
88
  return true;
89
}
4 ✔
90

91
auto ProfileInit::signGpgId(const QString &gpgIdFile, const AppSettings &s,
×
92
                            QString *note) -> bool {
93
  const GpgIdSigner signer(s.gpgExecutable,
NEW
94
                           GpgIdSigner::keysFromSetting(s.passSigningKey));
×
95
  QString err;
×
NEW
96
  if (!signer.sign(gpgIdFile, &err)) {
×
NEW
97
    *note = tr("Could not sign %1 with %2: %3")
×
NEW
98
                .arg(gpgIdFile, signer.keys().first(), err.trimmed());
×
UNCOV
99
    return false;
×
100
  }
101
  return true;
102
}
×
103

104
auto ProfileInit::commitGpgId(const QString &dir, const QString &gpgIdFile,
1 ✔
105
                              const QString &sigFile, const AppSettings &s,
106
                              QString *note) -> bool {
107
  // A process of our own in the profile directory: nothing here may touch
108
  // the shared Executor or PASSWORD_STORE_DIR of the active store.
109
  QProcess git;
1 ✔
110
  git.setWorkingDirectory(dir);
1 ✔
111
  auto run = [&](const QStringList &args) -> bool {
3 ✔
112
    QString err;
3 ✔
113
    const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
3 ✔
114
                                             QString(), nullptr, &err);
3 ✔
115
    if (rc != 0) {
3 ✔
116
      *note =
117
          tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
×
118
      return false;
×
119
    }
120
    return true;
121
  };
1 ✔
122
  QStringList files{QFileInfo(gpgIdFile).fileName()};
3 ✔
123
  if (!sigFile.isEmpty()) {
1 ✔
124
    files << QFileInfo(sigFile).fileName();
×
125
  }
126
  const QStringList add =
127
      QStringList{QStringLiteral("add"), QStringLiteral("--")} + files;
4 ✔
128
  const QStringList commit =
129
      QStringList{QStringLiteral("commit"), QStringLiteral("-m"),
7 ✔
130
                  QStringLiteral("Added .gpg-id using QtPass."),
1 ✔
131
                  QStringLiteral("--")} +
5 ✔
132
      files;
133
  return run({QStringLiteral("init")}) && run(add) && run(commit);
4 ✔
134
}
2 ✔
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