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

IJHack / QtPass / 24851551054

23 Apr 2026 06:19PM UTC coverage: 27.075% (-0.3%) from 27.334%
24851551054

Pull #1141

github

web-flow
Merge 145926bbf into c2eb59a14
Pull Request #1141: feat: multi-template support via .templates files

0 of 67 new or added lines in 2 files covered. (0.0%)

1 existing line in 1 file now uncovered.

1634 of 6035 relevant lines covered (27.08%)

29.36 hits per line

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

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

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

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

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

79
/**
80
 * @brief Resolves the path to the password store directory.
81
 * @details Initializes the environment, checks for the {@code
82
 * PASSWORD_STORE_DIR} variable, and falls back to a platform-specific default
83
 * location under the user's home directory.
84
 * @return QString - Normalized path to the password store folder.
85
 */
86
auto Util::findPasswordStore() -> QString {
2✔
87
  QString path;
2✔
88
  initialiseEnvironment();
2✔
89
  if (_env.contains("PASSWORD_STORE_DIR")) {
4✔
90
    path = _env.value("PASSWORD_STORE_DIR");
×
91
  } else {
92
#ifdef Q_OS_WIN
93
    path = QDir::homePath() + QDir::separator() + "password-store" +
94
           QDir::separator();
95
#else
96
    path = QDir::homePath() + QDir::separator() + ".password-store" +
4✔
97
           QDir::separator();
98
#endif
99
  }
100
  return Util::normalizeFolderPath(path);
4✔
101
}
102

103
auto Util::normalizeFolderPath(const QString &path) -> QString {
13✔
104
  QString normalizedPath = path;
105
  if (!normalizedPath.endsWith("/") &&
33✔
106
      !normalizedPath.endsWith(QDir::separator())) {
7✔
107
    normalizedPath += QDir::separator();
7✔
108
  }
109
  return QDir::toNativeSeparators(normalizedPath);
26✔
110
}
111

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

133
  initialiseEnvironment();
15✔
134

135
  QString ret;
15✔
136

137
  const QString binaryWithSep = QDir::separator() + binary;
15✔
138

139
  if (_env.contains("PATH")) {
30✔
140
    QString path = _env.value("PATH");
30✔
141
    const QChar delimiter = QDir::separator() == '\\' ? ';' : ':';
142
    QStringList entries = path.split(delimiter);
15✔
143

144
    for (const QString &entryConst : entries) {
237✔
145
      QString fullPath = entryConst + binaryWithSep;
220✔
146
      QFileInfo qfi(fullPath);
220✔
147
#ifdef Q_OS_WIN
148
      if (!qfi.exists()) {
149
        QString fullPathExe = fullPath + ".exe";
150
        qfi = QFileInfo(fullPathExe);
151
      }
152
#endif
153
      if (!qfi.exists()) {
220✔
154
        continue;
207✔
155
      }
156
      if (!qfi.isExecutable()) {
13✔
157
        continue;
×
158
      }
159

160
      ret = qfi.absoluteFilePath();
13✔
161
      break;
162
    }
220✔
163
  }
164
#ifdef Q_OS_WIN
165
  if (ret.isEmpty()) {
166
    static const QRegularExpression whitespaceRegex(QStringLiteral("\\s"));
167
    const bool hasWhitespace = binary.contains(whitespaceRegex);
168
    if (!binary.isEmpty() && !hasWhitespace) {
169
      QString wslCommand = QStringLiteral("wsl ") + binary;
170
#ifdef QT_DEBUG
171
      dbg() << "Util::findBinaryInPath(): falling back to WSL for binary"
172
            << binary;
173
#endif
174
      QString out, err;
175
      if (Executor::executeBlocking(wslCommand, {"--version"}, &out, &err) ==
176
              0 &&
177
          !out.isEmpty() && err.isEmpty()) {
178
#ifdef QT_DEBUG
179
        dbg() << "Util::findBinaryInPath(): using WSL binary" << wslCommand;
180
#endif
181
        ret = wslCommand;
182
      }
183
    }
184
  }
185
#endif
186

187
  return ret;
188
}
189

190
/**
191
 * @brief Checks whether the current QtPass configuration is valid.
192
 * @example
193
 * bool result = Util::configIsValid();
194
 * std::cout << std::boolalpha << result << std::endl; // Expected output: true
195
 * or false
196
 *
197
 * @return bool - True if the configuration file exists and the required
198
 * executable is available; otherwise false.
199
 */
