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:
2026-09-22 15:45:20 +03:00
parent b44004627b
commit e91d4605b1
19 changed files with 2120 additions and 3 deletions

137
src/ayla/crypto.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include "ayla/crypto.hpp"
#include <cstdio>
#include <cstring>
#include "mbedtls/platform_util.h"
namespace fgl::ayla {
namespace {
void hmac_sha256(const uint8_t* key, size_t key_len, const uint8_t* data,
size_t data_len, uint8_t out[kSha256Len]) {
const mbedtls_md_info_t* md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_hmac(md, key, key_len, data, data_len, out);
}
} // namespace
bool DirectionCrypto::derive(const uint8_t* key, size_t key_len,
const uint8_t* msg, size_t msg_len,
uint8_t out[kSha256Len]) {
// key = HMAC_k( HMAC_k(msg) || msg )
if (msg_len > sizeof(uint8_t) * (kSha256Len + 192) - kSha256Len) {
return false; // невозможно по протоколу (<=73 байт)
}
uint8_t inner[kSha256Len];
hmac_sha256(key, key_len, msg, msg_len, inner);
uint8_t buf[kSha256Len + 192];
memcpy(buf, inner, kSha256Len);
memcpy(buf + kSha256Len, msg, msg_len);
hmac_sha256(key, key_len, buf, kSha256Len + msg_len, out);
return true;
}
bool DirectionCrypto::init(const char* lanip_key, const char* rnd_a,
const char* rnd_b, int64_t t_a, int64_t t_b) {
if (lanip_key == nullptr || rnd_a == nullptr || rnd_b == nullptr) return false;
initialized_ = false;
mode_ = 0;
// затираем предыдущий материал
mbedtls_platform_zeroize(sign_key_, sizeof(sign_key_));
mbedtls_platform_zeroize(aes_key_, sizeof(aes_key_));
mbedtls_platform_zeroize(chain_iv_, sizeof(chain_iv_));
uint8_t msg[128];
size_t off = 0;
auto append = [&](const char* s) {
size_t n = strlen(s);
if (off + n > sizeof(msg)) return false;
memcpy(msg + off, s, n);
off += n;
return true;
};
char t_a_buf[24], t_b_buf[24];
snprintf(t_a_buf, sizeof(t_a_buf), "%lld", static_cast<long long>(t_a));
snprintf(t_b_buf, sizeof(t_b_buf), "%lld", static_cast<long long>(t_b));
if (!append(rnd_a) || !append(rnd_b) || !append(t_a_buf) || !append(t_b_buf) ||
off + 1 > sizeof(msg)) {
return false;
}
const uint8_t* k = reinterpret_cast<const uint8_t*>(lanip_key);
size_t klen = strlen(lanip_key);
uint8_t block[kSha256Len];
msg[off] = 0x30; // sign key
if (!derive(k, klen, msg, off + 1, block)) return false;
memcpy(sign_key_, block, kHmacKeyLen);
msg[off] = 0x31; // aes key
if (!derive(k, klen, msg, off + 1, block)) return false;
memcpy(aes_key_, block, kAesKeyLen);
msg[off] = 0x32; // iv seed (первые 16 байт)
if (!derive(k, klen, msg, off + 1, block)) return false;
memcpy(chain_iv_, block, kAesBlockLen);
mbedtls_aes_init(&aes_enc_);
mbedtls_aes_init(&aes_dec_);
if (mbedtls_aes_setkey_enc(&aes_enc_, aes_key_, 256) != 0) return false;
if (mbedtls_aes_setkey_dec(&aes_dec_, aes_key_, 256) != 0) return false;
initialized_ = true;
return true;
}
long DirectionCrypto::encrypt(uint8_t* out, size_t out_cap, const uint8_t* in,
size_t in_len) {
if (!initialized_) return -1;
if (mode_ == 0) mode_ = 1;
if (mode_ != 1) return -1; // направление уже в режиме расшифровки
// Java-паддинг: >=1 NUL, кратно 16.
size_t padded = ((in_len + 1 + kAesBlockLen - 1) / kAesBlockLen) * kAesBlockLen;
if (padded > out_cap) return -1;
uint8_t buf[kAesBlockLen * 128]; // до 2048 байт (kHttpdMaxBody)
if (padded > sizeof(buf)) return -1;
memcpy(buf, in, in_len);
memset(buf + in_len, 0, padded - in_len);
// mbedtls_aes_crypt_cbc использует и обновляет chain_iv_ на месте —
// это и есть состояние цепочки.
if (mbedtls_aes_crypt_cbc(&aes_enc_, MBEDTLS_AES_ENCRYPT, padded, chain_iv_,
buf, out) != 0) {
return -1;
}
return static_cast<long>(padded);
}
long DirectionCrypto::decrypt(uint8_t* out, size_t out_cap, const uint8_t* in,
size_t in_len) {
if (!initialized_) return -1;
if (mode_ == 0) mode_ = 2;
if (mode_ != 2) return -1; // направление уже в режиме шифрования
if (in_len == 0 || in_len % kAesBlockLen != 0 || in_len > out_cap) return -1;
if (mbedtls_aes_crypt_cbc(&aes_dec_, MBEDTLS_AES_DECRYPT, in_len, chain_iv_,
in, out) != 0) {
return -1;
}
// Снять завершающие NUL (по крайней мере один в Java-паддинге, но
// принимаем и строго-кратные длины без NUL).
size_t len = in_len;
while (len > 0 && out[len - 1] == 0) len--;
return static_cast<long>(len);
}
void DirectionCrypto::sign(uint8_t out[kSha256Len], const uint8_t* data,
size_t len) const {
hmac_sha256(sign_key_, kHmacKeyLen, data, len, out);
}
bool SessionCrypto::init(const char* lanip_key, const char* random_1,
const char* random_2, int64_t time_1, int64_t time_2) {
return app.init(lanip_key, random_1, random_2, time_1, time_2) &&
dev.init(lanip_key, random_2, random_1, time_2, time_1);
}
} // namespace fgl::ayla

