Skip to content

Commit

Permalink
Implemented CSHARP-424. Circular references now throw a BsonSerializa…
Browse files Browse the repository at this point in the history
…tionException which can be caught instead of a StackOverflowException. Default maximum serialization depth is 100, but can be set higher if required.
  • Loading branch information
Robert Stam committed Apr 1, 2012
1 parent 1e667c4 commit d53ff25
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 4 deletions.
10 changes: 10 additions & 0 deletions Bson/BsonDefaults.cs
Expand Up @@ -28,6 +28,7 @@ public static class BsonDefaults
// private static fields
private static GuidRepresentation __guidRepresentation = GuidRepresentation.CSharpLegacy;
private static int __maxDocumentSize = 4 * 1024 * 1024; // 4MiB
private static int __maxSerializationDepth = 100;

// public static properties
/// <summary>
Expand All @@ -49,5 +50,14 @@ public static int MaxDocumentSize
get { return __maxDocumentSize; }
set { __maxDocumentSize = value; }
}

/// <summary>
/// Gets or sets the default max serialization depth (used to detect circular references during serialization). The default is 100.
/// </summary>
public static int MaxSerializationDepth
{
get { return __maxSerializationDepth; }
set { __maxSerializationDepth = value; }
}
}
}
4 changes: 4 additions & 0 deletions Bson/IO/BsonBinaryWriter.cs
Expand Up @@ -254,6 +254,7 @@ public override void WriteEndArray()
ThrowInvalidContextType("WriteEndArray", _context.ContextType, ContextType.Array);
}

base.WriteEndArray();
_buffer.WriteByte(0);
BackpatchSize(); // size of document

Expand All @@ -276,6 +277,7 @@ public override void WriteEndDocument()
ThrowInvalidContextType("WriteEndDocument", _context.ContextType, ContextType.Document, ContextType.ScopeDocument);
}

base.WriteEndDocument();
_buffer.WriteByte(0);
BackpatchSize(); // size of document

Expand Down Expand Up @@ -478,6 +480,7 @@ public override void WriteStartArray()
ThrowInvalidState("WriteStartArray", BsonWriterState.Value);
}

base.WriteStartArray();
_buffer.WriteByte((byte)BsonType.Array);
WriteNameHelper();
_context = new BsonBinaryWriterContext(_context, ContextType.Array, _buffer.Position);
Expand All @@ -497,6 +500,7 @@ public override void WriteStartDocument()
ThrowInvalidState("WriteStartDocument", BsonWriterState.Initial, BsonWriterState.Value, BsonWriterState.ScopeDocument, BsonWriterState.Done);
}

base.WriteStartDocument();
if (State == BsonWriterState.Value)
{
_buffer.WriteByte((byte)BsonType.Document);
Expand Down
4 changes: 4 additions & 0 deletions Bson/IO/BsonDocumentWriter.cs
Expand Up @@ -161,6 +161,7 @@ public override void WriteEndArray()
ThrowInvalidContextType("WriteEndArray", _context.ContextType, ContextType.Array);
}

base.WriteEndArray();
var array = _context.Array;
_context = _context.ParentContext;
WriteValue(array);
Expand All @@ -182,6 +183,7 @@ public override void WriteEndDocument()
ThrowInvalidContextType("WriteEndDocument", _context.ContextType, ContextType.Document, ContextType.ScopeDocument);
}

base.WriteEndDocument();
if (_context.ContextType == ContextType.ScopeDocument)
{
var scope = _context.Document;
Expand Down Expand Up @@ -376,6 +378,7 @@ public override void WriteStartArray()
ThrowInvalidState("WriteStartArray", BsonWriterState.Value);
}

base.WriteStartArray();
_context = new BsonDocumentWriterContext(_context, ContextType.Array, new BsonArray());
State = BsonWriterState.Value;
}
Expand All @@ -391,6 +394,7 @@ public override void WriteStartDocument()
ThrowInvalidState("WriteStartDocument", BsonWriterState.Initial, BsonWriterState.Value, BsonWriterState.ScopeDocument, BsonWriterState.Done);
}

