Files
fgl-aircon/src/ayla/json.hpp
T
petr.polezhaev fd5f125d04 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.
2026-09-22 15:46:18 +03:00

254 lines
7.6 KiB
C++

// Минимальный JSON: writer с фиксированным буфером + парсер (jsmn).
// Никаких аллокаций; экранирование строк — достаточное для протокола.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#define JSMN_STATIC
#include "third_party/jsmn/jsmn.h"
namespace fgl::ayla::json {
// ---------------------------------------------------------------------------
// Writer. Стек глубин <= 8; состояние контейнеров (были ли элементы).
// ---------------------------------------------------------------------------
class Writer {
public:
Writer(char* buf, size_t cap) : buf_(buf), cap_(cap) {
buf_[0] = '\0';
}
void begin_object() { value_comma(); put('{'); push('{'); }
void end_object() { pop(); put('}'); }
void begin_array() { value_comma(); put('['); push('['); }
void end_array() { pop(); put(']'); }
// Ключ объекта; за ним должно следовать значение.
void key(const char* name) {
if (items_[depth_]) put(',');
items_[depth_] = true;
put('"');
str_escape(name);
put_raw("\":", 2);
await_value_ = true;
}
void string(const char* v) {
value_comma();
put('"');
str_escape(v);
put('"');
}
void integer(int64_t v) {
value_comma();
char tmp[24];
int n = snprintf(tmp, sizeof(tmp), "%lld", static_cast<long long>(v));
put_raw(tmp, static_cast<size_t>(n));
}
void boolean(bool v) { value_comma(); put_raw(v ? "true" : "false", v ? 4 : 5); }
void literal(const char* v) { value_comma(); put_raw(v, strlen(v)); }
void raw_json(const char* v) { value_comma(); put_raw(v, strlen(v)); }
const char* c_str() const { return buf_; }
size_t size() const { return pos_; }
bool ok() const { return ok_; }
private:
static constexpr int kMaxDepth = 8;
// Запятая/маркер перед ЗНАЧЕНИЕМ (не ключом, не закрытием).
void value_comma() {
if (await_value_) {
await_value_ = false;
return; // ключ уже поставил ':' — запятая не нужна
}
if (items_[depth_]) put(',');
items_[depth_] = true;
}
void push(char c) {
if (depth_ + 1 >= kMaxDepth) {
ok_ = false;
return;
}
depth_++;
items_[depth_] = false;
(void)c;
}
void pop() {
if (depth_ > 0) depth_--;
await_value_ = false;
}
void put(char c) {
if (!ok_ || pos_ + 1 >= cap_) {
ok_ = false;
return;
}
buf_[pos_++] = c;
buf_[pos_] = '\0';
}
void put_raw(const char* s, size_t n) {
if (!ok_ || pos_ + n + 1 > cap_) {
ok_ = false;
return;
}
memcpy(buf_ + pos_, s, n);
pos_ += n;
buf_[pos_] = '\0';
}
void str_escape(const char* s) {
for (const char* p = s; *p != '\0'; p++) {
char c = *p;
if (c == '"' || c == '\\') {
put('\\');
put(c);
} else if (c == '\n') {
put_raw("\\n", 2);
} else if (c == '\t') {
put_raw("\\t", 2);
} else if (c == '\r') {
put_raw("\\r", 2);
} else if (c == '\b') {
put_raw("\\b", 2);
} else if (c == '\f') {
put_raw("\\f", 2);
} else if (static_cast<unsigned char>(c) < 0x20) {
char tmp[8];
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
put_raw(tmp, 6);
} else {
put(c);
}
}
}
char* buf_;
size_t cap_;
size_t pos_ = 0;
bool ok_ = true;
int depth_ = 0;
bool items_[kMaxDepth + 1] = {};
bool await_value_ = false;
};
// ---------------------------------------------------------------------------
// Parser (jsmn): токены поверх исходного буфера, без копирований.
// ---------------------------------------------------------------------------
constexpr int kMaxTokens = 64;
class Doc {
public:
// Разбирает buf (NUL-terminated, будет модифицироваться при unescape).
// После вызова buf должен жить, пока используется Doc.
bool parse(char* buf) {
buf_ = buf;
jsmn_init(&p_);
count_ = jsmn_parse(&p_, buf, strlen(buf), tokens_, kMaxTokens);
return count_ >= 1 && tokens_[0].type == JSMN_OBJECT;
}
// Значение ключа по имени (первое вхождение в порядке документа —
// глубина не различается; для форматов протокора ключи уникальны).
// Для строки возвращает указатель внутрь buf_ (БЕЗ кавычек,
// НЕ NUL-terminated) и длину.
bool find(const char* name, const char** out_start, size_t* out_len,
jsmntype_t* out_type = nullptr) const {
for (int i = 1; i + 1 < count_; i++) {
if (tokens_[i].type == JSMN_STRING && tokens_[i].size == 1) {
int len = tokens_[i].end - tokens_[i].start;
if (len == static_cast<int>(strlen(name)) &&
strncmp(buf_ + tokens_[i].start, name, len) == 0) {
const jsmntok_t& v = tokens_[i + 1];
*out_start = buf_ + v.start;
*out_len = static_cast<size_t>(v.end - v.start);
if (out_type != nullptr) *out_type = v.type;
return true;
}
}
}
return false;
}
// Строка в out (снятие кавычек; поддержка \" \\ \/ \n \t \r \b \f).
bool get_string(const char* name, char* out, size_t out_cap) const {
const char* s;
size_t len;
jsmntype_t type;
if (!find(name, &s, &len, &type) || type != JSMN_STRING) return false;
size_t o = 0;
for (size_t i = 0; i < len; i++) {
if (o + 1 >= out_cap) return false;
char c = s[i];
if (c == '\\' && i + 1 < len) {
char e = s[++i];
switch (e) {
case '"': out[o++] = '"'; break;
case '\\': out[o++] = '\\'; break;
case '/': out[o++] = '/'; break;
case 'n': out[o++] = '\n'; break;
case 't': out[o++] = '\t'; break;
case 'r': out[o++] = '\r'; break;
case 'b': out[o++] = '\b'; break;
case 'f': out[o++] = '\f'; break;
default: return false; // \uXXXX не требуется протоколом
}
} else {
out[o++] = c;
}
}
out[o] = '\0';
return true;
}
bool get_int(const char* name, int64_t* out) const {
const char* s;
size_t len;
jsmntype_t type;
if (!find(name, &s, &len, &type) || type != JSMN_PRIMITIVE) return false;
int64_t v = 0;
bool neg = false;
size_t i = 0;
if (i < len && (s[i] == '-' || s[i] == '+')) {
neg = s[i] == '-';
i++;
}
if (i >= len) return false;
for (; i < len; i++) {
if (s[i] < '0' || s[i] > '9') return false;
int64_t digit = s[i] - '0';
if (v > (INT64_MAX - digit) / 10) return false; // переполнение
v = v * 10 + digit;
}
*out = neg ? -v : v;
return true;
}
bool get_bool(const char* name, bool* out) const {
const char* s;
size_t len;
jsmntype_t type;
if (!find(name, &s, &len, &type) || type != JSMN_PRIMITIVE) return false;
if (len == 4 && strncmp(s, "true", 4) == 0) {
*out = true;
return true;
}
if (len == 5 && strncmp(s, "false", 5) == 0) {
*out = false;
return true;
}
return false;
}
int tokens_used() const { return count_; }
private:
char* buf_ = nullptr;
jsmn_parser p_;
jsmntok_t tokens_[kMaxTokens];
int count_ = 0;
};
} // namespace fgl::ayla::json