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

IJHack / QtPass / 35470682517

19 Sep 2026 09:31PM UTC coverage: 78.067% (-0.1%) from 78.167%
35470682517

Pull #1851

github

web-flow
Merge e227a4715 into c3fb65fdb
Pull Request #1851: Store walks visit real directories only; junctions and links are not entries

92 of 137 new or added lines in 6 files covered. (67.15%)

10 existing lines in 4 files now uncovered.

6891 of 8827 relevant lines covered (78.07%)

74.77 hits per line

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

89.53
/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 "appsettings.h"
16
#include "executor.h"
17
#include <QDebug>
18
#include <QDir>
19
#include <QFile>
20
#include <QFileInfo>
21
#include <QHash>
22
#include <QRegularExpressionMatchIterator>
23
#include <QStandardPaths>
24
#include <QUrl>
25
#ifdef Q_OS_WIN
26
#include <windows.h>
27
#else
28
#include <sys/time.h>
29
#endif
30

31
#include "qtpasslogging.h"
32

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

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

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

95
/**
96
 * @brief Expand a leading current-user tilde in a path.
97
 *
98
 * Environment variables set in non-shell contexts (systemd units, .desktop
99
 * entries, quoted shell assignments) skip shell tilde expansion and keep a
100
 * literal "~". "~username" forms are intentionally not resolved.
101
 */
102
auto Util::expandTilde(const QString &path) -> QString {
9 ✔
103
  if (path == QLatin1String("~")) {
9 ✔
104
    return QDir::homePath();
1 ✔
105
  }
106
  if (path.startsWith(QLatin1String("~/"))) {
8 ✔
107
    return QDir::homePath() + path.mid(1);
4 ✔
108
  }
109
  return path;
110
}
111

112
auto Util::normalizeFolderPath(const QString &path) -> QString {
117 ✔
113
  QString normalizedPath = path;
114
  if (!normalizedPath.endsWith('/')) {
117 ✔
115
    normalizedPath += '/';
111 ✔
116
  }
117
  return normalizedPath;
117 ✔
118
}
119

120
/**
121
 * @brief Finds the absolute path of a binary by searching the PATH environment
122
 * variable.
123
 *
124
 * Splits the (platform-augmented) PATH into directories and delegates to the
125
 * two-argument overload. On Windows, if no local match is found, it may fall
126
 * back to a WSL invocation when the binary name is valid and WSL appears to
127
 * support it.
128
 *
129
 * @example
130
 * QString result = Util::findBinaryInPath("git");
131
 * // Expected output sample: "/usr/bin/git" or "wsl git"
132
 *
133
 * @param QString binary - The name of the binary to locate.
134
 * @return QString - The absolute path to the binary, or an empty string if not
135
 * found.
136
 */
