Skip to content

Commit 8f86eb9

Browse files
committed
add new name validation rules
1 parent 3e8c292 commit 8f86eb9

7 files changed

Lines changed: 613 additions & 3 deletions

File tree

docs/name_validation_rules.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Name Validation Rules
2+
3+
This document describes the validation rules for names in Groot2 and BehaviorTree.CPP. These rules ensure XML compatibility while supporting Unicode characters (Chinese, Japanese, Korean, etc.).
4+
5+
## Overview
6+
7+
The validation uses a **blacklist approach**: all characters are allowed except those explicitly forbidden. This enables Unicode support while blocking characters that would break XML serialization or cause path/filesystem issues.
8+
9+
## Forbidden Characters (Model Names & Port Names)
10+
11+
The following ASCII characters are forbidden in **Model Names** and **Port Names**:
12+
13+
| Category | Characters | Reason |
14+
|----------|------------|--------|
15+
| Whitespace | `space`, `\t`, `\n`, `\r` | Breaks XML element/attribute names |
16+
| XML special | `<`, `>`, `&`, `"`, `'` | Reserved in XML |
17+
| Path separators | `/`, `\`, `:` | Filesystem conflicts |
18+
| Wildcards | `*`, `?`, `\|` | Shell/glob conflicts |
19+
| Period | `.` | Ambiguous in port names (e.g., `request.name`) |
20+
| Control chars | ASCII 0-31, 127 | Non-printable |
21+
22+
## Allowed Characters
23+
24+
| Category | Examples |
25+
|----------|----------|
26+
| ASCII letters | `a-z`, `A-Z` |
27+
| Digits | `0-9` |
28+
| Underscore | `_` |
29+
| Hyphen | `-` |
30+
| Unicode letters | `中文`, `日本語`, `한국어`, `Ümlauts` |
31+
32+
## Validation Rules by Name Type
33+
34+
### Model Name (Node Type Name)
35+
- **Cannot be empty**
36+
- **Cannot be "Root"** (reserved)
37+
- No forbidden characters (see table above)
38+
39+
### Port Name
40+
- **Cannot be empty**
41+
- **Cannot start with a digit**
42+
- **Cannot be a reserved attribute**: `ID`, `name`, `_description`, `_skipIf`, `_successIf`, `_failureIf`, `_while`, `_onSuccess`, `_onFailure`, `_onHalted`, `_post`, `_autoremap`, `__shared_blackboard`
43+
- No forbidden characters (see table above)
44+
45+
### Instance Name
46+
- **Can be empty** (defaults to model name)
47+
- Instance names are XML attribute **values** (not element/attribute names), so most characters are allowed including spaces, periods, etc.
48+
- Only invalid XML control characters are forbidden (ASCII 0-8, 11-12, 14-31, 127)
49+
50+
## Implementation
51+
52+
### C++ Reference Implementation
53+
54+
```cpp
55+
#include <algorithm>
56+
#include <array>
57+
#include <string>
58+
59+
// Returns the forbidden character if found, or '\0' if valid
60+
static char findForbiddenChar(const std::string& name)
61+
{
62+
static constexpr std::array<char, 16> forbidden = {
63+
' ', '\t', '\n', '\r', '<', '>', '&', '"', '\'', '/', '\\', ':', '*', '?', '|', '.'};
64+
65+
for (unsigned char c : name)
66+
{
67+
// Allow UTF-8 multibyte sequences (high bit set)
68+
if (c >= 0x80)
69+
{
70+
continue;
71+
}
72+
// Block control characters
73+
if (c < 32 || c == 127)
74+
{
75+
return static_cast<char>(c);
76+
}
77+
// Check forbidden list
78+
if (std::find(forbidden.begin(), forbidden.end(), c) != forbidden.end())
79+
{
80+
return static_cast<char>(c);
81+
}
82+
}
83+
return '\0';
84+
}
85+
```
86+
87+
## Examples
88+
89+
### Valid Model/Port Names
90+
```
91+
MyAction
92+
my_action
93+
My-Action
94+
检查门状态 (Chinese)
95+
ドアを開ける (Japanese)
96+
Tür_öffnen (German)
97+
```
98+
99+
### Invalid Model/Port Names
100+
```
101+
My Action (contains space)
102+
request.name (contains period)
103+
My<Node> (contains XML chars)
104+
path/to/node (contains path separator)
105+
Root (reserved)
106+
```
107+
108+
### Valid Instance Names
109+
Instance names have relaxed rules since they are XML attribute values:
110+
```
111+
My Action (spaces allowed)
112+
node.name (periods allowed)
113+
Success 1 (spaces allowed)
114+
检查门状态 (Unicode allowed)
115+
```
116+
117+
### Invalid Instance Names
118+
```
119+
name_with_null\0 (null character)
120+
name_with_bell\x07 (control character)
121+
```
122+
123+
## Related Issues
124+
125+
- [#59](https://github.com/BehaviorTree/Groot2/issues/59) - Unicode support in node names
126+
- [#60](https://github.com/BehaviorTree/Groot2/issues/60) - i18n support request
127+
- [#64](https://github.com/BehaviorTree/Groot2/issues/64) - Clear error for forbidden characters in port names
128+
129+
## Files Modified in BehaviorTree.CPP
130+
131+
- `include/behaviortree_cpp/basic_types.h` - `findForbiddenChar()` declaration
132+
- `src/basic_types.cpp` - `findForbiddenChar()` implementation, `IsAllowedPortName()` update
133+
- `src/xml_parsing.cpp` - Validation functions and integration in XML parsing
134+
- `tests/gtest_name_validation.cpp` - Comprehensive tests for validation

src/basic_types.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
#include "behaviortree_cpp/tree_node.h"
33
#include "behaviortree_cpp/json_export.h"
44

5+
#include <algorithm>
6+
#include <array>
7+
#include <charconv>
8+
#include <clocale>
59
#include <cstdlib>
610
#include <cstring>
7-
#include <clocale>
8-
#include <charconv>
911
#include <tuple>
1012

1113
namespace BT

src/xml_parsing.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,92 @@ namespace
9191
auto StrEqual = [](const char* str1, const char* str2) -> bool {
9292
return strcmp(str1, str2) == 0;
9393
};
94+
95+
// Helper to format forbidden character for error messages
96+
std::string formatForbiddenChar(char c)
97+
{
98+
if(c < 32 || c == 127)
99+
{
100+
return "control character (ASCII " + std::to_string(static_cast<int>(c)) + ")";
101+
}
102+
return std::string("'") + c + "'";
103+
}
104+
105+
void validateModelName(const std::string& name, int line_number)
106+
{
107+
const auto line_str = std::to_string(line_number);
108+
if(name.empty())
109+
{
110+
throw RuntimeError("Error at line ", line_str,
111+
": Model/Node type name cannot be empty");
112+
}
113+
if(name == "Root" || name == "root")
114+
{
115+
throw RuntimeError("Error at line ", line_str,
116+
": 'Root' is a reserved name and cannot be used as a node type");
117+
}
118+
if(char c = findForbiddenChar(name); c != '\0')
119+
{
120+
throw RuntimeError("Error at line ", line_str, ": Model name '", name,
121+
"' contains forbidden character ", formatForbiddenChar(c));
122+
}
123+
}
124+
125+
void validatePortName(const std::string& name, int line_number)
126+
{
127+
const auto line_str = std::to_string(line_number);
128+
if(name.empty())
129+
{
130+
throw RuntimeError("Error at line ", line_str, ": Port name cannot be empty");
131+
}
132+
if(std::isdigit(static_cast<unsigned char>(name[0])))
133+
{
134+
throw RuntimeError("Error at line ", line_str, ": Port name '", name,
135+
"' cannot start with a digit");
136+
}
137+
if(char c = findForbiddenChar(name); c != '\0')
138+
{
139+
throw RuntimeError("Error at line ", line_str, ": Port name '", name,
140+
"' contains forbidden character ", formatForbiddenChar(c));
141+
}
142+
if(IsReservedAttribute(name))
143+
{
144+
throw RuntimeError("Error at line ", line_str, ": Port name '", name,
145+
"' is a reserved attribute name");
146+
}
147+
}
148+
149+
void validateInstanceName(const std::string& name, int line_number)
150+
{
151+
// Instance name CAN be empty (defaults to model name)
152+
// Instance names are XML attribute VALUES, so they can contain spaces,
153+
// periods, and most characters. We only reject control characters that
154+
// are invalid in XML.
155+
if(name.empty())
156+
{
157+
return;
158+
}
159+
for(const char c : name)
160+
{
161+
const auto uc = static_cast<unsigned char>(c);
162+
// Only reject control characters that are invalid in XML
163+
// (XML allows tab=0x09, newline=0x0A, carriage return=0x0D)
164+
if(uc < 32 && uc != 0x09 && uc != 0x0A && uc != 0x0D)
165+
{
166+
const auto line_str = std::to_string(line_number);
167+
throw RuntimeError("Error at line ", line_str, ": Instance name '", name,
168+
"' contains invalid control character (ASCII ",
169+
std::to_string(static_cast<int>(uc)), ")");
170+
}
171+
if(uc == 127)
172+
{
173+
const auto line_str = std::to_string(line_number);
174+
throw RuntimeError("Error at line ", line_str, ": Instance name '", name,
175+
"' contains invalid control character (ASCII 127)");
176+
}
177+
}
178+
}
179+
94180
} // namespace
95181

96182
struct SubtreeModel

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ set(BT_TESTS
1818
gtest_port_type_rules.cpp
1919
gtest_postconditions.cpp
2020
gtest_match.cpp
21+
gtest_name_validation.cpp
2122
gtest_json.cpp
2223
gtest_reactive.cpp
2324
gtest_reactive_backchaining.cpp

0 commit comments

Comments
 (0)