Skip to content
Open
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
181 changes: 181 additions & 0 deletions rmw/include/rmw/impl/cpp/buffer_backend_metadata.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright 2026 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef RMW__IMPL__CPP__BUFFER_BACKEND_METADATA_HPP_
#define RMW__IMPL__CPP__BUFFER_BACKEND_METADATA_HPP_

#include <algorithm>
#include <cstddef>
#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace rmw
{
namespace impl
{
namespace cpp
{

inline constexpr char kBufferBackendMetadataPrefix[] = "backends:";

inline std::string
escape_buffer_backend_metadata_field(const std::string & input)
{
std::string out;
out.reserve(input.size());
for (char c : input) {
switch (c) {
case '%':
out += "%25";
break;
case ';':
out += "%3B";
break;
case ':':
out += "%3A";
break;
case '/':
out += "%2F";
break;
default:
out += c;
break;
}
}
return out;
}

inline int
buffer_backend_metadata_hex_value(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'A' && c <= 'F') {
return 10 + (c - 'A');
}
if (c >= 'a' && c <= 'f') {
return 10 + (c - 'a');
}
return -1;
}

inline std::string
unescape_buffer_backend_metadata_field(const std::string_view input)
{
std::string out;
out.reserve(input.size());
for (size_t i = 0; i < input.size(); ++i) {
if (input[i] == '%' && i + 2 < input.size()) {
int hi = buffer_backend_metadata_hex_value(input[i + 1]);
int lo = buffer_backend_metadata_hex_value(input[i + 2]);
if (hi >= 0 && lo >= 0) {
out += static_cast<char>((hi << 4) | lo);
i += 2;
continue;
}
}
out += input[i];
}
return out;
}

template<typename MapT>
std::string
serialize_buffer_backend_metadata(const MapT & metadata)
{
if (metadata.empty()) {
return {};
}

std::vector<std::pair<std::string, std::string>> entries;
entries.reserve(metadata.size());
for (const auto & pair : metadata) {
if (!pair.first.empty()) {
entries.emplace_back(pair.first, pair.second);
}
}
if (entries.empty()) {
return {};
}

std::sort(
entries.begin(), entries.end(),
[](const auto & lhs, const auto & rhs) {
return lhs.first < rhs.first;
});

std::string output{kBufferBackendMetadataPrefix};
for (size_t i = 0; i < entries.size(); ++i) {
output += escape_buffer_backend_metadata_field(entries[i].first);
output += ":";
output += escape_buffer_backend_metadata_field(entries[i].second);
if (i + 1 < entries.size()) {
output += ";";
}
}
return output;
}

inline std::map<std::string, std::string>
parse_buffer_backend_metadata(const char * serialized_metadata)
{
std::map<std::string, std::string> result;
if (!serialized_metadata || serialized_metadata[0] == '\0') {
return result;
}

std::string_view input{serialized_metadata};
constexpr std::string_view prefix{kBufferBackendMetadataPrefix};
if (input.rfind(prefix, 0) != 0) {
return result;
}

std::string_view body = input.substr(prefix.size());
if (body.empty()) {
return result;
}

size_t start = 0;
while (start <= body.size()) {
size_t sep = body.find(';', start);
std::string_view entry = body.substr(
start, sep == std::string_view::npos ? std::string_view::npos : sep - start);
if (!entry.empty()) {
size_t colon = entry.find(':');
std::string name = unescape_buffer_backend_metadata_field(entry.substr(0, colon));
std::string metadata;
if (colon != std::string_view::npos) {
metadata = unescape_buffer_backend_metadata_field(entry.substr(colon + 1));
}
if (!name.empty()) {
result[std::move(name)] = std::move(metadata);
}
}
if (sep == std::string_view::npos) {
break;
}
start = sep + 1;
}
return result;
}

} // namespace cpp
} // namespace impl
} // namespace rmw

#endif // RMW__IMPL__CPP__BUFFER_BACKEND_METADATA_HPP_
43 changes: 43 additions & 0 deletions rmw/include/rmw/topic_endpoint_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ typedef struct RMW_PUBLIC_TYPE rmw_topic_endpoint_info_s
uint8_t endpoint_gid[RMW_GID_STORAGE_SIZE];
/// QoS profile of the endpoint
rmw_qos_profile_t qos_profile;
/// Serialized buffer backend metadata advertised for this endpoint
const char * buffer_backend_metadata;
} rmw_topic_endpoint_info_t;

