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

IJHack / QtPass / 25580803246

08 May 2026 09:37PM UTC coverage: 28.034% (-0.002%) from 28.036%
25580803246

push

github

web-flow
refactor: 6 reviewer cleanups in util.cpp + qtpass.cpp (#1436)

util.cpp (5 fixes):

- Tilde-expansion comment: noted the limitation that "~username"
  forms are intentionally not resolved (only "~" / "~/...").
- PATH delimiter: replaced runtime check
  (QDir::separator() == '\\' ? ';' : ':') with explicit
  #ifdef Q_OS_WIN block. Clearer intent, no Q-API call needed.
- findBinaryInPath whitespace test: dropped the static
  QRegularExpression in favour of std::any_of with
  QChar::isSpace(). Same Unicode coverage, no regex compile cost.
- findBinaryInPath WSL fallback: cache results in a static
  QHash<QString, QString>. The "wsl <binary> --version" probe is
  a blocking subprocess; without caching it ran once per call
  for any binary not on PATH.
- configIsValid WSL probe: wrap in a lambda-initialised static
  const bool — same caching reasoning. WSL availability doesn't
  change at runtime.

qtpass.cpp (1 fix):

- aboutToQuit connect: the existing GCC/clang #pragma deprecation
  suppression doesn't compile on MSVC. Added the MSVC variant
  (#pragma warning(push) / disable : 4996 / pop) under
  defined(_MSC_VER), with the GCC/clang block as the elif branch.

Skipped one reviewer finding (qtpass.cpp dwordBytes "missing
definition" — already defined in qtpass.h:29 as static inline,
visible at every call site through the existing #include).

Validation:
- tst_util: 106/106 pass.
- clang-tidy: 0 findings on touched files.
- clang-format clean.
- Build clean.

1 of 10 new or added lines in 1 file covered. (10.0%)

1855 of 6617 relevant lines covered (28.03%)

27.11 hits per line

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

47.59
/src/util.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3

4
/**
5
 * @class Util
6
 * @brief Static utility functions implementation.
7
 *
8
 * Implementation of utility functions for path handling, binary discovery,
9
 * and configuration validation.
10
 *
11
 * @see util.h
12
 */
13

14
#include "util.h"
15
#include "executor.h"
16
#include <QDebug>
17
#include <QDir>
18
#include <QFile>
19
#include <QFileInfo>
20
#include <QHash>
21
#include <QSaveFile>
22
#include <QTextStream>
23
#include <algorithm>
24
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
25
#include <QStringConverter>
26
#endif
27
#ifdef Q_OS_WIN
28
#include <windows.h>
29
#else
30
#include <sys/time.h>
31
#endif
32
#include "qtpasssettings.h"
33

34
#ifdef QT_DEBUG
35
#include "debughelper.h"
36
#endif
37

38
QProcessEnvironment Util::_env;
39
bool Util::_envInitialised = false;
40

41
/**
42
 * @brief Initializes the process environment and augments PATH with
43
 * platform-specific GPG locations.
44
 * @example
45
 * Util::initialiseEnvironment();
46
 *
47
 * @note On macOS, appends common MacGPG2 and /usr/local/bin paths if available.
48
 * @note On Windows, appends common WinGPG and GnuPG installation paths if
49
 * available.
50
 */
51
void Util::initialiseEnvironment() {
17 ✔
52
  if (!_envInitialised) {
17 ✔
53
    _env = QProcessEnvironment::systemEnvironment();
1 ✔
54
#ifdef __APPLE__
55
    QString path = _env.value("PATH");
56
    if (!path.contains("/usr/local/MacGPG2/bin") &&
57
        QDir("/usr/local/MacGPG2/bin").exists())
58
      path += ":/usr/local/MacGPG2/bin";
59
    if (!path.contains("/usr/local/bin"))
60
      path += ":/usr/local/bin";
61
    _env.insert("PATH", path);
62
#endif
63
#ifdef Q_OS_WIN
64
    QString path = _env.value("PATH");
65
    if (!path.contains("C:\\Program Files\\WinGPG\\x86") &&
66
        QDir("C:\\Program Files\\WinGPG\\x86").exists())
67
      path += ";C:\\Program Files\\WinGPG\\x86";
68
    if (!path.contains("C:\\Program Files\\GnuPG\\bin") &&
69
        QDir("C:\\Program Files\\GnuPG\\bin").exists())
70
      path += ";C:\\Program Files\\GnuPG\\bin";
71
    _env.insert("PATH", path);
72
#endif
73
#ifdef QT_DEBUG
74
    dbg() << _env.value("PATH");
75
#endif
76
    _envInitialised = true;
1 ✔
77
  }
78
}
17 ✔
79

80
/**
81
 * @brief Resolves the path to the password store directory.
82
 * @details Initializes the environment, checks for the {@code
83
 * PASSWORD_STORE_DIR} variable, and falls back to a platform-specific default
84
 * location under the user's home directory.
85
 * @return QString - Normalized path to the password store folder.
86
 */
87
auto Util::findPasswordStore() -> QString {
2 ✔
88
  QString path;
2 ✔
89
  initialiseEnvironment();
2 ✔
90
  if (_env.contains("PASSWORD_STORE_DIR")) {
4 ✔
91
    path = _env.value("PASSWORD_STORE_DIR");
×
92
    // Expand current-user tilde forms ("~" and "~/...") — env vars set in
93
    // non-shell contexts (systemd units, .desktop entries, quoted shell
94
    // assignments) skip shell tilde expansion, leaving "~" literal.
95
    // Note: "~username" forms are intentionally not resolved here.
96
    if (path == "~") {
×
97
      path = QDir::homePath();
×
98
    } else if (path.startsWith("~/")) {
×
99
      path = QDir::homePath() + path.mid(1);
×
100
    }
101
  } else {
102
#ifdef Q_OS_WIN
103
    path = QDir(QDir::homePath()).filePath("password-store");
104
#else
105
    path = QDir(QDir::homePath()).filePath(".password-store");
6 ✔
106
#endif
107
  }
108
  return Util::normalizeFolderPath(QDir::cleanPath(path));
4 ✔
109
}
110

111
auto Util::normalizeFolderPath(const QString &path) -> QString {
13 ✔
112
  QString normalizedPath = path;
113
  if (!normalizedPath.endsWith("/") &&
35 ✔
114
      !normalizedPath.endsWith(QDir::separator())) {
9 ✔
115
    normalizedPath += QDir::separator();
9 ✔
116
  }
117
  return QDir::toNativeSeparators(normalizedPath);
26 ✔
118
}
119

120
/**
121
 * @brief Finds the absolute path of a binary by searching the PATH environment
122
 * variable.
123
 *
124
 * Iterates through each PATH entry, checks whether the binary exists and is
125
 * executable, and returns the first matching absolute file path. On Windows, if
126
 * no local match is found, it may fall back to a WSL invocation when the binary
127
 * name is valid and WSL appears to support it.
128
 *
129
 * @example
130
 * QString result = Util::findBinaryInPath("git");
131
 * // Expected output sample: "/usr/bin/git" or "wsl git"
132
 *
133
 * @param QString binary - The name of the binary to locate.
134
 * @return QString - The absolute path to the binary, or an empty string if not
135
 * found.
136
 */
137
auto Util::findBinaryInPath(const QString &binary) -> QString {
16 ✔
138
  if (binary.isEmpty()) {
16 ✔
139
    return {};
140
  }
141

142
  initialiseEnvironment();
15 ✔
143

144
  QString ret;
15 ✔
145

146
  const QString binaryWithSep = QDir::separator() + binary;
15 ✔
147

148
  if (_env.contains("PATH")) {
30 ✔
149
    QString path = _env.value("PATH");
30 ✔
150
#ifdef Q_OS_WIN
151
    const QChar delimiter = ';';
152
#else
153
    const QChar delimiter = ':';
15 ✔
154
#endif
155
    QStringList entries = path.split(delimiter);
15 ✔
156

157
    for (const QString &entryConst : entries) {
237 ✔
158
      QString fullPath = entryConst + binaryWithSep;
220 ✔
159
      QFileInfo qfi(fullPath);
220 ✔
160
#ifdef Q_OS_WIN
161
      if (!qfi.exists()) {
162
        QString fullPathExe = fullPath + ".exe";
163
        qfi = QFileInfo(fullPathExe);
164
      }
165
#endif
166
      if (!qfi.exists()) {
220 ✔
167
        continue;
207 ✔
168
      }
169
      if (!qfi.isExecutable()) {
13 ✔
170
        continue;
×
171
      }
172

173
      ret = qfi.absoluteFilePath();
13 ✔
174
      break;
175
    }
220 ✔
176
  }
177
#ifdef Q_OS_WIN
178
  if (ret.isEmpty()) {
179
    // Cache per-binary WSL lookup result — the wsl --version probe is a
180
    // blocking subprocess that can run several times per session for
181
    // missing binaries; once decided, the answer doesn't change at runtime.
182
    static QHash<QString, QString> wslBinaryCache;
183
    const bool hasWhitespace =
184
        std::any_of(binary.cbegin(), binary.cend(),
185
                    [](const QChar ch) { return ch.isSpace(); });
186
    if (!hasWhitespace) {
187
      auto cached = wslBinaryCache.constFind(binary);
188
      if (cached != wslBinaryCache.constEnd()) {
189
        ret = cached.value();
190
      } else {
191
        QString wslCommand = QStringLiteral("wsl ") + binary;
192
#ifdef QT_DEBUG
193
        dbg() << "Util::findBinaryInPath(): falling back to WSL for binary"
194
              << binary;
195
#endif
196
        QString out, err;
197
        QString cachedResult;
198
        if (Executor::executeBlocking(wslCommand, {"--version"}, &out, &err) ==
199
                0 &&
200
            !out.isEmpty() && err.isEmpty()) {
201
#ifdef QT_DEBUG
202
          dbg() << "Util::findBinaryInPath(): using WSL binary" << wslCommand;
203
#endif
204
          cachedResult = wslCommand;
205
        }
206
        wslBinaryCache.insert(binary, cachedResult);
207
        ret = cachedResult;
208
      }
209
    }
210
  }
211
#endif
212

213
  return ret;
214
}
215

216
/**
217
 * @brief Checks whether the current QtPass configuration is valid.
218
 * @example
219
 * bool result = Util::configIsValid();
220
 * std::cout << std::boolalpha << result << std::endl; // Expected output: true
221
 * or false
222
 *
223
 * @return bool - True if the configuration file exists and the required
224
 * executable is available; otherwise false.
225
 */
226
auto Util::configIsValid() -> bool {
3 ✔
227
  const QString configFilePath =
228
      QDir(QtPassSettings::getPassStore()).filePath(".gpg-id");
9 ✔
229
  if (!QFile(configFilePath).exists()) {
3 ✔
230
    return false;
231
  }
232

233
  const QString executable = QtPassSettings::isUsePass()
4 ✔
234
                                 ? QtPassSettings::getPassExecutable()
2 ✔
235
                                 : QtPassSettings::getGpgExecutable();
4 ✔
236

237
  if (executable.startsWith(QStringLiteral("wsl "))) {
4 ✔
238
    // Probe WSL once per session — availability doesn't change at runtime
239
    // and the executeBlocking call is a blocking subprocess.
NEW
240
    static const bool wslAvailable = []() {
×
NEW
241
      QString out;
×
NEW
242
      QString err;
×
NEW
243
      return Executor::executeBlocking(QStringLiteral("wsl"),
×
NEW
244
                                       {QStringLiteral("--version")}, &out,
×
NEW
245
                                       &err) == 0 &&
×
NEW
246
             !out.isEmpty() && err.isEmpty();
×
NEW
247
    }();
×
NEW
248
    if (wslAvailable) {
×
249
      return true;
250
    }
251
  }
252
  return QFile(executable).exists();
2 ✔
253
}
254

255
/**
256
 * @brief Returns a directory path derived from a model index, optionally
257
 * relative to the pass store.
258
 * @example
259
 * QString result = Util::getDir(index, true, model, storeModel);
260
 * std::cout << result.toStdString() << std::endl; // Expected output: relative
261
 * directory path with trailing separator
262
 *
263
 * @param QModelIndex &index - Source index used to resolve the file or
264
 * directory path.
265
 * @param bool forPass - If true, returns a path relative to the pass store;
266
 * otherwise returns an absolute path.
267
 * @param QFileSystemModel &model - File system model used to obtain file
268
 * information.
269
 * @param StoreModel &storeModel - Proxy model used to map the provided index to
270
 * the source model.
271
 * @return QString - The resolved directory path, always ending with the
272
 * platform's directory separator.
273
 */
274
auto Util::getDir(const QModelIndex &index, bool forPass,
3 ✔
275
                  const QFileSystemModel &model, const StoreModel &storeModel)
276
    -> QString {
277
  QString abspath =
278
      QDir(QtPassSettings::getPassStore()).absolutePath() + QDir::separator();
9 ✔
279
  if (!index.isValid()) {
280
    return forPass ? "" : abspath;
2 ✔
281
  }
282
  QFileInfo info = model.fileInfo(storeModel.mapToSource(index));
1 ✔
283
  QString filePath =
284
      (info.isFile() ? info.absolutePath() : info.absoluteFilePath());
1 ✔
285
  if (forPass) {
1 ✔
286
    filePath = QDir(abspath).relativeFilePath(filePath);
×
287
  }
288
  filePath += QDir::separator();
1 ✔
289
  return filePath;
290
}
1 ✔
291

292
/**
293
 * @brief Returns a regex matching strings that end with the .gpg extension.
294
 *
295
 * @return QRegularExpression reference
296
 */
297
auto Util::endsWithGpg() -> const QRegularExpression & {
64 ✔
298
  static const QRegularExpression expr{"\\.gpg$"};
64 ✔
299
  return expr;
64 ✔
300
}
301

302
/**
303
 * @brief Returns a regex matching common remote/network protocol schemes.
304
 *
305
 * Matches http://, https://, ftp://, ftps://, ssh://, sftp://, webdav://,
306
 * webdavs://
307
 *
308
 * Note: Local file URLs (file:///) are intentionally excluded by design, as
309
 * they represent local paths rather than network protocols. If this behavior
310
 * needs to change, update both this function and the corresponding test.
311
 *
312
 * @return QRegularExpression reference
313
 */
314
auto Util::protocolRegex() -> const QRegularExpression & {
4 ✔
315
  static const QRegularExpression regex{
316
      R"(((?:https?|ftp|ssh|sftp|ftps|webdav|webdavs)://[^" <>\)\]\[]+))"};
4 ✔
317
  return regex;
4 ✔
318
}
319

