using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using Robust.Shared.Collections; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; using YamlDotNet.Core; using YamlDotNet.Core.Events; namespace Robust.Shared.Serialization.Markdown; // YDN has broken nullable annotations. Yeppers. #nullable disable public static class DataNodeParser { public static IEnumerable ParseYamlStream(TextReader reader) { return ParseYamlStream(reader, internStrings: false); } internal static IEnumerable ParseYamlStream(TextReader reader, bool internStrings) { return ParseYamlStream(new Parser(reader), internStrings); } internal static IEnumerable ParseYamlStream(Parser parser, bool internStrings = false) { var state = new ParserState(internStrings); parser.Consume(); while (!parser.TryConsume(out _)) { yield return ParseDocument(parser, state); } // System.Console.WriteLine(state.TotalStringsSaved); } private static DataNodeDocument ParseDocument(Parser parser, ParserState parserState) { var state = new DocumentState(parserState); parser.Consume(); var root = Parse(parser, state); parser.Consume(); ResolveAliases(state); return new DataNodeDocument(root); } private static DataNode Parse(Parser parser, DocumentState state) { if (parser.Current is Scalar) return ParseValue(parser, state); if (parser.Current is SequenceStart) return ParseSequence(parser, state); if (parser.Current is MappingStart) return ParseMapping(parser, state); if (parser.Current is AnchorAlias) return ParseAlias(parser, state); throw new NotSupportedException(); } private static DataNode ParseAlias(Parser parser, DocumentState state) { var alias = parser.Consume(); if (!state.Anchors.TryGetValue(alias.Value, out var node)) { // Don't have this anchor yet. It may be defined later in the document. return new DataNodeAlias(alias.Value); } return node; } private static ValueDataNode ParseValue(Parser parser, DocumentState state) { var ev = parser.Consume(); var node = new ValueDataNode(ev) { Tag = ConvertTag(ev.Tag, state.ParserState), Value = state.ParserState.InternString(ev.Value) }; NodeParsed(node, ev, false, state); return node; } private static string ParseKey(Parser parser) { var ev = parser.Consume(); if (!ev.Anchor.IsEmpty) throw new NotSupportedException(); return ev.Value; } private static SequenceDataNode ParseSequence(Parser parser, DocumentState state) { var ev = parser.Consume(); var node = new SequenceDataNode(); node.Tag = ConvertTag(ev.Tag, state.ParserState); node.Start = ev.Start; var unresolvedAlias = false; SequenceEnd seqEnd; while (!parser.TryConsume(out seqEnd)) { var value = Parse(parser, state); node.Add(value); unresolvedAlias |= value is DataNodeAlias; } node.End = seqEnd.End; NodeParsed(node, ev, unresolvedAlias, state); return node; } private static MappingDataNode ParseMapping(Parser parser, DocumentState state) { var ev = parser.Consume(); var node = new MappingDataNode(); node.Tag = ConvertTag(ev.Tag, state.ParserState); var unresolvedAlias = false; MappingEnd mapEnd; while (!parser.TryConsume(out mapEnd)) { var key = state.ParserState.InternString(ParseKey(parser)); var value = Parse(parser, state); node.Add(key, value); unresolvedAlias |= value is DataNodeAlias; } node.End = mapEnd.End; NodeParsed(node, ev, unresolvedAlias, state); return node; } private static void NodeParsed(DataNode node, NodeEvent ev, bool unresolvedAlias, DocumentState state) { if (unresolvedAlias) state.UnresolvedAliasOwners.Add(node); if (ev.Anchor.IsEmpty) return; if (state.Anchors.ContainsKey(ev.Anchor)) throw new DataParseException($"Duplicate anchor defined in document: {ev.Anchor}"); state.Anchors[ev.Anchor] = node; } private static void ResolveAliases(DocumentState state) { foreach (var node in state.UnresolvedAliasOwners) { switch (node) { case MappingDataNode mapping: ResolveMappingAliases(mapping, state); break; case SequenceDataNode sequence: ResolveSequenceAliases(sequence, state); break; } } } private static void ResolveMappingAliases(MappingDataNode mapping, DocumentState state) { var swaps = new ValueList<(string key, DataNode value)>(); foreach (var (key, value) in mapping) { if (value is not DataNodeAlias valueAlias) return; var newValue = ResolveAlias(valueAlias, state); swaps.Add((key, newValue)); mapping.Remove(key); } foreach (var (key, value) in swaps) { mapping[key] = value; } } private static void ResolveSequenceAliases(SequenceDataNode sequence, DocumentState state) { for (var i = 0; i < sequence.Count; i++) { if (sequence[i] is DataNodeAlias alias) sequence[i] = ResolveAlias(alias, state); } } private static DataNode ResolveAlias(DataNodeAlias alias, DocumentState state) { if (!state.Anchors.TryGetValue(alias.Anchor, out var node)) throw new DataParseException($"Unable to resolve alias '{alias.Anchor}'"); return node; } private static string ConvertTag(TagName tag, ParserState state) { return (tag.IsNonSpecific || tag.IsEmpty) ? null : state.InternString(tag.Value); } private sealed class DocumentState(ParserState parserState) { public readonly ParserState ParserState = parserState; public readonly Dictionary Anchors = new(); public ValueList UnresolvedAliasOwners; } private sealed class DataNodeAlias : DataNode { public readonly AnchorName Anchor; public DataNodeAlias(AnchorName anchor) : base(default, default) { Anchor = anchor; } public override bool IsEmpty => true; public override DataNode Copy() { throw new NotSupportedException(); } public override DataNode Except(DataNode node) { throw new NotSupportedException(); } [Obsolete("Use SerializationManager.PushComposition()")] public override DataNode PushInheritance(DataNode parent) { throw new NotSupportedException(); } } #nullable enable private sealed class ParserState(bool internStrings) { public readonly HashSet? StringInternIndex = internStrings ? [] : null; //public int TotalStringsSaved = 0; [return: NotNullIfNotNull(nameof(str))] public string? InternString(string? str) { if (StringInternIndex == null) return str; if (str == null) return null; // Use a basic string interning system to avoid releasing a bunch of equivalent strings. // This avoids having thousands of identical strings for stuff like "type" in prototypes stored in memory. if (StringInternIndex.TryGetValue(str, out var indexedString)) { // if (!ReferenceEquals(str, indexedString)) // TotalStringsSaved += 1; return indexedString; } StringInternIndex.Add(str); return str; } } } public sealed class DataParseException : Exception { public DataParseException() { } public DataParseException(string message) : base(message) { } public DataParseException(string message, Exception inner) : base(message, inner) { } } public sealed record DataNodeDocument(DataNode Root);