···11+//
22+// ScanItem.swift
33+// CBOR
44+//
55+// Created by Khan Winter on 9/1/25.
66+//
77+88+extension CBORScanner {
99+ /// After the scanner scans, this contains a map that allows the CBOR data to be scanned for values at arbitrary
1010+ /// positions, keys, etc. The map contents are represented literally as ints for performance but uses the
1111+ /// following map:
1212+ /// ```
1313+ /// enum ScanItem: Int {
1414+ /// case map // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
1515+ /// case array // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
1616+ ///
1717+ /// case int // (offset: Int, byteCount: Int)
1818+ /// case string
1919+ /// case byteString
2020+ /// case simple // (offset: Int)
2121+ /// case tagged
2222+ /// }
2323+ /// ```
2424+ struct Results {
2525+ var map: [Int] = []
2626+2727+ init(dataCount: Int) {
2828+ self.map = []
2929+ map.reserveCapacity(dataCount * 4)
3030+ }
3131+3232+ mutating func recordMapStart(currentByteIndex: Int) -> Int {
3333+ map.append(MajorType.map.intValue)
3434+ map.append(0)
3535+ map.append(map.count + 3)
3636+ map.append(currentByteIndex)
3737+ map.append(currentByteIndex)
3838+ return map.count - 5
3939+ }
4040+4141+ mutating func recordArrayStart(currentByteIndex: Int) -> Int {
4242+ map.append(MajorType.array.intValue)
4343+ map.append(0) // child count
4444+ map.append(map.count + 3) // map count
4545+ map.append(currentByteIndex) // start byte
4646+ map.append(currentByteIndex) // byte count
4747+ return map.count - 5
4848+ }
4949+5050+ mutating func recordEnd(childCount: Int, resultLocation: Int, currentByteIndex: Int) {
5151+ map[resultLocation + 1] = childCount
5252+ map[resultLocation + 2] = map.count - map[resultLocation + 2]
5353+ map[resultLocation + 4] = currentByteIndex - map[resultLocation + 4]
5454+ }
5555+5656+ mutating func recordType(_ type: UInt8, currentByteIndex: Int, length: Int) {
5757+ map.append(Int(type))
5858+ map.append(currentByteIndex)
5959+ map.append(length)
6060+ }
6161+6262+ mutating func recordSimple(_ type: UInt8, currentByteIndex: Int) {
6363+ map.append(Int(type))
6464+ map.append(currentByteIndex)
6565+ }
6666+6767+ // MARK: - Load Values
6868+6969+ func load(at mapIndex: Int, reader: DataReader) -> DataRegion {
7070+ assert(mapIndex < map.count)
7171+ let byte = UInt8(map[mapIndex])
7272+ let argument = byte & 0b1_1111
7373+ guard let type = MajorType(rawValue: byte) else {
7474+ fatalError("Invalid type found in map: \(map[mapIndex]) at index: \(mapIndex)")
7575+ }
7676+ switch type {
7777+ case .uint, .nint, .bytes, .string, .tagged:
7878+ assert(mapIndex + 1 < map.count)
7979+ let location = map[mapIndex + 1]
8080+ let length = map[mapIndex + 2]
8181+ let slice = reader.slice(location..<(location + length))
8282+ return DataRegion(type: type, argument: argument, childCount: nil, mapOffset: mapIndex, data: slice)
8383+ case .simple:
8484+ let length = UInt8(map[mapIndex]).simpleLength()
8585+ let slice: Slice<UnsafeRawBufferPointer>
8686+ if length == 0 {
8787+ slice = reader.slice(0..<0)
8888+ } else {
8989+ let location = map[mapIndex + 1] + 1 // skip type & arg byte.
9090+ slice = reader.slice(location..<(location + length))
9191+ }
9292+ return DataRegion(type: type, argument: argument, childCount: nil, mapOffset: mapIndex, data: slice)
9393+ case .array, .map:
9494+ // Map determines the slice size
9595+ let childCount = map[mapIndex + 1]
9696+ let location = map[mapIndex + 3]
9797+ let length = map[mapIndex + 4]
9898+ let slice = reader.slice(location..<(location + length))
9999+ return DataRegion(type: type, argument: argument, childCount: childCount, mapOffset: mapIndex, data: slice)
100100+ }
101101+ }
102102+103103+ // MARK: - Map Navigation
104104+105105+ func firstChildIndex(_ mapIndex: Int) -> Int {
106106+ let byte = UInt8(map[mapIndex])
107107+ guard let type = MajorType(rawValue: byte) else {
108108+ fatalError("Invalid type found in map: \(map[mapIndex]) at index: \(mapIndex)")
109109+ }
110110+ switch type {
111111+ case .uint, .nint, .bytes, .string, .tagged, .simple:
112112+ fatalError("Can't find child index for non-container type.")
113113+ case .array, .map: // type byte + 4 map values
114114+ return mapIndex + 5
115115+ }
116116+ }
117117+118118+ func siblingIndex(_ mapIndex: Int) -> Int {
119119+ let byte = UInt8(map[mapIndex])
120120+ guard let type = MajorType(rawValue: byte) else {
121121+ fatalError("Invalid type found in map: \(map[mapIndex]) at index: \(mapIndex)")
122122+ }
123123+ switch type {
124124+ case .uint, .nint, .bytes, .string:
125125+ return mapIndex + 3
126126+ case .simple:
127127+ return mapIndex + 2
128128+ case .array, .map: // Map contains the map/array count
129129+ return mapIndex + 5 + map[mapIndex + 2]
130130+ case .tagged:
131131+ // Find the next index after the scanned data for the tagged item.
132132+ return siblingIndex(mapIndex + 3)
133133+ }
134134+ }
135135+136136+ func loadTagData(tagMapIndex mapIndex: Int, reader: DataReader) -> DataRegion {
137137+ return load(at: mapIndex + 3, reader: reader)
138138+ }
139139+ }
140140+}
+47-148
Sources/CBOR/Decoder/Scanner/CBORScanner.swift
···1616 case typeInIndeterminateString(type: MajorType, offset: Int)
1717 case rejectedIndeterminateLength(type: MajorType, offset: Int)
1818 case cannotRepresentInt(max: UInt, found: UInt, offset: Int)
1919+ case noTagInformation(tag: UInt, offset: Int)
2020+ case invalidMajorTypeForTaggedItem(tag: UInt, expected: Set<MajorType>, found: MajorType, offset: Int)
1921}
20222123/// # Why Scan?
···3234/// take either indeterminate or specific lengths and decode them.
3335@usableFromInline
3436final class CBORScanner {
3535- // MARK: - Results
3636-3737- /// After the scanner scans, this contains a map that allows the CBOR data to be scanned for values at arbitrary
3838- /// positions, keys, etc. The map contents are represented literally as ints for performance but uses the
3939- /// following map:
4040- /// ```
4141- /// enum ScanItem: Int {
4242- /// case map // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
4343- /// case array // (childCount: Int, mapCount: Int, offset: Int, byteCount: Int)
4444- ///
4545- /// case int // (offset: Int, byteCount: Int)
4646- /// case string
4747- /// case byteString
4848- /// case tagged
4949- /// case simple (byteCount: Int)
5050- /// }
5151- /// ```
5252- struct Results {
5353- var map: [Int] = []
5454-5555- init(dataCount: Int) {
5656- self.map = []
5757- map.reserveCapacity(dataCount * 4)
5858- }
5959-6060- mutating func recordMapStart(currentByteIndex: Int) -> Int {
6161- map.append(MajorType.map.intValue)
6262- map.append(0)
6363- map.append(map.count + 3)
6464- map.append(currentByteIndex)
6565- map.append(currentByteIndex)
6666- return map.count - 5
6767- }
6868-6969- mutating func recordArrayStart(currentByteIndex: Int) -> Int {
7070- map.append(MajorType.array.intValue)
7171- map.append(0) // child count
7272- map.append(map.count + 3) // map count
7373- map.append(currentByteIndex) // start byte
7474- map.append(currentByteIndex) // byte count
7575- return map.count - 5
7676- }
7777-7878- mutating func recordEnd(childCount: Int, resultLocation: Int, currentByteIndex: Int) {
7979- map[resultLocation + 1] = childCount
8080- map[resultLocation + 2] = map.count - map[resultLocation + 2]
8181- map[resultLocation + 4] = currentByteIndex - map[resultLocation + 4]
8282- }
8383-8484- mutating func recordType(_ type: UInt8, currentByteIndex: Int, length: Int) {
8585- map.append(Int(type))
8686- map.append(currentByteIndex)
8787- map.append(length)
8888- }
8989-9090- mutating func recordSimple(_ type: UInt8, currentByteIndex: Int) {
9191- map.append(Int(type))
9292- map.append(currentByteIndex)
9393- }
9494- }
9595-9637 var reader: DataReader
9738 var results: Results
9839 let options: DecodingOptions
4040+4141+ let taggedScanMap: [UInt: Set<MajorType>] = [
4242+ 0: [.string], // Date (string)
4343+ 1: [.uint, .nint, .simple], // Date (epoch)
4444+ 37: [.bytes], // UUID
4545+ ]
994610047 var isEmpty: Bool {
10148 results.map.isEmpty
···10754 self.options = options
10855 }
10956110110- // MARK: - Load Values
111111-112112- func load(at mapIndex: Int) -> DataRegion {
113113- assert(mapIndex < results.map.count)
114114- let byte = UInt8(results.map[mapIndex])
115115- let argument = byte & 0b1_1111
116116- guard let type = MajorType(rawValue: byte) else {
117117- fatalError("Invalid type found in map: \(results.map[mapIndex]) at index: \(mapIndex)")
118118- }
119119- switch type {
120120- case .uint, .nint, .bytes, .string, .tagged:
121121- assert(mapIndex + 1 < results.map.count)
122122- let location = results.map[mapIndex + 1]
123123- let length = results.map[mapIndex + 2]
124124- let slice = reader.slice(location..<(location + length))
125125- return DataRegion(type: type, argument: argument, childCount: nil, mapOffset: mapIndex, data: slice)
126126- case .simple:
127127- let length = simpleLength(UInt8(results.map[mapIndex]))
128128- let slice: Slice<UnsafeRawBufferPointer>
129129- if length == 0 {
130130- slice = reader.slice(0..<0)
131131- } else {
132132- let location = results.map[mapIndex + 1] + 1 // skip type & arg byte.
133133- slice = reader.slice(location..<(location + length))
134134- }
135135- return DataRegion(type: type, argument: argument, childCount: nil, mapOffset: mapIndex, data: slice)
136136- case .array, .map:
137137- // Map determines the slice size
138138- let childCount = results.map[mapIndex + 1]
139139- let location = results.map[mapIndex + 3]
140140- let length = results.map[mapIndex + 4]
141141- let slice = reader.slice(location..<(location + length))
142142- return DataRegion(type: type, argument: argument, childCount: childCount, mapOffset: mapIndex, data: slice)
143143- }
144144- }
145145-146146- // MARK: - Map Navigation
147147-148148- func firstChildIndex(_ mapIndex: Int) -> Int {
149149- let byte = UInt8(results.map[mapIndex])
150150- guard let type = MajorType(rawValue: byte) else {
151151- fatalError("Invalid type found in map: \(results.map[mapIndex]) at index: \(mapIndex)")
152152- }
153153- switch type {
154154- case .uint, .nint, .bytes, .string, .tagged, .simple:
155155- fatalError("Can't find child index for non-container type.")
156156- case .array, .map: // type byte + 4 map values
157157- return mapIndex + 5
158158- }
159159- }
160160-161161- func siblingIndex(_ mapIndex: Int) -> Int {
162162- let byte = UInt8(results.map[mapIndex])
163163- guard let type = MajorType(rawValue: byte) else {
164164- fatalError("Invalid type found in map: \(results.map[mapIndex]) at index: \(mapIndex)")
165165- }
166166- switch type {
167167- case .uint, .nint, .bytes, .string, .tagged:
168168- return mapIndex + 3
169169- case .simple:
170170- return mapIndex + 2
171171- case .array, .map: // Map contains the map/array count
172172- return mapIndex + 5 + results.map[mapIndex + 2]
173173- }
174174- }
175175-17657 // MARK: - Scan
1775817859 func scan() throws {
···19273 }
19374 }
194757676+ try scanType(type: type, raw: raw)
7777+ }
7878+7979+ private func scanType(type: MajorType, raw: UInt8) throws {
19580 switch type {
19681 case .uint, .nint:
19782 try scanInt(raw: raw)
19883 case .bytes:
199199- try scanBytesOrString(.bytes)
8484+ try scanBytesOrString(.bytes, raw: raw)
20085 case .string:
201201- try scanBytesOrString(.string)
8686+ try scanBytesOrString(.string, raw: raw)
20287 case .array:
20388 try scanArray()
20489 case .map:
···20691 case .simple:
20792 try scanSimple(raw: raw)
20893 case .tagged:
209209- throw ScanError.expectedMajorType(offset: 0)
9494+ try scanTagged(raw: raw)
21095 }
21196 }
21297···225110 private func scanSimple(raw: UInt8) throws {
226111 let idx = reader.index
227112 results.recordSimple(reader.pop(), currentByteIndex: idx)
228228- guard reader.canRead(simpleLength(raw)) else {
113113+ guard reader.canRead(raw.simpleLength()) else {
229114 throw ScanError.unexpectedEndOfData
230115 }
231231- reader.pop(simpleLength(raw))
232232- }
233233-234234- private func simpleLength(_ arg: UInt8) -> Int {
235235- switch arg & 0b11111 {
236236- case 25:
237237- 2 // Half-float
238238- case 26:
239239- 4 // Float
240240- case 27:
241241- 8 // Double
242242- default:
243243- 0 // Just this byte.
244244- }
116116+ reader.pop(raw.simpleLength())
245117 }
246118247119 // MARK: - Scan String/Bytes
248120249249- private func scanBytesOrString(_ type: MajorType) throws {
250250- let raw = reader._peek() // already checked previously
251251-121121+ private func scanBytesOrString(_ type: MajorType, raw: UInt8) throws {
252122 guard peekIsIndeterminate() else {
253123 let size = try reader.readNextInt(as: Int.self)
254124 let offset = reader.index
···345215 // Pop the break byte
346216 reader.pop()
347217 results.recordEnd(childCount: count, resultLocation: mapIdx, currentByteIndex: reader.index)
218218+ }
219219+220220+ private func scanTagged(raw: UInt8) throws {
221221+ guard let size = reader.peekArgument()?.byteCount() else {
222222+ throw ScanError.invalidSize(byte: reader.peekArgument() ?? .max, offset: reader.index)
223223+ }
224224+ let offset = reader.index
225225+ results.recordType(raw, currentByteIndex: offset, length: Int(size))
226226+227227+ let tag = try reader.readNextInt(as: UInt.self)
228228+229229+ guard let validMajorTypes = taggedScanMap[tag] else {
230230+ throw ScanError.noTagInformation(tag: tag, offset: reader.index)
231231+ }
232232+233233+ guard let nextRaw = reader.peek(), let nextTag = MajorType(rawValue: nextRaw) else {
234234+ throw ScanError.unexpectedEndOfData
235235+ }
236236+237237+ guard validMajorTypes.contains(nextTag) else {
238238+ throw ScanError.invalidMajorTypeForTaggedItem(
239239+ tag: tag,
240240+ expected: validMajorTypes,
241241+ found: nextTag,
242242+ offset: offset
243243+ )
244244+ }
245245+246246+ try scanType(type: nextTag, raw: nextRaw)
348247 }
349248}
+8-25
Sources/CBOR/Encoder/CBOREncoder.swift
···2323 /// Create a new CBOR encoder.
2424 /// - Parameters:
2525 /// - forceStringKeys: See ``EncodingOptions/forceStringKeys``.
2626- /// - useStringDates: See ``EncodingOptions/useStringDates``.
2626+ /// - dateEncodingStrategy: See ``EncodingOptions/dateEncodingStrategy``.
2727 /// - assumeUInt8IsByteString: See ``EncodingOptions/assumeUInt8IsByteString``.
2828- public init(forceStringKeys: Bool = false, useStringDates: Bool = false, assumeUInt8IsByteString: Bool = true) {
2828+ public init(
2929+ forceStringKeys: Bool = false,
3030+ dateEncodingStrategy: EncodingOptions.DateStrategy = .double,
3131+ assumeUInt8IsByteString: Bool = true
3232+ ) {
2933 options = EncodingOptions(
3034 forceStringKeys: forceStringKeys,
3131- useStringDates: useStringDates,
3535+ dateEncodingStrategy: dateEncodingStrategy,
3236 assumeUInt8IsByteString: assumeUInt8IsByteString
3337 )
3438 }
···41454246 let encodingContext = EncodingContext(options: options)
4347 let encoder = SingleValueCBOREncodingContainer(parent: tempStorage, context: encodingContext)
4444- try value.encode(to: encoder)
4848+ try encoder.encode(value)
45494650 let dataSize = tempStorage.value.size
4751 var data = Data(count: dataSize)
4852 data.withUnsafeMutableBytes { ptr in
4953 var slice = ptr[...]
5054 tempStorage.value.write(to: &slice)
5151- assert(slice.isEmpty)
5252- }
5353- return data
5454- }
5555-5656- /// Returns a CBOR-encoded representation of the value you supply.
5757- /// - Note: This method is identical to ``encode(_:)-6zhmp``. This is a fast path included due to the lack of
5858- /// ability to specialize Codable containers for specific types, such as byte strings.
5959- /// - Parameter value: The value to encode as CBOR data.
6060- /// - Returns: The encoded CBOR data.
6161- public func encode(_ value: Data) throws -> Data {
6262- // Fast path for plain data objects. See comments in ``UnkekedCBOREncodingContainer`` for why this can't be done
6363- // via the real Codable APIs. Hate that we have to 'cheat' like this to get the performance I'd like for
6464- // byte strings. >:(
6565-6666- var optimizer = ByteStringOptimizer(value: value)
6767- let dataSize = optimizer.size
6868- var data = Data(count: dataSize)
6969- data.withUnsafeMutableBytes { ptr in
7070- var slice = ptr[...]
7171- optimizer.write(to: &slice)
7255 assert(slice.isEmpty)
7356 }
7457 return data
···6363 // special encoding cases. It's still lame.
64646565 if let date = value as? Date {
6666- if options.useStringDates {
6666+ switch options.dateEncodingStrategy {
6767+ case .string:
6768 parent.register(StringDateOptimizer(value: date))
6868- } else {
6969- parent.register(EpochDateOptimizer(value: date))
6969+ case .float:
7070+ parent.register(EpochFloatDateOptimizer(value: date))
7171+ case .double:
7272+ parent.register(EpochDoubleDateOptimizer(value: date))
7073 }
7174 } else if let uuid = value as? UUID {
7275 parent.register(UUIDOptimizer(value: uuid))
···2222 }
23232424 func encode<T: Encodable>(_ value: T) throws {
2525- try value.encode(
2626- to: SingleValueCBOREncodingContainer(parent: storage.forAppending(), context: nextContext())
2727- )
2828- }
2929-3030- func encode(_ value: UInt8) throws {
3131- storage.data.append(value)
3232- if !context.options.assumeUInt8IsByteString {
3333- try value.encode(
3434- to: SingleValueCBOREncodingContainer(parent: storage.forAppending(), context: nextContext())
3535- )
3636- }
2525+ try SingleValueCBOREncodingContainer(parent: storage.forAppending(), context: nextContext()).encode(value)
3726 }
38273928 mutating func encodeNil() throws {
···64536554 let parent: ParentStorage
6655 var items: [EncodingOptimizer] = []
6767- var data: [UInt8] = []
68566957 init(parent: ParentStorage) {
7058 self.parent = parent
···8674 }
87758876 deinit {
8989- // Swift doesn't give us a good way to detect a 'byte string'. So, we record both UInt8 values and
9090- // some optimizers. At this point, we can check if we're encoding either a collection of multiple
9191- // types (items.count > data.count), or a pure data collection (data.count == items.count).
9292- // This is terrible in terms of memory use, but lets us encode byte strings using the most optimal
9393- // encoding method.
9494- // CBOR also mandates that an empty collection is by default an array, so we check if this is empty.
9595- // Frankly, this blows and I wish Swift's Codable API was even a smidgen less fucked.
9696-9797- if items.count <= data.count && !data.isEmpty {
9898- parent.register(ByteStringOptimizer(value: data))
9999- } else {
100100- parent.register(UnkeyedOptimizer(value: items))
101101- }
7777+ parent.register(UnkeyedOptimizer(value: items))
10278 }
10379 }
10480
+17-5
Sources/CBOR/Encoder/EncodingOptions.swift
···99public struct EncodingOptions {
1010 /// Force encoded maps to use string keys even when integer keys are available.
1111 public let forceStringKeys: Bool
1212+1313+ /// Methods for encoding dates.
1414+ public enum DateStrategy {
1515+ /// Encodes dates as `ISO8601` date strings under tag `0`.
1616+ case string
1717+ /// Encodes dates as an epoch date using a Float value. Loses precision at the benefit of half the size
1818+ /// of a double.
1919+ case float
2020+ /// Encodes dates as an epoch date using a Double value.
2121+ /// Highest precision.
2222+ case double
2323+ }
12241313- /// Encode dates as strings instead of epoch timestamps (Doubles)
1414- public let useStringDates: Bool
2525+ /// Determine how to encode dates.
2626+ public let dateEncodingStrategy: DateStrategy
15271628 /// Codable can't tell us if we're encoding a Data or [UInt8] object. By default this library assumes that if it's
1729 /// encoding an unkeyed container or UInt8 objects it's a byte string. Toggle this to false to disable this.
···2133 /// Initialize new encoding options.
2234 /// - Parameters:
2335 /// - forceStringKeys: Force encoded maps to use string keys even when integer keys are available.
2424- /// - useStringDates: Encode dates as strings instead of epoch timestamps (Doubles)
3636+ /// - useStringDates: See ``dateEncodingStrategy`` and ``DateStrategy``.
2537 /// - assumeUInt8IsByteString: See ``assumeUInt8IsByteString``.
2626- public init(forceStringKeys: Bool, useStringDates: Bool, assumeUInt8IsByteString: Bool) {
3838+ public init(forceStringKeys: Bool, dateEncodingStrategy: DateStrategy, assumeUInt8IsByteString: Bool) {
2739 self.forceStringKeys = forceStringKeys
2828- self.useStringDates = useStringDates
4040+ self.dateEncodingStrategy = dateEncodingStrategy
2941 self.assumeUInt8IsByteString = assumeUInt8IsByteString
3042 }
3143}
···11+//
22+// UInt8+simpleLength.swift
33+// CBOR
44+//
55+// Created by Khan Winter on 9/1/25.
66+//
77+88+extension UInt8 {
99+ @inlinable
1010+ func simpleLength() -> Int {
1111+ switch self & 0b11111 {
1212+ case 25:
1313+ 2 // Half-float
1414+ case 26:
1515+ 4 // Float
1616+ case 27:
1717+ 8 // Double
1818+ default:
1919+ 0 // Just this byte.
2020+ }
2121+ }
2222+}
+6-5
Sources/CBOR/MajorType.swift
···55// Created by Khan Winter on 8/17/25.
66//
7788-@usableFromInline
99-enum MajorType: UInt8, Sendable, Hashable, Equatable {
88+/// Represents a major type as described by the CBOR specification.
99+@frozen public enum MajorType: UInt8, Sendable, Hashable, Equatable {
1010 case uint = 0
1111 case nint = 1
1212 case bytes = 2
···1616 case tagged = 6
1717 case simple = 7
18181919+ /// Create a major type using a raw integer (only uses the highest 3 bits).
1920 @inline(__always)
2020- @usableFromInline
2121- init?(rawValue: UInt8) {
2121+ public init?(rawValue: UInt8) {
2222 // We only care about the top 3 bits
2323 switch rawValue >> 5 {
2424 case MajorType.uint.rawValue: self = .uint
···3434 }
35353636 @inline(__always)
3737- @inlinable var bits: UInt8 {
3737+ var bits: UInt8 {
3838 rawValue << 5
3939 }
40404141+ @inline(__always)
4142 var intValue: Int {
4243 Int(bits)
4344 }
+9-1
Tests/CBORTests/DecodableTests.swift
···218218 let context = DecodingContext(scanner: scanner)
219219 let container = SingleValueCBORDecodingContainer(
220220 context: context,
221221- data: scanner.load(at: 0)
221221+ data: scanner.results.load(at: 0, reader: scanner.reader)
222222 )
223223224224 var unkeyedContainer = try container.unkeyedContainer()
···265265 func emptyData() throws {
266266 let data = Data()
267267 #expect(throws: DecodingError.self) { try CBORDecoder().decode(Data.self, from: data) }
268268+ }
269269+270270+ @Test
271271+ func date() throws {
272272+ let data = "C11A5C295C00".asHexData()
273273+ let expected = Date(timeIntervalSince1970: 1546214400.0)
274274+ let value = try CBORDecoder().decode(Date.self, from: data)
275275+ #expect(value == expected)
268276 }
269277}
+24
Tests/CBORTests/RoundTripTests.swift
···113113 #expect(first.name == Person.mock.name)
114114 }
115115 }
116116+117117+ @Test
118118+ func dateEpoch() throws {
119119+ let value = Date()
120120+ let encoded = try CBOREncoder().encode(value)
121121+ let decoded = try CBORDecoder().decode(Date.self, from: encoded)
122122+ #expect(decoded.timeIntervalSince1970.rounded(.down) == value.timeIntervalSince1970.rounded(.down))
123123+ }
124124+125125+ @Test
126126+ func dateString() throws {
127127+ let value = Date()
128128+ let encoded = try CBOREncoder(dateEncodingStrategy: .string).encode(value)
129129+ let decoded = try CBORDecoder().decode(Date.self, from: encoded)
130130+ #expect(decoded.timeIntervalSince1970.rounded(.down) == value.timeIntervalSince1970.rounded(.down))
131131+ }
132132+133133+ @Test
134134+ func uuid() throws {
135135+ let value = UUID()
136136+ let encoded = try CBOREncoder().encode(value)
137137+ let decoded = try CBORDecoder().decode(UUID.self, from: encoded)
138138+ #expect(decoded == value)
139139+ }
116140}