diff --git a/src/tree_node.cpp b/src/tree_node.cpp index 16171a135..e716eca97 100644 --- a/src/tree_node.cpp +++ b/src/tree_node.cpp @@ -383,11 +383,62 @@ bool TreeNode::isBlackboardPointer(StringView str, StringView* stripped_pointer) } const auto size = (last_index - front_index) + 1; auto valid = size >= 3 && str[front_index] == '{' && str[last_index] == '}'; - if(valid && stripped_pointer != nullptr) + + if(!valid) + { + return false; + } + + // Extract content between braces + StringView content(&str[front_index + 1], size - 2); + + // Special case: {=} is valid (auto-remapping syntax) + if(content == "=") + { + if(stripped_pointer != nullptr) + { + *stripped_pointer = content; + } + return true; + } + + // Validate that the content looks like a blackboard variable name: + // must match [@]?[a-zA-Z_][a-zA-Z0-9_./]* + // The optional '@' prefix refers to the root blackboard. + // This rejects JSON strings like {"key": "value"}. + if(content.empty()) + { + return false; + } + size_t start = 0; + if(content.front() == '@') + { + start = 1; + if(start >= content.size()) + { + return false; + } + } + const char first_ch = content[start]; + if(!std::isalpha(static_cast(first_ch)) && first_ch != '_') + { + return false; + } + for(size_t i = start + 1; i < content.size(); i++) + { + const char ch = content[i]; + if(!std::isalnum(static_cast(ch)) && ch != '_' && ch != '.' && + ch != '/') + { + return false; + } + } + + if(stripped_pointer != nullptr) { - *stripped_pointer = StringView(&str[front_index + 1], size - 2); + *stripped_pointer = content; } - return valid; + return true; } StringView TreeNode::stripBlackboardPointer(StringView str) diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 3e94d9f54..a3be78485 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -801,9 +801,7 @@ TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element, else { const auto& port_model = port_model_it->second; - const bool is_blackboard = port_value.size() >= 3 && - port_value.front() == '{' && - port_value.back() == '}'; + const bool is_blackboard = TreeNode::isBlackboardPointer(port_value); // let's test already if conversion is possible if(!is_blackboard && port_model.converter() && port_model.isStronglyTyped()) { diff --git a/tests/gtest_ports.cpp b/tests/gtest_ports.cpp index 9644d4fa4..5b233cff7 100644 --- a/tests/gtest_ports.cpp +++ b/tests/gtest_ports.cpp @@ -1,6 +1,7 @@ #include "behaviortree_cpp/basic_types.h" #include "behaviortree_cpp/bt_factory.h" #include "behaviortree_cpp/json_export.h" +#include "behaviortree_cpp/tree_node.h" #include "behaviortree_cpp/xml_parsing.h" #include @@ -815,3 +816,36 @@ TEST(PortTest, SubtreeStringLiteralToLoopDouble_Issue1065) EXPECT_DOUBLE_EQ(collected[1], 2.0); EXPECT_DOUBLE_EQ(collected[2], 3.0); } + +// Issue #883: JSON braces should not be treated as blackboard pointers +TEST(PortTest, IsBlackboardPointer_Issue883) +{ + using BT::TreeNode; + + // Valid blackboard pointers + EXPECT_TRUE(TreeNode::isBlackboardPointer("{my_var}")); + EXPECT_TRUE(TreeNode::isBlackboardPointer("{my.var}")); + EXPECT_TRUE(TreeNode::isBlackboardPointer("{my/var}")); + EXPECT_TRUE(TreeNode::isBlackboardPointer("{_private}")); + EXPECT_TRUE(TreeNode::isBlackboardPointer("{=}")); + EXPECT_TRUE(TreeNode::isBlackboardPointer("{a}")); + EXPECT_TRUE(TreeNode::isBlackboardPointer(" {my_var} ")); + EXPECT_TRUE(TreeNode::isBlackboardPointer("{@root_var}")); + + // Invalid: JSON strings should NOT be treated as blackboard pointers + EXPECT_FALSE(TreeNode::isBlackboardPointer(R"({"key": "value"})")); + EXPECT_FALSE(TreeNode::isBlackboardPointer(R"({"config": {"nested": true}})")); + + // Invalid: other non-variable content + EXPECT_FALSE(TreeNode::isBlackboardPointer("{123}")); + EXPECT_FALSE(TreeNode::isBlackboardPointer("{}")); + EXPECT_FALSE(TreeNode::isBlackboardPointer("ab")); + + // Verify stripped_pointer output + BT::StringView stripped; + EXPECT_TRUE(TreeNode::isBlackboardPointer("{foo_bar}", &stripped)); + EXPECT_EQ(stripped, "foo_bar"); + + EXPECT_TRUE(TreeNode::isBlackboardPointer("{=}", &stripped)); + EXPECT_EQ(stripped, "="); +}