diff --git a/Sources/Decoder.swift b/Sources/Decoder.swift index 86032b2..926ee12 100644 --- a/Sources/Decoder.swift +++ b/Sources/Decoder.swift @@ -20,13 +20,13 @@ public struct TLVDecoder { public var log: ((String) -> ())? /// Format for numeric values. - public var numericFormat: TLVNumericFormat = .littleEndian + public var numericFormatting: TLVNumericFormatting = .default /// Format for UUID values. - public var uuidFormat: TLVUUIDFormat = .bytes + public var uuidFormatting: TLVUUIDFormatting = .default /// Format for Date values. - public var dateFormat: TLVDateFormat = .secondsSince1970 + public var dateFormatting: TLVDateFormatting = .default // MARK: - Initialization @@ -41,9 +41,9 @@ public struct TLVDecoder { let items = try decode(data) let options = Decoder.Options( - numericFormat: numericFormat, - uuidFormat: uuidFormat, - dateFormat: dateFormat + numericFormatting: numericFormatting, + uuidFormatting: uuidFormatting, + dateFormatting: dateFormatting ) let decoder = Decoder(referencing: .items(items), @@ -110,6 +110,39 @@ public struct TLVDecoder { } } +// MARK: - Deprecated + +public extension TLVDecoder { + + @available(*, deprecated, message: "Renamed to numericFormatting") + var numericFormat: TLVNumericFormat { + get { return numericFormatting } + set { numericFormatting = newValue } + } + + @available(*, deprecated, message: "Renamed to uuidFormatting") + var uuidFormat: TLVUUIDFormat { + get { return uuidFormatting } + set { uuidFormatting = newValue } + } + + @available(*, deprecated, message: "Renamed to dateFormatting") + var dateFormat: TLVDateFormat { + get { return dateFormatting } + set { dateFormatting = newValue } + } +} + +// MARK: - Combine + +#if canImport(Combine) +import Combine + +extension TLVDecoder: TopLevelDecoder { } +#endif + +// MARK: - Decoder + internal extension TLVDecoder { final class Decoder: Swift.Decoder { @@ -206,7 +239,14 @@ internal extension TLVDecoder { internal extension TLVDecoder.Decoder { - typealias Options = TLVOptions + struct Options { + + let numericFormatting: TLVNumericFormatting + + let uuidFormatting: TLVUUIDFormatting + + let dateFormatting: TLVDateFormatting + } } // MARK: - Coding Key @@ -243,7 +283,7 @@ internal extension TLVDecoder.Decoder { func unboxNumeric (_ data: Data, as type: T.Type) throws -> T { var numericValue = try unbox(data, as: type) - switch options.numericFormat { + switch options.numericFormatting { case .bigEndian: numericValue = T.init(bigEndian: numericValue) case .littleEndian: @@ -291,7 +331,7 @@ private extension TLVDecoder.Decoder { func unboxUUID(_ data: Data) throws -> UUID { - switch options.uuidFormat { + switch options.uuidFormatting { case .bytes: guard data.count == MemoryLayout.size else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Invalud number of bytes (\(data.count)) for UUID")) @@ -310,7 +350,7 @@ private extension TLVDecoder.Decoder { func unboxDate(_ data: Data) throws -> Date { - switch options.dateFormat { + switch options.dateFormatting { case .secondsSince1970: let timeInterval = try unboxDouble(data) return Date(timeIntervalSince1970: timeInterval) @@ -320,7 +360,7 @@ private extension TLVDecoder.Decoder { case .iso8601: guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } - return try unboxDate(data, using: TLVDateFormat.iso8601Formatter) + return try unboxDate(data, using: TLVDateFormatting.iso8601Formatter) case let .formatted(formatter): return try unboxDate(data, using: formatter) } diff --git a/Sources/Encoder.swift b/Sources/Encoder.swift index e3c0062..874a4ac 100644 --- a/Sources/Encoder.swift +++ b/Sources/Encoder.swift @@ -20,13 +20,16 @@ public struct TLVEncoder { public var log: ((String) -> ())? /// Format for numeric values. - public var numericFormat: TLVNumericFormat = .littleEndian + public var outputFormatting: TLVOutputFormatting = .default + + /// Format for numeric values. + public var numericFormatting: TLVNumericFormatting = .default /// Format for UUID values. - public var uuidFormat: TLVUUIDFormat = .bytes + public var uuidFormatting: TLVUUIDFormatting = .default /// Format for Date values. - public var dateFormat: TLVDateFormat = .secondsSince1970 + public var dateFormatting: TLVDateFormatting = .default // MARK: - Initialization @@ -39,10 +42,12 @@ public struct TLVEncoder { log?("Will encode \(String(reflecting: T.self))") let options = Encoder.Options( - numericFormat: numericFormat, - uuidFormat: uuidFormat, - dateFormat: dateFormat + outputFormatting: outputFormatting, + numericFormatting: numericFormatting, + uuidFormatting: uuidFormatting, + dateFormatting: dateFormatting ) + let encoder = Encoder(userInfo: userInfo, log: log, options: options) try value.encode(to: encoder) assert(encoder.stack.containers.count == 1) @@ -63,6 +68,39 @@ public struct TLVEncoder { } } +// MARK: - Deprecated + +public extension TLVEncoder { + + @available(*, deprecated, message: "Renamed to numericFormatting") + var numericFormat: TLVNumericFormat { + get { return numericFormatting } + set { numericFormatting = newValue } + } + + @available(*, deprecated, message: "Renamed to uuidFormatting") + var uuidFormat: TLVUUIDFormat { + get { return uuidFormatting } + set { uuidFormatting = newValue } + } + + @available(*, deprecated, message: "Renamed to dateFormatting") + var dateFormat: TLVDateFormat { + get { return dateFormatting } + set { dateFormatting = newValue } + } +} + +// MARK: - Combine + +#if canImport(Combine) +import Combine + +extension TLVEncoder: TopLevelEncoder { } +#endif + +// MARK: - Encoder + internal extension TLVEncoder { final class Encoder: Swift.Encoder { @@ -134,7 +172,16 @@ internal extension TLVEncoder { internal extension TLVEncoder.Encoder { - typealias Options = TLVOptions + struct Options { + + public let outputFormatting: TLVOutputFormatting + + public let numericFormatting: TLVNumericFormatting + + public let uuidFormatting: TLVUUIDFormatting + + public let dateFormatting: TLVDateFormatting + } } internal extension TLVEncoder.Encoder { @@ -163,7 +210,7 @@ internal extension TLVEncoder.Encoder { func boxNumeric (_ value: T) -> Data { let numericValue: T - switch options.numericFormat { + switch options.numericFormatting { case .bigEndian: numericValue = value.bigEndian case .littleEndian: @@ -205,7 +252,7 @@ private extension TLVEncoder.Encoder { func boxUUID(_ uuid: UUID) -> Data { - switch options.uuidFormat { + switch options.uuidFormatting { case .bytes: return Data(uuid) case .string: @@ -215,7 +262,7 @@ private extension TLVEncoder.Encoder { func boxDate(_ date: Date) -> Data { - switch options.dateFormat { + switch options.dateFormatting { case .secondsSince1970: return boxDouble(date.timeIntervalSince1970) case .millisecondsSince1970: @@ -223,7 +270,7 @@ private extension TLVEncoder.Encoder { case .iso8601: guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } - return boxDate(date, using: TLVDateFormat.iso8601Formatter) + return boxDate(date, using: TLVDateFormatting.iso8601Formatter) case let .formatted(formatter): return boxDate(date, using: formatter) } @@ -280,13 +327,26 @@ internal extension TLVEncoder.Encoder { final class ItemsContainer { - var items = [TLVItem]() + private(set) var items = [TLVItem]() init() { } var data: Data { return Data(items) } + + @inline(__always) + func append(_ item: TLVItem, options: Options) { + items.append(item) + if options.outputFormatting.sortedKeys { + items.sort(by: { $0.type.rawValue < $1.type.rawValue }) + } + } + + @inline(__always) + fileprivate func append(_ item: TLVItem) { + items.append(item) + } } final class ItemContainer { @@ -457,7 +517,7 @@ internal final class TLVKeyedContainer : KeyedEncodingContainerP let type = try encoder.typeCode(for: key, value: value) let item = TLVItem(type: type, value: data) - self.container.items.append(item) + self.container.append(item, options: encoder.options) } } @@ -624,7 +684,7 @@ internal final class TLVUnkeyedEncodingContainer: UnkeyedEncodingContainer { let item = TLVItem(type: index, value: data) // write - self.container.items.append(item) + self.container.append(item) // already sorted } } diff --git a/Sources/Format.swift b/Sources/Format.swift index 21fefd5..e94d19f 100644 --- a/Sources/Format.swift +++ b/Sources/Format.swift @@ -8,22 +8,59 @@ import Foundation -/// TLV Numeric Encoding Format -public enum TLVNumericFormat: Equatable, Hashable { +/// The output formatting options that determine the readability, size, and element order of an encoded TLV object. +public struct TLVOutputFormatting: Equatable, Hashable { + + /// The output formatting option that sorts keys in numerical order. + public var sortedKeys: Bool +} + +public extension TLVOutputFormatting { + + /// The default TLV output formatting options. + static var `default`: TLVOutputFormatting { + return .init(sortedKeys: true) + } +} + +/// TLV number formatting (endianness). +public enum TLVNumericFormatting: Equatable, Hashable { - case bigEndian case littleEndian + case bigEndian } -/// TLV UUID Encoding Format -public enum TLVUUIDFormat: Equatable, Hashable { +public extension TLVNumericFormatting { + + /// The default TLV endianness for binary representation of numbers. + static var `default`: TLVNumericFormatting { + return .littleEndian + } +} + +@available(*, deprecated, message: "Renamed to TLVNumericFormatting") +public typealias TLVNumericFormat = TLVNumericFormatting + +/// TLV `UUID` Encoding Format +public enum TLVUUIDFormatting: Equatable, Hashable { case bytes case string } -/// TLV Date Encoding Format -public enum TLVDateFormat: Equatable { +public extension TLVUUIDFormatting { + + /// The default TLV `UUID` format. + static var `default`: TLVUUIDFormatting { + return .bytes + } +} + +@available(*, deprecated, message: "Renamed to TLVUUIDFormatting") +public typealias TLVUUIDFormat = TLVUUIDFormatting + +/// TLV `Date` Encoding Format +public enum TLVDateFormatting: Equatable { /// Encodes dates in terms of seconds since midnight UTC on January 1, 1970. case secondsSince1970 @@ -39,18 +76,20 @@ public enum TLVDateFormat: Equatable { case formatted(DateFormatter) } -internal struct TLVOptions { - - let numericFormat: TLVNumericFormat +public extension TLVDateFormatting { - let uuidFormat: TLVUUIDFormat - - let dateFormat: TLVDateFormat + /// The default TLV `Date` format. + static var `default`: TLVDateFormatting { + return .secondsSince1970 + } } +@available(*, deprecated, message: "Renamed to TLVDateFormatting") +public typealias TLVDateFormat = TLVDateFormatting + // MARK: - Formatters -internal extension TLVDateFormat { +internal extension TLVDateFormatting { @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) static let iso8601Formatter: ISO8601DateFormatter = { diff --git a/Sources/Item.swift b/Sources/Item.swift index 02f6ddf..6290a1b 100644 --- a/Sources/Item.swift +++ b/Sources/Item.swift @@ -13,12 +13,15 @@ import Foundation */ public struct TLVItem: Equatable, Hashable { + /// TLV code public var type: TLVTypeCode + /// TLV data payload public var value: Data public init(type: TLVTypeCode, value: Data) { + assert(value.count <= UInt8.max) self.type = type self.value = value } @@ -27,20 +30,13 @@ public struct TLVItem: Equatable, Hashable { public extension TLVItem { var length: UInt8 { - return UInt8(value.count) } } public extension TLVItem { - init?(data: Data) { - - fatalError() - } - var data: Data { - return Data(self) } } diff --git a/Sources/TLVCodable.swift b/Sources/TLVCodable.swift index b8290b8..aa0f982 100644 --- a/Sources/TLVCodable.swift +++ b/Sources/TLVCodable.swift @@ -17,6 +17,7 @@ public protocol TLVDecodable: Decodable { init?(tlvData: Data) } +/// TLV Encodable type public protocol TLVEncodable: Encodable { var tlvData: Data { get } diff --git a/Tests/TLVCodingTests/TLVCodingTests.swift b/Tests/TLVCodingTests/TLVCodingTests.swift index a3b9804..79c95e4 100644 --- a/Tests/TLVCodingTests/TLVCodingTests.swift +++ b/Tests/TLVCodingTests/TLVCodingTests.swift @@ -17,7 +17,8 @@ final class TLVCodingTests: XCTestCase { ("testCodingKeys", testCodingKeys), ("testUUID", testUUID), ("testDate", testDate), - ("testDateSecondsSince1970", testDateSecondsSince1970) + ("testDateSecondsSince1970", testDateSecondsSince1970), + ("testOutputFormatting", testOutputFormatting) ] func testCodable() { @@ -210,7 +211,7 @@ final class TLVCodingTests: XCTestCase { func testUUID() { - let formats: [TLVUUIDFormat] = [.bytes, .string] + let formats: [TLVUUIDFormatting] = [.bytes, .string] for format in formats { @@ -223,7 +224,7 @@ final class TLVCodingTests: XCTestCase { var encodedData = Data() var encoder = TLVEncoder() - encoder.uuidFormat = format + encoder.uuidFormatting = format encoder.log = { print("Encoder:", $0) } do { encodedData = try encoder.encode(value) @@ -234,7 +235,7 @@ final class TLVCodingTests: XCTestCase { } var decoder = TLVDecoder() - decoder.uuidFormat = format + decoder.uuidFormatting = format decoder.log = { print("Decoder:", $0) } do { let decodedValue = try decoder.decode(CustomEncodable.self, from: encodedData) @@ -254,7 +255,7 @@ final class TLVCodingTests: XCTestCase { dateFormatter.calendar = Calendar(identifier: .gregorian) dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - var formats: [TLVDateFormat] = [.secondsSince1970, .millisecondsSince1970, .formatted(dateFormatter)] + var formats: [TLVDateFormatting] = [.secondsSince1970, .millisecondsSince1970, .formatted(dateFormatter)] if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { formats.append(.iso8601) @@ -273,7 +274,7 @@ final class TLVCodingTests: XCTestCase { var encodedData = Data() var encoder = TLVEncoder() - encoder.dateFormat = format + encoder.dateFormatting = format encoder.log = { print("Encoder:", $0) } do { encodedData = try encoder.encode(value) @@ -284,7 +285,7 @@ final class TLVCodingTests: XCTestCase { } var decoder = TLVDecoder() - decoder.dateFormat = format + decoder.dateFormatting = format decoder.log = { print("Decoder:", $0) } do { let decodedValue = try decoder.decode(CustomEncodable.self, from: encodedData) @@ -313,10 +314,33 @@ final class TLVCodingTests: XCTestCase { ) var encoder = TLVEncoder() - encoder.dateFormat = .secondsSince1970 + encoder.dateFormatting = .secondsSince1970 encoder.log = { print("Encoder:", $0) } XCTAssertEqual(try encoder.encode(value), try encoder.encode(rawValue)) } + + func testOutputFormatting() { + + var encoder = TLVEncoder() + encoder.outputFormatting.sortedKeys = true + encoder.log = { print("Encoder:", $0) } + + let value = ProvisioningState( + state: .provisioning, + result: .success + ) + + let valueUnordered = ProvisioningStateUnordered( + result: value.result, + state: value.state + ) + + XCTAssertEqual(try encoder.encode(value), try encoder.encode(valueUnordered)) + + encoder.outputFormatting.sortedKeys = false + + XCTAssertNotEqual(try encoder.encode(value), try encoder.encode(valueUnordered)) + } } // MARK: - Supporting Types @@ -384,6 +408,21 @@ extension ProvisioningState.CodingKeys { } } +public struct ProvisioningStateUnordered: Codable, Equatable { + + typealias CodingKeys = ProvisioningState.CodingKeys + + public var result: ProvisioningState.Result + public var state: ProvisioningState.State + + public func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(result, forKey: .result) + try container.encode(state, forKey: .state) + } +} + public struct Profile: Codable, Equatable { public var person: Person