200
auto Util::configIsValid() -> bool {
3✔
201
  const QString configFilePath =
202
      QDir(QtPassSettings::getPassStore()).filePath(".gpg-id");
9✔
203
  if (!QFile(configFilePath).exists()) {
3✔
204
    return false;
205
  }
206

207
  const QString executable = QtPassSettings::isUsePass()
4✔
208
                                 ? QtPassSettings::getPassExecutable()
2✔
209
                                 : QtPassSettings::getGpgExecutable();
4✔
210

211
  if (executable.startsWith(QStringLiteral("wsl "))) {
4✔
212
    QString out;
×
213
    QString err;
×
214
    if (Executor::executeBlocking(QStringLiteral("wsl"),
×
215
                                  {QStringLiteral("--version")}, &out,
×
216
                                  &err) == 0 &&
×
217
        !out.isEmpty() && err.isEmpty()) {
×
218
      return true;
219
    }
220
  }
221
  return QFile(executable).exists();
2✔
222
}
223

224
/**
225
 * @brief Returns a directory path derived from a model index, optionally
226
 * relative to the pass store.
227
 * @example
228
 * QString result = Util::getDir(index, true, model, storeModel);
229
 * std::cout << result.toStdString() << std::endl; // Expected output: relative
230
 * directory path with trailing separator
231
 *
232
 * @param QModelIndex &index - Source index used to resolve the file or
233
 * directory path.
234
 * @param bool forPass - If true, returns a path relative to the pass store;
235
 * otherwise returns an absolute path.
236
 * @param QFileSystemModel &model - File system model used to obtain file
237
 * information.
238
 * @param StoreModel &storeModel - Proxy model used to map the provided index to
239
 * the source model.
240
 * @return QString - The resolved directory path, always ending with the
241
 * platform's directory separator.
242
 */
243
auto Util::getDir(const QModelIndex &index, bool forPass,
3✔
244
                  const QFileSystemModel &model, const StoreModel &storeModel)
245
    -> QString {
246
  QString abspath =
247
      QDir(QtPassSettings::getPassStore()).absolutePath() + QDir::separator();
9✔
248
  if (!index.isValid()) {
249
    return forPass ? "" : abspath;
2✔
250
  }
251
  QFileInfo info = model.fileInfo(storeModel.mapToSource(index));
1✔
252
  QString filePath =
253
      (info.isFile() ? info.absolutePath() : info.absoluteFilePath());
1✔
254
  if (forPass) {
1✔
255
    filePath = QDir(abspath).relativeFilePath(filePath);
×
256
  }
257
  filePath += QDir::separator();
1✔
258
  return filePath;
259
}
1✔
260

261
auto Util::endsWithGpg() -> const QRegularExpression & {
24✔
262
  static const QRegularExpression expr{"\\.gpg$"};
24✔
263
  return expr;
24✔
264
}
265

266
/**
267
 * @brief Returns a regex matching common remote/network protocol schemes.
268
 *
269
 * Matches http://, https://, ftp://, ftps://, ssh://, sftp://, webdav://,
270
 * webdavs://
271
 *
272
 * Note: Local file URLs (file:///) are intentionally excluded by design, as
273
 * they represent local paths rather than network protocols. If this behavior
274
 * needs to change, update both this function and the corresponding test.
275
 *
276
 * @return QRegularExpression reference
277
 */
278
auto Util::protocolRegex() -> const QRegularExpression & {
4✔
279
  static const QRegularExpression regex{
280
      "((?:https?|ftp|ssh|sftp|ftps|webdav|webdavs)://[^\" <>\\)\\]\\[]+)"};
4✔
281
  return regex;
4✔
282
}
283

284
auto Util::newLinesRegex() -> const QRegularExpression & {
13✔
285
  static const QRegularExpression regex{"[\r\n]"};
13✔
286
  return regex;
13✔
287
}
288

289
auto Util::isValidKeyId(const QString &keyId) -> bool {
50✔
290
  static const QRegularExpression hexPrefixRegex{"^0[xX]"};
50✔
291
  static const QRegularExpression specialPrefixRegex{"^[@/#&]"};
50✔
292
  static const QRegularExpression hexKeyIdRegex{"^[0-9A-Fa-f]{8,40}$"};
50✔
293

294
  if (keyId.isEmpty()) {
50✔
295
    return false;
296
  }
297

298
  QString normalized = keyId;
299
  if (normalized.startsWith('<') && normalized.endsWith('>')) {
49✔
300
    normalized = normalized.mid(1, normalized.length() - 2);
4✔
301
  }
302
  normalized.remove(hexPrefixRegex);
49✔
303

304
  if (specialPrefixRegex.match(normalized).hasMatch() ||
98✔
305
      normalized.contains('@')) {
46✔
306
    return true;
307
  }
308

309
  return hexKeyIdRegex.match(normalized).hasMatch();
42✔
310
}
311

312
/**
313
 * @brief Read templates from .templates file in password store.
314
 * @param storePath Path to password store root.
315
 * @return Hash of template name to field list.
316
 */
NEW
317
auto Util::readTemplates(const QString &storePath)
×
318
    -> QHash<QString, QStringList> {
NEW
319
  QHash<QString, QStringList> result;
×
NEW
320
  QFile file(QDir(storePath).filePath(".templates"));
×
NEW
321
  if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
×
322
    return result;
323
  }