70
src/ayla/crypto.hpp Normal file
View File

@@ -0,0 +1,70 @@
// Сессионная криптография Ayla LAN (docs/PROTOCOL.md §3).
// KDF: key = HMAC-SHA256_k(HMAC-SHA256_k(msg) || msg), msg = rnd_a|rnd_b|t_a|t_b|X,
// X: 0x30 sign / 0x31 crypto / 0x32 iv(первые 16 байт).
// Направления: app (rnd1,rnd2,t1,t2) — исходящие; dev (rnd2,rnd1,t2,t1) — входящие.
// ВАЖНО: AES-CBC-цепочка непрерывна в рамках сессии (каждое сообщение
// продолжает цепочку с последнего шифроблока предыдущего).
#pragma once
#include <cstddef>
#include <cstdint>
#include "mbedtls/aes.h"
#include "mbedtls/md.h"
namespace fgl::ayla {
constexpr size_t kSha256Len = 32;
constexpr size_t kAesKeyLen = 32; // AES-256
constexpr size_t kAesBlockLen = 16;
constexpr size_t kHmacKeyLen = 32;
class DirectionCrypto {
public:
// Выводит ключи направления. rnd_a/rnd_b — 16-символьные токены,
// t_a/t_b — 64-битные «времена» (материал KDF, значение произвольно).
// app: (random_1, random_2, time_1, time_2); dev: (random_2, random_1, time_2, time_1).
bool init(const char* lanip_key, const char* rnd_a, const char* rnd_b,
int64_t t_a, int64_t t_b);
// Шифрует in (любая длина) с zero-паддингом (>=1 NUL, кратно 16).
// out_cap должен быть >= padded_len = ((in_len+1)/16+1)*16.
// Возвращает длину шифротекста или -1. Продолжает CBC-цепочку.
// ВАЖНО: у направления ОДНА цепочка; первый вызов фиксирует режим
// (шифрование либо расшифровка) — смешение режимов возвращает -1.
long encrypt(uint8_t* out, size_t out_cap, const uint8_t* in, size_t in_len);
// Расшифровывает in (длина кратна 16), снимает завершающие NUL.
// Возвращает длину открытого текста или -1. Продолжает CBC-цепочку
// (даже если далее подпись не сойдётся — состояние обязано двигаться).
long decrypt(uint8_t* out, size_t out_cap, const uint8_t* in, size_t in_len);
// HMAC-SHA256(sign_key, data) — подпись/проверка сообщений направления.
void sign(uint8_t out[kSha256Len], const uint8_t* data, size_t len) const;
bool initialized() const { return initialized_; }
private:
static bool derive(const uint8_t* key, size_t key_len, const uint8_t* msg,
size_t msg_len, uint8_t out[kSha256Len]);
bool initialized_ = false;
uint8_t mode_ = 0; // 0 не зафиксирован, 1 шифрование, 2 расшифровка
uint8_t sign_key_[kHmacKeyLen];
uint8_t aes_key_[kAesKeyLen];
uint8_t chain_iv_[kAesBlockLen]; // текущее состояние CBC-цепочки
mbedtls_aes_context aes_enc_;
mbedtls_aes_context aes_dec_;
};
// Пара направлений одной сессии.
struct SessionCrypto {
DirectionCrypto app; // приложение -> модуль
DirectionCrypto dev; // модуль -> приложение
// lanip_key — строка base64 КАК ЕСТЬ (utf-8 байты, не декодировать).
bool init(const char* lanip_key, const char* random_1, const char* random_2,
int64_t time_1, int64_t time_2);
};
} // namespace fgl::ayla