320
/**
321
 * @brief Returns a regex matching newline characters (CR or LF).
322
 *
323
 * Useful for detecting or sanitising line breaks in text content.
324
 *
325
 * @return QRegularExpression reference
326
 */
327
auto Util::newLinesRegex() -> const QRegularExpression & {
13 ✔
328
  static const QRegularExpression regex{"[\r\n]"};
13 ✔
329
  return regex;
13 ✔
330
}
331

332
/**
333
 * @brief Validate whether a string is an accepted GPG key identifier.
334
 *
335
 * Accepted formats:
336
 * - Hexadecimal key IDs / fingerprints, length 8 to 40 hex characters,
337
 *   optionally prefixed with `0x` or `0X`.
338
 * - Identifiers wrapped in angle brackets (`<...>`); brackets are stripped
339
 *   before validation.
340
 * - Special routing prefixes: `@`, `/`, `#`, `&` (used by GnuPG to look up
341
 *   keys via mail-server / keyring / fingerprint substring matchers).
342
 * - Email-style user IDs (any value containing `@`).
343
 *
344
 * Empty input is invalid.
345
 *
346
 * @param keyId Input key identifier string to validate.
347
 * @return true if the input matches any accepted format; false otherwise.
348
 */
349
auto Util::isValidKeyId(const QString &keyId) -> bool {
50 ✔
350
  static const QRegularExpression hexPrefixRegex{"^0[xX]"};
50 ✔
351
  static const QRegularExpression specialPrefixRegex{"^[@/#&]"};
50 ✔
352
  static const QRegularExpression hexKeyIdRegex{"^[0-9A-Fa-f]{8,40}$"};
50 ✔
353

354
  if (keyId.isEmpty()) {
50 ✔
355
    return false;
356
  }
357

358
  QString normalized = keyId;
359
  if (normalized.startsWith('<') && normalized.endsWith('>')) {
49 ✔
360
    normalized = normalized.mid(1, normalized.length() - 2);
4 ✔
361
  }
362
  normalized.remove(hexPrefixRegex);
49 ✔
363

364
  if (specialPrefixRegex.match(normalized).hasMatch() ||
98 ✔
365
      normalized.contains('@')) {
46 ✔
366
    return true;
367
  }
368

369
  return hexKeyIdRegex.match(normalized).hasMatch();
42 ✔
370
}
371

