diff --git a/BinaryFormat.md b/BinaryFormat.md index e320723..2e7df48 100644 --- a/BinaryFormat.md +++ b/BinaryFormat.md @@ -87,7 +87,7 @@ This is useful if the integer values are often large, e.g. for random numbers. ### Strings Swift `String` values are encoded using their `UTF-8` representations. -If a string can't be encoded this way, then encoding fails. +If a string can't be encoded this way, then encoding fails.s ## Containers @@ -148,6 +148,33 @@ which translates to: 0x00 // Fourth element is not nil, length 0 ``` +### Packed sequences + +Some of these basic types can be decoded from a continuous stream, either because they have a fixed length (like `Double`), or because their encoding can detect when the type ends (like variable-length encoded types). +Since these types don't require a length, basic sequences (`Array` and `Set`) of these types are encoded in a "packed" format, where no additional length indicator is added for each element. + +For example, encoding a series of `Bool` values in an unkeyed container would result in the following encoded data: + +``` +// True, false, false +02 01 02 00 02 00 +``` + +The `02` bytes indicate the length of each `Bool`, which is unnecessary, since a `Bool` is always exactly one byte. + +When encoding a type of `[Bool]`, the encoded data is shortened to: + +``` +// True, false, false +01 00 00 +``` + +This encoding is only used for the following types: + +- Fixed-width types: `Double`, `Float`, `Bool`, `Int8`, `UInt8`, `Int16`, `UInt16`, `FixedLengthEncoded` +- Zig-zag types: `Int32`, `Int64`, `Int` +- Variable-length types: `UInt32`, `UInt64`, `UInt`, `VariableLengthEncoded` + ### Keyed containers Keyed containers work similar to unkeyed containers, except that each element also has a key inserted before the element data. @@ -207,16 +234,15 @@ will give the following binary data: | 4 | 0x06 | Length 3 | 5-7 | 0x42 0x6f 0x62 | String "Bob" | 8 | 0x06 | CodingKey(stringValue: 'references', intValue: 3) -| 9 | 0x0A | Length 5 -| 10 | 0x02 | Length 1 -| 11 | 0x06 | Int `3` -| 12 | 0x04 | Length 2 -| 13-14 | 0xAF 0x04 | Int `-280` +| 9 | 0x0A | Length 3 +| 10 | 0x06 | Int `3` +| 11-12 | 0xAF 0x04 | Int `-280` There are a few things to note: - The properties are all marked by their integer keys - The elements in the `references` array are also preceded by a length indicator - The top level keyed container has no length information, since it can be inferred from the length of the provided data +- `[Int]` is a packed field, so no length data is inserted before each element ### Dictionaries diff --git a/Sources/BinaryCodable/Decoding/DecodingDataProvider.swift b/Sources/BinaryCodable/Decoding/DecodingDataProvider.swift index b05ed54..3cd1eb4 100644 --- a/Sources/BinaryCodable/Decoding/DecodingDataProvider.swift +++ b/Sources/BinaryCodable/Decoding/DecodingDataProvider.swift @@ -20,7 +20,7 @@ extension DecodingDataProvider { Decode an unsigned integer using variable-length encoding starting at a position. - Returns: `Nil`, if insufficient data is available */ - private func decodeUInt64(at index: inout Index) -> UInt64? { + func decodeUInt64(at index: inout Index) -> UInt64? { guard let start = nextByte(at: &index) else { return nil } return decodeUInt64(startByte: start, at: &index) } diff --git a/Sources/BinaryCodable/Primitives/Array+Coding.swift b/Sources/BinaryCodable/Primitives/Array+Coding.swift new file mode 100644 index 0000000..a6ea862 --- /dev/null +++ b/Sources/BinaryCodable/Primitives/Array+Coding.swift @@ -0,0 +1,21 @@ +import Foundation + +extension Array: EncodablePrimitive where Element: PackedEncodable { + + var encodedData: Data { + mapAndJoin { $0.encodedData } + } +} + +extension Array: DecodablePrimitive where Element: PackedDecodable { + + init(data: Data) throws { + var index = data.startIndex + var elements = [Element]() + while !data.isAtEnd(at: index) { + let element = try Element.init(data: data, index: &index) + elements.append(element) + } + self.init(elements) + } +} diff --git a/Sources/BinaryCodable/Primitives/Bool+Coding.swift b/Sources/BinaryCodable/Primitives/Bool+Coding.swift index 4d3063c..0369047 100644 --- a/Sources/BinaryCodable/Primitives/Bool+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Bool+Coding.swift @@ -10,6 +10,17 @@ extension Bool: EncodablePrimitive { extension Bool: DecodablePrimitive { + private init(byte: UInt8) throws { + switch byte { + case 0: + self = false + case 1: + self = true + default: + throw CorruptedDataError(invalidBoolByte: byte) + } + } + /** Decode a boolean from encoded data. - Parameter data: The data to decode @@ -19,14 +30,51 @@ extension Bool: DecodablePrimitive { guard data.count == 1 else { throw CorruptedDataError(invalidSize: data.count, for: "Bool") } - let byte = data[data.startIndex] - switch byte { - case 0: - self = false - case 1: - self = true - default: - throw CorruptedDataError(invalidBoolByte: byte) + try self.init(byte: data[data.startIndex]) + } +} + +// - MARK: Fixed size + +extension Bool: FixedSizeEncodable { + + public var fixedSizeEncoded: Data { + encodedData + } +} + +extension Bool: FixedSizeDecodable { + + public init(fromFixedSize data: Data) throws { + try self.init(data: data) + } +} + +extension FixedSizeEncoded where WrappedValue == Bool { + + /** + Wrap a Bool to enforce fixed-size encoding. + - Parameter wrappedValue: The value to wrap + - Note: `Bool` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. + */ + @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Bool") + public init(wrappedValue: Bool) { + self.wrappedValue = wrappedValue + } +} + +// - MARK: Packed + +extension Bool: PackedEncodable { + +} + +extension Bool: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { + throw CorruptedDataError.init(prematureEndofDataDecoding: "Bool") } + try self.init(fromFixedSize: bytes) } } diff --git a/Sources/BinaryCodable/Primitives/Double+Coding.swift b/Sources/BinaryCodable/Primitives/Double+Coding.swift index 220af33..e0b1b8c 100644 --- a/Sources/BinaryCodable/Primitives/Double+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Double+Coding.swift @@ -17,3 +17,48 @@ extension Double: DecodablePrimitive { self.init(bitPattern: value) } } + +// - MARK: Fixed size + +extension Double: FixedSizeEncodable { + + public var fixedSizeEncoded: Data { + encodedData + } +} + +extension Double: FixedSizeDecodable { + + public init(fromFixedSize data: Data) throws { + try self.init(data: data) + } +} + +extension FixedSizeEncoded where WrappedValue == Double { + + /** + Wrap a double to enforce fixed-size encoding. + - Parameter wrappedValue: The value to wrap + - Note: `Double` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. + */ + @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Double") + public init(wrappedValue: Double) { + self.wrappedValue = wrappedValue + } +} + +// - MARK: Packed + +extension Double: PackedEncodable { + +} + +extension Double: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { + throw CorruptedDataError.init(prematureEndofDataDecoding: "Double") + } + try self.init(data: bytes) + } +} diff --git a/Sources/BinaryCodable/Primitives/Float+Coding.swift b/Sources/BinaryCodable/Primitives/Float+Coding.swift index 1ee410d..38c092a 100644 --- a/Sources/BinaryCodable/Primitives/Float+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Float+Coding.swift @@ -18,3 +18,47 @@ extension Float: DecodablePrimitive { } } +// - MARK: Fixed size + +extension Float: FixedSizeEncodable { + + public var fixedSizeEncoded: Data { + encodedData + } +} + +extension Float: FixedSizeDecodable { + + public init(fromFixedSize data: Data) throws { + try self.init(data: data) + } +} + +extension FixedSizeEncoded where WrappedValue == Float { + + /** + Wrap a float to enforce fixed-size encoding. + - Parameter wrappedValue: The value to wrap + - Note: `Float` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. + */ + @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Float") + public init(wrappedValue: Float) { + self.wrappedValue = wrappedValue + } +} + +// - MARK: Packed + +extension Float: PackedEncodable { + +} + +extension Float: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { + throw CorruptedDataError.init(prematureEndofDataDecoding: "Float") + } + try self.init(data: bytes) + } +} diff --git a/Sources/BinaryCodable/Primitives/Int+Coding.swift b/Sources/BinaryCodable/Primitives/Int+Coding.swift index 9683e58..b6b6908 100644 --- a/Sources/BinaryCodable/Primitives/Int+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Int+Coding.swift @@ -14,7 +14,8 @@ extension Int: DecodablePrimitive { - Throws: ``CorruptedDataError`` */ init(data: Data) throws { - try self.init(fromZigZag: data) + let raw = try UInt64(fromVarintData: data) + try self.init(fromZigZag: raw) } } @@ -35,8 +36,8 @@ extension Int: ZigZagDecodable { - Parameter data: The data of the zig-zag encoded value. - Throws: ``CorruptedDataError`` */ - public init(fromZigZag data: Data) throws { - let raw = try Int64(data: data) + public init(fromZigZag raw: UInt64) throws { + let raw = Int64(fromZigZag: raw) guard let value = Int(exactly: raw) else { throw CorruptedDataError(outOfRange: raw, forType: "Int") } @@ -74,8 +75,8 @@ extension Int: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { - let intValue = try Int64(fromVarint: data) + public init(fromVarint raw: UInt64) throws { + let intValue = Int64(fromVarint: raw) guard let value = Int(exactly: intValue) else { throw CorruptedDataError(outOfRange: intValue, forType: "Int") } @@ -109,3 +110,18 @@ extension Int: FixedSizeDecodable { } } +// - MARK: Packed + +extension Int: PackedEncodable { + +} + +extension Int: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let raw = data.decodeUInt64(at: &index) else { + throw CorruptedDataError(prematureEndofDataDecoding: "Int") + } + try self.init(fromZigZag: raw) + } +} diff --git a/Sources/BinaryCodable/Primitives/Int16+Coding.swift b/Sources/BinaryCodable/Primitives/Int16+Coding.swift index d10e8d6..8aaa394 100644 --- a/Sources/BinaryCodable/Primitives/Int16+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Int16+Coding.swift @@ -72,8 +72,8 @@ extension Int16: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { - let value = try UInt16(fromVarint: data) + public init(fromVarint raw: UInt64) throws { + let value = try UInt16(fromVarint: raw) self = Int16(bitPattern: value) } } @@ -95,11 +95,27 @@ extension Int16: ZigZagDecodable { - Parameter data: The data of the zig-zag encoded value. - Throws: ``CorruptedDataError`` */ - public init(fromZigZag data: Data) throws { - let raw = try Int64(fromZigZag: data) + public init(fromZigZag raw: UInt64) throws { + let raw = Int64(fromZigZag: raw) guard let value = Int16(exactly: raw) else { throw CorruptedDataError(outOfRange: raw, forType: "Int16") } self = value } } + +// - MARK: Packed + +extension Int16: PackedEncodable { + +} + +extension Int16: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { + throw CorruptedDataError.init(prematureEndofDataDecoding: "Int16") + } + try self.init(data: bytes) + } +} diff --git a/Sources/BinaryCodable/Primitives/Int32+Coding.swift b/Sources/BinaryCodable/Primitives/Int32+Coding.swift index 8118127..9627c1e 100644 --- a/Sources/BinaryCodable/Primitives/Int32+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Int32+Coding.swift @@ -14,7 +14,8 @@ extension Int32: DecodablePrimitive { - Throws: ``CorruptedDataError`` */ init(data: Data) throws { - try self.init(fromZigZag: data) + let raw = try UInt64(fromVarintData: data) + try self.init(fromZigZag: raw) } } @@ -35,8 +36,8 @@ extension Int32: ZigZagDecodable { - Parameter data: The data of the zig-zag encoded value. - Throws: ``CorruptedDataError`` */ - public init(fromZigZag data: Data) throws { - let raw = try Int64(fromZigZag: data) + public init(fromZigZag raw: UInt64) throws { + let raw = Int64(fromZigZag: raw) guard let value = Int32(exactly: raw) else { throw CorruptedDataError(outOfRange: raw, forType: "Int32") } @@ -74,8 +75,8 @@ extension Int32: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { - let value = try UInt32(fromVarint: data) + public init(fromVarint raw: UInt64) throws { + let value = try UInt32(fromVarint: raw) self = Int32(bitPattern: value) } } @@ -106,3 +107,19 @@ extension Int32: FixedSizeDecodable { self.init(bitPattern: value) } } + +// - MARK: Packed + +extension Int32: PackedEncodable { + +} + +extension Int32: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let raw = data.decodeUInt64(at: &index) else { + throw CorruptedDataError(prematureEndofDataDecoding: "Int32") + } + try self.init(fromZigZag: raw) + } +} diff --git a/Sources/BinaryCodable/Primitives/Int64+Coding.swift b/Sources/BinaryCodable/Primitives/Int64+Coding.swift index 1100658..e62e26e 100644 --- a/Sources/BinaryCodable/Primitives/Int64+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Int64+Coding.swift @@ -14,7 +14,8 @@ extension Int64: DecodablePrimitive { - Throws: ``CorruptedDataError`` */ init(data: Data) throws { - try self.init(fromZigZag: data) + let raw = try UInt64(fromVarintData: data) + self.init(fromZigZag: raw) } } @@ -38,16 +39,14 @@ extension Int64: ZigZagDecodable { - Parameter data: The data of the zig-zag encoded value. - Throws: ``CorruptedDataError`` */ - public init(fromZigZag data: Data) throws { - let unsigned = try UInt64(fromVarint: data) - + public init(fromZigZag raw: UInt64) { // Check the last bit to get sign - if unsigned & 1 > 0 { + if raw & 1 > 0 { // Divide by 2 and subtract one to get absolute value of negative values. - self = -Int64(unsigned >> 1) - 1 + self = -Int64(raw >> 1) - 1 } else { // Divide by two to get absolute value of positive values - self = Int64(unsigned >> 1) + self = Int64(raw >> 1) } } } @@ -83,9 +82,8 @@ extension Int64: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { - let value = try UInt64(fromVarint: data) - self = Int64(bitPattern: value) + public init(fromVarint raw: UInt64) { + self = Int64(bitPattern: raw) } } @@ -115,3 +113,19 @@ extension Int64: FixedSizeDecodable { self.init(bitPattern: value) } } + +// - MARK: Packed + +extension Int64: PackedEncodable { + +} + +extension Int64: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let raw = data.decodeUInt64(at: &index) else { + throw CorruptedDataError(prematureEndofDataDecoding: "Int64") + } + self.init(fromZigZag: raw) + } +} diff --git a/Sources/BinaryCodable/Primitives/Int8+Coding.swift b/Sources/BinaryCodable/Primitives/Int8+Coding.swift index 5860821..a4019d6 100644 --- a/Sources/BinaryCodable/Primitives/Int8+Coding.swift +++ b/Sources/BinaryCodable/Primitives/Int8+Coding.swift @@ -16,3 +16,48 @@ extension Int8: DecodablePrimitive { self.init(bitPattern: data[data.startIndex]) } } + +// - MARK: Fixed size + +extension Int8: FixedSizeEncodable { + + public var fixedSizeEncoded: Data { + encodedData + } +} + +extension Int8: FixedSizeDecodable { + + public init(fromFixedSize data: Data) throws { + try self.init(data: data) + } +} + +extension FixedSizeEncoded where WrappedValue == Int8 { + + /** + Wrap a Int8 to enforce fixed-size encoding. + - Parameter wrappedValue: The value to wrap + - Note: `Int8` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. + */ + @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type Int8") + public init(wrappedValue: Int8) { + self.wrappedValue = wrappedValue + } +} + +// - MARK: Packed + +extension Int8: PackedEncodable { + +} + +extension Int8: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { + throw CorruptedDataError.init(prematureEndofDataDecoding: "Int8") + } + try self.init(fromFixedSize: bytes) + } +} diff --git a/Sources/BinaryCodable/Primitives/Set+Coding.swift b/Sources/BinaryCodable/Primitives/Set+Coding.swift new file mode 100644 index 0000000..6a68e9f --- /dev/null +++ b/Sources/BinaryCodable/Primitives/Set+Coding.swift @@ -0,0 +1,21 @@ +import Foundation + +extension Set: EncodablePrimitive where Element: PackedEncodable { + + var encodedData: Data { + mapAndJoin { $0.encodedData } + } +} + +extension Set: DecodablePrimitive where Element: PackedDecodable { + + init(data: Data) throws { + var index = data.startIndex + var elements = [Element]() + while !data.isAtEnd(at: index) { + let element = try Element.init(data: data, index: &index) + elements.append(element) + } + self.init(elements) + } +} diff --git a/Sources/BinaryCodable/Primitives/UInt+Coding.swift b/Sources/BinaryCodable/Primitives/UInt+Coding.swift index 38426d9..04a4287 100644 --- a/Sources/BinaryCodable/Primitives/UInt+Coding.swift +++ b/Sources/BinaryCodable/Primitives/UInt+Coding.swift @@ -13,7 +13,8 @@ extension UInt: DecodablePrimitive { - Throws: ``CorruptedDataError`` */ init(data: Data) throws { - try self.init(fromVarint: data) + let raw = try UInt64(fromVarintData: data) + try self.init(fromVarint: raw) } } @@ -33,8 +34,7 @@ extension UInt: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { - let raw = try UInt64(fromVarint: data) + public init(fromVarint raw: UInt64) throws { guard let value = UInt(exactly: raw) else { throw CorruptedDataError(outOfRange: raw, forType: "UInt") } @@ -81,3 +81,18 @@ extension UInt: FixedSizeDecodable { } } +// - MARK: Packed + +extension UInt: PackedEncodable { + +} + +extension UInt: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let raw = data.decodeUInt64(at: &index) else { + throw CorruptedDataError(prematureEndofDataDecoding: "UInt") + } + try self.init(fromVarint: raw) + } +} diff --git a/Sources/BinaryCodable/Primitives/UInt16+Coding.swift b/Sources/BinaryCodable/Primitives/UInt16+Coding.swift index 11d5953..4e8c6ce 100644 --- a/Sources/BinaryCodable/Primitives/UInt16+Coding.swift +++ b/Sources/BinaryCodable/Primitives/UInt16+Coding.swift @@ -34,8 +34,8 @@ extension UInt16: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { - let raw = try UInt64(fromVarint: data) + public init(fromVarint raw: UInt64) throws { + let raw = UInt64(fromVarint: raw) guard let value = UInt16(exactly: raw) else { throw CorruptedDataError(outOfRange: raw, forType: "UInt16") } @@ -81,3 +81,18 @@ extension FixedSizeEncoded where WrappedValue == UInt16 { } } +// - MARK: Packed + +extension UInt16: PackedEncodable { + +} + +extension UInt16: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { + throw CorruptedDataError.init(prematureEndofDataDecoding: "UInt16") + } + try self.init(fromFixedSize: bytes) + } +} diff --git a/Sources/BinaryCodable/Primitives/UInt32+Coding.swift b/Sources/BinaryCodable/Primitives/UInt32+Coding.swift index 701e58b..724c109 100644 --- a/Sources/BinaryCodable/Primitives/UInt32+Coding.swift +++ b/Sources/BinaryCodable/Primitives/UInt32+Coding.swift @@ -14,7 +14,8 @@ extension UInt32: DecodablePrimitive { - Throws: ``CorruptedDataError`` */ init(data: Data) throws { - try self.init(fromVarint: data) + let raw = try UInt64(fromVarintData: data) + try self.init(fromVarint: raw) } } @@ -36,8 +37,7 @@ extension UInt32: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { - let raw = try UInt64(fromVarint: data) + public init(fromVarint raw: UInt64) throws { guard let value = UInt32(exactly: raw) else { throw CorruptedDataError(outOfRange: raw, forType: "UInt32") } @@ -82,3 +82,19 @@ extension UInt32: FixedSizeDecodable { self.init(littleEndian: data.interpreted()) } } + +// - MARK: Packed + +extension UInt32: PackedEncodable { + +} + +extension UInt32: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let raw = data.decodeUInt64(at: &index) else { + throw CorruptedDataError(prematureEndofDataDecoding: "UInt32") + } + try self.init(fromVarint: raw) + } +} diff --git a/Sources/BinaryCodable/Primitives/UInt64+Coding.swift b/Sources/BinaryCodable/Primitives/UInt64+Coding.swift index deca608..95a5f62 100644 --- a/Sources/BinaryCodable/Primitives/UInt64+Coding.swift +++ b/Sources/BinaryCodable/Primitives/UInt64+Coding.swift @@ -14,7 +14,7 @@ extension UInt64: DecodablePrimitive { - Throws: ``CorruptedDataError`` */ init(data: Data) throws { - try self.init(fromVarint: data) + try self.init(fromVarintData: data) } } @@ -53,7 +53,11 @@ extension UInt64: VariableLengthDecodable { - Parameter data: The data to decode. - Throws: ``CorruptedDataError`` */ - public init(fromVarint data: Data) throws { + public init(fromVarint raw: UInt64) { + self = raw + } + + init(fromVarintData data: Data) throws { var currentIndex = data.startIndex func nextByte() throws -> UInt64 { @@ -136,3 +140,19 @@ extension UInt64: FixedSizeDecodable { self.init(littleEndian: data.interpreted()) } } + +// - MARK: Packed + +extension UInt64: PackedEncodable { + +} + +extension UInt64: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let raw = data.decodeUInt64(at: &index) else { + throw CorruptedDataError(prematureEndofDataDecoding: "UInt64") + } + self = raw + } +} diff --git a/Sources/BinaryCodable/Primitives/UInt8+Coding.swift b/Sources/BinaryCodable/Primitives/UInt8+Coding.swift index 8d76d2f..6f0762b 100644 --- a/Sources/BinaryCodable/Primitives/UInt8+Coding.swift +++ b/Sources/BinaryCodable/Primitives/UInt8+Coding.swift @@ -16,3 +16,48 @@ extension UInt8: DecodablePrimitive { self = data[data.startIndex] } } + +// - MARK: Fixed size + +extension UInt8: FixedSizeEncodable { + + public var fixedSizeEncoded: Data { + encodedData + } +} + +extension UInt8: FixedSizeDecodable { + + public init(fromFixedSize data: Data) throws { + try self.init(data: data) + } +} + +extension FixedSizeEncoded where WrappedValue == UInt8 { + + /** + Wrap a UInt8 to enforce fixed-size encoding. + - Parameter wrappedValue: The value to wrap + - Note: `UInt8` is already encoded using fixed-size encoding, so wrapping it in `FixedSizeEncoded` does nothing. + */ + @available(*, deprecated, message: "Property wrapper @FixedSizeEncoded has no effect on type UInt8") + public init(wrappedValue: UInt8) { + self.wrappedValue = wrappedValue + } +} + +// - MARK: Packed + +extension UInt8: PackedEncodable { + +} + +extension UInt8: PackedDecodable { + + init(data: Data, index: inout Int) throws { + guard let bytes = data.nextBytes(Self.fixedEncodedByteCount, at: &index) else { + throw CorruptedDataError.init(prematureEndofDataDecoding: "UInt8") + } + try self.init(fromFixedSize: bytes) + } +} diff --git a/Sources/BinaryCodable/Protocols/PackedCodable.swift b/Sources/BinaryCodable/Protocols/PackedCodable.swift new file mode 100644 index 0000000..00360d2 --- /dev/null +++ b/Sources/BinaryCodable/Protocols/PackedCodable.swift @@ -0,0 +1,4 @@ +import Foundation + +/// A type that can be encoded and decoded without length information +typealias PackedCodable = PackedEncodable & PackedDecodable diff --git a/Sources/BinaryCodable/Protocols/PackedDecodable.swift b/Sources/BinaryCodable/Protocols/PackedDecodable.swift new file mode 100644 index 0000000..c170b3d --- /dev/null +++ b/Sources/BinaryCodable/Protocols/PackedDecodable.swift @@ -0,0 +1,16 @@ +import Foundation + +/** + A protocol for types which can be decoded from a continous stream of data + */ +protocol PackedDecodable { + + /** + Decode a value from a data stream at a given index. + + This function is expected to advance the buffer index appropriately. + */ + init(data: Data, index: inout Int) throws + +} + diff --git a/Sources/BinaryCodable/Protocols/PackedEncodable.swift b/Sources/BinaryCodable/Protocols/PackedEncodable.swift new file mode 100644 index 0000000..24b67bf --- /dev/null +++ b/Sources/BinaryCodable/Protocols/PackedEncodable.swift @@ -0,0 +1,8 @@ +import Foundation + +/** + A protocol for types which can be encoded without type information + */ +protocol PackedEncodable: EncodablePrimitive { + +} diff --git a/Sources/BinaryCodable/Wrappers/FixedSizeCodable.swift b/Sources/BinaryCodable/Wrappers/FixedSizeCodable.swift index 82a554d..545b60d 100644 --- a/Sources/BinaryCodable/Wrappers/FixedSizeCodable.swift +++ b/Sources/BinaryCodable/Wrappers/FixedSizeCodable.swift @@ -24,3 +24,9 @@ public protocol FixedSizeDecodable: Decodable { */ init(fromFixedSize data: Data) throws } + +extension FixedSizeDecodable { + + /// The number of bytes needed for a fixed-size encoding + static var fixedEncodedByteCount: Int { MemoryLayout.size } +} diff --git a/Sources/BinaryCodable/Wrappers/FixedSizeEncoded.swift b/Sources/BinaryCodable/Wrappers/FixedSizeEncoded.swift index 38ed44c..cd756d9 100644 --- a/Sources/BinaryCodable/Wrappers/FixedSizeEncoded.swift +++ b/Sources/BinaryCodable/Wrappers/FixedSizeEncoded.swift @@ -19,7 +19,7 @@ The `FixedSizeEncoded` property wrapper is supported for `UInt`, `UInt32`, `UInt - SeeAlso: [Language Guide (proto3): Scalar value types](https://developers.google.com/protocol-buffers/docs/proto3#scalar) */ @propertyWrapper -public struct FixedSizeEncoded where WrappedValue: FixedSizeCodable, WrappedValue: FixedWidthInteger { +public struct FixedSizeEncoded where WrappedValue: FixedSizeCodable { /// The value wrapped in the fixed-size container public var wrappedValue: WrappedValue @@ -33,18 +33,89 @@ public struct FixedSizeEncoded where WrappedValue: FixedSizeCodabl } } -extension FixedSizeEncoded: Numeric { +// MARK: - Fixed size + +extension FixedSizeEncoded: EncodablePrimitive where WrappedValue: EncodablePrimitive { + + /** + Encode the wrapped value to binary data compatible with the protobuf encoding. + - Returns: The binary data in host-independent format. + */ + var encodedData: Data { + wrappedValue.fixedSizeEncoded + } +} + +extension FixedSizeEncoded: DecodablePrimitive where WrappedValue: DecodablePrimitive { + + init(data: Data) throws { + let wrappedValue = try WrappedValue(fromFixedSize: data) + self.init(wrappedValue: wrappedValue) + } +} + +// MARK: - Codable + +extension FixedSizeEncoded: Encodable { + + /** + Encode the wrapped value transparently to the given encoder. + - Parameter encoder: The encoder to use for encoding. + - Throws: Errors from the decoder when attempting to encode a value in a single value container. + */ + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } +} + +extension FixedSizeEncoded: Decodable { + /** + Decode a wrapped value from a decoder. + - Parameter decoder: The decoder to use for decoding. + - Throws: Errors from the decoder when reading a single value container or decoding the wrapped value from it. + */ + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + wrappedValue = try container.decode(WrappedValue.self) + } +} + +extension FixedSizeEncoded: Equatable where WrappedValue: Equatable { + +} + +extension FixedSizeEncoded: Hashable where WrappedValue: Hashable { + +} + +extension FixedSizeEncoded: Comparable where WrappedValue: Comparable { + + public static func < (lhs: FixedSizeEncoded, rhs: FixedSizeEncoded) -> Bool { + lhs.wrappedValue < rhs.wrappedValue + } +} + +extension FixedSizeEncoded: CustomStringConvertible where WrappedValue: CustomStringConvertible { + + public var description: String { + wrappedValue.description + } +} + +extension FixedSizeEncoded: Numeric where WrappedValue: Numeric { + public init?(exactly source: T) where T : BinaryInteger { guard let wrapped = WrappedValue(exactly: source) else { return nil } self.init(wrappedValue: wrapped) } - + public var magnitude: WrappedValue.Magnitude { wrappedValue.magnitude } - + public static func * (lhs: FixedSizeEncoded, rhs: FixedSizeEncoded) -> FixedSizeEncoded { .init(wrappedValue: lhs.wrappedValue * rhs.wrappedValue) } @@ -54,7 +125,7 @@ extension FixedSizeEncoded: Numeric { } } -extension FixedSizeEncoded: AdditiveArithmetic { +extension FixedSizeEncoded: AdditiveArithmetic where WrappedValue: AdditiveArithmetic { /** The zero value. @@ -74,32 +145,81 @@ extension FixedSizeEncoded: AdditiveArithmetic { } } -extension FixedSizeEncoded: BinaryInteger { +extension FixedSizeEncoded: Strideable where WrappedValue: Strideable { + + + public typealias Stride = WrappedValue.Stride + + public func distance(to other: FixedSizeEncoded) -> WrappedValue.Stride { + wrappedValue.distance(to: other.wrappedValue) + } + + public func advanced(by n: WrappedValue.Stride) -> FixedSizeEncoded { + .init(wrappedValue: wrappedValue.advanced(by: n)) + } +} + +extension FixedSizeEncoded: BinaryInteger where WrappedValue: BinaryInteger { + + public static func <<= (lhs: inout FixedSizeEncoded, rhs: RHS) where RHS : BinaryInteger { + lhs.wrappedValue <<= rhs + } + + public static func >>= (lhs: inout FixedSizeEncoded, rhs: RHS) where RHS : BinaryInteger { + lhs.wrappedValue >>= rhs + } + + public static prefix func ~ (x: FixedSizeEncoded) -> FixedSizeEncoded { + .init(wrappedValue: ~x.wrappedValue) + } + + public var bitWidth: Int { + wrappedValue.bitWidth + } + + public init(clamping source: T) where T : BinaryInteger { + self.wrappedValue = .init(clamping: source) + } + + public init(truncatingIfNeeded source: T) where T : BinaryInteger { + self.wrappedValue = .init(truncatingIfNeeded: source) + } + + public init(_ source: T) where T : BinaryFloatingPoint { + self.wrappedValue = .init(source) + } + + public init?(exactly source: T) where T : BinaryFloatingPoint { + guard let wrappedValue = WrappedValue(exactly: source) else { + return nil + } + self.wrappedValue = wrappedValue + } public init(_ source: T) where T : BinaryInteger { self.init(wrappedValue: .init(source)) } - + public static var isSigned: Bool { WrappedValue.isSigned } - + public var words: WrappedValue.Words { wrappedValue.words } - + public var trailingZeroBitCount: Int { wrappedValue.trailingZeroBitCount } - + public static func / (lhs: FixedSizeEncoded, rhs: FixedSizeEncoded) -> FixedSizeEncoded { .init(wrappedValue: lhs.wrappedValue / rhs.wrappedValue) } - + public static func % (lhs: FixedSizeEncoded, rhs: FixedSizeEncoded) -> FixedSizeEncoded { .init(wrappedValue: lhs.wrappedValue % rhs.wrappedValue) } - + public static func /= (lhs: inout FixedSizeEncoded, rhs: FixedSizeEncoded) { lhs.wrappedValue /= rhs.wrappedValue } @@ -107,21 +227,31 @@ extension FixedSizeEncoded: BinaryInteger { public static func %= (lhs: inout FixedSizeEncoded, rhs: FixedSizeEncoded) { lhs.wrappedValue %= rhs.wrappedValue } - + public static func &= (lhs: inout FixedSizeEncoded, rhs: FixedSizeEncoded) { lhs.wrappedValue &= rhs.wrappedValue } - + public static func |= (lhs: inout FixedSizeEncoded, rhs: FixedSizeEncoded) { lhs.wrappedValue |= rhs.wrappedValue } - + public static func ^= (lhs: inout FixedSizeEncoded, rhs: FixedSizeEncoded) { lhs.wrappedValue ^= rhs.wrappedValue } } -extension FixedSizeEncoded: FixedWidthInteger { +extension FixedSizeEncoded: LosslessStringConvertible where WrappedValue: LosslessStringConvertible { + + public init?(_ description: String) { + guard let wrappedValue = WrappedValue(description) else { + return nil + } + self.wrappedValue = wrappedValue + } +} + +extension FixedSizeEncoded: FixedWidthInteger where WrappedValue: FixedWidthInteger { public typealias Words = WrappedValue.Words @@ -168,15 +298,15 @@ extension FixedSizeEncoded: FixedWidthInteger { public var nonzeroBitCount: Int { wrappedValue.nonzeroBitCount } - + public var leadingZeroBitCount: Int { wrappedValue.leadingZeroBitCount } - + public var byteSwapped: FixedSizeEncoded { .init(wrappedValue: wrappedValue.byteSwapped) } - + /// The maximum representable integer in this type. /// /// For unsigned integer types, this value is `(2 ** bitWidth) - 1`, where @@ -196,7 +326,7 @@ extension FixedSizeEncoded: FixedWidthInteger { } } -extension FixedSizeEncoded: ExpressibleByIntegerLiteral { +extension FixedSizeEncoded: ExpressibleByIntegerLiteral where WrappedValue: ExpressibleByIntegerLiteral { public typealias IntegerLiteralType = WrappedValue.IntegerLiteralType @@ -204,63 +334,3 @@ extension FixedSizeEncoded: ExpressibleByIntegerLiteral { self.wrappedValue = WrappedValue.init(integerLiteral: value) } } - -extension FixedSizeEncoded: Equatable { - - public static func == (lhs: FixedSizeEncoded, rhs: FixedSizeEncoded) -> Bool { - lhs.wrappedValue == rhs.wrappedValue - } -} - -extension FixedSizeEncoded: Comparable { - - public static func < (lhs: FixedSizeEncoded, rhs: FixedSizeEncoded) -> Bool { - lhs.wrappedValue < rhs.wrappedValue - } -} - -extension FixedSizeEncoded: Hashable { } - -extension FixedSizeEncoded: EncodablePrimitive where WrappedValue: EncodablePrimitive { - - /** - Encode the wrapped value to binary data compatible with the protobuf encoding. - - Returns: The binary data in host-independent format. - */ - var encodedData: Data { - wrappedValue.fixedSizeEncoded - } -} - -extension FixedSizeEncoded: DecodablePrimitive where WrappedValue: DecodablePrimitive { - - init(data: Data) throws { - let wrappedValue = try WrappedValue(fromFixedSize: data) - self.init(wrappedValue: wrappedValue) - } -} - -extension FixedSizeEncoded: Encodable { - - /** - Encode the wrapped value transparently to the given encoder. - - Parameter encoder: The encoder to use for encoding. - - Throws: Errors from the decoder when attempting to encode a value in a single value container. - */ - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(wrappedValue) - } -} - -extension FixedSizeEncoded: Decodable { - /** - Decode a wrapped value from a decoder. - - Parameter decoder: The decoder to use for decoding. - - Throws: Errors from the decoder when reading a single value container or decoding the wrapped value from it. - */ - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - wrappedValue = try container.decode(WrappedValue.self) - } -} diff --git a/Sources/BinaryCodable/Wrappers/Packed.swift b/Sources/BinaryCodable/Wrappers/Packed.swift new file mode 100644 index 0000000..b931d6d --- /dev/null +++ b/Sources/BinaryCodable/Wrappers/Packed.swift @@ -0,0 +1,75 @@ +import Foundation + +@propertyWrapper +public struct Packed where WrappedValue: RangeReplaceableCollection { + + /// The sequence wrapped in the packed container + public var wrappedValue: WrappedValue + + /** + Wrap an integer value in a fixed-size container + - Parameter wrappedValue: The sequence to wrap + */ + public init(wrappedValue: WrappedValue) { + self.wrappedValue = wrappedValue + } +} + +extension Packed: EncodablePrimitive where WrappedValue.Element: PackedEncodable { + + /** + Encode the wrapped value to binary data compatible with the protobuf encoding. + - Returns: The binary data in host-independent format. + */ + var encodedData: Data { + wrappedValue.mapAndJoin { + let data = $0.encodedData + return data.count.variableLengthEncoding + data + } + } +} + +extension Packed: DecodablePrimitive where WrappedValue.Element: PackedDecodable { + + init(data: Data) throws { + var index = data.startIndex + var elements = [WrappedValue.Element]() + while !data.isAtEnd(at: index) { + let element = try WrappedValue.Element.init(data: data, index: &index) + elements.append(element) + } + self.wrappedValue = WrappedValue.init(elements) + } +} + +extension Packed: Encodable where WrappedValue.Element: Encodable { + + /** + Encode the wrapped value transparently to the given encoder. + - Parameter encoder: The encoder to use for encoding. + - Throws: Errors from the decoder when attempting to encode a value in a single value container. + */ + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + for element in wrappedValue { + try container.encode(element) + } + } +} + +extension Packed: Decodable where WrappedValue.Element: Decodable { + /** + Decode a wrapped value from a decoder. + - Parameter decoder: The decoder to use for decoding. + - Throws: Errors from the decoder when reading a single value container or decoding the wrapped value from it. + */ + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var elements = WrappedValue() + while !container.isAtEnd { + let next = try container.decode(WrappedValue.Element.self) + elements.append(next) + } + self.wrappedValue = elements + } +} diff --git a/Sources/BinaryCodable/Wrappers/VariableLengthCodable.swift b/Sources/BinaryCodable/Wrappers/VariableLengthCodable.swift index f112d37..db5c97b 100644 --- a/Sources/BinaryCodable/Wrappers/VariableLengthCodable.swift +++ b/Sources/BinaryCodable/Wrappers/VariableLengthCodable.swift @@ -24,5 +24,5 @@ public protocol VariableLengthDecodable: FixedWidthInteger, Decodable { - Parameter data: The encoded value - Throws: ``CorruptedDataError`` */ - init(fromVarint data: Data) throws + init(fromVarint raw: UInt64) throws } diff --git a/Sources/BinaryCodable/Wrappers/VariableLengthEncoded.swift b/Sources/BinaryCodable/Wrappers/VariableLengthEncoded.swift index fac1953..7cda686 100644 --- a/Sources/BinaryCodable/Wrappers/VariableLengthEncoded.swift +++ b/Sources/BinaryCodable/Wrappers/VariableLengthEncoded.swift @@ -243,7 +243,8 @@ extension VariableLengthEncoded: EncodablePrimitive where WrappedValue: Encodabl extension VariableLengthEncoded: DecodablePrimitive where WrappedValue: DecodablePrimitive { init(data: Data) throws { - let wrappedValue = try WrappedValue(fromVarint: data) + let raw = try UInt64(fromVarintData: data) + let wrappedValue = try WrappedValue(fromVarint: raw) self.init(wrappedValue: wrappedValue) } } diff --git a/Sources/BinaryCodable/Wrappers/ZigZagCodable.swift b/Sources/BinaryCodable/Wrappers/ZigZagCodable.swift index 987a649..5c52c3c 100644 --- a/Sources/BinaryCodable/Wrappers/ZigZagCodable.swift +++ b/Sources/BinaryCodable/Wrappers/ZigZagCodable.swift @@ -29,6 +29,6 @@ public protocol ZigZagDecodable: Decodable { - Parameter data: The encoded value - Throws: ``CorruptedDataError`` */ - init(fromZigZag data: Data) throws + init(fromZigZag raw: UInt64) throws } diff --git a/Sources/BinaryCodable/Wrappers/ZigZagEncoded.swift b/Sources/BinaryCodable/Wrappers/ZigZagEncoded.swift index 6e3a3cb..476d228 100644 --- a/Sources/BinaryCodable/Wrappers/ZigZagEncoded.swift +++ b/Sources/BinaryCodable/Wrappers/ZigZagEncoded.swift @@ -243,7 +243,8 @@ extension ZigZagEncoded: EncodablePrimitive where WrappedValue: EncodablePrimiti extension ZigZagEncoded: DecodablePrimitive where WrappedValue: DecodablePrimitive { init(data: Data) throws { - let wrappedValue = try WrappedValue(fromZigZag: data) + let raw = try UInt64(fromVarintData: data) + let wrappedValue = try WrappedValue(fromZigZag: raw) self.init(wrappedValue: wrappedValue) } } diff --git a/Tests/BinaryCodableTests/ArrayEncodingTests.swift b/Tests/BinaryCodableTests/ArrayEncodingTests.swift index 03ed4cf..3ef795f 100644 --- a/Tests/BinaryCodableTests/ArrayEncodingTests.swift +++ b/Tests/BinaryCodableTests/ArrayEncodingTests.swift @@ -5,114 +5,108 @@ final class ArrayEncodingTests: XCTestCase { func testBoolArrayEncoding() throws { try compare([true, false, false], to: [ - 2, 1, - 2, 0, - 2, 0 + 1, 0, 0 ]) } func testInt8ArrayEncoding() throws { try compare([.zero, 123, .min, .max, -1], of: [Int8].self, to: [ - 2, 0, - 2, 123, - 2, 128, - 2, 127, - 2, 255 + 0, 123, 128, 127, 255 ]) } func testInt16ArrayEncoding() throws { try compare([.zero, 123, .min, .max, -1], of: [Int16].self, to: [ - 4, 0, 0, - 4, 123, 0, - 4, 0, 128, - 4, 255, 127, - 4, 255, 255 + 0, 0, + 123, 0, + 0, 128, + 255, 127, + 255, 255 ]) } func testInt32ArrayEncoding() throws { try compare([.zero, 123, .min, .max, -1], of: [Int32].self, to: [ - 2, 0, // 0 - 4, 246, 1, // 123 - 10, 255, 255, 255, 255, 15, // -2.147.483.648 - 10, 254, 255, 255, 255, 15, // 2.147.483.647 - 2, 1]) // -1 + 0, // 0 + 246, 1, // 123 + 255, 255, 255, 255, 15, // -2.147.483.648 + 254, 255, 255, 255, 15, // 2.147.483.647 + 1]) // -1 } func testInt64ArrayEncoding() throws { try compare([0, 123, .max, .min, -1], of: [Int64].self, to: [ - 2, 0, // 0 - 4, 246, 1, // 123 - 18, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // max - 18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // min - 2, 1]) // 1 + 0, // 0 + 246, 1, // 123 + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // max + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // min + 1]) // 1 } func testIntArrayEncoding() throws { try compare([0, 123, .max, .min, -1], of: [Int].self, to: [ - 2, 0, // 0 - 4, 246, 1, // 123 - 18, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // max - 18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // min - 2, 1]) // 1 + 0, // 0 + 246, 1, // 123 + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // max + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // min + 1]) // 1 } func testUInt8ArrayEncoding() throws { try compare([.zero, 123, .min, .max], of: [UInt8].self, to: [ - 2, 0, - 2, 123, - 2, 0, - 2, 255 + 0, + 123, + 0, + 255 ]) } func testUInt16ArrayEncoding() throws { try compare([.zero, 123, .min, .max, 12345], of: [UInt16].self, to: [ - 4, 0, 0, - 4, 123, 0, - 4, 0, 0, - 4, 255, 255, - 4, 0x39, 0x30 + 0, 0, + 123, 0, + 0, 0, + 255, 255, + 0x39, 0x30 ]) } func testUInt32ArrayEncoding() throws { try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, .max], of: [UInt32].self, to: [ - 2, 0, - 2, 123, - 2, 0, - 4, 0xB9, 0x60, - 6, 0xC0, 0xC4, 0x07, - 8, 0xCE, 0xC2, 0xF1, 0x05, - 10, 0xD2, 0x85, 0xD8, 0xCC, 0x04, - 10, 255, 255, 255, 255, 15]) + 0, + 123, + 0, + 0xB9, 0x60, + 0xC0, 0xC4, 0x07, + 0xCE, 0xC2, 0xF1, 0x05, + 0xD2, 0x85, 0xD8, 0xCC, 0x04, + 255, 255, 255, 255, 15]) } func testUInt64ArrayEncoding() throws { try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: [UInt64].self, to: [ - 2, 0, - 2, 123, - 2, 0, - 4, 0xB9, 0x60, - 6, 0xC0, 0xC4, 0x07, - 8, 0xCE, 0xC2, 0xF1, 0x05, - 10, 0xD2, 0x85, 0xD8, 0xCC, 0x04, - 14, 0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02, - 18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + 0, + 123, + 0, + 0xB9, 0x60, + 0xC0, 0xC4, 0x07, + 0xCE, 0xC2, 0xF1, 0x05, + 0xD2, 0x85, 0xD8, 0xCC, 0x04, + 0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) } func testUIntArrayEncoding() throws { try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: [UInt].self, to: [ - 2, 0, - 2, 123, - 2, 0, - 4, 0xB9, 0x60, - 6, 0xC0, 0xC4, 0x07, - 8, 0xCE, 0xC2, 0xF1, 0x05, - 10, 0xD2, 0x85, 0xD8, 0xCC, 0x04, - 14, 0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02, - 18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + 0, + 123, + 0, + 0xB9, 0x60, + 0xC0, 0xC4, 0x07, + 0xCE, 0xC2, 0xF1, 0x05, + 0xD2, 0x85, 0xD8, 0xCC, 0x04, + 0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) } func testStringArrayEncoding() throws { @@ -126,20 +120,20 @@ final class ArrayEncodingTests: XCTestCase { func testFloatArrayEncoding() throws { try compare([.greatestFiniteMagnitude, .zero, .pi, -.pi, .leastNonzeroMagnitude], of: [Float].self, to: [ - 8, 0x7F, 0x7F, 0xFF, 0xFF, - 8, 0x00, 0x00, 0x00, 0x00, - 8, 0x40, 0x49, 0x0F, 0xDA, - 8, 0xC0, 0x49, 0x0F, 0xDA, - 8, 0x00, 0x00, 0x00, 0x01]) + 0x7F, 0x7F, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x49, 0x0F, 0xDA, + 0xC0, 0x49, 0x0F, 0xDA, + 0x00, 0x00, 0x00, 0x01]) } func testDoubleArrayEncoding() throws { try compare([.greatestFiniteMagnitude, .zero, .pi, .leastNonzeroMagnitude, -.pi], of: [Double].self, to: [ - 16, 0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 16, 0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18, - 16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 16, 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18]) + 0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18]) } func testArrayOfOptionalsEncoding() throws { @@ -173,8 +167,8 @@ final class ArrayEncodingTests: XCTestCase { func testArrayOfArraysEncoding() throws { let values: [[Bool]] = [[false], [true, false]] try compare(values, of: [[Bool]].self, to: [ - 4, 2, 0, - 8, 2, 1, 2, 0 + 2, 0, + 4, 1, 0 ]) } diff --git a/Tests/BinaryCodableTests/BoolTests.swift b/Tests/BinaryCodableTests/BoolTests.swift index c093478..4d3d4f9 100644 --- a/Tests/BinaryCodableTests/BoolTests.swift +++ b/Tests/BinaryCodableTests/BoolTests.swift @@ -15,10 +15,10 @@ final class BoolTests: XCTestCase { } func testArrayBool() throws { - try compare([false], to: [2, 0]) - try compare([true], to: [2, 1]) - try compare([true, false], to: [2, 1, 2, 0]) - try compare([false, true], to: [2, 0, 2, 1]) + try compare([false], to: [0]) + try compare([true], to: [1]) + try compare([true, false], to: [1, 0]) + try compare([false, true], to: [0, 1]) } func testArrayOptionalBool() throws { @@ -34,10 +34,10 @@ final class BoolTests: XCTestCase { func testOptionalArrayBool() throws { try compare(nil, of: [Bool]?.self, to: [1]) - try compare([false], of: [Bool]?.self, to: [0, 2, 0]) - try compare([true], of: [Bool]?.self, to: [0, 2, 1]) - try compare([true, false], of: [Bool]?.self, to: [0, 2, 1, 2, 0]) - try compare([false, true], of: [Bool]?.self, to: [0, 2, 0, 2, 1]) + try compare([false], of: [Bool]?.self, to: [0, 0]) + try compare([true], of: [Bool]?.self, to: [0, 1]) + try compare([true, false], of: [Bool]?.self, to: [0, 1, 0]) + try compare([false, true], of: [Bool]?.self, to: [0, 0, 1]) } func testDoubleOptionalBool() throws { diff --git a/Tests/BinaryCodableTests/DecodingErrorTests.swift b/Tests/BinaryCodableTests/DecodingErrorTests.swift index 59e2ab2..3d54940 100644 --- a/Tests/BinaryCodableTests/DecodingErrorTests.swift +++ b/Tests/BinaryCodableTests/DecodingErrorTests.swift @@ -19,7 +19,8 @@ final class DecodingErrorTests: XCTestCase { } do { - _ = try Int32(fromZigZag: encoded) + let raw = try UInt64(fromVarintData: encoded) + _ = try Int32(fromZigZag: raw) XCTFail("Should fail to decode Int32") } catch is CorruptedDataError { @@ -37,7 +38,8 @@ final class DecodingErrorTests: XCTestCase { } do { - _ = try Int32(fromZigZag: encoded2) + let raw = try UInt64(fromVarintData: encoded2) + _ = try Int32(fromZigZag: raw) XCTFail("Should fail to decode Int32") } catch is CorruptedDataError { @@ -57,7 +59,7 @@ final class DecodingErrorTests: XCTestCase { } do { - _ = try UInt32(fromVarint: encoded) + _ = try UInt32(fromVarint: value) XCTFail("Should fail to decode UInt32") } catch is CorruptedDataError { diff --git a/Tests/BinaryCodableTests/SetTests.swift b/Tests/BinaryCodableTests/SetTests.swift new file mode 100644 index 0000000..091325d --- /dev/null +++ b/Tests/BinaryCodableTests/SetTests.swift @@ -0,0 +1,146 @@ +import XCTest +import BinaryCodable + +final class SetTests: XCTestCase { + + func testBoolSetEncoding() throws { + try compare([true, false], of: Set.self) + try compare([true], of: Set.self, to: [1]) + try compare([false], of: Set.self, to: [0]) + } + + func testInt8SetEncoding() throws { + try compare([.zero, 123, .min, .max, -1], of: Set.self) + try compare([-1], of: Set.self, to: [255]) + } + + func testInt16SetEncoding() throws { + try compare([.zero, 123, .min, .max, -1], of: Set.self) + try compare([-1], of: Set.self, to: [255, 255]) + } + + func testInt32SetEncoding() throws { + try compare([.zero, 123, .min, .max, -1], of: Set.self) + try compare([-1], of: Set.self, to: [1]) + } + + func testInt64SetEncoding() throws { + try compare([0, 123, .max, .min, -1], of: Set.self) + try compare([-1], of: Set.self, to: [1]) + } + + func testIntSetEncoding() throws { + try compare([0, 123, .max, .min, -1], of: Set.self) + try compare([0], of: Set.self, to: [0]) + try compare([123], of: Set.self, to: [246, 1]) + try compare([.max], of: Set.self, to: [0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + try compare([.min], of: Set.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + try compare([-1], of: Set.self, to: [1]) // 1 + } + + func testUInt8SetEncoding() throws { + try compare([.zero, 123, .min, .max], of: Set.self) + try compare([.zero], of: Set.self, to: [0]) + try compare([123], of: Set.self, to: [123]) + try compare([.min], of: Set.self, to: [0]) + try compare([.max], of: Set.self, to: [255]) + } + + func testUInt16SetEncoding() throws { + try compare([.zero, 123, .min, .max, 12345], of: Set.self) + try compare([.zero], of: Set.self, to: [0, 0]) + try compare([123], of: Set.self, to: [123, 0]) + try compare([.min], of: Set.self, to: [0, 0]) + try compare([.max], of: Set.self, to: [255, 255]) + try compare([12345], of: Set.self, to: [0x39, 0x30]) + } + + func testUInt32SetEncoding() throws { + try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, .max], of: Set.self) + + try compare([.zero], of: Set.self, to: [0]) + try compare([123], of: Set.self, to: [123]) + try compare([.min], of: Set.self, to: [0]) + try compare([12345], of: Set.self, to: [0xB9, 0x60]) + try compare([123456], of: Set.self, to: [0xC0, 0xC4, 0x07]) + try compare([12345678], of: Set.self, to: [0xCE, 0xC2, 0xF1, 0x05]) + try compare([1234567890], of: Set.self, to: [0xD2, 0x85, 0xD8, 0xCC, 0x04]) + try compare([.max], of: Set.self, to: [255, 255, 255, 255, 15]) + } + + func testUInt64SetEncoding() throws { + try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: Set.self) + + try compare([.zero], of: Set.self, to: [0]) + try compare([123], of: Set.self, to: [123]) + try compare([.min], of: Set.self, to: [0]) + try compare([12345], of: Set.self, to: [0xB9, 0x60]) + try compare([123456], of: Set.self, to: [0xC0, 0xC4, 0x07]) + try compare([12345678], of: Set.self, to: [0xCE, 0xC2, 0xF1, 0x05]) + try compare([1234567890], of: Set.self, to: [0xD2, 0x85, 0xD8, 0xCC, 0x04]) + try compare([12345678901234], of: Set.self, to: [0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02]) + try compare([.max], of: Set.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + } + + func testUIntSetEncoding() throws { + try compare([.zero, 123, .min, 12345, 123456, 12345678, 1234567890, 12345678901234, .max], of: Set.self) + + try compare([.zero], of: Set.self, to: [0]) + try compare([123], of: Set.self, to: [123]) + try compare([.min], of: Set.self, to: [0]) + try compare([12345], of: Set.self, to: [0xB9, 0x60]) + try compare([123456], of: Set.self, to: [0xC0, 0xC4, 0x07]) + try compare([12345678], of: Set.self, to: [0xCE, 0xC2, 0xF1, 0x05]) + try compare([1234567890], of: Set.self, to: [0xD2, 0x85, 0xD8, 0xCC, 0x04]) + try compare([12345678901234], of: Set.self, to: [0xF2, 0xDF, 0xB8, 0x9E, 0xA7, 0xE7, 0x02]) + try compare([.max], of: Set.self, to: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + } + + func testStringSetEncoding() throws { + let values1 = ["Some"] + try compare(values1, to: [ + 8, // Length 4 + 83, 111, 109, 101 // "Some" + ]) + + let values2: Set = ["Some", "A longer text with\n multiple lines", "More text", "eolqjwqu(Jan?!)§(!N"] + try compare(values2) + } + + func testFloatSetEncoding() throws { + try compare([.greatestFiniteMagnitude, .zero, .pi, -.pi, .leastNonzeroMagnitude], of: Set.self) + try compare([.greatestFiniteMagnitude], of: Set.self, to: [0x7F, 0x7F, 0xFF, 0xFF]) + try compare([.zero], of: Set.self, to: [0x00, 0x00, 0x00, 0x00]) + try compare([.pi], of: Set.self, to: [0x40, 0x49, 0x0F, 0xDA]) + try compare([-.pi], of: Set.self, to: [0xC0, 0x49, 0x0F, 0xDA]) + try compare([.leastNonzeroMagnitude], of: Set.self, to: [0x00, 0x00, 0x00, 0x01]) + } + + func testDoubleSetEncoding() throws { + try compare([.greatestFiniteMagnitude, .zero, .pi, .leastNonzeroMagnitude, -.pi], of: Set.self) + + try compare([.greatestFiniteMagnitude], of: Set.self, to: [0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + try compare([.zero], of: Set.self, to: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + try compare([.pi], of: Set.self, to: [0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18]) + try compare([.leastNonzeroMagnitude], of: Set.self, to: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) + try compare([-.pi], of: Set.self, to: [0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18]) + } + + func testSetOfOptionalsEncoding() throws { + try compare([true, false, nil], of: Set.self) + } + + func testSetOfDoubleOptionalsEncoding() throws { + try compare([.some(.some(true)), .some(.some(false)), .some(.none), .none], of: Set.self) + } + + func testSetOfTripleOptionalsEncoding() throws { + try compare([.some(.some(.some(true))), .some(.some(.some(false))), .some(.some(.none)), .some(.none), .none], of: Set.self) + } + + func testSetOfSetsEncoding() throws { + let values: Set> = [[false], [true, false]] + try compare(values, of: Set>.self) + } + +} diff --git a/Tests/BinaryCodableTests/StructEncodingTests.swift b/Tests/BinaryCodableTests/StructEncodingTests.swift index f06dc84..4e10215 100644 --- a/Tests/BinaryCodableTests/StructEncodingTests.swift +++ b/Tests/BinaryCodableTests/StructEncodingTests.swift @@ -10,10 +10,10 @@ final class StructEncodingTests: XCTestCase { let expected: [UInt8] = [ 7, // String key, length 3 118, 97, 108, - 12, // Length 6 - 2, 1, // true - 2, 0, // false - 2, 1] // true + 6, // Length 3 + 1, // true + 0, // false + 1] // true try compare(Test(val: [true, false, true]), to: expected) } @@ -37,6 +37,24 @@ final class StructEncodingTests: XCTestCase { try compare(value, to: expected) } + func testStructWithArrayOfOptionals() throws { + struct Test: Codable, Equatable { + let val: [Bool?] + } + let value = Test(val: [nil, true, nil, nil, false]) + let expected: [UInt8] = [ + 7, // String key, length 3 + 118, 97, 108, // 'val' + 14, // Length 7 + 1, // Nil + 2, 1, // True + 1, // Nil + 1, // Nil + 2, 0 // False + ] + try compare(value, to: expected) + } + func testArrayOfOptionalStructs() throws { struct Test: Codable, Equatable { let val: Int diff --git a/Tests/BinaryCodableTests/VariableLengthEncodingTests.swift b/Tests/BinaryCodableTests/VariableLengthEncodingTests.swift index 7c275e9..d3fdcf0 100644 --- a/Tests/BinaryCodableTests/VariableLengthEncodingTests.swift +++ b/Tests/BinaryCodableTests/VariableLengthEncodingTests.swift @@ -6,7 +6,8 @@ final class VariableLengthEncodingTests: XCTestCase { func compare(varInt value: T, of: T.Type, to result: [UInt8]) throws where T: VariableLengthCodable { let data = value.variableLengthEncoding XCTAssertEqual(Array(data), result) - let decoded = try T(fromVarint: data) + let raw = try UInt64(fromVarintData: data) + let decoded = try T(fromVarint: raw) XCTAssertEqual(decoded, value) } diff --git a/Tests/BinaryCodableTests/WrapperEncodingTests.swift b/Tests/BinaryCodableTests/WrapperEncodingTests.swift index 70c71a3..815f2cb 100644 --- a/Tests/BinaryCodableTests/WrapperEncodingTests.swift +++ b/Tests/BinaryCodableTests/WrapperEncodingTests.swift @@ -3,7 +3,7 @@ import XCTest final class WrapperEncodingTests: XCTestCase { - private func compareFixed(_ value: FixedSizeEncoded, of type: T.Type, to expected: [UInt8]) throws where T: FixedSizeCodable, T: CodablePrimitive { + private func compareFixed(_ value: FixedSizeEncoded, of type: T.Type, to expected: [UInt8]) throws where T: FixedSizeCodable, T: CodablePrimitive, T: Equatable { try compareEncoding(of: value, withType: FixedSizeEncoded.self, isEqualTo: expected) }