mqttutil.cpp 6.35 KB
/* ****************************************************************************
 * 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 <boost/algorithm/string/split.hpp>
#include <boost/regex.hpp>

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<std::string> existingTopicList;
    std::vector<std::string> 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