diff --git a/doc/source/commands/serialization.rst b/doc/source/commands/serialization.rst index 4e3c245ac1..6744e6929f 100644 --- a/doc/source/commands/serialization.rst +++ b/doc/source/commands/serialization.rst @@ -25,3 +25,14 @@ They allow to transform data structures into JSON objects and store them in a fi It serializes messages currently stored on message queues to ConfigNode (KSP data format) and adds them to KSP save files. It is **important** to remember that any data that you supply to :ref:`WRITEJSON` and :meth:`Connection:SENDMESSAGE` must be serializable. + +.. note:: + + It's not possible to serialize structures that loop on themselves. Only `Directed Acyclical Graphs `_ are supported. + + An example of a looping structure is:: + + SET a TO LIST(). + SET b TO LIST(a). + a:ADD(b). + WRITEJSON(a, "test"). // <-- This will fail \ No newline at end of file diff --git a/src/kOS.Safe/Serialization/SafeSerializationMgr.cs b/src/kOS.Safe/Serialization/SafeSerializationMgr.cs index 8c2470cef5..bfd2a5db00 100644 --- a/src/kOS.Safe/Serialization/SafeSerializationMgr.cs +++ b/src/kOS.Safe/Serialization/SafeSerializationMgr.cs @@ -36,16 +36,25 @@ public static bool IsPrimitiveStructure(object serialized) return serialized is PrimitiveStructure; } - private object DumpValue(object value, bool includeType) + private object DumpValue(object value, bool includeType, bool allowTruncatedRecursion, List seenList) { var valueDumper = value as IDumper; + if (!(value is string) && seenList.Contains(value)) + { + if (!allowTruncatedRecursion) + throw new KOSSerializationException("Trying to serialize a structure that loops on itself. Only Directed Acyclical Graphs are supported."); + return "...recurse..."; + } + if (valueDumper != null) { - return Dump(valueDumper, includeType); + return Dump(valueDumper, includeType, allowTruncatedRecursion, seenList); } else if (value is Dump) { return value; } else if (value is List) { - return (value as List).Select((v) => DumpValue(v, includeType)).ToList(); + var nextSeenList = new List(seenList); + nextSeenList.Add(value); + return (value as List).Select((v) => DumpValue(v, includeType, allowTruncatedRecursion, nextSeenList)).ToList(); } else if (IsSerializablePrimitive(value)) { return Structure.ToPrimitive(value); } else { @@ -53,15 +62,23 @@ private object DumpValue(object value, bool includeType) } } - public Dump Dump(IDumper dumper, bool includeType = true) + public Dump Dump(IDumper dumper, bool includeType = true, bool allowTruncatedRecursion = false, List seenList = null) { + // We want to allow DAG-like structure serialization (so nodes can occur in the output more than once. + // Cyclical graphs crash us with a stackoverflow however. To protect from this we check wether we're already + // in the list of objects that are between us and the root. + if (seenList == null) + seenList = new List(); + seenList = new List(seenList); + seenList.Add(dumper); + var dump = dumper.Dump(); List keys = new List(dump.Keys); foreach (object key in keys) { - dump[key] = DumpValue(dump[key], includeType); + dump[key] = DumpValue(dump[key], includeType, allowTruncatedRecursion, seenList); } if (includeType) @@ -72,9 +89,9 @@ public Dump Dump(IDumper dumper, bool includeType = true) return dump; } - public string Serialize(IDumper serialized, IFormatWriter formatter, bool includeType = true) + public string Serialize(IDumper serialized, IFormatWriter formatter, bool includeType = true, bool allowTruncatedRecursion = false) { - return formatter.Write(Dump(serialized, includeType)); + return formatter.Write(Dump(serialized, includeType, allowTruncatedRecursion)); } public object CreateValue(object value) @@ -221,7 +238,7 @@ public IDumper Deserialize(string input, IFormatReader formatter) public string ToString(IDumper dumper) { - return Serialize(dumper, TerminalFormatter.Instance, false); + return Serialize(dumper, TerminalFormatter.Instance, false, true); } } }