File indexing completed on 2024-06-23 05:13: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"