├── assets ├── logo.png ├── swift.svg ├── docs.svg └── platforms.svg ├── Sources └── BinaryCodable │ ├── Protocols │ ├── CodablePrimitive.swift │ ├── PackedCodable.swift │ ├── PackedEncodable.swift │ ├── DecodablePrimitive.swift │ ├── EncodablePrimitive.swift │ └── PackedDecodable.swift │ ├── Primitives │ ├── Data+Coding.swift │ ├── String+Coding.swift │ ├── Set+Coding.swift │ ├── Array+Coding.swift │ ├── UInt8+Coding.swift │ ├── Int8+Coding.swift │ ├── Float+Coding.swift │ ├── Double+Coding.swift │ ├── Bool+Coding.swift │ ├── UInt+Coding.swift │ ├── UInt16+Coding.swift │ ├── UInt32+Coding.swift │ ├── Int16+Coding.swift │ ├── Int+Coding.swift │ ├── Int32+Coding.swift │ ├── Int64+Coding.swift │ └── UInt64+Coding.swift │ ├── Encoding │ ├── Int+Length.swift │ ├── NilContainer.swift │ ├── PrimitiveEncodingContainer.swift │ ├── ValueEncoder.swift │ ├── HashableKey.swift │ ├── CodingKey+Encoding.swift │ ├── UnkeyedEncoderStorage.swift │ ├── UnkeyedEncoder.swift │ ├── EncodableContainer.swift │ ├── ValueEncoderStorage.swift │ ├── AbstractEncodingNode.swift │ ├── KeyedEncoder.swift │ ├── KeyedEncoderStorage.swift │ └── EncodingNode.swift │ ├── Extensions │ ├── String+Extensions.swift │ ├── DecodingError+Extensions.swift │ └── Data+Extensions.swift │ ├── Decoding │ ├── AbstractDecodingNode.swift │ ├── ValueDecoder.swift │ ├── Data+DecodingDataProvider.swift │ ├── AbstractDecoder.swift │ ├── DecodingKey.swift │ ├── CorruptedDataError.swift │ ├── UnkeyedDecoder.swift │ ├── DecodingNode.swift │ ├── KeyedDecoder.swift │ └── DecodingDataProvider.swift │ ├── Wrappers │ ├── VariableLengthCodable.swift │ ├── FixedSizeCodable.swift │ ├── ZigZagCodable.swift │ └── Packed.swift │ ├── Common │ ├── SuperCodingKey.swift │ └── AbstractNode.swift │ ├── BinaryDecoder.swift │ ├── BinaryFileEncoder.swift │ ├── BinaryStreamEncoder.swift │ ├── BinaryEncoder.swift │ └── BinaryFileDecoder.swift ├── .gitignore ├── Tests └── BinaryCodableTests │ ├── SIMDTests.swift │ ├── UUIDEncodingTests.swift │ ├── UserInfoTests.swift │ ├── CodingPathTests.swift │ ├── Helper.swift │ ├── GenericTestStruct.swift │ ├── UnkeyedContainerTests.swift │ ├── DecodingErrorTests.swift │ ├── VariableLengthEncodingTests.swift │ ├── BoolTests.swift │ ├── SequenceEncoderTests.swift │ ├── CustomDecodingTests.swift │ ├── SuperEncodingTests.swift │ ├── FileDecodingTests.swift │ ├── PropertyWrapperCodingTests.swift │ ├── ContainerTests.swift │ ├── EnumEncodingTests.swift │ ├── OptionalEncodingTests.swift │ ├── SetTests.swift │ ├── ArrayEncodingTests.swift │ └── StructEncodingTests.swift ├── Package@swift-5.5.swift ├── .github └── workflows │ └── tests.yml ├── Package.swift ├── Package@swift-5.6.swift └── License.md /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christophhagen/BinaryCodable/HEAD/assets/logo.png -------------------------------------------------------------------------------- /Sources/BinaryCodable/Protocols/CodablePrimitive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias CodablePrimitive = EncodablePrimitive & DecodablePrimitive 4 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Protocols/PackedCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type that can be encoded and decoded without length information 4 | typealias PackedCodable = PackedEncodable & PackedDecodable 5 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Protocols/PackedEncodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A protocol for types which can be encoded without type information 5 | */ 6 | protocol PackedEncodable: EncodablePrimitive { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | Package.resolved 11 | .swiftpm/ 12 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Data+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data: EncodablePrimitive { 4 | 5 | var encodedData: Data { 6 | self 7 | } 8 | } 9 | 10 | extension Data: DecodablePrimitive { 11 | 12 | init(data: Data) { 13 | self = data 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/Int+Length.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Int { 4 | 5 | /// Encodes the integer as the length of a nil/length indicator 6 | var lengthData: Data { 7 | // The first bit (LSB) is the `nil` bit (0) 8 | // The rest is the length, encoded as a varint 9 | (UInt64(self) << 1).encodedData 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Protocols/DecodablePrimitive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A protocol adopted by primitive types for decoding. 5 | */ 6 | protocol DecodablePrimitive { 7 | 8 | /** 9 | Decode a value from the data. 10 | - Note: All provided data can be used 11 | - Throws: Errors of type ``CorruptDataError`` 12 | */ 13 | init(data: Data) throws 14 | } 15 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/NilContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A container to signal that `nil` was encoded in a container 5 | */ 6 | struct NilContainer: EncodableContainer { 7 | 8 | let needsLengthData = true 9 | 10 | let needsNilIndicator = true 11 | 12 | let isNil = true 13 | 14 | func containedData() throws -> Data { 15 | fatalError() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Protocols/EncodablePrimitive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A protocol adopted by all base types (Int, Data, String, ...) to provide the encoded data. 5 | */ 6 | protocol EncodablePrimitive { 7 | 8 | /** 9 | The raw data of the encoded base type value. 10 | - Note: No length information must be included 11 | */ 12 | var encodedData: Data { get } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | func indented(by indentation: String = " ") -> String { 6 | components(separatedBy: "\n") 7 | .map { $0.trimmed == "" ? $0 : indentation + $0 } 8 | .joined(separator: "\n") 9 | } 10 | 11 | var trimmed: String { 12 | trimmingCharacters(in: .whitespacesAndNewlines) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Protocols/PackedDecodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A protocol for types which can be decoded from a continous stream of data 5 | */ 6 | protocol PackedDecodable { 7 | 8 | /** 9 | Decode a value from a data stream at a given index. 10 | 11 | This function is expected to advance the buffer index appropriately. 12 | */ 13 | init(data: Data, index: inout Int) throws 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/String+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String: EncodablePrimitive { 4 | 5 | var encodedData: Data { 6 | data(using: .utf8)! 7 | } 8 | } 9 | 10 | extension String: DecodablePrimitive { 11 | 12 | init(data: Data) throws { 13 | guard let value = String(data: data, encoding: .utf8) else { 14 | throw CorruptedDataError(invalidString: data.count) 15 | } 16 | self = value 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/SIMDTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import BinaryCodable 4 | #if canImport(simd) 5 | import simd 6 | 7 | 8 | final class SIMDTests: XCTestCase { 9 | 10 | func testSIMDDouble() throws { 11 | let double = 3.14 12 | let value = SIMD2(x: double, y: double) 13 | // Double has length 8, so prepend 16 14 | let doubleData = [16] + Array(double.encodedData) 15 | try compare(value, to: doubleData + doubleData) 16 | } 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/AbstractDecodingNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A class to provide decoding functions to all decoding containers. 5 | */ 6 | class AbstractDecodingNode: AbstractNode { 7 | 8 | let parentDecodedNil: Bool 9 | 10 | init(parentDecodedNil: Bool, codingPath: [CodingKey], userInfo: UserInfo) { 11 | self.parentDecodedNil = parentDecodedNil 12 | super.init(codingPath: codingPath, userInfo: userInfo) 13 | } 14 | } 15 | 16 | extension AbstractDecodingNode: AbstractDecoder { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/PrimitiveEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PrimitiveEncodingContainer: EncodableContainer { 4 | 5 | let needsLengthData: Bool 6 | 7 | let wrapped: EncodablePrimitive 8 | 9 | let needsNilIndicator = false 10 | 11 | let isNil = false 12 | 13 | init(wrapped: EncodablePrimitive, needsLengthData: Bool) { 14 | self.needsLengthData = needsLengthData 15 | self.wrapped = wrapped 16 | } 17 | 18 | func containedData() -> Data { 19 | wrapped.encodedData 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/ValueEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ValueEncoder: SingleValueEncodingContainer { 4 | 5 | var codingPath: [any CodingKey] { 6 | storage.codingPath 7 | } 8 | 9 | private var storage: ValueEncoderStorage 10 | 11 | init(storage: ValueEncoderStorage) { 12 | self.storage = storage 13 | } 14 | 15 | func encodeNil() throws { 16 | try storage.encodeNil() 17 | } 18 | 19 | func encode(_ value: T) throws where T : Encodable { 20 | try storage.encode(value) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Package@swift-5.5.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "BinaryCodable", 7 | products: [ 8 | .library( 9 | name: "BinaryCodable", 10 | targets: ["BinaryCodable"]), 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .target( 15 | name: "BinaryCodable", 16 | dependencies: []), 17 | .testTarget( 18 | name: "BinaryCodableTests", 19 | dependencies: ["BinaryCodable"]), 20 | ], 21 | swiftLanguageVersions: [.v5] 22 | ) 23 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Set+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Set: EncodablePrimitive where Element: PackedEncodable { 4 | 5 | var encodedData: Data { 6 | mapAndJoin { $0.encodedData } 7 | } 8 | } 9 | 10 | extension Set: DecodablePrimitive where Element: PackedDecodable { 11 | 12 | init(data: Data) throws { 13 | var index = data.startIndex 14 | var elements = [Element]() 15 | while !data.isAtEnd(at: index) { 16 | let element = try Element.init(data: data, index: &index) 17 | elements.append(element) 18 | } 19 | self.init(elements) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/ValueDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class ValueDecoder: AbstractDecodingNode, SingleValueDecodingContainer { 4 | 5 | private let data: Data? 6 | 7 | init(data: Data?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { 8 | self.data = data 9 | super.init(parentDecodedNil: false, codingPath: codingPath, userInfo: userInfo) 10 | } 11 | 12 | func decodeNil() -> Bool { 13 | data == nil 14 | } 15 | 16 | func decode(_ type: T.Type) throws -> T where T : Decodable { 17 | try decode(element: data, type: type, codingPath: codingPath) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Array+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array: EncodablePrimitive where Element: PackedEncodable { 4 | 5 | var encodedData: Data { 6 | mapAndJoin { $0.encodedData } 7 | } 8 | } 9 | 10 | extension Array: DecodablePrimitive where Element: PackedDecodable { 11 | 12 | init(data: Data) throws { 13 | var index = data.startIndex 14 | var elements = [Element]() 15 | while !data.isAtEnd(at: index) { 16 | let element = try Element.init(data: data, index: &index) 17 | elements.append(element) 18 | } 19 | self.init(elements) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | name: Swift ${{ matrix.swift }} on ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest] 15 | swift: ["5.7", "5.8", "5.9", "5.10"] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Setup Swift 19 | uses: swift-actions/setup-swift@v2.1.0 20 | with: 21 | swift-version: ${{ matrix.swift }} 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | - name: Build 25 | run: swift build 26 | - name: Run tests 27 | run: swift test 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "BinaryCodable", 7 | platforms: [.macOS(.v10_13), .iOS(.v12), .tvOS(.v12), .watchOS(.v4)], 8 | products: [ 9 | .library( 10 | name: "BinaryCodable", 11 | targets: ["BinaryCodable"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "BinaryCodable", 19 | dependencies: []), 20 | .testTarget( 21 | name: "BinaryCodableTests", 22 | dependencies: ["BinaryCodable"]), 23 | ], 24 | swiftLanguageModes: [.v6] 25 | ) 26 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/Data+DecodingDataProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data: DecodingDataProvider { 4 | 5 | func isAtEnd(at index: Index) -> Bool { 6 | index >= endIndex 7 | } 8 | 9 | func nextByte(at index: inout Index) -> UInt8? { 10 | guard index < endIndex else { 11 | return nil 12 | } 13 | defer { index += 1 } 14 | return self[index] 15 | } 16 | 17 | func nextBytes(_ count: Int, at index: inout Index) -> Data? { 18 | let newEndIndex = index + count 19 | guard newEndIndex <= endIndex else { 20 | return nil 21 | } 22 | defer { index = newEndIndex } 23 | return self[index.. Bool { 14 | lhs.key.stringValue == rhs.key.stringValue 15 | } 16 | } 17 | 18 | extension HashableKey: Hashable { 19 | 20 | func hash(into hasher: inout Hasher) { 21 | hasher.combine(key.stringValue) 22 | } 23 | } 24 | 25 | extension HashableKey: Comparable { 26 | 27 | static func < (lhs: HashableKey, rhs: HashableKey) -> Bool { 28 | guard let lhsInt = lhs.key.intValue, 29 | let rhsInt = rhs.key.intValue else { 30 | return lhs.key.stringValue < rhs.key.stringValue 31 | } 32 | return lhsInt < rhsInt 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Wrappers/VariableLengthCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type that can be encoded and decoded as a variable-length integer 5 | */ 6 | public typealias VariableLengthCodable = VariableLengthEncodable & VariableLengthDecodable 7 | 8 | /** 9 | A type that can be encoded as a variable-length integer 10 | */ 11 | public protocol VariableLengthEncodable: FixedWidthInteger, Encodable { 12 | 13 | /// The value encoded as binary data using variable-length integer encoding 14 | var variableLengthEncoding: Data { get } 15 | } 16 | 17 | /** 18 | A type that can be decoded as a variable-length integer 19 | */ 20 | public protocol VariableLengthDecodable: FixedWidthInteger, Decodable { 21 | 22 | /** 23 | Decode a value as a variable-length integer. 24 | - Parameter data: The encoded value 25 | - Throws: ``CorruptedDataError`` 26 | */ 27 | init(fromVarint raw: UInt64) throws 28 | } 29 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/AbstractDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol AbstractDecoder { 4 | 5 | var parentDecodedNil: Bool { get } 6 | 7 | var userInfo: UserInfo { get } 8 | } 9 | 10 | extension AbstractDecoder { 11 | 12 | func decode(element: Data?, type: T.Type, codingPath: [CodingKey]) throws -> T where T: Decodable { 13 | if let BaseType = T.self as? DecodablePrimitive.Type { 14 | guard let element else { 15 | throw DecodingError.valueNotFound(type, codingPath: codingPath, "Found nil instead of expected type \(type)") 16 | } 17 | return try wrapCorruptDataError(at: codingPath) { 18 | try BaseType.init(data: element) as! T 19 | } 20 | } 21 | let node = try DecodingNode(data: element, parentDecodedNil: parentDecodedNil, codingPath: codingPath, userInfo: userInfo) 22 | return try type.init(from: node) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/CodingKey+Encoding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension CodingKey { 4 | 5 | /** 6 | Returns the encoded data for the key. 7 | - Returns: The encoded data, or `nil`, if the integer coding key is invalid. 8 | */ 9 | func keyData() -> Data? { 10 | // String or Int key bit 11 | // Length of String key or Int key as varint 12 | // String Key Data 13 | guard let intValue else { 14 | let stringData = stringValue.data(using: .utf8)! 15 | // Set String bit to 1 16 | let lengthValue = (UInt64(stringData.count) << 1) + 0x01 17 | let lengthData = lengthValue.variableLengthEncoding 18 | return lengthData + stringData 19 | } 20 | guard intValue >= 0 else { 21 | return nil 22 | } 23 | // For integer keys: 24 | // The LSB is set to 0 25 | // Encode 2 * intValue 26 | return intValue.lengthData 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Wrappers/FixedSizeCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type that can be encoded and decoded as a fixed-size value. 5 | */ 6 | public typealias FixedSizeCodable = FixedSizeEncodable & FixedSizeDecodable 7 | 8 | /// An integer type which can be forced to use a fixed-length encoding instead of variable-length encoding. 9 | public protocol FixedSizeEncodable: Encodable { 10 | 11 | /// The value encoded as fixed size binary data 12 | var fixedSizeEncoded: Data { get } 13 | } 14 | 15 | /** 16 | A type that can be encoded with a fixed number of bytes. 17 | */ 18 | public protocol FixedSizeDecodable: Decodable { 19 | 20 | /** 21 | Decode the value from binary data. 22 | - Parameter data: The binary data of the correct size for the type. 23 | - Throws: ``CorruptedDataError`` 24 | */ 25 | init(fromFixedSize data: Data) throws 26 | } 27 | 28 | extension FixedSizeDecodable { 29 | 30 | /// The number of bytes needed for a fixed-size encoding 31 | static var fixedEncodedByteCount: Int { MemoryLayout.size } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/UUIDEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | final class UUIDEncodingTests: XCTestCase { 5 | 6 | func testUUID() throws { 7 | let id = UUID(uuidString: "D0829408-FA77-4511-ACFC-21504DE16CE1")! 8 | // Add nil indicator 9 | let expected = [0] + Array(id.uuidString.data(using: .utf8)!) 10 | try compare(id, to: expected) 11 | } 12 | 13 | func testEnumWithUUID() throws { 14 | let id = UUID(uuidString: "D0829408-FA77-4511-ACFC-21504DE16CE1")! 15 | let value = UUIDContainer.test(id) 16 | let expected = [9, // String key, length 4 17 | 116, 101, 115, 116, // "test" 18 | 80, // Length 40 19 | 5, // String key, length 2 20 | 95, 48, // "_0" 21 | 72] // Length of UUID 22 | + Array(id.uuidString.data(using: .utf8)!) 23 | try compare(value, to: expected) 24 | } 25 | 26 | } 27 | 28 | private enum UUIDContainer: Codable, Equatable { 29 | case test(UUID) 30 | } 31 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Common/SuperCodingKey.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | The default key used to encode `super` into a keyed container. 5 | 6 | The key uses either the string key `super`, or the integer key `0`. 7 | */ 8 | struct SuperCodingKey: CodingKey { 9 | 10 | /** 11 | Create a new super encoding key. 12 | 13 | The string value of the key will be set to `super` 14 | - Parameter stringValue: This parameter is ignored. 15 | */ 16 | init?(stringValue: String) { 17 | 18 | } 19 | 20 | /** 21 | Create a new super encoding key. 22 | 23 | The integer value of the key will be set to `0` 24 | - Parameter intValue: This parameter is ignored. 25 | */ 26 | init?(intValue: Int) { 27 | 28 | } 29 | 30 | /** 31 | Create a new super encoding key. 32 | */ 33 | init() { } 34 | 35 | /// The string value of the coding key (`super`) 36 | var stringValue: String { 37 | "super" 38 | } 39 | 40 | /// The integer value of the coding key (`0`) 41 | var intValue: Int? { 42 | 0 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /assets/swift.svg: -------------------------------------------------------------------------------- 1 | Swift: 5.6-5.10Swift5.6-5.10 -------------------------------------------------------------------------------- /assets/docs.svg: -------------------------------------------------------------------------------- 1 | documentation: 100%documentation100% -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christoph Hagen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Wrappers/ZigZagCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type that can be encoded and decoded as a variable-length integer using zig-zag encoding. 5 | 6 | Positive integers `p` are encoded as `2 * p` (the even numbers), while negative integers `n` are encoded as `2 * |n| - 1` (the odd numbers). 7 | The encoding thus "zig-zags" between positive and negative numbers. 8 | - SeeAlso: [Protobuf signed integers](https://developers.google.com/protocol-buffers/docs/encoding#signed-ints) 9 | */ 10 | public typealias ZigZagCodable = ZigZagEncodable & ZigZagDecodable 11 | 12 | /** 13 | A type that can be encoded as a zig-zag variable-length integer 14 | */ 15 | public protocol ZigZagEncodable: Encodable { 16 | 17 | /// The value encoded as binary data using zig-zag variable-length integer encoding 18 | var zigZagEncoded: Data { get } 19 | 20 | } 21 | 22 | /** 23 | A type that can be decoded as a zig-zag variable-length integer 24 | */ 25 | public protocol ZigZagDecodable: Decodable { 26 | 27 | /** 28 | Decode a value as a zig-zag variable-length integer. 29 | - Parameter data: The encoded value 30 | - Throws: ``CorruptedDataError`` 31 | */ 32 | init(fromZigZag raw: UInt64) throws 33 | } 34 | 35 | -------------------------------------------------------------------------------- /assets/platforms.svg: -------------------------------------------------------------------------------- 1 | Platforms: iOS | macOS | Linux | tvOS | watchOSPlatformsiOS | macOS | Linux | tvOS | watchOS -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/UnkeyedEncoderStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class UnkeyedEncoderStorage: AbstractEncodingNode { 4 | 5 | private var encodedValues: [EncodableContainer] = [] 6 | 7 | var count: Int { 8 | encodedValues.count 9 | } 10 | 11 | func encodeNil() throws { 12 | encodedValues.append(NilContainer()) 13 | } 14 | 15 | @discardableResult 16 | func add(_ value: T) -> T where T: EncodableContainer { 17 | encodedValues.append(value) 18 | return value 19 | } 20 | 21 | func addedNode() -> EncodingNode { 22 | let node = EncodingNode(needsLengthData: true, codingPath: codingPath, userInfo: userInfo) 23 | return add(node) 24 | } 25 | 26 | func encode(_ value: T) throws where T : Encodable { 27 | let encoded = try encodeValue(value, needsLengthData: true) 28 | add(encoded) 29 | } 30 | 31 | } 32 | 33 | extension UnkeyedEncoderStorage: EncodableContainer { 34 | 35 | var needsNilIndicator: Bool { 36 | false 37 | } 38 | 39 | var isNil: Bool { 40 | false 41 | } 42 | 43 | func containedData() throws -> Data { 44 | try encodedValues.mapAndJoin { 45 | try $0.completeData() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Common/AbstractNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Contextual information set by the user for encoding or decoding 4 | typealias UserInfo = [CodingUserInfoKey : Any] 5 | 6 | /** 7 | A node in the encoding and decoding hierarchy. 8 | 9 | Each node in the tree built during encoding and decoding inherits from this type, 10 | which just provides the basic properties of key path and the custom user info dictionary. 11 | 12 | Child classes: `AbstractEncodingNode` and `AbstractDecodingNode` 13 | */ 14 | class AbstractNode { 15 | 16 | /** 17 | The path of coding keys taken to get to this point in encoding or decoding. 18 | */ 19 | let codingPath: [CodingKey] 20 | 21 | /** 22 | Any contextual information set by the user for encoding or decoding. 23 | 24 | Contains also keys for any custom options set for the encoder and decoder. See `CodingOption`. 25 | */ 26 | let userInfo: UserInfo 27 | 28 | /** 29 | Create an abstract node. 30 | - Parameter codingPath: The path to get to this point in encoding or decoding 31 | - Parameter userInfo: Contextual information set by the user 32 | */ 33 | init(codingPath: [CodingKey], userInfo: UserInfo) { 34 | self.codingPath = codingPath 35 | self.userInfo = userInfo 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Extensions/DecodingError+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension DecodingError { 4 | 5 | static func variableLengthEncodedIntegerOutOfRange(_ codingPath: [CodingKey]) -> DecodingError { 6 | corrupted("Encoded variable-length integer out of range", codingPath: codingPath) 7 | } 8 | 9 | static func notFound(_ key: CodingKey, codingPath: [CodingKey], _ message: String) -> DecodingError { 10 | .keyNotFound(key, .init(codingPath: codingPath, debugDescription: message)) 11 | } 12 | 13 | static func valueNotFound(_ type: Any.Type, codingPath: [CodingKey], _ message: String) -> DecodingError { 14 | .valueNotFound(type, .init(codingPath: codingPath, debugDescription: message)) 15 | } 16 | 17 | static func corrupted(_ message: String, codingPath: [CodingKey]) -> DecodingError { 18 | return .dataCorrupted(.init(codingPath: codingPath, debugDescription: message)) 19 | } 20 | 21 | static func invalidSize(size: Int, for type: String, codingPath: [CodingKey]) -> DecodingError { 22 | .dataCorrupted(.init(codingPath: codingPath, debugDescription: "Invalid size \(size) for type \(type)")) 23 | } 24 | 25 | static func prematureEndOfData(_ codingPath: [CodingKey]) -> DecodingError { 26 | corrupted("Premature end of data", codingPath: codingPath) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/UserInfoTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | final class UserInfoTests: XCTestCase { 5 | 6 | func testUserInfoAvailableInEncoderAndDecoder() throws { 7 | let key = CodingUserInfoKey(rawValue: "SomeKey")! 8 | let value = true 9 | 10 | GenericTestStruct.encode { encoder in 11 | var container = encoder.singleValueContainer() 12 | if let value = encoder.userInfo[key] as? Bool { 13 | XCTAssertTrue(value) 14 | } else { 15 | XCTFail() 16 | } 17 | try container.encode(false) 18 | } 19 | 20 | GenericTestStruct.decode { decoder in 21 | let container = try decoder.singleValueContainer() 22 | if let value = decoder.userInfo[key] as? Bool { 23 | XCTAssertTrue(value) 24 | } else { 25 | XCTFail() 26 | } 27 | let decoded = try container.decode(Bool.self) 28 | XCTAssertEqual(decoded, false) 29 | } 30 | 31 | var encoder = BinaryEncoder() 32 | encoder.userInfo[key] = value 33 | let encoded = try encoder.encode(GenericTestStruct()) 34 | var decoder = BinaryDecoder() 35 | decoder.userInfo[key] = value 36 | _ = try decoder.decode(GenericTestStruct.self, from: encoded) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/UnkeyedEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct UnkeyedEncoder: UnkeyedEncodingContainer { 4 | 5 | private let storage: UnkeyedEncoderStorage 6 | 7 | init(storage: UnkeyedEncoderStorage) { 8 | self.storage = storage 9 | } 10 | 11 | var codingPath: [any CodingKey] { 12 | storage.codingPath 13 | } 14 | 15 | var userInfo: UserInfo { 16 | storage.userInfo 17 | } 18 | 19 | var count: Int { 20 | storage.count 21 | } 22 | 23 | func encodeNil() throws { 24 | try storage.encodeNil() 25 | } 26 | 27 | func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { 28 | let storage = KeyedEncoderStorage(needsLengthData: true, codingPath: codingPath, userInfo: userInfo) 29 | self.storage.add(storage) 30 | return KeyedEncodingContainer(KeyedEncoder(storage: storage)) 31 | } 32 | 33 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { 34 | let storage = UnkeyedEncoderStorage(needsLengthData: true, codingPath: codingPath, userInfo: userInfo) 35 | self.storage.add(storage) 36 | return UnkeyedEncoder(storage: storage) 37 | } 38 | 39 | func superEncoder() -> Encoder { 40 | storage.addedNode() 41 | } 42 | 43 | func encode(_ value: T) throws where T : Encodable { 44 | try storage.encode(value) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/DecodingKey.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A decoded key 5 | */ 6 | enum DecodingKey: Hashable { 7 | 8 | /// A decoded integer key 9 | case integer(Int) 10 | 11 | /// A decoded string key 12 | case string(String) 13 | 14 | func asKey(_ type: T.Type = T.self) -> T? where T: CodingKey { 15 | switch self { 16 | case .integer(let int): 17 | return .init(intValue: int) 18 | case .string(let string): 19 | return .init(stringValue: string) 20 | } 21 | } 22 | } 23 | 24 | extension DecodingKey { 25 | 26 | /** 27 | Create a decoding key from an abstract coding key 28 | */ 29 | init(key: CodingKey) { 30 | if let intValue = key.intValue { 31 | self = .integer(intValue) 32 | } else { 33 | self = .string(key.stringValue) 34 | } 35 | } 36 | } 37 | 38 | extension DecodingKey: ExpressibleByIntegerLiteral { 39 | 40 | init(integerLiteral value: IntegerLiteralType) { 41 | self = .integer(value) 42 | } 43 | } 44 | 45 | extension DecodingKey: ExpressibleByStringLiteral { 46 | 47 | init(stringLiteral value: StringLiteralType) { 48 | self = .string(value) 49 | } 50 | } 51 | 52 | extension DecodingKey: CustomStringConvertible { 53 | 54 | var description: String { 55 | switch self { 56 | case .integer(let int): 57 | return "\(int)" 58 | case .string(let string): 59 | return string 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/CodingPathTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | /** 5 | Tests to ensure that coding paths are correctly set. 6 | */ 7 | final class CodingPathTests: XCTestCase { 8 | 9 | func testCodingPathAtRoot() throws { 10 | GenericTestStruct.encode { encoder in 11 | // Need to set some value, otherwise encoding will fail 12 | var container = encoder.singleValueContainer() 13 | try container.encode(0) 14 | XCTAssertEqual(encoder.codingPath, []) 15 | } 16 | GenericTestStruct.decode { decoder in 17 | XCTAssertEqual(decoder.codingPath, []) 18 | } 19 | try compare(GenericTestStruct()) 20 | } 21 | 22 | func testCodingPathInKeyedContainer() throws { 23 | enum SomeKey: Int, CodingKey { 24 | case value = 1 25 | } 26 | GenericTestStruct.encode { encoder in 27 | XCTAssertEqual(encoder.codingPath, []) 28 | var container = encoder.container(keyedBy: SomeKey.self) 29 | let unkeyed = container.nestedUnkeyedContainer(forKey: .value) 30 | XCTAssertEqual(unkeyed.codingPath, [1]) 31 | } 32 | GenericTestStruct.decode { decoder in 33 | XCTAssertEqual(decoder.codingPath, []) 34 | let container = try decoder.container(keyedBy: SomeKey.self) 35 | let unkeyed = try container.nestedUnkeyedContainer(forKey: .value) 36 | XCTAssertEqual(unkeyed.codingPath, [1]) 37 | } 38 | try compare(GenericTestStruct()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Extensions/Data+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | 5 | /// An empty data instance 6 | static var empty: Data { 7 | .init() 8 | } 9 | 10 | /// The data converted to a byte array 11 | var bytes: [UInt8] { 12 | Array(self) 13 | } 14 | 15 | /// The data in reverse ordering 16 | var swapped: Data { 17 | Data(reversed()) 18 | } 19 | } 20 | 21 | extension Sequence { 22 | 23 | func mapAndJoin(_ closure: (Element) throws -> Data) rethrows -> Data { 24 | var result = Data() 25 | for (value) in self { 26 | let data = try closure(value) 27 | result.append(data) 28 | } 29 | return result 30 | } 31 | } 32 | 33 | extension Data { 34 | 35 | /** 36 | Interpret the binary data as another type. 37 | - Parameter type: The type to interpret 38 | */ 39 | func interpreted(as type: T.Type = T.self) -> T { 40 | Data(self).withUnsafeBytes { 41 | $0.baseAddress!.load(as: T.self) 42 | } 43 | } 44 | 45 | /** 46 | Extract the binary representation of a value. 47 | - Parameter value: The value to convert to binary data 48 | */ 49 | init(underlying value: T) { 50 | var target = value 51 | self = Swift.withUnsafeBytes(of: &target) { 52 | Data($0) 53 | } 54 | } 55 | } 56 | 57 | extension Optional { 58 | 59 | var view: String { 60 | guard let self else { 61 | return "nil" 62 | } 63 | return "\(Array(self))" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/EncodableContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A protocol adopted by primitive types for encoding. 5 | */ 6 | protocol EncodableContainer { 7 | 8 | /// Indicate if the container needs to have a length prepended 9 | var needsLengthData: Bool { get } 10 | 11 | /// Indicate if the container can encode nil 12 | var needsNilIndicator: Bool { get } 13 | 14 | /// Indicate if the container encodes nil 15 | /// - Note: This property must not be `true` if `needsNilIndicator` is set to `false` 16 | var isNil: Bool { get } 17 | 18 | /** 19 | Provide the data encoded in the container 20 | - Note: No length information must be included 21 | - Note: This function is only called if `isNil` is false 22 | */ 23 | func containedData() throws -> Data 24 | } 25 | 26 | extension EncodableContainer { 27 | 28 | /** 29 | The full data encoded in the container, including nil indicator and length, if needed 30 | */ 31 | func completeData() throws -> Data { 32 | guard !isNil else { 33 | // A nil value always means: 34 | // - That the length is zero 35 | // - That a nil indicator is needed 36 | return Data([0x01]) 37 | } 38 | let data = try containedData() 39 | if needsLengthData { 40 | // It doesn't matter if `needsNilIndicator` is true or false 41 | // Length always includes it 42 | return data.count.lengthData + data 43 | } 44 | if needsNilIndicator { 45 | return Data([0x00]) + data 46 | } 47 | return data 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/ValueEncoderStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | The backing storage for single value containers. 5 | 6 | Encodes a single value. 7 | It can be set multiple times, but only the last value is used. 8 | */ 9 | final class ValueEncoderStorage: AbstractEncodingNode { 10 | 11 | private var encodedValue: EncodableContainer? 12 | 13 | init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { 14 | super.init(needsLengthData: false, codingPath: codingPath, userInfo: userInfo) 15 | } 16 | 17 | func encodeNil() throws { 18 | // Note: An already encoded value will simply be replaced 19 | // This is consistent with the implementation of JSONEncoder() 20 | encodedValue = NilContainer() 21 | } 22 | 23 | func encode(_ value: T) throws where T : Encodable { 24 | // Note: An already encoded value will simply be replaced 25 | // This is consistent with the implementation of JSONEncoder() 26 | self.encodedValue = try encodeValue(value, needsLengthData: false) 27 | } 28 | } 29 | 30 | extension ValueEncoderStorage: EncodableContainer { 31 | 32 | var needsNilIndicator: Bool { true } 33 | 34 | var isNil: Bool { 35 | encodedValue is NilContainer 36 | } 37 | 38 | func containedData() throws -> Data { 39 | guard let encodedValue else { 40 | // TODO: Provide value of outer encoding node in error 41 | throw EncodingError.invalidValue(0, .init(codingPath: codingPath, debugDescription: "No value or nil encoded in single value container")) 42 | } 43 | let data = try encodedValue.completeData() 44 | return data 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/UInt8+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UInt8: EncodablePrimitive { 4 | 5 | var encodedData: Data { 6 | Data([self]) 7 | } 8 | } 9 | 10 | extension UInt8: DecodablePrimitive { 11 | 12 | init(data: Data) throws { 13 | guard data.count == 1 else { 14 | throw CorruptedDataError(invalidSize: data.count, for: "UInt8") 15 | } 16 | self = data[data.startIndex] 17 | } 18 | } 19 | 20 | // - MARK: Fixed size 21 | 22 | extension UInt8: FixedSizeEncodable { 23 | 24 | public var fixedSizeEncoded: Data { 25 | encodedData 26 | } 27 | } 28 | 29 | extension UInt8: FixedSizeDecodable { 30 | 31 | public init(fromFixedSize data: Data) throws { 32 | try self.init(data: data) 33 | } 34 | } 35 | 36 | extension FixedSizeEncoded where WrappedValue == UInt8 { 37 | 38 | /** 39 | Wrap a UInt8 to enforce fixed-size encoding. 40 | - Parameter wrappedValue: The value to wrap 41 | - Note: `UInt8` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. 42 | */ 43 | @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type UInt8") 44 | public init(wrappedValue: UInt8) { 45 | self.wrappedValue = wrappedValue 46 | } 47 | } 48 | 49 | // - MARK: Packed 50 | 51 | extension UInt8: PackedEncodable { 52 | 53 | } 54 | 55 | extension UInt8: PackedDecodable { 56 | 57 | init(data: Data, index: inout Int) throws { 58 | guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { 59 | throw CorruptedDataError.init(prematureEndofDataDecoding: "UInt8") 60 | } 61 | try self.init(fromFixedSize: bytes) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Int8+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Int8: EncodablePrimitive { 4 | 5 | var encodedData: Data { 6 | Data([UInt8(bitPattern: self)]) 7 | } 8 | } 9 | 10 | extension Int8: DecodablePrimitive { 11 | 12 | init(data: Data) throws { 13 | guard data.count == 1 else { 14 | throw CorruptedDataError(invalidSize: data.count, for: "Int8") 15 | } 16 | self.init(bitPattern: data[data.startIndex]) 17 | } 18 | } 19 | 20 | // - MARK: Fixed size 21 | 22 | extension Int8: FixedSizeEncodable { 23 | 24 | public var fixedSizeEncoded: Data { 25 | encodedData 26 | } 27 | } 28 | 29 | extension Int8: FixedSizeDecodable { 30 | 31 | public init(fromFixedSize data: Data) throws { 32 | try self.init(data: data) 33 | } 34 | } 35 | 36 | extension FixedSizeEncoded where WrappedValue == Int8 { 37 | 38 | /** 39 | Wrap a Int8 to enforce fixed-size encoding. 40 | - Parameter wrappedValue: The value to wrap 41 | - Note: `Int8` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. 42 | */ 43 | @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Int8") 44 | public init(wrappedValue: Int8) { 45 | self.wrappedValue = wrappedValue 46 | } 47 | } 48 | 49 | // - MARK: Packed 50 | 51 | extension Int8: PackedEncodable { 52 | 53 | } 54 | 55 | extension Int8: PackedDecodable { 56 | 57 | init(data: Data, index: inout Int) throws { 58 | guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { 59 | throw CorruptedDataError.init(prematureEndofDataDecoding: "Int8") 60 | } 61 | try self.init(fromFixedSize: bytes) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Float+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Float: EncodablePrimitive { 4 | 5 | var encodedData: Data { 6 | .init(underlying: bitPattern.bigEndian) 7 | } 8 | } 9 | 10 | extension Float: DecodablePrimitive { 11 | 12 | init(data: Data) throws { 13 | guard data.count == MemoryLayout.size else { 14 | throw CorruptedDataError(invalidSize: data.count, for: "Float") 15 | } 16 | let value = UInt32(bigEndian: data.interpreted()) 17 | self.init(bitPattern: value) 18 | } 19 | } 20 | 21 | // - MARK: Fixed size 22 | 23 | extension Float: FixedSizeEncodable { 24 | 25 | public var fixedSizeEncoded: Data { 26 | encodedData 27 | } 28 | } 29 | 30 | extension Float: FixedSizeDecodable { 31 | 32 | public init(fromFixedSize data: Data) throws { 33 | try self.init(data: data) 34 | } 35 | } 36 | 37 | extension FixedSizeEncoded where WrappedValue == Float { 38 | 39 | /** 40 | Wrap a float to enforce fixed-size encoding. 41 | - Parameter wrappedValue: The value to wrap 42 | - Note: `Float` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. 43 | */ 44 | @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Float") 45 | public init(wrappedValue: Float) { 46 | self.wrappedValue = wrappedValue 47 | } 48 | } 49 | 50 | // - MARK: Packed 51 | 52 | extension Float: PackedEncodable { 53 | 54 | } 55 | 56 | extension Float: PackedDecodable { 57 | 58 | init(data: Data, index: inout Int) throws { 59 | guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { 60 | throw CorruptedDataError.init(prematureEndofDataDecoding: "Float") 61 | } 62 | try self.init(data: bytes) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Double+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Double: EncodablePrimitive { 4 | 5 | var encodedData: Data { 6 | .init(underlying: bitPattern.bigEndian) 7 | } 8 | } 9 | 10 | extension Double: DecodablePrimitive { 11 | 12 | init(data: Data) throws { 13 | guard data.count == MemoryLayout.size else { 14 | throw CorruptedDataError(invalidSize: data.count, for: "Double") 15 | } 16 | let value = UInt64(bigEndian: data.interpreted()) 17 | self.init(bitPattern: value) 18 | } 19 | } 20 | 21 | // - MARK: Fixed size 22 | 23 | extension Double: FixedSizeEncodable { 24 | 25 | public var fixedSizeEncoded: Data { 26 | encodedData 27 | } 28 | } 29 | 30 | extension Double: FixedSizeDecodable { 31 | 32 | public init(fromFixedSize data: Data) throws { 33 | try self.init(data: data) 34 | } 35 | } 36 | 37 | extension FixedSizeEncoded where WrappedValue == Double { 38 | 39 | /** 40 | Wrap a double to enforce fixed-size encoding. 41 | - Parameter wrappedValue: The value to wrap 42 | - Note: `Double` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. 43 | */ 44 | @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Double") 45 | public init(wrappedValue: Double) { 46 | self.wrappedValue = wrappedValue 47 | } 48 | } 49 | 50 | // - MARK: Packed 51 | 52 | extension Double: PackedEncodable { 53 | 54 | } 55 | 56 | extension Double: PackedDecodable { 57 | 58 | init(data: Data, index: inout Int) throws { 59 | guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { 60 | throw CorruptedDataError.init(prematureEndofDataDecoding: "Double") 61 | } 62 | try self.init(data: bytes) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/AbstractEncodingNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A class that provides encoding functions for all encoding containers. 5 | */ 6 | class AbstractEncodingNode: AbstractNode { 7 | 8 | let needsLengthData: Bool 9 | 10 | /** 11 | - Parameter codingPath: The path to get to this point in encoding or decoding 12 | - Parameter userInfo: Contextual information set by the user 13 | */ 14 | init(needsLengthData: Bool, codingPath: [CodingKey], userInfo: UserInfo) { 15 | self.needsLengthData = needsLengthData 16 | super.init(codingPath: codingPath, userInfo: userInfo) 17 | } 18 | 19 | /** 20 | Sort keyed data in the binary representation. 21 | 22 | Enabling this option causes all keyed data (e.g. `Dictionary`, `Struct`) to be sorted by their keys before encoding. 23 | This enables deterministic encoding where the binary output is consistent across multiple invocations. 24 | 25 | Enabling this option introduces computational overhead due to sorting, which can become significant when dealing with many entries. 26 | 27 | This option has no impact on decoding using `BinaryDecoder`. 28 | 29 | - Note: The default value for this option is `false`. 30 | */ 31 | var sortKeysDuringEncoding: Bool { 32 | userInfo[BinaryEncoder.userInfoSortKey] as? Bool ?? false 33 | } 34 | 35 | func encodeValue(_ value: T, needsLengthData: Bool) throws -> EncodableContainer where T : Encodable { 36 | if T.self is EncodablePrimitive.Type, let base = value as? EncodablePrimitive { 37 | return PrimitiveEncodingContainer(wrapped: base, needsLengthData: needsLengthData) 38 | } else { 39 | let encoder = EncodingNode(needsLengthData: needsLengthData, codingPath: codingPath, userInfo: userInfo) 40 | try value.encode(to: encoder) 41 | return encoder 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/Helper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import BinaryCodable 4 | 5 | func XCTAssertEqual(_ path: [CodingKey], _ other: [DecodingKey]) { 6 | let convertedPath = path.map { DecodingKey(key: $0) } 7 | XCTAssertEqual(convertedPath, other) 8 | } 9 | 10 | 11 | extension XCTestCase { 12 | 13 | func compare(_ value: T, of type: T.Type = T.self, toOneOf possibleEncodings: [[UInt8]]) throws where T: Codable, T: Equatable { 14 | let encoder = BinaryEncoder() 15 | let data = try encoder.encode(value) 16 | let bytes = Array(data) 17 | if !possibleEncodings.contains(bytes) { 18 | XCTFail("Encoded data is: \(bytes), allowed options: \(possibleEncodings)") 19 | } 20 | let decoder = BinaryDecoder() 21 | let decoded = try decoder.decode(T.self, from: data) 22 | XCTAssertEqual(decoded, value) 23 | } 24 | 25 | func compare(_ value: T, of type: T.Type = T.self, to expected: [UInt8]? = nil, sortingKeys: Bool = false) throws where T: Codable, T: Equatable { 26 | var encoder = BinaryEncoder() 27 | if sortingKeys { 28 | encoder.sortKeysDuringEncoding = true 29 | } 30 | let data = try encoder.encode(value) 31 | if let expected { 32 | XCTAssertEqual(Array(data), expected) 33 | } else { 34 | print("Encoded data: \(Array(data))") 35 | } 36 | 37 | let decoder = BinaryDecoder() 38 | let decoded = try decoder.decode(T.self, from: data) 39 | XCTAssertEqual(value, decoded) 40 | } 41 | 42 | func compareEncoding(of value: T, withType type: T.Type = T.self, isEqualTo expected: [UInt8]) throws where T: EncodablePrimitive, T: DecodablePrimitive, T: Equatable { 43 | let encoded = value.encodedData 44 | XCTAssertEqual(Array(encoded), expected) 45 | let decoded = try T.init(data: encoded) 46 | XCTAssertEqual(decoded, value) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/GenericTestStruct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A struct to provide custom encode and decode functions via static properties 5 | for testing. Reduces the need to create full struct definitions when testing custom 6 | encoding and decoding routines 7 | */ 8 | struct GenericTestStruct: Codable, Equatable { 9 | 10 | init() { 11 | 12 | } 13 | 14 | init(from decoder: Decoder) throws { 15 | try GenericTestStruct.decodingRoutine(decoder) 16 | } 17 | 18 | func encode(to encoder: Encoder) throws { 19 | try GenericTestStruct.encodingRoutine(encoder) 20 | } 21 | 22 | private static nonisolated(unsafe) var _encodingRoutine: (Encoder) throws -> Void = { _ in } 23 | 24 | private static nonisolated(unsafe) var _decodingRoutine: (Decoder) throws -> Void = { _ in } 25 | 26 | private static let encodeSemaphore = DispatchSemaphore(value: 1) 27 | 28 | static var encodingRoutine: (Encoder) throws -> Void { 29 | get { 30 | encodeSemaphore.wait() 31 | let value = _encodingRoutine 32 | encodeSemaphore.signal() 33 | return value 34 | } 35 | set { 36 | encodeSemaphore.wait() 37 | _encodingRoutine = newValue 38 | encodeSemaphore.signal() 39 | } 40 | } 41 | 42 | private static let decodeSemaphore = DispatchSemaphore(value: 1) 43 | 44 | static var decodingRoutine: (Decoder) throws -> Void { 45 | get { 46 | decodeSemaphore.wait() 47 | let value = _decodingRoutine 48 | decodeSemaphore.signal() 49 | return value 50 | } 51 | set { 52 | decodeSemaphore.wait() 53 | _decodingRoutine = newValue 54 | decodeSemaphore.signal() 55 | } 56 | } 57 | 58 | static func encode(_ block: @escaping (Encoder) throws -> Void) { 59 | encodingRoutine = block 60 | } 61 | 62 | static func decode(_ block: @escaping (Decoder) throws -> Void) { 63 | decodingRoutine = block 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/KeyedEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct KeyedEncoder: KeyedEncodingContainerProtocol where Key: CodingKey { 4 | 5 | let storage: KeyedEncoderStorage 6 | 7 | init(storage: KeyedEncoderStorage) { 8 | self.storage = storage 9 | } 10 | 11 | var codingPath: [any CodingKey] { 12 | storage.codingPath 13 | } 14 | 15 | func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { 16 | // By wrapping the nested container in a node, it adds length information to it 17 | let node = storage.assignedNode(forKey: key) 18 | return KeyedEncodingContainer(node.container(keyedBy: keyType)) 19 | } 20 | 21 | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 22 | // By wrapping the nested container in a node, it adds length information to it 23 | storage.assignedNode(forKey: key).unkeyedContainer() 24 | } 25 | 26 | func superEncoder() -> Encoder { 27 | storage.assignedNode(forKey: SuperCodingKey()) 28 | } 29 | 30 | func superEncoder(forKey key: Key) -> Encoder { 31 | storage.assignedNode(forKey: key) 32 | } 33 | 34 | func encodeNil(forKey key: Key) throws { 35 | // If a value is nil, then it is not encoded 36 | // This is not consistent with the documentation of `decodeNil(forKey:)`, 37 | // which states that when decodeNil() should fail if the key is not present. 38 | // We could fix this by explicitly assigning a `nil` value: 39 | // `assign(NilContainer(), forKey: key)` 40 | // But this would cause other problems, either breaking the decoding of double optionals (e.g. Int??), 41 | // Or by requiring an additional `nil` indicator for ALL values in keyed containers, 42 | // which would make the format a lot less efficient 43 | } 44 | 45 | func encode(_ value: T, forKey key: Key) throws where T : Encodable { 46 | try storage.encode(value, forKey: key) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/CorruptedDataError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | An error which occurs when the data to decode is in an incorrect format. 5 | 6 | This error is used internally during decoding, and should not occur when using ``BinaryDecoder``, ``BinaryStreamDecoder`` or ``BinaryFileDecoder``. 7 | It is only relevant when directly using the decoding initializers for ``FixedSizeDecodable``, ``VariableLengthDecodable``, or ``ZigZagDecodable``. 8 | */ 9 | public struct CorruptedDataError: Error { 10 | 11 | /// A textual description of the error 12 | public let description: String 13 | 14 | init(invalidSize size: Int, for type: String) { 15 | self.description = "Invalid size \(size) for type \(type)" 16 | } 17 | 18 | init(multipleValuesForKey key: DecodingKey) { 19 | self.description = "Multiple values for key '\(key)' in keyed container" 20 | } 21 | 22 | init(outOfRange value: CustomStringConvertible, forType type: String) { 23 | self.description = "Decoded value '\(value)' is out of range for type \(type)" 24 | } 25 | 26 | init(unusedBytes: Int, during process: String) { 27 | self.description = "Found \(unusedBytes) unused bytes during \(process)" 28 | } 29 | 30 | init(invalidBoolByte: UInt8) { 31 | self.description = "Found invalid boolean value '\(invalidBoolByte)'" 32 | } 33 | 34 | init(prematureEndofDataDecoding decodingStep: String) { 35 | self.description = "Premature end of data decoding \(decodingStep)" 36 | } 37 | 38 | init(invalidString length: Int) { 39 | self.description = "Non-UTF8 string found (\(length) bytes)" 40 | } 41 | 42 | func adding(codingPath: [CodingKey]) -> DecodingError { 43 | .corrupted(description, codingPath: codingPath) 44 | } 45 | } 46 | 47 | func wrapCorruptDataError(at codingPath: [CodingKey] = [], _ closure: () throws -> T) rethrows -> T { 48 | do { 49 | return try closure() 50 | } catch let error as CorruptedDataError { 51 | throw error.adding(codingPath: codingPath) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Bool+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Bool: EncodablePrimitive { 4 | 5 | /// The boolean encoded as data 6 | var encodedData: Data { 7 | Data([self ? 1 : 0]) 8 | } 9 | } 10 | 11 | extension Bool: DecodablePrimitive { 12 | 13 | private init(byte: UInt8) throws { 14 | switch byte { 15 | case 0: 16 | self = false 17 | case 1: 18 | self = true 19 | default: 20 | throw CorruptedDataError(invalidBoolByte: byte) 21 | } 22 | } 23 | 24 | /** 25 | Decode a boolean from encoded data. 26 | - Parameter data: The data to decode 27 | - Throws: ``CorruptedDataError`` 28 | */ 29 | init(data: Data) throws { 30 | guard data.count == 1 else { 31 | throw CorruptedDataError(invalidSize: data.count, for: "Bool") 32 | } 33 | try self.init(byte: data[data.startIndex]) 34 | } 35 | } 36 | 37 | // - MARK: Fixed size 38 | 39 | extension Bool: FixedSizeEncodable { 40 | 41 | public var fixedSizeEncoded: Data { 42 | encodedData 43 | } 44 | } 45 | 46 | extension Bool: FixedSizeDecodable { 47 | 48 | public init(fromFixedSize data: Data) throws { 49 | try self.init(data: data) 50 | } 51 | } 52 | 53 | extension FixedSizeEncoded where WrappedValue == Bool { 54 | 55 | /** 56 | Wrap a Bool to enforce fixed-size encoding. 57 | - Parameter wrappedValue: The value to wrap 58 | - Note: `Bool` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. 59 | */ 60 | @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Bool") 61 | public init(wrappedValue: Bool) { 62 | self.wrappedValue = wrappedValue 63 | } 64 | } 65 | 66 | // - MARK: Packed 67 | 68 | extension Bool: PackedEncodable { 69 | 70 | } 71 | 72 | extension Bool: PackedDecodable { 73 | 74 | init(data: Data, index: inout Int) throws { 75 | guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { 76 | throw CorruptedDataError.init(prematureEndofDataDecoding: "Bool") 77 | } 78 | try self.init(fromFixedSize: bytes) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/BinaryDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A decoder to convert data encoded with `BinaryEncoder` back to a `Codable` types. 5 | 6 | To decode from data, instantiate a decoder and specify the type: 7 | ``` 8 | let decoder = BinaryDecoder() 9 | let message = try decoder.decode(Message.self, from: data) 10 | ``` 11 | Alternatively, the type can be inferred from context: 12 | ``` 13 | func decode(data: Data) throws -> Message { 14 | try BinaryDecoder().decode(from: data) 15 | } 16 | ``` 17 | There are also convenience functions to directly decode a single instance: 18 | ``` 19 | let message = try BinaryDecoder.decode(Message.self, from: data) 20 | ``` 21 | - Note: A single decoder can be used to decode multiple objects. 22 | */ 23 | public struct BinaryDecoder { 24 | 25 | /** 26 | Any contextual information set by the user for decoding. 27 | 28 | This dictionary is passed to all containers during the decoding process. 29 | */ 30 | public var userInfo: [CodingUserInfoKey : Any] = [:] 31 | 32 | /** 33 | Create a new decoder. 34 | - Note: A single decoder can be reused to decode multiple objects. 35 | */ 36 | public init() { 37 | 38 | } 39 | 40 | /** 41 | Decode a type from binary data. 42 | - Parameter type: The type to decode. 43 | - Parameter data: The binary data which encodes the instance 44 | - Returns: The decoded instance 45 | - Throws: Errors of type `DecodingError` 46 | */ 47 | public func decode(_ type: T.Type = T.self, from data: Data) throws -> T where T: Decodable { 48 | try decode(element: data, type: type, codingPath: []) 49 | } 50 | 51 | /** 52 | Decode a single value from binary data using a default decoder. 53 | - Parameter type: The type to decode. 54 | - Parameter data: The binary data which encodes the instance 55 | - Returns: The decoded instance 56 | - Throws: Errors of type `DecodingError` 57 | */ 58 | public static func decode(_ type: T.Type = T.self, from data: Data) throws -> T where T: Decodable { 59 | try BinaryDecoder().decode(type, from: data) 60 | } 61 | } 62 | 63 | extension BinaryDecoder: AbstractDecoder { 64 | 65 | var parentDecodedNil: Bool { false } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/UnkeyedContainerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | final class UnkeyedContainerTests: XCTestCase { 5 | 6 | func testCountAndIndexInUnkeyedContainer() throws { 7 | GenericTestStruct.encode { encoder in 8 | var container = encoder.unkeyedContainer() 9 | try container.encode(true) 10 | try container.encode("Some") 11 | try container.encode(123) 12 | } 13 | 14 | GenericTestStruct.decode { decoder in 15 | var container = try decoder.unkeyedContainer() 16 | if let count = container.count { 17 | XCTAssertEqual(count, 3) 18 | } else { 19 | XCTFail("No count in unkeyed container") 20 | } 21 | XCTAssertEqual(container.currentIndex, 0) 22 | XCTAssertEqual(container.isAtEnd, false) 23 | 24 | XCTAssertEqual(try container.decode(Bool.self), true) 25 | XCTAssertEqual(container.currentIndex, 1) 26 | XCTAssertEqual(container.isAtEnd, false) 27 | 28 | XCTAssertEqual(try container.decode(String.self), "Some") 29 | XCTAssertEqual(container.currentIndex, 2) 30 | XCTAssertEqual(container.isAtEnd, false) 31 | 32 | XCTAssertEqual(try container.decode(Int.self), 123) 33 | XCTAssertEqual(container.currentIndex, 3) 34 | XCTAssertEqual(container.isAtEnd, true) 35 | } 36 | try compare(GenericTestStruct()) 37 | } 38 | 39 | func testIntSet() throws { 40 | let value: Set = [1, 2, 3, 123, Int.max, Int.min] 41 | try compare(value) 42 | } 43 | 44 | func testSetOfStructs() throws { 45 | struct Test: Codable, Hashable { 46 | let value: String 47 | } 48 | let values: Set = [.init(value: "Some"), .init(value: "More"), .init(value: "Test")] 49 | try compare(values) 50 | } 51 | 52 | func testSetOfOptionals() throws { 53 | let value: Set = [true, false, nil] 54 | try compare(value) 55 | } 56 | 57 | func testOptionalSet() throws { 58 | let value: Set? = [true, false] 59 | try compare(value) 60 | 61 | let value2: Set? = nil 62 | try compare(value2) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/UnkeyedDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class UnkeyedDecoder: AbstractDecodingNode, UnkeyedDecodingContainer { 4 | 5 | var count: Int? { data.count } 6 | 7 | var isAtEnd: Bool { currentIndex >= data.count } 8 | 9 | private(set) var currentIndex: Int = 0 10 | 11 | private let data: [Data?] 12 | 13 | init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) throws { 14 | self.data = try wrapCorruptDataError(at: codingPath) { 15 | try data.decodeUnkeyedElements() 16 | } 17 | super.init(parentDecodedNil: true, codingPath: codingPath, userInfo: userInfo) 18 | } 19 | 20 | /// Get the next element without advancing the index. 21 | private func ensureNextElement() throws -> Data? { 22 | guard currentIndex < data.count else { 23 | throw DecodingError.corrupted("No more elements to decode", codingPath: codingPath) 24 | } 25 | return data[currentIndex] 26 | } 27 | 28 | /// Get the next element and advance the index. 29 | private func nextElement() throws -> Data? { 30 | let element = try ensureNextElement() 31 | currentIndex += 1 32 | return element 33 | } 34 | 35 | private func nextNode() throws -> DecodingNode { 36 | let element = try nextElement() 37 | return try DecodingNode(data: element, parentDecodedNil: true, codingPath: codingPath, userInfo: userInfo) 38 | } 39 | 40 | func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { 41 | KeyedDecodingContainer(try nextNode().container(keyedBy: type)) 42 | } 43 | 44 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 45 | try nextNode().unkeyedContainer() 46 | } 47 | 48 | func superDecoder() throws -> Decoder { 49 | try nextNode() 50 | } 51 | 52 | func decodeNil() throws -> Bool { 53 | if try ensureNextElement() == nil { 54 | currentIndex += 1 55 | return true 56 | } 57 | return false 58 | } 59 | 60 | func decode(_ type: T.Type) throws -> T where T : Decodable { 61 | let element = try nextElement() 62 | return try decode(element: element, type: type, codingPath: codingPath) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/DecodingErrorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | /** 5 | A variety of tests to ensure that errors are handled correctly 6 | */ 7 | final class DecodingErrorTests: XCTestCase { 8 | 9 | func testInt32OutOfRange() throws { 10 | // Encode a value that is out of range 11 | let value = Int64(Int32.max) + 1 12 | let encoded = try BinaryEncoder.encode(value) 13 | 14 | do { 15 | _ = try BinaryDecoder.decode(Int32.self, from: encoded) 16 | XCTFail("Should fail to decode Int32") 17 | } catch let DecodingError.dataCorrupted(context) { 18 | XCTAssertEqual(context.codingPath, []) 19 | } 20 | 21 | do { 22 | let raw = try UInt64(fromVarintData: encoded) 23 | _ = try Int32(fromZigZag: raw) 24 | XCTFail("Should fail to decode Int32") 25 | } catch is CorruptedDataError { 26 | 27 | } 28 | 29 | // Encode a value that is out of range 30 | let value2 = Int64(Int32.min) - 1 31 | let encoded2 = try BinaryEncoder.encode(value2) 32 | 33 | do { 34 | _ = try BinaryDecoder.decode(Int32.self, from: encoded2) 35 | XCTFail("Should fail to decode Int32") 36 | } catch let DecodingError.dataCorrupted(context) { 37 | XCTAssertEqual(context.codingPath, []) 38 | } 39 | 40 | do { 41 | let raw = try UInt64(fromVarintData: encoded2) 42 | _ = try Int32(fromZigZag: raw) 43 | XCTFail("Should fail to decode Int32") 44 | } catch is CorruptedDataError { 45 | 46 | } 47 | } 48 | 49 | func testUInt32OutOfRange() throws { 50 | // Encode a value that is out of range 51 | let value = UInt64(UInt32.max) + 1 52 | let encoded = try BinaryEncoder.encode(value) 53 | 54 | do { 55 | _ = try BinaryDecoder.decode(UInt32.self, from: encoded) 56 | XCTFail("Should fail to decode UInt32") 57 | } catch let DecodingError.dataCorrupted(context) { 58 | XCTAssertEqual(context.codingPath, []) 59 | } 60 | 61 | do { 62 | _ = try UInt32(fromVarint: value) 63 | XCTFail("Should fail to decode UInt32") 64 | } catch is CorruptedDataError { 65 | 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Wrappers/Packed.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | public struct Packed where WrappedValue: RangeReplaceableCollection { 5 | 6 | /// The sequence wrapped in the packed container 7 | public var wrappedValue: WrappedValue 8 | 9 | /** 10 | Wrap an integer value in a fixed-size container 11 | - Parameter wrappedValue: The sequence to wrap 12 | */ 13 | public init(wrappedValue: WrappedValue) { 14 | self.wrappedValue = wrappedValue 15 | } 16 | } 17 | 18 | extension Packed: EncodablePrimitive where WrappedValue.Element: PackedEncodable { 19 | 20 | /** 21 | Encode the wrapped value to binary data compatible with the protobuf encoding. 22 | - Returns: The binary data in host-independent format. 23 | */ 24 | var encodedData: Data { 25 | wrappedValue.mapAndJoin { 26 | let data = $0.encodedData 27 | return data.count.variableLengthEncoding + data 28 | } 29 | } 30 | } 31 | 32 | extension Packed: DecodablePrimitive where WrappedValue.Element: PackedDecodable { 33 | 34 | init(data: Data) throws { 35 | var index = data.startIndex 36 | var elements = [WrappedValue.Element]() 37 | while !data.isAtEnd(at: index) { 38 | let element = try WrappedValue.Element.init(data: data, index: &index) 39 | elements.append(element) 40 | } 41 | self.wrappedValue = WrappedValue.init(elements) 42 | } 43 | } 44 | 45 | extension Packed: Encodable where WrappedValue.Element: Encodable { 46 | 47 | /** 48 | Encode the wrapped value transparently to the given encoder. 49 | - Parameter encoder: The encoder to use for encoding. 50 | - Throws: Errors from the decoder when attempting to encode a value in a single value container. 51 | */ 52 | public func encode(to encoder: Encoder) throws { 53 | var container = encoder.unkeyedContainer() 54 | for element in wrappedValue { 55 | try container.encode(element) 56 | } 57 | } 58 | } 59 | 60 | extension Packed: Decodable where WrappedValue.Element: Decodable { 61 | /** 62 | Decode a wrapped value from a decoder. 63 | - Parameter decoder: The decoder to use for decoding. 64 | - Throws: Errors from the decoder when reading a single value container or decoding the wrapped value from it. 65 | */ 66 | public init(from decoder: Decoder) throws { 67 | var container = try decoder.unkeyedContainer() 68 | var elements = WrappedValue() 69 | while !container.isAtEnd { 70 | let next = try container.decode(WrappedValue.Element.self) 71 | elements.append(next) 72 | } 73 | self.wrappedValue = elements 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/KeyedEncoderStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class KeyedEncoderStorage: AbstractEncodingNode { 4 | 5 | private var encodedValues: [HashableKey : EncodableContainer] = [:] 6 | 7 | @discardableResult 8 | private func assign(_ value: T, forKey key: CodingKey) -> T where T: EncodableContainer { 9 | let hashableKey = HashableKey(key: key) 10 | encodedValues[hashableKey] = value 11 | return value 12 | } 13 | 14 | func assignedNode(forKey key: CodingKey) -> EncodingNode { 15 | let node = EncodingNode(needsLengthData: true, codingPath: codingPath + [key], userInfo: userInfo) 16 | return assign(node, forKey: key) 17 | } 18 | 19 | func superEncoder() -> Encoder { 20 | assignedNode(forKey: SuperCodingKey()) 21 | } 22 | 23 | func superEncoder(forKey key: CodingKey) -> Encoder { 24 | assignedNode(forKey: key) 25 | } 26 | 27 | func encodeNil(forKey key: CodingKey) throws { 28 | // If a value is nil, then it is not encoded 29 | // This is not consistent with the documentation of `decodeNil(forKey:)`, 30 | // which states that decodeNil() should fail if the key is not present. 31 | // We could fix this by explicitly assigning a `nil` value: 32 | // `assign(NilContainer(), forKey: key)` 33 | // But this would cause other problems, either breaking the decoding of double optionals (e.g. Int??), 34 | // Or by requiring an additional `nil` indicator for ALL values in keyed containers, 35 | // which would make the format a lot less efficient 36 | } 37 | 38 | func encode(_ value: T, forKey key: CodingKey) throws where T : Encodable { 39 | let encoded = try encodeValue(value, needsLengthData: true) 40 | assign(encoded, forKey: key) 41 | } 42 | } 43 | 44 | extension KeyedEncoderStorage: EncodableContainer { 45 | 46 | var needsNilIndicator: Bool { 47 | false 48 | } 49 | 50 | var isNil: Bool { 51 | false 52 | } 53 | 54 | func containedData() throws -> Data { 55 | guard sortKeysDuringEncoding else { 56 | return try encode(elements: encodedValues) 57 | } 58 | return try encode(elements: encodedValues.sorted { $0.key < $1.key }) 59 | } 60 | 61 | private func encode(elements: T) throws -> Data where T: Collection, T.Element == (key: HashableKey, value: EncodableContainer) { 62 | try elements.mapAndJoin { key, value in 63 | guard let keyData = key.key.keyData() else { 64 | throw EncodingError.invalidValue(key.key.intValue!, .init(codingPath: codingPath + [key.key], debugDescription: "Invalid integer value for coding key")) 65 | } 66 | let data = try value.completeData() 67 | return keyData + data 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/UInt+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UInt: EncodablePrimitive { 4 | 5 | var encodedData: Data { variableLengthEncoding } 6 | } 7 | 8 | extension UInt: DecodablePrimitive { 9 | 10 | /** 11 | Create an integer from variable-length encoded data. 12 | - Parameter data: The data to decode. 13 | - Throws: ``CorruptedDataError`` 14 | */ 15 | init(data: Data) throws { 16 | let raw = try UInt64(fromVarintData: data) 17 | try self.init(fromVarint: raw) 18 | } 19 | } 20 | 21 | // - MARK: Variable-length encoding 22 | 23 | extension UInt: VariableLengthEncodable { 24 | 25 | public var variableLengthEncoding: Data { 26 | UInt64(self).variableLengthEncoding 27 | } 28 | } 29 | 30 | extension UInt: VariableLengthDecodable { 31 | 32 | /** 33 | Create an integer from variable-length encoded data. 34 | - Parameter data: The data to decode. 35 | - Throws: ``CorruptedDataError`` 36 | */ 37 | public init(fromVarint raw: UInt64) throws { 38 | guard let value = UInt(exactly: raw) else { 39 | throw CorruptedDataError(outOfRange: raw, forType: "UInt") 40 | } 41 | self = value 42 | } 43 | } 44 | 45 | extension VariableLengthEncoded where WrappedValue == UInt { 46 | 47 | /** 48 | Wrap an integer to enforce variable-length encoding. 49 | - Parameter wrappedValue: The value to wrap 50 | - Note: `UInt` is already encoded using fixed-size encoding, so wrapping it in `VariableLengthEncoded` does nothing. 51 | */ 52 | @available(*, deprecated, message: "Property wrapper @VariableLengthEncoded has no effect on type UInt") 53 | public init(wrappedValue: UInt) { 54 | self.wrappedValue = wrappedValue 55 | } 56 | } 57 | 58 | // - MARK: Fixed-size encoding 59 | 60 | extension UInt: FixedSizeEncodable { 61 | 62 | /// The value encoded as fixed-size data 63 | public var fixedSizeEncoded: Data { 64 | UInt64(self).fixedSizeEncoded 65 | } 66 | } 67 | 68 | extension UInt: FixedSizeDecodable { 69 | 70 | /** 71 | Decode a value from fixed-size data. 72 | - Parameter data: The data to decode. 73 | - Throws: ``CorruptedDataError`` 74 | */ 75 | public init(fromFixedSize data: Data) throws { 76 | let intValue = try UInt64(fromFixedSize: data) 77 | guard let value = UInt(exactly: intValue) else { 78 | throw CorruptedDataError(outOfRange: intValue, forType: "UInt") 79 | } 80 | self = value 81 | } 82 | } 83 | 84 | // - MARK: Packed 85 | 86 | extension UInt: PackedEncodable { 87 | 88 | } 89 | 90 | extension UInt: PackedDecodable { 91 | 92 | init(data: Data, index: inout Int) throws { 93 | guard let raw = data.decodeUInt64(at: &index) else { 94 | throw CorruptedDataError(prematureEndofDataDecoding: "UInt") 95 | } 96 | try self.init(fromVarint: raw) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/VariableLengthEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | final class VariableLengthEncodingTests: XCTestCase { 5 | 6 | func compare(varInt value: T, of: T.Type, to result: [UInt8]) throws where T: VariableLengthCodable { 7 | let data = value.variableLengthEncoding 8 | XCTAssertEqual(Array(data), result) 9 | let raw = try UInt64(fromVarintData: data) 10 | let decoded = try T(fromVarint: raw) 11 | XCTAssertEqual(decoded, value) 12 | } 13 | 14 | func testEncodeInt() throws { 15 | try compare(varInt: 0, of: Int.self, to: [0]) 16 | try compare(varInt: 123, of: Int.self, to: [123]) 17 | // For max, all next-byte bits are set, and all other bits are also set, except for the 63rd 18 | try compare(varInt: .max, of: Int.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F]) 19 | // For min, only the 63rd bit is set, so the first 8 bytes have only the next-byte bit set, 20 | // and the last byte (which has no next-byte bit, has the highest bit set 21 | try compare(varInt: .min, of: Int.self, to: [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]) 22 | // For -1, all data bits are set, and also all next-byte bits. 23 | try compare(varInt: -1, of: Int.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 24 | } 25 | 26 | func testEncodeInt32() throws { 27 | try compare(varInt: 0, of: Int32.self, to: [0]) 28 | try compare(varInt: 123, of: Int32.self, to: [123]) 29 | // For max, all next-byte bits are set, and all other bits are also set, except for the 63rd 30 | try compare(varInt: .max, of: Int32.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0x07]) 31 | // For min, only the 63rd bit is set, so the first 8 bytes have only the next-byte bit set, 32 | // and the last byte (which has no next-byte bit, has the highest bit set 33 | try compare(varInt: .min, of: Int32.self, to: [0x80, 0x80, 0x80, 0x80, 0x08]) 34 | // For -1, all data bits are set, and also all next-byte bits. 35 | try compare(varInt: -1, of: Int32.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]) 36 | } 37 | 38 | func testEncodeUInt64() throws { 39 | try compare(varInt: 0, of: UInt64.self, to: [0]) 40 | try compare(varInt: 123, of: UInt64.self, to: [123]) 41 | try compare(varInt: 1234, of: UInt64.self, to: [0xD2, 0x09]) 42 | try compare(varInt: 123456, of: UInt64.self, to: [0xC0, 0xC4, 0x07]) 43 | try compare(varInt: 1234567890, of: UInt64.self, to: [0xD2, 0x85, 0xD8, 0xCC, 0x04]) 44 | try compare(varInt: 1234567890123456, of: UInt64.self, to: [0xC0, 0xF5, 0xAA, 0xE4, 0xD3, 0xDA, 0x98, 0x02]) 45 | try compare(varInt: .max - 1, of: UInt64.self, to: [0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 46 | try compare(varInt: .max, of: UInt64.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/UInt16+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UInt16: EncodablePrimitive { 4 | 5 | /// The value encoded as fixed-size data 6 | var encodedData: Data { fixedSizeEncoded } 7 | } 8 | 9 | extension UInt16: DecodablePrimitive { 10 | 11 | /** 12 | Decode a value from fixed-size data. 13 | - Parameter data: The data to decode. 14 | - Throws: ``CorruptedDataError`` 15 | */ 16 | init(data: Data) throws { 17 | try self.init(fromFixedSize: data) 18 | } 19 | } 20 | 21 | // - MARK: Variable-length encoding 22 | 23 | extension UInt16: VariableLengthEncodable { 24 | 25 | public var variableLengthEncoding: Data { 26 | UInt64(self).variableLengthEncoding 27 | } 28 | } 29 | 30 | extension UInt16: VariableLengthDecodable { 31 | 32 | /** 33 | Create an integer from variable-length encoded data. 34 | - Parameter data: The data to decode. 35 | - Throws: ``CorruptedDataError`` 36 | */ 37 | public init(fromVarint raw: UInt64) throws { 38 | let raw = UInt64(fromVarint: raw) 39 | guard let value = UInt16(exactly: raw) else { 40 | throw CorruptedDataError(outOfRange: raw, forType: "UInt16") 41 | } 42 | self = value 43 | } 44 | } 45 | 46 | // - MARK: Fixed-size encoding 47 | 48 | extension UInt16: FixedSizeEncodable { 49 | 50 | /// The value encoded as fixed-size data 51 | public var fixedSizeEncoded: Data { 52 | .init(underlying: littleEndian) 53 | } 54 | } 55 | 56 | extension UInt16: FixedSizeDecodable { 57 | 58 | /** 59 | Decode a value from fixed-size data. 60 | - Parameter data: The data to decode. 61 | - Throws: ``CorruptedDataError`` 62 | */ 63 | public init(fromFixedSize data: Data) throws { 64 | guard data.count == MemoryLayout.size else { 65 | throw CorruptedDataError(invalidSize: data.count, for: "UInt16") 66 | } 67 | self.init(littleEndian: data.interpreted()) 68 | } 69 | } 70 | 71 | extension FixedSizeEncoded where WrappedValue == UInt16 { 72 | 73 | /** 74 | Wrap an integer to enforce fixed-size encoding. 75 | - Parameter wrappedValue: The value to wrap 76 | - Note: `UInt16` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. 77 | */ 78 | @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type UInt16") 79 | public init(wrappedValue: UInt16) { 80 | self.wrappedValue = wrappedValue 81 | } 82 | } 83 | 84 | // - MARK: Packed 85 | 86 | extension UInt16: PackedEncodable { 87 | 88 | } 89 | 90 | extension UInt16: PackedDecodable { 91 | 92 | init(data: Data, index: inout Int) throws { 93 | guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { 94 | throw CorruptedDataError.init(prematureEndofDataDecoding: "UInt16") 95 | } 96 | try self.init(fromFixedSize: bytes) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/DecodingNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A class acting as a decoder, to provide different containers for decoding. 5 | */ 6 | final class DecodingNode: AbstractDecodingNode, Decoder { 7 | 8 | private let data: Data? 9 | 10 | init(data: Data?, parentDecodedNil: Bool, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) throws { 11 | self.data = data 12 | super.init(parentDecodedNil: parentDecodedNil, codingPath: codingPath, userInfo: userInfo) 13 | } 14 | 15 | private func getNonNilElement() throws -> Data { 16 | // Non-root containers just use the data, which can't be nil 17 | guard let data else { 18 | throw DecodingError.corrupted("Container requested, but `nil` found", codingPath: codingPath) 19 | } 20 | return data 21 | } 22 | 23 | /** 24 | Decode an element that can potentially be `nil` for a single value container. 25 | */ 26 | private func getPotentialNilElement() throws -> Data? { 27 | guard !parentDecodedNil else { 28 | return data 29 | } 30 | guard let data else { 31 | return nil 32 | } 33 | return try decodeSingleElementWithNilIndicator(from: data) 34 | } 35 | 36 | /** 37 | Decode just the nil indicator byte, but don't extract a length. Uses all remaining bytes for the value. 38 | - Note: This function is only used for the root node 39 | */ 40 | private func decodeSingleElementWithNilIndicator(from data: Data) throws -> Data? { 41 | guard let first = data.first else { 42 | throw DecodingError.corrupted("Premature end of data while decoding element with nil indicator", codingPath: codingPath) 43 | } 44 | // Check the nil indicator bit 45 | switch first { 46 | case 0: 47 | return data.dropFirst() 48 | case 1: 49 | guard data.count == 1 else { 50 | throw DecodingError.corrupted("\(data.count - 1) additional bytes found after nil indicator", codingPath: codingPath) 51 | } 52 | return nil 53 | default: 54 | throw DecodingError.corrupted("Found unexpected nil indicator \(first)", codingPath: codingPath) 55 | } 56 | } 57 | 58 | func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { 59 | let data = try getNonNilElement() 60 | return KeyedDecodingContainer(try KeyedDecoder(data: data, codingPath: codingPath, userInfo: userInfo)) 61 | } 62 | 63 | func unkeyedContainer() throws -> UnkeyedDecodingContainer { 64 | let data = try getNonNilElement() 65 | return try UnkeyedDecoder(data: data, codingPath: codingPath, userInfo: userInfo) 66 | } 67 | 68 | func singleValueContainer() throws -> SingleValueDecodingContainer { 69 | let data = try getPotentialNilElement() 70 | return ValueDecoder(data: data, codingPath: codingPath, userInfo: userInfo) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/UInt32+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UInt32: EncodablePrimitive { 4 | 5 | /// The value encoded using variable-length encoding 6 | var encodedData: Data { variableLengthEncoding } 7 | } 8 | 9 | extension UInt32: DecodablePrimitive { 10 | 11 | /** 12 | Create an integer from variable-length encoded data. 13 | - Parameter data: The data to decode. 14 | - Throws: ``CorruptedDataError`` 15 | */ 16 | init(data: Data) throws { 17 | let raw = try UInt64(fromVarintData: data) 18 | try self.init(fromVarint: raw) 19 | } 20 | } 21 | 22 | // - MARK: Variable-length encoding 23 | 24 | extension UInt32: VariableLengthEncodable { 25 | 26 | /// The value encoded using variable-length encoding 27 | public var variableLengthEncoding: Data { 28 | UInt64(self).variableLengthEncoding 29 | } 30 | 31 | } 32 | 33 | extension UInt32: VariableLengthDecodable { 34 | 35 | /** 36 | Create an integer from variable-length encoded data. 37 | - Parameter data: The data to decode. 38 | - Throws: ``CorruptedDataError`` 39 | */ 40 | public init(fromVarint raw: UInt64) throws { 41 | guard let value = UInt32(exactly: raw) else { 42 | throw CorruptedDataError(outOfRange: raw, forType: "UInt32") 43 | } 44 | self = value 45 | } 46 | } 47 | 48 | extension VariableLengthEncoded where WrappedValue == UInt32 { 49 | 50 | /** 51 | Wrap an integer to enforce variable-length encoding. 52 | - Parameter wrappedValue: The value to wrap 53 | - Note: `UInt32` is already encoded using fixed-size encoding, so wrapping it in `VariableLengthEncoded` does nothing. 54 | */ 55 | @available(*, deprecated, message: "Property wrapper @VariableLengthEncoded has no effect on type UInt32") 56 | public init(wrappedValue: UInt32) { 57 | self.wrappedValue = wrappedValue 58 | } 59 | } 60 | 61 | // - MARK: Fixed-size encoding 62 | 63 | extension UInt32: FixedSizeEncodable { 64 | 65 | /// The value encoded as fixed-size data 66 | public var fixedSizeEncoded: Data { 67 | Data(underlying: littleEndian) 68 | } 69 | } 70 | 71 | extension UInt32: FixedSizeDecodable { 72 | 73 | /** 74 | Decode a value from fixed-size data. 75 | - Parameter data: The data to decode. 76 | - Throws: ``CorruptedDataError`` 77 | */ 78 | public init(fromFixedSize data: Data) throws { 79 | guard data.count == MemoryLayout.size else { 80 | throw CorruptedDataError(invalidSize: data.count, for: "UInt32") 81 | } 82 | self.init(littleEndian: data.interpreted()) 83 | } 84 | } 85 | 86 | // - MARK: Packed 87 | 88 | extension UInt32: PackedEncodable { 89 | 90 | } 91 | 92 | extension UInt32: PackedDecodable { 93 | 94 | init(data: Data, index: inout Int) throws { 95 | guard let raw = data.decodeUInt64(at: &index) else { 96 | throw CorruptedDataError(prematureEndofDataDecoding: "UInt32") 97 | } 98 | try self.init(fromVarint: raw) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Encoding/EncodingNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class EncodingNode: AbstractEncodingNode, Encoder { 4 | 5 | private var encodedValue: EncodableContainer? = nil 6 | 7 | func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { 8 | guard let encodedValue else { 9 | let storage = KeyedEncoderStorage(needsLengthData: needsLengthData, codingPath: codingPath, userInfo: userInfo) 10 | self.encodedValue = storage 11 | return KeyedEncodingContainer(KeyedEncoder(storage: storage)) 12 | } 13 | guard let storage = encodedValue as? KeyedEncoderStorage else { 14 | fatalError("Call to container(keyedBy:) after already calling unkeyedContainer() or singleValueContainer()") 15 | } 16 | return KeyedEncodingContainer(KeyedEncoder(storage: storage)) 17 | } 18 | 19 | func unkeyedContainer() -> UnkeyedEncodingContainer { 20 | guard let encodedValue else { 21 | let storage = UnkeyedEncoderStorage(needsLengthData: needsLengthData, codingPath: codingPath, userInfo: userInfo) 22 | self.encodedValue = storage 23 | return UnkeyedEncoder(storage: storage) 24 | } 25 | guard let storage = encodedValue as? UnkeyedEncoderStorage else { 26 | fatalError("Call to unkeyedContainer() after already calling container(keyedBy:) or singleValueContainer()") 27 | } 28 | return UnkeyedEncoder(storage: storage) 29 | } 30 | 31 | func singleValueContainer() -> SingleValueEncodingContainer { 32 | guard let encodedValue else { 33 | // No previous container generated, create the storage 34 | // and return a wrapper to it 35 | let storage = ValueEncoderStorage(codingPath: codingPath, userInfo: userInfo) 36 | self.encodedValue = storage 37 | return ValueEncoder(storage: storage) 38 | } 39 | guard let storage = encodedValue as? ValueEncoderStorage else { 40 | fatalError("Call to singleValueContainer() after already calling unkeyedContainer() or container(keyedBy:)") 41 | } 42 | // Multiple calls to singleValueContainer() 43 | // Return a wrapper with the same underlying storage 44 | // The last value encoded to any of the wrappers will be used 45 | return ValueEncoder(storage: storage) 46 | } 47 | } 48 | 49 | extension EncodingNode: EncodableContainer { 50 | 51 | var needsNilIndicator: Bool { 52 | // If no value is encoded, then it doesn't matter what is returned, `encodedData()` will throw an error 53 | encodedValue?.needsNilIndicator ?? false 54 | } 55 | 56 | var isNil: Bool { 57 | // Returning false for missing encodedValue forces an error on `encodedData()` 58 | encodedValue?.isNil ?? false 59 | } 60 | 61 | func containedData() throws -> Data { 62 | guard let encodedValue else { 63 | throw EncodingError.invalidValue(0, .init(codingPath: codingPath, debugDescription: "No calls to container(keyedBy:), unkeyedContainer(), or singleValueContainer()")) 64 | } 65 | let data = try encodedValue.containedData() 66 | return data 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/BoolTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | final class BoolTests: XCTestCase { 5 | 6 | func testBoolAtTopLevel() throws { 7 | try compare(false, to: [0]) 8 | try compare(true, to: [1]) 9 | } 10 | 11 | func testOptionalBool() throws { 12 | try compare(false, of: Bool?.self, to: [0, 0]) 13 | try compare(true, of: Bool?.self, to: [0, 1]) 14 | try compare(nil, of: Bool?.self, to: [1]) 15 | } 16 | 17 | func testArrayBool() throws { 18 | try compare([false], to: [0]) 19 | try compare([true], to: [1]) 20 | try compare([true, false], to: [1, 0]) 21 | try compare([false, true], to: [0, 1]) 22 | } 23 | 24 | func testArrayOptionalBool() throws { 25 | try compare([false], of: [Bool?].self, to: [2, 0]) 26 | try compare([true], of: [Bool?].self, to: [2, 1]) 27 | try compare([true, false], of: [Bool?].self, to: [2, 1, 2, 0]) 28 | try compare([false, true], of: [Bool?].self, to: [2, 0, 2, 1]) 29 | try compare([nil], of: [Bool?].self, to: [1]) 30 | try compare([nil, nil], of: [Bool?].self, to: [1, 1]) 31 | try compare([false, nil], of: [Bool?].self, to: [2, 0, 1]) 32 | try compare([nil, true], of: [Bool?].self, to: [1, 2, 1]) 33 | } 34 | 35 | func testOptionalArrayBool() throws { 36 | try compare(nil, of: [Bool]?.self, to: [1]) 37 | try compare([false], of: [Bool]?.self, to: [0, 0]) 38 | try compare([true], of: [Bool]?.self, to: [0, 1]) 39 | try compare([true, false], of: [Bool]?.self, to: [0, 1, 0]) 40 | try compare([false, true], of: [Bool]?.self, to: [0, 0, 1]) 41 | } 42 | 43 | func testDoubleOptionalBool() throws { 44 | try compare(nil, of: Bool??.self, to: [1]) 45 | try compare(.some(nil), of: Bool??.self, to: [0, 1]) 46 | try compare(true, of: Bool??.self, to: [0, 0, 1]) 47 | try compare(false, of: Bool??.self, to: [0, 0, 0]) 48 | } 49 | 50 | func testTripleOptionalBool() throws { 51 | try compare(nil, of: Bool???.self, to: [1]) 52 | try compare(.some(nil), of: Bool???.self, to: [0, 1]) 53 | try compare(.some(.some(nil)), of: Bool???.self, to: [0, 0, 1]) 54 | try compare(true, of: Bool???.self, to: [0, 0, 0, 1]) 55 | try compare(false, of: Bool???.self, to: [0, 0, 0, 0]) 56 | } 57 | 58 | func testStructWithBool() throws { 59 | struct Test: Codable, Equatable { 60 | let value: Bool 61 | enum CodingKeys: Int, CodingKey { case value = 1 } 62 | } 63 | 64 | let expected: [UInt8] = [ 65 | 2, // Int key 1 (2x, String bit 0) 66 | 2, // Length of value (2x, Nil bit 0) 67 | 1, // Value 68 | ] 69 | try compare(Test(value: true), to: expected) 70 | } 71 | 72 | func testUnkeyedWithBool() throws { 73 | GenericTestStruct.encode { encoder in 74 | var container = encoder.unkeyedContainer() 75 | try container.encode(true) 76 | } 77 | GenericTestStruct.decode { decoder in 78 | var container = try decoder.unkeyedContainer() 79 | let value = try container.decode(Bool.self) 80 | XCTAssertEqual(value, true) 81 | } 82 | } 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/BinaryFileEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Encode a stream of elements to a binary file. 5 | 6 | This class complements ``BinaryStreamEncoder`` to directly write encoded elements to a file. 7 | */ 8 | public final class BinaryFileEncoder where Element: Encodable { 9 | 10 | private let handle: FileHandle 11 | 12 | private let stream: BinaryStreamEncoder 13 | 14 | /** 15 | Create a new file encoder. 16 | - Note: The file will be created, if it does not exist. 17 | If it exists, then the new elements will be appended to the end of the file. 18 | - Parameter url: The url to the file. 19 | - Parameter encoder: The encoder to use for each element. 20 | - Throws: Throws an error if the file could not be accessed or created. 21 | */ 22 | public init(fileAt url: URL, encoder: BinaryEncoder = .init()) throws { 23 | if !url.exists { 24 | try url.createEmptyFile() 25 | } 26 | let handle = try FileHandle(forWritingTo: url) 27 | if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { 28 | try handle.seekToEnd() 29 | } else { 30 | handle.seekToEndOfFile() 31 | } 32 | self.handle = handle 33 | self.stream = .init(encoder: encoder) 34 | } 35 | 36 | deinit { 37 | try? close() 38 | } 39 | 40 | /** 41 | Close the file. 42 | 43 | - Note: After closing the file, the encoder can no longer write elements, which will result in an error or an exception. 44 | - Throws: Currently throws a ObjC-style `Exception`, not an `Error`, even on modern systems. 45 | This is a bug in the Foundation framework. 46 | */ 47 | public func close() throws { 48 | if #available(macOS 10.15, iOS 13.0, tvOS 13.4, watchOS 6.2, *) { 49 | try handle.close() 50 | } else { 51 | handle.closeFile() 52 | } 53 | } 54 | 55 | /** 56 | Write a single element to the file. 57 | - Note: This function will throw an error or exception if the file handle has already been closed. 58 | - Parameter element: The element to encode. 59 | - Throws: Errors of type `EncodingError` 60 | */ 61 | public func write(_ element: Element) throws { 62 | let data = try stream.encode(element) 63 | if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) { 64 | try handle.write(contentsOf: data) 65 | } else { 66 | handle.write(data) 67 | } 68 | } 69 | 70 | /** 71 | Write a sequence of elements to the file. 72 | 73 | This is a convenience function calling `write(_ element:)` for each element of the sequence in order. 74 | 75 | - Parameter sequence: The sequence to encode 76 | - Throws: Errors of type `EncodingError` 77 | */ 78 | public func write(contentsOf sequence: S) throws where S: Sequence, S.Element == Element { 79 | try sequence.forEach(write) 80 | } 81 | } 82 | 83 | private extension URL { 84 | 85 | var exists: Bool { 86 | /* 87 | if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { 88 | return FileManager.default.fileExists(atPath: path()) 89 | } 90 | */ 91 | return FileManager.default.fileExists(atPath: path) 92 | } 93 | 94 | func createEmptyFile() throws { 95 | try Data().write(to: self) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/BinaryStreamEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Encode elements sequentially into a binary data stream. 5 | 6 | A stream encoder is used to encode individual elements of the same type to a continuous binary stream, 7 | which can be decoded sequentially. 8 | 9 | The encoding behaviour is different to ``BinaryEncoder``, where the full data must be present to successfully decode. 10 | Additional information is embedded into the stream to facilitate this behaviour. 11 | The binary data produced by a stream encoder is not compatible with ``BinaryDecoder`` and can only be decoded using 12 | ``BinaryStreamDecoder``. 13 | 14 | The special data format of an encoded stream also allows joining sequences of encoded data, where: 15 | `encode([a,b]) + encode([c,d]) == encode([a,b,c,d])` and `decode(encode([a]) + encode([b])) == [a,b]` 16 | 17 | Example: 18 | ``` 19 | let encoder = BinaryStreamEncoder() 20 | let encoded1 = try encoder.encode(1) 21 | 22 | let decoder = BinaryStreamDecoder() 23 | let decoded1 = try decoder.decode(encoded1) 24 | print(decoded1) // [1] 25 | 26 | let encoded2 = try encoder.encode(contentsOf: [2,3]) 27 | let decoded2 = try decoder.decode(encoded2) 28 | print(decoded2) // [2,3] 29 | ``` 30 | 31 | - Note: Stream decoders always work on a single type, because no type information is encoded into the data. 32 | */ 33 | public final class BinaryStreamEncoder where Element: Encodable { 34 | 35 | /** 36 | The encoder used to encode the individual elements. 37 | - Note: This property is `private` to prevent errors through reconfiguration between element encodings. 38 | */ 39 | private let encoder: BinaryEncoder 40 | 41 | /** 42 | Create a new stream encoder. 43 | 44 | - Note: The encoder should never be reconfigured after being passed to this function, 45 | to prevent decoding errors due to mismatching binary formats. 46 | - Parameter encoder: The encoder to use for the individual elements. 47 | */ 48 | public init(encoder: BinaryEncoder = .init()) { 49 | self.encoder = encoder 50 | } 51 | 52 | /** 53 | Encode an element for the data stream. 54 | 55 | Call this function to convert an element into binary data whenever new elements are available. 56 | The data provided as the result of this function should be processed (e.g. stored or transmitted) while conserving the 57 | order of the chunks, so that decoding can work reliably. 58 | 59 | Pass the encoded data back into an instance of ``BinaryStreamDecoder`` to convert each chunk back to an element. 60 | 61 | - Note: Data encoded by this function can only be decoded by an appropriate ``BinaryStreamDecoder``. 62 | Decoding using a simple ``BinaryDecoder`` will not be successful. 63 | - Parameter element: The element to encode. 64 | - Returns: The next chunk of the encoded binary stream. 65 | - Throws: Errors of type `EncodingError` 66 | */ 67 | public func encode(_ element: Element) throws -> Data { 68 | try encoder.encodeForStream(element) 69 | } 70 | 71 | /** 72 | Encode a sequence of elements. 73 | 74 | This function performs multiple calls to ``encode(_:)`` to convert all elements of the sequence, and then returns the joined data. 75 | - Parameter sequence: The sequence of elements to encode 76 | - Returns: The binary data of the encoded sequence elements 77 | - Throws: Errors of type `EncodingError` 78 | */ 79 | public func encode(contentsOf sequence: S) throws -> Data where S: Sequence, S.Element == Element { 80 | try sequence.mapAndJoin { value in 81 | try self.encode(value) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Int16+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Int16: EncodablePrimitive { 4 | 5 | var encodedData: Data { fixedSizeEncoded } 6 | } 7 | 8 | extension Int16: DecodablePrimitive { 9 | 10 | /** 11 | Decode a value from fixed-size data. 12 | - Parameter data: The data to decode. 13 | - Throws: ``CorruptedDataError`` 14 | */ 15 | init(data: Data) throws { 16 | try self.init(fromFixedSize: data) 17 | } 18 | } 19 | 20 | // - MARK: Fixed size 21 | 22 | extension Int16: FixedSizeEncodable { 23 | 24 | /// The value encoded as fixed-size data 25 | public var fixedSizeEncoded: Data { 26 | .init(underlying: UInt16(bitPattern: self).littleEndian) 27 | } 28 | } 29 | 30 | extension Int16: FixedSizeDecodable { 31 | 32 | /** 33 | Decode a value from fixed-size data. 34 | - Parameter data: The data to decode. 35 | - Throws: ``CorruptedDataError`` 36 | */ 37 | public init(fromFixedSize data: Data) throws { 38 | guard data.count == MemoryLayout.size else { 39 | throw CorruptedDataError(invalidSize: data.count, for: "Int16") 40 | } 41 | let value = UInt16(littleEndian: data.interpreted()) 42 | self.init(bitPattern: value) 43 | } 44 | } 45 | 46 | extension FixedSizeEncoded where WrappedValue == Int16 { 47 | 48 | /** 49 | Wrap an integer to enforce fixed-size encoding. 50 | - Parameter wrappedValue: The value to wrap 51 | - Note: `Int16` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. 52 | */ 53 | @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Int16") 54 | public init(wrappedValue: Int16) { 55 | self.wrappedValue = wrappedValue 56 | } 57 | } 58 | 59 | // - MARK: Variable length 60 | 61 | extension Int16: VariableLengthEncodable { 62 | 63 | public var variableLengthEncoding: Data { 64 | Int64(self).variableLengthEncoding 65 | } 66 | } 67 | 68 | extension Int16: VariableLengthDecodable { 69 | 70 | /** 71 | Create an integer from variable-length encoded data. 72 | - Parameter data: The data to decode. 73 | - Throws: ``CorruptedDataError`` 74 | */ 75 | public init(fromVarint raw: UInt64) throws { 76 | let value = try UInt16(fromVarint: raw) 77 | self = Int16(bitPattern: value) 78 | } 79 | } 80 | 81 | // - MARK: Zig-zag encoding 82 | 83 | extension Int16: ZigZagEncodable { 84 | 85 | /// The integer encoded using zig-zag encoding 86 | public var zigZagEncoded: Data { 87 | Int64(self).zigZagEncoded 88 | } 89 | } 90 | 91 | extension Int16: ZigZagDecodable { 92 | 93 | /** 94 | Decode an integer from zig-zag encoded data. 95 | - Parameter data: The data of the zig-zag encoded value. 96 | - Throws: ``CorruptedDataError`` 97 | */ 98 | public init(fromZigZag raw: UInt64) throws { 99 | let raw = Int64(fromZigZag: raw) 100 | guard let value = Int16(exactly: raw) else { 101 | throw CorruptedDataError(outOfRange: raw, forType: "Int16") 102 | } 103 | self = value 104 | } 105 | } 106 | 107 | // - MARK: Packed 108 | 109 | extension Int16: PackedEncodable { 110 | 111 | } 112 | 113 | extension Int16: PackedDecodable { 114 | 115 | init(data: Data, index: inout Int) throws { 116 | guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { 117 | throw CorruptedDataError.init(prematureEndofDataDecoding: "Int16") 118 | } 119 | try self.init(data: bytes) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/SequenceEncoderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | final class SequenceEncoderTests: XCTestCase { 5 | 6 | private func encodeSequence(_ input: Array) throws where T: Codable, T: Equatable { 7 | let encoder = BinaryStreamEncoder() 8 | 9 | let bytes = try input.mapAndJoin(encoder.encode) 10 | print(Array(bytes)) 11 | let decoder = BinaryStreamDecoder() 12 | 13 | decoder.add(bytes) 14 | let decoded = try decoder.decodeElements() 15 | XCTAssertEqual(decoded, input) 16 | } 17 | 18 | func testIntegerEncoding() throws { 19 | try encodeSequence([1, 2, 3]) 20 | try encodeSequence([1.0, 2.0, 3.0]) 21 | try encodeSequence([true, false, true]) 22 | try encodeSequence(["Some", "Text", "More"]) 23 | } 24 | 25 | func testComplexEncoding() throws { 26 | struct Test: Codable, Equatable { 27 | let a: Int 28 | let b: String 29 | } 30 | try encodeSequence([Test(a: 1, b: "Some"), Test(a: 2, b: "Text"), Test(a: 3, b: "More")]) 31 | } 32 | 33 | func testOptionalEncoding() throws { 34 | try encodeSequence([1, nil, 2, nil, 3]) 35 | } 36 | 37 | func testDecodePartialData() throws { 38 | struct Test: Codable, Equatable { 39 | let a: Int 40 | let b: String 41 | } 42 | let input = [Test(a: 1, b: "Some"), Test(a: 2, b: "Text"), Test(a: 3, b: "More")] 43 | 44 | let encoder = BinaryStreamEncoder() 45 | let bytes = try encoder.encode(contentsOf: input) 46 | 47 | let decoder = BinaryStreamDecoder() 48 | 49 | // Provide only the beginning of the stream 50 | decoder.add(bytes.dropLast(10)) 51 | let first = try decoder.decodeElements() 52 | // Decode remaining bytes 53 | decoder.add(bytes.suffix(10)) 54 | let remaining = try decoder.decodeElements() 55 | 56 | XCTAssertEqual(first + remaining, input) 57 | } 58 | 59 | func testDecodingError() throws { 60 | struct Test: Codable, Equatable, CustomStringConvertible { 61 | let a: Int 62 | let b: String 63 | var description: String { "\(a),\(b)"} 64 | } 65 | let input = [Test(a: 1, b: "Some"), Test(a: 2, b: "Text"), Test(a: 3, b: "More")] 66 | 67 | var enc = BinaryEncoder() 68 | enc.sortKeysDuringEncoding = true 69 | let encoder = BinaryStreamEncoder(encoder: enc) 70 | var data = try encoder.encode(contentsOf: input) 71 | // Add invalid byte 72 | data.insert(123, at: 15) 73 | let decoder = BinaryStreamDecoder() 74 | 75 | do { 76 | decoder.add(data) 77 | let decoded = try decoder.decodeElements() 78 | XCTAssertEqual(decoded, input) 79 | XCTFail("Should not be able to decode \(decoded)") 80 | } catch is DecodingError { 81 | 82 | } 83 | } 84 | 85 | func testDecodeUntilError() throws { 86 | struct Test: Codable, Equatable { 87 | let a: Int 88 | let b: String 89 | } 90 | let input = [Test(a: 1, b: "Some"), Test(a: 2, b: "Text"), Test(a: 3, b: "More")] 91 | 92 | var enc = BinaryEncoder() 93 | enc.sortKeysDuringEncoding = true 94 | let encoder = BinaryStreamEncoder(encoder: enc) 95 | var data = try encoder.encode(contentsOf: input) 96 | // Add invalid byte 97 | data.insert(123, at: 28) 98 | let decoder = BinaryStreamDecoder() 99 | 100 | decoder.add(data) 101 | let decoded = decoder.decodeElementsUntilError() 102 | XCTAssertNotNil(decoded.error) 103 | XCTAssertEqual(decoded.elements, [input[0], input[1]]) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Int+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Int: EncodablePrimitive { 4 | 5 | /// The integer encoded using zig-zag variable length encoding 6 | var encodedData: Data { zigZagEncoded } 7 | } 8 | 9 | extension Int: DecodablePrimitive { 10 | 11 | /** 12 | Decode an integer from zig-zag encoded data. 13 | - Parameter data: The data of the zig-zag encoded value. 14 | - Throws: ``CorruptedDataError`` 15 | */ 16 | init(data: Data) throws { 17 | let raw = try UInt64(fromVarintData: data) 18 | try self.init(fromZigZag: raw) 19 | } 20 | } 21 | 22 | // - MARK: Zig-zag encoding 23 | 24 | extension Int: ZigZagEncodable { 25 | 26 | /// The integer encoded using zig-zag encoding 27 | public var zigZagEncoded: Data { 28 | Int64(self).zigZagEncoded 29 | } 30 | } 31 | 32 | extension Int: ZigZagDecodable { 33 | 34 | /** 35 | Decode an integer from zig-zag encoded data. 36 | - Parameter data: The data of the zig-zag encoded value. 37 | - Throws: ``CorruptedDataError`` 38 | */ 39 | public init(fromZigZag raw: UInt64) throws { 40 | let raw = Int64(fromZigZag: raw) 41 | guard let value = Int(exactly: raw) else { 42 | throw CorruptedDataError(outOfRange: raw, forType: "Int") 43 | } 44 | self = value 45 | } 46 | } 47 | 48 | extension ZigZagEncoded where WrappedValue == Int { 49 | 50 | /** 51 | Wrap an integer to enforce zig-zag encoding. 52 | - Parameter wrappedValue: The value to wrap 53 | - Note: `Int` is already encoded using zig-zag encoding, so wrapping it in `ZigZagEncoded` does nothing. 54 | */ 55 | @available(*, deprecated, message: "Property wrapper @ZigZagEncoded has no effect on type Int") 56 | public init(wrappedValue: Int) { 57 | self.wrappedValue = wrappedValue 58 | } 59 | } 60 | 61 | // - MARK: Variable-length encoding 62 | 63 | extension Int: VariableLengthEncodable { 64 | 65 | /// The value encoded using variable length encoding 66 | public var variableLengthEncoding: Data { 67 | Int64(self).variableLengthEncoding 68 | } 69 | } 70 | 71 | extension Int: VariableLengthDecodable { 72 | 73 | /** 74 | Create an integer from variable-length encoded data. 75 | - Parameter data: The data to decode. 76 | - Throws: ``CorruptedDataError`` 77 | */ 78 | public init(fromVarint raw: UInt64) throws { 79 | let intValue = Int64(fromVarint: raw) 80 | guard let value = Int(exactly: intValue) else { 81 | throw CorruptedDataError(outOfRange: intValue, forType: "Int") 82 | } 83 | self = value 84 | } 85 | } 86 | 87 | // - MARK: Fixed-size encoding 88 | 89 | extension Int: FixedSizeEncodable { 90 | 91 | /// The value encoded as fixed-size data 92 | public var fixedSizeEncoded: Data { 93 | Int64(self).fixedSizeEncoded 94 | } 95 | } 96 | 97 | extension Int: FixedSizeDecodable { 98 | 99 | /** 100 | Decode a value from fixed-size data. 101 | - Parameter data: The data to decode. 102 | - Throws: ``CorruptedDataError`` 103 | */ 104 | public init(fromFixedSize data: Data) throws { 105 | let signed = try Int64(fromFixedSize: data) 106 | guard let value = Int(exactly: signed) else { 107 | throw CorruptedDataError(outOfRange: signed, forType: "Int") 108 | } 109 | self = value 110 | } 111 | } 112 | 113 | // - MARK: Packed 114 | 115 | extension Int: PackedEncodable { 116 | 117 | } 118 | 119 | extension Int: PackedDecodable { 120 | 121 | init(data: Data, index: inout Int) throws { 122 | guard let raw = data.decodeUInt64(at: &index) else { 123 | throw CorruptedDataError(prematureEndofDataDecoding: "Int") 124 | } 125 | try self.init(fromZigZag: raw) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Int32+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Int32: EncodablePrimitive { 4 | 5 | /// The integer encoded using zig-zag variable length encoding 6 | var encodedData: Data { zigZagEncoded } 7 | } 8 | 9 | extension Int32: DecodablePrimitive { 10 | 11 | /** 12 | Decode an integer from zig-zag encoded data. 13 | - Parameter data: The data of the zig-zag encoded value. 14 | - Throws: ``CorruptedDataError`` 15 | */ 16 | init(data: Data) throws { 17 | let raw = try UInt64(fromVarintData: data) 18 | try self.init(fromZigZag: raw) 19 | } 20 | } 21 | 22 | // - MARK: Zig-zag encoding 23 | 24 | extension Int32: ZigZagEncodable { 25 | 26 | /// The integer encoded using zig-zag encoding 27 | public var zigZagEncoded: Data { 28 | Int64(self).zigZagEncoded 29 | } 30 | } 31 | 32 | extension Int32: ZigZagDecodable { 33 | 34 | /** 35 | Decode an integer from zig-zag encoded data. 36 | - Parameter data: The data of the zig-zag encoded value. 37 | - Throws: ``CorruptedDataError`` 38 | */ 39 | public init(fromZigZag raw: UInt64) throws { 40 | let raw = Int64(fromZigZag: raw) 41 | guard let value = Int32(exactly: raw) else { 42 | throw CorruptedDataError(outOfRange: raw, forType: "Int32") 43 | } 44 | self = value 45 | } 46 | } 47 | 48 | extension ZigZagEncoded where WrappedValue == Int32 { 49 | 50 | /** 51 | Wrap an integer to enforce zig-zag encoding. 52 | - Parameter wrappedValue: The value to wrap 53 | - Note: `Int32` is already encoded using zig-zag encoding, so wrapping it in `ZigZagEncoded` does nothing. 54 | */ 55 | @available(*, deprecated, message: "Property wrapper @ZigZagEncoded has no effect on type Int32") 56 | public init(wrappedValue: Int32) { 57 | self.wrappedValue = wrappedValue 58 | } 59 | } 60 | 61 | // - MARK: Variable-length encoding 62 | 63 | extension Int32: VariableLengthEncodable { 64 | 65 | /// The value encoded using variable length encoding 66 | public var variableLengthEncoding: Data { 67 | UInt32(bitPattern: self).variableLengthEncoding 68 | } 69 | } 70 | 71 | extension Int32: VariableLengthDecodable { 72 | 73 | /** 74 | Create an integer from variable-length encoded data. 75 | - Parameter data: The data to decode. 76 | - Throws: ``CorruptedDataError`` 77 | */ 78 | public init(fromVarint raw: UInt64) throws { 79 | let value = try UInt32(fromVarint: raw) 80 | self = Int32(bitPattern: value) 81 | } 82 | } 83 | 84 | // - MARK: Fixed-size encoding 85 | 86 | extension Int32: FixedSizeEncodable { 87 | 88 | /// The value encoded as fixed-size data 89 | public var fixedSizeEncoded: Data { 90 | let value = UInt32(bitPattern: littleEndian) 91 | return Data(underlying: value) 92 | } 93 | } 94 | 95 | extension Int32: FixedSizeDecodable { 96 | 97 | /** 98 | Decode a value from fixed-size data. 99 | - Parameter data: The data to decode. 100 | - Throws: ``CorruptedDataError`` 101 | */ 102 | public init(fromFixedSize data: Data) throws { 103 | guard data.count == MemoryLayout.size else { 104 | throw CorruptedDataError(invalidSize: data.count, for: "Int32") 105 | } 106 | let value = UInt32(littleEndian: data.interpreted()) 107 | self.init(bitPattern: value) 108 | } 109 | } 110 | 111 | // - MARK: Packed 112 | 113 | extension Int32: PackedEncodable { 114 | 115 | } 116 | 117 | extension Int32: PackedDecodable { 118 | 119 | init(data: Data, index: inout Int) throws { 120 | guard let raw = data.decodeUInt64(at: &index) else { 121 | throw CorruptedDataError(prematureEndofDataDecoding: "Int32") 122 | } 123 | try self.init(fromZigZag: raw) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/Int64+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Int64: EncodablePrimitive { 4 | 5 | /// The value encoded using zig-zag variable length encoding 6 | var encodedData: Data { zigZagEncoded } 7 | } 8 | 9 | extension Int64: DecodablePrimitive { 10 | 11 | /** 12 | Decode an integer from zig-zag encoded data. 13 | - Parameter data: The data of the zig-zag encoded value. 14 | - Throws: ``CorruptedDataError`` 15 | */ 16 | init(data: Data) throws { 17 | let raw = try UInt64(fromVarintData: data) 18 | self.init(fromZigZag: raw) 19 | } 20 | } 21 | 22 | // - MARK: Zig-zag encoding 23 | 24 | extension Int64: ZigZagEncodable { 25 | 26 | /// The integer encoded using zig-zag encoding 27 | public var zigZagEncoded: Data { 28 | guard self < 0 else { 29 | return (UInt64(self.magnitude) << 1).variableLengthEncoding 30 | } 31 | return ((UInt64(-1 - self) << 1) + 1).variableLengthEncoding 32 | } 33 | } 34 | 35 | extension Int64: ZigZagDecodable { 36 | 37 | /** 38 | Decode an integer from zig-zag encoded data. 39 | - Parameter data: The data of the zig-zag encoded value. 40 | - Throws: ``CorruptedDataError`` 41 | */ 42 | public init(fromZigZag raw: UInt64) { 43 | // Check the last bit to get sign 44 | if raw & 1 > 0 { 45 | // Divide by 2 and subtract one to get absolute value of negative values. 46 | self = -Int64(raw >> 1) - 1 47 | } else { 48 | // Divide by two to get absolute value of positive values 49 | self = Int64(raw >> 1) 50 | } 51 | } 52 | } 53 | 54 | extension ZigZagEncoded where WrappedValue == Int64 { 55 | 56 | /** 57 | Wrap an integer to enforce zig-zag encoding. 58 | - Parameter wrappedValue: The value to wrap 59 | - Note: `Int64` is already encoded using zig-zag encoding, so wrapping it in `ZigZagEncoded` does nothing. 60 | */ 61 | @available(*, deprecated, message: "Property wrapper @ZigZagEncoded has no effect on type Int64") 62 | public init(wrappedValue: Int64) { 63 | self.wrappedValue = wrappedValue 64 | } 65 | } 66 | 67 | // - MARK: Variable-length encoding 68 | 69 | extension Int64: VariableLengthEncodable { 70 | 71 | /// The value encoded using variable length encoding 72 | public var variableLengthEncoding: Data { 73 | UInt64(bitPattern: self).variableLengthEncoding 74 | } 75 | 76 | } 77 | 78 | extension Int64: VariableLengthDecodable { 79 | 80 | /** 81 | Create an integer from variable-length encoded data. 82 | - Parameter data: The data to decode. 83 | - Throws: ``CorruptedDataError`` 84 | */ 85 | public init(fromVarint raw: UInt64) { 86 | self = Int64(bitPattern: raw) 87 | } 88 | } 89 | 90 | // - MARK: Fixed-size encoding 91 | 92 | extension Int64: FixedSizeEncodable { 93 | 94 | /// The value encoded as fixed-size data 95 | public var fixedSizeEncoded: Data { 96 | let value = UInt64(bitPattern: littleEndian) 97 | return Data.init(underlying: value) 98 | } 99 | } 100 | 101 | extension Int64: FixedSizeDecodable { 102 | 103 | /** 104 | Decode a value from fixed-size data. 105 | - Parameter data: The data to decode. 106 | - Throws: ``CorruptedDataError`` 107 | */ 108 | public init(fromFixedSize data: Data) throws { 109 | guard data.count == MemoryLayout.size else { 110 | throw CorruptedDataError(invalidSize: data.count, for: "Int64") 111 | } 112 | let value = UInt64(littleEndian: data.interpreted()) 113 | self.init(bitPattern: value) 114 | } 115 | } 116 | 117 | // - MARK: Packed 118 | 119 | extension Int64: PackedEncodable { 120 | 121 | } 122 | 123 | extension Int64: PackedDecodable { 124 | 125 | init(data: Data, index: inout Int) throws { 126 | guard let raw = data.decodeUInt64(at: &index) else { 127 | throw CorruptedDataError(prematureEndofDataDecoding: "Int64") 128 | } 129 | self.init(fromZigZag: raw) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/KeyedDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class KeyedDecoder: AbstractDecodingNode, KeyedDecodingContainerProtocol where Key: CodingKey { 4 | 5 | let allKeys: [Key] 6 | 7 | /// - Note: The keys are not of type `Key`, since `CodingKey`s are not `Hashable`. 8 | /// Also, some keys found in the data may not be convertable to `Key`, e.g. the `super` key, or obsoleted keys from older implementations. 9 | private let elements: [DecodingKey: Data?] 10 | 11 | init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) throws { 12 | self.elements = try wrapCorruptDataError(at: codingPath) { 13 | try data.decodeKeyDataPairs() 14 | } 15 | self.allKeys = elements.keys.compactMap { $0.asKey() } 16 | super.init(parentDecodedNil: true, codingPath: codingPath, userInfo: userInfo) 17 | } 18 | 19 | private func value(for intKey: Int?) -> Data?? { 20 | guard let intKey else { 21 | return nil 22 | } 23 | return elements[.integer(intKey)] 24 | } 25 | 26 | private func value(for stringKey: String) -> Data?? { 27 | elements[.string(stringKey)] 28 | } 29 | 30 | private func value(for key: CodingKey) throws -> Data? { 31 | let int = value(for: key.intValue) 32 | let string = value(for: key.stringValue) 33 | if int != nil && string != nil { 34 | throw DecodingError.corrupted("Found value for int and string key", codingPath: codingPath + [key]) 35 | } 36 | guard let value = int ?? string else { 37 | throw DecodingError.notFound(key, codingPath: codingPath + [key], "Missing value for key") 38 | } 39 | return value 40 | } 41 | 42 | private func node(for key: CodingKey) throws -> DecodingNode { 43 | let element = try value(for: key) 44 | return try DecodingNode(data: element, parentDecodedNil: true, codingPath: codingPath + [key], userInfo: userInfo) 45 | } 46 | 47 | func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { 48 | KeyedDecodingContainer(try node(for: key).container(keyedBy: type)) 49 | } 50 | 51 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { 52 | try node(for: key).unkeyedContainer() 53 | } 54 | 55 | func superDecoder() throws -> Decoder { 56 | try node(for: SuperCodingKey()) 57 | } 58 | 59 | func superDecoder(forKey key: Key) throws -> Decoder { 60 | try node(for: key) 61 | } 62 | 63 | func contains(_ key: Key) -> Bool { 64 | if let intValue = key.intValue, elements[.integer(intValue)] != .none { 65 | return true 66 | } 67 | return elements[.string(key.stringValue)] != .none 68 | } 69 | 70 | /** 71 | Decodes a null value for the given key. 72 | - Parameter key: The key that the decoded value is associated with. 73 | - Returns: Whether the encountered key is contained 74 | */ 75 | func decodeNil(forKey key: Key) -> Bool { 76 | /** 77 | **Important note**: The implementation of `encodeNil(forKey:)` and `decodeNil(forKey:)` are implemented differently than the `Codable` documentation specifies: 78 | - Throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. 79 | 80 | If a value is `nil`, then it is not encoded. 81 | We could change this by explicitly assigning a `nil` value during encoding. 82 | But it would cause other problems, either breaking the decoding of double optionals (e.g. Int??), 83 | or requiring an additional `nil` indicator for **all** values in keyed containers, 84 | which would make the format less efficient. 85 | 86 | The alternative would be: 87 | ``` 88 | try value(for: key) == nil 89 | ``` 90 | */ 91 | !contains(key) 92 | } 93 | 94 | func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { 95 | let element = try value(for: key) 96 | return try decode(element: element, type: type, codingPath: codingPath + [key]) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/BinaryEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | An encoder to convert `Codable` objects to binary data. 5 | 6 | Construct an encoder when converting instances to binary data, and feed the message(s) into it: 7 | 8 | ```swift 9 | let message: Message = ... 10 | 11 | let encoder = BinaryEncoder() 12 | let data = try encoder.encode(message) 13 | ``` 14 | 15 | - Note: An ecoder can be used to encode multiple messages. 16 | */ 17 | public struct BinaryEncoder { 18 | 19 | /// The user info key for the ``sortKeysDuringEncoding`` option. 20 | public static let userInfoSortKey: CodingUserInfoKey = .init(rawValue: "sortByKey")! 21 | 22 | /** 23 | Sort keyed data in the binary representation. 24 | 25 | Enabling this option causes all data in keyed containers (e.g. `Dictionary`, `Struct`) to be sorted by their keys before encoding. 26 | This option can enable deterministic encoding where the binary output is consistent across multiple invocations. 27 | 28 | - Warning: Output will not be deterministic when using `Set`, or `Dictionary` where `Key` is not `String` or `Int`. 29 | 30 | Enabling this option introduces computational overhead due to sorting, which can become significant when dealing with many entries. 31 | 32 | This option has no impact on decoding using `BinaryDecoder`. 33 | 34 | Enabling this option will add the `CodingUserInfoKey(rawValue: "sortByKey")` to the `userInfo` dictionary. 35 | This key is also available as ``userInfoSortKey`` 36 | 37 | - Note: The default value for this option is `false`. 38 | */ 39 | public var sortKeysDuringEncoding: Bool { 40 | get { 41 | userInfo[BinaryEncoder.userInfoSortKey] as? Bool ?? false 42 | } 43 | set { 44 | userInfo[BinaryEncoder.userInfoSortKey] = newValue 45 | } 46 | } 47 | 48 | /// Any contextual information set by the user for encoding. 49 | public var userInfo: [CodingUserInfoKey : Any] = [:] 50 | 51 | /** 52 | Create a new encoder. 53 | - Note: An encoder can be used to encode multiple messages. 54 | */ 55 | public init() { 56 | 57 | } 58 | 59 | /** 60 | Encode a value to binary data. 61 | - Parameter value: The value to encode 62 | - Returns: The encoded data 63 | - Throws: Errors of type `EncodingError` 64 | */ 65 | public func encode(_ value: T) throws -> Data where T: Encodable { 66 | // Directly encode primitives, otherwise: 67 | // - Data would be encoded as an unkeyed container 68 | // - There would always be a nil indicator byte 0x00 at the beginning 69 | // NOTE: The comparison of the types is necessary, since otherwise optionals are matched as well. 70 | if T.self is EncodablePrimitive.Type, let value = value as? EncodablePrimitive { 71 | return value.encodedData 72 | } 73 | let encoder = EncodingNode(needsLengthData: false, codingPath: [], userInfo: userInfo) 74 | try value.encode(to: encoder) 75 | return try encoder.completeData() 76 | } 77 | 78 | /** 79 | Encode a single value to binary data using a default encoder. 80 | - Parameter value: The value to encode 81 | - Returns: The encoded data 82 | - Throws: Errors of type `EncodingError` 83 | */ 84 | public static func encode(_ value: Encodable) throws -> Data { 85 | try BinaryEncoder().encode(value) 86 | } 87 | 88 | // MARK: Stream encoding 89 | 90 | /** 91 | Encodes a value to binary data for use in a data stream. 92 | 93 | This function differs from 'normal' encoding by the additional length information prepended to the element.. 94 | This information is used when decoding values from a data stream. 95 | 96 | - Note: This function is not exposed publicly to keep the API easy to understand. 97 | Advanced features like stream encoding are handled by ``BinaryStreamEncoder``. 98 | 99 | 100 | - Parameter value: The value to encode 101 | - Returns: The encoded data for the element 102 | - Throws: Errors of type `EncodingError` 103 | */ 104 | func encodeForStream(_ value: Encodable) throws -> Data { 105 | let encoder = EncodingNode(needsLengthData: true, codingPath: [], userInfo: userInfo) 106 | try value.encode(to: encoder) 107 | return try encoder.completeData() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Decoding/DecodingDataProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol DecodingDataProvider { 4 | 5 | associatedtype Index 6 | 7 | var startIndex: Index { get } 8 | 9 | func isAtEnd(at index: Index) -> Bool 10 | 11 | func nextByte(at index: inout Index) -> UInt8? 12 | 13 | func nextBytes(_ count: Int, at index: inout Index) -> Data? 14 | } 15 | 16 | 17 | extension DecodingDataProvider { 18 | 19 | /** 20 | Decode an unsigned integer using variable-length encoding starting at a position. 21 | - Returns: `Nil`, if insufficient data is available 22 | */ 23 | func decodeUInt64(at index: inout Index) -> UInt64? { 24 | guard let start = nextByte(at: &index) else { return nil } 25 | return decodeUInt64(startByte: start, at: &index) 26 | } 27 | 28 | /** 29 | Decode an unsigned integer using variable-length encoding starting at a position. 30 | */ 31 | private func decodeUInt64(startByte: UInt8, at index: inout Index) -> UInt64? { 32 | guard startByte & 0x80 > 0 else { 33 | return UInt64(startByte) 34 | } 35 | 36 | var result = UInt64(startByte & 0x7F) 37 | // There are always 7 usable bits per byte, for 8 bytes 38 | for byteIndex in 1..<8 { 39 | guard let nextByte = nextByte(at: &index) else { return nil } 40 | // Insert the last 7 bit of the byte at the end 41 | result += UInt64(nextByte & 0x7F) << (byteIndex*7) 42 | // Check if an additional byte is coming 43 | guard nextByte & 0x80 > 0 else { 44 | return result 45 | } 46 | } 47 | 48 | // The 9th byte has no next-byte bit, so all 8 bits are used 49 | guard let nextByte = nextByte(at: &index) else { return nil } 50 | result += UInt64(nextByte) << 56 51 | return result 52 | } 53 | 54 | func decodeNextDataOrNilElement(at index: inout Index) throws -> Data? { 55 | guard let first = nextByte(at: &index) else { 56 | throw CorruptedDataError(prematureEndofDataDecoding: "length or nil indicator") 57 | } 58 | 59 | // Check the nil indicator bit 60 | guard first & 0x01 == 0 else { 61 | return nil 62 | } 63 | // The rest is the length, encoded as a varint 64 | guard let rawLengthValue = decodeUInt64(startByte: first, at: &index) else { 65 | throw CorruptedDataError(prematureEndofDataDecoding: "element length") 66 | } 67 | 68 | // Remove the nil indicator bit 69 | let length = Int(rawLengthValue >> 1) 70 | guard let element = nextBytes(length, at: &index) else { 71 | throw CorruptedDataError(prematureEndofDataDecoding: "element length") 72 | } 73 | return element 74 | } 75 | 76 | func decodeUnkeyedElements() throws -> [Data?] { 77 | var elements = [Data?]() 78 | var index = startIndex 79 | while !isAtEnd(at: index) { 80 | let element = try decodeNextDataOrNilElement(at: &index) 81 | elements.append(element) 82 | } 83 | return elements 84 | } 85 | 86 | private func decodeNextKey(at index: inout Index) throws -> DecodingKey { 87 | // First, decode the next key 88 | guard let rawKeyOrLength = decodeUInt64(at: &index) else { 89 | throw CorruptedDataError(prematureEndofDataDecoding: "key") 90 | } 91 | let lengthOrKey = Int(rawKeyOrLength >> 1) 92 | 93 | guard rawKeyOrLength & 1 == 1 else { 94 | // Int key 95 | return .integer(lengthOrKey) 96 | } 97 | 98 | // String key, decode length bytes 99 | guard let stringData = nextBytes(lengthOrKey, at: &index) else { 100 | throw CorruptedDataError(prematureEndofDataDecoding: "string key bytes") 101 | } 102 | let stringKey = try String(data: stringData) 103 | return .string(stringKey) 104 | } 105 | 106 | func decodeKeyDataPairs() throws -> [DecodingKey : Data?] { 107 | var elements = [DecodingKey : Data?]() 108 | var index = startIndex 109 | while !isAtEnd(at: index) { 110 | let key = try decodeNextKey(at: &index) 111 | guard !isAtEnd(at: index) else { 112 | throw CorruptedDataError(prematureEndofDataDecoding: "element after key") 113 | } 114 | let element = try decodeNextDataOrNilElement(at: &index) 115 | guard elements[key] == nil else { 116 | throw CorruptedDataError(multipleValuesForKey: key) 117 | } 118 | elements[key] = element 119 | } 120 | return elements 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/CustomDecodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | private struct Timestamped { 5 | 6 | let timestamp: Date 7 | 8 | let value: Value 9 | 10 | init(value: Value, timestamp: Date = Date()) { 11 | self.timestamp = timestamp 12 | self.value = value 13 | } 14 | 15 | func mapValue(_ closure: (Value) -> T) -> Timestamped { 16 | .init(value: closure(value), timestamp: timestamp) 17 | } 18 | } 19 | 20 | extension Timestamped: Encodable where Value: Encodable { 21 | 22 | func encode(to encoder: Encoder) throws { 23 | var container = encoder.unkeyedContainer() 24 | try container.encode(timestamp) 25 | try container.encode(value) 26 | } 27 | } 28 | 29 | extension Timestamped: Decodable where Value: Decodable { 30 | 31 | init(from decoder: Decoder) throws { 32 | var container = try decoder.unkeyedContainer() 33 | self.timestamp = try container.decode(Date.self) 34 | self.value = try container.decode(Value.self) 35 | } 36 | } 37 | 38 | extension Timestamped: Equatable where Value: Equatable { 39 | 40 | } 41 | 42 | /// A special semantic version with a fourth number 43 | private struct Version { 44 | 45 | /// The major version of the software 46 | public let major: Int 47 | 48 | /// The minor version of the software 49 | public let minor: Int 50 | 51 | /// The patch version of the software 52 | public let patch: Int 53 | 54 | public let build: Int? 55 | 56 | public init(major: Int, minor: Int, patch: Int, build: Int? = nil) { 57 | self.major = major 58 | self.minor = minor 59 | self.patch = patch 60 | self.build = build 61 | } 62 | } 63 | 64 | extension Version: RawRepresentable { 65 | 66 | var rawValue: String { 67 | guard let build else { 68 | return "\(major).\(minor).\(patch)" 69 | } 70 | return "\(major).\(minor).\(patch).\(build)" 71 | } 72 | 73 | init?(rawValue: String) { 74 | let parts = rawValue 75 | .trimmingCharacters(in: .whitespaces) 76 | .components(separatedBy: ".") 77 | guard parts.count == 3 || parts.count == 4 else { 78 | return nil 79 | } 80 | guard let major = Int(parts[0]), 81 | let minor = Int(parts[1]), 82 | let patch = Int(parts[2]) else { 83 | return nil 84 | } 85 | self.major = major 86 | self.minor = minor 87 | self.patch = patch 88 | 89 | guard parts.count == 4 else { 90 | self.build = nil 91 | return 92 | } 93 | guard let build = Int(parts[3]) else { 94 | return nil 95 | } 96 | self.build = build 97 | } 98 | } 99 | 100 | extension Version: Decodable { } 101 | extension Version: Encodable { } 102 | extension Version: Equatable { } 103 | 104 | final class CustomDecodingTests: XCTestCase { 105 | 106 | func testCustomDecoding() throws { 107 | let value = Timestamped(value: "Some") 108 | let encoded = try BinaryEncoder().encode(value) 109 | let decoded = try BinaryDecoder().decode(Timestamped.self, from: encoded) 110 | XCTAssertEqual(value, decoded) 111 | } 112 | 113 | func testCustomVersionDecoding() throws { 114 | let version = Version(major: 1, minor: 2, patch: 3) 115 | let value = Timestamped(value: version) 116 | let encoded = try BinaryEncoder().encode(value) 117 | let decoded = try BinaryDecoder().decode(Timestamped.self, from: encoded) 118 | XCTAssertEqual(version, decoded.value) 119 | } 120 | 121 | /** 122 | This function tests if a `RawRepresentable` can be decoded directly as `RawRepresentable.RawValue?` 123 | */ 124 | func testDecodingAsDifferentType() throws { 125 | let version = Version(major: 1, minor: 2, patch: 3) 126 | let encoded = try BinaryEncoder().encode(version) 127 | let decoded = try BinaryDecoder().decode(String?.self, from: encoded) 128 | XCTAssertEqual(version.rawValue, decoded) 129 | 130 | let version2: String? = "1.2.3" 131 | let encoded2 = try BinaryEncoder().encode(version2) 132 | let decoded2 = try BinaryDecoder().decode(Version.self, from: encoded2) 133 | XCTAssertEqual(version, decoded2) 134 | } 135 | 136 | func testEncodingAsDifferentType() throws { 137 | let version = Version(major: 1, minor: 2, patch: 3) 138 | let time = Date() 139 | let sValue = Timestamped(value: version.rawValue, timestamp: time) 140 | let vValue = Timestamped(value: version, timestamp: time) 141 | let encoder = BinaryEncoder() 142 | let sEncoded = try encoder.encode(sValue) 143 | let vEncoded = try encoder.encode(vValue) 144 | XCTAssertEqual(Array(sEncoded), Array(vEncoded)) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/SuperEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | private class Base: Codable { 5 | 6 | let value: Int 7 | 8 | init(value: Int) { 9 | self.value = value 10 | } 11 | 12 | enum CodingKeys: Int, CodingKey { 13 | case value = 1 14 | } 15 | } 16 | 17 | private class Child1: Base { 18 | 19 | let other: Bool 20 | 21 | init(other: Bool, value: Int) { 22 | self.other = other 23 | super.init(value: value) 24 | } 25 | 26 | enum CodingKeys: Int, CodingKey { 27 | case other = 2 28 | } 29 | 30 | required init(from decoder: Decoder) throws { 31 | let container = try decoder.container(keyedBy: CodingKeys.self) 32 | self.other = try container.decode(Bool.self, forKey: .other) 33 | let superDecoder = try container.superDecoder() 34 | try super.init(from: superDecoder) 35 | } 36 | 37 | override func encode(to encoder: Encoder) throws { 38 | var container = encoder.container(keyedBy: CodingKeys.self) 39 | try container.encode(other, forKey: .other) 40 | let superEncoder = container.superEncoder() 41 | try super.encode(to: superEncoder) 42 | } 43 | } 44 | 45 | extension Child1: Equatable { 46 | static func == (lhs: Child1, rhs: Child1) -> Bool { 47 | lhs.other == rhs.other && lhs.value == rhs.value 48 | } 49 | } 50 | 51 | private class Child2: Base { 52 | 53 | let other: Bool 54 | 55 | init(other: Bool, value: Int) { 56 | self.other = other 57 | super.init(value: value) 58 | } 59 | 60 | enum CodingKeys: Int, CodingKey { 61 | case other = 2 62 | case `super` = 3 63 | } 64 | 65 | required init(from decoder: Decoder) throws { 66 | let container = try decoder.container(keyedBy: CodingKeys.self) 67 | self.other = try container.decode(Bool.self, forKey: .other) 68 | let superDecoder = try container.superDecoder(forKey: .super) 69 | try super.init(from: superDecoder) 70 | } 71 | 72 | override func encode(to encoder: Encoder) throws { 73 | var container = encoder.container(keyedBy: CodingKeys.self) 74 | try container.encode(other, forKey: .other) 75 | let superEncoder = container.superEncoder(forKey: .super) 76 | try super.encode(to: superEncoder) 77 | } 78 | } 79 | 80 | extension Child2: Equatable { 81 | static func == (lhs: Child2, rhs: Child2) -> Bool { 82 | lhs.other == rhs.other && lhs.value == rhs.value 83 | } 84 | } 85 | 86 | final class SuperEncodingTests: XCTestCase { 87 | 88 | func testSuperEncodingWithDefaultKey() throws { 89 | let value = Child1(other: true, value: 123) 90 | let part1: [UInt8] = [ 91 | 4, // Int key 2 92 | 2, // Length 1 93 | 1, // Bool true 94 | ] 95 | let part2: [UInt8] = [ 96 | 0, // Int key 0 97 | 8, // Length 4 98 | 2, // Int key 1 99 | 4, // Length 2 100 | 246, 1, // ZigZag(123) 101 | ] 102 | try compare(value, toOneOf: [part1 + part2, part2 + part1]) 103 | } 104 | 105 | func testSuperEncodingWithCustomKey() throws { 106 | let value = Child2(other: true, value: 123) 107 | let part1: [UInt8] = [ 108 | 4, // Int key 2 109 | 2, // Length 1 110 | 1, // Bool true 111 | ] 112 | let part2: [UInt8] = [ 113 | 6, // Int key 3 114 | 8, // Length 4 115 | 2, // Int key 1 116 | 4, // Length 2 117 | 246, 1, // ZigZag(123) 118 | ] 119 | try compare(value, toOneOf: [part1 + part2, part2 + part1]) 120 | } 121 | 122 | func testInheritance() throws { 123 | class ParentClass: Codable { 124 | var text: String = "" 125 | } 126 | 127 | final class ChildClass: ParentClass, Equatable { 128 | static func == (lhs: ChildClass, rhs: ChildClass) -> Bool { 129 | lhs.text == rhs.text && lhs.image == rhs.image 130 | } 131 | 132 | var image: Data? 133 | 134 | enum CodingKeys: String, CodingKey { 135 | case image 136 | } 137 | 138 | override init() { 139 | self.image = nil 140 | super.init() 141 | } 142 | 143 | required init(from decoder: any Decoder) throws { 144 | let container = try decoder.container(keyedBy: CodingKeys.self) 145 | self.image = try container.decodeIfPresent(Data.self, forKey: .image) 146 | try super.init(from: decoder) 147 | } 148 | 149 | override func encode(to encoder: any Encoder) throws { 150 | try super.encode(to: encoder) 151 | var container = encoder.container(keyedBy: CodingKeys.self) 152 | try container.encodeIfPresent(image, forKey: .image) 153 | } 154 | } 155 | 156 | let child = ChildClass() 157 | child.image = Data(repeating: 42, count: 42) 158 | 159 | try compare(child) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/Primitives/UInt64+Coding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UInt64: EncodablePrimitive { 4 | 5 | /// The value encoded using variable-length encoding 6 | var encodedData: Data { variableLengthEncoding } 7 | } 8 | 9 | extension UInt64: DecodablePrimitive { 10 | 11 | /** 12 | Create an integer from variable-length encoded data. 13 | - Parameter data: The data to decode. 14 | - Throws: ``CorruptedDataError`` 15 | */ 16 | init(data: Data) throws { 17 | try self.init(fromVarintData: data) 18 | } 19 | } 20 | 21 | // - MARK: Variable-length encoding 22 | 23 | extension UInt64: VariableLengthEncodable { 24 | 25 | /// The value encoded using variable-length encoding 26 | public var variableLengthEncoding: Data { 27 | var result = Data() 28 | var value = self 29 | // Iterate over the first 56 bit 30 | for _ in 0..<8 { 31 | // Extract 7 bit from value 32 | let nextByte = UInt8(value & 0x7F) 33 | value = value >> 7 34 | guard value > 0 else { 35 | result.append(nextByte) 36 | return result 37 | } 38 | // Set 8th bit to indicate another byte 39 | result.append(nextByte | 0x80) 40 | } 41 | // Add last byte if needed, no next byte indicator necessary 42 | if value > 0 { 43 | result.append(UInt8(value)) 44 | } 45 | return result 46 | } 47 | } 48 | 49 | extension UInt64: VariableLengthDecodable { 50 | 51 | /** 52 | Create an integer from variable-length encoded data. 53 | - Parameter data: The data to decode. 54 | - Throws: ``CorruptedDataError`` 55 | */ 56 | public init(fromVarint raw: UInt64) { 57 | self = raw 58 | } 59 | 60 | init(fromVarintData data: Data) throws { 61 | var currentIndex = data.startIndex 62 | 63 | func nextByte() throws -> UInt64 { 64 | guard currentIndex < data.endIndex else { 65 | throw CorruptedDataError(prematureEndofDataDecoding: "variable length integer") 66 | } 67 | defer { currentIndex += 1} 68 | return UInt64(data[currentIndex]) 69 | } 70 | 71 | func ensureDataIsAtEnd() throws { 72 | guard currentIndex == data.endIndex else { 73 | throw CorruptedDataError(unusedBytes: data.endIndex - currentIndex, during: "variable length integer decoding") 74 | } 75 | } 76 | 77 | let startByte = try nextByte() 78 | guard startByte & 0x80 > 0 else { 79 | try ensureDataIsAtEnd() 80 | self = startByte 81 | return 82 | } 83 | 84 | var result = startByte & 0x7F 85 | // There are always 7 usable bits per byte, for 8 bytes 86 | for byteIndex in 1..<8 { 87 | let nextByte = try nextByte() 88 | // Insert the last 7 bit of the byte at the end 89 | result += UInt64(nextByte & 0x7F) << (byteIndex*7) 90 | // Check if an additional byte is coming 91 | guard nextByte & 0x80 > 0 else { 92 | try ensureDataIsAtEnd() 93 | self = result 94 | return 95 | } 96 | } 97 | 98 | // The 9th byte has no next-byte bit, so all 8 bits are used 99 | let nextByte = try nextByte() 100 | result += UInt64(nextByte) << 56 101 | try ensureDataIsAtEnd() 102 | self = result 103 | } 104 | } 105 | 106 | extension VariableLengthEncoded where WrappedValue == UInt64 { 107 | 108 | /** 109 | Wrap an integer to enforce variable-length encoding. 110 | - Parameter wrappedValue: The value to wrap 111 | - Note: `UInt64` is already encoded using fixed-size encoding, so wrapping it in `VariableLengthEncoded` does nothing. 112 | */ 113 | @available(*, deprecated, message: "Property wrapper @VariableLengthEncoded has no effect on type UInt64") 114 | public init(wrappedValue: UInt64) { 115 | self.wrappedValue = wrappedValue 116 | } 117 | } 118 | 119 | // - MARK: Fixed-size encoding 120 | 121 | extension UInt64: FixedSizeEncodable { 122 | 123 | /// The value encoded as fixed-size data 124 | public var fixedSizeEncoded: Data { 125 | Data(underlying: littleEndian) 126 | } 127 | } 128 | 129 | extension UInt64: FixedSizeDecodable { 130 | 131 | /** 132 | Decode a value from fixed-size data. 133 | - Parameter data: The data to decode. 134 | - Throws: ``CorruptedDataError`` 135 | */ 136 | public init(fromFixedSize data: Data) throws { 137 | guard data.count == MemoryLayout.size else { 138 | throw CorruptedDataError(invalidSize: data.count, for: "UInt64") 139 | } 140 | self.init(littleEndian: data.interpreted()) 141 | } 142 | } 143 | 144 | // - MARK: Packed 145 | 146 | extension UInt64: PackedEncodable { 147 | 148 | } 149 | 150 | extension UInt64: PackedDecodable { 151 | 152 | init(data: Data, index: inout Int) throws { 153 | guard let raw = data.decodeUInt64(at: &index) else { 154 | throw CorruptedDataError(prematureEndofDataDecoding: "UInt64") 155 | } 156 | self = raw 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/FileDecodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | private struct Test: Codable, Equatable, CustomStringConvertible { 5 | let a: Int 6 | let b: String 7 | 8 | var description: String { 9 | b 10 | } 11 | } 12 | 13 | final class FileDecodingTests: XCTestCase { 14 | 15 | private let fileUrl: URL = { 16 | /* 17 | if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { 18 | return FileManager.default.temporaryDirectory.appending(path: "file", directoryHint: .notDirectory) 19 | } 20 | */ 21 | return FileManager.default.temporaryDirectory.appendingPathComponent("file") 22 | }() 23 | 24 | override func setUp() { 25 | if hasFile { 26 | try? FileManager.default.removeItem(at: fileUrl) 27 | } 28 | } 29 | 30 | var hasFile: Bool { 31 | /* 32 | if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { 33 | return FileManager.default.fileExists(atPath: fileUrl.path()) 34 | } 35 | */ 36 | return FileManager.default.fileExists(atPath: fileUrl.path) 37 | } 38 | 39 | override func tearDown() { 40 | if hasFile { 41 | try? FileManager.default.removeItem(at: fileUrl) 42 | } 43 | } 44 | 45 | func testEncodeToFile() throws { 46 | let encoder = try BinaryFileEncoder(fileAt: fileUrl) 47 | 48 | let input = (0..<1000).map { Test(a: $0, b: "\($0)") } 49 | try encoder.write(contentsOf: input) 50 | try encoder.close() 51 | 52 | let decoder = try BinaryFileDecoder(fileAt: fileUrl) 53 | var decoded = [Test]() 54 | try decoder.read { element in 55 | decoded.append(element) 56 | } 57 | try decoder.close() 58 | 59 | XCTAssertEqual(input, decoded) 60 | } 61 | 62 | func testAddToExistingFile() throws { 63 | let input = (0..<1000).map { Test(a: $0, b: "\($0)") } 64 | 65 | // Write ten distinct times to file 66 | for i in 0..<10 { 67 | let encoder = try BinaryFileEncoder(fileAt: fileUrl) 68 | let part = input[(i*100)..<(i+1)*100] 69 | try encoder.write(contentsOf: part) 70 | try encoder.close() 71 | } 72 | 73 | // Decode all together 74 | let decoder = try BinaryFileDecoder(fileAt: fileUrl) 75 | var decoded = [Test]() 76 | try decoder.read { element in 77 | decoded.append(element) 78 | } 79 | try decoder.close() 80 | 81 | XCTAssertEqual(input, decoded) 82 | } 83 | 84 | func testReadUntilError() throws { 85 | let input = (0..<1000).map { Test(a: $0, b: "\($0)") } 86 | 87 | // Write first 500 elements 88 | do { 89 | let encoder = try BinaryFileEncoder(fileAt: fileUrl) 90 | let part = input[0..<500] 91 | try encoder.write(contentsOf: part) 92 | try encoder.close() 93 | } 94 | 95 | // Write invalid byte 96 | do { 97 | let handle = try FileHandle(forWritingTo: fileUrl) 98 | if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) { 99 | try handle.seekToEnd() 100 | let data = Data([3]) 101 | try handle.write(contentsOf: data) 102 | } else { 103 | handle.seekToEndOfFile() 104 | let data = Data([3]) 105 | handle.write(data) 106 | } 107 | } 108 | 109 | // Write second 500 elements 110 | do { 111 | let encoder = try BinaryFileEncoder(fileAt: fileUrl) 112 | let part = input[500..<1000] 113 | try encoder.write(contentsOf: part) 114 | try encoder.close() 115 | } 116 | 117 | // Decode all together, which should fail 118 | do { 119 | let decoder = try BinaryFileDecoder(fileAt: fileUrl) 120 | defer { try? decoder.close() } 121 | let decoded = try decoder.readAll() 122 | XCTFail("Decoding should fail, but got \(decoded)") 123 | } catch DecodingError.dataCorrupted { 124 | 125 | } 126 | 127 | let decoder = try BinaryFileDecoder(fileAt: fileUrl) 128 | let decoded = decoder.readAllUntilError() 129 | try decoder.close() 130 | 131 | XCTAssertEqual(Array(input[0..<500]), decoded) 132 | } 133 | 134 | /** 135 | This test is currently not included, due to a bug in several `FileHandle` functions. 136 | Despite the documentation specifying that `write(contentsOf:)` throws, it actually 137 | produces an `NSException`, which can't be caught using try-catch. The test would therefore 138 | always fail. 139 | */ 140 | /* 141 | func testWriteAfterClosing() throws { 142 | let encoder = try BinaryFileEncoder(fileAt: fileUrl) 143 | try encoder.write(.init(a: 0, b: "\(0)")) 144 | try encoder.close() 145 | do { 146 | try encoder.write(.init(a: 1, b: "\(1)")) 147 | } catch { 148 | print(type(of: error)) 149 | print(error) 150 | } 151 | } 152 | */ 153 | 154 | func testCloseFileTwice() throws { 155 | let encoder = try BinaryFileEncoder(fileAt: fileUrl) 156 | try encoder.close() 157 | try encoder.close() 158 | // File also closing during deinit 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/PropertyWrapperCodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | /// While from the perspective of `Codable` nothing about property wrapper is special, 5 | /// they tend to encode their values via `singleValueContainer()`, which requires 6 | /// some considerations when it comes to dealing with optionals. 7 | /// 8 | final class PropertyWrapperCodingTests: XCTestCase { 9 | struct KeyedWrapper: Codable, Equatable { 10 | enum CodingKeys: String, CodingKey { 11 | case wrapper 12 | } 13 | 14 | @Wrapper 15 | var value: T 16 | 17 | init(_ value: T) { 18 | self._value = Wrapper(wrappedValue: value) 19 | } 20 | 21 | init(from decoder: Decoder) throws { 22 | let container = try decoder.container(keyedBy: CodingKeys.self) 23 | self._value = try container.decode(Wrapper.self, forKey: .wrapper) 24 | } 25 | 26 | func encode(to encoder: Encoder) throws { 27 | var container = encoder.container(keyedBy: CodingKeys.self) 28 | try container.encode(_value, forKey: .wrapper) 29 | } 30 | } 31 | 32 | @propertyWrapper 33 | struct Wrapper: Codable, Equatable { 34 | let wrappedValue: T 35 | 36 | init(wrappedValue: T) { 37 | self.wrappedValue = wrappedValue 38 | } 39 | 40 | init(from decoder: Decoder) throws { 41 | let container = try decoder.singleValueContainer() 42 | self.wrappedValue = try container.decode(T.self) 43 | } 44 | 45 | func encode(to encoder: Encoder) throws { 46 | var container = encoder.singleValueContainer() 47 | try container.encode(wrappedValue) 48 | } 49 | } 50 | 51 | struct WrappedString: Codable, Equatable { 52 | let val: String 53 | } 54 | 55 | func assert( 56 | encoding wrapped: T, 57 | as type: T.Type = T.self, 58 | expectByteSuffix byteSuffix: [UInt8], 59 | file: StaticString = #file, 60 | line: UInt = #line 61 | ) throws { 62 | let bytePrefix: [UInt8] = [ 63 | 15, // String key, length 7 64 | 119, 114, 97, 112, 112, 101, 114, // "wrapper" 65 | ] 66 | 67 | let wrapper = KeyedWrapper(wrapped) 68 | let data = try BinaryEncoder.encode(wrapper) 69 | 70 | // If the prefix differs, this error affects the test helper, so report it here 71 | XCTAssertEqual(Array(data.prefix(bytePrefix.count)), bytePrefix) 72 | 73 | // If the suffix differs, this error is specific to the individual test case, 74 | // so report it on the call-side 75 | XCTAssertEqual(Array(data.suffix(from: bytePrefix.count)), byteSuffix, file: (file), line: line) 76 | 77 | let decodedWrapper: KeyedWrapper = try BinaryDecoder.decode(from: data) 78 | XCTAssertEqual(decodedWrapper, wrapper, file: (file), line: line) 79 | } 80 | 81 | func testOptionalWrappedStringSome() throws { 82 | try assert( 83 | encoding: WrappedString(val: "Some"), 84 | as: WrappedString?.self, 85 | expectByteSuffix: [ 86 | 20, // Length 10 87 | 0, // Non-nil 88 | 7, // String key, length 3 89 | 118, 97, 108, // "val" 90 | 8, // Length 4, 91 | 83, 111, 109, 101, // String "Some" 92 | ] 93 | ) 94 | } 95 | 96 | func testOptionalWrappedStringNone() throws { 97 | try assert( 98 | encoding: nil, 99 | as: WrappedString?.self, 100 | expectByteSuffix: [ 101 | 2, // Length 1 102 | 1, // Nil indicator 103 | ] 104 | ) 105 | } 106 | 107 | func testOptionalBool() throws { 108 | try assert( 109 | encoding: .some(true), 110 | as: Bool?.self, 111 | expectByteSuffix: [ 112 | 4, // Length 2 113 | 0, // Non-nil 114 | 1, // Boolean is true 115 | ] 116 | ) 117 | 118 | try assert( 119 | encoding: .some(false), 120 | as: Bool?.self, 121 | expectByteSuffix: [ 122 | 4, // Length 2 123 | 0, // Non-nil 124 | 0, // Boolean is false 125 | ] 126 | ) 127 | 128 | try assert( 129 | encoding: nil, 130 | as: Bool?.self, 131 | expectByteSuffix: [ 132 | 2, // Length 1 133 | 1, // Nil 134 | ] 135 | ) 136 | } 137 | 138 | func testDoubleOptionalBool() throws { 139 | try assert( 140 | encoding: .some(.some(true)), 141 | as: Bool??.self, 142 | expectByteSuffix: [ 143 | 6, // Length 3 144 | 0, // Not nil 145 | 0, // Not nil 146 | 1 // true 147 | ] 148 | ) 149 | 150 | try assert( 151 | encoding: .some(.some(false)), 152 | as: Bool??.self, 153 | expectByteSuffix: [ 154 | 6, // Length 3 155 | 0, // Not nil 156 | 0, // Not nil 157 | 0 // false 158 | ] 159 | ) 160 | 161 | try assert( 162 | encoding: .some(nil), 163 | as: Bool??.self, 164 | expectByteSuffix: [ 165 | 4, // Length 2 166 | 0, // Not nil 167 | 1 // Nil 168 | ] 169 | ) 170 | 171 | try assert( 172 | encoding: nil, 173 | as: Bool??.self, 174 | expectByteSuffix: [ 175 | 2, // Length 1 176 | 1 // Nil 177 | ] 178 | ) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/ContainerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | final class ContainerTests: XCTestCase { 5 | 6 | /** 7 | Test that it's possible to create multiple single value containers on the same encoder, 8 | and that only the last value is saved, regardless of which container is used. 9 | */ 10 | func testMultipleCallsToSingleContainer() throws { 11 | struct Test: Codable, Equatable { 12 | let key: String 13 | 14 | init(key: String) { 15 | self.key = key 16 | } 17 | 18 | enum CodingKeys: CodingKey { 19 | case key 20 | } 21 | 22 | func encode(to encoder: any Encoder) throws { 23 | var container1 = encoder.singleValueContainer() 24 | var container2 = encoder.singleValueContainer() 25 | try container1.encode("abc") 26 | try container2.encode(key) 27 | } 28 | 29 | init(from decoder: any Decoder) throws { 30 | let container = try decoder.singleValueContainer() 31 | self.key = try container.decode(String.self) 32 | } 33 | } 34 | 35 | let value = Test(key: "ABC") 36 | try compare(value) 37 | } 38 | 39 | /** 40 | Test that encoding fails if no calls are made to a single value container. 41 | */ 42 | func testNoValueEncodedInSingleValueContainer() throws { 43 | struct Test: Codable, Equatable { 44 | let key: String 45 | 46 | init(key: String) { 47 | self.key = key 48 | } 49 | 50 | func encode(to encoder: any Encoder) throws { 51 | let _ = encoder.singleValueContainer() 52 | } 53 | } 54 | 55 | let value = Test(key: "ABC") 56 | do { 57 | _ = try BinaryEncoder().encode(value) 58 | XCTFail("Should not be able to encode type with unset single value container") 59 | } catch EncodingError.invalidValue(_, let context) { 60 | XCTAssertEqual(context.codingPath, []) 61 | XCTAssertNil(context.underlyingError) 62 | } 63 | } 64 | 65 | /** 66 | Test that multiple keyed containers can be used to encode values 67 | */ 68 | func testMultipleKeyedContainersForEncoding() throws { 69 | struct Test: Codable, Equatable { 70 | let a: String 71 | let b: String 72 | 73 | func encode(to encoder: any Encoder) throws { 74 | var container1 = encoder.container(keyedBy: CodingKeys.self) 75 | var container2 = encoder.container(keyedBy: CodingKeys.self) 76 | try container1.encode(a, forKey: .a) 77 | try container2.encode(b, forKey: .b) 78 | } 79 | } 80 | 81 | try compare(Test(a: "a", b: "b")) 82 | } 83 | 84 | /** 85 | Test that it's possible to encode values from a derived class and the super class 86 | in the same keyed container. 87 | */ 88 | func testEncodingSuperAndSubclassInSameKeyedContainer() throws { 89 | class TestSuper: Codable { 90 | let a: Int 91 | init(a: Int) { 92 | self.a = a 93 | } 94 | } 95 | 96 | class TestDerived: TestSuper, Equatable { 97 | 98 | static func == (lhs: TestDerived, rhs: TestDerived) -> Bool { 99 | lhs.a == rhs.a && lhs.b == rhs.b 100 | } 101 | 102 | let b: Int 103 | 104 | init(a: Int, b: Int) { 105 | self.b = b 106 | super.init(a: a) 107 | } 108 | 109 | required init(from decoder: any Decoder) throws { 110 | let container = try decoder.container(keyedBy: CodingKeys.self) 111 | self.b = try container.decode(Int.self, forKey: .b) 112 | try super.init(from: decoder) 113 | } 114 | 115 | override func encode(to encoder: any Encoder) throws { 116 | try super.encode(to: encoder) 117 | var container = encoder.container(keyedBy: CodingKeys.self) 118 | try container.encode(b, forKey: .b) 119 | } 120 | 121 | enum CodingKeys: CodingKey { 122 | case b 123 | } 124 | } 125 | 126 | let value = TestDerived(a: 123, b: 234) 127 | let data = try BinaryEncoder().encode(value) 128 | print(Array(data)) 129 | 130 | try compare(value) 131 | } 132 | 133 | func testUseMultipleKeyedDecoders() throws { 134 | struct Test: Codable, Equatable { 135 | let a: Int 136 | let b: Int 137 | 138 | init(a: Int, b: Int) { 139 | self.a = a 140 | self.b = b 141 | } 142 | 143 | enum CodingKeys1: CodingKey { 144 | case a 145 | } 146 | 147 | enum CodingKeys2: CodingKey { 148 | case b 149 | } 150 | 151 | init(from decoder: any Decoder) throws { 152 | let container = try decoder.container(keyedBy: CodingKeys1.self) 153 | self.a = try container.decode(Int.self, forKey: .a) 154 | let container2 = try decoder.container(keyedBy: CodingKeys2.self) 155 | self.b = try container2.decode(Int.self, forKey: .b) 156 | } 157 | } 158 | try compare(Test(a: 123, b: 234)) 159 | } 160 | 161 | /** 162 | Test that multiple unkeyed containers on the same node can be used, 163 | and that they encode values is the order of insertion independent of the container used. 164 | */ 165 | func testUseMultipleUnkeyedEncoders() throws { 166 | struct Test: Codable, Equatable { 167 | let a: Int 168 | let b: Int 169 | 170 | init(a: Int, b: Int) { 171 | self.a = a 172 | self.b = b 173 | } 174 | 175 | func encode(to encoder: any Encoder) throws { 176 | var container1 = encoder.unkeyedContainer() 177 | var container2 = encoder.unkeyedContainer() 178 | try container2.encode(a) 179 | try container1.encode(b) 180 | } 181 | 182 | init(from decoder: any Decoder) throws { 183 | var container = try decoder.unkeyedContainer() 184 | self.a = try container.decode(Int.self) 185 | self.b = try container.decode(Int.self) 186 | } 187 | } 188 | try compare(Test(a: 123, b: 234)) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Sources/BinaryCodable/BinaryFileDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Read elements from a binary file. 5 | 6 | The decoder allows reading individual elements from a file without loading all file data to memory all at once. 7 | This decreases memory usage, which is especially useful for large files. 8 | Elements can also be read all at once, and corrupted files can be read until the first decoding error occurs. 9 | 10 | The class internally uses ``BinaryStreamDecoder`` to encode the individual elements, 11 | which can also be used independently to decode the data for more complex operations. 12 | 13 | **Handling corrupted data** 14 | 15 | The binary format does not necessarily allow detection of data corruption, and various errors can occur 16 | as the result of added, changed, or missing bytes. Additional measures should be applied if there is an 17 | increased risk of data corruption. 18 | 19 | As an example, consider the simple encoding of a `String` inside a `struct`, which consists of a `key` 20 | followed by the length of the string in bytes, and the string content. The length of the string is encoded using 21 | variable length encoding, so a single bit flip (in the MSB of the length byte) could result in a very large `length` being decoded, 22 | causing the decoder to wait for a very large number of bytes to decode the string. This simple error would cause much 23 | data to be skipped. At the same time, it is not possible to determine *with certainty* where the error occured. 24 | 25 | The library does therefore only provide hints about the decoding errors likely occuring from non-conformance to the binary format 26 | or version incompatibility, which are not necessarily the *true* causes of the failures when data corruption is present. 27 | 28 | 29 | - Note: This class is compatible with ``BinaryFileEncoder`` and ``BinaryStreamEncoder``, 30 | but not with the outputs of ``BinaryEncoder``. 31 | */ 32 | public final class BinaryFileDecoder where Element: Decodable { 33 | 34 | private let file: FileHandle 35 | 36 | private let decoder: BinaryDecoder 37 | 38 | private let endIndex: UInt64 39 | 40 | private var currentIndex: UInt64 = 0 41 | 42 | /** 43 | Create a file decoder. 44 | 45 | The given file is opened, and decoding will begin at the start of the file. 46 | 47 | - Parameter url: The url of the file to read. 48 | - Parameter decoder: The decoder to use for decoding 49 | - Throws: An error, if the file handle could not be created. 50 | */ 51 | public init(fileAt url: URL, decoder: BinaryDecoder = .init()) throws { 52 | let file = try FileHandle(forReadingFrom: url) 53 | self.file = file 54 | self.decoder = decoder 55 | if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) { 56 | self.endIndex = try file.seekToEnd() 57 | try file.seek(toOffset: 0) 58 | } else { 59 | self.endIndex = file.seekToEndOfFile() 60 | file.seek(toFileOffset: 0) 61 | } 62 | } 63 | 64 | deinit { 65 | try? close() 66 | } 67 | 68 | /** 69 | Close the file. 70 | 71 | - Note: After closing the file, the decoder can no longer read elements, which will result in an error or an exception. 72 | - Throws: Currently throws a ObjC-style `Exception`, not an `Error`, even on modern systems. 73 | This is a bug in the Foundation framework. 74 | */ 75 | public func close() throws { 76 | if #available(macOS 10.15, iOS 13.0, tvOS 13.4, watchOS 6.2, *) { 77 | try file.close() 78 | } else { 79 | file.closeFile() 80 | } 81 | } 82 | 83 | // MARK: Decoding 84 | 85 | /** 86 | Read all elements in the file, and handle each element using a closure. 87 | 88 | - Parameter elementHandler: The closure to handle each element as it is decoded. 89 | - Throws: Decoding errors of type `DecodingError`. 90 | */ 91 | public func read(_ elementHandler: (Element) throws -> Void) throws { 92 | while let element = try readElement() { 93 | try elementHandler(element) 94 | } 95 | } 96 | 97 | /** 98 | Read all elements at once. 99 | - Returns: The elements decoded from the file. 100 | - Throws: Errors of type `DecodingError` 101 | */ 102 | public func readAll() throws -> [Element] { 103 | var result = [Element]() 104 | while let element = try readElement() { 105 | result.append(element) 106 | } 107 | return result 108 | } 109 | 110 | /** 111 | Read all elements at once, and ignore errors. 112 | 113 | This function reads elements until it reaches the end of the file or detects a decoding error. 114 | Any data after the first error will be ignored. 115 | - Returns: The elements successfully decoded from the file. 116 | */ 117 | public func readAllUntilError() -> [Element] { 118 | var result = [Element]() 119 | while let element = try? readElement() { 120 | result.append(element) 121 | } 122 | return result 123 | } 124 | 125 | /** 126 | Read a single elements from the current position in the file. 127 | - Returns: The element decoded from the file, or `nil`, if no more data is available. 128 | - Throws: Errors of type `DecodingError` 129 | */ 130 | public func readElement() throws -> Element? { 131 | guard !isAtEnd(at: currentIndex) else { 132 | return nil 133 | } 134 | // Read length/nil indicator 135 | let data = try wrapCorruptDataError { 136 | try decodeNextDataOrNilElement(at: ¤tIndex) 137 | } 138 | let node = try DecodingNode(data: data, parentDecodedNil: true, codingPath: [], userInfo: decoder.userInfo) 139 | return try Element.init(from: node) 140 | } 141 | 142 | private func getCurrentIndex() -> UInt64 { 143 | if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) { 144 | return (try? file.offset()) ?? endIndex 145 | } 146 | return file.offsetInFile 147 | } 148 | } 149 | 150 | extension BinaryFileDecoder: DecodingDataProvider { 151 | 152 | var startIndex: UInt64 { 0 } 153 | 154 | func isAtEnd(at index: UInt64) -> Bool { 155 | index >= endIndex 156 | } 157 | 158 | func nextByte(at index: inout UInt64) -> UInt8? { 159 | nextBytes(1, at: &index)?.first 160 | } 161 | 162 | func nextBytes(_ count: Int, at index: inout UInt64) -> Data? { 163 | guard #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) else { 164 | let data = file.readData(ofLength: count) 165 | guard data.count == count else { 166 | return nil 167 | } 168 | defer { index += UInt64(count) } 169 | return data 170 | } 171 | guard let data = try? file.read(upToCount: count) else { 172 | return nil 173 | } 174 | defer { index += UInt64(count) } 175 | return data 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/EnumEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | final class EnumEncodingTests: XCTestCase { 5 | 6 | func testEnumEncoding() throws { 7 | enum Test: Codable, Equatable { 8 | case one 9 | case two 10 | } 11 | let expected1: [UInt8] = [ 12 | 7, /// `String` key, length `3` 13 | 111, 110, 101, /// String key `one` 14 | 0 /// Encodes `0` as the `value` 15 | ] 16 | try compare(Test.one, to: expected1) 17 | 18 | let expected2: [UInt8] = [ 19 | 7, /// `String` key, length `3` 20 | 116, 119, 111, /// String key `one` 21 | 0 /// Encodes `0` as the `value` 22 | ] 23 | try compare(Test.two, to: expected2) 24 | } 25 | 26 | func testIntEnumEncoding() throws { 27 | enum Test: Int, Codable, Equatable { 28 | case one = 1 29 | case two = 2 30 | } 31 | try compare(Test.one, to: [0, 2]) // Nil indicator + raw value 32 | try compare(Test.two, to: [0, 4]) 33 | } 34 | 35 | func testStringEnumEncoding() throws { 36 | enum Test: String, Codable, Equatable { 37 | case one = "one" 38 | case two = "two" 39 | } 40 | try compare(Test.one, to: [0, 111, 110, 101]) 41 | try compare(Test.two, to: [0, 116, 119, 111]) 42 | } 43 | 44 | func testEnumWithAssociatedValues() throws { 45 | /** 46 | Note: `Codable` encoded associated values using the string keys `_0`, `_1`, ... 47 | This depends on the number of associated values 48 | */ 49 | enum Test: Codable, Equatable { 50 | 51 | case one(String) 52 | case two(Int) 53 | case three(Data) 54 | case four(Bool) 55 | case five(UInt, Int8) 56 | } 57 | try compare(Test.one("Some"), to: [ 58 | 7, // String key, length 3 59 | 111, 110, 101, // String "one" 60 | 16, // Length 8 61 | 5, // String key, length 2 62 | 95, 48, // String "_0" 63 | 8, // Length 4 64 | 83, 111, 109, 101 // String "Some" 65 | ]) 66 | try compare(Test.two(123), to: [ 67 | 7, // String key, length 3 68 | 116, 119, 111, // String "two" 69 | 12, // Length 6 70 | 5, // String key, length 2 71 | 95, 48, // String "_0" 72 | 4, // Length 2 73 | 246, 1 // Int(123) 74 | ]) 75 | try compare(Test.three(.init(repeating: 42, count: 3)), to: [ 76 | 11, // String key, length 5 77 | 116, 104, 114, 101, 101, // String "three" 78 | 14, // Length 7 79 | 5, // String key, length 2 80 | 95, 48, // String "_0" 81 | 6, // Length 3 82 | 42, 42, 42 // Data(42, 42, 42) 83 | ]) 84 | 85 | try compare(Test.four(true), to: [ 86 | 9, // String key, length 4 87 | 102, 111, 117, 114, // String "four" 88 | 10, // Length 5 89 | 5, // String key, length 2 90 | 95, 48, // String "_0" 91 | 2, 1 // Bool(true) 92 | ]) 93 | 94 | let start: [UInt8] = [ 95 | 9, // String key, length 4 96 | 102, 105, 118, 101, // String "five" 97 | 20] // Length 10 98 | let a: [UInt8] = [ 99 | 5, // String key, length 2 100 | 95, 48, // String "_0" 101 | 2, // Length 1 102 | 123] // UInt(123) 103 | let b: [UInt8] = [ 104 | 5, // String key, length 2 105 | 95, 49, // String "_1" 106 | 2, // Length 1 107 | 133] // Int8(-123) 108 | try compare(Test.five(123, -123), toOneOf: [start + a + b, start + b + a]) 109 | 110 | struct Wrap: Codable, Equatable { 111 | 112 | let value: Test 113 | 114 | enum CodingKeys: Int, CodingKey { 115 | case value = 1 116 | } 117 | } 118 | 119 | try compare(Wrap(value: .four(true)), to: [ 120 | 2, // Int key '1' 121 | 22, // Length 11 122 | 9, // String key, length 4 123 | 102, 111, 117, 114, // String "four" 124 | 10, // Length 5 125 | 5, // String key, length 2 126 | 95, 48, // String "_0" 127 | 2, // Length 1 128 | 1 // Bool(true) 129 | ]) 130 | } 131 | 132 | func testEnumWithAssociatedValuesAndIntegerKeys() throws { 133 | enum Test: Codable, Equatable { 134 | 135 | case one(String) 136 | case two(Int) 137 | case three(Data) 138 | case four(Bool) 139 | case five(UInt, Int8) 140 | 141 | enum CodingKeys: Int, CodingKey { 142 | case one = 1 143 | case two = 2 144 | case three = 3 145 | case four = 4 146 | case five = 5 147 | } 148 | } 149 | 150 | try compare(Test.one("Some"), to: [ 151 | 2, // Int key 1 152 | 16, // Length 8 153 | 5, // String key, length 2 154 | 95, 48, // String "_0" 155 | 8, // Length 4 156 | 83, 111, 109, 101 // String "Some" 157 | ]) 158 | 159 | try compare(Test.two(123), to: [ 160 | 4, // Int key 2 161 | 12, // Length 6 162 | 5, // String key, length 2 163 | 95, 48, // String "_0" 164 | 4, // Length 2 165 | 246, 1 // Int(123) 166 | ]) 167 | 168 | try compare(Test.three(.init(repeating: 42, count: 3)), to: [ 169 | 6, // Int key 3 170 | 14, // Length 7 171 | 5, // String key, length 2 172 | 95, 48, // String "_0" 173 | 6, // Length 3 174 | 42, 42, 42 // Data(42, 42, 42) 175 | ]) 176 | 177 | try compare(Test.four(true), to: [ 178 | 8, // Int key 4 179 | 10, // Length 5 180 | 5, // String key, length 2 181 | 95, 48, // String "_0" 182 | 2, // Length 1 183 | 1 // Bool(true) 184 | ]) 185 | 186 | let start: [UInt8] = [ 187 | 10, // Int key 5 188 | 20] // Length 10 189 | let a: [UInt8] = [ 190 | 5, // String key, length 2 191 | 95, 48, // String "_0" 192 | 2, // Length 1 193 | 123] // UInt(123) 194 | let b: [UInt8] = [ 195 | 5, // String key, length 2 196 | 95, 49, // String "_1" 197 | 2, // Length 1 198 | 133] // Int8(-123) 199 | try compare(Test.five(123, -123), toOneOf: [start + a + b, start + b + a]) 200 | } 201 | 202 | func testDecodeUnknownCase() throws { 203 | enum Test: Int, Codable { 204 | case one // No raw value assigns 0 205 | } 206 | 207 | try compare(Test.one, to: [0, 0]) // Nil indicator + RawValue 0 208 | 209 | let decoder = BinaryDecoder() 210 | do { 211 | _ = try decoder.decode(Test.self, from: Data([0, 1])) 212 | XCTFail("Enum decoding should fail for unknown case") 213 | } catch DecodingError.dataCorrupted(let context) { 214 | XCTAssertEqual(context.codingPath, []) 215 | // Correct error 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/OptionalEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | final class OptionalEncodingTests: XCTestCase { 5 | 6 | func testArrayOfOptionals() throws { 7 | let value: [Int?] = [1, nil] 8 | try compare(value, to: [ 9 | 2, // Not nil, length 1 10 | 2, // Int 1 11 | 1 // Nil, length zero 12 | ]) 13 | } 14 | 15 | func testOptionalBoolEncoding() throws { 16 | try compare(true, of: Bool?.self, to: [0, 1]) 17 | try compare(false, of: Bool?.self, to: [0, 0]) 18 | try compare(nil, of: Bool?.self, to: [1]) 19 | } 20 | 21 | func testDoubleOptionalBoolEncoding() throws { 22 | try compare(.some(.some(true)), of: Bool??.self, to: [0, 0, 1]) 23 | try compare(.some(.some(false)), of: Bool??.self, to: [0, 0, 0]) 24 | try compare(.some(.none), of: Bool??.self, to: [0, 1]) 25 | try compare(.none, of: Bool??.self, to: [1]) 26 | } 27 | 28 | func testOptionalStruct() throws { 29 | struct T: Codable, Equatable { 30 | var a: Int 31 | } 32 | try compare(T(a: 123), of: T?.self, to: [ 33 | 0, // Not nil 34 | 3, // String key, length 1 35 | 97, // String "a" 36 | 4, // Length 2 37 | 246, 1 // Int 123 38 | ]) 39 | try compare(nil, of: T?.self, to: [1]) 40 | } 41 | 42 | func testOptionalInStructEncoding() throws { 43 | struct Test: Codable, Equatable { 44 | let value: UInt16 45 | 46 | let opt: Int16? 47 | 48 | enum CodingKeys: Int, CodingKey { 49 | case value = 5 50 | case opt = 4 51 | } 52 | } 53 | // Note: `encodeNil()` is not called for single optionals 54 | try compare(Test(value: 123, opt: nil), to: [ 55 | 10, // Int key 5 56 | 4, // Length 2 57 | 123, 0 // Int 123 58 | ]) 59 | let part1: [UInt8] = [ 60 | 10, // Int key 5 61 | 4, // Length 2 62 | 123, 0 // value: 123 63 | ] 64 | let part2: [UInt8] = [ 65 | 8, // Int key 4 66 | 4, // Length 2 67 | 12, 0 // opt: 12 68 | ] 69 | try compare(Test(value: 123, opt: 12), toOneOf: [part1 + part2, part2 + part1]) 70 | } 71 | 72 | func testDoubleOptional() throws { 73 | struct Test: Codable, Equatable { 74 | let opt: Int16?? 75 | 76 | enum CodingKeys: Int, CodingKey { 77 | case opt = 4 78 | } 79 | } 80 | try compare(Test(opt: .some(nil)), to: [ 81 | 8, // Int key 4 82 | 1, // nil 83 | ]) 84 | } 85 | 86 | func testDoubleOptionalInStruct() throws { 87 | struct Test: Codable, Equatable { 88 | let value: UInt16 89 | 90 | let opt: Int16?? 91 | 92 | enum CodingKeys: Int, CodingKey { 93 | case value = 5 94 | case opt = 4 95 | } 96 | } 97 | try compare(Test(value: 123, opt: nil), to: [ 98 | 10, // Int key 5 99 | 4, // Length 2 100 | 123, 0 // Int 123 101 | ]) 102 | 103 | let part1: [UInt8] = [ 104 | 10, // Int key 5 105 | 4, // Length 2 106 | 123, 0 // value: 123 107 | ] 108 | let part2: [UInt8] = [ 109 | 8, // Int key 4 110 | 1, // nil 111 | ] 112 | try compare(Test(value: 123, opt: .some(nil)), toOneOf: [part1 + part2, part2 + part1]) 113 | 114 | let part3: [UInt8] = [ 115 | 10, // Int key 5 116 | 4, // Length 2 117 | 123, 0 // value: 123 118 | ] 119 | let part4: [UInt8] = [ 120 | 8, // Int key 4 121 | 4, // Not nil, Length 2 122 | 12, 0 // value: 12 123 | ] 124 | try compare(Test(value: 123, opt: 12), toOneOf: [part3 + part4, part4 + part3]) 125 | } 126 | 127 | func testTripleOptionalInStruct() throws { 128 | struct Test: Codable, Equatable { 129 | let opt: Int??? 130 | 131 | enum CodingKeys: Int, CodingKey { 132 | case opt = 4 133 | } 134 | } 135 | try compare(Test(opt: nil), to: []) 136 | 137 | try compare(Test(opt: .some(nil)), to: [ 138 | 8, // Int key 4 139 | 1, // nil 140 | ]) 141 | 142 | try compare(Test(opt: .some(.some(nil))), to: [ 143 | 8, // Int key 4 144 | 2, // Not nil, length 2 145 | 1, // nil 146 | ]) 147 | 148 | try compare(Test(opt: .some(.some(.some(5)))), to: [ 149 | 8, // Int key 4 150 | 4, // Not nil, length 2 151 | 0, // Not nil 152 | 10, // Int 10 153 | ]) 154 | } 155 | 156 | func testClassWithOptionalProperty() throws { 157 | // NOTE: Here, the field for 'date' is present in the data 158 | // because the optional is directly encoded using encode() 159 | // The field for 'partner' is not added, since it's encoded using `encodeIfPresent()` 160 | let item = TestClass(name: "Bob", date: nil, partner: nil) 161 | try compare(item, to: [ 162 | 2, // Int key 1 163 | 6, // Length 3 164 | 66, 111, 98, // Bob 165 | 4, // Int key 2 166 | 1 // Nil 167 | ], sortingKeys: true) 168 | 169 | let item2 = TestClass(name: "Bob", date: "s", partner: "Alice") 170 | try compare(item2, to: [ 171 | 2, // Int key 1 172 | 6, // Length 3 173 | 66, 111, 98, // Bob 174 | 4, // Int key 2 175 | 2, // Length 2 176 | 115, // "s" 177 | 6, // Int key 3 178 | 10, // Length 5 179 | 65, 108, 105, 99, 101 180 | ], sortingKeys: true) 181 | } 182 | } 183 | 184 | 185 | private final class TestClass: Codable, Equatable, CustomStringConvertible { 186 | let name: String 187 | let date: String? 188 | let partner: String? 189 | 190 | enum CodingKeys: Int, CodingKey { 191 | case name = 1 192 | case date = 2 193 | case partner = 3 194 | } 195 | 196 | convenience init(from decoder: Decoder) throws { 197 | let container = try decoder.container(keyedBy: CodingKeys.self) 198 | let name = try container.decode(String.self, forKey: .name) 199 | let date = try container.decode(String?.self, forKey: .date) 200 | let partner = try container.decodeIfPresent(String.self, forKey: .partner) 201 | self.init(name: name, date: date, partner: partner) 202 | } 203 | 204 | func encode(to encoder: Encoder) throws { 205 | var container = encoder.container(keyedBy: CodingKeys.self) 206 | try container.encode(name, forKey: .name) 207 | try container.encode(date, forKey: .date) 208 | try container.encodeIfPresent(partner, forKey: .partner) 209 | } 210 | 211 | init(name: String, date: String?, partner: String?) { 212 | self.name = name 213 | self.date = date 214 | self.partner = partner 215 | } 216 | 217 | static func == (lhs: TestClass, rhs: TestClass) -> Bool { 218 | lhs.name == rhs.name && lhs.date == rhs.date && lhs.partner == rhs.partner 219 | } 220 | 221 | var description: String { 222 | "\(name): \(date ?? "nil"), \(partner ?? "nil")" 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/SetTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | final class SetTests: XCTestCase { 5 | 6 | func testBoolSetEncoding() throws { 7 | try compare([true, false], of: Set.self) 8 | try compare([true], of: Set.self, to: [1]) 9 | try compare([false], of: Set.self, to: [0]) 10 | } 11 | 12 | func testInt8SetEncoding() throws { 13 | try compare([.zero, 123, .min, .max, -1], of: Set.self) 14 | try compare([-1], of: Set.self, to: [255]) 15 | } 16 | 17 | func testInt16SetEncoding() throws { 18 | try compare([.zero, 123, .min, .max, -1], of: Set.self) 19 | try compare([-1], of: Set.self, to: [255, 255]) 20 | } 21 | 22 | func testInt32SetEncoding() throws { 23 | try compare([.zero, 123, .min, .max, -1], of: Set.self) 24 | try compare([-1], of: Set.self, to: [1]) 25 | } 26 | 27 | func testInt64SetEncoding() throws { 28 | try compare([0, 123, .max, .min, -1], of: Set.self) 29 | try compare([-1], of: Set.self, to: [1]) 30 | } 31 | 32 | func testIntSetEncoding() throws { 33 | try compare([0, 123, .max, .min, -1], of: Set.self) 34 | try compare([0], of: Set.self, to: [0]) 35 | try compare([123], of: Set.self, to: [246, 1]) 36 | try compare([.max], of: Set.self, to: [0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 37 | try compare([.min], of: Set.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 38 | try compare([-1], of: Set.self, to: [1]) // 1 39 | } 40 | 41 | func testUInt8SetEncoding() throws { 42 | try compare([.zero, 123, .min, .max], of: Set.self) 43 | try compare([.zero], of: Set.self, to: [0]) 44 | try compare([123], of: Set.self, to: [123]) 45 | try compare([.min], of: Set.self, to: [0]) 46 | try compare([.max], of: Set.self, to: [255]) 47 | } 48 | 49 | func testUInt16SetEncoding() throws { 50 | try compare([.zero, 123, .min, .max, 12345], of: Set.self) 51 | try compare([.zero], of: Set.self, to: [0, 0]) 52 | try compare([123], of: Set.self, to: [123, 0]) 53 | try compare([.min], of: Set.self, to: [0, 0]) 54 | try compare([.max], of: Set.self, to: [255, 255]) 55 | try compare([12345], of: Set.self, to: [0x39, 0x30]) 56 | } 57 | 58 | func testUInt32SetEncoding() throws { 59 | try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, .max], of: Set.self) 60 | 61 | try compare([.zero], of: Set.self, to: [0]) 62 | try compare([123], of: Set.self, to: [123]) 63 | try compare([.min], of: Set.self, to: [0]) 64 | try compare([12345], of: Set.self, to: [0xB9, 0x60]) 65 | try compare([123456], of: Set.self, to: [0xC0, 0xC4, 0x07]) 66 | try compare([12345678], of: Set.self, to: [0xCE, 0xC2, 0xF1, 0x05]) 67 | try compare([1234567890], of: Set.self, to: [0xD2, 0x85, 0xD8, 0xCC, 0x04]) 68 | try compare([.max], of: Set.self, to: [255, 255, 255, 255, 15]) 69 | } 70 | 71 | func testUInt64SetEncoding() throws { 72 | try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: Set.self) 73 | 74 | try compare([.zero], of: Set.self, to: [0]) 75 | try compare([123], of: Set.self, to: [123]) 76 | try compare([.min], of: Set.self, to: [0]) 77 | try compare([12345], of: Set.self, to: [0xB9, 0x60]) 78 | try compare([123456], of: Set.self, to: [0xC0, 0xC4, 0x07]) 79 | try compare([12345678], of: Set.self, to: [0xCE, 0xC2, 0xF1, 0x05]) 80 | try compare([1234567890], of: Set.self, to: [0xD2, 0x85, 0xD8, 0xCC, 0x04]) 81 | try compare([12345678901234], of: Set.self, to: [0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02]) 82 | try compare([.max], of: Set.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 83 | } 84 | 85 | func testUIntSetEncoding() throws { 86 | try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: Set.self) 87 | 88 | try compare([.zero], of: Set.self, to: [0]) 89 | try compare([123], of: Set.self, to: [123]) 90 | try compare([.min], of: Set.self, to: [0]) 91 | try compare([12345], of: Set.self, to: [0xB9, 0x60]) 92 | try compare([123456], of: Set.self, to: [0xC0, 0xC4, 0x07]) 93 | try compare([12345678], of: Set.self, to: [0xCE, 0xC2, 0xF1, 0x05]) 94 | try compare([1234567890], of: Set.self, to: [0xD2, 0x85, 0xD8, 0xCC, 0x04]) 95 | try compare([12345678901234], of: Set.self, to: [0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02]) 96 | try compare([.max], of: Set.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 97 | } 98 | 99 | func testStringSetEncoding() throws { 100 | let values1 = ["Some"] 101 | try compare(values1, to: [ 102 | 8, // Length 4 103 | 83, 111, 109, 101 // "Some" 104 | ]) 105 | 106 | let values2: Set = ["Some", "A longer text with\n multiple lines", "More text", "eolqjwqu(Jan?!)§(!N"] 107 | try compare(values2) 108 | } 109 | 110 | func testFloatSetEncoding() throws { 111 | try compare([.greatestFiniteMagnitude, .zero, .pi, -.pi, .leastNonzeroMagnitude], of: Set.self) 112 | try compare([.greatestFiniteMagnitude], of: Set.self, to: [0x7F, 0x7F, 0xFF, 0xFF]) 113 | try compare([.zero], of: Set.self, to: [0x00, 0x00, 0x00, 0x00]) 114 | try compare([.pi], of: Set.self, to: [0x40, 0x49, 0x0F, 0xDA]) 115 | try compare([-.pi], of: Set.self, to: [0xC0, 0x49, 0x0F, 0xDA]) 116 | try compare([.leastNonzeroMagnitude], of: Set.self, to: [0x00, 0x00, 0x00, 0x01]) 117 | } 118 | 119 | func testDoubleSetEncoding() throws { 120 | try compare([.greatestFiniteMagnitude, .zero, .pi, .leastNonzeroMagnitude, -.pi], of: Set.self) 121 | 122 | try compare([.greatestFiniteMagnitude], of: Set.self, to: [0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 123 | try compare([.zero], of: Set.self, to: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) 124 | try compare([.pi], of: Set.self, to: [0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18]) 125 | try compare([.leastNonzeroMagnitude], of: Set.self, to: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) 126 | try compare([-.pi], of: Set.self, to: [0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18]) 127 | } 128 | 129 | func testSetOfOptionalsEncoding() throws { 130 | try compare([true, false, nil], of: Set.self) 131 | } 132 | 133 | func testSetOfDoubleOptionalsEncoding() throws { 134 | try compare([.some(.some(true)), .some(.some(false)), .some(.none), .none], of: Set.self) 135 | } 136 | 137 | func testSetOfTripleOptionalsEncoding() throws { 138 | try compare([.some(.some(.some(true))), .some(.some(.some(false))), .some(.some(.none)), .some(.none), .none], of: Set.self) 139 | } 140 | 141 | func testSetOfSetsEncoding() throws { 142 | let values: Set> = [[false], [true, false]] 143 | try compare(values, of: Set>.self) 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/ArrayEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BinaryCodable 3 | 4 | final class ArrayEncodingTests: XCTestCase { 5 | 6 | func testBoolArrayEncoding() throws { 7 | try compare([true, false, false], to: [ 8 | 1, 0, 0 9 | ]) 10 | } 11 | 12 | func testInt8ArrayEncoding() throws { 13 | try compare([.zero, 123, .min, .max, -1], of: [Int8].self, to: [ 14 | 0, 123, 128, 127, 255 15 | ]) 16 | } 17 | 18 | func testInt16ArrayEncoding() throws { 19 | try compare([.zero, 123, .min, .max, -1], of: [Int16].self, to: [ 20 | 0, 0, 21 | 123, 0, 22 | 0, 128, 23 | 255, 127, 24 | 255, 255 25 | ]) 26 | } 27 | 28 | func testInt32ArrayEncoding() throws { 29 | try compare([.zero, 123, .min, .max, -1], of: [Int32].self, to: [ 30 | 0, // 0 31 | 246, 1, // 123 32 | 255, 255, 255, 255, 15, // -2.147.483.648 33 | 254, 255, 255, 255, 15, // 2.147.483.647 34 | 1]) // -1 35 | } 36 | 37 | func testInt64ArrayEncoding() throws { 38 | try compare([0, 123, .max, .min, -1], of: [Int64].self, to: [ 39 | 0, // 0 40 | 246, 1, // 123 41 | 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // max 42 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // min 43 | 1]) // 1 44 | } 45 | 46 | func testIntArrayEncoding() throws { 47 | try compare([0, 123, .max, .min, -1], of: [Int].self, to: [ 48 | 0, // 0 49 | 246, 1, // 123 50 | 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // max 51 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // min 52 | 1]) // 1 53 | } 54 | 55 | func testUInt8ArrayEncoding() throws { 56 | try compare([.zero, 123, .min, .max], of: [UInt8].self, to: [ 57 | 0, 58 | 123, 59 | 0, 60 | 255 61 | ]) 62 | } 63 | 64 | func testUInt16ArrayEncoding() throws { 65 | try compare([.zero, 123, .min, .max, 12345], of: [UInt16].self, to: [ 66 | 0, 0, 67 | 123, 0, 68 | 0, 0, 69 | 255, 255, 70 | 0x39, 0x30 71 | ]) 72 | } 73 | 74 | func testUInt32ArrayEncoding() throws { 75 | try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, .max], of: [UInt32].self, to: [ 76 | 0, 77 | 123, 78 | 0, 79 | 0xB9, 0x60, 80 | 0xC0, 0xC4, 0x07, 81 | 0xCE, 0xC2, 0xF1, 0x05, 82 | 0xD2, 0x85, 0xD8, 0xCC, 0x04, 83 | 255, 255, 255, 255, 15]) 84 | } 85 | 86 | func testUInt64ArrayEncoding() throws { 87 | try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: [UInt64].self, to: [ 88 | 0, 89 | 123, 90 | 0, 91 | 0xB9, 0x60, 92 | 0xC0, 0xC4, 0x07, 93 | 0xCE, 0xC2, 0xF1, 0x05, 94 | 0xD2, 0x85, 0xD8, 0xCC, 0x04, 95 | 0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02, 96 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 97 | } 98 | 99 | func testUIntArrayEncoding() throws { 100 | try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: [UInt].self, to: [ 101 | 0, 102 | 123, 103 | 0, 104 | 0xB9, 0x60, 105 | 0xC0, 0xC4, 0x07, 106 | 0xCE, 0xC2, 0xF1, 0x05, 107 | 0xD2, 0x85, 0xD8, 0xCC, 0x04, 108 | 0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02, 109 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 110 | } 111 | 112 | func testStringArrayEncoding() throws { 113 | let values = ["Some", "A longer text with\n multiple lines", "More text", "eolqjwqu(Jan?!)§(!N"] 114 | let result = values.map { value -> [UInt8] in 115 | let data = Array(value.data(using: .utf8)!) 116 | return [UInt8(data.count * 2)] + data 117 | }.reduce([], +) 118 | try compare(values, to: result) 119 | } 120 | 121 | func testFloatArrayEncoding() throws { 122 | try compare([.greatestFiniteMagnitude, .zero, .pi, -.pi, .leastNonzeroMagnitude], of: [Float].self, to: [ 123 | 0x7F, 0x7F, 0xFF, 0xFF, 124 | 0x00, 0x00, 0x00, 0x00, 125 | 0x40, 0x49, 0x0F, 0xDA, 126 | 0xC0, 0x49, 0x0F, 0xDA, 127 | 0x00, 0x00, 0x00, 0x01]) 128 | } 129 | 130 | func testDoubleArrayEncoding() throws { 131 | try compare([.greatestFiniteMagnitude, .zero, .pi, .leastNonzeroMagnitude, -.pi], of: [Double].self, to: [ 132 | 0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 133 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 134 | 0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18, 135 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 136 | 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18]) 137 | } 138 | 139 | func testArrayOfOptionalsEncoding() throws { 140 | try compare([true, false, nil, true, nil, false], of: [Bool?].self, to: [ 141 | 2, 1, 142 | 2, 0, 143 | 1, 144 | 2, 1, 145 | 1, 146 | 2, 0 147 | ]) 148 | } 149 | 150 | func testArrayOfDoubleOptionalsEncoding() throws { 151 | try compare([.some(.some(true)), .some(.some(false)), .some(.none), .none], of: [Bool??].self, to: [ 152 | 4, 0, 1, 153 | 4, 0, 0, 154 | 2, 1, 155 | 1]) 156 | } 157 | 158 | func testArrayOfTripleOptionalsEncoding() throws { 159 | try compare([.some(.some(.some(true))), .some(.some(.some(false))), .some(.some(.none)), .some(.none), .none], of: [Bool???].self, to: [ 160 | 6, 0, 0, 1, 161 | 6, 0, 0, 0, 162 | 4, 0, 1, 163 | 2, 1, 164 | 1]) 165 | } 166 | 167 | func testArrayOfArraysEncoding() throws { 168 | let values: [[Bool]] = [[false], [true, false]] 169 | try compare(values, of: [[Bool]].self, to: [ 170 | 2, 0, 171 | 4, 1, 0 172 | ]) 173 | } 174 | 175 | func testDataEncoding() throws { 176 | let data = Data([1, 2, 3, 0, 255, 123]) 177 | let expected: [UInt8] = [1, 2, 3, 0, 255, 123] 178 | try compare(data, to: expected) 179 | try compare(Data(), to: []) 180 | } 181 | func testVeryLargePropertyPerformance() throws { 182 | struct Test: Codable { 183 | let values: [Float] 184 | 185 | enum CodingKeys: Int, CodingKey { 186 | case values = 1 187 | } 188 | 189 | func encode(to encoder: Encoder) throws { 190 | var container = encoder.container(keyedBy: CodingKeys.self) 191 | let data: Data = values.withUnsafeBufferPointer { Data(buffer: $0) } 192 | try container.encode(data, forKey: .values) 193 | } 194 | 195 | init(values: [Float]) { 196 | self.values = values 197 | } 198 | 199 | init(from decoder: Decoder) throws { 200 | let container = try decoder.container(keyedBy: CodingKeys.self) 201 | let data = try container.decode(Data.self, forKey: .values) 202 | self.values = data.withUnsafeBytes { Array($0.bindMemory(to: Float.self)) } 203 | } 204 | } 205 | 206 | let value = Test(values: .init(repeating: 3.14, count: 1000000)) 207 | 208 | self.measure { 209 | do { 210 | let data = try BinaryEncoder.encode(value) 211 | print(data.count) 212 | } catch { 213 | XCTFail(error.localizedDescription) 214 | } 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Tests/BinaryCodableTests/StructEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BinaryCodable 3 | 4 | final class StructEncodingTests: XCTestCase { 5 | 6 | func testStructWithArray() throws { 7 | struct Test: Codable, Equatable { 8 | let val: [Bool] 9 | } 10 | let expected: [UInt8] = [ 11 | 7, // String key, length 3 12 | 118, 97, 108, 13 | 6, // Length 3 14 | 1, // true 15 | 0, // false 16 | 1] // true 17 | try compare(Test(val: [true, false, true]), to: expected) 18 | } 19 | 20 | func testArrayOfStructs() throws { 21 | struct Test: Codable, Equatable { 22 | let val: Int 23 | } 24 | let value = [Test(val: 123), Test(val: 124)] 25 | let expected: [UInt8] = [ 26 | 14, // Length 7 27 | 7, // String key, length 3 28 | 118, 97, 108, // 'val' 29 | 4, // Length 2 30 | 246, 1, // Value '123' 31 | 14, // Length 7 32 | 7, // String key, length 3 33 | 118, 97, 108, // 'val' 34 | 4, // Length 2 35 | 248, 1, // Value '124' 36 | ] 37 | try compare(value, to: expected) 38 | } 39 | 40 | func testStructWithArrayOfOptionals() throws { 41 | struct Test: Codable, Equatable { 42 | let val: [Bool?] 43 | } 44 | let value = Test(val: [nil, true, nil, nil, false]) 45 | let expected: [UInt8] = [ 46 | 7, // String key, length 3 47 | 118, 97, 108, // 'val' 48 | 14, // Length 7 49 | 1, // Nil 50 | 2, 1, // True 51 | 1, // Nil 52 | 1, // Nil 53 | 2, 0 // False 54 | ] 55 | try compare(value, to: expected) 56 | } 57 | 58 | func testArrayOfOptionalStructs() throws { 59 | struct Test: Codable, Equatable { 60 | let val: Int 61 | } 62 | let value: [Test?] = [Test(val: 123), nil, Test(val: 124)] 63 | let expected: [UInt8] = [ 64 | 14, // Not nil, length 7 65 | 7, // String key, length 3 66 | 118, 97, 108, // 'val' 67 | 4, // Length 2 68 | 246, 1, // Value '123' 69 | 1, // Nil 70 | 14, // Not nil, length 7 71 | 7, // String key, length 3 72 | 118, 97, 108, // 'val' 73 | 4, // Length 2 74 | 248, 1, // Value '124' 75 | ] 76 | try compare(value, to: expected) 77 | } 78 | 79 | func testNegativeIntegerKeys() throws { 80 | struct Test: Codable, Equatable { 81 | let val: Bool 82 | 83 | enum CodingKeys: Int, CodingKey { 84 | case val = -1 85 | } 86 | } 87 | let encoder = BinaryEncoder() 88 | do { 89 | _ = try encoder.encode(Test(val: true)) 90 | } catch let error as EncodingError { 91 | guard case .invalidValue(let any, let context) = error else { 92 | XCTFail() 93 | return 94 | } 95 | XCTAssertEqual(context.codingPath, [-1]) 96 | guard let int = any as? Int else { 97 | XCTFail() 98 | return 99 | } 100 | XCTAssertEqual(int, -1) 101 | } 102 | } 103 | 104 | func testIntegerKeysValidLowerBound() throws { 105 | struct TestLowBound: Codable, Equatable { 106 | let val: Bool 107 | 108 | enum CodingKeys: Int, CodingKey { 109 | case val = 0 110 | } 111 | } 112 | let value = TestLowBound(val: true) 113 | let expected: [UInt8] = [ 114 | 0, // Int key 0 115 | 2, 1, /// Bool `true` 116 | ] 117 | try compare(value, to: expected) 118 | } 119 | 120 | func testIntegerKeysValidUpperBound() throws { 121 | struct TestUpperBound: Codable, Equatable { 122 | let val: Bool 123 | 124 | enum CodingKeys: Int, CodingKey { 125 | case val = 9223372036854775807 126 | } 127 | } 128 | let value = TestUpperBound(val: true) 129 | let expected: [UInt8] = [ 130 | 254, 255, 255, 255, 255, 255, 255, 255, 255, // Int key 9223372036854775807 131 | 2, 1, /// Bool `true` 132 | ] 133 | try compare(value, to: expected) 134 | } 135 | 136 | func testSortingStructKeys() throws { 137 | struct Test: Codable, Equatable { 138 | 139 | let one: Int 140 | 141 | let two: String 142 | 143 | let three: Bool 144 | 145 | enum CodingKeys: Int, CodingKey { 146 | case one = 1 147 | case two = 2 148 | case three = 3 149 | } 150 | } 151 | 152 | let val = Test(one: 123, two: "Some", three: true) 153 | try compare(val, to: [ 154 | 2, // Int key 1 155 | 4, // Length 2 156 | 246, 1, // Int(123) 157 | 4, // Int key 2 158 | 8, // Length 4 159 | 83, 111, 109, 101, // "Some" 160 | 6, // Int key 3 161 | 2, // Length 1 162 | 1, // 'true' 163 | ], sortingKeys: true) 164 | } 165 | 166 | func testDecodeDictionaryAsStruct() throws { 167 | struct Test: Codable, Equatable { 168 | let a: Int 169 | let b: Int 170 | let c: Int 171 | } 172 | 173 | let input: [String: Int] = ["a" : 123, "b": 0, "c": -123456] 174 | let encoded = try BinaryEncoder.encode(input) 175 | 176 | let decoded: Test = try BinaryDecoder.decode(from: encoded) 177 | XCTAssertEqual(decoded, Test(a: 123, b: 0, c: -123456)) 178 | } 179 | 180 | func testDecodeStructAsDictionary() throws { 181 | struct Test: Codable, Equatable { 182 | let a: Int 183 | let b: Int 184 | let c: Int 185 | } 186 | 187 | let input = Test(a: 123, b: 0, c: -123456) 188 | let encoded = try BinaryEncoder.encode(input) 189 | 190 | let decoded: [String: Int] = try BinaryDecoder.decode(from: encoded) 191 | XCTAssertEqual(decoded, ["a" : 123, "b": 0, "c": -123456]) 192 | } 193 | 194 | func testDecodeKeyedContainerInSingleValueContainer() throws { 195 | struct Wrapper: Codable, Equatable { 196 | let wrapped: Wrapped 197 | 198 | init(wrapped: Wrapped) { 199 | self.wrapped = wrapped 200 | } 201 | 202 | init(from decoder: Decoder) throws { 203 | let container = try decoder.singleValueContainer() 204 | self.wrapped = try container.decode(Wrapped.self) 205 | } 206 | 207 | func encode(to encoder: Encoder) throws { 208 | var container = encoder.singleValueContainer() 209 | try container.encode(wrapped) 210 | } 211 | } 212 | struct Wrapped: Codable, Equatable { 213 | let val: String 214 | } 215 | 216 | let expected: [UInt8] = [ 217 | 7, // String key, length 3 218 | 118, 97, 108, // "val" 219 | 8, // Length 4 220 | 83, 111, 109, 101, // "Some" 221 | ] 222 | 223 | let wrapped = Wrapped(val: "Some") 224 | let encodedWrapped = try BinaryEncoder.encode(wrapped) 225 | 226 | try compare(encodedWrapped, to: expected) 227 | 228 | let decodedWrapped: Wrapped = try BinaryDecoder.decode(from: encodedWrapped) 229 | XCTAssertEqual(decodedWrapped, wrapped) 230 | 231 | let wrapper = Wrapper(wrapped: wrapped) 232 | let encodedWrapper = try BinaryEncoder.encode(wrapper) 233 | 234 | // Prepend nil-indicator 235 | try compare(encodedWrapper, to: [0] + expected) 236 | 237 | let decodedWrapper: Wrapper = try BinaryDecoder.decode(from: encodedWrapper) 238 | XCTAssertEqual(decodedWrapper, wrapper) 239 | } 240 | } 241 | --------------------------------------------------------------------------------