From 49a7b78a2e47bdd25d430d000202cf4eae640c45 Mon Sep 17 00:00:00 2001 From: David Vadovszki Date: Thu, 1 Oct 2026 16:10:59 -0600 Subject: [PATCH] Name the forbidden character in the port name error (fixes #1221) Co-Authored-By: Claude Opus 5.5 --- include/behaviortree_cpp/basic_types.h | 8 ++++++++ src/basic_types.cpp | 9 +++++++++ src/xml_parsing.cpp | 10 ---------- tests/gtest_name_validation.cpp | 26 ++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index eb89602ac..f19917833 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -370,6 +370,9 @@ struct Timestamp /// and control characters (ASCII 0-31, 127). UTF-8 multibyte sequences are allowed. [[nodiscard]] char findForbiddenChar(StringView name); +/// Describes a character returned by findForbiddenChar() for an error message. +[[nodiscard]] std::string formatForbiddenChar(char c); + class TypeInfo { public: @@ -468,6 +471,11 @@ template auto sname = static_cast(name); if(!IsAllowedPortName(sname)) { + if(const char c = findForbiddenChar(sname); c != '\0') + { + throw RuntimeError("Port name '", sname, "' contains forbidden character ", + formatForbiddenChar(c)); + } throw RuntimeError("The name of a port must not be `name` or `ID` " "and must start with an alphabetic character. " "Underscore is reserved."); diff --git a/src/basic_types.cpp b/src/basic_types.cpp index 0c82b0d20..e4c1554eb 100644 --- a/src/basic_types.cpp +++ b/src/basic_types.cpp @@ -613,6 +613,15 @@ char findForbiddenChar(StringView name) return '\0'; } +std::string formatForbiddenChar(char c) +{ + if(c < 32 || c == 127) + { + return "control character (ASCII " + std::to_string(static_cast(c)) + ")"; + } + return std::string("'") + c + "'"; +} + Any convertFromJSON(StringView json_text, std::type_index type) { const nlohmann::json json = nlohmann::json::parse(json_text); diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index e37836d0f..aa920e067 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -103,16 +103,6 @@ auto StrEqual = [](const char* str1, const char* str2) -> bool { return strcmp(str1, str2) == 0; }; -// Helper to format forbidden character for error messages -std::string formatForbiddenChar(char c) -{ - if(c < 32 || c == 127) - { - return "control character (ASCII " + std::to_string(static_cast(c)) + ")"; - } - return std::string("'") + c + "'"; -} - void validateModelName(const std::string& name, int line_number) { const auto line_str = std::to_string(line_number); diff --git a/tests/gtest_name_validation.cpp b/tests/gtest_name_validation.cpp index f60352346..1fb581a67 100644 --- a/tests/gtest_name_validation.cpp +++ b/tests/gtest_name_validation.cpp @@ -120,6 +120,32 @@ TEST(NameValidation, IsAllowedPortName_Invalid) EXPECT_FALSE(IsAllowedPortName("port")); // angle brackets } +TEST(NameValidation, CreatePort_NamesForbiddenChar) +{ + try + { + (void)InputPort("goal.pose"); + FAIL() << "Expected RuntimeError to be thrown"; + } + catch(const RuntimeError& e) + { + const std::string msg = e.what(); + EXPECT_NE(msg.find("goal.pose"), std::string::npos) << msg; + EXPECT_NE(msg.find("forbidden character '.'"), std::string::npos) << msg; + } + + try + { + (void)InputPort("goal\tpose"); + FAIL() << "Expected RuntimeError to be thrown"; + } + catch(const RuntimeError& e) + { + const std::string msg = e.what(); + EXPECT_NE(msg.find("control character (ASCII 9)"), std::string::npos) << msg; + } +} + // ============== Tests for XML parsing validation ============== class NameValidationXMLTest : public testing::Test