137
auto Util::findBinaryInPath(const QString &binary) -> QString {
163 ✔
138
  if (binary.isEmpty()) {
163 ✔
139
    return {};
140
  }
141

142
  initialiseEnvironment();
162 ✔
143

144
  const QStringList dirs =
145
      _env.value(QStringLiteral("PATH"))
324 ✔
146
          .split(QDir::listSeparator(), Qt::SkipEmptyParts);
162 ✔
147
  QString ret;
162 ✔
148
  if (QDir::fromNativeSeparators(binary).contains(u'/')) {
162 ✔
149
    // An explicit path is not a PATH search: an absolute path is checked
150
    // as-is, a relative one is resolved against the PATH directories. The
151
    // directory-list overload refuses such names, so handle them here.
152
    if (QDir::isAbsolutePath(binary)) {
2 ✔
153
      ret = QStandardPaths::findExecutable(binary);
4 ✔
154
    } else if (!dirs.isEmpty()) {
×
155
      ret = QStandardPaths::findExecutable(binary, dirs);
×
156
    }
157
  } else {
158
    ret = findBinaryInPath(binary, dirs);
320 ✔
159
  }
160
#ifdef Q_OS_WIN
161
  if (ret.isEmpty()) {
162
    // Cache per-binary WSL lookup result — the wsl --version probe is a
163
    // blocking subprocess that can run several times per session for
164
    // missing binaries; once decided, the answer doesn't change at runtime.
165
    static QHash<QString, QString> wslBinaryCache;
166
    const bool hasWhitespace =
167
        std::any_of(binary.cbegin(), binary.cend(),
168
                    [](const QChar ch) { return ch.isSpace(); });
169
    if (!hasWhitespace) {
170
      auto cached = wslBinaryCache.constFind(binary);
171
      if (cached != wslBinaryCache.constEnd()) {
172
        ret = cached.value();
173
      } else {
174
        QString wslCommand = QStringLiteral("wsl ") + binary;
175
        qCDebug(lcQtPass)
176
            << "Util::findBinaryInPath(): falling back to WSL for binary"
177
            << binary;
178
        QString out, err;
179
        QString cachedResult;
180
        if (Executor::executeBlocking(wslCommand, {"--version"}, &out, &err) ==
181
                0 &&
182
            !out.isEmpty() && err.isEmpty()) {
183
          qCDebug(lcQtPass)
184
              << "Util::findBinaryInPath(): using WSL binary" << wslCommand;
185
          cachedResult = wslCommand;
186
        }
187
        wslBinaryCache.insert(binary, cachedResult);
188
        ret = cachedResult;
189
      }
190
    }
191
  }
192
#endif
193

194
  return ret;
195
}
196

197
/**
198
 * @brief Finds an executable in an explicit list of directories.
199
 *
200
 * Thin wrapper around QStandardPaths::findExecutable(): only regular files
201
 * that are executable match (a directory named like the binary is skipped),
202
 * and on Windows the PATHEXT extensions are tried. Empty entries are dropped
203
 * rather than being resolved against the current working directory, and an
204
 * empty list finds nothing instead of silently falling back to the process
205
 * PATH. Only bare names are accepted: QStandardPaths::findExecutable() would
206
 * return an absolute @p binary without consulting @p searchPaths at all, and
207
 * a relative one containing ".." could escape them, so both find nothing.
208
 *
209
 * @param binary The name of the binary to locate; must not contain a
210
 * directory separator.
211
 * @param searchPaths Directories to search, in order.
212
 * @return QString - The absolute path to the binary, or an empty string if not
213
 * found.
214
 */
215
auto Util::findBinaryInPath(const QString &binary,
174 ✔
216
                            const QStringList &searchPaths) -> QString {
217
  if (binary.isEmpty() || QDir::fromNativeSeparators(binary).contains(u'/')) {
347 ✔
218
    return {};
219
  }
220
  QStringList dirs;
170 ✔
221
  dirs.reserve(searchPaths.size());
170 ✔
222
  for (const QString &dir : searchPaths) {
3,026 ✔
223
    if (!dir.isEmpty()) {
2,856 ✔
224
      dirs.append(dir);
225
    }
226
  }
227
  if (dirs.isEmpty()) {
170 ✔
228
    // QStandardPaths::findExecutable() treats an empty list as "use PATH".
229
    return {};
230
  }
231
  return QStandardPaths::findExecutable(binary, dirs);
166 ✔
232
}
233

234
/**
235
 * @brief Checks whether the current QtPass configuration is valid.
236
 * @example
237
 * AppSettings s = QtPassSettings::load();
238
 * bool result = Util::configIsValid(s);
239
 * std::cout << std::boolalpha << result << std::endl; // Expected output: true
240
 * or false
241
 *
242
 * @param s Application settings snapshot to validate.
243
 * @return bool - True if the configuration file exists and the required
244
 * executable is available; otherwise false.
245
 */
