mqttutil.cpp 5.47 KB
/* Copyright (C) 2019
 *
 * This file is part of the osdev components suite
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
 */
#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