Skip to content

Commit

Permalink
DYN-6468 Add Guard for circular references that can impact Stringify.…
Browse files Browse the repository at this point in the history
…JSON and Remember node (#14651)

* Add depth gaurd for serializer

* typo in gate tooltip

* Add test and localized warning

---------

Co-authored-by: Craig Long <[email protected]>
  • Loading branch information
saintentropy and saintentropy authored Dec 1, 2023
1 parent b9ebaca commit 7fdfc7f
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 5 deletions.
4 changes: 2 additions & 2 deletions src/Libraries/CoreNodeModels/Logic/Gate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ namespace CoreNodeModels.Logic
[NodeSearchTags(nameof(Resources.GateSearchTags), typeof(Resources))]
[InPortNames(">")]
[InPortTypes("object")]
[InPortDescriptions(nameof(Resources.GateInPortToolTip), nameof(Resources))]
[InPortDescriptions(typeof(Resources), nameof(Resources.GateInPortToolTip))]
[OutPortNames(">")]
[OutPortTypes("object")]
[OutPortDescriptions(nameof(Resources.GateOutPortToolTip), nameof(Resources))]
[OutPortDescriptions(typeof(Resources), nameof(Resources.GateOutPortToolTip))]
[IsDesignScriptCompatible]
public class Gate : NodeModel
{
Expand Down
66 changes: 63 additions & 3 deletions src/Libraries/CoreNodes/Data.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
using Dynamo.Events;
using Dynamo.Logging;
using Dynamo.Session;
using System.Globalization;
using System.Text;

namespace DSCore
{
Expand Down Expand Up @@ -188,8 +190,9 @@ private static object DynamoJObjectToNative(JObject jObject)
/// <returns name="json">A JSON string where primitive types (e.g. double, int, boolean), Lists, and Dictionary's will be turned into the associated JSON type.</returns>
public static string StringifyJSON([ArbitraryDimensionArrayImport] object values)
{
return JsonConvert.SerializeObject(values,
new JsonConverter[]
var settings = new JsonSerializerSettings()
{
Converters = new JsonConverter[]
{
new DictConverter(),
new DesignScriptGeometryConverter(),
Expand All @@ -198,9 +201,66 @@ public static string StringifyJSON([ArbitraryDimensionArrayImport] object values
#if _WINDOWS
new PNGImageConverter(),
#endif
});
}
};

StringBuilder sb = new StringBuilder(256);
using (var writer = new StringWriter(sb, CultureInfo.InvariantCulture))
{
using (var jsonWriter = new MaxDepthJsonTextWriter(writer))
{
JsonSerializer.Create(settings).Serialize(jsonWriter, values);
}
return writer.ToString();
}
}

/// <summary>
/// Subclass of JsonTextWriter that limits a maximum supported object depth to prevent circular reference crashes when serializing arbitrary .NET objects types.
/// </summary>
private class MaxDepthJsonTextWriter : JsonTextWriter
{
private readonly int maxDepth = 15;
private int depth = 0;

public MaxDepthJsonTextWriter(TextWriter writer) : base(writer) { }

public override void WriteStartArray()
{
base.WriteStartArray();
depth++;
CheckDepth();
}

public override void WriteEndArray()
{
base.WriteEndArray();
depth--;
CheckDepth();
}

public override void WriteStartObject()
{
base.WriteStartObject();
depth++;
CheckDepth();
}

public override void WriteEndObject()
{
base.WriteEndObject();
depth--;
CheckDepth();
}

private void CheckDepth()
{
if (depth > maxDepth)
{
throw new JsonSerializationException(string.Format(Properties.Resources.Exception_Serialize_Depth_Unsupported, depth, maxDepth, Path));
}
}
}

#region Converters
/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Libraries/CoreNodes/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Libraries/CoreNodes/Properties/Resources.en-US.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
<data name="Exception_Deserialize_Unsupported_Cache" xml:space="preserve">
<value>The stored data can not be loaded.</value>
</data>
<data name="Exception_Serialize_Depth_Unsupported" xml:space="preserve">
<value>Depth {0} Exceeds MaxDepth {1} at path "{2}"</value>
</data>
<data name="Exception_Serialize_DesignScript_Unsupported" xml:space="preserve">
<value>This type of Geometry is not able to be serialized.</value>
</data>
Expand Down
3 changes: 3 additions & 0 deletions src/Libraries/CoreNodes/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
<data name="Exception_Deserialize_Unsupported_Cache" xml:space="preserve">
<value>The stored data can not be loaded.</value>
</data>
<data name="Exception_Serialize_Depth_Unsupported" xml:space="preserve">
<value>Depth {0} Exceeds MaxDepth {1} at path "{2}"</value>
</data>
<data name="Exception_Serialize_DesignScript_Unsupported" xml:space="preserve">
<value>This type of Geometry is not able to be serialized.</value>
</data>
Expand Down
20 changes: 20 additions & 0 deletions test/DynamoCoreTests/DSCoreDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Text;
using Dynamo.Graph.Nodes;
using Dynamo.Graph.Nodes.ZeroTouch;
using DynamoUnits;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
Expand Down Expand Up @@ -192,6 +193,25 @@ public void ParsingJSONInPythonReturnsSameResult()
AssertPreviewValue("cdad5bf1-f5f7-47f4-a119-ad42e5084cfa", true);
}

[Test]
[Category("UnitTests")]
public void SerializingObjectOverMaximumDepthFailes()
{
// Load test graph
string path = Path.Combine(TestDirectory, @"core\json\JSON_Serialization_Depth_Fail.dyn");
OpenModel(path);

var node = CurrentDynamoModel.CurrentWorkspace.NodeFromWorkspace<DSFunction>(
Guid.Parse("cc45bec3172e40dab4d967e9dd81cbdd"));

var expectedWarning = "Exceeds MaxDepth";

Assert.AreEqual(node.State, ElementState.Warning);
AssertPreviewValue("cc45bec3172e40dab4d967e9dd81cbdd", null);
Assert.AreEqual(node.Infos.Count, 1);
Assert.IsTrue(node.Infos.Any(x => x.Message.Contains(expectedWarning) && x.State == ElementState.Warning));
}

[Test]
[Category("UnitTests")]
public void RoundTripForBoundingBoxReturnsSameResult()
Expand Down
147 changes: 147 additions & 0 deletions test/core/json/JSON_Serialization_Depth_Fail.dyn
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
{
"Uuid": "4fac373f-9349-4b71-975a-9cf02784050b",
"IsCustomNode": false,
"Description": "",
"Name": "JSON_Serialization_Depth_Fail",
"ElementResolver": {
"ResolutionMap": {}
},
"Inputs": [],
"Outputs": [],
"Nodes": [
{
"ConcreteType": "Dynamo.Graph.Nodes.CodeBlockNodeModel, DynamoCore",
"Id": "498b579729a04e099c8209d4888fec47",
"NodeType": "CodeBlockNode",
"Inputs": [],
"Outputs": [
{
"Id": "32dfd39fdd044440aabe4c81155f1622",
"Name": "",
"Description": "Value of expression at line 1",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Replication": "Disabled",
"Description": "Allows for DesignScript code to be authored directly",
"Code": "[1,[2,[3,[4,[5,[6,[7,[8,[9,[10,[11,[12,[13,[14,[15,[false]]]]]]]]]]]]]]]];"
},
{
"ConcreteType": "Dynamo.Graph.Nodes.ZeroTouch.DSFunction, DynamoCore",
"Id": "cc45bec3172e40dab4d967e9dd81cbdd",
"NodeType": "FunctionNode",
"Inputs": [
{
"Id": "ad169e7d7ebc4fe9bd3f3807ad53385a",
"Name": "values",
"Description": "A List of values\n\nvar[]..[]",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Outputs": [
{
"Id": "c92ff66756e04454bc444cad9ed6eb43",
"Name": "json",
"Description": "A JSON string where primitive types (e.g. double, int, boolean), Lists, and Dictionary's will be turned into the associated JSON type.",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"FunctionSignature": "DSCore.Data.StringifyJSON@var[]..[]",
"Replication": "Auto",
"Description": "Stringify converts an arbitrary value or a list of arbitrary values to JSON. Replication can be used to apply the operation over a list, producing a list of JSON strings.\n\nData.StringifyJSON (values: var[]..[]): string"
}
],
"Connectors": [
{
"Start": "32dfd39fdd044440aabe4c81155f1622",
"End": "ad169e7d7ebc4fe9bd3f3807ad53385a",
"Id": "3775253b954646c9bb26d7ea0db8af10",
"IsHidden": "False"
}
],
"Dependencies": [],
"NodeLibraryDependencies": [],
"EnableLegacyPolyCurveBehavior": null,
"Thumbnail": "",
"GraphDocumentationURL": null,
"ExtensionWorkspaceData": [
{
"ExtensionGuid": "28992e1d-abb9-417f-8b1b-05e053bee670",
"Name": "Properties",
"Version": "3.0",
"Data": {}
},
{
"ExtensionGuid": "DFBD9CC0-DB40-457A-939E-8C8555555A9D",
"Name": "Generative Design",
"Version": "8.0",
"Data": {}
}
],
"Author": "",
"Linting": {
"activeLinter": "None",
"activeLinterId": "7b75fb44-43fd-4631-a878-29f4d5d8399a",
"warningCount": 0,
"errorCount": 0
},
"Bindings": [],
"View": {
"Dynamo": {
"ScaleFactor": 1.0,
"HasRunWithoutCrash": true,
"IsVisibleInDynamoLibrary": true,
"Version": "3.0.0.5795",
"RunType": "Automatic",
"RunPeriod": "1000"
},
"Camera": {
"Name": "_Background Preview",
"EyeX": -17.0,
"EyeY": 24.0,
"EyeZ": 50.0,
"LookX": 12.0,
"LookY": -13.0,
"LookZ": -58.0,
"UpX": 0.0,
"UpY": 1.0,
"UpZ": 0.0
},
"ConnectorPins": [],
"NodeViews": [
{
"Id": "498b579729a04e099c8209d4888fec47",
"Name": "Code Block",
"IsSetAsInput": false,
"IsSetAsOutput": false,
"Excluded": false,
"ShowGeometry": true,
"X": 74.5,
"Y": 257.5
},
{
"Id": "cc45bec3172e40dab4d967e9dd81cbdd",
"Name": "Data.StringifyJSON",
"IsSetAsInput": false,
"IsSetAsOutput": false,
"Excluded": false,
"ShowGeometry": true,
"X": 855.5,
"Y": 260.5
}
],
"Annotations": [],
"X": 0.0,
"Y": 0.0,
"Zoom": 1.0
}
}

0 comments on commit 7fdfc7f

Please sign in to comment.