QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
templateio.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
8#include "templateio.h"
9#include "util.h"
10
11#include <QDebug>
12#include <QDir>
13#include <QFile>
14#include <QRegularExpression>
15#include <QSaveFile>
16#include <QStringConverter>
17#include <QTextStream>
18#include <algorithm>
19#include <optional>
20
21namespace {
22
24constexpr qint64 kMaxTemplateFileBytes = 64 * 1024;
25
35auto readTemplateFile(const QString &path) -> std::optional<QString> {
36 QFile file;
37 if (!Util::openRegularFile(path, file)) {
38 return std::nullopt;
39 }
40 const QByteArray bytes = file.read(kMaxTemplateFileBytes + 1);
41 if (bytes.size() > kMaxTemplateFileBytes) {
42 qWarning() << "Ignoring" << path << "; a template file is at most"
43 << kMaxTemplateFileBytes << "bytes";
44 return std::nullopt;
45 }
46 // QTextStream skipped a UTF-8 byte order mark (older Notepad writes one);
47 // fromUtf8() keeps it as U+FEFF, which trimmed() leaves in place.
48 const QByteArray bom("\xEF\xBB\xBF");
49 return QString::fromUtf8(bytes.startsWith(bom) ? bytes.mid(bom.size())
50 : bytes);
51}
52
55auto sectionHeader(const QString &line) -> std::optional<QString> {
56 if (!line.startsWith('[') || !line.endsWith(']')) {
57 return std::nullopt;
58 }
59 return line.mid(1, line.length() - 2).trimmed();
60}
61
63auto defaultTemplateIn(const QDir &dir) -> QString {
64 const std::optional<QString> contents =
65 readTemplateFile(dir.filePath(".default_template"));
66 if (!contents) {
67 return {};
68 }
69 const QString name = contents->section(QLatin1Char('\n'), 0, 0).trimmed();
70 return name.startsWith('#') ? QString() : name;
71}
72
73} // namespace
74
75auto TemplateIO::readTemplates(const QString &storePath)
76 -> QHash<QString, QStringList> {
77 QHash<QString, QStringList> result;
78 std::optional<QString> contents =
79 readTemplateFile(QDir(storePath).filePath(".templates"));
80 if (!contents) {
81 return result;
82 }
83 QTextStream in(&*contents, QIODevice::ReadOnly);
84 // No section before the first header, nor for an empty `[]`: fields there
85 // belong to nothing and are dropped.
86 std::optional<QString> section;
87 QStringList fields;
88 const auto flush = [&] {
89 if (section) {
90 result.insert(*section, fields);
91 }
92 fields.clear();
93 };
94 while (!in.atEnd()) {
95 const QString line = in.readLine().trimmed();
96 if (const std::optional<QString> header = sectionHeader(line)) {
97 flush();
98 section = header->isEmpty() ? std::nullopt : header;
99 if (!section) {
100 qWarning()
101 << "Empty template section in .templates file, ignoring fields";
102 }
103 } else if (section && !line.isEmpty() && !line.startsWith('#')) {
104 fields.append(line);
105 }
106 }
107 flush();
108 return result;
109}
110
111auto TemplateIO::secretFieldsIn(const QString &templateText) -> QStringList {
112 static const QRegularExpression marker(
113 QStringLiteral("^#\\s*secret\\s*:(.*)$"),
114 QRegularExpression::CaseInsensitiveOption);
115 QStringList names;
116 const QStringList lines = templateText.split(QLatin1Char('\n'));
117 for (const QString &line : lines) {
118 const QRegularExpressionMatch match = marker.match(line.trimmed());
119 if (!match.hasMatch()) {
120 continue;
121 }
122 const QStringList listed = match.captured(1).split(QLatin1Char(','));
123 for (const QString &name : listed) {
124 const QString trimmed = name.trimmed();
125 if (!trimmed.isEmpty()) {
126 names << trimmed;
127 }
128 }
129 }
130 return names;
131}
132
133auto TemplateIO::readSecretFields(const QString &storePath) -> QStringList {
134 const std::optional<QString> contents =
135 readTemplateFile(QDir(storePath).filePath(".templates"));
136 return contents ? secretFieldsIn(*contents) : QStringList();
137}
138
139auto TemplateIO::fieldsIn(const QString &templateText) -> QStringList {
140 QStringList fields = templateText.split(QLatin1Char('\n'));
141 fields.removeIf([](const QString &line) {
142 return line.trimmed().startsWith(QLatin1Char('#'));
143 });
144 return fields;
145}
146
147auto TemplateIO::writeTemplates(const QString &storePath,
148 const QHash<QString, QStringList> &templates)
149 -> bool {
150 QSaveFile saveFile(QDir(storePath).filePath(".templates"));
151 if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
152 return false;
153 }
154 QTextStream out(&saveFile);
155 out.setEncoding(QStringConverter::Utf8);
156 out << "# QtPass templates configuration\n";
157 out << "# Format: INI-style with [template_name] sections,\n";
158 out << "# followed by field names (one per line)\n\n";
159
160 QStringList sortedKeys = templates.keys();
161 std::sort(sortedKeys.begin(), sortedKeys.end());
162 for (const QString &key : sortedKeys) {
163 out << "[" << key << "]\n";
164 for (const QString &field : templates.value(key)) {
165 out << field << "\n";
166 }
167 out << "\n";
168 }
169 out.flush();
170 if (out.status() != QTextStream::Ok) {
171 return false;
172 }
173 return saveFile.commit();
174}
175
176auto TemplateIO::getFolderTemplate(const QString &folderPath,
177 const QString &storePath) -> QString {
178 const QString store = QDir::cleanPath(QDir(storePath).absolutePath());
179 // Only the store root and its descendants are probed for
180 // .default_template; a path outside the store opens nothing.
181 const auto inStore = [&store](const QString &path) {
182 return path == store ||
183 (path.startsWith(store) && path.length() > store.length() &&
184 path.at(store.length()) == QChar('/'));
185 };
186 QDir dir(folderPath);
187 while (inStore(QDir::cleanPath(dir.absolutePath()))) {
188 const QString name = defaultTemplateIn(dir);
189 if (!name.isEmpty()) {
190 return name;
191 }
192 if (QDir::cleanPath(dir.absolutePath()) == store || !dir.cdUp()) {
193 break;
194 }
195 }
196 return {};
197}
static auto writeTemplates(const QString &storePath, const QHash< QString, QStringList > &templates) -> bool
Write templates to .templates file in password store.
static auto readTemplates(const QString &storePath) -> QHash< QString, QStringList >
Read templates from .templates file in password store.
static auto fieldsIn(const QString &templateText) -> QStringList
The field names of the template in the settings: one per line, with comment lines (starting with #,...
static auto getFolderTemplate(const QString &folderPath, const QString &storePath) -> QString
Get default template for a folder. Looks in folder, then parent folders up to root.
static auto readSecretFields(const QString &storePath) -> QStringList
The field names the store's .templates file marks as secret, in any of its sections (see secretFields...
static auto secretFieldsIn(const QString &templateText) -> QStringList
The field names a template marks as secret.
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
TemplateIO: the store's .templates and .default_template files and the # secret: marker in template t...
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...