diff --git a/Sources/Decoder/CodableCBORDecoder.swift b/Sources/Decoder/CodableCBORDecoder.swift index 4f77d43..6c55297 100644 --- a/Sources/Decoder/CodableCBORDecoder.swift +++ b/Sources/Decoder/CodableCBORDecoder.swift @@ -3,6 +3,7 @@ import Foundation final public class CodableCBORDecoder { public var useStringKeys: Bool = false public var dateStrategy: DateStrategy = .taggedAsEpochTimestamp + public var maximumDepth: Int = .max struct _Options { let useStringKeys: Bool @@ -29,7 +30,7 @@ final public class CodableCBORDecoder { } var options: _Options { - return _Options(useStringKeys: self.useStringKeys, dateStrategy: self.dateStrategy) + return _Options(useStringKeys: self.useStringKeys, dateStrategy: self.dateStrategy, maximumDepth: self.maximumDepth) } public init() {} @@ -66,6 +67,7 @@ final public class CodableCBORDecoder { func setOptions(_ newOptions: _Options) { self.useStringKeys = newOptions.useStringKeys self.dateStrategy = newOptions.dateStrategy + self.maximumDepth = newOptions.maximumDepth } } @@ -78,34 +80,55 @@ final class _CBORDecoder { fileprivate var data: ArraySlice let options: CodableCBORDecoder._Options + var currentDepth: Int - init(data: ArraySlice, options: CodableCBORDecoder._Options) { + init(data: ArraySlice, options: CodableCBORDecoder._Options, currentDepth: Int = 0) { self.data = data self.options = options + self.currentDepth = currentDepth } } extension _CBORDecoder: Decoder { func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + guard self.currentDepth < self.options.maximumDepth else { + let context = DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Maximum decoding depth of \(self.options.maximumDepth) exceeded" + ) + throw DecodingError.dataCorrupted(context) + } + try ensureMap(self.data.first, keyType: Key.self) - let container = KeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options) + let container = KeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth) self.container = container return KeyedDecodingContainer(container) } func unkeyedContainer() throws -> UnkeyedDecodingContainer { + guard self.currentDepth < self.options.maximumDepth else { + let context = DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Maximum decoding depth of \(self.options.maximumDepth) exceeded" + ) + throw DecodingError.dataCorrupted(context) + } + try ensureArray(self.data.first) - let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options) + // Check if this is a byte string (0x40-0x5f) being decoded as an array + let isByteString = (self.data.first ?? 0) >= 0x40 && (self.data.first ?? 0) <= 0x5f + + let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth, isByteString: isByteString) self.container = container return container } func singleValueContainer() throws -> SingleValueDecodingContainer { - let container = SingleValueContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options) + let container = SingleValueContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth) self.container = container return container @@ -134,8 +157,9 @@ extension _CBORDecoder: Decoder { func ensureArray(_ initialByte: UInt8?) throws { switch initialByte { - case .some(0x80...0x9f): - // all good, continue + case .some(0x80...0x9f), .some(0x40...0x5f): + // all good, continue (arrays 0x80-0x9f and byte strings 0x40-0x5f) + // Byte strings can be decoded as arrays of UInt8 return case nil: let context = DecodingError.Context( diff --git a/Sources/Decoder/KeyedDecodingContainer.swift b/Sources/Decoder/KeyedDecodingContainer.swift index 8847f37..c0636bb 100644 --- a/Sources/Decoder/KeyedDecodingContainer.swift +++ b/Sources/Decoder/KeyedDecodingContainer.swift @@ -14,13 +14,15 @@ extension _CBORDecoder { var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] let options: CodableCBORDecoder._Options + let currentDepth: Int - init(data: ArraySlice, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options) { + init(data: ArraySlice, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options, currentDepth: Int = 0) { self.codingPath = codingPath self.userInfo = userInfo self.data = data self.index = self.data.startIndex self.options = options + self.currentDepth = currentDepth } func checkCanDecodeValue(forKey key: Key) throws { @@ -41,7 +43,7 @@ extension _CBORDecoder { var nestedContainers: [AnyCodingKey: CBORDecodingContainer] = [:] - let unkeyedContainer = UnkeyedContainer(data: self.data.suffix(from: self.index), codingPath: self.codingPath, userInfo: self.userInfo, options: self.options) + let unkeyedContainer = UnkeyedContainer(data: self.data.suffix(from: self.index), codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth) unkeyedContainer.count = count * 2 var iterator = unkeyedContainer.nestedContainers.makeIterator() @@ -94,7 +96,7 @@ extension _CBORDecoder { // each key-value pair in the map. let nextIndex = self.data.startIndex.advanced(by: 1) let remainingData = self.data.suffix(from: nextIndex) - count = try? CBORDecoder(input: remainingData.map { $0 }).readPairsUntilBreak().keys.count + count = try? CBORDecoder(input: remainingData.map { $0 }, options: self.options.toCBOROptions()).readPairsUntilBreak().keys.count default: let context = DecodingError.Context( codingPath: self.codingPath, @@ -145,9 +147,10 @@ extension _CBORDecoder.KeyedContainer: KeyedDecodingContainerProtocol { try checkCanDecodeValue(forKey: key) let container = try self.nestedContainers()[anyCodingKeyForKey(key)]! - let decoder = CodableCBORDecoder() - decoder.setOptions(self.options) - return try decoder.decode(T.self, from: container.data) + let innerDecoder = _CBORDecoder(data: container.data, options: self.options, currentDepth: self.currentDepth + 1) + innerDecoder.codingPath = self.codingPath + [key] + innerDecoder.userInfo = self.userInfo + return try T(from: innerDecoder) } func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { @@ -170,17 +173,18 @@ extension _CBORDecoder.KeyedContainer: KeyedDecodingContainerProtocol { data: anyCodingKeyedContainer.data, codingPath: anyCodingKeyedContainer.codingPath, userInfo: anyCodingKeyedContainer.userInfo, - options: anyCodingKeyedContainer.options + options: anyCodingKeyedContainer.options, + currentDepth: anyCodingKeyedContainer.currentDepth ) return KeyedDecodingContainer(container) } func superDecoder() throws -> Decoder { - return _CBORDecoder(data: self.data, options: self.options) + return _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1) } func superDecoder(forKey key: Key) throws -> Decoder { - let decoder = _CBORDecoder(data: self.data, options: self.options) + let decoder = _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1) decoder.codingPath = [key] return decoder diff --git a/Sources/Decoder/SingleValueDecodingContainer.swift b/Sources/Decoder/SingleValueDecodingContainer.swift index f6b7446..ee6c743 100644 --- a/Sources/Decoder/SingleValueDecodingContainer.swift +++ b/Sources/Decoder/SingleValueDecodingContainer.swift @@ -7,13 +7,15 @@ extension _CBORDecoder { var data: ArraySlice var index: Data.Index let options: CodableCBORDecoder._Options + let currentDepth: Int - init(data: ArraySlice, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options) { + init(data: ArraySlice, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options, currentDepth: Int = 0) { self.codingPath = codingPath self.userInfo = userInfo self.data = data self.index = self.data.startIndex self.options = options + self.currentDepth = currentDepth } func checkCanDecode(_ type: T.Type, format: UInt8) throws { @@ -32,7 +34,7 @@ extension _CBORDecoder { extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { func decodeNil() -> Bool { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { return false } switch cbor { @@ -42,7 +44,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Bool.Type) throws -> Bool { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -55,7 +57,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: String.Type) throws -> String { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -68,7 +70,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Double.Type) throws -> Double { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -76,6 +78,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { case .double(let dbl): return dbl case .float(let flt): return Double(flt) case .half(let flt): return Double(flt) + case .date(let date): return date.timeIntervalSinceReferenceDate default: let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.typeMismatch(Double.self, context) @@ -83,7 +86,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Float.Type) throws -> Float { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -97,7 +100,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Int.Type) throws -> Int { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -111,7 +114,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Int8.Type) throws -> Int8 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -125,7 +128,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Int16.Type) throws -> Int16 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -139,7 +142,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Int32.Type) throws -> Int32 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -153,7 +156,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Int64.Type) throws -> Int64 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -167,7 +170,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: UInt.Type) throws -> UInt { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -180,7 +183,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: UInt8.Type) throws -> UInt8 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -193,7 +196,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: UInt16.Type) throws -> UInt16 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -206,7 +209,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: UInt32.Type) throws -> UInt32 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -219,7 +222,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: UInt64.Type) throws -> UInt64 { - guard let cbor = try? CBOR.decode(self.data.map { $0 }) else { + guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)") throw DecodingError.dataCorrupted(context) } @@ -232,7 +235,9 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: T.Type) throws -> T { - let decoder = _CBORDecoder(data: self.data, options: self.options) + let decoder = _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1) + decoder.codingPath = self.codingPath + decoder.userInfo = self.userInfo let value = try T(from: decoder) if let nextIndex = decoder.container?.index { self.index = nextIndex diff --git a/Sources/Decoder/UnkeyedDecodingContainer.swift b/Sources/Decoder/UnkeyedDecodingContainer.swift index 8b7fb25..f4ac032 100644 --- a/Sources/Decoder/UnkeyedDecodingContainer.swift +++ b/Sources/Decoder/UnkeyedDecodingContainer.swift @@ -14,6 +14,10 @@ extension _CBORDecoder { var index: Data.Index let options: CodableCBORDecoder._Options + let currentDepth: Int + + // Track if this is a byte string being decoded as an array + let isByteString: Bool lazy var count: Int? = { do { @@ -37,7 +41,18 @@ extension _CBORDecoder { // decoding each item in the array. let nextIndex = self.data.startIndex.advanced(by: 1) let remainingData = self.data.suffix(from: nextIndex) - return try? CBORDecoder(input: remainingData).readUntilBreak().count + return try? CBORDecoder(input: remainingData, options: self.options.toCBOROptions()).readUntilBreak().count + // Byte strings (0x40-0x5f) - return length so they can be decoded as arrays + case 0x40...0x57: + return Int(format & 0x1F) + case 0x58: + return Int(try read(UInt8.self)) + case 0x59: + return Int(try read(UInt16.self)) + case 0x5a: + return Int(try read(UInt32.self)) + case 0x5b: + return Int(try read(UInt64.self)) default: return nil } @@ -56,9 +71,40 @@ extension _CBORDecoder { var nestedContainers: [CBORDecodingContainer] = [] do { - for _ in 0.., codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options) { + init(data: ArraySlice, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options, currentDepth: Int = 0, isByteString: Bool = false) { self.codingPath = codingPath self.userInfo = userInfo self.data = data self.index = self.data.startIndex self.options = options + self.currentDepth = currentDepth + self.isByteString = isByteString } var isAtEnd: Bool { @@ -128,11 +176,10 @@ extension _CBORDecoder.UnkeyedContainer: UnkeyedDecodingContainer { defer { self.currentIndex += 1 } let container = self.nestedContainers[self.currentIndex] - let decoder = CodableCBORDecoder() - decoder.setOptions(self.options) - let value = try decoder.decode(T.self, from: container.data) - - return value + let innerDecoder = _CBORDecoder(data: container.data, options: self.options, currentDepth: self.currentDepth + 1) + innerDecoder.codingPath = self.codingPath + [AnyCodingKey(intValue: self.currentIndex)] + innerDecoder.userInfo = self.userInfo + return try T(from: innerDecoder) } func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { @@ -154,13 +201,14 @@ extension _CBORDecoder.UnkeyedContainer: UnkeyedDecodingContainer { data: anyCodingKeyContainer.data, codingPath: anyCodingKeyContainer.codingPath, userInfo: anyCodingKeyContainer.userInfo, - options: anyCodingKeyContainer.options + options: anyCodingKeyContainer.options, + currentDepth: anyCodingKeyContainer.currentDepth ) return KeyedDecodingContainer(container) } func superDecoder() throws -> Decoder { - return _CBORDecoder(data: self.data, options: self.options) + return _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1) } } @@ -194,7 +242,7 @@ extension _CBORDecoder.UnkeyedContainer { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Handling UTF8 strings with break bytes is not supported yet") // Arrays case 0x80...0x9f: - let container = _CBORDecoder.UnkeyedContainer(data: self.data.suffix(from: startIndex), codingPath: self.nestedCodingPath, userInfo: self.userInfo, options: self.options) + let container = _CBORDecoder.UnkeyedContainer(data: self.data.suffix(from: startIndex), codingPath: self.nestedCodingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth) _ = container.nestedContainers self.index = container.index @@ -208,7 +256,7 @@ extension _CBORDecoder.UnkeyedContainer { return container // Maps case 0xa0...0xbf: - let container = _CBORDecoder.KeyedContainer(data: self.data.suffix(from: startIndex), codingPath: self.nestedCodingPath, userInfo: self.userInfo, options: self.options) + let container = _CBORDecoder.KeyedContainer(data: self.data.suffix(from: startIndex), codingPath: self.nestedCodingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth) let _ = try container.nestedContainers() // FIXME self.index = container.index @@ -249,7 +297,8 @@ extension _CBORDecoder.UnkeyedContainer { data: self.data[range.startIndex..<(range.endIndex)], codingPath: self.codingPath, userInfo: self.userInfo, - options: self.options + options: self.options, + currentDepth: self.currentDepth ) return container } diff --git a/Tests/CodableCBORDecoderTests.swift b/Tests/CodableCBORDecoderTests.swift index 436ce6a..28ccada 100644 --- a/Tests/CodableCBORDecoderTests.swift +++ b/Tests/CodableCBORDecoderTests.swift @@ -125,4 +125,177 @@ class CodableCBORDecoderTests: XCTestCase { let dateTwo = try! CodableCBORDecoder().decode(Date.self, from: Data([0xc1, 0xfb, 0x41, 0xd4, 0x52, 0xd9, 0xec, 0x20, 0x00, 0x00])) XCTAssertEqual(dateTwo, expectedDateTwo) } + + /// Test that maximumDepth option is properly accessible and passed through + func testMaximumDepthOptionAccessible() throws { + // Test that maximumDepth is accessible and can be set + let decoder = CodableCBORDecoder() + XCTAssertEqual(decoder.maximumDepth, .max) // Default value + + decoder.maximumDepth = 100 + XCTAssertEqual(decoder.maximumDepth, 100) + + // Test that options are properly converted + let options = decoder.options + XCTAssertEqual(options.maximumDepth, 100) + + let cborOptions = options.toCBOROptions() + XCTAssertEqual(cborOptions.maximumDepth, 100) + + XCTAssertEqual(decoder.options.maximumDepth, decoder.maximumDepth) + } + + /// Test that depth is enforced across nested array structures + func testMaximumDepthEnforcedAcrossNestedArrays() throws { + // Create a deeply nested array: [[[[42]]]] (4 levels deep) + // Level 0: outer array + // Level 1: first nested array + // Level 2: second nested array + // Level 3: third nested array + // Level 4: innermost value (42) + let deeplyNested = try! CodableCBOREncoder().encode([[[[42]]]]) + + // Should succeed with depth limit of 5 or more + let decoder5 = CodableCBORDecoder() + decoder5.maximumDepth = 5 + XCTAssertNoThrow(try decoder5.decode([[[[Int]]]].self, from: deeplyNested)) + + // Should fail with depth limit of 3 (can't reach level 4) + let decoder3 = CodableCBORDecoder() + decoder3.maximumDepth = 3 + XCTAssertThrowsError(try decoder3.decode([[[[Int]]]].self, from: deeplyNested)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + XCTFail("Expected dataCorrupted error, got \(error)") + return + } + XCTAssertTrue(context.debugDescription.contains("Maximum decoding depth")) + } + + // Should fail with depth limit of 0 (can't even decode top level) + let decoder0 = CodableCBORDecoder() + decoder0.maximumDepth = 0 + XCTAssertThrowsError(try decoder0.decode([[[[Int]]]].self, from: deeplyNested)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + XCTFail("Expected dataCorrupted error, got \(error)") + return + } + XCTAssertTrue(context.debugDescription.contains("Maximum decoding depth")) + } + } + + /// Test that depth is enforced across nested map structures + func testMaximumDepthEnforcedAcrossNestedMaps() throws { + struct Level3: Codable, Equatable { let value: Int } + struct Level2: Codable, Equatable { let nested: Level3 } + struct Level1: Codable, Equatable { let nested: Level2 } + struct Level0: Codable, Equatable { let nested: Level1 } + + let deeply = Level0(nested: Level1(nested: Level2(nested: Level3(value: 42)))) + let encoded = try! CodableCBOREncoder().encode(deeply) + + // Should succeed with sufficient depth + let decoder5 = CodableCBORDecoder() + decoder5.maximumDepth = 5 + XCTAssertNoThrow(try decoder5.decode(Level0.self, from: encoded)) + + // Should fail with insufficient depth + let decoder2 = CodableCBORDecoder() + decoder2.maximumDepth = 2 + XCTAssertThrowsError(try decoder2.decode(Level0.self, from: encoded)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + XCTFail("Expected dataCorrupted error, got \(error)") + return + } + XCTAssertTrue(context.debugDescription.contains("Maximum decoding depth")) + } + } + + /// Test that depth is enforced across mixed array and map structures + func testMaximumDepthEnforcedAcrossMixedStructures() throws { + struct Inner: Codable, Equatable { let values: [Int] } + struct Outer: Codable, Equatable { let items: [Inner] } + + let mixed = Outer(items: [Inner(values: [1, 2]), Inner(values: [3, 4])]) + let encoded = try! CodableCBOREncoder().encode(mixed) + + // Structure depth: + // Level 0: Outer keyed container + // Level 1: items array + // Level 2: Inner keyed container + // Level 3: values array + // Level 4: Int values + + // Should succeed with depth 5 + let decoder5 = CodableCBORDecoder() + decoder5.maximumDepth = 5 + let decoded = try! decoder5.decode(Outer.self, from: encoded) + XCTAssertEqual(decoded, mixed) + + // Should fail with depth 2 (can't reach Inner level) + let decoder2 = CodableCBORDecoder() + decoder2.maximumDepth = 2 + XCTAssertThrowsError(try decoder2.decode(Outer.self, from: encoded)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + XCTFail("Expected dataCorrupted error, got \(error)") + return + } + XCTAssertTrue(context.debugDescription.contains("Maximum decoding depth")) + } + } + + /// Test the specific case from the bug report: 10-deep nested array with depth=5 should fail + func testDeepNestedArrayRespectDepthLimit() throws { + // Create 10-level deep nested array + typealias Level10 = [[[[[[[[[[Int]]]]]]]]]] + + let level1: [Int] = [42] + let level2: [[Int]] = [level1] + let level3: [[[Int]]] = [level2] + let level4: [[[[Int]]]] = [level3] + let level5: [[[[[Int]]]]] = [level4] + let level6: [[[[[[Int]]]]]] = [level5] + let level7: [[[[[[[Int]]]]]]] = [level6] + let level8: [[[[[[[[Int]]]]]]]] = [level7] + let level9: [[[[[[[[[Int]]]]]]]]] = [level8] + let level10: Level10 = [level9] + + let encoded = try! CodableCBOREncoder().encode(level10) + + // Should fail with depth limit of 5 + let decoder = CodableCBORDecoder() + decoder.maximumDepth = 5 + + XCTAssertThrowsError(try decoder.decode(Level10.self, from: encoded)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + XCTFail("Expected dataCorrupted error, got \(error)") + return + } + XCTAssertTrue(context.debugDescription.contains("Maximum decoding depth")) + } + } + + /// Test that depth tracking works correctly when decoding array elements + func testDepthTrackingInArrayElements() throws { + // Array of arrays: [[1], [2], [3]] + // Each inner array is at depth 1 when decoded as an element + let arrayOfArrays = [[1], [2], [3]] + let encoded = try! CodableCBOREncoder().encode(arrayOfArrays) + + // Should succeed with depth 3 + let decoder3 = CodableCBORDecoder() + decoder3.maximumDepth = 3 + let decoded = try! decoder3.decode([[Int]].self, from: encoded) + XCTAssertEqual(decoded, arrayOfArrays) + + // Should fail with depth 1 (can't decode inner arrays) + let decoder1 = CodableCBORDecoder() + decoder1.maximumDepth = 1 + XCTAssertThrowsError(try decoder1.decode([[Int]].self, from: encoded)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + XCTFail("Expected dataCorrupted error, got \(error)") + return + } + XCTAssertTrue(context.debugDescription.contains("Maximum decoding depth")) + } + } }