diff options
Diffstat (limited to 'src/zenserver/sessions/logtemplate.cpp')
| -rw-r--r-- | src/zenserver/sessions/logtemplate.cpp | 390 |
1 files changed, 390 insertions, 0 deletions
diff --git a/src/zenserver/sessions/logtemplate.cpp b/src/zenserver/sessions/logtemplate.cpp new file mode 100644 index 000000000..b4d8f37e8 --- /dev/null +++ b/src/zenserver/sessions/logtemplate.cpp @@ -0,0 +1,390 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "logtemplate.h" + +#include <zencore/fmtutils.h> +#include <zencore/guid.h> +#include <zencore/iohash.h> +#include <zencore/string.h> +#include <zencore/uid.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <fmt/format.h> +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 |