12#include <QCoreApplication>
19#include <QRegularExpression>
20#include <QRegularExpressionMatchIterator>
21#include <QStandardPaths>
22#include <QTemporaryFile>
48 _env = QProcessEnvironment::systemEnvironment();
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);
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);
68 qCDebug(lcQtPass) <<
_env.value(
"PATH");
76 if (
_env.contains(
"PASSWORD_STORE_DIR")) {
80 path = QDir(QDir::homePath()).filePath(
"password-store");
82 path = QDir(QDir::homePath()).filePath(
".password-store");
89 if (path == QLatin1String(
"~")) {
90 return QDir::homePath();
92 if (path.startsWith(QLatin1String(
"~/"))) {
93 return QDir::homePath() + path.mid(1);
99 QString normalizedPath = path;
100 if (!normalizedPath.endsWith(
'/')) {
101 normalizedPath +=
'/';
103 return normalizedPath;
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",
115 "GIT_GLOB_PATHSPECS",
"GIT_NOGLOB_PATHSPECS",
"GIT_ICASE_PATHSPECS"}) {
119 passUmask(qEnvironmentVariable(
"PASSWORD_STORE_UMASK")));
127 static const QRegularExpression octal(
128 QRegularExpression::anchoredPattern(QStringLiteral(
"[0-7]{1,4}")));
129 if (!octal.match(value).hasMatch()) {
132 return value.toInt(
nullptr, 8);
136 if (store.isEmpty()) {
139 QString path = QFileInfo(store).canonicalFilePath();
140 if (path.isEmpty()) {
141 path = QDir::cleanPath(QDir(store).absolutePath());
143 return QFileInfo(path).absolutePath();
151 if (!store.contains(QLatin1Char(
':'))) {
163 store.startsWith(u
'/') ? store : QDir::currentPath() + u
'/' + store;
164 while (path.size() > 1 && path.endsWith(u
'/')) {
168 while (!QFileInfo::exists(path)) {
169 const qsizetype slash = path.lastIndexOf(u
'/');
173 const QString name = path.mid(slash + 1);
174 rest = rest.isEmpty() ? name : name + u
'/' + rest;
175 path = slash == 0 ? QStringLiteral(
"/") : path.left(slash);
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);
188 bool afterEmpty =
false;
189 for (
const QString &piece :
190 (store + QStringLiteral(
"/..")).split(QLatin1Char(
':'))) {
191 if (piece.isEmpty()) {
195 if (!piece.startsWith(u
'/')) {
198 const QString entry =
199 afterEmpty ? piece : QFileInfo(piece).canonicalFilePath();
202 const QString under = entry == u
'/' ? entry : entry + u
'/';
203 if (!entry.isEmpty() && start.startsWith(under) &&
204 entry.size() > ceiling.size()) {
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()) {
217 QFile head(dir.filePath(QStringLiteral(
"HEAD")));
218 if (!head.open(QIODevice::ReadOnly)) {
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();
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);
234 return isGitDirectory(dotGit) ? Found::Repository : Found::Nothing;
236 if (!info.exists()) {
237 return Found::Nothing;
240 if (!file.open(QIODevice::ReadOnly)) {
241 return Found::Refused;
243 const QString line = QString::fromUtf8(file.readLine(4096)).trimmed();
244 if (!line.startsWith(QLatin1String(
"gitdir: "))) {
245 return Found::Refused;
247 const QString target = line.mid(8);
248 return isGitDirectory(target.startsWith(u
'/') ? target
249 : QDir(dir).filePath(target))
256 for (QString dir = start;;) {
257 switch (repositoryAt(dir)) {
258 case Found::Repository:
259 return dir == start ? QString() : dir;
267 if (isGitDirectory(dir)) {
270 const QString up = QFileInfo(dir).absolutePath();
271 if (up == dir || up == ceiling) {
281std::optional<uint> s_gitOwnerForTests;
285 s_gitOwnerForTests = user;
306auto isSafeDirectory(
const QString &root,
const QString &git) ->
bool {
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"));
320 env.remove(QStringLiteral(
"GIT_CONFIG"));
322 process.setProcessEnvironment(env);
323 process.setWorkingDirectory(QStringLiteral(
"/"));
326 constexpr int kQueryTimeoutMs = 5000;
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) {
337 const QStringList lines = out.split(QLatin1Char(
'\n'));
338 for (
const QString &line : lines) {
339 const qsizetype tab = line.indexOf(QLatin1Char(
'\t'));
343 const QString scope = line.left(tab);
344 if (scope != QLatin1String(
"system") && scope != QLatin1String(
"global") &&
345 scope != QLatin1String(
"command")) {
348 const QString value = line.mid(tab + 1);
349 if (value.isEmpty()) {
351 }
else if (value == QLatin1String(
"*")) {
353 }
else if (!QDir::isAbsolutePath(value)) {
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()) {
362 if (!prefix.endsWith(QLatin1Char(
'/'))) {
363 prefix += QLatin1Char(
'/');
365 safe = safe || root.startsWith(prefix);
367 const QString named = QFileInfo(value).canonicalFilePath();
368 safe = safe || (!named.isEmpty() && named == root);
377 std::optional<uint> asUser)
378 -> QList<std::pair<QString, QString>> {
380 if (ceiling.isEmpty()) {
383 QList<std::pair<QString, QString>> vars{
384 {QStringLiteral(
"GIT_CEILING_DIRECTORIES"), ceiling}};
386 const QChar separator = QLatin1Char(
';');
388 const QChar separator = QLatin1Char(
':');
392 if (ceiling.contains(separator)) {
393 QString root = QFileInfo(store).canonicalFilePath();
394 if (root.isEmpty()) {
395 root = QDir::cleanPath(QDir(store).absolutePath());
397 QString gitDir = QDir(root).filePath(QStringLiteral(
".git"));
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;
409 bool owned = ownedOrMissing(root) && ownedOrMissing(gitDir);
410 if (owned && QFileInfo(gitDir).isFile()) {
411 QFile gitFile(gitDir);
412 if (gitFile.open(QIODevice::ReadOnly)) {
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));
422 if (!owned && !isSafeDirectory(root, git)) {
423 gitDir = QStringLiteral(
"/dev/null/.git");
432 vars.append({QStringLiteral(
"GIT_DIR"), gitDir});
433 vars.append({QStringLiteral(
"GIT_WORK_TREE"), root});
439 const QString clean = QDir::cleanPath(QDir(path).absolutePath());
440 if (QFileInfo(clean).isDir()) {
443 const QString parent = QFileInfo(clean).absolutePath();
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];
464 const bool made = QDir().mkdir(clean, permissions) &&
465 QFile::setPermissions(clean, permissions);
469 const bool made = QDir().mkdir(clean);
471 return made || QFileInfo(clean).isDir();
475 if (binary.isEmpty()) {
481 const QStringList dirs =
482 _env.value(QStringLiteral(
"PATH"))
483 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
485 if (QDir::fromNativeSeparators(binary).contains(u
'/')) {
489 if (QDir::isAbsolutePath(binary)) {
490 ret = QStandardPaths::findExecutable(binary);
491 }
else if (!dirs.isEmpty()) {
492 ret = QStandardPaths::findExecutable(binary, dirs);
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();
511 QString wslCommand = QStringLiteral(
"wsl ") + binary;
513 <<
"Util::findBinaryInPath(): falling back to WSL for binary"
516 QString cachedResult;
519 !out.isEmpty() && err.isEmpty()) {
521 <<
"Util::findBinaryInPath(): using WSL binary" << wslCommand;
522 cachedResult = wslCommand;
524 wslBinaryCache.insert(binary, cachedResult);
538 const QStringList &searchPaths) -> QString {
539 if (binary.isEmpty() || QDir::fromNativeSeparators(binary).contains(u
'/')) {
543 dirs.reserve(searchPaths.size());
544 for (
const QString &dir : searchPaths) {
545 if (!dir.isEmpty()) {
549 if (dirs.isEmpty()) {
553 return QStandardPaths::findExecutable(binary, dirs);
557 const QString configFilePath = QDir(s.passStore).filePath(
".gpg-id");
558 if (!QFile(configFilePath).exists()) {
562 const QString executable = s.usePass ? s.passExecutable : s.gpgExecutable;
567 static const bool wslAvailable = [&wsl]() {
571 {QStringLiteral(
"--version")}, &out,
573 !out.isEmpty() && err.isEmpty();
579 return QFile(executable).exists();
583 static const QRegularExpression expr{R
"(\.gpg$)"};
591 static const QRegularExpression regex{
592 R
"(((?:https?|ftp|ssh|sftp|ftps|webdav|webdavs)://[^"\s<>\)\]\[]+))"};
597 const QString trimmed = value.trimmed();
598 if (trimmed.isEmpty()) {
603 for (
const QChar &c : trimmed) {
604 if (c == QLatin1Char(
'\r') || c == QLatin1Char(
'\n') ||
605 c == QChar(QChar::Null)) {
609 const QUrl url(trimmed, QUrl::StrictMode);
610 if (!url.isValid()) {
613 const QString scheme = url.scheme().toLower();
614 if (scheme != QLatin1String(
"http") && scheme != QLatin1String(
"https")) {
617 if (url.host().isEmpty()) {
621 if (!url.userName().isEmpty() || !url.password().isEmpty()) {
628 if (linked !=
nullptr) {
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);
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) {
652 html += text.mid(lastIndex).toHtmlEscaped();
657 static const QRegularExpression regex{
"[\r\n]"};
665 return !keyId.isEmpty() && !keyId.startsWith(
'-');
674template <
typename Visit>
void walkStore(
const QString &dir, Visit visit) {
677 QStringList pending{QDir(QDir::cleanPath(dir)).absolutePath()};
678 while (!pending.isEmpty()) {
679 const QDir current(pending.takeLast());
683 const QFileInfoList entries =
684 current.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden |
685 QDir::System | QDir::NoDotAndDotDot,
688 for (
const QFileInfo &entry : entries) {
690 subdirs << entry.filePath();
694 for (
auto it = subdirs.crbegin(); it != subdirs.crend(); ++it) {
701auto isLink(
const QFileInfo &entry) ->
bool {
702 return entry.isSymLink() || entry.isJunction();
710auto isHiddenEntry(
const QFileInfo &entry) ->
bool {
711 return entry.isHidden() || entry.fileName().startsWith(QLatin1Char(
'.'));
716auto isStoreDirectory(
const QFileInfo &entry) ->
bool {
717 return !isLink(entry) && entry.isDir() && !isHiddenEntry(entry);
722 QStringList *skipped,
bool hiddenFiles)
725 walkStore(dir, [&](
const QFileInfo &entry) {
726 if (isStoreDirectory(entry)) {
729 const bool named = QDir::match(nameFilters, entry.fileName());
730 if (isLink(entry) || (!entry.isDir() && !entry.isFile())) {
734 if (skipped !=
nullptr && (entry.isDir() || named)) {
735 qCWarning(lcQtPass) <<
"Skipping" << entry.filePath()
736 <<
": not a regular file or directory";
737 *skipped << entry.filePath();
741 if (entry.isFile() && named && (hiddenFiles || !isHiddenEntry(entry))) {
742 files << entry.filePath();
751 walkStore(dir, [&](
const QFileInfo &entry) {
752 if (!isStoreDirectory(entry)) {
755 dirs << entry.filePath();
762 return isLink(QFileInfo(QDir::cleanPath(path)));
766 bool includeSelf) ->
bool {
770 constexpr auto cs = Qt::CaseInsensitive;
772 constexpr auto cs = Qt::CaseSensitive;
774 const QString root = QDir::cleanPath(storeRoot);
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;
781 QString current = QDir::cleanPath(path);
782 if (!current.startsWith(prefix, cs)) {
784 return includeSelf && !isRoot(current) &&
isLinkedFolder(current);
787 current = QFileInfo(current).path();
789 for (; !isRoot(current) && current.startsWith(prefix, cs);
790 current = QFileInfo(current).path()) {
803auto hasNoHardLinks(
int error) ->
bool {
804 return error == EPERM || error == ENOTSUP || error == EOPNOTSUPP ||
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) {
818 if (errno == EINVAL || errno == ENOSYS || hasNoHardLinks(errno)) {
822#elif defined(Q_OS_MACOS)
823 if (::renamex_np(source.constData(), target.constData(), RENAME_EXCL) == 0) {
826 if (errno == EINVAL || errno == ENOTSUP) {
846auto placeWithoutReplacing(
const QByteArray &source,
const QByteArray &target)
851 if (::linkat(AT_FDCWD, source.constData(), AT_FDCWD, target.constData(), 0) ==
853 ::unlink(source.constData());
856 if (!hasNoHardLinks(errno)) {
859 if (
const std::optional<bool> renamed = renameExclusive(source, target)) {
863 if (::lstat(target.constData(), &st) == 0 || errno != ENOENT) {
867 return ::rename(source.constData(), target.constData()) == 0;
876 const std::wstring source =
877 QDir::toNativeSeparators(QFileInfo(from).absoluteFilePath())
879 const std::wstring target =
880 QDir::toNativeSeparators(QFileInfo(to).absoluteFilePath()).toStdWString();
882 return MoveFileExW(source.c_str(), target.c_str(),
883 (replace ? MOVEFILE_REPLACE_EXISTING : 0) |
884 MOVEFILE_WRITE_THROUGH) != 0;
886 const QByteArray source = QFile::encodeName(from);
887 const QByteArray target = QFile::encodeName(to);
889 if (::rename(source.constData(), target.constData()) != 0) {
893 if (!placeWithoutReplacing(source, target)) {
900 const int dir = ::open(QFile::encodeName(QFileInfo(to).path()).constData(),
901 O_RDONLY | O_DIRECTORY | O_CLOEXEC);
906 }
while (rc != 0 && errno == EINTR);
918 const std::wstring native =
919 QDir::toNativeSeparators(QFileInfo(path).absoluteFilePath())
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) {
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) {
937 const int fd = _open_osfhandle(
reinterpret_cast<intptr_t
>(handle),
938 _O_RDONLY | _O_BINARY);
943 if (!file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
951 const int fd = ::open(QFile::encodeName(path).constData(),
952 O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
957 if (::fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
964 if (!file.open(fd, QIODevice::ReadOnly,
965 QFileDevice::AutoCloseHandle)) {
974 -> std::optional<QByteArray> {
982 if (file.size() > maxBytes) {
985 QByteArray bytes = file.read(maxBytes + 1);
986 if (bytes.size() > maxBytes || file.error() != QFileDevice::NoError) {
997 const HANDLE handle =
reinterpret_cast<HANDLE
>(_get_osfhandle(file.handle()));
998 return handle != INVALID_HANDLE_VALUE && FlushFileBuffers(handle) != 0;
1000 return ::fsync(file.handle()) == 0;
1010struct FileIdentity {
1016auto operator==(
const FileIdentity &a,
const FileIdentity &b) ->
bool {
1017 return a.known && b.known && a.volume == b.volume && a.index == b.index;
1021auto identityOf(
const QFileDevice &file) -> FileIdentity {
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) {
1033 if (GetVolumeInformationByHandleW(handle,
nullptr, 0,
nullptr,
nullptr,
1034 &flags,
nullptr, 0) == 0 ||
1035 (flags & FILE_SUPPORTS_HARD_LINKS) == 0) {
1038 return {info.dwVolumeSerialNumber,
1039 (
static_cast<quint64
>(info.nFileIndexHigh) << 32) |
1044 if (::fstat(file.handle(), &st) != 0) {
1047 return {
static_cast<quint64
>(st.st_dev),
static_cast<quint64
>(st.st_ino),
1053auto fail(QString *error,
const QString &text) ->
bool {
1054 if (error !=
nullptr) {
1063 FileIdentity identity;
1072auto stageNextTo(
const QString &path,
const Util::Filler &fill, QString *why)
1073 -> std::optional<Staged> {
1076 QTemporaryFile file(QFileInfo(path).path() +
1077 QStringLiteral(
"/.qtpass-XXXXXX.tmp"));
1078 file.setAutoRemove(
false);
1080 *why = QCoreApplication::translate(
1081 "Util",
"Cannot create a temporary file next to %1: %2")
1082 .arg(path, file.errorString());
1083 return std::nullopt;
1085 staged.path = file.fileName();
1089 file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
1092 *why = QCoreApplication::translate(
"Util",
"Cannot write %1: %2")
1093 .arg(path, file.errorString());
1095 staged.identity = identityOf(file);
1097 if (!why->isEmpty()) {
1098 QFile::remove(staged.path);
1099 return std::nullopt;
1105auto placementError(
const QString &path,
bool replace) -> QString {
1107 return QCoreApplication::translate(
"Util",
"Failed to replace %1.")
1110 const QFileInfo taken(path);
1111 return taken.exists() || taken.isSymLink()
1112 ? QCoreApplication::translate(
"Util",
"%1 already exists.")
1114 : QCoreApplication::translate(
"Util",
"Failed to write %1.")
1125auto isTheFileWritten(
const QString &path,
const FileIdentity &written)
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";
1137 return there == written;
1143 const Filler &fill, QString *error) ->
bool {
1145 const std::optional<Staged> staged = stageNextTo(path, fill, &why);
1147 return fail(error, why);
1150 QFile::remove(staged->path);
1151 return fail(error, placementError(path, replace));
1155 if (!isTheFileWritten(path, staged->identity)) {
1158 QCoreApplication::translate(
1159 "Util",
"%1 was swapped for another file while it was written.")
1166 bool replace, QString *error) ->
bool {
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());
1180 bool replace, QString *error) ->
bool {
1184 *error = QCoreApplication::translate(
"Util",
"Cannot read %1.").arg(src);
1189 [&src, &dst, &in](QFileDevice &staged) -> QString {
1190 char buf[64 * 1024];
1192 const qint64 n = in.read(buf,
sizeof buf);
1194 return QCoreApplication::translate(
"Util",
"Cannot read %1: %2")
1195 .arg(src, in.errorString());
1200 if (staged.write(buf, n) != n) {
1201 return QCoreApplication::translate(
"Util",
"Cannot write %1: %2")
1202 .arg(dst, staged.errorString());
1215auto removeEntry(
const QFileInfo &entry) ->
bool {
1216 const QString path = entry.filePath();
1217 if (isLink(entry)) {
1218 if (QFile::remove(path) || QDir().rmdir(path)) {
1221 qCWarning(lcQtPass) <<
"Could not remove link" << path;
1224 if (QFile::remove(path)) {
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)) {
1233 qCWarning(lcQtPass) <<
"Could not remove" << path;
1242 const QString path = QDir::cleanPath(dir);
1243 const QFileInfo top(path);
1246 return QFile::remove(path) || QDir().rmdir(path);
1252 const QFileInfoList entries =
1253 QDir(path).entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden |
1254 QDir::System | QDir::NoDotAndDotDot,
1256 for (
const QFileInfo &entry : entries) {
1257 const bool removed = entry.isDir() && !isLink(entry)
1259 : removeEntry(entry);
1262 return ok && QDir().rmdir(path);
AppSettings, the value object holding every flat QtPass setting.
static auto childUmask() -> int
The umask setChildUmask() set.
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.
static void setChildUmask(int mask)
The umask every child QtPass starts gets, as pass sets its own.
static auto parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
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...
static auto passUmask(const QString &value) -> int
The umask pass would use for value of PASSWORD_STORE_UMASK.
static auto protocolRegex() -> const QRegularExpression &
Returns a regex to match URL protocols.
static auto repositoryPassClimbsTo(const QString &store) -> QString
The repository above store that pass itself would commit to, if any.
static auto removeTree(const QString &dir) -> bool
Remove dir and everything in it, the way rm -rf does: a symbolic link or NTFS junction,...
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
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...
static void setGitOwnerForTests(std::optional< uint > user)
For tests: the user gitStoreVariables() compares owners with when no asUser is given,...
static auto findPasswordStore() -> QString
Locate the password store directory.
static auto configIsValid(const AppSettings &s) -> bool
Verify that the required configuration is complete.
static auto writeFileReplacing(const QString &path, const QByteArray &bytes, bool replace, QString *error=nullptr) -> bool
Write bytes as the file path through stageFileReplacing().
static QProcessEnvironment _env
static auto isLaunchableWebUrl(const QString &value) -> bool
Check whether a value is a safe, launchable web URL.
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.
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...
static auto syncToDisk(QFileDevice &file) -> bool
Flush file's bytes to the device (fsync, FlushFileBuffers) before it is renamed into place,...
static auto mkpathForStore(const QString &path) -> bool
Create path and any missing parents as pass would.
static auto expandTilde(const QString &path) -> QString
Expand a leading current-user tilde in a path.
static auto isLinkedFolder(const QString &path) -> bool
Whether path names a symbolic link or NTFS junction, with or without a trailing separator (which woul...
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 ...
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...
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...
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 ...
std::function< QString(QFileDevice &)> Filler
Fills the temporary of stageFileReplacing() through its open handle; returns why it could not (transl...
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
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...
static auto normalizeFolderPath(const QString &path) -> QString
Ensure a folder path always ends with '/'.
static auto linkifyUrls(const QString &text, bool *linked=nullptr) -> QString
Turn plain text into HTML with clickable links for safe web URLs.
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 (...
static void initialiseEnvironment()
Fill _env once: the process environment with the usual GPG install directories appended to PATH (MacG...
static auto directoriesUnder(const QString &dir) -> QStringList
Every real, visible directory under dir, walked by the same rules as regularFilesUnder(): links,...
static bool _envInitialised
Whether initialiseEnvironment() has run.
static auto findBinaryInPath(const QString &binary) -> QString
Locate an executable by searching the process PATH and (on Windows) falling back to WSL.
static auto gitCeiling(const QString &store) -> QString
GIT_CEILING_DIRECTORIES for a store, as pass sets it.
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,...
The "qtpass" logging category.
Plain value-object holding all flat QtPass configuration.
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...