246
auto Util::configIsValid(const AppSettings &s) -> bool {
80 ✔
247
  const QString configFilePath = QDir(s.passStore).filePath(".gpg-id");
160 ✔
248
  if (!QFile(configFilePath).exists()) {
80 ✔
249
    return false;
250
  }
251

252
  const QString executable = s.usePass ? s.passExecutable : s.gpgExecutable;
46 ✔
253

254
  if (const auto wsl = Executor::parseWslCommand(executable)) {
46 ✔
255
    // Probe WSL once per session — availability doesn't change at runtime
256
    // and the executeBlocking call is a blocking subprocess.
257
    static const bool wslAvailable = [&wsl]() {
×
258
      QString out;
×
259
      QString err;
×
260
      return Executor::executeBlocking(wsl->launcher,
×
261
                                       {QStringLiteral("--version")}, &out,
×
262
                                       &err) == 0 &&
×
263
             !out.isEmpty() && err.isEmpty();
×
264
    }();
×
265
    if (wslAvailable) {
×
266
      return true;
267
    }
268
  }
269
  return QFile(executable).exists();
46 ✔
270
}
271

272
/**
273
 * @brief Returns a regex matching strings that end with the .gpg extension.
274
 *
275
 * @return QRegularExpression reference
276
 */
277
auto Util::endsWithGpg() -> const QRegularExpression & {
757 ✔
278
  static const QRegularExpression expr{R"(\.gpg$)"};
757 ✔
279
  return expr;
757 ✔
280
}
281

282
/**
283
 * @brief Returns a regex matching common remote/network protocol schemes.
284
 *
285
 * Matches http://, https://, ftp://, ftps://, ssh://, sftp://, webdav://,
286
 * webdavs://
287
 *
288
 * The URL text ends at the first whitespace character (space, tab, CR, LF),
289
 * quote or bracket, so a URL on its own line in multi-line text (pass file
290
 * bodies, gpg stderr) is captured without the line break that follows it.
291
 *
292
 * Note: Local file URLs (file:///) are intentionally excluded by design, as
293
 * they represent local paths rather than network protocols. If this behavior
294
 * needs to change, update both this function and the corresponding test.
295
 *
296
 * @return QRegularExpression reference
297
 */
298
auto Util::protocolRegex() -> const QRegularExpression & {
112 ✔
299
  static const QRegularExpression regex{
300
      R"(((?:https?|ftp|ssh|sftp|ftps|webdav|webdavs)://[^"\s<>\)\]\[]+))"};
112 ✔
301
  return regex;
112 ✔
302
}
303

304
/**
305
 * @brief Validate a value as a launchable http(s) URL.
306
 *
307
 * Security gate for the "open in browser" action. See util.h for the full
308
 * contract. Deliberately stricter than protocolRegex(): only http/https,
309
 * valid host, no embedded credentials, no control characters.
310
 *
311
 * @param value Candidate URL string.
312
 * @return true if launchable in a browser, false otherwise.
313
 */
314
auto Util::isLaunchableWebUrl(const QString &value) -> bool {
81 ✔
315
  const QString trimmed = value.trimmed();
316
  if (trimmed.isEmpty()) {
81 ✔
317
    return false;
318
  }
319
  // Reject control characters first, before QUrl normalisation can hide a
320
  // CR/LF/NUL injection into the OS URL handler.
321
  for (const QChar &c : trimmed) {
1,945 ✔
322
    if (c == QLatin1Char('\r') || c == QLatin1Char('\n') ||
323
        c == QChar(QChar::Null)) {
324
      return false;
325
    }
326
  }
327
  const QUrl url(trimmed, QUrl::StrictMode);
77 ✔
328
  if (!url.isValid()) {
77 ✔
329
    return false;
330
  }
331
  const QString scheme = url.scheme().toLower();
134 ✔
332
  if (scheme != QLatin1String("http") && scheme != QLatin1String("https")) {
129 ✔
333
    return false;
22 ✔
334
  }
335
  if (url.host().isEmpty()) {
90 ✔
336
    return false;
337
  }
338
  // Embedded userinfo (user:pass@host) would leak into browser history.
339
  if (!url.userName().isEmpty() || !url.password().isEmpty()) {
83 ✔
340
    return false;
341
  }
342
  return true;
343
}
77 ✔
344

