aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPer Larsson <[email protected]>2021-09-27 11:17:18 +0200
committerGitHub <[email protected]>2021-09-27 11:17:18 +0200
commit895b22378d310c4affb782267ae1cbb3bc725c36 (patch)
tree05d345570fc184bd045329f8d2ea54b68c5d5bac
parentPorted CbValue from Unreal to Zen (#10) (diff)
downloadzen-895b22378d310c4affb782267ae1cbb3bc725c36.tar.xz
zen-895b22378d310c4affb782267ae1cbb3bc725c36.zip
Compact binary to JSON (#12)
CompactBinary: Support for converting CbObject to JSON
-rw-r--r--zencore/base64.cpp105
-rw-r--r--zencore/compactbinary.cpp538
-rw-r--r--zencore/include/zencore/base64.h17
-rw-r--r--zencore/include/zencore/compactbinary.h55
-rw-r--r--zencore/include/zencore/string.h319
-rw-r--r--zencore/include/zencore/zencore.h2
-rw-r--r--zencore/zencore.vcxproj2
-rw-r--r--zencore/zencore.vcxproj.filters2
-rw-r--r--zenhttp/httpserver.cpp14
9 files changed, 1049 insertions, 5 deletions
diff --git a/zencore/base64.cpp b/zencore/base64.cpp
new file mode 100644
index 000000000..9a6ea3b75
--- /dev/null
+++ b/zencore/base64.cpp
@@ -0,0 +1,105 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/base64.h>
+
+namespace zen {
+
+/** The table used to encode a 6 bit value as an ascii character */
+static const uint8_t EncodingAlphabet[64] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
+
+/** The table used to convert an ascii character into a 6 bit value */
+static const uint8_t DecodingAlphabet[256] = {
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00-0x0f
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x10-0x1f
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0xFF, 0xFF, 0xFF, 0x3F, // 0x20-0x2f
+ 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x30-0x3f
+ 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 0x40-0x4f
+ 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x50-0x5f
+ 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, // 0x60-0x6f
+ 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x70-0x7f
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x80-0x8f
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x90-0x9f
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xa0-0xaf
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xb0-0xbf
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xc0-0xcf
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xd0-0xdf
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xe0-0xef
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 0xf0-0xff
+};
+
+template<typename CharType>
+uint32_t
+Base64::Encode(const uint8_t* Source, uint32_t Length, CharType* Dest)
+{
+ CharType* EncodedBytes = Dest;
+
+ // Loop through the buffer converting 3 bytes of binary data at a time
+ while (Length >= 3)
+ {
+ uint8_t A = *Source++;
+ uint8_t B = *Source++;
+ uint8_t C = *Source++;
+ Length -= 3;
+
+ // The algorithm takes 24 bits of data (3 bytes) and breaks it into 4 6bit chunks represented as ascii
+ uint32_t ByteTriplet = A << 16 | B << 8 | C;
+
+ // Use the 6bit block to find the representation ascii character for it
+ EncodedBytes[3] = EncodingAlphabet[ByteTriplet & 0x3F];
+ ByteTriplet >>= 6;
+ EncodedBytes[2] = EncodingAlphabet[ByteTriplet & 0x3F];
+ ByteTriplet >>= 6;
+ EncodedBytes[1] = EncodingAlphabet[ByteTriplet & 0x3F];
+ ByteTriplet >>= 6;
+ EncodedBytes[0] = EncodingAlphabet[ByteTriplet & 0x3F];
+
+ // Now we can append this buffer to our destination string
+ EncodedBytes += 4;
+ }
+
+ // Since this algorithm operates on blocks, we may need to pad the last chunks
+ if (Length > 0)
+ {
+ uint8_t A = *Source++;
+ uint8_t B = 0;
+ uint8_t C = 0;
+ // Grab the second character if it is a 2 uint8_t finish
+ if (Length == 2)
+ {
+ B = *Source;
+ }
+ uint32_t ByteTriplet = A << 16 | B << 8 | C;
+ // Pad with = to make a 4 uint8_t chunk
+ EncodedBytes[3] = '=';
+ ByteTriplet >>= 6;
+ // If there's only one 1 uint8_t left in the source, then you need 2 pad chars
+ if (Length == 1)
+ {
+ EncodedBytes[2] = '=';
+ }
+ else
+ {
+ EncodedBytes[2] = EncodingAlphabet[ByteTriplet & 0x3F];
+ }
+ // Now encode the remaining bits the same way
+ ByteTriplet >>= 6;
+ EncodedBytes[1] = EncodingAlphabet[ByteTriplet & 0x3F];
+ ByteTriplet >>= 6;
+ EncodedBytes[0] = EncodingAlphabet[ByteTriplet & 0x3F];
+
+ EncodedBytes += 4;
+ }
+
+ // Add a null terminator
+ *EncodedBytes = 0;
+
+ return uint32_t(EncodedBytes - Dest);
+}
+
+template ZENCORE_API uint32_t Base64::Encode<char>(const uint8_t* Source, uint32_t Length, char* Dest);
+template ZENCORE_API uint32_t Base64::Encode<wchar_t>(const uint8_t* Source, uint32_t Length, wchar_t* Dest);
+
+} // namespace zen
diff --git a/zencore/compactbinary.cpp b/zencore/compactbinary.cpp
index a68096c36..3b6d33e41 100644
--- a/zencore/compactbinary.cpp
+++ b/zencore/compactbinary.cpp
@@ -2,19 +2,35 @@
#include "zencore/compactbinary.h"
+#include <zencore/base64.h>
#include <zencore/compactbinaryvalidation.h>
#include <zencore/compactbinaryvalue.h>
#include <zencore/compress.h>
#include <zencore/endian.h>
+#include <zencore/fmtutils.h>
#include <zencore/stream.h>
+#include <zencore/string.h>
#include <zencore/testing.h>
+#include <zencore/uid.h>
+#include <fmt/format.h>
#include <string_view>
+#if ZEN_WITH_TESTS
+# include <json11.hpp>
+# include <zencore/compactbinarybuilder.h>
+#endif
+
namespace zen {
const int DaysToMonth[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};
+double
+GetJulianDay(uint64_t Ticks)
+{
+ return (double)(1721425.5 + Ticks / TimeSpan::TicksPerDay);
+}
+
bool
IsLeapYear(int Year)
{
@@ -64,6 +80,249 @@ TimeSpan::Set(int Days, int Hours, int Minutes, int Seconds, int FractionNano)
Ticks = TotalTicks;
}
+std::string
+TimeSpan::ToString(const char* Format) const
+{
+ using namespace fmt::literals;
+
+ StringBuilder<128> Result;
+
+ Result.Append((Ticks < 0) ? '-' : '+');
+
+ while (*Format != '\0')
+ {
+ if ((*Format == '%') && (*++Format != '\0'))
+ {
+ switch (*Format)
+ {
+ case 'd':
+ Result.Append("%i"_format(GetDays()));
+ break;
+ case 'D':
+ Result.Append("%08i"_format(GetDays()));
+ break;
+ case 'h':
+ Result.Append("%02i"_format(GetHours()));
+ break;
+ case 'm':
+ Result.Append("%02i"_format(GetMinutes()));
+ break;
+ case 's':
+ Result.Append("%02i"_format(GetSeconds()));
+ break;
+ case 'f':
+ Result.Append("%03i"_format(GetFractionMilli()));
+ break;
+ case 'u':
+ Result.Append("%06i"_format(GetFractionMicro()));
+ break;
+ case 't':
+ Result.Append("%07i"_format(GetFractionTicks()));
+ break;
+ case 'n':
+ Result.Append("%09i"_format(GetFractionNano()));
+ break;
+ default:
+ Result.Append(*Format);
+ }
+ }
+ else
+ {
+ Result.Append(*Format);
+ }
+
+ ++Format;
+ }
+
+ return Result.ToString();
+}
+
+std::string
+TimeSpan::ToString() const
+{
+ if (GetDays() == 0)
+ {
+ return ToString("%h:%m:%s.%f");
+ }
+
+ return ToString("%d.%h:%m:%s.%f");
+}
+
+int
+DateTime::GetYear() const
+{
+ int Year, Month, Day;
+ GetDate(Year, Month, Day);
+
+ return Year;
+}
+
+int
+DateTime::GetMonth() const
+{
+ int Year, Month, Day;
+ GetDate(Year, Month, Day);
+
+ return Month;
+}
+
+int
+DateTime::GetDay() const
+{
+ int Year, Month, Day;
+ GetDate(Year, Month, Day);
+
+ return Day;
+}
+
+int
+DateTime::GetHour() const
+{
+ return (int)((Ticks / TimeSpan::TicksPerHour) % 24);
+}
+
+int
+DateTime::GetHour12() const
+{
+ int Hour = GetHour();
+
+ if (Hour < 1)
+ {
+ return 12;
+ }
+
+ if (Hour > 12)
+ {
+ return (Hour - 12);
+ }
+
+ return Hour;
+}
+
+int
+DateTime::GetMinute() const
+{
+ return (int)((Ticks / TimeSpan::TicksPerMinute) % 60);
+}
+
+int
+DateTime::GetSecond() const
+{
+ return (int)((Ticks / TimeSpan::TicksPerSecond) % 60);
+}
+
+int
+DateTime::GetMillisecond() const
+{
+ return (int)((Ticks / TimeSpan::TicksPerMillisecond) % 1000);
+}
+
+void
+DateTime::GetDate(int& Year, int& Month, int& Day) const
+{
+ // Based on FORTRAN code in:
+ // Fliegel, H. F. and van Flandern, T. C.,
+ // Communications of the ACM, Vol. 11, No. 10 (October 1968).
+
+ int i, j, k, l, n;
+
+ l = int(GetJulianDay(Ticks) + 0.5) + 68569;
+ n = 4 * l / 146097;
+ l = l - (146097 * n + 3) / 4;
+ i = 4000 * (l + 1) / 1461001;
+ l = l - 1461 * i / 4 + 31;
+ j = 80 * l / 2447;
+ k = l - 2447 * j / 80;
+ l = j / 11;
+ j = j + 2 - 12 * l;
+ i = 100 * (n - 49) + i + l;
+
+ Year = i;
+ Month = j;
+ Day = k;
+}
+
+std::string
+DateTime::ToString(const char* Format) const
+{
+ using namespace fmt::literals;
+
+ StringBuilder<32> Result;
+ int Year, Month, Day;
+
+ GetDate(Year, Month, Day);
+
+ if (Format != nullptr)
+ {
+ while (*Format != '\0')
+ {
+ if ((*Format == '%') && (*(++Format) != '\0'))
+ {
+ switch (*Format)
+ {
+ // case 'a': Result.Append(IsMorning() ? TEXT("am") : TEXT("pm")); break;
+ // case 'A': Result.Append(IsMorning() ? TEXT("AM") : TEXT("PM")); break;
+ case 'd':
+ Result.Append("%02i"_format(Day));
+ break;
+ // case 'D': Result.Appendf(TEXT("%03i"), GetDayOfYear()); break;
+ case 'm':
+ Result.Append("%02i"_format(Month));
+ break;
+ case 'y':
+ Result.Append("%02i"_format(Year % 100));
+ break;
+ case 'Y':
+ Result.Append("%04i"_format(Year % 100));
+ break;
+ case 'h':
+ Result.Append("%02i"_format(GetHour12()));
+ break;
+ case 'H':
+ Result.Append("%02i"_format(GetHour()));
+ break;
+ case 'M':
+ Result.Append("%02i"_format(GetMinute()));
+ break;
+ case 'S':
+ Result.Append("%02i"_format(GetSecond()));
+ break;
+ case 's':
+ Result.Append("%03i"_format(GetMillisecond()));
+ break;
+ default:
+ Result.Append(*Format);
+ }
+ }
+ else
+ {
+ Result.Append(*Format);
+ }
+
+ // move to the next one
+ Format++;
+ }
+ }
+
+ return Result.ToString();
+}
+
+std::string
+DateTime::ToIso8601() const
+{
+ return ToString("%Y-%m-%dT%H:%M:%S.%sZ");
+}
+
+StringBuilderBase&
+Guid::ToString(StringBuilderBase& Sb) const
+{
+ char Buf[128];
+ snprintf(Buf, sizeof Buf, "%08x-%04x-%04x-%04x-%04x%08x", A, B >> 16, B & 0xFFFF, C >> 16, C & 0xFFFF, D);
+ Sb << Buf;
+
+ return Sb;
+}
+
//////////////////////////////////////////////////////////////////////////
namespace CompactBinaryPrivate {
@@ -1142,6 +1401,259 @@ SaveCompactBinary(BinaryWriter& Ar, const CbObjectView& Object)
//////////////////////////////////////////////////////////////////////////
+class CbJsonWriter
+{
+public:
+ explicit CbJsonWriter(StringBuilderBase& InBuilder) : Builder(InBuilder) { NewLineAndIndent << LINE_TERMINATOR_ANSI; }
+
+ void WriteField(CbFieldView Field)
+ {
+ using namespace fmt::literals;
+ using namespace std::literals;
+
+ WriteOptionalComma();
+ WriteOptionalNewLine();
+
+ if (std::u8string_view Name = Field.GetU8Name(); !Name.empty())
+ {
+ AppendQuotedString(Name);
+ Builder << ": "sv;
+ }
+
+ switch (CbValue Accessor = Field.GetValue(); Accessor.GetType())
+ {
+ case CbFieldType::Null:
+ Builder << "null"sv;
+ break;
+ case CbFieldType::Object:
+ case CbFieldType::UniformObject:
+ {
+ Builder << '{';
+ NewLineAndIndent << '\t';
+ NeedsNewLine = true;
+ for (CbFieldView It : Field)
+ {
+ WriteField(It);
+ }
+ NewLineAndIndent.RemoveSuffix(1);
+ if (NeedsComma)
+ {
+ WriteOptionalNewLine();
+ }
+ Builder << '}';
+ }
+ break;
+ case CbFieldType::Array:
+ case CbFieldType::UniformArray:
+ {
+ Builder << '[';
+ NewLineAndIndent << '\t';
+ NeedsNewLine = true;
+ for (CbFieldView It : Field)
+ {
+ WriteField(It);
+ }
+ NewLineAndIndent.RemoveSuffix(1);
+ if (NeedsComma)
+ {
+ WriteOptionalNewLine();
+ }
+ Builder << ']';
+ }
+ break;
+ case CbFieldType::Binary:
+ AppendBase64String(Accessor.AsBinary());
+ break;
+ case CbFieldType::String:
+ AppendQuotedString(Accessor.AsU8String());
+ break;
+ case CbFieldType::IntegerPositive:
+ Builder << Accessor.AsIntegerPositive();
+ break;
+ case CbFieldType::IntegerNegative:
+ Builder << Accessor.AsIntegerNegative();
+ break;
+ case CbFieldType::Float32:
+ Builder.Append("%.9g"_format(Accessor.AsFloat32()));
+ break;
+ case CbFieldType::Float64:
+ Builder.Append("%.17g"_format(Accessor.AsFloat64()));
+ break;
+ case CbFieldType::BoolFalse:
+ Builder << "false"sv;
+ break;
+ case CbFieldType::BoolTrue:
+ Builder << "true"sv;
+ break;
+ case CbFieldType::ObjectAttachment:
+ case CbFieldType::BinaryAttachment:
+ {
+ Builder << '"';
+ Accessor.AsAttachment().ToHexString(Builder);
+ Builder << '"';
+ }
+ break;
+ case CbFieldType::Hash:
+ {
+ Builder << '"';
+ Accessor.AsHash().ToHexString(Builder);
+ Builder << '"';
+ }
+ break;
+ case CbFieldType::Uuid:
+ {
+ Builder << '"';
+ Accessor.AsUuid().ToString(Builder);
+ Builder << '"';
+ }
+ break;
+ case CbFieldType::DateTime:
+ Builder << '"' << DateTime(Accessor.AsDateTimeTicks()).ToIso8601() << '"';
+ break;
+ case CbFieldType::TimeSpan:
+ {
+ const TimeSpan Span(Accessor.AsTimeSpanTicks());
+ if (Span.GetDays() == 0)
+ {
+ Builder << '"' << Span.ToString("%h:%m:%s.%n") << '"';
+ }
+ else
+ {
+ Builder << '"' << Span.ToString("%d.%h:%m:%s.%n") << '"';
+ }
+ break;
+ }
+ case CbFieldType::ObjectId:
+ Builder << '"';
+ Accessor.AsObjectId().ToString(Builder);
+ Builder << '"';
+ break;
+ case CbFieldType::CustomById:
+ {
+ CbCustomById Custom = Accessor.AsCustomById();
+ Builder << "{ \"Id\": ";
+ Builder << Custom.Id;
+ Builder << ", \"Data\": ";
+ AppendBase64String(Custom.Data);
+ Builder << " }";
+ break;
+ }
+ case CbFieldType::CustomByName:
+ {
+ CbCustomByName Custom = Accessor.AsCustomByName();
+ Builder << "{ \"Name\": ";
+ AppendQuotedString(Custom.Name);
+ Builder << ", \"Data\": ";
+ AppendBase64String(Custom.Data);
+ Builder << " }";
+ break;
+ }
+ default:
+ ZEN_ASSERT(false);
+ break;
+ }
+
+ NeedsComma = true;
+ NeedsNewLine = true;
+ }
+
+private:
+ void WriteOptionalComma()
+ {
+ if (NeedsComma)
+ {
+ NeedsComma = false;
+ Builder << ',';
+ }
+ }
+
+ void WriteOptionalNewLine()
+ {
+ if (NeedsNewLine)
+ {
+ NeedsNewLine = false;
+ Builder << NewLineAndIndent;
+ }
+ }
+
+ void AppendQuotedString(std::u8string_view Value)
+ {
+ using namespace std::literals;
+
+ const AsciiSet EscapeSet(
+ "\\\"\b\f\n\r\t"
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f");
+
+ Builder << '\"';
+ while (!Value.empty())
+ {
+ std::u8string_view Verbatim = AsciiSet::FindPrefixWithout(Value, EscapeSet);
+ Builder << Verbatim;
+
+ Value = Value.substr(Verbatim.size());
+
+ std::u8string_view Escape = AsciiSet::FindPrefixWith(Value, EscapeSet);
+ for (char Char : Escape)
+ {
+ switch (Char)
+ {
+ case '\\':
+ Builder << "\\\\"sv;
+ break;
+ case '\"':
+ Builder << "\\\""sv;
+ break;
+ case '\b':
+ Builder << "\\b"sv;
+ break;
+ case '\f':
+ Builder << "\\f"sv;
+ break;
+ case '\n':
+ Builder << "\\n"sv;
+ break;
+ case '\r':
+ Builder << "\\r"sv;
+ break;
+ case '\t':
+ Builder << "\\t"sv;
+ break;
+ default:
+ Builder << Char;
+ break;
+ }
+ }
+ Value = Value.substr(Escape.size());
+ }
+ Builder << '\"';
+ }
+
+ void AppendBase64String(MemoryView Value)
+ {
+ Builder << '"';
+ ZEN_ASSERT(Value.GetSize() <= 512 * 1024 * 1024);
+ const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
+ const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
+ Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
+ }
+
+private:
+ StringBuilderBase& Builder;
+ StringBuilder<32> NewLineAndIndent;
+ bool NeedsComma{false};
+ bool NeedsNewLine{false};
+};
+
+void
+CompactBinaryToJson(const CbObjectView& Object, StringBuilderBase& Builder)
+{
+ CbJsonWriter Writer(Builder);
+ Writer.WriteField(Object.AsFieldView());
+}
+
+//////////////////////////////////////////////////////////////////////////
+
#if ZEN_WITH_TESTS
void
uson_forcelink()
@@ -1294,6 +1806,32 @@ TEST_CASE("uson.null")
CHECK(Field.IsNull() == false);
}
}
+
+TEST_CASE("uson.json")
+{
+ SUBCASE("string")
+ {
+ CbObjectWriter Writer;
+ Writer << "KeyOne"
+ << "ValueOne";
+ Writer << "KeyTwo"
+ << "ValueTwo";
+ CbObject Obj = Writer.Save();
+
+ StringBuilder<128> Sb;
+ const std::string_view JsonText = Obj.ToJson(Sb).ToView();
+
+ std::string JsonError;
+ json11::Json Json = json11::Json::parse(JsonText.data(), JsonError);
+
+ const std::string ValueOne = Json["KeyOne"].string_value();
+ const std::string ValueTwo = Json["KeyTwo"].string_value();
+
+ CHECK(JsonError.empty());
+ CHECK(ValueOne == "ValueOne");
+ CHECK(ValueTwo == "ValueTwo");
+ }
+}
#endif
} // namespace zen
diff --git a/zencore/include/zencore/base64.h b/zencore/include/zencore/base64.h
new file mode 100644
index 000000000..4d78b085f
--- /dev/null
+++ b/zencore/include/zencore/base64.h
@@ -0,0 +1,17 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "zencore.h"
+
+namespace zen {
+
+struct Base64
+{
+ template<typename CharType>
+ static uint32_t Encode(const uint8_t* Source, uint32_t Length, CharType* Dest);
+
+ static inline constexpr int32_t GetEncodedDataSize(uint32_t Size) { return ((Size + 2) / 3) * 4; }
+};
+
+} // namespace zen
diff --git a/zencore/include/zencore/compactbinary.h b/zencore/include/zencore/compactbinary.h
index 60e9ec7aa..ab01402f8 100644
--- a/zencore/include/zencore/compactbinary.h
+++ b/zencore/include/zencore/compactbinary.h
@@ -42,8 +42,22 @@ public:
}
inline uint64_t GetTicks() const { return Ticks; }
- inline bool operator==(const DateTime& Rhs) const { return Ticks == Rhs.Ticks; }
- inline auto operator<=>(const DateTime& Rhs) const { return Ticks - Rhs.Ticks; }
+
+ int GetYear() const;
+ int GetMonth() const;
+ int GetDay() const;
+ int GetHour() const;
+ int GetHour12() const;
+ int GetMinute() const;
+ int GetSecond() const;
+ int GetMillisecond() const;
+ void GetDate(int& Year, int& Month, int& Day) const;
+
+ inline bool operator==(const DateTime& Rhs) const { return Ticks == Rhs.Ticks; }
+ inline auto operator<=>(const DateTime& Rhs) const { return Ticks - Rhs.Ticks; }
+
+ std::string ToString(const char* Format) const;
+ std::string ToIso8601() const;
private:
void Set(int Year, int Month, int Day, int Hours, int Minutes, int Seconds, int MilliSecond);
@@ -99,6 +113,25 @@ public:
/** The number of timespan ticks per year (365 days, not accounting for leap years). */
static constexpr int64_t TicksPerYear = 365 * TicksPerDay;
+ int GetFractionTicks() const { return (int)(Ticks % TicksPerSecond); }
+
+ int GetFractionMicro() const { return (int)((Ticks % TicksPerSecond) / TicksPerMicrosecond); }
+
+ int GetFractionMilli() const { return (int)((Ticks % TicksPerSecond) / TicksPerMillisecond); }
+
+ int GetFractionNano() const { return (int)((Ticks % TicksPerSecond) * NanosecondsPerTick); }
+
+ int GetDays() const { return (int)(Ticks / TicksPerDay); }
+
+ int GetHours() const { return (int)((Ticks / TicksPerHour) % 24); }
+
+ int GetMinutes() const { return (int)((Ticks / TicksPerMinute) % 60); }
+
+ int GetSeconds() const { return (int)((Ticks / TicksPerSecond) % 60); }
+
+ ZENCORE_API std::string ToString(const char* Format) const;
+ ZENCORE_API std::string ToString() const;
+
private:
void Set(int Days, int Hours, int Minutes, int Seconds, int FractionNano);
@@ -108,6 +141,8 @@ private:
struct Guid
{
uint32_t A, B, C, D;
+
+ StringBuilderBase& ToString(StringBuilderBase& OutString) const;
};
//////////////////////////////////////////////////////////////////////////
@@ -442,6 +477,11 @@ public:
/** Returns the name of the field if it has a name, otherwise an empty view. */
constexpr inline std::string_view GetName() const { return std::string_view(static_cast<const char*>(Payload) - NameLen, NameLen); }
+ /** Returns the name of the field if it has a name, otherwise an empty view. */
+ constexpr inline std::u8string_view GetU8Name() const
+ {
+ return std::u8string_view(static_cast<const char8_t*>(Payload) - NameLen, NameLen);
+ }
/** Returns the value for unchecked access. Prefer the typed accessors below. */
inline CbValue GetValue() const;
@@ -907,6 +947,11 @@ private:
inline explicit CbArrayView(const CbFieldView& Field) : CbFieldView(Field) {}
};
+/**
+ * Serialize a compact binary object to JSON.
+ */
+ZENCORE_API void CompactBinaryToJson(const CbObjectView& Object, StringBuilderBase& Builder);
+
class CbObjectView : protected CbFieldView
{
friend class CbFieldView;
@@ -989,6 +1034,12 @@ public:
/** Whether the field has a value. */
using CbFieldView::operator bool;
+ StringBuilderBase& ToJson(StringBuilderBase& Builder) const
+ {
+ CompactBinaryToJson(*this, Builder);
+ return Builder;
+ }
+
private:
friend inline CbFieldViewIterator begin(const CbObjectView& Object) { return Object.CreateViewIterator(); }
friend inline CbFieldViewIterator end(const CbObjectView&) { return CbFieldViewIterator(); }
diff --git a/zencore/include/zencore/string.h b/zencore/include/zencore/string.h
index bb9b1c896..2c0d10577 100644
--- a/zencore/include/zencore/string.h
+++ b/zencore/include/zencore/string.h
@@ -14,6 +14,8 @@
#include <span>
#include <string_view>
+#include <type_traits>
+
namespace zen {
//////////////////////////////////////////////////////////////////////////
@@ -94,6 +96,14 @@ public:
const StringBuilderImpl& operator=(const StringBuilderImpl&) = delete;
const StringBuilderImpl& operator=(const StringBuilderImpl&&) = delete;
+ inline size_t AddUninitialized(size_t Count)
+ {
+ EnsureCapacity(Count);
+ const size_t OldCount = Size();
+ m_CurPos += Count;
+ return OldCount;
+ }
+
StringBuilderImpl& Append(C OneChar)
{
EnsureCapacity(1);
@@ -209,6 +219,12 @@ public:
return AppendRange(String.data(), String.data() + String.size());
}
+ inline void RemoveSuffix(int32_t Count)
+ {
+ ZEN_ASSERT(Count <= Size());
+ m_CurPos -= Count;
+ }
+
inline const C* c_str() const
{
EnsureNulTerminated();
@@ -322,6 +338,12 @@ protected:
extern template class StringBuilderImpl<char>;
+inline StringBuilderImpl<char>&
+operator<<(StringBuilderImpl<char>& Builder, char Char)
+{
+ return Builder.Append(Char);
+}
+
class StringBuilderBase : public StringBuilderImpl<char>
{
public:
@@ -661,6 +683,303 @@ ForEachStrTok(const std::string_view& Str, char Delim, Fn&& Func)
//////////////////////////////////////////////////////////////////////////
+/**
+ * ASCII character bitset useful for fast and readable parsing
+ *
+ * Entirely constexpr. Works with both wide and narrow strings.
+ *
+ * Example use cases:
+ *
+ * constexpr AsciiSet WhitespaceCharacters(" \v\f\t\r\n");
+ * bool bIsWhitespace = WhitespaceCharacters.Test(MyChar);
+ * const char* HelloWorld = AsciiSet::Skip(" \t\tHello world!", WhitespaceCharacters);
+ *
+ * constexpr AsciiSet XmlEscapeChars("&<>\"'");
+ * check(AsciiSet::HasNone(EscapedXmlString, XmlEscapeChars));
+ *
+ * constexpr AsciiSet Delimiters(".:;");
+ * const TCHAR* DelimiterOrEnd = AsciiSet::FindFirstOrEnd(PrefixedName, Delimiters);
+ * FString Prefix(PrefixedName, DelimiterOrEnd - PrefixedName);
+ *
+ * constexpr AsciiSet Slashes("/\\");
+ * const TCHAR* SlashOrEnd = AsciiSet::FindLastOrEnd(PathName, Slashes);
+ * const TCHAR* FileName = *SlashOrEnd ? SlashOrEnd + 1 : PathName;
+ */
+class AsciiSet
+{
+public:
+ template<typename CharType, int N>
+ constexpr AsciiSet(const CharType (&Chars)[N]) : AsciiSet(StringToBitset(Chars))
+ {
+ }
+
+ /** Returns true if a character is part of the set */
+ template<typename CharType>
+ constexpr inline bool Contains(CharType Char) const
+ {
+ using UnsignedCharType = std::make_unsigned<CharType>::type;
+
+ return !!TestImpl((UnsignedCharType)Char);
+ }
+
+ /** Returns non-zero if a character is part of the set. Prefer Contains() to avoid VS2019 conversion warnings. */
+ template<typename CharType>
+ constexpr inline uint64_t Test(CharType Char) const
+ {
+ using UnsignedCharType = std::make_unsigned<CharType>::type;
+
+ return TestImpl((UnsignedCharType)Char);
+ }
+
+ /** Create new set with specified character in it */
+ constexpr inline AsciiSet operator+(char Char) const
+ {
+ using UnsignedCharType = std::make_unsigned<char>::type;
+
+ InitData Bitset = {LoMask, HiMask};
+ SetImpl(Bitset, (UnsignedCharType)Char);
+ return AsciiSet(Bitset);
+ }
+
+ /** Create new set containing inverse set of characters - likely including null-terminator */
+ constexpr inline AsciiSet operator~() const { return AsciiSet(~LoMask, ~HiMask); }
+
+ ////////// Algorithms for C strings //////////
+
+ /** Find first character of string inside set or end pointer. Never returns null. */
+ template<class CharType>
+ static constexpr const CharType* FindFirstOrEnd(const CharType* Str, AsciiSet Set)
+ {
+ for (AsciiSet SetOrNil(Set.LoMask | NilMask, Set.HiMask); !SetOrNil.Test(*Str); ++Str)
+ ;
+
+ return Str;
+ }
+
+ /** Find last character of string inside set or end pointer. Never returns null. */
+ template<class CharType>
+ static constexpr const CharType* FindLastOrEnd(const CharType* Str, AsciiSet Set)
+ {
+ const CharType* Last = FindFirstOrEnd(Str, Set);
+
+ for (const CharType* It = Last; *It; It = FindFirstOrEnd(It + 1, Set))
+ {
+ Last = It;
+ }
+
+ return Last;
+ }
+
+ /** Find first character of string outside of set. Never returns null. */
+ template<typename CharType>
+ static constexpr const CharType* Skip(const CharType* Str, AsciiSet Set)
+ {
+ while (Set.Contains(*Str))
+ {
+ ++Str;
+ }
+
+ return Str;
+ }
+
+ /** Test if string contains any character in set */
+ template<typename CharType>
+ static constexpr bool HasAny(const CharType* Str, AsciiSet Set)
+ {
+ return *FindFirstOrEnd(Str, Set) != '\0';
+ }
+
+ /** Test if string contains no character in set */
+ template<typename CharType>
+ static constexpr bool HasNone(const CharType* Str, AsciiSet Set)
+ {
+ return *FindFirstOrEnd(Str, Set) == '\0';
+ }
+
+ /** Test if string contains any character outside of set */
+ template<typename CharType>
+ static constexpr bool HasOnly(const CharType* Str, AsciiSet Set)
+ {
+ return *Skip(Str, Set) == '\0';
+ }
+
+ ////////// Algorithms for string types like FStringView and FString //////////
+
+ /** Get initial substring with all characters in set */
+ template<class StringType>
+ static constexpr StringType FindPrefixWith(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Forward, EInclude::Members, EKeep::Head>(Str, Set);
+ }
+
+ /** Get initial substring with no characters in set */
+ template<class StringType>
+ static constexpr StringType FindPrefixWithout(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Forward, EInclude::NonMembers, EKeep::Head>(Str, Set);
+ }
+
+ /** Trim initial characters in set */
+ template<class StringType>
+ static constexpr StringType TrimPrefixWith(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Forward, EInclude::Members, EKeep::Tail>(Str, Set);
+ }
+
+ /** Trim initial characters not in set */
+ template<class StringType>
+ static constexpr StringType TrimPrefixWithout(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Forward, EInclude::NonMembers, EKeep::Tail>(Str, Set);
+ }
+
+ /** Get trailing substring with all characters in set */
+ template<class StringType>
+ static constexpr StringType FindSuffixWith(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Reverse, EInclude::Members, EKeep::Tail>(Str, Set);
+ }
+
+ /** Get trailing substring with no characters in set */
+ template<class StringType>
+ static constexpr StringType FindSuffixWithout(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Reverse, EInclude::NonMembers, EKeep::Tail>(Str, Set);
+ }
+
+ /** Trim trailing characters in set */
+ template<class StringType>
+ static constexpr StringType TrimSuffixWith(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Reverse, EInclude::Members, EKeep::Head>(Str, Set);
+ }
+
+ /** Trim trailing characters not in set */
+ template<class StringType>
+ static constexpr StringType TrimSuffixWithout(const StringType& Str, AsciiSet Set)
+ {
+ return Scan<EDir::Reverse, EInclude::NonMembers, EKeep::Head>(Str, Set);
+ }
+
+ /** Test if string contains any character in set */
+ template<class StringType>
+ static constexpr bool HasAny(const StringType& Str, AsciiSet Set)
+ {
+ return !HasNone(Str, Set);
+ }
+
+ /** Test if string contains no character in set */
+ template<class StringType>
+ static constexpr bool HasNone(const StringType& Str, AsciiSet Set)
+ {
+ uint64_t Match = 0;
+ for (auto Char : Str)
+ {
+ Match |= Set.Test(Char);
+ }
+ return Match == 0;
+ }
+
+ /** Test if string contains any character outside of set */
+ template<class StringType>
+ static constexpr bool HasOnly(const StringType& Str, AsciiSet Set)
+ {
+ auto End = Str.data() + Str.size();
+ return FindFirst<EInclude::Members>(Set, GetData(Str), End) == End;
+ }
+
+private:
+ enum class EDir
+ {
+ Forward,
+ Reverse
+ };
+ enum class EInclude
+ {
+ Members,
+ NonMembers
+ };
+ enum class EKeep
+ {
+ Head,
+ Tail
+ };
+
+ template<EInclude Include, typename CharType>
+ static constexpr const CharType* FindFirst(AsciiSet Set, const CharType* It, const CharType* End)
+ {
+ for (; It != End && (Include == EInclude::Members) == !!Set.Test(*It); ++It)
+ ;
+ return It;
+ }
+
+ template<EInclude Include, typename CharType>
+ static constexpr const CharType* FindLast(AsciiSet Set, const CharType* It, const CharType* End)
+ {
+ for (; It != End && (Include == EInclude::Members) == !!Set.Test(*It); --It)
+ ;
+ return It;
+ }
+
+ template<EDir Dir, EInclude Include, EKeep Keep, class StringType>
+ static constexpr StringType Scan(const StringType& Str, AsciiSet Set)
+ {
+ auto Begin = Str.data();
+ auto End = Begin + Str.size();
+ auto It = Dir == EDir::Forward ? FindFirst<Include>(Set, Begin, End) : FindLast<Include>(Set, End - 1, Begin - 1) + 1;
+
+ return Keep == EKeep::Head ? StringType(Begin, static_cast<int32_t>(It - Begin)) : StringType(It, static_cast<int32_t>(End - It));
+ }
+
+ // Work-around for constexpr limitations
+ struct InitData
+ {
+ uint64_t Lo, Hi;
+ };
+ static constexpr uint64_t NilMask = uint64_t(1) << '\0';
+
+ static constexpr inline void SetImpl(InitData& Bitset, uint32_t Char)
+ {
+ uint64_t IsLo = uint64_t(0) - (Char >> 6 == 0);
+ uint64_t IsHi = uint64_t(0) - (Char >> 6 == 1);
+ uint64_t Bit = uint64_t(1) << uint8_t(Char & 0x3f);
+
+ Bitset.Lo |= Bit & IsLo;
+ Bitset.Hi |= Bit & IsHi;
+ }
+
+ constexpr inline uint64_t TestImpl(uint32_t Char) const
+ {
+ uint64_t IsLo = uint64_t(0) - (Char >> 6 == 0);
+ uint64_t IsHi = uint64_t(0) - (Char >> 6 == 1);
+ uint64_t Bit = uint64_t(1) << (Char & 0x3f);
+
+ return (Bit & IsLo & LoMask) | (Bit & IsHi & HiMask);
+ }
+
+ template<typename CharType, int N>
+ static constexpr InitData StringToBitset(const CharType (&Chars)[N])
+ {
+ using UnsignedCharType = std::make_unsigned<CharType>::type;
+
+ InitData Bitset = {0, 0};
+ for (int I = 0; I < N - 1; ++I)
+ {
+ SetImpl(Bitset, UnsignedCharType(Chars[I]));
+ }
+
+ return Bitset;
+ }
+
+ constexpr AsciiSet(InitData Bitset) : LoMask(Bitset.Lo), HiMask(Bitset.Hi) {}
+
+ constexpr AsciiSet(uint64_t Lo, uint64_t Hi) : LoMask(Lo), HiMask(Hi) {}
+
+ uint64_t LoMask, HiMask;
+};
+
+//////////////////////////////////////////////////////////////////////////
+
void string_forcelink(); // internal
} // namespace zen
diff --git a/zencore/include/zencore/zencore.h b/zencore/include/zencore/zencore.h
index f6093cb96..4b9c1af1b 100644
--- a/zencore/include/zencore/zencore.h
+++ b/zencore/include/zencore/zencore.h
@@ -102,9 +102,11 @@
// Tells the compiler to put the decorated function in a certain section (aka. segment) of the executable.
# define ZEN_CODE_SECTION(Name) __declspec(code_seg(Name))
# define ZEN_FORCENOINLINE __declspec(noinline) /* Force code to NOT be inline */
+# define LINE_TERMINATOR_ANSI "\r\n"
#else
# define ZEN_CODE_SECTION(Name)
# define ZEN_FORCENOINLINE
+# define LINE_TERMINATOR_ANSI "\n"
#endif
#if ZEN_ARCH_ARM64
diff --git a/zencore/zencore.vcxproj b/zencore/zencore.vcxproj
index 2322f7173..3adf779ed 100644
--- a/zencore/zencore.vcxproj
+++ b/zencore/zencore.vcxproj
@@ -113,6 +113,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="include\zencore\atomic.h" />
+ <ClInclude Include="include\zencore\base64.h" />
<ClInclude Include="include\zencore\blake3.h" />
<ClInclude Include="include\zencore\compositebuffer.h" />
<ClInclude Include="include\zencore\crc32.h" />
@@ -157,6 +158,7 @@
<ClInclude Include="include\zencore\zencore.h" />
</ItemGroup>
<ItemGroup>
+ <ClCompile Include="base64.cpp" />
<ClCompile Include="blake3.cpp" />
<ClCompile Include="compositebuffer.cpp" />
<ClCompile Include="compress.cpp" />
diff --git a/zencore/zencore.vcxproj.filters b/zencore/zencore.vcxproj.filters
index d2e7a3159..92aa0db1d 100644
--- a/zencore/zencore.vcxproj.filters
+++ b/zencore/zencore.vcxproj.filters
@@ -44,6 +44,7 @@
<ClInclude Include="include\zencore\testutils.h" />
<ClInclude Include="include\zencore\testing.h" />
<ClInclude Include="include\zencore\mpscqueue.h" />
+ <ClInclude Include="include\zencore\base64.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="sha1.cpp" />
@@ -77,6 +78,7 @@
<ClCompile Include="session.cpp" />
<ClCompile Include="testutils.cpp" />
<ClCompile Include="mpscqueue.cpp" />
+ <ClCompile Include="base64.cpp" />
</ItemGroup>
<ItemGroup>
<Filter Include="CAS">
diff --git a/zenhttp/httpserver.cpp b/zenhttp/httpserver.cpp
index d999f7f60..795e81ea8 100644
--- a/zenhttp/httpserver.cpp
+++ b/zenhttp/httpserver.cpp
@@ -319,9 +319,17 @@ HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, CbPackage Data)
void
HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, CbObject Data)
{
- SharedBuffer Buf = Data.GetBuffer();
- std::array<IoBuffer, 1> Buffers{IoBufferBuilder::MakeCloneFromMemory(Buf.GetData(), Buf.GetSize())};
- return WriteResponse(ResponseCode, HttpContentType::kCbObject, Buffers);
+ if (m_AcceptType == HttpContentType::kJSON)
+ {
+ ExtendableStringBuilder<1024> Sb;
+ WriteResponse(ResponseCode, HttpContentType::kJSON, Data.ToJson(Sb).ToView());
+ }
+ else
+ {
+ SharedBuffer Buf = Data.GetBuffer();
+ std::array<IoBuffer, 1> Buffers{IoBufferBuilder::MakeCloneFromMemory(Buf.GetData(), Buf.GetSize())};
+ return WriteResponse(ResponseCode, HttpContentType::kCbObject, Buffers);
+ }
}
void