// Copyright Epic Games, Inc. All Rights Reserved. #include "logtemplate.h" #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END namespace zen { using namespace std::literals; namespace { // Bounded recursion so pathological nesting (e.g. an object that references // itself through $format) can't stack-overflow the server. Depth counts // every nested template expansion OR value descent. constexpr size_t kMaxRecursionDepth = 16; void RenderTemplateInto(std::string_view Template, CbObjectView Fields, StringBuilderBase& Out, bool Localized, size_t Depth); void RenderValue(CbFieldView Field, StringBuilderBase& Out, bool Localized, size_t Depth); ////////////////////////////////////////////////////////////////////////// // // Path resolution: walk a UE field_path — `name` followed by zero or // more `.name` / `[N]` segments — starting from the fields root. Returns // an empty field on any miss. // CbFieldView ResolvePath(CbObjectView Root, std::string_view Path) { CbFieldView Cur; bool Entered = false; // have we applied at least one segment? const auto ApplyName = [&](std::string_view Name) -> bool { if (Name.empty()) { return false; } if (!Entered) { Cur = Root[Name]; } else { if (!Cur.IsObject()) { return false; } Cur = Cur.AsObjectView()[Name]; } Entered = true; return Cur.operator bool(); }; const auto ApplyIndex = [&](uint64_t Idx) -> bool { if (!Entered || !Cur.IsArray()) { return false; } uint64_t N = 0; for (CbFieldView Elem : Cur.AsArrayView().CreateViewIterator()) { if (N == Idx) { Cur = Elem; return Cur.operator bool(); } ++N; } return false; }; size_t i = 0; while (i < Path.size()) { const char C = Path[i]; if (C == '.') { ++i; continue; } if (C == '[') { const size_t End = Path.find(']', i + 1); if (End == std::string_view::npos) { return {}; } uint64_t Idx = 0; for (size_t j = i + 1; j < End; ++j) { const char D = Path[j]; if (D < '0' || D > '9') { return {}; } Idx = Idx * 10 + uint64_t(D - '0'); } if (!ApplyIndex(Idx)) { return {}; } i = End + 1; continue; } // Name segment: run until the next '.' or '['. const size_t NameStart = i; while (i < Path.size() && Path[i] != '.' && Path[i] != '[') { ++i; } if (!ApplyName(Path.substr(NameStart, i - NameStart))) { return {}; } } return Entered ? Cur : CbFieldView{}; } ////////////////////////////////////////////////////////////////////////// // // Primitive rendering. Uses natural string forms for each CbField type. // Non-string values are emitted without quotes — the caller (the JSON-ish // fallback) adds quotes only around string values. // void RenderPrimitive(CbFieldView Field, StringBuilderBase& Out) { if (Field.IsString()) { Out << Field.AsString(); return; } if (Field.IsBool()) { Out << (Field.AsBool() ? "true"sv : "false"sv); return; } if (Field.IsInteger()) { Out << Field.AsInt64(); return; } if (Field.IsFloat()) { // format_to into the builder directly — avoids the std::string // fmt::format would otherwise build just to hand to Append. fmt::format_to(StringBuilderAppender(Out), "{}", Field.AsDouble()); return; } if (Field.IsDateTime()) { Out.Append(Field.AsDateTime().ToIso8601()); return; } if (Field.IsObjectId()) { // ToString(char[]) writes the 24-char hex into a caller buffer; // the std::string overload would allocate. char Buf[Oid::StringLength + 1] = {}; Field.AsObjectId().ToString(Buf); Out << std::string_view(Buf, Oid::StringLength); return; } if (Field.IsHash()) { // Appender overload writes the 40-char hex directly into the // builder; the std::string overload would allocate. Field.AsHash().ToHexString(Out); return; } if (Field.IsUuid()) { Guid G = Field.AsUuid(); G.ToString(Out); return; } if (Field.IsNull()) { Out << "null"sv; return; } // Binary / attachment / custom / unknown → emit nothing rather than // a stream of garbage bytes. } ////////////////////////////////////////////////////////////////////////// // // JSON-ish fallback for bare objects / nested arrays. Compact single-line // with quoted string keys and string values, raw other types. Intended // for debug display — not strictly RFC-8259 JSON. // void AppendJsonishString(std::string_view S, StringBuilderBase& Out) { Out << '"'; for (char C : S) { switch (C) { case '"': Out << "\\\""sv; break; case '\\': Out << "\\\\"sv; break; case '\n': Out << "\\n"sv; break; case '\r': Out << "\\r"sv; break; case '\t': Out << "\\t"sv; break; default: Out << C; break; } } Out << '"'; } void AppendJsonishValue(CbFieldView Field, StringBuilderBase& Out, bool Localized, size_t Depth) { if (Field.IsString()) { AppendJsonishString(Field.AsString(), Out); return; } // Non-string leaves and nested objects/arrays go through RenderValue // so object short-circuits ($text / $format / ...) still apply. RenderValue(Field, Out, Localized, Depth); } ////////////////////////////////////////////////////////////////////////// // // Value rendering (the decision tree from the plan). // void RenderValue(CbFieldView Field, StringBuilderBase& Out, bool Localized, size_t Depth) { if (Depth >= kMaxRecursionDepth) { Out << "…"sv; return; } if (Field.IsObject()) { CbObjectView Obj = Field.AsObjectView(); if (CbFieldView Text = Obj["$text"sv]; Text.IsString()) { Out << Text.AsString(); return; } if (CbFieldView Format = Obj["$format"sv]; Format.IsString()) { RenderTemplateInto(Format.AsString(), Obj, Out, /*Localized=*/false, Depth + 1); return; } if (CbFieldView LocFormat = Obj["$locformat"sv]; LocFormat.IsString()) { RenderTemplateInto(LocFormat.AsString(), Obj, Out, /*Localized=*/true, Depth + 1); return; } // Bare object — JSON-ish fallback. Out << '{'; bool First = true; for (CbFieldView Entry : Obj.CreateViewIterator()) { if (!First) { Out << ", "sv; } First = false; AppendJsonishString(Entry.GetName(), Out); Out << ": "sv; AppendJsonishValue(Entry, Out, Localized, Depth + 1); } Out << '}'; return; } if (Field.IsArray()) { Out << '['; bool First = true; for (CbFieldView Elem : Field.AsArrayView().CreateViewIterator()) { if (!First) { Out << ", "sv; } First = false; AppendJsonishValue(Elem, Out, Localized, Depth + 1); } Out << ']'; return; } RenderPrimitive(Field, Out); } ////////////////////////////////////////////////////////////////////////// // // Template tokenizer + renderer. // void RenderTemplateInto(std::string_view Template, CbObjectView Fields, StringBuilderBase& Out, bool Localized, size_t Depth) { if (Depth >= kMaxRecursionDepth) { Out << "…"sv; return; } size_t i = 0; while (i < Template.size()) { const char C = Template[i]; // Localized escape: ` followed by {, }, or ` → literal. if (Localized && C == '`' && i + 1 < Template.size()) { const char Next = Template[i + 1]; if (Next == '{' || Next == '}' || Next == '`') { Out << Next; i += 2; continue; } } // Non-localized escape: {{ or }} → literal { or }. if (!Localized && C == '{' && i + 1 < Template.size() && Template[i + 1] == '{') { Out << '{'; i += 2; continue; } if (!Localized && C == '}' && i + 1 < Template.size() && Template[i + 1] == '}') { Out << '}'; i += 2; continue; } if (C == '{') { // Placeholder: scan until matching '}'. const size_t End = Template.find('}', i + 1); if (End == std::string_view::npos) { // Unterminated placeholder — emit the rest literally so we // don't silently drop data. UE would have asserted at emit. Out << Template.substr(i); return; } const std::string_view Path = Template.substr(i + 1, End - i - 1); const CbFieldView Resolved = ResolvePath(Fields, Path); if (Resolved) { RenderValue(Resolved, Out, Localized, Depth + 1); } // Missing placeholder: emit nothing. (UE asserts at emit time, // so in well-formed input this never fires.) i = End + 1; continue; } Out << C; ++i; } } } // namespace void RenderLogTemplate(std::string_view Template, CbObjectView Fields, StringBuilderBase& Out, bool Localized) { RenderTemplateInto(Template, Fields, Out, Localized, 0); } } // namespace zen