// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see .
#include "Game.h"
extern "C" {
#include
}
#include
#include
#include
#include
namespace Encoding::CBOR {
namespace {
constexpr int kMaxDepth = 64;
// ---------------- encode ----------------
// tinycbor's grow-and-retry contract: on buffer overflow the encoder
// switches to counting mode (end = NULL, accumulates bytes_needed) and
// encoding must CONTINUE so the final tally is complete — a nested
// container's tally only reaches the parent via close_container. So
// CborErrorOutOfMemory is not a failure here; the caller reads
// cbor_encoder_get_extra_bytes_needed() afterwards, resizes, retries.
bool EncOk(CborError err) {
return err == CborNoError || err == CborErrorOutOfMemory;
}
bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth);
bool IsLuaArray(void *L, int idx, int &outLen) {
outLen = 0;
int seen = 0, maxIdx = 0;
Game::Lua::PushNil(L);
while (Game::Lua::Next(L, idx) != 0) {
++seen;
if (Game::Lua::Type(L, -2) != Game::Lua::TYPE_NUMBER) {
Game::Lua::SetTop(L, -3);
return false;
}
const double k = Game::Lua::ToNumber(L, -2);
const int ki = static_cast(k);
if (ki < 1 || static_cast(ki) != k) {
Game::Lua::SetTop(L, -3);
return false;
}
if (ki > maxIdx) maxIdx = ki;
Game::Lua::SetTop(L, -2);
}
if (seen != maxIdx) return false;
outLen = seen;
return true;
}
int CountTablePairs(void *L, int idx) {
int n = 0;
Game::Lua::PushNil(L);
while (Game::Lua::Next(L, idx) != 0) {
++n;
Game::Lua::SetTop(L, -2);
}
return n;
}
bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
if (depth >= kMaxDepth) return false;
if (idx < 0) idx = Game::Lua::GetTop(L) + idx + 1;
int arrayLen = 0;
if (IsLuaArray(L, idx, arrayLen)) {
CborEncoder arr;
if (!EncOk(cbor_encoder_create_array(enc, &arr,
static_cast(arrayLen))))
return false;
for (int i = 1; i <= arrayLen; ++i) {
Game::Lua::PushNumber(L, static_cast(i));
Game::Lua::RawGet(L, idx);
if (!EncodeValue(L, -1, &arr, depth + 1)) {
Game::Lua::SetTop(L, -2);
return false;
}
Game::Lua::SetTop(L, -2);
}
return EncOk(cbor_encoder_close_container(enc, &arr));
}
const int pairs = CountTablePairs(L, idx);
CborEncoder map;
if (!EncOk(cbor_encoder_create_map(enc, &map, static_cast(pairs))))
return false;
Game::Lua::PushNil(L);
while (Game::Lua::Next(L, idx) != 0) {
if (!EncodeValue(L, -2, &map, depth + 1) ||
!EncodeValue(L, -1, &map, depth + 1)) {
Game::Lua::SetTop(L, -3);
return false;
}
Game::Lua::SetTop(L, -2);
}
return EncOk(cbor_encoder_close_container(enc, &map));
}
bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth) {
switch (Game::Lua::Type(L, idx)) {
case Game::Lua::TYPE_NIL:
return EncOk(cbor_encode_null(enc));
case Game::Lua::TYPE_BOOLEAN:
return EncOk(cbor_encode_boolean(enc,
Game::Lua::ToBoolean(L, idx) != 0));
case Game::Lua::TYPE_NUMBER: {
const double n = Game::Lua::ToNumber(L, idx);
// Whole numbers in [-2^53, 2^53] go out as integers so
// `42` doesn't round-trip as `42.0`.
if (std::isfinite(n) && n == std::floor(n) &&
n >= -9007199254740992.0 && n <= 9007199254740992.0) {
if (n >= 0.0)
return EncOk(cbor_encode_uint(enc,
static_cast(n)));
return EncOk(cbor_encode_negative_int(enc,
static_cast(-n)));
}
return EncOk(cbor_encode_double(enc, n));
}
case Game::Lua::TYPE_STRING: {
const char *s = Game::Lua::ToString(L, idx);
const unsigned int n = Game::Lua::StrLen(L, idx);
return EncOk(cbor_encode_text_string(enc, s, n));
}
case Game::Lua::TYPE_TABLE:
return EncodeTable(L, idx, enc, depth);
default:
return false;
}
}
int __fastcall Script_SerializeCBOR(void *L) {
if (Game::Lua::GetTop(L) < 1) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.SerializeCBOR(value)");
return 0;
}
// tinycbor encodes into a fixed-size buffer; on overflow it keeps
// encoding in counting mode (EncOk treats OutOfMemory as success) so
// the extra-bytes tally is complete, then we resize and retry once.
std::vector buf(256);
for (;;) {
CborEncoder enc;
cbor_encoder_init(&enc, buf.data(), buf.size(), 0);
if (!EncodeValue(L, 1, &enc, 0))
return 0; // genuine "value not representable" failure
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
if (extra > 0) {
buf.resize(buf.size() + extra);
continue;
}
const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data());
Game::Lua::PushLString(L, reinterpret_cast(buf.data()),
static_cast(used));
return 1;
}
}
// ---------------- decode ----------------
bool DecodeValue(void *L, CborValue *it, int depth);
bool DecodeContainer(void *L, CborValue *it, int depth, bool isMap) {
if (depth >= kMaxDepth) return false;
CborValue child;
if (cbor_value_enter_container(it, &child) != CborNoError)
return false;
Game::Lua::NewTable(L);
int arrayIdx = 1;
while (!cbor_value_at_end(&child)) {
if (isMap) {
// Read key.
if (!DecodeValue(L, &child, depth + 1))
return false;
// Read value.
if (!DecodeValue(L, &child, depth + 1))
return false;
Game::Lua::RawSet(L, -3);
} else {
if (!DecodeValue(L, &child, depth + 1))
return false;
Game::Lua::PushNumber(L, static_cast(arrayIdx++));
Game::Lua::Insert(L, -2);
Game::Lua::RawSet(L, -3);
}
}
return cbor_value_leave_container(it, &child) == CborNoError;
}
bool DecodeValue(void *L, CborValue *it, int depth) {
const CborType t = cbor_value_get_type(it);
switch (t) {
case CborIntegerType: {
// Unsigned or negative — tinycbor distinguishes via the
// raw value, but `get_int64_checked` clamps and signals
// overflow. For our Lua-number target we want a double
// anyway, so go via uint64/raw and convert.
uint64_t raw;
if (cbor_value_get_raw_integer(it, &raw) != CborNoError)
return false;
double n;
if (cbor_value_is_negative_integer(it))
n = -1.0 - static_cast(raw);
else
n = static_cast(raw);
Game::Lua::PushNumber(L, n);
break;
}
case CborBooleanType: {
bool b;
if (cbor_value_get_boolean(it, &b) != CborNoError)
return false;
Game::Lua::PushBool(L, b);
break;
}
case CborNullType:
case CborUndefinedType:
Game::Lua::PushNil(L);
break;
case CborFloatType: {
float f;
if (cbor_value_get_float(it, &f) != CborNoError)
return false;
Game::Lua::PushNumber(L, static_cast(f));
break;
}
case CborDoubleType: {
double d;
if (cbor_value_get_double(it, &d) != CborNoError)
return false;
Game::Lua::PushNumber(L, d);
break;
}
case CborHalfFloatType: {
float f;
if (cbor_value_get_half_float_as_float(it, &f) != CborNoError)
return false;
Game::Lua::PushNumber(L, static_cast(f));
break;
}
case CborTextStringType:
case CborByteStringType: {
char *s = nullptr;
size_t n = 0;
CborError err = (t == CborTextStringType)
? cbor_value_dup_text_string(it, &s, &n, it)
: cbor_value_dup_byte_string(it, reinterpret_cast(&s),
&n, it);
if (err != CborNoError) {
std::free(s);
return false;
}
Game::Lua::PushLString(L, s, static_cast(n));
std::free(s);
// dup_*_string advances `it` past the string, so skip the
// bottom-of-function advance.
return true;
}
case CborArrayType:
return DecodeContainer(L, it, depth, /*isMap=*/false);
case CborMapType:
return DecodeContainer(L, it, depth, /*isMap=*/true);
case CborTagType: {
// Skip the tag, decode the contained value.
if (cbor_value_advance_fixed(it) != CborNoError)
return false;
return DecodeValue(L, it, depth);
}
default:
return false;
}
return cbor_value_advance_fixed(it) == CborNoError;
}
int __fastcall Script_DeserializeCBOR(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.DeserializeCBOR(data)");
return 0;
}
const char *src = Game::Lua::ToString(L, 1);
const unsigned int srcLen = Game::Lua::StrLen(L, 1);
if (src == nullptr)
return 0;
CborParser parser;
CborValue it;
if (cbor_parser_init(reinterpret_cast(src), srcLen, 0,
&parser, &it) != CborNoError)
return 0;
if (!DecodeValue(L, &it, 0))
return 0;
return 1;
}
void Register() {
Game::Lua::RegisterTableFunction("C_EncodingUtil", "SerializeCBOR",
&Script_SerializeCBOR);
Game::Lua::RegisterTableFunction("C_EncodingUtil", "DeserializeCBOR",
&Script_DeserializeCBOR);
}
} // namespace
static const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace Encoding::CBOR