From 9cf08556d2439e0959875e410cca095971b61abb Mon Sep 17 00:00:00 2001 From: Yanis Plumit <> Date: Thu, 3 Apr 2025 01:14:21 +0300 Subject: [PATCH 1/4] KeyedEncodingContainer: Save the order to allow keys sorting in CodableCBOREncoder --- Sources/Encoder/KeyedEncodingContainer.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/Encoder/KeyedEncodingContainer.swift b/Sources/Encoder/KeyedEncodingContainer.swift index 2b0bbfe..6c7f04e 100644 --- a/Sources/Encoder/KeyedEncodingContainer.swift +++ b/Sources/Encoder/KeyedEncodingContainer.swift @@ -2,7 +2,7 @@ import Foundation extension _CBOREncoder { final class KeyedContainer { - var storage: [AnyCodingKey: CBOREncodingContainer] = [:] + var storage: [(AnyCodingKey, CBOREncodingContainer)] = [] var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] @@ -38,7 +38,9 @@ extension _CBOREncoder.KeyedContainer: KeyedEncodingContainerProtocol { userInfo: self.userInfo, options: self.options ) - self.storage[anyCodingKeyForKey(key)] = container + let codingKey = anyCodingKeyForKey(key) + self.storage.filter { $0.0 != codingKey } + self.storage.append( (codingKey, container) ) return container } @@ -48,7 +50,9 @@ extension _CBOREncoder.KeyedContainer: KeyedEncodingContainerProtocol { userInfo: self.userInfo, options: self.options ) - self.storage[anyCodingKeyForKey(key)] = container + let codingKey = anyCodingKeyForKey(key) + self.storage.filter { $0.0 != codingKey } + self.storage.append( (codingKey, container) ) return container } @@ -58,7 +62,9 @@ extension _CBOREncoder.KeyedContainer: KeyedEncodingContainerProtocol { userInfo: self.userInfo, options: self.options ) - self.storage[anyCodingKeyForKey(key)] = container + let codingKey = anyCodingKeyForKey(key) + self.storage.filter { $0.0 != codingKey } + self.storage.append( (codingKey, container) ) return KeyedEncodingContainer(container) } From 11f4867f11465ae785682692b3764ac692b4228a Mon Sep 17 00:00:00 2001 From: Yanis Plumit <> Date: Thu, 3 Apr 2025 01:52:35 +0300 Subject: [PATCH 2/4] updated testEncodeSimpleStructs: Codable structures have strong keys order --- Tests/CodableCBOREncoderTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/CodableCBOREncoderTests.swift b/Tests/CodableCBOREncoderTests.swift index eff44a1..f18596c 100644 --- a/Tests/CodableCBOREncoderTests.swift +++ b/Tests/CodableCBOREncoderTests.swift @@ -127,7 +127,6 @@ class CodableCBOREncoderTests: XCTestCase { XCTAssert( encoded == [0xa2, 0x63, 0x61, 0x67, 0x65, 0x18, 0x1b, 0x64, 0x6e, 0x61, 0x6d, 0x65, 0x63, 0x48, 0x61, 0x6d] - || encoded == [0xa2, 0x64, 0x6e, 0x61, 0x6d, 0x65, 0x63, 0x48, 0x61, 0x6d, 0x63, 0x61, 0x67, 0x65, 0x18, 0x1b] ) } } From 9f8c3f0004c3111a3baa05e2d08cd65ec9de18e8 Mon Sep 17 00:00:00 2001 From: Yanis Plumit <> Date: Thu, 3 Apr 2025 11:37:41 +0300 Subject: [PATCH 3/4] fixed keys filtering --- Sources/Encoder/KeyedEncodingContainer.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Encoder/KeyedEncodingContainer.swift b/Sources/Encoder/KeyedEncodingContainer.swift index 6c7f04e..53ec69f 100644 --- a/Sources/Encoder/KeyedEncodingContainer.swift +++ b/Sources/Encoder/KeyedEncodingContainer.swift @@ -39,7 +39,7 @@ extension _CBOREncoder.KeyedContainer: KeyedEncodingContainerProtocol { options: self.options ) let codingKey = anyCodingKeyForKey(key) - self.storage.filter { $0.0 != codingKey } + self.storage = self.storage.filter { $0.0 != codingKey } self.storage.append( (codingKey, container) ) return container } @@ -51,7 +51,7 @@ extension _CBOREncoder.KeyedContainer: KeyedEncodingContainerProtocol { options: self.options ) let codingKey = anyCodingKeyForKey(key) - self.storage.filter { $0.0 != codingKey } + self.storage = self.storage.filter { $0.0 != codingKey } self.storage.append( (codingKey, container) ) return container } @@ -63,7 +63,7 @@ extension _CBOREncoder.KeyedContainer: KeyedEncodingContainerProtocol { options: self.options ) let codingKey = anyCodingKeyForKey(key) - self.storage.filter { $0.0 != codingKey } + self.storage = self.storage.filter { $0.0 != codingKey } self.storage.append( (codingKey, container) ) return KeyedEncodingContainer(container) } From aa10eb11a69d8c84be40256e5ba1d39899d9daa6 Mon Sep 17 00:00:00 2001 From: Yanis Plumit <> Date: Thu, 22 May 2025 23:55:46 +0300 Subject: [PATCH 4/4] throw error instead of fatalError --- Sources/CBORDecoder.swift | 2 +- Sources/Decoder/CodableCBORDecoder.swift | 2 +- Sources/Decoder/KeyedDecodingContainer.swift | 3 +- .../Decoder/UnkeyedDecodingContainer.swift | 28 ++-- Tests/CBORDateTests.swift | 148 ++++++++++++++++++ 5 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 Tests/CBORDateTests.swift diff --git a/Sources/CBORDecoder.swift b/Sources/CBORDecoder.swift index 4e26806..b7fbce5 100644 --- a/Sources/CBORDecoder.swift +++ b/Sources/CBORDecoder.swift @@ -202,7 +202,7 @@ public class CBORDecoder { } } -func getDateFromTimestamp(_ item: CBOR) throws -> Date { +public func getDateFromTimestamp(_ item: CBOR) throws -> Date { switch item { case .double(let d): return Date(timeIntervalSince1970: TimeInterval(d)) diff --git a/Sources/Decoder/CodableCBORDecoder.swift b/Sources/Decoder/CodableCBORDecoder.swift index 4f77d43..6353398 100644 --- a/Sources/Decoder/CodableCBORDecoder.swift +++ b/Sources/Decoder/CodableCBORDecoder.swift @@ -154,7 +154,7 @@ extension _CBORDecoder: Decoder { } } -protocol CBORDecodingContainer: AnyObject { +public protocol CBORDecodingContainer: AnyObject { var codingPath: [CodingKey] { get set } var userInfo: [CodingUserInfoKey : Any] { get } diff --git a/Sources/Decoder/KeyedDecodingContainer.swift b/Sources/Decoder/KeyedDecodingContainer.swift index 6f0f3dc..8184d57 100644 --- a/Sources/Decoder/KeyedDecodingContainer.swift +++ b/Sources/Decoder/KeyedDecodingContainer.swift @@ -49,7 +49,8 @@ extension _CBORDecoder { for _ in 0.. ERROR: \(error)") // FIXME + nestedContainers.append(nil) } - } catch { - fatalError("\(error)") // FIXME } - + self.currentIndex = 0 return nestedContainers @@ -125,7 +126,10 @@ extension _CBORDecoder.UnkeyedContainer: UnkeyedDecodingContainer { try checkCanDecodeValue() defer { self.currentIndex += 1 } - let container = self.nestedContainers[self.currentIndex] + guard let container = self.nestedContainers[self.currentIndex] else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "Failed to decode \(T.self)") + } + let decoder = CodableCBORDecoder() decoder.setOptions(self.options) let value = try decoder.decode(T.self, from: container.data) @@ -219,8 +223,12 @@ extension _CBORDecoder.UnkeyedContainer { } return container case 0xc0: - throw DecodingError.dataCorruptedError(in: self, debugDescription: "Handling text-based date/time is not supported yet") - // Tagged value (epoch-baed date/time) + // Tagged value (epoch-baed date/time) + print(" ERROR: Handling text-based date/time is not supported yet") +// throw DecodingError.dataCorruptedError(in: self, debugDescription: "Handling text-based date/time is not supported yet") + // TODO: handle as c1 yet + length = try getLengthOfItem(format: try self.peekByte(), startIndex: startIndex.advanced(by: 1)) + 1 + case 0xc1: length = try getLengthOfItem(format: try self.peekByte(), startIndex: startIndex.advanced(by: 1)) + 1 case 0xc2...0xdb: diff --git a/Tests/CBORDateTests.swift b/Tests/CBORDateTests.swift new file mode 100644 index 0000000..a6c18d2 --- /dev/null +++ b/Tests/CBORDateTests.swift @@ -0,0 +1,148 @@ +import XCTest +import Foundation +@testable import SwiftCBOR + +class CBORDateTests: XCTestCase { + + struct CustomAndBool: Decodable { + let date: CustomDataDecodable + let bool: Bool + + enum CodingKeys: Int, CodingKey { + case date = 0 + case bool = 3 + } + } + + struct DateAndBool: Codable { + let date: Date + let bool: Bool + + enum CodingKeys: Int, CodingKey { + case date = 0 + case bool = 3 + } + } + + struct BoolOnly: Codable { + let bool: Bool + + enum CodingKeys: Int, CodingKey { + case bool = 3 + } + } + + func testDecodeDate_C1_and_Date() { + let testData = Data(hex: "A2 00C11A682C4A77 03F5" + .replacingOccurrences(of: " ", with: ""))! + let decodedStruct = try! CodableCBORDecoder().decode(DateAndBool.self, from: testData) + XCTAssertNotNil(decodedStruct) + } + + func testDecodeDate_C0_noFatalError() { + let testData = Data(hex: "A4 00C01A682C4A77 01C01A682C4A77 02C01A682C4A77 03F5" + .replacingOccurrences(of: " ", with: ""))! + do { + let _ = try CodableCBORDecoder().decode(DateAndBool.self, from: testData) + XCTFail("Must throw an error instead of fatalError") + } catch { + XCTAssertNotNil(error) + } + } + + func testDecodeDate_C0_and_bool_noFatalErrorForPartialModel() { + let testData = Data(hex: "A4 00C01A682C4A77 01C01A682C4A77 02C01A682C4A77 03F5" + .replacingOccurrences(of: " ", with: ""))! + let decodedStruct = try? CodableCBORDecoder().decode(BoolOnly.self, from: testData) + XCTAssertNotNil(decodedStruct) + } + + func testDecodeDate_C0_custom_epoch() { + // + let testData = Data(hex: "A4 00C01A682C4A7701C 01A682C4A77 02C01A682C4A77 03F5" + .replacingOccurrences(of: " ", with: ""))! + let decodedStruct = try? CodableCBORDecoder().decode(CustomAndBool.self, from: testData) + XCTAssertNotNil(decodedStruct?.date) + XCTAssertNotNil(decodedStruct) + XCTAssertNotNil(decodedStruct?.date.date) + } + + func testDecodeDate_C0_custom_string() { + for formatter in [CustomDataDecodable.dateTimeWithMillisFormatter, CustomDataDecodable.dateTimeFormatter, CustomDataDecodable.onlyDateFormatter] { + let isoDate = formatter.string(from: Date()) + let cbor = CBOR.tagged(CBOR.Tag.standardDateTimeString, .utf8String(isoDate)) + let c0DateString = Data(cbor.encode()).hex + + let testData = Data(hex: "A2 00 \(c0DateString) 03F5" + .replacingOccurrences(of: " ", with: ""))! + + let decodedStruct = try? CodableCBORDecoder().decode(CustomAndBool.self, from: testData) + XCTAssertNotNil(decodedStruct?.date.date) + + // just check that no any fatal error + let wrongDecodedStruct = try? CodableCBORDecoder().decode(DateAndBool.self, from: testData) + XCTAssertNil(wrongDecodedStruct) + } + } +} + +/// it decodes C0 and C1 dates +struct CustomDataDecodable: Decodable { + let date: Date + init(from decoder: Decoder) throws { + let container = (try decoder.singleValueContainer()) as? CBORDecodingContainer + guard let arrayData = container?.data else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "can't cast to `CBORDecodingContainer`")) + } + self.date = try Self.dateFromCBORData(Data(arrayData)) + } + + static func dateFromCBORData(_ data: Data) throws -> Date { + let c0 = 0xC0 + CBOR.Tag.standardDateTimeString.rawValue + let c1 = 0xC0 + CBOR.Tag.epochBasedDateTime.rawValue + if data.count > 1, data[0] == c0 || data[0] == c1 { // 0xC0 or 0xC1 + let timeData = data.suffix(from: 1) + let item = try CBOR.decode([UInt8](timeData)) + + if let item { + if let date = try? getDateFromTimestamp(item) { + return date + } else if case .utf8String(let string) = item { + for formatter in [Self.dateTimeWithMillisFormatter, Self.dateTimeFormatter, Self.onlyDateFormatter] { + if let date = formatter.date(from: string) { + return date + } + } + } + } + } + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "CBOR Data to Date decode failed")) + } +} + +extension CustomDataDecodable { + + // 2017-01-23T10:12:31.484Z + static let dateTimeWithMillisFormatter: ISO8601DateFormatter = { + let v = ISO8601DateFormatter() + v.timeZone = TimeZone(identifier: "UTC") + v.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return v + }() + + // 2021-06-30T19:22:31Z + static let dateTimeFormatter: ISO8601DateFormatter = { + let v = ISO8601DateFormatter() + v.timeZone = TimeZone(identifier: "UTC") + v.formatOptions = [.withInternetDateTime] + return v + }() + + // 2016-06-13 + static let onlyDateFormatter: ISO8601DateFormatter = { + let v = ISO8601DateFormatter() + v.timeZone = TimeZone(identifier: "UTC") + v.formatOptions = .withFullDate + return v + }() +}