372
/**
373
 * @brief Read templates from .templates file in password store.
374
 * @param storePath Path to password store root.
375
 * @return Hash of template name to field list.
376
 */
377
auto Util::readTemplates(const QString &storePath)
×
378
    -> QHash<QString, QStringList> {
379
  QHash<QString, QStringList> result;
×
380
  QFile file(QDir(storePath).filePath(".templates"));
×
381
  if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
×
382
    return result;
383
  }
384
  QTextStream in(&file);
×
385
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
386
  in.setEncoding(QStringConverter::Utf8);
×
387
#else
388
  in.setCodec("UTF-8");
389
#endif
390
  QString currentSection;
×
391
  QStringList currentFields;
×
392
  bool skipInvalidSection = false;
393
  while (!in.atEnd()) {
×
394
    QString line = in.readLine().trimmed();
×
395
    if (line.startsWith('[') && line.endsWith(']')) {
×
396
      if (!currentSection.isEmpty() && !skipInvalidSection) {
×
397
        result.insert(currentSection, currentFields);
398
      }
399
      currentSection = line.mid(1, line.length() - 2).trimmed();
×
400
      if (currentSection.isEmpty()) {
×
401
        qWarning()
×
402
            << "Empty template section in .templates file, ignoring fields";
×
403
        skipInvalidSection = true;
404
        currentFields.clear();
×
405
      } else {
406
        skipInvalidSection = false;
407
        currentFields.clear();
×
408
      }
409
    } else if (!line.isEmpty() && !line.startsWith('#') &&
×
410
               !skipInvalidSection) {
411
      currentFields.append(line);
412
    }
413
  }
