Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/behaviortree_cpp/basic_types.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <algorithm>
#include <cctype>
#include <chrono>
#include <iostream>
#include <functional>
Expand Down Expand Up @@ -348,6 +350,12 @@ struct Timestamp

[[nodiscard]] bool IsReservedAttribute(StringView str);

/// Throws RuntimeError if the string contains any whitespace character.
/// Used by port-creation paths (CreatePort and TreeNodesModel XML parsing)
/// to reject port names like "my port" that would be ambiguous in blackboard
/// remappings.
void ThrowIfPortNameContainsWhitespace(StringView name);

class TypeInfo
{
public:
Expand Down Expand Up @@ -447,6 +455,7 @@ template <typename T = AnyTypeAllowed>
"and must start with an alphabetic character. "
"Underscore is reserved.");
}
ThrowIfPortNameContainsWhitespace(sname);

std::pair<std::string, PortInfo> out;

Expand Down
13 changes: 13 additions & 0 deletions src/basic_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include "behaviortree_cpp/tree_node.h"
#include "behaviortree_cpp/json_export.h"

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <clocale>
Expand Down Expand Up @@ -465,6 +467,17 @@ bool IsReservedAttribute(StringView str)
return str == "name" || str == "ID" || str == "_autoremap";
}

void ThrowIfPortNameContainsWhitespace(StringView name)
{
const auto has_whitespace = std::any_of(
name.begin(), name.end(), [](unsigned char c) { return std::isspace(c); });
if(has_whitespace)
{
throw RuntimeError(
StrCat("The name of a port must not contain whitespace: '", name, "'"));
}
}

Any convertFromJSON(StringView json_text, std::type_index type)
{
nlohmann::json json = nlohmann::json::parse(json_text);
Expand Down
61 changes: 59 additions & 2 deletions src/xml_parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdio>
#include <cstring>
#include <functional>
#include <iostream>
#include <limits>
#include <list>
#include <sstream>
#include <string>
#include <string_view>
#include <typeindex>

#include "behaviortree_cpp/basic_types.h"
#include "behaviortree_cpp/utils/strcat.hpp"

Expand Down Expand Up @@ -214,6 +220,8 @@ void BT::XMLParser::PImpl::loadSubtreeModel(const XMLElement* xml_root)
{
throw RuntimeError("Missing attribute [name] in port (SubTree model)");
}
std::string_view sname(name);
ThrowIfPortNameContainsWhitespace(sname);
if(auto default_value = port_node->Attribute("default"))
{
port.setDefaultValue(default_value);
Expand Down Expand Up @@ -1000,10 +1008,59 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(const std::string& tree_ID,
}
else
{
// constant string: just set that constant value into the BB
// constant value: set it into the BB with appropriate type
// IMPORTANT: this must not be autoremapped!!!
new_bb->enableAutoRemapping(false);
new_bb->set(port_name, static_cast<std::string>(port_value));
const std::string str_value(port_value);

// Try to preserve numeric types so that Script expressions
// can perform arithmetic without type-mismatch errors.
// Use std::from_chars with strict full-string validation to avoid
// false positives on compound strings like "1;2;3" or "2.2;2.4".
bool stored = false;
if(!str_value.empty())
{
const char* begin = str_value.data();
const char* end = begin + str_value.size();
// Try integer first (no decimal point, no exponent notation).
// Use int when the value fits, to match the most common port
// declarations. Fall back to int64_t for larger values.
if(str_value.find('.') == std::string::npos &&
str_value.find('e') == std::string::npos &&
str_value.find('E') == std::string::npos)
{
int64_t int_val = 0;
auto [ptr, ec] = std::from_chars(begin, end, int_val);
if(ec == std::errc() && ptr == end)
{
if(int_val >= std::numeric_limits<int>::min() &&
int_val <= std::numeric_limits<int>::max())
{
new_bb->set(port_name, static_cast<int>(int_val));
}
else
{
new_bb->set(port_name, int_val);
}
stored = true;
}
}
// Try double
if(!stored)
{
double dbl_val = 0;
auto [ptr, ec] = std::from_chars(begin, end, dbl_val);
if(ec == std::errc() && ptr == end)
{
new_bb->set(port_name, dbl_val);
stored = true;
}
}
}
if(!stored)
{
new_bb->set(port_name, str_value);
}
Comment thread
JWhitleyWork marked this conversation as resolved.
new_bb->enableAutoRemapping(do_autoremap);
}
}
Expand Down
10 changes: 10 additions & 0 deletions tests/gtest_ports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -861,3 +861,13 @@ TEST(PortTest, VectorAny)
ASSERT_NO_THROW(status = tree.tickOnce());
ASSERT_EQ(status, NodeStatus::FAILURE);
}

