diff --git a/include/async_mqtt5/detail/utf8_mqtt.hpp b/include/async_mqtt5/detail/utf8_mqtt.hpp index a15554c..05efeb6 100644 --- a/include/async_mqtt5/detail/utf8_mqtt.hpp +++ b/include/async_mqtt5/detail/utf8_mqtt.hpp @@ -32,24 +32,39 @@ inline int pop_front_unichar(std::string_view& s) { return ch; } -inline bool is_valid_mqtt_utf8(std::string_view str) { - constexpr size_t max_sz = 65535; - - if (str.size() > max_sz) - return false; - +inline bool is_valid_mqtt_utf8_char(int c) { constexpr int fe_flag = 0xFE; constexpr int ff_flag = 0xFF; + return c > 0x001F && // U+0000...U+001F control characters + (c < 0x007F || c > 0x009F) && // U+007F...0+009F control characters + (c < 0xD800 || c > 0xDFFF) && // U+D800...U+DFFF surrogates + (c < 0xFDD0 || c > 0xFDEF) && // U+FDD0...U+FDEF non-characters + (c & fe_flag) != fe_flag && // non-characters + (c & ff_flag) != ff_flag; +} + +inline bool is_valid_mqtt_utf8_non_wildcard_char(int c) { + return c != '+' && c != '#' && is_valid_mqtt_utf8_char(c); +} + +inline bool is_valid_string_size(size_t sz) { + constexpr size_t max_sz = 65535; + return sz <= max_sz; +} + +inline bool is_valid_topic_size(size_t sz) { + constexpr size_t min_sz = 1; + return min_sz <= sz && is_valid_string_size(sz); +} + +template +bool is_valid_impl( + std::string_view str, ValidationFun&& validation_fun +) { while (!str.empty()) { int c = pop_front_unichar(str); - - auto is_valid = c > 0x001F && // U+0000...U+001F control characters - (c < 0x007F || c > 0x009F) && // U+007F...0+009F control characters - (c < 0xD800 || c > 0xDFFF) && // U+D800...U+DFFF surrogates - (c < 0xFDD0 || c > 0xFDEF) && // U+FDD0...U+FDEF non-characters - (c & fe_flag) != fe_flag && // non-characters - (c & ff_flag) != ff_flag; + bool is_valid = validation_fun(c); if (!is_valid) return false; @@ -58,8 +73,54 @@ inline bool is_valid_mqtt_utf8(std::string_view str) { return true; } -inline bool is_valid_utf8_topic(std::string_view str) { - return !str.empty() && is_valid_mqtt_utf8(str); +inline bool is_valid_mqtt_utf8(std::string_view str) { + return is_valid_string_size(str.size()) && + is_valid_impl(str, is_valid_mqtt_utf8_char); +} + +inline bool is_valid_topic_name(std::string_view str) { + return is_valid_topic_size(str.size()) && + is_valid_impl(str, is_valid_mqtt_utf8_non_wildcard_char); +} + +inline bool is_valid_topic_filter(std::string_view str) { + if (!is_valid_topic_size(str.size())) + return false; + + constexpr int multi_lvl_wildcard = '#'; + constexpr int single_lvl_wildcard = '+'; + + // must be the last character preceded by '/' or stand alone + // #, .../# + if (str.back() == multi_lvl_wildcard) { + str.remove_suffix(1); + + if (!str.empty() && str.back() != '/') + return false; + } + + int last_c = -1; + while (!str.empty()) { + int c = pop_front_unichar(str); + + // can be used at any level, but must occupy an entire level + // +, +/..., .../+/..., .../+ + bool is_valid_single_lvl = (c == single_lvl_wildcard) && + (str.empty() || str.front() == '/') && + (last_c == -1 || last_c == '/'); + + bool is_valid_mqtt_utf_8 = is_valid_mqtt_utf8_non_wildcard_char(c); + + + if (is_valid_mqtt_utf_8 || is_valid_single_lvl) { + last_c = c; + continue; + } + + return false; + } + + return true; } } // namespace async_mqtt5::detail diff --git a/include/async_mqtt5/impl/publish_send_op.hpp b/include/async_mqtt5/impl/publish_send_op.hpp index f83f35b..5063ca0 100644 --- a/include/async_mqtt5/impl/publish_send_op.hpp +++ b/include/async_mqtt5/impl/publish_send_op.hpp @@ -343,7 +343,7 @@ private: error_code validate_publish( const std::string& topic, retain_e retain, const publish_props& props ) { - if (!is_valid_utf8_topic(topic)) + if (!is_valid_topic_name(topic)) return client::error::invalid_topic; const auto& [max_qos, retain_avail, topic_alias_max] = diff --git a/include/async_mqtt5/impl/subscribe_op.hpp b/include/async_mqtt5/impl/subscribe_op.hpp index 62f8ba8..da790af 100644 --- a/include/async_mqtt5/impl/subscribe_op.hpp +++ b/include/async_mqtt5/impl/subscribe_op.hpp @@ -152,7 +152,7 @@ private: const std::vector& topics ) { for (const auto& topic: topics) - if (!is_valid_utf8_topic(topic.topic_filter)) + if (!is_valid_topic_filter(topic.topic_filter)) return client::error::invalid_topic; return error_code {}; } diff --git a/include/async_mqtt5/impl/unsubscribe_op.hpp b/include/async_mqtt5/impl/unsubscribe_op.hpp index 8b9aa91..77ce83a 100644 --- a/include/async_mqtt5/impl/unsubscribe_op.hpp +++ b/include/async_mqtt5/impl/unsubscribe_op.hpp @@ -149,7 +149,7 @@ private: static error_code validate_topics(const std::vector& topics) { for (const auto& topic : topics) - if (!is_valid_utf8_topic(topic)) + if (!is_valid_topic_filter(topic)) return client::error::invalid_topic; return error_code {}; } diff --git a/test/unit/test/publish_send_op.cpp b/test/unit/test/publish_send_op.cpp index 2994ec4..7d1b3d1 100644 --- a/test/unit/test/publish_send_op.cpp +++ b/test/unit/test/publish_send_op.cpp @@ -14,16 +14,6 @@ using namespace async_mqtt5; -namespace async_mqtt5::client { - -inline std::ostream& operator<<(std::ostream& os, const error& err) { - os << client_error_to_string(err); - return os; -} - -} // end namespace async_mqtt5::client - - BOOST_AUTO_TEST_SUITE(publish_send_op/*, *boost::unit_test::disabled()*/) template < @@ -51,7 +41,7 @@ BOOST_AUTO_TEST_CASE(test_pid_overrun) { auto handler = [&handlers_called](error_code ec, reason_code rc, puback_props) { ++handlers_called; - BOOST_CHECK_EQUAL(ec, client::error::pid_overrun); + BOOST_CHECK(ec == client::error::pid_overrun); BOOST_CHECK_EQUAL(rc, reason_codes::empty); }; @@ -66,23 +56,29 @@ BOOST_AUTO_TEST_CASE(test_pid_overrun) { BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); } -BOOST_AUTO_TEST_CASE(test_invalid_topic) { - constexpr int expected_handlers_called = 1; +BOOST_AUTO_TEST_CASE(test_invalid_topic_names) { + std::vector invalid_topics = { + "", "+", "#", + "invalid+", "invalid#", "invalid/#", "invalid/+" + }; + int expected_handlers_called = invalid_topics.size(); int handlers_called = 0; asio::io_context ioc; using client_service_type = test::test_service; auto svc_ptr = std::make_shared(ioc.get_executor()); - auto handler = [&handlers_called](error_code ec) { - ++handlers_called; - BOOST_CHECK_EQUAL(ec, client::error::invalid_topic); - }; + for (const auto& topic: invalid_topics) { + auto handler = [&handlers_called](error_code ec) { + ++handlers_called; + BOOST_CHECK(ec == client::error::invalid_topic); + }; - detail::publish_send_op< - client_service_type, decltype(handler), qos_e::at_most_once - > { svc_ptr, std::move(handler) } - .perform("", "payload", retain_e::no, {}); + detail::publish_send_op< + client_service_type, decltype(handler), qos_e::at_most_once + > { svc_ptr, std::move(handler) } + .perform(topic, "payload", retain_e::no, {}); + } ioc.run(); BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); @@ -99,7 +95,7 @@ BOOST_AUTO_TEST_CASE(test_publish_immediate_cancellation) { auto h = [&handlers_called](error_code ec, reason_code rc, puback_props) { ++handlers_called; - BOOST_CHECK_EQUAL(ec, asio::error::operation_aborted); + BOOST_CHECK(ec == asio::error::operation_aborted); BOOST_CHECK_EQUAL(rc, reason_codes::empty); }; @@ -128,7 +124,7 @@ BOOST_AUTO_TEST_CASE(test_publish_cancellation) { auto h = [&handlers_called](error_code ec, reason_code rc, puback_props) { ++handlers_called; - BOOST_CHECK_EQUAL(ec, asio::error::operation_aborted); + BOOST_CHECK(ec == asio::error::operation_aborted); BOOST_CHECK_EQUAL(rc, reason_codes::empty); }; diff --git a/test/unit/test/string_validation.cpp b/test/unit/test/string_validation.cpp new file mode 100644 index 0000000..bd3c495 --- /dev/null +++ b/test/unit/test/string_validation.cpp @@ -0,0 +1,104 @@ +#include + +#include + +BOOST_AUTO_TEST_SUITE(utf8_mqtt/*, *boost::unit_test::disabled()*/) + +std::string to_str(int utf8ch) { + if (utf8ch < 0x80) + return { char(utf8ch) }; + if (utf8ch < 0x800) + return { + char((utf8ch >> 6) | 0xC0), + char((utf8ch & 0x3F) | 0x80) + }; + if (utf8ch < 0xFFFF) + return { + char((utf8ch >> 12) | 0xE0), + char(((utf8ch >> 6) & 0x3F) | 0x80), + char((utf8ch & 0x3F) | 0x80) + }; + return { + char((utf8ch >> 18) | 0xF0), + char(((utf8ch >> 12) & 0x3F) | 0x80), + char(((utf8ch >> 6) & 0x3F) | 0x80), + char((utf8ch & 0x3F) | 0x80) + }; +} + +BOOST_AUTO_TEST_CASE(utf8_string_validation) { + using namespace async_mqtt5::detail; + + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8("stringy"), true); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(""), true); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(std::string(75000, 'a')), false); + + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1F)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x20)), true); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x7E)), true); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x7F)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x9F)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xA0)), true); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xD800)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xDFFF)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDD0)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDEF)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDF0)), true); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1FFFE)), false); + BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1FFFF)), false); +} + +BOOST_AUTO_TEST_CASE(topic_filter_validation) { + using namespace async_mqtt5::detail; + + BOOST_CHECK_EQUAL(is_valid_topic_filter(""), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("topic"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("topic/subtopic"), true); + + BOOST_CHECK_EQUAL(is_valid_topic_filter("#"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("#sport"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport#"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/#/tennis"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("#/sport"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("spo#rt/#"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/#"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/tennis/#"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/tennis#"), false); + + BOOST_CHECK_EQUAL(is_valid_topic_filter("+"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("+/"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("/+"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("+/+"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("+/+/+"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("+sport"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport+"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport+/player1"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+player1"), false); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+/player1"), true); + BOOST_CHECK_EQUAL(is_valid_topic_filter("+/sport/+/player1/+"), true); + + BOOST_CHECK_EQUAL(is_valid_topic_filter("+/tennis/#"), true); +} + +BOOST_AUTO_TEST_CASE(topic_name_validation) { + using namespace async_mqtt5::detail; + + BOOST_CHECK_EQUAL(is_valid_topic_name(""), false); + BOOST_CHECK_EQUAL(is_valid_topic_name("topic"), true); + BOOST_CHECK_EQUAL(is_valid_topic_name("topic/subtopic"), true); + + BOOST_CHECK_EQUAL(is_valid_topic_name("#"), false); + BOOST_CHECK_EQUAL(is_valid_topic_name("sport#"), false); + BOOST_CHECK_EQUAL(is_valid_topic_name("sport/#"), false); + + BOOST_CHECK_EQUAL(is_valid_topic_name("+"), false); + BOOST_CHECK_EQUAL(is_valid_topic_name("+sport"), false); + BOOST_CHECK_EQUAL(is_valid_topic_name("sport+"), false); + BOOST_CHECK_EQUAL(is_valid_topic_name("sport/+/player1"), false); + + BOOST_CHECK_EQUAL(is_valid_topic_name("+/tennis/#"), false); +} + +BOOST_AUTO_TEST_SUITE_END(); diff --git a/test/unit/test/subscribe_op.cpp b/test/unit/test/subscribe_op.cpp new file mode 100644 index 0000000..0153143 --- /dev/null +++ b/test/unit/test/subscribe_op.cpp @@ -0,0 +1,42 @@ +#include + +#include + +#include + +#include + +#include "test_common/test_service.hpp" + +using namespace async_mqtt5; + +BOOST_AUTO_TEST_SUITE(subscribe_op/*, *boost::unit_test::disabled()*/) + +BOOST_AUTO_TEST_CASE(test_invalid_topic_filters) { + std::vector invalid_topics = { + "", "+topic", "#topic", "some/#/topic", "topic+" + }; + const int expected_handlers_called = invalid_topics.size(); + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = test::test_service; + auto svc_ptr = std::make_shared(ioc.get_executor()); + + for (const auto& topic: invalid_topics) { + auto handler = [&handlers_called](error_code ec, auto, auto) { + ++handlers_called; + BOOST_CHECK(ec == client::error::invalid_topic); + }; + + detail::subscribe_op< + client_service_type, decltype(handler) + > { svc_ptr, std::move(handler) } + .perform({{ topic, { qos_e::exactly_once } }}, subscribe_props {}); + } + + ioc.run(); + BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/utf8_mqtt.cpp b/test/unit/test/utf8_mqtt.cpp deleted file mode 100644 index 7be0e6d..0000000 --- a/test/unit/test/utf8_mqtt.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include - -#include - -BOOST_AUTO_TEST_SUITE(utf8_mqtt/*, *boost::unit_test::disabled()*/) - -std::string to_str(int utf8ch) { - if (utf8ch < 0x80) - return { char(utf8ch) }; - if (utf8ch < 0x800) - return { - char((utf8ch >> 6) | 0xC0), - char((utf8ch & 0x3F) | 0x80) - }; - if (utf8ch < 0xFFFF) - return { - char((utf8ch >> 12) | 0xE0), - char(((utf8ch >> 6) & 0x3F) | 0x80), - char((utf8ch & 0x3F) | 0x80) - }; - return { - char((utf8ch >> 18) | 0xF0), - char(((utf8ch >> 12) & 0x3F) | 0x80), - char(((utf8ch >> 6) & 0x3F) | 0x80), - char((utf8ch & 0x3F) | 0x80) - }; -} - -BOOST_AUTO_TEST_CASE(utf8_string_validation) { - using namespace async_mqtt5::detail; - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8("stringy"), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(""), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(1)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(31)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(32)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(126)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(127)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(159)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(160)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(55296)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(57343)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(64976)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(65007)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(65008)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(131070)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(131071)), false); -} - -BOOST_AUTO_TEST_CASE(utf8_topic_validation) { - using namespace async_mqtt5::detail; - BOOST_CHECK_EQUAL(is_valid_utf8_topic(""), false); - BOOST_CHECK_EQUAL(is_valid_utf8_topic("topic"), true); -} - - -BOOST_AUTO_TEST_SUITE_END();