Skip to content

Commit 5049c5a

Browse files
committed
Add EncodingLimits type and reframe recursion limits in terms of that
Reorder declarations
1 parent 6129aae commit 5049c5a

2 files changed

Lines changed: 93 additions & 62 deletions

File tree

Sources/ToonFormat/Encoder.swift

Lines changed: 92 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -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
77
public 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

730752
fileprivate 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)

Tests/ToonFormatTests/EncoderTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1591,7 +1591,7 @@ struct EncoderTests {
15911591
}
15921592

15931593
let encoder = TOONEncoder()
1594-
encoder.recursionLimit = 10
1594+
encoder.limits.maxDepth = 10
15951595

15961596
do {
15971597
_ = try encoder.encode(Container(value: makeDeepNest(depth: 20)))

0 commit comments

Comments
 (0)