NEW
324
  QTextStream in(&file);
×
325
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
NEW
326
  in.setEncoding(QStringConverter::Utf8);
×
327
#else
328
  in.setCodec("UTF-8");
329
#endif
NEW
330
  QString currentSection;
×
NEW
331
  QStringList currentFields;
×
332
  bool skipInvalidSection = false;
NEW
333
  while (!in.atEnd()) {
×
NEW
334
    QString line = in.readLine().trimmed();
×
NEW
335
    if (line.startsWith('[') && line.endsWith(']')) {
×
NEW
336
      if (!currentSection.isEmpty() && !skipInvalidSection) {
×
337
        result.insert(currentSection, currentFields);
338
      }
NEW
339
      currentSection = line.mid(1, line.length() - 2).trimmed();
×
NEW
340
      if (currentSection.isEmpty()) {
×
NEW
341
        qWarning()
×
NEW
342
            << "Empty template section in .templates file, ignoring fields";
×
343
        skipInvalidSection = true;
NEW
344
        currentFields.clear();
×
345
      } else {
346
        skipInvalidSection = false;
NEW
347
        currentFields.clear();
×
348
      }
NEW
349
    } else if (!line.isEmpty() && !line.startsWith('#') &&
×
350
               !skipInvalidSection) {
351
      currentFields.append(line);
352
    }
353
  }
NEW
354
  if (!currentSection.isEmpty() && !skipInvalidSection) {
×
355
    result.insert(currentSection, currentFields);
356
  }
357
  return result;
NEW
358
}
×
359

360
/**
361
 * @brief Write templates to .templates file in password store.
362
 * @param storePath Path to password store root.
363
 * @param templates Hash of template name to field list.
364
 * @return true if write succeeded.
365
 */
NEW
366
auto Util::writeTemplates(const QString &storePath,
×
367
                          const QHash<QString, QStringList> &templates)
368
    -> bool {
NEW
369
  QSaveFile saveFile(QDir(storePath).filePath(".templates"));
×
NEW
370
  if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
371
    return false;
372
  }
NEW
373
  QTextStream out(&saveFile);
×
374
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
NEW
375
  out.setEncoding(QStringConverter::Utf8);
×
376
#else
377
  out.setCodec("UTF-8");
378
#endif
NEW
379
  out << "# QtPass templates configuration\n";
×
NEW
380
  out << "# Format: INI-style with [template_name] sections,\n";
×
NEW
381
  out << "# followed by field names (one per line)\n\n";
×
382

NEW
383
  QStringList sortedKeys = templates.keys();
×
NEW
384
  std::sort(sortedKeys.begin(), sortedKeys.end());
×
NEW
385
  for (const QString &key : sortedKeys) {
×
NEW
386
    out << "[" << key << "]\n";
×
NEW
387
    for (const QString &field : templates.value(key)) {
×
NEW
388
      out << field << "\n";
×
389
    }
NEW
390
    out << "\n";
×
391
  }
NEW
392
  out.flush();
×
NEW
393
  if (out.status() != QTextStream::Ok) {
×
394
    return false;
395
  }
NEW
396
  return saveFile.commit();
×
NEW
397
}
×
398

399
/**
400
 * @brief Get default template for a folder.
401
 * Looks in folder, then parent folders up to root.
402
 * @param folderPath Path to folder.
403
 * @param storePath Path to password store root.
404
 * @return Template name or empty if none found.
405
 */
NEW
406
auto Util::getFolderTemplate(const QString &folderPath,
×
407
                             const QString &storePath) -> QString {
NEW
408
  QDir storeDir(storePath);
×
NEW
409
  QString cleanStoreAbs = QDir::cleanPath(storeDir.absolutePath());
×
NEW
410
  QDir dir(folderPath);
×
411
  while (true) {
NEW
412
    QString currentPath = QDir::cleanPath(dir.absolutePath());
×
NEW
413
    if (currentPath == cleanStoreAbs) {
×
414
      break;
415
    }
NEW
416
    if (!currentPath.startsWith(cleanStoreAbs + "/")) {
×
417
      break;
418
    }
NEW
419
    if (dir.exists(".default_template")) {
×
NEW
420
      QFile file(dir.filePath(".default_template"));
×
NEW
421
      if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
×
NEW
422
        QTextStream in(&file);
×
423
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
NEW
424
        in.setEncoding(QStringConverter::Utf8);
×
425
#else
426
        in.setCodec("UTF-8");
427
#endif
NEW
428
        QString templateName = in.readLine().trimmed();
×
NEW
429
        file.close();
×
NEW
430
        if (!templateName.isEmpty() && !templateName.startsWith('#')) {
×
431
          return templateName;
432
        }
NEW
433
      }
×
NEW
434
    }
×
NEW
435
    if (!dir.cdUp()) {
×
436
      break;
437
    }
438
  }
439
  return QString();
UNCOV
440
}
×
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