// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Debug.Core:Value_impl; import std; import :Value; using namespace Crafter::Debug; namespace { // Nesting deeper than this is refused rather than recursed into: the // parser is recursive, and its text comes from another process. constexpr std::uint32_t maxDepth = 256; bool IsDigit(char c) { return c >= '0' && c <= '9'; } void AppendUtf8(std::string& out, std::uint32_t code) { if (code < 0x80) { out += static_cast(code); } else if (code < 0x800) { out += static_cast(0xC0 | (code >> 6)); out += static_cast(0x80 | (code & 0x3F)); } else if (code < 0x10000) { out += static_cast(0xE0 | (code >> 12)); out += static_cast(0x80 | ((code >> 6) & 0x3F)); out += static_cast(0x80 | (code & 0x3F)); } else { out += static_cast(0xF0 | (code >> 18)); out += static_cast(0x80 | ((code >> 12) & 0x3F)); out += static_cast(0x80 | ((code >> 6) & 0x3F)); out += static_cast(0x80 | (code & 0x3F)); } } // The length of the valid UTF-8 sequence that starts at text[at], or 0 // when it is not one: a stray continuation byte, a truncated or overlong // sequence, a surrogate, or a code point above U+10FFFF. std::size_t Utf8SequenceLength(std::string_view text, std::size_t at) { const auto byte = [&](std::size_t i) { return static_cast(text[at + i]); }; const std::uint8_t lead = byte(0); std::size_t length = 0; std::uint8_t low = 0x80; std::uint8_t high = 0xBF; if (lead >= 0xC2 && lead <= 0xDF) { length = 2; } else if (lead >= 0xE0 && lead <= 0xEF) { length = 3; if (lead == 0xE0) low = 0xA0; if (lead == 0xED) high = 0x9F; } else if (lead >= 0xF0 && lead <= 0xF4) { length = 4; if (lead == 0xF0) low = 0x90; if (lead == 0xF4) high = 0x8F; } else { return 0; } if (at + length > text.size()) return 0; if (byte(1) < low || byte(1) > high) return 0; for (std::size_t i = 2; i < length; ++i) { if (byte(i) < 0x80 || byte(i) > 0xBF) return 0; } return length; } // `text` as a JSON string. Invalid UTF-8 becomes U+FFFD, so the output // always parses, whatever bytes an app put in a name. void AppendEscaped(std::string& out, std::string_view text) { out += '"'; std::size_t i = 0; while (i < text.size()) { const std::uint8_t c = static_cast(text[i]); if (c < 0x80) { switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\b': out += "\\b"; break; case '\f': out += "\\f"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (c < 0x20 || c == 0x7F) { out += std::format("\\u{:04x}", c); } else { out += static_cast(c); } } ++i; continue; } const std::size_t length = Utf8SequenceLength(text, i); if (length == 0) { out += "\xEF\xBF\xBD"; ++i; continue; } out.append(text.substr(i, length)); i += length; } out += '"'; } void AppendNumber(std::string& out, double number) { if (std::isnan(number)) { out += "\"NaN\""; return; } if (std::isinf(number)) { out += number > 0 ? "\"Infinity\"" : "\"-Infinity\""; return; } std::array buffer{}; const auto [end, error] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), number); const std::string_view digits(buffer.data(), static_cast(end - buffer.data())); out += digits; // A whole double keeps a fraction, so it reads back as a Number. if (digits.find_first_of(".eE") == std::string_view::npos) out += ".0"; } class Parser { public: explicit Parser(std::string_view text) : text(text) {} Value Document() { SkipWhitespace(); Value value = Any(0); SkipWhitespace(); if (position != text.size()) Fail("unexpected text after the value"); return value; } private: std::string_view text; std::size_t position = 0; [[noreturn]] void Fail(std::string_view what) const { throw ParseError(position, what); } bool AtEnd() const { return position >= text.size(); } bool Peek(char c) const { return !AtEnd() && text[position] == c; } bool Consume(char c) { if (!Peek(c)) return false; ++position; return true; } void SkipWhitespace() { while (!AtEnd() && (text[position] == ' ' || text[position] == '\t' || text[position] == '\n' || text[position] == '\r')) ++position; } void Literal(std::string_view word) { if (text.substr(position, word.size()) != word) Fail("expected a value"); position += word.size(); } Value Any(std::uint32_t depth) { if (depth > maxDepth) Fail("nested too deeply"); if (AtEnd()) Fail("unexpected end of text"); switch (text[position]) { case '{': return Object(depth); case '[': return Array(depth); case '"': return Value(String()); case 't': Literal("true"); return Value(true); case 'f': Literal("false"); return Value(false); case 'n': Literal("null"); return Value(); default: return Number(); } } Value Object(std::uint32_t depth) { ++position; Value object = Value::MakeObject(); SkipWhitespace(); if (Consume('}')) return object; while (true) { SkipWhitespace(); if (!Peek('"')) Fail("expected a member name in quotes"); const std::string key = String(); SkipWhitespace(); if (!Consume(':')) Fail("expected ':' after a member name"); SkipWhitespace(); object.Set(key, Any(depth + 1)); SkipWhitespace(); if (Consume('}')) return object; if (!Consume(',')) Fail("expected ',' or '}'"); } } Value Array(std::uint32_t depth) { ++position; Value array = Value::MakeArray(); SkipWhitespace(); if (Consume(']')) return array; while (true) { SkipWhitespace(); array.Push(Any(depth + 1)); SkipWhitespace(); if (Consume(']')) return array; if (!Consume(',')) Fail("expected ',' or ']'"); } } std::uint32_t Hex4() { if (position + 4 > text.size()) Fail("truncated \\u escape"); std::uint32_t code = 0; const auto [end, error] = std::from_chars(text.data() + position, text.data() + position + 4, code, 16); if (error != std::errc() || end != text.data() + position + 4) Fail("malformed \\u escape"); position += 4; return code; } std::string String() { ++position; std::string out; while (true) { if (AtEnd()) Fail("unterminated string"); const char c = text[position]; if (c == '"') { ++position; return out; } if (static_cast(c) < 0x20) Fail("control character in a string"); ++position; if (c != '\\') { out += c; continue; } if (AtEnd()) Fail("unterminated escape"); const char escape = text[position++]; switch (escape) { case '"': out += '"'; break; case '\\': out += '\\'; break; case '/': out += '/'; break; case 'b': out += '\b'; break; case 'f': out += '\f'; break; case 'n': out += '\n'; break; case 'r': out += '\r'; break; case 't': out += '\t'; break; case 'u': { std::uint32_t code = Hex4(); if (code >= 0xD800 && code <= 0xDBFF) { if (text.substr(position, 2) != "\\u") Fail("unpaired surrogate"); position += 2; const std::uint32_t low = Hex4(); if (low < 0xDC00 || low > 0xDFFF) Fail("unpaired surrogate"); code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); } else if (code >= 0xDC00 && code <= 0xDFFF) { Fail("unpaired surrogate"); } AppendUtf8(out, code); break; } default: --position; Fail("unknown escape"); } } } Value Number() { const std::size_t start = position; Consume('-'); if (AtEnd() || !IsDigit(text[position])) Fail("expected a value"); if (!Consume('0')) { while (!AtEnd() && IsDigit(text[position])) ++position; } bool whole = true; if (Consume('.')) { whole = false; if (AtEnd() || !IsDigit(text[position])) Fail("expected a digit after the decimal point"); while (!AtEnd() && IsDigit(text[position])) ++position; } if (Consume('e') || Consume('E')) { whole = false; if (!Consume('+')) Consume('-'); if (AtEnd() || !IsDigit(text[position])) Fail("expected a digit in the exponent"); while (!AtEnd() && IsDigit(text[position])) ++position; } const char* first = text.data() + start; const char* last = text.data() + position; if (whole) { std::int64_t integer = 0; const auto [end, error] = std::from_chars(first, last, integer); if (error == std::errc() && end == last) return Value(integer); // Beyond int64: read as a double below. } double number = 0; const auto [end, error] = std::from_chars(first, last, number); if (error == std::errc::result_out_of_range) { position = start; Fail("number out of range"); } if (error != std::errc() || end != last) { position = start; Fail("malformed number"); } return Value(number); } }; std::logic_error WrongKind(ValueKind actual, std::string_view wanted) { return std::logic_error(std::format("Value is {}, not {}", KindName(actual), wanted)); } } std::string_view Crafter::Debug::KindName(ValueKind kind) { switch (kind) { case ValueKind::Null: return "null"; case ValueKind::Bool: return "boolean"; case ValueKind::Integer: return "integer"; case ValueKind::Number: return "number"; case ValueKind::String: return "string"; case ValueKind::Array: return "array"; case ValueKind::Object: return "object"; } return "unknown"; } ParseError::ParseError(std::size_t offset, std::string_view what) : std::runtime_error(std::format("{} at offset {}", what, offset)), offset(offset) {} Value Value::MakeArray(std::vector items) { Value value; value.kind = ValueKind::Array; value.items = std::move(items); return value; } Value Value::MakeObject() { Value value; value.kind = ValueKind::Object; return value; } bool Value::AsBool() const { if (kind != ValueKind::Bool) throw WrongKind(kind, "a boolean"); return boolean; } std::int64_t Value::AsInteger() const { if (kind != ValueKind::Integer) throw WrongKind(kind, "an integer"); return integer; } double Value::AsNumber() const { if (kind == ValueKind::Integer) return static_cast(integer); if (kind != ValueKind::Number) throw WrongKind(kind, "a number"); return number; } const std::string& Value::AsString() const { if (kind != ValueKind::String) throw WrongKind(kind, "a string"); return text; } std::span Value::Items() const { if (kind != ValueKind::Array) throw WrongKind(kind, "an array"); return items; } std::span Value::Items() { if (kind != ValueKind::Array) throw WrongKind(kind, "an array"); return items; } void Value::Push(Value item) { if (kind == ValueKind::Null) kind = ValueKind::Array; if (kind != ValueKind::Array) throw WrongKind(kind, "an array"); items.push_back(std::move(item)); } const Value* Value::Find(std::string_view key) const { if (kind != ValueKind::Object) return nullptr; const auto found = std::ranges::find(keys, key); return found == keys.end() ? nullptr : &values[static_cast(found - keys.begin())]; } Value* Value::Find(std::string_view key) { return const_cast(std::as_const(*this).Find(key)); } Value& Value::Set(std::string_view key, Value value) { if (kind == ValueKind::Null) kind = ValueKind::Object; if (kind != ValueKind::Object) throw WrongKind(kind, "an object"); if (Value* existing = Find(key)) { *existing = std::move(value); return *existing; } keys.emplace_back(key); values.push_back(std::move(value)); return values.back(); } Value& Value::operator[](std::string_view key) { if (Value* existing = Find(key)) return *existing; return Set(key, Value()); } bool Value::Erase(std::string_view key) { if (kind != ValueKind::Object) return false; const auto found = std::ranges::find(keys, key); if (found == keys.end()) return false; const auto index = found - keys.begin(); keys.erase(found); values.erase(values.begin() + index); return true; } std::size_t Value::Size() const { if (kind == ValueKind::Array) return items.size(); if (kind == ValueKind::Object) return keys.size(); return 0; } Value Value::Parse(std::string_view json) { return Parser(json).Document(); } std::string Value::Dump() const { std::string out; DumpTo(out, 0, 0); return out; } std::string Value::DumpPretty(std::uint32_t indent) const { std::string out; DumpTo(out, indent == 0 ? 1 : indent, 0); return out; } void Value::DumpTo(std::string& out, std::uint32_t indent, std::uint32_t depth) const { const auto newline = [&](std::uint32_t level) { if (indent == 0) return; out += '\n'; out.append(static_cast(indent) * level, ' '); }; switch (kind) { case ValueKind::Null: out += "null"; return; case ValueKind::Bool: out += boolean ? "true" : "false"; return; case ValueKind::Integer: out += std::to_string(integer); return; case ValueKind::Number: AppendNumber(out, number); return; case ValueKind::String: AppendEscaped(out, text); return; case ValueKind::Array: if (items.empty()) { out += "[]"; return; } out += '['; for (std::size_t i = 0; i < items.size(); ++i) { if (i > 0) out += ','; newline(depth + 1); items[i].DumpTo(out, indent, depth + 1); } newline(depth); out += ']'; return; case ValueKind::Object: if (keys.empty()) { out += "{}"; return; } out += '{'; for (std::size_t i = 0; i < keys.size(); ++i) { if (i > 0) out += ','; newline(depth + 1); AppendEscaped(out, keys[i]); out += indent == 0 ? ":" : ": "; values[i].DumpTo(out, indent, depth + 1); } newline(depth); out += '}'; return; } } namespace Crafter::Debug { bool operator==(const Value& a, const Value& b) { if (a.IsNumber() && b.IsNumber()) { if (a.kind == ValueKind::Integer && b.kind == ValueKind::Integer) return a.integer == b.integer; return a.AsNumber() == b.AsNumber(); } if (a.kind != b.kind) return false; switch (a.kind) { case ValueKind::Null: return true; case ValueKind::Bool: return a.boolean == b.boolean; case ValueKind::String: return a.text == b.text; case ValueKind::Array: return a.items == b.items; case ValueKind::Object: if (a.keys.size() != b.keys.size()) return false; for (std::size_t i = 0; i < a.keys.size(); ++i) { const Value* other = b.Find(a.keys[i]); if (other == nullptr || !(a.values[i] == *other)) return false; } return true; default: return false; } } }