File indexing completed on 2024-06-16 04:55:55

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     crypto/createchecksumscontroller.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 "checksumsutils_p.h"
0013 #include "createchecksumscontroller.h"
0014 
0015 #include <utils/input.h>
0016 #include <utils/kleo_assert.h>
0017 #include <utils/output.h>
0018 
0019 #include <Libkleo/ChecksumDefinition>
0020 #include <Libkleo/Classify>
0021 #include <Libkleo/Stl_Util>
0022 
0023 #include <KConfigGroup>
0024 #include <KLocalizedString>
0025 #include <KSharedConfig>
0026 #include <QTemporaryFile>
0027 
0028 #include <QDialog>
0029 #include <QDialogButtonBox>
0030 #include <QLabel>
0031 #include <QListWidget>
0032 #include <QVBoxLayout>
0033 
0034 #include <QDir>
0035 #include <QFileInfo>
0036 #include <QMutex>
0037 #include <QPointer>
0038 #include <QProcess>
0039 #include <QProgressDialog>
0040 #include <QThread>
0041 
0042 #include <gpg-error.h>
0043 
0044 #include <deque>
0045 #include <functional>
0046 #include <limits>
0047 #include <map>
0048 
0049 using namespace Kleo;
0050 using namespace Kleo::Crypto;
0051 
0052 namespace
0053 {
0054 
0055 class ResultDialog : public QDialog
0056 {
0057     Q_OBJECT
0058 public:
0059     ResultDialog(const QStringList &created, const QStringList &errors, QWidget *parent = nullptr, Qt::WindowFlags f = {})
0060         : QDialog(parent, f)
0061         , createdLB(created.empty() ? i18nc("@info", "No checksum files have been created.")
0062                                     : i18nc("@info", "These checksum files have been successfully created:"),
0063                     this)
0064         , createdLW(this)
0065         , errorsLB(errors.empty() ? i18nc("@info", "There were no errors.") //
0066                                   : i18nc("@info", "The following errors were encountered:"),
0067                    this)
0068         , errorsLW(this)
0069         , buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, this)
0070         , vlay(this)
0071     {
0072         KDAB_SET_OBJECT_NAME(createdLB);
0073         KDAB_SET_OBJECT_NAME(createdLW);
0074         KDAB_SET_OBJECT_NAME(errorsLB);
0075         KDAB_SET_OBJECT_NAME(errorsLW);
0076         KDAB_SET_OBJECT_NAME(buttonBox);
0077         KDAB_SET_OBJECT_NAME(vlay);
0078 
0079         createdLW.addItems(created);
0080         QRect r;
0081         for (int i = 0; i < created.size(); ++i) {
0082             r = r.united(createdLW.visualRect(createdLW.model()->index(0, i)));
0083         }
0084         createdLW.setMinimumWidth(qMin(1024, r.width() + 4 * createdLW.frameWidth()));
0085 
0086         errorsLW.addItems(errors);
0087 
0088         vlay.addWidget(&createdLB);
0089         vlay.addWidget(&createdLW, 1);
0090         vlay.addWidget(&errorsLB);
0091         vlay.addWidget(&errorsLW, 1);
0092         vlay.addWidget(&buttonBox);
0093 
0094         if (created.empty()) {
0095             createdLW.hide();
0096         }
0097         if (errors.empty()) {
0098             errorsLW.hide();
0099         }
0100 
0101         connect(&buttonBox, &QDialogButtonBox::accepted, this, &ResultDialog::accept);
0102         connect(&buttonBox, &QDialogButtonBox::rejected, this, &ResultDialog::reject);
0103         readConfig();
0104     }
0105     ~ResultDialog() override
0106     {
0107         writeConfig();
0108     }
0109 
0110     void readConfig()
0111     {
0112         KConfigGroup dialog(KSharedConfig::openStateConfig(), QStringLiteral("ResultDialog"));
0113         const QSize size = dialog.readEntry("Size", QSize(600, 400));
0114         if (size.isValid()) {
0115             resize(size);
0116         }
0117     }
0118     void writeConfig()
0119     {
0120         KConfigGroup dialog(KSharedConfig::openStateConfig(), QStringLiteral("ResultDialog"));
0121         dialog.writeEntry("Size", size());
0122         dialog.sync();
0123     }
0124 
0125 private:
0126     QLabel createdLB;
0127     QListWidget createdLW;
0128     QLabel errorsLB;
0129     QListWidget errorsLW;
0130     QDialogButtonBox buttonBox;
0131     QVBoxLayout vlay;
0132 };
0133 
0134 }
0135 
0136 static QStringList fs_sort(QStringList l)
0137 {
0138     std::sort(l.begin(), l.end(), [](const QString &lhs, const QString &rhs) {
0139         return QString::compare(lhs, rhs, ChecksumsUtils::fs_cs) < 0;
0140     });
0141     return l;
0142 }
0143 
0144 static QStringList fs_intersect(QStringList l1, QStringList l2)
0145 {
0146     fs_sort(l1);
0147     fs_sort(l2);
0148     QStringList result;
0149     std::set_intersection(l1.begin(), l1.end(), l2.begin(), l2.end(), std::back_inserter(result), [](const QString &lhs, const QString &rhs) {
0150         return QString::compare(lhs, rhs, ChecksumsUtils::fs_cs) < 0;
0151     });
0152     return result;
0153 }
0154 
0155 class CreateChecksumsController::Private : public QThread
0156 {
0157     Q_OBJECT
0158     friend class ::Kleo::Crypto::CreateChecksumsController;
0159     CreateChecksumsController *const q;
0160 
0161 public:
0162     explicit Private(CreateChecksumsController *qq);
0163     ~Private() override;
0164 
0165 Q_SIGNALS:
0166     void progress(int, int, const QString &);
0167 
0168 private:
0169     void slotOperationFinished()
0170     {
0171 #ifndef QT_NO_PROGRESSDIALOG
0172         if (progressDialog) {
0173             progressDialog->setValue(progressDialog->maximum());
0174             progressDialog->close();
0175         }
0176 #endif // QT_NO_PROGRESSDIALOG
0177         auto const dlg = new ResultDialog(created, errors);
0178         dlg->setAttribute(Qt::WA_DeleteOnClose);
0179         q->bringToForeground(dlg);
0180         if (!errors.empty())
0181             q->setLastError(gpg_error(GPG_ERR_GENERAL), errors.join(QLatin1Char('\n')));
0182         q->emitDoneOrError();
0183     }
0184     void slotProgress(int current, int total, const QString &what)
0185     {
0186         qCDebug(KLEOPATRA_LOG) << "progress: " << current << "/" << total << ": " << qPrintable(what);
0187 #ifndef QT_NO_PROGRESSDIALOG
0188         if (!progressDialog) {
0189             return;
0190         }
0191         progressDialog->setMaximum(total);
0192         progressDialog->setValue(current);
0193         progressDialog->setLabelText(what);
0194 #endif // QT_NO_PROGRESSDIALOG
0195     }
0196 
0197 private:
0198     void run() override;
0199 
0200 private:
0201 #ifndef QT_NO_PROGRESSDIALOG
0202     QPointer<QProgressDialog> progressDialog;
0203 #endif
0204     mutable QMutex mutex;
0205     const std::vector<std::shared_ptr<ChecksumDefinition>> checksumDefinitions;
0206     std::shared_ptr<ChecksumDefinition> checksumDefinition;
0207     QStringList files;
0208     QStringList errors, created;
0209     bool allowAddition;
0210     volatile bool canceled;
0211 };
0212 
0213 CreateChecksumsController::Private::Private(CreateChecksumsController *qq)
0214     : q(qq)
0215     ,
0216 #ifndef QT_NO_PROGRESSDIALOG
0217     progressDialog()
0218     ,
0219 #endif
0220     mutex()
0221     , checksumDefinitions(ChecksumDefinition::getChecksumDefinitions())
0222     , checksumDefinition(ChecksumDefinition::getDefaultChecksumDefinition(checksumDefinitions))
0223     , files()
0224     , errors()
0225     , created()
0226     , allowAddition(false)
0227     , canceled(false)
0228 {
0229     connect(this, SIGNAL(progress(int, int, QString)), q, SLOT(slotProgress(int, int, QString)));
0230     connect(this, &Private::progress, q, &Controller::progress);
0231     connect(this, SIGNAL(finished()), q, SLOT(slotOperationFinished()));
0232 }
0233 
0234 CreateChecksumsController::Private::~Private()
0235 {
0236     qCDebug(KLEOPATRA_LOG);
0237 }
0238 
0239 CreateChecksumsController::CreateChecksumsController(QObject *p)
0240     : Controller(p)
0241     , d(new Private(this))
0242 {
0243 }
0244 
0245 CreateChecksumsController::CreateChecksumsController(const std::shared_ptr<const ExecutionContext> &ctx, QObject *p)
0246     : Controller(ctx, p)
0247     , d(new Private(this))
0248 {
0249 }
0250 
0251 CreateChecksumsController::~CreateChecksumsController()
0252 {
0253     qCDebug(KLEOPATRA_LOG);
0254 }
0255 
0256 void CreateChecksumsController::setFiles(const QStringList &files)
0257 {
0258     kleo_assert(!d->isRunning());
0259     kleo_assert(!files.empty());
0260     const std::vector<QRegularExpression> patterns = ChecksumsUtils::get_patterns(d->checksumDefinitions);
0261     if (!std::all_of(files.cbegin(), files.cend(), ChecksumsUtils::matches_any(patterns))
0262         && !std::none_of(files.cbegin(), files.cend(), ChecksumsUtils::matches_any(patterns))) {
0263         throw Exception(gpg_error(GPG_ERR_INV_ARG),
0264                         i18n("Create Checksums: input files must be either all checksum files or all files to be checksummed, not a mixture of both."));
0265     }
0266     const QMutexLocker locker(&d->mutex);
0267     d->files = files;
0268 }
0269 
0270 void CreateChecksumsController::setAllowAddition(bool allow)
0271 {
0272     kleo_assert(!d->isRunning());
0273     const QMutexLocker locker(&d->mutex);
0274     d->allowAddition = allow;
0275 }
0276 
0277 bool CreateChecksumsController::allowAddition() const
0278 {
0279     const QMutexLocker locker(&d->mutex);
0280     return d->allowAddition;
0281 }
0282 
0283 void CreateChecksumsController::start()
0284 {
0285     {
0286         const QMutexLocker locker(&d->mutex);
0287 
0288 #ifndef QT_NO_PROGRESSDIALOG
0289         d->progressDialog = new QProgressDialog(i18n("Initializing..."), i18n("Cancel"), 0, 0);
0290         applyWindowID(d->progressDialog);
0291         d->progressDialog->setAttribute(Qt::WA_DeleteOnClose);
0292         d->progressDialog->setMinimumDuration(1000);
0293         d->progressDialog->setWindowTitle(i18nc("@title:window", "Create Checksum Progress"));
0294         connect(d->progressDialog.data(), &QProgressDialog::canceled, this, &CreateChecksumsController::cancel);
0295 #endif // QT_NO_PROGRESSDIALOG
0296 
0297         d->canceled = false;
0298         d->errors.clear();
0299         d->created.clear();
0300     }
0301 
0302     d->start();
0303 }
0304 
0305 void CreateChecksumsController::cancel()
0306 {
0307     qCDebug(KLEOPATRA_LOG);
0308     const QMutexLocker locker(&d->mutex);
0309     d->canceled = true;
0310 }
0311 
0312 namespace
0313 {
0314 
0315 struct Dir {
0316     QDir dir;
0317     QString sumFile;
0318     QStringList inputFiles;
0319     quint64 totalSize;
0320     std::shared_ptr<ChecksumDefinition> checksumDefinition;
0321 };
0322 
0323 }
0324 
0325 static QStringList remove_checksum_files(QStringList l, const std::vector<QRegularExpression> &rxs)
0326 {
0327     QStringList::iterator end = l.end();
0328     for (const auto &rx : rxs) {
0329         end = std::remove_if(l.begin(), end, [rx](const QString &str) {
0330             return rx.match(str).hasMatch();
0331         });
0332     }
0333     l.erase(end, l.end());
0334     return l;
0335 }
0336 
0337 static quint64 aggregate_size(const QDir &dir, const QStringList &files)
0338 {
0339     quint64 n = 0;
0340     for (const QString &file : files) {
0341         n += QFileInfo(dir.absoluteFilePath(file)).size();
0342     }
0343     return n;
0344 }
0345 
0346 static std::vector<Dir> find_dirs_by_sum_files(const QStringList &files,
0347                                                bool allowAddition,
0348                                                const std::function<void(int)> &progress,
0349                                                const std::vector<std::shared_ptr<ChecksumDefinition>> &checksumDefinitions)
0350 {
0351     const std::vector<QRegularExpression> patterns = ChecksumsUtils::get_patterns(checksumDefinitions);
0352 
0353     std::vector<Dir> dirs;
0354     dirs.reserve(files.size());
0355 
0356     int i = 0;
0357 
0358     for (const QString &file : files) {
0359         const QFileInfo fi(file);
0360         const QDir dir = fi.dir();
0361         const QStringList entries = remove_checksum_files(dir.entryList(QDir::Files), patterns);
0362 
0363         QStringList inputFiles;
0364         if (allowAddition) {
0365             inputFiles = entries;
0366         } else {
0367             const std::vector<ChecksumsUtils::File> parsed = ChecksumsUtils::parse_sum_file(fi.absoluteFilePath());
0368             QStringList oldInputFiles;
0369             oldInputFiles.reserve(parsed.size());
0370             std::transform(parsed.cbegin(), parsed.cend(), std::back_inserter(oldInputFiles), std::mem_fn(&ChecksumsUtils::File::name));
0371             inputFiles = fs_intersect(oldInputFiles, entries);
0372         }
0373 
0374         const Dir item = {
0375             dir,
0376             fi.fileName(),
0377             inputFiles,
0378             aggregate_size(dir, inputFiles),
0379             ChecksumsUtils::filename2definition(fi.fileName(), checksumDefinitions),
0380         };
0381 
0382         dirs.push_back(item);
0383 
0384         if (progress) {
0385             progress(++i);
0386         }
0387     }
0388     return dirs;
0389 }
0390 
0391 namespace
0392 {
0393 struct less_dir {
0394     bool operator()(const QDir &lhs, const QDir &rhs) const
0395     {
0396         return QString::compare(lhs.absolutePath(), rhs.absolutePath(), ChecksumsUtils::fs_cs) < 0;
0397     }
0398 };
0399 }
0400 
0401 static std::vector<Dir> find_dirs_by_input_files(const QStringList &files,
0402                                                  const std::shared_ptr<ChecksumDefinition> &checksumDefinition,
0403                                                  bool allowAddition,
0404                                                  const std::function<void(int)> &progress,
0405                                                  const std::vector<std::shared_ptr<ChecksumDefinition>> &checksumDefinitions)
0406 {
0407     Q_UNUSED(allowAddition)
0408     if (!checksumDefinition) {
0409         return std::vector<Dir>();
0410     }
0411 
0412     const std::vector<QRegularExpression> patterns = ChecksumsUtils::get_patterns(checksumDefinitions);
0413 
0414     std::map<QDir, QStringList, less_dir> dirs2files;
0415 
0416     // Step 1: sort files by the dir they're contained in:
0417 
0418     std::deque<QString> inputs(files.begin(), files.end());
0419 
0420     int i = 0;
0421     while (!inputs.empty()) {
0422         const QString file = inputs.front();
0423         inputs.pop_front();
0424         const QFileInfo fi(file);
0425         if (fi.isDir()) {
0426             QDir dir(file);
0427             dirs2files[dir] = remove_checksum_files(dir.entryList(QDir::Files), patterns);
0428             const auto entryList = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0429             std::transform(entryList.cbegin(), entryList.cend(), std::inserter(inputs, inputs.begin()), [&dir](const QString &entry) {
0430                 return dir.absoluteFilePath(entry);
0431             });
0432         } else {
0433             dirs2files[fi.dir()].push_back(file);
0434         }
0435         if (progress) {
0436             progress(++i);
0437         }
0438     }
0439 
0440     // Step 2: convert into vector<Dir>:
0441 
0442     std::vector<Dir> dirs;
0443     dirs.reserve(dirs2files.size());
0444 
0445     for (auto it = dirs2files.begin(), end = dirs2files.end(); it != end; ++it) {
0446         const QStringList inputFiles = remove_checksum_files(it->second, patterns);
0447         if (inputFiles.empty()) {
0448             continue;
0449         }
0450 
0451         const Dir dir = {
0452             it->first,
0453             checksumDefinition->outputFileName(),
0454             inputFiles,
0455             aggregate_size(it->first, inputFiles),
0456             checksumDefinition,
0457         };
0458         dirs.push_back(dir);
0459 
0460         if (progress) {
0461             progress(++i);
0462         }
0463     }
0464     return dirs;
0465 }
0466 
0467 static QString process(const Dir &dir, bool *fatal)
0468 {
0469     const QString absFilePath = dir.dir.absoluteFilePath(dir.sumFile);
0470     QTemporaryFile out;
0471     QProcess p;
0472     if (!out.open()) {
0473         return QStringLiteral("Failed to open Temporary file.");
0474     }
0475     p.setWorkingDirectory(dir.dir.absolutePath());
0476     p.setStandardOutputFile(out.fileName());
0477     const QString program = dir.checksumDefinition->createCommand();
0478     dir.checksumDefinition->startCreateCommand(&p, dir.inputFiles);
0479     p.waitForFinished(-1);
0480     qCDebug(KLEOPATRA_LOG) << "[" << &p << "] Exit code " << p.exitCode();
0481 
0482     if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0) {
0483         if (fatal && p.error() == QProcess::FailedToStart) {
0484             *fatal = true;
0485         }
0486         if (p.error() == QProcess::UnknownError)
0487             return i18n("Error while running %1: %2", program, QString::fromLocal8Bit(p.readAllStandardError().trimmed().constData()));
0488         else {
0489             return i18n("Failed to execute %1: %2", program, p.errorString());
0490         }
0491     }
0492 
0493     QFileInfo fi(absFilePath);
0494     if (!(fi.exists() && !QFile::remove(absFilePath)) && QFile::copy(out.fileName(), absFilePath)) {
0495         return QString();
0496     }
0497 
0498     return xi18n("Failed to overwrite <filename>%1</filename>.", dir.sumFile);
0499 }
0500 
0501 namespace
0502 {
0503 static QDebug operator<<(QDebug s, const Dir &dir)
0504 {
0505     return s << "Dir(" << dir.dir << "->" << dir.sumFile << "<-(" << dir.totalSize << ')' << dir.inputFiles << ")\n";
0506 }
0507 }
0508 
0509 void CreateChecksumsController::Private::run()
0510 {
0511     QMutexLocker locker(&mutex);
0512 
0513     const QStringList files = this->files;
0514     const std::vector<std::shared_ptr<ChecksumDefinition>> checksumDefinitions = this->checksumDefinitions;
0515     const std::shared_ptr<ChecksumDefinition> checksumDefinition = this->checksumDefinition;
0516     const bool allowAddition = this->allowAddition;
0517 
0518     locker.unlock();
0519 
0520     QStringList errors;
0521     QStringList created;
0522 
0523     if (!checksumDefinition) {
0524         errors.push_back(i18n("No checksum programs defined."));
0525         locker.relock();
0526         this->errors = errors;
0527         return;
0528     } else {
0529         qCDebug(KLEOPATRA_LOG) << "using checksum-definition" << checksumDefinition->id();
0530     }
0531 
0532     //
0533     // Step 1: build a list of work to do (no progress):
0534     //
0535 
0536     const QString scanning = i18n("Scanning directories...");
0537     Q_EMIT progress(0, 0, scanning);
0538 
0539     const bool haveSumFiles = std::all_of(files.cbegin(), files.cend(), ChecksumsUtils::matches_any(ChecksumsUtils::get_patterns(checksumDefinitions)));
0540     const auto progressCb = [this, &scanning](int c) {
0541         Q_EMIT progress(c, 0, scanning);
0542     };
0543     const std::vector<Dir> dirs = haveSumFiles ? find_dirs_by_sum_files(files, allowAddition, progressCb, checksumDefinitions)
0544                                                : find_dirs_by_input_files(files, checksumDefinition, allowAddition, progressCb, checksumDefinitions);
0545 
0546     for (const Dir &dir : dirs) {
0547         qCDebug(KLEOPATRA_LOG) << dir;
0548     }
0549 
0550     if (!canceled) {
0551         Q_EMIT progress(0, 0, i18n("Calculating total size..."));
0552 
0553         const quint64 total = kdtools::accumulate_transform(dirs.cbegin(), dirs.cend(), std::mem_fn(&Dir::totalSize), Q_UINT64_C(0));
0554 
0555         if (!canceled) {
0556             //
0557             // Step 2: perform work (with progress reporting):
0558             //
0559 
0560             // re-scale 'total' to fit into ints (wish QProgressDialog would use quint64...)
0561             const quint64 factor = total / std::numeric_limits<int>::max() + 1;
0562 
0563             quint64 done = 0;
0564             for (const Dir &dir : dirs) {
0565                 Q_EMIT progress(done / factor, total / factor, i18n("Checksumming (%2) in %1", dir.checksumDefinition->label(), dir.dir.path()));
0566                 bool fatal = false;
0567                 const QString error = process(dir, &fatal);
0568                 if (!error.isEmpty()) {
0569                     errors.push_back(error);
0570                 } else {
0571                     created.push_back(dir.dir.absoluteFilePath(dir.sumFile));
0572                 }
0573                 done += dir.totalSize;
0574                 if (fatal || canceled) {
0575                     break;
0576                 }
0577             }
0578             Q_EMIT progress(done / factor, total / factor, i18n("Done."));
0579         }
0580     }
0581 
0582     locker.relock();
0583 
0584     this->errors = errors;
0585     this->created = created;
0586 
0587     // mutex unlocked by QMutexLocker
0588 }
0589 
0590 #include "createchecksumscontroller.moc"
0591 #include "moc_createchecksumscontroller.cpp"