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

IJHack / QtPass / 24862562077

23 Apr 2026 10:43PM UTC coverage: 26.84% (+0.06%) from 26.78%
24862562077

Pull #1145

github

web-flow
Merge 00a7d617e into 3f475ec1a
Pull Request #1145: docs: update AGENTS.md with niche knowledge

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

1 existing line in 1 file now uncovered.

1641 of 6114 relevant lines covered (26.84%)

28.95 hits per line

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

68.37
/src/pass.cpp
1
// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "pass.h"
4
#include "gpgkeystate.h"
5
#include "helpers.h"
6
#include "qtpasssettings.h"
7
#include "util.h"
8
#include <QCoreApplication>
9
#include <QDebug>
10
#include <QDir>
11
#include <QFileInfo>
12
#include <QProcess>
13
#include <QRandomGenerator>
14
#include <QRegularExpression>
15
#include <algorithm>
16
#include <utility>
17

18
#ifdef QT_DEBUG
19
#include "debughelper.h"
20
#endif
21

22
using Enums::GIT_INIT;
23
using Enums::GIT_PULL;
24
using Enums::GIT_PUSH;
25
using Enums::GPG_GENKEYS;
26
using Enums::PASS_COPY;
27
using Enums::PASS_GREP;
28
using Enums::PASS_INIT;
29
using Enums::PASS_INSERT;
30
using Enums::PASS_MOVE;
31
using Enums::PASS_OTP_GENERATE;
32
using Enums::PASS_REMOVE;
33
using Enums::PASS_SHOW;
34

35
namespace {
36
auto fallbackCharset(const QString &input, const QString &fallback) -> QString {
37
  return input.isEmpty() ? fallback : input;
1,226✔
38
}
39

40
auto effectiveCharset(const PasswordConfiguration &passConfig) -> QString {
22✔
41
  int sel = passConfig.selected;
22✔
42
  if (sel < 0 || sel >= PasswordConfiguration::CHARSETS_COUNT)
22✔
43
    sel = PasswordConfiguration::ALLCHARS;
44
  return fallbackCharset(
45
      passConfig.Characters[sel],
22✔
46
      passConfig.Characters[PasswordConfiguration::ALLCHARS]);
22✔
47
}
48
} // namespace
49

50
/**
51
 * @brief Pass::Pass wrapper for using either pass or the pass imitation
52
 */
53
Pass::Pass() : wrapperRunning(false), env(QProcess::systemEnvironment()) {
35✔
54
  connect(&exec,
35✔
55
          static_cast<void (Executor::*)(int, int, const QString &,
56
                                         const QString &)>(&Executor::finished),
57
          this, &Pass::finished);
35✔
58

59
  // This was previously using direct QProcess signals.
60
  // The code now uses Executor instead of raw QProcess for better control.
61
  // connect(&process, SIGNAL(error(QProcess::ProcessError)), this,
62
  //        SIGNAL(error(QProcess::ProcessError)));
63

64
  connect(&exec, &Executor::starting, this, &Pass::startingExecuteWrapper);
35✔
65
  // Merge our vars into WSLENV rather than blindly appending a duplicate entry
66
  const QStringList wslenvVars = {
67
      QStringLiteral("PASSWORD_STORE_DIR/p"),
70✔
68
      QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH/w"),
35✔
69
      QStringLiteral("PASSWORD_STORE_CHARACTER_SET/w")};
175✔
70
  const QString wslenvPrefix = QStringLiteral("WSLENV=");
35✔
71
  auto it =
72
      std::find_if(env.begin(), env.end(), [&wslenvPrefix](const QString &s) {
70✔
73
        return s.startsWith(wslenvPrefix);
4,494✔
74
      });
75
  if (it == env.end()) {
35✔
76
    env.append(wslenvPrefix + wslenvVars.join(':'));
70✔
77
  } else {
78
    QStringList parts =
79
        it->mid(wslenvPrefix.size()).split(':', Qt::SkipEmptyParts);
×
80
    for (const QString &v : wslenvVars) {
×
81
      if (!parts.contains(v))
×
82
        parts.append(v);
83
    }
84
    *it = wslenvPrefix + parts.join(':');
×
85
  }
86
}
35✔
87

88
/**
89
 * @brief Executes a wrapper command.
90
 * @param id Process ID
91
 * @param app Application to execute
92
 * @param args Arguments
93
 * @param readStdout Whether to read stdout
94
 * @param readStderr Whether to read stderr
95
 */
96
void Pass::executeWrapper(PROCESS id, const QString &app,
×
97
                          const QStringList &args, bool readStdout,
98
                          bool readStderr) {
99
  executeWrapper(id, app, args, QString(), readStdout, readStderr);
×
100
}
×
101

