···7788import Foundation
991010+/// Decodes ``Decodable`` objects from CBOR data.
1111+///
1212+/// This type can be reused efficiently for multiple deserialization operations. Use the ``decode(_:from:)`` method
1313+/// to decode data.
1414+///
1515+/// To configure decoding behavior, pass options to the ``init(rejectIndeterminateLengths:)`` method or modify
1616+/// the ``options`` variable.
1017public struct CBORDecoder {
1111- var options: DecodingOptions
1818+ /// The options that determine decoding behavior.
1919+ public var options: DecodingOptions
12201313- public init(options: DecodingOptions = DecodingOptions()) {
1414- self.options = options
2121+ /// Creates a new decoder.
2222+ /// - Parameter rejectIndeterminateLengths: Set to `false` to allow indeterminate length objects to be decoded.
2323+ /// Defaults to *rejecting* indeterminate length items (strings, bytes,
2424+ /// maps, and arrays).
2525+ public init(rejectIndeterminateLengths: Bool = true) {
2626+ self.options = DecodingOptions(rejectIndeterminateLengths: rejectIndeterminateLengths)
1527 }
16282929+ /// Decodes the given type from CBOR binary data.
3030+ /// - Parameters:
3131+ /// - type: The decodable type to deserialize.
3232+ /// - data: The CBOR data to decode from.
3333+ /// - Returns: An instance of the decoded type.
3434+ /// - Throws: A ``DecodingError`` with context and a debug description for a failed deserialization operation.
1735 public func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
1836 do {
1937 return try data.withUnsafeBytes {
···3149 if let error = error as? CBORScanner.ScanError {
3250 switch error {
3351 case .unexpectedEndOfData:
3434- throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Unexpected end of data."))
3535- case .invalidMajorType(let byte, let offset):
5252+ throw DecodingError.dataCorrupted(
5353+ .init(codingPath: [], debugDescription: "Unexpected end of data.")
5454+ )
5555+ case let .invalidMajorType(byte, offset):
3656 throw DecodingError.dataCorrupted(.init(
3757 codingPath: [],
3858 debugDescription: "Unexpected major type: \(String(byte, radix: 2)) at offset \(offset)"
3959 ))
4040- case .invalidSize(let byte, let offset):
6060+ case let .invalidSize(byte, offset):
4161 throw DecodingError.dataCorrupted(.init(
4262 codingPath: [],
4363 debugDescription: "Unexpected size argument: \(String(byte, radix: 2)) at offset \(offset)"
4464 ))
4545- case .expectedMajorType(let offset):
6565+ case let .expectedMajorType(offset):
4666 throw DecodingError.dataCorrupted(.init(
4767 codingPath: [],
4868 debugDescription: "Expected major type at offset \(offset)"
4969 ))
5050- case .typeInIndeterminateString(let type, let offset):
7070+ case let .typeInIndeterminateString(type, offset):
5171 throw DecodingError.dataCorrupted(.init(
5272 codingPath: [],
5373 debugDescription: "Unexpected major type in indeterminate \(type) at offset \(offset)"
5474 ))
5555- case .rejectedIndeterminateLength(let type, let offset):
7575+ case let .rejectedIndeterminateLength(type, offset):
5676 throw DecodingError.dataCorrupted(.init(
5777 codingPath: [],
5878 debugDescription: "Rejected indeterminate length type \(type) at offset \(offset)"
···55// Created by Khan Winter on 8/23/25.
66//
7788+/// Options that determine the behavior of ``CBORDecoder``.
89public struct DecodingOptions {
99- public var rejectIndeterminateLengthData: Bool
1010- public var rejectIndeterminateLengthStrings: Bool
1111- public var rejectIndeterminateLengthArrays: Bool
1212- public var rejectIndeterminateLengthMaps: Bool
1010+ /// Set to `false` to allow indeterminate length objects to be decoded.
1111+ /// `true` by default.
1212+ ///
1313+ /// For deterministic encoding, this **must** be enabled.
1414+ public var rejectIndeterminateLengths: Bool
13151414- public init(
1515- rejectIndeterminateLengthData: Bool = true,
1616- rejectIndeterminateLengthStrings: Bool = true,
1717- rejectIndeterminateLengthArrays: Bool = true,
1818- rejectIndeterminateLengthMaps: Bool = true
1919- ) {
2020- self.rejectIndeterminateLengthData = rejectIndeterminateLengthData
2121- self.rejectIndeterminateLengthStrings = rejectIndeterminateLengthStrings
2222- self.rejectIndeterminateLengthArrays = rejectIndeterminateLengthArrays
2323- self.rejectIndeterminateLengthMaps = rejectIndeterminateLengthMaps
1616+ /// Create a new options object.
1717+ public init(rejectIndeterminateLengths: Bool = true) {
1818+ self.rejectIndeterminateLengths = rejectIndeterminateLengths
2419 }
2520}
+62-35
Sources/CBOR/Decoder/Scanner/CBORScanner.swift
···7788import Foundation
991010-/// # Why?
1111-/// I'd have loved to use a 'pop' method for this, where we only decode as data is requested. However, the way Swift's
1212-/// decoding APIs work forces us to be able to be able to do random access for keys in maps, which requires scanning.
1010+/// # Why Scan?
1111+/// I'd have loved to use a 'pop' method for decoding, where we only decode as data is requested. However, the way
1212+/// Swift's decoding APIs work forces us to be able to be able to do random access for keys in maps, which requires
1313+/// scanning.
1314///
1415/// Here we build a map of byte offsets and types to be able to quickly scan through a CBOR blob to find specific
1516/// indices and keys.
1717+///
1818+/// # Dev Notes
1919+///
2020+/// - This is where we do any indeterminate length validation and rejection. The decoder containers themselves will
2121+/// take either indeterminate or specific lengths and decode them.
1622@usableFromInline
1723final class CBORScanner {
1824 @usableFromInline
···2531 case rejectedIndeterminateLength(type: MajorType, offset: Int)
2632 }
27332828-// enum ScanItem: Int {
2929-// case map // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
3030-// case array // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
3131-//
3232-// case int // (offset: Int, byteCount: Int)
3333-// case string
3434-// case byteString
3535-// case tagged
3636-// case simple (byteCount: Int)
3737-// }
3434+ // MARK: - Results
38353636+ /// After the scanner scans, this contains a map that allows the CBOR data to be scanned for values at arbitrary
3737+ /// positions, keys, etc. The map contents are represented literally as ints for performance but uses the
3838+ /// following map:
3939+ /// ```
4040+ /// enum ScanItem: Int {
4141+ /// case map // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
4242+ /// case array // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
4343+ ///
4444+ /// case int // (offset: Int, byteCount: Int)
4545+ /// case string
4646+ /// case byteString
4747+ /// case tagged
4848+ /// case simple (byteCount: Int)
4949+ /// }
5050+ /// ```
3951 struct Results {
4052 var map: [Int] = []
4153···126138 }
127139 }
128140141141+ // MARK: - Map Navigation
142142+129143 func firstChildIndex(_ mapIndex: Int) -> Int {
130144 let byte = UInt8(results.map[mapIndex])
131145 guard let type = MajorType(rawValue: byte) else {
···173187 }
174188 }
175189176176-177190 switch type {
178178- case .uint:
179179- let size = try popByteCount()
180180- let offset = reader.index
181181- results.recordType(raw, currentByteIndex: offset, length: size)
182182- guard reader.canRead(size) else { throw ScanError.unexpectedEndOfData }
183183- reader.pop(size)
184184- case .nint:
185185- let size = try popByteCount()
186186- let offset = reader.index
187187- results.recordType(raw, currentByteIndex: offset, length: size)
188188- guard reader.canRead(size) else { throw ScanError.unexpectedEndOfData }
189189- reader.pop(size)
191191+ case .uint, .nint:
192192+ try scanInt(raw: raw)
190193 case .bytes:
191194 try scanBytesOrString(.bytes)
192195 case .string:
···196199 case .map:
197200 try scanMap()
198201 case .simple:
199199- let idx = reader.index
200200- results.recordSimple(reader.pop(), currentByteIndex: idx)
201201- reader.pop(simpleLength(raw))
202202+ scanSimple(raw: raw)
202203 case .tagged:
203204 fatalError()
204205 }
205206 }
206207208208+ // MARK: - Scan Int
209209+210210+ private func scanInt(raw: UInt8) throws {
211211+ let size = try popByteCount()
212212+ let offset = reader.index
213213+ results.recordType(raw, currentByteIndex: offset, length: size)
214214+ guard reader.canRead(size) else { throw ScanError.unexpectedEndOfData }
215215+ reader.pop(size)
216216+ }
217217+218218+ // MARK: - Scan Simple
219219+220220+ private func scanSimple(raw: UInt8) {
221221+ let idx = reader.index
222222+ results.recordSimple(reader.pop(), currentByteIndex: idx)
223223+ reader.pop(simpleLength(raw))
224224+ }
225225+207226 private func simpleLength(_ arg: UInt8) -> Int {
208227 switch arg & 0b11111 {
209228 case 25:
···216235 0 // Just this byte.
217236 }
218237 }
238238+239239+ // MARK: - Scan String/Bytes
219240220241 private func scanBytesOrString(_ type: MajorType) throws {
221242 let raw = reader._peek() // already checked previously
···229250 return
230251 }
231252232232- if (type == .string && options.rejectIndeterminateLengthStrings)
233233- || (type == .bytes && options.rejectIndeterminateLengthData) {
253253+ if (type == .string || type == .bytes) && options.rejectIndeterminateLengths {
234254 throw ScanError.rejectedIndeterminateLength(type: type, offset: reader.index)
235255 }
236256···255275 results.recordType(raw, currentByteIndex: start, length: reader.index - start)
256276 }
257277278278+ // MARK: - Scan Array
279279+258280 private func scanArray() throws {
259281 guard peekIsIndeterminate() else {
260282 let size = try reader.readNextInt(as: Int.self)
···266288 return
267289 }
268290269269- if options.rejectIndeterminateLengthArrays {
291291+ if options.rejectIndeterminateLengths {
270292 throw ScanError.rejectedIndeterminateLength(type: .array, offset: reader.index)
271293 }
272294···281303 reader.pop()
282304 results.recordEnd(childCount: count, resultLocation: mapIdx, currentByteIndex: reader.index)
283305 }
306306+307307+ // MARK: - Scan Map
284308285309 private func scanMap() throws {
286310 guard peekIsIndeterminate() else {
···293317 return
294318 }
295319296296- if options.rejectIndeterminateLengthMaps {
320320+ if options.rejectIndeterminateLengths {
297321 throw ScanError.rejectedIndeterminateLength(type: .map, offset: reader.index)
298322 }
299323···311335 }
312336}
313337338338+// MARK: - Utils
339339+314340extension CBORScanner {
315341 func popByteCount() throws -> Int {
316342 let byteCount = reader.popArgument()
···330356 }
331357}
332358359359+// MARK: - Debug Description
360360+333361#if DEBUG
334362extension CBORScanner: CustomDebugStringConvertible {
335335- @usableFromInline
336336- var debugDescription: String {
363363+ @usableFromInline var debugDescription: String {
337364 var string = ""
338365 func indent(_ other: String, d: Int) { string += String(repeating: " ", count: d * 2) + other + "\n" }
339366
+5
Sources/CBOR/Decoder/Scanner/DataReader.swift
···7788import Foundation
991010+/// A mutable struct used by the `CBORScanner` to iteratively scan a CBOR blob.
1111+/// Since this isn't passed by reference, this represents the *entire* blob instead of a single value
1212+/// like `DataRegion`.
1313+///
1414+/// This results in some duplicated code. I'd love to remove it but it works for now I suppose.
1015struct DataReader {
1116 private let data: Slice<UnsafeRawBufferPointer>
1217 private(set) var index = 0
+2-2
Sources/CBOR/Encoder/CBOREncoder.swift
···1111import Foundation
1212#endif
13131414-/// An object that can serialize ``Codable`` objects into the CBOR serialization format.
1414+/// Serializes ``Encodable`` objects using the CBOR serialization format.
1515///
1616/// To perform serialization, use the ``encode(_:)-6zhmp`` method to convert a Codable object to ``Data``. To
1717/// configure encoding behavior, either pass customization options in with
···2020 /// Options that determine the behavior of ``CBOREncoder``.
2121 public var options: EncodingOptions
22222323- /// Create a new CBOR encoder object.
2323+ /// Create a new CBOR encoder.
2424 /// - Parameters:
2525 /// - forceStringKeys: See ``EncodingOptions/forceStringKeys``.
2626 /// - useStringDates: See ``EncodingOptions/useStringDates``.
···55// Created by Khan Winter on 8/17/25.
66//
7788-@inlinable
88+@inlinable // swiftlint:disable:next cyclomatic_complexity
99func IntOptimizer<IntType: FixedWidthInteger>(value: IntType) -> EncodingOptimizer {
1010 let encodingValue: UInt
1111 if value < 0 {