Skip to content

Commit f4bdd82

Browse files
Add packed sequence encoding (#22)
* Add packed sequence encoding * Revert some public initializers, fix test
1 parent f1bb78f commit f4bdd82

34 files changed

+991
-225
lines changed

BinaryFormat.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ This is useful if the integer values are often large, e.g. for random numbers.
8787
### Strings
8888

8989
Swift `String` values are encoded using their `UTF-8` representations.
90-
If a string can't be encoded this way, then encoding fails.
90+
If a string can't be encoded this way, then encoding fails.s
9191

9292
## Containers
9393

@@ -148,6 +148,33 @@ which translates to:
148148
0x00 // Fourth element is not nil, length 0
149149
```
150150

151+
### Packed sequences
152+
153+
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).
154+
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.
155+
156+
For example, encoding a series of `Bool` values in an unkeyed container would result in the following encoded data:
157+
158+
```
159+
// True, false, false
160+
02 01 02 00 02 00
161+
```
162+
163+
The `02` bytes indicate the length of each `Bool`, which is unnecessary, since a `Bool` is always exactly one byte.
164+
165+
When encoding a type of `[Bool]`, the encoded data is shortened to:
166+
167+
```
168+
// True, false, false
169+
01 00 00
170+
```
171+
172+
This encoding is only used for the following types:
173+
174+
- Fixed-width types: `Double`, `Float`, `Bool`, `Int8`, `UInt8`, `Int16`, `UInt16`, `FixedLengthEncoded<T>`
175+
- Zig-zag types: `Int32`, `Int64`, `Int`
176+
- Variable-length types: `UInt32`, `UInt64`, `UInt`, `VariableLengthEncoded<T>`
177+
151178
### Keyed containers
152179

153180
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:
207234
| 4 | 0x06 | Length 3
208235
| 5-7 | 0x42 0x6f 0x62 | String "Bob"
209236
| 8 | 0x06 | CodingKey(stringValue: 'references', intValue: 3)
210-
| 9 | 0x0A | Length 5
211-
| 10 | 0x02 | Length 1
212-
| 11 | 0x06 | Int `3`
213-
| 12 | 0x04 | Length 2
214-
| 13-14 | 0xAF 0x04 | Int `-280`
237+
| 9 | 0x0A | Length 3
238+
| 10 | 0x06 | Int `3`
239+
| 11-12 | 0xAF 0x04 | Int `-280`
215240

216241
There are a few things to note:
217242
- The properties are all marked by their integer keys
218243
- The elements in the `references` array are also preceded by a length indicator
219244
- The top level keyed container has no length information, since it can be inferred from the length of the provided data
245+
- `[Int]` is a packed field, so no length data is inserted before each element
220246

221247
### Dictionaries
222248

Sources/BinaryCodable/Decoding/DecodingDataProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ extension DecodingDataProvider {
2020
Decode an unsigned integer using variable-length encoding starting at a position.
2121
- Returns: `Nil`, if insufficient data is available
2222
*/
23-
private func decodeUInt64(at index: inout Index) -> UInt64? {
23+
func decodeUInt64(at index: inout Index) -> UInt64? {
2424
guard let start = nextByte(at: &index) else { return nil }
2525
return decodeUInt64(startByte: start, at: &index)
2626
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
}

Sources/BinaryCodable/Primitives/Bool+Coding.swift

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ extension Bool: EncodablePrimitive {
1010

1111
extension Bool: DecodablePrimitive {
1212

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+
1324
/**
1425
Decode a boolean from encoded data.
1526
- Parameter data: The data to decode
@@ -19,14 +30,51 @@ extension Bool: DecodablePrimitive {
1930
guard data.count == 1 else {
2031
throw CorruptedDataError(invalidSize: data.count, for: "Bool")
2132
}
22-
let byte = data[data.startIndex]
23-
switch byte {
24-
case 0:
25-
self = false
26-
case 1:
27-
self = true
28-
default:
29-
throw CorruptedDataError(invalidBoolByte: byte)
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")
3077
}
78+
try self.init(fromFixedSize: bytes)
3179
}
3280
}

Sources/BinaryCodable/Primitives/Double+Coding.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,48 @@ extension Double: DecodablePrimitive {
1717
self.init(bitPattern: value)
1818
}
1919
}
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+
}

Sources/BinaryCodable/Primitives/Float+Coding.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,47 @@ extension Float: DecodablePrimitive {
1818
}
1919
}
2020

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+
}

Sources/BinaryCodable/Primitives/Int+Coding.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ extension Int: DecodablePrimitive {
1414
- Throws: ``CorruptedDataError``
1515
*/
1616
init(data: Data) throws {
17-
try self.init(fromZigZag: data)
17+
let raw = try UInt64(fromVarintData: data)
18+
try self.init(fromZigZag: raw)
1819
}
1920
}
2021

@@ -35,8 +36,8 @@ extension Int: ZigZagDecodable {
3536
- Parameter data: The data of the zig-zag encoded value.
3637
- Throws: ``CorruptedDataError``
3738
*/
38-
public init(fromZigZag data: Data) throws {
39-
let raw = try Int64(data: data)
39+
public init(fromZigZag raw: UInt64) throws {
40+
let raw = Int64(fromZigZag: raw)
4041
guard let value = Int(exactly: raw) else {
4142
throw CorruptedDataError(outOfRange: raw, forType: "Int")
4243
}
@@ -74,8 +75,8 @@ extension Int: VariableLengthDecodable {
7475
- Parameter data: The data to decode.
7576
- Throws: ``CorruptedDataError``
7677
*/
77-
public init(fromVarint data: Data) throws {
78-
let intValue = try Int64(fromVarint: data)
78+
public init(fromVarint raw: UInt64) throws {
79+
let intValue = Int64(fromVarint: raw)
7980
guard let value = Int(exactly: intValue) else {
8081
throw CorruptedDataError(outOfRange: intValue, forType: "Int")
8182
}
@@ -109,3 +110,18 @@ extension Int: FixedSizeDecodable {
109110
}
110111
}
111112

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+
}

Sources/BinaryCodable/Primitives/Int16+Coding.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ extension Int16: VariableLengthDecodable {
7272
- Parameter data: The data to decode.
7373
- Throws: ``CorruptedDataError``
7474
*/
75-
public init(fromVarint data: Data) throws {
76-
let value = try UInt16(fromVarint: data)
75+
public init(fromVarint raw: UInt64) throws {
76+
let value = try UInt16(fromVarint: raw)
7777
self = Int16(bitPattern: value)
7878
}
7979
}
@@ -95,11 +95,27 @@ extension Int16: ZigZagDecodable {
9595
- Parameter data: The data of the zig-zag encoded value.
9696
- Throws: ``CorruptedDataError``
9797
*/
98-
public init(fromZigZag data: Data) throws {
99-
let raw = try Int64(fromZigZag: data)
98+
public init(fromZigZag raw: UInt64) throws {
99+
let raw = Int64(fromZigZag: raw)
100100
guard let value = Int16(exactly: raw) else {
101101
throw CorruptedDataError(outOfRange: raw, forType: "Int16")
102102
}
103103
self = value
104104
}
105105
}
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+
}

0 commit comments

Comments
 (0)