180
src/ayla/envelope.cpp Normal file
View File

@@ -0,0 +1,180 @@
#include "ayla/envelope.hpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "mbedtls/base64.h"
namespace fgl::ayla {
namespace {
bool b64_encode(const uint8_t* in, size_t in_len, char* out, size_t out_cap) {
size_t olen = 0;
if (mbedtls_base64_encode(reinterpret_cast<unsigned char*>(out), out_cap,
&olen, in, in_len) != 0) {
return false;
}
return true; // mbedtls пишет NUL-терминатор
}
bool b64_decode(const char* in, uint8_t* out, size_t out_cap, size_t* out_len) {
size_t olen = 0;
if (mbedtls_base64_decode(out, out_cap, &olen,
reinterpret_cast<const unsigned char*>(in),
strlen(in)) != 0) {
return false;
}
*out_len = olen;
return true;
}
// Извлекает seq_no верхнего уровня из '{"seq_no":N,"data":...}'.
// Ручной depth-1 сканер (без ограничений на размер data; вход уже
// проверен подписью). Понимает пробелы между лексемами.
bool extract_seq_no(const char* json, int64_t* seq_no) {
const char* p = json;
while (*p == ' ' || *p == '\t') p++;
if (*p != '{') return false;
p++;
for (;;) {
while (*p == ' ' || *p == ',' || *p == '\t') p++;
if (*p == '}' || *p == '\0') return false;
// ключ
if (*p != '"') return false;
const char* k = ++p;
while (*p != '"' && *p != '\0') p++;
if (*p != '"') return false;
size_t klen = static_cast<size_t>(p - k);
p++;
while (*p == ' ' || *p == '\t') p++;
if (*p != ':') return false;
p++;
while (*p == ' ' || *p == '\t') p++;
if (klen == 6 && strncmp(k, "seq_no", 6) == 0) {
if (*p == '-' || (*p >= '0' && *p <= '9')) {
*seq_no = strtoll(p, nullptr, 10);
return true;
}
return false;
}
// пропустить значение другого ключа
if (*p == '{' || *p == '[') {
int depth = 0;
bool in_str = false;
for (; *p != '\0'; p++) {
if (in_str) {
if (*p == '\\') {
p++;
if (*p == '\0') return false; // обрыв после escape
} else if (*p == '"') {
in_str = false;
}
continue;
}
if (*p == '"') {
in_str = true;
} else if (*p == '{' || *p == '[') {
depth++;
} else if (*p == '}' || *p == ']') {
depth--;
if (depth == 0) {
p++;
break;
}
}
}
if (*p == '\0') return false;
} else if (*p == '"') {
p++;
while (*p != '"' && *p != '\0') {
if (*p == '\\') {
p++;
if (*p == '\0') return false; // обрыв после escape
}
p++;
}
if (*p != '"') return false;
p++;
} else {
while (*p != ',' && *p != '}' && *p != '\0') p++;
if (*p == '\0') return false;
}
}
}
// Сравнение за константное время (аналог mbedtls_ct_memcmp, без зависимости
// от версии библиотеки).
int ct_memcmp(const uint8_t* a, const uint8_t* b, size_t n) {
uint8_t diff = 0;
for (size_t i = 0; i < n; i++) diff |= static_cast<uint8_t>(a[i] ^ b[i]);
return diff != 0 ? 1 : 0;
}
} // namespace
bool envelope_pack(DirectionCrypto& dir, int64_t seq_no,
const char* data_json, char* out, size_t out_cap) {
if (data_json == nullptr || out == nullptr) return false;
// Открытый текст: {"seq_no":N,"data":<data_json>}
char plain[kEnvelopeMaxPlain];
int n = snprintf(plain, sizeof(plain), "{\"seq_no\":%lld,\"data\":%s}",
static_cast<long long>(seq_no), data_json);
if (n <= 0 || static_cast<size_t>(n) >= sizeof(plain)) return false;
size_t plain_len = static_cast<size_t>(n);
// Подпись — по байтам открытого текста без паддинга.
uint8_t mac[kSha256Len];
dir.sign(mac, reinterpret_cast<const uint8_t*>(plain), plain_len);
char sign_b64[64];
if (!b64_encode(mac, sizeof(mac), sign_b64, sizeof(sign_b64))) return false;
// Шифрование с zero-паддингом (>=1 NUL, кратно 16).
uint8_t ct[kEnvelopeMaxPlain + kAesBlockLen];
long ct_len = dir.encrypt(ct, sizeof(ct),
reinterpret_cast<const uint8_t*>(plain), plain_len);
if (ct_len < 0) return false;
char enc_b64[kEnvelopeMaxB64];
if (!b64_encode(ct, static_cast<size_t>(ct_len), enc_b64, sizeof(enc_b64))) {
return false;
}
int m = snprintf(out, out_cap, "{\"enc\":\"%s\",\"sign\":\"%s\"}", enc_b64,
sign_b64);
return m > 0 && static_cast<size_t>(m) < out_cap;
}
bool envelope_unpack(DirectionCrypto& dir, const char* enc_b64,
const char* sign_b64, char* json_out, size_t out_cap,
int64_t* seq_no) {
if (enc_b64 == nullptr || sign_b64 == nullptr || json_out == nullptr) {
return false;
}
uint8_t ct[kEnvelopeMaxPlain + kAesBlockLen];
size_t ct_len = 0;
if (!b64_decode(enc_b64, ct, sizeof(ct), &ct_len)) return false;
// Расшифровка: CBC-цепочка двигается независимо от исхода проверки подписи.
uint8_t plain[kEnvelopeMaxPlain + kAesBlockLen];
long plain_len = dir.decrypt(plain, sizeof(plain), ct, ct_len);
if (plain_len < 0 || static_cast<size_t>(plain_len) >= out_cap) return false;
// Проверка подписи по расшифрованным байтам (без паддинга);
// сравнение декодированных байт — константное время.
uint8_t mac[kSha256Len];
dir.sign(mac, plain, static_cast<size_t>(plain_len));
uint8_t given[kSha256Len];
size_t given_len = 0;
if (!b64_decode(sign_b64, given, sizeof(given), &given_len)) return false;
if (given_len != kSha256Len) return false;
if (ct_memcmp(mac, given, kSha256Len) != 0) return false;
memcpy(json_out, plain, static_cast<size_t>(plain_len));
json_out[plain_len] = '\0';
return extract_seq_no(json_out, seq_no);
}
} // namespace fgl::ayla

