@@ -5,18 +5,12 @@ import Foundation
55/// This decoder 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 TOONDecoder {
8- /// The TOON specification version this decoder conforms to
9- public static let specVersion = " 3.0 "
10-
11- /// Number of spaces per indentation level
12- public var indent : Int = 2
13-
148 /// Path expansion mode for dotted keys
159 ///
1610 /// When enabled, dotted keys like `a.b.c: value` are expanded to nested objects.
1711 /// This is the inverse operation of TOONEncoder's keyFolding option.
1812 ///
19- /// Example with `.safe`:
13+ /// Example with `.safe` or `.automatic` :
2014 /// ```toon
2115 /// user.profile.name: John
2216 /// ```
@@ -26,7 +20,7 @@ public final class TOONDecoder {
2620 /// profile:
2721 /// name: John
2822 /// ```
29- public var expandPaths : PathExpansion = . disabled
23+ public var expandPaths : PathExpansion = . automatic
3024
3125 /// Limits for decoding to prevent resource exhaustion
3226 ///
@@ -35,36 +29,59 @@ public final class TOONDecoder {
3529
3630 /// Path expansion mode
3731 public enum PathExpansion : Hashable , Sendable {
32+ /// Automatic path expansion - expands dotted keys when they match the target type structure
33+ /// Falls back gracefully if expansion causes conflicts
34+ case automatic
35+
3836 /// No path expansion - dotted keys decoded as literal strings
3937 case disabled
4038
4139 /// Safe path expansion: expand dotted keys to nested objects with collision detection
40+ /// Throws an error on collision
4241 case safe
4342 }
4443
4544 /// Limits for decoding to prevent resource exhaustion
4645 public struct DecodingLimits : Hashable , Sendable {
47- /// Maximum input size in bytes (default: 10 MB)
46+ /// Maximum input size in bytes
4847 public var maxInputSize : Int
4948
50- /// Maximum nesting depth (default: 128)
49+ /// Maximum nesting depth
5150 public var maxDepth : Int
5251
53- /// Maximum number of keys in a single object (default: 10,000)
52+ /// Maximum number of keys in a single object
5453 public var maxObjectKeys : Int
5554
56- /// Maximum array length (default: 100,000)
55+ /// Maximum array length
5756 public var maxArrayLength : Int
5857
5958 /// Default limits suitable for most use cases
59+ ///
60+ /// - `maxInputSize`: 10 MB
61+ /// - `maxDepth`: 32 (prevents stack overflow from deep nesting)
62+ /// - `maxObjectKeys`: 10,000
63+ /// - `maxArrayLength`: 100,000
6064 public static let `default` = DecodingLimits (
6165 maxInputSize: 10 * 1024 * 1024 ,
62- maxDepth: 128 ,
63- maxObjectKeys: 10000 ,
66+ maxDepth: 32 ,
67+ maxObjectKeys: 10_000 ,
6468 maxArrayLength: 100_000
6569 )
6670
67- /// No limits - use with caution on trusted input only
71+ /// Decoding limits that impose no restrictions.
72+ ///
73+ /// - Warning: This configuration is unsafe for untrusted input and
74+ /// should only be used with data from trusted sources.
75+ /// Without limits, malicious input can cause excessive memory usage,
76+ /// stack overflow from deep nesting, or denial of service attacks.
77+ ///
78+ /// Use this only when you have full control over the input data and
79+ /// need to decode arbitrarily large or complex TOON structures.
80+ ///
81+ /// For production use with external input, use
82+ /// ``default`` or
83+ /// ``init(maxInputSize:maxDepth:maxObjectKeys:maxArrayLength:)``
84+ /// with appropriate limits instead.
6885 public static let unlimited = DecodingLimits (
6986 maxInputSize: . max,
7087 maxDepth: . max,
@@ -83,8 +100,8 @@ public final class TOONDecoder {
83100 /// Creates a new TOON decoder with default configuration
84101 ///
85102 /// Default settings:
86- /// - `indent `: 2 spaces
87- /// - `expandPaths `: `.disabled `
103+ /// - `expandPaths `: `.automatic`
104+ /// - `limits `: `.default `
88105 public init ( ) { }
89106
90107 /// Decodes TOON format data to the specified type
@@ -103,7 +120,7 @@ public final class TOONDecoder {
103120 throw TOONDecodingError . invalidFormat ( " Data is not valid UTF-8 " )
104121 }
105122
106- let parser = Parser ( text: text, indentSize : indent , expandPaths: expandPaths, limits: limits)
123+ let parser = Parser ( text: text, expandPaths: expandPaths, limits: limits)
107124 let value = try parser. parse ( )
108125
109126 let decoder = _Decoder ( value: value, codingPath: [ ] , userInfo: [ : ] )
@@ -227,17 +244,17 @@ private enum Value: Equatable {
227244
228245private final class Parser {
229246 private let lines : [ String ]
230- private let indentSize : Int
247+ private var indentSize : Int = 2
231248 private let expandPaths : TOONDecoder . PathExpansion
232249 private let limits : TOONDecoder . DecodingLimits
233250 private var currentLine : Int = 0
251+ private var indentDetected : Bool = false
234252
235- init ( text: String , indentSize : Int , expandPaths: TOONDecoder . PathExpansion , limits: TOONDecoder . DecodingLimits ) {
253+ init ( text: String , expandPaths: TOONDecoder . PathExpansion , limits: TOONDecoder . DecodingLimits ) {
236254 // Split by LF, handling potential CR+LF
237255 lines = text. replacingOccurrences ( of: " \r \n " , with: " \n " )
238256 . split ( separator: " \n " , omittingEmptySubsequences: false )
239257 . map ( String . init)
240- self . indentSize = indentSize
241258 self . expandPaths = expandPaths
242259 self . limits = limits
243260 }
@@ -322,8 +339,14 @@ private final class Parser {
322339 index = line. index ( after: index)
323340 }
324341
325- // Validate indentation is multiple of indent size
326- let depth = spaces / indentSize
342+ // Auto-detect indent size from first indented line
343+ if spaces > 0 && !indentDetected {
344+ indentSize = spaces
345+ indentDetected = true
346+ }
347+
348+ // Calculate depth based on detected or default indent size
349+ let depth = indentSize > 0 ? spaces / indentSize : 0
327350 return ( depth, line [ index... ] )
328351 }
329352
@@ -384,8 +407,20 @@ private final class Parser {
384407 let ( key, value) = try parseKeyValuePair ( String ( content) , atDepth: depth)
385408
386409 // Handle path expansion if enabled
387- if expandPaths == . safe && key. contains ( " . " ) && key. isValidDottedPath {
388- try expandDottedKey ( key, value: value, into: & values, keyOrder: & keyOrder)
410+ if ( expandPaths == . safe || expandPaths == . automatic) && key. contains ( " . " ) && key. isValidDottedPath {
411+ do {
412+ try expandDottedKey ( key, value: value, into: & values, keyOrder: & keyOrder)
413+ } catch {
414+ // For .automatic mode, fall back to literal key on collision
415+ if expandPaths == . automatic {
416+ if !keyOrder. contains ( key) {
417+ keyOrder. append ( key)
418+ }
419+ values [ key] = value
420+ } else {
421+ throw error
422+ }
423+ }
389424 } else {
390425 if !keyOrder. contains ( key) {
391426 keyOrder. append ( key)
0 commit comments