546 lines
14 KiB
C++
546 lines
14 KiB
C++
/*
|
|
* ebusd - daemon for communication with eBUS heating systems.
|
|
* Copyright (C) 2022-2024 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>
|
|
#include <stack>
|
|
|
|
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);
|
|
}
|
|
if (!has("circuit")) {
|
|
if (m_parts.back().second >= 0) {
|
|
// add separator between two variables
|
|
m_parts.emplace_back(separator, -1);
|
|
} else if (m_parts.back().first.back() != '/') {
|
|
m_parts[m_parts.size() - 1] = {m_parts.back().first + separator, -1}; // ensure trailing slash
|
|
}
|
|
m_parts.emplace_back("circuit", 0); // index of circuit in knownFieldNames
|
|
}
|
|
if (!has("name")) {
|
|
if (m_parts.back().second >= 0) {
|
|
// add separator between two variables
|
|
m_parts.emplace_back(separator, -1);
|
|
} else if (m_parts.back().first.back() != '/') {
|
|
m_parts[m_parts.size() - 1] = {m_parts.back().first + separator, -1}; // ensure trailing slash
|
|
}
|
|
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 = line[pos-1] == '?';
|
|
bool append = !emptyIfMissing && line[pos-1] == '+';
|
|
string key = line.substr(0, (emptyIfMissing || append) ? pos-1 : pos);
|
|
FileReader::trim(&key);
|
|
string value = line.substr(pos+1);
|
|
if (append) {
|
|
value = get(key).str() + value;
|
|
}
|
|
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) {
|
|
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;
|
|
}
|
|
string key = it->first;
|
|
it = m_replacers.erase(it);
|
|
bool restart = set(key, str, false);
|
|
reduced = true;
|
|
if (restart) {
|
|
transform(key.begin(), key.end(), key.begin(), ::toupper);
|
|
if (m_replacers.erase(key) > 0) {
|
|
break; // restart as iterator is now invalid
|
|
}
|
|
}
|
|
}
|
|
} while (reduced);
|
|
}
|
|
|
|
vector<string> StringReplacers::keys() const {
|
|
vector<string> ret;
|
|
for (auto& it : m_constants) {
|
|
ret.push_back(it.first);
|
|
}
|
|
for (auto& it : m_replacers) {
|
|
ret.push_back(it.first);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
} // namespace ebusd
|