diff --git a/src/crypto/checksumsutils_p.h b/src/crypto/checksumsutils_p.h new file mode 100644 index 000000000..1d94f0ee3 --- /dev/null +++ b/src/crypto/checksumsutils_p.h @@ -0,0 +1,129 @@ +/* -*- mode: c++; c-basic-offset:4 -*- + crypto/createchecksumscontroller.cpp + + This file is part of Kleopatra, the KDE keymanager + SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef CHECKSUMSUTILS_P +#define CHECKSUMSUTILS_P + +#include + +#include +#include "kleopatra_debug.h" + +#ifdef Q_OS_UNIX +// can we use QAbstractFileEngine::caseSensitive()? +static const Qt::CaseSensitivity fs_cs = Qt::CaseSensitive; +#else +static const Qt::CaseSensitivity fs_cs = Qt::CaseInsensitive; +#endif + +using namespace Kleo; + +static QList get_patterns(const std::vector< std::shared_ptr > &checksumDefinitions) +{ + QList result; + for (const std::shared_ptr &cd : checksumDefinitions) + if (cd) { + const auto patterns = cd->patterns(); + for (const QString &pattern : patterns) { + result.push_back(QRegExp(pattern, fs_cs)); + } + } + return result; +} + +namespace +{ + +struct matches_any : std::unary_function { + const QList m_regexps; + explicit matches_any(const QList ®exps) : m_regexps(regexps) {} + bool operator()(const QString &s) const + { + return std::any_of(m_regexps.cbegin(), m_regexps.cend(), + [s](const QRegExp &rx) { return rx.exactMatch(s); }); + } +}; +} + +namespace +{ +struct File { + QString name; + QByteArray checksum; + bool binary; +}; +} + +static QString decode(const QString &encoded) +{ + QString decoded; + decoded.reserve(encoded.size()); + bool shift = false; + for (QChar ch : encoded) + if (shift) { + switch (ch.toLatin1()) { + case '\\': decoded += QLatin1Char('\\'); break; + case 'n': decoded += QLatin1Char('\n'); break; + default: + qCDebug(KLEOPATRA_LOG) << "invalid escape sequence" << '\\' << ch << "(interpreted as '" << ch << "')"; + decoded += ch; + break; + } + shift = false; + } else { + if (ch == QLatin1Char('\\')) { + shift = true; + } else { + decoded += ch; + } + } + return decoded; +} + +static std::vector parse_sum_file(const QString &fileName) +{ + std::vector files; + QFile f(fileName); + if (f.open(QIODevice::ReadOnly)) { + QTextStream s(&f); + QRegExp rx(QLatin1String("(\\?)([a-f0-9A-F]+) ([ *])([^\n]+)\n*")); + while (!s.atEnd()) { + const QString line = s.readLine(); + if (rx.exactMatch(line)) { + Q_ASSERT(!rx.cap(4).endsWith(QLatin1Char('\n'))); + const File file = { + rx.cap(1) == QLatin1String("\\") ? decode(rx.cap(4)) : rx.cap(4), + rx.cap(2).toLatin1(), + rx.cap(3) == QLatin1String("*"), + }; + files.push_back(file); + } + } + } + return files; +} + + +static std::shared_ptr filename2definition(const QString &fileName, + const std::vector< std::shared_ptr > &checksumDefinitions) +{ + for (const std::shared_ptr &cd : checksumDefinitions) { + if (cd) { + const auto patterns = cd->patterns(); + for (const QString &pattern : patterns) { + if (QRegExp(pattern, fs_cs).exactMatch(fileName)) { + return cd; + } + } + } + } + return std::shared_ptr(); +} + +#endif // CHECKSUMSUTILS_P diff --git a/src/crypto/createchecksumscontroller.cpp b/src/crypto/createchecksumscontroller.cpp index dcd0fbc8d..656a5d086 100644 --- a/src/crypto/createchecksumscontroller.cpp +++ b/src/crypto/createchecksumscontroller.cpp @@ -1,719 +1,609 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/createchecksumscontroller.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "createchecksumscontroller.h" +#include "checksumsutils_p.h" #include #include #include #include -#include #include #include -#include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Crypto; namespace { class ResultDialog : public QDialog { Q_OBJECT public: ResultDialog(const QStringList &created, const QStringList &errors, QWidget *parent = nullptr, Qt::WindowFlags f = {}) : QDialog(parent, f), createdLB(created.empty() ? i18nc("@info", "No checksum files have been created.") : i18nc("@info", "These checksum files have been successfully created:"), this), createdLW(this), errorsLB(errors.empty() ? i18nc("@info", "There were no errors.") : i18nc("@info", "The following errors were encountered:"), this), errorsLW(this), buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, this), vlay(this) { KDAB_SET_OBJECT_NAME(createdLB); KDAB_SET_OBJECT_NAME(createdLW); KDAB_SET_OBJECT_NAME(errorsLB); KDAB_SET_OBJECT_NAME(errorsLW); KDAB_SET_OBJECT_NAME(buttonBox); KDAB_SET_OBJECT_NAME(vlay); createdLW.addItems(created); QRect r; for (int i = 0; i < created.size(); ++i) { r = r.united(createdLW.visualRect(createdLW.model()->index(0, i))); } createdLW.setMinimumWidth(qMin(1024, r.width() + 4 * createdLW.frameWidth())); errorsLW.addItems(errors); vlay.addWidget(&createdLB); vlay.addWidget(&createdLW, 1); vlay.addWidget(&errorsLB); vlay.addWidget(&errorsLW, 1); vlay.addWidget(&buttonBox); if (created.empty()) { createdLW.hide(); } if (errors.empty()) { errorsLW.hide(); } connect(&buttonBox, &QDialogButtonBox::accepted, this, &ResultDialog::accept); connect(&buttonBox, &QDialogButtonBox::rejected, this, &ResultDialog::reject); readConfig(); } ~ResultDialog() override { writeConfig(); } void readConfig() { KConfigGroup dialog(KSharedConfig::openStateConfig(), "ResultDialog"); const QSize size = dialog.readEntry("Size", QSize(600, 400)); if (size.isValid()) { resize(size); } } void writeConfig() { KConfigGroup dialog(KSharedConfig::openStateConfig(), "ResultDialog"); dialog.writeEntry("Size", size()); dialog.sync(); } private: QLabel createdLB; QListWidget createdLW; QLabel errorsLB; QListWidget errorsLW; QDialogButtonBox buttonBox; QVBoxLayout vlay; }; } -#ifdef Q_OS_UNIX -static const bool HAVE_UNIX = true; -#else -static const bool HAVE_UNIX = false; -#endif - -static const Qt::CaseSensitivity fs_cs = HAVE_UNIX ? Qt::CaseSensitive : Qt::CaseInsensitive; // can we use QAbstractFileEngine::caseSensitive()? - static QStringList fs_sort(QStringList l) { std::sort(l.begin(), l.end(), [](const QString &lhs, const QString &rhs) { return QString::compare(lhs, rhs, fs_cs) < 0; }); return l; } static QStringList fs_intersect(QStringList l1, QStringList l2) { fs_sort(l1); fs_sort(l2); QStringList result; std::set_intersection(l1.begin(), l1.end(), l2.begin(), l2.end(), std::back_inserter(result), [](const QString &lhs, const QString &rhs) { return QString::compare(lhs, rhs, fs_cs) < 0; }); return result; } -static QList get_patterns(const std::vector< std::shared_ptr > &checksumDefinitions) -{ - QList result; - for (const std::shared_ptr &cd : checksumDefinitions) - if (cd) { - const auto patterns = cd->patterns(); - for (const QString &pattern : patterns) { - result.push_back(QRegExp(pattern, fs_cs)); - } - } - return result; -} - -namespace -{ - -struct matches_any : std::unary_function { - const QList m_regexps; - explicit matches_any(const QList ®exps) : m_regexps(regexps) {} - bool operator()(const QString &s) const - { - return std::any_of(m_regexps.cbegin(), m_regexps.cend(), - [s](const QRegExp &rx) { return rx.exactMatch(s); }); - } -}; -} - class CreateChecksumsController::Private : public QThread { Q_OBJECT friend class ::Kleo::Crypto::CreateChecksumsController; CreateChecksumsController *const q; public: explicit Private(CreateChecksumsController *qq); ~Private() override; Q_SIGNALS: void progress(int, int, const QString &); private: void slotOperationFinished() { #ifndef QT_NO_PROGRESSDIALOG if (progressDialog) { progressDialog->setValue(progressDialog->maximum()); progressDialog->close(); } #endif // QT_NO_PROGRESSDIALOG auto const dlg = new ResultDialog(created, errors); dlg->setAttribute(Qt::WA_DeleteOnClose); q->bringToForeground(dlg); if (!errors.empty()) q->setLastError(gpg_error(GPG_ERR_GENERAL), errors.join(QLatin1Char('\n'))); q->emitDoneOrError(); } void slotProgress(int current, int total, const QString &what) { qCDebug(KLEOPATRA_LOG) << "progress: " << current << "/" << total << ": " << qPrintable(what); #ifndef QT_NO_PROGRESSDIALOG if (!progressDialog) { return; } progressDialog->setMaximum(total); progressDialog->setValue(current); progressDialog->setLabelText(what); #endif // QT_NO_PROGRESSDIALOG } private: void run() override; private: #ifndef QT_NO_PROGRESSDIALOG QPointer progressDialog; #endif mutable QMutex mutex; const std::vector< std::shared_ptr > checksumDefinitions; std::shared_ptr checksumDefinition; QStringList files; QStringList errors, created; bool allowAddition; volatile bool canceled; }; CreateChecksumsController::Private::Private(CreateChecksumsController *qq) : q(qq), #ifndef QT_NO_PROGRESSDIALOG progressDialog(), #endif mutex(), checksumDefinitions(ChecksumDefinition::getChecksumDefinitions()), checksumDefinition(ChecksumDefinition::getDefaultChecksumDefinition(checksumDefinitions)), files(), errors(), created(), allowAddition(false), canceled(false) { connect(this, SIGNAL(progress(int,int,QString)), q, SLOT(slotProgress(int,int,QString))); connect(this, &Private::progress, q, &Controller::progress); connect(this, SIGNAL(finished()), q, SLOT(slotOperationFinished())); } CreateChecksumsController::Private::~Private() { qCDebug(KLEOPATRA_LOG); } CreateChecksumsController::CreateChecksumsController(QObject *p) : Controller(p), d(new Private(this)) { } CreateChecksumsController::CreateChecksumsController(const std::shared_ptr &ctx, QObject *p) : Controller(ctx, p), d(new Private(this)) { } CreateChecksumsController::~CreateChecksumsController() { qCDebug(KLEOPATRA_LOG); } void CreateChecksumsController::setFiles(const QStringList &files) { kleo_assert(!d->isRunning()); kleo_assert(!files.empty()); const QList patterns = get_patterns(d->checksumDefinitions); if (!std::all_of(files.cbegin(), files.cend(), matches_any(patterns)) && !std::none_of(files.cbegin(), files.cend(), matches_any(patterns))) { throw Exception(gpg_error(GPG_ERR_INV_ARG), i18n("Create Checksums: input files must be either all checksum files or all files to be checksummed, not a mixture of both.")); } const QMutexLocker locker(&d->mutex); d->files = files; } void CreateChecksumsController::setAllowAddition(bool allow) { kleo_assert(!d->isRunning()); const QMutexLocker locker(&d->mutex); d->allowAddition = allow; } bool CreateChecksumsController::allowAddition() const { const QMutexLocker locker(&d->mutex); return d->allowAddition; } void CreateChecksumsController::start() { { const QMutexLocker locker(&d->mutex); #ifndef QT_NO_PROGRESSDIALOG d->progressDialog = new QProgressDialog(i18n("Initializing..."), i18n("Cancel"), 0, 0); applyWindowID(d->progressDialog); d->progressDialog->setAttribute(Qt::WA_DeleteOnClose); d->progressDialog->setMinimumDuration(1000); d->progressDialog->setWindowTitle(i18nc("@title:window", "Create Checksum Progress")); connect(d->progressDialog.data(), &QProgressDialog::canceled, this, &CreateChecksumsController::cancel); #endif // QT_NO_PROGRESSDIALOG d->canceled = false; d->errors.clear(); d->created.clear(); } d->start(); } void CreateChecksumsController::cancel() { qCDebug(KLEOPATRA_LOG); const QMutexLocker locker(&d->mutex); d->canceled = true; } namespace { struct Dir { QDir dir; QString sumFile; QStringList inputFiles; quint64 totalSize; std::shared_ptr checksumDefinition; }; } static QStringList remove_checksum_files(QStringList l, const QList &rxs) { QStringList::iterator end = l.end(); for (const QRegExp &rx : rxs) { end = std::remove_if(l.begin(), end, [rx](const QString &str) { return rx.exactMatch(str); }); } l.erase(end, l.end()); return l; } -namespace -{ -struct File { - QString name; - QByteArray checksum; - bool binary; -}; -} - -static QString decode(const QString &encoded) -{ - QString decoded; - decoded.reserve(encoded.size()); - bool shift = false; - for (QChar ch : encoded) - if (shift) { - switch (ch.toLatin1()) { - case '\\': decoded += QLatin1Char('\\'); break; - case 'n': decoded += QLatin1Char('\n'); break; - default: - qCDebug(KLEOPATRA_LOG) << "invalid escape sequence" << '\\' << ch << "(interpreted as '" << ch << "')"; - decoded += ch; - break; - } - shift = false; - } else { - if (ch == QLatin1Char('\\')) { - shift = true; - } else { - decoded += ch; - } - } - return decoded; -} - -static std::vector parse_sum_file(const QString &fileName) -{ - std::vector files; - QFile f(fileName); - if (f.open(QIODevice::ReadOnly)) { - QTextStream s(&f); - QRegExp rx(QLatin1String("(\\?)([a-f0-9A-F]+) ([ *])([^\n]+)\n*")); - while (!s.atEnd()) { - const QString line = s.readLine(); - if (rx.exactMatch(line)) { - Q_ASSERT(!rx.cap(4).endsWith(QLatin1Char('\n'))); - const File file = { - rx.cap(1) == QLatin1String("\\") ? decode(rx.cap(4)) : rx.cap(4), - rx.cap(2).toLatin1(), - rx.cap(3) == QLatin1String("*"), - }; - files.push_back(file); - } - } - } - return files; -} - static quint64 aggregate_size(const QDir &dir, const QStringList &files) { quint64 n = 0; for (const QString &file : files) { n += QFileInfo(dir.absoluteFilePath(file)).size(); } return n; } -static std::shared_ptr filename2definition(const QString &fileName, - const std::vector< std::shared_ptr > &checksumDefinitions) -{ - for (const std::shared_ptr &cd : checksumDefinitions) { - if (cd) { - const auto patterns = cd->patterns(); - for (const QString &pattern : patterns) { - if (QRegExp(pattern, fs_cs).exactMatch(fileName)) { - return cd; - } - } - } - } - return std::shared_ptr(); -} - static std::vector find_dirs_by_sum_files(const QStringList &files, bool allowAddition, const std::function &progress, const std::vector< std::shared_ptr > &checksumDefinitions) { const QList patterns = get_patterns(checksumDefinitions); std::vector dirs; dirs.reserve(files.size()); int i = 0; for (const QString &file : files) { const QFileInfo fi(file); const QDir dir = fi.dir(); const QStringList entries = remove_checksum_files(dir.entryList(QDir::Files), patterns); QStringList inputFiles; if (allowAddition) { inputFiles = entries; } else { const std::vector parsed = parse_sum_file(fi.absoluteFilePath()); QStringList oldInputFiles; oldInputFiles.reserve(parsed.size()); std::transform(parsed.cbegin(), parsed.cend(), std::back_inserter(oldInputFiles), std::mem_fn(&File::name)); inputFiles = fs_intersect(oldInputFiles, entries); } const Dir item = { dir, fi.fileName(), inputFiles, aggregate_size(dir, inputFiles), filename2definition(fi.fileName(), checksumDefinitions) }; dirs.push_back(item); if (progress) { progress(++i); } } return dirs; } namespace { struct less_dir : std::binary_function { bool operator()(const QDir &lhs, const QDir &rhs) const { return QString::compare(lhs.absolutePath(), rhs.absolutePath(), fs_cs) < 0; } }; } static std::vector find_dirs_by_input_files(const QStringList &files, const std::shared_ptr &checksumDefinition, bool allowAddition, const std::function &progress, const std::vector< std::shared_ptr > &checksumDefinitions) { Q_UNUSED(allowAddition) if (!checksumDefinition) { return std::vector(); } const QList patterns = get_patterns(checksumDefinitions); std::map dirs2files; // Step 1: sort files by the dir they're contained in: std::deque inputs(files.begin(), files.end()); int i = 0; while (!inputs.empty()) { const QString file = inputs.front(); inputs.pop_front(); const QFileInfo fi(file); if (fi.isDir()) { QDir dir(file); dirs2files[ dir ] = remove_checksum_files(dir.entryList(QDir::Files), patterns); const auto entryList = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); std::transform(entryList.cbegin(), entryList.cend(), std::inserter(inputs, inputs.begin()), [&dir](const QString &entry) { return dir.absoluteFilePath(entry); }); } else { dirs2files[fi.dir()].push_back(file); } if (progress) { progress(++i); } } // Step 2: convert into vector: std::vector dirs; dirs.reserve(dirs2files.size()); for (auto it = dirs2files.begin(), end = dirs2files.end(); it != end; ++it) { const QStringList inputFiles = remove_checksum_files(it->second, patterns); if (inputFiles.empty()) { continue; } const Dir dir = { it->first, checksumDefinition->outputFileName(), inputFiles, aggregate_size(it->first, inputFiles), checksumDefinition }; dirs.push_back(dir); if (progress) { progress(++i); } } return dirs; } static QString process(const Dir &dir, bool *fatal) { const QString absFilePath = dir.dir.absoluteFilePath(dir.sumFile); QTemporaryFile out; QProcess p; if (!out.open()) { return QStringLiteral("Failed to open Temporary file."); } p.setWorkingDirectory(dir.dir.absolutePath()); p.setStandardOutputFile(out.fileName()); const QString program = dir.checksumDefinition->createCommand(); dir.checksumDefinition->startCreateCommand(&p, dir.inputFiles); p.waitForFinished(); qCDebug(KLEOPATRA_LOG) << "[" << &p << "] Exit code " << p.exitCode(); if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0) { if (fatal && p.error() == QProcess::FailedToStart) { *fatal = true; } if (p.error() == QProcess::UnknownError) return i18n("Error while running %1: %2", program, QString::fromLocal8Bit(p.readAllStandardError().trimmed().constData())); else { return i18n("Failed to execute %1: %2", program, p.errorString()); } } QFileInfo fi(absFilePath); if (!(fi.exists() && !QFile::remove(absFilePath)) && QFile::copy(out.fileName(), absFilePath)) { return QString(); } return xi18n("Failed to overwrite %1.", dir.sumFile); } namespace { static QDebug operator<<(QDebug s, const Dir &dir) { return s << "Dir(" << dir.dir << "->" << dir.sumFile << "<-(" << dir.totalSize << ')' << dir.inputFiles << ")\n"; } } void CreateChecksumsController::Private::run() { QMutexLocker locker(&mutex); const QStringList files = this->files; const std::vector< std::shared_ptr > checksumDefinitions = this->checksumDefinitions; const std::shared_ptr checksumDefinition = this->checksumDefinition; const bool allowAddition = this->allowAddition; locker.unlock(); QStringList errors; QStringList created; if (!checksumDefinition) { errors.push_back(i18n("No checksum programs defined.")); locker.relock(); this->errors = errors; return; } else { qCDebug(KLEOPATRA_LOG) << "using checksum-definition" << checksumDefinition->id(); } // // Step 1: build a list of work to do (no progress): // const QString scanning = i18n("Scanning directories..."); Q_EMIT progress(0, 0, scanning); const bool haveSumFiles = std::all_of(files.cbegin(), files.cend(), matches_any(get_patterns(checksumDefinitions))); const auto progressCb = [this, &scanning](int c) { Q_EMIT progress(c, 0, scanning); }; const std::vector dirs = haveSumFiles ? find_dirs_by_sum_files(files, allowAddition, progressCb, checksumDefinitions) : find_dirs_by_input_files(files, checksumDefinition, allowAddition, progressCb, checksumDefinitions); for (const Dir &dir : dirs) { qCDebug(KLEOPATRA_LOG) << dir; } if (!canceled) { Q_EMIT progress(0, 0, i18n("Calculating total size...")); const quint64 total = kdtools::accumulate_transform(dirs.cbegin(), dirs.cend(), std::mem_fn(&Dir::totalSize), Q_UINT64_C(0)); if (!canceled) { // // Step 2: perform work (with progress reporting): // // re-scale 'total' to fit into ints (wish QProgressDialog would use quint64...) const quint64 factor = total / std::numeric_limits::max() + 1; quint64 done = 0; for (const Dir &dir : dirs) { Q_EMIT progress(done / factor, total / factor, i18n("Checksumming (%2) in %1", dir.checksumDefinition->label(), dir.dir.path())); bool fatal = false; const QString error = process(dir, &fatal); if (!error.isEmpty()) { errors.push_back(error); } else { created.push_back(dir.dir.absoluteFilePath(dir.sumFile)); } done += dir.totalSize; if (fatal || canceled) { break; } } Q_EMIT progress(done / factor, total / factor, i18n("Done.")); } } locker.relock(); this->errors = errors; this->created = created; // mutex unlocked by QMutexLocker } #include "moc_createchecksumscontroller.cpp" #include "createchecksumscontroller.moc"