// Copyright Epic Games, Inc. All Rights Reserved. #include "JsonStringifyStructuredArchive.h" #include "Misc/Base64.h" #include "Misc/SecureHash.h" #include "JsonObjectGraphConventions.h" #include "JsonStringifyImpl.h" #include "UObject/Object.h" #include "UObject/LazyObjectPtr.h" #include "UObject/SoftObjectPtr.h" #include "UObject/WeakObjectPtr.h" #include "Serialization/Formatters/JsonArchiveOutputFormatter.h" #include #if WITH_TEXT_ARCHIVE_SUPPORT namespace UE::Private { FJsonStringifyStructuredArchive::FJsonStringifyStructuredArchive( const UObject* JsonObjectGraph , int32 InitialIndentLevel , FJsonStringifyImpl* InRootImpl , TArray& InVersionsToHarvest , bool bFilterEditorOnly) : VersionsToHarvest(&InVersionsToHarvest) , Object(JsonObjectGraph) , RootImpl(InRootImpl) , Inner(ResultBuff) { Newline.Add('\n'); IndentLevel = InitialIndentLevel; for (int32 I = 0; I < InitialIndentLevel; ++I) { Newline.Add('\t'); } Inner.SetIsPersistent(true); Inner.SetFilterEditorOnly(bFilterEditorOnly); Inner.SetIsTextFormat(true); } FJsonStringifyStructuredArchive::FJsonStringifyStructuredArchive(FArchive& ToWriter, int32 InitialIndentLevel) : VersionsToHarvest(nullptr) , Object(nullptr) , Inner(ResultBuff) , Override(&ToWriter) { Newline.Add('\n'); IndentLevel = InitialIndentLevel; for (int32 I = 0; I < InitialIndentLevel; ++I) { Newline.Add('\t'); } } TArray FJsonStringifyStructuredArchive::ToJson() { { FStructuredArchive StructuredArchive(*this); FStructuredArchive::FRecord ExportRecord = StructuredArchive.Open().EnterRecord(); const_cast(Object)->Serialize(ExportRecord); // RAII will close the json block.. so if you remove these containing {} you'll lose your trailing '}' // in the Json - I do not think this is a good design for FStructuredArchive.. alternatively // we can make this a long one liner, but that would also require an explanatory comment } if (ResultBuff.Num() > 2)// Num() > 2 here eliminates the default object {}, empty string, default container etc { if (VersionsToHarvest) { VersionsToHarvest->Append(GetUnderlyingArchive().GetCustomVersions().GetAllVersions()); } return ResultBuff; } return TArray(); } void FJsonStringifyStructuredArchive::WriteTextValueInline(FText Value, int32 IndentLevel, FArchive& ToWriter) { FJsonStringifyStructuredArchive Formatter(ToWriter, IndentLevel); Formatter.Serialize(Value); } void FJsonStringifyStructuredArchive::WriteCustomVersionValueInline(const TArray& Version, int32 IndentLevel, FArchive& ToWriter) { FJsonStringifyStructuredArchive Formatter(ToWriter, IndentLevel); FStructuredArchive ChildArchive(Formatter); ChildArchive.Open() << (TArray&)Version; ChildArchive.Close(); } FArchive& FJsonStringifyStructuredArchive::GetUnderlyingArchive() { return Override ? *Override : Inner; } bool FJsonStringifyStructuredArchive::HasDocumentTree() const { return true; } void FJsonStringifyStructuredArchive::EnterRecord() { if (ScopeSkipCount > 0) { return; } WriteOptionalComma(); WriteOptionalNewline(); Write("{"); Newline.Add('\t'); ++IndentLevel; bNeedsNewline = true; TextStartPosStack.Push(GetUnderlyingArchive().Tell()); } void FJsonStringifyStructuredArchive::LeaveRecord() { if (ScopeSkipCount > 0) { return; } --IndentLevel; Newline.Pop(EAllowShrinking::No); if (TextStartPosStack.Pop() == GetUnderlyingArchive().Tell()) { bNeedsNewline = false; } WriteOptionalNewline(); Write("}"); bNeedsComma = true; bNeedsNewline = true; } void FJsonStringifyStructuredArchive::EnterField(FArchiveFieldName Name) { // The base UObject serializer for structured archives is badly flawed, // so I have disabled it. The reflected properties are handled by FJsonStringifyImpl // so we filter them here. The macro generated BaseClassAutoGen is also // useless, and is handled by the FSerialDataJsonWriter which calls the // natively provided stream serializer. if (FCString::Strcmp(Name.Name, TEXT("Properties")) == 0 || FCString::Strcmp(Name.Name, TEXT("BaseClassAutoGen")) == 0 || ScopeSkipCount != 0) { ++ScopeSkipCount; } check(ScopeSkipCount >= 0); // scope overflow, halt WriteOptionalComma(); WriteOptionalNewline(); WriteFieldName(Name.Name); } void FJsonStringifyStructuredArchive::LeaveField() { const bool bWasSkipping = ScopeSkipCount > 0; if (ScopeSkipCount) { --ScopeSkipCount; } if (bWasSkipping) { return; } bNeedsComma = true; bNeedsNewline = true; } bool FJsonStringifyStructuredArchive::TryEnterField(FArchiveFieldName Name, bool bEnterWhenSaving) { if (bEnterWhenSaving) { EnterField(Name); } return bEnterWhenSaving; } void FJsonStringifyStructuredArchive::EnterArray(int32& NumElements) { EnterStream(); } void FJsonStringifyStructuredArchive::LeaveArray() { LeaveStream(); } void FJsonStringifyStructuredArchive::EnterArrayElement() { EnterStreamElement(); } void FJsonStringifyStructuredArchive::LeaveArrayElement() { LeaveStreamElement(); } void FJsonStringifyStructuredArchive::EnterStream() { if (ScopeSkipCount > 0) { return; } WriteOptionalComma(); WriteOptionalNewline(); Write("["); Newline.Add('\t'); ++IndentLevel; bNeedsNewline = true; TextStartPosStack.Push(GetUnderlyingArchive().Tell()); } void FJsonStringifyStructuredArchive::LeaveStream() { if (ScopeSkipCount > 0) { return; } --IndentLevel; Newline.Pop(EAllowShrinking::No); if (TextStartPosStack.Pop() == GetUnderlyingArchive().Tell()) { bNeedsNewline = false; } WriteOptionalNewline(); Write("]"); bNeedsComma = true; bNeedsNewline = true; } void FJsonStringifyStructuredArchive::EnterStreamElement() { if (ScopeSkipCount > 0) { return; } WriteOptionalComma(); WriteOptionalNewline(); } void FJsonStringifyStructuredArchive::LeaveStreamElement() { if (ScopeSkipCount > 0) { return; } bNeedsComma = true; bNeedsNewline = true; } void FJsonStringifyStructuredArchive::EnterMap(int32& NumElements) { EnterRecord(); } void FJsonStringifyStructuredArchive::LeaveMap() { LeaveRecord(); } void FJsonStringifyStructuredArchive::EnterMapElement(FString& Name) { EnterField(FArchiveFieldName(*Name)); } void FJsonStringifyStructuredArchive::LeaveMapElement() { if (ScopeSkipCount > 0) { return; } LeaveField(); } void FJsonStringifyStructuredArchive::EnterAttributedValue() { if (ScopeSkipCount > 0) { return; } NumAttributesStack.Push(0); } void FJsonStringifyStructuredArchive::EnterAttribute(FArchiveFieldName AttributeName) { if (ScopeSkipCount > 0) { return; } WriteOptionalComma(); WriteOptionalNewline(); WriteOptionalAttributedBlockOpening(); WriteOptionalComma(); WriteOptionalNewline(); checkf(FCString::Strcmp(AttributeName.Name, TEXT("Value")) != 0, TEXT("Attributes called 'Value' are reserved by the implementation")); WriteFieldName(*FString::Printf(TEXT("_%s"), AttributeName.Name)); ++NumAttributesStack.Top(); } void FJsonStringifyStructuredArchive::LeaveAttribute() { if (ScopeSkipCount > 0) { return; } bNeedsComma = true; bNeedsNewline = true; } void FJsonStringifyStructuredArchive::LeaveAttributedValue() { if (ScopeSkipCount > 0) { return; } WriteOptionalAttributedBlockClosing(); NumAttributesStack.Pop(); bNeedsComma = true; bNeedsNewline = true; } void FJsonStringifyStructuredArchive::EnterAttributedValueValue() { WriteOptionalComma(); WriteOptionalNewline(); WriteOptionalAttributedBlockValue(); } bool FJsonStringifyStructuredArchive::TryEnterAttributedValueValue() { return false; } bool FJsonStringifyStructuredArchive::TryEnterAttribute(FArchiveFieldName AttributeName, bool bEnterWhenSaving) { if (bEnterWhenSaving) { EnterAttribute(AttributeName); } return bEnterWhenSaving; } void FJsonStringifyStructuredArchive::Serialize(uint8& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(uint16& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(uint32& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(uint64& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(int8& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(int16& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(int32& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(int64& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(float& Value) { if (FPlatformMath::IsFinite(Value)) { FString String = FString::Printf(TEXT("%.17g"), Value); #if DO_GUARD_SLOW float RoundTripped; LexFromString(RoundTripped, *String); check(RoundTripped == Value); #endif WriteValue(String); } else if (FPlatformMath::IsNaN(Value)) { const uint32 ValueAsInt = BitCast(Value); const bool bIsNegative = !!(ValueAsInt & 0x80000000); const uint32 Significand = ValueAsInt & 0x007fffff; WriteValue(FString::Printf(TEXT("\"Number:%snan:0x%" PRIx32 "\""), bIsNegative ? TEXT("-") : TEXT("+"), Significand)); } else { WriteValue(Value < 0.0f ? TEXT("\"Number:-inf\"") : TEXT("\"Number:+inf\"")); } } void FJsonStringifyStructuredArchive::Serialize(double& Value) { if (FPlatformMath::IsFinite(Value)) { FString String = FString::Printf(TEXT("%.17g"), Value); #if DO_GUARD_SLOW double RoundTripped; LexFromString(RoundTripped, *String); check(RoundTripped == Value); #endif WriteValue(String); } else if (FPlatformMath::IsNaN(Value)) { const uint64 ValueAsInt = BitCast(Value); const bool bIsNegative = !!(ValueAsInt & 0x8000000000000000); const uint64 Significand = ValueAsInt & 0x000fffffffffffff; WriteValue(FString::Printf(TEXT("\"Number:%snan:0x%" PRIx64 "\""), bIsNegative ? TEXT("-") : TEXT("+"), Significand)); } else { WriteValue(Value < 0.0 ? TEXT("\"Number:-inf\"") : TEXT("\"Number:+inf\"")); } } void FJsonStringifyStructuredArchive::Serialize(bool& Value) { WriteValue(LexToString(Value)); } void FJsonStringifyStructuredArchive::Serialize(UTF32CHAR& Value) { WriteValue(LexToString(*(uint32*)&Value)); } void FJsonStringifyStructuredArchive::Serialize(FString& Value) { // Insert a "String:" prefix to prevent incorrect interpretation as another explicit type if (Value.StartsWith(TEXT("Object:")) || Value.StartsWith(TEXT("String:")) || Value.StartsWith(TEXT("Base64:"))) { SerializeStringInternal(FString::Printf(TEXT("String:%s"), *Value)); } else { SerializeStringInternal(Value); } } void FJsonStringifyStructuredArchive::Serialize(FName& Value) { SerializeStringInternal(*Value.ToString()); } void FJsonStringifyStructuredArchive::Serialize(UObject*& Value) { if (ScopeSkipCount > 0) { return; } RootImpl->WriteObjectAsJsonToArchive(Object, Value, &Inner, IndentLevel); } #if WITH_VERSE_VM || defined(__INTELLISENSE__) void FJsonStringifyStructuredArchive::Serialize(Verse::VCell*& Value) { WriteValue(TEXT("null")); } #endif void FJsonStringifyStructuredArchive::Serialize(FText& Value) { /* We could write using the structured serializer defined by FText but it is not as widely used as FTextStringHelper::WriteToBuffer and is currently private: FStructuredArchive ChildArchive(*this); FText::SerializeText(ChildArchive.Open(), Value); ChildArchive.Close(); */ FString AsString; FTextStringHelper::WriteToBuffer(AsString, Value); Serialize(AsString); } void FJsonStringifyStructuredArchive::Serialize(FWeakObjectPtr& Value) { UObject* Ptr = Value.IsValid() ? Value.Get() : nullptr; Serialize(Ptr); } void FJsonStringifyStructuredArchive::Serialize(FSoftObjectPtr& Value) { FSoftObjectPath Path = Value.ToSoftObjectPath(); Serialize(Path); } void FJsonStringifyStructuredArchive::Serialize(FSoftObjectPath& Value) { FString ValueStr; Value.ExportTextItem(ValueStr, FSoftObjectPath(), nullptr, 0, nullptr); Serialize(ValueStr); } void FJsonStringifyStructuredArchive::Serialize(FLazyObjectPtr& Value) { UObject* ObjectResolved = Value.Get(); Serialize(ObjectResolved); } void FJsonStringifyStructuredArchive::Serialize(FObjectPtr& Value) { UObject* ResolvedObject = Value.Get(); Serialize(ResolvedObject); } void FJsonStringifyStructuredArchive::Serialize(TArray& Data) { Serialize(Data.GetData(), Data.Num()); } void FJsonStringifyStructuredArchive::Serialize(void* Data, uint64 DataSize) { if (ScopeSkipCount > 0) { return; } static const int32 MaxLineChars = 120; static const int32 MaxLineBytes = FBase64::GetMaxDecodedDataSize(MaxLineChars); if (DataSize < MaxLineBytes) { // Encode the data on a single line. No need for hashing; intra-line merge conflicts are rare. WriteValue(FString::Printf(TEXT("\"Base64:%s\""), *FBase64::Encode((const uint8*)Data, static_cast(DataSize)))); } else { // Encode the data as a record containing a digest and array of base-64 encoded lines EnterRecord(); GetUnderlyingArchive().Serialize(Newline.GetData(), Newline.Num()); // Compute a SHA digest for the raw data, so we can check if it's corrupted uint8 Digest[FSHA1::DigestSize]; FSHA1::HashBuffer(Data, DataSize, Digest); // Convert the hash to a string ANSICHAR DigestString[(FSHA1::DigestSize * 2) + 1]; for (int32 Idx = 0; Idx < UE_ARRAY_COUNT(Digest); Idx++) { static const ANSICHAR HexDigits[] = "0123456789abcdef"; DigestString[(Idx * 2) + 0] = HexDigits[Digest[Idx] >> 4]; DigestString[(Idx * 2) + 1] = HexDigits[Digest[Idx] & 15]; } DigestString[UE_ARRAY_COUNT(DigestString) - 1] = 0; FArchive& Writer = GetUnderlyingArchive(); // Write the digest Write("\"Digest\": \""); Write(DigestString); Write("\","); Writer.Serialize(Newline.GetData(), Newline.Num()); // Write the base64 data Write("\"Base64\": "); for (uint64 DataPos = 0; DataPos < DataSize; DataPos += MaxLineBytes) { Write((DataPos > 0) ? ',' : '['); Writer.Serialize(Newline.GetData(), Newline.Num()); Write("\t\""); ANSICHAR LineData[MaxLineChars + 1]; uint64 NumLineChars = FBase64::Encode((const uint8*)Data + DataPos, FMath::Min(IntCastChecked(DataSize - DataPos), MaxLineBytes), LineData); Writer.Serialize(LineData, NumLineChars); Write("\""); } // Close the array Writer.Serialize(Newline.GetData(), Newline.Num()); Write(']'); bNeedsNewline = true; // Close the record LeaveRecord(); } } void FJsonStringifyStructuredArchive::Write(ANSICHAR Character) { if (ScopeSkipCount > 0) { return; } GetUnderlyingArchive().Serialize((void*)&Character, 1); } void FJsonStringifyStructuredArchive::Write(const ANSICHAR* Text) { if (ScopeSkipCount > 0) { return; } GetUnderlyingArchive().Serialize((void*)Text, TCString::Strlen(Text)); } void FJsonStringifyStructuredArchive::Write(const FString& Text) { Write(TCHAR_TO_UTF8(*Text)); } void FJsonStringifyStructuredArchive::WriteFieldName(const TCHAR* Name) { if (FCString::Stricmp(Name, TEXT("Base64")) == 0 || FCString::Stricmp(Name, TEXT("Digest")) == 0) { Write(FString::Printf(TEXT("\"_%s\": "), Name)); } else if (Name[0] == '_') { Write(FString::Printf(TEXT("\"_%s\": "), Name)); } else { Write(FString::Printf(TEXT("\"%s\": "), Name)); } } void FJsonStringifyStructuredArchive::WriteValue(const FString& Text) { Write(Text); } void FJsonStringifyStructuredArchive::WriteOptionalComma() { if (ScopeSkipCount > 0) { return; } if (bNeedsComma) { Write(','); bNeedsComma = false; } } void FJsonStringifyStructuredArchive::WriteOptionalNewline() { if (ScopeSkipCount > 0) { return; } if (bNeedsNewline) { GetUnderlyingArchive().Serialize(Newline.GetData(), Newline.Num()); bNeedsNewline = false; } } void FJsonStringifyStructuredArchive::WriteOptionalAttributedBlockOpening() { if (ScopeSkipCount > 0) { return; } if (NumAttributesStack.Top() == 0) { Write('{'); Newline.Add('\t'); ++IndentLevel; bNeedsNewline = true; } } void FJsonStringifyStructuredArchive::WriteOptionalAttributedBlockValue() { if (ScopeSkipCount > 0) { return; } if (NumAttributesStack.Top() != 0) { WriteFieldName(TEXT("_Value")); } } void FJsonStringifyStructuredArchive::WriteOptionalAttributedBlockClosing() { if (ScopeSkipCount > 0) { return; } if (NumAttributesStack.Top() != 0) { --IndentLevel; Newline.Pop(EAllowShrinking::No); WriteOptionalNewline(); Write("}"); bNeedsComma = true; bNeedsNewline = true; } } void FJsonStringifyStructuredArchive::SerializeStringInternal(const FString& String) { if (ScopeSkipCount > 0) { return; } FString Result = TEXT("\""); // Escape the string characters for (int32 Idx = 0; Idx < String.Len(); Idx++) { switch (String[Idx]) { case '\"': Result += "\\\""; break; case '\\': Result += "\\\\"; break; case '\b': Result += "\\b"; break; case '\f': Result += "\\f"; break; case '\n': Result += "\\n"; break; case '\r': Result += "\\r"; break; case '\t': Result += "\\t"; break; default: if (String[Idx] <= 0x1f || String[Idx] >= 0x7f) { Result += FString::Printf(TEXT("\\u%04x"), String[Idx]); } else { Result.AppendChar(String[Idx]); } break; } } Result += TEXT("\""); WriteValue(Result); } } #endif // WITH_TEXT_ARCHIVE_SUPPORT