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

IJHack / QtPass / 24849579138

23 Apr 2026 05:35PM UTC coverage: 27.089% (-0.2%) from 27.334%
24849579138

Pull #1141

github

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

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

1 existing line in 1 file now uncovered.

1634 of 6032 relevant lines covered (27.09%)

29.38 hits per line

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

51.13
/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 <QStringConverter>
19
#include <QTextStream>
20
#include <algorithm>
21
#ifdef Q_OS_WIN
22
#include <windows.h>
23
#else
24
#include <sys/time.h>
25
#endif
26
#include "qtpasssettings.h"
27

28
#ifdef QT_DEBUG
29
#include "debughelper.h"
30
#endif
31

32
QProcessEnvironment Util::_env;
33
bool Util::_envInitialised = false;
34

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

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

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

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

128
  initialiseEnvironment();
15✔
129

130
  QString ret;
15✔
131

132
  const QString binaryWithSep = QDir::separator() + binary;
15✔
133

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

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

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

182
  return ret;
183
}
184

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

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

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

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

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

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

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

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

289
  if (keyId.isEmpty()) {
50✔
290
    return false;
291
  }
292

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

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

304
  return hexKeyIdRegex.match(normalized).hasMatch();
42✔
305
}
306

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

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

NEW
374
  QStringList sortedKeys = templates.keys();
×
NEW
375
  std::sort(sortedKeys.begin(), sortedKeys.end());
×
NEW
376
  for (const QString &key : sortedKeys) {
×
NEW
377
    out << "[" << key << "]\n";
×
NEW
378
    for (const QString &field : templates.value(key)) {
×
NEW
379
      out << field << "\n";
×
380
    }
NEW
381
    out << "\n";
×
382
  }
NEW
383
  file.close();
×
384
  return true;
NEW
385
}
×
386

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