Line data Source code
1 : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements TemplateIO; template files are read through
6 : * readTemplateFile(), which refuses anything but a small regular file.
7 : */
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 :
21 : namespace {
22 :
23 : /// The template files are a few lines each; anything larger is not one.
24 : constexpr qint64 kMaxTemplateFileBytes = 64 * 1024;
25 :
26 : /**
27 : * Reads a template file of the store. Store contents are written by whoever
28 : * shares the store, so it has to be the regular file it claims to be (not a
29 : * link to a device or a FIFO, which would never end) and of a template
30 : * file's size, as read from the open handle: a file that grows after the
31 : * open is cut off at the limit there, not read on.
32 : * @return The contents, or nothing when the file is absent, not a regular
33 : * file or too large.
34 : */
35 29 : auto readTemplateFile(const QString &path) -> std::optional<QString> {
36 29 : QFile file;
37 29 : if (!Util::openRegularFile(path, file)) {
38 12 : return std::nullopt;
39 : }
40 17 : const QByteArray bytes = file.read(kMaxTemplateFileBytes + 1);
41 17 : if (bytes.size() > kMaxTemplateFileBytes) {
42 2 : qWarning() << "Ignoring" << path << "; a template file is at most"
43 1 : << kMaxTemplateFileBytes << "bytes";
44 1 : 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 16 : const QByteArray bom("\xEF\xBB\xBF");
49 32 : return QString::fromUtf8(bytes.startsWith(bom) ? bytes.mid(bom.size())
50 : : bytes);
51 29 : }
52 :
53 : /// The name of a `[section]` header line; nothing for any other line, and
54 : /// an empty name for `[]`.
55 48 : auto sectionHeader(const QString &line) -> std::optional<QString> {
56 48 : if (!line.startsWith('[') || !line.endsWith(']')) {
57 36 : return std::nullopt;
58 : }
59 24 : return line.mid(1, line.length() - 2).trimmed();
60 : }
61 :
62 : /// The first line of @p dir's .default_template, when it names a template.
63 15 : auto defaultTemplateIn(const QDir &dir) -> QString {
64 : const std::optional<QString> contents =
65 30 : readTemplateFile(dir.filePath(".default_template"));
66 15 : if (!contents) {
67 10 : return {};
68 : }
69 5 : const QString name = contents->section(QLatin1Char('\n'), 0, 0).trimmed();
70 9 : return name.startsWith('#') ? QString() : name;
71 : }
72 :
73 : } // namespace
74 :
75 10 : auto TemplateIO::readTemplates(const QString &storePath)
76 : -> QHash<QString, QStringList> {
77 10 : QHash<QString, QStringList> result;
78 : std::optional<QString> contents =
79 20 : readTemplateFile(QDir(storePath).filePath(".templates"));
80 10 : if (!contents) {
81 : return result;
82 : }
83 8 : 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 8 : std::optional<QString> section;
87 8 : QStringList fields;
88 20 : const auto flush = [&] {
89 20 : if (section) {
90 11 : result.insert(*section, fields);
91 : }
92 20 : fields.clear();
93 28 : };
94 56 : while (!in.atEnd()) {
95 48 : const QString line = in.readLine().trimmed();
96 48 : if (const std::optional<QString> header = sectionHeader(line)) {
97 12 : flush();
98 24 : section = header->isEmpty() ? std::nullopt : header;
99 12 : if (!section) {
100 2 : qWarning()
101 1 : << "Empty template section in .templates file, ignoring fields";
102 : }
103 36 : } else if (section && !line.isEmpty() && !line.startsWith('#')) {
104 : fields.append(line);
105 : }
106 : }
107 8 : flush();
108 : return result;
109 8 : }
110 :
111 7 : auto TemplateIO::secretFieldsIn(const QString &templateText) -> QStringList {
112 : static const QRegularExpression marker(
113 2 : QStringLiteral("^#\\s*secret\\s*:(.*)$"),
114 9 : QRegularExpression::CaseInsensitiveOption);
115 7 : QStringList names;
116 7 : const QStringList lines = templateText.split(QLatin1Char('\n'));
117 40 : for (const QString &line : lines) {
118 33 : const QRegularExpressionMatch match = marker.match(line.trimmed());
119 33 : if (!match.hasMatch()) {
120 : continue;
121 : }
122 10 : const QStringList listed = match.captured(1).split(QLatin1Char(','));
123 24 : for (const QString &name : listed) {
124 : const QString trimmed = name.trimmed();
125 14 : if (!trimmed.isEmpty()) {
126 : names << trimmed;
127 : }
128 : }
129 33 : }
130 7 : return names;
131 : }
132 :
133 4 : auto TemplateIO::readSecretFields(const QString &storePath) -> QStringList {
134 : const std::optional<QString> contents =
135 8 : readTemplateFile(QDir(storePath).filePath(".templates"));
136 8 : return contents ? secretFieldsIn(*contents) : QStringList();
137 : }
138 :
139 97 : auto TemplateIO::fieldsIn(const QString &templateText) -> QStringList {
140 97 : QStringList fields = templateText.split(QLatin1Char('\n'));
141 123 : fields.removeIf([](const QString &line) {
142 123 : return line.trimmed().startsWith(QLatin1Char('#'));
143 : });
144 97 : return fields;
145 : }
146 :
147 3 : auto TemplateIO::writeTemplates(const QString &storePath,
148 : const QHash<QString, QStringList> &templates)
149 : -> bool {
150 6 : QSaveFile saveFile(QDir(storePath).filePath(".templates"));
151 3 : if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
152 : return false;
153 : }
154 3 : QTextStream out(&saveFile);
155 3 : out.setEncoding(QStringConverter::Utf8);
156 3 : out << "# QtPass templates configuration\n";
157 3 : out << "# Format: INI-style with [template_name] sections,\n";
158 3 : out << "# followed by field names (one per line)\n\n";
159 :
160 3 : QStringList sortedKeys = templates.keys();
161 6 : std::sort(sortedKeys.begin(), sortedKeys.end());
162 7 : for (const QString &key : sortedKeys) {
163 4 : out << "[" << key << "]\n";
164 14 : for (const QString &field : templates.value(key)) {
165 6 : out << field << "\n";
166 : }
167 4 : out << "\n";
168 : }
169 3 : out.flush();
170 3 : if (out.status() != QTextStream::Ok) {
171 : return false;
172 : }
173 3 : return saveFile.commit();
174 3 : }
175 :
176 9 : auto TemplateIO::getFolderTemplate(const QString &folderPath,
177 : const QString &storePath) -> QString {
178 18 : 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 15 : const auto inStore = [&store](const QString &path) {
182 25 : return path == store ||
183 20 : (path.startsWith(store) && path.length() > store.length() &&
184 15 : path.at(store.length()) == QChar('/'));
185 9 : };
186 9 : QDir dir(folderPath);
187 30 : while (inStore(QDir::cleanPath(dir.absolutePath()))) {
188 15 : const QString name = defaultTemplateIn(dir);
189 15 : if (!name.isEmpty()) {
190 : return name;
191 : }
192 22 : if (QDir::cleanPath(dir.absolutePath()) == store || !dir.cdUp()) {
193 : break;
194 : }
195 : }
196 5 : return {};
197 9 : }
|