File indexing completed on 2024-04-28 17:05:53

0001 /*
0002     SPDX-FileCopyrightText: 2005 Shie Erlich <erlich@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2007-2008 Csaba Karai <cskarai@freemail.hu>
0004     SPDX-FileCopyrightText: 2008 Jonas Bähr <jonas.baehr@web.de>
0005     SPDX-FileCopyrightText: 2005-2022 Krusader Krew <https://krusader.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "checksumdlg.h"
0011 
0012 #include "../GUI/krlistwidget.h"
0013 #include "../GUI/krtreewidget.h"
0014 #include "../icon.h"
0015 #include "../krglobal.h"
0016 #include "../krservices.h"
0017 #include "../krusader.h"
0018 
0019 // QtCore
0020 #include <QDirIterator>
0021 #include <QFile>
0022 #include <QFileInfo>
0023 #include <QList>
0024 #include <QMap>
0025 #include <QTextStream>
0026 // QtWidgets
0027 #include <QDialogButtonBox>
0028 #include <QFileDialog>
0029 #include <QGridLayout>
0030 #include <QHBoxLayout>
0031 #include <QProgressBar>
0032 
0033 #include <QtConcurrent/QtConcurrentRun> // krazy:exclude=includes
0034 
0035 #include <KI18n/KLocalizedString>
0036 #include <KIOWidgets/KUrlRequester>
0037 #include <KWidgetsAddons/KMessageBox>
0038 
0039 void Checksum::startCreationWizard(const QString &path, const QStringList &files)
0040 {
0041     if (files.isEmpty())
0042         return;
0043 
0044     QDialog *dialog = new CHECKSUM_::CreateWizard(path, files);
0045     dialog->show();
0046 }
0047 
0048 void Checksum::startVerifyWizard(const QString &path, const QString &checksumFile)
0049 {
0050     QDialog *dialog = new CHECKSUM_::VerifyWizard(path, checksumFile);
0051     dialog->show();
0052 }
0053 
0054 namespace CHECKSUM_
0055 {
0056 
0057 bool stopListFiles;
0058 // async operation invoked by QtConcurrent::run in creation wizard
0059 QStringList listFiles(const QString &path, const QStringList &fileNames)
0060 {
0061     const QDir baseDir(path);
0062     QStringList allFiles;
0063     for (const QString &fileName : fileNames) {
0064         if (stopListFiles)
0065             return QStringList();
0066 
0067         QDir subDir = QDir(baseDir.filePath(fileName));
0068         if (subDir.exists()) {
0069             subDir.setFilter(QDir::Files);
0070             QDirIterator it(subDir, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
0071             while (it.hasNext()) {
0072                 if (stopListFiles)
0073                     return QStringList();
0074 
0075                 allFiles << baseDir.relativeFilePath(it.next());
0076             }
0077         } else {
0078             // assume this is a file
0079             allFiles << fileName;
0080         }
0081     }
0082     return allFiles;
0083 }
0084 
0085 // ------------- Checksum Process
0086 
0087 ChecksumProcess::ChecksumProcess(QObject *parent, const QString &path)
0088     : KProcess(parent)
0089     , m_tmpOutFile(QDir::tempPath() + QLatin1String("/krusader_XXXXXX.stdout"))
0090     , m_tmpErrFile(QDir::tempPath() + QLatin1String("/krusader_XXXXXX.stderr"))
0091 {
0092     m_tmpOutFile.open(); // necessary to create the filename
0093     m_tmpErrFile.open(); // necessary to create the filename
0094 
0095     setOutputChannelMode(KProcess::SeparateChannels); // without this the next 2 lines have no effect!
0096     setStandardOutputFile(m_tmpOutFile.fileName());
0097     setStandardErrorFile(m_tmpErrFile.fileName());
0098     setWorkingDirectory(path);
0099     connect(this, &ChecksumProcess::errorOccurred, this, &ChecksumProcess::slotError);
0100     connect(this, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &ChecksumProcess::slotFinished);
0101 }
0102 
0103 ChecksumProcess::~ChecksumProcess()
0104 {
0105     disconnect(this, nullptr, this, nullptr); // QProcess emits finished() on destruction
0106     close();
0107 }
0108 
0109 void ChecksumProcess::slotError(QProcess::ProcessError error)
0110 {
0111     if (error == QProcess::FailedToStart) {
0112         KMessageBox::error(nullptr, i18n("<qt>Could not start <b>%1</b>.</qt>", program().join(" ")));
0113     }
0114 }
0115 
0116 void ChecksumProcess::slotFinished(int, QProcess::ExitStatus exitStatus)
0117 {
0118     if (exitStatus != QProcess::NormalExit) {
0119         KMessageBox::error(nullptr, i18n("<qt>There was an error while running <b>%1</b>.</qt>", program().join(" ")));
0120         return;
0121     }
0122 
0123     // parse result files
0124     if (!KrServices::fileToStringList(&m_tmpOutFile, m_outputLines) || !KrServices::fileToStringList(&m_tmpErrFile, m_errorLines)) {
0125         KMessageBox::error(nullptr, i18n("Error reading stdout or stderr"));
0126         return;
0127     }
0128     emit resultReady();
0129 }
0130 
0131 // ------------- Generic Checksum Wizard
0132 
0133 ChecksumWizard::ChecksumWizard(const QString &path)
0134     : QWizard(krApp)
0135     , m_path(path)
0136     , m_process(nullptr)
0137 {
0138     setAttribute(Qt::WA_DeleteOnClose);
0139 
0140     // init the dictionary - pity it has to be manually
0141     m_checksumTools.insert("md5", "md5sum");
0142     m_checksumTools.insert("sha1", "sha1sum");
0143     m_checksumTools.insert("sha256", "sha256sum");
0144     m_checksumTools.insert("sha224", "sha224sum");
0145     m_checksumTools.insert("sha384", "sha384sum");
0146     m_checksumTools.insert("sha512", "sha512sum");
0147 
0148     connect(this, &QWizard::currentIdChanged, this, &ChecksumWizard::slotCurrentIdChanged);
0149 }
0150 
0151 ChecksumWizard::~ChecksumWizard()
0152 {
0153     if (m_process) {
0154         delete m_process;
0155     }
0156 }
0157 
0158 void ChecksumWizard::slotCurrentIdChanged(int id)
0159 {
0160     if (id == m_introId) {
0161         onIntroPage();
0162     } else if (id == m_progressId) {
0163         if (m_process) {
0164             // we are coming from the result page;
0165             delete m_process;
0166             m_process = nullptr;
0167             restart();
0168         } else {
0169             button(QWizard::BackButton)->hide();
0170             button(QWizard::NextButton)->hide();
0171             onProgressPage();
0172         }
0173     } else if (id == m_resultId) {
0174         onResultPage();
0175     }
0176 }
0177 
0178 QWizardPage *ChecksumWizard::createProgressPage(const QString &title)
0179 {
0180     auto *page = new QWizardPage;
0181 
0182     page->setTitle(title);
0183     page->setPixmap(QWizard::LogoPixmap, Icon("process-working").pixmap(32));
0184     page->setSubTitle(i18n("Please wait..."));
0185 
0186     auto *mainLayout = new QVBoxLayout;
0187     page->setLayout(mainLayout);
0188 
0189     // "busy" indicator
0190     auto *bar = new QProgressBar();
0191     bar->setRange(0, 0);
0192 
0193     mainLayout->addWidget(bar);
0194 
0195     return page;
0196 }
0197 
0198 bool ChecksumWizard::checkExists(const QString &type)
0199 {
0200     if (!KrServices::cmdExist(m_checksumTools[type])) {
0201         KMessageBox::error(this,
0202                            i18n("<qt>Krusader cannot find a checksum tool that handles %1 on your system. "
0203                                 "Please check the <b>Dependencies</b> page in Krusader's settings.</qt>",
0204                                 type));
0205         return false;
0206     }
0207     return true;
0208 }
0209 
0210 void ChecksumWizard::runProcess(const QString &type, const QStringList &args)
0211 {
0212     Q_ASSERT(m_process == nullptr);
0213 
0214     m_process = new ChecksumProcess(this, m_path);
0215     m_process->setProgram(KrServices::fullPathName(m_checksumTools[type]), args);
0216     // show next page (with results) (only) when process is done
0217     connect(m_process, &ChecksumProcess::resultReady, this, &QWizard::next);
0218     // run the process
0219     m_process->start();
0220 }
0221 
0222 void ChecksumWizard::addChecksumLine(KrTreeWidget *tree, const QString &line)
0223 {
0224     auto *item = new QTreeWidgetItem(tree);
0225     const int hashLength = line.indexOf(' '); // delimiter is either "  " or " *"
0226     item->setText(0, line.left(hashLength));
0227     QString fileName = line.mid(hashLength + 2);
0228     if (fileName.endsWith('\n'))
0229         fileName.chop(1);
0230     item->setText(1, fileName);
0231 }
0232 
0233 // ------------- Create Wizard
0234 
0235 CreateWizard::CreateWizard(const QString &path, const QStringList &_files)
0236     : ChecksumWizard(path)
0237     , m_fileNames(_files)
0238 {
0239     m_introId = addPage(createIntroPage());
0240     m_progressId = addPage(createProgressPage(i18n("Creating Checksums")));
0241     m_resultId = addPage(createResultPage());
0242 
0243     setButton(QWizard::FinishButton, QDialogButtonBox(QDialogButtonBox::Save).button(QDialogButtonBox::Save));
0244 
0245     connect(&m_listFilesWatcher, &QFutureWatcher<QStringList>::resultReadyAt, this, &CreateWizard::createChecksums);
0246 }
0247 
0248 QWizardPage *CreateWizard::createIntroPage()
0249 {
0250     auto *page = new QWizardPage;
0251 
0252     page->setTitle(i18n("Create Checksums"));
0253     page->setPixmap(QWizard::LogoPixmap, Icon("document-edit-sign").pixmap(32));
0254     page->setSubTitle(i18n("About to calculate checksum for the following files or folders:"));
0255 
0256     auto *mainLayout = new QVBoxLayout;
0257     page->setLayout(mainLayout);
0258 
0259     // file list
0260     auto *listWidget = new KrListWidget;
0261     listWidget->addItems(m_fileNames);
0262     mainLayout->addWidget(listWidget);
0263 
0264     // checksum method
0265     auto *hLayout = new QHBoxLayout;
0266 
0267     QLabel *methodLabel = new QLabel(i18n("Select the checksum method:"));
0268     hLayout->addWidget(methodLabel);
0269 
0270     m_methodBox = new KComboBox;
0271     // -- fill the combo with available methods
0272     for (const QString &type : m_checksumTools.keys())
0273         m_methodBox->addItem(type);
0274     m_methodBox->setFocus();
0275     hLayout->addWidget(m_methodBox);
0276 
0277     mainLayout->addLayout(hLayout);
0278 
0279     return page;
0280 }
0281 
0282 QWizardPage *CreateWizard::createResultPage()
0283 {
0284     auto *page = new QWizardPage;
0285 
0286     page->setTitle(i18n("Checksum Results"));
0287 
0288     auto *mainLayout = new QVBoxLayout;
0289     page->setLayout(mainLayout);
0290 
0291     m_hashesTreeWidget = new KrTreeWidget(this);
0292     m_hashesTreeWidget->setAllColumnsShowFocus(true);
0293     m_hashesTreeWidget->setHeaderLabels(QStringList() << i18n("Hash") << i18n("File"));
0294     mainLayout->addWidget(m_hashesTreeWidget);
0295 
0296     m_errorLabel = new QLabel(i18n("Errors received:"));
0297     mainLayout->addWidget(m_errorLabel);
0298 
0299     m_errorListWidget = new KrListWidget;
0300     mainLayout->addWidget(m_errorListWidget);
0301 
0302     m_onePerFileBox = new QCheckBox(i18n("Save one checksum file for each source file"));
0303     m_onePerFileBox->setChecked(false);
0304     mainLayout->addWidget(m_onePerFileBox);
0305 
0306     return page;
0307 }
0308 
0309 void CreateWizard::onIntroPage()
0310 {
0311     button(QWizard::NextButton)->show();
0312 }
0313 
0314 void CreateWizard::onProgressPage()
0315 {
0316     // first, get all files (recurse in directories) - async
0317     stopListFiles = false; // QFuture cannot cancel QtConcurrent::run
0318     connect(this, &CreateWizard::finished, this, [=]() {
0319         stopListFiles = true;
0320     });
0321     QFuture<QStringList> listFuture = QtConcurrent::run(listFiles, m_path, m_fileNames);
0322     m_listFilesWatcher.setFuture(listFuture);
0323 }
0324 
0325 void CreateWizard::createChecksums()
0326 {
0327     const QString type = m_methodBox->currentText();
0328     if (!checkExists(type)) {
0329         button(QWizard::BackButton)->show();
0330         return;
0331     }
0332 
0333     const QStringList &allFiles = m_listFilesWatcher.result();
0334     if (allFiles.isEmpty()) {
0335         KMessageBox::error(this, i18n("No files found"));
0336         button(QWizard::BackButton)->show();
0337         return;
0338     }
0339 
0340     runProcess(type, allFiles);
0341 
0342     // set suggested filename
0343     m_suggestedFilePath = QDir(m_path).filePath((m_fileNames.count() > 1 ? "checksum." : (m_fileNames[0] + '.')) + type);
0344 }
0345 
0346 void CreateWizard::onResultPage()
0347 {
0348     // hash tools display errors into stderr, so we'll use that to determine the result of the job
0349     const QStringList outputLines = m_process->stdOutput();
0350     const QStringList errorLines = m_process->errOutput();
0351     bool errors = !errorLines.isEmpty();
0352     bool successes = !outputLines.isEmpty();
0353 
0354     QWizardPage *page = currentPage();
0355     page->setPixmap(QWizard::LogoPixmap, Icon(errors || !successes ? "dialog-error" : "dialog-information").pixmap(32));
0356     page->setSubTitle(errors || !successes ? i18n("Errors were detected while creating the checksums") : i18n("Checksums were created successfully"));
0357 
0358     m_hashesTreeWidget->clear();
0359     m_hashesTreeWidget->setVisible(successes);
0360     if (successes) {
0361         for (const QString &line : outputLines)
0362             addChecksumLine(m_hashesTreeWidget, line);
0363         // m_hashesTreeWidget->sortItems(1, Qt::AscendingOrder);
0364     }
0365 
0366     m_errorLabel->setVisible(errors);
0367     m_errorListWidget->setVisible(errors);
0368     m_errorListWidget->clear();
0369     m_errorListWidget->addItems(errorLines);
0370 
0371     m_onePerFileBox->setEnabled(outputLines.size() > 1);
0372 
0373     button(QWizard::FinishButton)->setEnabled(successes);
0374 }
0375 
0376 bool CreateWizard::savePerFile()
0377 {
0378     const QString type = m_suggestedFilePath.mid(m_suggestedFilePath.lastIndexOf('.'));
0379 
0380     krApp->startWaiting(i18n("Saving checksum files..."), 0);
0381     for (const QString &line : m_process->stdOutput()) {
0382         const QString filename = line.mid(line.indexOf(' ') + 2) + type;
0383         if (!saveChecksumFile(QStringList() << line, filename)) {
0384             KMessageBox::error(this, i18n("Errors occurred while saving multiple checksums. Stopping"));
0385             krApp->stopWait();
0386             return false;
0387         }
0388     }
0389     krApp->stopWait();
0390 
0391     return true;
0392 }
0393 
0394 bool CreateWizard::saveChecksumFile(const QStringList &data, const QString &filename)
0395 {
0396     QString filePath = filename.isEmpty() ? m_suggestedFilePath : filename;
0397     if (filename.isEmpty() || QFile::exists(filePath)) {
0398         filePath = QFileDialog::getSaveFileName(this, QString(), filePath);
0399         if (filePath.isEmpty())
0400             return false; // user pressed cancel
0401     }
0402 
0403     QFile file(filePath);
0404     if (file.open(QIODevice::WriteOnly)) {
0405         QTextStream stream(&file);
0406         for (const QString &line : data)
0407             stream << line << "\n";
0408         file.close();
0409     }
0410 
0411     if (file.error() != QFile::NoError) {
0412         KMessageBox::detailedError(this, i18n("Error saving file %1", filePath), file.errorString());
0413         return false;
0414     }
0415 
0416     return true;
0417 }
0418 
0419 void CreateWizard::accept()
0420 {
0421     const bool saved = m_onePerFileBox->isChecked() ? savePerFile() : saveChecksumFile(m_process->stdOutput());
0422     if (saved)
0423         QWizard::accept();
0424 }
0425 
0426 // ------------- Verify Wizard
0427 
0428 VerifyWizard::VerifyWizard(const QString &path, const QString &inputFile)
0429     : ChecksumWizard(path)
0430 {
0431     m_checksumFile = isSupported(inputFile) ? inputFile : path;
0432 
0433     m_introId = addPage(createIntroPage()); // m_checksumFile must already be set
0434     m_progressId = addPage(createProgressPage(i18n("Verifying Checksums")));
0435     m_resultId = addPage(createResultPage());
0436 }
0437 
0438 void VerifyWizard::slotChecksumPathChanged(const QString &path)
0439 {
0440     m_hashesTreeWidget->clear();
0441     button(QWizard::NextButton)->setEnabled(false);
0442 
0443     if (!isSupported(path))
0444         return;
0445 
0446     m_checksumFile = path;
0447 
0448     // parse and display checksum file content; only for the user, parsed values are not used
0449     m_hashesTreeWidget->clear();
0450     QFile file(m_checksumFile);
0451     if (file.open(QFile::ReadOnly)) {
0452         QTextStream inStream(&file);
0453         while (!inStream.atEnd()) {
0454             addChecksumLine(m_hashesTreeWidget, file.readLine());
0455         }
0456     }
0457     file.close();
0458 
0459     button(QWizard::NextButton)->setEnabled(true);
0460 }
0461 
0462 QWizardPage *VerifyWizard::createIntroPage()
0463 {
0464     auto *page = new QWizardPage;
0465 
0466     page->setTitle(i18n("Verify Checksum File"));
0467     page->setPixmap(QWizard::LogoPixmap, Icon("document-edit-verify").pixmap(32));
0468     page->setSubTitle(i18n("About to verify the following checksum file"));
0469 
0470     auto *mainLayout = new QVBoxLayout;
0471     page->setLayout(mainLayout);
0472 
0473     // checksum file
0474     auto *hLayout = new QHBoxLayout;
0475     QLabel *checksumFileLabel = new QLabel(i18n("Checksum file:"));
0476     hLayout->addWidget(checksumFileLabel);
0477 
0478     auto *checksumFileReq = new KUrlRequester;
0479     QString typesFilter;
0480     for (const QString &ext : m_checksumTools.keys())
0481         typesFilter += ("*." + ext + ' ');
0482     checksumFileReq->setFilter(typesFilter);
0483     checksumFileReq->setText(m_checksumFile);
0484     checksumFileReq->setFocus();
0485     connect(checksumFileReq, &KUrlRequester::textChanged, this, &VerifyWizard::slotChecksumPathChanged);
0486     hLayout->addWidget(checksumFileReq);
0487 
0488     mainLayout->addLayout(hLayout);
0489 
0490     // content of checksum file
0491     m_hashesTreeWidget = new KrTreeWidget(page);
0492     m_hashesTreeWidget->setAllColumnsShowFocus(true);
0493     m_hashesTreeWidget->setHeaderLabels(QStringList() << i18n("Hash") << i18n("File"));
0494     mainLayout->addWidget(m_hashesTreeWidget);
0495 
0496     return page;
0497 }
0498 
0499 QWizardPage *VerifyWizard::createResultPage()
0500 {
0501     auto *page = new QWizardPage;
0502 
0503     page->setTitle(i18n("Verify Result"));
0504 
0505     auto *mainLayout = new QVBoxLayout;
0506     page->setLayout(mainLayout);
0507 
0508     m_outputLabel = new QLabel(i18n("Result output:"));
0509     mainLayout->addWidget(m_outputLabel);
0510 
0511     m_outputListWidget = new KrListWidget;
0512     mainLayout->addWidget(m_outputListWidget);
0513 
0514     return page;
0515 }
0516 
0517 void VerifyWizard::onIntroPage()
0518 {
0519     // cannot do this in constructor: NextButton->hide() is overridden
0520     slotChecksumPathChanged(m_checksumFile);
0521 }
0522 
0523 void VerifyWizard::onProgressPage()
0524 {
0525     // verify checksum file...
0526     const QString extension = QFileInfo(m_checksumFile).suffix();
0527     if (!checkExists(extension)) {
0528         button(QWizard::BackButton)->show();
0529         return;
0530     }
0531 
0532     runProcess(extension,
0533                QStringList() << "--strict"
0534                              << "-c" << m_checksumFile);
0535 }
0536 
0537 void VerifyWizard::onResultPage()
0538 {
0539     // better not only trust error output
0540     const bool errors = m_process->exitCode() != 0 || !m_process->errOutput().isEmpty();
0541 
0542     QWizardPage *page = currentPage();
0543     page->setPixmap(QWizard::LogoPixmap, Icon(errors ? "dialog-error" : "dialog-information").pixmap(32));
0544     page->setSubTitle(errors ? i18n("Errors were detected while verifying the checksums") : i18n("Checksums were verified successfully"));
0545 
0546     // print everything, errors first
0547     m_outputListWidget->clear();
0548     m_outputListWidget->addItems(m_process->errOutput() + m_process->stdOutput());
0549 
0550     button(QWizard::FinishButton)->setEnabled(!errors);
0551 }
0552 
0553 bool VerifyWizard::isSupported(const QString &path)
0554 {
0555     const QFileInfo fileInfo(path);
0556     return fileInfo.isFile() && m_checksumTools.contains(fileInfo.suffix());
0557 }
0558 
0559 } // NAMESPACE CHECKSUM_