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> ®exps) 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