Skip to content

Commit

Permalink
Add packed sequence encoding (#22)
Browse files Browse the repository at this point in the history
* Add packed sequence encoding

* Revert some public initializers, fix test
  • Loading branch information
christophhagen authored Apr 12, 2024
1 parent f1bb78f commit f4bdd82
Show file tree
Hide file tree
Showing 34 changed files with 991 additions and 225 deletions.
38 changes: 32 additions & 6 deletions BinaryFormat.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<T>`
- Zig-zag types: `Int32`, `Int64`, `Int`
- Variable-length types: `UInt32`, `UInt64`, `UInt`, `VariableLengthEncoded<T>`

### Keyed containers

Keyed containers work similar to unkeyed containers, except that each element also has a key inserted before the element data.
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Sources/BinaryCodable/Decoding/DecodingDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
21 changes: 21 additions & 0 deletions Sources/BinaryCodable/Primitives/Array+Coding.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
64 changes: 56 additions & 8 deletions Sources/BinaryCodable/Primitives/Bool+Coding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
45 changes: 45 additions & 0 deletions Sources/BinaryCodable/Primitives/Double+Coding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
44 changes: 44 additions & 0 deletions Sources/BinaryCodable/Primitives/Float+Coding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
26 changes: 21 additions & 5 deletions Sources/BinaryCodable/Primitives/Int+Coding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -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")
}
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
}
}
24 changes: 20 additions & 4 deletions Sources/BinaryCodable/Primitives/Int16+Coding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -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)
}
}
Loading

0 comments on commit f4bdd82

Please sign in to comment.