TEST(PortTest, WhitespaceInPortName)
{
ASSERT_ANY_THROW(BT::InputPort<std::string>("port name"));
ASSERT_ANY_THROW(BT::InputPort<std::string>("port\tname"));
ASSERT_ANY_THROW(BT::InputPort<std::string>("port\nname"));
ASSERT_ANY_THROW(BT::InputPort<std::string>(" leading"));
ASSERT_ANY_THROW(BT::InputPort<std::string>("trailing "));
ASSERT_NO_THROW(BT::InputPort<std::string>("valid_port_name"));
}
80 changes: 80 additions & 0 deletions tests/gtest_subtree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,48 @@ TEST(SubTree, SubtreeModels)
tree.tickWhileRunning();
}

TEST(SubTree, WhitespaceInSubtreeModel)
{
// clang-format off

static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree" BTCPP_format="4">
<TreeNodesModel>
<SubTree ID="MySub">
<input_port name="port with space" default="42"/>
</SubTree>
</TreeNodesModel>

<BehaviorTree ID="MainTree">
<Sequence>
<SubTree ID="MySub" />
</Sequence>
</BehaviorTree>

<BehaviorTree ID="MySub">
<Sequence>
<AlwaysSuccess />
</Sequence>
</BehaviorTree>
</root>
)";

// clang-format on

BehaviorTreeFactory factory;
try
{
auto _ = factory.createTreeFromText(xml_text);
}
catch(RuntimeError e)
{
EXPECT_NE(std::string_view(e.what()).find("not contain whitespace"),
std::string_view::npos);
return;
}
FAIL() << "Exception was not thrown.";
}

class PrintToConsole : public BT::SyncActionNode
{
public:
Expand Down Expand Up @@ -757,3 +799,41 @@ TEST(SubTree, SubtreeNameNotRegistered)
ASSERT_ANY_THROW(auto tree = factory.createTreeFromText(xml_text));
ASSERT_ANY_THROW(factory.registerBehaviorTreeFromText(xml_text));
}

// Regression test: literal numeric values passed to subtrees should preserve
// their numeric type so that Script expressions can do arithmetic.
TEST(SubTree, LiteralNumericPortsPreserveType)
{
// clang-format off
static const char* xml_text = R"(
<root BTCPP_format="4" main_tree_to_execute="MainTree">

<BehaviorTree ID="MainTree">
<Sequence>
<SubTree ID="DoMath" int_val="42" dbl_val="3.14" str_val="hello"
remapped_val="{from_parent}" />
</Sequence>
</BehaviorTree>

<BehaviorTree ID="DoMath">
<Sequence>
<ScriptCondition code=" int_val + 1 == 43 " />
<ScriptCondition code=" dbl_val > 3.0 " />
<ScriptCondition code=" remapped_val + 1 == 101 " />
</Sequence>
</BehaviorTree>

</root>
)";
// clang-format on

BehaviorTreeFactory factory;

auto tree = factory.createTreeFromText(xml_text);

// Set the remapped parent value as an integer
tree.rootBlackboard()->set("from_parent", 100);

const auto status = tree.tickWhileRunning();
ASSERT_EQ(status, NodeStatus::SUCCESS);
}
Loading