File indexing completed on 2024-06-23 05:14:15

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     utils/archivedefinition.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2009, 2010 Klarälvdalens Datakonsult AB
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include <config-kleopatra.h>
0011 
0012 #include "archivedefinition.h"
0013 
0014 #include <utils/input.h>
0015 #include <utils/kleo_assert.h>
0016 #include <utils/output.h>
0017 #include <utils/path-helper.h>
0018 
0019 #include <gpgme++/exception.h>
0020 
0021 #include "kleopatra_debug.h"
0022 #include <KConfig>
0023 #include <KConfigGroup>
0024 #include <KLocalizedString>
0025 #include <KSharedConfig>
0026 #include <KShell>
0027 
0028 #include <QCoreApplication>
0029 #include <QDir>
0030 #include <QMutex>
0031 #include <QRegularExpression>
0032 
0033 #include <QStandardPaths>
0034 
0035 using namespace GpgME;
0036 using namespace Kleo;
0037 
0038 static QMutex installPathMutex;
0039 Q_GLOBAL_STATIC(QString, _installPath)
0040 QString ArchiveDefinition::installPath()
0041 {
0042     const QMutexLocker locker(&installPathMutex);
0043     QString *const ip = _installPath();
0044     if (ip->isEmpty()) {
0045         if (QCoreApplication::instance()) {
0046             *ip = QCoreApplication::applicationDirPath();
0047         } else {
0048             qCWarning(KLEOPATRA_LOG) << "called before QCoreApplication was constructed";
0049         }
0050     }
0051     return *ip;
0052 }
0053 void ArchiveDefinition::setInstallPath(const QString &ip)
0054 {
0055     const QMutexLocker locker(&installPathMutex);
0056     *_installPath() = ip;
0057 }
0058 
0059 // Archive Definition #N groups
0060 static const QLatin1StringView ID_ENTRY("id");
0061 static const QLatin1StringView NAME_ENTRY("Name");
0062 static const QLatin1StringView PACK_COMMAND_ENTRY("pack-command");
0063 static const QLatin1StringView PACK_COMMAND_OPENPGP_ENTRY("pack-command-openpgp");
0064 static const QLatin1StringView PACK_COMMAND_CMS_ENTRY("pack-command-cms");
0065 static const QLatin1StringView UNPACK_COMMAND_ENTRY("unpack-command");
0066 static const QLatin1StringView UNPACK_COMMAND_OPENPGP_ENTRY("unpack-command-openpgp");
0067 static const QLatin1StringView UNPACK_COMMAND_CMS_ENTRY("unpack-command-cms");
0068 static const QLatin1StringView EXTENSIONS_ENTRY("extensions");
0069 static const QLatin1StringView EXTENSIONS_OPENPGP_ENTRY("extensions-openpgp");
0070 static const QLatin1StringView EXTENSIONS_CMS_ENTRY("extensions-cms");
0071 static const QLatin1StringView FILE_PLACEHOLDER("%f");
0072 static const QLatin1StringView FILE_PLACEHOLDER_7BIT("%F");
0073 static const QLatin1StringView INSTALLPATH_PLACEHOLDER("%I");
0074 static const QLatin1StringView NULL_SEPARATED_STDIN_INDICATOR("0|");
0075 static const QLatin1Char NEWLINE_SEPARATED_STDIN_INDICATOR('|');
0076 
0077 namespace
0078 {
0079 
0080 class ArchiveDefinitionError : public Kleo::Exception
0081 {
0082     const QString m_id;
0083 
0084 public:
0085     ArchiveDefinitionError(const QString &id, const QString &message)
0086         : Kleo::Exception(GPG_ERR_INV_PARAMETER, i18n("Error in archive definition %1: %2", id, message), MessageOnly)
0087         , m_id(id)
0088     {
0089     }
0090     ~ArchiveDefinitionError() throw() override
0091     {
0092     }
0093 
0094     const QString &archiveDefinitionId() const
0095     {
0096         return m_id;
0097     }
0098 };
0099 
0100 }
0101 
0102 static QString try_extensions(const QString &path)
0103 {
0104     static const char exts[][4] = {
0105         "",
0106         "exe",
0107         "bat",
0108         "bin",
0109         "cmd",
0110     };
0111     static const size_t numExts = sizeof exts / sizeof *exts;
0112     for (unsigned int i = 0; i < numExts; ++i) {
0113         const QFileInfo fi(path + QLatin1Char('.') + QLatin1StringView(exts[i]));
0114         if (fi.exists()) {
0115             return fi.filePath();
0116         }
0117     }
0118     return QString();
0119 }
0120 
0121 static void parse_command(QString cmdline,
0122                           const QString &id,
0123                           const QString &whichCommand,
0124                           QString *command,
0125                           QStringList *prefix,
0126                           QStringList *suffix,
0127                           ArchiveDefinition::ArgumentPassingMethod *method,
0128                           bool parseFilePlaceholder)
0129 {
0130     Q_ASSERT(prefix);
0131     Q_ASSERT(suffix);
0132     Q_ASSERT(method);
0133 
0134     KShell::Errors errors;
0135     QStringList l;
0136 
0137     if (cmdline.startsWith(NULL_SEPARATED_STDIN_INDICATOR)) {
0138         *method = ArchiveDefinition::NullSeparatedInputFile;
0139         cmdline.remove(0, 2);
0140     } else if (cmdline.startsWith(NEWLINE_SEPARATED_STDIN_INDICATOR)) {
0141         *method = ArchiveDefinition::NewlineSeparatedInputFile;
0142         cmdline.remove(0, 1);
0143     } else {
0144         *method = ArchiveDefinition::CommandLine;
0145     }
0146     if (*method != ArchiveDefinition::CommandLine && cmdline.contains(FILE_PLACEHOLDER)) {
0147         throw ArchiveDefinitionError(id, i18n("Cannot use both %f and | in '%1'", whichCommand));
0148     }
0149     cmdline.replace(FILE_PLACEHOLDER, QLatin1StringView("__files_go_here__"))
0150         .replace(INSTALLPATH_PLACEHOLDER, QStringLiteral("__path_goes_here__"))
0151         .replace(FILE_PLACEHOLDER_7BIT, QStringLiteral("__file7Bit_go_here__"));
0152     l = KShell::splitArgs(cmdline, KShell::AbortOnMeta | KShell::TildeExpand, &errors);
0153     l = l.replaceInStrings(QStringLiteral("__files_go_here__"), FILE_PLACEHOLDER);
0154     l = l.replaceInStrings(QStringLiteral("__file7Bit_go_here__"), FILE_PLACEHOLDER_7BIT);
0155     if (l.indexOf(QRegularExpression(QLatin1StringView(".*__path_goes_here__.*"))) >= 0) {
0156         l = l.replaceInStrings(QStringLiteral("__path_goes_here__"), ArchiveDefinition::installPath());
0157     }
0158     if (errors == KShell::BadQuoting) {
0159         throw ArchiveDefinitionError(id, i18n("Quoting error in '%1' entry", whichCommand));
0160     }
0161     if (errors == KShell::FoundMeta) {
0162         throw ArchiveDefinitionError(id, i18n("'%1' too complex (would need shell)", whichCommand));
0163     }
0164     qCDebug(KLEOPATRA_LOG) << "ArchiveDefinition[" << id << ']' << l;
0165     if (l.empty()) {
0166         throw ArchiveDefinitionError(id, i18n("'%1' entry is empty/missing", whichCommand));
0167     }
0168     const QFileInfo fi1(l.front());
0169     if (fi1.isAbsolute()) {
0170         *command = try_extensions(l.front());
0171     } else {
0172         *command = QStandardPaths::findExecutable(fi1.fileName());
0173     }
0174     if (command->isEmpty()) {
0175         throw ArchiveDefinitionError(id, i18n("'%1' empty or not found", whichCommand));
0176     }
0177     if (parseFilePlaceholder) {
0178         const int idx1 = l.indexOf(FILE_PLACEHOLDER);
0179         if (idx1 < 0) {
0180             // none -> append
0181             *prefix = l.mid(1);
0182         } else {
0183             *prefix = l.mid(1, idx1 - 1);
0184             *suffix = l.mid(idx1 + 1);
0185         }
0186     } else {
0187         *prefix = l.mid(1);
0188     }
0189     switch (*method) {
0190     case ArchiveDefinition::CommandLine:
0191         qCDebug(KLEOPATRA_LOG) << "ArchiveDefinition[" << id << ']' << *command << *prefix << FILE_PLACEHOLDER << *suffix;
0192         break;
0193     case ArchiveDefinition::NewlineSeparatedInputFile:
0194         qCDebug(KLEOPATRA_LOG) << "ArchiveDefinition[" << id << ']' << "find | " << *command << *prefix;
0195         break;
0196     case ArchiveDefinition::NullSeparatedInputFile:
0197         qCDebug(KLEOPATRA_LOG) << "ArchiveDefinition[" << id << ']' << "find -print0 | " << *command << *prefix;
0198         break;
0199     case ArchiveDefinition::NumArgumentPassingMethods:
0200         Q_ASSERT(!"Should not happen");
0201         break;
0202     }
0203 }
0204 
0205 namespace
0206 {
0207 
0208 class KConfigBasedArchiveDefinition : public ArchiveDefinition
0209 {
0210 public:
0211     explicit KConfigBasedArchiveDefinition(const KConfigGroup &group)
0212         : ArchiveDefinition(group.readEntryUntranslated(ID_ENTRY), group.readEntry(NAME_ENTRY))
0213     {
0214         if (id().isEmpty()) {
0215             throw ArchiveDefinitionError(group.name(), i18n("'%1' entry is empty/missing", ID_ENTRY));
0216         }
0217 
0218         QStringList extensions;
0219         QString extensionsKey;
0220 
0221         // extensions(-openpgp)
0222         if (group.hasKey(EXTENSIONS_OPENPGP_ENTRY)) {
0223             extensionsKey = EXTENSIONS_OPENPGP_ENTRY;
0224         } else {
0225             extensionsKey = EXTENSIONS_ENTRY;
0226         }
0227         extensions = group.readEntry(extensionsKey, QStringList());
0228         if (extensions.empty()) {
0229             throw ArchiveDefinitionError(id(), i18n("'%1' entry is empty/missing", extensionsKey));
0230         }
0231         setExtensions(OpenPGP, extensions);
0232 
0233         // extensions(-cms)
0234         if (group.hasKey(EXTENSIONS_CMS_ENTRY)) {
0235             extensionsKey = EXTENSIONS_CMS_ENTRY;
0236         } else {
0237             extensionsKey = EXTENSIONS_ENTRY;
0238         }
0239         extensions = group.readEntry(extensionsKey, QStringList());
0240         if (extensions.empty()) {
0241             throw ArchiveDefinitionError(id(), i18n("'%1' entry is empty/missing", extensionsKey));
0242         }
0243         setExtensions(CMS, extensions);
0244 
0245         ArgumentPassingMethod method;
0246 
0247         // pack-command(-openpgp)
0248         if (group.hasKey(PACK_COMMAND_OPENPGP_ENTRY))
0249             parse_command(group.readEntry(PACK_COMMAND_OPENPGP_ENTRY),
0250                           id(),
0251                           PACK_COMMAND_OPENPGP_ENTRY,
0252                           &m_packCommand[OpenPGP],
0253                           &m_packPrefixArguments[OpenPGP],
0254                           &m_packPostfixArguments[OpenPGP],
0255                           &method,
0256                           true);
0257         else
0258             parse_command(group.readEntry(PACK_COMMAND_ENTRY),
0259                           id(),
0260                           PACK_COMMAND_ENTRY,
0261                           &m_packCommand[OpenPGP],
0262                           &m_packPrefixArguments[OpenPGP],
0263                           &m_packPostfixArguments[OpenPGP],
0264                           &method,
0265                           true);
0266         setPackCommandArgumentPassingMethod(OpenPGP, method);
0267 
0268         // pack-command(-cms)
0269         if (group.hasKey(PACK_COMMAND_CMS_ENTRY))
0270             parse_command(group.readEntry(PACK_COMMAND_CMS_ENTRY),
0271                           id(),
0272                           PACK_COMMAND_CMS_ENTRY,
0273                           &m_packCommand[CMS],
0274                           &m_packPrefixArguments[CMS],
0275                           &m_packPostfixArguments[CMS],
0276                           &method,
0277                           true);
0278         else
0279             parse_command(group.readEntry(PACK_COMMAND_ENTRY),
0280                           id(),
0281                           PACK_COMMAND_ENTRY,
0282                           &m_packCommand[CMS],
0283                           &m_packPrefixArguments[CMS],
0284                           &m_packPostfixArguments[CMS],
0285                           &method,
0286                           true);
0287         setPackCommandArgumentPassingMethod(CMS, method);
0288 
0289         QStringList dummy;
0290 
0291         // unpack-command(-openpgp)
0292         if (group.hasKey(UNPACK_COMMAND_OPENPGP_ENTRY))
0293             parse_command(group.readEntry(UNPACK_COMMAND_OPENPGP_ENTRY),
0294                           id(),
0295                           UNPACK_COMMAND_OPENPGP_ENTRY,
0296                           &m_unpackCommand[OpenPGP],
0297                           &m_unpackArguments[OpenPGP],
0298                           &dummy,
0299                           &method,
0300                           false);
0301         else
0302             parse_command(group.readEntry(UNPACK_COMMAND_ENTRY),
0303                           id(),
0304                           UNPACK_COMMAND_ENTRY,
0305                           &m_unpackCommand[OpenPGP],
0306                           &m_unpackArguments[OpenPGP],
0307                           &dummy,
0308                           &method,
0309                           false);
0310         if (method != CommandLine) {
0311             throw ArchiveDefinitionError(id(), i18n("cannot use argument passing on standard input for unpack-command"));
0312         }
0313 
0314         // unpack-command(-cms)
0315         if (group.hasKey(UNPACK_COMMAND_CMS_ENTRY))
0316             parse_command(group.readEntry(UNPACK_COMMAND_CMS_ENTRY),
0317                           id(),
0318                           UNPACK_COMMAND_CMS_ENTRY,
0319                           &m_unpackCommand[CMS],
0320                           &m_unpackArguments[CMS],
0321                           &dummy,
0322                           &method,
0323                           false);
0324         else
0325             parse_command(group.readEntry(UNPACK_COMMAND_ENTRY),
0326                           id(),
0327                           UNPACK_COMMAND_ENTRY,
0328                           &m_unpackCommand[CMS],
0329                           &m_unpackArguments[CMS],
0330                           &dummy,
0331                           &method,
0332                           false);
0333         if (method != CommandLine) {
0334             throw ArchiveDefinitionError(id(), i18n("cannot use argument passing on standard input for unpack-command"));
0335         }
0336     }
0337 
0338 private:
0339     QString doGetPackCommand(GpgME::Protocol p) const override
0340     {
0341         return m_packCommand[p];
0342     }
0343     QString doGetUnpackCommand(GpgME::Protocol p) const override
0344     {
0345         return m_unpackCommand[p];
0346     }
0347     QStringList doGetPackArguments(GpgME::Protocol p, const QStringList &files) const override
0348     {
0349         return m_packPrefixArguments[p] + files + m_packPostfixArguments[p];
0350     }
0351     QStringList doGetUnpackArguments(GpgME::Protocol p, const QString &file) const override
0352     {
0353         QStringList copy = m_unpackArguments[p];
0354         if (copy.contains(FILE_PLACEHOLDER_7BIT)) {
0355             /* This is a crutch for missing a way to provide Unicode arguments
0356              * to gpgtar unless gpgtar offers a unicode interface we have
0357              * no defined way to provide non 7Bit arguments. So we filter out
0358              * the chars and replace them by _ to avoid completely broken
0359              * folder names when unpacking. This is only relevant for the
0360              * unpacked folder and does not effect files in the archive. */
0361             const QRegularExpression non7Bit(QLatin1StringView(R"([^\x{0000}-\x{007F}])"));
0362             QString underscore_filename = file;
0363             underscore_filename.replace(non7Bit, QStringLiteral("_"));
0364             copy.replaceInStrings(FILE_PLACEHOLDER_7BIT, underscore_filename);
0365         }
0366         copy.replaceInStrings(FILE_PLACEHOLDER, file);
0367         return copy;
0368     }
0369 
0370 private:
0371     QString m_packCommand[2], m_unpackCommand[2];
0372     QStringList m_packPrefixArguments[2], m_packPostfixArguments[2];
0373     QStringList m_unpackArguments[2];
0374 };
0375 
0376 }
0377 
0378 ArchiveDefinition::ArchiveDefinition(const QString &id, const QString &label)
0379     : m_id(id)
0380     , m_label(label)
0381 {
0382     m_packCommandMethod[GpgME::OpenPGP] = m_packCommandMethod[GpgME::CMS] = CommandLine;
0383 }
0384 
0385 ArchiveDefinition::~ArchiveDefinition()
0386 {
0387 }
0388 
0389 QString ArchiveDefinition::stripExtension(GpgME::Protocol p, const QString &filePath) const
0390 {
0391     checkProtocol(p);
0392     for (const auto &ext : std::as_const(m_extensions[p])) {
0393         if (filePath.endsWith(QLatin1Char('.') + ext)) {
0394             return filePath.chopped(ext.size() + 1);
0395         }
0396     }
0397     return filePath;
0398 }
0399 
0400 static QByteArray make_input(const QStringList &files, char sep)
0401 {
0402     QByteArray result;
0403     for (const QString &file : files) {
0404 #ifdef Q_OS_WIN
0405         // As encoding is more complicated on windows with different
0406         // 8 bit codepages we always use UTF-8 here and add this as an
0407         // option in the libkleopatrarc.desktop archive definition.
0408         result += file.toUtf8();
0409 #else
0410         result += QFile::encodeName(file);
0411 #endif
0412         result += sep;
0413     }
0414     return result;
0415 }
0416 
0417 std::shared_ptr<Input> ArchiveDefinition::createInputFromPackCommand(GpgME::Protocol p, const QStringList &files) const
0418 {
0419     checkProtocol(p);
0420     const QString base = heuristicBaseDirectory(files);
0421     if (base.isEmpty()) {
0422         throw Kleo::Exception(GPG_ERR_CONFLICT, i18n("Cannot find common base directory for these files:\n%1", files.join(QLatin1Char('\n'))));
0423     }
0424     qCDebug(KLEOPATRA_LOG) << "heuristicBaseDirectory(" << files << ") ->" << base;
0425     const QStringList relative = makeRelativeTo(base, files);
0426     qCDebug(KLEOPATRA_LOG) << "relative" << relative;
0427     switch (m_packCommandMethod[p]) {
0428     case CommandLine:
0429         return Input::createFromProcessStdOut(doGetPackCommand(p), doGetPackArguments(p, relative), QDir(base));
0430     case NewlineSeparatedInputFile:
0431         return Input::createFromProcessStdOut(doGetPackCommand(p), doGetPackArguments(p, QStringList()), QDir(base), make_input(relative, '\n'));
0432     case NullSeparatedInputFile:
0433         return Input::createFromProcessStdOut(doGetPackCommand(p), doGetPackArguments(p, QStringList()), QDir(base), make_input(relative, '\0'));
0434     case NumArgumentPassingMethods:
0435         Q_ASSERT(!"Should not happen");
0436     }
0437     return std::shared_ptr<Input>(); // make compiler happy
0438 }
0439 
0440 std::shared_ptr<Output> ArchiveDefinition::createOutputFromUnpackCommand(GpgME::Protocol p, const QString &file, const QDir &wd) const
0441 {
0442     checkProtocol(p);
0443     const QFileInfo fi(file);
0444     return Output::createFromProcessStdIn(doGetUnpackCommand(p), doGetUnpackArguments(p, fi.absoluteFilePath()), wd);
0445 }
0446 
0447 // static
0448 std::vector<std::shared_ptr<ArchiveDefinition>> ArchiveDefinition::getArchiveDefinitions()
0449 {
0450     QStringList errors;
0451     return getArchiveDefinitions(errors);
0452 }
0453 
0454 // static
0455 std::vector<std::shared_ptr<ArchiveDefinition>> ArchiveDefinition::getArchiveDefinitions(QStringList &errors)
0456 {
0457     std::vector<std::shared_ptr<ArchiveDefinition>> result;
0458     KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("libkleopatrarc"));
0459     const QStringList groups = config->groupList().filter(QRegularExpression(QStringLiteral("^Archive Definition #")));
0460     result.reserve(groups.size());
0461     for (const QString &group : groups)
0462         try {
0463             const std::shared_ptr<ArchiveDefinition> ad(new KConfigBasedArchiveDefinition(KConfigGroup(config, group)));
0464             result.push_back(ad);
0465         } catch (const std::exception &e) {
0466             qCDebug(KLEOPATRA_LOG) << e.what();
0467             errors.push_back(QString::fromLocal8Bit(e.what()));
0468         } catch (...) {
0469             errors.push_back(i18n("Caught unknown exception in group %1", group));
0470         }
0471     return result;
0472 }
0473 
0474 void ArchiveDefinition::checkProtocol(GpgME::Protocol p) const
0475 {
0476     kleo_assert(p == GpgME::OpenPGP || p == GpgME::CMS);
0477 }