Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ public class NewtonsoftJsonSerializationProvider : ISerializationProvider
/// </summary>
public string FileExtension => ".json";

private readonly JsonSerializerSettings settings = new();
// Newtonsoft enforces MaxDepth only when reading, so writing is held to the same limit
// by DepthLimitedJsonTextWriter. Without it, TrySerialize accepts graphs Deserialize refuses.
private const int MaxDepth = 64;

private readonly JsonSerializerSettings settings = new() { MaxDepth = MaxDepth };

/// <summary>
/// Tries to serialize the specified object into the writer.
Expand All @@ -36,7 +40,7 @@ public bool TrySerialize(object obj, TextWriter writer)
try
{
// The caller owns the writer, so disposing the JSON writer must not close it.
using JsonTextWriter jsonWriter = new(writer) { CloseOutput = false };
using DepthLimitedJsonTextWriter jsonWriter = new(writer, MaxDepth) { CloseOutput = false };
JsonSerializer serializer = JsonSerializer.Create(settings);
serializer.Serialize(jsonWriter, obj);
jsonWriter.Flush();
Expand Down Expand Up @@ -91,4 +95,31 @@ public bool TrySerialize(object obj, TextWriter writer)
return default;
}
}

/// <summary>
/// A <see cref="JsonTextWriter"/> that refuses to nest objects or arrays deeper than the
/// reader will accept, so everything it writes can be read back.
/// </summary>
private sealed class DepthLimitedJsonTextWriter(TextWriter textWriter, int maxDepth) : JsonTextWriter(textWriter)
{
public override void WriteStartObject()
{
base.WriteStartObject();
EnsureDepth();
}

public override void WriteStartArray()
{
base.WriteStartArray();
EnsureDepth();
}

private void EnsureDepth()
{
if (Top > maxDepth)
{
throw new JsonWriterException($"The object graph is deeper than the maximum depth of {maxDepth}.");
}
}
}
}
66 changes: 66 additions & 0 deletions Essentials.Tests/NewtonsoftJsonSerializationProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace ktsu.Essentials.Tests;

using System.Globalization;
using System.IO;
using ktsu.Essentials;
using ktsu.Essentials.SerializationProviders.NewtonsoftJson;
using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand All @@ -28,6 +30,70 @@ public void TrySerialize_WriterThrowingIOException_ReturnsFalse()
Assert.IsFalse(provider.TrySerialize(new { A = 1 }, writer));
}

[TestMethod]
[DataRow(64)]
[DataRow(65)]
[DataRow(200)]
public void TrySerialize_DeepChain_SucceedsOnlyWhenItDeserializes(int depth)
{
NewtonsoftJsonSerializationProvider provider = new();
Node root = new() { Name = "0" };
Node tail = root;
for (int i = 1; i < depth; i++)
{
tail.Next = new Node { Name = i.ToString(CultureInfo.InvariantCulture) };
tail = tail.Next;
}

using StringWriter writer = new();
bool serialized = provider.TrySerialize(root, writer);
Node? roundTripped = ((ISerializationProvider)provider).Deserialize<Node>(writer.ToString());

if (depth <= 64)
{
Assert.IsTrue(serialized);
Assert.IsNotNull(roundTripped);
}
else
{
Assert.IsFalse(serialized);
}
}

[TestMethod]
[DataRow(64)]
[DataRow(65)]
public void TrySerialize_DeeplyNestedArrays_SucceedsOnlyWhenItDeserializes(int depth)
{
NewtonsoftJsonSerializationProvider provider = new();
object[] root = [];
for (int i = 1; i < depth; i++)
{
root = [root];
}

using StringWriter writer = new();
bool serialized = provider.TrySerialize(root, writer);
object? roundTripped = ((ISerializationProvider)provider).Deserialize<object>(writer.ToString());

if (depth <= 64)
{
Assert.IsTrue(serialized);
Assert.IsNotNull(roundTripped);
}
else
{
Assert.IsFalse(serialized);
}
}

private sealed class Node
{
public string Name { get; set; } = string.Empty;

public Node? Next { get; set; }
}

private sealed class FailingWriter : StringWriter
{
public override void Write(char value) => throw new IOException("disk full");
Expand Down
Loading