345
/**
346
 * @brief Escape text as HTML and link only launchable http(s) URLs.
347
 *
348
 * See util.h for the contract. Detection uses protocolRegex() so that the
349
 * URL text is delimited the same way everywhere; the decision whether a
350
 * match becomes an anchor is isLaunchableWebUrl(), the same predicate that
351
 * gates the "open in browser" button.
352
 *
353
 * @param text Plain text, not yet HTML-escaped.
354
 * @param linked Set to true when at least one anchor was emitted.
355
 * @return HTML string safe to hand to QTextBrowser::setHtml().
356
 */
357
auto Util::linkifyUrls(const QString &text, bool *linked) -> QString {
108 ✔
358
  if (linked != nullptr) {
108 ✔
359
    *linked = false;
80 ✔
360
  }
361
  QString html;
108 ✔
362
  html.reserve(text.size());
108 ✔
363
  qsizetype lastIndex = 0;
364
  QRegularExpressionMatchIterator it = protocolRegex().globalMatch(text);
108 ✔
365
  while (it.hasNext()) {
143 ✔
366
    const QRegularExpressionMatch match = it.next();
35 ✔
367
    const QString url = match.captured(0);
35 ✔
368
    if (!isLaunchableWebUrl(url)) {
35 ✔
369
      // Not a web URL (or it carries credentials): leave it in the escaped
370
      // plain-text run instead of making it clickable.
371
      continue;
372
    }
373
    const qsizetype start = match.capturedStart(0);
22 ✔
374
    html += text.mid(lastIndex, start - lastIndex).toHtmlEscaped();
22 ✔
375
    const QString escapedUrl = url.toHtmlEscaped();
22 ✔
376
    html += QStringLiteral("<a href=\"%1\">%1</a>").arg(escapedUrl);
44 ✔
377
    lastIndex = match.capturedEnd(0);
22 ✔
378
    if (linked != nullptr) {
22 ✔
379
      *linked = true;
16 ✔
380
    }
381
  }
35 ✔
382
  html += text.mid(lastIndex).toHtmlEscaped();
108 ✔
383
  return html;
108 ✔
384
}
108 ✔
385

386
/**
387
 * @brief Returns a regex matching newline characters (CR or LF).
388
 *
389
 * Useful for detecting or sanitising line breaks in text content.
390
 *
391
 * @return QRegularExpression reference
392
 */
393
auto Util::newLinesRegex() -> const QRegularExpression & {
109 ✔
394
  static const QRegularExpression regex{"[\r\n]"};
109 ✔
395
  return regex;
109 ✔
396
}
397

398
/**
399
 * @brief Validate whether a string is an accepted GPG key identifier.
400
 *
401
 * Mirrors what `pass` itself accepts in `.gpg-id`: every non-empty token is
402
 * handed to gpg as a `-r` argument, and gpg resolves it — key ID or
403
 * fingerprint of any version (v4 hex, v6 hex, with or without `0x`),
404
 * `<email>`, `=Exact User ID`, a plain name substring, or a `@`/`/`/`#`/`&`
405
 * routing prefix. No content heuristics are applied here: they can only
406
 * reject recipients gpg would have accepted, and a rejected line is not just
407
 * skipped but erased the next time `.gpg-id` is rewritten.
408
 *
409
 * The one thing rejected is a token starting with `-`: the recipient list is
410
 * also passed positionally to `gpg --list-keys`, where such a token would be
411
 * parsed as an option instead of a key selector.
412
 *
413
 * Empty input is invalid.
414
 *
415
 * @param keyId Input key identifier string to validate.
416
 * @return true unless the input is empty or starts with `-`.
417
 */
418
auto Util::isValidKeyId(const QString &keyId) -> bool {
121 ✔
419
  return !keyId.isEmpty() && !keyId.startsWith('-');
121 ✔
420
}
421

