// Copyright Epic Games, Inc. All Rights Reserved. #include "JsonUtils/JsonPointer.h" #include "Dom/JsonObject.h" namespace UE::Json { namespace Private { // Stored as null-terminated strings in order to work with built-in string functions static constexpr TCHAR RootCharacterStr[] = TEXT("#"); static constexpr TCHAR PathDelimiterCharacterStr[] = TEXT("/"); static constexpr TCHAR ArrayAppendCharacterStr[] = TEXT("-"); // unused, but kept for reference as official part of spec } FJsonPointer::FJsonPointer(FStringView InPath) { ParsePath(FString(InPath)); } FJsonPointer::FJsonPointer(const FJsonPointer& Other) : PathParts(Other.PathParts) { } FJsonPointer::FJsonPointer(FJsonPointer&& Other) noexcept : PathParts(MoveTemp(Other.PathParts)) { } static const TSharedPtr* FindPartInJsonValue(const TSharedPtr& Value, const FString& PathPart) { const TSharedPtr* NextValue = &Value; TArray>* ArrayPtr = nullptr; if ((*NextValue)->TryGetArray(ArrayPtr)) { if(!PathPart.IsNumeric()) { // part expected to be index UE_LOG(LogJson, Warning, TEXT("FJsonPointer::TryGet: JsonValue was an array and expected a numeric part but was a string.")); return nullptr; } const int32 PartAsIndex = FCString::Atoi(*PathPart); if (!ArrayPtr->IsValidIndex(PartAsIndex)) { // index out of range UE_LOG(LogJson, Warning, TEXT("FJsonPointer::TryGet: Array index %d for array is out of range."), PartAsIndex); return nullptr; } NextValue = &(*ArrayPtr)[PartAsIndex]; } else { const TSharedPtr* ObjectPtr; if((*NextValue)->TryGetObject(ObjectPtr)) { NextValue = (*ObjectPtr)->Values.Find(PathPart); if (NextValue == nullptr) { // part of path didn't exist UE_LOG(LogJson, Warning, TEXT("FJsonPointer::TryGet: Field \"%s\" not found."), *PathPart); return nullptr; } } } return NextValue; } bool FJsonPointer::TryGet(const TSharedPtr& RootObject, TSharedPtr& OutValue) const { return TryGet(MakeShared(RootObject), OutValue); } bool FJsonPointer::TryGet(const TSharedPtr& RootValue, TSharedPtr& OutValue) const { if (!IsValid()) { return false; } const TSharedPtr* CurrentValue = &RootValue; for (int32 Idx = 0; Idx < Length(); ++Idx) { CurrentValue = FindPartInJsonValue(*CurrentValue, PathParts[Idx]); if (CurrentValue == nullptr) { return false; } } OutValue = *CurrentValue; return true; } bool FJsonPointer::TryGet(const TArray>& RootValues, TSharedPtr& OutValue) const { if (!IsValid()) { return false; } const FString& FirstPart = PathParts[0]; if(!FirstPart.IsNumeric()) { UE_LOG(LogJson, Warning, TEXT("FJsonPointer::TryGet: RootObject was an array, but JsonPointer path requested a field \"%s\"."), *FirstPart); return false; } const int32 PartAsIndex = FCString::Atoi(*FirstPart); if (!RootValues.IsValidIndex(PartAsIndex)) { // index out of range UE_LOG(LogJson, Warning, TEXT("FJsonPointer::TryGet: Array index %d for array %s is out of range."), PartAsIndex, *FirstPart); return false; } return TryGet(RootValues[PartAsIndex], OutValue); } bool FJsonPointer::ParsePath(const FString& InPath) { TArray PathStrings; InPath.ParseIntoArray(PathStrings, Private::PathDelimiterCharacterStr, false); if(InPath.IsEmpty()) { return false; } // We don't really care about the document root, remove it if it exists if (PathStrings[0] == Private::RootCharacterStr) { PathStrings.RemoveAt(0); } for (int32 Idx = 0; Idx < PathStrings.Num(); ++Idx) { FString PartString = PathStrings[Idx]; if (PartString.IsEmpty()) { PathParts.Empty(); UE_LOG(LogJson, Warning, TEXT("Path part was empty in JSON pointer \"%s\""), *InPath); return false; } PathParts.Add(PartString); } return true; } void FJsonPointer::Append(FStringView Part) { PathParts.Add(FString(Part)); } void FJsonPointer::SetArrayIndex(int32 Index) { check(PathParts.Num() > 0); PathParts.Last() = LexToString(Index); } FJsonPointer FJsonPointer::GetSubPath(int32 NumParts) const { // NumParts is or exceeds length, so just return the whole path if (NumParts >= Length()) { return *this; } FJsonPointer SubPath; SubPath.PathParts.Reserve(NumParts); for (int32 Idx = 0; Idx < NumParts; ++Idx) { SubPath.PathParts.Add(PathParts[Idx]); } return SubPath; } FString FJsonPointer::ToString() const { return FString(Private::RootCharacterStr) + Private::PathDelimiterCharacterStr + FString::Join(PathParts, Private::PathDelimiterCharacterStr); } FString FJsonPointer::EscapePart(const FString& Part) { return Part .Replace(TEXT("~"), TEXT("~0")) .Replace(TEXT("/"), TEXT("~1")); } FString FJsonPointer::UnescapePart(const FString& Part) { // Performed in order according to: https://datatracker.ietf.org/doc/html/rfc6901#section-4 return Part .Replace(TEXT("~1"), TEXT("/")) .Replace(TEXT("~0"), TEXT("~")); } }