[CardDB] Keep parse-time set filtering and bust the cache on enablement changes (#7448)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

The parsers keep dropping printings for disabled sets so that disabling a
set hides them everywhere, not just in deck editor search. The cached parse
output therefore depends on enablement, which the source hash did not cover:
the first parse froze whatever enablement state existed at that moment and
later toggles hit the stale cache.

- Fold a parser schema version and the sorted enabled set names into the
  binary cache source hash so toggling a set or enabling newly discovered
  sets (including custom ones) forces a re-parse on the next load.
- Harden the legacy set migration to only write keys that exist so missing
  legacy values can no longer clobber cardDatabase.ini and silently disable
  sets on upgrade.
- Make CardDatabase::reloadCardDatabasesAndNotify() bypass the binary cache
  so an explicit reload always re-reads the files on disk, even when their
  size/mtime hash still matches.
- Cover cache invalidation end to end: sandboxed loads assert that disabling
  a set, adding a custom database and enabling a fresh set each rewrite the
  cache, that a no-op load keeps it, and that an explicit reload picks up
  edits the source hash cannot see.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-10-09 06:09:18 +02:00 • committed by GitHub
parent aaafa72c0e
commit 205e1ecebf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 366 additions and 14 deletions

View file

@ -6,6 +6,7 @@
#include "libcockatrice/card/database/card_database_data.h"
#include "libcockatrice/card/set/card_set.h"
#include "libcockatrice/interfaces/interface_card_database_path_provider.h"
#include "libcockatrice/interfaces/interface_card_set_priority_controller.h"
#include "parser/card_database_parser.h"
#include "parser/cockatrice_xml_3.h"
#include "parser/cockatrice_xml_4.h"
@ -28,7 +29,6 @@
#include <utility>
class ICardPreferenceProvider;
class ICardSetPriorityController;
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
CardDatabase *db,
@ -44,8 +44,7 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
// the finished snapshot is swapped into the live database on the GUI thread.
// when SettingsCache's path changes, trigger reloads
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
&CardDatabaseLoader::loadCardDatabases);
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this, [this] { loadCardDatabases(); });
}
CardDatabaseLoader::~CardDatabaseLoader()
@ -90,12 +89,12 @@ LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabas
return tempLoadStatus;
}
LoadStatus CardDatabaseLoader::loadCardDatabases()
LoadStatus CardDatabaseLoader::loadCardDatabases(bool ignoreCache)
{
return doLoadCardDatabases();
return doLoadCardDatabases(ignoreCache);
}
LoadStatus CardDatabaseLoader::doLoadCardDatabases()
LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache)
{
QMutexLocker locker(reloadDatabaseMutex);
@ -113,9 +112,14 @@ LoadStatus CardDatabaseLoader::doLoadCardDatabases()
LoadStatus loadStatus = NotLoaded;
// Try the binary cache first: a cache hit avoids re-parsing the (large) XML.
// An explicit reload skips the lookup so the files on disk are always
// re-read, even when their size/mtime hash still matches the cache.
const QStringList customPaths = collectCustomDatabasePaths();
const QByteArray sourceHash = computeSourceHash(customPaths);
if (loadFromCache(data, sourceHash)) {
if (ignoreCache) {
qCInfo(CardDatabaseLoadingLog) << "Ignoring binary cache, re-parsing source files";
}
if (!ignoreCache && loadFromCache(data, sourceHash)) {
qCInfo(CardDatabaseLoadingLog) << "Loaded card database from binary cache";
loadStatus = Ok;
} else {
@ -180,6 +184,12 @@ QByteArray CardDatabaseLoader::computeSourceHash(const QStringList &customPaths)
hash.addData(QCoreApplication::applicationVersion().toUtf8());
hash.addData(QByteArray(1, '\0'));
// Include parser schema version to invalidate caches when parser behavior changes.
// Increment this if the parser logic changes in a way that affects cached data.
static const char SCHEMA_VERSION[] = "carddb-parse-schema-v1";
hash.addData(QByteArray(SCHEMA_VERSION, sizeof(SCHEMA_VERSION) - 1));
hash.addData(QByteArray(1, '\0'));
const QStringList inputs = QStringList()
<< pathProvider->getCardDatabasePath() << pathProvider->getTokenDatabasePath()
<< pathProvider->getSpoilerCardDatabasePath() << customPaths;
@ -194,6 +204,17 @@ QByteArray CardDatabaseLoader::computeSourceHash(const QStringList &customPaths)
hash.addData(QByteArray(1, '\0'));
}
}
// The parsers drop printings of disabled sets at parse time, so the cached
// snapshot depends on enablement: fold the enabled set names in so that
// toggling a set invalidates the cache and forces a re-parse.
if (priorityController) {
const QStringList enabledSets = priorityController->getEnabledSetNames();
for (const QString &setName : enabledSets) {
hash.addData(setName.toUtf8());
hash.addData(QByteArray(1, '\0'));
}
}
return hash.result();
}