diff --git a/Sources/Decoder.swift b/Sources/Decoder.swift index c6936e7..3fba4b6 100644 --- a/Sources/Decoder.swift +++ b/Sources/Decoder.swift @@ -25,6 +25,9 @@ public struct TLVDecoder { /// Format for UUID values. public var uuidFormat: TLVUUIDFormat = .bytes + /// Format for Date values. + public var dateFormat: TLVDateFormat = .secondsSince1970 + // MARK: - Initialization public init() { } @@ -39,7 +42,8 @@ public struct TLVDecoder { let options = Decoder.Options( numericFormat: numericFormat, - uuidFormat: uuidFormat + uuidFormat: uuidFormat, + dateFormat: dateFormat ) let decoder = Decoder(referencing: .items(items), @@ -226,7 +230,7 @@ internal extension TLVDecoder.Decoder { internal extension TLVDecoder.Decoder { - func unbox (_ data: Data, as type: T.Type) throws -> T { + func unbox (_ data: Data, as type: T.Type) throws -> T { guard let value = T.init(tlvData: data) else { @@ -236,7 +240,7 @@ internal extension TLVDecoder.Decoder { return value } - func unboxNumeric (_ data: Data, as type: T.Type) throws -> T { + func unboxNumeric (_ data: Data, as type: T.Type) throws -> T { var numericValue = try unbox(data, as: type) switch options.numericFormat { @@ -248,6 +252,16 @@ internal extension TLVDecoder.Decoder { return numericValue } + func unboxDouble(_ data: Data) throws -> Double { + let bitPattern = try unboxNumeric(data, as: UInt64.self) + return Double(bitPattern: bitPattern) + } + + func unboxFloat(_ data: Data) throws -> Float { + let bitPattern = try unboxNumeric(data, as: UInt32.self) + return Float(bitPattern: bitPattern) + } + /// Attempt to decode native value to expected type. func unboxDecodable (_ item: TLVItem, as type: T.Type) throws -> T { @@ -256,6 +270,8 @@ internal extension TLVDecoder.Decoder { return item.value as! T // In this case T is Data } else if type == UUID.self { return try unboxUUID(item.value) as! T + } else if type == Date.self { + return try unboxDate(item.value) as! T } else if let tlvCodable = type as? TLVCodable.Type { guard let value = tlvCodable.init(tlvData: item.value) else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Invalid data for \(String(reflecting: type))")) @@ -269,8 +285,11 @@ internal extension TLVDecoder.Decoder { return decoded } } +} + +private extension TLVDecoder.Decoder { - private func unboxUUID(_ data: Data) throws -> UUID { + func unboxUUID(_ data: Data) throws -> UUID { switch options.uuidFormat { case .bytes: @@ -288,6 +307,33 @@ internal extension TLVDecoder.Decoder { return uuid } } + + func unboxDate(_ data: Data) throws -> Date { + + switch options.dateFormat { + case .secondsSince1970: + let timeInterval = try unboxDouble(data) + return Date(timeIntervalSince1970: timeInterval) + case .millisecondsSince1970: + let timeInterval = try unboxDouble(data) + return Date(timeIntervalSince1970: timeInterval / 1000) + case .iso8601: + guard #available(OSX 10.12, *) + else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } + return try unboxDate(data, using: TLVDateFormat.iso8601Formatter) + case let .formatted(formatter): + return try unboxDate(data, using: formatter) + } + } + + @inline(__always) + func unboxDate (_ data: Data, using formatter: T) throws -> Date { + let string = try unbox(data, as: String.self) + guard let date = formatter.date(from: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Invalid Date string \(string)")) + } + return date + } } // MARK: - Stack @@ -489,12 +535,12 @@ internal struct TLVKeyedDecodingContainer : KeyedDecodingContaine // MARK: Private Methods /// Decode native value type from TLV data. - private func decodeTLV (_ type: T.Type, forKey key: Key) throws -> T { + private func decodeTLV (_ type: T.Type, forKey key: Key) throws -> T { return try self.value(for: key, type: type) { try decoder.unbox($0.value, as: type) } } - private func decodeNumeric (_ type: T.Type, forKey key: Key) throws -> T { + private func decodeNumeric (_ type: T.Type, forKey key: Key) throws -> T { return try self.value(for: key, type: type) { try decoder.unboxNumeric($0.value, as: type) } } @@ -788,7 +834,13 @@ internal extension TLVUnkeyedDecodingContainer { // MARK: - Decodable Types -extension String: TLVDecodable { +/// Private protocol for decoding TLV values into raw data. +internal protocol TLVRawDecodable { + + init?(tlvData data: Data) +} + +extension String: TLVRawDecodable { public init?(tlvData data: Data) { @@ -796,7 +848,7 @@ extension String: TLVDecodable { } } -extension Bool: TLVDecodable { +extension Bool: TLVRawDecodable { public init?(tlvData data: Data) { @@ -807,7 +859,7 @@ extension Bool: TLVDecodable { } } -extension UInt8: TLVDecodable { +extension UInt8: TLVRawDecodable { public init?(tlvData data: Data) { @@ -818,7 +870,7 @@ extension UInt8: TLVDecodable { } } -extension UInt16: TLVDecodable { +extension UInt16: TLVRawDecodable { public init?(tlvData data: Data) { @@ -829,7 +881,7 @@ extension UInt16: TLVDecodable { } } -extension UInt32: TLVDecodable { +extension UInt32: TLVRawDecodable { public init?(tlvData data: Data) { @@ -840,7 +892,7 @@ extension UInt32: TLVDecodable { } } -extension UInt64: TLVDecodable { +extension UInt64: TLVRawDecodable { public init?(tlvData data: Data) { @@ -851,7 +903,7 @@ extension UInt64: TLVDecodable { } } -extension Int8: TLVDecodable { +extension Int8: TLVRawDecodable { public init?(tlvData data: Data) { @@ -862,7 +914,7 @@ extension Int8: TLVDecodable { } } -extension Int16: TLVDecodable { +extension Int16: TLVRawDecodable { public init?(tlvData data: Data) { @@ -873,7 +925,7 @@ extension Int16: TLVDecodable { } } -extension Int32: TLVDecodable { +extension Int32: TLVRawDecodable { public init?(tlvData data: Data) { @@ -884,7 +936,7 @@ extension Int32: TLVDecodable { } } -extension Int64: TLVDecodable { +extension Int64: TLVRawDecodable { public init?(tlvData data: Data) { diff --git a/Sources/Encoder.swift b/Sources/Encoder.swift index e017515..cc6c902 100644 --- a/Sources/Encoder.swift +++ b/Sources/Encoder.swift @@ -25,6 +25,9 @@ public struct TLVEncoder { /// Format for UUID values. public var uuidFormat: TLVUUIDFormat = .bytes + /// Format for Date values. + public var dateFormat: TLVDateFormat = .secondsSince1970 + // MARK: - Initialization public init() { } @@ -37,7 +40,8 @@ public struct TLVEncoder { let options = Encoder.Options( numericFormat: numericFormat, - uuidFormat: uuidFormat + uuidFormat: uuidFormat, + dateFormat: dateFormat ) let encoder = Encoder(userInfo: userInfo, log: log, options: options) try value.encode(to: encoder) @@ -151,12 +155,12 @@ internal extension TLVEncoder.Encoder { internal extension TLVEncoder.Encoder { @inline(__always) - func box (_ value: T) -> Data { + func box (_ value: T) -> Data { return value.tlvData } @inline(__always) - func boxNumeric (_ value: T) -> Data { + func boxNumeric (_ value: T) -> Data { let numericValue: T switch options.numericFormat { @@ -168,12 +172,24 @@ internal extension TLVEncoder.Encoder { return box(numericValue) } + @inline(__always) + func boxDouble(_ double: Double) -> Data { + return boxNumeric(double.bitPattern) + } + + @inline(__always) + func boxFloat(_ float: Float) -> Data { + return boxNumeric(float.bitPattern) + } + func boxEncodable (_ value: T) throws -> Data { if let data = value as? Data { return data } else if let uuid = value as? UUID { return boxUUID(uuid) + } else if let date = value as? Date { + return boxDate(date) } else if let tlvEncodable = value as? TLVEncodable { return tlvEncodable.tlvData } else { @@ -183,8 +199,12 @@ internal extension TLVEncoder.Encoder { return nestedContainer.data } } +} + +private extension TLVEncoder.Encoder { - private func boxUUID(_ uuid: UUID) -> Data { + func boxUUID(_ uuid: UUID) -> Data { + switch options.uuidFormat { case .bytes: return Data(uuid) @@ -192,6 +212,26 @@ internal extension TLVEncoder.Encoder { return uuid.uuidString.tlvData } } + + func boxDate(_ date: Date) -> Data { + + switch options.dateFormat { + case .secondsSince1970: + return boxDouble(date.timeIntervalSince1970) + case .millisecondsSince1970: + return boxDouble(date.timeIntervalSince1970 * 1000) + case .iso8601: + guard #available(OSX 10.12, *) + else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } + return boxDate(date, using: TLVDateFormat.iso8601Formatter) + case let .formatted(formatter): + return boxDate(date, using: formatter) + } + } + + func boxDate (_ date: Date, using formatter: T) -> Data { + return box(formatter.string(from: date)) + } } // MARK: - Stack @@ -395,7 +435,7 @@ internal final class TLVKeyedContainer : KeyedEncodingContainerP // MARK: - Private Methods - private func encodeNumeric (_ value: T, forKey key: K) throws { + private func encodeNumeric (_ value: T, forKey key: K) throws { self.encoder.codingPath.append(key) defer { self.encoder.codingPath.removeLast() } @@ -403,7 +443,7 @@ internal final class TLVKeyedContainer : KeyedEncodingContainerP try setValue(value, data: data, for: key) } - private func encodeTLV (_ value: T, forKey key: K) throws { + private func encodeTLV (_ value: T, forKey key: K) throws { self.encoder.codingPath.append(key) defer { self.encoder.codingPath.removeLast() } @@ -459,9 +499,9 @@ internal final class TLVSingleValueEncodingContainer: SingleValueEncodingContain func encode(_ value: String) throws { write(encoder.box(value)) } - func encode(_ value: Double) throws { write(encoder.boxNumeric(value.bitPattern)) } + func encode(_ value: Double) throws { write(encoder.boxDouble(value)) } - func encode(_ value: Float) throws { write(encoder.boxNumeric(value.bitPattern)) } + func encode(_ value: Float) throws { write(encoder.boxFloat(value)) } func encode(_ value: Int) throws { write(encoder.boxNumeric(Int32(value))) } @@ -590,92 +630,97 @@ internal final class TLVUnkeyedEncodingContainer: UnkeyedEncodingContainer { // MARK: - Data Types -private extension TLVEncodable { +/// Private protocol for encoding TLV values into raw data. +internal protocol TLVRawEncodable { + + var tlvData: Data { get } +} + +private extension TLVRawEncodable { var copyingBytes: Data { - return withUnsafePointer(to: self, { Data(bytes: $0, count: MemoryLayout.size) }) } } -extension UInt8: TLVEncodable { +extension UInt8: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension UInt16: TLVEncodable { +extension UInt16: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension UInt32: TLVEncodable { +extension UInt32: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension UInt64: TLVEncodable { +extension UInt64: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension Int8: TLVEncodable { +extension Int8: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension Int16: TLVEncodable { +extension Int16: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension Int32: TLVEncodable { +extension Int32: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension Int64: TLVEncodable { +extension Int64: TLVRawEncodable { public var tlvData: Data { return copyingBytes } } -extension Float: TLVEncodable { +extension Float: TLVRawEncodable { public var tlvData: Data { return bitPattern.copyingBytes } } -extension Double: TLVEncodable { +extension Double: TLVRawEncodable { public var tlvData: Data { return bitPattern.copyingBytes } } -extension Bool: TLVEncodable { +extension Bool: TLVRawEncodable { public var tlvData: Data { return UInt8(self ? 1 : 0).copyingBytes } } -extension String: TLVEncodable { +extension String: TLVRawEncodable { public var tlvData: Data { return Data(self.utf8) diff --git a/Sources/Format.swift b/Sources/Format.swift index 5b68fbc..21fefd5 100644 --- a/Sources/Format.swift +++ b/Sources/Format.swift @@ -6,6 +6,8 @@ // Copyright © 2019 PureSwift. All rights reserved. // +import Foundation + /// TLV Numeric Encoding Format public enum TLVNumericFormat: Equatable, Hashable { @@ -20,9 +22,52 @@ public enum TLVUUIDFormat: Equatable, Hashable { case string } +/// TLV Date Encoding Format +public enum TLVDateFormat: Equatable { + + /// Encodes dates in terms of seconds since midnight UTC on January 1, 1970. + case secondsSince1970 + + /// Encodes dates in terms of milliseconds since midnight UTC on January 1, 1970. + case millisecondsSince1970 + + /// Formats dates according to the ISO 8601 and RFC 3339 standards. + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 + + /// Defers formatting settings to a supplied date formatter. + case formatted(DateFormatter) +} + internal struct TLVOptions { let numericFormat: TLVNumericFormat let uuidFormat: TLVUUIDFormat + + let dateFormat: TLVDateFormat +} + +// MARK: - Formatters + +internal extension TLVDateFormat { + + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + static let iso8601Formatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = .withInternetDateTime + return formatter + }() } + +internal protocol DateFormatterProtocol: class { + + func string(from date: Date) -> String + + func date(from string: String) -> Date? +} + +extension DateFormatter: DateFormatterProtocol { } + +@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) +extension ISO8601DateFormatter: DateFormatterProtocol { } diff --git a/Sources/TLVCodable.swift b/Sources/TLVCodable.swift index 6a9d1ce..b8290b8 100644 --- a/Sources/TLVCodable.swift +++ b/Sources/TLVCodable.swift @@ -12,12 +12,12 @@ import Foundation public typealias TLVCodable = TLVEncodable & TLVDecodable /// TLV Decodable type -public protocol TLVDecodable { +public protocol TLVDecodable: Decodable { init?(tlvData: Data) } -public protocol TLVEncodable { +public protocol TLVEncodable: Encodable { var tlvData: Data { get } } diff --git a/Tests/TLVCodingTests/TLVCodingTests.swift b/Tests/TLVCodingTests/TLVCodingTests.swift index 57b120f..a3b9804 100644 --- a/Tests/TLVCodingTests/TLVCodingTests.swift +++ b/Tests/TLVCodingTests/TLVCodingTests.swift @@ -12,10 +12,12 @@ import XCTest final class TLVCodingTests: XCTestCase { - static var allTests = [ + static let allTests = [ ("testCodable", testCodable), ("testCodingKeys", testCodingKeys), - ("testUUID", testUUID) + ("testUUID", testUUID), + ("testDate", testDate), + ("testDateSecondsSince1970", testDateSecondsSince1970) ] func testCodable() { @@ -190,8 +192,7 @@ final class TLVCodingTests: XCTestCase { Data([ 0, 32, 0, 1, 0, 1, 27, 0, 16, 184, 61, 214, 244, 164, 41, 65, 179, 148, 90, 62, 14, 229, 145, 92, 161, 1, 7, 86, 97, 108, 117, 101, 32, 49, 1, 32, 0, 1, 0, 1, 27, 0, 16, 184, 61, 214, 244, 164, 41, 65, 179, 148, 90, 62, 14, 229, 145, 92, 162, 1, 7, 86, 97, 108, 117, 101, 32, 50, - 2, 50, 0, 1, 1, 1, 45, 0, 16, 184, 61, 214, 244, 164, 41, 65, 179, 148, 90, 62, 14, 229, 145, 92, 163, 1, 15, 80, 101, 110, 100, 105, 110, 103, 32, 86, 97, 108, 117, 101, 32, 49, 2, 8, 0, 0, 0, 127, 195, 99, 45, 66 - ]) + 2, 50, 0, 1, 1, 1, 45, 0, 16, 184, 61, 214, 244, 164, 41, 65, 179, 148, 90, 62, 14, 229, 145, 92, 163, 1, 15, 80, 101, 110, 100, 105, 110, 103, 32, 86, 97, 108, 117, 101, 32, 49, 2, 8, 0, 0, 0, 16, 99, 216, 45, 66]) ) } @@ -216,7 +217,8 @@ final class TLVCodingTests: XCTestCase { let value = CustomEncodable( data: nil, uuid: UUID(), - number: nil + number: nil, + date: nil ) var encodedData = Data() @@ -243,6 +245,78 @@ final class TLVCodingTests: XCTestCase { } } } + + func testDate() { + + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + dateFormatter.calendar = Calendar(identifier: .gregorian) + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + var formats: [TLVDateFormat] = [.secondsSince1970, .millisecondsSince1970, .formatted(dateFormatter)] + + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + formats.append(.iso8601) + } + + let date = Date(timeIntervalSince1970: 60 * 60 * 24 * 365) + + for format in formats { + + let value = CustomEncodable( + data: nil, + uuid: nil, + number: nil, + date: date + ) + + var encodedData = Data() + var encoder = TLVEncoder() + encoder.dateFormat = format + encoder.log = { print("Encoder:", $0) } + do { + encodedData = try encoder.encode(value) + } catch { + dump(error) + XCTFail("Could not encode \(value)") + return + } + + var decoder = TLVDecoder() + decoder.dateFormat = format + decoder.log = { print("Decoder:", $0) } + do { + let decodedValue = try decoder.decode(CustomEncodable.self, from: encodedData) + XCTAssertEqual(decodedValue, value) + } catch { + dump(error) + XCTFail("Could not decode \(value)") + } + } + } + + func testDateSecondsSince1970() { + + let date = Date(timeIntervalSince1970: 60 * 60 * 24 * 365) + + let value = Transaction( + id: UUID(), + date: date, + description: "Test" + ) + + let rawValue = TransactionRaw( + id: value.id, + date: value.date.timeIntervalSince1970, + description: value.description + ) + + var encoder = TLVEncoder() + encoder.dateFormat = .secondsSince1970 + encoder.log = { print("Encoder:", $0) } + XCTAssertEqual(try encoder.encode(value), try encoder.encode(rawValue)) + } } // MARK: - Supporting Types @@ -259,6 +333,20 @@ public enum Gender: UInt8, Codable { case female } +public struct Transaction: Equatable, Codable { + + public let id: UUID + public let date: Date + public let description: String +} + +public struct TransactionRaw: Equatable, Codable { + + public let id: UUID + public let date: Double + public let description: String +} + public struct ProvisioningState: Codable, Equatable { public var state: State @@ -358,6 +446,7 @@ public struct CustomEncodable: Codable, Equatable { public var data: Data? public var uuid: UUID? public var number: TLVCodableNumber? + public var date: Date? } public struct DeviceInformation: Equatable, Codable {