// A minimal JSON reader for the test tools (reads the owner's designs JSON). // Header-only, no dependencies, no exceptions: parse() returns false and an // error message on malformed input. Numbers are kept as double plus the text. #pragma once #include #include #include #include #include #include namespace json_min { struct Value { enum class Kind { Null, Bool, Number, String, Array, Object }; Kind kind = Kind::Null; bool b = false; double num = 0; std::string text; // string value or number text std::vector items; // array std::vector> members; // object, file order bool is_null() const { return kind == Kind::Null; } bool is_string() const { return kind == Kind::String; } bool is_number() const { return kind == Kind::Number; } bool is_array() const { return kind == Kind::Array; } bool is_object() const { return kind == Kind::Object; } bool is_bool() const { return kind == Kind::Bool; } const Value* get(std::string_view key) const { for (const auto& m : members) if (m.first == key) return &m.second; return nullptr; } const Value& operator[](std::string_view key) const { static const Value null; const Value* v = get(key); return v ? *v : null; } const std::string& str() const { return text; } int as_int() const { return static_cast(num); } bool as_bool() const { return kind == Kind::Bool ? b : (kind == Kind::Number ? num != 0 : false); } }; class Parser { public: explicit Parser(std::string_view s) : s_(s) {} bool parse(Value& out, std::string& err) { skip(); if (!value(out, err)) return false; skip(); if (pos_ != s_.size()) return fail(err, "trailing characters"); return true; } private: std::string_view s_; std::size_t pos_ = 0; bool fail(std::string& err, const char* what) { err = std::string(what) + " at offset " + std::to_string(pos_); return false; } void skip() { while (pos_ < s_.size() && (s_[pos_] == ' ' || s_[pos_] == '\n' || s_[pos_] == '\r' || s_[pos_] == '\t')) ++pos_; } bool literal(const char* lit) { std::string_view l(lit); if (s_.substr(pos_, l.size()) != l) return false; pos_ += l.size(); return true; } static void put_utf8(std::string& out, unsigned cp) { if (cp < 0x80) { out += static_cast(cp); } else if (cp < 0x800) { out += static_cast(0xC0 | (cp >> 6)); out += static_cast(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { out += static_cast(0xE0 | (cp >> 12)); out += static_cast(0x80 | ((cp >> 6) & 0x3F)); out += static_cast(0x80 | (cp & 0x3F)); } else { out += static_cast(0xF0 | (cp >> 18)); out += static_cast(0x80 | ((cp >> 12) & 0x3F)); out += static_cast(0x80 | ((cp >> 6) & 0x3F)); out += static_cast(0x80 | (cp & 0x3F)); } } bool hex4(unsigned& cp) { if (pos_ + 4 > s_.size()) return false; cp = 0; for (int i = 0; i < 4; ++i) { char c = s_[pos_++]; cp <<= 4; if (c >= '0' && c <= '9') cp |= static_cast(c - '0'); else if (c >= 'a' && c <= 'f') cp |= static_cast(c - 'a' + 10); else if (c >= 'A' && c <= 'F') cp |= static_cast(c - 'A' + 10); else return false; } return true; } bool string(std::string& out, std::string& err) { if (pos_ >= s_.size() || s_[pos_] != '"') return fail(err, "expected string"); ++pos_; while (pos_ < s_.size()) { char c = s_[pos_++]; if (c == '"') return true; if (c != '\\') { out += c; continue; } if (pos_ >= s_.size()) break; char e = s_[pos_++]; switch (e) { 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': { unsigned cp; if (!hex4(cp)) return fail(err, "bad \\u escape"); if (cp >= 0xD800 && cp < 0xDC00 && literal("\\u")) { unsigned lo; if (!hex4(lo)) return fail(err, "bad surrogate"); cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } put_utf8(out, cp); break; } default: return fail(err, "bad escape"); } } return fail(err, "unterminated string"); } bool value(Value& v, std::string& err) { if (pos_ >= s_.size()) return fail(err, "unexpected end"); char c = s_[pos_]; if (c == '{') { ++pos_; v.kind = Value::Kind::Object; skip(); if (literal("}")) return true; for (;;) { skip(); std::string key; if (!string(key, err)) return false; skip(); if (!literal(":")) return fail(err, "expected ':'"); skip(); Value m; if (!value(m, err)) return false; v.members.emplace_back(std::move(key), std::move(m)); skip(); if (literal(",")) continue; if (literal("}")) return true; return fail(err, "expected ',' or '}'"); } } if (c == '[') { ++pos_; v.kind = Value::Kind::Array; skip(); if (literal("]")) return true; for (;;) { skip(); Value item; if (!value(item, err)) return false; v.items.push_back(std::move(item)); skip(); if (literal(",")) continue; if (literal("]")) return true; return fail(err, "expected ',' or ']'"); } } if (c == '"') { v.kind = Value::Kind::String; return string(v.text, err); } if (literal("null")) return true; if (literal("true")) { v.kind = Value::Kind::Bool; v.b = true; return true; } if (literal("false")) { v.kind = Value::Kind::Bool; v.b = false; return true; } std::size_t start = pos_; while (pos_ < s_.size()) { char d = s_[pos_]; if ((d >= '0' && d <= '9') || d == '-' || d == '+' || d == '.' || d == 'e' || d == 'E') ++pos_; else break; } if (start == pos_) return fail(err, "unexpected character"); v.kind = Value::Kind::Number; v.text = std::string(s_.substr(start, pos_ - start)); v.num = std::strtod(v.text.c_str(), nullptr); return true; } }; inline bool parse(std::string_view text, Value& out, std::string& err) { return Parser(text).parse(out, err); } // Writer helpers for the dump tool. inline std::string quote(std::string_view s) { std::string out = "\""; for (unsigned char c : s) { switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (c < 0x20) { char buf[8]; std::snprintf(buf, sizeof buf, "\\u%04x", c); out += buf; } else { out += static_cast(c); } } } return out + "\""; } } // namespace json_min