414
  if (!currentSection.isEmpty() && !skipInvalidSection) {
×
415
    result.insert(currentSection, currentFields);
416
  }
417
  return result;
418
}
×
419

420
/**
421
 * @brief Write templates to .templates file in password store.
422
 * @param storePath Path to password store root.
423
 * @param templates Hash of template name to field list.
424
 * @return true if write succeeded.
425
 */
426
auto Util::writeTemplates(const QString &storePath,
×
427
                          const QHash<QString, QStringList> &templates)
428
    -> bool {
429
  QSaveFile saveFile(QDir(storePath).filePath(".templates"));
×
430
  if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
431
    return false;
432
  }
433
  QTextStream out(&saveFile);
×
434
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
435
  out.setEncoding(QStringConverter::Utf8);
×
436
#else
437
  out.setCodec("UTF-8");
438
#endif
439
  out << "# QtPass templates configuration\n";
×
440
  out << "# Format: INI-style with [template_name] sections,\n";
×
441
  out << "# followed by field names (one per line)\n\n";
×
442

443
  QStringList sortedKeys = templates.keys();
×
444
  std::sort(sortedKeys.begin(), sortedKeys.end());
×
445
  for (const QString &key : sortedKeys) {
×
446
    out << "[" << key << "]\n";
×
447
    for (const QString &field : templates.value(key)) {
×
448
      out << field << "\n";
×
449
    }
450
    out << "\n";
×
451
  }