31
src/ayla/envelope.hpp Normal file
View File

@@ -0,0 +1,31 @@
// Защищённый конверт Ayla LAN: {"enc":"<b64 AES-CBC>","sign":"<b64 HMAC>"},
// открытый текст: {"seq_no":N,"data":<json-объект>} (docs/PROTOCOL.md §3.3).
#pragma once
#include <cstddef>
#include <cstdint>
#include "ayla/crypto.hpp"
namespace fgl::ayla {
constexpr size_t kEnvelopeMaxPlain = 1024; // открытый текст конверта
constexpr size_t kEnvelopeMaxB64 = 1800; // base64 шифротекста с запасом
// Собирает открытый текст '{"seq_no":<seq>,"data":<data_json>}' и упаковывает
// в конверт (envelope). dir — направление ОТПРАВИТЕЛЯ (app); состояние
// CBC-цепочки направления ДВИГАЕТСЯ. out — строка NUL-terminated.
// data_json — готовый JSON-объект (обычно построен json::Writer).
bool envelope_pack(DirectionCrypto& dir, int64_t seq_no,
const char* data_json, char* out, size_t out_cap);
// Распаковывает конверт (enc_b64/sign_b64 — NUL-terminated) направлением dev:
// расшифровывает (цепочка движется даже при последующей ошибке подписи!),
// проверяет подпись. При успехе json_out содержит ОТКРЫТЫЙ ТЕКСТ
// ('{"seq_no":..,"data":..}') NUL-terminated, seq_no записан в *seq_no.
// Возвращает false при ошибке base64/расшифровки/подписи.
bool envelope_unpack(DirectionCrypto& dir, const char* enc_b64,
const char* sign_b64, char* json_out, size_t out_cap,
int64_t* seq_no);
} // namespace fgl::ayla

128
src/ayla/httpc.cpp Normal file
View File

@@ -0,0 +1,128 @@
#include "ayla/httpc.hpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "ayla/log.hpp"
#include "ayla/platform/platform.hpp"
namespace fgl::ayla {
namespace {
// Читает статус-строку "HTTP/1.x NNN ..."; возвращает код или -1.
int read_status(int fd) {
char line[64];
size_t len = 0;
for (;;) {
if (len + 1 >= sizeof(line)) return -1;
uint8_t ch;
long n = fgl::plat::tcp_recv(fd, &ch, 1);
if (n <= 0) return -1;
if (ch == '\n') break;
line[len++] = static_cast<char>(ch);
}
// "HTTP/1.1 202 Accepted"
if (len < 12 || strncmp(line, "HTTP/", 5) != 0) return -1;
const char* sp = strchr(line, ' ');
if (sp == nullptr) return -1;
int code = atoi(sp + 1);
return (code >= 200 && code <= 599) ? code : -1;
}
// Дочитывает заголовки до пустой строки и тело по Content-Length (до лимита).
bool drain_response(int fd) {
char line[256];
long content_length = 0;
for (;;) {
size_t i = 0;
for (;;) {
if (i + 1 >= sizeof(line)) return false;
uint8_t ch;
long n = fgl::plat::tcp_recv(fd, &ch, 1);
if (n <= 0) return false;
if (ch == '\n') break;
line[i++] = static_cast<char>(ch);
}
if (i > 0 && line[i - 1] == '\r') i--;
line[i] = '\0';
if (i == 0) break; // конец заголовков
if ((line[0] | 0x20) == 'c' && strncasecmp(line, "Content-Length:", 15) == 0) {
content_length = strtol(line + 15, nullptr, 10);
}
}
if (content_length > 0) {
uint8_t sink[256];
long remaining = content_length;
if (remaining > 4096) remaining = 4096; // тела local_reg нет — ограничим
while (remaining > 0) {
size_t chunk = remaining < static_cast<long>(sizeof(sink))
? static_cast<size_t>(remaining)
: sizeof(sink);
long n = fgl::plat::tcp_recv(fd, sink, chunk);
if (n <= 0) break;
remaining -= n;
}
}
return true;
}
} // namespace
bool httpc_perform(const HttpcRequest& req, HttpcResponse* resp) {
if (resp == nullptr || req.host == nullptr) return false;
resp->status = 0;
resp->transport_ok = false;
int fd = fgl::plat::tcp_connect(req.host, req.port, req.timeout_ms);
if (fd < 0) {
FGL_LOGD("httpc: connect %s:%u failed", req.host,
static_cast<unsigned>(req.port));
return false;
}
fgl::plat::tcp_set_timeout(fd, req.timeout_ms, req.timeout_ms);
char head[256];
int n = snprintf(head, sizeof(head),
"%s %s%s%s HTTP/1.1\r\n"
"Host: %s\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"Content-Type: %s\r\n"
"Content-Length: %u\r\n"
"\r\n",
req.method, req.path, req.query != nullptr ? "?" : "",
req.query != nullptr ? req.query : "", req.host,
req.content_type, static_cast<unsigned>(req.body_len));
if (n <= 0 || static_cast<size_t>(n) >= sizeof(head)) {
fgl::plat::tcp_close(fd);
return false;
}
bool sent = fgl::plat::tcp_send(fd, head, static_cast<size_t>(n)) == n;
if (sent && req.body != nullptr && req.body_len > 0) {
sent = fgl::plat::tcp_send(fd, req.body, req.body_len) ==
static_cast<long>(req.body_len);
}
if (!sent) {
FGL_LOGD("httpc: send failed");
fgl::plat::tcp_close(fd);
return false;
}
int status = read_status(fd);
if (status < 0) {
fgl::plat::tcp_close(fd);
return false;
}
drain_response(fd);
fgl::plat::tcp_shutdown(fd);
fgl::plat::tcp_close(fd);
resp->status = status;
resp->transport_ok = true;
FGL_LOGD("httpc: %s %s -> %d", req.method, req.path, status);
return true;
}
} // namespace fgl::ayla

