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

IJHack / QtPass / 24849959351

23 Apr 2026 05:44PM UTC coverage: 27.06% (-0.3%) from 27.334%
24849959351

Pull #1141

github

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

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

1 existing line in 1 file now uncovered.

1632 of 6031 relevant lines covered (27.06%)

29.38 hits per line

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

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

30
#ifdef QT_DEBUG
31
#include "debughelper.h"
32
#endif
33

34
QProcessEnvironment Util::_env;
35
bool Util::_envInitialised = false;
36

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

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

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

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

130
  initialiseEnvironment();
15✔
131

132
  QString ret;
15✔
133

134
  const QString binaryWithSep = QDir::separator() + binary;
15✔
135

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

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

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

184
  return ret;
185
}
186

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

204
  const QString executable = QtPassSettings::isUsePass()
4✔
205
                                 ? QtPassSettings::getPassExecutable()
2✔
206
                                 : QtPassSettings::getGpgExecutable();
4✔
207

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

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

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

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

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

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

291
  if (keyId.isEmpty()) {
50✔
292
    return false;
293
  }
294

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

301
  if (specialPrefixRegex.match(normalized).hasMatch() ||
98✔
302
      normalized.contains('@')) {
46✔
303
    return true;
304
  }
305

306
  return hexKeyIdRegex.match(normalized).hasMatch();
42✔
307
}
308

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

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

NEW
380
  QStringList sortedKeys = templates.keys();
×
NEW
381
  std::sort(sortedKeys.begin(), sortedKeys.end());
×
NEW
382
  for (const QString &key : sortedKeys) {
×
NEW
383
    out << "[" << key << "]\n";
×
NEW
384
    for (const QString &field : templates.value(key)) {
×
NEW
385
      out << field << "\n";
×
386
    }
NEW
387
    out << "\n";
×
388
  }
NEW
389
  file.close();
×
390
  return true;
NEW
391
}
×
392

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