A fast, safe, and efficient CBOR serialization library for Swift on any platform. swiftpackageindex.com/thecoolwinter/CBOR/1.1.1/documentation/cbor
atproto swift cbor

Compare changes

Choose any two refs to compare.

+1059 -469
+4 -7
.github/workflows/ci.yml
··· 22 22 name: Testing CBOR (ubuntu) 23 23 needs: swiftlint 24 24 runs-on: ubuntu-latest 25 - container: swift:6.0-noble 25 + container: swift:6.2-noble 26 26 steps: 27 27 - name: Checkout repository 28 28 uses: actions/checkout@v5 29 - - uses: swift-actions/setup-swift@next 30 - with: 31 - swift-version: "6.0" 32 29 - name: Restore .build 33 30 id: "restore-build" 34 31 uses: actions/cache/restore@v4 ··· 56 53 uses: actions/checkout@v5 57 54 - uses: swift-actions/setup-swift@next 58 55 with: 59 - swift-version: "6.0" 56 + swift-version: "6.1" 60 57 - name: Restore .build 61 58 id: "restore-build" 62 59 uses: actions/cache/restore@v4 ··· 65 62 key: "swiftpm-tests-build-${{ runner.os }}-${{ github.event.pull_request.base.sha || github.event.after }}" 66 63 restore-keys: "swiftpm-tests-build-${{ runner.os }}-" 67 64 - name: Building Package 68 - run: set -o pipefail && swift build --build-tests | xcbeautify 65 + run: set -o pipefail && swift build --build-tests | xcbeautify --renderer github-actions 69 66 # run: set -o pipefail && swift build --build-tests // --sanitize fuzzer | xcbeautify 70 67 - name: Cache .build 71 68 if: steps.restore-build.outputs.cache-hit != 'true' ··· 74 71 path: .build 75 72 key: "swiftpm-tests-build-${{ runner.os }}-${{ github.event.pull_request.base.sha || github.event.after }}" 76 73 - name: Testing Package 77 - run: set -o pipefail && swift test --skip-build | xcbeautify 74 + run: set -o pipefail && swift test --skip-build | xcbeautify --renderer github-actions 78 75 # TODO: The Xcode version of the swift toolchain does not come with libfuzzer 79 76 # What needs to happen is we install the open source toolchain and then we can fuzz here... 80 77 # - name: Fuzzing For 15 Seconds
+12 -13
README.md
··· 32 32 - Supports decoding half precision floats (Float16) as a regular Float. 33 33 - Runs on Linux, Android, and Windows using the swift-foundation project when available. 34 34 - Fuzz tested for reliability against crashes. 35 - - Supports tagged items (will expand and add ability to inject your own tags in the future): 36 - - Dates 37 - - UUIDs 38 - - [CIDs](https://github.com/multiformats/cid) (tag 42) 35 + - ***NEW*** Supports tagged items with custom tag injection. 36 + - Dates are a special case, handled by the library. 37 + - Contains UUID example implementation. 39 38 - Flexible date parsing (tags `0` or `1` with support for any numeric value representation). 40 39 - Decoding multiple top-level objects using `decodeMultiple(_:from:)`. 41 - - *NEW* IPLD compatible DAG-CBOR encoder for content addressable data. 42 - - *NEW* Flexible date decoding for untagged date items encoded as strings, floating point values, or integers. 43 - 44 - > Note: This is not a valid CBOR/CDE encoder, it merely always outputs countable collections. CBOR/CDE should be implemented in the future as it's quite similar. 40 + - ***NEW*** IPLD compatible DAG-CBOR encoder for content addressable data. 41 + - ***NEW*** Flexible date decoding for untagged date items encoded as strings, floating point values, or integers. 45 42 46 43 ## Usage 47 44 ··· 90 87 let dagEncoder = DAGCBOREncoder(dateEncodingStrategy: .double) 91 88 ``` 92 89 93 - To use, conform your internal CID type to ``CIDType``. Do not conform standard types like `String` or `Data` to ``CIDType``, or the encoder will attempt to encode all of those data as tagged items. 90 + > [!NOTE] 91 + > DAG-CBOR does not allow tagged items (besides the CID item), and thus encoding dates must be done by encoding their 'raw' value directly. This is an application specific behavior, so ensure the encoder is using the correct date encoding behavior for compatibility. By default, the encoder will encode dates as an epoch `Double` timestamp. 92 + 93 + To use with CIDs, conform your internal CID type to ``CIDType``. Do not conform standard types like `String` or `Data` to ``CIDType``, or the encoder may attempt to encode all of those data as tagged items. 94 94 ```swift 95 95 struct CID: CIDType { 96 96 let data: Data ··· 112 112 } 113 113 } 114 114 ``` 115 + 115 116 > [!WARNING] 116 117 > 117 - > You **need** to prefix your data with the `NULL` byte when encoding. This library will not handle that for you. It is invalid DAG-CBOR encoding to not include the prefixed byte. 118 + > It's *your* responsibility to correctly encode CIDs in the data container. That includes the `NULL` byte for raw binary CID encoding (which DAG-CBOR expects). 118 119 119 120 Now, any time the encoder finds a `CID` type it will encode it using the correct tag. 120 121 ```swift ··· 127 128 // 4A # bytes(10) 128 129 // 00000102030405060708 # "\u0000\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b" 129 130 ``` 130 - > [!NOTE] 131 - > DAG-CBOR does not allow tagged items (besides the CID item), and thus encoding dates must be done by encoding their 'raw' value directly. This is an application specific behavior, so ensure the encoder is using the correct date encoding behavior for compatibility. By default, the encoder will encode dates as an epoch `Double` timestamp. 132 131 133 132 ## Documentation 134 133 ··· 140 139 141 140 ```swift 142 141 dependencies: [ 143 - .package(url: "https://github.com/thecoolwinter/CBOR.git", from: "1.0.0") 142 + .package(url: "https://github.com/thecoolwinter/CBOR.git", from: "1.1.0") 144 143 ] 145 144 ``` 146 145
+24 -51
Sources/CBOR/Decoder/CBORDecoder.swift
··· 23 23 public var options: DecodingOptions 24 24 25 25 /// Create a new CBOR decoder. 26 - /// - Parameter rejectIndeterminateLengths: Set to `false` to allow indeterminate length objects to be decoded. 27 - /// Defaults to *rejecting* indeterminate length items (strings, bytes, 28 - /// maps, and arrays). 29 - public init(rejectIndeterminateLengths: Bool = true) { 30 - self.options = DecodingOptions(rejectIndeterminateLengths: rejectIndeterminateLengths) 26 + /// 27 + /// All parameters match flags in ``DecodingOptions``. 28 + public init( 29 + rejectIndeterminateLengths: Bool = true, 30 + recursionDepth: Int = 50, 31 + rejectIntKeys: Bool = false, 32 + rejectUnorderedMap: Bool = false, 33 + rejectUndefined: Bool = false, 34 + rejectNaN: Bool = false, 35 + rejectInf: Bool = false, 36 + singleTopLevelItem: Bool = false 37 + ) { 38 + self.options = DecodingOptions( 39 + rejectIndeterminateLengths: rejectIndeterminateLengths, 40 + recursionDepth: recursionDepth, 41 + rejectIntKeys: rejectIntKeys, 42 + rejectUnorderedMap: rejectUnorderedMap, 43 + rejectUndefined: rejectUndefined, 44 + rejectNaN: rejectNaN, 45 + rejectInf: rejectInf, 46 + singleTopLevelItem: singleTopLevelItem 47 + ) 31 48 } 32 49 33 50 /// Create a new CBOR decoder ··· 61 78 } 62 79 } catch { 63 80 if let error = error as? ScanError { 64 - try throwScanError(error) 81 + throw error.decodingError() 65 82 } else { 66 83 throw error 67 84 } ··· 111 128 } 112 129 } catch { 113 130 if let error = error as? ScanError { 114 - try throwScanError(error) 131 + throw error.decodingError() 115 132 } else { 116 133 throw error 117 134 } 118 - } 119 - } 120 - 121 - private func throwScanError(_ error: ScanError) throws -> Never { 122 - switch error { 123 - case .unexpectedEndOfData: 124 - throw DecodingError.dataCorrupted( 125 - .init(codingPath: [], debugDescription: "Unexpected end of data.") 126 - ) 127 - case let .invalidMajorType(byte, offset): 128 - throw DecodingError.dataCorrupted(.init( 129 - codingPath: [], 130 - debugDescription: "Unexpected major type: \(String(byte, radix: 2)) at offset \(offset)" 131 - )) 132 - case let .invalidSize(byte, offset): 133 - throw DecodingError.dataCorrupted(.init( 134 - codingPath: [], 135 - debugDescription: "Unexpected size argument: \(String(byte, radix: 2)) at offset \(offset)" 136 - )) 137 - case let .expectedMajorType(offset): 138 - throw DecodingError.dataCorrupted(.init( 139 - codingPath: [], 140 - debugDescription: "Expected major type at offset \(offset)" 141 - )) 142 - case let .typeInIndeterminateString(type, offset): 143 - throw DecodingError.dataCorrupted(.init( 144 - codingPath: [], 145 - debugDescription: "Unexpected major type in indeterminate \(type) at offset \(offset)" 146 - )) 147 - case let .rejectedIndeterminateLength(type, offset): 148 - throw DecodingError.dataCorrupted(.init( 149 - codingPath: [], 150 - debugDescription: "Rejected indeterminate length type \(type) at offset \(offset)" 151 - )) 152 - case let .cannotRepresentInt(max, found, offset): 153 - throw DecodingError.dataCorrupted( 154 - .init( 155 - codingPath: [], 156 - debugDescription: "Failed to decode integer with maximum \(max), " 157 - + "found \(found) at \(offset)" 158 - ) 159 - ) 160 - case .noTagInformation, .invalidMajorTypeForTaggedItem: 161 - throw error // rethrow these guys 162 135 } 163 136 } 164 137 }
+12
Sources/CBOR/Decoder/Containers/DataRegion.swift
··· 107 107 } 108 108 } 109 109 110 + @inline(__always) 111 + @usableFromInline 112 + func checkIntSize<T: FixedWidthInteger, F: FixedWidthInteger>(_ value: T, previousSize: F) throws { 113 + guard value > previousSize else { 114 + throw ScanError.unnecessaryInt 115 + } 116 + } 117 + 110 118 @inlinable 111 119 func readInt<T: FixedWidthInteger>(as: T.Type, argument: UInt8, from index: Index) throws -> T { 112 120 let byteCount = argument ··· 118 126 case 24: 119 127 let intVal = try read(as: UInt8.self, from: index) 120 128 try checkIntConversion(T.self, val: intVal) 129 + try checkIntSize(intVal, previousSize: Constants.maxArgSize) 121 130 return T(intVal) 122 131 case 25: 123 132 let intVal = try read(as: UInt16.self, from: index) 124 133 try checkIntConversion(T.self, val: intVal) 134 + try checkIntSize(intVal, previousSize: UInt8.max) 125 135 return T(intVal) 126 136 case 26: 127 137 let intVal = try read(as: UInt32.self, from: index) 128 138 try checkIntConversion(T.self, val: intVal) 139 + try checkIntSize(intVal, previousSize: UInt16.max) 129 140 return T(intVal) 130 141 case 27: 131 142 let intVal = try read(as: UInt64.self, from: index) 132 143 try checkIntConversion(T.self, val: intVal) 144 + try checkIntSize(intVal, previousSize: UInt32.max) 133 145 return T(intVal) 134 146 default: 135 147 throw ScanError.invalidSize(byte: byteCount, offset: startIndex)
+34 -1
Sources/CBOR/Decoder/Containers/KeyedCBORDecodingContainer.swift
··· 12 12 #endif 13 13 14 14 struct KeyedCBORDecodingContainer<Key: CodingKey>: DecodingContextContainer, KeyedDecodingContainerProtocol { 15 - enum AnyKey: Hashable { 15 + enum AnyKey: Hashable, Comparable { 16 16 case int(Int) 17 17 case string(String) 18 18 ··· 32 32 Key(stringValue: string) 33 33 } 34 34 } 35 + 36 + static func < (_ lhs: AnyKey, _ rhs: AnyKey) -> Bool { 37 + switch (lhs, rhs) { 38 + case let (.string(lhs), .string(rhs)): 39 + if lhs.count == rhs.count { 40 + return lhs < rhs 41 + } 42 + return lhs.count < rhs.count 43 + case let (.int(lhs), .int(rhs)): 44 + return lhs < rhs 45 + default: 46 + print("Bruh") 47 + return false 48 + } 49 + } 35 50 } 36 51 37 52 let context: DecodingContext ··· 51 66 decodedKeys.reserveCapacity(childCount / 2) 52 67 53 68 var mapOffset = context.results.firstChildIndex(data.mapOffset) 69 + var lastKey: AnyKey? 54 70 for _ in 0..<childCount / 2 { 55 71 let key = context.results.load(at: mapOffset) 56 72 let decodedKey: AnyKey 57 73 switch key.type { 58 74 case .uint, .nint: 75 + guard !context.options.rejectIntKeys else { 76 + throw DecodingError.typeMismatch(String.self, context.error( 77 + "Configured to reject integer keys, found integer key in map at \(data.globalIndex)." 78 + )) 79 + } 59 80 let intVal = try SingleValueCBORDecodingContainer(context: context, data: key).decode(Int.self) 60 81 decodedKey = AnyKey.int(intVal) 61 82 case .string: ··· 72 93 let value = context.results.load(at: mapOffset) 73 94 mapOffset = context.results.siblingIndex(mapOffset) 74 95 96 + guard decodedKeys[decodedKey] == nil else { 97 + throw DecodingError.dataCorrupted(context.error("Duplicate map keys found: \(decodedKey)")) 98 + } 99 + 100 + if context.options.rejectUnorderedMap && lastKey != nil && lastKey! > decodedKey { 101 + throw DecodingError.dataCorrupted(context.error( 102 + "Configured to reject unordered map, found '\(decodedKey)' after" 103 + + "'\(String(describing: lastKey))', which is invalid." 104 + )) 105 + } 106 + 75 107 decodedKeys[decodedKey] = value 108 + lastKey = decodedKey 76 109 } 77 110 78 111 self.decodedKeys = decodedKeys
+60 -3
Sources/CBOR/Decoder/Containers/SingleValueCBORDecodingContainer.swift
··· 16 16 let data: DataRegion 17 17 } 18 18 19 + // MARK: - Decoder 20 + 19 21 extension SingleValueCBORDecodingContainer: Decoder { 20 22 func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey { 21 23 try KeyedDecodingContainer(KeyedCBORDecodingContainer(context: context, data: data)) ··· 31 33 } 32 34 33 35 extension SingleValueCBORDecodingContainer: SingleValueDecodingContainer { 36 + // MARK: - Nil 37 + 34 38 func decodeNil() -> Bool { 35 39 data.isNil() 36 40 } 37 41 42 + // MARK: - Bool 43 + 38 44 func decode(_: Bool.Type) throws -> Bool { 39 45 let argument = try checkType(.simple, arguments: 20, 21, as: Bool.self) 40 46 return argument == 21 41 47 } 42 48 49 + // MARK: - Floating Points 50 + 51 + /// Helper for checking floats against decoding flags. 52 + @inline(__always) 53 + private func checkFloatValidity<T: FloatingPoint>(_ value: T) throws -> T { 54 + if context.options.rejectNaN && value.isNaN { 55 + throw DecodingError.dataCorrupted(context.error("Found NaN \(T.self), configured to reject NaN values.")) 56 + } 57 + 58 + if context.options.rejectInf && value.isInfinite { 59 + throw DecodingError.dataCorrupted( 60 + context.error("Found \(value) \(T.self), configured to reject Infinite values.") 61 + ) 62 + } 63 + 64 + return value 65 + } 66 + 43 67 func decode(_: Float.Type) throws -> Float { 44 68 let arg = try checkType(.simple, arguments: 25, 26, as: Float.self) 45 69 if arg == 25 { ··· 49 73 context.error("Could not decode half-precision float into Swift Float.") 50 74 ) 51 75 } 52 - return value 76 + return try checkFloatValidity(value) 53 77 } 54 78 55 79 let floatRaw = try data.read(as: UInt32.self) 56 - return Float(bitPattern: floatRaw) 80 + return try checkFloatValidity(Float(bitPattern: floatRaw)) 57 81 } 58 82 59 83 func decode(_: Double.Type) throws -> Double { 60 84 try checkType(.simple, arguments: 27, as: Double.self) 61 85 let doubleRaw = try data.read(as: UInt64.self).littleEndian 62 - return Double(bitPattern: doubleRaw) 86 + return try checkFloatValidity(Double(bitPattern: doubleRaw)) 63 87 } 88 + 89 + // MARK: - Integers 64 90 65 91 func decode<T: Decodable & FixedWidthInteger>(_: T.Type) throws -> T { 66 92 try checkType(.uint, .nint, forType: T.self) ··· 77 103 return value 78 104 } 79 105 106 + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) 107 + func decode(_ type: Int128.Type) throws -> Int128 { 108 + try checkType(.uint, .nint, forType: Int128.self) 109 + let value = try data.readInt(as: Int128.self) 110 + if data.type == .nint { 111 + return -1 - value 112 + } 113 + return value 114 + } 115 + 116 + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) 117 + func decode(_ type: UInt128.Type) throws -> UInt128 { 118 + try checkType(.uint, .nint, forType: UInt128.self) 119 + let value = try data.readInt(as: UInt128.self) 120 + if data.type == .nint { 121 + throw DecodingError.typeMismatch( 122 + Int.self, 123 + context.error("Found a negative integer while attempting to decode an unsigned int \(UInt128.self).") 124 + ) 125 + } 126 + return value 127 + } 128 + 129 + // MARK: - String 130 + 80 131 func decode(_: String.Type) throws -> String { 81 132 try checkType(.string, forType: String.self) 82 133 ··· 128 179 } 129 180 return string 130 181 } 182 + 183 + // MARK: - Date 131 184 132 185 // Attempt first to decode a tagged date value, then move on and try decoding any of the following as a date: 133 186 // - Int ··· 190 243 } 191 244 } 192 245 246 + // MARK: - Data 247 + 193 248 private func _decode(_: Data.Type) throws -> Data { 194 249 try checkType(.bytes, forType: Data.self) 195 250 ··· 233 288 } 234 289 return string 235 290 } 291 + 292 + // MARK: - Decode 236 293 237 294 func decode<T: Decodable>(_ type: T.Type) throws -> T { 238 295 // Unfortunate force unwraps here, but necessary
+49
Sources/CBOR/Decoder/DAGCBORDecoder.swift
··· 1 + // 2 + // DAGCBORDecoder.swift 3 + // CBOR 4 + // 5 + // Created by Khan Winter on 10/20/25. 6 + // 7 + 8 + #if canImport(FoundationEssentials) 9 + import FoundationEssentials 10 + #else 11 + import Foundation 12 + #endif 13 + 14 + /// Decodes ``Decodable`` objects from DAG-CBOR data. 15 + /// 16 + /// This is really just a wrapper for ``CBORDecoder``, which constant configuration flags that ensure valid DAG-CBOR 17 + /// data is decoded. 18 + /// 19 + /// If this decoder is too strict, I'd suggest using ``CBORDecoder``, which will be much more flexible around decoding 20 + /// than this type. You can also loosen specific flags using the options member of that struct. For instance, this 21 + /// type will reject all unordered maps. If you find yourself needing to receive badly-defined DAG-CBOR, just 22 + /// use ``CBORDecoder``. 23 + public struct DAGCBORDecoder { 24 + /// The options that determine decoding behavior. 25 + let options: DecodingOptions 26 + 27 + /// Create a new DAG-CBOR decoder. 28 + public init() { 29 + self.options = DecodingOptions( 30 + rejectIndeterminateLengths: true, 31 + rejectIntKeys: true, 32 + rejectUnorderedMap: true, 33 + rejectUndefined: true, 34 + rejectNaN: true, 35 + rejectInf: true, 36 + singleTopLevelItem: true 37 + ) 38 + } 39 + 40 + /// Decodes the given type from DAG-CBOR binary data. 41 + /// - Parameters: 42 + /// - type: The decodable type to deserialize. 43 + /// - data: The DAG-CBOR data to decode from. 44 + /// - Returns: An instance of the decoded type. 45 + /// - Throws: A ``DecodingError`` with context and a debug description for a failed deserialization operation. 46 + public func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T { 47 + try CBORDecoder(options: options).decode(T.self, from: data) 48 + } 49 + }
+40 -1
Sources/CBOR/Decoder/DecodingOptions.swift
··· 13 13 /// For deterministic encoding, this **must** be enabled. 14 14 public var rejectIndeterminateLengths: Bool 15 15 16 + /// Maximum recursion depth. 17 + public var recursionDepth: Int = 50 18 + 19 + /// Reject maps with non-string keys.. 20 + public var rejectIntKeys: Bool 21 + 22 + /// Reject maps whose keys are out of order. 23 + public var rejectUnorderedMap: Bool 24 + 25 + /// Reject the `undefined` simple value (`23`). 26 + public var rejectUndefined: Bool 27 + 28 + /// Enable to reject decoded `NaN` floating point values. 29 + public var rejectNaN: Bool 30 + 31 + /// Enable to reject decoded infinite floating point values. 32 + public var rejectInf: Bool 33 + 34 + /// Require that CBOR data encapsulates the *entire* data object being decoded. 35 + /// When true, throws a decoding error if data is left over after scanning for valid CBOR structure, or if there 36 + /// are multiple top-level objects. 37 + public var singleTopLevelItem: Bool 38 + 16 39 /// Create a new options object. 17 - public init(rejectIndeterminateLengths: Bool = true) { 40 + public init( 41 + rejectIndeterminateLengths: Bool = true, 42 + recursionDepth: Int = 50, 43 + rejectIntKeys: Bool = false, 44 + rejectUnorderedMap: Bool = false, 45 + rejectUndefined: Bool = false, 46 + rejectNaN: Bool = false, 47 + rejectInf: Bool = false, 48 + singleTopLevelItem: Bool = false 49 + ) { 18 50 self.rejectIndeterminateLengths = rejectIndeterminateLengths 51 + self.recursionDepth = recursionDepth 52 + self.rejectIntKeys = rejectIntKeys 53 + self.rejectUnorderedMap = rejectUnorderedMap 54 + self.rejectUndefined = rejectUndefined 55 + self.rejectNaN = rejectNaN 56 + self.rejectInf = rejectInf 57 + self.singleTopLevelItem = singleTopLevelItem 19 58 } 20 59 }
+33 -29
Sources/CBOR/Decoder/Scanner/CBORScanner.swift
··· 11 11 import Foundation 12 12 #endif 13 13 14 - @usableFromInline 15 - enum ScanError: Error { 16 - case unexpectedEndOfData 17 - case invalidMajorType(byte: UInt8, offset: Int) 18 - case invalidSize(byte: UInt8, offset: Int) 19 - case expectedMajorType(offset: Int) 20 - case typeInIndeterminateString(type: MajorType, offset: Int) 21 - case rejectedIndeterminateLength(type: MajorType, offset: Int) 22 - case cannotRepresentInt(max: UInt, found: UInt, offset: Int) 23 - case noTagInformation(tag: UInt, offset: Int) 24 - case invalidMajorTypeForTaggedItem(tag: UInt, expected: Set<MajorType>, found: MajorType, offset: Int) 25 - } 26 - 27 14 /// # Why Scan? 28 15 /// I'd have loved to use a 'pop' method for decoding, where we only decode as data is requested. However, the way 29 16 /// Swift's decoding APIs work forces us to be able to be able to do random access for keys in maps, which requires ··· 52 39 53 40 consuming func scan() throws -> Results { 54 41 while !reader.isEmpty { 42 + if options.singleTopLevelItem && reader.index > 0 { 43 + throw ScanError.unreadDataAfterEnd 44 + } 45 + 55 46 let idx = reader.index 56 - try scanNext() 47 + try scanNext(depth: 0) 57 48 assert(idx < reader.index, "Scanner made no forward progress in scan") 58 49 } 50 + 51 + if options.singleTopLevelItem && !reader.isEmpty { 52 + throw ScanError.unreadDataAfterEnd 53 + } 54 + 59 55 return results 60 56 } 61 57 62 - private mutating func scanNext() throws { 58 + private mutating func scanNext(depth: Int) throws { 63 59 guard let type = reader.peekType(), let raw = reader.peek() else { 64 60 if reader.isEmpty { 65 61 throw ScanError.unexpectedEndOfData ··· 68 64 } 69 65 } 70 66 71 - try scanType(type: type, raw: raw) 67 + guard depth < options.recursionDepth else { 68 + throw ScanError.recursionLimit 69 + } 70 + 71 + try scanType(type: type, raw: raw, depth: depth) 72 72 } 73 73 74 - private mutating func scanType(type: MajorType, raw: UInt8) throws { 74 + private mutating func scanType(type: MajorType, raw: UInt8, depth: Int) throws { 75 75 switch type { 76 76 case .uint, .nint: 77 77 try scanInt(raw: raw) ··· 80 80 case .string: 81 81 try scanBytesOrString(.string, raw: raw) 82 82 case .array: 83 - try scanArray() 83 + try scanArray(depth: depth) 84 84 case .map: 85 - try scanMap() 85 + try scanMap(depth: depth) 86 86 case .simple: 87 87 try scanSimple(raw: raw) 88 88 case .tagged: 89 - try scanTagged(raw: raw) 89 + try scanTagged(raw: raw, depth: depth) 90 90 } 91 91 } 92 92 ··· 103 103 // MARK: - Scan Simple 104 104 105 105 private mutating func scanSimple(raw: UInt8) throws { 106 + guard !(options.rejectUndefined && reader.peekArgument() == 23) else { 107 + throw ScanError.rejectedUndefined 108 + } 109 + 106 110 let idx = reader.index 107 111 results.recordSimple(reader.pop(), currentByteIndex: idx) 108 112 guard reader.canRead(raw.simpleLength()) else { ··· 150 154 151 155 // MARK: - Scan Array 152 156 153 - private mutating func scanArray() throws { 157 + private mutating func scanArray(depth: Int) throws { 154 158 guard peekIsIndeterminate() else { 155 159 let size = try reader.readNextInt(as: Int.self) 156 160 let mapIdx = results.recordArrayStart(currentByteIndex: reader.index) 157 161 for _ in 0..<size { 158 - try scanNext() 162 + try scanNext(depth: depth + 1) 159 163 } 160 164 results.recordEnd(childCount: size, resultLocation: mapIdx, currentByteIndex: reader.index) 161 165 return ··· 169 173 reader.pop() // Pop type 170 174 var count = 0 171 175 while reader.peek() != Constants.breakCode { 172 - try scanNext() 176 + try scanNext(depth: depth + 1) 173 177 count += 1 174 178 } 175 179 // Pop the break byte ··· 179 183 180 184 // MARK: - Scan Map 181 185 182 - private mutating func scanMap() throws { 186 + private mutating func scanMap(depth: Int) throws { 183 187 guard peekIsIndeterminate() else { 184 188 let keyCount = try reader.readNextInt(as: Int.self) 185 189 guard keyCount < Int.max / 2 else { ··· 189 193 let size = keyCount * 2 190 194 let mapIdx = results.recordMapStart(currentByteIndex: reader.index) 191 195 for _ in 0..<size { 192 - try scanNext() 196 + try scanNext(depth: depth + 1) 193 197 } 194 198 results.recordEnd(childCount: size, resultLocation: mapIdx, currentByteIndex: reader.index) 195 199 return ··· 203 207 reader.pop() // Pop type 204 208 var count = 0 205 209 while reader.peek() != Constants.breakCode { 206 - try scanNext() // Maps should always have a multiple of two values. 207 - try scanNext() 210 + try scanNext(depth: depth + 1) // Maps should always have a multiple of two values. 211 + try scanNext(depth: depth + 1) 208 212 count += 2 209 213 } 210 214 // Pop the break byte ··· 214 218 215 219 // MARK: - Scan Tagged 216 220 217 - private mutating func scanTagged(raw: UInt8) throws { 221 + private mutating func scanTagged(raw: UInt8, depth: Int) throws { 218 222 // Scan the tag number (passing the raw value here makes it record a Tag rather than an Int) 219 223 try scanInt(raw: raw) 220 224 ··· 222 226 throw ScanError.unexpectedEndOfData 223 227 } 224 228 225 - try scanType(type: nextTag, raw: nextRaw) 229 + try scanType(type: nextTag, raw: nextRaw, depth: depth) 226 230 } 227 231 } 228 232
+103
Sources/CBOR/Decoder/Scanner/ScanError.swift
··· 1 + // 2 + // ScanError.swift 3 + // CBOR 4 + // 5 + // Created by Khan Winter on 10/21/25. 6 + // 7 + 8 + @usableFromInline 9 + enum ScanError: Error { 10 + case unexpectedEndOfData 11 + case invalidMajorType(byte: UInt8, offset: Int) 12 + case invalidSize(byte: UInt8, offset: Int) 13 + case expectedMajorType(offset: Int) 14 + case typeInIndeterminateString(type: MajorType, offset: Int) 15 + case rejectedIndeterminateLength(type: MajorType, offset: Int) 16 + case cannotRepresentInt(max: UInt, found: UInt, offset: Int) 17 + case recursionLimit 18 + case unreadDataAfterEnd 19 + case rejectedUndefined 20 + case unnecessaryInt 21 + 22 + // swiftlint:disable:next cyclomatic_complexity function_body_length 23 + func decodingError() -> DecodingError { 24 + switch self { 25 + case .unexpectedEndOfData: 26 + return DecodingError.dataCorrupted( 27 + .init(codingPath: [], debugDescription: "Unexpected end of data.", underlyingError: self) 28 + ) 29 + case let .invalidMajorType(byte, offset): 30 + return DecodingError.dataCorrupted(.init( 31 + codingPath: [], 32 + debugDescription: "Unexpected major type: \(String(byte, radix: 2)) at offset \(offset)", 33 + underlyingError: self 34 + )) 35 + case let .invalidSize(byte, offset): 36 + return DecodingError.dataCorrupted(.init( 37 + codingPath: [], 38 + debugDescription: "Unexpected size argument: \(String(byte, radix: 2)) at offset \(offset)", 39 + underlyingError: self 40 + )) 41 + case let .expectedMajorType(offset): 42 + return DecodingError.dataCorrupted(.init( 43 + codingPath: [], 44 + debugDescription: "Expected major type at offset \(offset)", 45 + underlyingError: self 46 + )) 47 + case let .typeInIndeterminateString(type, offset): 48 + return DecodingError.dataCorrupted(.init( 49 + codingPath: [], 50 + debugDescription: "Unexpected major type in indeterminate \(type) at offset \(offset)", 51 + underlyingError: self 52 + )) 53 + case let .rejectedIndeterminateLength(type, offset): 54 + return DecodingError.dataCorrupted(.init( 55 + codingPath: [], 56 + debugDescription: "Rejected indeterminate length type \(type) at offset \(offset)", 57 + underlyingError: self 58 + )) 59 + case let .cannotRepresentInt(max, found, offset): 60 + return DecodingError.dataCorrupted( 61 + .init( 62 + codingPath: [], 63 + debugDescription: "Failed to decode integer with maximum \(max), " 64 + + "found \(found) at \(offset)", 65 + underlyingError: self 66 + ) 67 + ) 68 + case .recursionLimit: 69 + return DecodingError.dataCorrupted( 70 + .init( 71 + codingPath: [], 72 + debugDescription: "Recursion depth limit exceeded (DecodingOptions.recursionDepth)", 73 + underlyingError: self 74 + ) 75 + ) 76 + case .unreadDataAfterEnd: 77 + return DecodingError.dataCorrupted( 78 + .init( 79 + codingPath: [], 80 + debugDescription: "Configured to reject CBOR data with trailing bytes (DecodingOptions." 81 + + "singleTopLevelItem)", 82 + underlyingError: self 83 + ) 84 + ) 85 + case .rejectedUndefined: 86 + return DecodingError.dataCorrupted( 87 + .init( 88 + codingPath: [], 89 + debugDescription: "Configured to reject undefined values (DecodingOptions.rejectUndefined)", 90 + underlyingError: self 91 + ) 92 + ) 93 + case .unnecessaryInt: 94 + return DecodingError.dataCorrupted( 95 + .init( 96 + codingPath: [], 97 + debugDescription: "Found integer that was encoded in a larger data format than necessary", 98 + underlyingError: self 99 + ) 100 + ) 101 + } 102 + } 103 + }
+1 -1
Sources/CBOR/Encoder/CBOREncoder.swift
··· 56 56 let tempStorage = TopLevelTemporaryEncodingStorage() 57 57 58 58 let encodingContext = EncodingContext(options: options) 59 - let encoder = SingleValueCBOREncodingContainer(parent: tempStorage, context: encodingContext) 59 + let encoder = try SingleValueCBOREncodingContainer(parent: tempStorage, context: encodingContext) 60 60 try encoder.encode(value) 61 61 62 62 let dataSize = tempStorage.value.size
+2 -2
Sources/CBOR/Encoder/Containers/KeyedCBOREncodingContainer.swift
··· 19 19 self.context = context 20 20 } 21 21 22 - func encoder(for key: CodingKey) -> SingleValueCBOREncodingContainer<KeyBuffer.Keyed> { 23 - return .init(parent: storage.withKey(key), context: context.appending(key)) 22 + func encoder(for key: CodingKey) throws -> SingleValueCBOREncodingContainer<KeyBuffer.Keyed> { 23 + return try .init(parent: storage.withKey(key), context: context.appending(key)) 24 24 } 25 25 26 26 func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
+15
Sources/CBOR/Encoder/Containers/SingleValueCBOREncodingContainer.swift
··· 19 19 var options: EncodingOptions { context.options } 20 20 var codingPath: [CodingKey] { context.codingPath } 21 21 22 + init(parent: Storage, context: EncodingContext) throws { 23 + self.parent = parent 24 + self.context = context 25 + } 26 + 22 27 func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey { 23 28 .init(KeyedCBOREncodingContainer(parent: parent, context: context)) 24 29 } ··· 64 69 } 65 70 66 71 func encode<T>(_ value: T) throws where T: Encodable, T: FixedWidthInteger { 72 + parent.register(IntOptimizer(value: value)) 73 + } 74 + 75 + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) 76 + func encode(_ value: Int128) throws { 77 + parent.register(IntOptimizer(value: value)) 78 + } 79 + 80 + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) 81 + func encode(_ value: UInt128) throws { 67 82 parent.register(IntOptimizer(value: value)) 68 83 } 69 84
+1 -1
Sources/CBOR/Encoder/DAGCBOREncoder.swift
··· 39 39 let tempStorage = TopLevelTemporaryEncodingStorage() 40 40 41 41 let encodingContext = EncodingContext(options: options) 42 - let encoder = SingleValueCBOREncodingContainer(parent: tempStorage, context: encodingContext) 42 + let encoder = try SingleValueCBOREncodingContainer(parent: tempStorage, context: encodingContext) 43 43 try encoder.encode(value) 44 44 45 45 let dataSize = tempStorage.value.size
+44 -8
Sources/CBOR/Encoder/Optimizers/KeyedOptimizer.swift
··· 8 8 @inlinable 9 9 func KeyedOptimizer(value: [String: EncodingOptimizer]) -> EncodingOptimizer { 10 10 if value.count < Constants.maxArgSize { 11 - return SmallKeyedOptimizer(value: value, optimizer: { StringOptimizer(value: $0) }) 11 + return SmallKeyedOptimizer( 12 + value: value, 13 + orderedUsing: { lhs, rhs in 14 + if lhs.key.count == rhs.key.count { 15 + return lhs.key < rhs.key 16 + } 17 + 18 + return lhs.key.count < rhs.key.count 19 + }, 20 + optimizer: { StringOptimizer(value: $0) } 21 + ) 12 22 } else { 13 - return LargeKeyedOptimizer(value: value, optimizer: { StringOptimizer(value: $0) }) 23 + return LargeKeyedOptimizer( 24 + value: value, 25 + orderedUsing: { lhs, rhs in 26 + if lhs.key.count == rhs.key.count { 27 + return lhs.key < rhs.key 28 + } 29 + 30 + return lhs.key.count < rhs.key.count 31 + }, 32 + optimizer: { StringOptimizer(value: $0) } 33 + ) 14 34 } 15 35 } 16 36 17 37 @inlinable 18 38 func KeyedOptimizer(value: [Int: EncodingOptimizer]) -> EncodingOptimizer { 19 39 if value.count < Constants.maxArgSize { 20 - return SmallKeyedOptimizer(value: value, optimizer: { IntOptimizer(value: $0) }) 40 + return SmallKeyedOptimizer( 41 + value: value, 42 + orderedUsing: { $0.key < $1.key }, 43 + optimizer: { IntOptimizer(value: $0) } 44 + ) 21 45 } else { 22 - return LargeKeyedOptimizer(value: value, optimizer: { IntOptimizer(value: $0) }) 46 + return LargeKeyedOptimizer( 47 + value: value, 48 + orderedUsing: { $0.key < $1.key }, 49 + optimizer: { IntOptimizer(value: $0) } 50 + ) 23 51 } 24 52 } 25 53 ··· 41 69 @usableFromInline var contentSize: Int 42 70 43 71 @usableFromInline 44 - init(value: [KeyType: EncodingOptimizer], optimizer: (KeyType) -> EncodingOptimizer) { 72 + init( 73 + value: [KeyType: EncodingOptimizer], 74 + orderedUsing: ((key: KeyType, value: EncodingOptimizer), (key: KeyType, value: EncodingOptimizer)) -> Bool, 75 + optimizer: (KeyType) -> EncodingOptimizer 76 + ) { 45 77 var size = 0 46 78 var array: [KeyValue] = [] 47 79 array.reserveCapacity(value.count) 48 80 49 - self.value = value.sorted(by: { $0.key < $1.key }).reduce(into: array, { array, keyValue in 81 + self.value = value.sorted(by: orderedUsing).reduce(into: array, { array, keyValue in 50 82 let optimized = KeyValue(key: optimizer(keyValue.key), value: keyValue.value) 51 83 size += optimized.size 52 84 array.append(optimized) ··· 74 106 @usableFromInline var contentSize: Int 75 107 76 108 @usableFromInline 77 - init(value: [KeyType: EncodingOptimizer], optimizer: (KeyType) -> EncodingOptimizer) { 109 + init( 110 + value: [KeyType: EncodingOptimizer], 111 + orderedUsing: ((key: KeyType, value: EncodingOptimizer), (key: KeyType, value: EncodingOptimizer)) -> Bool, 112 + optimizer: (KeyType) -> EncodingOptimizer 113 + ) { 78 114 var size = 0 79 115 var array: [KeyValue] = [] 80 116 array.reserveCapacity(value.count) 81 117 82 - self.value = value.sorted(by: { $0.key < $1.key }).reduce(into: array, { array, keyValue in 118 + self.value = value.sorted(by: orderedUsing).reduce(into: array, { array, keyValue in 83 119 let optimized = KeyValue(key: optimizer(keyValue.key), value: keyValue.value) 84 120 size += optimized.size 85 121 array.append(optimized)
+1 -1
Sources/CBOR/Encoder/Optimizers/TaggedItemOptimizer.swift
··· 22 22 23 23 init(value: TaggedCBORItem, context: EncodingContext) throws { 24 24 let storage = TopLevelTemporaryEncodingStorage() 25 - var container = SingleValueCBOREncodingContainer(parent: storage, context: context) 25 + var container = try SingleValueCBOREncodingContainer(parent: storage, context: context) 26 26 try value.encodeTaggedData(using: &container) 27 27 sizeOptimizer = IntOptimizer(value: Swift.type(of: value).tag) 28 28 optimizer = storage.value
+3 -1
Sources/ProfilingHelper/Profiling.swift
··· 15 15 func time(_ cbor: () throws -> Void, _ json: () throws -> Void) throws { 16 16 guard #available(macOS 15.0, *) else { fatalError() } 17 17 func calculateStats(_ measurements: [Duration]) -> (average: Double, stddev: Double) { 18 - let values = measurements.map { Double($0.attoseconds) / 1e15 } 18 + let values = measurements.map { 19 + Double($0.components.seconds) + (Double($0.components.attoseconds) / 1e15) 20 + } 19 21 let avg = values.reduce(0, +) / Double(values.count) 20 22 21 23 let variance = values.map { pow($0 - avg, 2) }.reduce(0, +) / Double(values.count)
+194
Tests/CBORTests/AnyDecodable.swift
··· 1 + // From https://github.com/Flight-School/AnyCodable/blob/master/Sources/AnyCodable/AnyDecodable.swift 2 + 3 + // swiftlint:disable cyclomatic_complexity 4 + // swiftlint:disable line_length 5 + // swiftlint:disable type_name 6 + 7 + #if canImport(FoundationEssentials) 8 + import FoundationEssentials 9 + #else 10 + import Foundation 11 + #endif 12 + 13 + /** 14 + A type-erased `Decodable` value. 15 + 16 + The `AnyDecodable` type forwards decoding responsibilities 17 + to an underlying value, hiding its specific underlying type. 18 + 19 + You can decode mixed-type values in dictionaries 20 + and other collections that require `Decodable` conformance 21 + by declaring their contained type to be `AnyDecodable`: 22 + 23 + let json = """ 24 + { 25 + "boolean": true, 26 + "integer": 42, 27 + "double": 3.141592653589793, 28 + "string": "string", 29 + "array": [1, 2, 3], 30 + "nested": { 31 + "a": "alpha", 32 + "b": "bravo", 33 + "c": "charlie" 34 + }, 35 + "null": null 36 + } 37 + """.data(using: .utf8)! 38 + 39 + let decoder = JSONDecoder() 40 + let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json) 41 + */ 42 + struct AnyDecodable: Decodable { 43 + let value: Any 44 + 45 + init<T>(_ value: T?) { 46 + self.value = value ?? () 47 + } 48 + } 49 + 50 + @usableFromInline 51 + protocol _AnyDecodable { 52 + var value: Any { get } 53 + init<T>(_ value: T?) 54 + } 55 + 56 + extension AnyDecodable: _AnyDecodable {} 57 + 58 + extension _AnyDecodable { 59 + init(from decoder: Decoder) throws { 60 + let container = try decoder.singleValueContainer() 61 + 62 + if container.decodeNil() { 63 + self.init(Optional<Self>.none) 64 + } else if let bool = try? container.decode(Bool.self) { 65 + self.init(bool) 66 + } else if let int = try? container.decode(Int.self) { 67 + self.init(int) 68 + } else if let uint = try? container.decode(UInt.self) { 69 + self.init(uint) 70 + } else if let double = try? container.decode(Double.self) { 71 + self.init(double) 72 + } else if let string = try? container.decode(String.self) { 73 + self.init(string) 74 + } else if let array = try? container.decode([AnyDecodable].self) { 75 + self.init(array.map { $0.value }) 76 + } else if let dictionary = try? container.decode([String: AnyDecodable].self) { 77 + self.init(dictionary.mapValues { $0.value }) 78 + } else { 79 + throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyDecodable value cannot be decoded") 80 + } 81 + } 82 + } 83 + 84 + extension AnyDecodable: Equatable { 85 + static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool { 86 + switch (lhs.value, rhs.value) { 87 + case (Optional<Self>.none, Optional<Self>.none), is (Void, Void): 88 + return true 89 + case let (lhs as Bool, rhs as Bool): 90 + return lhs == rhs 91 + case let (lhs as Int, rhs as Int): 92 + return lhs == rhs 93 + case let (lhs as Int8, rhs as Int8): 94 + return lhs == rhs 95 + case let (lhs as Int16, rhs as Int16): 96 + return lhs == rhs 97 + case let (lhs as Int32, rhs as Int32): 98 + return lhs == rhs 99 + case let (lhs as Int64, rhs as Int64): 100 + return lhs == rhs 101 + case let (lhs as UInt, rhs as UInt): 102 + return lhs == rhs 103 + case let (lhs as UInt8, rhs as UInt8): 104 + return lhs == rhs 105 + case let (lhs as UInt16, rhs as UInt16): 106 + return lhs == rhs 107 + case let (lhs as UInt32, rhs as UInt32): 108 + return lhs == rhs 109 + case let (lhs as UInt64, rhs as UInt64): 110 + return lhs == rhs 111 + case let (lhs as Float, rhs as Float): 112 + return lhs == rhs 113 + case let (lhs as Double, rhs as Double): 114 + return lhs == rhs 115 + case let (lhs as String, rhs as String): 116 + return lhs == rhs 117 + case let (lhs as [String: AnyDecodable], rhs as [String: AnyDecodable]): 118 + return lhs == rhs 119 + case let (lhs as [AnyDecodable], rhs as [AnyDecodable]): 120 + return lhs == rhs 121 + default: 122 + return false 123 + } 124 + } 125 + } 126 + 127 + extension AnyDecodable: CustomStringConvertible { 128 + var description: String { 129 + switch value { 130 + case is Void: 131 + return String(describing: nil as Any?) 132 + case let value as CustomStringConvertible: 133 + return value.description 134 + default: 135 + return String(describing: value) 136 + } 137 + } 138 + } 139 + 140 + extension AnyDecodable: CustomDebugStringConvertible { 141 + var debugDescription: String { 142 + switch value { 143 + case let value as CustomDebugStringConvertible: 144 + return "AnyDecodable(\(value.debugDescription))" 145 + default: 146 + return "AnyDecodable(\(description))" 147 + } 148 + } 149 + } 150 + 151 + extension AnyDecodable: Hashable { 152 + func hash(into hasher: inout Hasher) { 153 + switch value { 154 + case let value as Bool: 155 + hasher.combine(value) 156 + case let value as Int: 157 + hasher.combine(value) 158 + case let value as Int8: 159 + hasher.combine(value) 160 + case let value as Int16: 161 + hasher.combine(value) 162 + case let value as Int32: 163 + hasher.combine(value) 164 + case let value as Int64: 165 + hasher.combine(value) 166 + case let value as UInt: 167 + hasher.combine(value) 168 + case let value as UInt8: 169 + hasher.combine(value) 170 + case let value as UInt16: 171 + hasher.combine(value) 172 + case let value as UInt32: 173 + hasher.combine(value) 174 + case let value as UInt64: 175 + hasher.combine(value) 176 + case let value as Float: 177 + hasher.combine(value) 178 + case let value as Double: 179 + hasher.combine(value) 180 + case let value as String: 181 + hasher.combine(value) 182 + case let value as [String: AnyDecodable]: 183 + hasher.combine(value) 184 + case let value as [AnyDecodable]: 185 + hasher.combine(value) 186 + default: 187 + break 188 + } 189 + } 190 + } 191 + 192 + // swiftlint:enable cyclomatic_complexity 193 + // swiftlint:enable line_length 194 + // swiftlint:enable type_name
+48
Tests/CBORTests/DAGCBORDecoderTests.swift
··· 1 + // 2 + // DAGCBORDecoderTests.swift 3 + // CBOR 4 + // 5 + // Created by Khan Winter on 10/21/25. 6 + // 7 + 8 + import Testing 9 + @testable import CBOR 10 + 11 + @Suite 12 + struct DAGCBORDecoderTests { 13 + @Test 14 + func shortMap() throws { 15 + let data = "a1616100".asHexData() 16 + let decoded = try DAGCBORDecoder().decode([String: Int].self, from: data) 17 + #expect(decoded == ["a": 0]) 18 + } 19 + 20 + @Test 21 + func rejectUnorderedMaps() throws { 22 + let data = "a361610162616103616202".asHexData() 23 + #expect(throws: DecodingError.self) { 24 + return try DAGCBORDecoder().decode([String: Int].self, from: data) 25 + } 26 + } 27 + 28 + @Test 29 + func rejectOtherUnorderedMaps() throws { 30 + let data = "a2616201616100".asHexData() 31 + #expect(throws: DecodingError.self) { 32 + return try DAGCBORDecoder().decode([String: Int].self, from: data) 33 + } 34 + } 35 + 36 + @Test 37 + func rejectConcatenatedItems() throws { 38 + var data = "0000".asHexData() 39 + #expect(throws: DecodingError.self) { 40 + return try DAGCBORDecoder().decode(Int.self, from: data) 41 + } 42 + 43 + data = "0000AA".asHexData() 44 + #expect(throws: DecodingError.self) { 45 + return try DAGCBORDecoder().decode(Int.self, from: data) 46 + } 47 + } 48 + }
+326
Tests/CBORTests/DAGCBOREncoderTests.swift
··· 1 + // 2 + // DAGCBORTests.swift 3 + // CBOR 4 + // 5 + // Created by Khan Winter on 10/14/25. 6 + // 7 + 8 + import Foundation 9 + import Testing 10 + @testable import CBOR 11 + 12 + // swiftlint:disable:next private_over_fileprivate 13 + fileprivate struct CID: CIDType { 14 + let data: Data 15 + 16 + init(data: Data) { 17 + self.data = data 18 + } 19 + 20 + init<Container: SingleValueDecodingContainer>(decodeTaggedDataUsing container: Container) throws { 21 + var data = try container.decode(Data.self) 22 + data.removeFirst() 23 + self.data = data 24 + } 25 + 26 + func encodeTaggedData<Container: SingleValueEncodingContainer>(using container: inout Container) throws { 27 + try container.encode(Data([0x0]) + data) 28 + } 29 + } 30 + 31 + @Suite 32 + struct DAGCBOREncoderTests { 33 + @Test 34 + func customCIDTypeEncodedCorrectly() throws { 35 + let cid = CID(data: [0, 1, 2, 3, 4, 5, 6, 7, 8]) 36 + let data = try DAGCBOREncoder().encode(cid).hexString() 37 + #expect(data == "d82a4a00000102030405060708") 38 + } 39 + 40 + @Test 41 + func knownValidBase256EncodedID() throws { 42 + let cid = CID(data: "017112209fe4ccc6de16724f3a30c7e8f254f3c6471986acb1f8d8cf8e96ce2ad7dbe7fb".asHexData()) 43 + let data = try DAGCBOREncoder().encode(cid).hexString() 44 + let expected = "d82a582500017112209FE4CCC6DE16724F3A30C7E8F254F3C6471986ACB1F8D8CF8E96CE2AD7DBE7FB".lowercased() 45 + #expect(data == expected) 46 + } 47 + 48 + // MARK: Copied from EncodableTests 49 + 50 + @Test 51 + func encodeNull() throws { 52 + let encoded = try DAGCBOREncoder().encode(String?(nil)) 53 + #expect(encoded == Data([0xf6])) 54 + } 55 + 56 + @Test 57 + func encodeBools() throws { 58 + let falseVal = try DAGCBOREncoder().encode(false) 59 + #expect(falseVal == Data([0xf4])) 60 + 61 + let trueVal = try DAGCBOREncoder().encode(true) 62 + #expect(trueVal == Data([0xf5])) 63 + } 64 + 65 + @Test 66 + func encodeInts() throws { 67 + // Less than 24 68 + let zero = try DAGCBOREncoder().encode(0) 69 + #expect(zero == Data([0x00])) 70 + 71 + let eight = try DAGCBOREncoder().encode(8) 72 + #expect(eight == Data([0x08])) 73 + 74 + let ten = try DAGCBOREncoder().encode(10) 75 + #expect(ten == Data([0x0a])) 76 + 77 + let twentyThree = try DAGCBOREncoder().encode(23) 78 + #expect(twentyThree == Data([0x17])) 79 + 80 + // Just bigger than 23 81 + let twentyFour = try DAGCBOREncoder().encode(24) 82 + #expect(twentyFour == Data([0x18, 0x18])) 83 + 84 + let twentyFive = try DAGCBOREncoder().encode(25) 85 + #expect(twentyFive == Data([0x18, 0x19])) 86 + 87 + // Bigger 88 + let hundred = try DAGCBOREncoder().encode(100) 89 + #expect(hundred == Data([0x18, 0x64])) 90 + 91 + let thousand = try DAGCBOREncoder().encode(1_000) 92 + #expect(thousand == Data([0x19, 0x03, 0xe8])) 93 + 94 + let million = try DAGCBOREncoder().encode(1_000_000) 95 + #expect(million == Data([0x1a, 0x00, 0x0f, 0x42, 0x40])) 96 + 97 + let trillion = try DAGCBOREncoder().encode(1_000_000_000_000) 98 + #expect(trillion == Data([0x1b, 0x00, 0x00, 0x00, 0xe8, 0xd4, 0xa5, 0x10, 0x00])) 99 + } 100 + 101 + @Test 102 + func encodeNegativeInts() throws { 103 + // Less than 24 104 + let minusOne = try DAGCBOREncoder().encode(-1) 105 + #expect(minusOne == Data([0x20])) 106 + 107 + let minusTen = try DAGCBOREncoder().encode(-10) 108 + #expect(minusTen == Data([0x29])) 109 + 110 + // Bigger 111 + let minusHundred = try DAGCBOREncoder().encode(-100) 112 + #expect(minusHundred == Data([0x38, 0x63])) 113 + 114 + let minusThousand = try DAGCBOREncoder().encode(-1_000) 115 + #expect(minusThousand == Data([0x39, 0x03, 0xe7])) 116 + 117 + let minusMillion = try DAGCBOREncoder().encode(-1_000_001) 118 + #expect(minusMillion == Data([0x3A, 0x00, 0x0F, 0x42, 0x40])) 119 + 120 + let minusTrillion = try DAGCBOREncoder().encode(-1_000_000_001) 121 + #expect(minusTrillion == Data([0x3A, 0x3B, 0x9A, 0xCA, 0x00])) 122 + } 123 + 124 + @Test 125 + func encodeFloat() throws { 126 + let value: Float = 100000.0 127 + // Different. 128 + let data = "fb40f86a0000000000".asHexData() 129 + let result = try DAGCBOREncoder().encode(value) 130 + #expect(data == result) 131 + } 132 + 133 + @Test 134 + func encodeDouble() throws { 135 + let value: Double = 0.10035 136 + let data = "FB3FB9B089A0275254".asHexData() 137 + let result = try DAGCBOREncoder().encode(value) 138 + #expect(data == result) 139 + } 140 + 141 + @Test(arguments: [Double.nan, Double.infinity, -Double.infinity]) 142 + func NanAndInfiniteThrowErrors(value: Double) throws { 143 + #expect(throws: EncodingError.self) { 144 + try DAGCBOREncoder().encode(value) 145 + } 146 + } 147 + 148 + @Test 149 + func encodeStrings() throws { 150 + let empty = try DAGCBOREncoder().encode("") 151 + #expect(empty == Data([0x60])) 152 + 153 + let a = try DAGCBOREncoder().encode("a") 154 + #expect(a == Data([0x61, 0x61])) 155 + 156 + let IETF = try DAGCBOREncoder().encode("IETF") 157 + #expect(IETF == Data([0x64, 0x49, 0x45, 0x54, 0x46])) 158 + 159 + let quoteSlash = try DAGCBOREncoder().encode("\"\\") 160 + #expect(quoteSlash == Data([0x62, 0x22, 0x5c])) 161 + 162 + let littleUWithDiaeresis = try DAGCBOREncoder().encode("\u{00FC}") 163 + #expect(littleUWithDiaeresis == Data([0x62, 0xc3, 0xbc])) 164 + } 165 + 166 + @Test 167 + func encodeByteStrings() throws { 168 + let fourByteByteString = try DAGCBOREncoder().encode(Data([0x01, 0x02, 0x03, 0x04])) 169 + #expect(fourByteByteString == Data([0x44, 0x01, 0x02, 0x03, 0x04])) 170 + } 171 + 172 + @Test 173 + func mixedByteArraysEncodeCorrectly() throws { 174 + // TODO: Make the container swap to mixed mode if necessary. 175 + 176 + /// See note in ``UnkeyeyedCBOREncodingContainer`` about mixed collections of ints 177 + struct Mixed: Encodable { 178 + func encode(to encoder: any Encoder) throws { 179 + var container = encoder.unkeyedContainer() 180 + try container.encode(UInt8.zero) 181 + try container.encode(UInt8.max) 182 + try container.encode("Hello World") 183 + try container.encode(1000000) 184 + } 185 + } 186 + 187 + let data = try DAGCBOREncoder().encode(Mixed()) 188 + // swiftlint:disable:next line_length 189 + #expect(data == Data([0x84, 0x00, 0x18, 0xff, 0x6b, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x00, 0x0f, 0x42, 0x40])) 190 + } 191 + 192 + @Test 193 + func encodeArrays() throws { 194 + let empty = try DAGCBOREncoder().encode([String]()) 195 + #expect(empty == Data([0x80])) 196 + 197 + let oneTwoThree = try DAGCBOREncoder().encode([1, 2, 3]) 198 + #expect(oneTwoThree == Data([0x83, 0x01, 0x02, 0x03])) 199 + 200 + // swiftlint:disable:next line_length 201 + let lotsOfInts = try DAGCBOREncoder().encode([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]) 202 + 203 + // swiftlint:disable:next line_length 204 + #expect(lotsOfInts == Data([0x98, 0x19, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x18, 0x18, 0x19])) 205 + 206 + let nestedSimple = try DAGCBOREncoder().encode([[1], [2, 3], [4, 5]]) 207 + #expect(nestedSimple == Data([0x83, 0x81, 0x01, 0x82, 0x02, 0x03, 0x82, 0x04, 0x05])) 208 + } 209 + 210 + @Test 211 + func encodeMaps() throws { 212 + let empty = try DAGCBOREncoder().encode([String: String]()) 213 + #expect(empty == Data([0xa0])) 214 + 215 + let stringToString = try DAGCBOREncoder().encode(["a": "A", "b": "B", "c": "C", "d": "D", "e": "E"]) 216 + #expect(stringToString.first! == 0xa5) 217 + 218 + let dataMinusFirstByte = stringToString[1...] 219 + .map { $0 } 220 + .chunked(into: 4) 221 + .sorted(by: { $0.lexicographicallyPrecedes($1) }) 222 + let dataForKeyValuePairs: [[UInt8]] = [ 223 + [0x61, 0x61, 0x61, 0x41], 224 + [0x61, 0x62, 0x61, 0x42], 225 + [0x61, 0x63, 0x61, 0x43], 226 + [0x61, 0x64, 0x61, 0x44], 227 + [0x61, 0x65, 0x61, 0x45] 228 + ] 229 + #expect(dataMinusFirstByte == dataForKeyValuePairs) 230 + 231 + let encoder = DAGCBOREncoder() 232 + let encodedWithStringKeys = try encoder.encode([1: 2, 3: 4]) 233 + #expect( 234 + encodedWithStringKeys == Data([0xa2, 0x61, 0x31, 0x02, 0x61, 0x33, 0x04]) 235 + ) 236 + } 237 + 238 + @Test 239 + func encodeSimpleStructs() throws { 240 + struct MyStruct: Codable { 241 + let age: Int 242 + let name: String 243 + } 244 + 245 + let encoded = try DAGCBOREncoder().encode(MyStruct(age: 27, name: "Ham")).map { $0 } 246 + 247 + #expect( 248 + encoded == [0xa2, 0x63, 0x61, 0x67, 0x65, 0x18, 0x1b, 0x64, 0x6e, 0x61, 0x6d, 0x65, 0x63, 0x48, 0x61, 0x6d] 249 + ) 250 + } 251 + 252 + @Test 253 + func encodeMoreComplexStructs() throws { 254 + let encoder = DAGCBOREncoder() 255 + 256 + let data = try encoder.encode(Company.mock).hexString().uppercased() 257 + // swiftlint:disable:next line_length 258 + #expect(data == "A4646E616D656941636D6520436F727067666F756E6465641907CF686D65746164617461A268696E6475737472796474656368686C6F636174696F6E6672656D6F746569656D706C6F796565738AA563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5") 259 + } 260 + 261 + @Test 262 + func uint16() throws { 263 + let encoder = DAGCBOREncoder() 264 + let data = try encoder.encode(1999) 265 + #expect(data == "1907CF".asHexData()) 266 + } 267 + 268 + @Test( 269 + .disabled("Disabled until we can figure out how to make Float16 compile on all platforms."), 270 + arguments: [ 271 + ("F90000", 0), 272 + ("F93C00", 1.0), 273 + ("F9BE00", -1.5), 274 + ("F97C00", .infinity), 275 + ("F93E32", 1.548828125), 276 + ("F9F021", -8456) 277 + ] 278 + ) 279 + func halfFloat(expected: String, value: Float16) throws { 280 + let expectedData = expected.asHexData() 281 + let encoder = DAGCBOREncoder() 282 + let data = try encoder.encode(value) 283 + #expect(data == expectedData) 284 + } 285 + 286 + @Test 287 + func duplicateKeysAreDeduplicatedOnEncode() throws { 288 + let encoder = DAGCBOREncoder() 289 + struct Mock: Encodable { 290 + enum CodingKeys: String, CodingKey { 291 + case one 292 + } 293 + 294 + func encode(to encoder: any Encoder) throws { 295 + var container = encoder.container(keyedBy: CodingKeys.self) 296 + try container.encode(true, forKey: .one) 297 + try container.encode(false, forKey: .one) 298 + } 299 + } 300 + 301 + let data = try encoder.encode(Mock()).hexString() 302 + #expect(data == "a1636f6e65f4") 303 + } 304 + 305 + @Test 306 + func uuid() throws { 307 + let uuid = UUID(uuidString: "A0BDA068-60AD-4111-B4C9-04746791028B") 308 + #expect(throws: EncodingError.self) { 309 + try DAGCBOREncoder().encode(uuid) 310 + } 311 + } 312 + 313 + @Test 314 + func orderedMapKeys() throws { 315 + let value = ["a": 1, "b": 2, "aa": 3] 316 + let encoded = try DAGCBOREncoder().encode(value).hexString() 317 + #expect(encoded == "a361610161620262616103") 318 + } 319 + 320 + @Test 321 + func emptyData() throws { 322 + let value = Data() 323 + let encoded = try DAGCBOREncoder().encode(value).hexString() 324 + #expect(encoded == "40") 325 + } 326 + }
-312
Tests/CBORTests/DAGCBORTests.swift
··· 1 - // 2 - // DAGCBORTests.swift 3 - // CBOR 4 - // 5 - // Created by Khan Winter on 10/14/25. 6 - // 7 - 8 - import Foundation 9 - import Testing 10 - @testable import CBOR 11 - 12 - // swiftlint:disable:next private_over_fileprivate 13 - fileprivate struct CID: CIDType { 14 - let data: Data 15 - 16 - init(data: Data) { 17 - self.data = data 18 - } 19 - 20 - init<Container: SingleValueDecodingContainer>(decodeTaggedDataUsing container: Container) throws { 21 - var data = try container.decode(Data.self) 22 - data.removeFirst() 23 - self.data = data 24 - } 25 - 26 - func encodeTaggedData<Container: SingleValueEncodingContainer>(using container: inout Container) throws { 27 - try container.encode(Data([0x0]) + data) 28 - } 29 - } 30 - 31 - @Suite 32 - struct DAGCBORTests { 33 - @Test 34 - func customCIDTypeEncodedCorrectly() throws { 35 - let cid = CID(data: [0, 1, 2, 3, 4, 5, 6, 7, 8]) 36 - let data = try DAGCBOREncoder().encode(cid).hexString() 37 - #expect(data == "d82a4a00000102030405060708") 38 - } 39 - 40 - @Test 41 - func knownValidBase256EncodedID() throws { 42 - let cid = CID(data: "017112209fe4ccc6de16724f3a30c7e8f254f3c6471986acb1f8d8cf8e96ce2ad7dbe7fb".asHexData()) 43 - let data = try DAGCBOREncoder().encode(cid).hexString() 44 - let expected = "d82a582500017112209FE4CCC6DE16724F3A30C7E8F254F3C6471986ACB1F8D8CF8E96CE2AD7DBE7FB".lowercased() 45 - #expect(data == expected) 46 - } 47 - 48 - // MARK: Copied from EncodableTests 49 - 50 - @Test 51 - func encodeNull() throws { 52 - let encoded = try DAGCBOREncoder().encode(String?(nil)) 53 - #expect(encoded == Data([0xf6])) 54 - } 55 - 56 - @Test 57 - func encodeBools() throws { 58 - let falseVal = try DAGCBOREncoder().encode(false) 59 - #expect(falseVal == Data([0xf4])) 60 - 61 - let trueVal = try DAGCBOREncoder().encode(true) 62 - #expect(trueVal == Data([0xf5])) 63 - } 64 - 65 - @Test 66 - func encodeInts() throws { 67 - // Less than 24 68 - let zero = try DAGCBOREncoder().encode(0) 69 - #expect(zero == Data([0x00])) 70 - 71 - let eight = try DAGCBOREncoder().encode(8) 72 - #expect(eight == Data([0x08])) 73 - 74 - let ten = try DAGCBOREncoder().encode(10) 75 - #expect(ten == Data([0x0a])) 76 - 77 - let twentyThree = try DAGCBOREncoder().encode(23) 78 - #expect(twentyThree == Data([0x17])) 79 - 80 - // Just bigger than 23 81 - let twentyFour = try DAGCBOREncoder().encode(24) 82 - #expect(twentyFour == Data([0x18, 0x18])) 83 - 84 - let twentyFive = try DAGCBOREncoder().encode(25) 85 - #expect(twentyFive == Data([0x18, 0x19])) 86 - 87 - // Bigger 88 - let hundred = try DAGCBOREncoder().encode(100) 89 - #expect(hundred == Data([0x18, 0x64])) 90 - 91 - let thousand = try DAGCBOREncoder().encode(1_000) 92 - #expect(thousand == Data([0x19, 0x03, 0xe8])) 93 - 94 - let million = try DAGCBOREncoder().encode(1_000_000) 95 - #expect(million == Data([0x1a, 0x00, 0x0f, 0x42, 0x40])) 96 - 97 - let trillion = try DAGCBOREncoder().encode(1_000_000_000_000) 98 - #expect(trillion == Data([0x1b, 0x00, 0x00, 0x00, 0xe8, 0xd4, 0xa5, 0x10, 0x00])) 99 - } 100 - 101 - @Test 102 - func encodeNegativeInts() throws { 103 - // Less than 24 104 - let minusOne = try DAGCBOREncoder().encode(-1) 105 - #expect(minusOne == Data([0x20])) 106 - 107 - let minusTen = try DAGCBOREncoder().encode(-10) 108 - #expect(minusTen == Data([0x29])) 109 - 110 - // Bigger 111 - let minusHundred = try DAGCBOREncoder().encode(-100) 112 - #expect(minusHundred == Data([0x38, 0x63])) 113 - 114 - let minusThousand = try DAGCBOREncoder().encode(-1_000) 115 - #expect(minusThousand == Data([0x39, 0x03, 0xe7])) 116 - 117 - let minusMillion = try DAGCBOREncoder().encode(-1_000_001) 118 - #expect(minusMillion == Data([0x3A, 0x00, 0x0F, 0x42, 0x40])) 119 - 120 - let minusTrillion = try DAGCBOREncoder().encode(-1_000_000_001) 121 - #expect(minusTrillion == Data([0x3A, 0x3B, 0x9A, 0xCA, 0x00])) 122 - } 123 - 124 - @Test 125 - func encodeFloat() throws { 126 - let value: Float = 100000.0 127 - // Different. 128 - let data = "fb40f86a0000000000".asHexData() 129 - let result = try DAGCBOREncoder().encode(value) 130 - #expect(data == result) 131 - } 132 - 133 - @Test 134 - func encodeDouble() throws { 135 - let value: Double = 0.10035 136 - let data = "FB3FB9B089A0275254".asHexData() 137 - let result = try DAGCBOREncoder().encode(value) 138 - #expect(data == result) 139 - } 140 - 141 - @Test(arguments: [Double.nan, Double.infinity, -Double.infinity]) 142 - func NanAndInfiniteThrowErrors(value: Double) throws { 143 - #expect(throws: EncodingError.self) { 144 - try DAGCBOREncoder().encode(value) 145 - } 146 - } 147 - 148 - @Test 149 - func encodeStrings() throws { 150 - let empty = try DAGCBOREncoder().encode("") 151 - #expect(empty == Data([0x60])) 152 - 153 - let a = try DAGCBOREncoder().encode("a") 154 - #expect(a == Data([0x61, 0x61])) 155 - 156 - let IETF = try DAGCBOREncoder().encode("IETF") 157 - #expect(IETF == Data([0x64, 0x49, 0x45, 0x54, 0x46])) 158 - 159 - let quoteSlash = try DAGCBOREncoder().encode("\"\\") 160 - #expect(quoteSlash == Data([0x62, 0x22, 0x5c])) 161 - 162 - let littleUWithDiaeresis = try DAGCBOREncoder().encode("\u{00FC}") 163 - #expect(littleUWithDiaeresis == Data([0x62, 0xc3, 0xbc])) 164 - } 165 - 166 - @Test 167 - func encodeByteStrings() throws { 168 - let fourByteByteString = try DAGCBOREncoder().encode(Data([0x01, 0x02, 0x03, 0x04])) 169 - #expect(fourByteByteString == Data([0x44, 0x01, 0x02, 0x03, 0x04])) 170 - } 171 - 172 - @Test 173 - func mixedByteArraysEncodeCorrectly() throws { 174 - // TODO: Make the container swap to mixed mode if necessary. 175 - 176 - /// See note in ``UnkeyeyedCBOREncodingContainer`` about mixed collections of ints 177 - struct Mixed: Encodable { 178 - func encode(to encoder: any Encoder) throws { 179 - var container = encoder.unkeyedContainer() 180 - try container.encode(UInt8.zero) 181 - try container.encode(UInt8.max) 182 - try container.encode("Hello World") 183 - try container.encode(1000000) 184 - } 185 - } 186 - 187 - let data = try DAGCBOREncoder().encode(Mixed()) 188 - // swiftlint:disable:next line_length 189 - #expect(data == Data([0x84, 0x00, 0x18, 0xff, 0x6b, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x00, 0x0f, 0x42, 0x40])) 190 - } 191 - 192 - @Test 193 - func encodeArrays() throws { 194 - let empty = try DAGCBOREncoder().encode([String]()) 195 - #expect(empty == Data([0x80])) 196 - 197 - let oneTwoThree = try DAGCBOREncoder().encode([1, 2, 3]) 198 - #expect(oneTwoThree == Data([0x83, 0x01, 0x02, 0x03])) 199 - 200 - // swiftlint:disable:next line_length 201 - let lotsOfInts = try DAGCBOREncoder().encode([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]) 202 - 203 - // swiftlint:disable:next line_length 204 - #expect(lotsOfInts == Data([0x98, 0x19, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x18, 0x18, 0x19])) 205 - 206 - let nestedSimple = try DAGCBOREncoder().encode([[1], [2, 3], [4, 5]]) 207 - #expect(nestedSimple == Data([0x83, 0x81, 0x01, 0x82, 0x02, 0x03, 0x82, 0x04, 0x05])) 208 - } 209 - 210 - @Test 211 - func encodeMaps() throws { 212 - let empty = try DAGCBOREncoder().encode([String: String]()) 213 - #expect(empty == Data([0xa0])) 214 - 215 - let stringToString = try DAGCBOREncoder().encode(["a": "A", "b": "B", "c": "C", "d": "D", "e": "E"]) 216 - #expect(stringToString.first! == 0xa5) 217 - 218 - let dataMinusFirstByte = stringToString[1...] 219 - .map { $0 } 220 - .chunked(into: 4) 221 - .sorted(by: { $0.lexicographicallyPrecedes($1) }) 222 - let dataForKeyValuePairs: [[UInt8]] = [ 223 - [0x61, 0x61, 0x61, 0x41], 224 - [0x61, 0x62, 0x61, 0x42], 225 - [0x61, 0x63, 0x61, 0x43], 226 - [0x61, 0x64, 0x61, 0x44], 227 - [0x61, 0x65, 0x61, 0x45] 228 - ] 229 - #expect(dataMinusFirstByte == dataForKeyValuePairs) 230 - 231 - let encoder = DAGCBOREncoder() 232 - let encodedWithStringKeys = try encoder.encode([1: 2, 3: 4]) 233 - #expect( 234 - encodedWithStringKeys == Data([0xa2, 0x61, 0x31, 0x02, 0x61, 0x33, 0x04]) 235 - ) 236 - } 237 - 238 - @Test 239 - func encodeSimpleStructs() throws { 240 - struct MyStruct: Codable { 241 - let age: Int 242 - let name: String 243 - } 244 - 245 - let encoded = try DAGCBOREncoder().encode(MyStruct(age: 27, name: "Ham")).map { $0 } 246 - 247 - #expect( 248 - encoded == [0xa2, 0x63, 0x61, 0x67, 0x65, 0x18, 0x1b, 0x64, 0x6e, 0x61, 0x6d, 0x65, 0x63, 0x48, 0x61, 0x6d] 249 - ) 250 - } 251 - 252 - @Test 253 - func encodeMoreComplexStructs() throws { 254 - let encoder = DAGCBOREncoder() 255 - 256 - let data = try encoder.encode(Company.mock) 257 - // swiftlint:disable:next line_length 258 - #expect(data == "A469656D706C6F796565738AA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B67666F756E6465641907CF686D65746164617461A268696E6475737472796474656368686C6F636174696F6E6672656D6F7465646E616D656941636D6520436F7270".asHexData()) 259 - } 260 - 261 - @Test 262 - func uint16() throws { 263 - let encoder = DAGCBOREncoder() 264 - let data = try encoder.encode(1999) 265 - #expect(data == "1907CF".asHexData()) 266 - } 267 - 268 - @Test( 269 - .disabled("Disabled until we can figure out how to make Float16 compile on all platforms."), 270 - arguments: [ 271 - ("F90000", 0), 272 - ("F93C00", 1.0), 273 - ("F9BE00", -1.5), 274 - ("F97C00", .infinity), 275 - ("F93E32", 1.548828125), 276 - ("F9F021", -8456) 277 - ] 278 - ) 279 - func halfFloat(expected: String, value: Float16) throws { 280 - let expectedData = expected.asHexData() 281 - let encoder = DAGCBOREncoder() 282 - let data = try encoder.encode(value) 283 - #expect(data == expectedData) 284 - } 285 - 286 - @Test 287 - func duplicateKeysAreDeduplicatedOnEncode() throws { 288 - let encoder = DAGCBOREncoder() 289 - struct Mock: Encodable { 290 - enum CodingKeys: String, CodingKey { 291 - case one 292 - } 293 - 294 - func encode(to encoder: any Encoder) throws { 295 - var container = encoder.container(keyedBy: CodingKeys.self) 296 - try container.encode(true, forKey: .one) 297 - try container.encode(false, forKey: .one) 298 - } 299 - } 300 - 301 - let data = try encoder.encode(Mock()).hexString() 302 - #expect(data == "a1636f6e65f4") 303 - } 304 - 305 - @Test 306 - func uuid() throws { 307 - let uuid = UUID(uuidString: "A0BDA068-60AD-4111-B4C9-04746791028B") 308 - #expect(throws: EncodingError.self) { 309 - try DAGCBOREncoder().encode(uuid) 310 - } 311 - } 312 - }
+41 -34
Tests/CBORTests/DecodableTests.swift
··· 25 25 value = try CBORDecoder().decode(UInt8.self, from: [23]) 26 26 #expect(value == 23) 27 27 // Just above max arg size 28 - value = try CBORDecoder().decode(UInt8.self, from: [24, 24]) 29 - #expect(value == 24) 28 + // value = try CBORDecoder().decode(UInt8.self, from: [24, 24]) 29 + // #expect(value == 24) 30 30 // Max Int 31 31 value = try CBORDecoder().decode(UInt8.self, from: [24, UInt8.max]) 32 32 #expect(value == UInt8.max) ··· 49 49 value = try CBORDecoder().decode(UInt16.self, from: [23]) 50 50 #expect(value == 23) 51 51 // Just above max arg size 52 - value = try CBORDecoder().decode(UInt16.self, from: [24, 24]) 53 - #expect(value == 24) 52 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt16.self, from: [24, 24]) } 54 53 55 54 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt16.self, from: [128]) } 56 55 57 - value = try CBORDecoder().decode(UInt16.self, from: [25, 0, 1]) 58 - #expect(value == 1) 56 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt16.self, from: [25, 0, 1]) } 57 + 59 58 value = try CBORDecoder().decode(UInt16.self, from: [25, 1, 1]) 60 59 #expect(value == 257) 61 60 value = try CBORDecoder().decode(UInt16.self, from: [25, UInt8.max, UInt8.max]) ··· 74 73 // Just below max arg size 75 74 value = try CBORDecoder().decode(UInt32.self, from: [23]) 76 75 #expect(value == 23) 77 - // Just above max arg size 78 - value = try CBORDecoder().decode(UInt32.self, from: [24, 24]) 79 - #expect(value == 24) 80 76 81 - value = try CBORDecoder().decode(UInt32.self, from: [25, 0, 1]) 82 - #expect(value == 1) 83 77 value = try CBORDecoder().decode(UInt32.self, from: [25, 1, 1]) 84 78 #expect(value == 257) 85 79 value = try CBORDecoder().decode(UInt32.self, from: [25, UInt8.max, UInt8.max]) ··· 88 82 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt32.self, from: [25]) } 89 83 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt32.self, from: [25, 0]) } 90 84 91 - value = try CBORDecoder().decode(UInt32.self, from: [26, 0, 0, 0, 0]) 92 - #expect(value == 0) 93 - value = try CBORDecoder().decode(UInt32.self, from: [26, 0, 0, 0, 1]) 94 - #expect(value == 1) 95 - value = try CBORDecoder().decode(UInt32.self, from: [26, 0, 0, 1, 1]) 96 - #expect(value == 257) 85 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt32.self, from: [26, 0, 0, 0, 0]) } 86 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt32.self, from: [26, 0, 0, 0, 1]) } 87 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt32.self, from: [26, 0, 0, 1, 1]) } 88 + 97 89 value = try CBORDecoder().decode(UInt32.self, from: [26, 0, 1, 1, 1]) 98 90 #expect(value == 65793) 91 + 99 92 value = try CBORDecoder().decode(UInt32.self, from: [26, UInt8.max, UInt8.max, UInt8.max, UInt8.max]) 100 93 #expect(value == UInt32.max) 101 94 // Missing bytes from end ··· 112 105 // Just below max arg size 113 106 value = try CBORDecoder().decode(UInt64.self, from: [23]) 114 107 #expect(value == 23) 115 - // Just above max arg size 116 - value = try CBORDecoder().decode(UInt64.self, from: [24, 24]) 117 - #expect(value == 24) 118 108 119 - value = try CBORDecoder().decode(UInt64.self, from: [25, 0, 1]) 120 - #expect(value == 1) 121 109 value = try CBORDecoder().decode(UInt64.self, from: [25, 1, 1]) 122 110 #expect(value == 257) 123 111 value = try CBORDecoder().decode(UInt64.self, from: [25, UInt8.max, UInt8.max]) ··· 126 114 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [25]) } 127 115 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [25, 0]) } 128 116 129 - value = try CBORDecoder().decode(UInt64.self, from: [26, 0, 0, 0, 0]) 130 - #expect(value == 0) 131 - value = try CBORDecoder().decode(UInt64.self, from: [26, 0, 0, 0, 1]) 132 - #expect(value == 1) 133 - value = try CBORDecoder().decode(UInt64.self, from: [26, 0, 0, 1, 1]) 134 - #expect(value == 257) 117 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [26, 0, 0, 0, 0]) } 118 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [26, 0, 0, 0, 1]) } 119 + #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [26, 0, 0, 1, 1]) } 120 + 135 121 value = try CBORDecoder().decode(UInt64.self, from: [26, 0, 1, 1, 1]) 136 122 #expect(value == 65793) 137 123 value = try CBORDecoder().decode(UInt64.self, from: [26, UInt8.max, UInt8.max, UInt8.max, UInt8.max]) ··· 140 126 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [26]) } 141 127 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [26, 0, 0, 0]) } 142 128 143 - value = try CBORDecoder().decode(UInt64.self, from: [27, 0, 0, 0, 0, 0, 0, 0, 0]) 144 - #expect(value == 0) 145 - value = try CBORDecoder().decode(UInt64.self, from: [27, 0, 0, 0, 0, 0, 0, 0, 1]) 146 - #expect(value == 1) 147 - value = try CBORDecoder().decode(UInt64.self, from: [27, 0, 0, 0, 0, 0, 0, 1, 1]) 148 - #expect(value == 257) 129 + #expect(throws: DecodingError.self) { 130 + try CBORDecoder().decode(UInt64.self, from: [27, 0, 0, 0, 0, 0, 0, 0, 0]) 131 + } 132 + #expect(throws: DecodingError.self) { 133 + try CBORDecoder().decode(UInt64.self, from: [27, 0, 0, 0, 0, 0, 0, 0, 1]) 134 + } 135 + #expect(throws: DecodingError.self) { 136 + try CBORDecoder().decode(UInt64.self, from: [27, 0, 0, 0, 0, 0, 0, 1, 1]) 137 + } 138 + 149 139 // Missing bytes from end 150 140 #expect(throws: DecodingError.self) { try CBORDecoder().decode(UInt64.self, from: [27]) } 151 141 #expect(throws: DecodingError.self) { 152 142 try CBORDecoder().decode(UInt64.self, from: [27, 0, 0, 0, 0, 0, 0, 0]) 153 143 } 144 + } 145 + 146 + @available(macOS 15.0, *) 147 + @Test 148 + func minInt() throws { 149 + let data = "3bffffffffffffffff".asHexData() 150 + let decoded = try CBORDecoder().decode(Int128.self, from: data) 151 + #expect(decoded == -18446744073709551616) 154 152 } 155 153 156 154 @Test ··· 289 287 let data = data.asHexData() 290 288 let decoded = try CBORDecoder().decode(Float.self, from: data) 291 289 #expect(decoded == value) 290 + } 291 + 292 + @Test 293 + func recursionLimit() { 294 + #expect(throws: DecodingError.self) { 295 + // swiftlint:disable:next line_length 296 + let data = "a260818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818180652474797065726170702e62736b792e666565642e70736f74".asHexData() 297 + return try CBORDecoder().decode(AnyDecodable.self, from: data) 298 + } 292 299 } 293 300 }
+2 -2
Tests/CBORTests/DecodeMultipleTests.swift
··· 25 25 value = try CBORDecoder().decodeMultiple(UInt8.self, from: [23, 23]) 26 26 #expect(value == [23, 23]) 27 27 // Just above max arg size 28 - value = try CBORDecoder().decodeMultiple(UInt8.self, from: [24, 24, 24, 24]) 29 - #expect(value == [24, 24]) 28 + // value = try CBORDecoder().decodeMultiple(UInt8.self, from: [24, 24, 24, 24]) 29 + // #expect(value == [24, 24]) 30 30 // Max Int 31 31 value = try CBORDecoder().decodeMultiple(UInt8.self, from: [24, UInt8.max]) 32 32 #expect(value == [UInt8.max])
+2 -2
Tests/CBORTests/EncodableTests.swift
··· 219 219 func encodeMoreComplexStructs() throws { 220 220 let encoder = CBOREncoder() 221 221 222 - let data = try encoder.encode(Company.mock) 222 + let data = try encoder.encode(Company.mock).hexString().uppercased() 223 223 // swiftlint:disable:next line_length 224 - #expect(data == "A469656D706C6F796565738AA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726BA563616765181E65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B67666F756E6465641907CF686D65746164617461A268696E6475737472796474656368686C6F636174696F6E6672656D6F7465646E616D656941636D6520436F7270".asHexData()) 224 + #expect(data == "A4646E616D656941636D6520436F727067666F756E6465641907CF686D65746164617461A268696E6475737472796474656368686C6F636174696F6E6672656D6F746569656D706C6F796565738AA563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5A563616765181E646E616D6565416C6963656474616773836573776966746463626F726962656E63686D61726B65656D61696C71616C696365406578616D706C652E636F6D686973416374697665F5") 225 225 } 226 226 227 227 @Test
+8
Tests/CBORTests/RoundTripTests.swift
··· 141 141 let decoded = try CBORDecoder().decode(UUID.self, from: encoded) 142 142 #expect(decoded == value) 143 143 } 144 + 145 + @Test 146 + func map() throws { 147 + let value = ["a": 0] 148 + let encoded = try CBOREncoder().encode(value) 149 + let decoded = try CBORDecoder().decode([String: Int].self, from: encoded) 150 + #expect(decoded == value) 151 + } 144 152 }