/* **************************************************************************** * Copyright 2019 Open Systems Development BV * * * * Permission is hereby granted, free of charge, to any person obtaining a * * copy of this software and associated documentation files (the "Software"), * * to deal in the Software without restriction, including without limitation * * the rights to use, copy, modify, merge, publish, distribute, sublicense, * * and/or sell copies of the Software, and to permit persons to whom the * * Software is furnished to do so, subject to the following conditions: * * * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * * DEALINGS IN THE SOFTWARE. * * ***************************************************************************/ #include "mqttutil.h" // boost #include #include namespace osdev { namespace components { namespace mqtt { bool isValidTopic( const std::string &topic ) { if (topic.empty() || topic.size() > 65535) { return false; } auto posHash = topic.find('#'); if (std::string::npos != posHash && posHash < topic.size() - 1) { return false; } std::size_t pos = 0; while ((pos = topic.find('+', pos)) != std::string::npos) { if (pos > 0 && topic[pos - 1] != '/') { return false; } if (pos < topic.size() - 1 && topic[pos + 1] != '/') { return false; } ++pos; } return true; } bool hasWildcard( const std::string &topic ) { return ( topic.size() > 0 && (topic.find( '+' ) != std::string::npos || topic.size() - 1 == '#' ) ); } bool testForOverlap( const std::string &existingTopic, const std::string &newTopic ) { if (existingTopic == newTopic) { return true; } // A topic that starts with a $ can never overlap with topic that does not start with a $. // Topics that start!!! with a $ are so called system reserved topics and not meant for users to publish to. if ((existingTopic[0] == '$' && newTopic[0] != '$') || (existingTopic[0] != '$' && newTopic[0] == '$')) { return false; } std::vector existingTopicList; std::vector newTopicList; boost::algorithm::split(existingTopicList, existingTopic, [](char ch) { return ch == '/'; }); boost::algorithm::split(newTopicList, newTopic, [](char ch) { return ch == '/'; }); auto szExistingTopicList = existingTopicList.size(); auto szNewTopicList = newTopicList.size(); // Walk through the topic term by term until it is proved for certain that the topics either have no overlap // or do have overlap. for (std::size_t idx = 0; idx < std::minmax(szExistingTopicList, szNewTopicList).first; ++idx) { if ("#" == existingTopicList[idx] || "#" == newTopicList[idx]) { return true; // Match all wildcard found so there is always a possible overlap. } else if ("+" == existingTopicList[idx] || "+" == newTopicList[idx]) { // these terms can match each other based on wildcard. // Topics are still in the race for overlap, proceed to the next term } else if (existingTopicList[idx] != newTopicList[idx]) { return false; // no match possible because terms are not wildcards and differ from each other. } else { // term is an exact match. The topics are still in the race for overlap, proceed to the next term. } } // Still no certain prove of overlap. If the number of terms for both topics are the same at this point then they overlap. // If they are not the same than a match all wildcard on the longer topic can still make them match because of a special rule // that states that a topic with a match all wildcard also matches a topic without that term. // Example: topic /level/1 matches wildcard topic /level/1/# // A match single wildcard at the end or at the term before a match all wildcard must also be taken into account. // Example: /level/+ can overlap with /level/1/# // /level/1 can overlap with /level/+/# if (szNewTopicList != szExistingTopicList) { if (szNewTopicList == szExistingTopicList + 1 && "#" == newTopicList[szNewTopicList - 1]) { return (newTopicList[szNewTopicList - 2] == existingTopicList[szExistingTopicList - 1] || "+" == existingTopicList[szExistingTopicList - 1] || "+" == newTopicList[szNewTopicList - 2]); } if (szExistingTopicList == szNewTopicList + 1 && "#" == existingTopicList[szExistingTopicList - 1]) { return (existingTopicList[szExistingTopicList - 2] == newTopicList[szNewTopicList - 1] || "+" == newTopicList[szNewTopicList - 1] || "+" == existingTopicList[szExistingTopicList - 2]); } return false; } return true; } std::string convertTopicToRegex(const std::string& topic) { // escape the regex characters in msgTemplate static const boost::regex esc("[.^$|()\\[\\]{}*+?\\\\]"); static const std::string rep("\\\\$&"); // $&, refers to whatever is matched by the whole expression std::string out = boost::regex_replace(topic, esc, rep); static const boost::regex multiTopicRegex("#$"); static const boost::regex singleTopicRegex(R"((/|^|(?<=/))\\\+(/|$))"); out = boost::regex_replace(out, multiTopicRegex, ".*"); return boost::regex_replace(out, singleTopicRegex, "$1[^/]*?$2"); } } // End namespace mqtt } // End namespace components } // End namespace osdev