102
void Pass::executeWrapper(PROCESS id, const QString &app,
29✔
103
                          const QStringList &args, QString input,
104
                          bool readStdout, bool readStderr) {
105
#ifdef QT_DEBUG
106
  dbg() << app << args;
107
#endif
108
  exec.execute(id, QtPassSettings::getPassStore(), app, args, std::move(input),
58✔
109
               readStdout, readStderr);
110
}
29✔
111

112
/**
113
 * @brief Initializes the pass wrapper environment.
114
 */
115
void Pass::init() {
15✔
116
#ifdef __APPLE__
117
  // If it exists, add the gpgtools to PATH
118
  if (QFile("/usr/local/MacGPG2/bin").exists())
119
    env.replaceInStrings("PATH=", "PATH=/usr/local/MacGPG2/bin:");
120
  // Add missing /usr/local/bin
121
  if (env.filter("/usr/local/bin").isEmpty())
122
    env.replaceInStrings("PATH=", "PATH=/usr/local/bin:");
123
#endif
124

125
  if (!QtPassSettings::getGpgHome().isEmpty()) {
30✔
126
    QDir absHome(QtPassSettings::getGpgHome());
28✔
127
    absHome.makeAbsolute();
14✔
128
    env << "GNUPGHOME=" + absHome.path();
14✔
129
  }
14✔
130
}
15✔
131

132
/**
133
 * @brief Pass::Generate use either pwgen or internal password
134
 * generator
135
 * @param length of the desired password
136
 * @param charset to use for generation
137
 * @return the password
138
 */
139
auto Pass::generatePassword(unsigned int length, const QString &charset)
1,205✔
140
    -> QString {
141
  if (length == 0) {
1,205✔
142
    emit critical(tr("Invalid password length"),
2✔
143
                  tr("Can't generate password with zero length."));
1✔
144
    return {};
145
  }
146
  QString passwd;
1,204✔
147
  if (QtPassSettings::isUsePwgen()) {
1,204✔
148
    // --secure goes first as it overrides --no-* otherwise
149
    QStringList args;
×
150
    args.append("-1");
×
151
    if (!QtPassSettings::isLessRandom()) {
×
152
      args.append("--secure");
×
153
    }
154
    args.append(QtPassSettings::isAvoidCapitals() ? "--no-capitalize"
×
155
                                                  : "--capitalize");
156
    args.append(QtPassSettings::isAvoidNumbers() ? "--no-numerals"
×
157
                                                 : "--numerals");
158
    if (QtPassSettings::isUseSymbols()) {
×
159
      args.append("--symbols");
×
160
    }
161
    args.append(QString::number(length));
×
162
    // executeBlocking returns 0 on success, non-zero on failure
163
    if (Executor::executeBlocking(QtPassSettings::getPwgenExecutable(), args,
×
164
                                  &passwd) == 0) {
165
      static const QRegularExpression literalNewLines{"[\\n\\r]"};
×
166
      passwd.remove(literalNewLines);
×
167
    } else {
168
      passwd.clear();
×
169
#ifdef QT_DEBUG
170
      qDebug() << __FILE__ << ":" << __LINE__ << "\t"
171
               << "pwgen fail";
172
#endif
173
      // Error is already handled by clearing passwd; no need for critical
174
      // signal here
175
    }
176
  } else {
177
    // Validate charset - if CUSTOM is selected but chars are empty,
178
    // fall back to ALLCHARS to prevent weak passwords (issue #780)
179
    const QString cs = fallbackCharset(
180
        charset, QtPassSettings::getPasswordConfiguration()
2,408✔
181
                     .Characters[PasswordConfiguration::ALLCHARS]);
182
    if (cs.length() > 0) {
1,204✔
183
      passwd = generateRandomPassword(cs, length);
2,408✔
184
    } else {
185
      emit critical(
×
186
          tr("No characters chosen"),
×
187
          tr("Can't generate password, there are no characters to choose from "
×
188
             "set in the configuration!"));
189
    }
190
  }
191
  return passwd;
192
}
193

194
/**
195
 * @brief Pass::gpgSupportsEd25519 check if GPG supports ed25519 (ECC)
196
 * GPG 2.1+ supports ed25519 which is much faster for key generation
197
 * @return true if ed25519 is supported
198
 */
199
bool Pass::gpgSupportsEd25519() {
2✔
200
  QString out, err;
2✔
201
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(),
8✔
202
                                {"--version"}, &out, &err) != 0) {
203
    return false;
204
  }
205
  QRegularExpression versionRegex(R"(gpg \(GnuPG\) (\d+)\.(\d+))");
×
206
  QRegularExpressionMatch match = versionRegex.match(out);
×
207
  if (!match.hasMatch()) {
×
208
    return false;
209
  }