/// Return zero initialized topic endpoint info data structure.
Expand Down Expand Up @@ -363,6 +365,47 @@ rmw_topic_endpoint_info_set_qos_profile(
rmw_topic_endpoint_info_t * topic_endpoint_info,
const rmw_qos_profile_t * qos_profile);

/// Set serialized buffer backend metadata in the given topic endpoint info data structure.
/**
* Allocates memory and copies the serialized `buffer_backend_metadata` string
* into the topic endpoint info.
*
* <hr>
* Attribute | Adherence
* ------------------ | -------------
* Allocates Memory | Yes
* Thread-Safe | No
* Uses Atomics | No
* Lock-Free | Yes
*
* \par Thread-safety
* Setting a member is a reentrant procedure, but access to the
* topic endpoint info data structure is not synchronized.
* It is not safe to read or write the `buffer_backend_metadata` member of the
* given `topic_endpoint` while setting it.
* Access to C-style string arguments is read-only but it is not synchronized.
* Concurrent `buffer_backend_metadata` reads are safe, but concurrent reads and writes are not.
*
* \pre Given `buffer_backend_metadata` is a valid C-style string i.e. NULL terminated.
*
* \param[inout] topic_endpoint_info Data structure to be populated.
* \param[in] buffer_backend_metadata Serialized buffer backend metadata to be set.
* \param[in] allocator Allocator to be used.
* \returns `RMW_RET_OK` if successful, or
* \returns `RMW_RET_INVALID_ARGUMENT` if `topic_endpoint_info` is NULL, or
* \returns `RMW_RET_INVALID_ARGUMENT` if `buffer_backend_metadata` is NULL, or
* \returns `RMW_RET_BAD_ALLOC` if memory allocation fails, or
* \returns `RMW_RET_ERROR` when an unspecified error occurs.
* \remark This function sets the RMW error state on failure.
*/
RMW_PUBLIC
RMW_WARN_UNUSED
rmw_ret_t
rmw_topic_endpoint_info_set_buffer_backend_metadata(
rmw_topic_endpoint_info_t * topic_endpoint_info,
const char * buffer_backend_metadata,
rcutils_allocator_t * allocator);

#ifdef __cplusplus
}
#endif
Expand Down
31 changes: 31 additions & 0 deletions rmw/src/topic_endpoint_info.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ _rmw_topic_endpoint_info_fini_topic_type(
return _rmw_topic_endpoint_info_fini_str(&topic_endpoint_info->topic_type, allocator);
}

rmw_ret_t
_rmw_topic_endpoint_info_fini_buffer_backend_metadata(
rmw_topic_endpoint_info_t * topic_endpoint_info,
rcutils_allocator_t * allocator)
{
return _rmw_topic_endpoint_info_fini_str(
&topic_endpoint_info->buffer_backend_metadata, allocator);
}

rmw_ret_t
rmw_topic_endpoint_info_fini(
rmw_topic_endpoint_info_t * topic_endpoint_info,
Expand Down Expand Up @@ -90,6 +99,10 @@ rmw_topic_endpoint_info_fini(
if (ret != RMW_RET_OK) {
return ret;
}
ret = _rmw_topic_endpoint_info_fini_buffer_backend_metadata(topic_endpoint_info, allocator);
if (ret != RMW_RET_OK) {
return ret;
}

*topic_endpoint_info = rmw_get_zero_initialized_topic_endpoint_info();

Expand Down Expand Up @@ -246,3 +259,21 @@ rmw_topic_endpoint_info_set_qos_profile(
topic_endpoint_info->qos_profile = *qos_profile;
return RMW_RET_OK;
}

rmw_ret_t
rmw_topic_endpoint_info_set_buffer_backend_metadata(
rmw_topic_endpoint_info_t * topic_endpoint_info,
const char * buffer_backend_metadata,
rcutils_allocator_t * allocator)
{
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RMW_RET_INVALID_ARGUMENT);

if (!topic_endpoint_info) {
RMW_SET_ERROR_MSG("topic_endpoint_info is null");
return RMW_RET_INVALID_ARGUMENT;
}
return _rmw_topic_endpoint_info_copy_str(
&topic_endpoint_info->buffer_backend_metadata,
buffer_backend_metadata,
allocator);
}
67 changes: 66 additions & 1 deletion rmw/test/test_topic_endpoint_info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#include <cstdlib>
#include <cstring>
#include <map>
#include <string>

#include "gmock/gmock.h"
#include "osrf_testing_tools_cpp/scope_exit.hpp"
#include "rcutils/allocator.h"

#include "rmw/error_handling.h"
#include "rmw/impl/cpp/buffer_backend_metadata.hpp"
#include "rmw/topic_endpoint_info.h"
#include "rmw/types.h"

Expand Down Expand Up @@ -217,6 +222,61 @@ TEST(test_topic_endpoint_info, set_qos_profile) {
false) << "Unequal avoid namespace conventions";
}