32
src/ayla/httpc.hpp Normal file
View File

@@ -0,0 +1,32 @@
// Минимальный HTTP-клиент для исходящих запросов к модулю (local_reg).
// Один запрос — одно соединение; достаточно для keep-alive-цикла протокола
// (проверено на приборе: модуль принимает local_reg на новых соединениях).
#pragma once
#include <cstddef>
#include <cstdint>
namespace fgl::ayla {
struct HttpcRequest {
const char* method = "PUT"; // "POST" | "PUT"
const char* host = nullptr; // dotted-quad или DNS-имя
uint16_t port = 80;
const char* path = "/local_reg.json";
const char* query = nullptr; // без '?', может быть nullptr
const char* content_type = "application/json";
const uint8_t* body = nullptr;
size_t body_len = 0;
uint32_t timeout_ms = 10000;
};
struct HttpcResponse {
int status = 0; // HTTP-код или 0 при ошибке транспорта
bool transport_ok = false;
};
// Выполняет запрос блокирующе. Тело ответа не читается (у local_reg оно
// пусто), но соединение закрывается корректно (shutdown перед close).
bool httpc_perform(const HttpcRequest& req, HttpcResponse* resp);
} // namespace fgl::ayla

253
src/ayla/json.hpp Normal file
View File

@@ -0,0 +1,253 @@
// Минимальный 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