// Минимальный JSON: writer с фиксированным буфером + парсер (jsmn). // Никаких аллокаций; экранирование строк — достаточное для протокола. #pragma once #include #include #include #include #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(v)); put_raw(tmp, static_cast(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(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(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(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