Skip to content

Commit b52932c

Browse files
authored
Merge pull request #44 from IvanMurzak/fix/recursive-blacklist-check
Fix/recursive-blacklist-check
2 parents b3a5365 + 9b1cba9 commit b52932c

4 files changed

Lines changed: 398 additions & 17 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Xunit;
2+
using com.IvanMurzak.ReflectorNet.Model;
3+
using System.Text.Json;
4+
5+
namespace com.IvanMurzak.ReflectorNet.ReflectorTests
6+
{
7+
public class BlacklistedArrayItemTests
8+
{
9+
private class BaseItem { }
10+
private class AllowedItem : BaseItem { public int Id = 1; }
11+
private class BlacklistedItem : BaseItem { public int Secret = 999; }
12+
13+
[Fact]
14+
public void TestBlacklistedItemInArray()
15+
{
16+
var reflector = new Reflector();
17+
reflector.Converters.BlacklistType(typeof(BlacklistedItem));
18+
19+
var array = new BaseItem[]
20+
{
21+
new AllowedItem(),
22+
new BlacklistedItem(),
23+
new AllowedItem()
24+
};
25+
26+
var serialized = reflector.Serialize(array);
27+
var json = serialized.valueJsonElement?.GetRawText();
28+
29+
Assert.NotNull(json);
30+
31+
// Check if the second element is null
32+
using (var doc = JsonDocument.Parse(json))
33+
{
34+
var root = doc.RootElement;
35+
Assert.Equal(JsonValueKind.Array, root.ValueKind);
36+
Assert.Equal(3, root.GetArrayLength());
37+
38+
Assert.NotEqual(JsonValueKind.Null, root[0].ValueKind);
39+
40+
// This is what we want to verify:
41+
Assert.Equal(JsonValueKind.Null, root[1].ValueKind);
42+
43+
Assert.NotEqual(JsonValueKind.Null, root[2].ValueKind);
44+
}
45+
}
46+
47+
[Fact]
48+
public void TestSerializedMemberListToStringWithNull()
49+
{
50+
var list = new SerializedMemberList();
51+
list.Add(null!);
52+
var str = list.ToString();
53+
Assert.Contains("Item[0]", str);
54+
}
55+
}
56+
}

ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
36
using Xunit;
47
using Xunit.Abstractions;
58

@@ -22,6 +25,12 @@ public class ImplementsBlacklistedInterface : IBlacklistedInterface { }
2225
public interface IBlacklistedGenericInterface<T> { }
2326
public class ImplementsBlacklistedGenericInterface : IBlacklistedGenericInterface<string> { }
2427

28+
// Generic interface with blacklisted type argument testing
29+
public interface IGenericInterface<T> { }
30+
public class ImplementsGenericInterfaceWithBlacklisted : IGenericInterface<BlacklistedBaseClass> { }
31+
public class ImplementsGenericInterfaceWithDerived : IGenericInterface<DerivedFromBlacklisted> { }
32+
public class ImplementsGenericInterfaceWithNonBlacklisted : IGenericInterface<NonBlacklistedClass> { }
33+
2534
// Non-blacklisted types for negative tests
2635
public class NonBlacklistedClass { }
2736
public class AnotherNonBlacklistedClass { }
@@ -211,6 +220,51 @@ public void IsTypeBlacklisted_ImplementsIDisposable_WhenBlacklisted_ReturnsTrue(
211220
_output.WriteLine("MemoryStream (implements IDisposable) correctly detected as blacklisted");
212221
}
213222

223+
[Fact]
224+
public void IsTypeBlacklisted_ImplementsGenericInterfaceWithBlacklistedTypeArg_ReturnsTrue()
225+
{
226+
// Arrange
227+
var reflector = new Reflector();
228+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
229+
230+
// Act - Class implements IGenericInterface<BlacklistedBaseClass>
231+
var result = reflector.Converters.IsTypeBlacklisted(typeof(ImplementsGenericInterfaceWithBlacklisted));
232+
233+
// Assert
234+
Assert.True(result);
235+
_output.WriteLine("Type implementing generic interface with blacklisted type argument correctly returns true");
236+
}
237+
238+
[Fact]
239+
public void IsTypeBlacklisted_ImplementsGenericInterfaceWithDerivedBlacklistedTypeArg_ReturnsTrue()
240+
{
241+
// Arrange
242+
var reflector = new Reflector();
243+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
244+
245+
// Act - Class implements IGenericInterface<DerivedFromBlacklisted> where DerivedFromBlacklisted extends BlacklistedBaseClass
246+
var result = reflector.Converters.IsTypeBlacklisted(typeof(ImplementsGenericInterfaceWithDerived));
247+
248+
// Assert
249+
Assert.True(result);
250+
_output.WriteLine("Type implementing generic interface with derived blacklisted type argument correctly returns true");
251+
}
252+
253+
[Fact]
254+
public void IsTypeBlacklisted_ImplementsGenericInterfaceWithNonBlacklistedTypeArg_ReturnsFalse()
255+
{
256+
// Arrange
257+
var reflector = new Reflector();
258+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
259+
260+
// Act - Class implements IGenericInterface<NonBlacklistedClass>
261+
var result = reflector.Converters.IsTypeBlacklisted(typeof(ImplementsGenericInterfaceWithNonBlacklisted));
262+
263+
// Assert
264+
Assert.False(result);
265+
_output.WriteLine("Type implementing generic interface with non-blacklisted type argument correctly returns false");
266+
}
267+
214268
#endregion
215269

