mqtt_topic_util.hpp 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /*
  2. * Copyright (c) 2017-2023 zhllxt
  3. *
  4. * author : zhllxt
  5. * email : 37792738@qq.com
  6. *
  7. * Distributed under the Boost Software License, Version 1.0. (See accompanying
  8. * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
  9. *
  10. * refrenced from : mqtt_cpp/include/mqtt/topic_filter_tokenizer.hpp
  11. */
  12. #ifndef __ASIO2_MQTT_TOPIC_UTIL_HPP__
  13. #define __ASIO2_MQTT_TOPIC_UTIL_HPP__
  14. #if defined(_MSC_VER) && (_MSC_VER >= 1200)
  15. #pragma once
  16. #endif // defined(_MSC_VER) && (_MSC_VER >= 1200)
  17. #include <cstdint>
  18. #include <string>
  19. #include <string_view>
  20. #include <type_traits>
  21. #include <asio2/base/detail/util.hpp>
  22. namespace asio2::mqtt
  23. {
  24. template<typename = void>
  25. inline bool is_topic_name_valid(std::string_view str)
  26. {
  27. // All Topic Names and Topic Filters MUST be at least one character long [MQTT-4.7.3-1]
  28. return ((!str.empty()) && (str.find_first_of("+#") == std::string_view::npos));
  29. }
  30. /**
  31. * @return std::pair<shared_name, topic_filter>
  32. */
  33. template<typename = void>
  34. inline std::pair<std::string_view, std::string_view> parse_topic_filter(std::string_view topic_filter)
  35. {
  36. using namespace std::literals;
  37. constexpr std::string_view shared_prefix = "$share/"sv;
  38. if (topic_filter.empty())
  39. return { ""sv, ""sv };
  40. if (topic_filter.substr(0, shared_prefix.size()) != shared_prefix)
  41. return { ""sv, std::move(topic_filter) };
  42. // Remove $share/
  43. topic_filter.remove_prefix(shared_prefix.size());
  44. // This is the '/' seperating the subscription group from the actual topic_filter.
  45. std::string_view::size_type idx = topic_filter.find('/');
  46. if (idx == std::string_view::npos)
  47. return { ""sv, ""sv };
  48. // We return the share_name and the topic_filter as buffers that point to the same
  49. // storage. So we grab the substr for "share", and then remove it from topic_filter.
  50. std::string_view share_name = topic_filter.substr(0, idx);
  51. topic_filter.remove_prefix(idx + 1);
  52. if (share_name.empty() || topic_filter.empty())
  53. return { ""sv, ""sv };
  54. return { std::move(share_name), std::move(topic_filter) };
  55. }
  56. static constexpr char topic_filter_separator = '/';
  57. template<typename Iterator>
  58. inline Iterator topic_filter_tokenizer_next(Iterator first, Iterator last)
  59. {
  60. return std::find(first, last, topic_filter_separator);
  61. }
  62. template<typename Iterator, typename Function>
  63. inline std::size_t topic_filter_tokenizer(Iterator&& first, Iterator&& last, Function&& callback)
  64. {
  65. if (first >= last)
  66. {
  67. ASIO2_ASSERT(false);
  68. return static_cast<std::size_t>(0);
  69. }
  70. std::size_t count = static_cast<std::size_t>(1);
  71. auto iter = topic_filter_tokenizer_next(first, last);
  72. while (callback(asio2::detail::to_string_view(first, iter)) && iter != last)
  73. {
  74. first = std::next(iter);
  75. if (first >= last)
  76. break;
  77. iter = topic_filter_tokenizer_next(first, last);
  78. ++count;
  79. }
  80. return count;
  81. }
  82. template<typename Function>
  83. inline std::size_t topic_filter_tokenizer(std::string_view str, Function&& callback)
  84. {
  85. return topic_filter_tokenizer(std::begin(str), std::end(str), std::forward<Function>(callback));
  86. }
  87. // TODO: Technically this function is simply wrong, since it's treating the
  88. // topic pattern as if it were an ASCII sequence.
  89. // To make this function correct per the standard, it would be necessary
  90. // to conduct the search for the wildcard characters using a proper
  91. // UTF-8 API to avoid problems of interpreting parts of multi-byte characters
  92. // as if they were individual ASCII characters
  93. constexpr inline bool validate_topic_filter(std::string_view topic_filter)
  94. {
  95. // Confirm the topic pattern is valid before registering it.
  96. // Use rules from http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106
  97. // All Topic Names and Topic Filters MUST be at least one character long
  98. // Topic Names and Topic Filters are UTF-8 Encoded Strings; they MUST NOT encode to more than 65,535 bytes
  99. if (topic_filter.empty() || (topic_filter.size() > (std::numeric_limits<std::uint16_t>::max)()))
  100. {
  101. return false;
  102. }
  103. for (std::string_view::size_type idx = topic_filter.find_first_of(std::string_view("\0+#", 3));
  104. std::string_view::npos != idx;
  105. idx = topic_filter.find_first_of(std::string_view("\0+#", 3), idx + 1))
  106. {
  107. ASIO2_ASSERT(
  108. ('\0' == topic_filter[idx]) ||
  109. ('+' == topic_filter[idx]) ||
  110. ('#' == topic_filter[idx])
  111. );
  112. if ('\0' == topic_filter[idx])
  113. {
  114. // Topic Names and Topic Filters MUST NOT include the null character (Unicode U+0000)
  115. return false;
  116. }
  117. else if ('+' == topic_filter[idx])
  118. {
  119. /*
  120. * Either must be the first character,
  121. * or be preceeded by a topic seperator.
  122. */
  123. if ((0 != idx) && ('/' != topic_filter[idx - 1]))
  124. {
  125. return false;
  126. }
  127. /*
  128. * Either must be the last character,
  129. * or be followed by a topic seperator.
  130. */
  131. if ((topic_filter.size() - 1 != idx) && ('/' != topic_filter[idx + 1]))
  132. {
  133. return false;
  134. }
  135. }
  136. // multilevel wildcard
  137. else if ('#' == topic_filter[idx])
  138. {
  139. /*
  140. * Must be absolute last character.
  141. * Must only be one multi level wild card.
  142. */
  143. if (idx != topic_filter.size() - 1)
  144. {
  145. return false;
  146. }
  147. /*
  148. * If not the first character, then the
  149. * immediately preceeding character must
  150. * be a topic level separator.
  151. */
  152. if ((0 != idx) && ('/' != topic_filter[idx - 1]))
  153. {
  154. return false;
  155. }
  156. }
  157. else
  158. {
  159. return false;
  160. }
  161. }
  162. return true;
  163. }
  164. }
  165. #endif // !__ASIO2_MQTT_TOPIC_UTIL_HPP__