summaryrefslogtreecommitdiffstats
path: root/src/json.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/json.cpp')
-rw-r--r--src/json.cpp529
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