@@ -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,58 +83,41 @@ public final class TOONEncoder {
3983 /// - Output: `a.b.c: 1`
4084 public var flattenDepth : Int = . max
4185
42- /// Key folding mode .
43- public enum KeyFolding : Hashable , Sendable {
44- /// No key folding .
45- case disabled
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- /// Safe key folding .
91+ /// Default limits suitable for most use cases .
4892 ///
49- /// Only folds when all segments are valid identifiers.
50- case safe
51- }
52-
53- /// The delimiter character used to separate array values and tabular row cells.
54- ///
55- /// The delimiter determines how multiple values are separated in inline arrays
56- /// and tabular data rows.
57- ///
58- /// Example with `.comma`:
59- /// ```toon
60- /// tags[3]: reading,gaming,coding
61- /// ```
62- ///
63- /// Example with `.tab`:
64- /// ```toon
65- /// items[2 ]{sku name}:
66- /// A1 Widget
67- /// B2 Gadget
68- /// ```
69- ///
70- /// Example with `.pipe`:
71- /// ```toon
72- /// items[2|]{sku|name}:
73- /// A1|Widget
74- /// B2|Gadget
75- /// ```
76- public enum Delimiter : String , CaseIterable , Hashable , Sendable {
77- /// Comma separator (`,`).
78- case comma = " , "
93+ /// - `maxDepth`: 32 (prevents stack overflow from deep nesting)
94+ public static let `default` = EncodingLimits ( maxDepth: 32 )
7995
80- /// Tab separator (`\t`).
81- case tab = " \t "
96+ /// Encoding limits that impose no restrictions.
97+ ///
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)
82101
83- /// Pipe separator (`|`).
84- case pipe = " | "
102+ public init ( maxDepth: Int ) {
103+ self . maxDepth = maxDepth
104+ }
85105 }
86106
107+ /// Limits for encoding to prevent resource exhaustion.
108+ ///
109+ /// Use this to protect against accidental or malicious deep nesting
110+ /// when encoding untrusted data.
111+ public var limits : EncodingLimits = . default
112+
87113 /// Creates a new TOON encoder with default configuration.
88114 ///
89115 /// Default settings:
90116 /// - `indent`: 2 spaces
91117 /// - `delimiter`: `.comma`
92118 /// - `keyFolding`: `.disabled`
93119 /// - `flattenDepth`: `Int.max`
120+ /// - `limits`: `.default`
94121 public init ( ) { }
95122
96123 /// Encodes the given value into TOON format.
@@ -114,7 +141,9 @@ public final class TOONEncoder {
114141 } else if mirror. subjectType == Data . self, let data = value as? Data {
115142 v = . data( data)
116143 } else {
117- let encoder = Encoder ( userInfo: [ : ] )
144+ var userInfo = [ CodingUserInfoKey: Any] ( )
145+ userInfo [ . toonEncodingMaxDepth] = limits. maxDepth
146+ let encoder = Encoder ( userInfo: userInfo)
118147 try value. encode ( to: encoder)
119148 v = encoder. encodedValue
120149 }
@@ -720,6 +749,10 @@ public final class TOONEncoder {
720749 }
721750}
722751
752+ fileprivate extension CodingUserInfoKey {
753+ static let toonEncodingMaxDepth = CodingUserInfoKey ( rawValue: " toonEncodingMaxDepth " ) !
754+ }
755+
723756// MARK: - Internal Encoder
724757
725758extension TOONEncoder {
@@ -869,6 +902,15 @@ extension TOONEncoder {
869902 }
870903
871904 func encode< T: Encodable > ( _ value: T , forKey key: Key ) throws {
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+ )
913+ }
872914 trackKey ( key. stringValue)
873915
874916 // Handle special types by checking the mirror of the value
@@ -1146,6 +1188,15 @@ extension TOONEncoder {
11461188 }
11471189
11481190 func encode< T: Encodable > ( _ value: T ) throws {
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+ )
1199+ }
11491200 // Handle special types
11501201 let mirror = Mirror ( reflecting: value)
11511202 if mirror. subjectType == Date . self, let date = value as? Date {
@@ -1288,6 +1339,15 @@ extension TOONEncoder {
12881339 }
12891340
12901341 func encode< T: Encodable > ( _ value: T ) throws {
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+ )
1350+ }
12911351 // Handle special types
12921352 let mirror = Mirror ( reflecting: value)
12931353 if mirror. subjectType == Date . self, let date = value as? Date {
0 commit comments