210
  int major = match.captured(1).toInt();
×
211
  int minor = match.captured(2).toInt();
×
212
  return major > 2 || (major == 2 && minor >= 1);
×
213
}
2✔
214

215
/**
216
 * @brief Pass::getDefaultKeyTemplate return default key generation template
217
 * Uses ed25519 if supported, otherwise falls back to RSA
218
 * @return GPG batch template string
219
 */
220
QString Pass::getDefaultKeyTemplate() {
1✔
221
  if (gpgSupportsEd25519()) {
1✔
222
    return QStringLiteral("%echo Generating a default key\n"
×
223
                          "Key-Type: EdDSA\n"
224
                          "Key-Curve: Ed25519\n"
225
                          "Subkey-Type: ECDH\n"
226
                          "Subkey-Curve: Curve25519\n"
227
                          "Name-Real: \n"
228
                          "Name-Comment: QtPass\n"
229
                          "Name-Email: \n"
230
                          "Expire-Date: 0\n"
231
                          "%no-protection\n"
232
                          "%commit\n"
233
                          "%echo done");
234
  }
235
  return QStringLiteral("%echo Generating a default key\n"
1✔
236
                        "Key-Type: RSA\n"
237
                        "Subkey-Type: RSA\n"
238
                        "Name-Real: \n"
239
                        "Name-Comment: QtPass\n"
240
                        "Name-Email: \n"
241
                        "Expire-Date: 0\n"
242
                        "%no-protection\n"
243
                        "%commit\n"
244
                        "%echo done");
245
}
246

247
namespace {
248
auto resolveWslGpgconfPath(const QString &lastPart) -> QString {
3✔
249
  int lastSep = lastPart.lastIndexOf('/');
3✔
250
  if (lastSep < 0) {
3✔
251
    lastSep = lastPart.lastIndexOf('\\');
2✔
252
  }
253
  if (lastSep >= 0) {
2✔
254
    return lastPart.left(lastSep + 1) + "gpgconf";
2✔
255
  }
256
  return QStringLiteral("gpgconf");
2✔
257
}
258

259
/**
260
 * @brief Finds the path to the gpgconf executable in the same directory as the
261
 * given GPG path.
262
 * @example
263
 * QString result = findGpgconfInGpgDir(gpgPath);
264
 * std::cout << result.toStdString() << std::endl; // Expected output: path to
265
 * gpgconf or empty string
266
 *
267
 * @param gpgPath - Absolute path to a GPG executable or related file used to
268
 * locate gpgconf.
269
 * @return QString - The full path to gpgconf if found and executable; otherwise
270
 * an empty QString.
271
 */
272
QString findGpgconfInGpgDir(const QString &gpgPath) {
1✔
273
  QFileInfo gpgInfo(gpgPath);
1✔
274
  if (!gpgInfo.isAbsolute()) {
1✔
275
    return QString();
276
  }
277

278
  QDir dir(gpgInfo.absolutePath());
1✔
279

280
#ifdef Q_OS_WIN
281
  QFileInfo candidateExe(dir.filePath("gpgconf.exe"));
282
  if (candidateExe.isExecutable()) {
283
    return candidateExe.filePath();
284
  }
285
#endif
286

287
  QFileInfo candidate(dir.filePath("gpgconf"));
1✔
288
  if (candidate.isExecutable()) {
1✔
289
    return candidate.filePath();
×
290
  }
291
  return QString();
292
}
1✔
293

294
// Compatibility shim for Qt < 5.15 where QProcess::splitCommand is not
295
// available. Keep this fallback while supporting pre-5.15 builds; remove once
296
// the project's minimum supported Qt version is raised to 5.15 or newer.
297
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
298
/**
299
 * @brief Splits a command string into arguments while respecting quotes and
300
 * escape characters.
301
 * @example
302
 * QStringList result = splitCommandCompat("cmd \"arg one\" 'arg two'
303
 * escaped\\ space");
304
 * // Expected output: ["cmd", "arg one", "arg two", "escaped space"]
305
 *
306
 * @param command - The input command string to split into individual arguments.
307
 * @return QStringList - A list of parsed command arguments.
308
 */
309
QStringList splitCommandCompat(const QString &command) {
310
  QStringList result;
311
  QString current;
312
  bool inSingleQuote = false;
313
  bool inDoubleQuote = false;
314
  bool escaping = false;
315
  for (QChar ch : command) {
316
    if (escaping) {
317
      current.append(ch);
318
      escaping = false;
319
      continue;
320
    }
321
    if (ch == '\\') {
322
      escaping = true;
323
      continue;
324
    }
325
    if (ch == '\'' && !inDoubleQuote) {
326
      inSingleQuote = !inSingleQuote;
327
      continue;
328
    }
329
    if (ch == '"' && !inSingleQuote) {
330
      inDoubleQuote = !inDoubleQuote;
331
      continue;
332
    }
333
    if (ch.isSpace() && !inSingleQuote && !inDoubleQuote) {
334
      if (!current.isEmpty()) {
335
        result.append(current);
336
        current.clear();
337
      }
338
      continue;
339
    }
340
    current.append(ch);
341
  }
342
  if (escaping) {
343
    current.append('\\');
344
  }
345
  if (!current.isEmpty()) {
346
    result.append(current);
347
  }
348
  return result;
349
}
350
#endif
351

352
} // namespace
353

354
/**
355
 * @brief Resolves the appropriate gpgconf command from a given GPG executable
356
 * path or command string.
357
 * @example
358
 * ResolvedGpgconfCommand result = Pass::resolveGpgconfCommand("wsl.exe
359
 * /usr/bin/gpg"); std::cout << result.first.toStdString() << std::endl; //
360
 * Expected output sample
361
 *
362
 * @param const QString &gpgPath - Path or command string pointing to the GPG
363
 * executable.
364
 * @return ResolvedGpgconfCommand - A pair containing the resolved gpgconf
365
 * command and its arguments.
366
 */
367
auto Pass::resolveGpgconfCommand(const QString &gpgPath)
8✔
368
    -> ResolvedGpgconfCommand {
369
  if (gpgPath.trimmed().isEmpty()) {
8✔
370
    return {"gpgconf", {}};
371
  }
372

373
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
374
  QStringList parts = QProcess::splitCommand(gpgPath);
7✔
375
#else
376
  QStringList parts = splitCommandCompat(gpgPath);
377
#endif
378

379
  if (parts.isEmpty()) {
7✔
380
    return {"gpgconf", {}};
381
  }
382

383
  const QString first = parts.first();
384
  if (first.compare("wsl", Qt::CaseInsensitive) == 0 ||
16✔
385
      first.compare("wsl.exe", Qt::CaseInsensitive) == 0) {
9✔
386
    if (parts.size() >= 2 && parts.at(1).startsWith("sh")) {
9✔
387
      return {"gpgconf", {}};
388
    }
389
    if (parts.size() >= 2 &&
4✔
390
        QFileInfo(parts.last()).fileName().startsWith("gpg")) {
10✔
391
      QString wslGpgconf = resolveWslGpgconfPath(parts.last());
3✔
392
      parts.removeLast();
3✔
393
      parts.append(wslGpgconf);
394
      return {parts.first(), parts.mid(1)};
395
    }
396
    return {"gpgconf", {}};
397
  }
398

399
  if (!first.contains('/') && !first.contains('\\')) {
2✔
400
    return {"gpgconf", {}};
401
  }
402

403
  QString gpgconfPath = findGpgconfInGpgDir(first);
1✔
404
  if (!gpgconfPath.isEmpty()) {
1✔
405
    return {gpgconfPath, {}};
×
406
  }
407

408
  return {"gpgconf", {}};
409
}
8✔
410

411
/**
412
 * @brief Pass::GenerateGPGKeys internal gpg keypair generator . .
413
 * @param batch GnuPG style configuration string
414
 */
415
void Pass::GenerateGPGKeys(QString batch) {
×
416
  // Kill any stale GPG agents that might be holding locks on the key database
417
  // This helps avoid "database locked" timeouts during key generation
418
  QString gpgPath = QtPassSettings::getGpgExecutable();
×
419
  if (!gpgPath.isEmpty()) {
×
420
    ResolvedGpgconfCommand resolvedGpgconf = resolveGpgconfCommand(gpgPath);
×
421
    QStringList killArgs = resolvedGpgconf.arguments;
422
    killArgs << "--kill";
×
423
    killArgs << "gpg-agent";
×
424
    // Use same environment as key generation to target correct gpg-agent
425
    if (Executor::executeBlocking(env, resolvedGpgconf.program, killArgs) !=
×
426
        0) {
427
      qWarning() << "Failed to kill gpg-agent";
×
428
    }
429
  }
430

431
  executeWrapper(GPG_GENKEYS, gpgPath, {"--gen-key", "--no-tty", "--batch"},
×
432
                 std::move(batch));
433
}
×
434

435
/**
436
 * @brief Pass::listKeys list users
437
 * @param keystrings
438
 * @param secret list private keys
439
 * @return QList<UserInfo> users
440
 */
441
auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
×
442
  QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
×
443
  args.append(secret ? "--list-secret-keys" : "--list-keys");
×
444

445
  for (const QString &keystring : AS_CONST(keystrings)) {
×
446
    if (!keystring.isEmpty()) {
×
447
      args.append(keystring);
448
    }
449
  }
450
  QString p_out;
