Skip to content

Commit 62c4bb4

Browse files
authored
Validate model names in explicit XML node syntax (fixes #1220) (#1231)
Behavior change: the ID of explicit <Action>, <Condition>, <Decorator> and <Control> elements is now checked with the same model-name rules as the compact form, so an ID with a forbidden character (e.g. ID="My.Action") or the reserved name Root, which loaded before, now throws.
1 parent 7c46865 commit 62c4bb4

2 files changed

Lines changed: 182 additions & 5 deletions

File tree

‎src/xml_parsing.cpp‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -607,11 +607,8 @@ void VerifyXML(const std::string& xml_text,
607607
{
608608
// use ID for builtin node types, otherwise use the element name
609609
const auto lookup_name = is_builtin ? ID : name;
610-
// Validate model name for custom node types (non-builtin element names)
611-
if(!is_builtin)
612-
{
613-
validateModelName(name, line_number);
614-
}
610+
// Validate the resolved node type name for both compact and explicit forms.
611+
validateModelName(lookup_name, line_number);
615612
const auto search = registered_nodes.find(lookup_name);
616613
const bool found = (search != registered_nodes.end());
617614
if(!found)

‎tests/gtest_name_validation.cpp‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,117 @@
11
#include "behaviortree_cpp/basic_types.h"
22
#include "behaviortree_cpp/bt_factory.h"
3+
#include "behaviortree_cpp/control_node.h"
34
#include "behaviortree_cpp/xml_parsing.h"
45

6+
#include <array>
7+
#include <string>
8+
59
#include <gtest/gtest.h>
610

711
using namespace BT;
812

13+
namespace
14+
{
15+
16+
class NameValidationTestControlNode : public ControlNode
17+
{
18+
public:
19+
NameValidationTestControlNode(const std::string& name, const NodeConfig& config)
20+
: ControlNode(name, config)
21+
{}
22+
23+
static PortsList providedPorts()
24+
{
25+
return {};
26+
}
27+
28+
NodeStatus tick() override
29+
{
30+
return NodeStatus::SUCCESS;
31+
}
32+
};
33+
34+
struct ExplicitNodeCase
35+
{
36+
const char* tag;
37+
const char* child_xml;
38+
};
39+
40+
constexpr std::array<ExplicitNodeCase, 4> kExplicitNodeCases = {
41+
ExplicitNodeCase{ "Action", "" }, ExplicitNodeCase{ "Condition", "" },
42+
ExplicitNodeCase{ "Decorator", "<AlwaysSuccess/>" },
43+
ExplicitNodeCase{ "Control", "<AlwaysSuccess/>" }
44+
};
45+
46+
constexpr std::array<const char*, 3> kValidASCIIModelNames = { "Valid_Name", "Valid-Name",
47+
"1LeadingDigit" };
48+
49+
constexpr std::array<const char*, 1> kValidUnicodeModelNames = { "检查门状态" };
50+
51+
constexpr std::array<const char*, 2> kInvalidExplicitModelNames = { "Node.With.Dot",
52+
"Root" };
53+
54+
std::string MakeExplicitNodeXML(const ExplicitNodeCase& node_case,
55+
const std::string& model_name,
56+
const char* instance_name = nullptr)
57+
{
58+
std::string xml = "\n"
59+
" <root BTCPP_format=\"4\">\n"
60+
" <BehaviorTree ID=\"MainTree\">\n"
61+
" <";
62+
xml += node_case.tag;
63+
xml += " ID=\"";
64+
xml += model_name;
65+
xml += "\"";
66+
if(instance_name != nullptr)
67+
{
68+
xml += " name=\"";
69+
xml += instance_name;
70+
xml += "\"";
71+
}
72+
if(node_case.child_xml[0] == '\0')
73+
{
74+
xml += "/>\n";
75+
}
76+
else
77+
{
78+
xml += ">";
79+
xml += node_case.child_xml;
80+
xml += "</";
81+
xml += node_case.tag;
82+
xml += ">\n";
83+
}
84+
xml += " </BehaviorTree>\n"
85+
" </root>";
86+
return xml;
87+
}
88+
89+
void RegisterExplicitNodeModel(BehaviorTreeFactory& factory, std::string_view tag,
90+
const std::string& model_name)
91+
{
92+
if(tag == "Action")
93+
{
94+
auto tick = [](TreeNode&) { return NodeStatus::SUCCESS; };
95+
factory.registerSimpleAction(model_name, tick);
96+
}
97+
else if(tag == "Condition")
98+
{
99+
auto tick = [](TreeNode&) { return NodeStatus::SUCCESS; };
100+
factory.registerSimpleCondition(model_name, tick);
101+
}
102+
else if(tag == "Decorator")
103+
{
104+
auto tick = [](NodeStatus child_status, TreeNode&) { return child_status; };
105+
factory.registerSimpleDecorator(model_name, tick);
106+
}
107+
else if(tag == "Control")
108+
{
109+
factory.registerNodeType<NameValidationTestControlNode>(model_name);
110+
}
111+
}
112+
113+
} // namespace
114+
9115
// ============== Tests for findForbiddenChar() ==============
10116

11117
TEST(NameValidation, ForbiddenCharDetection_ValidNames)
@@ -255,6 +361,80 @@ TEST_F(NameValidationXMLTest, ValidInstanceName_WithPeriod)
255361
EXPECT_NO_THROW((void)factory.createTreeFromText(xml));
256362
}
257363

364+
TEST_F(NameValidationXMLTest, ExplicitNodeIDs_InvalidModelNamesThrowHelpfulErrors)
365+
{
366+
for(const auto& node_case : kExplicitNodeCases)
367+
{
368+
for(const char* invalid_model_name : kInvalidExplicitModelNames)
369+
{
370+
BehaviorTreeFactory explicit_factory;
371+
RegisterExplicitNodeModel(explicit_factory, node_case.tag, invalid_model_name);
372+
373+
SCOPED_TRACE(std::string(node_case.tag) + " / " + invalid_model_name);
374+
const auto xml = MakeExplicitNodeXML(node_case, invalid_model_name);
375+
376+
try
377+
{
378+
(void)explicit_factory.createTreeFromText(xml);
379+
FAIL() << "Expected RuntimeError for explicit model name: " << invalid_model_name;
380+
}
381+
catch(const RuntimeError& e)
382+
{
383+
const std::string msg = e.what();
384+
EXPECT_NE(msg.find(invalid_model_name), std::string::npos) << msg;
385+
if(std::string(invalid_model_name) == "Node.With.Dot")
386+
{
387+
EXPECT_NE(msg.find("forbidden character '.'"), std::string::npos) << msg;
388+
}
389+
else
390+
{
391+
EXPECT_NE(msg.find("reserved name"), std::string::npos) << msg;
392+
}
393+
}
394+
}
395+
}
396+
}
397+
398+
TEST_F(NameValidationXMLTest, ExplicitNodeIDs_ValidASCIIAndUnicodeAreAccepted)
399+
{
400+
for(const auto& node_case : kExplicitNodeCases)
401+
{
402+
for(const char* valid_model_name : kValidASCIIModelNames)
403+
{
404+
BehaviorTreeFactory explicit_factory;
405+
RegisterExplicitNodeModel(explicit_factory, node_case.tag, valid_model_name);
406+
407+
SCOPED_TRACE(std::string(node_case.tag) + " / " + valid_model_name);
408+
const auto xml = MakeExplicitNodeXML(node_case, valid_model_name);
409+
EXPECT_NO_THROW((void)explicit_factory.createTreeFromText(xml));
410+
}
411+
412+
for(const char* valid_model_name : kValidUnicodeModelNames)
413+
{
414+
BehaviorTreeFactory explicit_factory;
415+
RegisterExplicitNodeModel(explicit_factory, node_case.tag, valid_model_name);
416+
417+
SCOPED_TRACE(std::string(node_case.tag) + " / " + valid_model_name);
418+
const auto xml = MakeExplicitNodeXML(node_case, valid_model_name);
419+
EXPECT_NO_THROW((void)explicit_factory.createTreeFromText(xml));
420+
}
421+
}
422+
}
423+
424+
TEST_F(NameValidationXMLTest, ExplicitNodeIDs_PreserveRelaxedInstanceNames)
425+
{
426+
for(const auto& node_case : kExplicitNodeCases)
427+
{
428+
BehaviorTreeFactory explicit_factory;
429+
RegisterExplicitNodeModel(explicit_factory, node_case.tag, "Valid_Name");
430+
431+
SCOPED_TRACE(node_case.tag);
432+
const auto xml =
433+
MakeExplicitNodeXML(node_case, "Valid_Name", "node.name with spaces");
434+
EXPECT_NO_THROW((void)explicit_factory.createTreeFromText(xml));
435+
}
436+
}
437+
258438
TEST_F(NameValidationXMLTest, ValidSubTreeID)
259439
{
260440
const char* xml = R"(

0 commit comments

Comments
 (0)