extract string helper

This commit is contained in:
John
2022-04-18 08:20:20 +02:00
parent ba5d264098
commit 88916b9a18
7 changed files with 829 additions and 741 deletions
+10 -500
View File
@@ -205,7 +205,7 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) {
argp_error(state, "duplicate mqtttopic");
return EINVAL;
} else {
MqttReplacer replacer;
StringReplacer replacer;
if (!replacer.parse(arg, true)) {
argp_error(state, "malformed mqtttopic");
return EINVAL;
@@ -378,441 +378,6 @@ bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap
return true;
}
/** the known topic field names. */
static const char* knownFieldNames[] = {
"circuit",
"name",
"field",
};
/** the number of known field names. */
static const size_t knownFieldCount = sizeof(knownFieldNames) / sizeof(char*);
std::pair<string, int> MqttReplacer::makeField(const string& name, bool isField) {
if (!isField) {
return {name, -1};
}
for (int idx = 0; idx < static_cast<int>(knownFieldCount); idx++) {
if (name == knownFieldNames[idx]) {
return {name, idx};
}
}
return {name, knownFieldCount};
}
void MqttReplacer::addPart(ostringstream& stack, int inField) {
string str = stack.str();
if (inField == 1 && str == "_") {
inField = 0; // single "%_" pattern to reduce to "_"
} else if (inField == 2) {
str = "%{" + str;
inField = 0;
}
if (inField == 0 && str.empty()) {
return;
}
stack.str("");
if (inField == 0 && !m_parts.empty() && m_parts[m_parts.size()-1].second < 0) {
// append constant to previous constant
m_parts[m_parts.size()-1].first += str;
return;
}
m_parts.push_back(makeField(str, inField > 0));
}
bool MqttReplacer::parse(const string& templateStr, bool onlyKnown, bool noKnownDuplicates, bool emptyIfMissing) {
m_parts.clear();
int inField = 0; // 1 after '%', 2 after '%{'
ostringstream stack;
for (auto ch : templateStr) {
bool empty = stack.tellp() <= 0;
if (ch == '%') {
if (inField == 1 && empty) { // %% for plain %
inField = 0;
stack << ch;
} else {
addPart(stack, inField);
inField = 1;
}
} else if (ch == '{' && inField == 1 && empty) {
inField = 2;
} else if (ch == '}' && inField == 2) {
addPart(stack, 1);
inField = 0;
} else {
if (inField > 0 && !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_')) {
// invalid field character
addPart(stack, inField);
inField = 0;
}
stack << ch;
}
}
addPart(stack, inField);
if (onlyKnown || noKnownDuplicates) {
int foundMask = 0;
int knownCount = knownFieldCount;
for (const auto &it : m_parts) {
if (it.second < 0) {
continue; // unknown field
}
if (onlyKnown && it.second >= knownCount) {
return false;
}
if (noKnownDuplicates && it.second < knownCount) {
int bit = 1 << it.second;
if (foundMask & bit) {
return false; // duplicate known field
}
foundMask |= bit;
}
}
}
m_emptyIfMissing = emptyIfMissing;
return true;
}
void MqttReplacer::normalize(string& str) {
transform(str.begin(), str.end(), str.begin(), [](unsigned char c){
return isalnum(c) ? c : '_';
});
}
const string MqttReplacer::str() const {
ostringstream ret;
for (const auto &it : m_parts) {
if (it.second >= 0) {
ret << '%';
}
ret << it.first;
}
return ret.str();
}
void MqttReplacer::ensureDefault() {
if (m_parts.empty()) {
m_parts.emplace_back(string(PACKAGE) + "/", -1);
} else if (m_parts.size() == 1 && m_parts[0].second < 0 && m_parts[0].first.find('/') == string::npos) {
m_parts[0] = {m_parts[0].first + "/", -1}; // ensure trailing slash
}
if (!has("circuit")) {
m_parts.emplace_back("circuit", 0); // index of circuit in knownFieldNames
m_parts.emplace_back("/", -1);
}
if (!has("name")) {
m_parts.emplace_back("name", 1); // index of name in knownFieldNames
}
}
bool MqttReplacer::empty() const {
return m_parts.empty();
}
bool MqttReplacer::has(const string& field) const {
for (const auto &it : m_parts) {
if (it.second >= 0 && it.first == field) {
return true;
}
}
return false;
}
string MqttReplacer::get(const map<string, string>& values, bool untilFirstEmpty, bool onlyAlphanum) const {
ostringstream ret;
for (const auto &it : m_parts) {
if (it.second < 0) {
ret << it.first;
continue;
}
const auto pos = values.find(it.first);
if (pos == values.cend()) {
if (untilFirstEmpty) {
break;
}
if (m_emptyIfMissing) {
return "";
}
} else if (pos->second.empty()) {
if (untilFirstEmpty) {
break;
}
if (m_emptyIfMissing) {
return "";
}
} else {
ret << pos->second;
}
}
if (!onlyAlphanum) {
return ret.str();
}
string str = ret.str();
normalize(str);
return str;
}
string MqttReplacer::get(const string& circuit, const string& name, const string& fieldName) const {
map <string, string> values;
values["circuit"] = circuit;
values["name"] = name;
if (!fieldName.empty()) {
values["field"] = fieldName;
}
return get(values, true);
}
string MqttReplacer::get(const Message* message, const string& fieldName) const {
map<string, string> values;
values["circuit"] = message->getCircuit();
values["name"] = message->getName();
if (!fieldName.empty()) {
values["field"] = fieldName;
}
return get(message->getCircuit(), message->getName(), fieldName);
}
bool MqttReplacer::isReducable(const map<string, string>& values) const {
for (const auto &it : m_parts) {
if (it.second < 0) {
continue;
}
const auto pos = values.find(it.first);
if (pos == values.cend()) {
return false;
}
}
return true;
}
void MqttReplacer::compress(const map<string, string>& values) {
bool lastConstant = false;
for (auto it = m_parts.begin(); it != m_parts.end(); ) {
bool isConstant = it->second < 0;
if (!isConstant) {
const auto pos = values.find(it->first);
if (pos != values.cend()) {
it->second = -1;
it->first = pos->second;
isConstant = true;
}
}
if (!lastConstant || !isConstant) {
lastConstant = isConstant;
++it;
continue;
}
(it-1)->first += it->first;
it = m_parts.erase(it);
}
}
bool MqttReplacer::reduce(const map<string, string>& values, string& result, bool onlyAlphanum) const {
ostringstream ret;
for (const auto &it : m_parts) {
if (it.second < 0) {
ret << it.first;
continue;
}
const auto pos = values.find(it.first);
if (pos == values.cend()) {
if (m_emptyIfMissing) {
result = "";
} else {
result = ret.str();
}
return false;
}
if (m_emptyIfMissing && pos->second.empty()) {
result = "";
return true;
}
ret << pos->second;
}
result = ret.str();
if (onlyAlphanum) {
normalize(result);
}
return true;
}
bool MqttReplacer::checkMatch() const {
bool lastField = false;
for (const auto& part : m_parts) {
bool field = part.second >= 0;
if (field && lastField) {
return false;
}
lastField = field;
}
return true;
}
ssize_t MqttReplacer::matchTopic(const string& topic, string* circuit, string* name, string* field) const {
size_t last = 0;
size_t count = m_parts.size();
size_t idx;
bool incomplete = false;
for (idx = 0; idx < count && !incomplete; idx++) {
const auto part = m_parts[idx];
if (part.second < 0) {
if (topic.substr(last, part.first.length()) != part.first) {
return static_cast<ssize_t>(idx);
}
last += part.first.length();
continue;
}
string value;
if (idx+1 < count) {
size_t pos = topic.find(m_parts[idx+1].first, last);
if (pos == string::npos) {
// next part not found, consume the rest and mark incomplete
value = topic.substr(last);
incomplete = true;
} else {
value = topic.substr(last, pos - last);
}
} else {
// last part is a field name
if (topic.find('/', last) != string::npos) {
// non-name in remainder found
return -static_cast<ssize_t>(idx)-1;
}
value = topic.substr(last);
}
last += value.length();
switch (part.second) {
case 0: *circuit = value; break;
case 1: *name = value; break;
case 2: *field = value; break;
default: // unknown field
break;
}
}
if (incomplete) {
return -static_cast<ssize_t>(idx)-1;
}
return static_cast<ssize_t>(idx);
}
static const string EMPTY = "";
const string& MqttReplacers::operator[](const string& key) const {
auto itc = m_constants.find(key);
if (itc == m_constants.end()) {
return EMPTY;
}
return itc->second;
}
bool MqttReplacers::uses(const string& field) const {
for (const auto &it : m_replacers) {
if (it.second.has(field)) {
return true;
}
}
return false;
}
MqttReplacer& MqttReplacers::get(const string& key) {
MqttReplacer& ret = m_replacers[key];
auto it = m_constants.find(key);
if (it != m_constants.end()) {
// constant with the same name found
if (ret.empty()) {
ret.parse(it->second); // convert to replacer
}
m_constants.erase(it);
}
return ret;
}
MqttReplacer MqttReplacers::get(const string& key) const {
const auto& it = m_replacers.find(key);
if (it != m_replacers.cend()) {
return it->second;
}
return MqttReplacer();
}
string MqttReplacers::get(const string& key, bool untilFirstEmpty, bool onlyAlphanum, const string& fallbackKey) const {
auto itc = m_constants.find(key);
if (itc != m_constants.end()) {
return itc->second;
}
auto itv = m_replacers.find(key);
if (itv != m_replacers.end()) {
return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum);
}
if (!fallbackKey.empty()) {
itc = m_constants.find(fallbackKey);
if (itc != m_constants.end()) {
return itc->second;
}
itv = m_replacers.find(fallbackKey);
if (itv != m_replacers.end()) {
return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum);
}
}
return "";
}
bool MqttReplacers::set(const string& key, const string& value, bool removeReplacer) {
m_constants[key] = value;
if (removeReplacer) {
m_replacers.erase(key);
}
if (key.find_first_of("-_") != string::npos) {
return false;
}
string upper = key;
transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (upper == key) {
return false;
}
string val = value;
MqttReplacer::normalize(val);
m_constants[upper] = val;
if (removeReplacer) {
m_replacers.erase(upper);
}
return true;
}
void MqttReplacers::set(const string& key, int value) {
std::ostringstream str;
str << static_cast<signed>(value);
m_constants[key] = str.str();
}
void MqttReplacers::reduce(bool compress) {
// iterate through variables and reduce as many to constants as possible
bool reduced = false;
do {
reduced = false;
for (auto it = m_replacers.begin(); it != m_replacers.end(); ) {
string str;
if (!it->second.isReducable(m_constants)
|| !it->second.reduce(m_constants, str)) {
if (compress) {
it->second.compress(m_constants);
}
++it;
continue;
}
bool restart = set(it->first, str, false);
it = m_replacers.erase(it);
reduced = true;
if (restart) {
string upper = it->first;
transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (m_replacers.erase(upper) > 0) {
break; // restart as iterator is now invalid
}
}
}
} while (reduced);
}
#if (LIBMOSQUITTO_MAJOR >= 1)
int on_keypassword(char *buf, int size, int rwflag, void *userdata) {
@@ -888,33 +453,6 @@ void on_message(
handler->notifyTopic(topic, data);
}
void MqttHandler::parseIntegration(const string& line) {
if (line.empty()) {
return;
}
size_t pos = line.find('=');
if (pos == string::npos || pos == 0) {
return;
}
bool emptyIfMissing = false;
string key;
if (line[pos-1] == '?') {
emptyIfMissing = true;
key = line.substr(0, pos-1);
} else {
key = line.substr(0, pos);
}
FileReader::trim(&key);
string value = line.substr(pos+1);
FileReader::trim(&value);
if (value.find('%') == string::npos) {
m_replacers.set(key, value); // constant value
} else {
// simple variable
m_replacers.get(key).parse(value, false, false, emptyIfMissing);
}
}
/**
* possible data type names.
*/
@@ -946,49 +484,21 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap*
m_mosquitto = nullptr;
bool hasIntegration = false;
if (g_integrationFile != nullptr) {
std::ifstream stream;
stream.open(g_integrationFile, std::ifstream::in);
if (!stream.is_open()) {
if (!m_replacers.parseFile(g_integrationFile)) {
logOtherError("mqtt", "unable to open integration file %s", g_integrationFile);
} else {
string line, last;
while (stream.peek() != EOF && getline(stream, line)) {
if (line.empty()) {
parseIntegration(last);
last = "";
continue;
}
if (line[0] == '#') {
// only ignore it to allow commented lines in the middle of e.g. payload
continue;
}
if (last.empty()) {
last = line;
} else if (line[0] == '\t' || line[0] == ' ') { // continuation
last += "\n" + line;
} else {
parseIntegration(last);
last = line;
}
}
stream.close();
parseIntegration(last);
hasIntegration = true;
if (g_integrationVars) {
vector<string> strs;
splitFields(g_integrationVars, &strs);
for (auto& str : strs) {
size_t pos = str.find('=');
if (pos == string::npos || pos == 0) {
continue;
}
m_replacers.set(str.substr(0, pos), str.substr(pos+1));
m_replacers.parseLine(str);
}
}
}
}
// determine topic and prefix
MqttReplacer& topic = m_replacers.get("topic");
StringReplacer& topic = m_replacers.get("topic");
if (g_topic) {
string str = g_topic;
bool noDefault = str[str.size()-1] == '#';
@@ -1009,7 +519,7 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap*
if (!topic.parse(str, true, true)) {
logOtherNotice("mqtt", "unknown or duplicate topic parts potentially prevent matching incoming topics");
topic.parse(str, true);
} else if (!topic.checkMatch()) {
} else if (!topic.checkMatchability()) {
logOtherNotice("mqtt", "missing separators between topic parts potentially prevent matching incoming topics");
}
}
@@ -1229,7 +739,7 @@ void MqttHandler::notifyTopic(const string& topic, const string& data) {
logOtherDebug("mqtt", "received topic %s with data %s", topic.c_str(), data.c_str());
string circuit, name, field;
ssize_t match = m_replacers.get("topic").matchTopic(matchTopic, &circuit, &name, &field);
ssize_t match = m_replacers.get("topic").match(matchTopic, &circuit, &name, &field);
if (match < 0 && !isList) {
logOtherError("mqtt", "received unmatchable topic %s", topic.c_str());
}
@@ -1485,7 +995,7 @@ void MqttHandler::run() {
continue;
}
MqttReplacers msgValues = m_replacers; // need a copy here as the contents are manipulated
StringReplacers msgValues = m_replacers; // need a copy here as the contents are manipulated
msgValues.set("circuit", message->getCircuit());
msgValues.set("name", message->getName());
msgValues.set("priority", static_cast<int>(message->getPollPriority()));
@@ -1540,7 +1050,7 @@ void MqttHandler::run() {
if (str.empty()) {
continue;
}
MqttReplacers values = msgValues; // need a copy here as the contents are manipulated
StringReplacers values = msgValues; // need a copy here as the contents are manipulated
values.set("index", static_cast<signed>(index));
values.set("field", fieldName);
values.set("fieldname", field->getName(-1));
@@ -1680,7 +1190,7 @@ void MqttHandler::run() {
}
}
void MqttHandler::publishDefinition(MqttReplacers values, const string& prefix, const string& topic,
void MqttHandler::publishDefinition(StringReplacers values, const string& prefix, const string& topic,
const string& circuit, const string& name, const string& fallbackPrefix) {
bool reduce = false;
if (!topic.empty()) {
@@ -1712,7 +1222,7 @@ void MqttHandler::publishDefinition(MqttReplacers values, const string& prefix,
publishTopic(defTopic, payload, retain);
}
void MqttHandler::publishDefinition(const MqttReplacers& values) {
void MqttHandler::publishDefinition(const StringReplacers& values) {
string defTopic = values.get("definition-topic", false);
if (defTopic.empty()) {
if (needsLog(lf_other, ll_debug)) {
+7 -227
View File
@@ -24,14 +24,14 @@
#include <string>
#include <list>
#include <vector>
#include <utility>
#include "ebusd/datahandler.h"
#include "ebusd/bushandler.h"
#include "lib/ebus/message.h"
#include "lib/ebus/stringhelper.h"
namespace ebusd {
/** @file ebusd/mqtthandler.h
/** \file ebusd/mqtthandler.h
* A data handler enabling MQTT support via mosquitto.
*/
@@ -56,223 +56,6 @@ const struct argp_child* mqtthandler_getargs();
bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages,
list<DataHandler*>* handlers);
/**
* Helper class for replacing a template string with real values.
*/
class MqttReplacer {
public:
/**
* Normalize the string to contain only alpha numeric characters plus underscore by replacing other characters with
* an underscore.
* @param str the string to normalize.
*/
static void normalize(string& str);
/**
* Get the template string.
* @return the template string (might already be partially reduced).
*/
const string str() const;
/**
* Parse the template string.
* @param templateStr the template string.
* @param onlyKnown true to allow only known field names from @a knownFieldNames.
* @param noKnownDuplicates true to now allow duplicates from @a knownFieldNames.
* @param emptyIfMissing true when the complete result is supposed to be empty when at least one referenced variable
* is empty or not defined.
* @return true on success, false on malformed template string.
*/
bool parse(const string& templateStr, bool onlyKnown = false, bool noKnownDuplicates = false,
bool emptyIfMissing = false);
/**
* Ensure the default topic parts are present (circuit and message).
*/
void ensureDefault();
/**
* Return whether this replacer is completely empty.
* @return true when empty.
*/
bool empty() const;
/**
* Return whether the specified field is used.
* @param field the field name to check.
* @return true when the specified field is used.
*/
bool has(const string& field) const;
/**
* Get the replaced template string.
* @param values the named values for replacement.
* @param untilFirstEmpty true to only return the prefix before the first empty field.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @return the replaced template string.
*/
string get(const map<string, string>& values, bool untilFirstEmpty = true, bool onlyAlphanum = false) const;
/**
* Get the replaced template string.
* @param circuit the circuit name for replacement.
* @param name the message name for replacement.
* @param fieldName the field name for replacement.
* @return the replaced template string.
*/
string get(const string& circuit, const string& name, const string& fieldName = "") const;
/**
* Get the replaced template string.
* @param message the Message from which to extract the values for replacement.
* @param fieldName the field name for replacement.
* @return the replaced template string.
*/
string get(const Message* message, const string& fieldName = "") const;
/**
* Check if the fields can be reduced to a constant value.
* @param values the named values for replacement.
* @return true if the result is final.
*/
bool isReducable(const map<string, string>& values) const;
/**
* Compress all subsequent constant values to a single constant value if possible.
* @param values the named values for replacement.
*/
void compress(const map<string, string>& values);
/**
* Reduce the fields to a constant value if possible.
* @param values the named values for replacement.
* @param result the string to store the result in.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @return true if the result is final.
*/
bool reduce(const map<string, string>& values, string& result, bool onlyAlphanum = false) const;
/**
* Check match-ability against topics.
* @return true on success, false on bad match-ability.
*/
bool checkMatch() const;
/**
* Match a topic string against the constant and variables parts.
* @param topic the topic string to match.
* @param circuit pointer to the string receiving the circuit name if present.
* @param name pointer to the string receiving the message name if present.
* @param field pointer to the string receiving the field name if present.
* @return the index of the last unmatched part, or the negative index minus one for extra non-matched non-field parts.
*/
ssize_t matchTopic(const string& topic, string* circuit, string* name, string* field) const;
private:
/**
* the list of parts the template is composed of.
* the string is either the plain string or the name of the field.
* the number is negative for plain strings, the index to @a knownFieldNames for a known field, or the size of
* @a knownFieldNames for an unknown field.
*/
vector<std::pair<string, int>> m_parts;
/** true when the complete result is supposed to be empty when at least one referenced variable
* is empty or not defined. */
bool m_emptyIfMissing;
/**
* Create a named field or constant.
* @param name the plain string or the name of the field.
* @param isField true when it is a field.
* @return the created pair.
*/
static std::pair<string, int> makeField(const string& name, bool isField);
/**
* Add a part to the list of parts.
* @param stack the parsing stack.
* @param inField 1 after '%', 2 after '%{', 0 otherwise.
*/
void addPart(ostringstream& stack, int inField);
};
/**
* A set of constants and @a MqttReplacer variables.
*/
class MqttReplacers {
public:
/**
* Get the value of the specified key from the constants only.
* @param key the key for which to get the value.
* @return the value string or empty.
*/
const string& operator[](const string& key) const;
/**
* Check if the specified field is used by one of the replacers.
* @param field the name of the field to check.
* @return true if the specified field is used by one of the replacers.
*/
bool uses(const string& field) const;
/**
* Get the variable value of the specified key.
* @param key the key for which to get the value.
* @return the value @a MqttReplacer.
*/
MqttReplacer& get(const string& key);
/**
* Get the variable value of the specified key.
* @param key the key for which to get the value.
* @return the value @a MqttReplacer.
*/
MqttReplacer get(const string& key) const;
/**
* Get the variable or constant value of the specified key.
* @param key the key for which to get the value.
* @param untilFirstEmpty true to only return the prefix before the first empty field.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @param fallbackKey optional fallback key to use when key value is undefined.
* @return the value string or empty.
*/
string get(const string& key, bool untilFirstEmpty, bool onlyAlphanum = false, const string& fallbackKey = "") const;
/**
* Set the constant value of the specified key and additionally normalized with uppercase key only (if the key does
* not contain an underscore).
* @param key the key to store.
* @param value the value string.
* @param removeReplacer true to remove a replacer with the same name.
* @return true when an upper case key was stored/updates as well.
*/
bool set(const string& key, const string& value, bool removeReplacer = true);
/**
* Set the constant value of the specified key.
* @param key the key to store.
* @param value the numeric value (converted to a string).
*/
void set(const string& key, int value);
/**
* Reduce as many variables to constants as possible.
* @param compress true to compress non-reducable replacers if possible.
*/
void reduce(bool compress = false);
private:
/** constant values from the integration file. */
map<string, string> m_constants;
/** variable values from the integration file. */
map<string, MqttReplacer> m_replacers;
};
/**
* The main class supporting MQTT data handling.
@@ -287,9 +70,6 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread {
*/
MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages);
private:
void parseIntegration(const string& line);
public:
/**
* Destructor.
@@ -333,14 +113,14 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread {
* @param name optional name to set before building the topic/payload, or empty.
* @param fallbackPrefix optional fallback prefix to use when topic/payload/retain with prefix above is not defined.
*/
void publishDefinition(MqttReplacers values, const string& prefix, const string& topic,
void publishDefinition(StringReplacers values, const string& prefix, const string& topic,
const string& circuit, const string& name, const string& fallbackPrefix);
/**
* Publish a definition topic as specified in the given values.
* @param values the values with the message specification.
*/
void publishDefinition(const MqttReplacers& values);
void publishDefinition(const StringReplacers& values);
/**
* Called regularly to handle MQTT traffic.
@@ -384,7 +164,7 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread {
MessageMap* m_messages;
/** the global topic replacer. */
MqttReplacer m_globalTopic;
StringReplacer m_globalTopic;
/** the topic to subscribe to. */
string m_subscribeTopic;
@@ -395,8 +175,8 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread {
/** whether to publish a separate topic for each message field. */
bool m_publishByField;
/** the @a MqttReplacers from the integration file. */
MqttReplacers m_replacers;
/** the @a StringReplacers from the integration file. */
StringReplacers m_replacers;
/** whether the @a m_replacers includes the definition_topic. */
bool m_hasDefinitionTopic;
+7 -12
View File
@@ -1,20 +1,15 @@
add_definitions(-Wconversion -Wno-unused-parameter)
set(libebus_a_SOURCES
result.cpp
result.h
symbol.cpp
symbol.h
filereader.h
filereader.cpp
datatype.cpp
datatype.h
data.cpp
data.h
device.cpp
device.h
result.h result.cpp
symbol.h symbol.cpp
filereader.h filereader.cpp
datatype.h datatype.cpp
data.h data.cpp
device.h device.cpp
message.cpp
message.h
stringhelper.h stringhelper.cpp
)
if(HAVE_CONTRIB)
+2 -1
View File
@@ -17,7 +17,8 @@ libebus_a_SOURCES = result.cpp \
device.cpp \
device.h \
message.cpp \
message.h
message.h \
stringhelper.h stringhelper.cpp
if CONTRIB
SUBDIRS = contrib
+524
View File
@@ -0,0 +1,524 @@
/*
* ebusd - daemon for communication with eBUS heating systems.
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
*
* 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 3 of the License, 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, see <http://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include "lib/ebus/stringhelper.h"
#include <algorithm>
namespace ebusd {
using std::ostringstream;
/** the known field names for identifying a message field. */
static const char* knownFieldNames[] = {
"circuit",
"name",
"field",
};
/** the number of known field names. */
static const size_t knownFieldCount = sizeof(knownFieldNames) / sizeof(char*);
std::pair<string, int> StringReplacer::makeField(const string& name, bool isField) {
if (!isField) {
return {name, -1};
}
for (int idx = 0; idx < static_cast<int>(knownFieldCount); idx++) {
if (name == knownFieldNames[idx]) {
return {name, idx};
}
}
return {name, knownFieldCount};
}
void StringReplacer::addPart(ostringstream& stack, int inField) {
string str = stack.str();
if (inField == 1 && str == "_") {
inField = 0; // single "%_" pattern to reduce to "_"
} else if (inField == 2) {
str = "%{" + str;
inField = 0;
}
if (inField == 0 && str.empty()) {
return;
}
stack.str("");
if (inField == 0 && !m_parts.empty() && m_parts[m_parts.size()-1].second < 0) {
// append constant to previous constant
m_parts[m_parts.size()-1].first += str;
return;
}
m_parts.push_back(makeField(str, inField > 0));
}
bool StringReplacer::parse(const string& templateStr, bool onlyKnown, bool noKnownDuplicates, bool emptyIfMissing) {
m_parts.clear();
int inField = 0; // 1 after '%', 2 after '%{'
ostringstream stack;
for (auto ch : templateStr) {
bool empty = stack.tellp() <= 0;
if (ch == '%') {
if (inField == 1 && empty) { // %% for plain %
inField = 0;
stack << ch;
} else {
addPart(stack, inField);
inField = 1;
}
} else if (ch == '{' && inField == 1 && empty) {
inField = 2;
} else if (ch == '}' && inField == 2) {
addPart(stack, 1);
inField = 0;
} else {
if (inField > 0 && !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_')) {
// invalid field character
addPart(stack, inField);
inField = 0;
}
stack << ch;
}
}
addPart(stack, inField);
if (onlyKnown || noKnownDuplicates) {
int foundMask = 0;
int knownCount = knownFieldCount;
for (const auto &it : m_parts) {
if (it.second < 0) {
continue; // unknown field
}
if (onlyKnown && it.second >= knownCount) {
return false;
}
if (noKnownDuplicates && it.second < knownCount) {
int bit = 1 << it.second;
if (foundMask & bit) {
return false; // duplicate known field
}
foundMask |= bit;
}
}
}
m_emptyIfMissing = emptyIfMissing;
return true;
}
void StringReplacer::normalize(string& str) {
transform(str.begin(), str.end(), str.begin(), [](unsigned char c){
return isalnum(c) ? c : '_';
});
}
const string StringReplacer::str() const {
ostringstream ret;
for (const auto &it : m_parts) {
if (it.second >= 0) {
ret << '%';
}
ret << it.first;
}
return ret.str();
}
void StringReplacer::ensureDefault(const string& separator) {
if (m_parts.empty()) {
m_parts.emplace_back(string(PACKAGE) + separator, -1);
} else if (m_parts.size() == 1 && m_parts[0].second < 0 && m_parts[0].first.find('/') == string::npos) {
m_parts[0] = {m_parts[0].first + separator, -1}; // ensure trailing slash
}
if (!has("circuit")) {
m_parts.emplace_back("circuit", 0); // index of circuit in knownFieldNames
m_parts.emplace_back(separator, -1);
}
if (!has("name")) {
m_parts.emplace_back("name", 1); // index of name in knownFieldNames
}
}
bool StringReplacer::empty() const {
return m_parts.empty();
}
bool StringReplacer::has(const string& field) const {
for (const auto &it : m_parts) {
if (it.second >= 0 && it.first == field) {
return true;
}
}
return false;
}
string StringReplacer::get(const map<string, string>& values, bool untilFirstEmpty, bool onlyAlphanum) const {
ostringstream ret;
for (const auto &it : m_parts) {
if (it.second < 0) {
ret << it.first;
continue;
}
const auto pos = values.find(it.first);
if (pos == values.cend()) {
if (untilFirstEmpty) {
break;
}
if (m_emptyIfMissing) {
return "";
}
} else if (pos->second.empty()) {
if (untilFirstEmpty) {
break;
}
if (m_emptyIfMissing) {
return "";
}
} else {
ret << pos->second;
}
}
if (!onlyAlphanum) {
return ret.str();
}
string str = ret.str();
normalize(str);
return str;
}
string StringReplacer::get(const string& circuit, const string& name, const string& fieldName) const {
map <string, string> values;
values["circuit"] = circuit;
values["name"] = name;
if (!fieldName.empty()) {
values["field"] = fieldName;
}
return get(values, true);
}
string StringReplacer::get(const Message* message, const string& fieldName) const {
map<string, string> values;
values["circuit"] = message->getCircuit();
values["name"] = message->getName();
if (!fieldName.empty()) {
values["field"] = fieldName;
}
return get(message->getCircuit(), message->getName(), fieldName);
}
bool StringReplacer::isReducable(const map<string, string>& values) const {
for (const auto &it : m_parts) {
if (it.second < 0) {
continue;
}
const auto pos = values.find(it.first);
if (pos == values.cend()) {
return false;
}
}
return true;
}
void StringReplacer::compress(const map<string, string>& values) {
bool lastConstant = false;
for (auto it = m_parts.begin(); it != m_parts.end(); ) {
bool isConstant = it->second < 0;
if (!isConstant) {
const auto pos = values.find(it->first);
if (pos != values.cend()) {
it->second = -1;
it->first = pos->second;
isConstant = true;
}
}
if (!lastConstant || !isConstant) {
lastConstant = isConstant;
++it;
continue;
}
(it-1)->first += it->first;
it = m_parts.erase(it);
}
}
bool StringReplacer::reduce(const map<string, string>& values, string& result, bool onlyAlphanum) const {
ostringstream ret;
for (const auto &it : m_parts) {
if (it.second < 0) {
ret << it.first;
continue;
}
const auto pos = values.find(it.first);
if (pos == values.cend()) {
if (m_emptyIfMissing) {
result = "";
} else {
result = ret.str();
}
return false;
}
if (m_emptyIfMissing && pos->second.empty()) {
result = "";
return true;
}
ret << pos->second;
}
result = ret.str();
if (onlyAlphanum) {
normalize(result);
}
return true;
}
bool StringReplacer::checkMatchability() const {
bool lastField = false;
for (const auto& part : m_parts) {
bool field = part.second >= 0;
if (field && lastField) {
return false;
}
lastField = field;
}
return true;
}
ssize_t StringReplacer::match(const string& str, string* circuit, string* name, string* field, const string& separator) const {
size_t last = 0;
size_t count = m_parts.size();
size_t idx;
bool incomplete = false;
for (idx = 0; idx < count && !incomplete; idx++) {
const auto part = m_parts[idx];
if (part.second < 0) {
if (str.substr(last, part.first.length()) != part.first) {
return static_cast<ssize_t>(idx);
}
last += part.first.length();
continue;
}
string value;
if (idx+1 < count) {
size_t pos = str.find(m_parts[idx+1].first, last);
if (pos == string::npos) {
// next part not found, consume the rest and mark incomplete
value = str.substr(last);
incomplete = true;
} else {
value = str.substr(last, pos - last);
}
} else {
// last part is a field name
if (str.find(separator, last) != string::npos) {
// non-name in remainder found
return -static_cast<ssize_t>(idx)-1;
}
value = str.substr(last);
}
last += value.length();
switch (part.second) {
case 0: *circuit = value; break;
case 1: *name = value; break;
case 2: *field = value; break;
default: // unknown field
break;
}
}
if (incomplete) {
return -static_cast<ssize_t>(idx)-1;
}
return static_cast<ssize_t>(idx);
}
static const string EMPTY = "";
const string& StringReplacers::operator[](const string& key) const {
auto itc = m_constants.find(key);
if (itc == m_constants.end()) {
return EMPTY;
}
return itc->second;
}
void StringReplacers::parseLine(const string& line) {
if (line.empty()) {
return;
}
size_t pos = line.find('=');
if (pos == string::npos || pos == 0) {
return;
}
bool emptyIfMissing = false;
string key;
if (line[pos-1] == '?') {
emptyIfMissing = true;
key = line.substr(0, pos-1);
} else {
key = line.substr(0, pos);
}
FileReader::trim(&key);
string value = line.substr(pos+1);
FileReader::trim(&value);
if (value.find('%') == string::npos) {
set(key, value); // constant value
} else {
// simple variable
get(key).parse(value, false, false, emptyIfMissing);
}
}
bool StringReplacers::parseFile(const char* filename) {
std::ifstream stream;
stream.open(filename, std::ifstream::in);
if (!stream.is_open()) {
return false;
}
string line, last;
while (stream.peek() != EOF && getline(stream, line)) {
if (line.empty()) {
parseLine(last);
last = "";
continue;
}
if (line[0] == '#') {
// only ignore it to allow commented lines in the middle of e.g. payload
continue;
}
if (last.empty()) {
last = line;
} else if (line[0] == '\t' || line[0] == ' ') { // continuation
last += "\n" + line;
} else {
parseLine(last);
last = line;
}
}
stream.close();
parseLine(last);
return true;
}
bool StringReplacers::uses(const string& field) const {
for (const auto &it : m_replacers) {
if (it.second.has(field)) {
return true;
}
}
return false;
}
StringReplacer& StringReplacers::get(const string& key) {
StringReplacer& ret = m_replacers[key];
auto it = m_constants.find(key);
if (it != m_constants.end()) {
// constant with the same name found
if (ret.empty()) {
ret.parse(it->second); // convert to replacer
}
m_constants.erase(it);
}
return ret;
}
StringReplacer StringReplacers::get(const string& key) const {
const auto& it = m_replacers.find(key);
if (it != m_replacers.cend()) {
return it->second;
}
return StringReplacer();
}
string StringReplacers::get(const string& key, bool untilFirstEmpty, bool onlyAlphanum, const string& fallbackKey) const {
auto itc = m_constants.find(key);
if (itc != m_constants.end()) {
return itc->second;
}
auto itv = m_replacers.find(key);
if (itv != m_replacers.end()) {
return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum);
}
if (!fallbackKey.empty()) {
itc = m_constants.find(fallbackKey);
if (itc != m_constants.end()) {
return itc->second;
}
itv = m_replacers.find(fallbackKey);
if (itv != m_replacers.end()) {
return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum);
}
}
return "";
}
bool StringReplacers::set(const string& key, const string& value, bool removeReplacer) {
m_constants[key] = value;
if (removeReplacer) {
m_replacers.erase(key);
}
if (key.find_first_of("-_") != string::npos) {
return false;
}
string upper = key;
transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (upper == key) {
return false;
}
string val = value;
StringReplacer::normalize(val);
m_constants[upper] = val;
if (removeReplacer) {
m_replacers.erase(upper);
}
return true;
}
void StringReplacers::set(const string& key, int value) {
std::ostringstream str;
str << static_cast<signed>(value);
m_constants[key] = str.str();
}
void StringReplacers::reduce(bool compress) {
// iterate through variables and reduce as many to constants as possible
bool reduced = false;
do {
reduced = false;
for (auto it = m_replacers.begin(); it != m_replacers.end(); ) {
string str;
if (!it->second.isReducable(m_constants)
|| !it->second.reduce(m_constants, str)) {
if (compress) {
it->second.compress(m_constants);
}
++it;
continue;
}
bool restart = set(it->first, str, false);
it = m_replacers.erase(it);
reduced = true;
if (restart) {
string upper = it->first;
transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (m_replacers.erase(upper) > 0) {
break; // restart as iterator is now invalid
}
}
}
} while (reduced);
}
} // namespace ebusd
+277
View File
@@ -0,0 +1,277 @@
/*
* ebusd - daemon for communication with eBUS heating systems.
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
*
* 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 3 of the License, 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIB_EBUS_STRINGHELPER_H_
#define LIB_EBUS_STRINGHELPER_H_
#include <unistd.h>
#include <cstdint>
#include <map>
#include <string>
#include <sstream>
#include <vector>
#include "lib/ebus/message.h"
namespace ebusd {
/** @file lib/ebus/stringhelper.h
* Helper classes for string replacement.
*/
using std::map;
using std::ostringstream;
using std::string;
using std::vector;
/**
* Helper class for replacing a template string with real values.
*/
class StringReplacer {
public:
/**
* Normalize the string to contain only alpha numeric characters plus underscore by replacing other characters with
* an underscore.
* @param str the string to normalize.
*/
static void normalize(string& str);
/**
* Get the template string.
* @return the template string (might already be partially reduced).
*/
const string str() const;
/**
* Parse the template string.
* @param templateStr the template string.
* @param onlyKnown true to allow only known field names from @a knownFieldNames.
* @param noKnownDuplicates true to now allow duplicates from @a knownFieldNames.
* @param emptyIfMissing true when the complete result is supposed to be empty when at least one referenced variable
* is empty or not defined.
* @return true on success, false on malformed template string.
*/
bool parse(const string& templateStr, bool onlyKnown = false, bool noKnownDuplicates = false,
bool emptyIfMissing = false);
/**
* Ensure the default parts are present (package prefix if empty, circuit and message name).
* @param separator the separator between prefix, circuit, and message name (default slash).
*/
void ensureDefault(const string& separator = "/");
/**
* Return whether this replacer is completely empty.
* @return true when empty.
*/
bool empty() const;
/**
* Return whether the specified field is used.
* @param field the field name to check.
* @return true when the specified field is used.
*/
bool has(const string& field) const;
/**
* Get the replaced template string.
* @param values the named values for replacement.
* @param untilFirstEmpty true to only return the prefix before the first empty field.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @return the replaced template string.
*/
string get(const map<string, string>& values, bool untilFirstEmpty = true, bool onlyAlphanum = false) const;
/**
* Get the replaced template string.
* @param circuit the circuit name for replacement.
* @param name the message name for replacement.
* @param fieldName the field name for replacement.
* @return the replaced template string.
*/
string get(const string& circuit, const string& name, const string& fieldName = "") const;
/**
* Get the replaced template string.
* @param message the Message from which to extract the values for replacement.
* @param fieldName the field name for replacement.
* @return the replaced template string.
*/
string get(const Message* message, const string& fieldName = "") const;
/**
* Check if the fields can be reduced to a constant value.
* @param values the named values for replacement.
* @return true if the result is final.
*/
bool isReducable(const map<string, string>& values) const;
/**
* Compress all subsequent constant values to a single constant value if possible.
* @param values the named values for replacement.
*/
void compress(const map<string, string>& values);
/**
* Reduce the fields to a constant value if possible.
* @param values the named values for replacement.
* @param result the string to store the result in.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @return true if the result is final.
*/
bool reduce(const map<string, string>& values, string& result, bool onlyAlphanum = false) const;
/**
* Check match-ability against a string.
* @return true on success, false on bad match-ability.
*/
bool checkMatchability() const;
/**
* Match a string against the constant and variables parts.
* @param str the string to match.
* @param circuit pointer to the string receiving the circuit name if present.
* @param name pointer to the string receiving the message name if present.
* @param field pointer to the string receiving the field name if present.
* @param separator the separator expected in the extra non-matched non-field parts (default slash).
* @return the index of the last unmatched part, or the negative index minus one for extra non-matched non-field parts.
*/
ssize_t match(const string& str, string* circuit, string* name, string* field, const string& separator = "/") const;
private:
/**
* the list of parts the template is composed of.
* the string is either the plain string or the name of the field.
* the number is negative for plain strings, the index to @a knownFieldNames for a known field, or the size of
* @a knownFieldNames for an unknown field.
*/
vector<std::pair<string, int>> m_parts;
/** true when the complete result is supposed to be empty when at least one referenced variable
* is empty or not defined. */
bool m_emptyIfMissing;
/**
* Create a named field or constant.
* @param name the plain string or the name of the field.
* @param isField true when it is a field.
* @return the created pair.
*/
static std::pair<string, int> makeField(const string& name, bool isField);
/**
* Add a part to the list of parts.
* @param stack the parsing stack.
* @param inField 1 after '%', 2 after '%{', 0 otherwise.
*/
void addPart(ostringstream& stack, int inField);
};
/**
* A set of constants and @a StringReplacer variables.
*/
class StringReplacers {
public:
/**
* Get the value of the specified key from the constants only.
* @param key the key for which to get the value.
* @return the value string or empty.
*/
const string& operator[](const string& key) const;
/**
* Parse a continuation-normalized line.
* @param line the line to parse.
*/
void parseLine(const string& line);
/**
* Parse a file with constants and variables.
* @param filename the name (and path) of the file to parse.
* @return true on success, false if the file is not readable.
*/
bool parseFile(const char* filename);
/**
* Check if the specified field is used by one of the replacers.
* @param field the name of the field to check.
* @return true if the specified field is used by one of the replacers.
*/
bool uses(const string& field) const;
/**
* Get the variable value of the specified key.
* @param key the key for which to get the value.
* @return the value @a StringReplacer.
*/
StringReplacer& get(const string& key);
/**
* Get the variable value of the specified key.
* @param key the key for which to get the value.
* @return the value @a StringReplacer.
*/
StringReplacer get(const string& key) const;
/**
* Get the variable or constant value of the specified key.
* @param key the key for which to get the value.
* @param untilFirstEmpty true to only return the prefix before the first empty field.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @param fallbackKey optional fallback key to use when key value is undefined.
* @return the value string or empty.
*/
string get(const string& key, bool untilFirstEmpty, bool onlyAlphanum = false, const string& fallbackKey = "") const;
/**
* Set the constant value of the specified key and additionally normalized with uppercase key only (if the key does
* not contain an underscore).
* @param key the key to store.
* @param value the value string.
* @param removeReplacer true to remove a replacer with the same name.
* @return true when an upper case key was stored/updates as well.
*/
bool set(const string& key, const string& value, bool removeReplacer = true);
/**
* Set the constant value of the specified key.
* @param key the key to store.
* @param value the numeric value (converted to a string).
*/
void set(const string& key, int value);
/**
* Reduce as many variables to constants as possible.
* @param compress true to compress non-reducable replacers if possible.
*/
void reduce(bool compress = false);
private:
/** constant values from the integration file. */
map<string, string> m_constants;
/** variable values from the integration file. */
map<string, StringReplacer> m_replacers;
};
} // namespace ebusd
#endif // LIB_EBUS_STRINGHELPER_H_
+2 -1
View File
@@ -8,6 +8,7 @@ set(libutils_a_SOURCES
queue.h
notify.h
rotatefile.h rotatefile.cpp
httpclient.h httpclient.cpp)
httpclient.h httpclient.cpp
)
add_library(utils ${libutils_a_SOURCES})