mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
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 / vcpkg baseline (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 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (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 Desktop / Prune caches (push) Waiting to run
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
* [Format] Small fixup. * [CardDB] Stop leaking the loader's mutexes. Both mutexes were heap-allocated through default member initializers and never freed, so every CardDatabase destruction leaked 16 bytes. LeakSanitizer flags this while discovering tests that create the database in main. Hold them by value instead, as the loader is a QObject and therefore neither copyable nor movable. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
291 lines
11 KiB
C++
291 lines
11 KiB
C++
#include "card_database_loader.h"
|
|
|
|
#include "card_database.h"
|
|
#include "card_database_cache.h"
|
|
#include "libcockatrice/card/card_info.h"
|
|
#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"
|
|
|
|
#include <QByteArray>
|
|
#include <QCoreApplication>
|
|
#include <QCryptographicHash>
|
|
#include <QDateTime>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QDirIterator>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QIODevice>
|
|
#include <QMessageLogger>
|
|
#include <QMutexLocker>
|
|
#include <QSharedPointer>
|
|
#include <QTime>
|
|
#include <qalgorithms.h>
|
|
#include <utility>
|
|
|
|
class ICardPreferenceProvider;
|
|
|
|
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
|
CardDatabase *db,
|
|
ICardDatabasePathProvider *_pathProvider,
|
|
ICardSetPriorityController *_priorityController)
|
|
: QObject(parent), database(db), pathProvider(_pathProvider), priorityController(_priorityController)
|
|
{
|
|
// instantiate available parsers here
|
|
availableParsers << new CockatriceXml4Parser(_priorityController);
|
|
availableParsers << new CockatriceXml3Parser(_priorityController);
|
|
|
|
// The load path parses into a snapshot and never emits per-card signals;
|
|
// 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, [this] {
|
|
loadCardDatabases();
|
|
// This reload runs synchronously on the emitting (GUI) thread, so the
|
|
// finished snapshot is already swapped in when it returns: enable any
|
|
// sets the newly pointed-at database introduced.
|
|
database->checkUnknownSets();
|
|
});
|
|
}
|
|
|
|
CardDatabaseLoader::~CardDatabaseLoader()
|
|
{
|
|
qDeleteAll(availableParsers);
|
|
availableParsers.clear();
|
|
}
|
|
|
|
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName, CardDatabaseData &data)
|
|
{
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
return FileError;
|
|
}
|
|
|
|
for (auto parser : availableParsers) {
|
|
file.reset();
|
|
if (parser->getCanParseFile(fileName, file)) {
|
|
file.reset();
|
|
parser->parseFileInto(file, data);
|
|
return Ok;
|
|
}
|
|
}
|
|
|
|
return Invalid;
|
|
}
|
|
|
|
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabaseData &data)
|
|
{
|
|
auto startTime = QTime::currentTime();
|
|
LoadStatus tempLoadStatus = NotLoaded;
|
|
if (!path.isEmpty()) {
|
|
QMutexLocker locker(&loadFromFileMutex);
|
|
tempLoadStatus = loadFromFile(path, data);
|
|
}
|
|
|
|
int msecs = startTime.msecsTo(QTime::currentTime());
|
|
qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus
|
|
<< "Cards =" << data.cards.size() << "Sets =" << data.sets.size()
|
|
<< QString("%1ms").arg(msecs);
|
|
|
|
return tempLoadStatus;
|
|
}
|
|
|
|
LoadStatus CardDatabaseLoader::loadCardDatabases(bool ignoreCache)
|
|
{
|
|
return doLoadCardDatabases(ignoreCache);
|
|
}
|
|
|
|
LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache)
|
|
{
|
|
QMutexLocker locker(&reloadDatabaseMutex);
|
|
|
|
if (!database) {
|
|
qCWarning(CardDatabaseLoadingLog) << "Loader has no database pointer";
|
|
emit loadingFailed();
|
|
return FileError;
|
|
}
|
|
emit loadingStarted();
|
|
qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started";
|
|
|
|
ICardDatabaseParser::clearSetlist();
|
|
|
|
CardDatabaseData data;
|
|
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 (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 {
|
|
loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath(), data); // main card database
|
|
if (loadStatus == Ok) {
|
|
loadCardDatabase(pathProvider->getTokenDatabasePath(), data); // tokens database
|
|
loadCardDatabase(pathProvider->getSpoilerCardDatabasePath(), data); // spoilers database
|
|
|
|
// find all custom card databases, recursively & following symlinks
|
|
// then load them alphabetically
|
|
for (int i = 0; i < customPaths.size(); ++i) {
|
|
const auto &p = customPaths.at(i);
|
|
qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")";
|
|
loadCardDatabase(p, data);
|
|
}
|
|
|
|
// A snapshot parsed while no set is enabled has had every printing
|
|
// stripped by the parsers, and the cache reader re-derives enablement
|
|
// from the priority controller, so such an entry can never be reused
|
|
// meaningfully. Skip writing it so a first-run (blank config) load
|
|
// cannot poison the cache with an empty snapshot. The check mirrors
|
|
// what the parsers saw: any set still reporting enabled means the
|
|
// snapshot is complete for the current enablement.
|
|
bool anySetEnabled = false;
|
|
for (const CardSetPtr &set : data.sets) {
|
|
if (set && set->getEnabled()) {
|
|
anySetEnabled = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!data.sets.isEmpty() && !anySetEnabled) {
|
|
qCInfo(CardDatabaseLoadingLog) << "Skipping binary cache: no set is enabled yet, snapshot was stripped";
|
|
} else if (!saveToCache(data, sourceHash)) {
|
|
qCWarning(CardDatabaseLoadingLog) << "Failed to write binary cache to" << cachePath();
|
|
}
|
|
}
|
|
}
|
|
|
|
// AFTER all the cards have been loaded: resolve the reverse-related tags
|
|
// against the fully-built snapshot.
|
|
if (loadStatus == Ok) {
|
|
database->refreshCachedReverseRelatedCards(data.cards);
|
|
|
|
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
|
|
// NOTE: checkUnknownSets() is intentionally NOT called here. During the
|
|
// front-loaded parse in main() MainWindow does not exist yet, so
|
|
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled would be
|
|
// emitted with no receivers, and enabling the sets up-front would
|
|
// suppress the first-run experience MainWindow::startupConfigCheck()
|
|
// and the onboarding wizard are designed around. Callers that load
|
|
// after startup own the check (and any reload it triggers).
|
|
emit databaseDataReady(std::move(data));
|
|
emit loadingFinished();
|
|
} else {
|
|
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
|
|
emit loadingFailed(); // bring up the settings dialog
|
|
}
|
|
|
|
return loadStatus;
|
|
}
|
|
|
|
QString CardDatabaseLoader::cachePath() const
|
|
{
|
|
return pathProvider->getCardDatabasePath() + ".cache";
|
|
}
|
|
|
|
QByteArray CardDatabaseLoader::computeSourceHash(const QStringList &customPaths) const
|
|
{
|
|
// Hash over the paths, sizes and modification times of every input file so
|
|
// the cache invalidates when any source changes. Cheap (no content read).
|
|
QCryptographicHash hash(QCryptographicHash::Sha256);
|
|
|
|
// Include the application version so parser changes automatically invalidate
|
|
// old caches even when the XML files are byte-identical.
|
|
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;
|
|
for (const QString &path : inputs) {
|
|
QFileInfo info(path);
|
|
if (info.exists()) {
|
|
hash.addData(path.toUtf8());
|
|
hash.addData(QByteArray(1, '\0'));
|
|
hash.addData(QByteArray::number(info.size()));
|
|
hash.addData(QByteArray(1, '\0'));
|
|
hash.addData(QByteArray::number(info.lastModified().toSecsSinceEpoch()));
|
|
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();
|
|
}
|
|
|
|
bool CardDatabaseLoader::loadFromCache(CardDatabaseData &data, const QByteArray &sourceHash)
|
|
{
|
|
if (sourceHash.isEmpty()) {
|
|
return false;
|
|
}
|
|
return CardDatabaseCache::read(cachePath(), data, sourceHash, priorityController);
|
|
}
|
|
|
|
bool CardDatabaseLoader::saveToCache(const CardDatabaseData &data, const QByteArray &sourceHash)
|
|
{
|
|
if (sourceHash.isEmpty()) {
|
|
return false;
|
|
}
|
|
return CardDatabaseCache::write(cachePath(), data, sourceHash);
|
|
}
|
|
|
|
QStringList CardDatabaseLoader::collectCustomDatabasePaths() const
|
|
{
|
|
QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files,
|
|
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
|
|
|
QStringList paths;
|
|
while (it.hasNext()) {
|
|
paths << it.next();
|
|
}
|
|
paths.sort();
|
|
return paths;
|
|
}
|
|
|
|
bool CardDatabaseLoader::saveCustomTokensToFile()
|
|
{
|
|
if (!database) {
|
|
qCWarning(CardDatabaseLog) << "saveCustomTokensToFile: database pointer missing";
|
|
return false;
|
|
}
|
|
|
|
QString fileName = pathProvider->getCustomCardDatabasePath() + "/" + CardSet::TOKENS_SETNAME + ".xml";
|
|
|
|
SetNameMap tmpSets;
|
|
CardSetPtr customTokensSet = database->getSet(CardSet::TOKENS_SETNAME);
|
|
tmpSets.insert(CardSet::TOKENS_SETNAME, customTokensSet);
|
|
|
|
CardNameMap tmpCards;
|
|
for (const CardInfoPtr &card : database->cards) {
|
|
if (card->getSets().contains(CardSet::TOKENS_SETNAME)) {
|
|
tmpCards.insert(card->getName(), card);
|
|
}
|
|
}
|
|
|
|
availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName);
|
|
return true;
|
|
}
|