From d32ac53adf4456515ca4145d777e12ea574cd66f Mon Sep 17 00:00:00 2001 From: h2zero Date: Tue, 22 Mar 2022 10:10:48 -0600 Subject: [PATCH] Add nesting and NOT condition support for device conditions. * Conditions for device detection can now use nested JSON arrays to allow more specific evaluation. * Device conditions can now support a NOT operation when the condition check type "index" and a "!" is preceded before the value to test for. --- docs/participate/adding-decoders.md | 26 +++ src/decoder.cpp | 249 ++++++++++++++++------------ src/decoder.h | 4 +- 3 files changed, 169 insertions(+), 110 deletions(-) diff --git a/docs/participate/adding-decoders.md b/docs/participate/adding-decoders.md index 4a79fce4e..3f05ad0e1 100644 --- a/docs/participate/adding-decoders.md +++ b/docs/participate/adding-decoders.md @@ -62,6 +62,21 @@ The third parameter (fifth if data length is specified) can be either the index For example: `"condition":["servicedata", "index", 0, "0804", '|', "servicedata", "index", 0, "8804"]` This will match if the service data at index 0 is "0804" `OR` "8804". +`condition` can contain JSON arrays that can be processed separately. This allows for nesting of detection tests such as: +`"condition": [["servicedata", "index", 0, "1234", "&" "servicedata", "index", 5, "5678"], "|", "servicedata", "index", 30, "ABCD"]` +This will result in a positive detection if the service data at index `0` == `0x1234` and the service data at index `5` == `0x5678`, otherwise, if the service data at index `30` == `0xABCD`, the result will also be positive. + +::: warning Note +Nesting is discouraged from use wherever possible as the recursive nature may cause stack overflowing in some circumstaces. +The above example could be re-written as: +`"condition": ["servicedata", "index", 30, "ABCD", "|", "servicedata", "index", 0, "1234", "&" "servicedata", "index", 5, "5678"]` +Which has the same result, without nesting. +::: + +`condition` NOT(!) testing; Anytime a condition test value is preceded by a "!", the inverse of the result will be used to determine the result. +Example: `"condition": ["servicedata", "index", 30, "!", "ABCD", "&", "servicedata", "index", 0, "1234"] +If the value of the service data at index 30 is not 0xABCD and the data at index 0 is 0x1234, the result is a positive detection. + ### Properties Properties is a nested JSON object containing one or more JSON objects. In the example above it looks like: ``` @@ -82,6 +97,17 @@ Here we have a single property that defines a value that we want to decode. The The second parameter is the index of the data source to look for the value. The third parameter is the value to test for. If the condition is met the data will be decoded and added to the JsonObject. +`condition` can contain JSON arrays that can be processed separately. This allows for nesting of detection tests such as: +`"condition": [["servicedata", 25, "4", "&" "servicedata", 26, "5"], "|", "servicedata", 30, "ABCD"]` +This will result in a positive detection if the service data at index `25` == `4` and the service data at index `26` == `5`, otherwise, if the service data at index `30` == `0xABCD`, the result will also be positive. + +::: warning Note +Nesting is discouraged from use wherever possible as the recursive nature may cause stack overflowing in some circumstaces. +The above example could be re-written as: +`"condition": ["servicedata", 30, "ABCD", "|", "servicedata", 25, "4", "&" "servicedata", 5, "5"]` +Which has the same result, without nesting. +::: + Property conditions also allow for a NOT comparison, as in ``` "properties":{ diff --git a/src/decoder.cpp b/src/decoder.cpp index 60809e782..334f4532a 100644 --- a/src/decoder.cpp +++ b/src/decoder.cpp @@ -128,7 +128,7 @@ bool TheengsDecoder::data_index_is_valid(const char* str, size_t index, size_t l } int TheengsDecoder::data_length_is_valid(size_t data_len, size_t default_min, - JsonArray& condition, int idx) { + const JsonArray& condition, int idx) { std::string op = condition[idx + 1].as(); if (!op.empty() && op.length() > 2) { return (data_len >= default_min) ? 0 : -1; @@ -149,6 +149,140 @@ int TheengsDecoder::data_length_is_valid(size_t data_len, size_t default_min, return -1; } +bool TheengsDecoder::checkDeviceMatch(const JsonArray& condition, + const char* svc_data, + const char* mfg_data, + const char* dev_name, + const char* svc_uuid) { + bool match = false; + size_t cond_size = condition.size(); + + for (size_t i = 0; i < cond_size;) { + if (condition[i].is()) { + DEBUG_PRINT("found nested array\n"); + match = checkDeviceMatch(condition[i], svc_data, mfg_data, dev_name, svc_uuid); + + if (++i < cond_size) { + if (!match && *condition[i].as() == '|') { + } else if (match && *condition[i].as() == '&') { + match = false; + } else { + break; + } + i++; + } else { + break; + } + } + + const char* cmp_str; + const char* cond_str = condition[i].as(); + int len_idx; + if (svc_data != nullptr && strstr(cond_str, SVC_DATA) != nullptr) { + len_idx = data_length_is_valid(strlen(svc_data), m_minSvcDataLen, condition, i); + if (len_idx >= 0) { + i += len_idx; + cmp_str = svc_data; + match = true; + } else { + match = false; + break; + } + } else if (mfg_data != nullptr && strstr(cond_str, MFG_DATA) != nullptr) { + len_idx = data_length_is_valid(strlen(mfg_data), m_minMfgDataLen, condition, i); + if (len_idx >= 0) { + i += len_idx; + cmp_str = mfg_data; + match = true; + } else { + match = false; + break; + } + } else if (dev_name != nullptr && strstr(cond_str, "name") != nullptr) { + cmp_str = dev_name; + } else if (svc_uuid != nullptr && strstr(cond_str, "uuid") != nullptr) { + cmp_str = svc_uuid; + } else { + break; + } + + cond_str = condition[i + 1].as(); + if (cond_str) { + if (cmp_str == svc_uuid && !strncmp(cmp_str, "0x", 2)) { + cmp_str += 2; + } + + if (strstr(cond_str, "contain") != nullptr) { + if (strstr(cmp_str, condition[i + 2].as()) != nullptr) { + match = true; + } else { + match = false; + } + i += 3; + } else if (strstr(cond_str, "index") != nullptr) { + size_t cond_index = condition[i + 2].as(); + size_t cond_len = strlen(condition[i + 3].as()); + + if (!data_index_is_valid(cmp_str, cond_index, cond_len)) { + DEBUG_PRINT("Invalid data %s; skipping\n", cmp_str); + match = false; + break; + } + + bool inverse = false; + if (*condition[i + 3].as() == '!') { + inverse = true; + } + + DEBUG_PRINT("comparing value: %s to %s at index %u\n", + &cmp_str[cond_index], + condition[i + 3 + inverse].as(), + condition[i + 2].as()); + + if (strncmp(&cmp_str[cond_index], + condition[i + 3 + inverse].as(), + cond_len) == 0) { + match = inverse ? false : true; + } else { + match = inverse ? true : false; + } + + i += 4 + inverse; + } + + cond_str = condition[i].as(); + } + + size_t cond_size = condition.size(); + + if (i < cond_size && cond_str != nullptr) { + if (!match && *cond_str == '|') { + i++; + continue; + } else if (match && *cond_str == '&') { + i++; + match = false; + continue; + } else if (match) { // check for AND case before exit + while (i < cond_size && *cond_str != '&') { + if (!condition[++i].is()) { + continue; + } + cond_str = condition[++i].as(); + } + + if (i < cond_size && cond_str != nullptr) { + i++; + match = false; + continue; + } + } + } + break; + } + return match; +} + bool TheengsDecoder::checkPropCondition(const JsonArray& prop_condition, const char* svc_data, const char* mfg_data) { @@ -189,17 +323,15 @@ bool TheengsDecoder::checkPropCondition(const JsonArray& prop_condition, if (!strncmp(&data_src[prop_condition[i + 1].as()], prop_condition[i + 2 + inverse].as(), cond_len)) { cond_met = inverse ? false : true; - } else if (inverse) { - cond_met = true; + } else { + cond_met = inverse ? true : false; } } else { DEBUG_PRINT("ERROR property condition data source invalid\n"); return false; } - if (inverse) { - i++; - } + i += inverse; if (cond_size > (i + 3)) { if (!cond_met && *prop_condition[i + 3].as() == '|') { @@ -254,108 +386,8 @@ int TheengsDecoder::decodeBLEJson(JsonObject& jsondata) { peakDocSize = doc.memoryUsage(); #endif - JsonArray condition = doc["condition"]; - bool match = false; - size_t min_len = m_minMfgDataLen; - - for (unsigned int i = 0; i < condition.size();) { - const char* cmp_str; - const char* cond_str = condition[i].as(); - int len_idx; - if (svc_data != nullptr && strstr(cond_str, SVC_DATA) != nullptr) { - len_idx = data_length_is_valid(strlen(svc_data), m_minSvcDataLen, condition, i); - if (len_idx >= 0) { - i += len_idx; - cmp_str = svc_data; - match = true; - } else { - match = false; - break; - } - } else if (mfg_data != nullptr && strstr(cond_str, MFG_DATA) != nullptr) { - len_idx = data_length_is_valid(strlen(mfg_data), m_minMfgDataLen, condition, i); - if (len_idx >= 0) { - i += len_idx; - cmp_str = mfg_data; - match = true; - } else { - match = false; - break; - } - } else if (dev_name != nullptr && strstr(cond_str, "name") != nullptr) { - cmp_str = dev_name; - } else if (svc_uuid != nullptr && strstr(cond_str, "uuid") != nullptr) { - cmp_str = svc_uuid; - } else { - break; - } - - cond_str = condition[i + 1].as(); - if (cond_str) { - if (cmp_str == svc_uuid && !strncmp(cmp_str, "0x", 2)) { - cmp_str += 2; - } - - if (strstr(cond_str, "contain") != nullptr) { - if (strstr(cmp_str, condition[i + 2].as()) != nullptr) { - match = true; - } else { - match = false; - } - i += 3; - } else if (strstr(cond_str, "index") != nullptr) { - size_t cond_index = condition[i + 2].as(); - size_t cond_len = strlen(condition[i + 3].as()); - - if (!data_index_is_valid(cmp_str, cond_index, cond_len)) { - DEBUG_PRINT("Invalid data %s; skipping\n", cmp_str); - match = false; - break; - } - DEBUG_PRINT("comparing index: %s to %s at index %u\n", - &cmp_str[condition[i + 2].as()], - condition[i + 3].as(), condition[i + 2].as()); - if (strncmp(&cmp_str[cond_index], condition[i + 3].as(), cond_len) == 0) { - match = true; - } else { - match = false; - } - i += 4; - } - - cond_str = condition[i].as(); - } - - unsigned int cond_size = condition.size(); - - if (i < cond_size && cond_str != nullptr) { - if (!match && *cond_str == '|') { - i++; - continue; - } else if (match && *cond_str == '&') { - i++; - match = false; - continue; - } else if (match) { // check for AND case before exit - while (i < cond_size && *cond_str != '&') { - if (!condition[++i].is()) { - continue; - } - cond_str = condition[++i].as(); - } - - if (i < condition.size() && cond_str != nullptr) { - i++; - match = false; - continue; - } - } - } - break; - } - /* found a match, extract the data */ - if (match) { + if (checkDeviceMatch(doc["condition"], svc_data, mfg_data, dev_name, svc_uuid)) { jsondata["brand"] = doc["brand"]; jsondata["model"] = doc["model"]; jsondata["model_id"] = doc["model_id"]; @@ -365,9 +397,8 @@ int TheengsDecoder::decodeBLEJson(JsonObject& jsondata) { /* Loop through all the devices properties and extract the values */ for (JsonPair kv : properties) { JsonObject prop = kv.value().as(); - bool cond_met = checkPropCondition(prop["condition"], svc_data, mfg_data); - if (cond_met) { + if (checkPropCondition(prop["condition"], svc_data, mfg_data)) { JsonArray decoder = prop["decoder"]; if (strstr((const char*)decoder[0], "value_from_hex_data") != nullptr) { const char* src = svc_data; diff --git a/src/decoder.h b/src/decoder.h index eb0a102a2..c3191fcd4 100644 --- a/src/decoder.h +++ b/src/decoder.h @@ -93,8 +93,10 @@ class TheengsDecoder { double value_from_hex_string(const char* data_str, int offset, int data_length, bool reverse, bool canBeNegative = true); double bf_value_from_hex_string(const char* data_str, int offset, int data_length, bool reverse, bool canBeNegative = true); bool data_index_is_valid(const char* str, size_t index, size_t len); - int data_length_is_valid(size_t data_len, size_t default_min, JsonArray& condition, int idx); + int data_length_is_valid(size_t data_len, size_t default_min, const JsonArray& condition, int idx); bool checkPropCondition(const JsonArray& prop, const char* svc_data, const char* mfg_data); + bool checkDeviceMatch(const JsonArray& condition, const char* svc_data, const char* mfg_data, + const char* dev_name, const char* svc_uuid); std::string sanitizeJsonKey(const char* key_in); size_t m_docMax = 7168;