Prepare for the removal of sendmail support in mailtransport.
[kdepim.git] / kleopatra / crypto / verifychecksumscontroller.cpp
blobaf650341fbddecf2512f809bf907b354978af6c5
1 /* -*- mode: c++; c-basic-offset:4 -*-
2 crypto/verifychecksumscontroller.cpp
4 This file is part of Kleopatra, the KDE keymanager
5 Copyright (c) 2010 Klarälvdalens Datakonsult AB
7 Kleopatra is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
12 Kleopatra is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 In addition, as a special exception, the copyright holders give
22 permission to link the code of this program with any edition of
23 the Qt library by Trolltech AS, Norway (or with modified versions
24 of Qt that use the same license as Qt), and distribute linked
25 combinations including the two. You must obey the GNU General
26 Public License in all respects for all of the code used other than
27 Qt. If you modify this file, you may extend this exception to
28 your version of the file, but you are not obligated to do so. If
29 you do not wish to do so, delete this exception statement from
30 your version.
33 #include <config-kleopatra.h>
35 #include "verifychecksumscontroller.h"
37 #ifndef QT_NO_DIRMODEL
39 #include <crypto/gui/verifychecksumsdialog.h>
41 #include <utils/input.h>
42 #include <utils/output.h>
43 #include <utils/classify.h>
44 #include <utils/kleo_assert.h>
46 #include <kleo/stl_util.h>
47 #include <kleo/checksumdefinition.h>
49 #include <KLocalizedString>
51 #include "kleopatra_debug.h"
52 #include <QPointer>
53 #include <QFileInfo>
54 #include <QThread>
55 #include <QMutex>
56 #include <QProgressDialog>
57 #include <QDir>
58 #include <QProcess>
60 #include <boost/bind.hpp>
61 #include <boost/function.hpp>
63 #include <gpg-error.h>
65 #include <deque>
66 #include <limits>
67 #include <set>
69 using namespace Kleo;
70 using namespace Kleo::Crypto;
71 using namespace Kleo::Crypto::Gui;
72 using namespace boost;
74 #ifdef Q_OS_UNIX
75 static const bool HAVE_UNIX = true;
76 #else
77 static const bool HAVE_UNIX = false;
78 #endif
80 static const QLatin1String CHECKSUM_DEFINITION_ID_ENTRY("checksum-definition-id");
82 static const Qt::CaseSensitivity fs_cs = HAVE_UNIX ? Qt::CaseSensitive : Qt::CaseInsensitive ; // can we use QAbstractFileEngine::caseSensitive()?
84 #if 0
85 static QStringList fs_sort(QStringList l)
87 int (*QString_compare)(const QString &, const QString &, Qt::CaseSensitivity) = &QString::compare;
88 kdtools::sort(l, boost::bind(QString_compare, _1, _2, fs_cs) < 0);
89 return l;
92 static QStringList fs_intersect(QStringList l1, QStringList l2)
94 int (*QString_compare)(const QString &, const QString &, Qt::CaseSensitivity) = &QString::compare;
95 fs_sort(l1);
96 fs_sort(l2);
97 QStringList result;
98 std::set_intersection(l1.begin(), l1.end(),
99 l2.begin(), l2.end(),
100 std::back_inserter(result),
101 boost::bind(QString_compare, _1, _2, fs_cs) < 0);
102 return result;
104 #endif
106 static QList<QRegExp> get_patterns(const std::vector< shared_ptr<ChecksumDefinition> > &checksumDefinitions)
108 QList<QRegExp> result;
109 Q_FOREACH (const shared_ptr<ChecksumDefinition> &cd, checksumDefinitions)
110 if (cd)
111 Q_FOREACH (const QString &pattern, cd->patterns()) {
112 result.push_back(QRegExp(pattern, fs_cs));
114 return result;
117 namespace
119 struct matches_any : std::unary_function<QString, bool> {
120 const QList<QRegExp> m_regexps;
121 explicit matches_any(const QList<QRegExp> &regexps) : m_regexps(regexps) {}
122 bool operator()(const QString &s) const
124 return kdtools::any(m_regexps, boost::bind(&QRegExp::exactMatch, _1, s));
127 struct matches_none_of : std::unary_function<QString, bool> {
128 const QList<QRegExp> m_regexps;
129 explicit matches_none_of(const QList<QRegExp> &regexps) : m_regexps(regexps) {}
130 bool operator()(const QString &s) const
132 return kdtools::none_of(m_regexps, boost::bind(&QRegExp::exactMatch, _1, s));
137 class VerifyChecksumsController::Private : public QThread
139 Q_OBJECT
140 friend class ::Kleo::Crypto::VerifyChecksumsController;
141 VerifyChecksumsController *const q;
142 public:
143 explicit Private(VerifyChecksumsController *qq);
144 ~Private();
146 Q_SIGNALS:
147 void baseDirectories(const QStringList &);
148 void progress(int, int, const QString &);
149 void status(const QString &file, Kleo::Crypto::Gui::VerifyChecksumsDialog::Status);
151 private:
152 void slotOperationFinished()
154 if (dialog) {
155 dialog->setProgress(100, 100);
156 dialog->setErrors(errors);
159 if (!errors.empty())
160 q->setLastError(gpg_error(GPG_ERR_GENERAL),
161 errors.join(QLatin1String("\n")));
162 q->emitDoneOrError();
165 private:
166 void run() Q_DECL_OVERRIDE;
168 private:
169 QPointer<VerifyChecksumsDialog> dialog;
170 mutable QMutex mutex;
171 const std::vector< shared_ptr<ChecksumDefinition> > checksumDefinitions;
172 QStringList files;
173 QStringList errors;
174 volatile bool canceled;
177 VerifyChecksumsController::Private::Private(VerifyChecksumsController *qq)
178 : q(qq),
179 dialog(),
180 mutex(),
181 checksumDefinitions(ChecksumDefinition::getChecksumDefinitions()),
182 files(),
183 errors(),
184 canceled(false)
186 connect(this, SIGNAL(progress(int,int,QString)),
187 q, SIGNAL(progress(int,int,QString)));
188 connect(this, SIGNAL(finished()),
189 q, SLOT(slotOperationFinished()));
192 VerifyChecksumsController::Private::~Private()
194 qCDebug(KLEOPATRA_LOG);
197 VerifyChecksumsController::VerifyChecksumsController(QObject *p)
198 : Controller(p), d(new Private(this))
203 VerifyChecksumsController::VerifyChecksumsController(const shared_ptr<const ExecutionContext> &ctx, QObject *p)
204 : Controller(ctx, p), d(new Private(this))
209 VerifyChecksumsController::~VerifyChecksumsController()
211 qCDebug(KLEOPATRA_LOG);
214 void VerifyChecksumsController::setFiles(const QStringList &files)
216 kleo_assert(!d->isRunning());
217 kleo_assert(!files.empty());
218 const QMutexLocker locker(&d->mutex);
219 d->files = files;
222 void VerifyChecksumsController::start()
226 const QMutexLocker locker(&d->mutex);
228 d->dialog = new VerifyChecksumsDialog;
229 d->dialog->setAttribute(Qt::WA_DeleteOnClose);
230 d->dialog->setWindowTitle(i18nc("@title:window", "Verify Checksum Results"));
232 connect(d->dialog, SIGNAL(canceled()),
233 this, SLOT(cancel()));
234 connect(d.get(), SIGNAL(baseDirectories(QStringList)),
235 d->dialog, SLOT(setBaseDirectories(QStringList)));
236 connect(d.get(), SIGNAL(progress(int,int,QString)),
237 d->dialog, SLOT(setProgress(int,int)));
238 connect(d.get(), SIGNAL(status(QString,Kleo::Crypto::Gui::VerifyChecksumsDialog::Status)),
239 d->dialog, SLOT(setStatus(QString,Kleo::Crypto::Gui::VerifyChecksumsDialog::Status)));
241 d->canceled = false;
242 d->errors.clear();
245 d->start();
247 d->dialog->show();
251 void VerifyChecksumsController::cancel()
253 qCDebug(KLEOPATRA_LOG);
254 const QMutexLocker locker(&d->mutex);
255 d->canceled = true;
258 namespace
261 struct SumFile {
262 QDir dir;
263 QString sumFile;
264 quint64 totalSize;
265 shared_ptr<ChecksumDefinition> checksumDefinition;
270 static QStringList filter_checksum_files(QStringList l, const QList<QRegExp> &rxs)
272 l.erase(std::remove_if(l.begin(), l.end(),
273 matches_none_of(rxs)),
274 l.end());
275 return l;
278 namespace
280 struct File {
281 QString name;
282 QByteArray checksum;
283 bool binary;
287 static QString decode(const QString &encoded)
289 QString decoded;
290 decoded.reserve(encoded.size());
291 bool shift = false;
292 Q_FOREACH (const QChar ch, encoded)
293 if (shift) {
294 switch (ch.toLatin1()) {
295 case '\\': decoded += QLatin1Char('\\'); break;
296 case 'n': decoded += QLatin1Char('\n'); break;
297 default:
298 qCDebug(KLEOPATRA_LOG) << "invalid escape sequence" << '\\' << ch << "(interpreted as '" << ch << "')";
299 decoded += ch;
300 break;
302 shift = false;
303 } else {
304 if (ch == QLatin1Char('\\')) {
305 shift = true;
306 } else {
307 decoded += ch;
310 return decoded;
313 static std::vector<File> parse_sum_file(const QString &fileName)
315 std::vector<File> files;
316 QFile f(fileName);
317 if (f.open(QIODevice::ReadOnly)) {
318 QTextStream s(&f);
319 QRegExp rx(QLatin1String("(\\?)([a-f0-9A-F]+) ([ *])([^\n]+)\n*"));
320 while (!s.atEnd()) {
321 const QString line = s.readLine();
322 if (rx.exactMatch(line)) {
323 assert(!rx.cap(4).endsWith(QLatin1Char('\n')));
324 const File file = {
325 rx.cap(1) == QLatin1String("\\") ? decode(rx.cap(4)) : rx.cap(4),
326 rx.cap(2).toLatin1(),
327 rx.cap(3) == QLatin1String("*"),
329 files.push_back(file);
333 return files;
336 static quint64 aggregate_size(const QDir &dir, const QStringList &files)
338 quint64 n = 0;
339 Q_FOREACH (const QString &file, files) {
340 n += QFileInfo(dir.absoluteFilePath(file)).size();
342 return n;
345 static shared_ptr<ChecksumDefinition> filename2definition(const QString &fileName,
346 const std::vector< shared_ptr<ChecksumDefinition> > &checksumDefinitions)
348 Q_FOREACH (const shared_ptr<ChecksumDefinition> &cd, checksumDefinitions)
349 if (cd)
350 Q_FOREACH (const QString &pattern, cd->patterns())
351 if (QRegExp(pattern, fs_cs).exactMatch(fileName)) {
352 return cd;
354 return shared_ptr<ChecksumDefinition>();
357 namespace
359 struct less_dir : std::binary_function<QDir, QDir, bool> {
360 bool operator()(const QDir &lhs, const QDir &rhs) const
362 return QString::compare(lhs.absolutePath(), rhs.absolutePath(), fs_cs) < 0 ;
365 struct less_file : std::binary_function<QString, QString, bool> {
366 bool operator()(const QString &lhs, const QString &rhs) const
368 return QString::compare(lhs, rhs, fs_cs) < 0 ;
371 struct sumfile_contains_file : std::unary_function<QString, bool> {
372 const QDir dir;
373 const QString fileName;
374 sumfile_contains_file(const QDir &dir_, const QString &fileName_)
375 : dir(dir_), fileName(fileName_) {}
376 bool operator()(const QString &sumFile) const
378 const std::vector<File> files = parse_sum_file(dir.absoluteFilePath(sumFile));
379 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << files.size()
380 << " files listed in " << qPrintable(dir.absoluteFilePath(sumFile));
381 Q_FOREACH (const File &file, files) {
382 const bool isSameFileName = (QString::compare(file.name, fileName, fs_cs) == 0);
383 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: "
384 << qPrintable(file.name) << " == "
385 << qPrintable(fileName) << " ? "
386 << isSameFileName;
387 if (isSameFileName) {
388 return true;
391 return false;
397 // IF is_dir(file)
398 // add all sumfiles \in dir(file)
399 // inputs.prepend( all dirs \in dir(file) )
400 // ELSE IF is_sum_file(file)
401 // add
402 // ELSE IF \exists sumfile in dir(file) \where sumfile \contains file
403 // add sumfile
404 // ELSE
405 // error: no checksum found for "file"
407 static QStringList find_base_directiories(const QStringList &files)
410 // Step 1: find base dirs:
412 std::set<QDir, less_dir> dirs;
413 Q_FOREACH (const QString &file, files) {
414 const QFileInfo fi(file);
415 const QDir dir = fi.isDir() ? QDir(file) : fi.dir() ;
416 dirs.insert(dir);
419 // Step 1a: collapse direct child directories
421 bool changed;
422 do {
423 changed = false;
424 std::set<QDir, less_dir>::iterator it = dirs.begin();
425 while (it != dirs.end()) {
426 QDir dir = *it;
427 if (dir.cdUp() && dirs.count(dir)) {
428 dirs.erase(it++);
429 changed = true;
430 } else {
431 ++it;
434 } while (changed);
436 return kdtools::transform<QStringList>(dirs, mem_fn(&QDir::absolutePath));
439 static std::vector<SumFile> find_sums_by_input_files(const QStringList &files, QStringList &errors,
440 const function<void(int)> &progress,
441 const std::vector< shared_ptr<ChecksumDefinition> > &checksumDefinitions)
443 const QList<QRegExp> patterns = get_patterns(checksumDefinitions);
445 const matches_any is_sum_file(patterns);
447 std::map<QDir, std::set<QString, less_file>, less_dir> dirs2sums;
449 // Step 1: find the sumfiles we need to check:
451 std::deque<QString> inputs(files.begin(), files.end());
453 int i = 0;
454 while (!inputs.empty()) {
455 const QString file = inputs.front();
456 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: considering " << qPrintable(file);
457 inputs.pop_front();
458 const QFileInfo fi(file);
459 const QString fileName = fi.fileName();
460 if (fi.isDir()) {
461 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: it's a directory";
462 QDir dir(file);
463 const QStringList sumfiles = filter_checksum_files(dir.entryList(QDir::Files), patterns);
464 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << sumfiles.size()
465 << " sum files: " << qPrintable(sumfiles.join(QStringLiteral(", ")));
466 dirs2sums[ dir ].insert(sumfiles.begin(), sumfiles.end());
467 const QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
468 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << dirs.size()
469 << " subdirs, prepending";
470 kdtools::transform(dirs,
471 std::inserter(inputs, inputs.begin()),
472 boost::bind(&QDir::absoluteFilePath, cref(dir), _1));
473 } else if (is_sum_file(fileName)) {
474 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: it's a sum file";
475 dirs2sums[fi.dir()].insert(fileName);
476 } else {
477 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: it's something else; checking whether we'll find a sumfile for it...";
478 const QDir dir = fi.dir();
479 const QStringList sumfiles = filter_checksum_files(dir.entryList(QDir::Files), patterns);
480 qCDebug(KLEOPATRA_LOG) << "find_sums_by_input_files: found " << sumfiles.size()
481 << " potential sumfiles: " << qPrintable(sumfiles.join(QStringLiteral(", ")));
482 const QStringList::const_iterator it = kdtools::find_if(sumfiles, sumfile_contains_file(dir, fileName));
483 if (it == sumfiles.end()) {
484 errors.push_back(i18n("Cannot find checksums file for file %1", file));
485 } else {
486 dirs2sums[dir].insert(*it);
489 if (!progress.empty()) {
490 progress(++i);
494 // Step 2: convert into vector<SumFile>:
496 std::vector<SumFile> sumfiles;
497 sumfiles.reserve(dirs2sums.size());
499 for (std::map<QDir, std::set<QString, less_file>, less_dir>::const_iterator it = dirs2sums.begin(), end = dirs2sums.end() ; it != end ; ++it) {
501 if (it->second.empty()) {
502 continue;
505 const QDir &dir = it->first;
507 Q_FOREACH (const QString &sumFileName, it->second) {
509 const std::vector<File> summedfiles = parse_sum_file(dir.absoluteFilePath(sumFileName));
511 const SumFile sumFile = {
512 it->first,
513 sumFileName,
514 aggregate_size(it->first, kdtools::transform<QStringList>(summedfiles, mem_fn(&File::name))),
515 filename2definition(sumFileName, checksumDefinitions),
517 sumfiles.push_back(sumFile);
521 if (!progress.empty()) {
522 progress(++i);
526 return sumfiles;
529 static QStringList c_lang_environment()
531 QStringList env = QProcess::systemEnvironment();
532 env.erase(std::remove_if(env.begin(), env.end(),
533 boost::bind(&QRegExp::exactMatch,
534 QRegExp(QLatin1String("^LANG=.*"), fs_cs), _1)),
535 env.end());
536 env.push_back(QLatin1String("LANG=C"));
537 return env;
540 static const struct {
541 const char *string;
542 VerifyChecksumsDialog::Status status;
543 } statusStrings[] = {
544 { "OK", VerifyChecksumsDialog::OK },
545 { "FAILED", VerifyChecksumsDialog::Failed },
547 static const size_t numStatusStrings = sizeof statusStrings / sizeof * statusStrings ;
549 static VerifyChecksumsDialog::Status string2status(const QByteArray &str)
551 for (unsigned int i = 0 ; i < numStatusStrings ; ++i)
552 if (str == statusStrings[i].string) {
553 return statusStrings[i].status;
555 return VerifyChecksumsDialog::Unknown;
558 static QString process(const SumFile &sumFile, bool *fatal, const QStringList &env,
559 const function<void(const QString &, VerifyChecksumsDialog::Status)> &status)
561 QProcess p;
562 p.setEnvironment(env);
563 p.setWorkingDirectory(sumFile.dir.absolutePath());
564 p.setReadChannel(QProcess::StandardOutput);
566 const QString absFilePath = sumFile.dir.absoluteFilePath(sumFile.sumFile);
568 const QString program = sumFile.checksumDefinition->verifyCommand();
569 sumFile.checksumDefinition->startVerifyCommand(&p, QStringList(absFilePath));
571 QByteArray remainder; // used for filenames with newlines in them
572 while (p.state() != QProcess::NotRunning) {
573 p.waitForReadyRead();
574 while (p.canReadLine()) {
575 const QByteArray line = p.readLine();
576 const int colonIdx = line.lastIndexOf(':');
577 if (colonIdx < 0) {
578 remainder += line; // no colon -> probably filename with a newline
579 continue;
581 const QString file = QFile::decodeName(remainder + line.left(colonIdx));
582 remainder.clear();
583 const VerifyChecksumsDialog::Status result = string2status(line.mid(colonIdx + 1).trimmed());
584 status(sumFile.dir.absoluteFilePath(file), result);
587 qCDebug(KLEOPATRA_LOG) << "[" << &p << "] Exit code " << p.exitCode();
589 if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0) {
590 if (fatal && p.error() == QProcess::FailedToStart) {
591 *fatal = true;
593 if (p.error() == QProcess::UnknownError)
594 return i18n("Error while running %1: %2", program,
595 QString::fromLocal8Bit(p.readAllStandardError().trimmed().constData()));
596 else {
597 return i18n("Failed to execute %1: %2", program, p.errorString());
601 return QString();
604 namespace
606 static QDebug operator<<(QDebug s, const SumFile &sum)
608 return s << "SumFile(" << sum.dir << "->" << sum.sumFile << "<-(" << sum.totalSize << ')' << ")\n";
612 void VerifyChecksumsController::Private::run()
615 QMutexLocker locker(&mutex);
617 const QStringList files = this->files;
618 const std::vector< shared_ptr<ChecksumDefinition> > checksumDefinitions = this->checksumDefinitions;
620 locker.unlock();
622 QStringList errors;
625 // Step 0: find base directories:
628 Q_EMIT baseDirectories(find_base_directiories(files));
631 // Step 1: build a list of work to do (no progress):
634 const QString scanning = i18n("Scanning directories...");
635 Q_EMIT progress(0, 0, scanning);
637 const function<void(int)> progressCb = boost::bind(&Private::progress, this, _1, 0, scanning);
638 const function<void(const QString &, VerifyChecksumsDialog::Status)>
639 statusCb = boost::bind(&Private::status, this, _1, _2);
640 const std::vector<SumFile> sumfiles = find_sums_by_input_files(files, errors, progressCb, checksumDefinitions);
642 Q_FOREACH (const SumFile &sumfile, sumfiles) {
643 qCDebug(KLEOPATRA_LOG) << sumfile;
646 if (!canceled) {
648 Q_EMIT progress(0, 0, i18n("Calculating total size..."));
650 const quint64 total
651 = kdtools::accumulate_transform(sumfiles, mem_fn(&SumFile::totalSize), Q_UINT64_C(0));
653 if (!canceled) {
656 // Step 2: perform work (with progress reporting):
659 const QStringList env = c_lang_environment();
661 // re-scale 'total' to fit into ints (wish QProgressDialog would use quint64...)
662 const quint64 factor = total / std::numeric_limits<int>::max() + 1 ;
664 quint64 done = 0;
665 Q_FOREACH (const SumFile &sumFile, sumfiles) {
666 Q_EMIT progress(done / factor, total / factor,
667 i18n("Verifying checksums (%2) in %1", sumFile.checksumDefinition->label(), sumFile.dir.path()));
668 bool fatal = false;
669 const QString error = process(sumFile, &fatal, env, statusCb);
670 if (!error.isEmpty()) {
671 errors.push_back(error);
673 done += sumFile.totalSize;
674 if (fatal || canceled) {
675 break;
678 Q_EMIT progress(done / factor, total / factor, i18n("Done."));
683 locker.relock();
685 this->errors = errors;
687 // mutex unlocked by QMutexLocker
691 #include "moc_verifychecksumscontroller.cpp"
692 #include "verifychecksumscontroller.moc"
694 #endif // QT_NO_DIRMODEL