Skip to content

Commit

Permalink
Add nesting and NOT condition support for device conditions.
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
h2zero committed Mar 28, 2022
1 parent 87ed288 commit d32ac53
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 110 deletions.
26 changes: 26 additions & 0 deletions docs/participate/adding-decoders.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
```
Expand All @@ -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":{
Expand Down
249 changes: 140 additions & 109 deletions src/decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>();
if (!op.empty() && op.length() > 2) {
return (data_len >= default_min) ? 0 : -1;
Expand All @@ -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<JsonArray>()) {
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<const char*>() == '|') {
} else if (match && *condition[i].as<const char*>() == '&') {
match = false;
} else {
break;
}
i++;
} else {
break;
}
}

const char* cmp_str;
const char* cond_str = condition[i].as<const char*>();
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<const char*>();
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<const char*>()) != 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>();
size_t cond_len = strlen(condition[i + 3].as<const char*>());

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<const char*>() == '!') {
inverse = true;
}

DEBUG_PRINT("comparing value: %s to %s at index %u\n",
&cmp_str[cond_index],
condition[i + 3 + inverse].as<const char*>(),
condition[i + 2].as<size_t>());

if (strncmp(&cmp_str[cond_index],
condition[i + 3 + inverse].as<const char*>(),
cond_len) == 0) {
match = inverse ? false : true;
} else {
match = inverse ? true : false;
}

i += 4 + inverse;
}

cond_str = condition[i].as<const char*>();
}

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<const char*>()) {
continue;
}
cond_str = condition[++i].as<const char*>();
}

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) {
Expand Down Expand Up @@ -189,17 +323,15 @@ bool TheengsDecoder::checkPropCondition(const JsonArray& prop_condition,
if (!strncmp(&data_src[prop_condition[i + 1].as<int>()],
prop_condition[i + 2 + inverse].as<const char*>(), 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<const char*>() == '|') {
Expand Down Expand Up @@ -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<const char*>();
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<const char*>();
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<const char*>()) != 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>();
size_t cond_len = strlen(condition[i + 3].as<const char*>());

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<unsigned int>()],
condition[i + 3].as<const char*>(), condition[i + 2].as<unsigned int>());
if (strncmp(&cmp_str[cond_index], condition[i + 3].as<const char*>(), cond_len) == 0) {
match = true;
} else {
match = false;
}
i += 4;
}

cond_str = condition[i].as<const char*>();
}

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<const char*>()) {
continue;
}
cond_str = condition[++i].as<const char*>();
}

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"];
Expand All @@ -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<JsonObject>();
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;
Expand Down
4 changes: 3 additions & 1 deletion src/decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit d32ac53

Please sign in to comment.