summaryrefslogtreecommitdiff
path: root/zen/json.h
diff options
context:
space:
mode:
Diffstat (limited to 'zen/json.h')
-rw-r--r--zen/json.h541
1 files changed, 541 insertions, 0 deletions
diff --git a/zen/json.h b/zen/json.h
new file mode 100644
index 00000000..374a3f14
--- /dev/null
+++ b/zen/json.h
@@ -0,0 +1,541 @@
+// *****************************************************************************
+// * This file is part of the FreeFileSync project. It is distributed under *
+// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
+// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
+// *****************************************************************************
+
+#ifndef JSON_H_0187348321748321758934215734
+#define JSON_H_0187348321748321758934215734
+
+#include <zen/string_tools.h>
+
+
+namespace zen
+{
+//https://tools.ietf.org/html/rfc8259
+struct JsonValue
+{
+ enum class Type
+ {
+ null, //
+ boolean, //primitive types
+ number, //
+ string, //
+ object,
+ array,
+ };
+
+ explicit JsonValue() {}
+ explicit JsonValue(Type t) : type(t) {}
+ explicit JsonValue(bool b) : type(Type::boolean), primVal(b ? "true" : "false") {}
+ explicit JsonValue(int64_t num) : type(Type::number), primVal(numberTo<std::string>(num)) {}
+ explicit JsonValue(double num) : type(Type::number), primVal(numberTo<std::string>(num)) {}
+ explicit JsonValue(const std::string& str) : type(Type::string), primVal(str) {}
+
+ Type type = Type::null;
+ std::string primVal; //for primitive types
+ std::map<std::string, std::unique_ptr<JsonValue>> objectVal; //"[...] most implementations of JSON libraries do not accept duplicate keys [...]" => fine!
+ std::vector<std::unique_ptr<JsonValue>> arrayVal;
+};
+
+
+std::string serializeJson(const JsonValue& jval,
+ const std::string& lineBreak = "\r\n",
+ const std::string& indent = " "); //noexcept
+
+
+struct JsonParsingError
+{
+ JsonParsingError(size_t rowNo, size_t colNo) : row(rowNo), col(colNo) {}
+ const size_t row; //beginning with 0
+ const size_t col; //
+};
+JsonValue parseJson(const std::string& stream); //throw JsonParsingError
+
+
+
+//helper functions for JsonValue access:
+inline
+const JsonValue* getChildFromJsonObject(const JsonValue& jvalue, const std::string& name)
+{
+ if (jvalue.type != JsonValue::Type::object)
+ return nullptr;
+
+ auto it = jvalue.objectVal.find(name);
+ if (it == jvalue.objectVal.end())
+ return nullptr;
+
+ return it->second.get();
+}
+
+
+inline
+std::optional<std::string> getPrimitiveFromJsonObject(const JsonValue& jvalue, const std::string& name)
+{
+ if (const JsonValue* childValue = getChildFromJsonObject(jvalue, name))
+ if (childValue->type != JsonValue::Type::object &&
+ childValue->type != JsonValue::Type::array)
+ return childValue->primVal;
+ return std::nullopt;
+}
+
+
+
+
+
+//---------------------- implementation ----------------------
+namespace json_impl
+{
+namespace
+{
+std::string jsonEscape(const std::string& str)
+{
+ std::string output;
+ for (const char c : str)
+ {
+ if (c == '"') output += "\\\""; //escaping mandatory
+ else if (c == '\\') output += "\\\\"; //
+
+ else if (c == '\b') output += "\\b"; //
+ else if (c == '\f') output += "\\f"; //
+ else if (c == '\n') output += "\\n"; //prefer compact escaping
+ else if (c == '\r') output += "\\r"; //
+ else if (c == '\t') output += "\\t"; //
+
+ else if (static_cast<unsigned char>(c) < 32)
+ {
+ const auto hexDigits = hexify(c);
+ output += "\\u00";
+ output += hexDigits.first;
+ output += hexDigits.second;
+ }
+ else
+ output += c;
+ }
+ return output;
+}
+
+
+std::string jsonUnescape(const std::string& str)
+{
+ std::string output;
+ std::basic_string<impl::Char16> utf16Buf;
+
+ auto flushUtf16 = [&]
+ {
+ if (!utf16Buf.empty())
+ {
+ impl::UtfDecoder<impl::Char16> decoder(utf16Buf.c_str(), utf16Buf.size());
+ while (std::optional<impl::CodePoint> cp = decoder.getNext())
+ impl::codePointToUtf<char>(*cp, [&](char c) { output += c; });
+ utf16Buf.clear();
+ }
+ };
+ auto writeOut = [&](char c)
+ {
+ flushUtf16();
+ output += c;
+ };
+
+ for (auto it = str.begin(); it != str.end(); ++it)
+ {
+ const char c = *it;
+
+ if (c == '\\')
+ {
+ ++it;
+ if (it == str.end()) //unexpected end!
+ {
+ writeOut(c);
+ break;
+ }
+
+ const char c2 = *it;
+ if (c2 == '"' ||
+ c2 == '\\' ||
+ c2 == '/')
+ writeOut(c2);
+ else if (c2 == 'b') writeOut('\b');
+ else if (c2 == 'f') writeOut('\f');
+ else if (c2 == 'n') writeOut('\n');
+ else if (c2 == 'r') writeOut('\r');
+ else if (c2 == 't') writeOut('\t');
+
+ else if (c2 == 'u' &&
+ str.end() - it >= 5 &&
+ isHexDigit(it[1]) &&
+ isHexDigit(it[2]) &&
+ isHexDigit(it[3]) &&
+ isHexDigit(it[4]))
+ {
+ utf16Buf += static_cast<impl::Char16>(static_cast<unsigned char>(unhexify(it[1], it[2])) * 256 +
+ static_cast<unsigned char>(unhexify(it[3], it[4])));
+ it += 4;
+ }
+ else //unknown escape sequence!
+ {
+ writeOut(c);
+ writeOut(c2);
+ }
+ }
+ else
+ writeOut(c);
+ }
+ flushUtf16();
+ return output;
+}
+
+
+void serialize(const JsonValue& jval, std::string& stream,
+ const std::string& lineBreak,
+ const std::string& indent,
+ size_t indentLevel)
+{
+ //unlike our XML serialization the caller is repsonsible for line breaks and indentation of *first* line
+ auto writeIndent = [&](size_t level)
+ {
+ for (size_t i = 0; i < level; ++i)
+ stream += indent;
+ };
+
+ switch (jval.type)
+ {
+ case JsonValue::Type::null:
+ stream += "null";
+ break;
+
+ case JsonValue::Type::boolean:
+ case JsonValue::Type::number:
+ stream += jval.primVal;
+ break;
+
+ case JsonValue::Type::string:
+ stream += '"' + jsonEscape(jval.primVal) + '"';
+ break;
+
+ case JsonValue::Type::object:
+ stream += '{';
+ if (!jval.objectVal.empty())
+ {
+ for (auto it = jval.objectVal.begin(); it != jval.objectVal.end(); ++it)
+ {
+ const auto& [childName, childValue] = *it;
+
+ if (it != jval.objectVal.begin())
+ stream += ',';
+
+ stream += lineBreak;
+ writeIndent(indentLevel + 1);
+
+ stream += '"' + jsonEscape(childName) + "\":";
+
+ if ((childValue->type == JsonValue::Type::object && !childValue->objectVal.empty()) ||
+ (childValue->type == JsonValue::Type::array && !childValue->arrayVal .empty()))
+ {
+ stream += lineBreak;
+ writeIndent(indentLevel + 1);
+ }
+ else if (!indent.empty())
+ stream += ' ';
+
+ serialize(*childValue, stream, lineBreak, indent, indentLevel + 1);
+ }
+ stream += lineBreak;
+ writeIndent(indentLevel);
+ }
+ stream += '}';
+ break;
+
+ case JsonValue::Type::array:
+ stream += '[';
+ if (!jval.arrayVal.empty())
+ {
+ for (auto it = jval.arrayVal.begin(); it != jval.arrayVal.end(); ++it)
+ {
+ const auto& childValue = **it;
+
+ if (it != jval.arrayVal.begin())
+ stream += ',';
+
+ stream += lineBreak;
+ writeIndent(indentLevel + 1);
+
+ serialize(childValue, stream, lineBreak, indent, indentLevel + 1);
+ }
+ stream += lineBreak;
+ writeIndent(indentLevel);
+ }
+ stream += ']';
+ break;
+ }
+}
+}
+}
+
+
+inline
+std::string serializeJson(const JsonValue& jval,
+ const std::string& lineBreak,
+ const std::string& indent) //noexcept
+{
+ std::string output;
+ json_impl::serialize(jval, output, lineBreak, indent, 0);
+ output += lineBreak;
+ return output;
+}
+
+
+namespace json_impl
+{
+struct Token
+{
+ enum class Type
+ {
+ eof,
+ curlyOpen,
+ curlyClose,
+ squareOpen,
+ squareClose,
+ colon,
+ comma,
+ string, //
+ number, //primitive types
+ boolean, //
+ null, //
+ };
+
+ Token(Type t) : type(t) {}
+
+ Type type;
+ std::string primVal; //for primitive types
+};
+
+class Scanner
+{
+public:
+ Scanner(const std::string& stream) : stream_(stream), pos_(stream_.begin())
+ {
+ if (zen::startsWith(stream_, BYTE_ORDER_MARK_UTF8))
+ pos_ += strLength(BYTE_ORDER_MARK_UTF8);
+ }
+
+ Token getNextToken() //throw JsonParsingError
+ {
+ //skip whitespace
+ pos_ = std::find_if(pos_, stream_.end(), std::not_fn(isJsonWhiteSpace));
+
+ if (pos_ == stream_.end())
+ return Token::Type::eof;
+
+ if (*pos_ == '{') return ++pos_, Token::Type::curlyOpen;
+ if (*pos_ == '}') return ++pos_, Token::Type::curlyClose;
+ if (*pos_ == '[') return ++pos_, Token::Type::squareOpen;
+ if (*pos_ == ']') return ++pos_, Token::Type::squareClose;
+ if (*pos_ == ':') return ++pos_, Token::Type::colon;
+ if (*pos_ == ',') return ++pos_, Token::Type::comma;
+ if (startsWith("null")) return pos_ += 4, Token(Token::Type::null);
+
+ if (startsWith("true"))
+ {
+ pos_ += 4;
+ Token tk(Token::Type::boolean);
+ tk.primVal = "true";
+ return tk;
+ }
+ if (startsWith("false"))
+ {
+ pos_ += 5;
+ Token tk(Token::Type::boolean);
+ tk.primVal = "false";
+ return tk;
+ }
+
+ if (*pos_ == '"')
+ {
+ for (auto it = ++pos_; it != stream_.end(); ++it)
+ if (*it == '"')
+ {
+ Token tk(Token::Type::string);
+ tk.primVal = jsonUnescape({ pos_, it });
+ pos_ = ++it;
+ return tk;
+ }
+ else if (*it == '\\') //skip next char
+ if (++it == stream_.end())
+ break;
+
+ throw JsonParsingError(posRow(), posCol());
+ }
+
+ //expect a number:
+ const auto itNumEnd = std::find_if(pos_, stream_.end(), std::not_fn(isJsonNumDigit));
+ if (itNumEnd == pos_)
+ throw JsonParsingError(posRow(), posCol());
+
+ Token tk(Token::Type::number);
+ tk.primVal.assign(pos_, itNumEnd);
+ pos_ = itNumEnd;
+ return tk;
+ }
+
+ size_t posRow() const //current row beginning with 0
+ {
+ const size_t crSum = std::count(stream_.begin(), pos_, '\r'); //carriage returns
+ const size_t nlSum = std::count(stream_.begin(), pos_, '\n'); //new lines
+ assert(crSum == 0 || nlSum == 0 || crSum == nlSum);
+ return std::max(crSum, nlSum); //be compatible with Linux/Mac/Win
+ }
+
+ size_t posCol() const //current col beginning with 0
+ {
+ //seek beginning of line
+ for (auto it = pos_; it != stream_.begin(); )
+ {
+ --it;
+ if (*it == '\r' || *it == '\n')
+ return pos_ - it - 1;
+ }
+ return pos_ - stream_.begin();
+ }
+
+private:
+ Scanner (const Scanner&) = delete;
+ Scanner& operator=(const Scanner&) = delete;
+
+ static bool isJsonWhiteSpace(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }
+ static bool isJsonNumDigit (char c) { return ('0' <= c && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e'|| c == 'E'; }
+
+ bool startsWith(const std::string& prefix) const
+ {
+ return zen::startsWith(StringRef<const char>(pos_, stream_.end()), prefix);
+ }
+
+ const std::string stream_;
+ std::string::const_iterator pos_;
+};
+
+
+class JsonParser
+{
+public:
+ JsonParser(const std::string& stream) :
+ scn_(stream),
+ tk_(scn_.getNextToken()) {} //throw JsonParsingError
+
+ JsonValue parse() //throw JsonParsingError
+ {
+ JsonValue jval = parseValue(); //throw JsonParsingError
+ expectToken(Token::Type::eof); //
+ return jval;
+ }
+
+private:
+ JsonParser (const JsonParser&) = delete;
+ JsonParser& operator=(const JsonParser&) = delete;
+
+ JsonValue parseValue() //throw JsonParsingError
+ {
+ if (token().type == Token::Type::curlyOpen)
+ {
+ nextToken(); //throw JsonParsingError
+
+ JsonValue jval(JsonValue::Type::object);
+
+ if (token().type != Token::Type::curlyClose)
+ for (;;)
+ {
+ expectToken(Token::Type::string); //throw JsonParsingError
+ std::string name = token().primVal;
+ nextToken(); //throw JsonParsingError
+
+ consumeToken(Token::Type::colon); //throw JsonParsingError
+
+ JsonValue value = parseValue(); //throw JsonParsingError
+ jval.objectVal.emplace(std::move(name), std::make_unique<JsonValue>(std::move(value)));
+
+ if (token().type != Token::Type::comma)
+ break;
+ nextToken(); //throw JsonParsingError
+ }
+
+ consumeToken(Token::Type::curlyClose); //throw JsonParsingError
+ return jval;
+ }
+ else if (token().type == Token::Type::squareOpen)
+ {
+ nextToken(); //throw JsonParsingError
+
+ JsonValue jval(JsonValue::Type::array);
+
+ if (token().type != Token::Type::squareClose)
+ for (;;)
+ {
+ JsonValue value = parseValue(); //throw JsonParsingError
+ jval.arrayVal.emplace_back(std::make_unique<JsonValue>(std::move(value)));
+
+ if (token().type != Token::Type::comma)
+ break;
+ nextToken(); //throw JsonParsingError
+ }
+
+ consumeToken(Token::Type::squareClose); //throw JsonParsingError
+ return jval;
+ }
+ else if (token().type == Token::Type::string)
+ {
+ JsonValue jval(token().primVal);
+ nextToken(); //throw JsonParsingError
+ return jval;
+ }
+ else if (token().type == Token::Type::number)
+ {
+ JsonValue jval(JsonValue::Type::number);
+ jval.primVal = token().primVal;
+ nextToken(); //throw JsonParsingError
+ return jval;
+ }
+ else if (token().type == Token::Type::boolean)
+ {
+ JsonValue jval(JsonValue::Type::boolean);
+ jval.primVal = token().primVal;
+ nextToken(); //throw JsonParsingError
+ return jval;
+ }
+ else if (token().type == Token::Type::null)
+ {
+ nextToken(); //throw JsonParsingError
+ return JsonValue();
+ }
+ else //unexpected token
+ throw JsonParsingError(scn_.posRow(), scn_.posCol());
+ }
+
+ const Token& token() const { return tk_; }
+
+ void nextToken() { tk_ = scn_.getNextToken(); } //throw JsonParsingError
+
+ void expectToken(Token::Type t) //throw JsonParsingError
+ {
+ if (token().type != t)
+ throw JsonParsingError(scn_.posRow(), scn_.posCol());
+ }
+
+ void consumeToken(Token::Type t) //throw JsonParsingError
+ {
+ expectToken(t); //throw JsonParsingError
+ nextToken(); //
+ }
+
+ Scanner scn_;
+ Token tk_;
+};
+}
+
+inline
+JsonValue parseJson(const std::string& stream) //throw JsonParsingError
+{
+ return json_impl::JsonParser(stream).parse(); //throw JsonParsingError
+}
+}
+
+#endif //JSON_H_0187348321748321758934215734
bgstack15