×
451
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
452
                                &p_out) != 0) {
453
    return QList<UserInfo>();
×
454
  }
455
  return parseGpgColonOutput(p_out, secret);
×
456
}
×
457

458
/**
459
 * @brief Pass::listKeys list users
460
 * @param keystring
461
 * @param secret list private keys
462
 * @return QList<UserInfo> users
463
 */
464
auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
×
465
  return listKeys(QStringList(keystring), secret);
×
466
}
467

468
/**
469
 * @brief Maps GPG stderr (which may include --status-fd 2 tokens) to a
470
 * user-friendly encryption error string.
471
 *
472
 * Checked in order: machine-readable [GNUPG:] status tokens first (locale-
473
 * independent), then case-insensitive substring fallbacks for GPG builds that
474
 * don't emit status tokens.
475
 *
476
 * @param err Raw stderr from GPG
477
 * @return Translated human-readable error, or empty string if not recognised
478
 */
479
namespace {
480

481
auto containsAny(const QString &str, const QStringList &patterns) -> bool {
31✔
482
  for (const QString &p : patterns) {
82✔
483
    if (str.contains(p)) {
58✔
484
      return true;
485
    }
486
  }
487
  return false;
488
}
489

490
/**
491
 * @brief Checks if str contains any of the patterns (case-insensitive).
492
 * @param str String to search in (will be lowercased once).
493
 * @param patterns List of patterns to search for (must be lowercase; caller
494
 * should convert patterns to lowercase before calling).
495
 * @return true if any pattern is found.
496
 */
497
auto containsAnyCaseInsensitive(const QString &str, const QStringList &patterns)
13✔
498
    -> bool {
499
  const QString lower = str.toLower();
500
  for (const QString &p : patterns) {
32✔
501
    if (lower.contains(p)) {
23✔
502
      return true;
503
    }
504
  }
505
  return false;
506
}
507

508
} // namespace
509

510
auto gpgErrorMessage(const QString &err) -> QString {
13✔
511
  // Machine-readable status tokens added by --status-fd 2
512
  if (containsAny(err, {QStringLiteral("[GNUPG:] KEYEXPIRED"),
65✔
513
                        QStringLiteral("[GNUPG:] INV_RECP 5 ")}))
13✔
514
    return QCoreApplication::translate(
515
        "Pass", "Encryption failed: GPG key has expired. Please renew or "
516
                "replace it.");
3✔
517
  if (containsAny(err, {QStringLiteral("[GNUPG:] KEYREVOKED"),
50✔
518
                        QStringLiteral("[GNUPG:] INV_RECP 4 ")}))
10✔
519
    return QCoreApplication::translate(
520
        "Pass", "Encryption failed: GPG key has been revoked.");
2✔
521
  if (containsAny(err, {QStringLiteral("[GNUPG:] NO_PUBKEY"),
40✔
522
                        QStringLiteral("[GNUPG:] INV_RECP")}))
8✔
523
    return QCoreApplication::translate(
524
        "Pass", "Encryption failed: recipient GPG key not found or invalid. "
525
                "Check that the key ID in .gpg-id is correct and imported.");
2✔
526
  if (err.contains(QStringLiteral("[GNUPG:] FAILURE")))
6✔
527
    return QCoreApplication::translate(
528
        "Pass", "Encryption failed. Check that your GPG key is valid.");
1✔
529

530
  // Locale-dependent fallbacks
531
  if (containsAnyCaseInsensitive(err, {QLatin1String("key has expired"),
20✔
532
                                       QLatin1String("key expired")}))
533
    return QCoreApplication::translate(
534
        "Pass", "Encryption failed: GPG key has expired. Please renew or "
535
                "replace it.");
1✔
536
  if (containsAnyCaseInsensitive(err, {QLatin1String("key has been revoked"),
16✔
537
                                       QLatin1String("revoked")}))
538
    return QCoreApplication::translate(
539
        "Pass", "Encryption failed: GPG key has been revoked.");
1✔
540
  if (containsAnyCaseInsensitive(err, {QLatin1String("no public key"),
15✔
541
                                       QLatin1String("unusable public key"),
542
                                       QLatin1String("no secret key")}))
543
    return QCoreApplication::translate(
544
        "Pass", "Encryption failed: recipient GPG key not found or invalid. "
545
                "Check that the key ID in .gpg-id is correct and imported.");
2✔
546
  if (containsAnyCaseInsensitive(err, {QLatin1String("encryption failed")}))
3✔
547
    return QCoreApplication::translate(
548
        "Pass", "Encryption failed. Check that your GPG key is valid.");
×
549

550
  return {};
551
}
×
552

