From 8992c745ef7a635cf90ccfb7962d2a8436d1c360 Mon Sep 17 00:00:00 2001 From: KraPete <86825564+KraPete@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:59:33 +0500 Subject: [PATCH] Validate conditions syntax (#850) Validate conditions syntax on device template loading and during package build --- debian/changelog | 6 +++ src/expression_evaluator.h | 1 + src/templates_map.cpp | 44 +++++++++++++++++++ .../config-invalid-channel-condition.json | 16 +++++++ .../config-invalid-param-condition.json | 30 +++++++++++++ .../config-invalid-setup-condition.json | 23 ++++++++++ test/device_templates_file_extension_test.cpp | 32 ++++++++++++++ 7 files changed, 152 insertions(+) create mode 100644 test/device-templates/config-invalid-channel-condition.json create mode 100644 test/device-templates/config-invalid-param-condition.json create mode 100644 test/device-templates/config-invalid-setup-condition.json diff --git a/debian/changelog b/debian/changelog index 98262cf84..6998568a5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +wb-mqtt-serial (2.152.5) stable; urgency=medium + + * Validate conditions syntax in device templates + + -- Petr Krasnoshchekov Tue, 14 Jan 2025 12:13:58 +0500 + wb-mqtt-serial (2.152.4) stable; urgency=medium * Disable events for `RGB Strip Hue` for WB-LED and WB-MRGBW-D diff --git a/src/expression_evaluator.h b/src/expression_evaluator.h index 5ada71d79..c3381c9c5 100644 --- a/src/expression_evaluator.h +++ b/src/expression_evaluator.h @@ -130,6 +130,7 @@ namespace Expressions * * @param str string containing expression to parse * @return resulting AST + * @throw std::runtime_error on parsing error */ std::unique_ptr Parse(const std::string& str); }; diff --git a/src/templates_map.cpp b/src/templates_map.cpp index c6242145a..4263e6aee 100644 --- a/src/templates_map.cpp +++ b/src/templates_map.cpp @@ -2,6 +2,7 @@ #include +#include "expression_evaluator.h" #include "file_utils.h" #include "json_common.h" #include "log.h" @@ -36,6 +37,48 @@ namespace } } } + + void ValidateCondition(const Json::Value& node, std::unordered_set& validConditions) + { + if (node.isMember("condition")) { + auto condition = node["condition"].asString(); + if (validConditions.find(condition) == validConditions.end()) { + Expressions::TParser parser; + parser.Parse(condition); + validConditions.insert(condition); + } + } + } + + std::string GetNodeName(const Json::Value& node, const std::string& name) + { + const std::vector keys = {"name", "title"}; + for (const auto& key: keys) { + if (node.isMember(key)) { + return node[key].asString(); + } + } + return name; + } + + void ValidateConditions(Json::Value& deviceTemplate) + { + std::unordered_set validConditions; + std::vector sections = {"channels", "setup", "parameters"}; + for (const auto& section: sections) { + if (deviceTemplate.isMember(section)) { + Json::Value& sectionNodes = deviceTemplate[section]; + for (auto it = sectionNodes.begin(); it != sectionNodes.end(); ++it) { + try { + ValidateCondition(*it, validConditions); + } catch (const runtime_error& e) { + throw runtime_error("Failed to parse condition in " + section + "[" + + GetNodeName(*it, it.name()) + "]: " + e.what()); + } + } + } + } + } } //============================================================================= @@ -253,6 +296,7 @@ const Json::Value& TDeviceTemplate::GetTemplate() if (!IsDeprecated()) { try { Validator->Validate(root); + ValidateConditions(root["device"]); } catch (const std::runtime_error& e) { throw std::runtime_error("File: " + GetFilePath() + " error: " + e.what()); } diff --git a/test/device-templates/config-invalid-channel-condition.json b/test/device-templates/config-invalid-channel-condition.json new file mode 100644 index 000000000..6ddb0c7e7 --- /dev/null +++ b/test/device-templates/config-invalid-channel-condition.json @@ -0,0 +1,16 @@ +{ + "device_type": "invalid_channel_condition", + "device": { + "name": "invalid channel condition", + "id": "invalid channel condition", + "channels": [ + { + "name": "Temperature", + "reg_type": "input", + "format": "s32", + "address": "0x0504", + "condition": "( in1 > 3 )" + } + ] + } +} diff --git a/test/device-templates/config-invalid-param-condition.json b/test/device-templates/config-invalid-param-condition.json new file mode 100644 index 000000000..f092323fb --- /dev/null +++ b/test/device-templates/config-invalid-param-condition.json @@ -0,0 +1,30 @@ +{ + "device_type": "invalid_param_condition", + "device": { + "name": "invalid param condition", + "id": "invalid param condition", + "setup": [ + { + "title": "Param", + "address": "0x0504", + "value": 1 + } + ], + "channels": [ + { + "name": "Temperature", + "reg_type": "input", + "format": "s32", + "address": "0x0504" + } + ], + "parameters": [ + { + "id": "Test", + "title": "p1", + "address": 9992, + "condition": "( in1 > 3 )" + } + ] + } +} diff --git a/test/device-templates/config-invalid-setup-condition.json b/test/device-templates/config-invalid-setup-condition.json new file mode 100644 index 000000000..b6462c074 --- /dev/null +++ b/test/device-templates/config-invalid-setup-condition.json @@ -0,0 +1,23 @@ +{ + "device_type": "invalid_setup_condition", + "device": { + "name": "invalid setup condition", + "id": "invalid setup condition", + "setup": [ + { + "title": "Param", + "address": "0x0504", + "value": 1, + "condition": "( in1 > 3 )" + } + ], + "channels": [ + { + "name": "Temperature", + "reg_type": "input", + "format": "s32", + "address": "0x0504" + } + ] + } +} diff --git a/test/device_templates_file_extension_test.cpp b/test/device_templates_file_extension_test.cpp index fe184ce29..877065d96 100644 --- a/test/device_templates_file_extension_test.cpp +++ b/test/device_templates_file_extension_test.cpp @@ -134,3 +134,35 @@ TEST_F(TDeviceTemplatesTest, InvalidParameterName) EXPECT_NO_THROW(templates.GetTemplate("parameters_object_invalid_name")->GetTemplate()); EXPECT_THROW(templates.GetTemplate("tpl1_parameters_object_invalid_name")->GetTemplate(), std::runtime_error); } + +TEST_F(TDeviceTemplatesTest, InvalidCondition) +{ + auto commonDeviceSchema( + WBMQTT::JSON::Parse(TLoggedFixture::GetDataFilePath("../wb-mqtt-serial-confed-common.schema.json"))); + Json::Value templatesSchema( + LoadConfigTemplatesSchema(TLoggedFixture::GetDataFilePath("../wb-mqtt-serial-device-template.schema.json"), + commonDeviceSchema)); + TTemplateMap templates(templatesSchema); + templates.AddTemplatesDir(TLoggedFixture::GetDataFilePath("device-templates"), false); + + const std::unordered_map expectedErrors = { + {"invalid_channel_condition", + "File: test/device-templates/config-invalid-channel-condition.json error: Failed to parse condition in " + "channels[Temperature]: unexpected symbol ' ' at position 2"}, + {"invalid_setup_condition", + "File: test/device-templates/config-invalid-setup-condition.json error: Failed to parse condition in " + "setup[Param]: unexpected symbol ' ' at position 2"}, + {"invalid_param_condition", + "File: test/device-templates/config-invalid-param-condition.json error: Failed to parse condition in " + "parameters[p1]: unexpected symbol ' ' at position 2"}, + }; + + for (const auto& [deviceType, expectedError]: expectedErrors) { + try { + templates.GetTemplate(deviceType)->GetTemplate(); + ADD_FAILURE() << "Expect std::runtime_error"; + } catch (const std::runtime_error& e) { + ASSERT_STREQ(expectedError.c_str(), e.what()); + } + } +}