Skip to content

Commit

Permalink
Produce a detailed error message when too much recursion is detected …
Browse files Browse the repository at this point in the history
…during serialization
  • Loading branch information
Antoine Aubry committed Feb 12, 2019
1 parent 2c6d522 commit 650b303
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 66 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

# Changelog

## Version 5.3.1

New features:
* Produce a detailed error message when too much recursion is detected during serialization.

## Version 5.3.0

New features:
Expand Down
20 changes: 20 additions & 0 deletions YamlDotNet.Test/Serialization/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1625,6 +1625,26 @@ public void TypesAreConvertedWhenNeededInsideDictionary()
Assert.Equal(3, result[5]);
}

[Fact]
public void InfiniteRecursionIsDetected()
{
var sut = new SerializerBuilder()
.DisableAliases()
.Build();

var recursionRoot = new
{
Nested = new[]
{
new Dictionary<string, object>()
}
};

recursionRoot.Nested[0].Add("loop", recursionRoot);

var exception = Assert.Throws<MaximumRecursionLevelReachedException>(() => sut.Serialize(recursionRoot));
}

[TypeConverter(typeof(DoublyConvertedTypeConverter))]
public class DoublyConverted
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using YamlDotNet.Core;
using YamlDotNet.Helpers;
using YamlDotNet.Serialization.Utilities;

Expand Down Expand Up @@ -66,77 +69,120 @@ public FullObjectGraphTraversalStrategy(ITypeInspector typeDescriptor, ITypeReso

void IObjectGraphTraversalStrategy.Traverse<TContext>(IObjectDescriptor graph, IObjectGraphVisitor<TContext> visitor, TContext context)
{
Traverse(graph, visitor, 0, context);
Traverse("<root>", graph, visitor, context, new Stack<ObjectPathSegment>(maxRecursion));
}

protected virtual void Traverse<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, int currentDepth, TContext context)
protected struct ObjectPathSegment
{
if (++currentDepth > maxRecursion)
public object name;
public IObjectDescriptor value;

public ObjectPathSegment(object name, IObjectDescriptor value)
{
throw new InvalidOperationException("Too much recursion when traversing the object graph");
this.name = name;
this.value = value;
}
}

protected virtual void Traverse<TContext>(object name, IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, TContext context, Stack<ObjectPathSegment> path)
{
if (path.Count >= maxRecursion)
{
var message = new StringBuilder();
message.AppendLine("Too much recursion when traversing the object graph.");
message.AppendLine("The path to reach this recursion was:");

var lines = new Stack<KeyValuePair<string, string>>(path.Count);
var maxNameLength = 0;
foreach (var segment in path)
{
var segmentName = TypeConverter.ChangeType<string>(segment.name);
maxNameLength = Math.Max(maxNameLength, segmentName.Length);
lines.Push(new KeyValuePair<string, string>(segmentName, segment.value.Type.FullName));
}

foreach (var line in lines)
{
message
.Append(" -> ")
.Append(line.Key.PadRight(maxNameLength))
.Append(" [")
.Append(line.Value)
.AppendLine("]");
}

throw new MaximumRecursionLevelReachedException(message.ToString());
}

if (!visitor.Enter(value, context))
{
return;
}

var typeCode = value.Type.GetTypeCode();
switch (typeCode)
path.Push(new ObjectPathSegment(name, value));
try
{
case TypeCode.Boolean:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
case TypeCode.String:
case TypeCode.Char:
case TypeCode.DateTime:
visitor.VisitScalar(value, context);
break;

case TypeCode.Empty:
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "TypeCode.{0} is not supported.", typeCode));

default:
if (value.IsDbNull())
{
visitor.VisitScalar(new ObjectDescriptor(null, typeof(object), typeof(object)), context);
}

if (value.Value == null || value.Type == typeof(TimeSpan))
{
var typeCode = value.Type.GetTypeCode();
switch (typeCode)
{
case TypeCode.Boolean:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
case TypeCode.String:
case TypeCode.Char:
case TypeCode.DateTime:
visitor.VisitScalar(value, context);
break;
}

var underlyingType = Nullable.GetUnderlyingType(value.Type);
if (underlyingType != null)
{
// This is a nullable type, recursively handle it with its underlying type.
// Note that if it contains null, the condition above already took care of it
Traverse(new ObjectDescriptor(value.Value, underlyingType, value.Type, value.ScalarStyle), visitor, currentDepth, context);
}
else
{
TraverseObject(value, visitor, currentDepth, context);
}
break;

case TypeCode.Empty:
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "TypeCode.{0} is not supported.", typeCode));

default:
if (value.IsDbNull())
{
visitor.VisitScalar(new ObjectDescriptor(null, typeof(object), typeof(object)), context);
}

if (value.Value == null || value.Type == typeof(TimeSpan))
{
visitor.VisitScalar(value, context);
break;
}

var underlyingType = Nullable.GetUnderlyingType(value.Type);
if (underlyingType != null)
{
// This is a nullable type, recursively handle it with its underlying type.
// Note that if it contains null, the condition above already took care of it
Traverse("Value", new ObjectDescriptor(value.Value, underlyingType, value.Type, value.ScalarStyle), visitor, context, path);
}
else
{
TraverseObject(value, visitor, context, path);
}
break;
}
}
finally
{
path.Pop();
}
}

protected virtual void TraverseObject<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, int currentDepth, TContext context)
protected virtual void TraverseObject<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, TContext context, Stack<ObjectPathSegment> path)
{
if (typeof(IDictionary).IsAssignableFrom(value.Type))
{
TraverseDictionary(value, visitor, currentDepth, typeof(object), typeof(object), context);
TraverseDictionary(value, visitor, typeof(object), typeof(object), context, path);
return;
}

Expand All @@ -145,56 +191,59 @@ protected virtual void TraverseObject<TContext>(IObjectDescriptor value, IObject
{
var adaptedDictionary = new GenericDictionaryToNonGenericAdapter(value.Value, genericDictionaryType);
var genericArguments = genericDictionaryType.GetGenericArguments();
TraverseDictionary(new ObjectDescriptor(adaptedDictionary, value.Type, value.StaticType, value.ScalarStyle), visitor, currentDepth, genericArguments[0], genericArguments[1], context);
TraverseDictionary(new ObjectDescriptor(adaptedDictionary, value.Type, value.StaticType, value.ScalarStyle), visitor, genericArguments[0], genericArguments[1], context, path);
return;
}

if (typeof(IEnumerable).IsAssignableFrom(value.Type))
{
TraverseList(value, visitor, currentDepth, context);
TraverseList(value, visitor, context, path);
return;
}

TraverseProperties(value, visitor, currentDepth, context);
TraverseProperties(value, visitor, context, path);
}

protected virtual void TraverseDictionary<TContext>(IObjectDescriptor dictionary, IObjectGraphVisitor<TContext> visitor, int currentDepth, Type keyType, Type valueType, TContext context)
protected virtual void TraverseDictionary<TContext>(IObjectDescriptor dictionary, IObjectGraphVisitor<TContext> visitor, Type keyType, Type valueType, TContext context, Stack<ObjectPathSegment> path)
{
visitor.VisitMappingStart(dictionary, keyType, valueType, context);

var isDynamic = dictionary.Type.FullName.Equals("System.Dynamic.ExpandoObject");
foreach (DictionaryEntry entry in (IDictionary)dictionary.Value)
{
var keyString = isDynamic ? namingConvention.Apply(entry.Key.ToString()) : entry.Key;
var key = GetObjectDescriptor(keyString, keyType);
var keyValue = isDynamic ? namingConvention.Apply(entry.Key.ToString()) : entry.Key;
var key = GetObjectDescriptor(keyValue, keyType);
var value = GetObjectDescriptor(entry.Value, valueType);

if (visitor.EnterMapping(key, value, context))
{
Traverse(key, visitor, currentDepth, context);
Traverse(value, visitor, currentDepth, context);
var keyAsString = TypeConverter.ChangeType<string>(key);
Traverse(keyValue, key, visitor, context, path);
Traverse(keyValue, value, visitor, context, path);
}
}

visitor.VisitMappingEnd(dictionary, context);
}

private void TraverseList<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, int currentDepth, TContext context)
private void TraverseList<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, TContext context, Stack<ObjectPathSegment> path)
{
var enumerableType = ReflectionUtility.GetImplementedGenericInterface(value.Type, typeof(IEnumerable<>));
var itemType = enumerableType != null ? enumerableType.GetGenericArguments()[0] : typeof(object);

visitor.VisitSequenceStart(value, itemType, context);

var index = 0;
foreach (var item in (IEnumerable)value.Value)
{
Traverse(GetObjectDescriptor(item, itemType), visitor, currentDepth, context);
Traverse(index, GetObjectDescriptor(item, itemType), visitor, context, path);
++index;
}

visitor.VisitSequenceEnd(value, context);
}

protected virtual void TraverseProperties<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, int currentDepth, TContext context)
protected virtual void TraverseProperties<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, TContext context, Stack<ObjectPathSegment> path)
{
visitor.VisitMappingStart(value, typeof(string), typeof(object), context);

Expand All @@ -204,8 +253,8 @@ protected virtual void TraverseProperties<TContext>(IObjectDescriptor value, IOb

if (visitor.EnterMapping(propertyDescriptor, propertyValue, context))
{
Traverse(new ObjectDescriptor(propertyDescriptor.Name, typeof(string), typeof(string)), visitor, currentDepth, context);
Traverse(propertyValue, visitor, currentDepth, context);
Traverse(propertyDescriptor.Name, new ObjectDescriptor(propertyDescriptor.Name, typeof(string), typeof(string)), visitor, context, path);
Traverse(propertyDescriptor.Name, propertyValue, visitor, context, path);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ public RoundtripObjectGraphTraversalStrategy(IEnumerable<IYamlTypeConverter> con
this.converters = converters;
}

protected override void TraverseProperties<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, int currentDepth, TContext context)
protected override void TraverseProperties<TContext>(IObjectDescriptor value, IObjectGraphVisitor<TContext> visitor, TContext context, Stack<ObjectPathSegment> path)
{
if (!value.Type.HasDefaultConstructor() && !converters.Any(c => c.Accepts(value.Type)))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Type '{0}' cannot be deserialized because it does not have a default constructor or a type converter.", value.Type));
}

base.TraverseProperties(value, visitor, currentDepth, context);
base.TraverseProperties(value, visitor, context, path);
}
}
}

0 comments on commit 650b303

Please sign in to comment.