Fix the Oracle refresh verdict and cover the parser write path

A background refresh that already installed cards.xml exited 1 when the
token save afterwards failed, so the client kept its old in-memory
database and reported the update as failed. Return the same verdict
main() already computes: a reject after the database was written is a
successful update.

Cover the writer Oracle actually uses: add a short-write death test for
CockatriceXml4Parser::saveToFile, which reverted to QFile would truncate
an installed database without any test noticing.

Route a failed background token download through reject() instead of a
modal box on a hidden wizard, which left the client waiting with no
timeout and blocked later updates. Test the first non-space byte in
place rather than copying the payload, and report an import refusal with
the importer's own message.
This commit is contained in:
Lukas Brübach 2026-11-04 17:27:59 +01:00
parent b719ee4006
commit 18b2896bf5
4 changed files with 89 additions and 6 deletions

View file

@ -143,9 +143,11 @@ void OracleWizard::reject()
QWizard::reject();
// A hidden wizard does not close the application's last visible window.
// Stop background refreshes explicitly so the hosting client receives failure.
// Stop background refreshes explicitly so the hosting client receives a verdict,
// using the same rule as main(): a reject after the database was written is still
// a successful update, only a reject that wrote nothing counts as a failure.
if (backgroundMode) {
QCoreApplication::exit(1);
QCoreApplication::exit(hasSavedDatabase() ? 0 : 1);
}
}

View file

@ -44,6 +44,7 @@
#include <QWizard>
#include <QtConcurrentRun>
#include <QtGlobal>
#include <algorithm>
#include <cstdio>
#include <functional>
#include <libcockatrice/settings/personal_settings.h>
@ -170,7 +171,12 @@ LoadSetsResult loadSetsData(const QPointer<OracleImporter> &importer, QByteArray
break;
}
if (data.trimmed().startsWith("<")) {
// Test the first non-space byte in place: trimmed() would duplicate the whole
// payload, which on the plain-XML path is the largest download we handle.
const auto firstNonSpace = std::find_if_not(data.cbegin(), data.cend(),
[](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; });
const bool looksLikeXml = firstNonSpace != data.cend() && *firstNonSpace == '<';
if (looksLikeXml) {
result.errorMessage = OracleDatabase::validateXml(data);
if (!result.errorMessage.isEmpty()) {
return result;
@ -916,7 +922,10 @@ bool SaveSetsPage::validatePage()
wizard()->xmlData.clear();
} else if (!wizard()->importer->saveToFile(fileName, wizard()->getCardSourceUrl(),
wizard()->getCardSourceVersion())) {
return failSave(tr("The file could not be saved to %1").arg(fileName));
// The importer refuses to write a database whose import did not finish, and
// reports the reason there; do not dress an import failure up as a disk failure.
const QString importError = wizard()->importer->getImportError();
return failSave(importError.isEmpty() ? tr("The file could not be saved to %1").arg(fileName) : importError);
}
wizard()->markDatabaseSaved();

View file

@ -185,9 +185,17 @@ void SimpleDownloadFilePage::actDownloadFinished()
}
QNetworkReply::NetworkError errorCode = reply->error();
if (errorCode != QNetworkReply::NoError) {
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
wizard()->enableButtons();
const QString message = tr("Network error: %1.").arg(reply->errorString());
reply->deleteLater();
// A hidden wizard cannot show a modal box: without an explicit stop it would sit
// here forever and the hosting client would wait with no timeout.
if (wizard()->backgroundMode) {
qWarning() << message;
wizard()->reject();
return;
}
QMessageBox::critical(this, tr("Error"), message);
wizard()->enableButtons();
return;
}