base.WriteStartDocument();
switch (State)
{
case BsonWriterState.Initial:
Expand Down
37 changes: 33 additions & 4 deletions Bson/IO/BsonWriter.cs
Expand Up @@ -35,6 +35,7 @@ public abstract class BsonWriter : IDisposable
private string _name;
private bool _checkElementNames;
private bool _checkUpdateDocument;
private int _serializationDepth;

// constructors
/// <summary>
Expand Down Expand Up @@ -66,6 +67,14 @@ public bool CheckUpdateDocument
set { _checkUpdateDocument = value; }
}

/// <summary>
/// Gets the current serialization depth.
/// </summary>
public int SerializationDepth
{
get { return _serializationDepth; }
}

/// <summary>
/// Gets the settings of the writer.
/// </summary>
Expand Down Expand Up @@ -324,12 +333,18 @@ public void WriteDouble(string name, double value)
/// <summary>
/// Writes the end of a BSON array to the writer.
/// </summary>
public abstract void WriteEndArray();
public virtual void WriteEndArray()
{
_serializationDepth--;
}

/// <summary>
/// Writes the end of a BSON document to the writer.
/// </summary>
public abstract void WriteEndDocument();
public virtual void WriteEndDocument()
{
_serializationDepth--;
}

/// <summary>
/// Writes a BSON Int32 to the writer.
Expand Down Expand Up @@ -506,7 +521,14 @@ public void WriteRegularExpression(string name, string pattern, string options)
/// <summary>
/// Writes the start of a BSON array to the writer.
/// </summary>
public abstract void WriteStartArray();
public virtual void WriteStartArray()
{
_serializationDepth++;
if (_serializationDepth > _settings.MaxSerializationDepth)
{
throw new BsonSerializationException("Maximum serialization depth exceeded (does the object being serialized have a circular reference?).");
}
}

/// <summary>
/// Writes the start of a BSON array element to the writer.
Expand All @@ -521,7 +543,14 @@ public void WriteStartArray(string name)
/// <summary>
/// Writes the start of a BSON document to the writer.
/// </summary>
public abstract void WriteStartDocument();
public virtual void WriteStartDocument()
{
_serializationDepth++;
if (_serializationDepth > _settings.MaxSerializationDepth)
{
throw new BsonSerializationException("Maximum serialization depth exceeded (does the object being serialized have a circular reference?).");
}
}

/// <summary>
/// Writes the start of a BSON document element to the writer.
Expand Down
14 changes: 14 additions & 0 deletions Bson/IO/BsonWriterSettings.cs
Expand Up @@ -29,6 +29,7 @@ public abstract class BsonWriterSettings
// private fields
private GuidRepresentation _guidRepresentation = BsonDefaults.GuidRepresentation;
private bool _isFrozen;
private int _maxSerializationDepth = BsonDefaults.MaxSerializationDepth;

// constructors
/// <summary>
Expand Down Expand Up @@ -69,6 +70,19 @@ public bool IsFrozen
get { return _isFrozen; }
}

/// <summary>
/// Gets or sets the max serialization depth allowed (used to detect circular references).
/// </summary>
public int MaxSerializationDepth
{
get { return _maxSerializationDepth; }
set
{
if (_isFrozen) { ThrowFrozenException(); }
_maxSerializationDepth = value;
}
}

// public methods
/// <summary>
/// Creates a clone of the settings.
Expand Down
4 changes: 4 additions & 0 deletions Bson/IO/JsonWriter.cs
Expand Up @@ -271,6 +271,7 @@ public override void WriteEndArray()
ThrowInvalidState("WriteEndArray", BsonWriterState.Value);
}

base.WriteEndArray();
_textWriter.Write("]");

