diff --git a/.github/workflows/swift-wasm.yml b/.github/workflows/swift-wasm.yml deleted file mode 100644 index 7809c3d..0000000 --- a/.github/workflows/swift-wasm.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: SwiftWASM -on: [push] -jobs: - test: - name: Test - runs-on: ubuntu-latest - container: ghcr.io/swiftwasm/carton:latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Swift Version - run: swift --version - - name: Test - run: carton test diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..a72ae52 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "939a0047c46c413f58dd76630aea11ebe4db64ab8e78e6e29d5241e906dcf8d9", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", + "version" : "603.0.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 5a75b0b..dfff4dc 100644 --- a/Package.swift +++ b/Package.swift @@ -1,13 +1,25 @@ -// swift-tools-version:5.1 +// swift-tools-version:6.0 import PackageDescription +import CompilerPluginSupport import class Foundation.ProcessInfo +// get environment variables +let environment = ProcessInfo.processInfo.environment +let dynamicLibrary = environment["SWIFT_BUILD_DYNAMIC_LIBRARY"] != nil +let enableMacros = environment["SWIFTPM_ENABLE_MACROS"] != "0" +let buildDocs = environment["BUILDING_FOR_DOCUMENTATION_GENERATION"] != nil + // force building as dynamic library -let dynamicLibrary = ProcessInfo.processInfo.environment["SWIFT_BUILD_DYNAMIC_LIBRARY"] != nil let libraryType: PackageDescription.Product.Library.LibraryType? = dynamicLibrary ? .dynamic : nil var package = Package( name: "TLVCoding", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + ], products: [ .library( name: "TLVCoding", @@ -17,8 +29,7 @@ var package = Package( ], targets: [ .target( - name: "TLVCoding", - path: "./Sources" + name: "TLVCoding" ), .testTarget( name: "TLVCodingTests", @@ -27,12 +38,76 @@ var package = Package( ] ) -// SwiftPM command plugins are only supported by Swift version 5.6 and later. -#if swift(>=5.6) -let buildDocs = ProcessInfo.processInfo.environment["BUILDING_FOR_DOCUMENTATION_GENERATION"] != nil +// SwiftPM plugins if buildDocs { package.dependencies += [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package( + url: "https://github.com/swiftlang/swift-docc-plugin.git", + from: "1.4.5" + ) + ] +} + +if enableMacros { + let version: Version + #if swift(>=6.3) + version = "603.0.1" + #elseif swift(>=6.2) + version = "602.0.0" + #elseif swift(>=6.1) + version = "601.0.1" + #else + version = "600.0.1" + #endif + package.targets[0].swiftSettings = [ + .define("SWIFTPM_ENABLE_MACROS") + ] + package.dependencies += [ + .package( + url: "https://github.com/swiftlang/swift-syntax.git", + from: version + ) + ] + package.targets += [ + .macro( + name: "TLVCodingMacros", + dependencies: [ + .product( + name: "SwiftSyntaxMacros", + package: "swift-syntax" + ), + .product( + name: "SwiftCompilerPlugin", + package: "swift-syntax" + ) + ] + ) + ] + package.targets[0].dependencies += [ + "TLVCodingMacros" + ] + package.targets += [ + .testTarget( + name: "TLVCodingMacrosTests", + dependencies: [ + "TLVCodingMacros", + .product( + name: "SwiftSyntaxMacros", + package: "swift-syntax" + ), + .product( + name: "SwiftSyntaxMacroExpansion", + package: "swift-syntax" + ), + .product( + name: "SwiftParser", + package: "swift-syntax" + ), + .product( + name: "SwiftSyntaxMacrosTestSupport", + package: "swift-syntax" + ) + ] + ) ] } -#endif diff --git a/README.md b/README.md index 3c5f295..a8f358b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,96 @@ # TLVCoding -[![Build Status](https://travis-ci.org/PureSwift/TLVCoding.svg?branch=master)](https://travis-ci.org/PureSwift/TLVCoding) +[![Swift](https://github.com/PureSwift/TLVCoding/actions/workflows/swift.yml/badge.svg)](https://github.com/PureSwift/TLVCoding/actions/workflows/swift.yml) -TLV8 (Type-Length-Value) Coder library +TLV8 (Type-Length-Value) Coder library, compatible with [Embedded Swift](https://github.com/swiftlang/swift/tree/main/docs/EmbeddedSwift). -# See Also +Types are encoded and decoded explicitly via `TLVCodable` — either with conformances generated by the `@TLVCodable` macro, or written by hand. The library does not depend on `Codable` runtime machinery or Foundation, so it can be used on bare-metal targets. +## Usage + +### Macro-generated conformance + +```swift +import TLVCoding + +@TLVCodable +struct ProvisioningState: Equatable { + + var state: State + + var result: Result + + enum CodingKeys: UInt8, TLVCodingKey { + case state = 0x01 + case result = 0x02 + } +} + +let value = ProvisioningState(state: .provisioning, result: .success) +let data = value.tlvData // Data([0x01, 0x01, 0x01, 0x02, 0x01, 0x01]) +let decoded = ProvisioningState(tlvData: data) +``` + +If no `CodingKeys` enum is declared, one is generated with sequential type codes in property declaration order. + +### Hand-written conformance + +The same conformance can be written manually (e.g. for Embedded Swift or custom binary layouts): + +```swift +extension ProvisioningState: TLVCodable { + + init?(tlvData: Data) { + guard let container = TLVContainer(data: tlvData), + let state = container.decode(State.self, forKey: CodingKeys.state), + let result = container.decode(Result.self, forKey: CodingKeys.result) + else { return nil } + self.state = state + self.result = result + } + + var tlvData: Data { + var container = TLVContainer() + container.encode(state, forKey: CodingKeys.state) + container.encode(result, forKey: CodingKeys.result) + return container.data + } +} +``` + +Types with a custom binary layout can skip `TLVContainer` entirely and produce their payload directly: + +```swift +extension Version: TLVCodable { + + init?(tlvData: Data) { + guard tlvData.count == 3 else { return nil } + self.major = tlvData[0] + self.minor = tlvData[1] + self.patch = tlvData[2] + } + + var tlvData: Data { + Data([major, minor, patch]) + } +} +``` + +### Embedded Swift + +Macros are disabled under Embedded Swift (write conformances by hand) and the swift-syntax dependency can be skipped entirely with the `SWIFTPM_ENABLE_MACROS` environment variable: + +```bash +SWIFTPM_ENABLE_MACROS=0 swift build --target TLVCoding \ + --triple armv7em-none-none-eabi \ + -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ + -Xswiftc -wmo +``` + +On platforms without Foundation, a minimal `Data` type is provided by the library. + +## See Also + +- [CoreModel](https://github.com/PureSwift/CoreModel) - Swift ORM with the same explicit coding and macro design - [BluetoothLinux](https://github.com/PureSwift/BluetoothLinux) - Pure Swift Linux Bluetooth Stack - [GATT](https://github.com/PureSwift/GATT) - Bluetooth Generic Attribute Profile (GATT) for Swift -- [SwiftFoundation](https://github.com/PureSwift/SwiftFoundation) - Cross-Platform, Protocol-Oriented Programming base library to complement the Swift Standard Library. -- [Cacao](https://github.com/PureSwift/Cacao) - Pure Swift Cross-platform UIKit -- [Silica](https://github.com/PureSwift/Silica) - Pure Swift CoreGraphics (Quartz2D) implementation -- [Predicate](https://github.com/PureSwift/Predicate) - Pure Swift Predicate implementation diff --git a/Sources/CodingKey.swift b/Sources/CodingKey.swift deleted file mode 100644 index da3d5bd..0000000 --- a/Sources/CodingKey.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// CodingKey.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 5/12/19. -// Copyright © 2019 PureSwift. All rights reserved. -// - -/// TLV Coding Key -public protocol TLVCodingKey: CodingKey { - - init?(code: TLVTypeCode) - - var code: TLVTypeCode { get } -} - -public extension TLVCodingKey { - - init?(intValue: Int) { - - guard intValue <= Int(UInt8.max), - intValue >= Int(UInt8.min) - else { return nil } - - self.init(code: TLVTypeCode(rawValue: UInt8(intValue))) - } - - var intValue: Int? { - return Int(code.rawValue) - } -} - -public extension TLVCodingKey where Self: RawRepresentable, Self.RawValue == TLVTypeCode.RawValue { - - init?(code: TLVTypeCode) { - self.init(rawValue: code.rawValue) - } - - var code: TLVTypeCode { - return TLVTypeCode(rawValue: rawValue) - } -} - -public extension TLVCodingKey where Self: CaseIterable, Self: RawRepresentable, Self.RawValue == TLVTypeCode.RawValue { - - init?(stringValue: String) { - - guard let value = Self.allCases.first(where: { $0.stringValue == stringValue }) - else { return nil } - - self = value - } -} - -internal extension TLVTypeCode { - - init? (codingKey: K) { - - if let tlvCodingKey = codingKey as? TLVCodingKey { - - self = tlvCodingKey.code - - } else if let intValue = codingKey.intValue { - - guard intValue <= Int(UInt8.max), - intValue >= Int(UInt8.min) - else { return nil } - - self.init(rawValue: UInt8(intValue)) - - } else if Mirror(reflecting: codingKey).displayStyle == .enum { - - switch MemoryLayout.size { - case MemoryLayout.size: - self.init(rawValue: unsafeBitCast(codingKey, to: UInt8.self)) - case 0: // single case enum - self.init(rawValue: 0) - default: - return nil - } - } else { - return nil - } - } -} - -internal extension Sequence where Element == CodingKey { - - /// KVC path string for current coding path. - var path: String { - return reduce("", { $0 + "\($0.isEmpty ? "" : ".")" + $1.stringValue }) - } -} - -internal extension CodingKey { - - static var sanitizedName: String { - - let rawName = String(reflecting: self) - var elements = rawName.split(separator: ".") - guard elements.count > 2 - else { return rawName } - elements.removeFirst() - elements.removeAll { $0.contains("(unknown context") } - return elements.reduce("", { $0 + ($0.isEmpty ? "" : ".") + $1 }) - } -} - diff --git a/Sources/Data.swift b/Sources/Data.swift deleted file mode 100644 index 1929bb1..0000000 --- a/Sources/Data.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Data.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 5/12/19. -// Copyright © 2019 PureSwift. All rights reserved. -// - -import Foundation - -internal extension Data { - - func subdataNoCopy(in range: Range) -> Data { - - // stored in heap, can reuse buffer - if count > Data.inlineBufferSize { - return withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in - Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer.baseAddress!.advanced(by: range.lowerBound)), - count: range.count, - deallocator: .none) - } - } else { - // stored in stack, must copy - return subdata(in: range) - } - } - - func suffixNoCopy(from index: Int) -> Data { - return subdataNoCopy(in: index ..< count) - } - - func suffixCheckingBounds(from start: Int) -> Data { - if count > start { - return Data(suffix(from: start)) - } else { - return Data() - } - } -} - -private extension Data { - - /// Size of the inline buffer for `Foundation.Data` used in Swift 5. - /// - /// Used to determine wheather data is stored on stack or in heap. - static var inlineBufferSize: Int { - - // Keep up to date - // https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/Data.swift#L621 - #if arch(x86_64) || arch(arm64) || arch(arm64_32) || arch(s390x) || arch(powerpc64) || arch(powerpc64le) - typealias Buffer = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) //len //enum - #elseif arch(i386) || arch(arm) || arch(wasm32) - typealias Buffer = (UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8) - #else - #error("Platform not supported") - #endif - - return MemoryLayout.size - } -} diff --git a/Sources/DataConvertible.swift b/Sources/DataConvertible.swift deleted file mode 100644 index d7a50c2..0000000 --- a/Sources/DataConvertible.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// DataConvertible.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 5/12/19. -// Copyright © 2019 PureSwift. All rights reserved. -// - -import Foundation - -/// Can be converted into data. -internal protocol DataConvertible { - - /// Append data representation into buffer. - static func += (data: inout T, value: Self) - - /// Length of value when encoded into data. - var dataLength: Int { get } -} - -extension Data { - - /// Initialize data with contents of value. - @inline(__always) - init (_ value: T) { - self.init(capacity: value.dataLength) - self += value - } - - init (_ sequence: T) where T.Element: DataConvertible { - - let dataLength = sequence.reduce(0, { $0 + $1.dataLength }) - self.init(capacity: dataLength) - sequence.forEach { self += $0 } - } -} - -// MARK: - UnsafeDataConvertible - -/// Internal Data casting protocol -internal protocol UnsafeDataConvertible: DataConvertible { } - -extension UnsafeDataConvertible { - - var dataLength: Int { - return MemoryLayout.size - } - - /// Append data representation into buffer. - static func += (data: inout T, value: Self) { - var value = value - withUnsafePointer(to: &value) { - $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size) { - data.append($0, count: MemoryLayout.size) - } - } - } -} - -extension UInt16: UnsafeDataConvertible { } -extension UInt32: UnsafeDataConvertible { } -extension UInt64: UnsafeDataConvertible { } -extension UUID: UnsafeDataConvertible { } - -// MARK: - DataContainer - -/// Data container type. -internal protocol DataContainer: RandomAccessCollection where Self.Index == Int { - - subscript(index: Int) -> UInt8 { get } - - mutating func append(_ newElement: UInt8) - - mutating func append(_ pointer: UnsafePointer, count: Int) - - mutating func append (contentsOf bytes: C) where C.Element == UInt8 - - static func += (lhs: inout Self, rhs: UInt8) - - static func += (lhs: inout Self, rhs: C) where C.Element == UInt8 -} - -extension DataContainer { - - mutating func append (_ value: T) { - self += value - } -} - -extension Data: DataContainer { - - static func += (lhs: inout Data, rhs: UInt8) { - lhs.append(rhs) - } -} diff --git a/Sources/Decoder.swift b/Sources/Decoder.swift deleted file mode 100644 index afc34f8..0000000 --- a/Sources/Decoder.swift +++ /dev/null @@ -1,999 +0,0 @@ -// -// Decoder.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 3/8/18. -// Copyright © 2018 PureSwift. All rights reserved. -// - -import Foundation - -/// TLV8 Decoder -public struct TLVDecoder { - - // MARK: - Properties - - /// Any contextual information set by the user for encoding. - public var userInfo = [CodingUserInfoKey : Any]() - - /// Logger handler - public var log: ((String) -> ())? - - /// Format for numeric values. - public var numericFormatting: TLVNumericFormatting = .default - - /// Format for UUID values. - public var uuidFormatting: TLVUUIDFormatting = .default - - /// Format for Date values. - public var dateFormatting: TLVDateFormatting = .default - - // MARK: - Initialization - - public init() { } - - // MARK: - Methods - - public func decode (_ type: T.Type, from data: Data) throws -> T { - - log?("Will decode \(String(reflecting: T.self))") - - if let tlvCodable = type as? TLVDecodable.Type { - guard let value = tlvCodable.init(tlvData: data) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid data for \(String(reflecting: type))")) - } - return value as! T - } else { - let items = try decode(data) - let options = Decoder.Options( - numericFormatting: numericFormatting, - uuidFormatting: uuidFormatting, - dateFormatting: dateFormatting - ) - let decoder = Decoder( - referencing: .items(items), - userInfo: userInfo, - log: log, - options: options - ) - // decode from container - return try T.init(from: decoder) - } - } - - public func decode(_ data: Data) throws -> [TLVItem] { - - return try TLVDecoder.decode(data, codingPath: []) - } - - internal static func decode(_ data: Data, codingPath: [CodingKey]) throws -> [TLVItem] { - - var offset = 0 - var items = [TLVItem]() - - while offset < data.count { - - // validate size - guard data.count >= offset + 2 else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Not enough bytes (\(data.count)) to continue parsing at offset \(offset)")) - } - - // get type - let typeByte = data[offset] // 0 - offset += 1 - - let length = Int(data[offset]) // 1 - offset += 1 - - let valueData: Data - - if length > 0 { - - // validate size - guard data.count >= offset + length else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Not enough bytes (\(data.count)) to continue parsing at offset \(offset)")) - } - - // get value - valueData = Data(data[offset ..< offset + length]) - - } else { - - valueData = Data() - } - - let item = TLVItem(type: TLVTypeCode(rawValue: typeByte), value: valueData) - - // append result - items.append(item) - - // adjust offset for next value - offset += length - } - - return items - } -} - -// 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 { - - /// The path of coding keys taken to get to this point in decoding. - fileprivate(set) var codingPath: [CodingKey] - - /// Any contextual information set by the user for decoding. - let userInfo: [CodingUserInfoKey : Any] - - fileprivate var stack: Stack - - /// Logger - let log: ((String) -> ())? - - let options: Options - - // MARK: - Initialization - - fileprivate init(referencing container: Stack.Container, - at codingPath: [CodingKey] = [], - userInfo: [CodingUserInfoKey : Any], - log: ((String) -> ())?, - options: Options) { - - self.stack = Stack(container) - self.codingPath = codingPath - self.userInfo = userInfo - self.log = log - self.options = options - } - - // MARK: - Methods - - func container (keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - - log?("Requested container keyed by \(type.sanitizedName) for path \"\(codingPath.path)\"") - - let container = self.stack.top - - switch container { - case let .items(items): - let keyedContainer = TLVKeyedDecodingContainer(referencing: self, wrapping: items) - return KeyedDecodingContainer(keyedContainer) - case let .item(item): - let items = try decode(item.value, codingPath: codingPath) - let keyedContainer = TLVKeyedDecodingContainer(referencing: self, wrapping: items) - return KeyedDecodingContainer(keyedContainer) - } - } - - func unkeyedContainer() throws -> UnkeyedDecodingContainer { - - log?("Requested unkeyed container for path \"\(codingPath.path)\"") - - let container = self.stack.top - - switch container { - - case let .items(items): - - return TLVUnkeyedDecodingContainer(referencing: self, wrapping: items) - - case let .item(item): - - // forceably cast to array - do { - let items = try TLVDecoder.decode(item.value, codingPath: codingPath) - self.stack.pop() // replace stack - self.stack.push(.items(items)) - return TLVUnkeyedDecodingContainer(referencing: self, wrapping: items) - } catch { - log?("Could not decode for unkeyed container: \(error)") - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot get unkeyed decoding container, invalid top container \(container).")) - } - } - } - - func singleValueContainer() throws -> SingleValueDecodingContainer { - - log?("Requested single value container for path \"\(codingPath.path)\"") - - let container = self.stack.top - - guard case let .item(item) = container else { - - throw DecodingError.typeMismatch(SingleValueDecodingContainer.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot get single value decoding container, invalid top container \(container).")) - } - - return TLVSingleValueDecodingContainer(referencing: self, wrapping: item) - } - } -} - -internal extension TLVDecoder.Decoder { - - struct Options { - - let numericFormatting: TLVNumericFormatting - - let uuidFormatting: TLVUUIDFormatting - - let dateFormatting: TLVDateFormatting - } -} - -// MARK: - Coding Key - -internal extension TLVDecoder.Decoder { - - func typeCode (for key: Key) throws -> TLVTypeCode { - - guard let typeCode = TLVTypeCode(codingKey: key) else { - if let intValue = key.intValue { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "Coding key \(key) has an invalid integer value \(intValue)")) - } else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "Coding key \(key) has no integer value")) - } - } - return typeCode - } -} - -// MARK: - Unboxing Values - -internal extension TLVDecoder.Decoder { - - func unbox (_ data: Data, as type: T.Type) throws -> T { - - guard let value = T.init(tlvData: data) else { - - throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Could not parse \(type) from \(data)")) - } - - return value - } - - func unboxNumeric (_ data: Data, as type: T.Type) throws -> T { - - var numericValue = try unbox(data, as: type) - switch options.numericFormatting { - case .bigEndian: - numericValue = T.init(bigEndian: numericValue) - case .littleEndian: - numericValue = T.init(littleEndian: numericValue) - } - 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 { - - // override for native types - if type == Data.self { - 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? TLVDecodable.Type { - guard let value = tlvCodable.init(tlvData: item.value) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Invalid data for \(String(reflecting: type))")) - } - return value as! T - } else { - // push container to stack and decode using Decodable implementation - stack.push(.item(item)) - let decoded = try T(from: self) - stack.pop() - return decoded - } - } -} - -private extension TLVDecoder.Decoder { - - func unboxUUID(_ data: Data) throws -> UUID { - - 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")) - } - return UUID(uuid: (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15])) - case .string: - guard let string = String(tlvData: data) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Invalid string data for UUID")) - } - guard let uuid = UUID(uuidString: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Invalid UUID string \(string)")) - } - return uuid - } - } - - func unboxDate(_ data: Data) throws -> Date { - - switch options.dateFormatting { - case .secondsSince1970: - let timeInterval = try unboxDouble(data) - return Date(timeIntervalSince1970: timeInterval) - case .millisecondsSince1970: - let timeInterval = try unboxDouble(data) - return Date(timeIntervalSince1970: timeInterval / 1000) - #if !os(WASI) - 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: TLVDateFormatting.iso8601Formatter) - case let .formatted(formatter): - return try unboxDate(data, using: formatter) - #endif - } - } - - #if !os(WASI) - 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 - } - #endif -} - -// MARK: - Stack - -private extension TLVDecoder { - - struct Stack { - - private(set) var containers = [Container]() - - fileprivate init(_ container: Container) { - - self.containers = [container] - } - - var top: Container { - - guard let container = containers.last - else { fatalError("Empty container stack.") } - - return container - } - - mutating func push(_ container: Container) { - - containers.append(container) - } - - @discardableResult - mutating func pop() -> Container { - - guard let container = containers.popLast() - else { fatalError("Empty container stack.") } - - return container - } - } -} - -fileprivate extension TLVDecoder.Stack { - - enum Container { - - case items([TLVItem]) - case item(TLVItem) - } -} - - -// MARK: - KeyedDecodingContainer - -internal struct TLVKeyedDecodingContainer : KeyedDecodingContainerProtocol { - - typealias Key = K - - // MARK: Properties - - /// A reference to the encoder we're reading from. - let decoder: TLVDecoder.Decoder - - /// A reference to the container we're reading from. - let container: [TLVItem] - - /// The path of coding keys taken to get to this point in decoding. - let codingPath: [CodingKey] - - /// All the keys the Decoder has for this container. - let allKeys: [Key] - - // MARK: Initialization - - /// Initializes `self` by referencing the given decoder and container. - init(referencing decoder: TLVDecoder.Decoder, wrapping container: [TLVItem]) { - - self.decoder = decoder - self.container = container - self.codingPath = decoder.codingPath - self.allKeys = container.compactMap { Key(intValue: Int($0.type.rawValue)) } - } - - // MARK: KeyedDecodingContainerProtocol - - func contains(_ key: Key) -> Bool { - - self.decoder.log?("Check whether key \"\(key.stringValue)\" exists") - guard let typeCode = try? self.decoder.typeCode(for: key) - else { return false } - return container.contains { $0.type == typeCode } - } - - func decodeNil(forKey key: Key) throws -> Bool { - - // set coding key context - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - self.decoder.log?("Check if nil at path \"\(decoder.codingPath.path)\"") - - // check if key exists since there is no way to represent nil in TLV - // empty data and strings should not be falsely reported as nil - return try self.value(for: key) == nil - } - - func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { - - return try decodeTLV(type, forKey: key) - } - - func decode(_ type: Int.Type, forKey key: Key) throws -> Int { - - let value = try decodeNumeric(Int32.self, forKey: key) - return Int(value) - } - - func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { - - return try decodeTLV(type, forKey: key) - } - - func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { - - return try decodeNumeric(type, forKey: key) - } - - func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { - - return try decodeNumeric(type, forKey: key) - } - - func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { - - return try decodeNumeric(type, forKey: key) - } - - func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { - - let value = try decodeNumeric(UInt32.self, forKey: key) - return UInt(value) - } - - func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { - - return try decodeTLV(type, forKey: key) - } - - func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { - - return try decodeNumeric(type, forKey: key) - } - - func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { - - return try decodeNumeric(type, forKey: key) - } - - func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { - - return try decodeNumeric(type, forKey: key) - } - - func decode(_ type: Float.Type, forKey key: Key) throws -> Float { - - let bitPattern = try decodeNumeric(UInt32.self, forKey: key) - return Float(bitPattern: bitPattern) - } - - func decode(_ type: Double.Type, forKey key: Key) throws -> Double { - - let bitPattern = try decodeNumeric(UInt64.self, forKey: key) - return Double(bitPattern: bitPattern) - } - - func decode(_ type: String.Type, forKey key: Key) throws -> String { - - return try decodeTLV(type, forKey: key) - } - - func decode (_ type: T.Type, forKey key: Key) throws -> T { - - return try self.value(for: key, type: type) { try decoder.unboxDecodable($0, as: type) } - } - - func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { - - fatalError() - } - - func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { - - fatalError() - } - - func superDecoder() throws -> Decoder { - - fatalError() - } - - func superDecoder(forKey key: Key) throws -> Decoder { - - fatalError() - } - - // MARK: Private Methods - - /// Decode native value type from TLV data. - 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 { - - return try self.value(for: key, type: type) { try decoder.unboxNumeric($0.value, as: type) } - } - - /// Access actual value - @inline(__always) - private func value (for key: Key, type: T.Type, decode: (TLVItem) throws -> T) throws -> T { - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - decoder.log?("Will read value at path \"\(decoder.codingPath.path)\"") - guard let item = try self.value(for: key) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - return try decode(item) - } - - /// Access actual value - private func value(for key: Key) throws -> TLVItem? { - let typeCode = try self.decoder.typeCode(for: key) - return container.first { $0.type == typeCode } - } -} - -// MARK: - SingleValueDecodingContainer - -internal struct TLVSingleValueDecodingContainer: SingleValueDecodingContainer { - - // MARK: Properties - - /// A reference to the decoder we're reading from. - let decoder: TLVDecoder.Decoder - - /// A reference to the container we're reading from. - let container: TLVItem - - /// The path of coding keys taken to get to this point in decoding. - let codingPath: [CodingKey] - - // MARK: Initialization - - /// Initializes `self` by referencing the given decoder and container. - init(referencing decoder: TLVDecoder.Decoder, wrapping container: TLVItem) { - - self.decoder = decoder - self.container = container - self.codingPath = decoder.codingPath - } - - // MARK: SingleValueDecodingContainer - - func decodeNil() -> Bool { - - return container.value.isEmpty - } - - func decode(_ type: Bool.Type) throws -> Bool { - - return try self.decoder.unbox(container.value, as: type) - } - - func decode(_ type: Int.Type) throws -> Int { - - let value = try self.decoder.unboxNumeric(container.value, as: Int32.self) - return Int(value) - } - - func decode(_ type: Int8.Type) throws -> Int8 { - - return try self.decoder.unbox(container.value, as: type) - } - - func decode(_ type: Int16.Type) throws -> Int16 { - - return try self.decoder.unboxNumeric(container.value, as: type) - } - - func decode(_ type: Int32.Type) throws -> Int32 { - - return try self.decoder.unboxNumeric(container.value, as: type) - } - - func decode(_ type: Int64.Type) throws -> Int64 { - - return try self.decoder.unboxNumeric(container.value, as: type) - } - - func decode(_ type: UInt.Type) throws -> UInt { - - let value = try self.decoder.unboxNumeric(container.value, as: UInt32.self) - return UInt(value) - } - - func decode(_ type: UInt8.Type) throws -> UInt8 { - - return try self.decoder.unbox(container.value, as: type) - } - - func decode(_ type: UInt16.Type) throws -> UInt16 { - - return try self.decoder.unboxNumeric(container.value, as: type) - } - - func decode(_ type: UInt32.Type) throws -> UInt32 { - - return try self.decoder.unboxNumeric(container.value, as: type) - } - - func decode(_ type: UInt64.Type) throws -> UInt64 { - - return try self.decoder.unboxNumeric(container.value, as: type) - } - - func decode(_ type: Float.Type) throws -> Float { - - let value = try self.decoder.unboxNumeric(container.value, as: UInt32.self) - return Float(bitPattern: value) - } - - func decode(_ type: Double.Type) throws -> Double { - - let value = try self.decoder.unboxNumeric(container.value, as: UInt64.self) - return Double(bitPattern: value) - } - - func decode(_ type: String.Type) throws -> String { - - return try self.decoder.unbox(container.value, as: type) - } - - func decode (_ type: T.Type) throws -> T { - - return try self.decoder.unboxDecodable(container, as: type) - } -} - -// MARK: UnkeyedDecodingContainer - -internal struct TLVUnkeyedDecodingContainer: UnkeyedDecodingContainer { - - // MARK: Properties - - /// A reference to the encoder we're reading from. - let decoder: TLVDecoder.Decoder - - /// A reference to the container we're reading from. - let container: [TLVItem] - - /// The path of coding keys taken to get to this point in decoding. - let codingPath: [CodingKey] - - private(set) var currentIndex: Int = 0 - - // MARK: Initialization - - /// Initializes `self` by referencing the given decoder and container. - init(referencing decoder: TLVDecoder.Decoder, wrapping container: [TLVItem]) { - - self.decoder = decoder - self.container = container - self.codingPath = decoder.codingPath - } - - // MARK: UnkeyedDecodingContainer - - var count: Int? { - return _count - } - - private var _count: Int { - return container.count - } - - var isAtEnd: Bool { - return currentIndex >= _count - } - - mutating func decodeNil() throws -> Bool { - - try assertNotEnd() - - // never optional, decode - return false - } - - mutating func decode(_ type: Bool.Type) throws -> Bool { fatalError("stub") } - mutating func decode(_ type: Int.Type) throws -> Int { fatalError("stub") } - mutating func decode(_ type: Int8.Type) throws -> Int8 { fatalError("stub") } - mutating func decode(_ type: Int16.Type) throws -> Int16 { fatalError("stub") } - mutating func decode(_ type: Int32.Type) throws -> Int32 { fatalError("stub") } - mutating func decode(_ type: Int64.Type) throws -> Int64 { fatalError("stub") } - mutating func decode(_ type: UInt.Type) throws -> UInt { fatalError("stub") } - mutating func decode(_ type: UInt8.Type) throws -> UInt8 { fatalError("stub") } - mutating func decode(_ type: UInt16.Type) throws -> UInt16 { fatalError("stub") } - mutating func decode(_ type: UInt32.Type) throws -> UInt32 { fatalError("stub") } - mutating func decode(_ type: UInt64.Type) throws -> UInt64 { fatalError("stub") } - mutating func decode(_ type: Float.Type) throws -> Float { fatalError("stub") } - mutating func decode(_ type: Double.Type) throws -> Double { fatalError("stub") } - mutating func decode(_ type: String.Type) throws -> String { fatalError("stub") } - - mutating func decode (_ type: T.Type) throws -> T { - - try assertNotEnd() - - self.decoder.codingPath.append(Index(intValue: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - let item = self.container[self.currentIndex] - - let decoded = try self.decoder.unboxDecodable(item, as: type) - - self.currentIndex += 1 - - return decoded - } - - mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { - - throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(type)")) - } - - mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { - - throw DecodingError.typeMismatch([Any].self, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode unkeyed container.")) - } - - mutating func superDecoder() throws -> Decoder { - - // set coding key context - self.decoder.codingPath.append(Index(intValue: currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - // log - self.decoder.log?("Requested super decoder for path \"\(self.decoder.codingPath.path)\"") - - // check for end of array - try assertNotEnd() - - // get item - let item = container[currentIndex] - - // increment counter - self.currentIndex += 1 - - // create new decoder - let decoder = TLVDecoder.Decoder(referencing: .item(item), - at: self.decoder.codingPath, - userInfo: self.decoder.userInfo, - log: self.decoder.log, - options: self.decoder.options) - - return decoder - } - - // MARK: Private Methods - - @inline(__always) - private func assertNotEnd() throws { - - guard isAtEnd == false else { - - throw DecodingError.valueNotFound(Any?.self, DecodingError.Context(codingPath: self.decoder.codingPath + [Index(intValue: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - } -} - -internal extension TLVUnkeyedDecodingContainer { - - struct Index: CodingKey { - - public let index: Int - - public init(intValue: Int) { - self.index = intValue - } - - public init?(stringValue: String) { - return nil - } - - public var intValue: Int? { - return index - } - - public var stringValue: String { - return "\(index)" - } - } -} - -// MARK: - Decodable Types - -/// Private protocol for decoding TLV values into raw data. -internal protocol TLVRawDecodable { - - init?(tlvData data: Data) -} - -extension String: TLVRawDecodable { - - public init?(tlvData data: Data) { - - self.init(data: data, encoding: .utf8) - } -} - -extension Bool: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self = data[0] != 0 - } -} - -extension UInt8: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self = data[0] - } -} - -extension UInt16: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self.init(bytes: (data[0], data[1])) - } -} - -extension UInt32: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self.init(bytes: (data[0], data[1], data[2], data[3])) - } -} - -extension UInt64: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self.init(bytes: (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7])) - } -} - -extension Int8: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self = Int8(bitPattern: data[0]) - } -} - -extension Int16: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self.init(bytes: (data[0], data[1])) - } -} - -extension Int32: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self.init(bytes: (data[0], data[1], data[2], data[3])) - } -} - -extension Int64: TLVRawDecodable { - - public init?(tlvData data: Data) { - - guard data.count == MemoryLayout.size - else { return nil } - - self.init(bytes: (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7])) - } -} diff --git a/Sources/Encoder.swift b/Sources/Encoder.swift deleted file mode 100644 index 6d3f8a7..0000000 --- a/Sources/Encoder.swift +++ /dev/null @@ -1,793 +0,0 @@ -// -// Encoder.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 3/8/18. -// Copyright © 2018 PureSwift. All rights reserved. -// - -import Foundation - -/// TLV8 Encoder -public struct TLVEncoder { - - // MARK: - Properties - - /// Any contextual information set by the user for encoding. - public var userInfo = [CodingUserInfoKey : Any]() - - /// Logger handler - public var log: ((String) -> ())? - - /// Format for numeric values. - public var outputFormatting: TLVOutputFormatting = .default - - /// Format for numeric values. - public var numericFormatting: TLVNumericFormatting = .default - - /// Format for UUID values. - public var uuidFormatting: TLVUUIDFormatting = .default - - /// Format for Date values. - public var dateFormatting: TLVDateFormatting = .default - - // MARK: - Initialization - - public init() { } - - // MARK: - Methods - - public func encode (_ value: T) throws -> Data { - - log?("Will encode \(String(reflecting: T.self))") - - if let encodable = value as? TLVEncodable { - return encodable.tlvData - } else { - let options = Encoder.Options( - 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) - guard case let .items(container) = encoder.stack.root else { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) is not encoded as items.")) - } - return container.data - } - } - - public func encode(_ items: [TLVItem]) -> Data { - return Data(items) - } - - public func encode(_ items: TLVItem...) -> Data { - return Data(items) - } -} - -// 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 { - - // MARK: - Properties - - /// The path of coding keys taken to get to this point in encoding. - fileprivate(set) var codingPath: [CodingKey] - - /// Any contextual information set by the user for encoding. - let userInfo: [CodingUserInfoKey : Any] - - /// Logger - let log: ((String) -> ())? - - let options: Options - - private(set) var stack: Stack - - // MARK: - Initialization - - init(codingPath: [CodingKey] = [], - userInfo: [CodingUserInfoKey : Any], - log: ((String) -> ())?, - options: Options) { - - self.stack = Stack() - self.codingPath = codingPath - self.userInfo = userInfo - self.log = log - self.options = options - } - - // MARK: - Encoder - - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { - - log?("Requested container keyed by \(type.sanitizedName) for path \"\(codingPath.path)\"") - - let stackContainer = ItemsContainer() - self.stack.push(.items(stackContainer)) - - let keyedContainer = TLVKeyedContainer(referencing: self, wrapping: stackContainer) - - return KeyedEncodingContainer(keyedContainer) - } - - func unkeyedContainer() -> UnkeyedEncodingContainer { - - log?("Requested unkeyed container for path \"\(codingPath.path)\"") - - let stackContainer = ItemsContainer() - self.stack.push(.items(stackContainer)) - - return TLVUnkeyedEncodingContainer(referencing: self, wrapping: stackContainer) - } - - func singleValueContainer() -> SingleValueEncodingContainer { - - log?("Requested single value container for path \"\(codingPath.path)\"") - - let stackContainer = ItemContainer() - self.stack.push(.item(stackContainer)) - - return TLVSingleValueEncodingContainer(referencing: self, wrapping: stackContainer) - } - } -} - -internal extension TLVEncoder.Encoder { - - struct Options { - - public let outputFormatting: TLVOutputFormatting - - public let numericFormatting: TLVNumericFormatting - - public let uuidFormatting: TLVUUIDFormatting - - public let dateFormatting: TLVDateFormatting - } -} - -internal extension TLVEncoder.Encoder { - - func typeCode (for key: Key, value: T) throws -> TLVTypeCode { - - guard let typeCode = TLVTypeCode(codingKey: key) else { - if let intValue = key.intValue { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "Coding key \(key) has an invalid integer value \(intValue)")) - } else { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "Coding key \(key) has no integer value")) - } - } - return typeCode - } -} - -internal extension TLVEncoder.Encoder { - - @inline(__always) - func box (_ value: T) -> Data { - return value.tlvData - } - - @inline(__always) - func boxNumeric (_ value: T) -> Data { - - let numericValue: T - switch options.numericFormatting { - case .bigEndian: - numericValue = value.bigEndian - case .littleEndian: - numericValue = value.littleEndian - } - 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 { - // encode using Encodable, should push new container. - try value.encode(to: self) - let nestedContainer = stack.pop() - return nestedContainer.data - } - } -} - -private extension TLVEncoder.Encoder { - - func boxUUID(_ uuid: UUID) -> Data { - - switch options.uuidFormatting { - case .bytes: - return Data(uuid) - case .string: - return uuid.uuidString.tlvData - } - } - - func boxDate(_ date: Date) -> Data { - - switch options.dateFormatting { - case .secondsSince1970: - return boxDouble(date.timeIntervalSince1970) - case .millisecondsSince1970: - return boxDouble(date.timeIntervalSince1970 * 1000) - #if !os(WASI) - 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: TLVDateFormatting.iso8601Formatter) - case let .formatted(formatter): - return boxDate(date, using: formatter) - #endif - } - } - - #if !os(WASI) - func boxDate (_ date: Date, using formatter: T) -> Data { - return box(formatter.string(from: date)) - } - #endif -} - -// MARK: - Stack - -internal extension TLVEncoder.Encoder { - - struct Stack { - - private(set) var containers = [Container]() - - fileprivate init() { } - - var top: Container { - - guard let container = containers.last - else { fatalError("Empty container stack.") } - - return container - } - - var root: Container { - - guard let container = containers.first - else { fatalError("Empty container stack.") } - - return container - } - - mutating func push(_ container: Container) { - - containers.append(container) - } - - @discardableResult - mutating func pop() -> Container { - - guard let container = containers.popLast() - else { fatalError("Empty container stack.") } - - return container - } - } -} - -internal extension TLVEncoder.Encoder { - - final class ItemsContainer { - - 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 { - - var data: Data - - init(_ data: Data = Data()) { - - self.data = data - } - } - - enum Container { - - case items(ItemsContainer) - case item(ItemContainer) - - var data: Data { - - switch self { - case let .items(container): - return container.data - case let .item(container): - return container.data - } - } - } -} - -// MARK: - KeyedEncodingContainerProtocol - -internal final class TLVKeyedContainer : KeyedEncodingContainerProtocol { - - typealias Key = K - - // MARK: - Properties - - /// A reference to the encoder we're writing to. - let encoder: TLVEncoder.Encoder - - /// The path of coding keys taken to get to this point in encoding. - let codingPath: [CodingKey] - - /// A reference to the container we're writing to. - let container: TLVEncoder.Encoder.ItemsContainer - - // MARK: - Initialization - - init(referencing encoder: TLVEncoder.Encoder, - wrapping container: TLVEncoder.Encoder.ItemsContainer) { - - self.encoder = encoder - self.codingPath = encoder.codingPath - self.container = container - } - - // MARK: - Methods - - func encodeNil(forKey key: K) throws { - // do nothing - } - - func encode(_ value: Bool, forKey key: K) throws { - try encodeTLV(value, forKey: key) - } - - func encode(_ value: Int, forKey key: K) throws { - try encodeNumeric(Int32(value), forKey: key) - } - - func encode(_ value: Int8, forKey key: K) throws { - try encodeTLV(value, forKey: key) - } - - func encode(_ value: Int16, forKey key: K) throws { - try encodeNumeric(value, forKey: key) - } - - func encode(_ value: Int32, forKey key: K) throws { - try encodeNumeric(value, forKey: key) - } - - func encode(_ value: Int64, forKey key: K) throws { - try encodeNumeric(value, forKey: key) - } - - func encode(_ value: UInt, forKey key: K) throws { - try encodeNumeric(UInt32(value), forKey: key) - } - - func encode(_ value: UInt8, forKey key: K) throws { - try encodeTLV(value, forKey: key) - } - - func encode(_ value: UInt16, forKey key: K) throws { - try encodeNumeric(value, forKey: key) - } - - func encode(_ value: UInt32, forKey key: K) throws { - try encodeNumeric(value, forKey: key) - } - - func encode(_ value: UInt64, forKey key: K) throws { - try encodeNumeric(value, forKey: key) - } - - func encode(_ value: Float, forKey key: K) throws { - try encodeNumeric(value.bitPattern, forKey: key) - } - - func encode(_ value: Double, forKey key: K) throws { - try encodeNumeric(value.bitPattern, forKey: key) - } - - func encode(_ value: String, forKey key: K) throws { - try encodeTLV(value, forKey: key) - } - - func encode (_ value: T, forKey key: K) throws { - - self.encoder.codingPath.append(key) - defer { self.encoder.codingPath.removeLast() } - let data = try encoder.boxEncodable(value) - try setValue(value, data: data, for: key) - } - - func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer where NestedKey : CodingKey { - - fatalError() - } - - func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer { - - fatalError() - } - - func superEncoder() -> Encoder { - - fatalError() - } - - func superEncoder(forKey key: K) -> Encoder { - - fatalError() - } - - // MARK: - Private Methods - - private func encodeNumeric (_ value: T, forKey key: K) throws { - - self.encoder.codingPath.append(key) - defer { self.encoder.codingPath.removeLast() } - let data = encoder.boxNumeric(value) - try setValue(value, data: data, for: key) - } - - private func encodeTLV (_ value: T, forKey key: K) throws { - - self.encoder.codingPath.append(key) - defer { self.encoder.codingPath.removeLast() } - let data = encoder.box(value) - try setValue(value, data: data, for: key) - } - - private func setValue (_ value: T, data: Data, for key: Key) throws { - - encoder.log?("Will encode value for key \(key.stringValue) at path \"\(encoder.codingPath.path)\"") - - let type = try encoder.typeCode(for: key, value: value) - let item = TLVItem(type: type, value: data) - self.container.append(item, options: encoder.options) - } -} - -// MARK: - SingleValueEncodingContainer - -internal final class TLVSingleValueEncodingContainer: SingleValueEncodingContainer { - - // MARK: - Properties - - /// A reference to the encoder we're writing to. - let encoder: TLVEncoder.Encoder - - /// The path of coding keys taken to get to this point in encoding. - let codingPath: [CodingKey] - - /// A reference to the container we're writing to. - let container: TLVEncoder.Encoder.ItemContainer - - /// Whether the data has been written - private var didWrite = false - - // MARK: - Initialization - - init(referencing encoder: TLVEncoder.Encoder, - wrapping container: TLVEncoder.Encoder.ItemContainer) { - - self.encoder = encoder - self.codingPath = encoder.codingPath - self.container = container - } - - // MARK: - Methods - - func encodeNil() throws { - // do nothing - } - - func encode(_ value: Bool) throws { write(encoder.box(value)) } - - func encode(_ value: String) throws { write(encoder.box(value)) } - - func encode(_ value: Double) throws { write(encoder.boxDouble(value)) } - - func encode(_ value: Float) throws { write(encoder.boxFloat(value)) } - - func encode(_ value: Int) throws { write(encoder.boxNumeric(Int32(value))) } - - func encode(_ value: Int8) throws { write(encoder.box(value)) } - - func encode(_ value: Int16) throws { write(encoder.boxNumeric(value)) } - - func encode(_ value: Int32) throws { write(encoder.boxNumeric(value)) } - - func encode(_ value: Int64) throws { write(encoder.boxNumeric(value)) } - - func encode(_ value: UInt) throws { write(encoder.boxNumeric(UInt32(value))) } - - func encode(_ value: UInt8) throws { write(encoder.box(value)) } - - func encode(_ value: UInt16) throws { write(encoder.boxNumeric(value)) } - - func encode(_ value: UInt32) throws { write(encoder.boxNumeric(value)) } - - func encode(_ value: UInt64) throws { write(encoder.boxNumeric(value)) } - - func encode (_ value: T) throws { write(try encoder.boxEncodable(value)) } - - // MARK: - Private Methods - - private func write(_ data: Data) { - - precondition(didWrite == false, "Data already written") - self.container.data = data - self.didWrite = true - } -} - -// MARK: - UnkeyedEncodingContainer - -internal final class TLVUnkeyedEncodingContainer: UnkeyedEncodingContainer { - - // MARK: - Properties - - /// A reference to the encoder we're writing to. - let encoder: TLVEncoder.Encoder - - /// The path of coding keys taken to get to this point in encoding. - let codingPath: [CodingKey] - - /// A reference to the container we're writing to. - let container: TLVEncoder.Encoder.ItemsContainer - - // MARK: - Initialization - - init(referencing encoder: TLVEncoder.Encoder, - wrapping container: TLVEncoder.Encoder.ItemsContainer) { - - self.encoder = encoder - self.codingPath = encoder.codingPath - self.container = container - } - - // MARK: - Methods - - /// The number of elements encoded into the container. - var count: Int { - return container.items.count - } - - func encodeNil() throws { - // do nothing - } - - func encode(_ value: Bool) throws { append(encoder.box(value)) } - - func encode(_ value: String) throws { append(encoder.box(value)) } - - func encode(_ value: Double) throws { append(encoder.boxNumeric(value.bitPattern)) } - - func encode(_ value: Float) throws { append(encoder.boxNumeric(value.bitPattern)) } - - func encode(_ value: Int) throws { append(encoder.boxNumeric(Int32(value))) } - - func encode(_ value: Int8) throws { append(encoder.box(value)) } - - func encode(_ value: Int16) throws { append(encoder.boxNumeric(value)) } - - func encode(_ value: Int32) throws { append(encoder.boxNumeric(value)) } - - func encode(_ value: Int64) throws { append(encoder.boxNumeric(value)) } - - func encode(_ value: UInt) throws { append(encoder.boxNumeric(UInt32(value))) } - - func encode(_ value: UInt8) throws { append(encoder.box(value)) } - - func encode(_ value: UInt16) throws { append(encoder.boxNumeric(value)) } - - func encode(_ value: UInt32) throws { append(encoder.boxNumeric(value)) } - - func encode(_ value: UInt64) throws { append(encoder.boxNumeric(value)) } - - func encode (_ value: T) throws { append(try encoder.boxEncodable(value)) } - - func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { - - fatalError() - } - - func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - - fatalError() - } - - func superEncoder() -> Encoder { - - fatalError() - } - - // MARK: - Private Methods - - private func append(_ data: Data) { - - let index = TLVTypeCode(rawValue: UInt8(count)) // current index - let item = TLVItem(type: index, value: data) - - // write - self.container.append(item) // already sorted - } -} - -// MARK: - Data Types - -/// 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: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension UInt16: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension UInt32: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension UInt64: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension Int8: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension Int16: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension Int32: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension Int64: TLVRawEncodable { - - public var tlvData: Data { - return copyingBytes - } -} - -extension Float: TLVRawEncodable { - - public var tlvData: Data { - return bitPattern.copyingBytes - } -} - -extension Double: TLVRawEncodable { - - public var tlvData: Data { - return bitPattern.copyingBytes - } -} - -extension Bool: TLVRawEncodable { - - public var tlvData: Data { - return UInt8(self ? 1 : 0).copyingBytes - } -} - -extension String: TLVRawEncodable { - - public var tlvData: Data { - return Data(self.utf8) - } -} diff --git a/Sources/Format.swift b/Sources/Format.swift deleted file mode 100644 index 4cd4a3d..0000000 --- a/Sources/Format.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// NumericFormat.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 5/13/19. -// Copyright © 2019 PureSwift. All rights reserved. -// - -import Foundation - -/// 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 littleEndian - case bigEndian -} - -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 -} - -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 - - /// Encodes dates in terms of milliseconds since midnight UTC on January 1, 1970. - case millisecondsSince1970 - - #if !os(WASI) - /// 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) - #endif -} - -public extension TLVDateFormatting { - - /// The default TLV `Date` format. - static var `default`: TLVDateFormatting { - return .secondsSince1970 - } -} - -@available(*, deprecated, message: "Renamed to TLVDateFormatting") -public typealias TLVDateFormat = TLVDateFormatting - -// MARK: - Formatters - -#if !os(WASI) -internal protocol DateFormatterProtocol: AnyObject { - - func string(from date: Date) -> String - - func date(from string: String) -> Date? -} - -internal extension TLVDateFormatting { - - @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 - }() -} - -extension DateFormatter: DateFormatterProtocol { } - -@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) -extension ISO8601DateFormatter: DateFormatterProtocol { } -#endif diff --git a/Sources/Integer.swift b/Sources/Integer.swift deleted file mode 100644 index 1c34038..0000000 --- a/Sources/Integer.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// Integer.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 5/12/19. -// Copyright © 2019 PureSwift. All rights reserved. -// - -internal extension UInt16 { - - /// Initializes value from two bytes. - init(bytes: (UInt8, UInt8)) { - - self = unsafeBitCast(bytes, to: UInt16.self) - } - - /// Converts to two bytes. - var bytes: (UInt8, UInt8) { - - return unsafeBitCast(self, to: (UInt8, UInt8).self) - } -} - -internal extension UInt32 { - - /// Initializes value from four bytes. - init(bytes: (UInt8, UInt8, UInt8, UInt8)) { - - self = unsafeBitCast(bytes, to: UInt32.self) - } - - /// Converts to four bytes. - var bytes: (UInt8, UInt8, UInt8, UInt8) { - - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8).self) - } -} - -internal extension UInt64 { - - /// Initializes value from four bytes. - init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) { - - self = unsafeBitCast(bytes, to: UInt64.self) - } - - /// Converts to eight bytes. - var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) { - - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self) - } -} - -internal extension Int16 { - - /// Initializes value from two bytes. - init(bytes: (UInt8, UInt8)) { - - self = unsafeBitCast(bytes, to: Int16.self) - } - - /// Converts to two bytes. - var bytes: (UInt8, UInt8) { - - return unsafeBitCast(self, to: (UInt8, UInt8).self) - } -} - -internal extension Int32 { - - /// Initializes value from four bytes. - init(bytes: (UInt8, UInt8, UInt8, UInt8)) { - - self = unsafeBitCast(bytes, to: Int32.self) - } - - /// Converts to four bytes. - var bytes: (UInt8, UInt8, UInt8, UInt8) { - - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8).self) - } -} - -internal extension Int64 { - - /// Initializes value from four bytes. - init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) { - - self = unsafeBitCast(bytes, to: Int64.self) - } - - /// Converts to eight bytes. - var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) { - - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self) - } -} diff --git a/Sources/Item.swift b/Sources/Item.swift deleted file mode 100644 index 6290a1b..0000000 --- a/Sources/Item.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Item.swift -// TLVCoding -// -// Created by Alsey Coleman Miller on 5/12/19. -// Copyright © 2019 PureSwift. All rights reserved. -// - -import Foundation - -/** - TLV8 (Type-length-value) Item - */ -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 - } -} - -public extension TLVItem { - - var length: UInt8 { - return UInt8(value.count) - } -} - -public extension TLVItem { - - var data: Data { - return Data(self) - } -} - -// MARK: - DataConvertible - -extension TLVItem: DataConvertible { - - var dataLength: Int { - - return 1 + 1 + value.count - } - - static func += (data: inout T, value: TLVItem) { - - data += value.type.rawValue - data += value.length - data += value.value - } -} - diff --git a/Sources/TLVCodable.swift b/Sources/TLVCodable.swift deleted file mode 100644 index aa0f982..0000000 --- a/Sources/TLVCodable.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TLVCoding.swift -// PureSwift -// -// Created by Alsey Coleman Miller on 3/8/18. -// Copyright © 2018 PureSwift. All rights reserved. -// - -import Foundation - -/// Type-Length-Value Codable -public typealias TLVCodable = TLVEncodable & TLVDecodable - -/// TLV Decodable type -public protocol TLVDecodable: Decodable { - - init?(tlvData: Data) -} - -/// TLV Encodable type -public protocol TLVEncodable: Encodable { - - var tlvData: Data { get } -} diff --git a/Sources/TLVCoding/CodingKey.swift b/Sources/TLVCoding/CodingKey.swift new file mode 100644 index 0000000..8264bfc --- /dev/null +++ b/Sources/TLVCoding/CodingKey.swift @@ -0,0 +1,49 @@ +// +// CodingKey.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 5/12/19. +// Copyright © 2019 PureSwift. All rights reserved. +// + +/// TLV Coding Key +/// +/// A key identifying a TLV8 item within a container. +/// Typically implemented as a `UInt8` raw value enum: +/// +/// ```swift +/// enum CodingKeys: UInt8, TLVCodingKey { +/// case state = 0x01 +/// case result = 0x02 +/// } +/// ``` +public protocol TLVCodingKey: Sendable { + + init?(code: TLVTypeCode) + + var code: TLVTypeCode { get } +} + +public extension TLVCodingKey where Self: RawRepresentable, Self.RawValue == TLVTypeCode.RawValue { + + init?(code: TLVTypeCode) { + self.init(rawValue: code.rawValue) + } + + var code: TLVTypeCode { + TLVTypeCode(rawValue: rawValue) + } +} + +// MARK: - TLVTypeCode + +extension TLVTypeCode: TLVCodingKey { + + public init(code: TLVTypeCode) { + self = code + } + + public var code: TLVTypeCode { + self + } +} diff --git a/Sources/TLVCoding/Container.swift b/Sources/TLVCoding/Container.swift new file mode 100644 index 0000000..65c5409 --- /dev/null +++ b/Sources/TLVCoding/Container.swift @@ -0,0 +1,158 @@ +// +// Container.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +#if canImport(FoundationEssentials) && !hasFeature(Embedded) +import FoundationEssentials +#elseif canImport(Foundation) && !hasFeature(Embedded) +import Foundation +#endif + +/** + TLV8 keyed coding container. + + An ordered collection of ``TLVItem`` values used to explicitly encode and decode + structured types without `Codable`. + + ```swift + // Encoding + var container = TLVContainer() + container.encode(gender, forKey: CodingKeys.gender) + container.encode(name, forKey: CodingKeys.name) + let data = container.data + + // Decoding + guard let container = TLVContainer(data: data), + let gender = container.decode(Gender.self, forKey: CodingKeys.gender), + let name = container.decode(String.self, forKey: CodingKeys.name) + else { return nil } + ``` + */ +public struct TLVContainer: Equatable, Hashable, Sendable { + + /// TLV items, in encoding order. + public var items: [TLVItem] + + /// Initialize an empty container. + public init() { + self.items = [] + } + + /// Initialize with the specified items. + public init(items: [TLVItem]) { + self.items = items + } + + /// Parse a container from TLV8 data. + public init?(data: Data) { + var items = [TLVItem]() + var index = data.startIndex + while index < data.endIndex { + // read type and length bytes + let type = TLVTypeCode(rawValue: data[index]) + index = data.index(after: index) + guard index < data.endIndex else { + return nil // missing length byte + } + let length = Int(data[index]) + index = data.index(after: index) + // read value payload + guard let valueEnd = data.index(index, offsetBy: length, limitedBy: data.endIndex) else { + return nil // truncated value + } + items.append(TLVItem(type: type, value: Data(data[index ..< valueEnd]))) + index = valueEnd + } + self.items = items + } +} + +public extension TLVContainer { + + /// Serialized TLV8 data. + var data: Data { + var data = Data() + data.reserveCapacity(items.reduce(0) { $0 + $1.dataLength }) + for item in items { + item.append(to: &data) + } + return data + } +} + +// MARK: - Keyed Access + +public extension TLVContainer { + + /// The value payload of the first item with the specified type code, if any. + subscript(code: TLVTypeCode) -> Data? { + items.first(where: { $0.type == code })?.value + } + + /// Whether the container includes an item for the specified key. + func contains(_ key: K) -> Bool { + items.contains(where: { $0.type == key.code }) + } +} + +// MARK: - Encoding + +public extension TLVContainer { + + /// Append the encoded value for the specified key. + mutating func encode(_ value: T, forKey key: K) { + items.append(TLVItem(type: key.code, value: value.tlvData)) + } + + /// Append the encoded value for the specified key, skipping `nil`. + mutating func encode(_ value: T?, forKey key: K) { + guard let value else { return } + encode(value, forKey: key) + } +} + +// MARK: - Decoding + +public extension TLVContainer { + + /// Decode the value for the specified key. + /// + /// Returns `nil` if the key is missing or the payload is invalid for the specified type. + func decode(_ type: T.Type, forKey key: K) -> T? { + guard let value = self[key.code] else { + return nil + } + return T(tlvData: value) + } + + /// Decode an optional value for the specified key. + /// + /// Returns `.some(nil)` if the key is missing, + /// `nil` if the payload is present but invalid for the specified type. + func decodeIfPresent(_ type: T.Type, forKey key: K) -> T?? { + guard let value = self[key.code] else { + return .some(nil) + } + guard let decoded = T(tlvData: value) else { + return nil + } + return .some(decoded) + } +} + +// MARK: - TLVCodable + +extension TLVContainer: TLVCodable { + + public init?(tlvData: Data) { + self.init(data: tlvData) + } + + public var tlvData: Data { + data + } +} diff --git a/Sources/TLVCoding/Data.swift b/Sources/TLVCoding/Data.swift new file mode 100644 index 0000000..a43181c --- /dev/null +++ b/Sources/TLVCoding/Data.swift @@ -0,0 +1,75 @@ +// +// Data.swift +// TLVCoding +// +// Minimal Foundation-free `Data` for platforms without Foundation +// (e.g. Embedded Swift). Not API-complete — TLV round-tripping only. +// + +#if (!canImport(FoundationEssentials) && !canImport(Foundation)) || hasFeature(Embedded) + +public struct Data: Sendable { + + internal var bytes: [UInt8] + + public init() { + self.bytes = [] + } + + public init(_ elements: S) where S.Element == UInt8 { + self.bytes = Array(elements) + } + + public init(repeating byte: UInt8, count: Int) { + self.bytes = Array(repeating: byte, count: count) + } + + public mutating func reserveCapacity(_ capacity: Int) { + bytes.reserveCapacity(capacity) + } +} + +// MARK: - Collection + +extension Data: RandomAccessCollection, MutableCollection { + + public typealias Element = UInt8 + public typealias Index = Int + + public var startIndex: Int { bytes.startIndex } + public var endIndex: Int { bytes.endIndex } + + public subscript(position: Int) -> UInt8 { + get { bytes[position] } + set { bytes[position] = newValue } + } + + public func index(after i: Int) -> Int { bytes.index(after: i) } + public func index(before i: Int) -> Int { bytes.index(before: i) } +} + +extension Data { + + public mutating func append(_ byte: UInt8) { + bytes.append(byte) + } + + public mutating func append(contentsOf newElements: S) where S.Element == UInt8 { + bytes.append(contentsOf: newElements) + } +} + +// MARK: - Equatable, Hashable + +extension Data: Equatable, Hashable { + + public static func == (lhs: Data, rhs: Data) -> Bool { + lhs.bytes == rhs.bytes + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(bytes) + } +} + +#endif diff --git a/Sources/TLVCoding/Foundation.swift b/Sources/TLVCoding/Foundation.swift new file mode 100644 index 0000000..926c3da --- /dev/null +++ b/Sources/TLVCoding/Foundation.swift @@ -0,0 +1,55 @@ +// +// Foundation.swift +// TLVCoding +// +// Foundation-only conformances, unavailable under Embedded Swift. +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +#if (canImport(FoundationEssentials) || canImport(Foundation)) && !hasFeature(Embedded) + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// MARK: - UUID + +/// `UUID` is encoded as its 16 byte value. +extension UUID: TLVCodable { + + public init?(tlvData: Data) { + guard tlvData.count == 16 else { return nil } + let bytes = [UInt8](tlvData) + self.init(uuid: ( + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + bytes[8], bytes[9], bytes[10], bytes[11], + bytes[12], bytes[13], bytes[14], bytes[15] + )) + } + + public var tlvData: Data { + withUnsafeBytes(of: uuid) { Data($0) } + } +} + +// MARK: - Date + +/// `Date` is encoded as a `Double` of seconds since midnight UTC on January 1, 1970. +extension Date: TLVCodable { + + public init?(tlvData: Data) { + guard let timeInterval = Double(tlvData: tlvData) else { return nil } + self.init(timeIntervalSince1970: timeInterval) + } + + public var tlvData: Data { + timeIntervalSince1970.tlvData + } +} + +#endif diff --git a/Sources/TLVCoding/Item.swift b/Sources/TLVCoding/Item.swift new file mode 100644 index 0000000..8430492 --- /dev/null +++ b/Sources/TLVCoding/Item.swift @@ -0,0 +1,58 @@ +// +// Item.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 5/12/19. +// Copyright © 2019 PureSwift. All rights reserved. +// + +#if canImport(FoundationEssentials) && !hasFeature(Embedded) +import FoundationEssentials +#elseif canImport(Foundation) && !hasFeature(Embedded) +import Foundation +#endif + +/** + TLV8 (Type-length-value) Item + */ +public struct TLVItem: Equatable, Hashable, Sendable { + + /// 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 + } +} + +public extension TLVItem { + + /// Length of the value payload. + var length: UInt8 { + UInt8(value.count) + } + + /// Data representation (type byte, length byte, value payload). + var data: Data { + var data = Data() + data.reserveCapacity(dataLength) + append(to: &data) + return data + } + + /// Length of the item when encoded into data. + internal var dataLength: Int { + 2 + value.count + } + + internal func append(to data: inout Data) { + data.append(type.rawValue) + data.append(length) + data.append(contentsOf: value) + } +} diff --git a/Sources/TLVCoding/Macros.swift b/Sources/TLVCoding/Macros.swift new file mode 100644 index 0000000..aa7d35c --- /dev/null +++ b/Sources/TLVCoding/Macros.swift @@ -0,0 +1,37 @@ +// +// Macros.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +#if !hasFeature(Embedded) && SWIFTPM_ENABLE_MACROS +/// Generates `TLVCodable` conformance for a struct. +/// +/// Adds a `CodingKeys` enum (unless one is declared), an `init?(tlvData:)` initializer, +/// and a `tlvData` computed property that encode the stored properties in declaration order. +/// +/// ```swift +/// @TLVCodable +/// struct ProvisioningState: Equatable { +/// +/// var state: State +/// +/// var result: Result +/// +/// enum CodingKeys: UInt8, TLVCodingKey { +/// case state = 0x01 +/// case result = 0x02 +/// } +/// } +/// ``` +/// +/// Under Embedded Swift, write the equivalent conformance by hand. +@attached(member, names: named(CodingKeys), named(init(tlvData:)), named(tlvData)) +@attached(extension, conformances: TLVEncodable, TLVDecodable) +public macro TLVCodable() = #externalMacro( + module: "TLVCodingMacros", + type: "TLVCodableMacro" +) +#endif diff --git a/Sources/TLVCoding/Primitives.swift b/Sources/TLVCoding/Primitives.swift new file mode 100644 index 0000000..bd5f6f8 --- /dev/null +++ b/Sources/TLVCoding/Primitives.swift @@ -0,0 +1,260 @@ +// +// Primitives.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +#if canImport(FoundationEssentials) && !hasFeature(Embedded) +import FoundationEssentials +#elseif canImport(Foundation) && !hasFeature(Embedded) +import Foundation +#endif + +// MARK: - FixedWidthInteger + +internal extension FixedWidthInteger { + + /// Initialize from a little-endian TLV payload of exactly `MemoryLayout.size` bytes. + init?(littleEndianTLV tlvData: Data) { + guard tlvData.count == MemoryLayout.size else { + return nil + } + var value = Self.zero + withUnsafeMutableBytes(of: &value) { buffer in + var index = tlvData.startIndex + for offset in 0 ..< buffer.count { + buffer[offset] = tlvData[index] + index = tlvData.index(after: index) + } + } + self.init(littleEndian: value) + } + + /// Little-endian TLV payload. + var littleEndianTLV: Data { + withUnsafeBytes(of: littleEndian) { Data($0) } + } +} + +// MARK: - Integers + +extension UInt8: TLVCodable { + + public init?(tlvData: Data) { + guard tlvData.count == 1 else { return nil } + self = tlvData[tlvData.startIndex] + } + + public var tlvData: Data { + Data([self]) + } +} + +extension UInt16: TLVCodable { + + public init?(tlvData: Data) { + self.init(littleEndianTLV: tlvData) + } + + public var tlvData: Data { + littleEndianTLV + } +} + +extension UInt32: TLVCodable { + + public init?(tlvData: Data) { + self.init(littleEndianTLV: tlvData) + } + + public var tlvData: Data { + littleEndianTLV + } +} + +extension UInt64: TLVCodable { + + public init?(tlvData: Data) { + self.init(littleEndianTLV: tlvData) + } + + public var tlvData: Data { + littleEndianTLV + } +} + +extension Int8: TLVCodable { + + public init?(tlvData: Data) { + guard tlvData.count == 1 else { return nil } + self = Int8(bitPattern: tlvData[tlvData.startIndex]) + } + + public var tlvData: Data { + Data([UInt8(bitPattern: self)]) + } +} + +extension Int16: TLVCodable { + + public init?(tlvData: Data) { + self.init(littleEndianTLV: tlvData) + } + + public var tlvData: Data { + littleEndianTLV + } +} + +extension Int32: TLVCodable { + + public init?(tlvData: Data) { + self.init(littleEndianTLV: tlvData) + } + + public var tlvData: Data { + littleEndianTLV + } +} + +extension Int64: TLVCodable { + + public init?(tlvData: Data) { + self.init(littleEndianTLV: tlvData) + } + + public var tlvData: Data { + littleEndianTLV + } +} + +/// `Int` is encoded as a 32-bit little-endian integer for a stable wire format across platforms. +extension Int: TLVCodable { + + public init?(tlvData: Data) { + guard let value = Int32(littleEndianTLV: tlvData) else { return nil } + self.init(value) + } + + public var tlvData: Data { + Int32(self).littleEndianTLV + } +} + +/// `UInt` is encoded as a 32-bit little-endian integer for a stable wire format across platforms. +extension UInt: TLVCodable { + + public init?(tlvData: Data) { + guard let value = UInt32(littleEndianTLV: tlvData) else { return nil } + self.init(value) + } + + public var tlvData: Data { + UInt32(self).littleEndianTLV + } +} + +// MARK: - Bool + +extension Bool: TLVCodable { + + public init?(tlvData: Data) { + guard tlvData.count == 1 else { return nil } + self = tlvData[tlvData.startIndex] != 0 + } + + public var tlvData: Data { + Data([self ? 1 : 0]) + } +} + +// MARK: - Floating Point + +extension Float: TLVCodable { + + public init?(tlvData: Data) { + guard let bitPattern = UInt32(littleEndianTLV: tlvData) else { return nil } + self.init(bitPattern: bitPattern) + } + + public var tlvData: Data { + bitPattern.littleEndianTLV + } +} + +extension Double: TLVCodable { + + public init?(tlvData: Data) { + guard let bitPattern = UInt64(littleEndianTLV: tlvData) else { return nil } + self.init(bitPattern: bitPattern) + } + + public var tlvData: Data { + bitPattern.littleEndianTLV + } +} + +// MARK: - String + +extension String: TLVCodable { + + public init?(tlvData: Data) { + self.init(decoding: tlvData, as: UTF8.self) + } + + public var tlvData: Data { + Data(utf8) + } +} + +// MARK: - Data + +extension Data: TLVCodable { + + public init?(tlvData: Data) { + self = tlvData + } + + public var tlvData: Data { + self + } +} + +// MARK: - Array + +extension Array: TLVEncodable where Element: TLVEncodable { + + /// Encodes each element as a TLV item keyed by its index. + public var tlvData: Data { + var container = TLVContainer() + for (index, element) in enumerated() { + container.items.append( + TLVItem( + type: TLVTypeCode(rawValue: UInt8(index)), + value: element.tlvData + ) + ) + } + return container.data + } +} + +extension Array: TLVDecodable where Element: TLVDecodable { + + public init?(tlvData: Data) { + guard let container = TLVContainer(data: tlvData) else { + return nil + } + var elements = [Element]() + elements.reserveCapacity(container.items.count) + for item in container.items { + guard let element = Element(tlvData: item.value) else { + return nil + } + elements.append(element) + } + self = elements + } +} diff --git a/Sources/TLVCoding/TLVCodable.swift b/Sources/TLVCoding/TLVCodable.swift new file mode 100644 index 0000000..fd11f4d --- /dev/null +++ b/Sources/TLVCoding/TLVCodable.swift @@ -0,0 +1,53 @@ +// +// TLVCoding.swift +// PureSwift +// +// Created by Alsey Coleman Miller on 3/8/18. +// Copyright © 2018 PureSwift. All rights reserved. +// + +#if canImport(FoundationEssentials) && !hasFeature(Embedded) +@_exported import struct FoundationEssentials.Data +#elseif canImport(Foundation) && !hasFeature(Embedded) +@_exported import struct Foundation.Data +#endif + +/// Type-Length-Value Codable +public typealias TLVCodable = TLVEncodable & TLVDecodable + +/// TLV Decodable type +/// +/// A type that can initialize itself from a TLV8 binary payload. +/// Conformances are either written by hand or generated with the `@TLVCodable` macro. +public protocol TLVDecodable { + + init?(tlvData: Data) +} + +/// TLV Encodable type +/// +/// A type that can encode itself into a TLV8 binary payload. +/// Conformances are either written by hand or generated with the `@TLVCodable` macro. +public protocol TLVEncodable { + + var tlvData: Data { get } +} + +// MARK: - RawRepresentable + +public extension TLVEncodable where Self: RawRepresentable, RawValue: TLVEncodable { + + var tlvData: Data { + rawValue.tlvData + } +} + +public extension TLVDecodable where Self: RawRepresentable, RawValue: TLVDecodable { + + init?(tlvData: Data) { + guard let rawValue = RawValue(tlvData: tlvData) else { + return nil + } + self.init(rawValue: rawValue) + } +} diff --git a/Sources/TypeCode.swift b/Sources/TLVCoding/TypeCode.swift similarity index 62% rename from Sources/TypeCode.swift rename to Sources/TLVCoding/TypeCode.swift index 2ceb29f..f2d86ce 100644 --- a/Sources/TypeCode.swift +++ b/Sources/TLVCoding/TypeCode.swift @@ -6,15 +6,21 @@ // Copyright © 2019 PureSwift. All rights reserved. // -import Foundation - /// TLV8 type code -public struct TLVTypeCode: RawRepresentable, Equatable, Hashable { - +public struct TLVTypeCode: RawRepresentable, Equatable, Hashable, Sendable { + public let rawValue: UInt8 - + public init(rawValue: UInt8) { - self.rawValue = rawValue } } + +// MARK: - ExpressibleByIntegerLiteral + +extension TLVTypeCode: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: UInt8) { + self.init(rawValue: value) + } +} diff --git a/Sources/TLVCodingMacros/Error.swift b/Sources/TLVCodingMacros/Error.swift new file mode 100644 index 0000000..72e32c4 --- /dev/null +++ b/Sources/TLVCodingMacros/Error.swift @@ -0,0 +1,26 @@ +// +// Error.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +/// TLVCoding macro error +enum MacroError: Error, CustomStringConvertible { + + case invalidType + case noStoredProperties + case missingTypeAnnotation(for: String) + + var description: String { + switch self { + case .invalidType: + return "@TLVCodable can only be applied to a struct" + case .noStoredProperties: + return "@TLVCodable requires at least one stored property" + case let .missingTypeAnnotation(propertyName): + return "Stored property '\(propertyName)' requires an explicit type annotation" + } + } +} diff --git a/Sources/TLVCodingMacros/Plugin.swift b/Sources/TLVCodingMacros/Plugin.swift new file mode 100644 index 0000000..103607f --- /dev/null +++ b/Sources/TLVCodingMacros/Plugin.swift @@ -0,0 +1,18 @@ +// +// Plugin.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct TLVCodingMacrosPlugin: CompilerPlugin { + + let providingMacros: [Macro.Type] = [ + TLVCodableMacro.self + ] +} diff --git a/Sources/TLVCodingMacros/TLVCodable.swift b/Sources/TLVCodingMacros/TLVCodable.swift new file mode 100644 index 0000000..f3205ef --- /dev/null +++ b/Sources/TLVCodingMacros/TLVCodable.swift @@ -0,0 +1,222 @@ +// +// TLVCodable.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +import Foundation +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +/// `@TLVCodable` macro +/// +/// Adds `TLVCodable` protocol conformance with a generated `CodingKeys` enum, +/// `init?(tlvData:)` initializer and `tlvData` computed property. +public struct TLVCodableMacro: MemberMacro, ExtensionMacro { + + // Add protocol conformance via extension + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + let extensionDecl = ExtensionDeclSyntax( + leadingTrivia: nil, + attributes: [], + modifiers: [], + extensionKeyword: .keyword(.extension), + extendedType: TypeSyntax(type), + inheritanceClause: InheritanceClauseSyntax( + colon: .colonToken(trailingTrivia: .space), + inheritedTypes: InheritedTypeListSyntax { + InheritedTypeSyntax( + type: TypeSyntax(stringLiteral: "TLVCoding.TLVCodable") + ) + } + ), + genericWhereClause: nil, + memberBlock: MemberBlockSyntax(members: []) + ) + return [extensionDecl] + } + + // Add members + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard declaration.is(StructDeclSyntax.self) else { + throw MacroError.invalidType + } + let properties = try storedProperties(of: declaration) + guard properties.isEmpty == false else { + throw MacroError.noStoredProperties + } + var declarations = [DeclSyntax]() + if hasCodingKeys(declaration) == false { + declarations.append(codingKeysDeclarationSyntax(for: properties, of: declaration)) + } + declarations.append(initDeclarationSyntax(for: properties, of: declaration)) + declarations.append(tlvDataDeclarationSyntax(for: properties, of: declaration)) + return declarations + } +} + +extension TLVCodableMacro { + + /// A stored property to encode or decode. + struct Property { + + /// Property name. + let name: String + + /// Type name, without optional suffix. + let type: String + + /// Whether the declared type is optional. + let isOptional: Bool + } + + /// Collects stored instance properties in declaration order. + static func storedProperties( + of declaration: some DeclGroupSyntax + ) throws -> [Property] { + var properties = [Property]() + for member in declaration.memberBlock.members { + guard let varDecl = member.decl.as(VariableDeclSyntax.self), + let binding = varDecl.bindings.first, + let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text + else { continue } + // skip static and computed properties + guard varDecl.modifiers.contains(where: { $0.name.tokenKind == .keyword(.static) }) == false, + binding.accessorBlock == nil + else { continue } + // skip constants with a default value, which cannot be assigned in an initializer + if varDecl.bindingSpecifier.tokenKind == .keyword(.let), binding.initializer != nil { + continue + } + guard let typeSyntax = binding.typeAnnotation?.type else { + throw MacroError.missingTypeAnnotation(for: identifier) + } + let rawTypeName = typeSyntax.description.trimmingCharacters(in: .whitespacesAndNewlines) + // Strip Optional + let typeName: String + let isOptional: Bool + if rawTypeName.hasPrefix("Optional<"), rawTypeName.hasSuffix(">") { + typeName = String(rawTypeName.dropFirst("Optional<".count).dropLast()) + .trimmingCharacters(in: .whitespacesAndNewlines) + isOptional = true + } else if rawTypeName.hasSuffix("?") { + typeName = String(rawTypeName.dropLast()).trimmingCharacters(in: .whitespacesAndNewlines) + isOptional = true + } else { + typeName = rawTypeName + isOptional = false + } + properties.append(Property(name: identifier, type: typeName, isOptional: isOptional)) + } + return properties + } + + /// Whether the declaration already defines `CodingKeys`. + static func hasCodingKeys( + _ declaration: some DeclGroupSyntax + ) -> Bool { + for member in declaration.memberBlock.members { + if let enumDecl = member.decl.as(EnumDeclSyntax.self), + enumDecl.name.text == "CodingKeys" { + return true + } + if let typealiasDecl = member.decl.as(TypeAliasDeclSyntax.self), + typealiasDecl.name.text == "CodingKeys" { + return true + } + } + return false + } + + /// Access control prefix for generated members (`public `, `package ` or empty). + static func accessPrefix( + of declaration: some DeclGroupSyntax + ) -> String { + for modifier in declaration.modifiers { + switch modifier.name.tokenKind { + case .keyword(.public), .keyword(.open): + return "public " + case .keyword(.package): + return "package " + default: + continue + } + } + return "" + } + + static func codingKeysDeclarationSyntax( + for properties: [Property], + of declaration: some DeclGroupSyntax + ) -> DeclSyntax { + let access = accessPrefix(of: declaration) + var lines = [String]() + lines.append("\(access)enum CodingKeys: UInt8, TLVCoding.TLVCodingKey {") + for (index, property) in properties.enumerated() { + lines.append(" case \(property.name) = \(index)") + } + lines.append("}") + return DeclSyntax(stringLiteral: lines.joined(separator: "\n")) + } + + static func initDeclarationSyntax( + for properties: [Property], + of declaration: some DeclGroupSyntax + ) -> DeclSyntax { + let access = accessPrefix(of: declaration) + var lines = [String]() + lines.append("\(access)init?(tlvData: Data) {") + lines.append(" guard let container = TLVContainer(data: tlvData) else {") + lines.append(" return nil") + lines.append(" }") + for property in properties { + if property.isOptional { + lines.append(" if container.contains(CodingKeys.\(property.name)) {") + lines.append(" guard let \(property.name) = container.decode(\(property.type).self, forKey: CodingKeys.\(property.name)) else {") + lines.append(" return nil") + lines.append(" }") + lines.append(" self.\(property.name) = \(property.name)") + lines.append(" } else {") + lines.append(" self.\(property.name) = nil") + lines.append(" }") + } else { + lines.append(" guard let \(property.name) = container.decode(\(property.type).self, forKey: CodingKeys.\(property.name)) else {") + lines.append(" return nil") + lines.append(" }") + lines.append(" self.\(property.name) = \(property.name)") + } + } + lines.append("}") + return DeclSyntax(stringLiteral: lines.joined(separator: "\n")) + } + + static func tlvDataDeclarationSyntax( + for properties: [Property], + of declaration: some DeclGroupSyntax + ) -> DeclSyntax { + let access = accessPrefix(of: declaration) + var lines = [String]() + lines.append("\(access)var tlvData: Data {") + lines.append(" var container = TLVContainer()") + for property in properties { + lines.append(" container.encode(self.\(property.name), forKey: CodingKeys.\(property.name))") + } + lines.append(" return container.data") + lines.append("}") + return DeclSyntax(stringLiteral: lines.joined(separator: "\n")) + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 7ca4d71..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -@testable import TLVCodingTests - -XCTMain([ - testCase(TLVCodingTests.allTests), -]) diff --git a/Tests/TLVCodingMacrosTests/TLVCodingMacrosTests.swift b/Tests/TLVCodingMacrosTests/TLVCodingMacrosTests.swift new file mode 100644 index 0000000..38bfe4c --- /dev/null +++ b/Tests/TLVCodingMacrosTests/TLVCodingMacrosTests.swift @@ -0,0 +1,164 @@ +// +// TLVCodingMacrosTests.swift +// TLVCoding +// +// Created by Alsey Coleman Miller on 7/17/26. +// Copyright © 2026 PureSwift. All rights reserved. +// + +import XCTest +import SwiftSyntax +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +@testable import TLVCodingMacros + +final class TLVCodingMacrosTests: XCTestCase { + + let testMacros: [String: Macro.Type] = [ + "TLVCodable": TLVCodableMacro.self + ] + + func testGeneratedCodingKeys() { + assertMacroExpansion( + """ + @TLVCodable + public struct Person: Equatable { + + public var gender: Gender + + public var name: String + } + """, + expandedSource: """ + public struct Person: Equatable { + + public var gender: Gender + + public var name: String + + public enum CodingKeys: UInt8, TLVCoding.TLVCodingKey { + case gender = 0 + case name = 1 + } + + public init?(tlvData: Data) { + guard let container = TLVContainer(data: tlvData) else { + return nil + } + guard let gender = container.decode(Gender.self, forKey: CodingKeys.gender) else { + return nil + } + self.gender = gender + guard let name = container.decode(String.self, forKey: CodingKeys.name) else { + return nil + } + self.name = name + } + + public var tlvData: Data { + var container = TLVContainer() + container.encode(self.gender, forKey: CodingKeys.gender) + container.encode(self.name, forKey: CodingKeys.name) + return container.data + } + } + + extension Person: TLVCoding.TLVCodable { + } + """, + macros: testMacros + ) + } + + func testExplicitCodingKeys() { + assertMacroExpansion( + """ + @TLVCodable + struct ProvisioningState { + + var state: State + + enum CodingKeys: UInt8, TLVCodingKey { + case state = 0x01 + } + } + """, + expandedSource: """ + struct ProvisioningState { + + var state: State + + enum CodingKeys: UInt8, TLVCodingKey { + case state = 0x01 + } + + init?(tlvData: Data) { + guard let container = TLVContainer(data: tlvData) else { + return nil + } + guard let state = container.decode(State.self, forKey: CodingKeys.state) else { + return nil + } + self.state = state + } + + var tlvData: Data { + var container = TLVContainer() + container.encode(self.state, forKey: CodingKeys.state) + return container.data + } + } + + extension ProvisioningState: TLVCoding.TLVCodable { + } + """, + macros: testMacros + ) + } + + func testOptionalProperty() { + assertMacroExpansion( + """ + @TLVCodable + struct CustomEncodable { + + var data: Data? + } + """, + expandedSource: """ + struct CustomEncodable { + + var data: Data? + + enum CodingKeys: UInt8, TLVCoding.TLVCodingKey { + case data = 0 + } + + init?(tlvData: Data) { + guard let container = TLVContainer(data: tlvData) else { + return nil + } + if container.contains(CodingKeys.data) { + guard let data = container.decode(Data.self, forKey: CodingKeys.data) else { + return nil + } + self.data = data + } else { + self.data = nil + } + } + + var tlvData: Data { + var container = TLVContainer() + container.encode(self.data, forKey: CodingKeys.data) + return container.data + } + } + + extension CustomEncodable: TLVCoding.TLVCodable { + } + """, + macros: testMacros + ) + } +} diff --git a/Tests/TLVCodingTests/TLVCodingTests.swift b/Tests/TLVCodingTests/TLVCodingTests.swift index 576fe1e..03eb424 100644 --- a/Tests/TLVCodingTests/TLVCodingTests.swift +++ b/Tests/TLVCodingTests/TLVCodingTests.swift @@ -11,18 +11,9 @@ import XCTest @testable import TLVCoding final class TLVCodingTests: XCTestCase { - - static let allTests = [ - ("testCodable", testCodable), - ("testCodingKeys", testCodingKeys), - ("testUUID", testUUID), - ("testDate", testDate), - ("testDateSecondsSince1970", testDateSecondsSince1970), - ("testOutputFormatting", testOutputFormatting) - ] - + func testCodable() { - + compare( [ UInt8(0xAA), @@ -35,29 +26,29 @@ final class TLVCodingTests: XCTestCase { 2,1,0xCC ]) ) - + compare(Person(gender: .male, name: "Coleman"), Data([0, 1, 0, 1, 7, 67, 111, 108, 101, 109, 97, 110])) - + compare(Person(gender: .male, name: "Coleman"), Data([0, 1]), shouldFail: true ) - + compare(Person(gender: .male, name: "Coleman"), Data([0, 2, 0]), shouldFail: true ) - + compare(Person(gender: .male, name: ""), Data([0, 1, 0, 1, 0])) - + compare(ProvisioningState(state: .idle, result: .notAvailible), Data([0x01, 0x01, 0x00, 0x02, 0x01, 0x00])) - + compare(ProvisioningState(state: .provisioning, result: .notAvailible), Data([0x01, 0x01, 0x01, 0x02, 0x01, 0x00])) - + compare(Numeric( boolean: true, int: -10, @@ -73,12 +64,12 @@ final class TLVCodingTests: XCTestCase { uint32: 3000, uint64: 30_000), Data([0, 1, 1, 1, 4, 246, 255, 255, 255, 2, 4, 10, 0, 0, 0, 3, 4, 146, 203, 143, 63, 4, 8, 114, 138, 142, 228, 242, 255, 37, 64, 5, 1, 127, 6, 2, 56, 255, 7, 4, 48, 248, 255, 255, 8, 8, 224, 177, 255, 255, 255, 255, 255, 255, 9, 1, 255, 10, 2, 44, 1, 11, 4, 184, 11, 0, 0, 12, 8, 48, 117, 0, 0, 0, 0, 0, 0])) - + compare( Version(major: 1, minor: 2, patch: 3), Data([0x01, 0x02, 0x03]) ) - + compare( CustomEncodable( data: nil, @@ -88,7 +79,7 @@ final class TLVCodingTests: XCTestCase { ), Data([]) ) - + compare( CustomEncodable( data: Data(), @@ -98,7 +89,7 @@ final class TLVCodingTests: XCTestCase { ), Data([0, 0]) ) - + compare( CustomEncodable( data: Data([0x00, 0x01]), @@ -108,7 +99,7 @@ final class TLVCodingTests: XCTestCase { ), Data([0, 2, 0x00, 0x01]) ) - + compare( Profile( person: Person( @@ -119,13 +110,12 @@ final class TLVCodingTests: XCTestCase { gender: .male, name: "Coleman" ) - ], - userInfo: nil + ] ), Data([0, 12, 0, 1, 0, 1, 7, 67, 111, 108, 101, 109, 97, 110, 1, 14, 0, 12, 0, 1, 0, 1, 7, 67, 111, 108, 101, 109, 97, 110]) ) - + compare( Profile( person: Person( @@ -143,8 +133,7 @@ final class TLVCodingTests: XCTestCase { gender: .male, name: "Jorge" ) - ], - userInfo: nil + ] ), Data([0, 12, 0, 1, @@ -169,7 +158,7 @@ final class TLVCodingTests: XCTestCase { 74, 111, 114, 103, 101 ]) ) - + compare( Binary( data: Data([0x01, 0x02, 0x03, 0x04]), @@ -177,7 +166,7 @@ final class TLVCodingTests: XCTestCase { ), Data([0, 4, 1, 2, 3, 4, 1, 2, 1, 0]) ) - + compare( PrimitiveArray( strings: ["1", "two", "three", ""], @@ -185,330 +174,187 @@ final class TLVCodingTests: XCTestCase { ), Data([0, 17, 0, 1, 49, 1, 3, 116, 119, 111, 2, 5, 116, 104, 114, 101, 101, 3, 0, 1, 60, 0, 4, 1, 0, 0, 0, 1, 4, 2, 0, 0, 0, 2, 4, 3, 0, 0, 0, 3, 4, 4, 0, 0, 0, 4, 4, 5, 0, 0, 0, 5, 4, 6, 0, 0, 0, 6, 4, 7, 0, 0, 0, 7, 4, 8, 0, 0, 0, 8, 4, 9, 0, 0, 0, 9, 4, 10, 0, 0, 0]) ) - - compare( - DeviceInformation( - identifier: UUID(uuidString: "B83DD6F4-A429-41B3-945A-3E0EE5915CA1")!, - buildVersion: DeviceInformation.BuildVersion(rawValue: 1), - version: Version(major: 1, minor: 2, patch: 3), - status: .provisioned, - features: .all - ), - Data([0, 16, 184, 61, 214, 244, 164, 41, 65, 179, 148, 90, 62, 14, 229, 145, 92, 161, 1, 8, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 1, 2, 3, 3, 1, 2, 4, 1, 7]) - ) - - compare( - CryptoRequest(secret: CryptoData()), - Data([0, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]) - ) - - compare( - CustomEncodableArray(elements: [ - .value( - CustomEncodableArray.Value( - identifier: UUID(uuidString: "B83DD6F4-A429-41B3-945A-3E0EE5915CA1")!, - name: "Value 1" - ) - ), - .value( - CustomEncodableArray.Value( - identifier: UUID(uuidString: "B83DD6F4-A429-41B3-945A-3E0EE5915CA2")!, - name: "Value 2" - ) - ), - .pendingValue( - CustomEncodableArray.PendingValue( - identifier: UUID(uuidString: "B83DD6F4-A429-41B3-945A-3E0EE5915CA3")!, - name: "Pending Value 1", - expiration: Date.distantFuture - ) - ) - ] - ), - 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, 16, 99, 216, 45, 66]) - ) - } - + func testCodingKeys() { - + typealias CodingKeys = ProvisioningState.CodingKeys - - for codingKey in ProvisioningState.CodingKeys.allCases { - + + XCTAssertEqual(CodingKeys.state.rawValue, 0x01) + XCTAssertEqual(CodingKeys.result.rawValue, 0x02) + + for codingKey in [CodingKeys.state, .result] { XCTAssertEqual(CodingKeys(rawValue: codingKey.rawValue), codingKey) - XCTAssertEqual(CodingKeys(stringValue: codingKey.stringValue), codingKey) + XCTAssertEqual(CodingKeys(code: codingKey.code), codingKey) + XCTAssertEqual(codingKey.code.rawValue, codingKey.rawValue) } + + // generated coding keys are sequential, in declaration order + XCTAssertEqual(Person.CodingKeys.gender.rawValue, 0) + XCTAssertEqual(Person.CodingKeys.name.rawValue, 1) } - - func testUUID() { - - let formats: [TLVUUIDFormatting] = [.bytes, .string] - - for format in formats { - - let value = CustomEncodable( - data: nil, - uuid: UUID(), - number: nil, - date: nil - ) - - var encodedData = Data() - var encoder = TLVEncoder() - encoder.uuidFormatting = 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.uuidFormatting = 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 testContainer() { + + let value = Person(gender: .male, name: "Coleman") + + // explicit encoding matches the macro-generated implementation + var container = TLVContainer() + container.encode(value.gender, forKey: Person.CodingKeys.gender) + container.encode(value.name, forKey: Person.CodingKeys.name) + XCTAssertEqual(container.data, value.tlvData) + + // keyed access + guard let decoded = TLVContainer(data: container.data) else { + XCTFail("Could not parse container") + return } + XCTAssertEqual(decoded, container) + XCTAssertEqual(decoded.items.count, 2) + XCTAssert(decoded.contains(Person.CodingKeys.gender)) + XCTAssertEqual(decoded.decode(Gender.self, forKey: Person.CodingKeys.gender), .male) + XCTAssertEqual(decoded.decode(String.self, forKey: Person.CodingKeys.name), "Coleman") + XCTAssertEqual(decoded[TLVTypeCode(rawValue: 1)], Data([67, 111, 108, 101, 109, 97, 110])) + + // invalid data + XCTAssertNil(TLVContainer(data: Data([0])), "Missing length byte") + XCTAssertNil(TLVContainer(data: Data([0, 2, 0])), "Truncated value") } - - func testDate() { - - var formats: [TLVDateFormatting] = [ - .secondsSince1970, - .millisecondsSince1970 - ] - - #if !os(WASI) - 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) - formats.append(.formatted(dateFormatter)) - if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - formats.append(.iso8601) - } - #endif - 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.dateFormatting = 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.dateFormatting = 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 testUUID() { + + let value = CustomEncodable( + data: nil, + uuid: UUID(), + number: nil, + date: nil + ) + + let encodedData = value.tlvData + guard let decodedValue = CustomEncodable(tlvData: encodedData) else { + XCTFail("Could not decode \(value)") + return } + XCTAssertEqual(decodedValue, value) + + // 16 byte big endian value + let uuid = UUID(uuidString: "B83DD6F4-A429-41B3-945A-3E0EE5915CA1")! + XCTAssertEqual(uuid.tlvData, Data([184, 61, 214, 244, 164, 41, 65, 179, 148, 90, 62, 14, 229, 145, 92, 161])) + XCTAssertEqual(UUID(tlvData: uuid.tlvData), uuid) } - - func testDateSecondsSince1970() { - + + func testDate() { + 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.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 + + let value = CustomEncodable( + data: nil, + uuid: nil, + number: nil, + date: date ) - - XCTAssertEqual(try encoder.encode(value), try encoder.encode(valueUnordered)) - encoder.outputFormatting.sortedKeys = false - XCTAssertNotEqual(try encoder.encode(value), try encoder.encode(valueUnordered)) + + let encodedData = value.tlvData + guard let decodedValue = CustomEncodable(tlvData: encodedData) else { + XCTFail("Could not decode \(value)") + return + } + XCTAssertEqual(decodedValue, value) + + // encoded as seconds since 1970 + XCTAssertEqual(date.tlvData, date.timeIntervalSince1970.tlvData) } } private extension TLVCodingTests { - - func compare (_ value: T, _ data: Data, shouldFail: Bool = false) { - - var didFail = false - var encoder = TLVEncoder() - encoder.log = { print("Encoder:", $0) } - do { - let encodedData = try encoder.encode(value) - if shouldFail == false { - XCTAssertEqual(encodedData, data, "Invalid data \(Array(encodedData))") - } - } catch { - dump(error) - if shouldFail == false { - XCTFail("Could not encode \(value)") - } - didFail = true + + func compare (_ value: T, _ data: Data, shouldFail: Bool = false) { + + if shouldFail == false { + let encodedData = value.tlvData + XCTAssertEqual(encodedData, data, "Invalid data \(Array(encodedData))") } - - var decoder = TLVDecoder() - decoder.log = { print("Decoder:", $0) } - do { - let decodedValue = try decoder.decode(T.self, from: data) - XCTAssertEqual(decodedValue, value) - } catch { - dump(error) - if shouldFail == false { - XCTFail("Could not decode \(value)") + + if let decodedValue = T(tlvData: data) { + if shouldFail { + XCTFail("Decoding should have failed for \(Array(data))") + } else { + XCTAssertEqual(decodedValue, value) } - didFail = true - } - - if shouldFail { - XCTAssert(didFail, "No error thrown") + } else if shouldFail == false { + XCTFail("Could not decode \(value)") } } } // MARK: - Supporting Types -public struct Person: Codable, Equatable, Hashable { - +@TLVCodable +public struct Person: Equatable, Hashable { + public var gender: Gender + public var name: String + + public init(gender: Gender, name: String) { + self.gender = gender + self.name = name + } } -public enum Gender: UInt8, Codable { - +public enum Gender: UInt8, TLVCodable { + case male 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 -} +@TLVCodable +public struct ProvisioningState: Equatable { -public struct ProvisioningState: Codable, Equatable { - public var state: State + public var result: Result - - public enum State: UInt8, Codable { - - case idle = 0x00 - case provisioning = 0x01 - } - - public enum Result: UInt8, Codable { - - case notAvailible = 0x00 - case success = 0x01 + + public init(state: State, result: Result) { + self.state = state + self.result = result } -} -internal extension ProvisioningState { - - enum CodingKeys: UInt8, TLVCodingKey, CaseIterable { - + public enum CodingKeys: UInt8, TLVCodingKey { + case state = 0x01 case result = 0x02 } -} -extension ProvisioningState.CodingKeys { - - var stringValue: String { - switch self { - case .state: return "state" - case .result: return "result" - } + public enum State: UInt8, TLVCodable { + + case idle = 0x00 + case provisioning = 0x01 } -} -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 enum Result: UInt8, TLVCodable { + + case notAvailible = 0x00 + case success = 0x01 } } -public struct Profile: Codable, Equatable { - +@TLVCodable +public struct Profile: Equatable { + public var person: Person + public var friends: [Person] - public var userInfo: [UInt: String]? + + public init(person: Person, friends: [Person]) { + self.person = person + self.friends = friends + } } -public struct Numeric: Codable, Equatable, Hashable { - +@TLVCodable +public struct Numeric: Equatable, Hashable { + public var boolean: Bool public var int: Int public var uint: UInt @@ -522,453 +368,126 @@ public struct Numeric: Codable, Equatable, Hashable { public var uint16: UInt16 public var uint32: UInt32 public var uint64: UInt64 -} -public struct Binary: Codable, Equatable, Hashable { - + public init( + boolean: Bool, + int: Int, + uint: UInt, + float: Float, + double: Double, + int8: Int8, + int16: Int16, + int32: Int32, + int64: Int64, + uint8: UInt8, + uint16: UInt16, + uint32: UInt32, + uint64: UInt64 + ) { + self.boolean = boolean + self.int = int + self.uint = uint + self.float = float + self.double = double + self.int8 = int8 + self.int16 = int16 + self.int32 = int32 + self.int64 = int64 + self.uint8 = uint8 + self.uint16 = uint16 + self.uint32 = uint32 + self.uint64 = uint64 + } +} + +@TLVCodable +public struct Binary: Equatable, Hashable { + public var data: Data + public var value: TLVCodableNumber + + public init(data: Data, value: TLVCodableNumber) { + self.data = data + self.value = value + } } -public enum TLVCodableNumber: UInt16, Equatable, Hashable, Swift.Codable, TLVCodable { - +public enum TLVCodableNumber: UInt16, TLVCodable { + case zero case one case two case three - - public init?(tlvData: Data) { - - guard let rawValue = UInt16(tlvData: tlvData)?.littleEndian - else { return nil } - - self.init(rawValue: rawValue) - } - - public var tlvData: Data { - - return rawValue.littleEndian.tlvData - } } -public struct PrimitiveArray: Codable, Equatable { - - var strings: [String] - var integers: [Int] -} +@TLVCodable +public struct PrimitiveArray: Equatable { -public struct CustomEncodable: Codable, Equatable { - - public var data: Data? - public var uuid: UUID? - public var number: TLVCodableNumber? - public var date: Date? -} + public var strings: [String] -public struct DeviceInformation: Equatable, Codable { - - public let identifier: UUID - public let buildVersion: BuildVersion - public let version: Version - public var status: Status - public let features: BitMaskOptionSet -} + public var integers: [Int] -public extension DeviceInformation { - - enum Status: UInt8, Codable { - case idle = 0x00 - case provisioning = 0x01 - case provisioned = 0x02 - } - - struct BuildVersion: RawRepresentable, Equatable, Hashable, Codable { - - public let rawValue: UInt64 - - public init(rawValue: UInt64) { - self.rawValue = rawValue - } - - public init(from decoder: Decoder) throws { - - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(RawValue.self) - self.init(rawValue: rawValue) - } - - public func encode(to encoder: Encoder) throws { - - var container = encoder.singleValueContainer() - try container.encode(rawValue) - } - } - - enum Feature: UInt8, BitMaskOption, Codable { - - case bluetooth = 0b001 - case camera = 0b010 - case gps = 0b100 + public init(strings: [String], integers: [Int]) { + self.strings = strings + self.integers = integers } } -public struct Version: Equatable, Hashable, Codable { - - public var major: UInt8 - - public var minor: UInt8 - - public var patch: UInt8 -} +@TLVCodable +public struct CustomEncodable: Equatable { -extension Version: TLVCodable { - - internal static var length: Int { return 3 } - - public init?(tlvData: Data) { - guard tlvData.count == Version.length - else { return nil } - - self.major = tlvData[0] - self.minor = tlvData[1] - self.patch = tlvData[2] - } - - public var tlvData: Data { - return Data([major, minor, patch]) - } -} - -public struct CryptoRequest: Equatable, Codable { - - /// Private key data. - public let secret: CryptoData - - public init(secret: CryptoData) { - self.secret = secret - } -} + public var data: Data? -public protocol SecureData: Hashable { - - /// The data length. - static var length: Int { get } - - /// The data. - var data: Data { get } - - /// Initialize with data. - init?(data: Data) - - /// Initialize with random value. - init() -} + public var uuid: UUID? -public extension SecureData where Self: Decodable { - - init(from decoder: Decoder) throws { - - let container = try decoder.singleValueContainer() - let data = try container.decode(Data.self) - guard let value = Self(data: data) else { - throw DecodingError.typeMismatch(Self.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of bytes \(data.count) for \(String(reflecting: Self.self))")) - } - self = value - } -} + public var number: TLVCodableNumber? -public extension SecureData where Self: Encodable { - - func encode(to encoder: Encoder) throws { - - var container = encoder.singleValueContainer() - try container.encode(data) - } -} + public var date: Date? -/// Crypto Data -public struct CryptoData: SecureData, Codable { - - public static let length = 256 / 8 // 32 - - public let data: Data - - public init?(data: Data) { - - guard data.count == type(of: self).length - else { return nil } - + public init( + data: Data?, + uuid: UUID?, + number: TLVCodableNumber?, + date: Date? + ) { self.data = data - } - - /// Initializes with a random value. - public init() { - - self.data = Data(repeating: 0xFF, count: type(of: self).length) // not really random + self.uuid = uuid + self.number = number + self.date = date } } -struct CustomEncodableArray: Equatable { - - var elements: [Element] -} +/// Hand-written `TLVCodable` conformance with a custom binary layout. +public struct Version: Equatable, Hashable { -extension CustomEncodableArray { - - enum Element: Equatable { - case value(Value) - case pendingValue(PendingValue) - } - - struct Value: Codable, Equatable { - let identifier: UUID - let name: String - } - - struct PendingValue: Codable, Equatable { - let identifier: UUID - let name: String - let expiration: Date - } - - enum ValueType: UInt8, Codable { - case value - case pendingValue - } -} - -extension CustomEncodableArray.Element: Codable { - - private enum CodingKeys: UInt8, TLVCodingKey, CaseIterable { - - case type = 0x00 - case value = 0x01 - - var stringValue: String { - switch self { - case .type: return "type" - case .value: return "value" - } - } - } - - public init(from decoder: Decoder) throws { - - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(CustomEncodableArray.ValueType.self, forKey: .type) - switch type { - case .value: - let value = try container.decode(CustomEncodableArray.Value.self, forKey: .value) - self = .value(value) - case .pendingValue: - let pendingValue = try container.decode(CustomEncodableArray.PendingValue.self, forKey: .value) - self = .pendingValue(pendingValue) - } - } - - public func encode(to encoder: Encoder) throws { - - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case let .value(value): - try container.encode(CustomEncodableArray.ValueType.value, forKey: .type) - try container.encode(value, forKey: .value) - case let .pendingValue(pendingValue): - try container.encode(CustomEncodableArray.ValueType.pendingValue, forKey: .type) - try container.encode(pendingValue, forKey: .value) - } - } -} - -extension CustomEncodableArray: Codable { - - public init(from decoder: Decoder) throws { - self.elements = try .init(from: decoder) - } - - public func encode(to encoder: Encoder) throws { - try elements.encode(to: encoder) - } -} - -/// Enum that represents a bit mask flag / option. -/// -/// Basically `Swift.OptionSet` for enums. -public protocol BitMaskOption: RawRepresentable, Hashable, CaseIterable where RawValue: FixedWidthInteger { } - -public extension Sequence where Element: BitMaskOption { - - /// Convert Swift enums for bit mask options into their raw values OR'd. - var rawValue: Element.RawValue { - - @inline(__always) - get { return reduce(0, { $0 | $1.rawValue }) } - } -} - -public extension BitMaskOption { - - /// Whether the enum case is present in the raw value. - @inline(__always) - func isContained(in rawValue: RawValue) -> Bool { - - return (self.rawValue & rawValue) != 0 - } - - @inline(__always) - static func from(rawValue: RawValue) -> [Self] { - - return Self.allCases.filter { $0.isContained(in: rawValue) } - } -} - -// MARK: - BitMaskOptionSet - -/// Integer-backed array type for `BitMaskOption`. -/// -/// The elements are packed in the integer with bitwise math and stored on the stack. -public struct BitMaskOptionSet : RawRepresentable { - - public typealias RawValue = Element.RawValue - - public private(set) var rawValue: RawValue - - @inline(__always) - public init(rawValue: RawValue) { - - self.rawValue = rawValue - } - - @inline(__always) - public init() { - - self.rawValue = 0 - } - - public static var all: BitMaskOptionSet { - - return BitMaskOptionSet(rawValue: Element.allCases.rawValue) - } - - @inline(__always) - public mutating func insert(_ element: Element) { - - rawValue = rawValue | element.rawValue - } - - @discardableResult - public mutating func remove(_ element: Element) -> Bool { - - guard contains(element) else { return false } - - rawValue = rawValue & ~element.rawValue - - return true - } - - @inline(__always) - public mutating func removeAll() { - - self.rawValue = 0 - } - - @inline(__always) - public func contains(_ element: Element) -> Bool { - - return element.isContained(in: rawValue) - } - - public func contains (_ other: S) -> Bool where S.Iterator.Element == Element { - - for element in other { - - guard element.isContained(in: rawValue) - else { return false } - } - - return true - } - - public var count: Int { - - return Element.allCases.reduce(0, { $0 + ($1.isContained(in: rawValue) ? 1 : 0) }) - } - - public var isEmpty: Bool { - - return rawValue == 0 - } -} - -// MARK: - Sequence Conversion + public var major: UInt8 -public extension BitMaskOptionSet { - - init(_ sequence: S) where S.Iterator.Element == Element { - self.rawValue = sequence.rawValue - } -} + public var minor: UInt8 -extension BitMaskOptionSet: Equatable { - - public static func == (lhs: BitMaskOptionSet, rhs: BitMaskOptionSet) -> Bool { - return lhs.rawValue == rhs.rawValue - } -} + public var patch: UInt8 -extension BitMaskOptionSet: CustomStringConvertible { - - public var description: String { - - return Element.from(rawValue: rawValue) - .sorted(by: { $0.rawValue < $1.rawValue }) - .description + public init(major: UInt8, minor: UInt8, patch: UInt8) { + self.major = major + self.minor = minor + self.patch = patch } } -extension BitMaskOptionSet: Hashable { - - public func hash(into hasher: inout Hasher) { - rawValue.hash(into: &hasher) - } -} +extension Version: TLVCodable { -extension BitMaskOptionSet: ExpressibleByArrayLiteral { - - public init(arrayLiteral elements: Element...) { - - self.init(elements) - } -} + internal static var length: Int { 3 } -extension BitMaskOptionSet: ExpressibleByIntegerLiteral { - - public init(integerLiteral value: UInt64) { - - self.init(rawValue: numericCast(value)) - } -} + public init?(tlvData: Data) { + guard tlvData.count == Version.length + else { return nil } -extension BitMaskOptionSet: Sequence { - - public func makeIterator() -> IndexingIterator<[Element]> { - - return Element.from(rawValue: rawValue).makeIterator() + self.major = tlvData[0] + self.minor = tlvData[1] + self.patch = tlvData[2] } -} -extension BitMaskOptionSet: Codable where BitMaskOptionSet.RawValue: Codable { - - public init(from decoder: Decoder) throws { - - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(RawValue.self) - self.init(rawValue: rawValue) - } - - public func encode(to encoder: Encoder) throws { - - var container = encoder.singleValueContainer() - try container.encode(rawValue) + public var tlvData: Data { + Data([major, minor, patch]) } }