422
namespace {
423
/**
424
 * @brief Walk @p dir's real, visible directories in sorted pre-order and hand
425
 * every entry to @p visit before any recursion; a directory is entered only
426
 * when @p visit returns true for it.
427
 */
428
template <typename Visit> void walkStore(const QString &dir, Visit visit) {
80 ✔
429
  // Absolute, so the results are whatever the caller's cwd; not canonical,
430
  // so a linked root keeps the name it was configured under.
431
  QStringList pending{QDir(QDir::cleanPath(dir)).absolutePath()};
240 ✔
432
  while (!pending.isEmpty()) {
294 ✔
433
    const QDir current(pending.takeLast());
214 ✔
434
    // Every entry once, links and hidden entries included, so the decision
435
    // what to do with each is taken here, before any recursion. QDir::System
436
    // keeps dangling links in the listing.
437
    const QFileInfoList entries =
107 ✔
438
        current.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden |
439
                                  QDir::System | QDir::NoDotAndDotDot,
440
                              QDir::Name);
441
    QStringList subdirs;
107 ✔
442
    for (const QFileInfo &entry : entries) {
429 ✔
443
      if (visit(entry)) {
322 ✔
444
        subdirs << entry.filePath();
54 ✔
445
      }
446
    }
447
    // Pushed last-to-first so the next one taken is the first by name.
448
    for (auto it = subdirs.crbegin(); it != subdirs.crend(); ++it) {
134 ✔
449
      pending << *it;
450
    }
451
  }
452
}
160 ✔
453

454
/// A symlink or an NTFS junction: never entered, never listed.
455
auto isLink(const QFileInfo &entry) -> bool {
649 ✔
456
  return entry.isSymLink() || entry.isJunction();
649 ✔
457
}
458

459
/// Hidden by attribute, or by the dot convention on every platform: Qt 6.11
460
/// on macOS answers isHidden() from the UF_HIDDEN flag alone once an entry
461
/// was lstat()ed (qfilesystemengine_unix.cpp marks the attribute known there
462
/// without the dot check), and Windows does not consider .stversions hidden
463
/// at all.
464
auto isHiddenEntry(const QFileInfo &entry) -> bool {
101 ✔
465
  return entry.isHidden() || entry.fileName().startsWith(QLatin1Char('.'));
195 ✔
466
}
467

468
/// A real directory that is part of the store: .git, .stversions,
469
/// .Trash-1000 are not, as with QDirIterator without QDir::Hidden.
470
auto isStoreDirectory(const QFileInfo &entry) -> bool {
322 ✔
471
  return !isLink(entry) && entry.isDir() && !isHiddenEntry(entry);
322 ✔
472
}
473
} // namespace
474

475
auto Util::regularFilesUnder(const QString &dir, const QStringList &nameFilters,
78 ✔
476
                             QStringList *skipped, bool hiddenFiles)
477
    -> QStringList {
478
  QStringList files;
78 ✔
479
  walkStore(dir, [&](const QFileInfo &entry) {
78 ✔
480
    if (isStoreDirectory(entry)) {
312 ✔
481
      return true;
482
    }
483
    const bool named = QDir::match(nameFilters, entry.fileName());
289 ✔
484
    if (isLink(entry) || (!entry.isDir() && !entry.isFile())) {
289 ✔
485
      // A linked directory hides everything behind it; a linked file, or a
486
      // FIFO, socket or device, is only of interest under a name the caller
487
      // asked for.
488
      if (skipped != nullptr && (entry.isDir() || named)) {
15 ✔
489
        qCWarning(lcQtPass) << "Skipping" << entry.filePath()
20 ✔
490
                            << ": not a regular file or directory";
10 ✔
491
        *skipped << entry.filePath();
20 ✔
492
      }
493
      return false;
15 ✔
494
    }
495
    if (entry.isFile() && named && (hiddenFiles || !isHiddenEntry(entry))) {
274 ✔
496
      files << entry.filePath();
148 ✔
497
    }
498
    return false;
499
  });
500
  return files;
78 ✔
501
}
502

