File indexing completed on 2024-06-16 04:56:00

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     crypto/verifychecksumscontroller.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 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 "verifychecksumscontroller.h"
0013 
0014 #include "checksumsutils_p.h"
0015 
0016 #ifndef QT_NO_DIRMODEL
0017 
0018 #include <crypto/gui/verifychecksumsdialog.h>
0019 
0020 #include <utils/input.h>
0021 #include <utils/kleo_assert.h>
0022 #include <utils/output.h>
0023 
0024 #include <Libkleo/ChecksumDefinition>
0025 #include <Libkleo/Classify>
0026 #include <Libkleo/Stl_Util>
0027 
0028 #include <KLocalizedString>
0029 
0030 #include <QDir>
0031 #include <QFileInfo>
0032 #include <QMutex>
0033 #include <QPointer>
0034 #include <QProcess>
0035 #include <QProgressDialog>
0036 #include <QThread>
0037 
0038 #include <gpg-error.h>
0039 
0040 #include <deque>
0041 #include <limits>
0042 #include <set>
0043 
0044 using namespace Kleo;
0045 using namespace Kleo::Crypto;
0046 using namespace Kleo::Crypto::Gui;
0047 
0048 #if 0
0049 static QStringList fs_sort(QStringList l)
0050 {
0051     int (*QString_compare)(const QString &, const QString &, Qt::CaseSensitivity) = &QString::compare;
0052     std::sort(l.begin(), l.end(),
0053               [](const QString &lhs, const QString &rhs) {
0054                 return QString::compare(lhs, rhs, ChecksumsUtils::fs_cs) < 0;
0055               });
0056     return l;
0057 }
0058 
0059 static QStringList fs_intersect(QStringList l1, QStringList l2)
0060 {
0061     int (*QString_compare)(const QString &, const QString &, Qt::CaseSensitivity) = &QString::compare;
0062     fs_sort(l1);
0063     fs_sort(l2);
0064     QStringList result;
0065     std::set_intersection(l1.begin(), l1.end(),
0066                           l2.begin(), l2.end(),
0067                           std::back_inserter(result),
0068                           [](const QString &lhs, const QString &rhs) {
0069                             return QString::compare(lhs, rhs, ChecksumsUtils::fs_cs) < 0;
0070                           });
0071     return result;
0072 }
0073 #endif
0074 
0075 namespace
0076 {
0077 struct matches_none_of {
0078     const std::vector<QRegularExpression> m_regexps;
0079     explicit matches_none_of(const std::vector<QRegularExpression> &regexps)
0080         : m_regexps(regexps)
0081     {
0082     }
0083     bool operator()(const QString &s) const
0084     {
0085         return std::none_of(m_regexps.cbegin(), m_regexps.cend(), [&s](const QRegularExpression &rx) {
0086             return rx.match(s).hasMatch();
0087         });
0088     }
0089 };
0090 }
0091 
0092 class VerifyChecksumsController::Private : public QThread
0093 {
0094     Q_OBJECT
0095     friend class ::Kleo::Crypto::VerifyChecksumsController;
0096     VerifyChecksumsController *const q;
0097 
0098 public:
0099     explicit Private(VerifyChecksumsController *qq);
0100     ~Private() override;
0101 
0102 Q_SIGNALS:
0103     void baseDirectories(const QStringList &);
0104     void progress(int, int, const QString &);
0105     void status(const QString &file, Kleo::Crypto::Gui::VerifyChecksumsDialog::Status);
0106 
0107 private:
0108     void slotOperationFinished()
0109     {
0110         if (dialog) {
0111             dialog->setProgress(100, 100);
0112             dialog->setErrors(errors);
0113         }
0114 
0115         if (!errors.empty())
0116             q->setLastError(gpg_error(GPG_ERR_GENERAL), errors.join(QLatin1Char('\n')));
0117         q->emitDoneOrError();
0118     }
0119 
0120 private:
0121     void run() override;
0122 
0123 private:
0124     QPointer<VerifyChecksumsDialog> dialog;
0125     mutable QMutex mutex;
0126     const std::vector<std::shared_ptr<ChecksumDefinition>> checksumDefinitions;
0127     QStringList files;
0128     QStringList errors;
0129     volatile bool canceled;
0130 };
0131 
0132 VerifyChecksumsController::Private::Private(VerifyChecksumsController *qq)
0133     : q(qq)
0134     , dialog()
0135     , mutex()
0136     , checksumDefinitions(ChecksumDefinition::getChecksumDefinitions())
0137     , files()
0138     , errors()
0139     , canceled(false)
0140 {
0141     connect(this, &Private::progress, q, &Controller::progress);
0142     connect(this, SIGNAL(finished()), q, SLOT(slotOperationFinished()));
0143 }
0144 
0145 VerifyChecksumsController::Private::~Private()
0146 {
0147     qCDebug(KLEOPATRA_LOG);
0148 }
0149 
0150 VerifyChecksumsController::VerifyChecksumsController(QObject *p)
0151     : Controller(p)
0152     , d(new Private(this))
0153 {
0154 }
0155 
0156 VerifyChecksumsController::VerifyChecksumsController(const std::shared_ptr<const ExecutionContext> &ctx, QObject *p)
0157     : Controller(ctx, p)
0158     , d(new Private(this))
0159 {
0160 }
0161 
0162 VerifyChecksumsController::~VerifyChecksumsController()
0163 {
0164     qCDebug(KLEOPATRA_LOG);
0165 }
0166 
0167 void VerifyChecksumsController::setFiles(const QStringList &files)
0168 {
0169     kleo_assert(!d->isRunning());
0170     kleo_assert(!files.empty());
0171     const QMutexLocker locker(&d->mutex);
0172     d->files = files;
0173 }
0174 
0175 void VerifyChecksumsController::start()
0176 {
0177     {
0178         const QMutexLocker locker(&d->mutex);
0179 
0180         d->dialog = new VerifyChecksumsDialog;
0181         d->dialog->setAttribute(Qt::WA_DeleteOnClose);
0182         d->dialog->setWindowTitle(i18nc("@title:window", "Verify Checksum Results"));
0183 
0184         connect(d->dialog.data(), &VerifyChecksumsDialog::canceled, this, &VerifyChecksumsController::cancel);
0185         connect(d.get(), &Private::baseDirectories, d->dialog.data(), &VerifyChecksumsDialog::setBaseDirectories);
0186         connect(d.get(), &Private::progress, d->dialog.data(), &VerifyChecksumsDialog::setProgress);
0187         connect(d.get(), &Private::status, d->dialog.data(), &VerifyChecksumsDialog::setStatus);
0188 
0189         d->canceled = false;
0190         d->errors.clear();
0191     }
0192 
0193     d->start();
0194 
0195     d->dialog->show();
0196 }
0197 
0198 void VerifyChecksumsController::cancel()
0199 {
0200     qCDebug(KLEOPATRA_LOG);
0201     const QMutexLocker locker(&d->mutex);
0202     d->canceled = true;
0203 }
0204 
0205 namespace
0206 {
0207 
0208 struct SumFile {
0209     QDir dir;
0210     QString sumFile;
0211     quint64 totalSize;
0212     std::shared_ptr<ChecksumDefinition> checksumDefinition;
0213 };
0214 
0215 }
0216 
0217 static QStringList filter_checksum_files(QStringList l, const std::vector<QRegularExpression> &rxs)
0218 {
0219     l.erase(std::remove_if(l.begin(), l.end(), matches_none_of(rxs)), l.end());
0220     return l;
0221 }
0222 
0223 static quint64 aggregate_size(const QDir &dir, const QStringList &files)
0224 {
0225     quint64 n = 0;
0226     for (const QString &file : files) {
0227         n += QFileInfo(dir.absoluteFilePath(file)).size();
0228     }
0229     return n;
0230 }
0231 
0232 namespace
0233 {
0234 struct less_dir {
0235     bool operator()(const QDir &lhs, const QDir &rhs) const
0236     {
0237         return QString::compare(lhs.absolutePath(), rhs.absolutePath(), ChecksumsUtils::fs_cs) < 0;
0238     }
0239 };
0240 struct less_file {
0241     bool operator()(const QString &lhs, const QString &rhs) const
0242     {
0243         return QString::compare(lhs, rhs, ChecksumsUtils::fs_cs) < 0;
0244     }
0245 };
0246 struct sumfile_contains_file {
0247     const QDir dir;
0248     const QString fileName;
0249     sumfile_contains_file(const QDir &dir_, const QString &fileName_)
0250         : dir(dir_)
0251         , fileName(fileName_)
0252     {
0253     }
0254     bool operator()(const QString &sumFile) const
0255     {
0256         const std::vector<ChecksumsUtils::File> files = ChecksumsUtils::parse_sum_file(dir.absoluteFilePath(sumFile));
0257         qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:      found " << files.size() << " files listed in " << qPrintable(dir.absoluteFilePath(sumFile));
0258         for (const ChecksumsUtils::File &file : files) {
0259             const bool isSameFileName = (QString::compare(file.name, fileName, ChecksumsUtils::fs_cs) == 0);
0260             qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:        " << qPrintable(file.name) << " == " << qPrintable(fileName) << " ? " << isSameFileName;
0261             if (isSameFileName) {
0262                 return true;
0263             }
0264         }
0265         return false;
0266     }
0267 };
0268 }
0269 
0270 // IF is_dir(file)
0271 //   add all sumfiles \in dir(file)
0272 //   inputs.prepend( all dirs \in dir(file) )
0273 // ELSE IF is_sum_file(file)
0274 //   add
0275 // ELSE IF \exists sumfile in dir(file) \where sumfile \contains file
0276 //   add sumfile
0277 // ELSE
0278 //   error: no checksum found for "file"
0279 
0280 static QStringList find_base_directories(const QStringList &files)
0281 {
0282     // Step 1: find base dirs:
0283 
0284     std::set<QDir, less_dir> dirs;
0285     for (const QString &file : files) {
0286         const QFileInfo fi(file);
0287         const QDir dir = fi.isDir() ? QDir(file) : fi.dir();
0288         dirs.insert(dir);
0289     }
0290 
0291     // Step 1a: collapse direct child directories
0292 
0293     bool changed;
0294     do {
0295         changed = false;
0296         auto it = dirs.begin();
0297         while (it != dirs.end()) {
0298             QDir dir = *it;
0299             if (dir.cdUp() && dirs.count(dir)) {
0300                 dirs.erase(it++);
0301                 changed = true;
0302             } else {
0303                 ++it;
0304             }
0305         }
0306     } while (changed);
0307 
0308     QStringList rv;
0309     rv.reserve(dirs.size());
0310     std::transform(dirs.cbegin(), dirs.cend(), std::back_inserter(rv), std::mem_fn(&QDir::absolutePath));
0311     return rv;
0312 }
0313 
0314 static std::vector<SumFile> find_sums_by_input_files(const QStringList &files,
0315                                                      QStringList &errors,
0316                                                      const std::function<void(int)> &progress,
0317                                                      const std::vector<std::shared_ptr<ChecksumDefinition>> &checksumDefinitions)
0318 {
0319     const std::vector<QRegularExpression> patterns = ChecksumsUtils::get_patterns(checksumDefinitions);
0320 
0321     const ChecksumsUtils::matches_any is_sum_file(patterns);
0322 
0323     std::map<QDir, std::set<QString, less_file>, less_dir> dirs2sums;
0324 
0325     // Step 1: find the sumfiles we need to check:
0326 
0327     std::deque<QString> inputs(files.begin(), files.end());
0328 
0329     int i = 0;
0330     while (!inputs.empty()) {
0331         const QString file = inputs.front();
0332         qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: considering " << qPrintable(file);
0333         inputs.pop_front();
0334         const QFileInfo fi(file);
0335         const QString fileName = fi.fileName();
0336         if (fi.isDir()) {
0337             qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:   it's a directory";
0338             QDir dir(file);
0339             const QStringList sumfiles = filter_checksum_files(dir.entryList(QDir::Files), patterns);
0340             qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:   found " << sumfiles.size()
0341                                    << " sum files: " << qPrintable(sumfiles.join(QLatin1StringView(", ")));
0342             dirs2sums[dir].insert(sumfiles.begin(), sumfiles.end());
0343             const QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0344             qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:   found " << dirs.size() << " subdirs, prepending";
0345             std::transform(dirs.cbegin(), dirs.cend(), std::inserter(inputs, inputs.begin()), [&dir](const QString &path) {
0346                 return dir.absoluteFilePath(path);
0347             });
0348         } else if (is_sum_file(fileName)) {
0349             qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:   it's a sum file";
0350             dirs2sums[fi.dir()].insert(fileName);
0351         } else {
0352             qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:   it's something else; checking whether we'll find a sumfile for it...";
0353             const QDir dir = fi.dir();
0354             const QStringList sumfiles = filter_checksum_files(dir.entryList(QDir::Files), patterns);
0355             qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files:   found " << sumfiles.size()
0356                                    << " potential sumfiles: " << qPrintable(sumfiles.join(QLatin1StringView(", ")));
0357             const auto it = std::find_if(sumfiles.cbegin(), sumfiles.cend(), sumfile_contains_file(dir, fileName));
0358             if (it == sumfiles.end()) {
0359                 errors.push_back(i18n("Cannot find checksums file for file %1", file));
0360             } else {
0361                 dirs2sums[dir].insert(*it);
0362             }
0363         }
0364         if (progress) {
0365             progress(++i);
0366         }
0367     }
0368 
0369     // Step 2: convert into vector<SumFile>:
0370 
0371     std::vector<SumFile> sumfiles;
0372     sumfiles.reserve(dirs2sums.size());
0373 
0374     for (auto it = dirs2sums.begin(), end = dirs2sums.end(); it != end; ++it) {
0375         if (it->second.empty()) {
0376             continue;
0377         }
0378 
0379         const QDir &dir = it->first;
0380 
0381         for (const QString &sumFileName : std::as_const(it->second)) {
0382             const std::vector<ChecksumsUtils::File> summedfiles = ChecksumsUtils::parse_sum_file(dir.absoluteFilePath(sumFileName));
0383             QStringList files;
0384             files.reserve(summedfiles.size());
0385             std::transform(summedfiles.cbegin(), summedfiles.cend(), std::back_inserter(files), std::mem_fn(&ChecksumsUtils::File::name));
0386             const SumFile sumFile = {
0387                 it->first,
0388                 sumFileName,
0389                 aggregate_size(it->first, files),
0390                 ChecksumsUtils::filename2definition(sumFileName, checksumDefinitions),
0391             };
0392             sumfiles.push_back(sumFile);
0393         }
0394 
0395         if (progress) {
0396             progress(++i);
0397         }
0398     }
0399     return sumfiles;
0400 }
0401 
0402 static QStringList c_lang_environment()
0403 {
0404     static const QRegularExpression re(QRegularExpression::anchoredPattern(u"LANG=.*"), ChecksumsUtils::s_regex_cs);
0405     QStringList env = QProcess::systemEnvironment();
0406     env.erase(std::remove_if(env.begin(),
0407                              env.end(),
0408                              [](const QString &str) {
0409                                  return re.match(str).hasMatch();
0410                              }),
0411               env.end());
0412     env.push_back(QStringLiteral("LANG=C"));
0413     return env;
0414 }
0415 
0416 static const struct {
0417     const char *string;
0418     VerifyChecksumsDialog::Status status;
0419 } statusStrings[] = {
0420     {"OK", VerifyChecksumsDialog::OK},
0421     {"FAILED", VerifyChecksumsDialog::Failed},
0422 };
0423 static const size_t numStatusStrings = sizeof statusStrings / sizeof *statusStrings;
0424 
0425 static VerifyChecksumsDialog::Status string2status(const QByteArray &str)
0426 {
0427     for (unsigned int i = 0; i < numStatusStrings; ++i)
0428         if (str == statusStrings[i].string) {
0429             return statusStrings[i].status;
0430         }
0431     return VerifyChecksumsDialog::Unknown;
0432 }
0433 
0434 static QString
0435 process(const SumFile &sumFile, bool *fatal, const QStringList &env, const std::function<void(const QString &, VerifyChecksumsDialog::Status)> &status)
0436 {
0437     QProcess p;
0438     p.setEnvironment(env);
0439     p.setWorkingDirectory(sumFile.dir.absolutePath());
0440     p.setReadChannel(QProcess::StandardOutput);
0441 
0442     const QString absFilePath = sumFile.dir.absoluteFilePath(sumFile.sumFile);
0443 
0444     const QString program = sumFile.checksumDefinition->verifyCommand();
0445     sumFile.checksumDefinition->startVerifyCommand(&p, QStringList(absFilePath));
0446 
0447     QByteArray remainder; // used for filenames with newlines in them
0448     while (p.state() != QProcess::NotRunning) {
0449         p.waitForReadyRead();
0450         while (p.canReadLine()) {
0451             const QByteArray line = p.readLine();
0452             const int colonIdx = line.lastIndexOf(':');
0453             if (colonIdx < 0) {
0454                 remainder += line; // no colon -> probably filename with a newline
0455                 continue;
0456             }
0457 #ifdef Q_OS_WIN
0458             const QString file = QString::fromUtf8(remainder + line.left(colonIdx));
0459 #else
0460             const QString file = QFile::decodeName(remainder + line.left(colonIdx));
0461 #endif
0462             remainder.clear();
0463             const VerifyChecksumsDialog::Status result = string2status(line.mid(colonIdx + 1).trimmed());
0464             status(sumFile.dir.absoluteFilePath(file), result);
0465         }
0466     }
0467     qCDebug(KLEOPATRA_LOG) << "[" << &p << "] Exit code " << p.exitCode();
0468 
0469     if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0) {
0470         if (fatal && p.error() == QProcess::FailedToStart) {
0471             *fatal = true;
0472         }
0473         if (p.error() == QProcess::UnknownError)
0474             return i18n("Error while running %1: %2", program, QString::fromLocal8Bit(p.readAllStandardError().trimmed().constData()));
0475         else {
0476             return i18n("Failed to execute %1: %2", program, p.errorString());
0477         }
0478     }
0479 
0480     return QString();
0481 }
0482 
0483 namespace
0484 {
0485 static QDebug operator<<(QDebug s, const SumFile &sum)
0486 {
0487     return s << "SumFile(" << sum.dir << "->" << sum.sumFile << "<-(" << sum.totalSize << ')' << ")\n";
0488 }
0489 }
0490 
0491 void VerifyChecksumsController::Private::run()
0492 {
0493     QMutexLocker locker(&mutex);
0494 
0495     const QStringList files = this->files;
0496     const std::vector<std::shared_ptr<ChecksumDefinition>> checksumDefinitions = this->checksumDefinitions;
0497 
0498     locker.unlock();
0499 
0500     QStringList errors;
0501 
0502     //
0503     // Step 0: find base directories:
0504     //
0505 
0506     Q_EMIT baseDirectories(find_base_directories(files));
0507 
0508     //
0509     // Step 1: build a list of work to do (no progress):
0510     //
0511 
0512     const QString scanning = i18n("Scanning directories...");
0513     Q_EMIT progress(0, 0, scanning);
0514 
0515     const auto progressCb = [this, scanning](int arg) {
0516         Q_EMIT progress(arg, 0, scanning);
0517     };
0518     const auto statusCb = [this](const QString &str, VerifyChecksumsDialog::Status st) {
0519         Q_EMIT status(str, st);
0520     };
0521 
0522     const std::vector<SumFile> sumfiles = find_sums_by_input_files(files, errors, progressCb, checksumDefinitions);
0523 
0524     for (const SumFile &sumfile : sumfiles) {
0525         qCDebug(KLEOPATRA_LOG) << sumfile;
0526     }
0527 
0528     if (!canceled) {
0529         Q_EMIT progress(0, 0, i18n("Calculating total size..."));
0530 
0531         const quint64 total = kdtools::accumulate_transform(sumfiles.cbegin(), sumfiles.cend(), std::mem_fn(&SumFile::totalSize), Q_UINT64_C(0));
0532 
0533         if (!canceled) {
0534             //
0535             // Step 2: perform work (with progress reporting):
0536             //
0537 
0538             const QStringList env = c_lang_environment();
0539 
0540             // re-scale 'total' to fit into ints (wish QProgressDialog would use quint64...)
0541             const quint64 factor = total / std::numeric_limits<int>::max() + 1;
0542 
0543             quint64 done = 0;
0544             for (const SumFile &sumFile : sumfiles) {
0545                 Q_EMIT progress(done / factor, total / factor, i18n("Verifying checksums (%2) in %1", sumFile.checksumDefinition->label(), sumFile.dir.path()));
0546                 bool fatal = false;
0547                 const QString error = process(sumFile, &fatal, env, statusCb);
0548                 if (!error.isEmpty()) {
0549                     errors.push_back(error);
0550                 }
0551                 done += sumFile.totalSize;
0552                 if (fatal || canceled) {
0553                     break;
0554                 }
0555             }
0556             Q_EMIT progress(done / factor, total / factor, i18n("Done."));
0557         }
0558     }
0559 
0560     locker.relock();
0561 
0562     this->errors = errors;
0563 
0564     // mutex unlocked by QMutexLocker
0565 }
0566 
0567 #include "moc_verifychecksumscontroller.cpp"
0568 #include "verifychecksumscontroller.moc"
0569 
0570 #endif // QT_NO_DIRMODEL