QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
util.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
9#include "util.h"
10#include "appsettings.h"
11#include "executor.h"
12#include <QCoreApplication>
13#include <QDebug>
14#include <QDir>
15#include <QFile>
16#include <QFileDevice>
17#include <QFileInfo>
18#include <QHash>
19#include <QRegularExpression>
20#include <QRegularExpressionMatchIterator>
21#include <QStandardPaths>
22#include <QTemporaryFile>
23#include <QUrl>
24
25#include <optional>
26
27#ifdef Q_OS_WIN
28#include <fcntl.h>
29#include <io.h>
30#include <windows.h>
31#else
32#include <cerrno>
33#include <cstdio>
34#include <fcntl.h>
35#include <sys/stat.h>
36#include <sys/time.h>
37#include <unistd.h>
38#endif
39
40#include "qtpasslogging.h"
41
42QProcessEnvironment Util::_env;
43bool Util::_envInitialised = false;
44
45// macOS and Windows: append the usual GPG install directories to PATH.
47 if (!_envInitialised) {
48 _env = QProcessEnvironment::systemEnvironment();
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");
69 _envInitialised = true;
70 }
71}
72
73auto Util::findPasswordStore() -> QString {
74 QString path;
76 if (_env.contains("PASSWORD_STORE_DIR")) {
77 path = Util::expandTilde(_env.value("PASSWORD_STORE_DIR"));
78 } else {
79#ifdef Q_OS_WIN
80 path = QDir(QDir::homePath()).filePath("password-store");
81#else
82 path = QDir(QDir::homePath()).filePath(".password-store");
83#endif
84 }
85 return Util::normalizeFolderPath(QDir::cleanPath(path));
86}
87
88auto Util::expandTilde(const QString &path) -> QString {
89 if (path == QLatin1String("~")) {
90 return QDir::homePath();
91 }
92 if (path.startsWith(QLatin1String("~/"))) {
93 return QDir::homePath() + path.mid(1);
94 }
95 return path;
96}
97
98auto Util::normalizeFolderPath(const QString &path) -> QString {
99 QString normalizedPath = path;
100 if (!normalizedPath.endsWith('/')) {
101 normalizedPath += '/';
102 }
103 return normalizedPath;
104}
105
107 // pass unsets these before it runs git: a GIT_DIR inherited from the
108 // environment (QtPass started from a git hook, or an IDE terminal) would
109 // send every commit and push to that repository instead of the store.
110 for (const char *variable :
111 {"GIT_DIR", "GIT_WORK_TREE", "GIT_NAMESPACE", "GIT_INDEX_FILE",
112 "GIT_INDEX_VERSION", "GIT_OBJECT_DIRECTORY", "GIT_COMMON_DIR",
113 // QtPass sets GIT_LITERAL_PATHSPECS for its git; git refuses a
114 // pathspec when one of these is set as well.
115 "GIT_GLOB_PATHSPECS", "GIT_NOGLOB_PATHSPECS", "GIT_ICASE_PATHSPECS"}) {
116 qunsetenv(variable);
117 }
119 passUmask(qEnvironmentVariable("PASSWORD_STORE_UMASK")));
120}
121
122auto Util::passUmask(const QString &value) -> int {
123 // Taken as it is, as bash's umask builtin (which pass hands it to) takes
124 // it: " 022" is no umask there either. pass then keeps the session's;
125 // QtPass falls back to 077.
126 // anchoredPattern(), not ^...$: $ also matches before a final newline.
127 static const QRegularExpression octal(
128 QRegularExpression::anchoredPattern(QStringLiteral("[0-7]{1,4}")));
129 if (!octal.match(value).hasMatch()) {
130 return 077;
131 }
132 return value.toInt(nullptr, 8);
133}
134
135auto Util::gitCeiling(const QString &store) -> QString {
136 if (store.isEmpty()) {
137 return {};
138 }
139 QString path = QFileInfo(store).canonicalFilePath();
140 if (path.isEmpty()) {
141 path = QDir::cleanPath(QDir(store).absolutePath());
142 }
143 return QFileInfo(path).absolutePath();
144}
145
146auto Util::repositoryPassClimbsTo(const QString &store) -> QString {
147#ifdef Q_OS_WIN
148 Q_UNUSED(store);
149 return {};
150#else
151 if (!store.contains(QLatin1Char(':'))) {
152 return {};
153 }
154 // git walks up from the real folder, so the real path is the one to walk,
155 // and for a store that does not exist yet the one it will have: its
156 // nearest existing folder resolved, the rest appended. By the raw string:
157 // QDir and QFileInfo clean a path by name, and "link/.." is the parent
158 // of the link's target, not the folder the link is in. On macOS the
159 // temporary folder is under /var, a link to /private/var.
160 // Absolute as git means it, a leading "/" (Qt would take ":name" for a
161 // resource path).
162 QString path =
163 store.startsWith(u'/') ? store : QDir::currentPath() + u'/' + store;
164 while (path.size() > 1 && path.endsWith(u'/')) {
165 path.chop(1);
166 }
167 QString rest;
168 while (!QFileInfo::exists(path)) {
169 const qsizetype slash = path.lastIndexOf(u'/');
170 if (slash < 0) {
171 return {};
172 }
173 const QString name = path.mid(slash + 1);
174 rest = rest.isEmpty() ? name : name + u'/' + rest;
175 path = slash == 0 ? QStringLiteral("/") : path.left(slash);
176 }
177 const QString real = QFileInfo(path).canonicalFilePath();
178 QString start = real.isEmpty() ? QDir::cleanPath(path) : real;
179 if (!rest.isEmpty()) {
180 start = QDir::cleanPath(start + u'/' + rest);
181 }
182 // The pieces of pass's ceiling as git takes them (setup.c,
183 // canonicalize_ceiling_entry): relative ones are dropped, the rest
184 // resolved, one that cannot be resolved is dropped; after an empty piece
185 // ("::") they are kept exactly as written. A piece can still be a real
186 // ceiling: "/repo/:vault/.." holds "/repo/".
187 QString ceiling;
188 bool afterEmpty = false;
189 for (const QString &piece :
190 (store + QStringLiteral("/..")).split(QLatin1Char(':'))) {
191 if (piece.isEmpty()) {
192 afterEmpty = true;
193 continue;
194 }
195 if (!piece.startsWith(u'/')) {
196 continue;
197 }
198 const QString entry =
199 afterEmpty ? piece : QFileInfo(piece).canonicalFilePath();
200 // git stops below the longest entry the store lies under
201 // (longest_ancestor_length: "/" is above everything).
202 const QString under = entry == u'/' ? entry : entry + u'/';
203 if (!entry.isEmpty() && start.startsWith(under) &&
204 entry.size() > ceiling.size()) {
205 ceiling = entry;
206 }
207 }
208 // What git takes for a repository (setup.c, is_git_directory()): HEAD
209 // naming a ref or an object, objects/ and refs/. An empty or broken .git
210 // is passed by, as git passes it by.
211 const auto isGitDirectory = [](const QString &candidate) {
212 const QDir dir(candidate);
213 if (!QFileInfo(dir.filePath(QStringLiteral("objects"))).isDir() ||
214 !QFileInfo(dir.filePath(QStringLiteral("refs"))).isDir()) {
215 return false;
216 }
217 QFile head(dir.filePath(QStringLiteral("HEAD")));
218 if (!head.open(QIODevice::ReadOnly)) {
219 return false;
220 }
221 static const QRegularExpression valid(
222 QStringLiteral("^(?:ref: refs/|[0-9a-f]{40}\\s*$|[0-9a-f]{64}\\s*$)"));
223 return valid.match(QString::fromUtf8(head.read(256))).hasMatch();
224 };
225 // At each folder, as git: a .git folder that is a repository, or a .git
226 // file naming one ("gitdir: ..."), is the repository. Any other .git file
227 // stops git with an error, and pass then commits nothing; a .git folder
228 // that is no repository is passed by.
229 enum class Found { Nothing, Repository, Refused };
230 const auto repositoryAt = [&isGitDirectory](const QString &dir) {
231 const QString dotGit = QDir(dir).filePath(QStringLiteral(".git"));
232 const QFileInfo info(dotGit);
233 if (info.isDir()) {
234 return isGitDirectory(dotGit) ? Found::Repository : Found::Nothing;
235 }
236 if (!info.exists()) {
237 return Found::Nothing;
238 }
239 QFile file(dotGit);
240 if (!file.open(QIODevice::ReadOnly)) {
241 return Found::Refused;
242 }
243 const QString line = QString::fromUtf8(file.readLine(4096)).trimmed();
244 if (!line.startsWith(QLatin1String("gitdir: "))) {
245 return Found::Refused;
246 }
247 const QString target = line.mid(8);
248 return isGitDirectory(target.startsWith(u'/') ? target
249 : QDir(dir).filePath(target))
250 ? Found::Repository
251 : Found::Refused;
252 };
253 // The store's own repository comes first; only without one does git look
254 // further up, and never into the ceiling. By name from here: the real
255 // path holds no link or "..".
256 for (QString dir = start;;) {
257 switch (repositoryAt(dir)) {
258 case Found::Repository:
259 return dir == start ? QString() : dir;
260 case Found::Refused:
261 return {};
262 case Found::Nothing:
263 break;
264 }
265 // A bare repository: git stops there, and pass, finding no work tree,
266 // commits nothing.
267 if (isGitDirectory(dir)) {
268 return {};
269 }
270 const QString up = QFileInfo(dir).absolutePath();
271 if (up == dir || up == ceiling) {
272 return {};
273 }
274 dir = up;
275 }
276#endif
277}
278
279namespace {
281std::optional<uint> s_gitOwnerForTests;
282} // namespace
283
284void Util::setGitOwnerForTests(std::optional<uint> user) {
285 s_gitOwnerForTests = user;
286}
287
288#ifdef Q_OS_UNIX
289namespace {
306auto isSafeDirectory(const QString &root, const QString &git) -> bool {
307 // Outside any repository, as git reads its protected config: no
308 // repository's own config, and no includeIf "gitdir:" or "onbranch:"
309 // that matches (a GIT_DIR, even one that cannot exist, would be matched
310 // against them, and one that cannot exist makes git config fail). From
311 // "/", which a user cannot make a repository; a local scope is dropped
312 // below in any case.
313 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
314 env.remove(QStringLiteral("GIT_DIR"));
315 env.remove(QStringLiteral("GIT_WORK_TREE"));
316 env.remove(QStringLiteral("GIT_COMMON_DIR"));
317 // git config alone reads GIT_CONFIG in place of the system and global
318 // configuration (and reports it as "command"); the ownership check never
319 // reads it.
320 env.remove(QStringLiteral("GIT_CONFIG"));
321 QProcess process;
322 process.setProcessEnvironment(env);
323 process.setWorkingDirectory(QStringLiteral("/"));
324 // Bounded: Git calls wait for this, and a git that never answers trusts
325 // nothing.
326 constexpr int kQueryTimeoutMs = 5000;
327 QString out;
329 process, git.isEmpty() ? QStringLiteral("git") : git,
330 {QStringLiteral("config"), QStringLiteral("--show-scope"),
331 QStringLiteral("--type=path"), QStringLiteral("--get-all"),
332 QStringLiteral("safe.directory")},
333 QString(), &out, nullptr, nullptr, kQueryTimeoutMs) != 0) {
334 return false;
335 }
336 bool safe = false;
337 const QStringList lines = out.split(QLatin1Char('\n'));
338 for (const QString &line : lines) {
339 const qsizetype tab = line.indexOf(QLatin1Char('\t'));
340 if (tab < 0) {
341 continue;
342 }
343 const QString scope = line.left(tab);
344 if (scope != QLatin1String("system") && scope != QLatin1String("global") &&
345 scope != QLatin1String("command")) {
346 continue;
347 }
348 const QString value = line.mid(tab + 1);
349 if (value.isEmpty()) {
350 safe = false;
351 } else if (value == QLatin1String("*")) {
352 safe = true;
353 } else if (!QDir::isAbsolutePath(value)) {
354 continue;
355 } else if (value.endsWith(QLatin1String("/*"))) {
356 const QString base = value.chopped(2);
357 QString prefix = QFileInfo(base.isEmpty() ? QStringLiteral("/") : base)
358 .canonicalFilePath();
359 if (prefix.isEmpty()) {
360 continue;
361 }
362 if (!prefix.endsWith(QLatin1Char('/'))) {
363 prefix += QLatin1Char('/');
364 }
365 safe = safe || root.startsWith(prefix);
366 } else {
367 const QString named = QFileInfo(value).canonicalFilePath();
368 safe = safe || (!named.isEmpty() && named == root);
369 }
370 }
371 return safe;
372}
373} // namespace
374#endif
375
376auto Util::gitStoreVariables(const QString &store, const QString &git,
377 std::optional<uint> asUser)
378 -> QList<std::pair<QString, QString>> {
379 const QString ceiling = gitCeiling(store);
380 if (ceiling.isEmpty()) {
381 return {};
382 }
383 QList<std::pair<QString, QString>> vars{
384 {QStringLiteral("GIT_CEILING_DIRECTORIES"), ceiling}};
385#ifdef Q_OS_WIN
386 const QChar separator = QLatin1Char(';');
387#else
388 const QChar separator = QLatin1Char(':');
389#endif
390 // git splits the ceiling list on the separator: "/home/a:b" would be
391 // "/home/a" and "b", neither of them the parent, and git would climb.
392 if (ceiling.contains(separator)) {
393 QString root = QFileInfo(store).canonicalFilePath();
394 if (root.isEmpty()) {
395 root = QDir::cleanPath(QDir(store).absolutePath());
396 }
397 QString gitDir = QDir(root).filePath(QStringLiteral(".git"));
398#ifdef Q_OS_UNIX
399 // What discovery's ownership check would have looked at: the work tree,
400 // the .git, and the directory a gitfile names. Not the user's: no
401 // repository at all, under a path that cannot be created (its parent
402 // is a device), rather than another user's hooks run as this one.
403 const uint me = asUser.value_or(
404 s_gitOwnerForTests.value_or(static_cast<uint>(::geteuid())));
405 const auto ownedOrMissing = [me](const QString &path) {
406 const QFileInfo info(path);
407 return !info.exists() || info.ownerId() == me;
408 };
409 bool owned = ownedOrMissing(root) && ownedOrMissing(gitDir);
410 if (owned && QFileInfo(gitDir).isFile()) {
411 QFile gitFile(gitDir);
412 if (gitFile.open(QIODevice::ReadOnly)) {
413 const QString line =
414 QString::fromUtf8(gitFile.readLine(4096)).trimmed();
415 if (line.startsWith(QLatin1String("gitdir:"))) {
416 const QString target = line.mid(7).trimmed();
417 owned = ownedOrMissing(QDir(root).absoluteFilePath(target));
418 }
419 }
420 }
421 // Or trusted as git would trust it in place of ownership.
422 if (!owned && !isSafeDirectory(root, git)) {
423 gitDir = QStringLiteral("/dev/null/.git");
424 }
425#else
426 // Git for Windows runs the same ownership check during discovery, on
427 // SIDs, with its own rules for the Administrators group; this pin skips
428 // it there and does not repeat it (a known gap, #2089).
429 Q_UNUSED(git);
430 Q_UNUSED(asUser);
431#endif
432 vars.append({QStringLiteral("GIT_DIR"), gitDir});
433 vars.append({QStringLiteral("GIT_WORK_TREE"), root});
434 }
435 return vars;
436}
437
438auto Util::mkpathForStore(const QString &path) -> bool {
439 const QString clean = QDir::cleanPath(QDir(path).absolutePath());
440 if (QFileInfo(clean).isDir()) {
441 return true;
442 }
443 const QString parent = QFileInfo(clean).absolutePath();
444 if (parent != clean && !mkpathForStore(parent)) {
445 return false;
446 }
447#ifdef Q_OS_UNIX
448 // The store's umask, as git and gpg get it: a store shared with a group
449 // (PASSWORD_STORE_UMASK=007) gets group folders, the default owner-only.
450 const int umask = Executor::childUmask() >= 0 ? Executor::childUmask() : 077;
451 QFile::Permissions permissions;
452 const QFile::Permission bits[] = {
453 QFile::ReadOwner, QFile::WriteOwner, QFile::ExeOwner,
454 QFile::ReadGroup, QFile::WriteGroup, QFile::ExeGroup,
455 QFile::ReadOther, QFile::WriteOther, QFile::ExeOther};
456 for (int i = 0; i < 9; ++i) {
457 if ((umask & (0400 >> i)) == 0) {
458 permissions |= bits[i];
459 }
460 }
461 // mkdir() still takes QtPass's own umask off (a session's 022 would drop
462 // the group's write bit); set the mode exactly afterwards. That only ever
463 // widens it to what was asked, never past.
464 const bool made = QDir().mkdir(clean, permissions) &&
465 QFile::setPermissions(clean, permissions);
466#else
467 // Windows: permissions here would become an ACL of their own, replacing
468 // the inherited one; the folder takes its parent's, as before.
469 const bool made = QDir().mkdir(clean);
470#endif
471 return made || QFileInfo(clean).isDir();
472}
473
474auto Util::findBinaryInPath(const QString &binary) -> QString {
475 if (binary.isEmpty()) {
476 return {};
477 }
478
480
481 const QStringList dirs =
482 _env.value(QStringLiteral("PATH"))
483 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
484 QString ret;
485 if (QDir::fromNativeSeparators(binary).contains(u'/')) {
486 // An explicit path is not a PATH search: an absolute path is checked
487 // as-is, a relative one is resolved against the PATH directories. The
488 // directory-list overload refuses such names, so handle them here.
489 if (QDir::isAbsolutePath(binary)) {
490 ret = QStandardPaths::findExecutable(binary);
491 } else if (!dirs.isEmpty()) {
492 ret = QStandardPaths::findExecutable(binary, dirs);
493 }
494 } else {
495 ret = findBinaryInPath(binary, dirs);
496 }
497#ifdef Q_OS_WIN
498 if (ret.isEmpty()) {
499 // Cache per-binary WSL lookup result — the wsl --version probe is a
500 // blocking subprocess that can run several times per session for
501 // missing binaries; once decided, the answer doesn't change at runtime.
502 static QHash<QString, QString> wslBinaryCache;
503 const bool hasWhitespace =
504 std::any_of(binary.cbegin(), binary.cend(),
505 [](const QChar ch) { return ch.isSpace(); });
506 if (!hasWhitespace) {
507 auto cached = wslBinaryCache.constFind(binary);
508 if (cached != wslBinaryCache.constEnd()) {
509 ret = cached.value();
510 } else {
511 QString wslCommand = QStringLiteral("wsl ") + binary;
512 qCDebug(lcQtPass)
513 << "Util::findBinaryInPath(): falling back to WSL for binary"
514 << binary;
515 QString out, err;
516 QString cachedResult;
517 if (Executor::executeBlocking(wslCommand, {"--version"}, &out, &err) ==
518 0 &&
519 !out.isEmpty() && err.isEmpty()) {
520 qCDebug(lcQtPass)
521 << "Util::findBinaryInPath(): using WSL binary" << wslCommand;
522 cachedResult = wslCommand;
523 }
524 wslBinaryCache.insert(binary, cachedResult);
525 ret = cachedResult;
526 }
527 }
528 }
529#endif
530
531 return ret;
532}
533
534// Bare names only: QStandardPaths::findExecutable() returns an absolute binary
535// without consulting searchPaths, and a relative one with ".." could escape
536// them.
537auto Util::findBinaryInPath(const QString &binary,
538 const QStringList &searchPaths) -> QString {
539 if (binary.isEmpty() || QDir::fromNativeSeparators(binary).contains(u'/')) {
540 return {};
541 }
542 QStringList dirs;
543 dirs.reserve(searchPaths.size());
544 for (const QString &dir : searchPaths) {
545 if (!dir.isEmpty()) {
546 dirs.append(dir);
547 }
548 }
549 if (dirs.isEmpty()) {
550 // QStandardPaths::findExecutable() treats an empty list as "use PATH".
551 return {};
552 }
553 return QStandardPaths::findExecutable(binary, dirs);
554}
555
556auto Util::configIsValid(const AppSettings &s) -> bool {
557 const QString configFilePath = QDir(s.passStore).filePath(".gpg-id");
558 if (!QFile(configFilePath).exists()) {
559 return false;
560 }
561
562 const QString executable = s.usePass ? s.passExecutable : s.gpgExecutable;
563
564 if (const auto wsl = Executor::parseWslCommand(executable)) {
565 // Probe WSL once per session — availability doesn't change at runtime
566 // and the executeBlocking call is a blocking subprocess.
567 static const bool wslAvailable = [&wsl]() {
568 QString out;
569 QString err;
570 return Executor::executeBlocking(wsl->launcher,
571 {QStringLiteral("--version")}, &out,
572 &err) == 0 &&
573 !out.isEmpty() && err.isEmpty();
574 }();
575 if (wslAvailable) {
576 return true;
577 }
578 }
579 return QFile(executable).exists();
580}
581
582auto Util::endsWithGpg() -> const QRegularExpression & {
583 static const QRegularExpression expr{R"(\.gpg$)"};
584 return expr;
585}
586
587// Stops at whitespace, quotes and brackets so a URL on its own line keeps no
588// line break. file:/// is excluded on purpose (local, not network); the test
589// relies on that.
590auto Util::protocolRegex() -> const QRegularExpression & {
591 static const QRegularExpression regex{
592 R"(((?:https?|ftp|ssh|sftp|ftps|webdav|webdavs)://[^"\s<>\)\‍]\‍[]+))"};
593 return regex;
594}
595
596auto Util::isLaunchableWebUrl(const QString &value) -> bool {
597 const QString trimmed = value.trimmed();
598 if (trimmed.isEmpty()) {
599 return false;
600 }
601 // Reject control characters first, before QUrl normalisation can hide a
602 // CR/LF/NUL injection into the OS URL handler.
603 for (const QChar &c : trimmed) {
604 if (c == QLatin1Char('\r') || c == QLatin1Char('\n') ||
605 c == QChar(QChar::Null)) {
606 return false;
607 }
608 }
609 const QUrl url(trimmed, QUrl::StrictMode);
610 if (!url.isValid()) {
611 return false;
612 }
613 const QString scheme = url.scheme().toLower();
614 if (scheme != QLatin1String("http") && scheme != QLatin1String("https")) {
615 return false;
616 }
617 if (url.host().isEmpty()) {
618 return false;
619 }
620 // Embedded userinfo (user:pass@host) would leak into browser history.
621 if (!url.userName().isEmpty() || !url.password().isEmpty()) {
622 return false;
623 }
624 return true;
625}
626
627auto Util::linkifyUrls(const QString &text, bool *linked) -> QString {
628 if (linked != nullptr) {
629 *linked = false;
630 }
631 QString html;
632 html.reserve(text.size());
633 qsizetype lastIndex = 0;
634 QRegularExpressionMatchIterator it = protocolRegex().globalMatch(text);
635 while (it.hasNext()) {
636 const QRegularExpressionMatch match = it.next();
637 const QString url = match.captured(0);
638 if (!isLaunchableWebUrl(url)) {
639 // Not a web URL (or it carries credentials): leave it in the escaped
640 // plain-text run instead of making it clickable.
641 continue;
642 }
643 const qsizetype start = match.capturedStart(0);
644 html += text.mid(lastIndex, start - lastIndex).toHtmlEscaped();
645 const QString escapedUrl = url.toHtmlEscaped();
646 html += QStringLiteral("<a href=\"%1\">%1</a>").arg(escapedUrl);
647 lastIndex = match.capturedEnd(0);
648 if (linked != nullptr) {
649 *linked = true;
650 }
651 }
652 html += text.mid(lastIndex).toHtmlEscaped();
653 return html;
654}
655
656auto Util::newLinesRegex() -> const QRegularExpression & {
657 static const QRegularExpression regex{"[\r\n]"};
658 return regex;
659}
660
661// No content heuristics: a line rejected here is erased the next time .gpg-id
662// is rewritten. A leading `-` is refused because the recipients also go
663// positionally to `gpg --list-keys`, where it would parse as an option.
664auto Util::isValidKeyId(const QString &keyId) -> bool {
665 return !keyId.isEmpty() && !keyId.startsWith('-');
666}
667
668namespace {
674template <typename Visit> void walkStore(const QString &dir, Visit visit) {
675 // Absolute, so the results are whatever the caller's cwd; not canonical,
676 // so a linked root keeps the name it was configured under.
677 QStringList pending{QDir(QDir::cleanPath(dir)).absolutePath()};
678 while (!pending.isEmpty()) {
679 const QDir current(pending.takeLast());
680 // Every entry once, links and hidden entries included, so the decision
681 // what to do with each is taken here, before any recursion. QDir::System
682 // keeps dangling links in the listing.
683 const QFileInfoList entries =
684 current.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden |
685 QDir::System | QDir::NoDotAndDotDot,
686 QDir::Name);
687 QStringList subdirs;
688 for (const QFileInfo &entry : entries) {
689 if (visit(entry)) {
690 subdirs << entry.filePath();
691 }
692 }
693 // Pushed last-to-first so the next one taken is the first by name.
694 for (auto it = subdirs.crbegin(); it != subdirs.crend(); ++it) {
695 pending << *it;
696 }
697 }
698}
699
701auto isLink(const QFileInfo &entry) -> bool {
702 return entry.isSymLink() || entry.isJunction();
703}
704
710auto isHiddenEntry(const QFileInfo &entry) -> bool {
711 return entry.isHidden() || entry.fileName().startsWith(QLatin1Char('.'));
712}
713
716auto isStoreDirectory(const QFileInfo &entry) -> bool {
717 return !isLink(entry) && entry.isDir() && !isHiddenEntry(entry);
718}
719} // namespace
720
721auto Util::regularFilesUnder(const QString &dir, const QStringList &nameFilters,
722 QStringList *skipped, bool hiddenFiles)
723 -> QStringList {
724 QStringList files;
725 walkStore(dir, [&](const QFileInfo &entry) {
726 if (isStoreDirectory(entry)) {
727 return true;
728 }
729 const bool named = QDir::match(nameFilters, entry.fileName());
730 if (isLink(entry) || (!entry.isDir() && !entry.isFile())) {
731 // A linked directory hides everything behind it; a linked file, or a
732 // FIFO, socket or device, is only of interest under a name the caller
733 // asked for.
734 if (skipped != nullptr && (entry.isDir() || named)) {
735 qCWarning(lcQtPass) << "Skipping" << entry.filePath()
736 << ": not a regular file or directory";
737 *skipped << entry.filePath();
738 }
739 return false;
740 }
741 if (entry.isFile() && named && (hiddenFiles || !isHiddenEntry(entry))) {
742 files << entry.filePath();
743 }
744 return false;
745 });
746 return files;
747}
748
749auto Util::directoriesUnder(const QString &dir) -> QStringList {
750 QStringList dirs;
751 walkStore(dir, [&](const QFileInfo &entry) {
752 if (!isStoreDirectory(entry)) {
753 return false;
754 }
755 dirs << entry.filePath();
756 return true;
757 });
758 return dirs;
759}
760
761auto Util::isLinkedFolder(const QString &path) -> bool {
762 return isLink(QFileInfo(QDir::cleanPath(path)));
763}
764
765auto Util::isUnderLink(const QString &path, const QString &storeRoot,
766 bool includeSelf) -> bool {
767 // "C:/Store" and "c:/store" are one directory on Windows; a path spelled
768 // the other way must not skip the walk (as in Pass::getGpgIdPath).
769#ifdef Q_OS_WIN
770 constexpr auto cs = Qt::CaseInsensitive;
771#else
772 constexpr auto cs = Qt::CaseSensitive;
773#endif
774 const QString root = QDir::cleanPath(storeRoot);
775 // A root of "/" or "C:/" already ends in the separator.
776 const QString prefix =
777 root.endsWith(QLatin1Char('/')) ? root : root + QLatin1Char('/');
778 const auto isRoot = [&](const QString &p) {
779 return p.compare(root, cs) == 0;
780 };
781 QString current = QDir::cleanPath(path);
782 if (!current.startsWith(prefix, cs)) {
783 // Not under the store as named: only the entry itself can be judged.
784 return includeSelf && !isRoot(current) && isLinkedFolder(current);
785 }
786 if (!includeSelf) {
787 current = QFileInfo(current).path();
788 }
789 for (; !isRoot(current) && current.startsWith(prefix, cs);
790 current = QFileInfo(current).path()) {
791 if (isLinkedFolder(current)) {
792 return true;
793 }
794 }
795 return false;
796}
797
798#ifndef Q_OS_WIN
799namespace {
800
803auto hasNoHardLinks(int error) -> bool {
804 return error == EPERM || error == ENOTSUP || error == EOPNOTSUPP ||
805 error == ENOSYS;
806}
807
810auto renameExclusive(const QByteArray &source, const QByteArray &target)
811 -> std::optional<bool> {
812#if defined(__GLIBC__) && \
813 (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 28))
814 if (::renameat2(AT_FDCWD, source.constData(), AT_FDCWD, target.constData(),
815 RENAME_NOREPLACE) == 0) {
816 return true;
817 }
818 if (errno == EINVAL || errno == ENOSYS || hasNoHardLinks(errno)) {
819 return std::nullopt;
820 }
821 return false;
822#elif defined(Q_OS_MACOS)
823 if (::renamex_np(source.constData(), target.constData(), RENAME_EXCL) == 0) {
824 return true;
825 }
826 if (errno == EINVAL || errno == ENOTSUP) {
827 return std::nullopt;
828 }
829 return false;
830#else
831 Q_UNUSED(source);
832 Q_UNUSED(target);
833 return std::nullopt;
834#endif
835}
836
846auto placeWithoutReplacing(const QByteArray &source, const QByteArray &target)
847 -> bool {
848 // linkat() without AT_SYMLINK_FOLLOW follows nothing; link() would, on
849 // macOS and the BSDs, hard-link whatever a symlink planted under the
850 // source's name points at.
851 if (::linkat(AT_FDCWD, source.constData(), AT_FDCWD, target.constData(), 0) ==
852 0) {
853 ::unlink(source.constData());
854 return true;
855 }
856 if (!hasNoHardLinks(errno)) {
857 return false;
858 }
859 if (const std::optional<bool> renamed = renameExclusive(source, target)) {
860 return *renamed;
861 }
862 struct stat st{};
863 if (::lstat(target.constData(), &st) == 0 || errno != ENOENT) {
864 errno = EEXIST;
865 return false;
866 }
867 return ::rename(source.constData(), target.constData()) == 0;
868}
869
870} // namespace
871#endif
872
873auto Util::replaceFile(const QString &from, const QString &to, bool replace)
874 -> bool {
875#ifdef Q_OS_WIN
876 const std::wstring source =
877 QDir::toNativeSeparators(QFileInfo(from).absoluteFilePath())
878 .toStdWString();
879 const std::wstring target =
880 QDir::toNativeSeparators(QFileInfo(to).absoluteFilePath()).toStdWString();
881 // WRITE_THROUGH: the new entry is on the device when this returns.
882 return MoveFileExW(source.c_str(), target.c_str(),
883 (replace ? MOVEFILE_REPLACE_EXISTING : 0) |
884 MOVEFILE_WRITE_THROUGH) != 0;
885#else
886 const QByteArray source = QFile::encodeName(from);
887 const QByteArray target = QFile::encodeName(to);
888 if (replace) {
889 if (::rename(source.constData(), target.constData()) != 0) {
890 return false;
891 }
892 } else {
893 if (!placeWithoutReplacing(source, target)) {
894 return false;
895 }
896 }
897 // Sync the directory entry so a crash does not lose the new name. Best
898 // effort: the bytes are already synced, and some network filesystems
899 // cannot sync a directory.
900 const int dir = ::open(QFile::encodeName(QFileInfo(to).path()).constData(),
901 O_RDONLY | O_DIRECTORY | O_CLOEXEC);
902 if (dir >= 0) {
903 int rc;
904 do {
905 rc = ::fsync(dir);
906 } while (rc != 0 && errno == EINTR);
907 ::close(dir);
908 }
909 return true;
910#endif
911}
912
913auto Util::openRegularFile(const QString &path, QFile &file) -> bool {
914#ifdef Q_OS_WIN
915 // FILE_FLAG_OPEN_REPARSE_POINT opens a symbolic link or junction itself
916 // rather than its target, so the handle's attributes say what the name
917 // was at the moment of the open.
918 const std::wstring native =
919 QDir::toNativeSeparators(QFileInfo(path).absoluteFilePath())
920 .toStdWString();
921 HANDLE handle = CreateFileW(
922 native.c_str(), GENERIC_READ,
923 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
924 OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
925 if (handle == INVALID_HANDLE_VALUE) {
926 return false;
927 }
928 BY_HANDLE_FILE_INFORMATION info{};
929 if (!GetFileInformationByHandle(handle, &info) ||
930 (info.dwFileAttributes &
931 (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY |
932 FILE_ATTRIBUTE_DEVICE)) != 0 ||
933 GetFileType(handle) != FILE_TYPE_DISK) {
934 CloseHandle(handle);
935 return false;
936 }
937 const int fd = _open_osfhandle(reinterpret_cast<intptr_t>(handle),
938 _O_RDONLY | _O_BINARY);
939 if (fd < 0) {
940 CloseHandle(handle);
941 return false;
942 }
943 if (!file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
944 _close(fd);
945 return false;
946 }
947 return true;
948#else
949 // O_NOFOLLOW fails with ELOOP on a symbolic link; O_NONBLOCK keeps a FIFO
950 // from blocking the open until fstat() can refuse it.
951 const int fd = ::open(QFile::encodeName(path).constData(),
952 O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
953 if (fd < 0) {
954 return false;
955 }
956 struct stat st{};
957 if (::fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
958 ::close(fd);
959 return false;
960 }
961 // The descriptor, not the name, is what QFile opens here: the object
962 // fstat() judged is the object read. CodeQL's check-then-use pattern
963 // matcher sees an open after a check and cannot tell.
964 if (!file.open(fd, QIODevice::ReadOnly, // codeql[cpp/toctou-race-condition]
965 QFileDevice::AutoCloseHandle)) {
966 ::close(fd);
967 return false;
968 }
969 return true;
970#endif
971}
972
973auto Util::readRegularFile(const QString &path, qint64 maxBytes)
974 -> std::optional<QByteArray> {
975 QFile file;
976 if (!openRegularFile(path, file)) {
977 return std::nullopt;
978 }
979 // A regular file's size is reliable, also when sparse: refuse a large one
980 // before reading anything. One byte past the limit still guards a file
981 // that grows meanwhile.
982 if (file.size() > maxBytes) {
983 return std::nullopt;
984 }
985 QByteArray bytes = file.read(maxBytes + 1);
986 if (bytes.size() > maxBytes || file.error() != QFileDevice::NoError) {
987 return std::nullopt;
988 }
989 return bytes;
990}
991
992auto Util::syncToDisk(QFileDevice &file) -> bool {
993 if (!file.flush()) {
994 return false;
995 }
996#ifdef Q_OS_WIN
997 const HANDLE handle = reinterpret_cast<HANDLE>(_get_osfhandle(file.handle()));
998 return handle != INVALID_HANDLE_VALUE && FlushFileBuffers(handle) != 0;
999#else
1000 return ::fsync(file.handle()) == 0;
1001#endif
1002}
1003
1004namespace {
1005
1010struct FileIdentity {
1011 quint64 volume = 0;
1012 quint64 index = 0;
1013 bool known = false;
1014};
1015
1016auto operator==(const FileIdentity &a, const FileIdentity &b) -> bool {
1017 return a.known && b.known && a.volume == b.volume && a.index == b.index;
1018}
1019
1021auto identityOf(const QFileDevice &file) -> FileIdentity {
1022#ifdef Q_OS_WIN
1023 const HANDLE handle = reinterpret_cast<HANDLE>(_get_osfhandle(file.handle()));
1024 BY_HANDLE_FILE_INFORMATION info;
1025 if (handle == INVALID_HANDLE_VALUE ||
1026 GetFileInformationByHandle(handle, &info) == 0) {
1027 return {};
1028 }
1029 // On FAT/exFAT the file ID is the directory entry's offset, which a rename
1030 // can move. They have no hard links either (the swap this catches needs
1031 // one), so the identity is left unknown there.
1032 DWORD flags = 0;
1033 if (GetVolumeInformationByHandleW(handle, nullptr, 0, nullptr, nullptr,
1034 &flags, nullptr, 0) == 0 ||
1035 (flags & FILE_SUPPORTS_HARD_LINKS) == 0) {
1036 return {};
1037 }
1038 return {info.dwVolumeSerialNumber,
1039 (static_cast<quint64>(info.nFileIndexHigh) << 32) |
1040 info.nFileIndexLow,
1041 true};
1042#else
1043 struct stat st{};
1044 if (::fstat(file.handle(), &st) != 0) {
1045 return {};
1046 }
1047 return {static_cast<quint64>(st.st_dev), static_cast<quint64>(st.st_ino),
1048 true};
1049#endif
1050}
1051
1053auto fail(QString *error, const QString &text) -> bool {
1054 if (error != nullptr) {
1055 *error = text;
1056 }
1057 return false;
1058}
1059
1061struct Staged {
1062 QString path;
1063 FileIdentity identity;
1064};
1065
1072auto stageNextTo(const QString &path, const Util::Filler &fill, QString *why)
1073 -> std::optional<Staged> {
1074 Staged staged;
1075 {
1076 QTemporaryFile file(QFileInfo(path).path() +
1077 QStringLiteral("/.qtpass-XXXXXX.tmp"));
1078 file.setAutoRemove(false);
1079 if (!file.open()) {
1080 *why = QCoreApplication::translate(
1081 "Util", "Cannot create a temporary file next to %1: %2")
1082 .arg(path, file.errorString());
1083 return std::nullopt;
1084 }
1085 staged.path = file.fileName();
1086 // Owner-only: a .gpg-id names the keys a store is encrypted to, an
1087 // entry is an entry. QTemporaryFile creates 0600 already; say so for
1088 // platforms where it may not.
1089 file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
1090 *why = fill(file);
1091 if (why->isEmpty() && !Util::syncToDisk(file)) {
1092 *why = QCoreApplication::translate("Util", "Cannot write %1: %2")
1093 .arg(path, file.errorString());
1094 }
1095 staged.identity = identityOf(file);
1096 }
1097 if (!why->isEmpty()) {
1098 QFile::remove(staged.path);
1099 return std::nullopt;
1100 }
1101 return staged;
1102}
1103
1105auto placementError(const QString &path, bool replace) -> QString {
1106 if (replace) {
1107 return QCoreApplication::translate("Util", "Failed to replace %1.")
1108 .arg(path);
1109 }
1110 const QFileInfo taken(path);
1111 return taken.exists() || taken.isSymLink()
1112 ? QCoreApplication::translate("Util", "%1 already exists.")
1113 .arg(path)
1114 : QCoreApplication::translate("Util", "Failed to write %1.")
1115 .arg(path);
1116}
1117
1125auto isTheFileWritten(const QString &path, const FileIdentity &written)
1126 -> bool {
1127 QFile placed;
1128 if (!Util::openRegularFile(path, placed)) {
1129 return false;
1130 }
1131 const FileIdentity there = identityOf(placed);
1132 if (!written.known || !there.known) {
1133 qCWarning(lcQtPass) << "Cannot tell whether" << path
1134 << "is the file that was written here";
1135 return true;
1136 }
1137 return there == written;
1138}
1139
1140} // namespace
1141
1142auto Util::stageFileReplacing(const QString &path, bool replace,
1143 const Filler &fill, QString *error) -> bool {
1144 QString why;
1145 const std::optional<Staged> staged = stageNextTo(path, fill, &why);
1146 if (!staged) {
1147 return fail(error, why);
1148 }
1149 if (!replaceFile(staged->path, path, replace)) {
1150 QFile::remove(staged->path);
1151 return fail(error, placementError(path, replace));
1152 }
1153 // A mismatch is reported and left: removing by name could take another
1154 // writer's file.
1155 if (!isTheFileWritten(path, staged->identity)) {
1156 return fail(
1157 error,
1158 QCoreApplication::translate(
1159 "Util", "%1 was swapped for another file while it was written.")
1160 .arg(path));
1161 }
1162 return true;
1163}
1164
1165auto Util::writeFileReplacing(const QString &path, const QByteArray &bytes,
1166 bool replace, QString *error) -> bool {
1167 return stageFileReplacing(
1168 path, replace,
1169 [&path, &bytes](QFileDevice &staged) -> QString {
1170 if (staged.write(bytes) != bytes.size()) {
1171 return QCoreApplication::translate("Util", "Cannot write %1: %2")
1172 .arg(path, staged.errorString());
1173 }
1174 return {};
1175 },
1176 error);
1177}
1178
1179auto Util::copyFileReplacing(const QString &src, const QString &dst,
1180 bool replace, QString *error) -> bool {
1181 QFile in;
1182 if (!openRegularFile(src, in)) {
1183 if (error)
1184 *error = QCoreApplication::translate("Util", "Cannot read %1.").arg(src);
1185 return false;
1186 }
1187 return stageFileReplacing(
1188 dst, replace,
1189 [&src, &dst, &in](QFileDevice &staged) -> QString {
1190 char buf[64 * 1024];
1191 for (;;) {
1192 const qint64 n = in.read(buf, sizeof buf);
1193 if (n < 0) {
1194 return QCoreApplication::translate("Util", "Cannot read %1: %2")
1195 .arg(src, in.errorString());
1196 }
1197 if (n == 0) {
1198 return {};
1199 }
1200 if (staged.write(buf, n) != n) {
1201 return QCoreApplication::translate("Util", "Cannot write %1: %2")
1202 .arg(dst, staged.errorString());
1203 }
1204 }
1205 },
1206 error);
1207}
1208
1209namespace {
1210
1215auto removeEntry(const QFileInfo &entry) -> bool {
1216 const QString path = entry.filePath();
1217 if (isLink(entry)) {
1218 if (QFile::remove(path) || QDir().rmdir(path)) {
1219 return true;
1220 }
1221 qCWarning(lcQtPass) << "Could not remove link" << path;
1222 return false;
1223 }
1224 if (QFile::remove(path)) {
1225 return true;
1226 }
1227 const QFile::Permissions perms = QFile::permissions(path);
1228 if (!perms.testFlag(QFile::WriteUser) &&
1229 QFile::setPermissions(path, perms | QFile::WriteUser) &&
1230 QFile::remove(path)) {
1231 return true;
1232 }
1233 qCWarning(lcQtPass) << "Could not remove" << path;
1234 return false;
1235}
1236
1237} // namespace
1238
1239auto Util::removeTree(const QString &dir) -> bool {
1240 // A trailing separator makes lstat follow a link ("link/" is the target
1241 // directory); the link itself is what this is about.
1242 const QString path = QDir::cleanPath(dir);
1243 const QFileInfo top(path);
1244 if (isLink(top)) {
1245 // rm -rf on a link removes the link.
1246 return QFile::remove(path) || QDir().rmdir(path);
1247 }
1248 if (!top.isDir()) {
1249 return false;
1250 }
1251 bool ok = true;
1252 const QFileInfoList entries =
1253 QDir(path).entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden |
1254 QDir::System | QDir::NoDotAndDotDot,
1255 QDir::Name);
1256 for (const QFileInfo &entry : entries) {
1257 const bool removed = entry.isDir() && !isLink(entry)
1258 ? removeTree(entry.filePath())
1259 : removeEntry(entry);
1260 ok = removed && ok;
1261 }
1262 return ok && QDir().rmdir(path);
1263}
AppSettings, the value object holding every flat QtPass setting.
static auto childUmask() -> int
The umask setChildUmask() set.
Definition executor.cpp:67
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
Definition executor.cpp:422
static void setChildUmask(int mask)
The umask every child QtPass starts gets, as pass sets its own.
Definition executor.cpp:65
static auto parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
Definition executor.cpp:114
static auto regularFilesUnder(const QString &dir, const QStringList &nameFilters, QStringList *skipped=nullptr, bool hiddenFiles=false) -> QStringList
Regular files under dir whose name matches one of nameFilters, found by walking only real directories...
Definition util.cpp:721
static auto passUmask(const QString &value) -> int
The umask pass would use for value of PASSWORD_STORE_UMASK.
Definition util.cpp:122
static auto protocolRegex() -> const QRegularExpression &
Returns a regex to match URL protocols.
Definition util.cpp:590
static auto repositoryPassClimbsTo(const QString &store) -> QString
The repository above store that pass itself would commit to, if any.
Definition util.cpp:146
static auto removeTree(const QString &dir) -> bool
Remove dir and everything in it, the way rm -rf does: a symbolic link or NTFS junction,...
Definition util.cpp:1239
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
Definition util.cpp:582
static auto isUnderLink(const QString &path, const QString &storeRoot, bool includeSelf=true) -> bool
Whether the way from storeRoot down to path passes through a symbolic link or NTFS junction: path its...
Definition util.cpp:765
static void setGitOwnerForTests(std::optional< uint > user)
For tests: the user gitStoreVariables() compares owners with when no asUser is given,...
Definition util.cpp:284
static auto findPasswordStore() -> QString
Locate the password store directory.
Definition util.cpp:73
static auto configIsValid(const AppSettings &s) -> bool
Verify that the required configuration is complete.
Definition util.cpp:556
static auto writeFileReplacing(const QString &path, const QByteArray &bytes, bool replace, QString *error=nullptr) -> bool
Write bytes as the file path through stageFileReplacing().
Definition util.cpp:1165
static QProcessEnvironment _env
Definition util.h:479
static auto isLaunchableWebUrl(const QString &value) -> bool
Check whether a value is a safe, launchable web URL.
Definition util.cpp:596
static auto readRegularFile(const QString &path, qint64 maxBytes) -> std::optional< QByteArray >
Read path whole, as the regular file it is (openRegularFile()), when it holds at most maxBytes.
Definition util.cpp:973
static auto gitStoreVariables(const QString &store, const QString &git={}, std::optional< uint > asUser=std::nullopt) -> QList< std::pair< QString, QString > >
The variables that keep git in store's own repository, never one above it: GIT_CEILING_DIRECTORIES (g...
Definition util.cpp:376
static auto syncToDisk(QFileDevice &file) -> bool
Flush file's bytes to the device (fsync, FlushFileBuffers) before it is renamed into place,...
Definition util.cpp:992
static auto mkpathForStore(const QString &path) -> bool
Create path and any missing parents as pass would.
Definition util.cpp:438
static auto expandTilde(const QString &path) -> QString
Expand a leading current-user tilde in a path.
Definition util.cpp:88
static auto isLinkedFolder(const QString &path) -> bool
Whether path names a symbolic link or NTFS junction, with or without a trailing separator (which woul...
Definition util.cpp:761
static auto replaceFile(const QString &from, const QString &to, bool replace) -> bool
Give the file at from the name to, atomically and without following anything: the operating system's ...
Definition util.cpp:873
static void applyPassProcessDefaults()
Set up the process the way pass sets up its own, before anything runs git or gpg or writes into the s...
Definition util.cpp:106
static auto isValidKeyId(const QString &keyId) -> bool
Check whether a .gpg-id token may be passed to gpg as a key selector. Accepts any non-empty token tha...
Definition util.cpp:664
static auto copyFileReplacing(const QString &src, const QString &dst, bool replace, QString *error=nullptr) -> bool
Copy the regular file src to the name dst through stageFileReplacing(): src is read through a handle ...
Definition util.cpp:1179
std::function< QString(QFileDevice &)> Filler
Fills the temporary of stageFileReplacing() through its open handle; returns why it could not (transl...
Definition util.h:405
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
Definition util.cpp:656
static auto openRegularFile(const QString &path, QFile &file) -> bool
Open path for reading as the regular file it is, not as what a link under that name points to: the ob...
Definition util.cpp:913
static auto normalizeFolderPath(const QString &path) -> QString
Ensure a folder path always ends with '/'.
Definition util.cpp:98
static auto linkifyUrls(const QString &text, bool *linked=nullptr) -> QString
Turn plain text into HTML with clickable links for safe web URLs.
Definition util.cpp:627
static auto stageFileReplacing(const QString &path, bool replace, const Filler &fill, QString *error=nullptr) -> bool
The staged write every file QtPass puts into a store goes through: a temporary created next to path (...
Definition util.cpp:1142
static void initialiseEnvironment()
Fill _env once: the process environment with the usual GPG install directories appended to PATH (MacG...
Definition util.cpp:46
static auto directoriesUnder(const QString &dir) -> QStringList
Every real, visible directory under dir, walked by the same rules as regularFilesUnder(): links,...
Definition util.cpp:749
static bool _envInitialised
Whether initialiseEnvironment() has run.
Definition util.h:481
static auto findBinaryInPath(const QString &binary) -> QString
Locate an executable by searching the process PATH and (on Windows) falling back to WSL.
Definition util.cpp:474
static auto gitCeiling(const QString &store) -> QString
GIT_CEILING_DIRECTORIES for a store, as pass sets it.
Definition util.cpp:135
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
bool operator==(const NamedValue &a, const NamedValue &b)
Two fields are equal when name and value match; originalLine is formatting, not content,...
Definition filecontent.h:49
The "qtpass" logging category.
Plain value-object holding all flat QtPass configuration.
Definition appsettings.h:36
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...