Compare commits

...

2 commits

Author SHA1 Message Date
BruebachL
0fe4fd0d8c
[CardDB] Guard guessCard and findPrintingWithId against missing cards (#7592)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Desktop / Prune caches (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / Ubuntu 26.04 cover (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
* [CardDB] Guard guessCard and findPrintingWithId against missing cards

guessCard() dereferenced a null CardInfoPtr through findPrintingWithId() when the name was unknown and a providerId was given. Return an empty ExactCard instead, and make findPrintingWithId() null-safe for every caller.

Add regression tests covering both paths.

* [CardDB] Rely on PrintingInfo's default null set in findPrintingWithId

The explicit nullptr was redundant: the constructor already defaults
the set to null.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-20 21:33:56 +02:00
BruebachL
9980c90cea
[Filter] Allow spaces around the colon in search queries (#7529)
* [Filter] Allow spaces around the colon in search queries

* [Filter] Keep whitespace out of the color-identity operator

Allowing `ws?` between `color` and the identity marker made `c i: g`
parse as identity green, changing the meaning of text that previously
read as a generic `c` plus a field query on `i`. Whitespace belongs
around the separator, not inside an operator token: `ci : g` and
`ci: g` stay valid, `c i: g` keeps its old parse.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-20 21:20:10 +02:00
4 changed files with 78 additions and 9 deletions

View file

@ -133,6 +133,10 @@ ExactCard CardDatabaseQuerier::getCard(const CardRef &cardRef) const
ExactCard CardDatabaseQuerier::guessCard(const CardRef &cardRef) const
{
auto card = lookupCardByName(cardRef.name);
if (!card) {
return {};
}
auto printing =
cardRef.providerId.isEmpty() ? getPreferredPrinting(card) : findPrintingWithId(card, cardRef.providerId);
@ -173,10 +177,14 @@ ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const
*
* @param cardInfo The CardInfo to search
* @param providerId The uuid to look for
* @return The PrintingInfo, or a default-constructed PrintingInfo if not found.
* @return The PrintingInfo, or a default-constructed PrintingInfo if not found or if cardInfo is null.
*/
PrintingInfo CardDatabaseQuerier::findPrintingWithId(const CardInfoPtr &cardInfo, const QString &providerId) const
{
if (!cardInfo) {
return PrintingInfo();
}
for (const auto &printings : cardInfo->getSets()) {
for (const auto &printing : printings) {
if (printing.getUuid() == providerId) {

View file

@ -43,31 +43,31 @@ SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
SetQuery <- ('e'/'set') SetQueryValue
SetQueryValue <- ([=:] FlexStringValue) / (<[!][=]?> FlexStringValue) / SetExpression
OracleQuery <- 'o' [:] MatcherString
SetQuery <- ('e'/'set') ws? SetQueryValue
SetQueryValue <- ([=:] ws? FlexStringValue) / (<[!][=]?> ws? FlexStringValue) / SetExpression
OracleQuery <- 'o' ws? [:] ws? MatcherString
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
PowerQuery <- [Pp] 'ow' 'er'? ws? NumericExpression
ToughnessQuery <- [Tt] 'ou' 'ghness'? ws? NumericExpression
RarityQuery <- [rR] ':' Rarity
RarityQuery <- [rR] ws? ':' ws? Rarity
Rarity <- [Cc] 'ommon'? / [Uu] 'ncommon'? / [Rr] 'are'? / [Mm] 'ythic'? / [Ss] 'pecial'? / [a-zA-Z] [a-z]*
FormatQuery <- 'f' ':' Format / Legality ':' Format
FormatQuery <- 'f' ws? ':' ws? Format / Legality ws? ':' ws? Format
Format <- [a-zA-Z] [a-z]*
Legality <- [Ll] 'egal'? / [Bb] 'anned'? / [Rr] 'estricted'
TypeQuery <- [tT] 'ype'? [:] StringValue
TypeQuery <- [tT] 'ype'? ws? [:] ws? StringValue
Color <- < [Ww] 'hite'? / [Uu] / [Bb] 'lack'? / [Rr] 'ed'? / [Gg] 'reen'? / [Bb] 'lue'? >
ColorEx <- Color / [mc]
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
ColorQuery <- [cC] 'olor'? <[iI]?> ws? <[:!]> ws? ColorEx*
FieldQuery <- String [:] MatcherString / String ws? NumericExpression
FieldQuery <- String ws? [:] ws? MatcherString / String ws? NumericExpression
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].

View file

@ -1,6 +1,7 @@
#include "libcockatrice/card/database/card_database.h"
#include "libcockatrice/card/database/card_database_loader.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/card/set/card_set.h"
#include "libcockatrice/card/set/card_set_list.h"
#include "test_card_database_path_provider.h"
@ -29,6 +30,7 @@
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/utility/card_ref.h>
#include <string>
namespace
@ -60,6 +62,31 @@ TEST(CardDatabaseTest, LoadXml)
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear";
}
TEST(CardDatabaseTest, GuessCardWithProviderIdOnUnknownCardReturnsEmpty)
{
// An unknown name plus a providerId used to dereference the missing card
// inside findPrintingWithId(); the lookup must come back empty instead.
NoopCardPreferenceProvider prefs;
TestCardDatabasePathProvider pathProvider;
NoopCardSetPriorityController controller;
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
const ExactCard guessed = db.query()->guessCard({"Not a Card", "card_Not a Card_fixture"});
EXPECT_TRUE(guessed.isEmpty());
EXPECT_FALSE(guessed);
}
TEST(CardDatabaseTest, FindPrintingWithIdOnNullCardInfoReturnsEmpty)
{
NoopCardPreferenceProvider prefs;
TestCardDatabasePathProvider pathProvider;
NoopCardSetPriorityController controller;
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
EXPECT_TRUE(db.query()->findPrintingWithId(nullptr, "card_Not a Card_fixture").isEmpty());
}
TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
{
NoopCardSetPriorityController controller;

View file

@ -99,6 +99,17 @@ QUERY(SetCodeWrongSet2, doctor, "set:cat", false)
QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true)
// Spaces around the colon are optional for all field queries (e.g. "o: flying")
QUERY(OracleSpaceAfterColon, cat, "o: meow", true)
QUERY(OracleSpaceBeforeColon, cat, "o :meow", true)
QUERY(OracleSpacesAroundColon, cat, "o : meow", true)
QUERY(OracleSpaceAfterColonNoMatch, cat, "o: woof", false)
QUERY(TypeSpaceAfterColon, cat, "t: creature", true)
QUERY(FieldSpaceAfterColon, cat, "pt: \"3/3\"", true)
QUERY(ColorSpaceAfterColon, cat, "c: g", true)
QUERY(ColorSpaceBeforeOperator, cat, "c !g", true)
QUERY(SetSpaceAfterColon, cat, "set: cat", true)
CardInfoPtr localizedCat()
{
CardInfoPtr localized = CardInfo::newInstance("Cat", "Meow!", false, {}, {}, {}, {}, {});
@ -107,6 +118,22 @@ CardInfoPtr localizedCat()
return localized;
}
CardInfoPtr greenIdentityCard()
{
return CardInfo::newInstance("Identity Cat", "", false, {{"colors", "G"}, {"coloridentity", "G"}}, {}, {}, {}, {});
}
// Whitespace belongs around the separator, never inside an operator token: "c i: g" must not
// be read as the color-identity operator "ci" (it is a generic "c" AND a field query on "i").
TEST_F(CardQuery, ColorIdentityOperatorIsNotSplitByWhitespace)
{
const CardData identityCard = greenIdentityCard();
ASSERT_TRUE(FilterString("ci:g").check(identityCard));
ASSERT_TRUE(FilterString("ci: g").check(identityCard));
ASSERT_TRUE(FilterString("ci : g").check(identityCard));
ASSERT_FALSE(FilterString("c i: g").check(identityCard));
}
TEST_F(CardQuery, SearchLanguageEnglishMatchesOnlyEnglish)
{
const CardData localized = localizedCat();
@ -136,6 +163,13 @@ TEST_F(CardQuery, SearchLanguageBothMatchesEitherLanguage)
ASSERT_TRUE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
}
TEST_F(CardQuery, SearchLanguageBothMatchesOracleTextWithSpaceAfterColon)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("o: Meow", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
ASSERT_TRUE(FilterString("o: miaut", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
}
TEST_F(CardQuery, SearchLanguageIsBoundPerInstance)
{
const CardData localized = localizedCat();