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

IJHack / QtPass / 24849229813

23 Apr 2026 05:28PM UTC coverage: 27.11% (-0.2%) from 27.334%
24849229813

Pull #1141

github

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

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

1 existing line in 1 file now uncovered.

1632 of 6020 relevant lines covered (27.11%)

29.46 hits per line

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

55.74
/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
#ifdef Q_OS_WIN
20
#include <windows.h>
21
#else
22
#include <sys/time.h>
23
#endif
24
#include "qtpasssettings.h"
25

26
#ifdef QT_DEBUG
27
#include "debughelper.h"
28
#endif
29

30
QProcessEnvironment Util::_env;
31
bool Util::_envInitialised = false;
32

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

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

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

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

126
  initialiseEnvironment();
15✔
127

128
  QString ret;
15✔
129

130
  const QString binaryWithSep = QDir::separator() + binary;
15✔
131

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

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

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

180
  return ret;
181
}
182

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

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

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

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

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

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

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

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

287
  if (keyId.isEmpty()) {
50✔
288
    return false;
289
  }
290

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

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

302
  return hexKeyIdRegex.match(normalized).hasMatch();
42✔
303
}
304

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

339
/**
340
 * @brief Write templates to .templates file in password store.
341
 * @param storePath Path to password store root.
342
 * @param templates Hash of template name to field list.
343
 * @return true if write succeeded.
344
 */
NEW
345
auto Util::writeTemplates(const QString &storePath,
×
346
                          const QHash<QString, QStringList> &templates)
347
    -> bool {
NEW
348
  QFile file(QDir(storePath).filePath(".templates"));
×
NEW
349
  if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
350
    return false;
351
  }
NEW
352
  QTextStream out(&file);
×
NEW
353
  out << "# QtPass templates configuration\n";
×
NEW
354
  out << "# Format: INI-style with [template_name] sections,\n";
×
NEW
355
  out << "# followed by field names (one per line)\n\n";
×
NEW
356
  for (auto it = templates.constKeyValueBegin();
×
357
       it != templates.constKeyValueEnd(); ++it) {
NEW
358
    out << "[" << it->first << "]\n";
×
NEW
359
    for (const QString &field : it->second) {
×
NEW
360
      out << field << "\n";
×
361
    }
NEW
362
    out << "\n";
×
363
  }
NEW
364
  file.close();
×
365
  return true;
NEW
366
}
×
367

368
/**
369
 * @brief Get default template for a folder.
370
 * Looks in folder, then parent folders up to root.
371
 * @param folderPath Path to folder.
372
 * @param storePath Path to password store root.
373
 * @return Template name or empty if none found.
374
 */
NEW
375
auto Util::getFolderTemplate(const QString &folderPath,
×
376
                             const QString &storePath) -> QString {
NEW
377
  QDir dir(folderPath);
×
NEW
378
  while (dir.exists(".default_template")) {
×
NEW
379
    QFile file(dir.filePath(".default_template"));
×
NEW
380
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
×
NEW
381
      QTextStream in(&file);
×
NEW
382
      QString templateName = in.readLine().trimmed();
×
NEW
383
      file.close();
×
NEW
384
      if (!templateName.isEmpty() && !templateName.startsWith('#')) {
×
385
        return templateName;
386
      }
NEW
387
    }
×
NEW
388
    if (dir.absolutePath() == storePath) {
×
389
      break;
390
    }
NEW
391
    if (!dir.cdUp()) {
×
392
      break;
393
    }
NEW
394
  }
×
395
  return QString();
UNCOV
396
}
×
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