EncodingUtil

This commit is contained in:
Brues
2026-05-15 23:12:16 -05:00
parent 52fc53a08c
commit 2cc6cfda86
14 changed files with 986 additions and 3 deletions
+1
View File
@@ -1,6 +1,7 @@
/build
/external
/debug.log
/_stash
# Prerequisites
*.d
+6
View File
@@ -1,3 +1,9 @@
[submodule "external/minhook"]
path = external/minhook
url = https://github.com/TsudaKageyu/minhook.git
[submodule "external/picojson"]
path = external/picojson
url = https://github.com/kazuho/picojson.git
[submodule "external/tinycbor"]
path = external/tinycbor
url = https://github.com/intel/tinycbor.git
+13 -2
View File
@@ -11,7 +11,16 @@ add_subdirectory(external/minhook)
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS "src/*.h")
add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS})
set(TINYCBOR_SOURCES
external/tinycbor/src/cborencoder.c
external/tinycbor/src/cborencoder_close_container_checked.c
external/tinycbor/src/cborencoder_float.c
external/tinycbor/src/cborparser.c
external/tinycbor/src/cborparser_dup_string.c
external/tinycbor/src/cborparser_float.c
external/tinycbor/src/cborerrorstrings.c)
add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
set(CLASSICAPI_VERSION_VALUE 1)
@@ -40,7 +49,9 @@ target_link_libraries(${PROJECT_NAME} PRIVATE minhook)
target_include_directories(
${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
external/minhook/include)
external/minhook/include
external
external/tinycbor/src)
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI")
+2 -1
View File
@@ -24,6 +24,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| Combat | `InCombatLockdown` |
| CVar | `C_CVar.GetCVarBool` |
| Container | `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.PlayerHasHearthstone`, `C_Container.UseHearthstone` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| EquipmentSet | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
| Events | `C_EventUtils.IsEventValid` |
| Faction | `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
@@ -161,7 +162,7 @@ WoW.exe is x86, so the DLL must be built as Win32; an x64 build will not
load.
```powershell
git submodule update --init --recursive # fetches MinHook
git submodule update --init --recursive # fetches MinHook, picojson, tinycbor
cmake -B build -A Win32
cmake --build build --config Release
```
Vendored Submodule
+1
Submodule external/picojson added at 25fc213cca
Vendored Submodule
+1
Submodule external/tinycbor added at d393c16f3e
+2
View File
@@ -23,10 +23,12 @@ const lua_isstring_t IsString = reinterpret_cast<lua_isstring_t>(Offsets::LUA_IS
const lua_tonumber_t ToNumber = reinterpret_cast<lua_tonumber_t>(Offsets::LUA_TO_NUMBER);
const lua_toboolean_t ToBoolean = reinterpret_cast<lua_toboolean_t>(Offsets::LUA_TO_BOOLEAN);
const lua_tostring_t ToString = reinterpret_cast<lua_tostring_t>(Offsets::LUA_TO_STRING);
const lua_strlen_t StrLen = reinterpret_cast<lua_strlen_t>(Offsets::LUA_STR_LEN);
const lua_pushnumber_t PushNumber = reinterpret_cast<lua_pushnumber_t>(Offsets::LUA_PUSH_NUMBER);
const lua_pushnil_t PushNil = reinterpret_cast<lua_pushnil_t>(Offsets::LUA_PUSH_NIL);
const lua_pushboolean_t PushBoolean = reinterpret_cast<lua_pushboolean_t>(Offsets::LUA_PUSH_BOOLEAN);
const lua_pushstring_t PushString = reinterpret_cast<lua_pushstring_t>(Offsets::LUA_PUSH_STRING);
const lua_pushlstring_t PushLString = reinterpret_cast<lua_pushlstring_t>(Offsets::LUA_PUSH_LSTRING);
const lua_pushvalue_t PushValue = reinterpret_cast<lua_pushvalue_t>(Offsets::LUA_PUSH_VALUE);
const lua_pushcclosure_t PushCClosure = reinterpret_cast<lua_pushcclosure_t>(Offsets::LUA_PUSH_CCLOSURE);
const lua_newtable_t NewTable = reinterpret_cast<lua_newtable_t>(Offsets::LUA_NEW_TABLE);
+4
View File
@@ -50,10 +50,12 @@ using lua_isstring_t = bool(__fastcall *)(void *L, int index);
using lua_tonumber_t = double(__fastcall *)(void *L, int index);
using lua_toboolean_t = int(__fastcall *)(void *L, int index);
using lua_tostring_t = const char *(__fastcall *)(void *L, int index);
using lua_strlen_t = unsigned int(__fastcall *)(void *L, int index);
using lua_pushnumber_t = void(__fastcall *)(void *L, double n);
using lua_pushnil_t = void(__fastcall *)(void *L);
using lua_pushboolean_t = void(__fastcall *)(void *L, int b);
using lua_pushstring_t = void(__fastcall *)(void *L, const char *s);
using lua_pushlstring_t = void(__fastcall *)(void *L, const char *s, unsigned int len);
using lua_pushvalue_t = void(__fastcall *)(void *L, int idx);
using lua_pushcclosure_t = void(__fastcall *)(void *L, CFunction fn, int upvals);
using lua_newtable_t = void(__fastcall *)(void *L);
@@ -76,10 +78,12 @@ extern const lua_isstring_t IsString;
extern const lua_tonumber_t ToNumber;
extern const lua_toboolean_t ToBoolean;
extern const lua_tostring_t ToString;
extern const lua_strlen_t StrLen;
extern const lua_pushnumber_t PushNumber;
extern const lua_pushnil_t PushNil;
extern const lua_pushboolean_t PushBoolean;
extern const lua_pushstring_t PushString;
extern const lua_pushlstring_t PushLString;
extern const lua_pushvalue_t PushValue;
extern const lua_pushcclosure_t PushCClosure;
extern const lua_newtable_t NewTable;
+22
View File
@@ -1469,6 +1469,14 @@ enum Offsets {
LUA_PUSH_NIL = 0x6F37F0,
LUA_PUSH_BOOLEAN = 0x6F39F0,
LUA_PUSH_STRING = 0x6F3890,
// `lua_pushlstring(L, ptr, len)` — binary-safe push. Same TValue
// tag as `pushstring` but takes an explicit length so the string
// can contain NULs.
LUA_PUSH_LSTRING = 0x6F3840,
// `lua_strlen(L, idx)` — returns the byte length of the string
// value at `idx`. Pairs with `lua_tostring` for binary-safe
// string reads.
LUA_STR_LEN = 0x6F36E0,
// Verified empirically via `_classicapi_PushValueProbe` —
// pushes a number then dups via PushValue, checks GetTop tracks.
// **Doc note:** `docs/LuaCAPI.md` reports `0x6F30D0` as lua_pushvalue
@@ -1504,6 +1512,20 @@ enum Offsets {
// RegisterTableFunction during LoadScriptFunctions).
VAR_LUA_STATE = 0x00CEEF74,
// zlib 1.2.2, statically linked into WoW.exe (version string at
// `0x008745F0`, copyright banners at `0x00815528`/`0x00815608`).
// Standard zlib one-shot helpers — `compress2` for the deflate
// side, `uncompress` for inflate. Both `__fastcall` (caller passes
// dest/destLen in ECX/EDX, the rest on stack; callee cleans).
//
// int compress2(dest, *destLen, source, sourceLen, level)
// int uncompress(dest, *destLen, source, sourceLen)
//
// Return codes match zlib's: 0 = Z_OK, -3 = Z_DATA_ERROR,
// -4 = Z_MEM_ERROR, -5 = Z_BUF_ERROR.
FUN_ZLIB_COMPRESS2 = 0x00730BC0,
FUN_ZLIB_UNCOMPRESS = 0x00734810,
// Static event-name "table" at runtime VA `0x00BE11D8` — this is what
// `C_EventUtils.IsEventValid` walks. It is NOT the structure that
// `frame:RegisterEvent` and the dispatcher actually use; that lives at
+156
View File
@@ -0,0 +1,156 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include <cstdint>
#include <vector>
namespace Encoding::Base64 {
namespace {
constexpr const char kAlphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// `C_EncodingUtil.EncodeBase64(data) → string` — standard RFC 4648
// base64 with `+`/`/` (not URL-safe) and `=` padding. Output length is
// `((len + 2) / 3) * 4`.
int __fastcall Script_EncodeBase64(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.EncodeBase64(data)");
return 0;
}
const auto *src = reinterpret_cast<const uint8_t *>(
Game::Lua::ToString(L, 1));
const unsigned int len = Game::Lua::StrLen(L, 1);
if (src == nullptr || len == 0) {
Game::Lua::PushString(L, "");
return 1;
}
const unsigned int outLen = ((len + 2) / 3) * 4;
std::vector<char> out(outLen);
unsigned int o = 0;
unsigned int i = 0;
for (; i + 3 <= len; i += 3) {
const uint32_t v = (static_cast<uint32_t>(src[i]) << 16) |
(static_cast<uint32_t>(src[i + 1]) << 8) |
static_cast<uint32_t>(src[i + 2]);
out[o++] = kAlphabet[(v >> 18) & 0x3F];
out[o++] = kAlphabet[(v >> 12) & 0x3F];
out[o++] = kAlphabet[(v >> 6) & 0x3F];
out[o++] = kAlphabet[v & 0x3F];
}
const unsigned int rem = len - i;
if (rem == 1) {
const uint32_t v = static_cast<uint32_t>(src[i]) << 16;
out[o++] = kAlphabet[(v >> 18) & 0x3F];
out[o++] = kAlphabet[(v >> 12) & 0x3F];
out[o++] = '=';
out[o++] = '=';
} else if (rem == 2) {
const uint32_t v = (static_cast<uint32_t>(src[i]) << 16) |
(static_cast<uint32_t>(src[i + 1]) << 8);
out[o++] = kAlphabet[(v >> 18) & 0x3F];
out[o++] = kAlphabet[(v >> 12) & 0x3F];
out[o++] = kAlphabet[(v >> 6) & 0x3F];
out[o++] = '=';
}
Game::Lua::PushLString(L, out.data(), outLen);
return 1;
}
// `C_EncodingUtil.DecodeBase64(b64) → string | nil` — accepts the
// standard alphabet plus the URL-safe variant (`-_`). Padding (`=`) is
// required when the encoded length is not already a multiple of 4 —
// modern WoW's implementation rejects unpadded input and we match.
// Returns nil on any non-alphabet character or length-not-multiple-of-4.
int __fastcall Script_DecodeBase64(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.DecodeBase64(b64)");
return 0;
}
const char *src = Game::Lua::ToString(L, 1);
const unsigned int len = Game::Lua::StrLen(L, 1);
if (src == nullptr || len == 0) {
Game::Lua::PushString(L, "");
return 1;
}
if ((len & 3) != 0)
return 0;
auto decode = [](char c) -> int {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return 26 + (c - 'a');
if (c >= '0' && c <= '9') return 52 + (c - '0');
if (c == '+' || c == '-') return 62;
if (c == '/' || c == '_') return 63;
return -1;
};
std::vector<char> out;
out.reserve((len / 4) * 3);
for (unsigned int i = 0; i < len; i += 4) {
const char c0 = src[i];
const char c1 = src[i + 1];
const char c2 = src[i + 2];
const char c3 = src[i + 3];
const int v0 = decode(c0);
const int v1 = decode(c1);
if (v0 < 0 || v1 < 0)
return 0;
// Padding only allowed in the final quad's c2/c3 slots.
const bool isLastQuad = (i + 4 == len);
if (c2 == '=') {
if (!isLastQuad || c3 != '=')
return 0;
out.push_back(static_cast<char>((v0 << 2) | (v1 >> 4)));
break;
}
const int v2 = decode(c2);
if (v2 < 0)
return 0;
if (c3 == '=') {
if (!isLastQuad)
return 0;
out.push_back(static_cast<char>((v0 << 2) | (v1 >> 4)));
out.push_back(static_cast<char>((v1 << 4) | (v2 >> 2)));
break;
}
const int v3 = decode(c3);
if (v3 < 0)
return 0;
out.push_back(static_cast<char>((v0 << 2) | (v1 >> 4)));
out.push_back(static_cast<char>((v1 << 4) | (v2 >> 2)));
out.push_back(static_cast<char>((v2 << 6) | v3));
}
Game::Lua::PushLString(L, out.data(),
static_cast<unsigned int>(out.size()));
return 1;
}
void Register() {
Game::Lua::RegisterTableFunction("C_EncodingUtil", "EncodeBase64",
&Script_EncodeBase64);
Game::Lua::RegisterTableFunction("C_EncodingUtil", "DecodeBase64",
&Script_DecodeBase64);
}
} // namespace
static const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace Encoding::Base64
+322
View File
@@ -0,0 +1,322 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
extern "C" {
#include <cbor.h>
}
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <vector>
namespace Encoding::CBOR {
namespace {
constexpr int kMaxDepth = 64;
// ---------------- encode ----------------
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<int>(k);
if (ki < 1 || static_cast<double>(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 (cbor_encoder_create_array(enc, &arr,
static_cast<size_t>(arrayLen)) != CborNoError)
return false;
for (int i = 1; i <= arrayLen; ++i) {
Game::Lua::PushNumber(L, static_cast<double>(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 cbor_encoder_close_container(enc, &arr) == CborNoError;
}
const int pairs = CountTablePairs(L, idx);
CborEncoder map;
if (cbor_encoder_create_map(enc, &map, static_cast<size_t>(pairs)) != CborNoError)
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 cbor_encoder_close_container(enc, &map) == CborNoError;
}
bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth) {
switch (Game::Lua::Type(L, idx)) {
case Game::Lua::TYPE_NIL:
return cbor_encode_null(enc) == CborNoError;
case Game::Lua::TYPE_BOOLEAN:
return cbor_encode_boolean(enc,
Game::Lua::ToBoolean(L, idx) != 0) == CborNoError;
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 cbor_encode_uint(enc,
static_cast<uint64_t>(n)) == CborNoError;
return cbor_encode_negative_int(enc,
static_cast<uint64_t>(-n)) == CborNoError;
}
return cbor_encode_double(enc, n) == CborNoError;
}
case Game::Lua::TYPE_STRING: {
const char *s = Game::Lua::ToString(L, idx);
const unsigned int n = Game::Lua::StrLen(L, idx);
return cbor_encode_text_string(enc, s, n) == CborNoError;
}
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; if it overflows, it
// tracks the extra bytes needed so we can resize and retry.
std::vector<uint8_t> buf(256);
for (;;) {
CborEncoder enc;
cbor_encoder_init(&enc, buf.data(), buf.size(), 0);
if (!EncodeValue(L, 1, &enc, 0)) {
// If it was a buffer-size issue we'll catch it via the
// extra-bytes-needed check below; otherwise this is a
// genuine "value not representable" failure.
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
if (extra == 0)
return 0;
buf.resize(buf.size() + extra);
continue;
}
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<const char *>(buf.data()),
static_cast<unsigned int>(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<double>(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<double>(raw);
else
n = static_cast<double>(raw);
Game::Lua::PushNumber(L, n);
break;
}
case CborBooleanType: {
bool b;
if (cbor_value_get_boolean(it, &b) != CborNoError)
return false;
Game::Lua::PushBoolean(L, b ? 1 : 0);
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<double>(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<double>(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<uint8_t **>(&s),
&n, it);
if (err != CborNoError) {
std::free(s);
return false;
}
Game::Lua::PushLString(L, s, static_cast<unsigned int>(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<const uint8_t *>(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
+135
View File
@@ -0,0 +1,135 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include "Offsets.h"
#include <vector>
namespace Encoding::Compress {
namespace {
// zlib one-shot helpers, statically linked into WoW.exe (1.2.2).
// Both are `__fastcall`: ECX = dest, EDX = destLen ptr, the remaining
// args on the stack. Callee cleans (RET 0xC).
using compress2_t = int(__fastcall *)(void *dest, unsigned long *destLen,
const void *source,
unsigned long sourceLen, int level);
using uncompress_t = int(__fastcall *)(void *dest, unsigned long *destLen,
const void *source,
unsigned long sourceLen);
// zlib 1.2.x's `compressBound` is inlined in the header; we replicate
// the formula here so we don't have to chase the symbol.
unsigned long CompressBound(unsigned long sourceLen) {
return sourceLen + (sourceLen >> 12) + (sourceLen >> 14) +
(sourceLen >> 25) + 13;
}
// `C_EncodingUtil.CompressString(data) → string` — deflate-compresses
// `data` (any binary string) at zlib's default level (6) and returns
// the compressed stream. Round-trips through `DecompressString`.
//
// Output is the zlib-format stream (2-byte header + raw deflate +
// adler32 trailer) as produced by `compress2`. Same format `_GAMES_*`
// addon-side libraries like `LibDeflate` produce when using
// `CompressZlib`.
int __fastcall Script_CompressString(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.CompressString(data)");
return 0;
}
const char *src = Game::Lua::ToString(L, 1);
const unsigned int srcLen = Game::Lua::StrLen(L, 1);
if (src == nullptr || srcLen == 0) {
Game::Lua::PushString(L, "");
return 1;
}
const unsigned long bound = CompressBound(srcLen);
std::vector<char> out(bound);
unsigned long outLen = bound;
auto compress2 = reinterpret_cast<compress2_t>(Offsets::FUN_ZLIB_COMPRESS2);
const int rc = compress2(out.data(), &outLen, src, srcLen,
/*Z_DEFAULT_COMPRESSION*/ -1);
if (rc != 0)
return 0; // Z_MEM_ERROR / Z_BUF_ERROR
Game::Lua::PushLString(L, out.data(),
static_cast<unsigned int>(outLen));
return 1;
}
// `C_EncodingUtil.DecompressString(data) → string | nil` — inflates a
// zlib-format stream produced by `CompressString` (or any compatible
// encoder). Returns nil on malformed input (`Z_DATA_ERROR`) or any
// other zlib failure.
//
// Output sizing: we don't know the decompressed length up front, so
// we start with a buffer 4x the input size and grow exponentially on
// `Z_BUF_ERROR`. zlib's one-shot `uncompress` can't stream a partial
// result, so a single resize-and-retry loop is the simplest correct
// approach for arbitrary-size payloads.
int __fastcall Script_DecompressString(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.DecompressString(data)");
return 0;
}
const char *src = Game::Lua::ToString(L, 1);
const unsigned int srcLen = Game::Lua::StrLen(L, 1);
if (src == nullptr || srcLen == 0) {
Game::Lua::PushString(L, "");
return 1;
}
auto uncompress = reinterpret_cast<uncompress_t>(Offsets::FUN_ZLIB_UNCOMPRESS);
unsigned long capacity = static_cast<unsigned long>(srcLen) * 4 + 32;
// Hard cap to keep a corrupt header that claims gigabytes from
// looping forever. 64 MiB matches modern's documented inflate
// ceiling (large enough for any legitimate addon import string).
constexpr unsigned long kCapacityCeiling = 64UL * 1024UL * 1024UL;
std::vector<char> out;
for (;;) {
if (capacity > kCapacityCeiling)
return 0;
out.resize(capacity);
unsigned long outLen = capacity;
const int rc = uncompress(out.data(), &outLen, src, srcLen);
if (rc == 0 /*Z_OK*/) {
Game::Lua::PushLString(L, out.data(),
static_cast<unsigned int>(outLen));
return 1;
}
if (rc != -5 /*Z_BUF_ERROR*/)
return 0; // Z_DATA_ERROR, Z_MEM_ERROR, etc.
// Output buffer too small — grow and retry.
capacity *= 2;
}
}
void Register() {
Game::Lua::RegisterTableFunction("C_EncodingUtil", "CompressString",
&Script_CompressString);
Game::Lua::RegisterTableFunction("C_EncodingUtil", "DecompressString",
&Script_DecompressString);
}
} // namespace
static const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace Encoding::Compress
+98
View File
@@ -0,0 +1,98 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include <cstdint>
#include <cstring>
#include <vector>
namespace Encoding::Hex {
namespace {
// `C_EncodingUtil.EncodeHex(data) → string` — lowercase hex pair per
// input byte. Empty input maps to empty string. Output is binary-safe
// (no embedded NULs) and `2 * len(data)` bytes long.
int __fastcall Script_EncodeHex(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.EncodeHex(data)");
return 0;
}
const char *src = Game::Lua::ToString(L, 1);
const unsigned int len = Game::Lua::StrLen(L, 1);
if (src == nullptr || len == 0) {
Game::Lua::PushString(L, "");
return 1;
}
static const char kHex[] = "0123456789abcdef";
std::vector<char> out(static_cast<std::size_t>(len) * 2);
for (unsigned int i = 0; i < len; ++i) {
const uint8_t b = static_cast<uint8_t>(src[i]);
out[i * 2 + 0] = kHex[b >> 4];
out[i * 2 + 1] = kHex[b & 0x0F];
}
Game::Lua::PushLString(L, out.data(), len * 2);
return 1;
}
// `C_EncodingUtil.DecodeHex(hex) → string | nil` — inverse of
// `EncodeHex`. Accepts either case (`abcdef` / `ABCDEF`). Returns nil
// on odd-length input or any non-hex character — matches modern's
// reject-bad-input semantic rather than silently dropping characters.
int __fastcall Script_DecodeHex(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.DecodeHex(hex)");
return 0;
}
const char *src = Game::Lua::ToString(L, 1);
const unsigned int len = Game::Lua::StrLen(L, 1);
if (src == nullptr || len == 0) {
Game::Lua::PushString(L, "");
return 1;
}
if ((len & 1) != 0)
return 0;
auto nibble = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
return -1;
};
std::vector<char> out(len / 2);
for (unsigned int i = 0; i < len; i += 2) {
const int hi = nibble(src[i]);
const int lo = nibble(src[i + 1]);
if (hi < 0 || lo < 0)
return 0;
out[i / 2] = static_cast<char>((hi << 4) | lo);
}
Game::Lua::PushLString(L, out.data(), len / 2);
return 1;
}
void Register() {
Game::Lua::RegisterTableFunction("C_EncodingUtil", "EncodeHex",
&Script_EncodeHex);
Game::Lua::RegisterTableFunction("C_EncodingUtil", "DecodeHex",
&Script_DecodeHex);
}
} // namespace
static const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace Encoding::Hex
+223
View File
@@ -0,0 +1,223 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#define PICOJSON_USE_INT64
#include <picojson/picojson.h>
#include <cmath>
#include <cstdio>
#include <string>
namespace Encoding::JSON {
namespace {
constexpr int kMaxDepth = 64;
bool LuaToPicojson(void *L, int idx, picojson::value &out, int depth);
// Consecutive 1..N integer keys → JSON array; anything else → object.
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<int>(k);
if (ki < 1 || static_cast<double>(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;
}
bool LuaTableToPicojson(void *L, int idx, picojson::value &out, 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)) {
picojson::array arr;
arr.reserve(arrayLen);
for (int i = 1; i <= arrayLen; ++i) {
Game::Lua::PushNumber(L, static_cast<double>(i));
Game::Lua::RawGet(L, idx);
picojson::value child;
if (!LuaToPicojson(L, -1, child, depth + 1)) {
Game::Lua::SetTop(L, -2);
return false;
}
arr.push_back(std::move(child));
Game::Lua::SetTop(L, -2);
}
out = picojson::value(arr);
return true;
}
picojson::object obj;
Game::Lua::PushNil(L);
while (Game::Lua::Next(L, idx) != 0) {
const int keyType = Game::Lua::Type(L, -2);
std::string key;
if (keyType == Game::Lua::TYPE_STRING) {
const char *kstr = Game::Lua::ToString(L, -2);
const unsigned int klen = Game::Lua::StrLen(L, -2);
key.assign(kstr, klen);
} else if (keyType == Game::Lua::TYPE_NUMBER) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%.14g",
Game::Lua::ToNumber(L, -2));
key = buf;
} else {
// Drop unsupported key types.
Game::Lua::SetTop(L, -2);
continue;
}
picojson::value child;
if (!LuaToPicojson(L, -1, child, depth + 1)) {
Game::Lua::SetTop(L, -3);
return false;
}
obj[key] = std::move(child);
Game::Lua::SetTop(L, -2);
}
out = picojson::value(obj);
return true;
}
bool LuaToPicojson(void *L, int idx, picojson::value &out, int depth) {
switch (Game::Lua::Type(L, idx)) {
case Game::Lua::TYPE_NIL:
out = picojson::value();
return true;
case Game::Lua::TYPE_BOOLEAN:
out = picojson::value(Game::Lua::ToBoolean(L, idx) != 0);
return true;
case Game::Lua::TYPE_NUMBER: {
const double n = Game::Lua::ToNumber(L, idx);
if (std::isnan(n) || std::isinf(n)) {
out = picojson::value(); // JSON has no nan/inf → null
return true;
}
out = picojson::value(n);
return true;
}
case Game::Lua::TYPE_STRING: {
const char *s = Game::Lua::ToString(L, idx);
const unsigned int n = Game::Lua::StrLen(L, idx);
out = picojson::value(std::string(s, n));
return true;
}
case Game::Lua::TYPE_TABLE:
return LuaTableToPicojson(L, idx, out, depth);
default:
return false;
}
}
void PicojsonToLua(void *L, const picojson::value &v) {
if (v.is<picojson::null>()) {
Game::Lua::PushNil(L);
} else if (v.is<bool>()) {
Game::Lua::PushBoolean(L, v.get<bool>() ? 1 : 0);
} else if (v.is<double>()) {
Game::Lua::PushNumber(L, v.get<double>());
} else if (v.is<int64_t>()) {
Game::Lua::PushNumber(L,
static_cast<double>(v.get<int64_t>()));
} else if (v.is<std::string>()) {
const auto &s = v.get<std::string>();
Game::Lua::PushLString(L, s.data(),
static_cast<unsigned int>(s.size()));
} else if (v.is<picojson::array>()) {
const auto &arr = v.get<picojson::array>();
Game::Lua::NewTable(L);
int i = 1;
for (const auto &child : arr) {
Game::Lua::PushNumber(L, static_cast<double>(i++));
PicojsonToLua(L, child);
Game::Lua::RawSet(L, -3);
}
} else if (v.is<picojson::object>()) {
const auto &obj = v.get<picojson::object>();
Game::Lua::NewTable(L);
for (const auto &kv : obj) {
Game::Lua::PushLString(L, kv.first.data(),
static_cast<unsigned int>(kv.first.size()));
PicojsonToLua(L, kv.second);
Game::Lua::RawSet(L, -3);
}
} else {
Game::Lua::PushNil(L);
}
}
int __fastcall Script_SerializeJSON(void *L) {
if (Game::Lua::GetTop(L) < 1) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.SerializeJSON(value)");
return 0;
}
picojson::value v;
if (!LuaToPicojson(L, 1, v, 0))
return 0;
const std::string out = v.serialize();
Game::Lua::PushLString(L, out.data(),
static_cast<unsigned int>(out.size()));
return 1;
}
int __fastcall Script_DeserializeJSON(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: C_EncodingUtil.DeserializeJSON(json)");
return 0;
}
const char *src = Game::Lua::ToString(L, 1);
const unsigned int srcLen = Game::Lua::StrLen(L, 1);
if (src == nullptr)
return 0;
picojson::value v;
std::string err;
const char *endp = picojson::parse(v, src, src + srcLen, &err);
if (!err.empty() || endp != src + srcLen)
return 0;
PicojsonToLua(L, v);
return 1;
}
void Register() {
Game::Lua::RegisterTableFunction("C_EncodingUtil", "SerializeJSON",
&Script_SerializeJSON);
Game::Lua::RegisterTableFunction("C_EncodingUtil", "DeserializeJSON",
&Script_DeserializeJSON);
}
} // namespace
static const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace Encoding::JSON