1+ using System ;
12using System . Collections ;
23using System . Collections . Generic ;
34using System . Globalization ;
@@ -7,10 +8,14 @@ namespace Immutable.Audience
78{
89 internal static class Json
910 {
11+ // Depth cap so a pathological input throws FormatException
12+ // instead of blowing the stack (StackOverflow is uncatchable).
13+ internal const int MaxDepth = 64 ;
14+
1015 internal static string Serialize ( Dictionary < string , object > data )
1116 {
1217 var sb = new StringBuilder ( ) ;
13- WriteObject ( sb , data , indent : 0 , depth : 0 ) ;
18+ WriteObject ( sb , data , indent : 0 , depth : 0 , visited : null ) ;
1419 return sb . ToString ( ) ;
1520 }
1621
@@ -22,11 +27,11 @@ internal static string Serialize(Dictionary<string, object> data, int indent)
2227 {
2328 if ( indent <= 0 ) return Serialize ( data ) ;
2429 var sb = new StringBuilder ( ) ;
25- WriteObject ( sb , data , indent , depth : 0 ) ;
30+ WriteObject ( sb , data , indent , depth : 0 , visited : null ) ;
2631 return sb . ToString ( ) ;
2732 }
2833
29- private static void WriteValue ( StringBuilder sb , object value , int indent , int depth )
34+ private static void WriteValue ( StringBuilder sb , object value , int indent , int depth , HashSet < object > visited )
3035 {
3136 if ( value == null )
3237 {
@@ -68,22 +73,25 @@ private static void WriteValue(StringBuilder sb, object value, int indent, int d
6873 }
6974 else if ( value is Dictionary < string , object > dict )
7075 {
71- WriteObject ( sb , dict , indent , depth ) ;
76+ WriteObject ( sb , dict , indent , depth , visited ) ;
7277 }
7378 else if ( value is IList list )
7479 {
75- WriteArray ( sb , list , indent , depth ) ;
80+ WriteArray ( sb , list , indent , depth , visited ) ;
7681 }
7782 else
7883 {
7984 WriteString ( sb , value . ToString ( ) ) ;
8085 }
8186 }
8287
83- private static void WriteObject ( StringBuilder sb , Dictionary < string , object > dict , int indent , int depth )
88+ private static void WriteObject ( StringBuilder sb , Dictionary < string , object > dict , int indent , int depth , HashSet < object > visited )
8489 {
90+ GuardDepth ( depth ) ;
91+ visited = EnterContainer ( dict , visited ) ;
92+
8593 sb . Append ( '{' ) ;
86- if ( dict . Count == 0 ) { sb . Append ( '}' ) ; return ; }
94+ if ( dict . Count == 0 ) { sb . Append ( '}' ) ; visited . Remove ( dict ) ; return ; }
8795
8896 var pretty = indent > 0 ;
8997 var first = true ;
@@ -94,26 +102,31 @@ private static void WriteObject(StringBuilder sb, Dictionary<string, object> dic
94102 if ( pretty ) AppendNewline ( sb , indent , depth + 1 ) ;
95103 WriteString ( sb , kvp . Key ) ;
96104 sb . Append ( pretty ? ": " : ":" ) ;
97- WriteValue ( sb , kvp . Value , indent , depth + 1 ) ;
105+ WriteValue ( sb , kvp . Value , indent , depth + 1 , visited ) ;
98106 }
99107 if ( pretty ) AppendNewline ( sb , indent , depth ) ;
100108 sb . Append ( '}' ) ;
109+ visited . Remove ( dict ) ;
101110 }
102111
103- private static void WriteArray ( StringBuilder sb , IList list , int indent , int depth )
112+ private static void WriteArray ( StringBuilder sb , IList list , int indent , int depth , HashSet < object > visited )
104113 {
114+ GuardDepth ( depth ) ;
115+ visited = EnterContainer ( list , visited ) ;
116+
105117 sb . Append ( '[' ) ;
106- if ( list . Count == 0 ) { sb . Append ( ']' ) ; return ; }
118+ if ( list . Count == 0 ) { sb . Append ( ']' ) ; visited . Remove ( list ) ; return ; }
107119
108120 var pretty = indent > 0 ;
109121 for ( var i = 0 ; i < list . Count ; i ++ )
110122 {
111123 if ( i > 0 ) sb . Append ( ',' ) ;
112124 if ( pretty ) AppendNewline ( sb , indent , depth + 1 ) ;
113- WriteValue ( sb , list [ i ] , indent , depth + 1 ) ;
125+ WriteValue ( sb , list [ i ] , indent , depth + 1 , visited ) ;
114126 }
115127 if ( pretty ) AppendNewline ( sb , indent , depth ) ;
116128 sb . Append ( ']' ) ;
129+ visited . Remove ( list ) ;
117130 }
118131
119132 private static void AppendNewline ( StringBuilder sb , int indent , int depth )
@@ -122,6 +135,29 @@ private static void AppendNewline(StringBuilder sb, int indent, int depth)
122135 sb . Append ( ' ' , indent * depth ) ;
123136 }
124137
138+ private static void GuardDepth ( int depth )
139+ {
140+ if ( depth >= MaxDepth )
141+ throw new FormatException (
142+ $ "JSON nesting exceeds { MaxDepth } levels — refusing to serialize. " +
143+ "Check for a cyclic or excessively deep dictionary/list." ) ;
144+ }
145+
146+ private static HashSet < object > EnterContainer ( object container , HashSet < object > visited )
147+ {
148+ visited ??= new HashSet < object > ( ReferenceEqualityComparer . Instance ) ;
149+ if ( ! visited . Add ( container ) )
150+ throw new FormatException ( "JSON graph contains a cycle — refusing to serialize." ) ;
151+ return visited ;
152+ }
153+
154+ private sealed class ReferenceEqualityComparer : IEqualityComparer < object >
155+ {
156+ internal static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer ( ) ;
157+ public new bool Equals ( object x , object y ) => ReferenceEquals ( x , y ) ;
158+ public int GetHashCode ( object obj ) => System . Runtime . CompilerServices . RuntimeHelpers . GetHashCode ( obj ) ;
159+ }
160+
125161 private static void WriteString ( StringBuilder sb , string s )
126162 {
127163 sb . Append ( '"' ) ;
0 commit comments