Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562// AI-authored (Claude).#include "json.hpp"
#include <array>#include <charconv>#include <cstddef>#include <cstdint>#include <expected>#include <optional>#include <string>#include <string_view>#include <system_error>#include <unordered_set>#include <utility>
namespace packutils::json {
Value Value::of_string(std::string_view text) { Value value; value.type = Type::string; value.text = text; return value;}
Value Value::of_integer(std::int64_t number) { Value value; value.type = Type::number; value.text = std::to_string(number); return value;}
Value Value::of_bool(bool on) { Value value; value.type = Type::boolean; value.boolean = on; return value;}
Value Value::array() { Value value; value.type = Type::array; return value;}
Value Value::object() { Value value; value.type = Type::object; return value;}
const Value *Value::get(std::string_view key) const { for (const Member &member : members) { if (member.key == key) { return &member.value; } } return nullptr;}
void Value::put(std::string_view key, Value value) { members.push_back(Member{.key = std::string(key), .value = std::move(value)});}
void Value::push(Value value) { items.push_back(std::move(value));}
std::optional<std::string_view> Value::string_at(std::string_view key) const { const Value *found = get(key); if (found == nullptr || found->type != Type::string) { return std::nullopt; } return found->text;}
std::optional<std::int64_t> Value::integer(std::int64_t min, std::int64_t max) const { if (type != Type::number && type != Type::string) { return std::nullopt; } std::int64_t parsed = 0; const char *first = text.data(); // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): from_chars takes a range. const char *last = first + text.size(); const auto [end, error] = std::from_chars(first, last, parsed); if (error != std::errc{} || end != last || parsed < min || parsed > max) { return std::nullopt; } return parsed;}
namespace {
constexpr std::size_t max_depth = 64;
class Reader { public: explicit Reader(std::string_view bytes) : bytes_(bytes) {}
std::expected<Value, ParseError> document() { skip_space(); auto value = read_value(0); if (!value) { return value; } skip_space(); if (at_ < bytes_.size()) { return fail("trailing bytes after the document"); } return value; }
private: std::string_view bytes_; std::size_t at_ = 0;
[[nodiscard]] std::unexpected<ParseError> fail(std::string_view message) const { return std::unexpected(ParseError{.message = std::string(message), .offset = at_}); }
void skip_space() { while (at_ < bytes_.size() && (bytes_.at(at_) == ' ' || bytes_.at(at_) == '\t' || bytes_.at(at_) == '\n' || bytes_.at(at_) == '\r')) { ++at_; } }
bool literal(std::string_view word) { if (bytes_.substr(at_, word.size()) != word) { return false; } at_ += word.size(); return true; }
// NOLINTNEXTLINE(misc-no-recursion): bounded by max_depth. std::expected<Value, ParseError> read_value(std::size_t depth) { if (depth > max_depth) { return fail("the document nests too deeply"); } if (at_ >= bytes_.size()) { return fail("the document ends early"); } const char next = bytes_.at(at_); if (next == '{') { return read_object(depth); } if (next == '[') { return read_array(depth); } if (next == '"') { auto text = read_string(); if (!text) { return std::unexpected(text.error()); } return Value::of_string(*text); } if (literal("true")) { return Value::of_bool(true); } if (literal("false")) { return Value::of_bool(false); } if (literal("null")) { return Value{}; } return read_number(); }
// NOLINTNEXTLINE(misc-no-recursion): bounded by max_depth. std::expected<Value, ParseError> read_object(std::size_t depth) { ++at_; Value object = Value::object(); std::unordered_set<std::string> keys; skip_space(); if (at_ < bytes_.size() && bytes_.at(at_) == '}') { ++at_; return object; } while (true) { skip_space(); if (at_ >= bytes_.size() || bytes_.at(at_) != '"') { return fail("expected a member name"); } auto key = read_string(); if (!key) { return std::unexpected(key.error()); } if (!keys.insert(*key).second) { return fail("a member is named twice"); } skip_space(); if (at_ >= bytes_.size() || bytes_.at(at_) != ':') { return fail("expected ':'"); } ++at_; skip_space(); auto value = read_value(depth + 1); if (!value) { return value; } object.put(*key, std::move(*value)); skip_space(); if (at_ < bytes_.size() && bytes_.at(at_) == ',') { ++at_; continue; } if (at_ < bytes_.size() && bytes_.at(at_) == '}') { ++at_; return object; } return fail("expected ',' or '}'"); } }
// NOLINTNEXTLINE(misc-no-recursion): bounded by max_depth. std::expected<Value, ParseError> read_array(std::size_t depth) { ++at_; Value array = Value::array(); skip_space(); if (at_ < bytes_.size() && bytes_.at(at_) == ']') { ++at_; return array; } while (true) { skip_space(); auto value = read_value(depth + 1); if (!value) { return value; } array.push(std::move(*value)); skip_space(); if (at_ < bytes_.size() && bytes_.at(at_) == ',') { ++at_; continue; } if (at_ < bytes_.size() && bytes_.at(at_) == ']') { ++at_; return array; } return fail("expected ',' or ']'"); } }
std::expected<Value, ParseError> read_number() { const std::size_t start = at_; if (at_ < bytes_.size() && bytes_.at(at_) == '-') { ++at_; } if (at_ >= bytes_.size() || bytes_.at(at_) < '0' || bytes_.at(at_) > '9') { return fail("expected a value"); } if (bytes_.at(at_) == '0') { ++at_; } else { digits(); } if (at_ < bytes_.size() && bytes_.at(at_) == '.') { ++at_; if (digits() == 0) { return fail("expected a digit after '.'"); } } if (at_ < bytes_.size() && (bytes_.at(at_) == 'e' || bytes_.at(at_) == 'E')) { ++at_; if (at_ < bytes_.size() && (bytes_.at(at_) == '+' || bytes_.at(at_) == '-')) { ++at_; } if (digits() == 0) { return fail("expected a digit in the exponent"); } } Value value; value.type = Value::Type::number; value.text = bytes_.substr(start, at_ - start); return value; }
std::size_t digits() { const std::size_t start = at_; while (at_ < bytes_.size() && bytes_.at(at_) >= '0' && bytes_.at(at_) <= '9') { ++at_; } return at_ - start; }
std::expected<std::uint32_t, ParseError> hex4() { if (at_ + 4 > bytes_.size()) { return fail("a \\u escape ends early"); } std::uint32_t unit = 0; for (std::size_t i = 0; i < 4; ++i) { const char digit = bytes_.at(at_ + i); unit <<= 4U; if (digit >= '0' && digit <= '9') { unit |= static_cast<std::uint32_t>(digit - '0'); } else if (digit >= 'a' && digit <= 'f') { unit |= static_cast<std::uint32_t>(digit - 'a' + 10); } else if (digit >= 'A' && digit <= 'F') { unit |= static_cast<std::uint32_t>(digit - 'A' + 10); } else { return fail("a \\u escape is not hex"); } } at_ += 4; return unit; }
static void append_utf8(std::string &out, std::uint32_t code) { if (code < 0x80U) { out.push_back(static_cast<char>(code)); } else if (code < 0x800U) { out.push_back(static_cast<char>(0xC0U | (code >> 6U))); out.push_back(static_cast<char>(0x80U | (code & 0x3FU))); } else if (code < 0x10000U) { out.push_back(static_cast<char>(0xE0U | (code >> 12U))); out.push_back(static_cast<char>(0x80U | ((code >> 6U) & 0x3FU))); out.push_back(static_cast<char>(0x80U | (code & 0x3FU))); } else { out.push_back(static_cast<char>(0xF0U | (code >> 18U))); out.push_back(static_cast<char>(0x80U | ((code >> 12U) & 0x3FU))); out.push_back(static_cast<char>(0x80U | ((code >> 6U) & 0x3FU))); out.push_back(static_cast<char>(0x80U | (code & 0x3FU))); } }
std::expected<std::string, ParseError> read_string() { ++at_; std::string out; while (true) { if (at_ >= bytes_.size()) { return fail("a string is not closed"); } const auto byte = static_cast<unsigned char>(bytes_.at(at_)); if (byte == '"') { ++at_; return out; } if (byte < 0x20U) { return fail("a control character in a string"); } if (byte != '\\') { out.push_back(static_cast<char>(byte)); ++at_; continue; } ++at_; if (at_ >= bytes_.size()) { return fail("an escape ends early"); } const char escape = bytes_.at(at_++); switch (escape) { case '"': case '\\': case '/': out.push_back(escape); break; case 'b': out.push_back('\b'); break; case 'f': out.push_back('\f'); break; case 'n': out.push_back('\n'); break; case 'r': out.push_back('\r'); break; case 't': out.push_back('\t'); break; case 'u': { auto unit = hex4(); if (!unit) { return std::unexpected(unit.error()); } std::uint32_t code = *unit; if (code >= 0xD800U && code <= 0xDBFFU) { if (!literal("\\u")) { return fail("a lone surrogate"); } auto low = hex4(); if (!low) { return std::unexpected(low.error()); } if (*low < 0xDC00U || *low > 0xDFFFU) { return fail("a lone surrogate"); } code = 0x10000U + ((code - 0xD800U) << 10U) + (*low - 0xDC00U); } else if (code >= 0xDC00U && code <= 0xDFFFU) { return fail("a lone surrogate"); } append_utf8(out, code); break; } default: return fail("an unknown escape"); } } }};
void write_string(std::string &out, std::string_view text) { static constexpr std::array<char, 16> hex{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; out.push_back('"'); for (const char raw : text) { const auto byte = static_cast<unsigned char>(raw); switch (byte) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case 0x08U: out += "\\b"; break; case 0x0CU: out += "\\f"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (byte < 0x20U) { out += "\\u00"; out.push_back(hex.at(byte >> 4U)); out.push_back(hex.at(byte & 0x0FU)); } else { out.push_back(raw); } } } out.push_back('"');}
void write_scalar(std::string &out, const Value &value) { switch (value.type) { case Value::Type::null: out += "null"; break; case Value::Type::boolean: out += value.boolean ? "true" : "false"; break; case Value::Type::number: out += value.text; break; case Value::Type::string: write_string(out, value.text); break; case Value::Type::array: case Value::Type::object: break; }}
// NOLINTNEXTLINE(misc-no-recursion): bounded by the depth the document was read or built with.void write_pretty(std::string &out, const Value &value, std::size_t depth) { const std::string inner((depth + 1) * 2, ' '); const std::string outer(depth * 2, ' '); if (value.type == Value::Type::object) { if (value.members.empty()) { out += "{}"; return; } out += "{\n"; bool first = true; for (const Member &member : value.members) { if (!first) { out += ",\n"; } first = false; out += inner; write_string(out, member.key); out += ": "; write_pretty(out, member.value, depth + 1); } out += "\n" + outer + "}"; return; } if (value.type == Value::Type::array) { if (value.items.empty()) { out += "[]"; return; } out += "[\n"; bool first = true; for (const Value &item : value.items) { if (!first) { out += ",\n"; } first = false; out += inner; write_pretty(out, item, depth + 1); } out += "\n" + outer + "]"; return; } write_scalar(out, value);}
// NOLINTNEXTLINE(misc-no-recursion): bounded by the depth the document was read or built with.void write_compact(std::string &out, const Value &value) { if (value.type == Value::Type::object) { out.push_back('{'); bool first = true; for (const Member &member : value.members) { if (!first) { out.push_back(','); } first = false; write_string(out, member.key); out.push_back(':'); write_compact(out, member.value); } out.push_back('}'); return; } if (value.type == Value::Type::array) { out.push_back('['); bool first = true; for (const Value &item : value.items) { if (!first) { out.push_back(','); } first = false; write_compact(out, item); } out.push_back(']'); return; } write_scalar(out, value);}
} // namespace
std::expected<Value, ParseError> parse(std::string_view bytes) { return Reader(bytes).document();}
std::string canonical(const Value &value) { std::string out; write_pretty(out, value, 0); out.push_back('\n'); return out;}
std::string compact(const Value &value) { std::string out; write_compact(out, value); return out;}
} // namespace packutils::json