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

IJHack / QtPass / 24852205990

23 Apr 2026 06:34PM UTC coverage: 27.038% (-0.3%) from 27.334%
24852205990

Pull #1141

github

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

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

1 existing line in 1 file now uncovered.

1632 of 6036 relevant lines covered (27.04%)

29.36 hits per line

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

49.64
/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 "qtpasssettings.h"
31

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

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

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

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

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

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

132
  initialiseEnvironment();
15✔
133

134
  QString ret;
15✔
135

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

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

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

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

186
  return ret;
187
}
188

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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