From e62e4cf0d07f5175066d5870286df059d2502eba Mon Sep 17 00:00:00 2001 From: Marcus O'Flaherty Date: Wed, 24 Jun 2026 13:00:11 +0100 Subject: [PATCH] pull evgenii's changes to Json class and update BStore signature as required --- src/Store/BStore.cpp | 2 +- src/Store/BStore.h | 2 +- src/Store/Json.cpp | 440 ++++++++++++++++++++++++++++++++++++++++++- src/Store/Json.h | 392 ++++++++++++++++++++++++++++++++------ 4 files changed, 772 insertions(+), 64 deletions(-) diff --git a/src/Store/BStore.cpp b/src/Store/BStore.cpp index 5d52515..7ce91cf 100644 --- a/src/Store/BStore.cpp +++ b/src/Store/BStore.cpp @@ -957,7 +957,7 @@ BStore::~BStore(){ } namespace ToolFramework { - bool json_encode(std::ostream& output, const BStore& store) { + bool json_encode_r(std::ostream& output, const BStore& store, adl_tag) { return store.JsonEncode(output); } } diff --git a/src/Store/BStore.h b/src/Store/BStore.h index 34b9d1d..b45e540 100644 --- a/src/Store/BStore.h +++ b/src/Store/BStore.h @@ -258,7 +258,7 @@ namespace ToolFramework{ }; - bool json_encode(std::ostream&, const BStore&); + bool json_encode_r(std::ostream&, const BStore&, adl_tag); } #endif diff --git a/src/Store/Json.cpp b/src/Store/Json.cpp index f3ecb43..b22ab30 100644 --- a/src/Store/Json.cpp +++ b/src/Store/Json.cpp @@ -1,20 +1,444 @@ +#include + +#include +#include + #include namespace ToolFramework { -bool json_encode(std::ostream& output, const char* datum) { +static bool json_encode( + std::ostream& output, + const char* begin, + const char* end +) { output << '"'; - while (*datum) { - if (*datum == '"' || *datum == '\\') output << '\\'; - output << *datum; - ++datum; - }; + for (auto c = begin; c < end; ++c) + switch (*c) { + case '\a': + output << "\\a"; + break; + case '\b': + output << "\\b"; + break; + case '\f': + output << "\\f"; + break; + case '\n': + output << "\\n"; + break; + case '\r': + output << "\\r"; + break; + case '\t': + output << "\\t"; + break; + case '\\': + case '"': + output << '\\' << *c; + break; + default: + { + uint8_t byte = static_cast(*c); + if (byte < 0x20 || byte > 0x7F) { + uint16_t codepoint = byte; + if (++c != end) + codepoint = codepoint << 8 | static_cast(*c); + output + << "\\u" + << std::setw(4) + << std::setfill('0') + << std::hex + << codepoint + << std::dec; + } else + output << byte; + }; + }; output << '"'; return true; } -bool json_encode(std::ostream& output, const std::string& datum) { - return json_encode(output, datum.c_str()); +bool json_encode( + std::ostream& output, + std::string::const_iterator begin, + std::string::const_iterator end +) { + return json_encode(output, &*begin, &*end); +} + +bool json_encode_r(std::ostream& output, const std::string& datum, adl_tag) { + return json_encode(output, datum.begin(), datum.end()); +} + +bool json_encode_r(std::ostream& output, const char* datum, adl_tag) { + return json_encode(output, datum, datum + strlen(datum)); +} + +const char* json_scan_whitespace(const char* input) { + if (!input) return nullptr; + while (isspace(*input)) ++input; + return input; +} + +const char* json_scan_token(const char* input, char token) { + input = json_scan_whitespace(input); + return input && *input == token ? input + 1 : nullptr; +} + +const char* json_scan_token(const char* input, const char* token) { + input = json_scan_whitespace(input); + if (!input) return nullptr; + while (*token) if (*input++ != *token++) return nullptr; + return isalnum(*input) ? nullptr : input; +} + +const char* json_scan_number(const char* input) { + input = json_scan_whitespace(input); + if (!input) return nullptr; + + if (*input == '-' || *input == '+') ++input; + while (isdigit(*input)) ++input; + if (*input == '.') { + ++input; + while (isdigit(*input)) ++input; + }; + if (tolower(*input) == 'e') { + ++input; + if (*input == '-' || *input == '+') ++input; + while (isdigit(*input)) ++input; + }; + + return isalnum(*input) ? nullptr : input; +} + +const char* json_scan_string(const char* input) { + input = json_scan_token(input, '"'); + if (!input) return nullptr; + + bool escaped = false; + for (; *input; ++input) + if (escaped) + escaped = false; + else + switch (*input) { + case '\\': + escaped = true; + break; + case '"': + return input + 1; + break; + default: + break; + }; + + return nullptr; +} + +static const char* json_scan_list( + const char* input, + char start, + char end, + const char* (*scan_item)(const char*) +) { + bool comma = false; + input = json_scan_token(input, start); + while (input) { + input = json_scan_whitespace(input); + if (*input == end) return input + 1; + + if (comma) { + if (*input++ != ',') return nullptr; + } else + comma = true; + + input = scan_item(input); + }; + return nullptr; +} + +const char* json_scan_object(const char* input) { + return json_scan_list( + input, '{', '}', + [](const char* i) -> const char* { + i = json_scan_string(i); + i = json_scan_token(i, ':'); + return json_scan(i); + } + ); +} + +const char* json_scan_array(const char* input) { + return json_scan_list(input, '[', ']', json_scan); +} + +const char* json_scan(const char* input) { + input = json_scan_whitespace(input); + if (!input) return nullptr; + switch (*input) { + case 'f': + return json_scan_token(input, "false"); + case 't': + return json_scan_token(input, "true"); + case 'n': + return json_scan_token(input, "null"); + case '-': + case '+': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return json_scan_number(input); + case '"': + return json_scan_string(input); + case '{': + return json_scan_object(input); + case '[': + return json_scan_array(input); + default: + return nullptr; + }; +} + +bool json_valid(const char* input) { + input = json_scan(input); + input = json_scan_whitespace(input); + return input && !*input; +}; + +bool json_decode_r(const char*& input, bool& value, adl_tag) { + const char* i = json_scan_whitespace(input); + if (!i) return false; + + switch (*i) { + case 'f': + i = json_scan_token(i, "false"); + if (!i) return false; + value = false; + break; + case 't': + i = json_scan_token(i, "true"); + if (!i) return false; + value = true; + break; + case '0': + if (isalnum(*++i)) return false; + value = false; + break; + case '1': + if (isalnum(*++i)) return false; + value = true; + break; + default: + return false; + }; + + input = i; + return true; +} + +namespace json_internal { + +bool json_decode_string( + const char*& input, + std::string& value, + bool keep_quotes +) { + const char* i = json_scan_token(input, '"'); + if (!i) return false; + + std::stringstream ss; + if (keep_quotes) ss << '"'; + + bool escaped = false; + for (; *i; ++i) { + if (escaped) { + escaped = false; + switch (*i) { + case '\\': + ss << '\\'; + break; + case 'a': + ss << '\a'; + break; + case 'b': + ss << '\b'; + break; + case 'f': + ss << '\f'; + break; + case 'n': + ss << '\n'; + break; + case 'r': + ss << '\r'; + break; + case 't': + ss << '\t'; + break; + case 'u': + { + uint16_t codepoint = 0; + for (int b = 0; b < 2; ++b) { + for (int d = 0; d < 2; ++d) { + char digit = *++i; + if (digit >= '0' && digit <= '9') + digit -= '0'; + else if (digit >= 'a' && digit <= 'f') + digit = digit - 'a' + 10; + else if (digit >= 'A' && digit <= 'F') + digit = digit - 'A' + 10; + else + return false; // TODO: invalid string + codepoint = codepoint << 4 | digit; + }; + }; + if (codepoint > 0xFF) ss << static_cast(codepoint >> 8); + ss << static_cast(codepoint & 0xFF); + }; + break; + default: + // TODO: invalid escape sequence. Store anyway? + ss << *i; + }; + } else { + switch (*i) { + case '\\': + escaped = true; + continue; + case '"': + if (keep_quotes) ss << '"'; + input = ++i; + value = ss.str(); + return true; + default: + ss << *i; + }; + }; + }; + + return false; +} + +} + +bool json_decode_r(const char*& input, std::string& value, adl_tag) { + return json_internal::json_decode_string(input, value, false); +} + +static bool json_decode_list( + const char*& input, + char start, + char end, + const std::function decode_item +) { + const char* i = json_scan_token(input, start); + if (!i) return false; + + bool comma = false; + while (true) { + i = json_scan_whitespace(i); + if (*i == end) break; + + if (comma) { + if (*i++ != ',') return false; + } else + comma = true; + + if (!decode_item(i)) return false; + }; + + input = i + 1; + return true; +} + +bool json_decode_array( + const char*& input, + const std::function& decode_item +) { + return json_decode_list(input, '[', ']', decode_item); +} + +bool json_decode_object( + const char*& input, + const std::function& decode_value +) { + return json_decode_list( + input, '{', '}', + [&](const char*& input_) -> bool { + const char* i = input_; + std::string key; + if (!json_decode_r(i, key, adl_tag {})) return false; + i = json_scan_token(i, ':'); + if (!i) return false; + if (!decode_value(i, std::move(key))) return false; + input_ = i; + return true; + } + ); +} + +// ================ + +bool JsonParser::Parse(const char* input) { + if (!json_valid(input)) return false; // invalid JSON string + + // We have validated input, so in the following `return false` should never + // happen. We keep it, however, just in case. + bool result = json_decode_object( + input, + [this](const char*& input_, std::string key) -> bool { + printf("key: '%s', value: '%s'\n",key.c_str(),input_); + const char* i = input_; + i = json_scan_whitespace(i); + switch (*i) { + case '"': + { + std::string value; + if (!json_internal::json_decode_string(i, value, true)) + return false; + m_variables[std::move(key)] = std::move(value); + }; + break; + case 'f': + i = json_scan_token(i, "false"); + if (!i) return false; + m_variables[std::move(key)] = "0"; + break; + case 't': + i = json_scan_token(i, "true"); + if (!i) return false; + m_variables[std::move(key)] = "1"; + break; + case 'n': + i = json_scan_token(i, "null"); + if (!i) return false; + m_variables[std::move(key)] = "0"; + break; + default: + const char* value = i; + i = json_scan(i); + if (!i) return false; + m_variables[std::move(key)] = std::string(value, i); + }; + printf("outgoing input_ is: '%s', i is '%s'\n",input_, i); + input_ = i; + return true; + } + ); + if (!result) return false; + + return true; +} + +bool JsonParser::Parse(const std::string& input) { + return Parse(input.c_str()); } } diff --git a/src/Store/Json.h b/src/Store/Json.h index 2cb23c1..65f34fe 100644 --- a/src/Store/Json.h +++ b/src/Store/Json.h @@ -1,82 +1,175 @@ #ifndef TOOLFRAMEWORK_JSON_H #define TOOLFRAMEWORK_JSON_H +#include #include #include #include #include #include +#include #include + +/** + This file provides functions working with JSON. + + Three families of functions are provided: json_encode for JSON + serialization, json_scan for JSON validation, and json_decode for JSON + parsing. +*/ + namespace ToolFramework { +/** + JSON serialization functions + + json_encode comes in two flavours: json_encode(std::ostream&, const T&) + serializes a value into a stream, json_encode(std::string&, const T&) + serializes a value into a string. The latter calls the former, and the + former calls json_encode_r to encode specific objects. Provide overloads of + json_encode_r function or template to extend for custom classes. + + json_encode returns false if encoding has failed; for the function working + with a stream it can only happen in user extensions, functions in this file + always return true. json_encode(std::string&, const T&) checks the stream + flags. +*/ + +template bool json_encode(std::ostream& output, const T& datum); +template bool json_encode(std::string& output, const T& datum); + +// Encode a part of a string as a string +bool json_encode( + std::ostream& output, + std::string::const_iterator begin, + std::string::const_iterator end +); + +// A helper function to write fixed-size objects +// Example: call `json_encode_object(output, "x", 42, "a", false)` +// to produce `{"x":42,"a":false}` +template +bool json_encode_object(std::ostream& output, Fields... fields); + +/* + JSON decoder function + Use this if you know the structure of JSON data in advance + + Returns false if the JSON string is invalid. + If the object was decoded successfully, input is set one character past it, + otherwise it is left unchanged. +*/ +template bool json_decode(const char*& input, T& value); + +// Expects an array in input, calls decode_item(item) on each array item +bool json_decode_array( + const char*& input, + const std::function& decode_item +); + +// Expects an object in input, calls decode_value(key, value) on each key-value pair +bool json_decode_object( + const char*& input, + const std::function& + decode_value +); + +// A class to trigger argument-dependent lookup (ADL) in json_encode_r and +// json_decode_r functions. It is required for the compiler to find overloads +// declared after this file. See +// https://akrzemi1.wordpress.com/2016/01/16/a-customizable-framework/ +// for caveats. +struct adl_tag {}; + + +/** + JSON scanner functions + + Scanner functions find the end of an object without further interpretation. + + Each function returns nullptr if the JSON string is invalid. + It is okay to pass nullptr as input. +*/ +const char* json_scan_whitespace(const char* input); +const char* json_scan_token(const char* input, char token); +const char* json_scan_token(const char* input, const char* token); +const char* json_scan_number(const char* input); +const char* json_scan_string(const char* input); +const char* json_scan_object(const char* input); +const char* json_scan_array(const char* input); +const char* json_scan(const char* input); // generic + +bool json_valid(const char* input); + + +// json_encode_r implements encoding of specific objects. Add overloads to this +// function to support custom classes. + +template +typename std::enable_if::value, bool>::type +json_encode_r(std::ostream& output, T datum, adl_tag); + +bool json_encode_r(std::ostream& output, const std::string& datum, adl_tag); + +bool json_encode_r(std::ostream& output, const char* datum, adl_tag); + +template +bool json_encode_r(std::ostream& output, const T* data, size_t size, adl_tag); + template -bool json_encode(std::ostream&, const std::array&); +bool json_encode_r(std::ostream&, const std::array&, adl_tag); template -bool json_encode(std::ostream&, const std::vector&); +bool json_encode_r(std::ostream&, const std::vector&, adl_tag); template -bool json_encode(std::ostream&, const std::map&); +bool json_encode_r(std::ostream&, const std::map&, adl_tag); + +// json_decode_r implements decoding of specific objects. Add overloads to this +// function to support custom classes. + +bool json_decode_r(const char*& input, bool& value, adl_tag); template -typename std::enable_if::value, bool>::type -json_encode(std::ostream& output, T datum) { - output << datum; - return true; -} +typename std::enable_if< + std::is_integral::value && std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value, adl_tag); -bool json_encode(std::ostream& output, const char* datum); -bool json_encode(std::ostream& output, const std::string& datum); +template +typename std::enable_if< + std::is_integral::value && !std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value); template -bool json_encode(std::ostream& output, const T* data, size_t size) { - output << '['; - bool comma = false; - for (size_t i = 0; i < size; ++i) { - if (comma) - output << ','; - comma = true; - if (!json_encode(output, data[i])) return false; - }; - output << ']'; - return true; -} +typename std::enable_if::value, bool>::type +json_decode_r(const char*& input, T& value, adl_tag); -template -bool json_encode(std::ostream& output, const std::array& array) { - return json_encode(output, array.data(), array.size()); -} +bool json_decode_r(const char*& input, std::string& value, adl_tag); template -bool json_encode(std::ostream& output, const std::vector& vector) { - return json_encode(output, vector.data(), vector.size()); -} +bool json_decode_r(const char*& input, std::vector& value, adl_tag); template -bool json_encode(std::ostream& output, const std::map& data) { - output << '{'; - bool comma = false; - for (auto& datum : data) { - if (comma) - output << ','; - else - comma = true; - if (!json_encode(output, datum.first)) return false; - output << ':'; - if (!json_encode(output, datum.second)) return false; - }; - output << '}'; - return true; -} +bool json_decode_r( + const char*& input, + std::map& value, + adl_tag +); template -bool json_encode(std::string& output, T data) { - std::stringstream ss; - if (!json_encode(ss, data)) return false; - output = ss.str(); - return true; -} +bool json_decode_r( + const char*& input, + std::unordered_map& value, + adl_tag +); + + +// Implementation namespace json_internal { @@ -95,7 +188,7 @@ namespace json_internal { if (comma) output << ','; comma = true; output << '"' << name << '"' << ':'; - if (!json_encode(output, slot)) return false; + if (!json_encode_r(output, slot, adl_tag {})) return false; return json_encode_object_slots(output, comma, rest...); } @@ -110,12 +203,27 @@ namespace json_internal { return json_encode_object_slots(output, comma, name.c_str(), slot, rest...); } + bool json_decode_string( + const char*& input, + std::string& value, + bool keep_quotes + ); + } // json_internal +template +bool json_encode(std::ostream& output, const T& datum) { + return json_encode_r(output, datum, adl_tag {}); +} + +template +bool json_encode(std::string& output, const T& datum) { + std::stringstream ss; + if (!json_encode(ss, datum) || !ss) return false; + output = ss.str(); + return true; +} -// A helper function to write fixed-size objects -// Example: call `json_encode_object(output, "x", 42, "a", false)` -// to produce `{"x":42,"a":false}` template bool json_encode_object(std::ostream& output, Args... args) { output << '{'; @@ -126,6 +234,182 @@ bool json_encode_object(std::ostream& output, Args... args) { return true; } +template +bool json_decode(const char*& input, T& value) { + return json_decode_r(input, value, adl_tag {}); +} + +template +typename std::enable_if::value, bool>::type +json_encode_r(std::ostream& output, T datum, adl_tag) { + output << datum; + return true; +} + +template +bool json_encode_r(std::ostream& output, const T* data, size_t size, adl_tag tag) { + output << '['; + bool comma = false; + for (size_t i = 0; i < size; ++i) { + if (comma) + output << ','; + comma = true; + if (!json_encode_r(output, data[i], tag)) return false; + }; + output << ']'; + return true; +} + +template +bool json_encode_r( + std::ostream& output, + const std::array& array, + adl_tag tag +) { + return json_encode_r(output, array.data(), array.size(), tag); +} + +template +bool json_encode_r( + std::ostream& output, + const std::vector& vector, + adl_tag tag +) { + return json_encode_r(output, vector.data(), vector.size(), tag); +} + +template +bool json_encode_r_map(std::ostream& output, const Map& data, adl_tag tag) { + output << '{'; + bool comma = false; + for (auto& datum : data) { + if (comma) + output << ','; + else + comma = true; + if (!json_encode_r(output, datum.first, tag)) return false; + output << ':'; + if (!json_encode_r(output, datum.second, tag)) return false; + }; + output << '}'; + return true; +} + +template +bool json_encode_r( + std::ostream& output, + const std::map& data, + adl_tag tag +) { + return json_encode_r_map, T>(output, data, tag); +} + +template +bool json_encode_r( + std::ostream& output, + const std::unordered_map& data, + adl_tag tag +) { + return json_encode_r_map, T>( + output, data, tag + ); +} + +template +typename std::enable_if< + std::is_integral::value && std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value, adl_tag) { + char* i; + value = strtol(input, &i, 10); + if (isalnum(*i)) return false; + input = i; + return true; +} + +template +typename std::enable_if< + std::is_integral::value && !std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value, adl_tag) { + char* i; + value = strtoul(input, &i, 10); + if (isalnum(*i)) return false; + input = i; + return true; +} + +template +typename std::enable_if::value, bool>::type +json_decode_r(const char*& input, T& value, adl_tag) { + char* i; + value = strtod(input, &i); + if (isalnum(*i)) return false; + input = i; + return true; +} + +template +bool json_decode_r(const char*& input, std::vector& value, adl_tag tag) { + value.clear(); + return json_decode_array( + input, + [&](const char*& i) -> bool { + T item; + if (!json_decode_r(i, item, tag)) return false; + value.push_back(std::move(item)); + return true; + } + ); +} + +template +bool json_decode_r_map(const char*& input, Map& value, adl_tag tag) { + value.clear(); + return json_decode_object( + input, + [&](const char*& i, std::string key) -> bool { + T item; + if (!json_decode_r(i, item, tag)) return false; + value[std::move(key)] = std::move(item); + return true; + } + ); +} + +template +bool json_decode_r( + const char*& input, + std::map& value, + adl_tag tag +) { + return json_decode_r_map, T>(input, value, tag); +} + +template +bool json_decode_r( + const char*& input, + std::unordered_map& value, + adl_tag tag +) { + return json_decode_r_map, T>( + input, value, tag + ); +} + +// =============== +// while these methods aren't merged into Store class, put them in another +class JsonParser { + public: + JsonParser(){}; + ~JsonParser(){}; + bool Parse(const char* input); + bool Parse(const std::string& input); + std::map m_variables; +}; + } // ToolFramework #endif