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

IJHack / QtPass / 25577074236

08 May 2026 08:09PM UTC coverage: 28.048%. Remained the same
25577074236

Pull #1433

github

web-flow
Merge 2c5b65181 into 804118787
Pull Request #1433: refactor(util): cleaner path construction + brace consistency

6 of 6 new or added lines in 1 file covered. (100.0%)

44 existing lines in 1 file now uncovered.

1854 of 6610 relevant lines covered (28.05%)

27.13 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 "executor.h"
16
#include <QDebug>
17
#include <QDir>
18
#include <QFile>
19
#include <QFileInfo>
20
#include <QSaveFile>
21
#include <QTextStream>
22
#include <algorithm>
23
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
24
#include <QStringConverter>
25
#endif
26
#ifdef Q_OS_WIN
27
#include <windows.h>
28
#else
29
#include <sys/time.h>
30
#endif
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(QDir::homePath()).filePath("password-store");
94
#else
95
    path = QDir(QDir::homePath()).filePath(".password-store");
6✔
96
#endif
97
  }
98
  return Util::normalizeFolderPath(QDir::cleanPath(path));
4✔
99
}
100

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

110
/**
111
 * @brief Finds the absolute path of a binary by searching the PATH environment
112
 * variable.
113
 *
114
 * Iterates through each PATH entry, checks whether the binary exists and is
115
 * executable, and returns the first matching absolute file path. On Windows, if
116
 * no local match is found, it may fall back to a WSL invocation when the binary
117
 * name is valid and WSL appears to support it.
118
 *
119
 * @example
120
 * QString result = Util::findBinaryInPath("git");
121
 * // Expected output sample: "/usr/bin/git" or "wsl git"
122
 *
123
 * @param QString binary - The name of the binary to locate.
124
 * @return QString - The absolute path to the binary, or an empty string if not
125
 * found.
126
 */
127
auto Util::findBinaryInPath(const QString &binary) -> QString {
16✔
128
  if (binary.isEmpty()) {
16✔
129
    return {};
130
  }
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✔
UNCOV
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✔
UNCOV
211
    QString out;
×
UNCOV
212
    QString err;
×
UNCOV
213
    if (Executor::executeBlocking(QStringLiteral("wsl"),
×
UNCOV
214
                                  {QStringLiteral("--version")}, &out,
×
UNCOV
215
                                  &err) == 0 &&
×
UNCOV
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✔
UNCOV
254
    filePath = QDir(abspath).relativeFilePath(filePath);
×
255
  }
256
  filePath += QDir::separator();
1✔
257
  return filePath;
258
}
1✔
259

260
auto Util::endsWithGpg() -> const QRegularExpression & {
64✔
261
  static const QRegularExpression expr{"\\.gpg$"};
64✔
262
  return expr;
64✔
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
      R"(((?: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
 */
UNCOV
316
auto Util::readTemplates(const QString &storePath)
×
317
    -> QHash<QString, QStringList> {
UNCOV
318
  QHash<QString, QStringList> result;
×
UNCOV
319
  QFile file(QDir(storePath).filePath(".templates"));
×
UNCOV
320
  if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
×
321
    return result;
322
  }
UNCOV
323
  QTextStream in(&file);
×
324
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
UNCOV
325
  in.setEncoding(QStringConverter::Utf8);
×
326
#else
327
  in.setCodec("UTF-8");
328
#endif
UNCOV
329
  QString currentSection;
×
UNCOV
330
  QStringList currentFields;
×
331
  bool skipInvalidSection = false;
UNCOV
332
  while (!in.atEnd()) {
×
333
    QString line = in.readLine().trimmed();
×
UNCOV
334
    if (line.startsWith('[') && line.endsWith(']')) {
×
UNCOV
335
      if (!currentSection.isEmpty() && !skipInvalidSection) {
×
336
        result.insert(currentSection, currentFields);
337
      }
338
      currentSection = line.mid(1, line.length() - 2).trimmed();
×
UNCOV
339
      if (currentSection.isEmpty()) {
×
340
        qWarning()
×
341
            << "Empty template section in .templates file, ignoring fields";
×
342
        skipInvalidSection = true;
343
        currentFields.clear();
×
344
      } else {
345
        skipInvalidSection = false;
346
        currentFields.clear();
×
347
      }
348
    } else if (!line.isEmpty() && !line.startsWith('#') &&
×
349
               !skipInvalidSection) {
350
      currentFields.append(line);
351
    }
352
  }
UNCOV
353
  if (!currentSection.isEmpty() && !skipInvalidSection) {
×
354
    result.insert(currentSection, currentFields);
355
  }
356
  return result;
UNCOV
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
 */
365
auto Util::writeTemplates(const QString &storePath,
×
366
                          const QHash<QString, QStringList> &templates)
367
    -> bool {
UNCOV
368
  QSaveFile saveFile(QDir(storePath).filePath(".templates"));
×
UNCOV
369
  if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
370
    return false;
371
  }
UNCOV
372
  QTextStream out(&saveFile);
×
373
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
UNCOV
374
  out.setEncoding(QStringConverter::Utf8);
×
375
#else
376
  out.setCodec("UTF-8");
377
#endif
UNCOV
378
  out << "# QtPass templates configuration\n";
×
UNCOV
379
  out << "# Format: INI-style with [template_name] sections,\n";
×
380
  out << "# followed by field names (one per line)\n\n";
×
381

382
  QStringList sortedKeys = templates.keys();
×
UNCOV
383
  std::sort(sortedKeys.begin(), sortedKeys.end());
×
UNCOV
384
  for (const QString &key : sortedKeys) {
×
UNCOV
385
    out << "[" << key << "]\n";
×
386
    for (const QString &field : templates.value(key)) {
×
387
      out << field << "\n";
×
388
    }
UNCOV
389
    out << "\n";
×
390
  }
391
  out.flush();
×
392
  if (out.status() != QTextStream::Ok) {
×
393
    return false;
394
  }
395
  return saveFile.commit();
×
UNCOV
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
 */
UNCOV
405
auto Util::getFolderTemplate(const QString &folderPath,
×
406
                             const QString &storePath) -> QString {
UNCOV
407
  QDir storeDir(storePath);
×
UNCOV
408
  QString cleanStoreAbs = QDir::cleanPath(storeDir.absolutePath());
×
UNCOV
409
  QString sep = QDir::separator();
×
UNCOV
410
  QDir dir(folderPath);
×
411
  while (true) {
UNCOV
412
    if (dir.exists(".default_template")) {
×
413
      QFile file(dir.filePath(".default_template"));
×
UNCOV
414
      if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
×
415
        QTextStream in(&file);
×
416
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
417
        in.setEncoding(QStringConverter::Utf8);
×
418
#else
419
        in.setCodec("UTF-8");
420
#endif
421
        QString templateName = in.readLine().trimmed();
×
422
        file.close();
×
423
        if (!templateName.isEmpty() && !templateName.startsWith('#')) {
×
424
          return templateName;
425
        }
UNCOV
426
      }
×
UNCOV
427
    }
×
UNCOV
428
    QString currentPath = QDir::cleanPath(dir.absolutePath());
×
429
    if (currentPath == cleanStoreAbs) {
×
430
      break;
431
    }
UNCOV
432
    if (!currentPath.startsWith(cleanStoreAbs + sep)) {
×
433
      break;
434
    }
435
    if (!dir.cdUp()) {
×
436
      break;
437
    }
438
  }
439
  return {};
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