diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 6190084dc..7007329e9 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,12 +1,19 @@ # SPDX-FileCopyrightText: none # SPDX-License-Identifier: BSD-3-Clause set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) include_directories(${CMAKE_SOURCE_DIR}/src) set(kuniqueservicetest_src kuniqueservicetest.cpp ${CMAKE_SOURCE_DIR}/src/utils/kuniqueservice.cpp) ecm_qt_declare_logging_category(kuniqueservicetest_src HEADER kleopatra_debug.h IDENTIFIER KLEOPATRA_LOG CATEGORY_NAME org.kde.pim.kleopatra) add_executable(kuniqueservicetest ${kuniqueservicetest_src}) add_test(NAME kuniqueservicetest COMMAND kuniqueservicetest) ecm_mark_as_test(kuniqueservicetest) target_link_libraries(kuniqueservicetest Qt${QT_MAJOR_VERSION}::Test ${_kleopatra_dbusaddons_libs}) + +set(stripsuffixtest_src stripsuffixtest.cpp ${CMAKE_SOURCE_DIR}/src/utils/path-helper.cpp) +ecm_qt_declare_logging_category(stripsuffixtest_src HEADER kleopatra_debug.h IDENTIFIER KLEOPATRA_LOG CATEGORY_NAME org.kde.pim.kleopatra) +add_executable(stripsuffixtest ${stripsuffixtest_src}) +add_test(NAME stripsuffixtest COMMAND stripsuffixtest) +ecm_mark_as_test(stripsuffixtest) +target_link_libraries(stripsuffixtest Qt${QT_MAJOR_VERSION}::Test KF5::Libkleo KF5::I18n) diff --git a/autotests/stripsuffixtest.cpp b/autotests/stripsuffixtest.cpp new file mode 100644 index 000000000..9c971e223 --- /dev/null +++ b/autotests/stripsuffixtest.cpp @@ -0,0 +1,57 @@ +/* -*- mode: c++; c-basic-offset:4 -*- + autotests/stripsuffixtest.cpp + + This file is part of Kleopatra's test suite. + SPDX-FileCopyrightText: 2022 Carlo Vanini + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include "utils/path-helper.h" + +class StripSuffixTest: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testStripSuffix_data(); + void testStripSuffix(); +}; + +void StripSuffixTest::testStripSuffix_data() +{ + QTest::addColumn("fileName"); + QTest::addColumn("baseName"); + + QTest::newRow("absolute path") + << QString::fromLatin1("/home/user/test.sig") + << QString::fromLatin1("/home/user/test"); + QTest::newRow("relative path") + << QString::fromLatin1("home/user.name/test.sig") + << QString::fromLatin1("home/user.name/test"); + QTest::newRow("file name") + << QString::fromLatin1("t.sig") + << QString::fromLatin1("./t"); + QTest::newRow("short extension") + << QString::fromLatin1("/path/to/test.s") + << QString::fromLatin1("/path/to/test"); + QTest::newRow("long extension") + << QString::fromLatin1("/test.sign") + << QString::fromLatin1("/test"); + QTest::newRow("multiple extension") + << QString::fromLatin1("some/test.tar.gz.asc") + << QString::fromLatin1("some/test.tar.gz"); +} + +void StripSuffixTest::testStripSuffix() +{ + QFETCH(QString, fileName); + QFETCH(QString, baseName); + + QCOMPARE(Kleo::stripSuffix(fileName), baseName); +} + +QTEST_MAIN(StripSuffixTest) +#include "stripsuffixtest.moc" diff --git a/src/crypto/autodecryptverifyfilescontroller.cpp b/src/crypto/autodecryptverifyfilescontroller.cpp index 49a824408..ca42c1159 100644 --- a/src/crypto/autodecryptverifyfilescontroller.cpp +++ b/src/crypto/autodecryptverifyfilescontroller.cpp @@ -1,522 +1,522 @@ /* -*- mode: c++; c-basic-offset:4 -*- autodecryptverifyfilescontroller.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "autodecryptverifyfilescontroller.h" #include "fileoperationspreferences.h" #include #include #include #include #include "commands/decryptverifyfilescommand.h" #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include using namespace GpgME; using namespace Kleo; using namespace Kleo::Crypto; using namespace Kleo::Crypto::Gui; class AutoDecryptVerifyFilesController::Private { AutoDecryptVerifyFilesController *const q; public: explicit Private(AutoDecryptVerifyFilesController *qq); ~Private() { qCDebug(KLEOPATRA_LOG); delete m_workDir; } void slotDialogCanceled(); void schedule(); void exec(); std::vector > buildTasks(const QStringList &, QStringList &); struct CryptoFile { QString baseName; QString fileName; GpgME::Protocol protocol = GpgME::UnknownProtocol; int classification = 0; std::shared_ptr output; }; QVector classifyAndSortFiles(const QStringList &files); void reportError(int err, const QString &details) { q->setLastError(err, details); q->emitDoneOrError(); } void cancelAllTasks(); QStringList m_passedFiles, m_filesAfterPreparation; std::vector > m_results; std::vector > m_runnableTasks, m_completedTasks; std::shared_ptr m_runningTask; bool m_errorDetected = false; DecryptVerifyOperation m_operation = DecryptVerify; DecryptVerifyFilesDialog *m_dialog = nullptr; QTemporaryDir *m_workDir = nullptr; }; AutoDecryptVerifyFilesController::Private::Private(AutoDecryptVerifyFilesController *qq) : q(qq) { qRegisterMetaType(); } void AutoDecryptVerifyFilesController::Private::slotDialogCanceled() { qCDebug(KLEOPATRA_LOG); } void AutoDecryptVerifyFilesController::Private::schedule() { if (!m_runningTask && !m_runnableTasks.empty()) { const std::shared_ptr t = m_runnableTasks.back(); m_runnableTasks.pop_back(); t->start(); m_runningTask = t; } if (!m_runningTask) { kleo_assert(m_runnableTasks.empty()); for (const std::shared_ptr &i : std::as_const(m_results)) { Q_EMIT q->verificationResult(i->verificationResult()); } } } void AutoDecryptVerifyFilesController::Private::exec() { Q_ASSERT(!m_dialog); QStringList undetected; std::vector > tasks = buildTasks(m_passedFiles, undetected); if (!undetected.isEmpty()) { // Since GpgME 1.7.0 Classification is supposed to be reliable // so we really can't do anything with this data. reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to find encrypted or signed data in one or more files." "You can manually select what to do with the files now." "If they contain signed or encrypted data please report a bug (see Help->Report Bug).")); auto cmd = new Commands::DecryptVerifyFilesCommand(undetected, nullptr, true); cmd->start(); } if (tasks.empty()) { q->emitDoneOrError(); return; } Q_ASSERT(m_runnableTasks.empty()); m_runnableTasks.swap(tasks); std::shared_ptr coll(new TaskCollection); for (const std::shared_ptr &i : std::as_const(m_runnableTasks)) { q->connectTask(i); } coll->setTasks(m_runnableTasks); m_dialog = new DecryptVerifyFilesDialog(coll); m_dialog->setOutputLocation(heuristicBaseDirectory(m_passedFiles)); QTimer::singleShot(0, q, SLOT(schedule())); if (m_dialog->exec() == QDialog::Accepted && m_workDir) { // Without workdir there is nothing to move. const QDir workdir(m_workDir->path()); const QDir outDir(m_dialog->outputLocation()); bool overWriteAll = false; qCDebug(KLEOPATRA_LOG) << workdir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); for (const QFileInfo &fi: workdir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot)) { const auto inpath = fi.absoluteFilePath(); if (fi.isDir()) { // A directory. Assume that the input was an archive // and avoid directory merges by trying to find a non // existing directory. auto candidate = fi.baseName(); if (candidate.startsWith(QLatin1Char('-'))) { // Bug in GpgTar Extracts stdout passed archives to a dir named - candidate = QFileInfo(m_passedFiles.first()).baseName(); } QString suffix; QFileInfo ofi; int i = 0; do { ofi = QFileInfo(outDir.absoluteFilePath(candidate + suffix)); if (!ofi.exists()) { break; } suffix = QStringLiteral("_%1").arg(++i); } while (i < 1000); if (!moveDir(inpath, ofi.absoluteFilePath())) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to move %1 to %2.", inpath, ofi.absoluteFilePath())); } continue; } const auto outpath = outDir.absoluteFilePath(fi.fileName()); qCDebug(KLEOPATRA_LOG) << "Moving " << inpath << " to " << outpath; const QFileInfo ofi(outpath); if (ofi.exists()) { int sel = KMessageBox::No; if (!overWriteAll) { sel = KMessageBox::questionYesNoCancel(m_dialog, i18n("The file %1 already exists.\n" "Overwrite?", outpath), i18n("Overwrite Existing File?"), KStandardGuiItem::overwrite(), KGuiItem(i18n("Overwrite All")), KStandardGuiItem::cancel()); } if (sel == KMessageBox::Cancel) { qCDebug(KLEOPATRA_LOG) << "Overwriting canceled for: " << outpath; continue; } if (sel == KMessageBox::No) { //Overwrite All overWriteAll = true; } if (!QFile::remove(outpath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to delete %1.", outpath)); continue; } } if (!QFile::rename(inpath, outpath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to move %1 to %2.", inpath, outpath)); } } } q->emitDoneOrError(); delete m_dialog; m_dialog = nullptr; } QVector AutoDecryptVerifyFilesController::Private::classifyAndSortFiles(const QStringList &files) { const auto isSignature = [](int classification) -> bool { return mayBeDetachedSignature(classification) || mayBeOpaqueSignature(classification) || (classification & Class::TypeMask) == Class::ClearsignedMessage; }; QVector out; for (const auto &file : files) { CryptoFile cFile; cFile.fileName = file; - cFile.baseName = file.left(file.length() - 4); + cFile.baseName = stripSuffix(file); cFile.classification = classify(file); cFile.protocol = findProtocol(cFile.classification); auto it = std::find_if(out.begin(), out.end(), [&cFile](const CryptoFile &other) { return other.protocol == cFile.protocol && other.baseName == cFile.baseName; }); if (it != out.end()) { // If we found a file with the same basename, make sure that encrypted // file is before the signature file, so that we first decrypt and then // verify if (isSignature(cFile.classification) && isCipherText(it->classification)) { out.insert(it + 1, cFile); } else if (isCipherText(cFile.classification) && isSignature(it->classification)) { out.insert(it, cFile); } else { // both are signatures or both are encrypted files, in which // case order does not matter out.insert(it, cFile); } } else { out.push_back(cFile); } } return out; } std::vector< std::shared_ptr > AutoDecryptVerifyFilesController::Private::buildTasks(const QStringList &fileNames, QStringList &undetected) { // sort files so that we make sure we first decrypt and then verify QVector cryptoFiles = classifyAndSortFiles(fileNames); std::vector > tasks; for (auto it = cryptoFiles.begin(), end = cryptoFiles.end(); it != end; ++it) { auto &cFile = (*it); QFileInfo fi(cFile.fileName); qCDebug(KLEOPATRA_LOG) << "classified" << cFile.fileName << "as" << printableClassification(cFile.classification); if (!fi.isReadable()) { reportError(makeGnuPGError(GPG_ERR_ASS_NO_INPUT), xi18n("Cannot open %1 for reading.", cFile.fileName)); continue; } if (mayBeAnyCertStoreType(cFile.classification)) { // Trying to verify a certificate. Possible because extensions are often similar // for PGP Keys. reportError(makeGnuPGError(GPG_ERR_ASS_NO_INPUT), xi18n("The file %1 contains certificates and can't be decrypted or verified.", cFile.fileName)); qCDebug(KLEOPATRA_LOG) << "reported error"; continue; } // We can't reliably detect CMS detached signatures, so we will try to do // our best to use the current file as a detached signature and fallback to // opaque signature otherwise. if (cFile.protocol == GpgME::CMS && mayBeDetachedSignature(cFile.classification)) { // First, see if previous task was a decryption task for the same file // and "pipe" it's output into our input std::shared_ptr input; bool prepend = false; if (it != cryptoFiles.begin()) { const auto prev = it - 1; if (prev->protocol == cFile.protocol && prev->baseName == cFile.baseName) { input = Input::createFromOutput(prev->output); prepend = true; } } if (!input) { if (QFile::exists(cFile.baseName)) { input = Input::createFromFile(cFile.baseName); } } if (input) { qCDebug(KLEOPATRA_LOG) << "Detached CMS verify: " << cFile.fileName; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(input); t->setProtocol(cFile.protocol); if (prepend) { // Put the verify task BEFORE the decrypt task in the tasks queue, // because the tasks are executed in reverse order! tasks.insert(tasks.end() - 1, t); } else { tasks.push_back(t); } continue; } else { // No signed data, maybe not a detached signature } } if (isDetachedSignature(cFile.classification)) { // Detached signature, try to find data or ask the user. QString signedDataFileName = cFile.baseName; if (signedDataFileName.isEmpty()) { signedDataFileName = QFileDialog::getOpenFileName(nullptr, xi18n("Select the file to verify with \"%1\"", fi.fileName()), fi.dir().dirName()); } if (signedDataFileName.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "No signed data selected. Verify aborted."; } else { qCDebug(KLEOPATRA_LOG) << "Detached verify: " << cFile.fileName << " Data: " << signedDataFileName; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(Input::createFromFile(signedDataFileName)); t->setProtocol(cFile.protocol); tasks.push_back(t); } continue; } if (!mayBeAnyMessageType(cFile.classification)) { // Not a Message? Maybe there is a signature for this file? const auto signatures = findSignatures(cFile.fileName); bool foundSig = false; if (!signatures.empty()) { for (const QString &sig : signatures) { const auto classification = classify(sig); qCDebug(KLEOPATRA_LOG) << "Guessing: " << sig << " is a signature for: " << cFile.fileName << "Classification: " << classification; const auto proto = findProtocol(classification); if (proto == GpgME::UnknownProtocol) { qCDebug(KLEOPATRA_LOG) << "Could not determine protocol. Skipping guess."; continue; } foundSig = true; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(sig)); t->setSignedData(Input::createFromFile(cFile.fileName)); t->setProtocol(proto); tasks.push_back(t); } } if (!foundSig) { undetected << cFile.fileName; qCDebug(KLEOPATRA_LOG) << "Failed detection for: " << cFile.fileName << " adding to undetected."; } } else { // Any Message type so we have input and output. const auto input = Input::createFromFile(cFile.fileName); const auto archiveDefinitions = ArchiveDefinition::getArchiveDefinitions(); const auto ad = q->pick_archive_definition(cFile.protocol, archiveDefinitions, cFile.fileName); if (FileOperationsPreferences().dontUseTmpDir()) { if (!m_workDir) { m_workDir = new QTemporaryDir(heuristicBaseDirectory(fileNames) + QStringLiteral("/kleopatra-XXXXXX")); } if (!m_workDir->isValid()) { qCDebug(KLEOPATRA_LOG) << m_workDir->path() << "not a valid temporary directory."; delete m_workDir; m_workDir = new QTemporaryDir(); } } else if (!m_workDir) { m_workDir = new QTemporaryDir(); } qCDebug(KLEOPATRA_LOG) << "Using:" << m_workDir->path() << "as temporary directory."; const auto wd = QDir(m_workDir->path()); const auto output = ad ? ad->createOutputFromUnpackCommand(cFile.protocol, cFile.fileName, wd) : /*else*/ Output::createFromFile(wd.absoluteFilePath(outputFileName(fi.fileName())), false); // If this might be opaque CMS signature, then try that. We already handled // detached CMS signature above const auto isCMSOpaqueSignature = cFile.protocol == GpgME::CMS && mayBeOpaqueSignature(cFile.classification); if (isOpaqueSignature(cFile.classification) || isCMSOpaqueSignature) { qCDebug(KLEOPATRA_LOG) << "creating a VerifyOpaqueTask"; std::shared_ptr t(new VerifyOpaqueTask); t->setInput(input); t->setOutput(output); t->setProtocol(cFile.protocol); tasks.push_back(t); } else { // Any message. That is not an opaque signature needs to be // decrypted. Verify we always do because we can't know if // an encrypted message is also signed. qCDebug(KLEOPATRA_LOG) << "creating a DecryptVerifyTask"; std::shared_ptr t(new DecryptVerifyTask); t->setInput(input); t->setOutput(output); t->setProtocol(cFile.protocol); cFile.output = output; tasks.push_back(t); } } } return tasks; } void AutoDecryptVerifyFilesController::setFiles(const QStringList &files) { d->m_passedFiles = files; } AutoDecryptVerifyFilesController::AutoDecryptVerifyFilesController(QObject *parent) : DecryptVerifyFilesController(parent), d(new Private(this)) { } AutoDecryptVerifyFilesController::AutoDecryptVerifyFilesController(const std::shared_ptr &ctx, QObject *parent) : DecryptVerifyFilesController(ctx, parent), d(new Private(this)) { } AutoDecryptVerifyFilesController::~AutoDecryptVerifyFilesController() { qCDebug(KLEOPATRA_LOG); } void AutoDecryptVerifyFilesController::start() { d->exec(); } void AutoDecryptVerifyFilesController::setOperation(DecryptVerifyOperation op) { d->m_operation = op; } DecryptVerifyOperation AutoDecryptVerifyFilesController::operation() const { return d->m_operation; } void AutoDecryptVerifyFilesController::Private::cancelAllTasks() { // we just kill all runnable tasks - this will not result in // signal emissions. m_runnableTasks.clear(); // a cancel() will result in a call to if (m_runningTask) { m_runningTask->cancel(); } } void AutoDecryptVerifyFilesController::cancel() { qCDebug(KLEOPATRA_LOG); try { d->m_errorDetected = true; if (d->m_dialog) { d->m_dialog->close(); } d->cancelAllTasks(); } catch (const std::exception &e) { qCDebug(KLEOPATRA_LOG) << "Caught exception: " << e.what(); } } void AutoDecryptVerifyFilesController::doTaskDone(const Task *task, const std::shared_ptr &result) { Q_ASSERT(task); Q_UNUSED(task) // We could just delete the tasks here, but we can't use // Qt::QueuedConnection here (we need sender()) and other slots // might not yet have executed. Therefore, we push completed tasks // into a burial container d->m_completedTasks.push_back(d->m_runningTask); d->m_runningTask.reset(); if (const std::shared_ptr &dvr = std::dynamic_pointer_cast(result)) { d->m_results.push_back(dvr); } QTimer::singleShot(0, this, SLOT(schedule())); } #include "moc_autodecryptverifyfilescontroller.cpp" diff --git a/src/utils/path-helper.cpp b/src/utils/path-helper.cpp index efdffbb1a..2d86395d0 100644 --- a/src/utils/path-helper.cpp +++ b/src/utils/path-helper.cpp @@ -1,162 +1,168 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/path-helper.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "path-helper.h" #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include using namespace Kleo; static QString commonPrefix(const QString &s1, const QString &s2) { return QString(s1.data(), std::mismatch(s1.data(), s1.data() + std::min(s1.size(), s2.size()), s2.data()).first - s1.data()); } static QString longestCommonPrefix(const QStringList &sl) { if (sl.empty()) { return QString(); } QString result = sl.front(); for (const QString &s : sl) { result = commonPrefix(s, result); } return result; } QString Kleo::heuristicBaseDirectory(const QStringList &fileNames) { QStringList dirs; for (const QString &fileName : fileNames) { dirs.push_back(QFileInfo(fileName).path() + QLatin1Char('/')); } qCDebug(KLEOPATRA_LOG) << "dirs" << dirs; const QString candidate = longestCommonPrefix(dirs); /* Special case handling for Outlook attachment temporary path. * This is otherwise something like: * c:\users\username\AppData\Local\Microsoft\Windows\INetCache\ * Content.Outlook\ADSDFG9\foo.txt * * This is very common when encrypted attachments are opened * within outlook. */ if (candidate.contains(QStringLiteral("Content.Outlook"))) { return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } const int idx = candidate.lastIndexOf(QLatin1Char('/')); return candidate.left(idx); } QStringList Kleo::makeRelativeTo(const QString &base, const QStringList &fileNames) { if (base.isEmpty()) { return fileNames; } else { return makeRelativeTo(QDir(base), fileNames); } } QStringList Kleo::makeRelativeTo(const QDir &baseDir, const QStringList &fileNames) { QStringList rv; rv.reserve(fileNames.size()); std::transform(fileNames.cbegin(), fileNames.cend(), std::back_inserter(rv), [&baseDir](const QString &file) { return baseDir.relativeFilePath(file); }); return rv; } +QString Kleo::stripSuffix(const QString &fileName) +{ + const QFileInfo fi(fileName); + return fi.dir().filePath(fi.completeBaseName()); +} + void Kleo::recursivelyRemovePath(const QString &path) { const QFileInfo fi(path); if (fi.isDir()) { QDir dir(path); const auto dirs{dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot)}; for (const QString &fname : dirs) { recursivelyRemovePath(dir.filePath(fname)); } const QString dirName = fi.fileName(); dir.cdUp(); if (!dir.rmdir(dirName)) { throw Exception(GPG_ERR_EPERM, i18n("Cannot remove directory %1", path)); } } else { QFile file(path); if (!file.remove()) { throw Exception(GPG_ERR_EPERM, i18n("Cannot remove file %1: %2", path, file.errorString())); } } } bool Kleo::recursivelyCopy(const QString &src,const QString &dest) { QDir srcDir(src); if(!srcDir.exists()) { return false; } QDir destDir(dest); if(!destDir.exists() && !destDir.mkdir(dest)) { return false; } for(const auto &file: srcDir.entryList(QDir::Files)) { const QString srcName = src + QLatin1Char('/') + file; const QString destName = dest + QLatin1Char('/') + file; if(!QFile::copy(srcName, destName)) { return false; } } for (const auto &dir: srcDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { const QString srcName = src + QLatin1Char('/') + dir; const QString destName = dest + QLatin1Char('/') + dir; if (!recursivelyCopy(srcName, destName)) { return false; } } return true; } bool Kleo::moveDir(const QString &src, const QString &dest) { if (QStorageInfo(src).device() == QStorageInfo(dest).device()) { // Easy same partition we can use qt. return QFile::rename(src, dest); } // first copy if (!recursivelyCopy(src, dest)) { return false; } // Then delete original recursivelyRemovePath(src); return true; } diff --git a/src/utils/path-helper.h b/src/utils/path-helper.h index 9389aa9ca..aba3f72b2 100644 --- a/src/utils/path-helper.h +++ b/src/utils/path-helper.h @@ -1,27 +1,28 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/path-helper.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once class QString; #include class QDir; namespace Kleo { QString heuristicBaseDirectory(const QStringList &files); QStringList makeRelativeTo(const QDir &dir, const QStringList &files); QStringList makeRelativeTo(const QString &dir, const QStringList &files); +QString stripSuffix(const QString &fileName); void recursivelyRemovePath(const QString &path); bool recursivelyCopy(const QString &src, const QString &dest); bool moveDir(const QString &src, const QString &dest); }