Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add packed sequence encoding #22

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading