// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Containers/Utf8String.h" #include "CoreMinimal.h" #include "Serialization/JsonTypes.h" #include "Policies/PrettyJsonPrintPolicy.h" #include "Serialization/MemoryWriter.h" /** * Takes an input source char representing and returns if it is possible to represent in DstChar expected encoding * * @param Char source char * @return false if character is a control char or if it is out of range of representation in case of converting to ANSICHAR. true otherwise **/ template bool HasDestinationJsonStringCharRepresentation(SrcChar Char) { return Char >= CHARTEXT(SrcChar, ' ') && (std::is_same_v || !std::is_same_v || Char <= (SrcChar)0x7E); } /** * Takes an input string and escapes it so it can be written as a valid Json string. Also adds the quotes. * Appends to a given string-like object to avoid reallocations. * String-like object must support operator+=(const TCHAR*) and operation+=(TCHAR) * * @param AppendTo the string to append to. * @param StringVal the string to escape * @return the AppendTo string for convenience. */ template inline StringType& AppendEscapeJsonString(StringType& AppendTo, const FString& StringVal) { using CharType = typename StringType::ElementType; AppendTo += TEXT("\""); for (const CharType* Char = *StringVal; *Char != CHARTEXT(CharType, '\0'); ++Char) { switch (*Char) { case CHARTEXT(CharType, '\\'): AppendTo += CHARTEXT(CharType, "\\\\"); break; case CHARTEXT(CharType, '\n'): AppendTo += CHARTEXT(CharType, "\\n"); break; case CHARTEXT(CharType, '\t'): AppendTo += CHARTEXT(CharType, "\\t"); break; case CHARTEXT(CharType, '\b'): AppendTo += CHARTEXT(CharType, "\\b"); break; case CHARTEXT(CharType, '\f'): AppendTo += CHARTEXT(CharType, "\\f"); break; case CHARTEXT(CharType, '\r'): AppendTo += CHARTEXT(CharType, "\\r"); break; case CHARTEXT(CharType, '\"'): AppendTo += CHARTEXT(CharType, "\\\""); break; default: // Must escape control characters or non representable characters if (HasDestinationJsonStringCharRepresentation(*Char) ) { AppendTo += *Char; } else { AppendTo.Appendf(CHARTEXT(CharType, "\\u%04x"), *Char); } } } AppendTo += CHARTEXT(CharType, "\""); return AppendTo; } /** * Takes an input string and escapes it so it can be written as a valid Json string. Also adds the quotes. * * @param StringVal the string to escape * @return the given string, escaped to produce a valid Json string. */ inline FString EscapeJsonString(const FString& StringVal) { FString Result; return AppendEscapeJsonString(Result, StringVal); } /** * Template for Json writers. * * @param CharType The type of characters to print, i.e. TCHAR or ANSICHAR. * @param PrintPolicy The print policy to use when writing the output string (default = TPrettyJsonPrintPolicy). */ template > class TJsonWriter { public: static TSharedRef< TJsonWriter > Create( FArchive* const Stream, int32 InitialIndentLevel = 0 ) { return MakeShareable( new TJsonWriter< CharType, PrintPolicy >( Stream, InitialIndentLevel ) ); } public: virtual ~TJsonWriter() { } FORCEINLINE int32 GetIndentLevel() const { return IndentLevel; } bool CanWriteObjectStart() const { return CanWriteObjectWithoutIdentifier(); } EJson GetCurrentElementType() const { return Stack.Num() > 0 ? Stack.Top() : EJson::None; } void WriteObjectStart() { check(CanWriteObjectWithoutIdentifier()); WriteCommaIfNeeded(); if ( PreviousTokenWritten != EJsonToken::None ) { PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); } PrintPolicy::WriteChar(Stream, CharType('{')); ++IndentLevel; Stack.Push( EJson::Object ); PreviousTokenWritten = EJsonToken::CurlyOpen; } template void WriteObjectStart(IdentifierType&& Identifier) { check( Stack.Top() == EJson::Object ); WriteIdentifier(Forward(Identifier)); PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); PrintPolicy::WriteChar(Stream, CharType('{')); ++IndentLevel; Stack.Push( EJson::Object ); PreviousTokenWritten = EJsonToken::CurlyOpen; } void WriteObjectEnd() { check( Stack.Top() == EJson::Object ); PrintPolicy::WriteLineTerminator(Stream); --IndentLevel; PrintPolicy::WriteTabs(Stream, IndentLevel); PrintPolicy::WriteChar(Stream, CharType('}')); Stack.Pop(); PreviousTokenWritten = EJsonToken::CurlyClose; } void WriteArrayStart() { check(CanWriteValueWithoutIdentifier()); WriteCommaIfNeeded(); if ( PreviousTokenWritten != EJsonToken::None ) { PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); } PrintPolicy::WriteChar(Stream, CharType('[')); ++IndentLevel; Stack.Push( EJson::Array ); PreviousTokenWritten = EJsonToken::SquareOpen; } template void WriteArrayStart(IdentifierType&& Identifier) { check( Stack.Top() == EJson::Object ); WriteIdentifier(Forward(Identifier)); PrintPolicy::WriteSpace( Stream ); PrintPolicy::WriteChar(Stream, CharType('[')); ++IndentLevel; Stack.Push( EJson::Array ); PreviousTokenWritten = EJsonToken::SquareOpen; } void WriteArrayEnd() { check( Stack.Top() == EJson::Array ); --IndentLevel; if ( PreviousTokenWritten == EJsonToken::SquareClose || PreviousTokenWritten == EJsonToken::CurlyClose || PreviousTokenWritten == EJsonToken::String ) { PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); } else if ( PreviousTokenWritten != EJsonToken::SquareOpen ) { PrintPolicy::WriteSpace( Stream ); } PrintPolicy::WriteChar(Stream, CharType(']')); Stack.Pop(); PreviousTokenWritten = EJsonToken::SquareClose; } template void WriteValue(FValue Value) { check(CanWriteValueWithoutIdentifier()); WriteCommaIfNeeded(); if (PreviousTokenWritten == EJsonToken::SquareOpen || EJsonToken_IsShortValue(PreviousTokenWritten)) { PrintPolicy::WriteSpace( Stream ); } else { PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); } PreviousTokenWritten = WriteValueOnly( Value ); } void WriteValue(FStringView Value) { check(CanWriteValueWithoutIdentifier()); WriteCommaIfNeeded(); PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); PreviousTokenWritten = WriteValueOnly(Value); } void WriteValue(const FString& Value) { check(CanWriteValueWithoutIdentifier()); WriteCommaIfNeeded(); PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); PreviousTokenWritten = WriteValueOnly(Value); } template void WriteValue(IdentifierType&& Identifier, FValue Value) { check( Stack.Top() == EJson::Object ); WriteIdentifier(Forward(Identifier)); PrintPolicy::WriteSpace(Stream); PreviousTokenWritten = WriteValueOnly(MoveTemp(Value)); } template void WriteValue(IdentifierType&& Identifier, const TArray& Array) { WriteArrayStart(Forward(Identifier)); for (int Idx = 0; Idx < Array.Num(); Idx++) { WriteValue(Array[Idx]); } WriteArrayEnd(); } template void WriteValue(IdentifierType&& Identifier, const TMap& Map) { WriteObjectStart(Forward(Identifier)); for (const TPair& Element : Map) { WriteValue(Element.Key, Element.Value); } WriteObjectEnd(); } template void WriteValue(const TMap& Map) { WriteObjectStart(); for (const TPair& Element : Map) { WriteValue(Element.Key, Element.Value); } WriteObjectEnd(); } void WriteValue(FStringView Identifier, const TCHAR* Value) { WriteValue(Identifier, FStringView(Value)); } // WARNING: THIS IS DANGEROUS. Use this only if you know for a fact that the Value is valid JSON! // Use this to insert the results of a different JSON Writer in. void WriteRawJSONValue( FStringView Identifier, FStringView Value ) { WriteRawJSONValueImpl(Identifier, Value); } // WARNING: THIS IS DANGEROUS. Use this only if you know for a fact that the Value is valid JSON! // Use this to insert the results of a different JSON Writer in. void WriteRawJSONValue( FUtf8StringView Identifier, FUtf8StringView Value ) { WriteRawJSONValueImpl(Identifier, Value); } template void WriteNull(IdentifierType&& Identifier) { WriteValue(Forward(Identifier), nullptr); } void WriteValue(const TCHAR* Value) { WriteValue(FStringView(Value)); } // WARNING: THIS IS DANGEROUS. Use this only if you know for a fact that the Value is valid JSON! // Use this to insert the results of a different JSON Writer in. void WriteRawJSONValue(FStringView Value) { WriteRawJSONValueImpl(Value); } // WARNING: THIS IS DANGEROUS. Use this only if you know for a fact that the Value is valid JSON! // Use this to insert the results of a different JSON Writer in. void WriteRawJSONValue(FUtf8StringView Value) { WriteRawJSONValueImpl(Value); } void WriteNull() { WriteValue(nullptr); } virtual bool Close() { return ( PreviousTokenWritten == EJsonToken::None || PreviousTokenWritten == EJsonToken::CurlyClose || PreviousTokenWritten == EJsonToken::SquareClose ) && Stack.Num() == 0; } /** * WriteValue("Foo", Bar) should be equivalent to WriteIdentifierPrefix("Foo"), WriteValue(Bar) */ template void WriteIdentifierPrefix(IdentifierType&& Identifier) { check(Stack.Top() == EJson::Object); WriteIdentifier(Forward(Identifier)); PrintPolicy::WriteSpace(Stream); PreviousTokenWritten = EJsonToken::Identifier; } protected: /** * Creates and initializes a new instance. * * @param InStream An archive containing the input. * @param InitialIndentLevel The initial indentation level. */ TJsonWriter( FArchive* const InStream, int32 InitialIndentLevel ) : Stream( InStream ) , Stack() , PreviousTokenWritten(EJsonToken::None) , IndentLevel(InitialIndentLevel) { } protected: FORCEINLINE bool CanWriteValueWithoutIdentifier() const { return Stack.Num() <= 0 || Stack.Top() == EJson::Array || PreviousTokenWritten == EJsonToken::Identifier; } FORCEINLINE bool CanWriteObjectWithoutIdentifier() const { return Stack.Num() <= 0 || Stack.Top() == EJson::Array || PreviousTokenWritten == EJsonToken::Identifier || PreviousTokenWritten == EJsonToken::Colon; } FORCEINLINE void WriteCommaIfNeeded() { if ( PreviousTokenWritten != EJsonToken::CurlyOpen && PreviousTokenWritten != EJsonToken::SquareOpen && PreviousTokenWritten != EJsonToken::Identifier && PreviousTokenWritten != EJsonToken::None) { PrintPolicy::WriteChar(Stream, CharType(',')); } } template void WriteIdentifier(const InCharType* Identifier) { WriteCommaIfNeeded(); PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); WriteStringValue(TStringView(Identifier)); PrintPolicy::WriteChar(Stream, CharType(':')); } template void WriteIdentifier(TStringView Identifier) { WriteCommaIfNeeded(); PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); WriteStringValue(Identifier); PrintPolicy::WriteChar(Stream, CharType(':')); } void WriteIdentifier(const FText& Identifier) { WriteIdentifier(Identifier.ToString()); // Does not copy } FORCEINLINE void WriteIdentifier(const FString& Identifier) { WriteCommaIfNeeded(); PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); WriteStringValue(FStringView(Identifier)); PrintPolicy::WriteChar(Stream, CharType(':')); } FORCEINLINE EJsonToken WriteValueOnly(bool Value) { PrintPolicy::WriteString(Stream, Value ? TEXTVIEW("true") : TEXTVIEW("false")); return Value ? EJsonToken::True : EJsonToken::False; } FORCEINLINE EJsonToken WriteValueOnly(float Value) { PrintPolicy::WriteFloat(Stream, Value); return EJsonToken::Number; } FORCEINLINE EJsonToken WriteValueOnly(double Value) { // Specify 17 significant digits, the most that can ever be useful from a double // In particular, this ensures large integers are written correctly PrintPolicy::WriteDouble(Stream, Value); return EJsonToken::Number; } FORCEINLINE EJsonToken WriteValueOnly(int32 Value) { return WriteValueOnly((int64)Value); } FORCEINLINE EJsonToken WriteValueOnly(int64 Value) { PrintPolicy::WriteString(Stream, WriteToString<32>(Value)); return EJsonToken::Number; } FORCEINLINE EJsonToken WriteValueOnly(uint32 Value) { return WriteValueOnly((uint64)Value); } FORCEINLINE EJsonToken WriteValueOnly(uint64 Value) { PrintPolicy::WriteString(Stream, WriteToString<32>(Value)); return EJsonToken::Number; } FORCEINLINE EJsonToken WriteValueOnly(TYPE_OF_NULLPTR) { PrintPolicy::WriteString(Stream, TEXTVIEW("null")); return EJsonToken::Null; } FORCEINLINE EJsonToken WriteValueOnly(const TCHAR* Value) { WriteStringValue(FStringView(Value)); return EJsonToken::String; } FORCEINLINE EJsonToken WriteValueOnly(FStringView Value) { WriteStringValue(Value); return EJsonToken::String; } FORCEINLINE EJsonToken WriteValueOnly(FUtf8StringView Value) { WriteStringValue(Value); return EJsonToken::String; } virtual void WriteStringValue(FAnsiStringView String) { PrintPolicy::WriteChar(Stream, CharType('"')); WriteEscapedString(String); PrintPolicy::WriteChar(Stream, CharType('"')); } virtual void WriteStringValue(FStringView String) { PrintPolicy::WriteChar(Stream, CharType('"')); WriteEscapedString(String); PrintPolicy::WriteChar(Stream, CharType('"')); } virtual void WriteStringValue(const FString& String) { TJsonWriter::WriteStringValue(FStringView(String)); } virtual void WriteStringValue(FUtf8StringView String) { PrintPolicy::WriteChar(Stream, CharType('"')); WriteEscapedString(String); PrintPolicy::WriteChar(Stream, CharType('"')); } virtual void WriteStringValue(const FUtf8String& String) { TJsonWriter::WriteStringValue(FUtf8StringView(String)); } // WARNING: THIS IS DANGEROUS. Use this only if you know for a fact that the Value is valid JSON! // Use this to insert the results of a different JSON Writer in. template void WriteRawJSONValueImpl(FStringView Identifier, TStringView Value) { check(Stack.Top() == EJson::Object); WriteIdentifier(Identifier); PrintPolicy::WriteSpace(Stream); PrintPolicy::WriteString(Stream, Value); PreviousTokenWritten = EJsonToken::String; } template void WriteRawJSONValueImpl(TStringView Value) { check(CanWriteValueWithoutIdentifier()); WriteCommaIfNeeded(); if (PreviousTokenWritten != EJsonToken::True && PreviousTokenWritten != EJsonToken::False && PreviousTokenWritten != EJsonToken::SquareOpen) { PrintPolicy::WriteLineTerminator(Stream); PrintPolicy::WriteTabs(Stream, IndentLevel); } else { PrintPolicy::WriteSpace(Stream); } PrintPolicy::WriteString(Stream, Value); PreviousTokenWritten = EJsonToken::String; } template void WriteEscapedString(TStringView InView) { auto NeedsEscaping = [](InCharType Char) -> bool { switch (Char) { case CHARTEXT(InCharType, '\\'): return true; case CHARTEXT(InCharType, '\n'): return true; case CHARTEXT(InCharType, '\t'): return true; case CHARTEXT(InCharType, '\b'): return true; case CHARTEXT(InCharType, '\f'): return true; case CHARTEXT(InCharType, '\r'): return true; case CHARTEXT(InCharType, '\"'): return true; default: // Must escape control characters or non representable characters if (HasDestinationJsonStringCharRepresentation(Char)) { return false; } else { return true; } } }; // Write successive runs of unescaped and escaped characters until the view is exhausted while (!InView.IsEmpty()) { // In case we are handed a very large string, avoid checking all of it at once without writing anything constexpr int32 LongestRun = 2048; int32 EndIndex = 0; for (; EndIndex < InView.Len() && EndIndex < LongestRun; ++EndIndex) { if (NeedsEscaping(InView[EndIndex])) { break; } } if (TStringView Blittable = InView.Left(EndIndex); !Blittable.IsEmpty()) { PrintPolicy::WriteString(Stream, Blittable); } InView.RightChopInline(EndIndex); for (EndIndex = 0; EndIndex < InView.Len(); ++EndIndex) { InCharType Char = InView[EndIndex]; switch (Char) { case CHARTEXT(InCharType, '\\'): PrintPolicy::WriteString(Stream, TEXTVIEW("\\\\")); continue; case CHARTEXT(InCharType, '\n'): PrintPolicy::WriteString(Stream, TEXTVIEW("\\n")); continue; case CHARTEXT(InCharType, '\t'): PrintPolicy::WriteString(Stream, TEXTVIEW("\\t")); continue; case CHARTEXT(InCharType, '\b'): PrintPolicy::WriteString(Stream, TEXTVIEW("\\b")); continue; case CHARTEXT(InCharType, '\f'): PrintPolicy::WriteString(Stream, TEXTVIEW("\\f")); continue; case CHARTEXT(InCharType, '\r'): PrintPolicy::WriteString(Stream, TEXTVIEW("\\r")); continue; case CHARTEXT(InCharType, '\"'): PrintPolicy::WriteString(Stream, TEXTVIEW("\\\"")); continue; default: break; } // Must escape control characters or non representable characters if (HasDestinationJsonStringCharRepresentation(Char)) { break; } else { TAnsiStringBuilder<8> Builder; Builder.Appendf("\\u%04x", Char); PrintPolicy::WriteString(Stream, Builder.ToView()); } } InView.RightChopInline(EndIndex); } } FArchive* const Stream; TArray Stack; EJsonToken PreviousTokenWritten; int32 IndentLevel; }; template > class TJsonStringWriter : public TJsonWriter { public: using CharType = typename PrintPolicy::CharType; using StringType = TString; static TSharedRef Create(StringType* const InStream, int32 InitialIndent = 0 ) { return MakeShareable(new TJsonStringWriter(InStream, InitialIndent)); } public: virtual ~TJsonStringWriter() { check(this->Stream->Close()); delete this->Stream; } virtual bool Close() override { OutString->Reset(Bytes.Num()/sizeof(CharType)); for (int32 i = 0; i < Bytes.Num(); i+=sizeof(CharType)) { CharType* Char = static_cast(static_cast(&Bytes[i])); *OutString += *Char; } return TJsonWriter::Close(); } protected: TJsonStringWriter(StringType* const InOutString, int32 InitialIndent ) : TJsonWriter(new FMemoryWriter(Bytes), InitialIndent) , Bytes() , OutString(InOutString) { } private: TArray Bytes; StringType* OutString; }; template > class TJsonWriterFactory { public: using StringType = TString; static TSharedRef> Create(FArchive* const Stream, int32 InitialIndent = 0) { return TJsonWriter< CharType, PrintPolicy >::Create(Stream, InitialIndent); } static TSharedRef> Create(StringType* const Stream, int32 InitialIndent = 0) { return StaticCastSharedRef>(TJsonStringWriter::Create(Stream, InitialIndent)); } };