452
  out.flush();
×
453
  if (out.status() != QTextStream::Ok) {
×
454
    return false;
455
  }
456
  return saveFile.commit();
×
457
}
×
458

459
/**
460
 * @brief Get default template for a folder.
461
 * Looks in folder, then parent folders up to root.
462
 * @param folderPath Path to folder.
463
 * @param storePath Path to password store root.
464
 * @return Template name or empty if none found.
465
 */
466
auto Util::getFolderTemplate(const QString &folderPath,
×
467
                             const QString &storePath) -> QString {
468
  QDir storeDir(storePath);
×
469
  QString cleanStoreAbs = QDir::cleanPath(storeDir.absolutePath());
×
470
  QString sep = QDir::separator();
×
471
  QDir dir(folderPath);
×
472
  while (true) {
473
    if (dir.exists(".default_template")) {
×
474
      QFile file(dir.filePath(".default_template"));
×
475
      if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
×
476
        QTextStream in(&file);
×
477
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
478
        in.setEncoding(QStringConverter::Utf8);
×
479
#else
480
        in.setCodec("UTF-8");
481
#endif
482
        QString templateName = in.readLine().trimmed();
×
483
        file.close();
×
484
        if (!templateName.isEmpty() && !templateName.startsWith('#')) {
×
485
          return templateName;
486
        }
487
      }
×
488
    }
×
489
    QString currentPath = QDir::cleanPath(dir.absolutePath());
×
490
    if (currentPath == cleanStoreAbs) {
×
491
      break;
492
    }
493
    if (!currentPath.startsWith(cleanStoreAbs + sep)) {
×
494
      break;
495
    }
496
    if (!dir.cdUp()) {
×
497
      break;
498
    }
499
  }
500
  return {};
501
}
×
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