From 91f4c49cb4720695ab0cd52ff8be2438fd00a112 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 21 Sep 2025 23:56:26 -0500 Subject: [PATCH] Gui: Improve file corruption checks --- src/Gui/DocumentRecovery.cpp | 120 +++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 11 deletions(-) diff --git a/src/Gui/DocumentRecovery.cpp b/src/Gui/DocumentRecovery.cpp index 7043724ecb..14799c622c 100644 --- a/src/Gui/DocumentRecovery.cpp +++ b/src/Gui/DocumentRecovery.cpp @@ -41,6 +41,7 @@ # include # include # include +# include # include # include @@ -148,8 +149,9 @@ public: Unknown = 0, /*!< The file is not available */ Created = 1, /*!< The file was created but not processed so far*/ Overage = 2, /*!< The recovery file is older than the actual project file */ - Success = 3, /*!< The file could be recovered */ - Failure = 4, /*!< The file could not be recovered */ + Corrupted = 3, /*!< The original file is corrupted */ + Success = 4, /*!< The file could be recovered */ + Failure = 5 /*!< The file could not be recovered */ }; struct Info { QString projectFile; @@ -186,13 +188,19 @@ DocumentRecovery::DocumentRecovery(const QList& dirs, QWidget* parent for (QList::const_iterator it = dirs.begin(); it != dirs.end(); ++it) { DocumentRecoveryPrivate::Info info = d_ptr->getRecoveryInfo(*it); - if (info.status == DocumentRecoveryPrivate::Created) { + if (info.status == DocumentRecoveryPrivate::Created || + info.status == DocumentRecoveryPrivate::Corrupted) { d_ptr->recoveryInfo << info; auto item = new QTreeWidgetItem(d_ptr->ui.treeWidget); item->setText(0, info.label); item->setToolTip(0, info.tooltip); - item->setText(1, tr("Not yet recovered")); + if (info.status == DocumentRecoveryPrivate::Corrupted) { + item->setText(1, tr("Original file corrupted")); + item->setForeground(1, QColor(170,0,0)); // TODO: Don't hardcode colors + } else { + item->setText(1, tr("Not yet recovered")); + } item->setToolTip(1, info.projectFile); d_ptr->ui.treeWidget->addTopLevelItem(item); } @@ -368,6 +376,9 @@ void DocumentRecoveryPrivate::writeRecoveryInfo(const DocumentRecoveryPrivate::I case Overage: str << " Deprecated\n"; break; + case Corrupted: + str << " Corrupted\n"; + break; case Success: str << " Success\n"; break; @@ -385,6 +396,7 @@ void DocumentRecoveryPrivate::writeRecoveryInfo(const DocumentRecoveryPrivate::I } } + DocumentRecoveryPrivate::Info DocumentRecoveryPrivate::getRecoveryInfo(const QFileInfo& fi) const { DocumentRecoveryPrivate::Info info; @@ -425,6 +437,8 @@ DocumentRecoveryPrivate::Info DocumentRecoveryPrivate::getRecoveryInfo(const QFi QString status = cfg[QStringLiteral("Status")]; if (status == QLatin1String("Deprecated")) info.status = DocumentRecoveryPrivate::Overage; + if (status == QLatin1String("Corrupted")) + info.status = DocumentRecoveryPrivate::Corrupted; else if (status == QLatin1String("Success")) info.status = DocumentRecoveryPrivate::Success; else if (status == QLatin1String("Failure")) @@ -434,14 +448,21 @@ DocumentRecoveryPrivate::Info DocumentRecoveryPrivate::getRecoveryInfo(const QFi if (info.status == DocumentRecoveryPrivate::Created) { // compare the modification dates QFileInfo fileInfo(info.fileName); - if (!info.fileName.isEmpty() && isValidProject(fileInfo)) { - QDateTime dateRecv = QFileInfo(file).lastModified(); - QDateTime dateProj = fileInfo.lastModified(); - if (dateRecv < dateProj) { - info.status = DocumentRecoveryPrivate::Overage; + if (!info.fileName.isEmpty()) { + if (isValidProject(fileInfo)) { + QDateTime dateRecv = QFileInfo(file).lastModified(); + QDateTime dateProj = fileInfo.lastModified(); + if (dateRecv < dateProj) { + info.status = DocumentRecoveryPrivate::Overage; + writeRecoveryInfo(info); + qWarning() << "Ignore recovery file " << file.toUtf8() + << " because it is older than the project file" + << info.fileName.toUtf8() << "\n"; + } + } else { + info.status = DocumentRecoveryPrivate::Corrupted; writeRecoveryInfo(info); - qWarning() << "Ignore recovery file " << file.toUtf8() - << " because it is older than the project file" << info.fileName.toUtf8() << "\n"; + qWarning() << "Original project file is corrupted: " << info.fileName << "\n"; } } } @@ -450,12 +471,89 @@ DocumentRecoveryPrivate::Info DocumentRecoveryPrivate::getRecoveryInfo(const QFi return info; } + +/// Rough check to see if the ZIP data is valid. No CRC calculation, just a fast iteration over the +/// contents to see if it seems basically OK. +bool zipDataIsValid(const QString& zipData) +{ + try { + zipios::ZipFile zf(zipData.toStdString()); + auto entries = zf.entries(); + int n = 0; + for (auto it = entries.begin(); it != entries.end(); ++it) { + auto s = zf.getInputStream(*it); + if (!s || !(*s)) { + return false; + } + ++n; + } + if (n == 0) { + return false; + } + return true; + } catch (...) { + return false; + } +} + +static zipios::ConstEntryPointer findEntry(zipios::ZipFile& zf, const std::string &name) { + auto entries = zf.entries(); + for (auto it = entries.begin(); it != entries.end(); ++it) + if ((*it)->getName() == name) return *it; + return {}; +} + +bool xmlFilesAreValid(const QString& fcstdFile) +{ + try { + zipios::ZipFile zf(fcstdFile.toStdString()); + auto doc = findEntry(zf, "Document.xml"); + if (!doc) { + return false; + } + { + auto s = zf.getInputStream(doc); + QByteArray bytes; + bytes.resize(0); + std::string tmp((std::istreambuf_iterator(*s)), std::istreambuf_iterator()); + QXmlStreamReader xr(QByteArray(tmp.data(), int(tmp.size()))); + while (!xr.atEnd()) xr.readNext(); + if (xr.hasError()) { + return false; + } + } + + // GuiDocument.xml is optional, but if it's present it must be well-formed + if (auto gui = findEntry(zf, "GuiDocument.xml")) { + auto s = zf.getInputStream(gui); + std::string tmp((std::istreambuf_iterator(*s)), std::istreambuf_iterator()); + QXmlStreamReader xr(QByteArray(tmp.data(), int(tmp.size()))); + while (!xr.atEnd()) xr.readNext(); + if (xr.hasError()) { + return false; + } + } + + return true; + } catch (...) { + return false; + } +} + bool DocumentRecoveryPrivate::isValidProject(const QFileInfo& fi) const { if (!fi.exists()) { return false; } + if (!zipDataIsValid(fi.fileName())) { + return false; + } + + if (!xmlFilesAreValid(fi.fileName())) { + return false; + } + App::ProjectFile project(fi.absoluteFilePath().toStdString()); return project.loadDocument(); }