553
namespace {
554
auto isGrepHeaderLine(const QString &rawLine, const QString &trimmedLine)
45✔
555
    -> bool {
556
  // ANSI-colored header starts with the blue escape; plain-text fallback:
557
  // a non-indented line ending with ':' (pass grep format without color)
558
  return rawLine.startsWith(QStringLiteral("\x1B[94m")) ||
123✔
559
         (!rawLine.startsWith(' ') && !rawLine.startsWith('\t') &&
91✔
560
          trimmedLine.endsWith(':'));
119✔
561
}
562
} // namespace
563

564
/**
565
 * @brief Parses 'pass grep' raw output into (entry, matches) pairs.
566
 *
567
 * pass grep emits ANSI blue color (\x1B[94m) at the start of each entry
568
 * header line. This is checked before stripping ANSI so headers are detected
569
 * reliably regardless of locale.
570
 */
571
auto parseGrepOutput(const QString &rawOut)
12✔
572
    -> QList<QPair<QString, QStringList>> {
573
  static const QRegularExpression ansi(
574
      QStringLiteral(R"(\x1B\[[0-9;]*[a-zA-Z])"));
13✔
575
  QList<QPair<QString, QStringList>> results;
12✔
576
  QString currentEntry;
12✔
577
  QStringList currentMatches;
12✔
578
  for (const QString &rawLine : rawOut.split('\n')) {
69✔
579
    QString line = rawLine;
580
    line.remove('\r');
45✔
581
    line.remove(ansi);
45✔
582
    line = line.trimmed();
45✔
583
    const bool isHeader = isGrepHeaderLine(rawLine, line);
45✔
584
    if (isHeader) {
45✔
585
      if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
14✔
586
        results.append({currentEntry, currentMatches});
3✔
587
      currentEntry = line.endsWith(':') ? line.chopped(1) : line;
14✔
588
      currentMatches.clear();
14✔
589
    } else if (!currentEntry.isEmpty()) {
31✔
590
      if (!line.isEmpty())
29✔
591
        currentMatches << line;
592
    }
593
  }
594
  if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
12✔
595
    results.append({currentEntry, currentMatches});
11✔
596
  return results;
12✔
597
}
598

599
/**
600
 * @brief Pass::processFinished reemits specific signal based on what process
601
 * has finished
602
 * @param id    id of Pass process that was scheduled and finished
603
 * @param exitCode  return code of a process
604
 * @param out   output generated by process(if capturing was requested, empty
605
 *              otherwise)
606
 * @param err   error output generated by process(if capturing was requested,
607
 *              or error occurred)
608
 */
609
void Pass::finished(int id, int exitCode, const QString &out,
30✔
610
                    const QString &err) {
611
  auto pid = static_cast<PROCESS>(id);
30✔
612

613
  if (exitCode != 0) {
30✔
614
    handleProcessError(pid, exitCode, out, err);
2✔
615
    return;
2✔
616
  }
617

618
  emitProcessFinishedSignal(pid, out, err);
28✔
619
}
620

621
void Pass::handleProcessError(PROCESS pid, int exitCode, const QString &out,
2✔
622
                              const QString &err) {
623
  Q_UNUSED(out);
624

625
  if (pid == PASS_GREP) {
2✔
626
    handleGrepError(exitCode, err);
2✔
627
    return;
2✔
628
  }
629

630
  if (pid == PASS_INSERT) {
×
631
    const QString friendly = gpgErrorMessage(err);
×
632
    if (!friendly.isEmpty()) {
×
633
      emit processErrorExit(exitCode, formatInsertError(friendly, err));
×
634
      return;
635
    }
636
  }
637

638
  emit processErrorExit(exitCode, err);
×
639
}
640

641
void Pass::handleGrepError(int exitCode, const QString &err) {
2✔
642
  if (exitCode == 1) {
2✔
643
    emit finishedGrep({});
2✔
644
  } else {
645
    emit processErrorExit(exitCode, err);
1✔
646
    emit finishedGrep({});
2✔
647
  }
648
}
2✔
649

650
auto Pass::formatInsertError(const QString &friendly, const QString &err)
×
651
    -> QString {
652
  QStringList humanLines;
×
653
  for (const QString &line : err.split('\n')) {
×
654
    QString cleanedLine = line;
655
    cleanedLine.remove('\r');
×
656
    if (!cleanedLine.startsWith(QLatin1String("[GNUPG:]")))
×
657
      humanLines.append(cleanedLine);
658
  }
659
  const QString humanErr = humanLines.join('\n').trimmed();
×
660
  return humanErr.isEmpty() ? friendly : friendly + "\n\n" + humanErr;
×
661
}
662

663
/**
664
 * @brief Emit the appropriate finished signal for a completed subprocess.
665
 *
666
 * Emits a specific Qt signal corresponding to the given process identifier; for
667
 * grep results the stdout is parsed into a list of matches before emitting.
668
 *
669
 * @param pid The process identifier indicating which finished signal to emit.
670
 * @param out Standard output produced by the process.
671
 * @param err Standard error produced by the process.
672
 */
