QtPass Developer documentation
QtPass test coverage
Current view: top level - src/templateio.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 100.0 % 99 99
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 12 12

            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 : }
        

Generated by: LCOV version 2.0-1