/* * ebusd - daemon for communication with eBUS heating systems. * Copyright (C) 2022-2024 John Baier * * 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 . */ #ifdef HAVE_CONFIG_H # include #endif #include "lib/ebus/stringhelper.h" #include #include 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 StringReplacer::makeField(const string& name, bool isField) { if (!isField) { return {name, -1}; } for (int idx = 0; idx < static_cast(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& 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 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 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& 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& 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& 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(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(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(idx)-1; } return static_cast(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(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 StringReplacers::keys() const { vector 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