673
void Pass::emitProcessFinishedSignal(PROCESS pid, const QString &out,
28✔
674
                                     const QString &err) {
675
  switch (pid) {
28✔
676
  case GIT_INIT:
×
677
    emit finishedGitInit(out, err);
×
678
    break;
×
679
  case GIT_PULL:
×
680
    emit finishedGitPull(out, err);
×
681
    break;
×
682
  case GIT_PUSH:
×
683
    emit finishedGitPush(out, err);
×
684
    break;
×
685
  case PASS_SHOW:
10✔
686
    emit finishedShow(out);
10✔
687
    break;
10✔
688
  case PASS_OTP_GENERATE:
×
689
    emit finishedOtpGenerate(out);
×
690
    break;
×
691
  case PASS_INSERT:
17✔
692
    emit finishedInsert(out, err);
17✔
693
    break;
17✔
694
  case PASS_REMOVE:
×
695
    emit finishedRemove(out, err);
×
696
    break;
×
697
  case PASS_INIT:
×
698
    emit finishedInit(out, err);
×
699
    break;
×
700
  case PASS_MOVE:
×
701
    emit finishedMove(out, err);
×
702
    break;
×
703
  case PASS_COPY:
×
704
    emit finishedCopy(out, err);
×
705
    break;
×
706
  case GPG_GENKEYS:
×
707
    emit finishedGenerateGPGKeys(out, err);
×
708
    break;
×
709
  case PASS_GREP:
1✔
710
    emit finishedGrep(parseGrepOutput(out));
1✔
711
    break;
1✔
712
  default:
713
#ifdef QT_DEBUG
714
    dbg() << "Unhandled process type" << pid;
715
#endif
716
    break;
717
  }
718
}
28✔
719

720
/**
721
 * @brief Set or remove a single environment variable in the local env list.
722
 *
723
 * The provided key must include a trailing '=' (e.g. "FOO="). Existing entries
724
 * whose text begins with the given key are removed before the new value is
725
 * applied. If value is non-empty the pair "key+value" is appended; if value is
726
 * empty the variable is removed.
727
 *
728
 * The function asserts if key does not end with '='; if the assertion is not
729
 * active it will emit a warning and return without modifying env.
730
 *
731
 * @param key Environment variable name with trailing '=' (anchors the lookup).
732
 * @param value Value to set for the variable; an empty string unsets the
733
 * variable.
734
 */
735
void Pass::setEnvVar(const QString &key, const QString &value) {
94✔
736
  const bool hasEq = key.endsWith('=');
94✔
737
  Q_ASSERT_X(hasEq, "Pass::setEnvVar",
738
             "called with malformed key (missing '=')");
739
  if (!hasEq) {
94✔
740
    qWarning() << "Pass::setEnvVar called with malformed key (missing '='):"
×
741
               << key;
×
742
    return;
×
743
  }
744
  env.erase(std::remove_if(
188✔
745
                env.begin(), env.end(),
746
                [&key](const QString &entry) { return entry.startsWith(key); }),
12,326✔
747
            env.end());
748
  if (!value.isEmpty())
94✔
749
    env.append(key + value);
140✔
750
}
751

752
/**
753
 * @brief Update the process environment used for executing external commands.
754
 *
755
 * Updates environment entries for PASSWORD_STORE_SIGNING_KEY,
756
 * PASSWORD_STORE_DIR, PASSWORD_STORE_GENERATED_LENGTH, and
757
 * PASSWORD_STORE_CHARACTER_SET based on current settings, then applies the
758
 * environment to the internal executor.
759
 */
760
void Pass::updateEnv() {
22✔
761
  setEnvVar(QStringLiteral("PASSWORD_STORE_SIGNING_KEY="),
44✔
762
            QtPassSettings::getPassSigningKey());
44✔
763
  setEnvVar(QStringLiteral("PASSWORD_STORE_DIR="),
44✔
764
            QtPassSettings::getPassStore());
44✔
765

766
  PasswordConfiguration passConfig = QtPassSettings::getPasswordConfiguration();
22✔
767
  setEnvVar(QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH="),
44✔
768
            QString::number(passConfig.length));
22✔
769

770
  setEnvVar(QStringLiteral("PASSWORD_STORE_CHARACTER_SET="),
44✔
771
            effectiveCharset(passConfig));
22✔
772

773
  exec.setEnvironment(env);
22✔
774
}
22✔
775

776
/**
777
 * @brief Pass::getGpgIdPath return gpgid file path for some file (folder).
778
 * @param for_file which file (folder) would you like the gpgid file path for.
779
 * @return path to the gpgid file.
780
 */
