Files
ebusd/src/lib/ebus/stringhelper.cpp
T
2024-10-02 07:02:31 +02:00

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