diff options
Diffstat (limited to 'src/json.cpp')
| -rw-r--r-- | src/json.cpp | 529 |
1 files changed, 529 insertions, 0 deletions
diff --git a/src/json.cpp b/src/json.cpp new file mode 100644 index 0000000..4d8bf03 --- /dev/null +++ b/src/json.cpp @@ -0,0 +1,529 @@ +#include "cepis/json.hpp" + +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <string> + +namespace cepis { +namespace { + +const std::string& empty_string() { + static const std::string value; + return value; +} + +const Json::Array& empty_array() { + static const Json::Array value; + return value; +} + +const Json::Object& empty_object() { + static const Json::Object value; + return value; +} + +void append_utf8(std::string& out, std::uint32_t code_point) { + if (code_point <= 0x7F) { + out.push_back(static_cast<char>(code_point)); + } else if (code_point <= 0x7FF) { + out.push_back(static_cast<char>(0xC0 | (code_point >> 6))); + out.push_back(static_cast<char>(0x80 | (code_point & 0x3F))); + } else if (code_point <= 0xFFFF) { + out.push_back(static_cast<char>(0xE0 | (code_point >> 12))); + out.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3F))); + out.push_back(static_cast<char>(0x80 | (code_point & 0x3F))); + } else { + out.push_back(static_cast<char>(0xF0 | (code_point >> 18))); + out.push_back(static_cast<char>(0x80 | ((code_point >> 12) & 0x3F))); + out.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3F))); + out.push_back(static_cast<char>(0x80 | (code_point & 0x3F))); + } +} + +class Parser { + public: + explicit Parser(std::string_view text) : text_(text) {} + + std::optional<Json> run() { + skip_whitespace(); + std::optional<Json> value = parse_value(0); + if (!value) { + return std::nullopt; + } + skip_whitespace(); + if (pos_ != text_.size()) { + return std::nullopt; + } + return value; + } + + private: + static constexpr int kMaxDepth = 64; + + [[nodiscard]] bool at_end() const { return pos_ >= text_.size(); } + [[nodiscard]] char peek() const { return text_[pos_]; } + + void skip_whitespace() { + while (!at_end()) { + const char c = peek(); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + ++pos_; + } else { + break; + } + } + } + + bool consume(char expected) { + if (at_end() || peek() != expected) { + return false; + } + ++pos_; + return true; + } + + bool consume(std::string_view literal) { + if (text_.compare(pos_, literal.size(), literal) != 0) { + return false; + } + pos_ += literal.size(); + return true; + } + + std::optional<Json> parse_value(int depth) { + if (depth > kMaxDepth || at_end()) { + return std::nullopt; + } + switch (peek()) { + case '{': + return parse_object(depth); + case '[': + return parse_array(depth); + case '"': { + std::optional<std::string> text = parse_string(); + if (!text) { + return std::nullopt; + } + return Json::string(std::move(*text)); + } + case 't': + return consume("true") ? std::optional<Json>{Json::boolean(true)} : std::nullopt; + case 'f': + return consume("false") ? std::optional<Json>{Json::boolean(false)} : std::nullopt; + case 'n': + return consume("null") ? std::optional<Json>{Json{}} : std::nullopt; + default: + return parse_number(); + } + } + + std::optional<Json> parse_object(int depth) { + ++pos_; + Json result = Json::object(); + skip_whitespace(); + if (consume('}')) { + return result; + } + while (true) { + skip_whitespace(); + if (at_end() || peek() != '"') { + return std::nullopt; + } + std::optional<std::string> key = parse_string(); + if (!key) { + return std::nullopt; + } + skip_whitespace(); + if (!consume(':')) { + return std::nullopt; + } + skip_whitespace(); + std::optional<Json> value = parse_value(depth + 1); + if (!value) { + return std::nullopt; + } + result.set(std::move(*key), std::move(*value)); + skip_whitespace(); + if (consume(',')) { + continue; + } + if (consume('}')) { + return result; + } + return std::nullopt; + } + } + + std::optional<Json> parse_array(int depth) { + ++pos_; + Json result = Json::array(); + skip_whitespace(); + if (consume(']')) { + return result; + } + while (true) { + skip_whitespace(); + std::optional<Json> value = parse_value(depth + 1); + if (!value) { + return std::nullopt; + } + result.push_back(std::move(*value)); + skip_whitespace(); + if (consume(',')) { + continue; + } + if (consume(']')) { + return result; + } + return std::nullopt; + } + } + + std::optional<std::string> parse_string() { + ++pos_; + std::string out; + while (true) { + if (at_end()) { + return std::nullopt; + } + const char c = text_[pos_++]; + if (c == '"') { + return out; + } + if (static_cast<unsigned char>(c) < 0x20) { + return std::nullopt; + } + if (c != '\\') { + out.push_back(c); + continue; + } + if (at_end()) { + return std::nullopt; + } + const char escape = text_[pos_++]; + switch (escape) { + case '"': + out.push_back('"'); + break; + case '\\': + out.push_back('\\'); + break; + case '/': + out.push_back('/'); + 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': { + std::optional<std::uint32_t> unit = parse_hex4(); + if (!unit) { + return std::nullopt; + } + std::uint32_t code_point = *unit; + if (code_point >= 0xD800 && code_point <= 0xDBFF) { + if (text_.compare(pos_, 2, "\\u") == 0) { + const std::size_t saved = pos_; + pos_ += 2; + std::optional<std::uint32_t> low = parse_hex4(); + if (low && *low >= 0xDC00 && *low <= 0xDFFF) { + code_point = 0x10000 + ((code_point - 0xD800) << 10) + (*low - 0xDC00); + } else { + pos_ = saved; + code_point = 0xFFFD; + } + } else { + code_point = 0xFFFD; + } + } else if (code_point >= 0xDC00 && code_point <= 0xDFFF) { + code_point = 0xFFFD; + } + append_utf8(out, code_point); + break; + } + default: + return std::nullopt; + } + } + } + + std::optional<std::uint32_t> parse_hex4() { + if (pos_ + 4 > text_.size()) { + return std::nullopt; + } + std::uint32_t value = 0; + for (int i = 0; i < 4; ++i) { + const char c = text_[pos_++]; + value <<= 4; + if (c >= '0' && c <= '9') { + value |= static_cast<std::uint32_t>(c - '0'); + } else if (c >= 'a' && c <= 'f') { + value |= static_cast<std::uint32_t>(c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + value |= static_cast<std::uint32_t>(c - 'A' + 10); + } else { + return std::nullopt; + } + } + return value; + } + + std::optional<Json> parse_number() { + const std::size_t start = pos_; + if (!at_end() && peek() == '-') { + ++pos_; + } + if (at_end() || !is_digit(peek())) { + return std::nullopt; + } + if (peek() == '0') { + ++pos_; + } else { + while (!at_end() && is_digit(peek())) { + ++pos_; + } + } + if (!at_end() && peek() == '.') { + ++pos_; + if (at_end() || !is_digit(peek())) { + return std::nullopt; + } + while (!at_end() && is_digit(peek())) { + ++pos_; + } + } + if (!at_end() && (peek() == 'e' || peek() == 'E')) { + ++pos_; + if (!at_end() && (peek() == '+' || peek() == '-')) { + ++pos_; + } + if (at_end() || !is_digit(peek())) { + return std::nullopt; + } + while (!at_end() && is_digit(peek())) { + ++pos_; + } + } + const std::string literal(text_.substr(start, pos_ - start)); + return Json::number(std::strtod(literal.c_str(), nullptr)); + } + + static bool is_digit(char c) { return c >= '0' && c <= '9'; } + + std::string_view text_; + std::size_t pos_ = 0; +}; + +void dump_string(const std::string& value, std::string& out) { + out.push_back('"'); + for (const char c : value) { + 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 (static_cast<unsigned char>(c) < 0x20) { + char buffer[7]; + std::snprintf(buffer, sizeof(buffer), "\\u%04x", static_cast<unsigned char>(c)); + out += buffer; + } else { + out.push_back(c); + } + } + } + out.push_back('"'); +} + +void dump_number(double value, std::string& out) { + if (!std::isfinite(value)) { + out += "null"; + return; + } + if (value == static_cast<double>(static_cast<std::int64_t>(value)) && std::fabs(value) < 9.0e15) { + out += std::to_string(static_cast<std::int64_t>(value)); + return; + } + char buffer[40]; + for (const int digits : {15, 16, 17}) { + std::snprintf(buffer, sizeof(buffer), "%.*g", digits, value); + if (std::strtod(buffer, nullptr) == value) { + break; + } + } + out += buffer; +} + +void dump_value(const Json& value, std::string& out) { + switch (value.kind()) { + case Json::Kind::Null: + out += "null"; + break; + case Json::Kind::Bool: + out += value.as_bool() ? "true" : "false"; + break; + case Json::Kind::Number: + dump_number(value.as_number(), out); + break; + case Json::Kind::String: + dump_string(value.as_string(), out); + break; + case Json::Kind::Array: { + out.push_back('['); + bool first = true; + for (const Json& element : value.as_array()) { + if (!first) { + out.push_back(','); + } + first = false; + dump_value(element, out); + } + out.push_back(']'); + break; + } + case Json::Kind::Object: { + out.push_back('{'); + bool first = true; + for (const auto& [key, member] : value.as_object()) { + if (!first) { + out.push_back(','); + } + first = false; + dump_string(key, out); + out.push_back(':'); + dump_value(member, out); + } + out.push_back('}'); + break; + } + } +} + +} // namespace + +Json Json::boolean(bool value) { + Json result; + result.kind_ = Kind::Bool; + result.bool_ = value; + return result; +} + +Json Json::number(double value) { + Json result; + result.kind_ = Kind::Number; + result.number_ = value; + return result; +} + +Json Json::string(std::string value) { + Json result; + result.kind_ = Kind::String; + result.string_ = std::move(value); + return result; +} + +Json Json::array(Array value) { + Json result; + result.kind_ = Kind::Array; + result.array_ = std::move(value); + return result; +} + +Json Json::object(Object value) { + Json result; + result.kind_ = Kind::Object; + result.object_ = std::move(value); + return result; +} + +std::optional<Json> Json::parse(std::string_view text) { return Parser(text).run(); } + +std::string Json::dump() const { + std::string out; + dump_value(*this, out); + return out; +} + +bool Json::as_bool(bool fallback) const noexcept { return kind_ == Kind::Bool ? bool_ : fallback; } + +double Json::as_number(double fallback) const noexcept { + return kind_ == Kind::Number ? number_ : fallback; +} + +const std::string& Json::as_string() const noexcept { + return kind_ == Kind::String ? string_ : empty_string(); +} + +const Json::Array& Json::as_array() const noexcept { + return kind_ == Kind::Array ? array_ : empty_array(); +} + +const Json::Object& Json::as_object() const noexcept { + return kind_ == Kind::Object ? object_ : empty_object(); +} + +const Json* Json::find(std::string_view key) const noexcept { + if (kind_ != Kind::Object) { + return nullptr; + } + for (const auto& member : object_) { + if (member.first == key) { + return &member.second; + } + } + return nullptr; +} + +void Json::set(std::string key, Json value) { + if (kind_ != Kind::Object) { + kind_ = Kind::Object; + object_.clear(); + } + for (auto& member : object_) { + if (member.first == key) { + member.second = std::move(value); + return; + } + } + object_.emplace_back(std::move(key), std::move(value)); +} + +void Json::push_back(Json value) { + if (kind_ != Kind::Array) { + kind_ = Kind::Array; + array_.clear(); + } + array_.push_back(std::move(value)); +} + +} // namespace cepis |