core(M1): ayla-криптография, конверт, JSON (jsmn), HTTP-клиент
- crypto: KDF Ayla (двойной HMAC, suffix 0x30/31/32; app/dev направления),
AES-256-CBC с непрерывной цепочкой (iv обновляется mbedtls на месте),
Java-паддинг >=1 NUL; mode-latch против misuse (encrypt|decrypt);
zeroize ключей при повторном init; векторы из APK (4 сессии × 4 сообщения,
включая legacy-приём без NUL) — scripts/gen_kdf_vectors.py.
- envelope: pack/unpack {"enc","sign"}; расшифровка (движение цепочки)
ДО проверки подписи; сравнение подписи в константном времени;
extract_seq_no — depth-1 сканер без лимита токенов (OOB после escape
исправлен, регресс-тесты по ASan-репро ревьюера).
- json: Writer (фикс. буфер, стек глубин, escape, ok()=false при
переполнении) + Doc на jsmn (64 токена, unescape, overflow-guard).
- httpc: блокирующий POST/PUT для local_reg (статус 200-599, дренаж,
shutdown перед close).
- third_party/jsmn (MIT, JSMN_STATIC).
- CMake: mbedtls системный (/usr/include/mbedtls3) или FetchContent;
IDF: PRIV_REQUIRES mbedtls.
- CI: 3 конфигурации — gcc-Release, gcc-Debug+ASan/UBSan, clang-Release;
6/6 тестов стабильно; ESP-IDF esp32 build complete.
Ревью под-агентом: 3 круга (OOB-блокер + тестовые флаки закрыты), APPROVED.
This commit is contained in:
156
tests/ayla/test_json.cpp
Normal file
156
tests/ayla/test_json.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// Тесты JSON: writer (фиксированный буфер, вложенность, переполнение)
|
||||
// и парсер (jsmn) — форматы протокола.
|
||||
#include "doctest/doctest.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "ayla/json.hpp"
|
||||
|
||||
using fgl::ayla::json::Doc;
|
||||
using fgl::ayla::json::Writer;
|
||||
|
||||
TEST_CASE("json writer: формы протокола") {
|
||||
// datapoint-команда (properties)
|
||||
char buf[512];
|
||||
Writer w(buf, sizeof(buf));
|
||||
w.begin_object(); // {
|
||||
w.key("properties");
|
||||
w.begin_array();
|
||||
w.begin_object();
|
||||
w.key("property");
|
||||
w.begin_object();
|
||||
w.key("base_type"); w.string("integer");
|
||||
w.key("name"); w.string("fan_speed");
|
||||
w.key("value"); w.integer(3);
|
||||
w.key("id"); w.string("ab12cd34");
|
||||
w.end_object();
|
||||
w.end_object();
|
||||
w.end_array();
|
||||
w.end_object();
|
||||
REQUIRE(w.ok());
|
||||
CHECK(std::string(buf) ==
|
||||
"{\"properties\":[{\"property\":{\"base_type\":\"integer\","
|
||||
"\"name\":\"fan_speed\",\"value\":3,\"id\":\"ab12cd34\"}}]}");
|
||||
|
||||
// GET-команда (cmds)
|
||||
Writer w2(buf, sizeof(buf));
|
||||
w2.begin_object();
|
||||
w2.key("cmds");
|
||||
w2.begin_array();
|
||||
w2.begin_object();
|
||||
w2.key("cmd");
|
||||
w2.begin_object();
|
||||
w2.key("cmd_id"); w2.integer(5);
|
||||
w2.key("method"); w2.string("GET");
|
||||
w2.key("resource"); w2.string("property.json?name=op");
|
||||
w2.key("data"); w2.string("");
|
||||
w2.key("uri"); w2.string("/local_lan/property/datapoint.json");
|
||||
w2.end_object();
|
||||
w2.end_object();
|
||||
w2.end_array();
|
||||
w2.end_object();
|
||||
REQUIRE(w2.ok());
|
||||
CHECK(std::string(buf) ==
|
||||
"{\"cmds\":[{\"cmd\":{\"cmd_id\":5,\"method\":\"GET\","
|
||||
"\"resource\":\"property.json?name=op\",\"data\":\"\","
|
||||
"\"uri\":\"/local_lan/property/datapoint.json\"}}]}");
|
||||
|
||||
// key_exchange ответ + массив целых + bool/null
|
||||
Writer w3(buf, sizeof(buf));
|
||||
w3.begin_object();
|
||||
w3.key("random_2"); w3.string("abcd1234");
|
||||
w3.key("time_2"); w3.integer(-123);
|
||||
w3.key("flags");
|
||||
w3.begin_array();
|
||||
w3.integer(1); w3.integer(2); w3.integer(3);
|
||||
w3.end_array();
|
||||
w3.key("ok"); w3.boolean(true);
|
||||
w3.key("nothing"); w3.literal("null");
|
||||
w3.end_object();
|
||||
REQUIRE(w3.ok());
|
||||
CHECK(std::string(buf) ==
|
||||
"{\"random_2\":\"abcd1234\",\"time_2\":-123,\"flags\":[1,2,3],"
|
||||
"\"ok\":true,\"nothing\":null}");
|
||||
}
|
||||
|
||||
TEST_CASE("json writer: экранирование и переполнение") {
|
||||
char buf[64];
|
||||
Writer w(buf, sizeof(buf));
|
||||
w.begin_object();
|
||||
w.key("a\"b\\c"); // ключ с кавычкой и обратным слешем
|
||||
w.string("x\ny\"z");
|
||||
w.end_object();
|
||||
REQUIRE(w.ok());
|
||||
CHECK(std::string(buf) == "{\"a\\\"b\\\\c\":\"x\\ny\\\"z\"}");
|
||||
|
||||
// Переполнение буфера — ok() = false, не выходим за границы.
|
||||
Writer w2(buf, sizeof(buf));
|
||||
w2.begin_object();
|
||||
w2.key("data");
|
||||
w2.string("0123456789012345678901234567890123456789012345678901234567890123456789");
|
||||
w.end_object();
|
||||
CHECK_FALSE(w2.ok());
|
||||
CHECK(strlen(buf) < sizeof(buf)); // NUL-терминировано всегда
|
||||
}
|
||||
|
||||
TEST_CASE("json parser: форматы протокола") {
|
||||
// datapoint push
|
||||
char buf[128];
|
||||
strcpy(buf, "{\"name\":\"fan_speed\",\"value\":3,\"metadata\":{}}");
|
||||
Doc doc;
|
||||
REQUIRE(doc.parse(buf));
|
||||
char name[32];
|
||||
REQUIRE(doc.get_string("name", name, sizeof(name)));
|
||||
CHECK(std::string(name) == "fan_speed");
|
||||
int64_t value = -1;
|
||||
REQUIRE(doc.get_int("value", &value));
|
||||
CHECK(value == 3);
|
||||
bool b = true;
|
||||
CHECK_FALSE(doc.get_bool("value", &b)); // не bool
|
||||
CHECK_FALSE(doc.get_string("absent", name, sizeof(name)));
|
||||
|
||||
// key_exchange
|
||||
strcpy(buf, "{\"key_exchange\":{\"ver\":1,\"proto\":1,\"key_id\":62999,"
|
||||
"\"random_1\":\"ab+/12\",\"time_1\":-987654321,\"sec\":\"\"}}");
|
||||
REQUIRE(doc.parse(buf));
|
||||
int64_t ver = -1, key_id = -1, time_1 = -1;
|
||||
REQUIRE(doc.get_int("ver", &ver));
|
||||
REQUIRE(doc.get_int("key_id", &key_id));
|
||||
REQUIRE(doc.get_int("time_1", &time_1));
|
||||
char rnd[32];
|
||||
REQUIRE(doc.get_string("random_1", rnd, sizeof(rnd)));
|
||||
CHECK(ver == 1);
|
||||
CHECK(key_id == 62999);
|
||||
CHECK(time_1 == -987654321);
|
||||
CHECK(std::string(rnd) == "ab+/12");
|
||||
|
||||
// envelope payload
|
||||
strcpy(buf, "{\"seq_no\":42,\"data\":{\"name\":\"x\",\"value\":true}}");
|
||||
REQUIRE(doc.parse(buf));
|
||||
int64_t seq = -1;
|
||||
REQUIRE(doc.get_int("seq_no", &seq));
|
||||
CHECK(seq == 42);
|
||||
bool v = false;
|
||||
REQUIRE(doc.get_bool("value", &v));
|
||||
CHECK(v);
|
||||
|
||||
// Мусор
|
||||
strcpy(buf, "not json");
|
||||
CHECK_FALSE(doc.parse(buf));
|
||||
strcpy(buf, "[1,2]");
|
||||
CHECK_FALSE(doc.parse(buf)); // верхний уровень — объект
|
||||
}
|
||||
|
||||
TEST_CASE("json parser: экранированные строки") {
|
||||
char buf[96];
|
||||
strcpy(buf, "{\"name\":\"a\\\"b\\\\c\\nd\"}");
|
||||
Doc doc;
|
||||
REQUIRE(doc.parse(buf));
|
||||
char out[32];
|
||||
REQUIRE(doc.get_string("name", out, sizeof(out)));
|
||||
CHECK(std::string(out) == "a\"b\\c\nd");
|
||||
// Не помещается в out
|
||||
char small[4];
|
||||
CHECK_FALSE(doc.get_string("name", small, sizeof(small)));
|
||||
}
|
||||
Reference in New Issue
Block a user