@@ -5,13 +5,57 @@ import Foundation
55/// This encoder conforms to the TOON (Token-Oriented Object Notation) specification version 3.0.
66/// For more information, see https://github.com/toon-format/spec
77public final class TOONEncoder {
8-
98 /// The number of spaces per indentation level.
109 public var indent : Int = 2
1110
11+ /// The delimiter character used to separate array values and tabular row cells.
12+ ///
13+ /// The delimiter determines how multiple values are separated in inline arrays
14+ /// and tabular data rows.
15+ ///
16+ /// Example with `.comma`:
17+ /// ```toon
18+ /// tags[3]: reading,gaming,coding
19+ /// ```
20+ ///
21+ /// Example with `.tab`:
22+ /// ```toon
23+ /// items[2 ]{sku name}:
24+ /// A1 Widget
25+ /// B2 Gadget
26+ /// ```
27+ ///
28+ /// Example with `.pipe`:
29+ /// ```toon
30+ /// items[2|]{sku|name}:
31+ /// A1|Widget
32+ /// B2|Gadget
33+ /// ```
34+ public enum Delimiter : String , CaseIterable , Hashable , Sendable {
35+ /// Comma separator (`,`).
36+ case comma = " , "
37+
38+ /// Tab separator (`\t`).
39+ case tab = " \t "
40+
41+ /// Pipe separator (`|`).
42+ case pipe = " | "
43+ }
44+
1245 /// The delimiter to use for array values and tabular rows.
1346 public var delimiter : Delimiter = . comma
1447
48+ /// Key folding mode.
49+ public enum KeyFolding : Hashable , Sendable {
50+ /// No key folding.
51+ case disabled
52+
53+ /// Safe key folding.
54+ ///
55+ /// Only folds when all segments are valid identifiers.
56+ case safe
57+ }
58+
1559 /// Key folding mode for collapsing single-key object chains into dotted paths.
1660 ///
1761 /// When enabled, single-key nested objects like `{ a: { b: { c: 1 } } }`
@@ -39,55 +83,32 @@ public final class TOONEncoder {
3983 /// - Output: `a.b.c: 1`
4084 public var flattenDepth : Int = . max
4185
42- /// The maximum nesting depth allowed during encoding .
43- ///
44- /// - Default is 1000 .
45- public var recursionLimit : Int = 1000
86+ /// Limits for encoding to prevent resource exhaustion .
87+ public struct EncodingLimits : Hashable , Sendable {
88+ /// Maximum nesting depth .
89+ public var maxDepth : Int
4690
47- /// Key folding mode .
48- public enum KeyFolding : Hashable , Sendable {
49- /// No key folding.
50- case disabled
91+ /// Default limits suitable for most use cases .
92+ ///
93+ /// - `maxDepth`: 32 (prevents stack overflow from deep nesting)
94+ public static let `default` = EncodingLimits ( maxDepth : 32 )
5195
52- /// Safe key folding .
96+ /// Encoding limits that impose no restrictions .
5397 ///
54- /// Only folds when all segments are valid identifiers.
55- case safe
98+ /// - Warning: This configuration is unsafe for untrusted input
99+ /// and should only be used with trusted data.
100+ public static let unlimited = EncodingLimits ( maxDepth: . max)
101+
102+ public init ( maxDepth: Int ) {
103+ self . maxDepth = maxDepth
104+ }
56105 }
57106
58- /// The delimiter character used to separate array values and tabular row cells.
59- ///
60- /// The delimiter determines how multiple values are separated in inline arrays
61- /// and tabular data rows.
62- ///
63- /// Example with `.comma`:
64- /// ```toon
65- /// tags[3]: reading,gaming,coding
66- /// ```
107+ /// Limits for encoding to prevent resource exhaustion.
67108 ///
68- /// Example with `.tab`:
69- /// ```toon
70- /// items[2 ]{sku name}:
71- /// A1 Widget
72- /// B2 Gadget
73- /// ```
74- ///
75- /// Example with `.pipe`:
76- /// ```toon
77- /// items[2|]{sku|name}:
78- /// A1|Widget
79- /// B2|Gadget
80- /// ```
81- public enum Delimiter : String , CaseIterable , Hashable , Sendable {
82- /// Comma separator (`,`).
83- case comma = " , "
84-
85- /// Tab separator (`\t`).
86- case tab = " \t "
87-
88- /// Pipe separator (`|`).
89- case pipe = " | "
90- }
109+ /// Use this to protect against accidental or malicious deep nesting
110+ /// when encoding untrusted data.
111+ public var limits : EncodingLimits = . default
91112
92113 /// Creates a new TOON encoder with default configuration.
93114 ///
@@ -96,6 +117,7 @@ public final class TOONEncoder {
96117 /// - `delimiter`: `.comma`
97118 /// - `keyFolding`: `.disabled`
98119 /// - `flattenDepth`: `Int.max`
120+ /// - `limits`: `.default`
99121 public init ( ) { }
100122
101123 /// Encodes the given value into TOON format.
@@ -120,7 +142,7 @@ public final class TOONEncoder {
120142 v = . data( data)
121143 } else {
122144 var userInfo = [ CodingUserInfoKey: Any] ( )
123- userInfo [ . toonRecursionLimit ] = recursionLimit
145+ userInfo [ . toonEncodingMaxDepth ] = limits . maxDepth
124146 let encoder = Encoder ( userInfo: userInfo)
125147 try value. encode ( to: encoder)
126148 v = encoder. encodedValue
@@ -728,7 +750,7 @@ public final class TOONEncoder {
728750}
729751
730752fileprivate extension CodingUserInfoKey {
731- static let toonRecursionLimit = CodingUserInfoKey ( rawValue: " toonRecursionLimit " ) !
753+ static let toonEncodingMaxDepth = CodingUserInfoKey ( rawValue: " toonEncodingMaxDepth " ) !
732754}
733755
734756// MARK: - Internal Encoder
@@ -880,11 +902,14 @@ extension TOONEncoder {
880902 }
881903
882904 func encode< T: Encodable > ( _ value: T , forKey key: Key ) throws {
883- if let limit = encoder. userInfo [ . toonRecursionLimit] as? Int , encoder. codingPath. count > limit {
884- throw EncodingError . invalidValue ( value, EncodingError . Context (
885- codingPath: codingPath + [ key] ,
886- debugDescription: " Recursion limit of \( limit) exceeded "
887- ) )
905+ if let limit = encoder. userInfo [ . toonEncodingMaxDepth] as? Int , encoder. codingPath. count > limit {
906+ throw EncodingError . invalidValue (
907+ value,
908+ EncodingError . Context (
909+ codingPath: codingPath + [ key] ,
910+ debugDescription: " Recursion limit of \( limit) exceeded "
911+ )
912+ )
888913 }
889914 trackKey ( key. stringValue)
890915
@@ -1163,11 +1188,14 @@ extension TOONEncoder {
11631188 }
11641189
11651190 func encode< T: Encodable > ( _ value: T ) throws {
1166- if let limit = encoder. userInfo [ . toonRecursionLimit] as? Int , encoder. codingPath. count > limit {
1167- throw EncodingError . invalidValue ( value, EncodingError . Context (
1168- codingPath: codingPath + [ IndexedCodingKey ( intValue: count) ] ,
1169- debugDescription: " Recursion limit of \( limit) exceeded "
1170- ) )
1191+ if let limit = encoder. userInfo [ . toonEncodingMaxDepth] as? Int , encoder. codingPath. count > limit {
1192+ throw EncodingError . invalidValue (
1193+ value,
1194+ EncodingError . Context (
1195+ codingPath: codingPath + [ IndexedCodingKey ( intValue: count) ] ,
1196+ debugDescription: " Recursion limit of \( limit) exceeded "
1197+ )
1198+ )
11711199 }
11721200 // Handle special types
11731201 let mirror = Mirror ( reflecting: value)
@@ -1311,11 +1339,14 @@ extension TOONEncoder {
13111339 }
13121340
13131341 func encode< T: Encodable > ( _ value: T ) throws {
1314- if let limit = encoder. userInfo [ . toonRecursionLimit] as? Int , encoder. codingPath. count > limit {
1315- throw EncodingError . invalidValue ( value, EncodingError . Context (
1316- codingPath: codingPath,
1317- debugDescription: " Recursion limit of \( limit) exceeded "
1318- ) )
1342+ if let limit = encoder. userInfo [ . toonEncodingMaxDepth] as? Int , encoder. codingPath. count > limit {
1343+ throw EncodingError . invalidValue (
1344+ value,
1345+ EncodingError . Context (
1346+ codingPath: codingPath,
1347+ debugDescription: " Recursion limit of \( limit) exceeded "
1348+ )
1349+ )
13191350 }
13201351 // Handle special types
13211352 let mirror = Mirror ( reflecting: value)
0 commit comments