diff --git a/src/crypto/verifychecksumscontroller.cpp b/src/crypto/verifychecksumscontroller.cpp index 41c4f6229..530717673 100644 --- a/src/crypto/verifychecksumscontroller.cpp +++ b/src/crypto/verifychecksumscontroller.cpp @@ -1,689 +1,587 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/verifychecksumscontroller.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 "verifychecksumscontroller.h" +#include "checksumsutils_p.h" #ifndef QT_NO_DIRMODEL #include #include #include #include #include -#include #include #include -#include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Crypto; using namespace Kleo::Crypto::Gui; -#ifdef Q_OS_UNIX -static const bool HAVE_UNIX = true; -#else -static const bool HAVE_UNIX = false; -#endif - static const QLatin1String CHECKSUM_DEFINITION_ID_ENTRY("checksum-definition-id"); -static const Qt::CaseSensitivity fs_cs = HAVE_UNIX ? Qt::CaseSensitive : Qt::CaseInsensitive; // can we use QAbstractFileEngine::caseSensitive()? - #if 0 static QStringList fs_sort(QStringList l) { int (*QString_compare)(const QString &, const QString &, Qt::CaseSensitivity) = &QString::compare; 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) { int (*QString_compare)(const QString &, const QString &, Qt::CaseSensitivity) = &QString::compare; 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; } #endif -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); }); - } -}; struct matches_none_of : std::unary_function { const QList m_regexps; explicit matches_none_of(const QList ®exps) : m_regexps(regexps) {} bool operator()(const QString &s) const { return std::none_of(m_regexps.cbegin(), m_regexps.cend(), [&s](const QRegExp &rx) { return rx.exactMatch(s); }); } }; } class VerifyChecksumsController::Private : public QThread { Q_OBJECT friend class ::Kleo::Crypto::VerifyChecksumsController; VerifyChecksumsController *const q; public: explicit Private(VerifyChecksumsController *qq); ~Private() override; Q_SIGNALS: void baseDirectories(const QStringList &); void progress(int, int, const QString &); void status(const QString &file, Kleo::Crypto::Gui::VerifyChecksumsDialog::Status); private: void slotOperationFinished() { if (dialog) { dialog->setProgress(100, 100); dialog->setErrors(errors); } if (!errors.empty()) q->setLastError(gpg_error(GPG_ERR_GENERAL), errors.join(QLatin1Char('\n'))); q->emitDoneOrError(); } private: void run() override; private: QPointer dialog; mutable QMutex mutex; const std::vector< std::shared_ptr > checksumDefinitions; QStringList files; QStringList errors; volatile bool canceled; }; VerifyChecksumsController::Private::Private(VerifyChecksumsController *qq) : q(qq), dialog(), mutex(), checksumDefinitions(ChecksumDefinition::getChecksumDefinitions()), files(), errors(), canceled(false) { connect(this, &Private::progress, q, &Controller::progress); connect(this, SIGNAL(finished()), q, SLOT(slotOperationFinished())); } VerifyChecksumsController::Private::~Private() { qCDebug(KLEOPATRA_LOG); } VerifyChecksumsController::VerifyChecksumsController(QObject *p) : Controller(p), d(new Private(this)) { } VerifyChecksumsController::VerifyChecksumsController(const std::shared_ptr &ctx, QObject *p) : Controller(ctx, p), d(new Private(this)) { } VerifyChecksumsController::~VerifyChecksumsController() { qCDebug(KLEOPATRA_LOG); } void VerifyChecksumsController::setFiles(const QStringList &files) { kleo_assert(!d->isRunning()); kleo_assert(!files.empty()); const QMutexLocker locker(&d->mutex); d->files = files; } void VerifyChecksumsController::start() { { const QMutexLocker locker(&d->mutex); d->dialog = new VerifyChecksumsDialog; d->dialog->setAttribute(Qt::WA_DeleteOnClose); d->dialog->setWindowTitle(i18nc("@title:window", "Verify Checksum Results")); connect(d->dialog.data(), &VerifyChecksumsDialog::canceled, this, &VerifyChecksumsController::cancel); connect(d.get(), &Private::baseDirectories, d->dialog.data(), &VerifyChecksumsDialog::setBaseDirectories); connect(d.get(), &Private::progress, d->dialog.data(), &VerifyChecksumsDialog::setProgress); connect(d.get(), &Private::status, d->dialog.data(), &VerifyChecksumsDialog::setStatus); d->canceled = false; d->errors.clear(); } d->start(); d->dialog->show(); } void VerifyChecksumsController::cancel() { qCDebug(KLEOPATRA_LOG); const QMutexLocker locker(&d->mutex); d->canceled = true; } namespace { struct SumFile { QDir dir; QString sumFile; quint64 totalSize; std::shared_ptr checksumDefinition; }; } static QStringList filter_checksum_files(QStringList l, const QList &rxs) { l.erase(std::remove_if(l.begin(), l.end(), matches_none_of(rxs)), 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 (const 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(); -} - 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; } }; struct less_file : std::binary_function { bool operator()(const QString &lhs, const QString &rhs) const { return QString::compare(lhs, rhs, fs_cs) < 0; } }; struct sumfile_contains_file : std::unary_function { const QDir dir; const QString fileName; sumfile_contains_file(const QDir &dir_, const QString &fileName_) : dir(dir_), fileName(fileName_) {} bool operator()(const QString &sumFile) const { const std::vector files = parse_sum_file(dir.absoluteFilePath(sumFile)); qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << files.size() << " files listed in " << qPrintable(dir.absoluteFilePath(sumFile)); for (const File &file : files) { const bool isSameFileName = (QString::compare(file.name, fileName, fs_cs) == 0); qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: " << qPrintable(file.name) << " == " << qPrintable(fileName) << " ? " << isSameFileName; if (isSameFileName) { return true; } } return false; } }; } // IF is_dir(file) // add all sumfiles \in dir(file) // inputs.prepend( all dirs \in dir(file) ) // ELSE IF is_sum_file(file) // add // ELSE IF \exists sumfile in dir(file) \where sumfile \contains file // add sumfile // ELSE // error: no checksum found for "file" static QStringList find_base_directories(const QStringList &files) { // Step 1: find base dirs: std::set dirs; for (const QString &file : files) { const QFileInfo fi(file); const QDir dir = fi.isDir() ? QDir(file) : fi.dir(); dirs.insert(dir); } // Step 1a: collapse direct child directories bool changed; do { changed = false; auto it = dirs.begin(); while (it != dirs.end()) { QDir dir = *it; if (dir.cdUp() && dirs.count(dir)) { dirs.erase(it++); changed = true; } else { ++it; } } } while (changed); QStringList rv; rv.reserve(dirs.size()); std::transform(dirs.cbegin(), dirs.cend(), std::back_inserter(rv), std::mem_fn(&QDir::absolutePath)); return rv; } static std::vector find_sums_by_input_files(const QStringList &files, QStringList &errors, const std::function &progress, const std::vector< std::shared_ptr > &checksumDefinitions) { const QList patterns = get_patterns(checksumDefinitions); const matches_any is_sum_file(patterns); std::map, less_dir> dirs2sums; // Step 1: find the sumfiles we need to check: std::deque inputs(files.begin(), files.end()); int i = 0; while (!inputs.empty()) { const QString file = inputs.front(); qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: considering " << qPrintable(file); inputs.pop_front(); const QFileInfo fi(file); const QString fileName = fi.fileName(); if (fi.isDir()) { qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: it's a directory"; QDir dir(file); const QStringList sumfiles = filter_checksum_files(dir.entryList(QDir::Files), patterns); qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << sumfiles.size() << " sum files: " << qPrintable(sumfiles.join(QLatin1String(", "))); dirs2sums[ dir ].insert(sumfiles.begin(), sumfiles.end()); const QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << dirs.size() << " subdirs, prepending"; std::transform(dirs.cbegin(), dirs.cend(), std::inserter(inputs, inputs.begin()), [&dir](const QString &path) { return dir.absoluteFilePath(path); }); } else if (is_sum_file(fileName)) { qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: it's a sum file"; dirs2sums[fi.dir()].insert(fileName); } else { qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: it's something else; checking whether we'll find a sumfile for it..."; const QDir dir = fi.dir(); const QStringList sumfiles = filter_checksum_files(dir.entryList(QDir::Files), patterns); qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << sumfiles.size() << " potential sumfiles: " << qPrintable(sumfiles.join(QLatin1String(", "))); const auto it = std::find_if(sumfiles.cbegin(), sumfiles.cend(), sumfile_contains_file(dir, fileName)); if (it == sumfiles.end()) { errors.push_back(i18n("Cannot find checksums file for file %1", file)); } else { dirs2sums[dir].insert(*it); } } if (progress) { progress(++i); } } // Step 2: convert into vector: std::vector sumfiles; sumfiles.reserve(dirs2sums.size()); for (auto it = dirs2sums.begin(), end = dirs2sums.end(); it != end; ++it) { if (it->second.empty()) { continue; } const QDir &dir = it->first; for (const QString &sumFileName : std::as_const(it->second)) { const std::vector summedfiles = parse_sum_file(dir.absoluteFilePath(sumFileName)); QStringList files; files.reserve(summedfiles.size()); std::transform(summedfiles.cbegin(), summedfiles.cend(), std::back_inserter(files), std::mem_fn(&File::name)); const SumFile sumFile = { it->first, sumFileName, aggregate_size(it->first, files), filename2definition(sumFileName, checksumDefinitions), }; sumfiles.push_back(sumFile); } if (progress) { progress(++i); } } return sumfiles; } static QStringList c_lang_environment() { QStringList env = QProcess::systemEnvironment(); env.erase(std::remove_if(env.begin(), env.end(), [](const QString &str) { return QRegExp(QLatin1String("^LANG=.*"), fs_cs).exactMatch(str); }), env.end()); env.push_back(QStringLiteral("LANG=C")); return env; } static const struct { const char *string; VerifyChecksumsDialog::Status status; } statusStrings[] = { { "OK", VerifyChecksumsDialog::OK }, { "FAILED", VerifyChecksumsDialog::Failed }, }; static const size_t numStatusStrings = sizeof statusStrings / sizeof * statusStrings; static VerifyChecksumsDialog::Status string2status(const QByteArray &str) { for (unsigned int i = 0; i < numStatusStrings; ++i) if (str == statusStrings[i].string) { return statusStrings[i].status; } return VerifyChecksumsDialog::Unknown; } static QString process(const SumFile &sumFile, bool *fatal, const QStringList &env, const std::function &status) { QProcess p; p.setEnvironment(env); p.setWorkingDirectory(sumFile.dir.absolutePath()); p.setReadChannel(QProcess::StandardOutput); const QString absFilePath = sumFile.dir.absoluteFilePath(sumFile.sumFile); const QString program = sumFile.checksumDefinition->verifyCommand(); sumFile.checksumDefinition->startVerifyCommand(&p, QStringList(absFilePath)); QByteArray remainder; // used for filenames with newlines in them while (p.state() != QProcess::NotRunning) { p.waitForReadyRead(); while (p.canReadLine()) { const QByteArray line = p.readLine(); const int colonIdx = line.lastIndexOf(':'); if (colonIdx < 0) { remainder += line; // no colon -> probably filename with a newline continue; } const QString file = QFile::decodeName(remainder + line.left(colonIdx)); remainder.clear(); const VerifyChecksumsDialog::Status result = string2status(line.mid(colonIdx + 1).trimmed()); status(sumFile.dir.absoluteFilePath(file), result); } } 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()); } } return QString(); } namespace { static QDebug operator<<(QDebug s, const SumFile &sum) { return s << "SumFile(" << sum.dir << "->" << sum.sumFile << "<-(" << sum.totalSize << ')' << ")\n"; } } void VerifyChecksumsController::Private::run() { QMutexLocker locker(&mutex); const QStringList files = this->files; const std::vector< std::shared_ptr > checksumDefinitions = this->checksumDefinitions; locker.unlock(); QStringList errors; // // Step 0: find base directories: // Q_EMIT baseDirectories(find_base_directories(files)); // // Step 1: build a list of work to do (no progress): // const QString scanning = i18n("Scanning directories..."); Q_EMIT progress(0, 0, scanning); const auto progressCb = [this, scanning](int arg) { Q_EMIT progress(arg, 0, scanning); }; const auto statusCb = [this](const QString &str, VerifyChecksumsDialog::Status st) { Q_EMIT status(str, st); }; const std::vector sumfiles = find_sums_by_input_files(files, errors, progressCb, checksumDefinitions); for (const SumFile &sumfile : sumfiles) { qCDebug(KLEOPATRA_LOG) << sumfile; } if (!canceled) { Q_EMIT progress(0, 0, i18n("Calculating total size...")); const quint64 total = kdtools::accumulate_transform(sumfiles.cbegin(), sumfiles.cend(), std::mem_fn(&SumFile::totalSize), Q_UINT64_C(0)); if (!canceled) { // // Step 2: perform work (with progress reporting): // const QStringList env = c_lang_environment(); // 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 SumFile &sumFile : sumfiles) { Q_EMIT progress(done / factor, total / factor, i18n("Verifying checksums (%2) in %1", sumFile.checksumDefinition->label(), sumFile.dir.path())); bool fatal = false; const QString error = process(sumFile, &fatal, env, statusCb); if (!error.isEmpty()) { errors.push_back(error); } done += sumFile.totalSize; if (fatal || canceled) { break; } } Q_EMIT progress(done / factor, total / factor, i18n("Done.")); } } locker.relock(); this->errors = errors; // mutex unlocked by QMutexLocker } #include "moc_verifychecksumscontroller.cpp" #include "verifychecksumscontroller.moc" #endif // QT_NO_DIRMODEL