_context = _context.ParentContext;
Expand All @@ -288,6 +289,7 @@ public override void WriteEndDocument()
ThrowInvalidState("WriteEndDocument", BsonWriterState.Name);
}

base.WriteEndDocument();
if (_jsonWriterSettings.Indent && _context.HasElements)
{
_textWriter.Write(_jsonWriterSettings.NewLineChars);
Expand Down Expand Up @@ -559,6 +561,7 @@ public override void WriteStartArray()
ThrowInvalidState("WriteStartArray", BsonWriterState.Value, BsonWriterState.Initial);
}

base.WriteStartArray();
WriteNameHelper(Name);
_textWriter.Write("[");

Expand All @@ -577,6 +580,7 @@ public override void WriteStartDocument()
ThrowInvalidState("WriteStartDocument", BsonWriterState.Value, BsonWriterState.Initial, BsonWriterState.ScopeDocument);
}

base.WriteStartDocument();
if (State == BsonWriterState.Value || State == BsonWriterState.ScopeDocument)
{
WriteNameHelper(Name);
Expand Down
1 change: 1 addition & 0 deletions BsonUnitTests/BsonUnitTests.csproj
Expand Up @@ -95,6 +95,7 @@
<Compile Include="DefaultSerializer\SerializeFlagsTests.cs" />
<Compile Include="DefaultSerializer\Serializers\AnimalHierarchyWithAttributesTests.cs" />
<Compile Include="DefaultSerializer\Serializers\AnimalHierarchyWithoutAttributesTests.cs" />
<Compile Include="DefaultSerializer\Serializers\CircularReferencesTests.cs" />
<Compile Include="DefaultSerializer\Serializers\JaggedArraySerializerTests.cs" />
<Compile Include="DefaultSerializer\Serializers\ObjectSerializerTests.cs" />
<Compile Include="DefaultSerializer\Serializers\ThreeDimensionalArraySerializerTests.cs" />
Expand Down
@@ -0,0 +1,94 @@
/* Copyright 2010-2012 10gen Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;

namespace MongoDB.BsonUnitTests.Serialization
{
[TestFixture]
public class CircularReferencesTests
{
public class C
{
public int X { get; set; }
public C NestedDocument { get; set; }
public BsonArray BsonArray { get; set; }
}

[Test]
public void TestCircularBsonArray()
{
// note: setting a breakpoint in this method will crash the debugger if the locals window is open
// because it tries to display the value of array (presumably it's getting an internal stack overflow)
var array = new BsonArray();
array.Add(array);
var c1 = new C { X = 1, BsonArray = array };
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBson(); });
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBsonDocument(); });
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToJson(); });
}

[Test]
public void TestCircularDocument()
{
var c1 = new C { X = 1 };
c1.NestedDocument = c1;
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBson(); });
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBsonDocument(); });
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToJson(); });
}

[Test]
public void TestNoCircularReference()
{
var c2 = new C { X = 2 };
var c1 = new C { X = 1, NestedDocument = c2 };

var json = c1.ToJson();
var expected = "{ 'X' : 1, 'NestedDocument' : { 'X' : 2, 'NestedDocument' : null, 'BsonArray' : null }, 'BsonArray' : null }".Replace("'", "\"");
Assert.AreEqual(expected, json);

var memoryWriter = new MemoryStream();
using (var writer = BsonWriter.Create(memoryWriter))
{
BsonSerializer.Serialize(writer, c1);
Assert.AreEqual(0, writer.SerializationDepth);
}

var document = new BsonDocument();
using (var writer = BsonWriter.Create(document))
{
BsonSerializer.Serialize(writer, c1);
Assert.AreEqual(0, writer.SerializationDepth);
}

var stringWriter = new StringWriter();
using (var writer = BsonWriter.Create(stringWriter))
{
BsonSerializer.Serialize(writer, c1);
Assert.AreEqual(0, writer.SerializationDepth);
}
}
}
}

0 comments on commit d53ff25

Please sign in to comment.