216270
#region Array Tests
@@ -757,5 +811,208 @@ public void IsTypeBlacklisted_SeparateReflectorInstances_IndependentBlacklists()
757811
}
758812

759813
#endregion
814+
815+
#region Inheritance with Generics Tests
816+
817+
public class GenericBase<T> { }
818+
819+
// Case 1: Extended from a class that has a generic argument which is blacklisted
820+
public class DerivedFromGenericWithBlacklistedArg : GenericBase<BlacklistedBaseClass> { }
821+
822+
// Case 3: Extended from a class that extends a class with blacklisted generic arg
823+
public class DeeplyDerived : DerivedFromGenericWithBlacklistedArg { }
824+
825+
[Fact]
826+
public void IsTypeBlacklisted_DerivedFromGenericWithBlacklistedArg_ReturnsTrue()
827+
{
828+
var reflector = new Reflector();
829+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
830+
831+
Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(DerivedFromGenericWithBlacklistedArg)),
832+
"DerivedFromGenericWithBlacklistedArg should be blacklisted because it inherits from GenericBase<BlacklistedBaseClass>");
833+
}
834+
835+
[Fact]
836+
public void IsTypeBlacklisted_DeeplyDerivedFromGenericWithBlacklistedArg_ReturnsTrue()
837+
{
838+
var reflector = new Reflector();
839+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
840+
841+
Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(DeeplyDerived)),
842+
"DeeplyDerived should be blacklisted because it inherits from DerivedFromGenericWithBlacklistedArg");
843+
}
844+
845+
#endregion
846+
847+
#region Concurrency and Cache Tests
848+
849+
[Fact]
850+
public async Task IsTypeBlacklisted_ConcurrentAccess_NoExceptionsAndCorrectResults()
851+
{
852+
// Arrange
853+
var reflector = new Reflector();
854+
var exceptions = new System.Collections.Concurrent.ConcurrentBag<Exception>();
855+
var results = new System.Collections.Concurrent.ConcurrentBag<bool>();
856+
var barrier = new Barrier(4);
857+
858+
// Act - Run concurrent reads and writes
859+
var tasks = new Task[4];
860+
861+
// Task 0: Adds types to blacklist
862+
tasks[0] = Task.Run(() =>
863+
{
864+
try
865+
{
866+
barrier.SignalAndWait();
867+
for (int i = 0; i < 100; i++)
868+
{
869+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
870+
reflector.Converters.BlacklistType(typeof(IBlacklistedInterface));
871+
}
872+
}
873+
catch (Exception ex) { exceptions.Add(ex); }
874+
});
875+
876+
// Tasks 1-3: Read from blacklist concurrently
877+
for (int t = 1; t < 4; t++)
878+
{
879+
tasks[t] = Task.Run(() =>
880+
{
881+
try
882+
{
883+
barrier.SignalAndWait();
884+
for (int i = 0; i < 100; i++)
885+
{
886+
results.Add(reflector.Converters.IsTypeBlacklisted(typeof(DerivedFromBlacklisted)));
887+
results.Add(reflector.Converters.IsTypeBlacklisted(typeof(NonBlacklistedClass)));
888+
results.Add(reflector.Converters.IsTypeBlacklisted(typeof(List<BlacklistedBaseClass>)));
889+
}
890+
}
891+
catch (Exception ex) { exceptions.Add(ex); }
892+
});
893+
}
894+
895+
await Task.WhenAll(tasks);
896+
897+
// Assert
898+
Assert.Empty(exceptions);
899+
_output.WriteLine($"Completed {results.Count} concurrent blacklist checks without exceptions");
900+
}
901+
902+
[Fact]
903+
public async Task IsTypeBlacklisted_ConcurrentBlacklistModification_RetriesAndReturnsCorrectResult()
904+
{
905+
// Arrange
906+
var reflector = new Reflector();
907+
var correctResultsCount = 0;
908+
var barrier = new Barrier(2);
909+
910+
// Act - One thread modifies blacklist while another reads
911+
var writerTask = Task.Run(() =>
912+
{
913+
barrier.SignalAndWait();
914+
for (int i = 0; i < 50; i++)
915+
{
916+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
917+
Thread.Sleep(1); // Small delay to interleave
918+
reflector.Converters.RemoveBlacklistedType(typeof(BlacklistedBaseClass));
919+
}
920+
// End with type blacklisted
921+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
922+
});
923+
924+
var readerTask = Task.Run(() =>
925+
{
926+
barrier.SignalAndWait();
927+
for (int i = 0; i < 100; i++)
928+
{
929+
// Result may vary during concurrent modification, but should never throw
930+
var result = reflector.Converters.IsTypeBlacklisted(typeof(DerivedFromBlacklisted));
931+
if (result) Interlocked.Increment(ref correctResultsCount);
932+
}
933+
});
934+
935+
await Task.WhenAll(writerTask, readerTask);
936+
937+
// Final state should be consistent
938+
Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(DerivedFromBlacklisted)),
939+
"After writer finishes with BlacklistType, derived types should be blacklisted");
940+
_output.WriteLine($"Reader got 'true' result {correctResultsCount} times during concurrent modification");
941+
}
942+
943+
// Helper types for cache size test - generate many unique types
944+
public class CacheTestType1 { }
945+
public class CacheTestType2 { }
946+
public class CacheTestType3 { }
947+
public class CacheTestType4 { }
948+
public class CacheTestType5 { }
949+
950+
[Fact]
951+
public void IsTypeBlacklisted_CacheSizeLimit_ClearsWhenExceeded()
952+
{
953+
// Arrange
954+
var reflector = new Reflector();
955+
reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass));
956+
957+
// Get all types from the current assembly to fill the cache
958+
var types = typeof(IsTypeBlacklistedTests).Assembly.GetTypes()
959+
.Where(t => t != null)
960+
.Take(1100) // More than MaxBlacklistCacheSize (1000)
961+
.ToList();
962+
963+
// Act - Query many types to fill the cache beyond its limit
964+
foreach (var type in types)
965+
{
966+
try
967+
{
968+
reflector.Converters.IsTypeBlacklisted(type);
969+
}
970+
catch
971+
{
972+
// Some types may throw during reflection, ignore
973+
}
974+
}
975+
976+
// Query again - should still work correctly after cache was cleared
977+
var result1 = reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass));
978+
var result2 = reflector.Converters.IsTypeBlacklisted(typeof(DerivedFromBlacklisted));
979+
var result3 = reflector.Converters.IsTypeBlacklisted(typeof(NonBlacklistedClass));
980+
981+
// Assert - Results should still be correct after cache overflow
982+
Assert.True(result1, "Exact blacklisted type should return true");
983+
Assert.True(result2, "Derived from blacklisted type should return true");
984+
Assert.False(result3, "Non-blacklisted type should return false");
985+
986+
_output.WriteLine($"Queried {types.Count} types, cache correctly handles overflow");
987+
}
988+
989+
[Fact]
990+
public void IsTypeBlacklisted_CacheInvalidation_ReturnsCorrectResultAfterBlacklistChange()
991+
{
992+
// Arrange
993+
var reflector = new Reflector();
994+
995+
// Prime the cache with a "false" result
996+
var initialResult = reflector.Converters.IsTypeBlacklisted(typeof(NonBlacklistedClass));
997+
Assert.False(initialResult);
998+
999+
// Act - Blacklist the type
1000+
reflector.Converters.BlacklistType(typeof(NonBlacklistedClass));
1001+
1002+
// Assert - Cache should be invalidated, new result should be correct
1003+
var afterBlacklistResult = reflector.Converters.IsTypeBlacklisted(typeof(NonBlacklistedClass));
1004+
Assert.True(afterBlacklistResult, "After blacklisting, type should be detected as blacklisted");
1005+
1006+
// Act - Remove from blacklist
1007+
reflector.Converters.RemoveBlacklistedType(typeof(NonBlacklistedClass));
1008+
1009+
// Assert - Cache should be invalidated again
1010+
var afterRemoveResult = reflector.Converters.IsTypeBlacklisted(typeof(NonBlacklistedClass));
1011+
Assert.False(afterRemoveResult, "After removing from blacklist, type should not be detected as blacklisted");
1012+
1013+
_output.WriteLine("Cache correctly invalidated after blacklist modifications");
1014+
}
1015+
1016+
#endregion
7601017
}
7611018
}

