From d2de0ba4e6d5c856c8bf76207f82a03b25e7724e Mon Sep 17 00:00:00 2001 From: Hamilton Chapman Date: Tue, 24 May 2022 11:48:29 +0100 Subject: [PATCH] Update the `CBOREncodable` protocol to require that a `toCBOR` function be implemented. The `encode` function can then rely on a default implementation that will call `toCBOR()` and then encode the resulting `CBOR` object. --- Sources/CBOREncodable.swift | 129 +++++++++++++++++++++++++++------ Sources/CBOREncoder.swift | 86 ++++++++++++++++++++++ Tests/CBOREncodableTests.swift | 66 +++++++++++++++++ Tests/CBOREncoderTests.swift | 4 + 4 files changed, 263 insertions(+), 22 deletions(-) create mode 100644 Tests/CBOREncodableTests.swift diff --git a/Sources/CBOREncodable.swift b/Sources/CBOREncodable.swift index cb8b34c..82d0a92 100644 --- a/Sources/CBOREncodable.swift +++ b/Sources/CBOREncodable.swift @@ -3,7 +3,17 @@ import Foundation #endif public protocol CBOREncodable { + /// Optional function that can potentially serve as an opportunity to optimize encoding. func encode(options: CBOROptions) -> [UInt8] + + /// Required function that returns the appropriate `CBOR` variant for a `CBOREncodable`-conforming value. + func toCBOR(options: CBOROptions) -> CBOR +} + +extension CBOREncodable { + func encode(options: CBOROptions) -> [UInt8] { + self.toCBOR(options: options).encode(options: options) + } } extension CBOR: CBOREncodable { @@ -30,45 +40,49 @@ extension CBOR: CBOREncodable { case .break: return CBOR.encodeBreak() } } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return self + } } extension Int: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { - if (self < 0) { - return CBOR.encodeNegativeInt(Int64(self)) - } else { - return CBOR.encodeVarUInt(UInt64(self)) - } + return Int64(self).encode(options: options) + } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return Int64(self).toCBOR(options: options) } } extension Int8: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { - if (self < 0) { - return CBOR.encodeNegativeInt(Int64(self)) - } else { - return CBOR.encodeUInt8(UInt8(self)) - } + return Int64(self).encode(options: options) + } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return Int64(self).toCBOR(options: options) } } extension Int16: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { - if (self < 0) { - return CBOR.encodeNegativeInt(Int64(self)) - } else { - return CBOR.encodeUInt16(UInt16(self)) - } + return Int64(self).encode(options: options) + } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return Int64(self).toCBOR(options: options) } } extension Int32: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { - if (self < 0) { - return CBOR.encodeNegativeInt(Int64(self)) - } else { - return CBOR.encodeUInt32(UInt32(self)) - } + return Int64(self).encode(options: options) + } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return Int64(self).toCBOR(options: options) } } @@ -77,14 +91,26 @@ extension Int64: CBOREncodable { if (self < 0) { return CBOR.encodeNegativeInt(self) } else { - return CBOR.encodeUInt64(UInt64(self)) + return CBOR.encodeVarUInt(UInt64(self)) + } + } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + if self < 0 { + return CBOR.negativeInt(~UInt64(bitPattern: self)) + } else { + return CBOR.unsignedInt(UInt64(self)) } } } extension UInt: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { - return CBOR.encodeVarUInt(UInt64(self)) + return UInt64(self).encode(options: options) + } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return UInt64(self).toCBOR(options: options) } } @@ -92,60 +118,100 @@ extension UInt8: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeUInt8(self) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return UInt64(self).toCBOR(options: options) + } } extension UInt16: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeUInt16(self) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return UInt64(self).toCBOR(options: options) + } } extension UInt32: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeUInt32(self) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return UInt64(self).toCBOR(options: options) + } } extension UInt64: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeUInt64(self) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.unsignedInt(self) + } } extension String: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeString(self, options: options) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.utf8String(self) + } } extension Float: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeFloat(self) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.float(self) + } } extension Double: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeDouble(self) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.double(self) + } } extension Bool: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeBool(self) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.boolean(self) + } } extension Array where Element: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeArray(self, options: options) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.array(self.map { $0.toCBOR(options: options) }) + } } extension Dictionary where Key: CBOREncodable, Value: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeMap(self, options: options) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.map(Dictionary(uniqueKeysWithValues: self.map { ($0.key.toCBOR(options: options), $0.value.toCBOR(options: options)) })) + } } extension Optional where Wrapped: CBOREncodable { @@ -155,12 +221,23 @@ extension Optional where Wrapped: CBOREncodable { case .none: return CBOR.encodeNull() } } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + switch self { + case .some(let wrapped): return wrapped.toCBOR(options: options) + case .none: return CBOR.null + } + } } extension NSNull: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeNull() } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.null + } } #if canImport(Foundation) @@ -168,11 +245,19 @@ extension Date: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeDate(self, options: options) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.date(self) + } } extension Data: CBOREncodable { public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] { return CBOR.encodeByteString(self.map{ $0 }, options: options) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR.byteString(self.map { $0 }) + } } #endif diff --git a/Sources/CBOREncoder.swift b/Sources/CBOREncoder.swift index f3f6780..fff080f 100644 --- a/Sources/CBOREncoder.swift +++ b/Sources/CBOREncoder.swift @@ -297,6 +297,14 @@ extension CBOR { switch any { case is Int: return (any as! Int).encode() + case is Int8: + return (any as! Int8).encode() + case is Int16: + return (any as! Int16).encode() + case is Int32: + return (any as! Int32).encode() + case is Int64: + return (any as! Int64).encode() case is UInt: return (any as! UInt).encode() case is UInt8: @@ -356,6 +364,84 @@ extension CBOR { } } + internal static func cborFromAny(_ any: Any?, options: CBOROptions = CBOROptions()) throws -> CBOR { + switch any { + case is Int: + return cborFromInt64(Int64(any as! Int)) + case is Int8: + return cborFromInt64(Int64(any as! Int8)) + case is Int16: + return cborFromInt64(Int64(any as! Int16)) + case is Int32: + return cborFromInt64(Int64(any as! Int32)) + case is Int64: + return cborFromInt64(any as! Int64) + case is UInt: + return CBOR.unsignedInt(UInt64(any as! UInt)) + case is UInt8: + return CBOR.unsignedInt(UInt64(any as! UInt8)) + case is UInt16: + return CBOR.unsignedInt(UInt64(any as! UInt16)) + case is UInt32: + return CBOR.unsignedInt(UInt64(any as! UInt32)) + case is UInt64: + return CBOR.unsignedInt(any as! UInt64) + case is String: + return CBOR.utf8String(any as! String) + case is Float: + return CBOR.float(any as! Float) + case is Double: + return CBOR.double(any as! Double) + case is Bool: + return CBOR.boolean(any as! Bool) + case is [UInt8]: + return CBOR.byteString(any as! [UInt8]) + #if canImport(Foundation) + case is Data: + return CBOR.byteString((any as! Data).map { $0 }) + case is Date: + return CBOR.date(any as! Date) + case is NSNull: + return CBOR.null + #endif + case is [Any?]: + let anyArr = any as! [Any?] + return try CBOR.array(anyArr.map { try cborFromAny($0) }) + case is [String: Any?]: + let anyMap = any as! [String: Any?] + return try CBOR.map(Dictionary( + uniqueKeysWithValues: anyMap.map { try (cborFromAny($0.key), cborFromAny($0.value)) } + )) + case is Void: + return CBOR.undefined + case nil: + return CBOR.null + default: + if let encodable = any as? CBOREncodable { + return encodable.toCBOR(options: options) + } else if let encodable = any as? Codable { + // This is very much a slow path - we fully encode and then + // decode the value to get it as a `CBOR` + let encoder = CodableCBOREncoder() + encoder.setOptions(options.toCodableEncoderOptions()) + let encoded = try [UInt8](encoder.encode(encodable)) + guard let decoded = try CBOR.decode(encoded) else { + throw CBOREncoderError.invalidType + } + return decoded + } + throw CBOREncoderError.invalidType + } + } + + private static func cborFromInt64(_ i: Int64) -> CBOR { + if i < 0 { + return CBOR.negativeInt(~UInt64(bitPattern: i)) + } else { + return CBOR.unsignedInt(UInt64(i)) + } + } + private static func encodeMap(_ map: [A: Any?], into res: inout [UInt8], options: CBOROptions = CBOROptions()) throws { if options.forbidNonStringMapKeys { try ensureStringKey(A.self) diff --git a/Tests/CBOREncodableTests.swift b/Tests/CBOREncodableTests.swift new file mode 100644 index 0000000..83f7a0d --- /dev/null +++ b/Tests/CBOREncodableTests.swift @@ -0,0 +1,66 @@ +import XCTest +@testable import SwiftCBOR + +class CBOREncodableTests: XCTestCase { + func testToCBOR() { + XCTAssertEqual(CBOR.unsignedInt(0), 0.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(1), 1.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(20), 20.toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.unsignedInt(UInt64(Int8.max)), Int8.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(127, Int8.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(UInt64(Int16.max)), Int16.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(32_767, Int16.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(UInt64(Int32.max)), Int32.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(2_147_483_647, Int32.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(UInt64(Int64.max)), Int64.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(9_223_372_036_854_775_807, Int64.max.toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.unsignedInt(UInt64(UInt8.max)), UInt8.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(255, UInt8.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(UInt64(UInt16.max)), UInt16.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(65_535, UInt16.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(UInt64(UInt32.max)), UInt32.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(4_294_967_295, UInt32.max.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.unsignedInt(UInt64.max), UInt64.max.toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int8.min))), Int8.min.toCBOR(options: CBOROptions())) + XCTAssertEqual(-128, Int8.min.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int16.min))), Int16.min.toCBOR(options: CBOROptions())) + XCTAssertEqual(-32_768, Int16.min.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int32.min))), Int32.min.toCBOR(options: CBOROptions())) + XCTAssertEqual(-2_147_483_648, Int32.min.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int64.min))), Int64.min.toCBOR(options: CBOROptions())) + XCTAssertEqual(-9_223_372_036_854_775_808, Int64.min.toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.double(Double.greatestFiniteMagnitude), Double.greatestFiniteMagnitude.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.double(Double.leastNonzeroMagnitude), Double.leastNonzeroMagnitude.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.double(Double.pi), Double.pi.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.double(0.123456789), 0.123456789.toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.float(Float.greatestFiniteMagnitude), Float.greatestFiniteMagnitude.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.float(Float.leastNonzeroMagnitude), Float.leastNonzeroMagnitude.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.float(Float.pi), Float.pi.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.float(0.123456789), Float(0.123456789).toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.boolean(true), true.toCBOR(options: CBOROptions())) + XCTAssertEqual(true, true.toCBOR(options: CBOROptions())) + XCTAssertEqual(CBOR.boolean(false), false.toCBOR(options: CBOROptions())) + XCTAssertEqual(false, false.toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.utf8String("test"), "test".toCBOR(options: CBOROptions())) + XCTAssertEqual("test", "test".toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.null, Optional.none.toCBOR(options: CBOROptions())) + XCTAssertEqual(nil, Optional.none.toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.byteString([1, 2, 3]), Data([1, 2, 3]).toCBOR(options: CBOROptions())) + + XCTAssertEqual(CBOR.array([CBOR.unsignedInt(1), CBOR.unsignedInt(2)]), [1, 2].toCBOR(options: CBOROptions())) + + XCTAssertEqual( + CBOR.map([CBOR.utf8String("a"): CBOR.unsignedInt(1), CBOR.utf8String("b"): CBOR.unsignedInt(2)]), + ["a": 1, "b": 2].toCBOR(options: CBOROptions()) + ) + } +} diff --git a/Tests/CBOREncoderTests.swift b/Tests/CBOREncoderTests.swift index e80aaa8..5d07cd9 100644 --- a/Tests/CBOREncoderTests.swift +++ b/Tests/CBOREncoderTests.swift @@ -250,6 +250,10 @@ class CBOREncoderTests: XCTestCase { ] return cborWrapper.encode(options: options) } + + public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR { + return CBOR(dictionaryLiteral:("x", CBOR(integerLiteral: self.x)), ("y", .utf8String(self.y))) + } } let encoded = MyStruct(x: 42, y: "words").encode()