diff --git a/docs/participate/adding-decoders.md b/docs/participate/adding-decoders.md index 4a79fce4..6d13b1dc 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 circumstances. +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 circumstances. +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 60809e78..334f4532 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 6ece6910..2a866906 100644 --- a/src/decoder.h +++ b/src/decoder.h @@ -94,8 +94,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;