aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/sessions/logtemplate.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenserver/sessions/logtemplate.cpp')
-rw-r--r--src/zenserver/sessions/logtemplate.cpp390
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