TEST(test_topic_endpoint_info, set_buffer_backend_metadata) {
rmw_topic_endpoint_info_t topic_endpoint_info = rmw_get_zero_initialized_topic_endpoint_info();
rcutils_allocator_t allocator = rcutils_get_default_allocator();
char * val = get_mallocd_string("backends:test:version=1.0");

rmw_ret_t ret =
rmw_topic_endpoint_info_set_buffer_backend_metadata(&topic_endpoint_info, val, nullptr);
EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) << "Expected invalid argument for null allocator";
rmw_reset_error();

ret = rmw_topic_endpoint_info_set_buffer_backend_metadata(
&topic_endpoint_info, nullptr, &allocator);
EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) << "Expected invalid argument for null metadata";
rmw_reset_error();

ret = rmw_topic_endpoint_info_set_buffer_backend_metadata(nullptr, val, &allocator);
EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) <<
"Expected invalid argument for null topic_endpoint_info";
rmw_reset_error();

OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT(
{
allocator.deallocate(
const_cast<char *>(topic_endpoint_info.buffer_backend_metadata),
allocator.state);
});
ret = rmw_topic_endpoint_info_set_buffer_backend_metadata(
&topic_endpoint_info, val, &allocator);
EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid arguments";
free(val);
EXPECT_STREQ(topic_endpoint_info.buffer_backend_metadata, "backends:test:version=1.0");
}

TEST(test_topic_endpoint_info, buffer_backend_metadata_serialization) {
std::map<std::string, std::string> metadata{
{"test", "version=1.0"},
{"cpu", ""},
{"with:colon", "semi;slash/percent%"},
};

const std::string serialized =
rmw::impl::cpp::serialize_buffer_backend_metadata(metadata);
EXPECT_EQ(
serialized,
"backends:cpu:;test:version=1.0;with%3Acolon:semi%3Bslash%2Fpercent%25");

EXPECT_EQ(rmw::impl::cpp::parse_buffer_backend_metadata(serialized.c_str()), metadata);
EXPECT_TRUE(
rmw::impl::cpp::serialize_buffer_backend_metadata(
std::map<std::string, std::string>{}).empty());
EXPECT_TRUE(rmw::impl::cpp::parse_buffer_backend_metadata(nullptr).empty());
EXPECT_TRUE(rmw::impl::cpp::parse_buffer_backend_metadata("").empty());
EXPECT_TRUE(rmw::impl::cpp::parse_buffer_backend_metadata("not-backends:cpu:").empty());
}

TEST(test_topic_endpoint_info, zero_init) {
rmw_topic_endpoint_info_t topic_endpoint_info = rmw_get_zero_initialized_topic_endpoint_info();
EXPECT_FALSE(topic_endpoint_info.node_name);
Expand Down Expand Up @@ -245,6 +305,7 @@ TEST(test_topic_endpoint_info, zero_init) {
EXPECT_EQ(
topic_endpoint_info.qos_profile.avoid_ros_namespace_conventions,
false) << "Non-zero avoid namespace conventions";
EXPECT_FALSE(topic_endpoint_info.buffer_backend_metadata);
}

TEST(test_topic_endpoint_info, fini) {
Expand Down Expand Up @@ -279,6 +340,9 @@ TEST(test_topic_endpoint_info, fini) {
EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid node_name arguments";
ret = rmw_topic_endpoint_info_set_topic_type(&topic_endpoint_info, "type", &allocator);
EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid topic_type arguments";
ret = rmw_topic_endpoint_info_set_buffer_backend_metadata(
&topic_endpoint_info, "backends:test:", &allocator);
EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid buffer backend metadata";
ret = rmw_topic_endpoint_info_fini(&topic_endpoint_info, nullptr);
EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) << "Expected invalid argument for null allocator";
rmw_reset_error();
Expand Down Expand Up @@ -314,4 +378,5 @@ TEST(test_topic_endpoint_info, fini) {
"Non-zero liveliness lease duration nsec";
EXPECT_EQ(topic_endpoint_info.qos_profile.avoid_ros_namespace_conventions, false) <<
"Non-zero avoid namespace conventions";
EXPECT_FALSE(topic_endpoint_info.buffer_backend_metadata);
}