#include "json_write.hpp" #include "json.hpp" #include "lua.h" #include "report.hpp" #include #include #include #include #include #include #include #include #include #include namespace packluau { namespace { constexpr double exact_integer_limit = 9007199254740992.0; // 2^53 class Writer { public: Writer(lua_State *state, std::string &out) noexcept : state_(state), out_(&out) {} // NOLINTNEXTLINE(misc-no-recursion): bounded by max_depth. std::expected value(int index, int depth, const std::string &path) { if (depth > max_depth) { return fail(path, "is nested too deeply (or refers to itself)"); } switch (lua_type(state_, index)) { case LUA_TBOOLEAN: *out_ += lua_toboolean(state_, index) != 0 ? "true" : "false"; return {}; case LUA_TNUMBER: return number(lua_tonumber(state_, index), path); case LUA_TSTRING: { std::size_t length = 0; const char *text = lua_tolstring(state_, index, &length); const std::string_view bytes(text, length); if (!is_utf8(bytes)) { return fail(path, "is not valid UTF-8"); } append_json_string(*out_, bytes); return {}; } case LUA_TTABLE: return table(index, depth, path); case LUA_TNIL: return fail(path, "is nil, which JSON cannot hold here"); default: return fail(path, std::string("is a ") + lua_typename(state_, lua_type(state_, index)) + ", which JSON cannot hold"); } } private: static std::expected fail(const std::string &path, std::string_view why) { return std::unexpected("value" + path + " " + std::string(why)); } std::expected number(double value, const std::string &path) { if (!std::isfinite(value)) { return fail(path, "is not a finite number"); } std::array buffer{}; char *first = buffer.data(); char *last = std::to_address(buffer.end()); std::to_chars_result result{}; if (value == std::floor(value) && std::fabs(value) < exact_integer_limit) { result = std::to_chars(first, last, static_cast(value)); } else { result = std::to_chars(first, last, value); } if (result.ec != std::errc{}) { return fail(path, "could not be written"); } out_->append(first, result.ptr); return {}; } // NOLINTNEXTLINE(misc-no-recursion): bounded by max_depth. std::expected table(int index, int depth, const std::string &path) { const int absolute = index < 0 ? lua_gettop(state_) + index + 1 : index; if (lua_checkstack(state_, 3) == 0) { return fail(path, "is nested too deeply"); } std::vector keys; std::size_t numeric = 0; bool other = false; lua_pushnil(state_); while (lua_next(state_, absolute) != 0) { lua_pop(state_, 1); if (lua_type(state_, -1) == LUA_TSTRING) { std::size_t length = 0; const char *text = lua_tolstring(state_, -1, &length); keys.emplace_back(text, length); } else if (lua_type(state_, -1) == LUA_TNUMBER) { ++numeric; } else { other = true; } } const auto count = static_cast(lua_objlen(state_, absolute)); if (other || (numeric > 0 && !keys.empty()) || (numeric > 0 && numeric != count)) { return fail(path, "is a table that is neither an array (keys 1..n) nor an object " "(string keys)"); } if (numeric > 0) { *out_ += '['; for (std::size_t i = 1; i <= count; ++i) { if (i > 1) { *out_ += ','; } lua_rawgeti(state_, absolute, static_cast(i)); auto written = value(-1, depth + 1, path + "[" + std::to_string(i) + "]"); lua_pop(state_, 1); if (!written) { return written; } } *out_ += ']'; return {}; } std::ranges::sort(keys); *out_ += '{'; for (std::size_t i = 0; i < keys.size(); ++i) { const std::string &key = keys.at(i); if (!is_utf8(key)) { return fail(path, "has a key that is not valid UTF-8"); } if (i > 0) { *out_ += ','; } append_json_string(*out_, key); *out_ += ':'; lua_pushlstring(state_, key.empty() ? "" : key.data(), key.size()); lua_rawget(state_, absolute); std::string member_path = path; member_path += "."; member_path += key; auto written = value(-1, depth + 1, member_path); lua_pop(state_, 1); if (!written) { return written; } } *out_ += '}'; return {}; } lua_State *state_; std::string *out_; }; } // namespace std::expected write_json(lua_State *state, int index, std::string &out) { Writer writer(state, out); return writer.value(index, 0, ""); } } // namespace packluau