781
auto Pass::getGpgIdPath(const QString &for_file) -> QString {
46✔
782
  QString passStore =
783
      QDir::fromNativeSeparators(QtPassSettings::getPassStore());
92✔
784
  QString normalizedFile = QDir::fromNativeSeparators(for_file);
46✔
785
  QString fullPath = normalizedFile.startsWith(passStore)
46✔
786
                         ? normalizedFile
46✔
787
                         : passStore + "/" + normalizedFile;
41✔
788
  QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
46✔
789
  QString cleanPassStore = QDir::cleanPath(passStore);
46✔
790
  QString sep = QDir::separator();
46✔
791
  bool found = false;
792
  while (gpgIdDir.exists()) {
63✔
793
    QString currentPath = QDir::cleanPath(gpgIdDir.absolutePath());
112✔
794
    if (currentPath != cleanPassStore &&
74✔
795
        !currentPath.startsWith(cleanPassStore + sep)) {
74✔
796
      break;
797
    }
798
    if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
110✔
799
      found = true;
800
      break;
801
    }
802
    if (!gpgIdDir.cdUp()) {
17✔
803
      break;
804
    }
805
  }
806
  QString gpgIdPath(
807
      found ? gpgIdDir.absoluteFilePath(".gpg-id")
46✔
808
            : QDir(QtPassSettings::getPassStore()).filePath(".gpg-id"));
108✔
809

810
  return gpgIdPath;
46✔
811
}
46✔
812

813
/**
814
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
815
 * @param for_file which file (folder) would you like recipients for
816
 * @return recipients gpg-id contents
817
 */
818
auto Pass::getRecipientList(const QString &for_file) -> QStringList {
24✔
819
  QFile gpgId(getGpgIdPath(for_file));
24✔
820
  if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
24✔
UNCOV
821
    return {};
×
822
  }
823
  QStringList recipients;
24✔
824
  while (!gpgId.atEnd()) {
56✔
825
    QString recipient(gpgId.readLine());
64✔
826
    recipient = recipient.split("#")[0].trimmed();
64✔
827
    if (!recipient.isEmpty() && Util::isValidKeyId(recipient)) {
32✔
828
      recipients += recipient;
829
    }
830
  }
831
  return recipients;
832
}
24✔
833

834
/**
835
 * @brief Pass::getRecipientString formatted string for use with GPG
836
 * @param for_file which file (folder) would you like recipients for
837
 * @param separator formating separator eg: " -r "
838
 * @param count
839
 * @return recipient string
840
 */
841
auto Pass::getRecipientString(const QString &for_file, const QString &separator,
2✔
842
                              int *count) -> QStringList {
843
  Q_UNUSED(separator)
844
  QStringList recipients = Pass::getRecipientList(for_file);
2✔
845
  if (count) {
2✔
846
    *count = recipients.size();
1✔
847
  }
848
  return recipients;
2✔
849
}
850

851
/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
852
 */
853

854
/**
855
 * @brief Generates a random number bounded by the given value.
856
 * @param bound Upper bound (exclusive)
857
 * @return Random number in range [0, bound)
858
 */
859
auto Pass::boundedRandom(quint32 bound) -> quint32 {
7,592✔
860
  if (bound < 2) {
7,592✔
861
    return 0;
862
  }
863

864
  quint32 randval;
865
  // Rejection-sampling threshold to avoid modulo bias.
866
  // This follows the well-known "arc4random_uniform"-style approach:
867
  // reject values in the low range [0, min), where
868
  //   min = 2^32 % bound
869
  // so that the remaining range size is an exact multiple of `bound`.
870
  //
871
  // In quint32 arithmetic, (1 + ~bound) wraps to (2^32 - bound), therefore
872
  //   (1 + ~bound) % bound == 2^32 % bound.
873
  const quint32 rejectionThreshold = (1 + ~bound) % bound;
7,592✔
874

875
  do {
876
    randval = QRandomGenerator::system()->generate();
877
  } while (randval < rejectionThreshold);
7,592✔
878

879
  return randval % bound;
7,592✔
880
}
881

882
/**
883
 * @brief Generates a random password from the given charset.
884
 * @param charset Characters to use in the password
885
 * @param length Desired password length
886
 * @return Generated password string
887
 */
888
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,204✔
889
    -> QString {
890
  if (charset.isEmpty() || length == 0U) {
1,204✔
891
    return {};
892
  }
893
  QString out;
1,204✔
894
  for (unsigned int i = 0; i < length; ++i) {
8,796✔
895
    out.append(charset.at(static_cast<int>(
7,592✔
896
        boundedRandom(static_cast<quint32>(charset.length())))));
7,592✔
897
  }
898
  return out;
899
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc