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"); argp_error(state, "duplicate mqtttopic");
return EINVAL; return EINVAL;
} else { } else {
MqttReplacer replacer; StringReplacer replacer;
if (!replacer.parse(arg, true)) { if (!replacer.parse(arg, true)) {
argp_error(state, "malformed mqtttopic"); argp_error(state, "malformed mqtttopic");
return EINVAL; return EINVAL;
@@ -378,441 +378,6 @@ bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap
return true; 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) #if (LIBMOSQUITTO_MAJOR >= 1)
int on_keypassword(char *buf, int size, int rwflag, void *userdata) { int on_keypassword(char *buf, int size, int rwflag, void *userdata) {
@@ -888,33 +453,6 @@ void on_message(
handler->notifyTopic(topic, data); 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. * possible data type names.
*/ */
@@ -946,49 +484,21 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap*
m_mosquitto = nullptr; m_mosquitto = nullptr;
bool hasIntegration = false; bool hasIntegration = false;
if (g_integrationFile != nullptr) { if (g_integrationFile != nullptr) {
std::ifstream stream; if (!m_replacers.parseFile(g_integrationFile)) {
stream.open(g_integrationFile, std::ifstream::in);
if (!stream.is_open()) {
logOtherError("mqtt", "unable to open integration file %s", g_integrationFile); logOtherError("mqtt", "unable to open integration file %s", g_integrationFile);
} else { } 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; hasIntegration = true;
if (g_integrationVars) { if (g_integrationVars) {
vector<string> strs; vector<string> strs;
splitFields(g_integrationVars, &strs); splitFields(g_integrationVars, &strs);
for (auto& str : strs) { for (auto& str : strs) {
size_t pos = str.find('='); m_replacers.parseLine(str);
if (pos == string::npos || pos == 0) {
continue;
}
m_replacers.set(str.substr(0, pos), str.substr(pos+1));
} }
} }
} }
} }
// determine topic and prefix // determine topic and prefix
MqttReplacer& topic = m_replacers.get("topic"); StringReplacer& topic = m_replacers.get("topic");
if (g_topic) { if (g_topic) {
string str = g_topic; string str = g_topic;
bool noDefault = str[str.size()-1] == '#'; bool noDefault = str[str.size()-1] == '#';
@@ -1009,7 +519,7 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap*
if (!topic.parse(str, true, true)) { if (!topic.parse(str, true, true)) {
logOtherNotice("mqtt", "unknown or duplicate topic parts potentially prevent matching incoming topics"); logOtherNotice("mqtt", "unknown or duplicate topic parts potentially prevent matching incoming topics");
topic.parse(str, true); topic.parse(str, true);
} else if (!topic.checkMatch()) { } else if (!topic.checkMatchability()) {
logOtherNotice("mqtt", "missing separators between topic parts potentially prevent matching incoming topics"); 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()); logOtherDebug("mqtt", "received topic %s with data %s", topic.c_str(), data.c_str());
string circuit, name, field; 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) { if (match < 0 && !isList) {
logOtherError("mqtt", "received unmatchable topic %s", topic.c_str()); logOtherError("mqtt", "received unmatchable topic %s", topic.c_str());
} }
@@ -1485,7 +995,7 @@ void MqttHandler::run() {
continue; 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("circuit", message->getCircuit());
msgValues.set("name", message->getName()); msgValues.set("name", message->getName());
msgValues.set("priority", static_cast<int>(message->getPollPriority())); msgValues.set("priority", static_cast<int>(message->getPollPriority()));
@@ -1540,7 +1050,7 @@ void MqttHandler::run() {
if (str.empty()) { if (str.empty()) {
continue; 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("index", static_cast<signed>(index));
values.set("field", fieldName); values.set("field", fieldName);
values.set("fieldname", field->getName(-1)); 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) { const string& circuit, const string& name, const string& fallbackPrefix) {
bool reduce = false; bool reduce = false;
if (!topic.empty()) { if (!topic.empty()) {
@@ -1712,7 +1222,7 @@ void MqttHandler::publishDefinition(MqttReplacers values, const string& prefix,
publishTopic(defTopic, payload, retain); publishTopic(defTopic, payload, retain);
} }
void MqttHandler::publishDefinition(const MqttReplacers& values) { void MqttHandler::publishDefinition(const StringReplacers& values) {
string defTopic = values.get("definition-topic", false); string defTopic = values.get("definition-topic", false);
if (defTopic.empty()) { if (defTopic.empty()) {
if (needsLog(lf_other, ll_debug)) { if (needsLog(lf_other, ll_debug)) {
+7 -227
View File
@@ -24,14 +24,14 @@
#include <string> #include <string>
#include <list> #include <list>
#include <vector> #include <vector>
#include <utility>
#include "ebusd/datahandler.h" #include "ebusd/datahandler.h"
#include "ebusd/bushandler.h" #include "ebusd/bushandler.h"
#include "lib/ebus/message.h" #include "lib/ebus/message.h"
#include "lib/ebus/stringhelper.h"
namespace ebusd { namespace ebusd {
/** @file ebusd/mqtthandler.h /** \file ebusd/mqtthandler.h
* A data handler enabling MQTT support via mosquitto. * 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, bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages,
list<DataHandler*>* handlers); 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. * 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); MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages);
private:
void parseIntegration(const string& line);
public: public:
/** /**
* Destructor. * 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 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. * @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); const string& circuit, const string& name, const string& fallbackPrefix);
/** /**
* Publish a definition topic as specified in the given values. * Publish a definition topic as specified in the given values.
* @param values the values with the message specification. * @param values the values with the message specification.
*/ */
void publishDefinition(const MqttReplacers& values); void publishDefinition(const StringReplacers& values);
/** /**
* Called regularly to handle MQTT traffic. * Called regularly to handle MQTT traffic.
@@ -384,7 +164,7 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread {
MessageMap* m_messages; MessageMap* m_messages;
/** the global topic replacer. */ /** the global topic replacer. */
MqttReplacer m_globalTopic; StringReplacer m_globalTopic;
/** the topic to subscribe to. */ /** the topic to subscribe to. */
string m_subscribeTopic; 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. */ /** whether to publish a separate topic for each message field. */
bool m_publishByField; bool m_publishByField;
/** the @a MqttReplacers from the integration file. */ /** the @a StringReplacers from the integration file. */
MqttReplacers m_replacers; StringReplacers m_replacers;
/** whether the @a m_replacers includes the definition_topic. */ /** whether the @a m_replacers includes the definition_topic. */
bool m_hasDefinitionTopic; bool m_hasDefinitionTopic;
+7 -12
View File
@@ -1,20 +1,15 @@
add_definitions(-Wconversion -Wno-unused-parameter) add_definitions(-Wconversion -Wno-unused-parameter)
set(libebus_a_SOURCES set(libebus_a_SOURCES
result.cpp result.h result.cpp
result.h symbol.h symbol.cpp
symbol.cpp filereader.h filereader.cpp
symbol.h datatype.h datatype.cpp
filereader.h data.h data.cpp
filereader.cpp device.h device.cpp
datatype.cpp
datatype.h
data.cpp
data.h
device.cpp
device.h
message.cpp message.cpp
message.h message.h
stringhelper.h stringhelper.cpp
) )
if(HAVE_CONTRIB) if(HAVE_CONTRIB)
+2 -1
View File
@@ -17,7 +17,8 @@ libebus_a_SOURCES = result.cpp \
device.cpp \ device.cpp \
device.h \ device.h \
message.cpp \ message.cpp \
message.h message.h \
stringhelper.h stringhelper.cpp
if CONTRIB if CONTRIB
SUBDIRS = 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 queue.h
notify.h notify.h
rotatefile.h rotatefile.cpp rotatefile.h rotatefile.cpp
httpclient.h httpclient.cpp) httpclient.h httpclient.cpp
)
add_library(utils ${libutils_a_SOURCES}) add_library(utils ${libutils_a_SOURCES})