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_