···46 guard let childCount = data.childCount else { fatalError("Map scanned but no child count recorded.") }
47 decodedKeys.reserveCapacity(childCount / 2)
4849- var mapOffset = context.scanner.firstChildIndex(data.mapOffset)
50 for _ in 0..<childCount / 2 {
51- let key = context.scanner.load(at: mapOffset)
52 let decodedKey: AnyKey
53 switch key.type {
54 case .uint, .nint:
···63 context.error("Invalid key type found in map. Found \(key.type), expected an integer or string.")
64 )
65 }
66- mapOffset = context.scanner.siblingIndex(mapOffset)
6768- let value = context.scanner.load(at: mapOffset)
69- mapOffset = context.scanner.siblingIndex(mapOffset)
7071 decodedKeys[decodedKey] = value
72 }
···46 guard let childCount = data.childCount else { fatalError("Map scanned but no child count recorded.") }
47 decodedKeys.reserveCapacity(childCount / 2)
4849+ var mapOffset = context.scanner.results.firstChildIndex(data.mapOffset)
50 for _ in 0..<childCount / 2 {
51+ let key = context.scanner.results.load(at: mapOffset, reader: context.scanner.reader)
52 let decodedKey: AnyKey
53 switch key.type {
54 case .uint, .nint:
···63 context.error("Invalid key type found in map. Found \(key.type), expected an integer or string.")
64 )
65 }
66+ mapOffset = context.scanner.results.siblingIndex(mapOffset)
6768+ let value = context.scanner.results.load(at: mapOffset, reader: context.scanner.reader)
69+ mapOffset = context.scanner.results.siblingIndex(mapOffset)
7071 decodedKeys[decodedKey] = value
72 }
···23 /// Create a new CBOR encoder.
24 /// - Parameters:
25 /// - forceStringKeys: See ``EncodingOptions/forceStringKeys``.
26- /// - useStringDates: See ``EncodingOptions/useStringDates``.
27 /// - assumeUInt8IsByteString: See ``EncodingOptions/assumeUInt8IsByteString``.
28- public init(forceStringKeys: Bool = false, useStringDates: Bool = false, assumeUInt8IsByteString: Bool = true) {
000029 options = EncodingOptions(
30 forceStringKeys: forceStringKeys,
31- useStringDates: useStringDates,
32 assumeUInt8IsByteString: assumeUInt8IsByteString
33 )
34 }
···4142 let encodingContext = EncodingContext(options: options)
43 let encoder = SingleValueCBOREncodingContainer(parent: tempStorage, context: encodingContext)
44- try value.encode(to: encoder)
4546 let dataSize = tempStorage.value.size
47 var data = Data(count: dataSize)
48 data.withUnsafeMutableBytes { ptr in
49 var slice = ptr[...]
50 tempStorage.value.write(to: &slice)
51- assert(slice.isEmpty)
52- }
53- return data
54- }
55-56- /// Returns a CBOR-encoded representation of the value you supply.
57- /// - Note: This method is identical to ``encode(_:)-6zhmp``. This is a fast path included due to the lack of
58- /// ability to specialize Codable containers for specific types, such as byte strings.
59- /// - Parameter value: The value to encode as CBOR data.
60- /// - Returns: The encoded CBOR data.
61- public func encode(_ value: Data) throws -> Data {
62- // Fast path for plain data objects. See comments in ``UnkekedCBOREncodingContainer`` for why this can't be done
63- // via the real Codable APIs. Hate that we have to 'cheat' like this to get the performance I'd like for
64- // byte strings. >:(
65-66- var optimizer = ByteStringOptimizer(value: value)
67- let dataSize = optimizer.size
68- var data = Data(count: dataSize)
69- data.withUnsafeMutableBytes { ptr in
70- var slice = ptr[...]
71- optimizer.write(to: &slice)
72 assert(slice.isEmpty)
73 }
74 return data
···23 /// Create a new CBOR encoder.
24 /// - Parameters:
25 /// - forceStringKeys: See ``EncodingOptions/forceStringKeys``.
26+ /// - dateEncodingStrategy: See ``EncodingOptions/dateEncodingStrategy``.
27 /// - assumeUInt8IsByteString: See ``EncodingOptions/assumeUInt8IsByteString``.
28+ public init(
29+ forceStringKeys: Bool = false,
30+ dateEncodingStrategy: EncodingOptions.DateStrategy = .double,
31+ assumeUInt8IsByteString: Bool = true
32+ ) {
33 options = EncodingOptions(
34 forceStringKeys: forceStringKeys,
35+ dateEncodingStrategy: dateEncodingStrategy,
36 assumeUInt8IsByteString: assumeUInt8IsByteString
37 )
38 }
···4546 let encodingContext = EncodingContext(options: options)
47 let encoder = SingleValueCBOREncodingContainer(parent: tempStorage, context: encodingContext)
48+ try encoder.encode(value)
4950 let dataSize = tempStorage.value.size
51 var data = Data(count: dataSize)
52 data.withUnsafeMutableBytes { ptr in
53 var slice = ptr[...]
54 tempStorage.value.write(to: &slice)
00000000000000000000055 assert(slice.isEmpty)
56 }
57 return data
···63 // special encoding cases. It's still lame.
6465 if let date = value as? Date {
66- if options.useStringDates {
067 parent.register(StringDateOptimizer(value: date))
68- } else {
69- parent.register(EpochDateOptimizer(value: date))
0070 }
71 } else if let uuid = value as? UUID {
72 parent.register(UUIDOptimizer(value: uuid))
···63 // special encoding cases. It's still lame.
6465 if let date = value as? Date {
66+ switch options.dateEncodingStrategy {
67+ case .string:
68 parent.register(StringDateOptimizer(value: date))
69+ case .float:
70+ parent.register(EpochFloatDateOptimizer(value: date))
71+ case .double:
72+ parent.register(EpochDoubleDateOptimizer(value: date))
73 }
74 } else if let uuid = value as? UUID {
75 parent.register(UUIDOptimizer(value: uuid))
···22 }
2324 func encode<T: Encodable>(_ value: T) throws {
25- try value.encode(
26- to: SingleValueCBOREncodingContainer(parent: storage.forAppending(), context: nextContext())
27- )
28- }
29-30- func encode(_ value: UInt8) throws {
31- storage.data.append(value)
32- if !context.options.assumeUInt8IsByteString {
33- try value.encode(
34- to: SingleValueCBOREncodingContainer(parent: storage.forAppending(), context: nextContext())
35- )
36- }
37 }
3839 mutating func encodeNil() throws {
···6465 let parent: ParentStorage
66 var items: [EncodingOptimizer] = []
67- var data: [UInt8] = []
6869 init(parent: ParentStorage) {
70 self.parent = parent
···86 }
8788 deinit {
89- // Swift doesn't give us a good way to detect a 'byte string'. So, we record both UInt8 values and
90- // some optimizers. At this point, we can check if we're encoding either a collection of multiple
91- // types (items.count > data.count), or a pure data collection (data.count == items.count).
92- // This is terrible in terms of memory use, but lets us encode byte strings using the most optimal
93- // encoding method.
94- // CBOR also mandates that an empty collection is by default an array, so we check if this is empty.
95- // Frankly, this blows and I wish Swift's Codable API was even a smidgen less fucked.
96-97- if items.count <= data.count && !data.isEmpty {
98- parent.register(ByteStringOptimizer(value: data))
99- } else {
100- parent.register(UnkeyedOptimizer(value: items))
101- }
102 }
103 }
104
···9public struct EncodingOptions {
10 /// Force encoded maps to use string keys even when integer keys are available.
11 public let forceStringKeys: Bool
0000000000001213- /// Encode dates as strings instead of epoch timestamps (Doubles)
14- public let useStringDates: Bool
1516 /// Codable can't tell us if we're encoding a Data or [UInt8] object. By default this library assumes that if it's
17 /// encoding an unkeyed container or UInt8 objects it's a byte string. Toggle this to false to disable this.
···21 /// Initialize new encoding options.
22 /// - Parameters:
23 /// - forceStringKeys: Force encoded maps to use string keys even when integer keys are available.
24- /// - useStringDates: Encode dates as strings instead of epoch timestamps (Doubles)
25 /// - assumeUInt8IsByteString: See ``assumeUInt8IsByteString``.
26- public init(forceStringKeys: Bool, useStringDates: Bool, assumeUInt8IsByteString: Bool) {
27 self.forceStringKeys = forceStringKeys
28- self.useStringDates = useStringDates
29 self.assumeUInt8IsByteString = assumeUInt8IsByteString
30 }
31}
···9public struct EncodingOptions {
10 /// Force encoded maps to use string keys even when integer keys are available.
11 public let forceStringKeys: Bool
12+13+ /// Methods for encoding dates.
14+ public enum DateStrategy {
15+ /// Encodes dates as `ISO8601` date strings under tag `0`.
16+ case string
17+ /// Encodes dates as an epoch date using a Float value. Loses precision at the benefit of half the size
18+ /// of a double.
19+ case float
20+ /// Encodes dates as an epoch date using a Double value.
21+ /// Highest precision.
22+ case double
23+ }
2425+ /// Determine how to encode dates.
26+ public let dateEncodingStrategy: DateStrategy
2728 /// Codable can't tell us if we're encoding a Data or [UInt8] object. By default this library assumes that if it's
29 /// encoding an unkeyed container or UInt8 objects it's a byte string. Toggle this to false to disable this.
···33 /// Initialize new encoding options.
34 /// - Parameters:
35 /// - forceStringKeys: Force encoded maps to use string keys even when integer keys are available.
36+ /// - useStringDates: See ``dateEncodingStrategy`` and ``DateStrategy``.
37 /// - assumeUInt8IsByteString: See ``assumeUInt8IsByteString``.
38+ public init(forceStringKeys: Bool, dateEncodingStrategy: DateStrategy, assumeUInt8IsByteString: Bool) {
39 self.forceStringKeys = forceStringKeys
40+ self.dateEncodingStrategy = dateEncodingStrategy
41 self.assumeUInt8IsByteString = assumeUInt8IsByteString
42 }
43}
···1+//
2+// UInt8+simpleLength.swift
3+// CBOR
4+//
5+// Created by Khan Winter on 9/1/25.
6+//
7+8+extension UInt8 {
9+ @inlinable
10+ func simpleLength() -> Int {
11+ switch self & 0b11111 {
12+ case 25:
13+ 2 // Half-float
14+ case 26:
15+ 4 // Float
16+ case 27:
17+ 8 // Double
18+ default:
19+ 0 // Just this byte.
20+ }
21+ }
22+}
+6-5
Sources/CBOR/MajorType.swift
···5// Created by Khan Winter on 8/17/25.
6//
78-@usableFromInline
9-enum MajorType: UInt8, Sendable, Hashable, Equatable {
10 case uint = 0
11 case nint = 1
12 case bytes = 2
···16 case tagged = 6
17 case simple = 7
18019 @inline(__always)
20- @usableFromInline
21- init?(rawValue: UInt8) {
22 // We only care about the top 3 bits
23 switch rawValue >> 5 {
24 case MajorType.uint.rawValue: self = .uint
···34 }
3536 @inline(__always)
37- @inlinable var bits: UInt8 {
38 rawValue << 5
39 }
40041 var intValue: Int {
42 Int(bits)
43 }
···5// Created by Khan Winter on 8/17/25.
6//
78+/// Represents a major type as described by the CBOR specification.
9+@frozen public enum MajorType: UInt8, Sendable, Hashable, Equatable {
10 case uint = 0
11 case nint = 1
12 case bytes = 2
···16 case tagged = 6
17 case simple = 7
1819+ /// Create a major type using a raw integer (only uses the highest 3 bits).
20 @inline(__always)
21+ public init?(rawValue: UInt8) {
022 // We only care about the top 3 bits
23 switch rawValue >> 5 {
24 case MajorType.uint.rawValue: self = .uint
···34 }
3536 @inline(__always)
37+ var bits: UInt8 {
38 rawValue << 5
39 }
4041+ @inline(__always)
42 var intValue: Int {
43 Int(bits)
44 }
+9-1
Tests/CBORTests/DecodableTests.swift
···218 let context = DecodingContext(scanner: scanner)
219 let container = SingleValueCBORDecodingContainer(
220 context: context,
221- data: scanner.load(at: 0)
222 )
223224 var unkeyedContainer = try container.unkeyedContainer()
···265 func emptyData() throws {
266 let data = Data()
267 #expect(throws: DecodingError.self) { try CBORDecoder().decode(Data.self, from: data) }
00000000268 }
269}
···218 let context = DecodingContext(scanner: scanner)
219 let container = SingleValueCBORDecodingContainer(
220 context: context,
221+ data: scanner.results.load(at: 0, reader: scanner.reader)
222 )
223224 var unkeyedContainer = try container.unkeyedContainer()
···265 func emptyData() throws {
266 let data = Data()
267 #expect(throws: DecodingError.self) { try CBORDecoder().decode(Data.self, from: data) }
268+ }
269+270+ @Test
271+ func date() throws {
272+ let data = "C11A5C295C00".asHexData()
273+ let expected = Date(timeIntervalSince1970: 1546214400.0)
274+ let value = try CBORDecoder().decode(Date.self, from: data)
275+ #expect(value == expected)
276 }
277}