507 lines
18 KiB
C++
507 lines
18 KiB
C++
|
|
// 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<char>(code);
|
||
|
|
} else if (code < 0x800) {
|
||
|
|
out += static_cast<char>(0xC0 | (code >> 6));
|
||
|
|
out += static_cast<char>(0x80 | (code & 0x3F));
|
||
|
|
} else if (code < 0x10000) {
|
||
|
|
out += static_cast<char>(0xE0 | (code >> 12));
|
||
|
|
out += static_cast<char>(0x80 | ((code >> 6) & 0x3F));
|
||
|
|
out += static_cast<char>(0x80 | (code & 0x3F));
|
||
|
|
} else {
|
||
|
|
out += static_cast<char>(0xF0 | (code >> 18));
|
||
|
|
out += static_cast<char>(0x80 | ((code >> 12) & 0x3F));
|
||
|
|
out += static_cast<char>(0x80 | ((code >> 6) & 0x3F));
|
||
|
|
out += static_cast<char>(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<std::uint8_t>(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<std::uint8_t>(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<char>(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<char, 32> buffer{};
|
||
|
|
const auto [end, error] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), number);
|
||
|
|
const std::string_view digits(buffer.data(), static_cast<std::size_t>(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<std::uint8_t>(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<Value> 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<double>(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<const Value> Value::Items() const {
|
||
|
|
if (kind != ValueKind::Array) throw WrongKind(kind, "an array");
|
||
|
|
return items;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::span<Value> 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<std::size_t>(found - keys.begin())];
|
||
|
|
}
|
||
|
|
|
||
|
|
Value* Value::Find(std::string_view key) {
|
||
|
|
return const_cast<Value*>(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<std::size_t>(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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|