mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 10:23:02 -07:00
[Oracle/Client] Report card database download progress
Card database updates ran invisibly: MTGJSON parsing spun an indeterminate bar on the UI thread and the set import blocked the window, while the onboarding wizard spawned `oracle -b` with no progress to show at all. - Add byte-level scan progress to `RawJson::scanSetRanges` via an optional callback, throttled to ~100 reports per scan. - Emit `OracleImporter::dataReadProgress` during the scan and import sets on a worker thread, driving the wizard's progress bar per set. - With `-b`, write machine-readable `PROGRESS <stage> <done> <total>` lines to stdout for the download/scan/import stages; stderr keeps the log output. - Parse the oracle stdout in `MainWindow` and forward it to the onboarding wizard, giving the card database step a determinate bar with stage-specific status text. - Guard the async workers against the wizard being closed mid-run. - Add Google Test coverage for scan progress reporting.
This commit is contained in:
parent
ada774f5cc
commit
c47dc578e0
15 changed files with 510 additions and 98 deletions
|
|
@ -47,8 +47,14 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s
|
|||
|
||||
bool OracleImporter::readSetsFromByteArray(QByteArray data)
|
||||
{
|
||||
const RawJson::ScanProgressCallback progress =
|
||||
progressReporting ? RawJson::ScanProgressCallback([this](qsizetype bytesRead, qsizetype totalBytes) {
|
||||
emit dataReadProgress(static_cast<int>(bytesRead), static_cast<int>(totalBytes));
|
||||
})
|
||||
: RawJson::ScanProgressCallback();
|
||||
|
||||
RawJson::ScanError error;
|
||||
const QList<RawJson::SetRange> ranges = RawJson::scanSetRanges(data, &error);
|
||||
const QList<RawJson::SetRange> ranges = RawJson::scanSetRanges(data, &error, progress);
|
||||
if (error.isError()) {
|
||||
qDebug() << "error: RawJson::scanSetRanges():" << error.message;
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -155,6 +155,14 @@ private:
|
|||
*/
|
||||
QByteArray rawSetsData;
|
||||
|
||||
/**
|
||||
* Whether readSetsFromByteArray() should report scan progress via
|
||||
* dataReadProgress. A background run routes that signal to stdout (for the
|
||||
* hosting Cockatrice client to parse); the flag exists to skip the scanner
|
||||
* instrumentation entirely when no consumer needs it.
|
||||
*/
|
||||
bool progressReporting = true;
|
||||
|
||||
CardInfoPtr addCard(QString name,
|
||||
const QString &text,
|
||||
bool isToken,
|
||||
|
|
@ -167,6 +175,18 @@ signals:
|
|||
|
||||
public:
|
||||
explicit OracleImporter(QObject *parent = nullptr);
|
||||
/**
|
||||
* @brief Controls whether readSetsFromByteArray() instruments the raw scan.
|
||||
*
|
||||
* When enabled (the default) the raw scanner reports progress via
|
||||
* dataReadProgress(), which an interactive wizard shows on its progress bar
|
||||
* and a background run routes to stdout for the hosting client. Switch it
|
||||
* off only when nothing will consume scan progress.
|
||||
*/
|
||||
void setProgressReporting(bool enabled)
|
||||
{
|
||||
progressReporting = enabled;
|
||||
}
|
||||
/**
|
||||
* Scans the given JSON document for set metadata. Takes the data by value so
|
||||
* the wizard can hand over its decompressed buffer without copying it.
|
||||
|
|
|
|||
|
|
@ -110,6 +110,13 @@ void OracleWizard::accept()
|
|||
QDialog::accept();
|
||||
}
|
||||
|
||||
void OracleWizard::runInBackground()
|
||||
{
|
||||
backgroundMode = true;
|
||||
hide();
|
||||
currentPage()->initializePage();
|
||||
}
|
||||
|
||||
void OracleWizard::enableButtons()
|
||||
{
|
||||
button(QWizard::NextButton)->setDisabled(false);
|
||||
|
|
|
|||
|
|
@ -52,12 +52,7 @@ public:
|
|||
}
|
||||
bool saveTokensToFile(const QString &fileName);
|
||||
|
||||
void runInBackground()
|
||||
{
|
||||
backgroundMode = true;
|
||||
hide();
|
||||
currentPage()->initializePage();
|
||||
}
|
||||
void runInBackground();
|
||||
|
||||
public:
|
||||
OracleImporter *importer;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <QBuffer>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QGridLayout>
|
||||
|
|
@ -20,14 +21,17 @@
|
|||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPointer>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QScrollBar>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextEdit>
|
||||
#include <QTextStream>
|
||||
#include <QtConcurrent>
|
||||
#include <QtGui>
|
||||
#include <cstdio>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
#ifdef HAS_LZMA
|
||||
|
|
@ -53,6 +57,20 @@
|
|||
#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Emits one machine-readable background-run progress line to stdout.
|
||||
*
|
||||
* Used only in background mode, so the hosting Cockatrice client can parse these
|
||||
* lines to drive a determinate progress bar. stderr stays reserved for
|
||||
* human-readable log output.
|
||||
*/
|
||||
static void emitBackgroundProgress(const char *stage, qint64 done, qint64 total)
|
||||
{
|
||||
QTextStream out(stdout);
|
||||
out << "PROGRESS " << stage << ' ' << done << ' ' << total << '\n';
|
||||
out.flush();
|
||||
}
|
||||
|
||||
#define TOKENS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml"
|
||||
#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
|
||||
|
||||
|
|
@ -339,6 +357,9 @@ void LoadSetsPage::downloadSetsFile(const QUrl &url)
|
|||
|
||||
void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total)
|
||||
{
|
||||
if (wizard()->backgroundMode) {
|
||||
emitBackgroundProgress("download", received, total);
|
||||
}
|
||||
if (total > 0) {
|
||||
progressBar->setMaximum(static_cast<int>(total));
|
||||
progressBar->setValue(static_cast<int>(received));
|
||||
|
|
@ -384,6 +405,21 @@ void LoadSetsPage::actDownloadFinishedSetsFile()
|
|||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void LoadSetsPage::updateParsingProgress(int bytesRead, int totalBytes)
|
||||
{
|
||||
if (totalBytes <= 0) {
|
||||
return;
|
||||
}
|
||||
progressBar->setValue(bytesRead);
|
||||
const int percent = static_cast<int>((100.0 * bytesRead) / totalBytes);
|
||||
progressLabel->setText(tr("Parsing file (%1%)").arg(percent));
|
||||
}
|
||||
|
||||
void LoadSetsPage::scanProgressToStdout(int bytesRead, int totalBytes)
|
||||
{
|
||||
emitBackgroundProgress("scan", bytesRead, totalBytes);
|
||||
}
|
||||
|
||||
void LoadSetsPage::readSetsFromByteArray(QByteArray _data)
|
||||
{
|
||||
// show an infinite progressbar
|
||||
|
|
@ -469,9 +505,23 @@ void LoadSetsPage::readSetsFromByteArrayRef(QByteArray &_data)
|
|||
return;
|
||||
#endif
|
||||
} else if (_data.startsWith("{")) {
|
||||
// Start the computation.
|
||||
jsonData = std::move(_data);
|
||||
future = QtConcurrent::run([this] { return wizard()->importer->readSetsFromByteArray(std::move(jsonData)); });
|
||||
if (wizard()->backgroundMode) {
|
||||
qInfo() << tr("Parsing file");
|
||||
connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::scanProgressToStdout,
|
||||
Qt::UniqueConnection);
|
||||
} else {
|
||||
// Start the computation.
|
||||
progressBar->setRange(0, static_cast<int>(_data.size()));
|
||||
progressBar->setValue(0);
|
||||
progressLabel->setText(tr("Parsing file (0%)"));
|
||||
connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::updateParsingProgress,
|
||||
Qt::UniqueConnection);
|
||||
}
|
||||
|
||||
const QPointer<OracleImporter> importer = wizard()->importer;
|
||||
future = QtConcurrent::run([importer, data = std::move(_data)]() mutable {
|
||||
return importer ? importer->readSetsFromByteArray(std::move(data)) : false;
|
||||
});
|
||||
watcher.setFuture(future);
|
||||
} else if (_data.startsWith("<")) {
|
||||
// save xml file and don't do any processing
|
||||
|
|
@ -514,7 +564,16 @@ void LoadSetsPage::importFinished()
|
|||
progressLabel->hide();
|
||||
progressBar->hide();
|
||||
|
||||
if (wizard()->downloadedPlainXml || watcher.future().result()) {
|
||||
const bool hasData = wizard()->downloadedPlainXml || watcher.future().result();
|
||||
if (wizard()->backgroundMode) {
|
||||
if (!hasData) {
|
||||
qWarning() << tr("The file was retrieved successfully, but it does not contain any sets data.");
|
||||
}
|
||||
emit readyToContinue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasData) {
|
||||
wizard()->next();
|
||||
} else {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
|
|
@ -527,16 +586,22 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent)
|
|||
pathLabel = new QLabel(this);
|
||||
saveLabel = new QLabel(this);
|
||||
|
||||
progressBar = new QProgressBar(this);
|
||||
progressBar->hide();
|
||||
|
||||
defaultPathCheckBox = new QCheckBox(this);
|
||||
|
||||
messageLog = new QTextEdit(this);
|
||||
messageLog->setReadOnly(true);
|
||||
|
||||
auto *layout = new QGridLayout(this);
|
||||
layout->addWidget(messageLog, 0, 0);
|
||||
layout->addWidget(saveLabel, 1, 0);
|
||||
layout->addWidget(pathLabel, 2, 0);
|
||||
layout->addWidget(defaultPathCheckBox, 3, 0);
|
||||
layout->addWidget(progressBar, 0, 0);
|
||||
layout->addWidget(messageLog, 1, 0);
|
||||
layout->addWidget(saveLabel, 2, 0);
|
||||
layout->addWidget(pathLabel, 3, 0);
|
||||
layout->addWidget(defaultPathCheckBox, 4, 0);
|
||||
|
||||
connect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished);
|
||||
|
||||
setLayout(layout);
|
||||
}
|
||||
|
|
@ -549,27 +614,54 @@ void SaveSetsPage::cleanupPage()
|
|||
|
||||
void SaveSetsPage::initializePage()
|
||||
{
|
||||
messageLog->clear();
|
||||
|
||||
retranslateUi();
|
||||
if (wizard()->downloadedPlainXml) {
|
||||
messageLog->hide();
|
||||
} else {
|
||||
messageLog->show();
|
||||
connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress);
|
||||
|
||||
int setsImported = wizard()->importer->startImport();
|
||||
|
||||
// JSON data no longer needed after CardInfo objects are built
|
||||
wizard()->importer->releaseSetData();
|
||||
|
||||
if (setsImported == 0) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("No set has been imported."));
|
||||
progressBar->hide();
|
||||
if (wizard()->backgroundMode) {
|
||||
emit readyToContinue();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
messageLog->clear();
|
||||
messageLog->show();
|
||||
progressBar->show();
|
||||
progressBar->setRange(0, wizard()->importer->getSets().size());
|
||||
progressBar->setValue(0);
|
||||
|
||||
connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
wizard()->disableButtons();
|
||||
|
||||
const QPointer<OracleImporter> importer = wizard()->importer;
|
||||
importFuture = QtConcurrent::run([importer] { return importer ? importer->startImport() : 0; });
|
||||
importWatcher.setFuture(importFuture);
|
||||
}
|
||||
|
||||
void SaveSetsPage::importFinished()
|
||||
{
|
||||
wizard()->enableButtons();
|
||||
|
||||
const int setsImported = importWatcher.result();
|
||||
const QPointer<OracleImporter> importer = wizard()->importer;
|
||||
if (importer) {
|
||||
importer->releaseSetData();
|
||||
}
|
||||
|
||||
if (wizard()->backgroundMode) {
|
||||
if (setsImported == 0) {
|
||||
qWarning() << tr("No set has been imported.");
|
||||
}
|
||||
emit readyToContinue();
|
||||
return;
|
||||
}
|
||||
|
||||
progressBar->setValue(progressBar->maximum());
|
||||
|
||||
if (setsImported == 0) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("No set has been imported."));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -591,13 +683,27 @@ void SaveSetsPage::retranslateUi()
|
|||
setButtonText(QWizard::NextButton, tr("&Save"));
|
||||
}
|
||||
|
||||
void SaveSetsPage::updateTotalProgress(int cardsImported, int /* setIndex */, const QString &setName)
|
||||
void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName)
|
||||
{
|
||||
const bool background = wizard()->backgroundMode;
|
||||
const int totalSets = wizard()->importer->getSets().size();
|
||||
if (setName.isEmpty()) {
|
||||
messageLog->append("<b>" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) +
|
||||
"</b>");
|
||||
progressBar->setValue(progressBar->maximum());
|
||||
if (background) {
|
||||
qInfo() << tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size());
|
||||
emitBackgroundProgress("import", totalSets, totalSets);
|
||||
} else {
|
||||
messageLog->append("<b>" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) +
|
||||
"</b>");
|
||||
}
|
||||
} else {
|
||||
messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported));
|
||||
progressBar->setValue(setIndex);
|
||||
if (background) {
|
||||
qInfo() << tr("%1: %2 cards imported").arg(setName).arg(cardsImported);
|
||||
emitBackgroundProgress("import", setIndex, totalSets);
|
||||
} else {
|
||||
messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported));
|
||||
}
|
||||
}
|
||||
|
||||
messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum());
|
||||
|
|
|
|||
|
|
@ -83,13 +83,14 @@ private:
|
|||
|
||||
QFutureWatcher<bool> watcher;
|
||||
QFuture<bool> future;
|
||||
QByteArray jsonData;
|
||||
|
||||
private slots:
|
||||
void actLoadSetsFile();
|
||||
void actRestoreDefaultUrl();
|
||||
void actDownloadProgressSetsFile(qint64 received, qint64 total);
|
||||
void actDownloadFinishedSetsFile();
|
||||
void updateParsingProgress(int bytesRead, int totalBytes);
|
||||
void scanProgressToStdout(int bytesRead, int totalBytes);
|
||||
void importFinished();
|
||||
void zipDownloadFailed(const QString &message);
|
||||
};
|
||||
|
|
@ -103,16 +104,21 @@ public:
|
|||
|
||||
private:
|
||||
QTextEdit *messageLog;
|
||||
QProgressBar *progressBar;
|
||||
QCheckBox *defaultPathCheckBox;
|
||||
QLabel *pathLabel;
|
||||
QLabel *saveLabel;
|
||||
|
||||
QFutureWatcher<int> importWatcher;
|
||||
QFuture<int> importFuture;
|
||||
|
||||
protected:
|
||||
void initializePage() override;
|
||||
void cleanupPage() override;
|
||||
bool validatePage() override;
|
||||
|
||||
private slots:
|
||||
void importFinished();
|
||||
void updateTotalProgress(int cardsImported, int setIndex, const QString &setName);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "raw_json_scanner.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
|
|
@ -11,6 +12,43 @@ namespace
|
|||
// reason and reports DeepNesting).
|
||||
constexpr int kMaxNestingDepth = 1024;
|
||||
|
||||
/**
|
||||
* @brief Throttled byte-position reporting for scanSetRanges().
|
||||
*
|
||||
* Threaded through the skip walk so progress can be reported without materializing
|
||||
* the whole document. Reports are rate-limited so a GUI showing progress isn't
|
||||
* flooded with interrupts: a callback is invoked at most ~100 times per scan
|
||||
* regardless of element count.
|
||||
*/
|
||||
struct ScanProgress
|
||||
{
|
||||
const char *begin = nullptr;
|
||||
qsizetype size = 0;
|
||||
RawJson::ScanProgressCallback callback;
|
||||
qsizetype step = 1;
|
||||
qsizetype lastReported = 0;
|
||||
|
||||
/**
|
||||
* @brief Reports the scanner's absolute offset, unless within @p step bytes
|
||||
* of the previous report and not yet at the end of the document.
|
||||
*/
|
||||
void report(const char *p)
|
||||
{
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
const qsizetype offset = p - begin;
|
||||
if (offset == lastReported) {
|
||||
return; // the final element often already sits exactly at the end
|
||||
}
|
||||
if (offset - lastReported < step && offset < size) {
|
||||
return;
|
||||
}
|
||||
lastReported = offset;
|
||||
callback(offset, size);
|
||||
}
|
||||
};
|
||||
|
||||
inline bool isWhitespace(char c)
|
||||
{
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||
|
|
@ -296,9 +334,9 @@ bool skipNumber(const char *&p, const char *end)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool skipValue(const char *&p, const char *end, int depth);
|
||||
bool skipObject(const char *&p, const char *end, int depth);
|
||||
bool skipArray(const char *&p, const char *end, int depth);
|
||||
bool skipValue(const char *&p, const char *end, int depth, ScanProgress &scan);
|
||||
bool skipObject(const char *&p, const char *end, int depth, ScanProgress &scan);
|
||||
bool skipArray(const char *&p, const char *end, int depth, ScanProgress &scan);
|
||||
|
||||
bool skipPrimitive(const char *&p, const char *end)
|
||||
{
|
||||
|
|
@ -324,7 +362,7 @@ bool skipPrimitive(const char *&p, const char *end)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool skipObject(const char *&p, const char *end, int depth)
|
||||
bool skipObject(const char *&p, const char *end, int depth, ScanProgress &scan)
|
||||
{
|
||||
if (depth <= 0) {
|
||||
return false; // nest deeper than the cap
|
||||
|
|
@ -348,9 +386,10 @@ bool skipObject(const char *&p, const char *end, int depth)
|
|||
return false;
|
||||
}
|
||||
++p;
|
||||
if (!skipValue(p, end, depth - 1)) {
|
||||
if (!skipValue(p, end, depth - 1, scan)) {
|
||||
return false;
|
||||
}
|
||||
scan.report(p);
|
||||
p = skipWhitespace(p, end);
|
||||
if (p >= end) {
|
||||
return false;
|
||||
|
|
@ -367,7 +406,7 @@ bool skipObject(const char *&p, const char *end, int depth)
|
|||
}
|
||||
}
|
||||
|
||||
bool skipArray(const char *&p, const char *end, int depth)
|
||||
bool skipArray(const char *&p, const char *end, int depth, ScanProgress &scan)
|
||||
{
|
||||
if (depth <= 0) {
|
||||
return false; // nest deeper than the cap
|
||||
|
|
@ -379,9 +418,10 @@ bool skipArray(const char *&p, const char *end, int depth)
|
|||
return true;
|
||||
}
|
||||
for (;;) {
|
||||
if (!skipValue(p, end, depth - 1)) {
|
||||
if (!skipValue(p, end, depth - 1, scan)) {
|
||||
return false;
|
||||
}
|
||||
scan.report(p);
|
||||
p = skipWhitespace(p, end);
|
||||
if (p >= end) {
|
||||
return false;
|
||||
|
|
@ -398,7 +438,7 @@ bool skipArray(const char *&p, const char *end, int depth)
|
|||
}
|
||||
}
|
||||
|
||||
bool skipValue(const char *&p, const char *end, int depth)
|
||||
bool skipValue(const char *&p, const char *end, int depth, ScanProgress &scan)
|
||||
{
|
||||
p = skipWhitespace(p, end);
|
||||
if (p >= end) {
|
||||
|
|
@ -407,10 +447,10 @@ bool skipValue(const char *&p, const char *end, int depth)
|
|||
const char c = *p;
|
||||
if (c == '{') {
|
||||
// pass depth through: skipObject consumes the single decrement for this level
|
||||
return skipObject(p, end, depth);
|
||||
return skipObject(p, end, depth, scan);
|
||||
}
|
||||
if (c == '[') {
|
||||
return skipArray(p, end, depth);
|
||||
return skipArray(p, end, depth, scan);
|
||||
}
|
||||
// a primitive is a leaf, so it never wastes a nesting level
|
||||
return skipPrimitive(p, end);
|
||||
|
|
@ -422,7 +462,8 @@ bool skipValue(const char *&p, const char *end, int depth)
|
|||
* For each member invokes @p memberCallback with the key and the byte range of
|
||||
* its value. Advancing @p p is unaffected by the callback.
|
||||
*/
|
||||
template <typename F> bool forEachObjectMember(const char *&p, const char *end, int depth, F &&memberCallback)
|
||||
template <typename F>
|
||||
bool forEachObjectMember(const char *&p, const char *end, int depth, F &&memberCallback, ScanProgress &scan)
|
||||
{
|
||||
if (depth <= 0) {
|
||||
return false; // nest deeper than the cap
|
||||
|
|
@ -449,7 +490,7 @@ template <typename F> bool forEachObjectMember(const char *&p, const char *end,
|
|||
++p;
|
||||
const char *valueStart = skipWhitespace(p, end);
|
||||
const char *valueEnd = valueStart;
|
||||
if (!skipValue(valueEnd, end, depth - 1)) {
|
||||
if (!skipValue(valueEnd, end, depth - 1, scan)) {
|
||||
return false;
|
||||
}
|
||||
if (!memberCallback(key, valueStart, valueEnd)) {
|
||||
|
|
@ -473,7 +514,7 @@ template <typename F> bool forEachObjectMember(const char *&p, const char *end,
|
|||
}
|
||||
|
||||
// Counts the direct elements of an array value; returns -1 if the array is malformed.
|
||||
int countArrayElements(const char *p, const char *end, int depth)
|
||||
int countArrayElements(const char *p, const char *end, int depth, ScanProgress &scan)
|
||||
{
|
||||
if (depth <= 0) {
|
||||
return -1; // nest deeper than the cap
|
||||
|
|
@ -485,9 +526,10 @@ int countArrayElements(const char *p, const char *end, int depth)
|
|||
return 0;
|
||||
}
|
||||
for (;;) {
|
||||
if (!skipValue(p, end, depth - 1)) {
|
||||
if (!skipValue(p, end, depth - 1, scan)) {
|
||||
return -1;
|
||||
}
|
||||
scan.report(p);
|
||||
++count;
|
||||
p = skipWhitespace(p, end);
|
||||
if (p >= end) {
|
||||
|
|
@ -509,7 +551,7 @@ int countArrayElements(const char *p, const char *end, int depth)
|
|||
namespace RawJson
|
||||
{
|
||||
|
||||
QList<SetRange> scanSetRanges(const QByteArray &json, ScanError *error)
|
||||
QList<SetRange> scanSetRanges(const QByteArray &json, ScanError *error, const ScanProgressCallback &progress)
|
||||
{
|
||||
QList<SetRange> ranges;
|
||||
if (error) {
|
||||
|
|
@ -529,6 +571,14 @@ QList<SetRange> scanSetRanges(const QByteArray &json, ScanError *error)
|
|||
return fail(QStringLiteral("empty JSON document"));
|
||||
}
|
||||
|
||||
// Throttle reports to ~100 per scan so a GUI thread unthrottling them never
|
||||
// drowns under per-card interrupts, whatever the document size.
|
||||
ScanProgress scan;
|
||||
scan.begin = begin;
|
||||
scan.size = end - begin;
|
||||
scan.step = std::max<qsizetype>(1, scan.size / 100);
|
||||
scan.callback = progress;
|
||||
|
||||
const char *p = skipWhitespace(begin, end);
|
||||
if (p >= end || *p != '{') {
|
||||
return fail(QStringLiteral("top-level JSON must be an object"));
|
||||
|
|
@ -545,55 +595,57 @@ QList<SetRange> scanSetRanges(const QByteArray &json, ScanError *error)
|
|||
return false;
|
||||
}
|
||||
const char *setP = valueStart;
|
||||
const bool ok = forEachObjectMember(setP, valueEnd, kMaxNestingDepth - 1,
|
||||
[&](const QString &setCode, const char *setStart, const char *setEnd) {
|
||||
if (setStart >= setEnd || *setStart != '{') {
|
||||
malformedSetData = true;
|
||||
return false;
|
||||
}
|
||||
SetRange range;
|
||||
range.dataRange.start = setStart - begin;
|
||||
range.dataRange.length = setEnd - setStart;
|
||||
range.code = setCode;
|
||||
const bool ok = forEachObjectMember(
|
||||
setP, valueEnd, kMaxNestingDepth - 1,
|
||||
[&](const QString &setCode, const char *setStart, const char *setEnd) {
|
||||
if (setStart >= setEnd || *setStart != '{') {
|
||||
malformedSetData = true;
|
||||
return false;
|
||||
}
|
||||
SetRange range;
|
||||
range.dataRange.start = setStart - begin;
|
||||
range.dataRange.length = setEnd - setStart;
|
||||
range.code = setCode;
|
||||
|
||||
const char *memberP = setStart;
|
||||
const bool metaOk = forEachObjectMember(
|
||||
memberP, setEnd, kMaxNestingDepth - 2,
|
||||
[&](const QString &field, const char *fs, const char *fe) {
|
||||
if (field == QStringLiteral("code")) {
|
||||
return decodeStringMember(fs, fe, range.code);
|
||||
}
|
||||
if (field == QStringLiteral("name")) {
|
||||
return decodeStringMember(fs, fe, range.name);
|
||||
}
|
||||
if (field == QStringLiteral("type")) {
|
||||
return decodeStringMember(fs, fe, range.type);
|
||||
}
|
||||
if (field == QStringLiteral("releaseDate")) {
|
||||
return decodeStringMember(fs, fe, range.releaseDate);
|
||||
}
|
||||
if (field == QStringLiteral("cards")) {
|
||||
if (fs >= fe) {
|
||||
return false;
|
||||
}
|
||||
if (*fs != '[') {
|
||||
// e.g. "cards": null — treat as an empty array,
|
||||
// matching Qt's tolerance.
|
||||
return true;
|
||||
}
|
||||
range.dataRange.cardCount =
|
||||
countArrayElements(fs, fe, kMaxNestingDepth - 2);
|
||||
return range.dataRange.cardCount >= 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!metaOk) {
|
||||
malformedSetData = true;
|
||||
return false;
|
||||
}
|
||||
ranges.append(range);
|
||||
return true;
|
||||
});
|
||||
const char *memberP = setStart;
|
||||
const bool metaOk = forEachObjectMember(
|
||||
memberP, setEnd, kMaxNestingDepth - 2,
|
||||
[&](const QString &field, const char *fs, const char *fe) {
|
||||
if (field == QStringLiteral("code")) {
|
||||
return decodeStringMember(fs, fe, range.code);
|
||||
}
|
||||
if (field == QStringLiteral("name")) {
|
||||
return decodeStringMember(fs, fe, range.name);
|
||||
}
|
||||
if (field == QStringLiteral("type")) {
|
||||
return decodeStringMember(fs, fe, range.type);
|
||||
}
|
||||
if (field == QStringLiteral("releaseDate")) {
|
||||
return decodeStringMember(fs, fe, range.releaseDate);
|
||||
}
|
||||
if (field == QStringLiteral("cards")) {
|
||||
if (fs >= fe) {
|
||||
return false;
|
||||
}
|
||||
if (*fs != '[') {
|
||||
// e.g. "cards": null — treat as an empty array,
|
||||
// matching Qt's tolerance.
|
||||
return true;
|
||||
}
|
||||
range.dataRange.cardCount = countArrayElements(fs, fe, kMaxNestingDepth - 2, scan);
|
||||
return range.dataRange.cardCount >= 0;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
scan);
|
||||
if (!metaOk) {
|
||||
malformedSetData = true;
|
||||
return false;
|
||||
}
|
||||
ranges.append(range);
|
||||
return true;
|
||||
},
|
||||
scan);
|
||||
if (!ok) {
|
||||
malformedSetData = true;
|
||||
return false;
|
||||
|
|
@ -602,7 +654,7 @@ QList<SetRange> scanSetRanges(const QByteArray &json, ScanError *error)
|
|||
return true;
|
||||
};
|
||||
|
||||
if (!forEachObjectMember(p, end, kMaxNestingDepth, topLevelCallback)) {
|
||||
if (!forEachObjectMember(p, end, kMaxNestingDepth, topLevelCallback, scan)) {
|
||||
return fail(malformedSetData ? QStringLiteral("malformed set data") : QStringLiteral("malformed JSON"));
|
||||
}
|
||||
p = skipWhitespace(p, end);
|
||||
|
|
@ -615,6 +667,7 @@ QList<SetRange> scanSetRanges(const QByteArray &json, ScanError *error)
|
|||
if (ranges.isEmpty()) {
|
||||
return fail(QStringLiteral("no sets found in \"data\""));
|
||||
}
|
||||
scan.report(end);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
namespace RawJson
|
||||
{
|
||||
|
|
@ -42,6 +43,13 @@ struct ScanError
|
|||
QString message;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Optional progress callback receiving @c (bytesRead, totalBytes) while
|
||||
* the document is walked. Invoked from the scanning thread; the caller decides
|
||||
* how the throttled offsets are relayed to a GUI event loop.
|
||||
*/
|
||||
using ScanProgressCallback = std::function<void(qsizetype bytesRead, qsizetype totalBytes)>;
|
||||
|
||||
/**
|
||||
* @brief Scans a full MTGJSON document without materializing the JSON tree.
|
||||
*
|
||||
|
|
@ -67,9 +75,14 @@ struct ScanError
|
|||
* @param error Out parameter. Set to an error ScanError when the document
|
||||
* cannot be parsed, otherwise left empty. Passing a null
|
||||
* pointer disables error reporting.
|
||||
* @param progress Optional progress callback. When non-empty it is invoked as
|
||||
* the scanner advances through the document, throttled to a
|
||||
* tiny fraction of the total size.
|
||||
* @return The detected per-set ranges, or an empty list on failure.
|
||||
*/
|
||||
QList<SetRange> scanSetRanges(const QByteArray &json, ScanError *error = nullptr);
|
||||
QList<SetRange> scanSetRanges(const QByteArray &json,
|
||||
ScanError *error = nullptr,
|
||||
const ScanProgressCallback &progress = ScanProgressCallback());
|
||||
|
||||
} // namespace RawJson
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue