From 203e2d4ce8d5fdfd9f323ad45717890b333465b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindel=C3=B6w?= Date: Thu, 16 Apr 2026 15:16:10 +0200 Subject: [PATCH] Add tcs_opt_membership_add_str and drop_str --- include/tinycsocket.h | 70 +++++++++++++++++++++++++++++++++++++- src/tinycsocket_common.c | 28 +++++++++++++++ src/tinycsocket_internal.h | 34 +++++++++++++++++- src/tinycsocket_posix.c | 4 +++ src/tinycsocket_win32.c | 4 +++ tests/tests.cpp | 66 ++++++++++++++++++++++++++++++++++- 6 files changed, 203 insertions(+), 3 deletions(-) diff --git a/include/tinycsocket.h b/include/tinycsocket.h index 7700ead..af51aa1 100644 --- a/include/tinycsocket.h +++ b/include/tinycsocket.h @@ -29,7 +29,7 @@ #ifndef TINYCSOCKET_INTERNAL_H_ #define TINYCSOCKET_INTERNAL_H_ -static const char* const TCS_VERSION_TXT = "v0.3.70"; +static const char* const TCS_VERSION_TXT = "v0.3.71"; static const char* const TCS_LICENSE_TXT = "Copyright 2018 Markus Lindelöw\n" "\n" @@ -125,8 +125,10 @@ static const char* const TCS_LICENSE_TXT = * - TcsResult tcs_opt_nonblocking_set(TcsSocket socket_ctx, bool do_nonblocking); * - TcsResult tcs_opt_nonblocking_get(TcsSocket socket_ctx, bool* is_nonblocking); * - TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +* - TcsResult tcs_opt_membership_add_str(TcsSocket socket_ctx, const char* multicast_address); * - TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, const struct TcsAddress* local_address, const struct TcsAddress* multicast_address); * - TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +* - TcsResult tcs_opt_membership_drop_str(TcsSocket socket_ctx, const char* multicast_address); * - TcsResult tcs_opt_membership_drop_from(TcsSocket socket_ctx, const struct TcsAddress* local_address, const struct TcsAddress* multicast_address); * - TcsResult tcs_opt_multicast_interface_set(TcsSocket socket_ctx, const struct TcsAddress* local_address); * - TcsResult tcs_opt_multicast_loop_set(TcsSocket socket_ctx, bool do_loopback); @@ -1681,6 +1683,21 @@ TcsResult tcs_opt_membership_drop_from(TcsSocket socket_ctx, */ TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +/** +* @brief Join a multicast group by address string. +* +* Resolves the multicast address string and joins the group using the default local interface. +* +* @param socket_ctx socket to configure. +* @param multicast_address multicast group address string (e.g. "239.1.2.3" or "ff02::1"). +* @return #TCS_SUCCESS if successful, otherwise the error code. +* @retval #TCS_ERROR_INVALID_ARGUMENT if multicast_address is NULL. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if the address string could not be resolved. +* @see tcs_opt_membership_add() +* @see tcs_opt_membership_drop_str() +*/ +TcsResult tcs_opt_membership_add_str(TcsSocket socket_ctx, const char* multicast_address); + /** * @brief Leave a multicast group using the default local interface. * @@ -1690,6 +1707,21 @@ TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* */ TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +/** +* @brief Leave a multicast group by address string. +* +* Parses the multicast address string and leaves the group using the default local interface. +* +* @param socket_ctx socket to configure. +* @param multicast_address multicast group address string (e.g. "239.1.2.3" or "ff02::1"). +* @return #TCS_SUCCESS if successful, otherwise the error code. +* @retval #TCS_ERROR_INVALID_ARGUMENT if multicast_address is NULL. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if the address string could not be resolved. +* @see tcs_opt_membership_drop() +* @see tcs_opt_membership_add_str() +*/ +TcsResult tcs_opt_membership_drop_str(TcsSocket socket_ctx, const char* multicast_address); + /** * @brief Set the outgoing interface for multicast packets. * @@ -3417,6 +3449,8 @@ TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* return tcs_opt_membership_add_to(socket_ctx, &local_address, multicast_address); } +// tcs_opt_membership_add_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, const struct TcsAddress* local_address, const struct TcsAddress* multicast_address) @@ -3499,6 +3533,8 @@ TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, return TCS_ERROR_NOT_IMPLEMENTED; } +// tcs_opt_membership_drop_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address) { if (socket_ctx == TCS_SOCKET_INVALID) @@ -5405,6 +5441,8 @@ TcsResult tcs_opt_nonblocking_get(TcsSocket socket_ctx, bool* is_non_blocking) return TCS_ERROR_NOT_SUPPORTED; } +// tcs_opt_membership_add_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* multicast_address) { if (socket_ctx == TCS_SOCKET_INVALID) @@ -5460,6 +5498,8 @@ TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, return TCS_ERROR_NOT_IMPLEMENTED; } +// tcs_opt_membership_drop_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address) { if (socket_ctx == TCS_SOCKET_INVALID) @@ -6784,8 +6824,36 @@ TcsResult tcs_opt_priority_get(TcsSocket socket_ctx, int* priority) // tcs_opt_nonblocking_get() is defined in OS specific files // tcs_opt_membership_add() is defined in OS specific files + +TcsResult tcs_opt_membership_add_str(TcsSocket socket_ctx, const char* multicast_address) +{ + if (multicast_address == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + + struct TcsAddress addr = TCS_ADDRESS_NONE; + TcsResult res = tcs_address_parse(multicast_address, &addr); + if (res != TCS_SUCCESS) + return res; + + return tcs_opt_membership_add(socket_ctx, &addr); +} + // tcs_opt_membership_add_to() is defined in OS specific files // tcs_opt_membership_drop() is defined in OS specific files + +TcsResult tcs_opt_membership_drop_str(TcsSocket socket_ctx, const char* multicast_address) +{ + if (multicast_address == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + + struct TcsAddress addr = TCS_ADDRESS_NONE; + TcsResult res = tcs_address_parse(multicast_address, &addr); + if (res != TCS_SUCCESS) + return res; + + return tcs_opt_membership_drop(socket_ctx, &addr); +} + // tcs_opt_membership_drop_from() is defined in OS specific files // ######## Address and Interface Utilities ######## diff --git a/src/tinycsocket_common.c b/src/tinycsocket_common.c index b1b9f3b..1c7998b 100644 --- a/src/tinycsocket_common.c +++ b/src/tinycsocket_common.c @@ -805,8 +805,36 @@ TcsResult tcs_opt_priority_get(TcsSocket socket_ctx, int* priority) // tcs_opt_nonblocking_get() is defined in OS specific files // tcs_opt_membership_add() is defined in OS specific files + +TcsResult tcs_opt_membership_add_str(TcsSocket socket_ctx, const char* multicast_address) +{ + if (multicast_address == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + + struct TcsAddress addr = TCS_ADDRESS_NONE; + TcsResult res = tcs_address_parse(multicast_address, &addr); + if (res != TCS_SUCCESS) + return res; + + return tcs_opt_membership_add(socket_ctx, &addr); +} + // tcs_opt_membership_add_to() is defined in OS specific files // tcs_opt_membership_drop() is defined in OS specific files + +TcsResult tcs_opt_membership_drop_str(TcsSocket socket_ctx, const char* multicast_address) +{ + if (multicast_address == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + + struct TcsAddress addr = TCS_ADDRESS_NONE; + TcsResult res = tcs_address_parse(multicast_address, &addr); + if (res != TCS_SUCCESS) + return res; + + return tcs_opt_membership_drop(socket_ctx, &addr); +} + // tcs_opt_membership_drop_from() is defined in OS specific files // ######## Address and Interface Utilities ######## diff --git a/src/tinycsocket_internal.h b/src/tinycsocket_internal.h index c192ffd..ba09ff0 100644 --- a/src/tinycsocket_internal.h +++ b/src/tinycsocket_internal.h @@ -23,7 +23,7 @@ #ifndef TINYCSOCKET_INTERNAL_H_ #define TINYCSOCKET_INTERNAL_H_ -static const char* const TCS_VERSION_TXT = "v0.3.70"; +static const char* const TCS_VERSION_TXT = "v0.3.71"; static const char* const TCS_LICENSE_TXT = "Copyright 2018 Markus Lindelöw\n" "\n" @@ -119,8 +119,10 @@ static const char* const TCS_LICENSE_TXT = * - TcsResult tcs_opt_nonblocking_set(TcsSocket socket_ctx, bool do_nonblocking); * - TcsResult tcs_opt_nonblocking_get(TcsSocket socket_ctx, bool* is_nonblocking); * - TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +* - TcsResult tcs_opt_membership_add_str(TcsSocket socket_ctx, const char* multicast_address); * - TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, const struct TcsAddress* local_address, const struct TcsAddress* multicast_address); * - TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +* - TcsResult tcs_opt_membership_drop_str(TcsSocket socket_ctx, const char* multicast_address); * - TcsResult tcs_opt_membership_drop_from(TcsSocket socket_ctx, const struct TcsAddress* local_address, const struct TcsAddress* multicast_address); * - TcsResult tcs_opt_multicast_interface_set(TcsSocket socket_ctx, const struct TcsAddress* local_address); * - TcsResult tcs_opt_multicast_loop_set(TcsSocket socket_ctx, bool do_loopback); @@ -1675,6 +1677,21 @@ TcsResult tcs_opt_membership_drop_from(TcsSocket socket_ctx, */ TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +/** +* @brief Join a multicast group by address string. +* +* Resolves the multicast address string and joins the group using the default local interface. +* +* @param socket_ctx socket to configure. +* @param multicast_address multicast group address string (e.g. "239.1.2.3" or "ff02::1"). +* @return #TCS_SUCCESS if successful, otherwise the error code. +* @retval #TCS_ERROR_INVALID_ARGUMENT if multicast_address is NULL. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if the address string could not be resolved. +* @see tcs_opt_membership_add() +* @see tcs_opt_membership_drop_str() +*/ +TcsResult tcs_opt_membership_add_str(TcsSocket socket_ctx, const char* multicast_address); + /** * @brief Leave a multicast group using the default local interface. * @@ -1684,6 +1701,21 @@ TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* */ TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address); +/** +* @brief Leave a multicast group by address string. +* +* Parses the multicast address string and leaves the group using the default local interface. +* +* @param socket_ctx socket to configure. +* @param multicast_address multicast group address string (e.g. "239.1.2.3" or "ff02::1"). +* @return #TCS_SUCCESS if successful, otherwise the error code. +* @retval #TCS_ERROR_INVALID_ARGUMENT if multicast_address is NULL. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if the address string could not be resolved. +* @see tcs_opt_membership_drop() +* @see tcs_opt_membership_add_str() +*/ +TcsResult tcs_opt_membership_drop_str(TcsSocket socket_ctx, const char* multicast_address); + /** * @brief Set the outgoing interface for multicast packets. * diff --git a/src/tinycsocket_posix.c b/src/tinycsocket_posix.c index fc20abd..5aeafcf 100644 --- a/src/tinycsocket_posix.c +++ b/src/tinycsocket_posix.c @@ -1196,6 +1196,8 @@ TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* return tcs_opt_membership_add_to(socket_ctx, &local_address, multicast_address); } +// tcs_opt_membership_add_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, const struct TcsAddress* local_address, const struct TcsAddress* multicast_address) @@ -1278,6 +1280,8 @@ TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, return TCS_ERROR_NOT_IMPLEMENTED; } +// tcs_opt_membership_drop_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address) { if (socket_ctx == TCS_SOCKET_INVALID) diff --git a/src/tinycsocket_win32.c b/src/tinycsocket_win32.c index a3d256c..14dc418 100644 --- a/src/tinycsocket_win32.c +++ b/src/tinycsocket_win32.c @@ -1264,6 +1264,8 @@ TcsResult tcs_opt_nonblocking_get(TcsSocket socket_ctx, bool* is_non_blocking) return TCS_ERROR_NOT_SUPPORTED; } +// tcs_opt_membership_add_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_add(TcsSocket socket_ctx, const struct TcsAddress* multicast_address) { if (socket_ctx == TCS_SOCKET_INVALID) @@ -1319,6 +1321,8 @@ TcsResult tcs_opt_membership_add_to(TcsSocket socket_ctx, return TCS_ERROR_NOT_IMPLEMENTED; } +// tcs_opt_membership_drop_str() is defined in tinycsocket_common.c + TcsResult tcs_opt_membership_drop(TcsSocket socket_ctx, const struct TcsAddress* multicast_address) { if (socket_ctx == TCS_SOCKET_INVALID) diff --git a/tests/tests.cpp b/tests/tests.cpp index d6bbc57..34c7276 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -110,7 +110,8 @@ TEST_CASE("Example from README") size_t bytes_received = 0; CHECK(tcs_receive(client_socket, recv_buffer, 8192, TCS_FLAG_NONE, &bytes_received) == TCS_SUCCESS); TcsResult shutdown_res = tcs_shutdown(client_socket, TCS_SD_BOTH); - CHECK((shutdown_res == TCS_SUCCESS || shutdown_res == TCS_ERROR_NOT_CONNECTED)); + CHECK((shutdown_res == TCS_SUCCESS || shutdown_res == TCS_ERROR_NOT_CONNECTED || + shutdown_res == TCS_ERROR_CONNECTION_RESET)); CHECK(tcs_close(&client_socket) == TCS_SUCCESS); REQUIRE(tcs_lib_free() == TCS_SUCCESS); @@ -1516,6 +1517,28 @@ TEST_CASE("Simple Multicast Add Membership") REQUIRE(tcs_lib_free() == TCS_SUCCESS); } +TEST_CASE("tcs_opt_membership_add_str") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + TcsAddress loopback = TCS_ADDRESS_NONE; + loopback.family = TCS_AF_IP4; + loopback.data.ip4.address = TCS_ADDRESS_LOOPBACK_IP4; + loopback.data.ip4.port = 1902; + + TcsSocket socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket(&socket, TCS_AF_IP4, TCS_SOCK_DGRAM, 0) == TCS_SUCCESS); + CHECK(tcs_bind(socket, &loopback) == TCS_SUCCESS); + + // When/Then + CHECK(tcs_opt_membership_add_str(socket, "239.255.255.251") == TCS_SUCCESS); + + // Clean up + CHECK(tcs_close(&socket) == TCS_SUCCESS); + REQUIRE(tcs_lib_free() == TCS_SUCCESS); +} + TEST_CASE("Multicast Add-Drop-Add Membership") { // Setup @@ -1573,6 +1596,29 @@ TEST_CASE("Multicast Add-Drop-Add Membership") REQUIRE(tcs_lib_free() == TCS_SUCCESS); } +TEST_CASE("tcs_opt_membership_drop_str") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + TcsAddress loopback = TCS_ADDRESS_NONE; + loopback.family = TCS_AF_IP4; + loopback.data.ip4.address = TCS_ADDRESS_LOOPBACK_IP4; + loopback.data.ip4.port = 1903; + + TcsSocket socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket(&socket, TCS_AF_IP4, TCS_SOCK_DGRAM, 0) == TCS_SUCCESS); + CHECK(tcs_bind(socket, &loopback) == TCS_SUCCESS); + + // When/Then + CHECK(tcs_opt_membership_add_str(socket, "239.255.255.251") == TCS_SUCCESS); + CHECK(tcs_opt_membership_drop_str(socket, "239.255.255.251") == TCS_SUCCESS); + + // Clean up + CHECK(tcs_close(&socket) == TCS_SUCCESS); + REQUIRE(tcs_lib_free() == TCS_SUCCESS); +} + #if defined(__linux__) TEST_CASE("Simple AVTP talker") { @@ -2170,6 +2216,24 @@ TEST_CASE("tcs_socket_packet_str DGRAM") // Clean up REQUIRE(tcs_lib_free() == TCS_SUCCESS); } + +TEST_CASE("tcs_socket_packet multicast add and drop str") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // Given - create and bind a DGRAM packet socket on lo + TcsSocket socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_packet_str(&socket, "lo", 0x22F0, TCS_SOCK_DGRAM) == TCS_SUCCESS); + + // When/Then - join and leave a multicast MAC group via str + CHECK(tcs_opt_membership_add_str(socket, "91:e0:f0:00:fe:00") == TCS_SUCCESS); + CHECK(tcs_opt_membership_drop_str(socket, "91:e0:f0:00:fe:00") == TCS_SUCCESS); + + // Clean up + CHECK(tcs_close(&socket) == TCS_SUCCESS); + REQUIRE(tcs_lib_free() == TCS_SUCCESS); +} #endif TEST_CASE("tcs_socket_packet invalid arguments")