503
auto Util::directoriesUnder(const QString &dir) -> QStringList {
2 ✔
504
  QStringList dirs;
2 ✔
505
  walkStore(dir, [&](const QFileInfo &entry) {
2 ✔
506
    if (!isStoreDirectory(entry)) {
10 ✔
507
      return false;
508
    }
509
    dirs << entry.filePath();
4 ✔
510
    return true;
4 ✔
511
  });
512
  return dirs;
2 ✔
513
}
514

515
auto Util::isLinkedFolder(const QString &path) -> bool {
24 ✔
516
  return isLink(QFileInfo(QDir::cleanPath(path)));
48 ✔
517
}
518

519
auto Util::isUnderLink(const QString &path, const QString &storeRoot) -> bool {
44 ✔
520
  const QString root = QDir::cleanPath(storeRoot);
44 ✔
521
  // A root of "/" or "C:/" already ends in the separator.
522
  const QString prefix =
523
      root.endsWith(QLatin1Char('/')) ? root : root + QLatin1Char('/');
44 ✔
524
  QString current = QDir::cleanPath(path);
44 ✔
525
  if (!current.startsWith(prefix)) {
44 ✔
526
    // Not under the store as named: only the entry itself can be judged.
527
    return current != root && isLinkedFolder(current);
33 ✔
528
  }
529
  for (; current != root && current.startsWith(prefix);
22 ✔
530
       current = QFileInfo(current).path()) {
22 ✔
531
    if (isLinkedFolder(current)) {
17 ✔
532
      return true;
533
    }
534
  }
535
  return false;
536
}
537

538
auto Util::removeTree(const QString &dir) -> bool {
7 ✔
539
  // A trailing separator makes lstat follow a link ("link/" is the target
540
  // directory); the link itself is what this is about.
541
  const QString path = QDir::cleanPath(dir);
7 ✔
542
  const QFileInfo top(path);
7 ✔
543
  if (isLink(top)) {
7 ✔
544
    // rm -rf on a link removes the link.
545
    return QFile::remove(path) || QDir().rmdir(path);
3 ✔
546
  }
547
  if (!top.isDir()) {
4 ✔
548
    return false;
549
  }
550
  bool ok = true;
551
  const QFileInfoList entries =
552
      QDir(path).entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden |
6 ✔
553
                                   QDir::System | QDir::NoDotAndDotDot,
554
                               QDir::Name);
3 ✔
555
  for (const QFileInfo &entry : entries) {
10 ✔
556
    const QString entryPath = entry.filePath();
7 ✔
557
    if (isLink(entry)) {
7 ✔
558
      // The entry itself, never the target. A junction or a directory
559
      // symlink on Windows is a directory entry and goes with rmdir.
560
      if (!QFile::remove(entryPath) && !QDir().rmdir(entryPath)) {
3 ✔
NEW
561
        qCWarning(lcQtPass) << "Could not remove link" << entryPath;
×
562
        ok = false;
563
      }
564
    } else if (entry.isDir()) {
4 ✔
565
      ok = removeTree(entryPath) && ok;
1 ✔
566
    } else if (!QFile::remove(entryPath)) {
3 ✔
567
      // A read-only file blocks deletion on Windows; give it write access
568
      // and try once more, as QDir::removeRecursively() does.
NEW
569
      const QFile::Permissions perms = QFile::permissions(entryPath);
×
NEW
570
      if (perms.testFlag(QFile::WriteUser) ||
×
NEW
571
          !QFile::setPermissions(entryPath, perms | QFile::WriteUser) ||
×
NEW
572
          !QFile::remove(entryPath)) {
×
NEW
573
        qCWarning(lcQtPass) << "Could not remove" << entryPath;
×
574
        ok = false;
575
      }
576
    }
577
  }
578
  return ok && QDir().rmdir(path);
6 ✔
579
}
7 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc