TopicMatcher matching to parent when using '#' wildcard.

This commit is contained in:
fpagliughi 2025-06-24 10:22:57 -04:00
parent 55f7fecdda
commit 670b38df37
6 changed files with 33 additions and 20 deletions

View File

@ -312,12 +312,21 @@ public:
auto snode = std::move(nodes_.back());
nodes_.pop_back();
const auto map_end = snode.node_->children.end();
typename node_map::iterator child;
// If we're at the end of the topic fields, we either have a value,
// or need to move on to the next node to search.
if (snode.fields_.empty()) {
pval_ = snode.node_->content.get();
if (!pval_)
if (!pval_) {
// ...but a '#' matches the parent topic
if ((child = snode.node_->children.find("#")) != map_end) {
pval_ = child->second->content.get();
return;
}
this->next();
}
return;
}
@ -325,9 +334,6 @@ public:
auto field = std::move(snode.fields_.front());
snode.fields_.pop_front();
typename node_map::iterator child;
const auto map_end = snode.node_->children.end();
// Look for an exact match
if ((child = snode.node_->children.find(field)) != map_end) {
nodes_.push_back({child->second.get(), snode.fields_});

View File

@ -109,7 +109,10 @@ bool topic_filter::matches(const string& topic) const
{
auto n = fields_.size();
// Edge case of topic_filter("")
// Spec: All Topic Names and Topic Filters MUST be at least one
// character long [MQTT-4.7.3-1]
// So, an empty filter can't match anything, and is technically an
// error.
if (n == 0) {
return false;
}
@ -118,7 +121,7 @@ bool topic_filter::matches(const string& topic) const
auto nt = topic_fields.size();
// Filter can't match a topic that is shorter
if (n > nt) {
if (n > nt && !(n == nt + 1 && fields_[n - 1] == "#")) {
return false;
}
@ -140,6 +143,9 @@ bool topic_filter::matches(const string& topic) const
if (fields_[i] == "#") {
break;
}
if (i == nt && i < n - 1) {
return fields_[i + 1] == "#";
}
if (fields_[i] != "+" && fields_[i] != topic_fields[i]) {
return false;
}

View File

@ -578,8 +578,7 @@ TEST_CASE("async_client publish 5 args", "[client]")
const void* payload{PAYLOAD.data()};
const size_t payload_size{PAYLOAD.size()};
delivery_token_ptr token_pub{
cli.publish(TOPIC, payload, payload_size, GOOD_QOS, RETAINED)
delivery_token_ptr token_pub{cli.publish(TOPIC, payload, payload_size, GOOD_QOS, RETAINED)
};
REQUIRE(token_pub);
token_pub->wait_for(TIMEOUT);
@ -738,8 +737,8 @@ TEST_CASE("async_client subscribe many topics 2 args_single", "[client]")
cli.connect()->wait();
REQUIRE(cli.is_connected());
mqtt::const_string_collection_ptr TOPIC_1_COLL{mqtt::string_collection::create({"TOPIC0"}
)};
mqtt::const_string_collection_ptr TOPIC_1_COLL{mqtt::string_collection::create({"TOPIC0"})
};
iasync_client::qos_collection GOOD_QOS_1_COLL{0};
try {
cli.subscribe(TOPIC_1_COLL, GOOD_QOS_1_COLL)->wait_for(TIMEOUT);

View File

@ -534,8 +534,8 @@ public:
// NOTE: Only tokens for messages with QOS=1 and QOS=2 are kept. That's
// why the vector's size does not account for QOS=0 message tokens
std::vector<mqtt::delivery_token_ptr> tokens_pending{cli.get_pending_delivery_tokens(
)};
std::vector<mqtt::delivery_token_ptr> tokens_pending{cli.get_pending_delivery_tokens()
};
CPPUNIT_ASSERT_EQUAL(2, static_cast<int>(tokens_pending.size()));
mqtt::token_ptr token_disconn{cli.disconnect()};
@ -831,8 +831,7 @@ public:
CPPUNIT_ASSERT(cli.is_connected());
mqtt::test::dummy_action_listener listener;
mqtt::token_ptr token_sub{
cli.subscribe(TOPIC_COLL, GOOD_QOS_COLL, &CONTEXT, listener)
mqtt::token_ptr token_sub{cli.subscribe(TOPIC_COLL, GOOD_QOS_COLL, &CONTEXT, listener)
};
CPPUNIT_ASSERT(token_sub);
token_sub->wait_for(TIMEOUT);

View File

@ -232,6 +232,7 @@ TEST_CASE("topic matches", "[topic_filter]")
REQUIRE(filt.matches("my/topic/name"));
REQUIRE(filt.matches("my/topic/id"));
REQUIRE(filt.matches("my/topic/name/and/id"));
REQUIRE(filt.matches("my/topic"));
REQUIRE(!filt.matches("my/other/name"));
REQUIRE(!filt.matches("my/other/id"));
@ -244,14 +245,14 @@ TEST_CASE("topic matches", "[topic_filter]")
SECTION("should_match")
{
REQUIRE(topic_filter{"foo/bar"}.matches("foo/bar"));
REQUIRE(
topic_filter{
"foo/+",
}
.matches("foo/bar")
);
REQUIRE(topic_filter{
"foo/+",
}
.matches("foo/bar"));
REQUIRE(topic_filter{"foo/+/baz"}.matches("foo/bar/baz"));
REQUIRE(topic_filter{"foo/+/#"}.matches("foo/bar/baz"));
REQUIRE(topic_filter("foo/bar/#").matches("foo/bar/baz"));
REQUIRE(topic_filter("foo/bar/#").matches("foo/bar"));
REQUIRE(topic_filter{"A/B/+/#"}.matches("A/B/B/C"));
REQUIRE(topic_filter{"#"}.matches("foo/bar/baz"));
REQUIRE(topic_filter{"#"}.matches("/foo/bar"));

View File

@ -69,6 +69,8 @@ TEST_CASE("matcher matches", "[topic_matcher]")
REQUIRE((topic_matcher<int>{{"foo/+", 42}}.has_match("foo/bar")));
REQUIRE((topic_matcher<int>{{"foo/+/baz", 42}}.has_match("foo/bar/baz")));
REQUIRE((topic_matcher<int>{{"foo/+/#", 42}}.has_match("foo/bar/baz")));
REQUIRE((topic_matcher<int>{{"foo/bar/#", 42}}.has_match("foo/bar/baz")));
REQUIRE((topic_matcher<int>{{"foo/bar/#", 42}}.has_match("foo/bar")));
REQUIRE((topic_matcher<int>{{"A/B/+/#", 42}}.has_match("A/B/B/C")));
REQUIRE((topic_matcher<int>{{"#", 42}}.has_match("foo/bar/baz")));
REQUIRE((topic_matcher<int>{{"#", 42}}.has_match("/foo/bar")));