ReflectorNet/src/Converter/Reflection/ArrayReflectionConverter.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,23 @@ protected override SerializedMember InternalSerialize(
7676

7777
foreach (var element in enumerable)
7878
{
79+
var thisElementType = element?.GetType();
80+
var currentType = thisElementType ?? elementType;
81+
7982
if (logger?.IsEnabled(LogLevel.Trace) == true)
8083
logger.LogTrace("{padding} Serializing item '{index}' of type '{type}' in '{objType}'.\nPath: {path}",
81-
StringUtils.GetPadding(depth), index, element?.GetType().GetTypeId() ?? elementType?.GetTypeId(), obj.GetType().GetTypeId(), context?.GetPath(obj));
84+
StringUtils.GetPadding(depth), index, currentType?.GetTypeId(), obj.GetType().GetTypeId(), context?.GetPath(obj));
85+
86+
if (thisElementType != null && reflector.Converters.IsTypeBlacklisted(thisElementType))
87+
{
88+
serializedList.Add(null!);
89+
index++;
90+
continue;
91+
}
8292

8393
serializedList.Add(reflector.Serialize(
8494
element,
85-
fallbackType: element?.GetType() ?? elementType,
95+
fallbackType: currentType,
8696
name: $"[{index++}]",
8797
recursive: recursive,
8898
flags: flags,

0 commit comments

Comments
 (0)