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 }